@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
package/cli/index.js CHANGED
@@ -42,6 +42,17 @@ const generate_1 = require("./commands/generate");
42
42
  const generate_orm_1 = require("./commands/generate-orm");
43
43
  const watch_1 = require("./watch");
44
44
  const config_1 = require("../types/config");
45
+ /**
46
+ * Format duration in a human-readable way
47
+ * - Under 1 second: show milliseconds (e.g., "123ms")
48
+ * - Over 1 second: show seconds with 2 decimal places (e.g., "1.23s")
49
+ */
50
+ function formatDuration(ms) {
51
+ if (ms < 1000) {
52
+ return `${Math.round(ms)}ms`;
53
+ }
54
+ return `${(ms / 1000).toFixed(2)}s`;
55
+ }
45
56
  const program = new commander_1.Command();
46
57
  /**
47
58
  * Load configuration for watch mode, merging CLI options with config file
@@ -61,23 +72,27 @@ async function loadWatchConfig(options) {
61
72
  }
62
73
  baseConfig = loadResult.config;
63
74
  }
64
- // Merge CLI options with config
65
- const mergedConfig = {
66
- endpoint: options.endpoint || baseConfig.endpoint || '',
67
- output: baseConfig.output,
68
- headers: baseConfig.headers,
69
- tables: baseConfig.tables,
70
- queries: baseConfig.queries,
71
- mutations: baseConfig.mutations,
72
- excludeFields: baseConfig.excludeFields,
73
- hooks: baseConfig.hooks,
74
- postgraphile: baseConfig.postgraphile,
75
- codegen: baseConfig.codegen,
76
- orm: baseConfig.orm,
77
- reactQuery: baseConfig.reactQuery,
75
+ if ((0, config_1.isMultiConfig)(baseConfig)) {
76
+ if (!options.target) {
77
+ console.error('x Watch mode requires --target when using multiple targets.');
78
+ return null;
79
+ }
80
+ if (!baseConfig.targets[options.target]) {
81
+ console.error(`x Target "${options.target}" not found in config file.`);
82
+ return null;
83
+ }
84
+ }
85
+ else if (options.target) {
86
+ console.error('x Config file does not define targets. Remove --target.');
87
+ return null;
88
+ }
89
+ const sourceOverrides = {};
90
+ if (options.endpoint) {
91
+ sourceOverrides.endpoint = options.endpoint;
92
+ sourceOverrides.schema = undefined;
93
+ }
94
+ const watchOverrides = {
78
95
  watch: {
79
- ...baseConfig.watch,
80
- // CLI options override config
81
96
  ...(options.pollInterval !== undefined && {
82
97
  pollInterval: options.pollInterval,
83
98
  }),
@@ -86,11 +101,26 @@ async function loadWatchConfig(options) {
86
101
  ...(options.clear !== undefined && { clearScreen: options.clear }),
87
102
  },
88
103
  };
89
- if (!mergedConfig.endpoint) {
90
- console.error('x No endpoint specified. Use --endpoint or create a config file with "graphql-codegen init".');
104
+ let mergedTarget;
105
+ if ((0, config_1.isMultiConfig)(baseConfig)) {
106
+ const defaults = baseConfig.defaults ?? {};
107
+ const targetConfig = baseConfig.targets[options.target];
108
+ mergedTarget = (0, config_1.mergeConfig)(defaults, targetConfig);
109
+ }
110
+ else {
111
+ mergedTarget = baseConfig;
112
+ }
113
+ mergedTarget = (0, config_1.mergeConfig)(mergedTarget, sourceOverrides);
114
+ mergedTarget = (0, config_1.mergeConfig)(mergedTarget, watchOverrides);
115
+ if (!mergedTarget.endpoint) {
116
+ console.error('x No endpoint specified. Watch mode only supports live endpoints.');
117
+ return null;
118
+ }
119
+ if (mergedTarget.schema) {
120
+ console.error('x Watch mode is only supported with an endpoint, not schema.');
91
121
  return null;
92
122
  }
93
- return (0, config_1.resolveConfig)(mergedConfig);
123
+ return (0, config_1.resolveConfig)(mergedTarget);
94
124
  }
95
125
  program
96
126
  .name('graphql-codegen')
@@ -105,17 +135,19 @@ program
105
135
  .option('-e, --endpoint <url>', 'GraphQL endpoint URL to pre-populate')
106
136
  .option('-o, --output <dir>', 'Output directory to pre-populate', './generated')
107
137
  .action(async (options) => {
138
+ const startTime = performance.now();
108
139
  const result = await (0, init_1.initCommand)({
109
140
  directory: options.directory,
110
141
  force: options.force,
111
142
  endpoint: options.endpoint,
112
143
  output: options.output,
113
144
  });
145
+ const duration = formatDuration(performance.now() - startTime);
114
146
  if (result.success) {
115
- console.log('[ok]', result.message);
147
+ console.log('[ok]', result.message, `(${duration})`);
116
148
  }
117
149
  else {
118
- console.error('x', result.message);
150
+ console.error('x', result.message, `(${duration})`);
119
151
  process.exit(1);
120
152
  }
121
153
  });
@@ -124,6 +156,7 @@ program
124
156
  .command('generate')
125
157
  .description('Generate SDK from GraphQL endpoint or schema file')
126
158
  .option('-c, --config <path>', 'Path to config file')
159
+ .option('-t, --target <name>', 'Target name in config file')
127
160
  .option('-e, --endpoint <url>', 'GraphQL endpoint URL (overrides config)')
128
161
  .option('-s, --schema <path>', 'Path to GraphQL schema file (.graphql)')
129
162
  .option('-o, --output <dir>', 'Output directory (overrides config)')
@@ -136,6 +169,7 @@ program
136
169
  .option('--touch <file>', 'File to touch on schema change')
137
170
  .option('--no-clear', 'Do not clear terminal on regeneration')
138
171
  .action(async (options) => {
172
+ const startTime = performance.now();
139
173
  // Validate source options
140
174
  if (options.endpoint && options.schema) {
141
175
  console.error('x Cannot use both --endpoint and --schema. Choose one source.');
@@ -156,6 +190,8 @@ program
156
190
  generatorType: 'generate',
157
191
  verbose: options.verbose,
158
192
  authorization: options.authorization,
193
+ configPath: options.config,
194
+ target: options.target,
159
195
  outputDir: options.output,
160
196
  });
161
197
  return;
@@ -163,6 +199,7 @@ program
163
199
  // Normal one-shot generation
164
200
  const result = await (0, generate_1.generateCommand)({
165
201
  config: options.config,
202
+ target: options.target,
166
203
  endpoint: options.endpoint,
167
204
  schema: options.schema,
168
205
  output: options.output,
@@ -170,8 +207,34 @@ program
170
207
  verbose: options.verbose,
171
208
  dryRun: options.dryRun,
172
209
  });
210
+ const duration = formatDuration(performance.now() - startTime);
211
+ const targetResults = result.targets ?? [];
212
+ const hasNamedTargets = targetResults.length > 0 &&
213
+ (targetResults.length > 1 || targetResults[0]?.name !== 'default');
214
+ if (hasNamedTargets) {
215
+ console.log(result.success ? '[ok]' : 'x', result.message);
216
+ targetResults.forEach((target) => {
217
+ const status = target.success ? '[ok]' : 'x';
218
+ console.log(`\n${status} ${target.message}`);
219
+ if (target.tables && target.tables.length > 0) {
220
+ console.log(' Tables:');
221
+ target.tables.forEach((table) => console.log(` - ${table}`));
222
+ }
223
+ if (target.filesWritten && target.filesWritten.length > 0) {
224
+ console.log(' Files written:');
225
+ target.filesWritten.forEach((file) => console.log(` - ${file}`));
226
+ }
227
+ if (!target.success && target.errors) {
228
+ target.errors.forEach((error) => console.error(` - ${error}`));
229
+ }
230
+ });
231
+ if (!result.success) {
232
+ process.exit(1);
233
+ }
234
+ return;
235
+ }
173
236
  if (result.success) {
174
- console.log('[ok]', result.message);
237
+ console.log('[ok]', result.message, `(${duration})`);
175
238
  if (result.tables && result.tables.length > 0) {
176
239
  console.log('\nTables:');
177
240
  result.tables.forEach((t) => console.log(` - ${t}`));
@@ -182,7 +245,7 @@ program
182
245
  }
183
246
  }
184
247
  else {
185
- console.error('x', result.message);
248
+ console.error('x', result.message, `(${duration})`);
186
249
  if (result.errors) {
187
250
  result.errors.forEach((e) => console.error(' -', e));
188
251
  }
@@ -194,9 +257,10 @@ program
194
257
  .command('generate-orm')
195
258
  .description('Generate Prisma-like ORM client from GraphQL endpoint or schema file')
196
259
  .option('-c, --config <path>', 'Path to config file')
260
+ .option('-t, --target <name>', 'Target name in config file')
197
261
  .option('-e, --endpoint <url>', 'GraphQL endpoint URL (overrides config)')
198
262
  .option('-s, --schema <path>', 'Path to GraphQL schema file (.graphql)')
199
- .option('-o, --output <dir>', 'Output directory (overrides config)', './generated/orm')
263
+ .option('-o, --output <dir>', 'Output directory (overrides config)')
200
264
  .option('-a, --authorization <header>', 'Authorization header value')
201
265
  .option('-v, --verbose', 'Verbose output', false)
202
266
  .option('--dry-run', 'Dry run - show what would be generated without writing files', false)
@@ -207,6 +271,7 @@ program
207
271
  .option('--touch <file>', 'File to touch on schema change')
208
272
  .option('--no-clear', 'Do not clear terminal on regeneration')
209
273
  .action(async (options) => {
274
+ const startTime = performance.now();
210
275
  // Validate source options
211
276
  if (options.endpoint && options.schema) {
212
277
  console.error('x Cannot use both --endpoint and --schema. Choose one source.');
@@ -227,6 +292,8 @@ program
227
292
  generatorType: 'generate-orm',
228
293
  verbose: options.verbose,
229
294
  authorization: options.authorization,
295
+ configPath: options.config,
296
+ target: options.target,
230
297
  outputDir: options.output,
231
298
  skipCustomOperations: options.skipCustomOperations,
232
299
  });
@@ -235,6 +302,7 @@ program
235
302
  // Normal one-shot generation
236
303
  const result = await (0, generate_orm_1.generateOrmCommand)({
237
304
  config: options.config,
305
+ target: options.target,
238
306
  endpoint: options.endpoint,
239
307
  schema: options.schema,
240
308
  output: options.output,
@@ -243,8 +311,42 @@ program
243
311
  dryRun: options.dryRun,
244
312
  skipCustomOperations: options.skipCustomOperations,
245
313
  });
314
+ const duration = formatDuration(performance.now() - startTime);
315
+ const targetResults = result.targets ?? [];
316
+ const hasNamedTargets = targetResults.length > 0 &&
317
+ (targetResults.length > 1 || targetResults[0]?.name !== 'default');
318
+ if (hasNamedTargets) {
319
+ console.log(result.success ? '[ok]' : 'x', result.message);
320
+ targetResults.forEach((target) => {
321
+ const status = target.success ? '[ok]' : 'x';
322
+ console.log(`\n${status} ${target.message}`);
323
+ if (target.tables && target.tables.length > 0) {
324
+ console.log(' Tables:');
325
+ target.tables.forEach((table) => console.log(` - ${table}`));
326
+ }
327
+ if (target.customQueries && target.customQueries.length > 0) {
328
+ console.log(' Custom Queries:');
329
+ target.customQueries.forEach((query) => console.log(` - ${query}`));
330
+ }
331
+ if (target.customMutations && target.customMutations.length > 0) {
332
+ console.log(' Custom Mutations:');
333
+ target.customMutations.forEach((mutation) => console.log(` - ${mutation}`));
334
+ }
335
+ if (target.filesWritten && target.filesWritten.length > 0) {
336
+ console.log(' Files written:');
337
+ target.filesWritten.forEach((file) => console.log(` - ${file}`));
338
+ }
339
+ if (!target.success && target.errors) {
340
+ target.errors.forEach((error) => console.error(` - ${error}`));
341
+ }
342
+ });
343
+ if (!result.success) {
344
+ process.exit(1);
345
+ }
346
+ return;
347
+ }
246
348
  if (result.success) {
247
- console.log('[ok]', result.message);
349
+ console.log('[ok]', result.message, `(${duration})`);
248
350
  if (result.tables && result.tables.length > 0) {
249
351
  console.log('\nTables:');
250
352
  result.tables.forEach((t) => console.log(` - ${t}`));
@@ -263,7 +365,7 @@ program
263
365
  }
264
366
  }
265
367
  else {
266
- console.error('x', result.message);
368
+ console.error('x', result.message, `(${duration})`);
267
369
  if (result.errors) {
268
370
  result.errors.forEach((e) => console.error(' -', e));
269
371
  }
@@ -279,6 +381,7 @@ program
279
381
  .option('-a, --authorization <header>', 'Authorization header value')
280
382
  .option('--json', 'Output as JSON', false)
281
383
  .action(async (options) => {
384
+ const startTime = performance.now();
282
385
  // Validate source options
283
386
  if (!options.endpoint && !options.schema) {
284
387
  console.error('x Either --endpoint or --schema must be provided.');
@@ -299,11 +402,12 @@ program
299
402
  console.log('Fetching schema from', source.describe(), '...');
300
403
  const { introspection } = await source.fetch();
301
404
  const tables = inferTablesFromIntrospection(introspection);
405
+ const duration = formatDuration(performance.now() - startTime);
302
406
  if (options.json) {
303
407
  console.log(JSON.stringify(tables, null, 2));
304
408
  }
305
409
  else {
306
- console.log(`\n[ok] Found ${tables.length} tables:\n`);
410
+ console.log(`\n[ok] Found ${tables.length} tables (${duration}):\n`);
307
411
  tables.forEach((table) => {
308
412
  const fieldCount = table.fields.length;
309
413
  const relationCount = table.relations.belongsTo.length +
@@ -315,7 +419,8 @@ program
315
419
  }
316
420
  }
317
421
  catch (err) {
318
- console.error('x Failed to introspect schema:', err instanceof Error ? err.message : err);
422
+ const duration = formatDuration(performance.now() - startTime);
423
+ console.error('x Failed to introspect schema:', err instanceof Error ? err.message : err, `(${duration})`);
319
424
  process.exit(1);
320
425
  }
321
426
  });
@@ -10,6 +10,10 @@ export interface WatchOrchestratorOptions {
10
10
  generatorType: GeneratorType;
11
11
  verbose: boolean;
12
12
  authorization?: string;
13
+ /** Config file path for regeneration */
14
+ configPath?: string;
15
+ /** Target name for multi-target configs */
16
+ target?: string;
13
17
  /** Override output directory (for ORM) */
