@constructive-io/graphql-codegen 3.3.1 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +0 -4
  2. package/core/ast.js +6 -5
  3. package/core/codegen/custom-mutations.js +22 -22
  4. package/core/codegen/custom-queries.js +24 -17
  5. package/core/codegen/hooks-ast.d.ts +1 -1
  6. package/core/codegen/hooks-ast.js +22 -5
  7. package/core/codegen/index.d.ts +1 -1
  8. package/core/codegen/mutations.js +16 -12
  9. package/core/codegen/orm/custom-ops-generator.js +37 -3
  10. package/core/codegen/orm/input-types-generator.js +161 -89
  11. package/core/codegen/orm/model-generator.js +72 -73
  12. package/core/codegen/orm/select-types.d.ts +27 -17
  13. package/core/codegen/queries.js +37 -25
  14. package/core/codegen/schema-types-generator.js +21 -0
  15. package/core/codegen/templates/hooks-selection.ts +12 -0
  16. package/core/codegen/templates/query-builder.ts +103 -59
  17. package/core/codegen/templates/select-types.ts +59 -33
  18. package/core/codegen/types.js +26 -0
  19. package/core/codegen/utils.d.ts +1 -1
  20. package/core/codegen/utils.js +1 -1
  21. package/core/custom-ast.js +9 -8
  22. package/core/database/index.js +2 -3
  23. package/core/index.d.ts +2 -0
  24. package/core/index.js +2 -0
  25. package/core/introspect/infer-tables.js +144 -58
  26. package/core/introspect/transform-schema.d.ts +1 -1
  27. package/core/introspect/transform-schema.js +3 -1
  28. package/esm/core/ast.js +6 -5
  29. package/esm/core/codegen/custom-mutations.js +23 -23
  30. package/esm/core/codegen/custom-queries.js +25 -18
  31. package/esm/core/codegen/hooks-ast.d.ts +1 -1
  32. package/esm/core/codegen/hooks-ast.js +22 -5
  33. package/esm/core/codegen/index.d.ts +1 -1
  34. package/esm/core/codegen/mutations.js +16 -12
  35. package/esm/core/codegen/orm/custom-ops-generator.js +38 -4
  36. package/esm/core/codegen/orm/input-types-generator.js +163 -91
  37. package/esm/core/codegen/orm/model-generator.js +73 -74
  38. package/esm/core/codegen/orm/select-types.d.ts +27 -17
  39. package/esm/core/codegen/queries.js +37 -25
  40. package/esm/core/codegen/schema-types-generator.js +21 -0
  41. package/esm/core/codegen/types.js +26 -0
  42. package/esm/core/codegen/utils.d.ts +1 -1
  43. package/esm/core/codegen/utils.js +1 -1
  44. package/esm/core/custom-ast.js +9 -8
  45. package/esm/core/database/index.js +2 -3
  46. package/esm/core/index.d.ts +2 -0
  47. package/esm/core/index.js +2 -0
  48. package/esm/core/introspect/infer-tables.js +144 -58
  49. package/esm/core/introspect/transform-schema.d.ts +1 -1
  50. package/esm/core/introspect/transform-schema.js +3 -1
  51. package/esm/generators/field-selector.js +1 -0
  52. package/esm/generators/index.d.ts +3 -0
  53. package/esm/generators/index.js +3 -0
  54. package/esm/generators/mutations.js +4 -4
  55. package/esm/generators/select.js +4 -4
  56. package/esm/index.d.ts +1 -1
  57. package/esm/index.js +1 -1
  58. package/esm/types/schema.d.ts +5 -3
  59. package/generators/field-selector.js +1 -0
  60. package/generators/index.d.ts +3 -0
  61. package/generators/index.js +3 -0
  62. package/generators/mutations.js +3 -3
  63. package/generators/select.js +3 -3
  64. package/index.d.ts +1 -1
  65. package/index.js +1 -1
  66. package/package.json +11 -11
  67. package/types/schema.d.ts +5 -3
@@ -38,6 +38,7 @@ exports.generateTypesFile = generateTypesFile;
38
38
  * Types generator - generates types.ts with entity interfaces using Babel AST
39
39
  */
40
40
  const t = __importStar(require("@babel/types"));
