@constructive-io/graphql-codegen 2.24.0 → 2.26.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 (66) hide show
  1. package/README.md +403 -279
  2. package/cli/codegen/babel-ast.d.ts +7 -0
  3. package/cli/codegen/babel-ast.js +15 -0
  4. package/cli/codegen/barrel.js +43 -14
  5. package/cli/codegen/custom-mutations.js +4 -4
  6. package/cli/codegen/custom-queries.js +12 -22
  7. package/cli/codegen/gql-ast.js +22 -1
  8. package/cli/codegen/index.js +1 -0
  9. package/cli/codegen/mutations.d.ts +2 -0
  10. package/cli/codegen/mutations.js +26 -13
  11. package/cli/codegen/orm/client-generator.js +475 -136
  12. package/cli/codegen/orm/custom-ops-generator.js +8 -3
  13. package/cli/codegen/orm/input-types-generator.js +22 -0
  14. package/cli/codegen/orm/model-generator.js +18 -5
  15. package/cli/codegen/orm/select-types.d.ts +33 -0
  16. package/cli/codegen/queries.d.ts +1 -1
  17. package/cli/codegen/queries.js +112 -35
  18. package/cli/codegen/utils.d.ts +6 -0
  19. package/cli/codegen/utils.js +19 -0
  20. package/cli/commands/generate-orm.d.ts +14 -0
  21. package/cli/commands/generate-orm.js +160 -44
  22. package/cli/commands/generate.d.ts +22 -0
  23. package/cli/commands/generate.js +195 -55
  24. package/cli/commands/init.js +29 -9
  25. package/cli/index.js +133 -28
  26. package/cli/watch/orchestrator.d.ts +4 -0
  27. package/cli/watch/orchestrator.js +4 -0
  28. package/esm/cli/codegen/babel-ast.d.ts +7 -0
  29. package/esm/cli/codegen/babel-ast.js +14 -0
  30. package/esm/cli/codegen/barrel.js +44 -15
  31. package/esm/cli/codegen/custom-mutations.js +5 -5
  32. package/esm/cli/codegen/custom-queries.js +13 -23
  33. package/esm/cli/codegen/gql-ast.js +23 -2
  34. package/esm/cli/codegen/index.js +1 -0
  35. package/esm/cli/codegen/mutations.d.ts +2 -0
  36. package/esm/cli/codegen/mutations.js +27 -14
  37. package/esm/cli/codegen/orm/client-generator.js +475 -136
  38. package/esm/cli/codegen/orm/custom-ops-generator.js +8 -3
  39. package/esm/cli/codegen/orm/input-types-generator.js +22 -0
  40. package/esm/cli/codegen/orm/model-generator.js +18 -5
  41. package/esm/cli/codegen/orm/select-types.d.ts +33 -0
  42. package/esm/cli/codegen/queries.d.ts +1 -1
  43. package/esm/cli/codegen/queries.js +114 -37
  44. package/esm/cli/codegen/utils.d.ts +6 -0
  45. package/esm/cli/codegen/utils.js +18 -0
  46. package/esm/cli/commands/generate-orm.d.ts +14 -0
  47. package/esm/cli/commands/generate-orm.js +161 -45
  48. package/esm/cli/commands/generate.d.ts +22 -0
  49. package/esm/cli/commands/generate.js +195 -56
  50. package/esm/cli/commands/init.js +29 -9
  51. package/esm/cli/index.js +134 -29
  52. package/esm/cli/watch/orchestrator.d.ts +4 -0
  53. package/esm/cli/watch/orchestrator.js +5 -1
  54. package/esm/types/config.d.ts +39 -2
  55. package/esm/types/config.js +88 -4
  56. package/esm/types/index.d.ts +2 -2
  57. package/esm/types/index.js +1 -1
  58. package/package.json +10 -7
  59. package/types/config.d.ts +39 -2
  60. package/types/config.js +91 -4
  61. package/types/index.d.ts +2 -2
  62. package/types/index.js +2 -1
  63. package/cli/codegen/orm/query-builder.d.ts +0 -161
  64. package/cli/codegen/orm/query-builder.js +0 -366
  65. package/esm/cli/codegen/orm/query-builder.d.ts +0 -161
  66. package/esm/cli/codegen/orm/query-builder.js +0 -353