14
18
  outputDir?: string;
15
19
  /** Skip custom operations flag */
@@ -138,6 +138,8 @@ class WatchOrchestrator {
138
138
  let result;
139
139
  if (this.options.generatorType === 'generate') {
140
140
  result = await (0, generate_1.generateCommand)({
141
+ config: this.options.configPath,
142
+ target: this.options.target,
141
143
  endpoint: this.options.config.endpoint,
142
144
  output: this.options.outputDir ?? this.options.config.output,
143
145
  authorization: this.options.authorization,
@@ -147,6 +149,8 @@ class WatchOrchestrator {
147
149
  }
148
150
  else {
149
151
  result = await (0, generate_orm_1.generateOrmCommand)({
152
+ config: this.options.configPath,
153
+ target: this.options.target,
150
154
  endpoint: this.options.config.endpoint,
151
155
  output: this.options.outputDir ?? this.options.config.orm?.output,
152
156
  authorization: this.options.authorization,
@@ -44,3 +44,10 @@ export declare function typedParam(name: string, typeAnnotation: t.TSType, optio
44
44
  * Create keyof typeof expression - complex nested type operators
45
45
  */
46
46
  export declare function keyofTypeof(name: string): t.TSTypeOperator;
47
+ /**
48
+ * Create a call expression with TypeScript type parameters
49
+ *
50
+ * This is used to generate typed function calls like:
51
+ * execute<ResultType, VariablesType>(document, variables)
52
+ */
53
+ export declare function createTypedCallExpression(callee: t.Expression, args: (t.Expression | t.SpreadElement)[], typeParams: t.TSType[]): t.CallExpression;
@@ -95,3 +95,17 @@ export function keyofTypeof(name) {
95
95
  keyofOp.operator = 'keyof';
96
96
  return keyofOp;
97
97
  }
98
+ /**
99
+ * Create a call expression with TypeScript type parameters
100
+ *
101
+ * This is used to generate typed function calls like:
102
+ * execute<ResultType, VariablesType>(document, variables)
103
+ */
104
+ export function createTypedCallExpression(callee, args, typeParams) {
105
+ const call = t.callExpression(callee, args);
106
+ if (typeParams.length > 0) {
107
+ // @ts-ignore - Babel types support typeParameters on CallExpression for TS
108
+ call.typeParameters = t.tsTypeParameterInstantiation(typeParams);
109
+ }
110
+ return call;
111
+ }
@@ -1,6 +1,6 @@
1
1
  import * as t from '@babel/types';
2
2
  import { generateCode, addJSDocComment } from './babel-ast';
3
- import { getListQueryHookName, getSingleQueryHookName, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, } from './utils';
3
+ import { getListQueryHookName, getSingleQueryHookName, getCreateMutationHookName, getUpdateMutationHookName, getDeleteMutationHookName, hasValidPrimaryKey, } from './utils';
4
4
  import { getOperationHookName } from './type-resolver';
5
5
  /**
6
6
  * Helper to create export * from './module' statement
@@ -16,9 +16,12 @@ export function generateQueriesBarrel(tables) {
16
16
  // Export all query hooks
17
17
  for (const table of tables) {
18
18
  const listHookName = getListQueryHookName(table);
19
- const singleHookName = getSingleQueryHookName(table);
20
19
  statements.push(exportAllFrom(`./${listHookName}`));
21
- statements.push(exportAllFrom(`./${singleHookName}`));
20
+ // Only export single query hook if table has valid primary key
21
+ if (hasValidPrimaryKey(table)) {
22
+ const singleHookName = getSingleQueryHookName(table);
23
+ statements.push(exportAllFrom(`./${singleHookName}`));
24
+ }
22
25
  }
23
26
  // Add file header as leading comment on first statement
24
27
  if (statements.length > 0) {
@@ -135,17 +138,30 @@ export function generateMainBarrel(tables, options = {}) {
135
138
  */
136
139
  export function generateCustomQueriesBarrel(tables, customQueryNames) {
137
140
  const statements = [];
141
+ const exportedHooks = new Set();
138
142
  // Export all table query hooks
139
143
  for (const table of tables) {
140
144
  const listHookName = getListQueryHookName(table);
141
- const singleHookName = getSingleQueryHookName(table);
142
- statements.push(exportAllFrom(`./${listHookName}`));
143
- statements.push(exportAllFrom(`./${singleHookName}`));
145
+ if (!exportedHooks.has(listHookName)) {
146
+ statements.push(exportAllFrom(`./${listHookName}`));
147
+ exportedHooks.add(listHookName);
148
+ }
149
+ // Only export single query hook if table has valid primary key
150
+ if (hasValidPrimaryKey(table)) {
151
+ const singleHookName = getSingleQueryHookName(table);
152
+ if (!exportedHooks.has(singleHookName)) {
153
+ statements.push(exportAllFrom(`./${singleHookName}`));
154
+ exportedHooks.add(singleHookName);
155
+ }
156
+ }
144
157
  }
145
- // Add custom query hooks
158
+ // Add custom query hooks (skip if already exported from table hooks)
146
159
  for (const name of customQueryNames) {
147
160
  const hookName = getOperationHookName(name, 'query');
148
- statements.push(exportAllFrom(`./${hookName}`));
161
+ if (!exportedHooks.has(hookName)) {
162
+ statements.push(exportAllFrom(`./${hookName}`));
163
+ exportedHooks.add(hookName);
164
+ }
149
165
  }
150
166
  // Add file header as leading comment on first statement
151
167
  if (statements.length > 0) {
@@ -162,24 +178,37 @@ export function generateCustomQueriesBarrel(tables, customQueryNames) {
162
178
  */
163
179
  export function generateCustomMutationsBarrel(tables, customMutationNames) {
164
180
  const statements = [];
181
+ const exportedHooks = new Set();
165
182
  // Export all table mutation hooks
166
183
  for (const table of tables) {
167
184
  const createHookName = getCreateMutationHookName(table);
168
- const updateHookName = getUpdateMutationHookName(table);
169
- const deleteHookName = getDeleteMutationHookName(table);
170
- statements.push(exportAllFrom(`./${createHookName}`));
185
+ if (!exportedHooks.has(createHookName)) {
186
+ statements.push(exportAllFrom(`./${createHookName}`));
187
+ exportedHooks.add(createHookName);
188
+ }
171
189
  // Only add update/delete if they exist
172
190
  if (table.query?.update !== null) {
173
- statements.push(exportAllFrom(`./${updateHookName}`));
191
+ const updateHookName = getUpdateMutationHookName(table);
192
+ if (!exportedHooks.has(updateHookName)) {
193
+ statements.push(exportAllFrom(`./${updateHookName}`));
194
+ exportedHooks.add(updateHookName);
195
+ }
174
196
  }
175
197
  if (table.query?.delete !== null) {
176
- statements.push(exportAllFrom(`./${deleteHookName}`));
198
+ const deleteHookName = getDeleteMutationHookName(table);
199
+ if (!exportedHooks.has(deleteHookName)) {
200
+ statements.push(exportAllFrom(`./${deleteHookName}`));
201
+ exportedHooks.add(deleteHookName);
202
+ }
177
203
  }
178
204
  }
179
- // Add custom mutation hooks
205
+ // Add custom mutation hooks (skip if already exported from table hooks)
180
206
  for (const name of customMutationNames) {
181
207
  const hookName = getOperationHookName(name, 'mutation');
182
- statements.push(exportAllFrom(`./${hookName}`));
208
+ if (!exportedHooks.has(hookName)) {
209
+ statements.push(exportAllFrom(`./${hookName}`));
210
+ exportedHooks.add(hookName);
211
+ }
183
212
  }
184
213
  // Add file header as leading comment on first statement
185
214
  if (statements.length > 0) {
@@ -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 { buildCustomMutationString } from './schema-gql-ast';
4
4
  import { typeRefToTsType, isTypeRequired, getOperationHookName, getOperationFileName, getOperationVariablesTypeName, getOperationResultTypeName, getDocumentConstName, createTypeTracker, } from './type-resolver';
5
5
  import { getGeneratedFileHeader } from './utils';
@@ -94,13 +94,13 @@ function generateCustomMutationHookInternal(options) {
94
94
  mutationOptions.push(t.objectProperty(t.identifier('mutationKey'), t.callExpression(t.memberExpression(t.identifier('customMutationKeys'), t.identifier(operation.name)), [])));
95
95
  }
96
96
  if (hasArgs) {
97
- mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)))], t.callExpression(t.identifier('execute'), [
98
- t.identifier(documentConstName),
99
- t.identifier('variables'),
97
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([typedParam('variables', t.tsTypeReference(t.identifier(variablesTypeName)))], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('variables')], [
98
+ t.tsTypeReference(t.identifier(resultTypeName)),
99
+ t.tsTypeReference(t.identifier(variablesTypeName)),
100
100
  ]))));
101
101
  }
102
102
  else {
103
- mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [t.identifier(documentConstName)]))));
103
+ mutationOptions.push(t.objectProperty(t.identifier('mutationFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName)], [t.tsTypeReference(t.identifier(resultTypeName))]))));
104
104
  }
105
105
  mutationOptions.push(t.spreadElement(t.identifier('options')));
106
106
  hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useMutation'), [t.objectExpression(mutationOptions)])));
@@ -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 { buildCustomQueryString } from './schema-gql-ast';
4
4
  import { typeRefToTsType, isTypeRequired, getOperationHookName, getOperationFileName, getOperationVariablesTypeName, getOperationResultTypeName, getDocumentConstName, getQueryKeyName, createTypeTracker, } from './type-resolver';
5
5
  import { ucFirst, getGeneratedFileHeader } from './utils';
@@ -115,9 +115,9 @@ export function generateCustomQueryHook(options) {
115
115
  const useQueryOptions = [];
116
116
  if (hasArgs) {
117
117
  useQueryOptions.push(t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(queryKeyName), [t.identifier('variables')])));
118
- useQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
119
- t.identifier(documentConstName),
120
- t.identifier('variables'),
118
+ useQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('variables')], [
119
+ t.tsTypeReference(t.identifier(resultTypeName)),
120
+ t.tsTypeReference(t.identifier(variablesTypeName)),
121
121
  ]))));
122
122
  if (hasRequiredArgs) {
123
123
  useQueryOptions.push(t.objectProperty(t.identifier('enabled'), t.logicalExpression('&&', t.unaryExpression('!', t.unaryExpression('!', t.identifier('variables'))), t.binaryExpression('!==', t.optionalMemberExpression(t.identifier('options'), t.identifier('enabled'), false, true), t.booleanLiteral(false)))));
@@ -125,7 +125,7 @@ export function generateCustomQueryHook(options) {
125
125
  }
126
126
  else {
127
127
  useQueryOptions.push(t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(queryKeyName), [])));
128
- useQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [t.identifier(documentConstName)]))));
128
+ useQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName)], [t.tsTypeReference(t.identifier(resultTypeName))]))));
129
129
  }
