@constructive-io/graphql-codegen 3.3.2 → 4.0.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 (67) hide show
  1. package/README.md +0 -4
  2. package/core/ast.js +6 -5
  3. package/core/codegen/custom-mutations.js +22 -22
  4. package/core/codegen/custom-queries.js +24 -17
  5. package/core/codegen/hooks-ast.d.ts +1 -1
  6. package/core/codegen/hooks-ast.js +22 -5
  7. package/core/codegen/index.d.ts +1 -1
  8. package/core/codegen/mutations.js +16 -12
  9. package/core/codegen/orm/custom-ops-generator.js +37 -3
  10. package/core/codegen/orm/input-types-generator.js +161 -89
  11. package/core/codegen/orm/model-generator.js +72 -73
  12. package/core/codegen/orm/select-types.d.ts +27 -17
  13. package/core/codegen/queries.js +37 -25
  14. package/core/codegen/schema-types-generator.js +21 -0
  15. package/core/codegen/templates/hooks-selection.ts +12 -0
  16. package/core/codegen/templates/query-builder.ts +103 -59
  17. package/core/codegen/templates/select-types.ts +59 -33
  18. package/core/codegen/types.js +26 -0
  19. package/core/codegen/utils.d.ts +1 -1
  20. package/core/codegen/utils.js +1 -1
  21. package/core/custom-ast.js +9 -8
  22. package/core/database/index.js +2 -3
  23. package/core/index.d.ts +2 -0
  24. package/core/index.js +2 -0
  25. package/core/introspect/infer-tables.js +144 -58
  26. package/core/introspect/transform-schema.d.ts +1 -1
  27. package/core/introspect/transform-schema.js +3 -1
  28. package/esm/core/ast.js +6 -5
  29. package/esm/core/codegen/custom-mutations.js +23 -23
  30. package/esm/core/codegen/custom-queries.js +25 -18
  31. package/esm/core/codegen/hooks-ast.d.ts +1 -1
  32. package/esm/core/codegen/hooks-ast.js +22 -5
  33. package/esm/core/codegen/index.d.ts +1 -1
  34. package/esm/core/codegen/mutations.js +16 -12
  35. package/esm/core/codegen/orm/custom-ops-generator.js +38 -4
  36. package/esm/core/codegen/orm/input-types-generator.js +163 -91
  37. package/esm/core/codegen/orm/model-generator.js +73 -74
  38. package/esm/core/codegen/orm/select-types.d.ts +27 -17
  39. package/esm/core/codegen/queries.js +37 -25
  40. package/esm/core/codegen/schema-types-generator.js +21 -0
  41. package/esm/core/codegen/types.js +26 -0
  42. package/esm/core/codegen/utils.d.ts +1 -1
  43. package/esm/core/codegen/utils.js +1 -1
  44. package/esm/core/custom-ast.js +9 -8
  45. package/esm/core/database/index.js +2 -3
  46. package/esm/core/index.d.ts +2 -0
  47. package/esm/core/index.js +2 -0
  48. package/esm/core/introspect/infer-tables.js +144 -58
  49. package/esm/core/introspect/transform-schema.d.ts +1 -1
  50. package/esm/core/introspect/transform-schema.js +3 -1
  51. package/esm/generators/field-selector.js +1 -0
  52. package/esm/generators/index.d.ts +3 -0
  53. package/esm/generators/index.js +3 -0
  54. package/esm/generators/mutations.js +4 -4
  55. package/esm/generators/select.js +4 -4
  56. package/esm/index.d.ts +1 -1
  57. package/esm/index.js +1 -1
  58. package/esm/types/schema.d.ts +5 -3
  59. package/generators/field-selector.js +1 -0
  60. package/generators/index.d.ts +3 -0
  61. package/generators/index.js +3 -0
  62. package/generators/mutations.js +3 -3
  63. package/generators/select.js +3 -3
  64. package/index.d.ts +1 -1
  65. package/index.js +1 -1
  66. package/package.json +10 -10
  67. package/types/schema.d.ts +5 -3
@@ -44,25 +44,27 @@ export interface NestedSelectConfig {
44
44
  filter?: Record<string, unknown>;
45
45
  orderBy?: string[];
46
46
  }