@@ -122,9 +122,14 @@ function buildOperationMethod(op, operationType) {
122
122
  const optionsParam = t.identifier('options');
123
123
  optionsParam.optional = true;
124
124
  if (selectTypeName) {
125
+ // Use DeepExact<S, SelectType> to enforce strict field validation
126
+ // This catches invalid fields even when mixed with valid ones
125
127
  optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeLiteral([
126
128
  (() => {
127
- const prop = t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier('S'))));
129
+ const prop = t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
130
+ t.tsTypeReference(t.identifier('S')),
131
+ t.tsTypeReference(t.identifier(selectTypeName)),
132
+ ]))));
128
133
  prop.optional = true;
129
134
  return prop;
130
135
  })(),
@@ -192,7 +197,7 @@ export function generateCustomQueryOpsFile(operations) {
192
197
  // Add imports
193
198
  statements.push(createImportDeclaration('../client', ['OrmClient']));
194
199
  statements.push(createImportDeclaration('../query-builder', ['QueryBuilder', 'buildCustomDocument']));
195
- statements.push(createImportDeclaration('../select-types', ['InferSelectResult'], true));
200
+ statements.push(createImportDeclaration('../select-types', ['InferSelectResult', 'DeepExact'], true));
196
201
  if (allTypeImports.length > 0) {
197
202
  statements.push(createImportDeclaration('../input-types', allTypeImports, true));
198
203
  }
@@ -230,7 +235,7 @@ export function generateCustomMutationOpsFile(operations) {
230
235
  // Add imports
231
236
  statements.push(createImportDeclaration('../client', ['OrmClient']));
232
237
  statements.push(createImportDeclaration('../query-builder', ['QueryBuilder', 'buildCustomDocument']));
233
- statements.push(createImportDeclaration('../select-types', ['InferSelectResult'], true));
238
+ statements.push(createImportDeclaration('../select-types', ['InferSelectResult', 'DeepExact'], true));
234
239
  if (allTypeImports.length > 0) {
235
240
  statements.push(createImportDeclaration('../input-types', allTypeImports, true));
236
241
  }
@@ -193,6 +193,22 @@ const SCALAR_FILTER_CONFIGS = [
193
193
  operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'],
194
194
  },
195
195
  { name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] },
196
+ // List filters (for array fields like string[], int[], uuid[])
197
+ {
198
+ name: 'StringListFilter',
199
+ tsType: 'string[]',
200
+ operators: ['equality', 'distinct', 'comparison', 'listArray'],
201
+ },
202
+ {
203
+ name: 'IntListFilter',
204
+ tsType: 'number[]',
205
+ operators: ['equality', 'distinct', 'comparison', 'listArray'],
206
+ },
207
+ {
208
+ name: 'UUIDListFilter',
209
+ tsType: 'string[]',
210
+ operators: ['equality', 'distinct', 'comparison', 'listArray'],
211
+ },
196
212
  ];
