@constructive-io/graphql-codegen 2.24.1 → 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 (38) hide show
  1. package/README.md +403 -279
  2. package/cli/codegen/orm/client-generator.js +475 -136
  3. package/cli/codegen/orm/custom-ops-generator.js +8 -3
  4. package/cli/codegen/orm/model-generator.js +18 -5
  5. package/cli/codegen/orm/select-types.d.ts +33 -0
  6. package/cli/commands/generate-orm.d.ts +14 -0
  7. package/cli/commands/generate-orm.js +160 -44
  8. package/cli/commands/generate.d.ts +22 -0
  9. package/cli/commands/generate.js +195 -55
  10. package/cli/commands/init.js +29 -9
  11. package/cli/index.js +133 -28
  12. package/cli/watch/orchestrator.d.ts +4 -0
  13. package/cli/watch/orchestrator.js +4 -0
  14. package/esm/cli/codegen/orm/client-generator.js +475 -136
  15. package/esm/cli/codegen/orm/custom-ops-generator.js +8 -3
  16. package/esm/cli/codegen/orm/model-generator.js +18 -5
  17. package/esm/cli/codegen/orm/select-types.d.ts +33 -0
  18. package/esm/cli/commands/generate-orm.d.ts +14 -0
  19. package/esm/cli/commands/generate-orm.js +161 -45
  20. package/esm/cli/commands/generate.d.ts +22 -0
  21. package/esm/cli/commands/generate.js +195 -56
  22. package/esm/cli/commands/init.js +29 -9
  23. package/esm/cli/index.js +134 -29
  24. package/esm/cli/watch/orchestrator.d.ts +4 -0
  25. package/esm/cli/watch/orchestrator.js +5 -1
  26. package/esm/types/config.d.ts +39 -2
  27. package/esm/types/config.js +88 -4
  28. package/esm/types/index.d.ts +2 -2
  29. package/esm/types/index.js +1 -1
  30. package/package.json +10 -7
  31. package/types/config.d.ts +39 -2
  32. package/types/config.js +91 -4
  33. package/types/index.d.ts +2 -2
  34. package/types/index.js +2 -1
  35. package/cli/codegen/orm/query-builder.d.ts +0 -161
  36. package/cli/codegen/orm/query-builder.js +0 -366
  37. package/esm/cli/codegen/orm/query-builder.d.ts +0 -161
  38. package/esm/cli/codegen/orm/query-builder.js +0 -353