41
+ const scalars_1 = require("./scalars");
41
42
  const babel_ast_1 = require("./babel-ast");
42
43
  const utils_1 = require("./utils");
43
44
  /** All filter type configurations - scalar and list filters */
@@ -187,12 +188,34 @@ function createInterfaceDeclaration(name, properties) {
187
188
  const interfaceDecl = t.tsInterfaceDeclaration(t.identifier(name), null, null, t.tsInterfaceBody(props));
188
189
  return t.exportNamedDeclaration(interfaceDecl);
189
190
  }
191
+ function createTypeAlias(name, typeNode) {
192
+ const typeAlias = t.tsTypeAliasDeclaration(t.identifier(name), null, typeNode);
193
+ return t.exportNamedDeclaration(typeAlias);
194
+ }
195
+ function collectCustomScalarTypes(tables, excludedTypeNames) {
196
+ const customScalarTypes = new Set();
197
+ const tableTypeNames = new Set(tables.map((table) => table.name));
198
+ for (const table of tables) {
199
+ for (const field of (0, utils_1.getScalarFields)(table)) {
200
+ const cleanType = field.type.gqlType.replace(/!/g, '');
201
+ if (scalars_1.SCALAR_NAMES.has(cleanType))
202
+ continue;
203
+ if (excludedTypeNames.has(cleanType))
204
+ continue;
205
+ if (tableTypeNames.has(cleanType))
206
+ continue;
207
+ customScalarTypes.add(cleanType);
208
+ }
209
+ }
210
+ return Array.from(customScalarTypes).sort();
211
+ }
190
212
  /**
191
213
  * Generate types.ts content with all entity interfaces and base filter types
192
214
  */
