@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
@@ -136,6 +136,14 @@ export function generateQueryBuilderFile() {
136
136
  * DO NOT EDIT - changes will be overwritten
137
137
  */
138
138
 
139
+ import * as t from 'gql-ast';
140
+ import { parseType, print } from 'graphql';
141
+ import type {
142
+ ArgumentNode,
143
+ FieldNode,
144
+ VariableDefinitionNode,
145
+ EnumValueNode,
146
+ } from 'graphql';
139
147
  import { OrmClient, QueryResult, GraphQLRequestError } from './client';
140
148
 
141
149
  export interface QueryBuilderConfig {
@@ -209,19 +217,25 @@ export class QueryBuilder<TResult> {
209
217
  }
210
218
 
211
219
  // ============================================================================
212
- // Document Builders
220
+ // Selection Builders
213
221
  // ============================================================================
214
222
 
215
- export function buildSelections<T>(select: T): string {
216
- if (!select) return '';
223
+ export function buildSelections(
224
+ select: Record<string, unknown> | undefined
225
+ ): FieldNode[] {
226
+ if (!select) {
227
+ return [];
228
+ }
217
229
 
218
- const fields: string[] = [];
230
+ const fields: FieldNode[] = [];
219
231
 
220
232
  for (const [key, value] of Object.entries(select)) {
221
- if (value === false || value === undefined) continue;
233
+ if (value === false || value === undefined) {
234
+ continue;
235
+ }
222
236
 
223
237
  if (value === true) {
224
- fields.push(key);
238
+ fields.push(t.field({ name: key }));
225
239
  continue;
226
240
  }
227
241
 
@@ -231,39 +245,53 @@ export function buildSelections<T>(select: T): string {
231
245
  first?: number;
232
246
  filter?: Record<string, unknown>;
233
247
  orderBy?: string[];
234
- // New: connection flag to differentiate connection types from regular objects
235
248
  connection?: boolean;
236
249
  };
237
250
 
238
251
  if (nested.select) {
239
252
  const nestedSelections = buildSelections(nested.select);
240
-
241
- // Check if this is a connection type (has pagination args or explicit connection flag)
242
- const isConnection = nested.connection === true || nested.first !== undefined || nested.filter !== undefined;
243
-
253
+ const isConnection =
254
+ nested.connection === true ||
255
+ nested.first !== undefined ||
256
+ nested.filter !== undefined;
257
+ const args = buildArgs([
258
+ buildOptionalArg('first', nested.first),
259
+ nested.filter
260
+ ? t.argument({ name: 'filter', value: buildValueAst(nested.filter) })
261
+ : null,
262
+ buildEnumListArg('orderBy', nested.orderBy),
263
+ ]);
264
+
244
265
  if (isConnection) {
245
- // Connection type - wrap in nodes/totalCount/pageInfo
246
- const args: string[] = [];
247
- if (nested.first !== undefined) args.push(\`first: \${nested.first}\`);
248
- if (nested.orderBy?.length) args.push(\`orderBy: [\${nested.orderBy.join(', ')}]\`);
249
- const argsStr = args.length > 0 ? \`(\${args.join(', ')})\` : '';
250
-
251
- fields.push(\`\${key}\${argsStr} {
252
- nodes { \${nestedSelections} }
253
- totalCount
254
- pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
255
- }\`);
266
+ fields.push(
267
+ t.field({
268
+ name: key,
269
+ args,
270
+ selectionSet: t.selectionSet({
271
+ selections: buildConnectionSelections(nestedSelections),
272
+ }),
273
+ })
274
+ );
256
275
  } else {
257
- // Regular nested object - just wrap in braces
258
- fields.push(\`\${key} { \${nestedSelections} }\`);
276
+ fields.push(
277
+ t.field({
278
+ name: key,
279
+ args,
280
+ selectionSet: t.selectionSet({ selections: nestedSelections }),
281
+ })
282
+ );
259
283
  }
260
284
  }
261
285
  }
262
286
  }
263
287
 
264
- return fields.join('\\n ');
288
+ return fields;
265
289
  }
266
290
 
291
+ // ============================================================================
292
+ // Document Builders
293
+ // ============================================================================
294
+
267
295
  export function buildFindManyDocument<TSelect, TWhere>(
268
296
  operationName: string,
269
297
  queryField: string,
@@ -280,60 +308,44 @@ export function buildFindManyDocument<TSelect, TWhere>(
280
308
  filterTypeName: string,
281
309
  orderByTypeName: string
282
310
  ): { document: string; variables: Record<string, unknown> } {
283
- const selections = select ? buildSelections(select) : 'id';
311
+ const selections = select
312
+ ? buildSelections(select as Record<string, unknown>)
313
+ : [t.field({ name: 'id' })];
284
314
 
285
- const varDefs: string[] = [];
286
- const queryArgs: string[] = [];
315
+ const variableDefinitions: VariableDefinitionNode[] = [];
316
+ const queryArgs: ArgumentNode[] = [];
287
317
  const variables: Record<string, unknown> = {};
288
318
 
289
- if (args.where) {
290
- varDefs.push(\`$where: \${filterTypeName}\`);
291
- queryArgs.push('filter: $where');
292
- variables.where = args.where;
293
- }
294
- if (args.orderBy?.length) {
295
- varDefs.push(\`$orderBy: [\${orderByTypeName}!]\`);
296
- queryArgs.push('orderBy: $orderBy');
297
- variables.orderBy = args.orderBy;
298
- }
299
- if (args.first !== undefined) {
300
- varDefs.push('$first: Int');
301
- queryArgs.push('first: $first');
302
- variables.first = args.first;
303
- }
304
- if (args.last !== undefined) {
305
- varDefs.push('$last: Int');
306
- queryArgs.push('last: $last');
307
- variables.last = args.last;
308
- }
309
- if (args.after) {
310
- varDefs.push('$after: Cursor');
311
- queryArgs.push('after: $after');
312
- variables.after = args.after;
313
- }
314
- if (args.before) {
315
- varDefs.push('$before: Cursor');
316
- queryArgs.push('before: $before');
317
- variables.before = args.before;
318
- }
319
- if (args.offset !== undefined) {
320
- varDefs.push('$offset: Int');
321
- queryArgs.push('offset: $offset');
322
- variables.offset = args.offset;
323
- }
324
-
325
- const varDefsStr = varDefs.length > 0 ? \`(\${varDefs.join(', ')})\` : '';
326
- const queryArgsStr = queryArgs.length > 0 ? \`(\${queryArgs.join(', ')})\` : '';
327
-
328
- const document = \`query \${operationName}Query\${varDefsStr} {
329
- \${queryField}\${queryArgsStr} {
330
- nodes { \${selections} }
331
- totalCount
332
- pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
333
- }
334
- }\`;
319
+ addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
320
+ addVariable({ varName: 'orderBy', typeName: '[' + orderByTypeName + '!]', value: args.orderBy?.length ? args.orderBy : undefined }, variableDefinitions, queryArgs, variables);
321
+ addVariable({ varName: 'first', typeName: 'Int', value: args.first }, variableDefinitions, queryArgs, variables);
322
+ addVariable({ varName: 'last', typeName: 'Int', value: args.last }, variableDefinitions, queryArgs, variables);
323
+ addVariable({ varName: 'after', typeName: 'Cursor', value: args.after }, variableDefinitions, queryArgs, variables);
324
+ addVariable({ varName: 'before', typeName: 'Cursor', value: args.before }, variableDefinitions, queryArgs, variables);
325
+ addVariable({ varName: 'offset', typeName: 'Int', value: args.offset }, variableDefinitions, queryArgs, variables);
326
+
327
+ const document = t.document({
328
+ definitions: [
329
+ t.operationDefinition({
330
+ operation: 'query',
331
+ name: operationName + 'Query',
332
+ variableDefinitions: variableDefinitions.length ? variableDefinitions : undefined,
333
+ selectionSet: t.selectionSet({
334
+ selections: [
335
+ t.field({
336
+ name: queryField,
337
+ args: queryArgs.length ? queryArgs : undefined,
338
+ selectionSet: t.selectionSet({
339
+ selections: buildConnectionSelections(selections),
340
+ }),
341
+ }),
342
+ ],
343
+ }),
344
+ }),
345
+ ],
346
+ });
335
347
 
336
- return { document, variables };
348
+ return { document: print(document), variables };
337
349
  }
338
350
 
339
351
  export function buildFindFirstDocument<TSelect, TWhere>(
@@ -343,25 +355,45 @@ export function buildFindFirstDocument<TSelect, TWhere>(
343
355
  args: { where?: TWhere },
344
356
  filterTypeName: string
345
357
  ): { document: string; variables: Record<string, unknown> } {
346
- const selections = select ? buildSelections(select) : 'id';
347
-
348
- const varDefs: string[] = ['$first: Int'];
349
- const queryArgs: string[] = ['first: $first'];
350
- const variables: Record<string, unknown> = { first: 1 };
358
+ const selections = select
359
+ ? buildSelections(select as Record<string, unknown>)
360
+ : [t.field({ name: 'id' })];
351
361
 
352
- if (args.where) {
353
- varDefs.push(\`$where: \${filterTypeName}\`);
354
- queryArgs.push('filter: $where');
355
- variables.where = args.where;
356
- }
362
+ const variableDefinitions: VariableDefinitionNode[] = [];
363
+ const queryArgs: ArgumentNode[] = [];
364
+ const variables: Record<string, unknown> = {};
357
365
 
358
- const document = \`query \${operationName}Query(\${varDefs.join(', ')}) {
359
- \${queryField}(\${queryArgs.join(', ')}) {
360
- nodes { \${selections} }
361
- }
362
- }\`;
366
+ // Always add first: 1 for findFirst
367
+ addVariable({ varName: 'first', typeName: 'Int', value: 1 }, variableDefinitions, queryArgs, variables);
368
+ addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
369
+
370
+ const document = t.document({
371
+ definitions: [
372
+ t.operationDefinition({
373
+ operation: 'query',
374
+ name: operationName + 'Query',
375
+ variableDefinitions,
376
+ selectionSet: t.selectionSet({
377
+ selections: [
378
+ t.field({
379
+ name: queryField,
380
+ args: queryArgs,
381
+ selectionSet: t.selectionSet({
382
+ selections: [
383
+ t.field({
384
+ name: 'nodes',
385
+ selectionSet: t.selectionSet({ selections }),
386
+ }),
387
+ ],
388
+ }),
389
+ }),
390
+ ],
391
+ }),
392
+ }),
393
+ ],
394
+ });
363
395
 
364
- return { document, variables };
396
+ return { document: print(document), variables };
365
397
  }
366
398
 
367
399
  export function buildCreateDocument<TSelect, TData>(
@@ -372,17 +404,27 @@ export function buildCreateDocument<TSelect, TData>(
372
404
  data: TData,
373
405
  inputTypeName: string
374
406
  ): { document: string; variables: Record<string, unknown> } {
375
- const selections = select ? buildSelections(select) : 'id';
376
-
377
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
378
- \${mutationField}(input: $input) {
379
- \${entityField} { \${selections} }
380
- }
381
- }\`;
407
+ const selections = select
408
+ ? buildSelections(select as Record<string, unknown>)
409
+ : [t.field({ name: 'id' })];
382
410
 
383
411
  return {
384
- document,
385
- variables: { input: { [entityField]: data } },
412
+ document: buildInputMutationDocument({
413
+ operationName,
414
+ mutationField,
415
+ inputTypeName,
416
+ resultSelections: [
417
+ t.field({
418
+ name: entityField,
419
+ selectionSet: t.selectionSet({ selections }),
420
+ }),
421
+ ],
422
+ }),
423
+ variables: {
424
+ input: {
425
+ [entityField]: data,
426
+ },
427
+ },
386
428
  };
387
429
  }
388
430
 
@@ -395,17 +437,28 @@ export function buildUpdateDocument<TSelect, TWhere extends { id: string }, TDat
395
437
  data: TData,
396
438
  inputTypeName: string
397
439
  ): { document: string; variables: Record<string, unknown> } {
398
- const selections = select ? buildSelections(select) : 'id';
399
-
400
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
401
- \${mutationField}(input: $input) {
402
- \${entityField} { \${selections} }
403
- }
404
- }\`;
440
+ const selections = select
441
+ ? buildSelections(select as Record<string, unknown>)
442
+ : [t.field({ name: 'id' })];
405
443
 
406
444
  return {
407
- document,
408
- variables: { input: { id: where.id, patch: data } },
445
+ document: buildInputMutationDocument({
446
+ operationName,
447
+ mutationField,
448
+ inputTypeName,
449
+ resultSelections: [
450
+ t.field({
451
+ name: entityField,
452
+ selectionSet: t.selectionSet({ selections }),
453
+ }),
454
+ ],
455
+ }),
456
+ variables: {
457
+ input: {
458
+ id: where.id,
459
+ patch: data,
460
+ },
461
+ },
409
462
  };
410
463
  }
411
464
 
@@ -416,15 +469,25 @@ export function buildDeleteDocument<TWhere extends { id: string }>(
416
469
  where: TWhere,
417
470
  inputTypeName: string
418
471
  ): { document: string; variables: Record<string, unknown> } {
419
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
420
- \${mutationField}(input: $input) {
421
- \${entityField} { id }
422
- }
423
- }\`;
424
-
425
472
  return {
426
- document,
427
- variables: { input: { id: where.id } },
473
+ document: buildInputMutationDocument({
474
+ operationName,
475
+ mutationField,
476
+ inputTypeName,
477
+ resultSelections: [
478
+ t.field({
479
+ name: entityField,
480
+ selectionSet: t.selectionSet({
481
+ selections: [t.field({ name: 'id' })],
482
+ }),
483
+ }),
484
+ ],
485
+ }),
486
+ variables: {
487
+ input: {
488
+ id: where.id,
489
+ },
490
+ },
428
491
  };
429
492
  }
430
493
 
@@ -436,21 +499,251 @@ export function buildCustomDocument<TSelect, TArgs>(
436
499
  args: TArgs,
437
500
  variableDefinitions: Array<{ name: string; type: string }>
438
501
  ): { document: string; variables: Record<string, unknown> } {
439
- const selections = select ? buildSelections(select) : '';
440
-
441
- const varDefs = variableDefinitions.map(v => \`$\${v.name}: \${v.type}\`);
442
- const fieldArgs = variableDefinitions.map(v => \`\${v.name}: $\${v.name}\`);
443
-
444
- const varDefsStr = varDefs.length > 0 ? \`(\${varDefs.join(', ')})\` : '';
445
- const fieldArgsStr = fieldArgs.length > 0 ? \`(\${fieldArgs.join(', ')})\` : '';
446
- const selectionsBlock = selections ? \` { \${selections} }\` : '';
447
-
448
- const opType = operationType === 'query' ? 'query' : 'mutation';
449
- const document = \`\${opType} \${operationName}\${varDefsStr} {
450
- \${fieldName}\${fieldArgsStr}\${selectionsBlock}
451
- }\`;
452
-
453
- return { document, variables: (args ?? {}) as Record<string, unknown> };
502
+ let actualSelect = select;
503
+ let isConnection = false;
504
+
505
+ if (select && typeof select === 'object' && 'select' in select) {
506
+ const wrapper = select as { select?: TSelect; connection?: boolean };
507
+ if (wrapper.select) {
508
+ actualSelect = wrapper.select;
509
+ isConnection = wrapper.connection === true;
510
+ }
511
+ }
512
+
513
+ const selections = actualSelect
514
+ ? buildSelections(actualSelect as Record<string, unknown>)
515
+ : [];
516
+
517
+ const variableDefs = variableDefinitions.map((definition) =>
518
+ t.variableDefinition({
519
+ variable: t.variable({ name: definition.name }),
520
+ type: parseType(definition.type),
521
+ })
522
+ );
523
+ const fieldArgs = variableDefinitions.map((definition) =>
524
+ t.argument({
525
+ name: definition.name,
526
+ value: t.variable({ name: definition.name }),
527
+ })
528
+ );
529
+
530
+ const fieldSelections = isConnection
531
+ ? buildConnectionSelections(selections)
532
+ : selections;
533
+
534
+ const document = t.document({
535
+ definitions: [
536
+ t.operationDefinition({
537
+ operation: operationType,
538
+ name: operationName,
539
+ variableDefinitions: variableDefs.length ? variableDefs : undefined,
540
+ selectionSet: t.selectionSet({
541
+ selections: [
542
+ t.field({
543
+ name: fieldName,
544
+ args: fieldArgs.length ? fieldArgs : undefined,
545
+ selectionSet: fieldSelections.length
546
+ ? t.selectionSet({ selections: fieldSelections })
547
+ : undefined,
548
+ }),
549
+ ],
550
+ }),
551
+ }),
552
+ ],
553
+ });
554
+
555
+ return {
556
+ document: print(document),
557
+ variables: (args ?? {}) as Record<string, unknown>,
558
+ };
559
+ }
560
+
561
+ // ============================================================================
562
+ // Helper Functions
563
+ // ============================================================================
564
+
565
+ function buildArgs(args: Array<ArgumentNode | null>): ArgumentNode[] {
566
+ return args.filter((arg): arg is ArgumentNode => arg !== null);
567
+ }
568
+
569
+ function buildOptionalArg(
570
+ name: string,
571
+ value: number | string | undefined
572
+ ): ArgumentNode | null {
573
+ if (value === undefined) {
574
+ return null;
575
+ }
576
+ const valueNode =
577
+ typeof value === 'number'
578
+ ? t.intValue({ value: value.toString() })
579
+ : t.stringValue({ value });
580
+ return t.argument({ name, value: valueNode });
581
+ }
582
+
583
+ function buildEnumListArg(
584
+ name: string,
585
+ values: string[] | undefined
586
+ ): ArgumentNode | null {
587
+ if (!values || values.length === 0) {
588
+ return null;
589
+ }
590
+ return t.argument({
591
+ name,
592
+ value: t.listValue({
593
+ values: values.map((value) => buildEnumValue(value)),
594
+ }),
595
+ });
596
+ }
597
+
598
+ function buildEnumValue(value: string): EnumValueNode {
599
+ return {
600
+ kind: 'EnumValue',
601
+ value,
602
+ };
603
+ }
604
+
605
+ function buildPageInfoSelections(): FieldNode[] {
606
+ return [
607
+ t.field({ name: 'hasNextPage' }),
608
+ t.field({ name: 'hasPreviousPage' }),
609
+ t.field({ name: 'startCursor' }),
610
+ t.field({ name: 'endCursor' }),
611
+ ];
612
+ }
613
+
614
+ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] {
615
+ return [
616
+ t.field({
617
+ name: 'nodes',
618
+ selectionSet: t.selectionSet({ selections: nodeSelections }),
619
+ }),
620
+ t.field({ name: 'totalCount' }),
621
+ t.field({
622
+ name: 'pageInfo',
623
+ selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }),
624
+ }),
625
+ ];
626
+ }
627
+
628
+ interface VariableSpec {
629
+ varName: string;
630
+ argName?: string;
631
+ typeName: string;
632
+ value: unknown;
633
+ }
634
+
635
+ interface InputMutationConfig {
636
+ operationName: string;
637
+ mutationField: string;
638
+ inputTypeName: string;
639
+ resultSelections: FieldNode[];
640
+ }
641
+
642
+ function buildInputMutationDocument(config: InputMutationConfig): string {
643
+ const document = t.document({
644
+ definitions: [
645
+ t.operationDefinition({
646
+ operation: 'mutation',
647
+ name: config.operationName + 'Mutation',
648
+ variableDefinitions: [
649
+ t.variableDefinition({
650
+ variable: t.variable({ name: 'input' }),
651
+ type: parseType(config.inputTypeName + '!'),
652
+ }),
653
+ ],
654
+ selectionSet: t.selectionSet({
655
+ selections: [
656
+ t.field({
657
+ name: config.mutationField,
658
+ args: [
659
+ t.argument({
660
+ name: 'input',
661
+ value: t.variable({ name: 'input' }),
662
+ }),
663
+ ],
664
+ selectionSet: t.selectionSet({
665
+ selections: config.resultSelections,
666
+ }),
667
+ }),
668
+ ],
669
+ }),
670
+ }),
671
+ ],
672
+ });
673
+ return print(document);
674
+ }
675
+
676
+ function addVariable(
677
+ spec: VariableSpec,
678
+ definitions: VariableDefinitionNode[],
679
+ args: ArgumentNode[],
680
+ variables: Record<string, unknown>
681
+ ): void {
682
+ if (spec.value === undefined) return;
683
+
684
+ definitions.push(
685
+ t.variableDefinition({
686
+ variable: t.variable({ name: spec.varName }),
687
+ type: parseType(spec.typeName),
688
+ })
689
+ );
690
+ args.push(
691
+ t.argument({
692
+ name: spec.argName ?? spec.varName,
693
+ value: t.variable({ name: spec.varName }),
694
+ })
695
+ );
696
+ variables[spec.varName] = spec.value;
697
+ }
698
+
699
+ function buildValueAst(
700
+ value: unknown
701
+ ):
702
+ | ReturnType<typeof t.stringValue>
703
+ | ReturnType<typeof t.intValue>
704
+ | ReturnType<typeof t.floatValue>
705
+ | ReturnType<typeof t.booleanValue>
706
+ | ReturnType<typeof t.listValue>
707
+ | ReturnType<typeof t.objectValue>
708
+ | ReturnType<typeof t.nullValue>
709
+ | EnumValueNode {
710
+ if (value === null) {
711
+ return t.nullValue();
712
+ }
713
+
714
+ if (typeof value === 'boolean') {
715
+ return t.booleanValue({ value });
716
+ }
717
+
718
+ if (typeof value === 'number') {
719
+ return Number.isInteger(value)
720
+ ? t.intValue({ value: value.toString() })
721
+ : t.floatValue({ value: value.toString() });
722
+ }
723
+
724
+ if (typeof value === 'string') {
725
+ return t.stringValue({ value });
726
+ }
727
+
728
+ if (Array.isArray(value)) {
729
+ return t.listValue({
730
+ values: value.map((item) => buildValueAst(item)),
731
+ });
732
+ }
733
+
734
+ if (typeof value === 'object' && value !== null) {
735
+ const obj = value as Record<string, unknown>;
736
+ return t.objectValue({
737
+ fields: Object.entries(obj).map(([key, val]) =>
738
+ t.objectField({
739
+ name: key,
740
+ value: buildValueAst(val),
741
+ })
742
+ ),
743
+ });
744
+ }
745
+
746
+ throw new Error('Unsupported value type: ' + typeof value);
454
747
  }
455
748
  `;
456
749
  return {
@@ -512,6 +805,44 @@ export interface DeleteArgs<TWhere> {
512
805
  where: TWhere;
513
806
  }
514
807
 
808
+ /**
809
+ * Recursively validates select objects, rejecting unknown keys.
810
+ *
811
+ * This type ensures that users can only select fields that actually exist
812
+ * in the GraphQL schema. It returns \`never\` if any excess keys are found
813
+ * at any nesting level, causing a TypeScript compile error.
814
+ *
815
+ * Why this is needed:
816
+ * TypeScript's excess property checking has a quirk where it only catches
817
+ * invalid fields when they are the ONLY fields. When mixed with valid fields
818
+ * (e.g., \`{ id: true, invalidField: true }\`), the structural typing allows
819
+ * the excess property through. This type explicitly checks for and rejects
820
+ * such cases.
821
+ *
822
+ * @example
823
+ * // This will cause a type error because 'invalid' doesn't exist:
824
+ * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
825
+ * // Result = never (causes assignment error)
826
+ *
827
+ * @example
828
+ * // This works because all fields are valid:
829
+ * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
830
+ * // Result = { id: true }
831
+ */
832
+ export type DeepExact<T, Shape> = T extends Shape
833
+ ? Exclude<keyof T, keyof Shape> extends never
834
+ ? {
835
+ [K in keyof T]: K extends keyof Shape
836
+ ? T[K] extends { select: infer NS }
837
+ ? Shape[K] extends { select?: infer ShapeNS }
838
+ ? { select: DeepExact<NS, NonNullable<ShapeNS>> }
839
+ : T[K]
840
+ : T[K]
841
+ : never;
842
+ }
843
+ : never
844
+ : never;
845
+
515
846
  /**
516
847
  * Infer result type from select configuration
517
848
  */
@@ -577,9 +908,13 @@ export function generateCreateClientFile(tables, hasCustomQueries, hasCustomMuta
577
908
  typeExportDecl.exportKind = 'type';
578
909
  statements.push(typeExportDecl);
579
910
  // export { GraphQLRequestError } from './client';
580
- statements.push(t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier('GraphQLRequestError'), t.identifier('GraphQLRequestError'))], t.stringLiteral('./client')));
911
+ statements.push(t.exportNamedDeclaration(null, [
912
+ t.exportSpecifier(t.identifier('GraphQLRequestError'), t.identifier('GraphQLRequestError')),
913
+ ], t.stringLiteral('./client')));
581
914
  // export { QueryBuilder } from './query-builder';
582
- statements.push(t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier('QueryBuilder'), t.identifier('QueryBuilder'))], t.stringLiteral('./query-builder')));
915
+ statements.push(t.exportNamedDeclaration(null, [
916
+ t.exportSpecifier(t.identifier('QueryBuilder'), t.identifier('QueryBuilder')),
917
+ ], t.stringLiteral('./query-builder')));
583
918
  // export * from './select-types';
584
919
  statements.push(t.exportAllDeclaration(t.stringLiteral('./select-types')));
585
920
  // Build the return object properties
@@ -590,10 +925,14 @@ export function generateCreateClientFile(tables, hasCustomQueries, hasCustomMuta
590
925
  returnProperties.push(t.objectProperty(t.identifier(singularName), t.newExpression(t.identifier(modelName), [t.identifier('client')])));
591
926
  }
592
927
  if (hasCustomQueries) {
593
- returnProperties.push(t.objectProperty(t.identifier('query'), t.callExpression(t.identifier('createQueryOperations'), [t.identifier('client')])));
928
+ returnProperties.push(t.objectProperty(t.identifier('query'), t.callExpression(t.identifier('createQueryOperations'), [
929
+ t.identifier('client'),
930
+ ])));
594
931
  }
595
932
  if (hasCustomMutations) {
596
- returnProperties.push(t.objectProperty(t.identifier('mutation'), t.callExpression(t.identifier('createMutationOperations'), [t.identifier('client')])));
933
+ returnProperties.push(t.objectProperty(t.identifier('mutation'), t.callExpression(t.identifier('createMutationOperations'), [
934
+ t.identifier('client'),
935
+ ])));
597
936
  }
598
937
  // Build the createClient function body
599
938
  const clientDecl = t.variableDeclaration('const', [