47
+ type DepthLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
48
+ type DecrementDepth = {
49
+ 0: 0;
50
+ 1: 0;
51
+ 2: 1;
52
+ 3: 2;
53
+ 4: 3;
54
+ 5: 4;
55
+ 6: 5;
56
+ 7: 6;
57
+ 8: 7;
58
+ 9: 8;
59
+ 10: 9;
60
+ };
47
61
  /**
48
62
  * Recursively validates select objects, rejecting unknown keys.
49
63
  *
50
- * NOTE: This type is intentionally NOT used in generated parameter positions
51
- * (conditional types block IDE autocompletion). Parameters use `S` directly
52
- * with `S extends XxxSelect` constraints, which provides full
53
- * autocompletion via TypeScript's contextual typing.
54
- *
55
- * @example
56
- * // This will cause a type error because 'invalid' doesn't exist:
57
- * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
58
- * // Result = never (causes assignment error)
59
- *
60
- * @example
61
- * // This works because all fields are valid:
62
- * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
63
- * // Result = { id: true }
64
+ * NOTE: Depth is intentionally capped to avoid circular-instantiation issues
65
+ * in very large cyclic schemas.
64
66
  */
65
- export type DeepExact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? {
67
+ export type DeepExact<T, Shape, Depth extends DepthLevel = 10> = Depth extends 0 ? T extends Shape ? T : never : T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? {
66
68
  [K in keyof T]: K extends keyof Shape ? T[K] extends {
67
69
  select: infer NS;
68
70
  } ? Extract<Shape[K], {
@@ -70,10 +72,10 @@ export type DeepExact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape
70
72
  }> extends {
71
73
  select?: infer ShapeNS;
72
74
  } ? DeepExact<Omit<T[K], 'select'> & {
73
- select: DeepExact<NS, NonNullable<ShapeNS>>;
75
+ select: DeepExact<NS, NonNullable<ShapeNS>, DecrementDepth[Depth]>;
74
76
  }, Extract<Shape[K], {
75
77
  select?: unknown;
76
- }>> : never : T[K] : never;
78
+ }>, DecrementDepth[Depth]> : never : T[K] : never;
77
79
  } : never : never;
78
80
  /**
79
81
  * Enforces exact select shape while keeping contextual typing on `S extends XxxSelect`.
@@ -81,6 +83,13 @@ export type DeepExact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape
81
83
  * `{ select: S } & StrictSelect<S, XxxSelect>`.
82
84
  */
83
85
  export type StrictSelect<S, Shape> = S extends DeepExact<S, Shape> ? {} : never;