193
215
  function generateTypesFile(tables, options = {}) {
194
216
  const { enumsFromSchemaTypes = [] } = options;
195
217
  const enumSet = new Set(enumsFromSchemaTypes);
218
+ const customScalarTypes = collectCustomScalarTypes(tables, enumSet);
196
219
  const statements = [];
197
220
  // Collect which enums are actually used by entity fields
198
221
  const usedEnums = new Set();
@@ -215,6 +238,9 @@ function generateTypesFile(tables, options = {}) {
215
238
  importDecl.importKind = 'type';
216
239
  statements.push(importDecl);
217
240
  }
241
+ for (const scalarType of customScalarTypes) {
242
+ statements.push(createTypeAlias(scalarType, t.tsUnknownKeyword()));
243
+ }
218
244
  // Generate entity interfaces
219
245
  for (const table of tables) {
220
246
  const scalarFields = (0, utils_1.getScalarFields)(table);
@@ -72,7 +72,7 @@ export declare function getUpdateMutationFileName(table: CleanTable): string;
72
72
  export declare function getDeleteMutationFileName(table: CleanTable): string;
73
73
  /**
74
74
  * Get the GraphQL query name for fetching all rows
75
- * Uses inflection from _meta, falls back to convention
75
+ * Uses inflection from introspection, falls back to convention
76
76
  */
77
77
  export declare function getAllRowsQueryName(table: CleanTable): string;
78
78
  /**
@@ -169,7 +169,7 @@ function getDeleteMutationFileName(table) {
169
169
  // ============================================================================
170
170
  /**
171
171
  * Get the GraphQL query name for fetching all rows
172
- * Uses inflection from _meta, falls back to convention
172
+ * Uses inflection from introspection, falls back to convention
173
173
  */
174
174
  function getAllRowsQueryName(table) {
175
175
  return (table.query?.all ||
@@ -42,6 +42,7 @@ exports.geometryAst = geometryAst;
42
42
  exports.intervalAst = intervalAst;
43
43
  exports.isIntervalType = isIntervalType;
44
44
  const t = __importStar(require("gql-ast"));
45
+ const graphql_1 = require("graphql");
45
46
  /**
46
47
  * Get custom AST for MetaField type - handles PostgreSQL types that need subfield selections
47
48
  */
@@ -119,28 +120,28 @@ function geometryPointAst(name) {
119
120
  function geometryCollectionAst(name) {
120
121
  // Manually create inline fragment since gql-ast doesn't support it
121
122
  const inlineFragment = {
122
- kind: 'InlineFragment',
123
+ kind: graphql_1.Kind.INLINE_FRAGMENT,
123
124
  typeCondition: {
124
- kind: 'NamedType',
125
+ kind: graphql_1.Kind.NAMED_TYPE,
125
126
  name: {
126
- kind: 'Name',
127
+ kind: graphql_1.Kind.NAME,
127
128
  value: 'GeometryPoint',
128
129
  },
129
130
  },
130
131
  selectionSet: {
131
- kind: 'SelectionSet',
132
+ kind: graphql_1.Kind.SELECTION_SET,
132
133
  selections: [
133
134
  {
134
- kind: 'Field',
135
+ kind: graphql_1.Kind.FIELD,
135
136
  name: {
136
- kind: 'Name',
137
+ kind: graphql_1.Kind.NAME,
137
138
  value: 'x',
138
139
  },
139
140
  },
140
141
  {
141
- kind: 'Field',
142
+ kind: graphql_1.Kind.FIELD,
142
143
  name: {
143
- kind: 'Name',
144
+ kind: graphql_1.Kind.NAME,
144
145
  value: 'y',
145
146
  },
146
147
  },
@@ -56,11 +56,10 @@ async function buildSchemaFromDatabase(options) {
56
56
  const { database, schemas, outDir, filename = 'schema.graphql' } = options;
57
57
  // Ensure output directory exists
58
58
  await fs.promises.mkdir(outDir, { recursive: true });
59
- // Build schema SDL from database
59
+ // Build schema SDL from database (PostGraphile v5 preset-driven settings)
60
60
  const sdl = await (0, graphql_server_1.buildSchemaSDL)({
61
61
  database,
62
62
  schemas,
63
- graphile: { pgSettings: async () => ({ role: 'administrator' }) },
64
63
  });
65
64
  // Write schema to file
66
65
  const schemaPath = path.join(outDir, filename);
@@ -77,9 +76,9 @@ async function buildSchemaFromDatabase(options) {
77
76
  */
78
77
  async function buildSchemaSDLFromDatabase(options) {
79
78
  const { database, schemas } = options;
79
+ // PostGraphile v5 resolves role/settings via preset configuration.
80
80
  return (0, graphql_server_1.buildSchemaSDL)({
81
81
  database,
82
82
  schemas,
83
- graphile: { pgSettings: async () => ({ role: 'administrator' }) },
84
83
  });
85
84
  }
package/core/index.d.ts CHANGED
@@ -9,7 +9,9 @@ export { generate } from './generate';
9
9
  export * from './types';
10
10
  export * from './ast';
11
11
  export * from './custom-ast';
12
+ /** @deprecated Legacy v4 query builder — use v5 ORM codegen instead */
12
13
  export { MetaObject, QueryBuilder } from './query-builder';
14
+ /** @deprecated Legacy v4 meta-object utilities — v5 uses standard introspection */
13
15
  export { convertFromMetaSchema, validateMetaObject } from './meta-object';
14
16
  export * from './config';
15
17
  export * from './codegen';
package/core/index.js CHANGED
@@ -29,10 +29,12 @@ __exportStar(require("./types"), exports);
29
29
  __exportStar(require("./ast"), exports);
30
30
  __exportStar(require("./custom-ast"), exports);
31
31
  // Query builder
32
+ /** @deprecated Legacy v4 query builder — use v5 ORM codegen instead */
32
33
  var query_builder_1 = require("./query-builder");
33
34
  Object.defineProperty(exports, "MetaObject", { enumerable: true, get: function () { return query_builder_1.MetaObject; } });
34
35
  Object.defineProperty(exports, "QueryBuilder", { enumerable: true, get: function () { return query_builder_1.QueryBuilder; } });
35
36
  // Meta object utilities
37
+ /** @deprecated Legacy v4 meta-object utilities — v5 uses standard introspection */
36
38
  var meta_object_1 = require("./meta-object");
37
39
  Object.defineProperty(exports, "convertFromMetaSchema", { enumerable: true, get: function () { return meta_object_1.convertFromMetaSchema; } });
38
40
  Object.defineProperty(exports, "validateMetaObject", { enumerable: true, get: function () { return meta_object_1.validateMetaObject; } });
@@ -86,20 +86,19 @@ function inferTablesFromIntrospection(introspection, options = {}) {
86
86
  const { types, queryType, mutationType } = schema;
87
87
  // Build lookup maps for efficient access
88
88
  const typeMap = buildTypeMap(types);
89
+ const { entityNames, entityToConnection, connectionToEntity } = buildEntityConnectionMaps(types, typeMap);
89
90
  const queryFields = getTypeFields(typeMap.get(queryType.name));
90
91
  const mutationFields = mutationType
91
92
  ? getTypeFields(typeMap.get(mutationType.name))
92
93
  : [];
93
- // Step 1: Detect entity types by finding Connection types
94
- const entityNames = detectEntityTypes(types);
95
- // Step 2: Build CleanTable for each entity
94
+ // Step 1: Build CleanTable for each inferred entity
96
95
  const tables = [];
97
96
  for (const entityName of entityNames) {
98
97
  const entityType = typeMap.get(entityName);
99
98
  if (!entityType)
100
99
  continue;
101
100
  // Infer all metadata for this entity
102
- const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields);
101
+ const { table, hasRealOperation } = buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity);
103
102
  // Only include tables that have at least one real operation
104
103
  if (hasRealOperation) {
105
104
  tables.push(table);
@@ -107,17 +106,17 @@ function inferTablesFromIntrospection(introspection, options = {}) {
107
106
  }
108
107
  return tables;
109
108
  }
110
- // ============================================================================
111
- // Entity Detection
112
- // ============================================================================
113
109
  /**
114
- * Detect entity types by finding Connection types in the schema
110
+ * Infer entity <-> connection mappings from schema types.
115
111
  *
116
- * PostGraphile generates a {PluralName}Connection type for each table.
117
- * From this, we can derive the entity type name.
112
+ * Prefer deriving entity names from the `nodes` field of connection types.
113
+ * This is robust across v4/v5 naming variations (e.g. `UsersConnection` and
114
+ * `UserConnection`).
118
115
  */
119
- function detectEntityTypes(types) {
116
+ function buildEntityConnectionMaps(types, typeMap) {
120
117
  const entityNames = new Set();
118
+ const entityToConnection = new Map();
119
+ const connectionToEntity = new Map();
121
120
  const typeNames = new Set(types.map((t) => t.name));
122
121
  for (const type of types) {
123
122
  // Skip internal types
@@ -128,26 +127,58 @@ function detectEntityTypes(types) {
128
127
  // Check for Connection pattern
129
128
  const connectionMatch = type.name.match(PATTERNS.connection);
130
129
  if (connectionMatch) {
131
- const pluralName = connectionMatch[1]; // e.g., "Users" from "UsersConnection"
132
- const singularName = (0, inflekt_1.singularize)(pluralName); // e.g., "User"
133
- // Verify the entity type actually exists
134
- if (typeNames.has(singularName)) {
135
- entityNames.add(singularName);
130
+ const fallbackEntityName = (0, inflekt_1.singularize)(connectionMatch[1]);
131
+ const entityName = resolveEntityNameFromConnectionType(type, typeMap) ?? fallbackEntityName;
132
+ // Verify the entity type actually exists and is not a built-in/internal type.
133
+ if (typeNames.has(entityName) &&
134
+ !BUILTIN_TYPES.has(entityName) &&
135
+ !isInternalType(entityName)) {
136
+ entityNames.add(entityName);
137
+ entityToConnection.set(entityName, type.name);
138
+ connectionToEntity.set(type.name, entityName);
136
139
  }
137
140
  }
138
141
  }
139
- return entityNames;
142
+ return {
143
+ entityNames,
144
+ entityToConnection,
145
+ connectionToEntity,
146
+ };
147
+ }
148
+ /**
149
+ * Attempt to resolve an entity name from a connection type by inspecting its
150
+ * `nodes` field.
151
+ */
152
+ function resolveEntityNameFromConnectionType(connectionType, typeMap) {
153
+ if (!connectionType.fields)
154
+ return null;
155
+ const nodesField = connectionType.fields.find((field) => field.name === 'nodes');
156
+ if (!nodesField)
157
+ return null;
158
+ const nodeTypeName = (0, introspection_1.getBaseTypeName)(nodesField.type);
159
+ if (!nodeTypeName)
160
+ return null;
161
+ const nodeType = typeMap.get(nodeTypeName);
162
+ if (!nodeType || nodeType.kind !== 'OBJECT')
163
+ return null;
164
+ if (nodeTypeName.endsWith('Connection'))
165
+ return null;
166
+ if (BUILTIN_TYPES.has(nodeTypeName))
167
+ return null;
168
+ if (isInternalType(nodeTypeName))
169
+ return null;
170
+ return nodeTypeName;
140
171
  }
141
172
  /**
142
173
  * Build a complete CleanTable from an entity type
143
174
  */
144
- function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields) {
175
+ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationFields, entityToConnection, connectionToEntity) {
145
176
  // Extract scalar fields from entity type
146
- const fields = extractEntityFields(entityType, typeMap);
177
+ const fields = extractEntityFields(entityType, typeMap, entityToConnection);
147
178
  // Infer relations from entity fields
148
- const relations = inferRelations(entityType, typeMap);
179
+ const relations = inferRelations(entityType, entityToConnection, connectionToEntity);
149
180
  // Match query and mutation operations
150
- const queryOps = matchQueryOperations(entityName, queryFields, typeMap);
181
+ const queryOps = matchQueryOperations(entityName, queryFields, entityToConnection);
151
182
  const mutationOps = matchMutationOperations(entityName, mutationFields);
152
183
  // Check if we found at least one real operation (not a fallback)
153
184
  const hasRealOperation = !!(queryOps.all ||
@@ -157,16 +188,19 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
157
188
  mutationOps.delete);
158
189
  // Infer primary key from mutation inputs
159
190
  const constraints = inferConstraints(entityName, typeMap);
191
+ // Infer the patch field name from UpdateXxxInput (e.g., "userPatch")
192
+ const patchFieldName = inferPatchFieldName(entityName, typeMap);
160
193
  // Build inflection map from discovered types
161
- const inflection = buildInflection(entityName, typeMap);
194
+ const inflection = buildInflection(entityName, typeMap, entityToConnection);
162
195
  // Combine query operations with fallbacks for UI purposes
163
196
  // (but hasRealOperation indicates if we should include this table)
164
197
  const query = {
165
198
  all: queryOps.all ?? (0, inflekt_1.lcFirst)((0, inflekt_1.pluralize)(entityName)),
166
- one: queryOps.one ?? (0, inflekt_1.lcFirst)(entityName),
199
+ one: queryOps.one,
167
200
  create: mutationOps.create ?? `create${entityName}`,
168
201
  update: mutationOps.update,
169
202
  delete: mutationOps.delete,
203
+ patchFieldName,
170
204
  };
171
205
  return {
172
206
  table: {
@@ -187,7 +221,7 @@ function buildCleanTable(entityName, entityType, typeMap, queryFields, mutationF
187
221
  * Extract scalar fields from an entity type
188
222
  * Excludes relation fields (those returning other entity types or connections)
189
223
  */
190
- function extractEntityFields(entityType, typeMap) {
224
+ function extractEntityFields(entityType, typeMap, entityToConnection) {
191
225
  const fields = [];
192
226
  if (!entityType.fields)
193
227
  return fields;
@@ -200,7 +234,7 @@ function extractEntityFields(entityType, typeMap) {
200
234
  if (fieldType?.kind === 'OBJECT') {
201
235
  // Check if it's a Connection type (hasMany) or entity type (belongsTo)
202
236
  if (baseTypeName.endsWith('Connection') ||
203
- isEntityType(baseTypeName, typeMap)) {
237
+ isEntityType(baseTypeName, entityToConnection)) {
204
238
  continue; // Skip relation fields
205
239
  }
206
240
  }
@@ -215,9 +249,8 @@ function extractEntityFields(entityType, typeMap) {
215
249
  /**
216
250
  * Check if a type name is an entity type (has a corresponding Connection)
217
251
  */
218
- function isEntityType(typeName, typeMap) {
219
- const connectionName = `${(0, inflekt_1.pluralize)(typeName)}Connection`;
220
- return typeMap.has(connectionName);
252
+ function isEntityType(typeName, entityToConnection) {
253
+ return entityToConnection.has(typeName);
221
254
  }
222
255
  /**
223
256
  * Convert IntrospectionTypeRef to CleanFieldType
@@ -238,7 +271,7 @@ function convertToCleanFieldType(typeRef) {
238
271
  /**
239
272
  * Infer relations from entity type fields
240
273
  */
241
- function inferRelations(entityType, typeMap) {
274
+ function inferRelations(entityType, entityToConnection, connectionToEntity) {
242
275
  const belongsTo = [];
243
276
  const hasMany = [];
244
277
  const manyToMany = [];
@@ -251,17 +284,17 @@ function inferRelations(entityType, typeMap) {
251
284
  continue;
252
285
  // Check for Connection type → hasMany or manyToMany
253
286
  if (baseTypeName.endsWith('Connection')) {
254
- const relation = inferHasManyOrManyToMany(field, baseTypeName, typeMap);
255
- if (relation.type === 'manyToMany') {
256
- manyToMany.push(relation.relation);
287
+ const resolvedRelation = inferHasManyOrManyToMany(field, baseTypeName, connectionToEntity);
288
+ if (resolvedRelation.type === 'manyToMany') {
289
+ manyToMany.push(resolvedRelation.relation);
257
290
  }
258
291
  else {
259
- hasMany.push(relation.relation);
292
+ hasMany.push(resolvedRelation.relation);
260
293
  }
261
294
  continue;
262
295
  }
263
296
  // Check for entity type → belongsTo
264
- if (isEntityType(baseTypeName, typeMap)) {
297
+ if (isEntityType(baseTypeName, entityToConnection)) {
265
298
  belongsTo.push({
266
299
  fieldName: field.name,
267
300
  isUnique: false, // Can't determine from introspection alone
@@ -279,11 +312,13 @@ function inferRelations(entityType, typeMap) {
279
312
  * ManyToMany pattern: field name contains "By" and "And"
280
313
  * e.g., "productsByOrderItemOrderIdAndProductId"
281
314
  */
282
- function inferHasManyOrManyToMany(field, connectionTypeName, typeMap) {
283
- // Extract the related entity name from Connection type
284
- const match = connectionTypeName.match(PATTERNS.connection);
285
- const relatedPluralName = match ? match[1] : connectionTypeName;
286
- const relatedEntityName = (0, inflekt_1.singularize)(relatedPluralName);
315
+ function inferHasManyOrManyToMany(field, connectionTypeName, connectionToEntity) {
316
+ // Resolve the related entity from discovered connection mappings first.
317
+ const relatedEntityName = connectionToEntity.get(connectionTypeName) ?? (() => {
318
+ const match = connectionTypeName.match(PATTERNS.connection);
319
+ const relatedPluralName = match ? match[1] : connectionTypeName;
320
+ return (0, inflekt_1.singularize)(relatedPluralName);
321
+ })();
287
322
  // Check for manyToMany pattern in field name
288
323
  const isManyToMany = field.name.includes('By') && field.name.includes('And');
289
324
  if (isManyToMany) {
@@ -329,9 +364,9 @@ function inferHasManyOrManyToMany(field, connectionTypeName, typeMap) {
329
364
  * - List query: returns {PluralName}Connection (e.g., users → UsersConnection)
330
365
  * - Single query: returns {EntityName} with id/nodeId arg (e.g., user → User)
331
366
  */
332
- function matchQueryOperations(entityName, queryFields, typeMap) {
367
+ function matchQueryOperations(entityName, queryFields, entityToConnection) {
333
368
  const pluralName = (0, inflekt_1.pluralize)(entityName);
334
- const connectionTypeName = `${pluralName}Connection`;
369
+ const connectionTypeName = entityToConnection.get(entityName) ?? `${pluralName}Connection`;
335
370
  let all = null;
336
371
  let one = null;
337
372
  for (const field of queryFields) {
@@ -415,21 +450,18 @@ function inferConstraints(entityName, typeMap) {
415
450
  const deleteInputName = `Delete${entityName}Input`;
416
451
  const updateInput = typeMap.get(updateInputName);
417
452
  const deleteInput = typeMap.get(deleteInputName);
418
- // Check update input for id field
419
- const inputToCheck = updateInput || deleteInput;
420
- if (inputToCheck?.inputFields) {
421
- const idField = inputToCheck.inputFields.find((f) => f.name === 'id' || f.name === 'nodeId');
422
- if (idField) {
423
- primaryKey.push({
424
- name: 'primary',
425
- fields: [
426
- {
427
- name: idField.name,
428
- type: convertToCleanFieldType(idField.type),
429
- },
430
- ],
431
- });
432
- }
453
+ const keyInputField = inferPrimaryKeyFromInputObject(updateInput) ||
454
+ inferPrimaryKeyFromInputObject(deleteInput);
455
+ if (keyInputField) {
456
+ primaryKey.push({
457
+ name: 'primary',
458
+ fields: [
459
+ {
460
+ name: keyInputField.name,
461
+ type: convertToCleanFieldType(keyInputField.type),
462
+ },
463
+ ],
464
+ });
433
465
  }
434
466
  // If no PK found from inputs, try to find 'id' field in entity type
435
467
  if (primaryKey.length === 0) {
@@ -455,16 +487,70 @@ function inferConstraints(entityName, typeMap) {
455
487
  unique: [], // Would need constraint info to populate
456
488
  };
457
489
  }
490
+ /**
491
+ * Infer a single-row lookup key from an Update/Delete input object.
492
+ *
493
+ * Priority:
494
+ * 1. Canonical keys: id, nodeId, rowId
495
+ * 2. Single non-patch/non-clientMutationId scalar-ish field
496
+ *
497
+ * If multiple possible key fields remain, return null to avoid guessing.
498
+ */
499
+ function inferPrimaryKeyFromInputObject(inputType) {
500
+ const inputFields = inputType?.inputFields ?? [];
501
+ if (inputFields.length === 0)
502
+ return null;
503
+ const canonicalKey = inputFields.find((field) => field.name === 'id' || field.name === 'nodeId' || field.name === 'rowId');
504
+ if (canonicalKey)
505
+ return canonicalKey;
506
+ const candidates = inputFields.filter((field) => {
507
+ if (field.name === 'clientMutationId')
508
+ return false;
509
+ const baseTypeName = (0, introspection_1.getBaseTypeName)(field.type);
510
+ const lowerName = field.name.toLowerCase();
511
+ // Exclude patch payload fields (patch / fooPatch)
512
+ if (lowerName === 'patch' || lowerName.endsWith('patch'))
513
+ return false;
514
+ if (baseTypeName?.endsWith('Patch'))
515
+ return false;
516
+ return true;
517
+ });
518
+ return candidates.length === 1 ? candidates[0] : null;
519
+ }
520
+ /**
521
+ * Infer the patch field name from an Update input type.
522
+ *
523
+ * PostGraphile v5 uses entity-specific patch field names:
524
+ * UpdateUserInput → { id, userPatch: UserPatch }
525
+ * UpdateDatabaseInput → { id, databasePatch: DatabasePatch }
526
+ *
527
+ * Pattern: {lcFirst(entityTypeName)}Patch
528
+ */
529
+ function inferPatchFieldName(entityName, typeMap) {
530
+ const updateInputName = `Update${entityName}Input`;
531
+ const updateInput = typeMap.get(updateInputName);
532
+ const inputFields = updateInput?.inputFields ?? [];
533
+ // Find the field whose type name ends in 'Patch'
534
+ const patchField = inputFields.find((f) => {
535
+ const baseName = (0, introspection_1.getBaseTypeName)(f.type);
536
+ return baseName?.endsWith('Patch');
537
+ });
538
+ if (patchField)
539
+ return patchField.name;
540
+ // Fallback: compute from entity name (v5 default inflection)
541
+ return (0, inflekt_1.lcFirst)(entityName) + 'Patch';
542
+ }
458
543
  // ============================================================================
459
544
  // Inflection Building
460
545
  // ============================================================================
461
546
  /**
462
547
  * Build inflection map from discovered types
463
548
  */
464
- function buildInflection(entityName, typeMap) {
549
+ function buildInflection(entityName, typeMap, entityToConnection) {
465
550
  const pluralName = (0, inflekt_1.pluralize)(entityName);
466
551
  const singularFieldName = (0, inflekt_1.lcFirst)(entityName);
467
552
  const pluralFieldName = (0, inflekt_1.lcFirst)(pluralName);
553
+ const connectionTypeName = entityToConnection.get(entityName) ?? `${pluralName}Connection`;
468
554
  // Check which types actually exist in the schema
469
555
  const hasFilter = typeMap.has(`${entityName}Filter`);
470
556
  const hasPatch = typeMap.has(`${entityName}Patch`);
@@ -478,7 +564,7 @@ function buildInflection(entityName, typeMap) {
478
564
  allRows: pluralFieldName,
479
565
  allRowsSimple: pluralFieldName,
480
566
  conditionType: `${entityName}Condition`,
481
- connection: `${pluralName}Connection`,
567
+ connection: connectionTypeName,
482
568
  createField: `create${entityName}`,
483
569
  createInputType: `Create${entityName}Input`,
484
570
  createPayloadType: `Create${entityName}Payload`,
@@ -58,7 +58,7 @@ export declare function getTableOperationNames(tables: Array<{
58
58
  name: string;
59
59
  query?: {
60
60
  all: string;
61
- one: string;
61
+ one: string | null;
62
62
  create: string;
63
63
  update: string | null;
64
64
  delete: string | null;
@@ -240,7 +240,9 @@ function getTableOperationNames(tables) {
240
240
  if (table.query) {
241
241
  // Add exact query names from _meta
242
242
  queries.add(table.query.all);
243
- queries.add(table.query.one);
243
+ if (table.query.one) {
244
+ queries.add(table.query.one);
245
+ }
244
246
  // Add exact mutation names from _meta
245
247
  mutations.add(table.query.create);
246
248
  if (table.query.update)
package/esm/core/ast.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as t from 'gql-ast';
2
+ import { OperationTypeNode } from 'graphql';
2
3
  import { camelize, singularize } from 'inflekt';
3
4
  import { getCustomAst } from './custom-ast';
4
5
  const NON_MUTABLE_PROPS = ['createdAt', 'createdBy', 'updatedAt', 'updatedBy'];
@@ -39,7 +40,7 @@ const createGqlMutation = ({ operationName, mutationName, selectArgs, selections
39
40
  return t.document({
40
41
  definitions: [
41
42
  t.operationDefinition({
42
- operation: 'mutation',
43
+ operation: OperationTypeNode.MUTATION,
43
44
  name: mutationName,
44
45
  variableDefinitions,
45
46
  selectionSet: t.selectionSet({ selections: opSel }),
@@ -68,7 +69,7 @@ export const getAll = ({ queryName, operationName, selection, }) => {
68
69
  const ast = t.document({
69
70
  definitions: [
70
71
  t.operationDefinition({
71
- operation: 'query',
72
+ operation: OperationTypeNode.QUERY,
72
73
  name: queryName,
73
74
  selectionSet: t.selectionSet({ selections: opSel }),
74
75
  }),
@@ -111,7 +112,7 @@ export const getCount = ({ queryName, operationName, query, }) => {
111
112
  const ast = t.document({
112
113
  definitions: [
113
114
  t.operationDefinition({
114
- operation: 'query',
115
+ operation: OperationTypeNode.QUERY,
115
116
  name: queryName,
116
117
  variableDefinitions,
117
118
  selectionSet: t.selectionSet({ selections: opSel }),
@@ -210,7 +211,7 @@ export const getMany = ({ builder, queryName, operationName, query, selection, }
210
211
  const ast = t.document({
211
212
  definitions: [
212
213
  t.operationDefinition({
213
- operation: 'query',
214
+ operation: OperationTypeNode.QUERY,
214
215
  name: queryName,
215
216
  variableDefinitions,
216
217
  selectionSet: t.selectionSet({
@@ -266,7 +267,7 @@ export const getOne = ({ queryName, operationName, query, selection, }) => {
266
267
  const ast = t.document({
267
268
  definitions: [
268
269
  t.operationDefinition({
269
- operation: 'query',
270
+ operation: OperationTypeNode.QUERY,
270
271
  name: queryName,
271
272
  variableDefinitions,
272
273
  selectionSet: t.selectionSet({ selections: opSel }),