@@ -175,6 +175,14 @@ function generateQueryBuilderFile() {
175
175
  * DO NOT EDIT - changes will be overwritten
176
176
  */
177
177
 
178
+ import * as t from 'gql-ast';
179
+ import { parseType, print } from 'graphql';
180
+ import type {
181
+ ArgumentNode,
182
+ FieldNode,
183
+ VariableDefinitionNode,
184
+ EnumValueNode,
185
+ } from 'graphql';
178
186
  import { OrmClient, QueryResult, GraphQLRequestError } from './client';
179
187
 
180
188
  export interface QueryBuilderConfig {
@@ -248,19 +256,25 @@ export class QueryBuilder<TResult> {
248
256
  }
249
257
 
250
258
  // ============================================================================
251
- // Document Builders
259
+ // Selection Builders
252
260
  // ============================================================================
253
261
 
254
- export function buildSelections<T>(select: T): string {
255
- if (!select) return '';
262
+ export function buildSelections(
263
+ select: Record<string, unknown> | undefined
264
+ ): FieldNode[] {
265
+ if (!select) {
266
+ return [];
267
+ }
256
268
 
257
- const fields: string[] = [];
269
+ const fields: FieldNode[] = [];
258
270
 
259
271
  for (const [key, value] of Object.entries(select)) {
260
- if (value === false || value === undefined) continue;
272
+ if (value === false || value === undefined) {
273
+ continue;
274
+ }
261
275
 
262
276
  if (value === true) {
263
- fields.push(key);
277
+ fields.push(t.field({ name: key }));
264
278
  continue;
265
279
  }
266
280
 
@@ -270,39 +284,53 @@ export function buildSelections<T>(select: T): string {
270
284
  first?: number;
271
285
  filter?: Record<string, unknown>;
272
286
  orderBy?: string[];
273
- // New: connection flag to differentiate connection types from regular objects
274
287
  connection?: boolean;
275
288
  };
276
289
 
277
290
  if (nested.select) {
278
291
  const nestedSelections = buildSelections(nested.select);
279
-
280
- // Check if this is a connection type (has pagination args or explicit connection flag)
281
- const isConnection = nested.connection === true || nested.first !== undefined || nested.filter !== undefined;
282
-
292
+ const isConnection =
293
+ nested.connection === true ||
294
+ nested.first !== undefined ||
295
+ nested.filter !== undefined;
296
+ const args = buildArgs([
297
+ buildOptionalArg('first', nested.first),
298
+ nested.filter
299
+ ? t.argument({ name: 'filter', value: buildValueAst(nested.filter) })
300
+ : null,
301
+ buildEnumListArg('orderBy', nested.orderBy),
302
+ ]);
303
+
283
304
  if (isConnection) {
284
- // Connection type - wrap in nodes/totalCount/pageInfo
285
- const args: string[] = [];
286
- if (nested.first !== undefined) args.push(\`first: \${nested.first}\`);
287
- if (nested.orderBy?.length) args.push(\`orderBy: [\${nested.orderBy.join(', ')}]\`);
288
- const argsStr = args.length > 0 ? \`(\${args.join(', ')})\` : '';
289
-
290
- fields.push(\`\${key}\${argsStr} {
291
- nodes { \${nestedSelections} }
292
- totalCount
293
- pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
294
- }\`);
305
+ fields.push(
306
+ t.field({
307
+ name: key,
308
+ args,
309
+ selectionSet: t.selectionSet({
310
+ selections: buildConnectionSelections(nestedSelections),
311
+ }),
312
+ })
313
+ );
295
314
  } else {
296
- // Regular nested object - just wrap in braces
297
- fields.push(\`\${key} { \${nestedSelections} }\`);
315
+ fields.push(
316
+ t.field({
317
+ name: key,
318
+ args,
319
+ selectionSet: t.selectionSet({ selections: nestedSelections }),
320
+ })
321
+ );
298
322
  }
299
323
  }
300
324
  }
301
325
  }
302
326
 
303
- return fields.join('\\n ');
327
+ return fields;
304
328
  }
305
329
 
330
+ // ============================================================================
331
+ // Document Builders
332
+ // ============================================================================
333
+
306
334
  export function buildFindManyDocument<TSelect, TWhere>(
307
335
  operationName: string,
308
336
  queryField: string,
@@ -319,60 +347,44 @@ export function buildFindManyDocument<TSelect, TWhere>(
319
347
  filterTypeName: string,
320
348
  orderByTypeName: string
321
349
  ): { document: string; variables: Record<string, unknown> } {
322
- const selections = select ? buildSelections(select) : 'id';
350
+ const selections = select
351
+ ? buildSelections(select as Record<string, unknown>)
352
+ : [t.field({ name: 'id' })];
323
353
 
324
- const varDefs: string[] = [];
325
- const queryArgs: string[] = [];
354
+ const variableDefinitions: VariableDefinitionNode[] = [];
355
+ const queryArgs: ArgumentNode[] = [];
326
356
  const variables: Record<string, unknown> = {};
327
357
 
328
- if (args.where) {
329
- varDefs.push(\`$where: \${filterTypeName}\`);
330
- queryArgs.push('filter: $where');
331
- variables.where = args.where;
332
- }
333
- if (args.orderBy?.length) {
334
- varDefs.push(\`$orderBy: [\${orderByTypeName}!]\`);
335
- queryArgs.push('orderBy: $orderBy');
336
- variables.orderBy = args.orderBy;
337
- }
338
- if (args.first !== undefined) {
339
- varDefs.push('$first: Int');
340
- queryArgs.push('first: $first');
341
- variables.first = args.first;
342
- }
343
- if (args.last !== undefined) {
344
- varDefs.push('$last: Int');
345
- queryArgs.push('last: $last');
346
- variables.last = args.last;
347
- }
348
- if (args.after) {
349
- varDefs.push('$after: Cursor');
350
- queryArgs.push('after: $after');
351
- variables.after = args.after;
352
- }
353
- if (args.before) {
354
- varDefs.push('$before: Cursor');
355
- queryArgs.push('before: $before');
356
- variables.before = args.before;
357
- }
358
- if (args.offset !== undefined) {
359
- varDefs.push('$offset: Int');
360
- queryArgs.push('offset: $offset');
361
- variables.offset = args.offset;
362
- }
363
-
364
- const varDefsStr = varDefs.length > 0 ? \`(\${varDefs.join(', ')})\` : '';
365
- const queryArgsStr = queryArgs.length > 0 ? \`(\${queryArgs.join(', ')})\` : '';
366
-
367
- const document = \`query \${operationName}Query\${varDefsStr} {
368
- \${queryField}\${queryArgsStr} {
369
- nodes { \${selections} }
370
- totalCount
371
- pageInfo { hasNextPage hasPreviousPage startCursor endCursor }
372
- }
373
- }\`;
358
+ addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
359
+ addVariable({ varName: 'orderBy', typeName: '[' + orderByTypeName + '!]', value: args.orderBy?.length ? args.orderBy : undefined }, variableDefinitions, queryArgs, variables);
360
+ addVariable({ varName: 'first', typeName: 'Int', value: args.first }, variableDefinitions, queryArgs, variables);
361
+ addVariable({ varName: 'last', typeName: 'Int', value: args.last }, variableDefinitions, queryArgs, variables);
362
+ addVariable({ varName: 'after', typeName: 'Cursor', value: args.after }, variableDefinitions, queryArgs, variables);
363
+ addVariable({ varName: 'before', typeName: 'Cursor', value: args.before }, variableDefinitions, queryArgs, variables);
364
+ addVariable({ varName: 'offset', typeName: 'Int', value: args.offset }, variableDefinitions, queryArgs, variables);
365
+
366
+ const document = t.document({
367
+ definitions: [
368
+ t.operationDefinition({
369
+ operation: 'query',
370
+ name: operationName + 'Query',
371
+ variableDefinitions: variableDefinitions.length ? variableDefinitions : undefined,
372
+ selectionSet: t.selectionSet({
373
+ selections: [
374
+ t.field({
375
+ name: queryField,
376
+ args: queryArgs.length ? queryArgs : undefined,
377
+ selectionSet: t.selectionSet({
378
+ selections: buildConnectionSelections(selections),
379
+ }),
380
+ }),
381
+ ],
382
+ }),
383
+ }),
384
+ ],
385
+ });
374
386
 
375
- return { document, variables };
387
+ return { document: print(document), variables };
376
388
  }
377
389
 
378
390
  export function buildFindFirstDocument<TSelect, TWhere>(
@@ -382,25 +394,45 @@ export function buildFindFirstDocument<TSelect, TWhere>(
382
394
  args: { where?: TWhere },
383
395
  filterTypeName: string
384
396
  ): { document: string; variables: Record<string, unknown> } {
385
- const selections = select ? buildSelections(select) : 'id';
386
-
387
- const varDefs: string[] = ['$first: Int'];
388
- const queryArgs: string[] = ['first: $first'];
389
- const variables: Record<string, unknown> = { first: 1 };
397
+ const selections = select
398
+ ? buildSelections(select as Record<string, unknown>)
399
+ : [t.field({ name: 'id' })];
390
400
 
391
- if (args.where) {
392
- varDefs.push(\`$where: \${filterTypeName}\`);
393
- queryArgs.push('filter: $where');
394
- variables.where = args.where;
395
- }
401
+ const variableDefinitions: VariableDefinitionNode[] = [];
402
+ const queryArgs: ArgumentNode[] = [];
403
+ const variables: Record<string, unknown> = {};
396
404
 
397
- const document = \`query \${operationName}Query(\${varDefs.join(', ')}) {
398
- \${queryField}(\${queryArgs.join(', ')}) {
399
- nodes { \${selections} }
400
- }
401
- }\`;
405
+ // Always add first: 1 for findFirst
406
+ addVariable({ varName: 'first', typeName: 'Int', value: 1 }, variableDefinitions, queryArgs, variables);
407
+ addVariable({ varName: 'where', argName: 'filter', typeName: filterTypeName, value: args.where }, variableDefinitions, queryArgs, variables);
408
+
409
+ const document = t.document({
410
+ definitions: [
411
+ t.operationDefinition({
412
+ operation: 'query',
413
+ name: operationName + 'Query',
414
+ variableDefinitions,
415
+ selectionSet: t.selectionSet({
416
+ selections: [
417
+ t.field({
418
+ name: queryField,
419
+ args: queryArgs,
420
+ selectionSet: t.selectionSet({
421
+ selections: [
422
+ t.field({
423
+ name: 'nodes',
424
+ selectionSet: t.selectionSet({ selections }),
425
+ }),
426
+ ],
427
+ }),
428
+ }),
429
+ ],
430
+ }),
431
+ }),
432
+ ],
433
+ });
402
434
 
403
- return { document, variables };
435
+ return { document: print(document), variables };
404
436
  }
405
437
 
406
438
  export function buildCreateDocument<TSelect, TData>(
@@ -411,17 +443,27 @@ export function buildCreateDocument<TSelect, TData>(
411
443
  data: TData,
412
444
  inputTypeName: string
413
445
  ): { document: string; variables: Record<string, unknown> } {
414
- const selections = select ? buildSelections(select) : 'id';
415
-
416
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
417
- \${mutationField}(input: $input) {
418
- \${entityField} { \${selections} }
419
- }
420
- }\`;
446
+ const selections = select
447
+ ? buildSelections(select as Record<string, unknown>)
448
+ : [t.field({ name: 'id' })];
421
449
 
422
450
  return {
423
- document,
424
- variables: { input: { [entityField]: data } },
451
+ document: buildInputMutationDocument({
452
+ operationName,
453
+ mutationField,
454
+ inputTypeName,
455
+ resultSelections: [
456
+ t.field({
457
+ name: entityField,
458
+ selectionSet: t.selectionSet({ selections }),
459
+ }),
460
+ ],
461
+ }),
462
+ variables: {
463
+ input: {
464
+ [entityField]: data,
465
+ },
466
+ },
425
467
  };
426
468
  }
427
469
 
@@ -434,17 +476,28 @@ export function buildUpdateDocument<TSelect, TWhere extends { id: string }, TDat
434
476
  data: TData,
435
477
  inputTypeName: string
436
478
  ): { document: string; variables: Record<string, unknown> } {
437
- const selections = select ? buildSelections(select) : 'id';
438
-
439
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
440
- \${mutationField}(input: $input) {
441
- \${entityField} { \${selections} }
442
- }
443
- }\`;
479
+ const selections = select
480
+ ? buildSelections(select as Record<string, unknown>)
481
+ : [t.field({ name: 'id' })];
444
482
 
445
483
  return {
446
- document,
447
- variables: { input: { id: where.id, patch: data } },
484
+ document: buildInputMutationDocument({
485
+ operationName,
486
+ mutationField,
487
+ inputTypeName,
488
+ resultSelections: [
489
+ t.field({
490
+ name: entityField,
491
+ selectionSet: t.selectionSet({ selections }),
492
+ }),
493
+ ],
494
+ }),
495
+ variables: {
496
+ input: {
497
+ id: where.id,
498
+ patch: data,
499
+ },
500
+ },
448
501
  };
449
502
  }
450
503
 
@@ -455,15 +508,25 @@ export function buildDeleteDocument<TWhere extends { id: string }>(
455
508
  where: TWhere,
456
509
  inputTypeName: string
457
510
  ): { document: string; variables: Record<string, unknown> } {
458
- const document = \`mutation \${operationName}Mutation($input: \${inputTypeName}!) {
459
- \${mutationField}(input: $input) {
460
- \${entityField} { id }
461
- }
462
- }\`;
463
-
464
511
  return {
465
- document,
466
- variables: { input: { id: where.id } },
512
+ document: buildInputMutationDocument({
513
+ operationName,
514
+ mutationField,
515
+ inputTypeName,
516
+ resultSelections: [
517
+ t.field({
518
+ name: entityField,
519
+ selectionSet: t.selectionSet({
520
+ selections: [t.field({ name: 'id' })],
521
+ }),
522
+ }),
523
+ ],
524
+ }),
525
+ variables: {
526
+ input: {
527
+ id: where.id,
528
+ },
529
+ },
467
530
  };
468
531
  }
469
532
 
@@ -475,21 +538,251 @@ export function buildCustomDocument<TSelect, TArgs>(
475
538
  args: TArgs,
476
539
  variableDefinitions: Array<{ name: string; type: string }>
477
540
  ): { document: string; variables: Record<string, unknown> } {
478
- const selections = select ? buildSelections(select) : '';
479
-
480
- const varDefs = variableDefinitions.map(v => \`$\${v.name}: \${v.type}\`);
481
- const fieldArgs = variableDefinitions.map(v => \`\${v.name}: $\${v.name}\`);
482
-
483
- const varDefsStr = varDefs.length > 0 ? \`(\${varDefs.join(', ')})\` : '';
484
- const fieldArgsStr = fieldArgs.length > 0 ? \`(\${fieldArgs.join(', ')})\` : '';
485
- const selectionsBlock = selections ? \` { \${selections} }\` : '';
486
-
487
- const opType = operationType === 'query' ? 'query' : 'mutation';
488
- const document = \`\${opType} \${operationName}\${varDefsStr} {
489
- \${fieldName}\${fieldArgsStr}\${selectionsBlock}
490
- }\`;
491
-
492
- return { document, variables: (args ?? {}) as Record<string, unknown> };
541
+ let actualSelect = select;
542
+ let isConnection = false;
543
+
544
+ if (select && typeof select === 'object' && 'select' in select) {
545
+ const wrapper = select as { select?: TSelect; connection?: boolean };
546
+ if (wrapper.select) {
547
+ actualSelect = wrapper.select;
548
+ isConnection = wrapper.connection === true;
549
+ }
550
+ }
551
+
552
+ const selections = actualSelect
553
+ ? buildSelections(actualSelect as Record<string, unknown>)
554
+ : [];
555
+
556
+ const variableDefs = variableDefinitions.map((definition) =>
557
+ t.variableDefinition({
558
+ variable: t.variable({ name: definition.name }),
559
+ type: parseType(definition.type),
560
+ })
561
+ );
562
+ const fieldArgs = variableDefinitions.map((definition) =>
563
+ t.argument({
564
+ name: definition.name,
565
+ value: t.variable({ name: definition.name }),
566
+ })
567
+ );
568
+
569
+ const fieldSelections = isConnection
570
+ ? buildConnectionSelections(selections)
571
+ : selections;
572
+
573
+ const document = t.document({
574
+ definitions: [
575
+ t.operationDefinition({
576
+ operation: operationType,
577
+ name: operationName,
578
+ variableDefinitions: variableDefs.length ? variableDefs : undefined,
579
+ selectionSet: t.selectionSet({
580
+ selections: [
581
+ t.field({
582
+ name: fieldName,
583
+ args: fieldArgs.length ? fieldArgs : undefined,
584
+ selectionSet: fieldSelections.length
585
+ ? t.selectionSet({ selections: fieldSelections })
586
+ : undefined,
587
+ }),
588
+ ],
589
+ }),
590
+ }),
591
+ ],
592
+ });
593
+
594
+ return {
595
+ document: print(document),
596
+ variables: (args ?? {}) as Record<string, unknown>,
597
+ };
598
+ }
599
+
600
+ // ============================================================================
601
+ // Helper Functions
602
+ // ============================================================================
603
+
604
+ function buildArgs(args: Array<ArgumentNode | null>): ArgumentNode[] {
605
+ return args.filter((arg): arg is ArgumentNode => arg !== null);
606
+ }
607
+
608
+ function buildOptionalArg(
609
+ name: string,
610
+ value: number | string | undefined
611
+ ): ArgumentNode | null {
612
+ if (value === undefined) {
613
+ return null;
614
+ }
615
+ const valueNode =
616
+ typeof value === 'number'
617
+ ? t.intValue({ value: value.toString() })
618
+ : t.stringValue({ value });
619
+ return t.argument({ name, value: valueNode });
620
+ }
621
+
622
+ function buildEnumListArg(
623
+ name: string,
624
+ values: string[] | undefined
625
+ ): ArgumentNode | null {
626
+ if (!values || values.length === 0) {
627
+ return null;
628
+ }
629
+ return t.argument({
630
+ name,
631
+ value: t.listValue({
632
+ values: values.map((value) => buildEnumValue(value)),
633
+ }),
634
+ });
635
+ }
636
+
637
+ function buildEnumValue(value: string): EnumValueNode {
638
+ return {
639
+ kind: 'EnumValue',
640
+ value,
641
+ };
642
+ }
643
+
644
+ function buildPageInfoSelections(): FieldNode[] {
645
+ return [
646
+ t.field({ name: 'hasNextPage' }),
647
+ t.field({ name: 'hasPreviousPage' }),
648
+ t.field({ name: 'startCursor' }),
649
+ t.field({ name: 'endCursor' }),
650
+ ];
651
+ }
652
+
653
+ function buildConnectionSelections(nodeSelections: FieldNode[]): FieldNode[] {
654
+ return [
655
+ t.field({
656
+ name: 'nodes',
657
+ selectionSet: t.selectionSet({ selections: nodeSelections }),
658
+ }),
659
+ t.field({ name: 'totalCount' }),
660
+ t.field({
661
+ name: 'pageInfo',
662
+ selectionSet: t.selectionSet({ selections: buildPageInfoSelections() }),
663
+ }),
664
+ ];
665
+ }
666
+
667
+ interface VariableSpec {
668
+ varName: string;
669
+ argName?: string;
670
+ typeName: string;
671
+ value: unknown;
672
+ }
673
+
674
+ interface InputMutationConfig {
675
+ operationName: string;
676
+ mutationField: string;
677
+ inputTypeName: string;
678
+ resultSelections: FieldNode[];
679
+ }
680
+
681
+ function buildInputMutationDocument(config: InputMutationConfig): string {
682
+ const document = t.document({
683
+ definitions: [
684
+ t.operationDefinition({
685
+ operation: 'mutation',
686
+ name: config.operationName + 'Mutation',
687
+ variableDefinitions: [
688
+ t.variableDefinition({
689
+ variable: t.variable({ name: 'input' }),
690
+ type: parseType(config.inputTypeName + '!'),
691
+ }),
692
+ ],
693
+ selectionSet: t.selectionSet({
694
+ selections: [
695
+ t.field({
696
+ name: config.mutationField,
697
+ args: [
698
+ t.argument({
699
+ name: 'input',
700
+ value: t.variable({ name: 'input' }),
701
+ }),
702
+ ],
703
+ selectionSet: t.selectionSet({
704
+ selections: config.resultSelections,
705
+ }),
706
+ }),
707
+ ],
708
+ }),
709
+ }),
710
+ ],
711
+ });
712
+ return print(document);
713
+ }
714
+
715
+ function addVariable(
716
+ spec: VariableSpec,
717
+ definitions: VariableDefinitionNode[],
718
+ args: ArgumentNode[],
719
+ variables: Record<string, unknown>
720
+ ): void {
721
+ if (spec.value === undefined) return;
722
+
723
+ definitions.push(
724
+ t.variableDefinition({
725
+ variable: t.variable({ name: spec.varName }),
726
+ type: parseType(spec.typeName),
727
+ })
728
+ );
729
+ args.push(
730
+ t.argument({
731
+ name: spec.argName ?? spec.varName,
732
+ value: t.variable({ name: spec.varName }),
733
+ })
734
+ );
735
+ variables[spec.varName] = spec.value;
736
+ }
737
+
738
+ function buildValueAst(
739
+ value: unknown
740
+ ):
741
+ | ReturnType<typeof t.stringValue>
742
+ | ReturnType<typeof t.intValue>
743
+ | ReturnType<typeof t.floatValue>
744
+ | ReturnType<typeof t.booleanValue>
745
+ | ReturnType<typeof t.listValue>
746
+ | ReturnType<typeof t.objectValue>
747
+ | ReturnType<typeof t.nullValue>
748
+ | EnumValueNode {
749
+ if (value === null) {
750
+ return t.nullValue();
751
+ }
752
+
753
+ if (typeof value === 'boolean') {
754
+ return t.booleanValue({ value });
755
+ }
756
+
757
+ if (typeof value === 'number') {
758
+ return Number.isInteger(value)
759
+ ? t.intValue({ value: value.toString() })
760
+ : t.floatValue({ value: value.toString() });
761
+ }
762
+
763
+ if (typeof value === 'string') {
764
+ return t.stringValue({ value });
765
+ }
766
+
767
+ if (Array.isArray(value)) {
768
+ return t.listValue({
769
+ values: value.map((item) => buildValueAst(item)),
770
+ });
771
+ }
772
+
773
+ if (typeof value === 'object' && value !== null) {
774
+ const obj = value as Record<string, unknown>;
775
+ return t.objectValue({
776
+ fields: Object.entries(obj).map(([key, val]) =>
777
+ t.objectField({
778
+ name: key,
779
+ value: buildValueAst(val),
780
+ })
781
+ ),
782
+ });
783
+ }
784
+
785
+ throw new Error('Unsupported value type: ' + typeof value);
493
786
  }
494
787
  `;
495
788
  return {
@@ -551,6 +844,44 @@ export interface DeleteArgs<TWhere> {
551
844
  where: TWhere;
552
845
  }
553
846
 
847
+ /**
848
+ * Recursively validates select objects, rejecting unknown keys.
849
+ *
850
+ * This type ensures that users can only select fields that actually exist
851
+ * in the GraphQL schema. It returns \`never\` if any excess keys are found
852
+ * at any nesting level, causing a TypeScript compile error.
853
+ *
854
+ * Why this is needed:
855
+ * TypeScript's excess property checking has a quirk where it only catches
856
+ * invalid fields when they are the ONLY fields. When mixed with valid fields
857
+ * (e.g., \`{ id: true, invalidField: true }\`), the structural typing allows
858
+ * the excess property through. This type explicitly checks for and rejects
859
+ * such cases.
860
+ *
861
+ * @example
862
+ * // This will cause a type error because 'invalid' doesn't exist:
863
+ * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
864
+ * // Result = never (causes assignment error)
865
+ *
866
+ * @example
867
+ * // This works because all fields are valid:
868
+ * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
869
+ * // Result = { id: true }
870
+ */
871
+ export type DeepExact<T, Shape> = T extends Shape
872
+ ? Exclude<keyof T, keyof Shape> extends never
873
+ ? {
874
+ [K in keyof T]: K extends keyof Shape
875
+ ? T[K] extends { select: infer NS }
876
+ ? Shape[K] extends { select?: infer ShapeNS }
877
+ ? { select: DeepExact<NS, NonNullable<ShapeNS>> }
878
+ : T[K]
879
+ : T[K]
880
+ : never;
881
+ }
882
+ : never
883
+ : never;
884
+
554
885
  /**
555
886
  * Infer result type from select configuration
556
887
  */
@@ -616,9 +947,13 @@ function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations)
616
947
  typeExportDecl.exportKind = 'type';
617
948
  statements.push(typeExportDecl);
618
949
  // export { GraphQLRequestError } from './client';
619
- statements.push(t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier('GraphQLRequestError'), t.identifier('GraphQLRequestError'))], t.stringLiteral('./client')));
950
+ statements.push(t.exportNamedDeclaration(null, [
951
+ t.exportSpecifier(t.identifier('GraphQLRequestError'), t.identifier('GraphQLRequestError')),
952
+ ], t.stringLiteral('./client')));
620
953
  // export { QueryBuilder } from './query-builder';
621
- statements.push(t.exportNamedDeclaration(null, [t.exportSpecifier(t.identifier('QueryBuilder'), t.identifier('QueryBuilder'))], t.stringLiteral('./query-builder')));
954
+ statements.push(t.exportNamedDeclaration(null, [
955
+ t.exportSpecifier(t.identifier('QueryBuilder'), t.identifier('QueryBuilder')),
956
+ ], t.stringLiteral('./query-builder')));
622
957
  // export * from './select-types';
623
958
  statements.push(t.exportAllDeclaration(t.stringLiteral('./select-types')));
624
959
  // Build the return object properties
@@ -629,10 +964,14 @@ function generateCreateClientFile(tables, hasCustomQueries, hasCustomMutations)
629
964
  returnProperties.push(t.objectProperty(t.identifier(singularName), t.newExpression(t.identifier(modelName), [t.identifier('client')])));
630
965
  }
631
966
  if (hasCustomQueries) {
632
- returnProperties.push(t.objectProperty(t.identifier('query'), t.callExpression(t.identifier('createQueryOperations'), [t.identifier('client')])));
967
+ returnProperties.push(t.objectProperty(t.identifier('query'), t.callExpression(t.identifier('createQueryOperations'), [
968
+ t.identifier('client'),
969
+ ])));
633
970
  }
634
971
  if (hasCustomMutations) {
635
- returnProperties.push(t.objectProperty(t.identifier('mutation'), t.callExpression(t.identifier('createMutationOperations'), [t.identifier('client')])));
972
+ returnProperties.push(t.objectProperty(t.identifier('mutation'), t.callExpression(t.identifier('createMutationOperations'), [
973
+ t.identifier('client'),
974
+ ])));
636
975
  }
637
976
  // Build the createClient function body
638
977
  const clientDecl = t.variableDeclaration('const', [