130
130
  useQueryOptions.push(t.spreadElement(t.identifier('options')));
131
131
  hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [t.objectExpression(useQueryOptions)])));
@@ -162,18 +162,13 @@ export function generateCustomQueryHook(options) {
162
162
  const hasRequiredArgs = operation.args.some((arg) => isTypeRequired(arg.type));
163
163
  const fetchBodyStatements = [];
164
164
  if (hasArgs) {
165
- fetchBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('execute'), [
166
- t.identifier(documentConstName),
167
- t.identifier('variables'),
168
- t.identifier('options'),
165
+ fetchBodyStatements.push(t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('variables'), t.identifier('options')], [
166
+ t.tsTypeReference(t.identifier(resultTypeName)),
167
+ t.tsTypeReference(t.identifier(variablesTypeName)),
169
168
  ])));
170
169
  }
171
170
  else {
172
- fetchBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('execute'), [
173
- t.identifier(documentConstName),
174
- t.identifier('undefined'),
175
- t.identifier('options'),
176
- ])));
171
+ fetchBodyStatements.push(t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('undefined'), t.identifier('options')], [t.tsTypeReference(t.identifier(resultTypeName))])));
177
172
  }
178
173
  const fetchParams = [];
179
174
  if (hasArgs) {
@@ -201,19 +196,14 @@ export function generateCustomQueryHook(options) {
201
196
  const prefetchQueryOptions = [];
202
197
  if (hasArgs) {
203
198
  prefetchQueryOptions.push(t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(queryKeyName), [t.identifier('variables')])));
204
- prefetchQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
205
- t.identifier(documentConstName),
206
- t.identifier('variables'),
207
- t.identifier('options'),
199
+ prefetchQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('variables'), t.identifier('options')], [
200
+ t.tsTypeReference(t.identifier(resultTypeName)),
201
+ t.tsTypeReference(t.identifier(variablesTypeName)),
208
202
  ]))));