197
213
  /**
198
214
  * Build filter properties based on operator sets
@@ -232,6 +248,12 @@ function buildScalarFilterProperties(config) {
232
248
  if (operators.includes('fulltext')) {
233
249
  props.push({ name: 'matches', type: 'string', optional: true });
234
250
  }
251
+ // List/Array operators (contains, overlaps, anyEqualTo, etc.)
252
+ if (operators.includes('listArray')) {
253
+ // Extract base type from array type (e.g., 'string[]' -> 'string')
254
+ const baseType = tsType.replace('[]', '');
255
+ props.push({ name: 'contains', type: tsType, optional: true }, { name: 'containedBy', type: tsType, optional: true }, { name: 'overlaps', type: tsType, optional: true }, { name: 'anyEqualTo', type: baseType, optional: true }, { name: 'anyNotEqualTo', type: baseType, optional: true }, { name: 'anyLessThan', type: baseType, optional: true }, { name: 'anyLessThanOrEqualTo', type: baseType, optional: true }, { name: 'anyGreaterThan', type: baseType, optional: true }, { name: 'anyGreaterThanOrEqualTo', type: baseType, optional: true });
256
+ }
235
257
  return props;
236
258
  }
237
259
  /**
@@ -63,7 +63,7 @@ export function generateModelFile(table, _useSharedTypes) {
63
63
  ]));
64
64
  statements.push(createImportDeclaration('../select-types', [
65
65
  'ConnectionResult', 'FindManyArgs', 'FindFirstArgs', 'CreateArgs',
66
- 'UpdateArgs', 'DeleteArgs', 'InferSelectResult',
66
+ 'UpdateArgs', 'DeleteArgs', 'InferSelectResult', 'DeepExact',
67
67
  ], true));
68
68
  statements.push(createImportDeclaration('../input-types', [
69
69
  typeName, relationTypeName, selectTypeName, whereTypeName, orderByTypeName,
@@ -77,10 +77,14 @@ export function generateModelFile(table, _useSharedTypes) {
77
77
  paramProp.accessibility = 'private';
78
78
  classBody.push(t.classMethod('constructor', t.identifier('constructor'), [paramProp], t.blockStatement([])));
79
79
  // findMany method
80
+ // Use DeepExact<S, SelectType> to enforce strict field validation
80
81
  const findManyParam = t.identifier('args');
81
82
  findManyParam.optional = true;
82
83
  findManyParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FindManyArgs'), t.tsTypeParameterInstantiation([
83
- t.tsTypeReference(t.identifier('S')),
84
+ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
85
+ t.tsTypeReference(t.identifier('S')),
86
+ t.tsTypeReference(t.identifier(selectTypeName)),
87
+ ])),
84
88
  t.tsTypeReference(t.identifier(whereTypeName)),
85
89
  t.tsTypeReference(t.identifier(orderByTypeName)),
86
90
  ])));
@@ -115,7 +119,10 @@ export function generateModelFile(table, _useSharedTypes) {
115
119
  const findFirstParam = t.identifier('args');
116
120
  findFirstParam.optional = true;
117
121
  findFirstParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FindFirstArgs'), t.tsTypeParameterInstantiation([
118
- t.tsTypeReference(t.identifier('S')),
122
+ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
123
+ t.tsTypeReference(t.identifier('S')),
124
+ t.tsTypeReference(t.identifier(selectTypeName)),
125
+ ])),
119
126
  t.tsTypeReference(t.identifier(whereTypeName)),
120
127
  ])));
121
128
  const findFirstReturnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
@@ -141,7 +148,10 @@ export function generateModelFile(table, _useSharedTypes) {
141
148
  // create method
142
149
  const createParam = t.identifier('args');
143
150
  createParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('CreateArgs'), t.tsTypeParameterInstantiation([
144
- t.tsTypeReference(t.identifier('S')),
151
+ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
152
+ t.tsTypeReference(t.identifier('S')),
153
+ t.tsTypeReference(t.identifier(selectTypeName)),
154
+ ])),
145
155
  t.tsIndexedAccessType(t.tsTypeReference(t.identifier(createInputTypeName)), t.tsLiteralType(t.stringLiteral(singularName))),
146
156
  ])));
147
157
  const createReturnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
@@ -167,7 +177,10 @@ export function generateModelFile(table, _useSharedTypes) {
167
177
  if (updateMutationName) {
168
178
  const updateParam = t.identifier('args');
169
179
  updateParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('UpdateArgs'), t.tsTypeParameterInstantiation([
170
- t.tsTypeReference(t.identifier('S')),
180
+ t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
181
+ t.tsTypeReference(t.identifier('S')),
182
+ t.tsTypeReference(t.identifier(selectTypeName)),
183
+ ])),
171
184
  t.tsTypeLiteral([t.tsPropertySignature(t.identifier('id'), t.tsTypeAnnotation(t.tsStringKeyword()))]),
172
185
  t.tsTypeReference(t.identifier(patchTypeName)),
173
186
  ])));
@@ -44,6 +44,39 @@ export interface NestedSelectConfig {
44
44
  filter?: Record<string, unknown>;
45
45
  orderBy?: string[];
46
46
  }
47
+ /**
48
+ * Recursively validates select objects, rejecting unknown keys.
49
+ *
50
+ * This type ensures that users can only select fields that actually exist
51
+ * in the GraphQL schema. It returns `never` if any excess keys are found
52
+ * at any nesting level, causing a TypeScript compile error.
53
+ *
54
+ * Why this is needed:
55
+ * TypeScript's excess property checking has a quirk where it only catches
56
+ * invalid fields when they are the ONLY fields. When mixed with valid fields
57
+ * (e.g., `{ id: true, invalidField: true }`), the structural typing allows
58
+ * the excess property through. This type explicitly checks for and rejects
59
+ * such cases.
60
+ *
61
+ * @example
62
+ * // This will cause a type error because 'invalid' doesn't exist:
63
+ * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
64
+ * // Result = never (causes assignment error)
65
+ *
66
+ * @example
67
+ * // This works because all fields are valid:
68
+ * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
69
+ * // Result = { id: true }
70
+ */
71
+ export type DeepExact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? {
72
+ [K in keyof T]: K extends keyof Shape ? T[K] extends {
73
+ select: infer NS;
74
+ } ? Shape[K] extends {
75
+ select?: infer ShapeNS;
76
+ } ? {
77
+ select: DeepExact<NS, NonNullable<ShapeNS>>;
78
+ } : T[K] : T[K] : never;
79
+ } : never : never;
47
80
  /**
48
81
  * Infers the result type from a select configuration
49
82
  *
@@ -17,5 +17,5 @@ export interface QueryGeneratorOptions {
17
17
  hasRelationships?: boolean;
18
18
  }
19
19
  export declare function generateListQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile;
20
- export declare function generateSingleQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile;
20
+ export declare function generateSingleQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile | null;
21
21
  export declare function generateAllQueryHooks(tables: CleanTable[], options?: QueryGeneratorOptions): GeneratedQueryFile[];
@@ -1,7 +1,7 @@
1
1
  import * as t from '@babel/types';
2
- import { generateCode, addJSDocComment, typedParam } from './babel-ast';
2
+ import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast';
3
3
  import { buildListQueryAST, buildSingleQueryAST, printGraphQL, } from './gql-ast';
4
- import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, toScreamingSnake, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
4
+ import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getConditionTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, hasValidPrimaryKey, fieldTypeToTs, toScreamingSnake, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
5
5
  function createUnionType(values) {
6
6
  return t.tsUnionType(values.map((v) => t.tsLiteralType(t.stringLiteral(v))));
7
7
  }
@@ -34,6 +34,7 @@ export function generateListQueryHook(table, options = {}) {
34
34
  const hookName = getListQueryHookName(table);
35
35
  const queryName = getAllRowsQueryName(table);
36
36
  const filterTypeName = getFilterTypeName(table);
37
+ const conditionTypeName = getConditionTypeName(table);
37
38
  const orderByTypeName = getOrderByTypeName(table);
38
39
  const scalarFields = getScalarFields(table);
39
40
  const keysName = `${lcFirst(typeName)}Keys`;
@@ -98,6 +99,59 @@ export function generateListQueryHook(table, options = {}) {
98
99
  })
99
100
  .filter((f) => f !== null);
100
101
  statements.push(createFilterInterfaceDeclaration(filterTypeName, fieldFilters, false));
102
+ // Generate Condition interface (simple equality filter with scalar types)
103
+ // Track non-primitive types (enums) that need to be imported
104
+ const enumTypesUsed = new Set();
105
+ const conditionProperties = scalarFields.map((field) => {
106
+ const tsType = fieldTypeToTs(field.type);
107
+ const isPrimitive = tsType === 'string' ||
108
+ tsType === 'number' ||
109
+ tsType === 'boolean' ||
110
+ tsType === 'unknown' ||
111
+ tsType.endsWith('[]');
112
+ let typeAnnotation;
113
+ if (field.type.isArray) {
114
+ const baseType = tsType.replace('[]', '');
115
+ const isBasePrimitive = baseType === 'string' ||
116
+ baseType === 'number' ||
117
+ baseType === 'boolean' ||
118
+ baseType === 'unknown';
119
+ if (!isBasePrimitive) {
120
+ enumTypesUsed.add(baseType);
121
+ }
122
+ typeAnnotation = t.tsArrayType(baseType === 'string'
123
+ ? t.tsStringKeyword()
124
+ : baseType === 'number'
125
+ ? t.tsNumberKeyword()
126
+ : baseType === 'boolean'
127
+ ? t.tsBooleanKeyword()
128
+ : t.tsTypeReference(t.identifier(baseType)));
129
+ }
130
+ else {
131
+ if (!isPrimitive) {
132
+ enumTypesUsed.add(tsType);
133
+ }
134
+ typeAnnotation =
135
+ tsType === 'string'
136
+ ? t.tsStringKeyword()
137
+ : tsType === 'number'
138
+ ? t.tsNumberKeyword()
139
+ : tsType === 'boolean'
140
+ ? t.tsBooleanKeyword()
141
+ : t.tsTypeReference(t.identifier(tsType));
142
+ }
143
+ const prop = t.tsPropertySignature(t.identifier(field.name), t.tsTypeAnnotation(typeAnnotation));
144
+ prop.optional = true;
145
+ return prop;
146
+ });
147
+ // Add import for enum types if any are used
148
+ if (enumTypesUsed.size > 0) {
149
+ const schemaTypesImport = t.importDeclaration(Array.from(enumTypesUsed).map((et) => t.importSpecifier(t.identifier(et), t.identifier(et))), t.stringLiteral('../schema-types'));
150
+ schemaTypesImport.importKind = 'type';
151
+ statements.push(schemaTypesImport);
152
+ }
153
+ const conditionInterface = t.tsInterfaceDeclaration(t.identifier(conditionTypeName), null, null, t.tsInterfaceBody(conditionProperties));
154
+ statements.push(conditionInterface);
101
155
  const orderByValues = [
102
156
  ...scalarFields.flatMap((f) => [
103
157
  `${toScreamingSnake(f.name)}_ASC`,
@@ -115,16 +169,36 @@ export function generateListQueryHook(table, options = {}) {
115
169
  p.optional = true;
116
170
  return p;
117
171
  })(),
172
+ (() => {
173
+ const p = t.tsPropertySignature(t.identifier('last'), t.tsTypeAnnotation(t.tsNumberKeyword()));
174
+ p.optional = true;
175
+ return p;
176
+ })(),
118
177
  (() => {
119
178
  const p = t.tsPropertySignature(t.identifier('offset'), t.tsTypeAnnotation(t.tsNumberKeyword()));
120
179
  p.optional = true;
121
180
  return p;
122
181
  })(),
182
+ (() => {
183
+ const p = t.tsPropertySignature(t.identifier('before'), t.tsTypeAnnotation(t.tsStringKeyword()));
184
+ p.optional = true;
185
+ return p;
186
+ })(),
187
+ (() => {
188
+ const p = t.tsPropertySignature(t.identifier('after'), t.tsTypeAnnotation(t.tsStringKeyword()));
189
+ p.optional = true;
190
+ return p;
191
+ })(),
123
192
  (() => {
124
193
  const p = t.tsPropertySignature(t.identifier('filter'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filterTypeName))));
125
194
  p.optional = true;
126
195
  return p;
127
196
  })(),
197
+ (() => {
198
+ const p = t.tsPropertySignature(t.identifier('condition'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(conditionTypeName))));
199
+ p.optional = true;
200
+ return p;
201
+ })(),
128
202
  (() => {
129
203
  const p = t.tsPropertySignature(t.identifier('orderBy'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(orderByTypeName)))));
130
204
  p.optional = true;
@@ -184,9 +258,9 @@ export function generateListQueryHook(table, options = {}) {
184
258
  hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
185
259
  t.objectExpression([
186
260
  t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables'), t.identifier('scope')])),
187
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
188
- t.identifier(`${queryName}QueryDocument`),
189
- t.identifier('variables'),
261
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
262
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
263
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
190
264
  ]))),
191
265
  t.spreadElement(t.identifier('queryOptions')),
192
266
  ]),
@@ -196,9 +270,9 @@ export function generateListQueryHook(table, options = {}) {
196
270
  hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
197
271
  t.objectExpression([
198
272
  t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables')])),
199
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
200
- t.identifier(`${queryName}QueryDocument`),
201
- t.identifier('variables'),
273
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
274
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
275
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
202
276
  ]))),
203
277
  t.spreadElement(t.identifier('options')),
204
278
  ]),
@@ -210,9 +284,9 @@ export function generateListQueryHook(table, options = {}) {
210
284
  t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
211
285
  t.identifier('variables'),
212
286
  ])),
213
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
214
- t.identifier(`${queryName}QueryDocument`),
215
- t.identifier('variables'),
287
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
288
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
289
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
216
290
  ]))),
217
291
  t.spreadElement(t.identifier('options')),
218
292
  ]),
@@ -260,10 +334,9 @@ export function generateListQueryHook(table, options = {}) {
260
334
  statements.push(hookExport);
261
335
  }
262
336
  const fetchFuncBody = t.blockStatement([
263
- t.returnStatement(t.callExpression(t.identifier('execute'), [
264
- t.identifier(`${queryName}QueryDocument`),
265
- t.identifier('variables'),
266
- t.identifier('options'),
337
+ t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
338
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
339
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
267
340
  ])),
268
341
  ]);
269
342
  const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(pluralName)}Query`), [
@@ -314,10 +387,9 @@ export function generateListQueryHook(table, options = {}) {
314
387
  t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
315
388
  t.objectExpression([
316
389
  t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
317
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
318
- t.identifier(`${queryName}QueryDocument`),
319
- t.identifier('variables'),
320
- t.identifier('options'),
390
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
391
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
392
+ t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
321
393
  ]))),
322
394
  ]),
323
395
  ]))),
@@ -347,6 +419,10 @@ export function generateListQueryHook(table, options = {}) {
347
419
  };
348
420
  }
349
421
  export function generateSingleQueryHook(table, options = {}) {
422
+ // Skip tables with composite keys - they are handled as custom queries
423
+ if (!hasValidPrimaryKey(table)) {
424
+ return null;
425
+ }
350
426
  const { reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, } = options;
351
427
  const { typeName, singularName } = getTableNames(table);
352
428
  const hookName = getSingleQueryHookName(table);
@@ -454,9 +530,9 @@ export function generateSingleQueryHook(table, options = {}) {
454
530
  t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
455
531
  t.identifier('scope'),
456
532
  ])),
457
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
458
- t.identifier(`${queryName}QueryDocument`),
459
- t.identifier('variables'),
533
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
534
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
535
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
460
536
  ]))),
461
537
  t.spreadElement(t.identifier('queryOptions')),
462
538
  ]),
@@ -468,9 +544,9 @@ export function generateSingleQueryHook(table, options = {}) {
468
544
  t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [
469
545
  t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
470
546
  ])),
471
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
472
- t.identifier(`${queryName}QueryDocument`),
473
- t.identifier('variables'),
547
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
548
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
549
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
474
550
  ]))),
475
551
  t.spreadElement(t.identifier('options')),
476
552
  ]),
@@ -482,9 +558,9 @@ export function generateSingleQueryHook(table, options = {}) {
482
558
  t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
483
559
  t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
484
560
  ])),
485
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
486
- t.identifier(`${queryName}QueryDocument`),
487
- t.identifier('variables'),
561
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
562
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
563
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
488
564
  ]))),
489
565
  t.spreadElement(t.identifier('options')),
490
566
  ]),
@@ -528,10 +604,9 @@ export function generateSingleQueryHook(table, options = {}) {
528
604
  statements.push(hookExport);
529
605
  }
530
606
  const fetchFuncBody = t.blockStatement([
531
- t.returnStatement(t.callExpression(t.identifier('execute'), [
532
- t.identifier(`${queryName}QueryDocument`),
533
- t.identifier('variables'),
534
- t.identifier('options'),
607
+ t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
608
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
609
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
535
610
  ])),
536
611
  ]);
537
612
  const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(singularName)}Query`), [
@@ -578,10 +653,9 @@ export function generateSingleQueryHook(table, options = {}) {
578
653
  t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
579
654
  t.objectExpression([
580
655
  t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
581
- t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
582
- t.identifier(`${queryName}QueryDocument`),
583
- t.identifier('variables'),
584
- t.identifier('options'),
656
+ t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
657
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
658
+ t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
585
659
  ]))),
586
660
  ]),
587
661
  ]))),
@@ -614,7 +688,10 @@ export function generateAllQueryHooks(tables, options = {}) {
614
688
  const files = [];
615
689
  for (const table of tables) {
616
690
  files.push(generateListQueryHook(table, options));
617
- files.push(generateSingleQueryHook(table, options));
691
+ const singleHook = generateSingleQueryHook(table, options);
692
+ if (singleHook) {
693
+ files.push(singleHook);
694
+ }
618
695
  }
619
696
  return files;
620
697
  }
@@ -171,6 +171,12 @@ export declare function getPrimaryKeyInfo(table: CleanTable): PrimaryKeyField[];
171
171
  * Get primary key field names (convenience wrapper)
172
172
  */