86
+ /**
87
+ * Hook-optimized strict select variant.
88
+ *
89
+ * Uses a shallower recursion depth to keep editor autocomplete responsive
90
+ * in large schemas while still validating common nested-select mistakes.
91
+ */
92
+ export type HookStrictSelect<S, Shape> = S extends DeepExact<S, Shape, 5> ? {} : never;
84
93
  /**
85
94
  * Infers the result type from a select configuration
86
95
  *
@@ -205,3 +214,4 @@ export type MutationResult<TEntity, TSelect, TPayloadKey extends string> = Query
205
214
  [EntityKey: string]: ResolveSelectResult<TEntity, TSelect>;
206
215
  };
207
216
  }>;
217
+ export {};
@@ -76,7 +76,12 @@ function generateListQueryHook(table, options = {}) {
76
76
  }
77
77
  }
78
78
  statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/input-types', [selectTypeName, relationTypeName, filterTypeName, orderByTypeName], true));
79
- statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/select-types', ['FindManyArgs', 'InferSelectResult', 'ConnectionResult', 'StrictSelect'], true));
79
+ statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/select-types', [
80
+ 'FindManyArgs',
81
+ 'InferSelectResult',
82
+ 'ConnectionResult',
83
+ 'HookStrictSelect',
84
+ ], true));
80
85
  // Re-exports
81
86
  statements.push((0, hooks_ast_1.createTypeReExport)([selectTypeName, relationTypeName, filterTypeName, orderByTypeName], '../../orm/input-types'));
82
87
  // Query key
@@ -193,8 +198,10 @@ function generateListQueryHook(table, options = {}) {
193
198
  const fetchFnName = `fetch${(0, utils_1.ucFirst)(pluralName)}Query`;
194
199
  {
195
200
  // Overload 1: with fields
196
- const f1ParamType = t.tsTypeLiteral([
197
- t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsListSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName, filterTypeName, orderByTypeName))),
201
+ const f1ParamType = t.tsIntersectionType([
202
+ t.tsTypeLiteral([
203
+ t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsListSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName, filterTypeName, orderByTypeName))),
204
+ ]),
198
205
  ]);
199
206
  const f1Decl = (0, hooks_ast_1.exportAsyncDeclareFunction)(fetchFnName, (0, hooks_ast_1.createSTypeParam)(selectTypeName), [(0, hooks_ast_1.createFunctionParam)('params', f1ParamType)], (0, hooks_ast_1.typeRef)('Promise', [listResultTypeAST((0, hooks_ast_1.sRef)())]));
200
207
  (0, hooks_ast_1.addJSDocComment)(f1Decl, [
@@ -224,15 +231,17 @@ function generateListQueryHook(table, options = {}) {
224
231
  if (reactQueryEnabled) {
225
232
  const prefetchFnName = `prefetch${(0, utils_1.ucFirst)(pluralName)}Query`;
226
233
  // Overload 1: with fields
227
- const p1Params = [
228
- t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsListSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName, filterTypeName, orderByTypeName))),
229
- ];
234
+ const p1BaseParamType = t.tsIntersectionType([
235
+ t.tsTypeLiteral([
236
+ t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsListSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName, filterTypeName, orderByTypeName))),
237
+ ]),
238
+ ]);
230
239
  const p1ParamType = hasRelationships && useCentralizedKeys
231
240
  ? t.tsIntersectionType([
232
- t.tsTypeLiteral(p1Params),
241
+ p1BaseParamType,
233
242
  (0, hooks_ast_1.scopeTypeLiteral)(scopeTypeName),
234
243
  ])
235
- : t.tsTypeLiteral(p1Params);
244
+ : p1BaseParamType;
236
245
  const p1Decl = (0, hooks_ast_1.exportAsyncDeclareFunction)(prefetchFnName, (0, hooks_ast_1.createSTypeParam)(selectTypeName), [
237
246
  (0, hooks_ast_1.createFunctionParam)('queryClient', (0, hooks_ast_1.typeRef)('QueryClient')),
238
247
  (0, hooks_ast_1.createFunctionParam)('params', p1ParamType),
@@ -315,7 +324,7 @@ function generateSingleQueryHook(table, options = {}) {
315
324
  }
316
325
  }
317
326
  statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/input-types', [selectTypeName, relationTypeName], true));
318
- statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/select-types', ['InferSelectResult', 'StrictSelect'], true));
327
+ statements.push((0, hooks_ast_1.createImportDeclaration)('../../orm/select-types', ['InferSelectResult', 'HookStrictSelect'], true));
319
328
  // Re-exports
320
329
  statements.push((0, hooks_ast_1.createTypeReExport)([selectTypeName, relationTypeName], '../../orm/input-types'));
321
330
  // Query key
@@ -383,12 +392,11 @@ function generateSingleQueryHook(table, options = {}) {
383
392
  docLines.push('```');
384
393
  }
385
394
  // Overload 1: with fields
386
- const o1Props = [
387
- t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
388
- t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
389
- ];
390
395
  const o1ParamType = t.tsIntersectionType([
391
- t.tsTypeLiteral(o1Props),
396
+ t.tsTypeLiteral([
397
+ t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
398
+ t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
399
+ ]),
392
400
  buildSingleOptionsType(singleResultTypeAST((0, hooks_ast_1.sRef)()), (0, hooks_ast_1.typeRef)('TData')),
393
401
  ]);
394
402
  const o1 = (0, hooks_ast_1.exportDeclareFunction)(hookName, (0, hooks_ast_1.createSAndTDataTypeParams)(selectTypeName, singleResultTypeAST((0, hooks_ast_1.sRef)())), [(0, hooks_ast_1.createFunctionParam)('params', o1ParamType)], (0, hooks_ast_1.typeRef)('UseQueryResult', [(0, hooks_ast_1.typeRef)('TData')]));
@@ -431,11 +439,13 @@ function generateSingleQueryHook(table, options = {}) {
431
439
  const fetchFnName = `fetch${(0, utils_1.ucFirst)(singularName)}Query`;
432
440
  {
433
441
  // Overload 1: with fields
434
- const f1Props = [
435
- t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
436
- t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
437
- ];
438
- const f1Decl = (0, hooks_ast_1.exportAsyncDeclareFunction)(fetchFnName, (0, hooks_ast_1.createSTypeParam)(selectTypeName), [(0, hooks_ast_1.createFunctionParam)('params', t.tsTypeLiteral(f1Props))], (0, hooks_ast_1.typeRef)('Promise', [singleResultTypeAST((0, hooks_ast_1.sRef)())]));
442
+ const f1ParamType = t.tsIntersectionType([
443
+ t.tsTypeLiteral([
444
+ t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
445
+ t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
446
+ ]),
447
+ ]);
448
+ const f1Decl = (0, hooks_ast_1.exportAsyncDeclareFunction)(fetchFnName, (0, hooks_ast_1.createSTypeParam)(selectTypeName), [(0, hooks_ast_1.createFunctionParam)('params', f1ParamType)], (0, hooks_ast_1.typeRef)('Promise', [singleResultTypeAST((0, hooks_ast_1.sRef)())]));
439
449
  (0, hooks_ast_1.addJSDocComment)(f1Decl, [
440
450
  `Fetch a single ${typeName} without React hooks`,
441
451
  '',
@@ -462,16 +472,18 @@ function generateSingleQueryHook(table, options = {}) {
462
472
  if (reactQueryEnabled) {
463
473
  const prefetchFnName = `prefetch${(0, utils_1.ucFirst)(singularName)}Query`;
464
474
  // Overload 1: with fields
465
- const p1Props = [
466
- t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
467
- t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
468
- ];
475
+ const p1BaseParamType = t.tsIntersectionType([
476
+ t.tsTypeLiteral([
477
+ t.tsPropertySignature(t.identifier(pkFieldName), t.tsTypeAnnotation(pkTsType)),
478
+ t.tsPropertySignature(t.identifier('selection'), t.tsTypeAnnotation((0, hooks_ast_1.withFieldsSelectionType)((0, hooks_ast_1.sRef)(), selectTypeName))),
479
+ ]),
480
+ ]);
469
481
  const p1ParamType = hasRelationships && useCentralizedKeys
470
482
  ? t.tsIntersectionType([
471
- t.tsTypeLiteral(p1Props),
483
+ p1BaseParamType,
472
484
  (0, hooks_ast_1.scopeTypeLiteral)(scopeTypeName),
473
485
  ])
474
- : t.tsTypeLiteral(p1Props);
486
+ : p1BaseParamType;
475
487
  const p1Decl = (0, hooks_ast_1.exportAsyncDeclareFunction)(prefetchFnName, (0, hooks_ast_1.createSTypeParam)(selectTypeName), [
476
488
  (0, hooks_ast_1.createFunctionParam)('queryClient', (0, hooks_ast_1.typeRef)('QueryClient')),
477
489
  (0, hooks_ast_1.createFunctionParam)('params', p1ParamType),
@@ -96,6 +96,25 @@ function shouldSkipType(typeName, tableTypeNames) {
96
96
  }
97
97
  return false;
98
98
  }
99
+ function collectCustomScalarTypes(typeRegistry) {
100
+ const customScalarTypes = new Set();
101
+ for (const [typeName, typeInfo] of typeRegistry) {
102
+ if (typeInfo.kind !== 'SCALAR')
103
+ continue;
104
+ if (scalars_1.SCALAR_NAMES.has(typeName))
105
+ continue;
106
+ customScalarTypes.add(typeName);
107
+ }
108
+ return Array.from(customScalarTypes).sort();
109
+ }
110
+ function generateCustomScalarTypes(customScalarTypes) {
111
+ const statements = [];
112
+ for (const scalarType of customScalarTypes) {
113
+ const alias = t.tsTypeAliasDeclaration(t.identifier(scalarType), null, t.tsUnknownKeyword());
114
+ statements.push(t.exportNamedDeclaration(alias));
115
+ }
116
+ return statements;
117
+ }
99
118
  function generateEnumTypes(typeRegistry, tableTypeNames) {
100
119
  const statements = [];
101
120
  const generatedTypes = new Set();
@@ -260,6 +279,7 @@ function generateSchemaTypesFile(options) {
260
279
  const { typeRegistry, tableTypeNames } = options;
261
280
  const allStatements = [];
262
281
  let generatedTypes = new Set();
282
+ const customScalarTypes = collectCustomScalarTypes(typeRegistry);
263
283
  const enumResult = generateEnumTypes(typeRegistry, tableTypeNames);
264
284
  generatedTypes = new Set([...generatedTypes, ...enumResult.generatedTypes]);
265
285
  const unionResult = generateUnionTypes(typeRegistry, tableTypeNames, generatedTypes);
@@ -275,6 +295,7 @@ function generateSchemaTypesFile(options) {
275
295
  typesImport.importKind = 'type';
276
296
  allStatements.push(typesImport);
277
297
  }
298
+ allStatements.push(...generateCustomScalarTypes(customScalarTypes));
278
299
  allStatements.push(...enumResult.statements);
279
300
  allStatements.push(...unionResult.statements);
280
301
  allStatements.push(...inputResult.statements);
@@ -27,9 +27,20 @@ export interface ListSelectionConfig<
27
27
  offset?: number;
28
28
  }
29
29
 
30
+ function ensureSelectionFields(
31
+ selection: SelectionConfig<unknown> | undefined,
32
+ ): asserts selection is SelectionConfig<unknown> {
33
+ if (!selection || typeof selection !== 'object' || !('fields' in selection)) {
34
+ throw new Error(
35
+ 'Invalid hook params: `selection.fields` is required. Example: { selection: { fields: { id: true } } }',
36
+ );
37
+ }
38
+ }
39
+
30
40
  export function buildSelectionArgs<TFields>(
31
41
  selection: SelectionConfig<TFields>,
32
42
  ): { select: TFields } {
43
+ ensureSelectionFields(selection);
33
44
  return { select: selection.fields };
34
45
  }
35
46
 
@@ -45,6 +56,7 @@ export function buildListSelectionArgs<TFields, TWhere, TOrderBy>(
45
56
  before?: string;
46
57
  offset?: number;
47
58
  } {
59
+ ensureSelectionFields(selection);
48
60
  return {
49
61
  select: selection.fields,
50
62
  where: selection.where,
@@ -19,19 +19,20 @@ import type {
19
19
 
20
20
  import { GraphQLRequestError, OrmClient, QueryResult } from './client';
21
21
 
22
- export interface QueryBuilderConfig {
22
+ export interface QueryBuilderConfig<TResult> {
23
23
  client: OrmClient;
24
24
  operation: 'query' | 'mutation';
25
25
  operationName: string;
26
26
  fieldName: string;
27
27
  document: string;
28
28
  variables?: Record<string, unknown>;
29
+ transform?: (data: any) => TResult;
29
30
  }
30
31
 
31
32
  export class QueryBuilder<TResult> {
32
- private config: QueryBuilderConfig;
33
+ private config: QueryBuilderConfig<TResult>;
33
34
 
34
- constructor(config: QueryBuilderConfig) {
35
+ constructor(config: QueryBuilderConfig<TResult>) {
35
36
  this.config = config;
36
37
  }
37
38
 
@@ -40,10 +41,21 @@ export class QueryBuilder<TResult> {
40
41
  * Use result.ok to check success, or .unwrap() to throw on error
41
42
  */