209
203
  }
210
204
  else {
211
205
  prefetchQueryOptions.push(t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(queryKeyName), [])));
212
- prefetchQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.callExpression(t.identifier('execute'), [
213
- t.identifier(documentConstName),
214
- t.identifier('undefined'),
215
- t.identifier('options'),
216
- ]))));
206
+ prefetchQueryOptions.push(t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(documentConstName), t.identifier('undefined'), t.identifier('options')], [t.tsTypeReference(t.identifier(resultTypeName))]))));
217
207
  }
218
208
  prefetchBodyStatements.push(t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [t.objectExpression(prefetchQueryOptions)]))));
219
209
  const prefetchParams = [
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import * as t from 'gql-ast';
8
8
  import { print } from 'graphql';
9
- import { getTableNames, getAllRowsQueryName, getSingleRowQueryName, getCreateMutationName, getUpdateMutationName, getDeleteMutationName, getFilterTypeName, getOrderByTypeName, getScalarFields, getPrimaryKeyInfo, ucFirst, } from './utils';
9
+ import { getTableNames, getAllRowsQueryName, getSingleRowQueryName, getCreateMutationName, getUpdateMutationName, getDeleteMutationName, getFilterTypeName, getConditionTypeName, getOrderByTypeName, getScalarFields, getPrimaryKeyInfo, ucFirst, } from './utils';
10
10
  // ============================================================================
11
11
  // Field selection builders
12
12
  // ============================================================================
@@ -39,22 +39,39 @@ export function buildListQueryAST(config) {
39
39
  const { table } = config;
40
40
  const queryName = getAllRowsQueryName(table);
41
41
  const filterType = getFilterTypeName(table);
42
+ const conditionType = getConditionTypeName(table);
42
43
  const orderByType = getOrderByTypeName(table);
43
44
  const scalarFields = getScalarFields(table);
44
- // Variable definitions
45
+ // Variable definitions - all pagination arguments from PostGraphile
45
46
  const variableDefinitions = [
46
47
  t.variableDefinition({
47
48
  variable: t.variable({ name: 'first' }),
48
49
  type: t.namedType({ type: 'Int' }),
49
50
  }),
51
+ t.variableDefinition({
52
+ variable: t.variable({ name: 'last' }),
53
+ type: t.namedType({ type: 'Int' }),
54
+ }),
50
55
  t.variableDefinition({
51
56
  variable: t.variable({ name: 'offset' }),
52
57
  type: t.namedType({ type: 'Int' }),
53
58
  }),
59
+ t.variableDefinition({
60
+ variable: t.variable({ name: 'before' }),
61
+ type: t.namedType({ type: 'Cursor' }),
62
+ }),
63
+ t.variableDefinition({
64
+ variable: t.variable({ name: 'after' }),
65
+ type: t.namedType({ type: 'Cursor' }),
66
+ }),
54
67
  t.variableDefinition({
55
68
  variable: t.variable({ name: 'filter' }),
56
69
  type: t.namedType({ type: filterType }),
57
70
  }),
71
+ t.variableDefinition({
72
+ variable: t.variable({ name: 'condition' }),
73
+ type: t.namedType({ type: conditionType }),
74
+ }),
58
75
  t.variableDefinition({
59
76
  variable: t.variable({ name: 'orderBy' }),
60
77
  type: t.listType({
@@ -65,8 +82,12 @@ export function buildListQueryAST(config) {
65
82
  // Query arguments
66
83
  const args = [
67
84
  t.argument({ name: 'first', value: t.variable({ name: 'first' }) }),
85
+ t.argument({ name: 'last', value: t.variable({ name: 'last' }) }),
68
86
  t.argument({ name: 'offset', value: t.variable({ name: 'offset' }) }),
87
+ t.argument({ name: 'before', value: t.variable({ name: 'before' }) }),
88
+ t.argument({ name: 'after', value: t.variable({ name: 'after' }) }),
69
89
  t.argument({ name: 'filter', value: t.variable({ name: 'filter' }) }),
90
+ t.argument({ name: 'condition', value: t.variable({ name: 'condition' }) }),
70
91
  t.argument({ name: 'orderBy', value: t.variable({ name: 'orderBy' }) }),
71
92
  ];
72
93
  // Field selections
@@ -152,6 +152,7 @@ export function generate(options) {
152
152
  enumsFromSchemaTypes: generatedEnumNames,
153
153
  useCentralizedKeys,
154
154
  hasRelationships,
155
+ tableTypeNames,
155
156
  });
156
157
  for (const hook of mutationHooks) {
157
158
  files.push({