@constructive-io/graphql-codegen 2.24.0 → 2.24.1

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.
@@ -1,5 +1,5 @@
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 { buildCreateMutationAST, buildUpdateMutationAST, buildDeleteMutationAST, printGraphQL, } from './gql-ast';
4
4
  import { getTableNames, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, getCreateMutationFileName, getUpdateMutationFileName, getDeleteMutationFileName, getCreateMutationName, getUpdateMutationName, getDeleteMutationName, getScalarFields, getPrimaryKeyInfo, fieldTypeToTs, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
5
5
  function isAutoGeneratedField(fieldName, pkFieldNames) {
@@ -14,7 +14,7 @@ function isAutoGeneratedField(fieldName, pkFieldNames) {
14
14
  return timestampPatterns.includes(name);
15
15
  }
16
16
  export function generateCreateMutationHook(table, options = {}) {
17
- const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, } = options;
17
+ const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, tableTypeNames = new Set(), } = options;
18
18
  if (!reactQueryEnabled) {
19
19
  return null;
20
20
  }
@@ -28,11 +28,16 @@ export function generateCreateMutationHook(table, options = {}) {
28
28
  const scalarFields = getScalarFields(table);
29
29
  const pkFieldNames = new Set(getPrimaryKeyInfo(table).map((pk) => pk.name));
30
30
  const usedEnums = new Set();
31
+ const usedTableTypes = new Set();
31
32
  for (const field of scalarFields) {
32
33
  const cleanType = field.type.gqlType.replace(/!/g, '');
33
34
  if (enumSet.has(cleanType)) {
34
35
  usedEnums.add(cleanType);
35
36
  }
37
+ else if (tableTypeNames.has(cleanType) && cleanType !== typeName) {
38
+ // Track table types used in scalar fields (excluding the main type which is already imported)
39
+ usedTableTypes.add(cleanType);
40
+ }
36
41
  }
37
42
  const mutationAST = buildCreateMutationAST({ table });
38
43
  const mutationDocument = printGraphQL(mutationAST);
@@ -47,7 +52,9 @@ export function generateCreateMutationHook(table, options = {}) {
47
52
  statements.push(reactQueryTypeImport);
48
53
  const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
49
54
  statements.push(clientImport);
50
- const typesImport = t.importDeclaration([t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
55
+ // Import the main type and any other table types used in scalar fields
56
+ const allTypesToImport = [typeName, ...Array.from(usedTableTypes)].sort();
57
+ const typesImport = t.importDeclaration(allTypesToImport.map((t_) => t.importSpecifier(t.identifier(t_), t.identifier(t_))), t.stringLiteral('../types'));
51
58
  typesImport.importKind = 'type';
52
59
  statements.push(typesImport);
53
60
  if (usedEnums.size > 0) {
@@ -108,9 +115,9 @@ export function generateCreateMutationHook(table, options = {}) {
108
115
  if (useCentralizedKeys) {
109
116
  mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.callExpression(t.memberExpression(t.identifier(mutationKeysName), t.identifier('create')), [])));
110
117
  }
111
- mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
112
- t.identifier(`${mutationName}MutationDocument`),
113
- t.identifier('variables'),
118
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], [
119
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)),
120
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)),
114
121
  ]))));
115
122
  const invalidateQueryKey = useCentralizedKeys
116
123
  ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('lists')), [])
@@ -151,7 +158,7 @@ export function generateCreateMutationHook(table, options = {}) {
151
158
  };
152
159
  }
153
160
  export function generateUpdateMutationHook(table, options = {}) {
154
- const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, } = options;
161
+ const { reactQueryEnabled = true, enumsFromSchemaTypes = [], useCentralizedKeys = true, hasRelationships = false, tableTypeNames = new Set(), } = options;
155
162
  if (!reactQueryEnabled) {
156
163
  return null;
157
164
  }
@@ -170,11 +177,15 @@ export function generateUpdateMutationHook(table, options = {}) {
170
177
  const pkField = pkFields[0];
171
178
  const pkFieldNames = new Set(pkFields.map((pk) => pk.name));
172
179
  const usedEnums = new Set();
180
+ const usedTableTypes = new Set();
173
181
  for (const field of scalarFields) {
174
182
  const cleanType = field.type.gqlType.replace(/!/g, '');
175
183
  if (enumSet.has(cleanType)) {
176
184
  usedEnums.add(cleanType);
177
185
  }
186
+ else if (tableTypeNames.has(cleanType) && cleanType !== typeName) {
187
+ usedTableTypes.add(cleanType);
188
+ }
178
189
  }
179
190
  const mutationAST = buildUpdateMutationAST({ table });
180
191
  const mutationDocument = printGraphQL(mutationAST);
@@ -189,7 +200,9 @@ export function generateUpdateMutationHook(table, options = {}) {
189
200
  statements.push(reactQueryTypeImport);
190
201
  const clientImport = t.importDeclaration([t.importSpecifier(t.identifier('execute'), t.identifier('execute'))], t.stringLiteral('../client'));
191
202
  statements.push(clientImport);
192
- const typesImport = t.importDeclaration([t.importSpecifier(t.identifier(typeName), t.identifier(typeName))], t.stringLiteral('../types'));
203
+ // Import the main type and any other table types used in scalar fields
204
+ const allTypesToImportUpdate = [typeName, ...Array.from(usedTableTypes)].sort();
205
+ const typesImport = t.importDeclaration(allTypesToImportUpdate.map((t_) => t.importSpecifier(t.identifier(t_), t.identifier(t_))), t.stringLiteral('../types'));
193
206
  typesImport.importKind = 'type';
194
207
  statements.push(typesImport);
195
208
  if (usedEnums.size > 0) {
@@ -256,9 +269,9 @@ export function generateUpdateMutationHook(table, options = {}) {
256
269
  if (useCentralizedKeys) {
257
270
  mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.memberExpression(t.identifier(mutationKeysName), t.identifier('all'))));
258
271
  }
259
- mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
260
- t.identifier(`${mutationName}MutationDocument`),
261
- t.identifier('variables'),
272
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], [
273
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)),
274
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)),
262
275
  ]))));
263
276
  const detailQueryKey = useCentralizedKeys
264
277
  ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))])
@@ -378,9 +391,9 @@ export function generateDeleteMutationHook(table, options = {}) {
378
391
  if (useCentralizedKeys) {
379
392
  mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.memberExpression(t.identifier(mutationKeysName), t.identifier('all'))));
380
393
  }
381
- mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], t.callExpression(t.identifier('execute'), [
382
- t.identifier(`${mutationName}MutationDocument`),
383
- t.identifier('variables'),
394
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)))], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${mutationName}MutationDocument`), t.identifier('variables')], [
395
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationResult`)),
396
+ t.tsTypeReference(t.identifier(`${ucFirst(mutationName)}MutationVariables`)),
384
397
  ]))));
385
398
  const detailQueryKey = useCentralizedKeys
386
399
  ? t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [t.memberExpression(t.memberExpression(t.identifier('variables'), t.identifier('input')), t.identifier(pkField.name))])
@@ -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
  /**
@@ -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
  // ============================================================================
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "2.24.0",
3
+ "version": "2.24.1",
4
4
  "description": "CLI-based GraphQL SDK generator for PostGraphile endpoints with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -83,5 +83,5 @@
83
83
  "tsx": "^4.21.0",
84
84
  "typescript": "^5.9.3"
85
85
  },
86
- "gitHead": "36d48ae1cde080add7953f46a6738a31909cf050"
86
+ "gitHead": "3e08dd946ec5eab57c296e8df180f1dcb1d06514"
87
87
  }