42
43
  async execute(): Promise<QueryResult<TResult>> {
43
- return this.config.client.execute<TResult>(
44
+ const rawResult = await this.config.client.execute<any>(
44
45
  this.config.document,
45
46
  this.config.variables,
46
47
  );
48
+ if (!rawResult.ok) {
49
+ return rawResult;
50
+ }
51
+ if (!this.config.transform) {
52
+ return rawResult as unknown as QueryResult<TResult>;
53
+ }
54
+ return {
55
+ ok: true,
56
+ data: this.config.transform(rawResult.data),
57
+ errors: undefined,
58
+ };
47
59
  }
48
60
 
49
61
  /**
@@ -91,6 +103,10 @@ export class QueryBuilder<TResult> {
91
103
  }
92
104
  }
93
105
 
106
+ const OP_QUERY = 'query' as unknown as import('graphql').OperationTypeNode;
107
+ const OP_MUTATION = 'mutation' as unknown as import('graphql').OperationTypeNode;
108
+ const ENUM_VALUE_KIND = 'EnumValue' as unknown as EnumValueNode['kind'];
109
+
94
110
  // ============================================================================
95
111
  // Selection Builders
96
112
  // ============================================================================
@@ -128,48 +144,52 @@ export function buildSelections(
128
144
  connection?: boolean;
129
145
  };
130
146
 
131
- if (nested.select) {
132
- const relatedEntityType = entityConnections?.[key];
133
- const nestedSelections = buildSelections(
134
- nested.select,
135
- connectionFieldsMap,
136
- relatedEntityType,
147
+ if (!nested.select || typeof nested.select !== 'object') {
148
+ throw new Error(
149
+ `Invalid selection for field "${key}": nested selections must include a "select" object.`,
137
150
  );
138
- const isConnection =
139
- nested.connection === true ||
140
- nested.first !== undefined ||
141
- nested.filter !== undefined ||
142
- relatedEntityType !== undefined;
143
- const args = buildArgs([
144
- buildOptionalArg('first', nested.first),
145
- nested.filter
146
- ? t.argument({
147
- name: 'filter',
148
- value: buildValueAst(nested.filter),
149
- })
150
- : null,
151
- buildEnumListArg('orderBy', nested.orderBy),
152
- ]);
153
-
154
- if (isConnection) {
155
- fields.push(
156
- t.field({
157
- name: key,
158
- args,
159
- selectionSet: t.selectionSet({
160
- selections: buildConnectionSelections(nestedSelections),
161
- }),
162
- }),
163
- );
164
- } else {
165
- fields.push(
166
- t.field({
167
- name: key,
168
- args,
169
- selectionSet: t.selectionSet({ selections: nestedSelections }),
151
+ }
152
+
153
+ const relatedEntityType = entityConnections?.[key];
154
+ const nestedSelections = buildSelections(
155
+ nested.select,
156
+ connectionFieldsMap,
157
+ relatedEntityType,
158
+ );
159
+ const isConnection =
160
+ nested.connection === true ||
161
+ nested.first !== undefined ||
162
+ nested.filter !== undefined ||
163
+ relatedEntityType !== undefined;
164
+ const args = buildArgs([
165
+ buildOptionalArg('first', nested.first),
166
+ nested.filter
167
+ ? t.argument({
168
+ name: 'filter',
169
+ value: buildValueAst(nested.filter),
170
+ })
171
+ : null,
172
+ buildEnumListArg('orderBy', nested.orderBy),
173
+ ]);
174
+
175
+ if (isConnection) {
176
+ fields.push(
177
+ t.field({
178
+ name: key,
179
+ args,
180
+ selectionSet: t.selectionSet({
181
+ selections: buildConnectionSelections(nestedSelections),
170
182
  }),
171
- );
172
- }
183
+ }),
184
+ );
185
+ } else {
186
+ fields.push(
187
+ t.field({
188
+ name: key,
189
+ args,
190
+ selectionSet: t.selectionSet({ selections: nestedSelections }),
191
+ }),
192
+ );
173
193
  }
174
194
  }
175
195
  }
@@ -265,7 +285,7 @@ export function buildFindManyDocument<TSelect, TWhere>(
265
285
  const document = t.document({
266
286
  definitions: [
267
287
  t.operationDefinition({
268
- operation: 'query',
288
+ operation: OP_QUERY,
269
289
  name: operationName + 'Query',
270
290
  variableDefinitions: variableDefinitions.length
271
291
  ? variableDefinitions
@@ -330,7 +350,7 @@ export function buildFindFirstDocument<TSelect, TWhere>(
330
350
  const document = t.document({
331
351
  definitions: [
332
352
  t.operationDefinition({
333
- operation: 'query',
353
+ operation: OP_QUERY,
334
354
  name: operationName + 'Query',
335
355
  variableDefinitions,
336
356
  selectionSet: t.selectionSet({
@@ -405,6 +425,7 @@ export function buildUpdateDocument<
405
425
  where: TWhere,
406
426
  data: TData,
407
427
  inputTypeName: string,
428
+ patchFieldName: string,
408
429
  connectionFieldsMap?: Record<string, Record<string, string>>,
409
430
  ): { document: string; variables: Record<string, unknown> } {
410
431
  const selections = select
@@ -430,7 +451,7 @@ export function buildUpdateDocument<
430
451
  variables: {
431
452
  input: {
432
453
  id: where.id,
433
- patch: data,
454
+ [patchFieldName]: data,
434
455
  },
435
456
  },
436
457
  };
@@ -445,6 +466,7 @@ export function buildUpdateByPkDocument<TSelect, TData>(
445
466
  data: TData,
446
467
  inputTypeName: string,
447
468
  idFieldName: string,
469
+ patchFieldName: string,
448
470
  connectionFieldsMap?: Record<string, Record<string, string>>,
449
471
  ): { document: string; variables: Record<string, unknown> } {
450
472
  const selections = select
@@ -470,7 +492,7 @@ export function buildUpdateByPkDocument<TSelect, TData>(
470
492
  variables: {
471
493
  input: {
472
494
  [idFieldName]: id,
473
- patch: data,
495
+ [patchFieldName]: data,
474
496
  },
475
497
  },
476
498
  };
@@ -510,7 +532,7 @@ export function buildFindOneDocument<TSelect>(
510
532
  const document = t.document({
511
533
  definitions: [
512
534
  t.operationDefinition({
513
- operation: 'query',
535
+ operation: OP_QUERY,
514
536
  name: operationName + 'Query',
515
537
  variableDefinitions,
516
538
  selectionSet: t.selectionSet({
@@ -622,15 +644,12 @@ export function buildCustomDocument<TSelect, TArgs>(
622
644
  connectionFieldsMap?: Record<string, Record<string, string>>,
623
645
  entityType?: string,
624
646
  ): { document: string; variables: Record<string, unknown> } {
625
- let actualSelect = select;
647
+ let actualSelect: TSelect = select;
626
648
  let isConnection = false;
627
649
 
628
- if (select && typeof select === 'object' && 'select' in select) {
629
- const wrapper = select as { select?: TSelect; connection?: boolean };
630
- if (wrapper.select) {
631
- actualSelect = wrapper.select;
632
- isConnection = wrapper.connection === true;
633
- }
650
+ if (isCustomSelectionWrapper(select)) {
651
+ actualSelect = select.select as TSelect;
652
+ isConnection = select.connection === true;
634
653
  }
635
654
 
636
655
  const selections = actualSelect
@@ -661,7 +680,7 @@ export function buildCustomDocument<TSelect, TArgs>(
661
680
  const document = t.document({
662
681
  definitions: [
663
682
  t.operationDefinition({
664
- operation: operationType,
683
+ operation: operationType === 'mutation' ? OP_MUTATION : OP_QUERY,
665
684
  name: operationName,
666
685
  variableDefinitions: variableDefs.length ? variableDefs : undefined,
667
686
  selectionSet: t.selectionSet({
@@ -685,6 +704,31 @@ export function buildCustomDocument<TSelect, TArgs>(
685
704
  };
686
705
  }
687
706
 
707
+ function isCustomSelectionWrapper(
708
+ value: unknown,
709
+ ): value is { select: Record<string, unknown>; connection?: boolean } {
710
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
711
+ return false;
712
+ }
713
+
714
+ const record = value as Record<string, unknown>;
715
+ const keys = Object.keys(record);
716
+
717
+ if (!keys.includes('select') || !keys.includes('connection')) {
718
+ return false;
719
+ }
720
+
721
+ if (keys.some((key) => key !== 'select' && key !== 'connection')) {
722
+ return false;
723
+ }
724
+
725
+ return (
726
+ !!record.select &&
727
+ typeof record.select === 'object' &&
728
+ !Array.isArray(record.select)
729
+ );
730
+ }
731
+
688
732
  // ============================================================================
689
733
  // Helper Functions
690
734
  // ============================================================================
@@ -724,7 +768,7 @@ function buildEnumListArg(
724
768
 
725
769
  function buildEnumValue(value: string): EnumValueNode {
726
770
  return {
727
- kind: 'EnumValue',
771
+ kind: ENUM_VALUE_KIND,
728
772
  value,
729
773
  };
730
774
  }
@@ -770,7 +814,7 @@ function buildInputMutationDocument(config: InputMutationConfig): string {
770
814
  const document = t.document({
771
815
  definitions: [
772
816
  t.operationDefinition({
773
- operation: 'mutation',
817
+ operation: OP_MUTATION,
774
818
  name: config.operationName + 'Mutation',
775
819
  variableDefinitions: [
776
820
  t.variableDefinition({
@@ -61,44 +61,60 @@ export interface DeleteArgs<TWhere, TSelect = undefined> {
61
61
  select?: TSelect;
62
62
  }
63
63
 
64
+ type DepthLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
65
+ type DecrementDepth = {
66
+ 0: 0;
67
+ 1: 0;
68
+ 2: 1;
69
+ 3: 2;
70
+ 4: 3;
71
+ 5: 4;
72
+ 6: 5;
73
+ 7: 6;
74
+ 8: 7;
75
+ 9: 8;
76
+ 10: 9;
77
+ };
78
+
64
79
  /**
65
80
  * Recursively validates select objects, rejecting unknown keys.
66
81
  *
67
- * NOTE: This type is intentionally NOT used in generated parameter positions
68
- * (conditional types block IDE autocompletion). Parameters use `S` directly
69
- * with `S extends XxxSelect` constraints, which provides full
70
- * autocompletion via TypeScript's contextual typing.
71
- *
72
- * @example
73
- * // This will cause a type error because 'invalid' doesn't exist:
74
- * type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
75
- * // Result = never (causes assignment error)
76
- *
77
- * @example
78
- * // This works because all fields are valid:
79
- * type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
80
- * // Result = { id: true }
82
+ * NOTE: Depth is intentionally capped to avoid circular-instantiation issues
83
+ * in very large cyclic schemas.
81
84
  */
82
- export type DeepExact<T, Shape> = T extends Shape
83
- ? Exclude<keyof T, keyof Shape> extends never
84
- ? {
85
- [K in keyof T]: K extends keyof Shape
86
- ? T[K] extends { select: infer NS }
87
- ? Extract<Shape[K], { select?: unknown }> extends {
88
- select?: infer ShapeNS;
89
- }
90
- ? DeepExact<
91
- Omit<T[K], 'select'> & {
92
- select: DeepExact<NS, NonNullable<ShapeNS>>;
93
- },
94
- Extract<Shape[K], { select?: unknown }>
95
- >
96
- : never
97
- : T[K]
98
- : never;
99
- }
85
+ export type DeepExact<
86
+ T,
87
+ Shape,
88
+ Depth extends DepthLevel = 10,
89
+ > = Depth extends 0
90
+ ? T extends Shape
91
+ ? T
100
92
  : never
101
- : never;
93
+ : T extends Shape
94
+ ? Exclude<keyof T, keyof Shape> extends never
95
+ ? {
96
+ [K in keyof T]: K extends keyof Shape
97
+ ? T[K] extends { select: infer NS }
98
+ ? Extract<Shape[K], { select?: unknown }> extends {
99
+ select?: infer ShapeNS;
100
+ }
101
+ ? DeepExact<
102
+ Omit<T[K], 'select'> & {
103
+ select: DeepExact<
104
+ NS,
105
+ NonNullable<ShapeNS>,
106
+ DecrementDepth[Depth]
107
+ >;
108
+ },
109
+ Extract<Shape[K], { select?: unknown }>,
110
+ DecrementDepth[Depth]
111
+ >
112
+ : never
113
+ : T[K]
114
+ : never;
115
+ }
116
+ : never
117
+ : never;
102
118
 
103
119
  /**
104
120
  * Enforces exact select shape while keeping contextual typing on `S extends XxxSelect`.
@@ -107,6 +123,16 @@ export type DeepExact<T, Shape> = T extends Shape
107
123
  */
108
124
  export type StrictSelect<S, Shape> = S extends DeepExact<S, Shape> ? {} : never;
109
125
 
126
+ /**
127
+ * Hook-optimized strict select variant.
128
+ *
129
+ * Uses a shallower recursion depth to keep editor autocomplete responsive
130
+ * in large schemas while still validating common nested-select mistakes.
131
+ */
132
+ export type HookStrictSelect<S, Shape> = S extends DeepExact<S, Shape, 5>
133
+ ? {}
134
+ : never;
135
+
110
136
  /**
111
137
  * Infer result type from select configuration
112
138
  */