173
173
  export declare function getPrimaryKeyFields(table: CleanTable): string[];
174
+ /**
175
+ * Check if table has a valid single-field primary key
176
+ * Used to determine if a single query hook can be generated
177
+ * Tables with composite keys return false (handled as custom queries)
178
+ */
179
+ export declare function hasValidPrimaryKey(table: CleanTable): boolean;
174
180
  /**
175
181
  * Generate query key prefix for a table
176
182
  * e.g., "cars" for list queries, "car" for detail queries
@@ -291,6 +291,24 @@ export function getPrimaryKeyInfo(table) {
291
291
  export function getPrimaryKeyFields(table) {
292
292
  return getPrimaryKeyInfo(table).map((pk) => pk.name);
293
293
  }
294
+ /**
295
+ * Check if table has a valid single-field primary key
296
+ * Used to determine if a single query hook can be generated
297
+ * Tables with composite keys return false (handled as custom queries)
298
+ */
299
+ export function hasValidPrimaryKey(table) {
300
+ // Check for explicit primary key constraint with single field
301
+ const pk = table.constraints?.primaryKey?.[0];
302
+ if (pk && pk.fields.length === 1) {
303
+ return true;
304
+ }
305
+ // Check for 'id' field as fallback
306
+ const idField = table.fields.find((f) => f.name.toLowerCase() === 'id');
307
+ if (idField) {
308
+ return true;
309
+ }
310
+ return false;
311
+ }
294
312
  // ============================================================================
295
313
  // Query key generation
296
314
  // ============================================================================
@@ -9,6 +9,8 @@
9
9
  export interface GenerateOrmOptions {
10
10
  /** Path to config file */
11
11
  config?: string;
12
+ /** Named target in a multi-target config */
13
+ target?: string;
12
14
  /** GraphQL endpoint URL (overrides config) */
13
15
  endpoint?: string;
14
16
  /** Path to GraphQL schema file (.graphql) */
@@ -24,9 +26,21 @@ export interface GenerateOrmOptions {
24
26
  /** Skip custom operations (only generate table CRUD) */
25
27
  skipCustomOperations?: boolean;
26
28
  }
29
+ export interface GenerateOrmTargetResult {
30
+ name: string;
31
+ output: string;
32
+ success: boolean;
33
+ message: string;
34
+ tables?: string[];
35
+ customQueries?: string[];
36
+ customMutations?: string[];
37
+ filesWritten?: string[];
38
+ errors?: string[];
39
+ }
27
40
  export interface GenerateOrmResult {
28
41
  success: boolean;
29
42
  message: string;
43
+ targets?: GenerateOrmTargetResult[];
30
44
  tables?: string[];
31
45
  customQueries?: string[];
32
46
  customMutations?: string[];