@constructive-io/graphql-codegen 3.3.1 → 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.
- package/README.md +0 -4
- package/core/ast.js +6 -5
- package/core/codegen/custom-mutations.js +22 -22
- package/core/codegen/custom-queries.js +24 -17
- package/core/codegen/hooks-ast.d.ts +1 -1
- package/core/codegen/hooks-ast.js +22 -5
- package/core/codegen/index.d.ts +1 -1
- package/core/codegen/mutations.js +16 -12
- package/core/codegen/orm/custom-ops-generator.js +37 -3
- package/core/codegen/orm/input-types-generator.js +161 -89
- package/core/codegen/orm/model-generator.js +72 -73
- package/core/codegen/orm/select-types.d.ts +27 -17
- package/core/codegen/queries.js +37 -25
- package/core/codegen/schema-types-generator.js +21 -0
- package/core/codegen/templates/hooks-selection.ts +12 -0
- package/core/codegen/templates/query-builder.ts +103 -59
- package/core/codegen/templates/select-types.ts +59 -33
- package/core/codegen/types.js +26 -0
- package/core/codegen/utils.d.ts +1 -1
- package/core/codegen/utils.js +1 -1
- package/core/custom-ast.js +9 -8
- package/core/database/index.js +2 -3
- package/core/index.d.ts +2 -0
- package/core/index.js +2 -0
- package/core/introspect/infer-tables.js +144 -58
- package/core/introspect/transform-schema.d.ts +1 -1
- package/core/introspect/transform-schema.js +3 -1
- package/esm/core/ast.js +6 -5
- package/esm/core/codegen/custom-mutations.js +23 -23
- package/esm/core/codegen/custom-queries.js +25 -18
- package/esm/core/codegen/hooks-ast.d.ts +1 -1
- package/esm/core/codegen/hooks-ast.js +22 -5
- package/esm/core/codegen/index.d.ts +1 -1
- package/esm/core/codegen/mutations.js +16 -12
- package/esm/core/codegen/orm/custom-ops-generator.js +38 -4
- package/esm/core/codegen/orm/input-types-generator.js +163 -91
- package/esm/core/codegen/orm/model-generator.js +73 -74
- package/esm/core/codegen/orm/select-types.d.ts +27 -17
- package/esm/core/codegen/queries.js +37 -25
- package/esm/core/codegen/schema-types-generator.js +21 -0
- package/esm/core/codegen/types.js +26 -0
- package/esm/core/codegen/utils.d.ts +1 -1
- package/esm/core/codegen/utils.js +1 -1
- package/esm/core/custom-ast.js +9 -8
- package/esm/core/database/index.js +2 -3
- package/esm/core/index.d.ts +2 -0
- package/esm/core/index.js +2 -0
- package/esm/core/introspect/infer-tables.js +144 -58
- package/esm/core/introspect/transform-schema.d.ts +1 -1
- package/esm/core/introspect/transform-schema.js +3 -1
- package/esm/generators/field-selector.js +1 -0
- package/esm/generators/index.d.ts +3 -0
- package/esm/generators/index.js +3 -0
- package/esm/generators/mutations.js +4 -4
- package/esm/generators/select.js +4 -4
- package/esm/index.d.ts +1 -1
- package/esm/index.js +1 -1
- package/esm/types/schema.d.ts +5 -3
- package/generators/field-selector.js +1 -0
- package/generators/index.d.ts +3 -0
- package/generators/index.js +3 -0
- package/generators/mutations.js +3 -3
- package/generators/select.js +3 -3
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/package.json +11 -11
- 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:
|
|
51
|
-
*
|
|
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
|
-
}
|
|
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 {};
|
package/core/codegen/queries.js
CHANGED
|
@@ -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', [
|
|
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.
|
|
197
|
-
t.
|
|
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
|
|
228
|
-
t.
|
|
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
|
-
|
|
241
|
+
p1BaseParamType,
|
|
233
242
|
(0, hooks_ast_1.scopeTypeLiteral)(scopeTypeName),
|
|
234
243
|
])
|
|
235
|
-
:
|
|
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', '
|
|
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(
|
|
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
|
|
435
|
-
t.
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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
|
|
466
|
-
t.
|
|
467
|
-
|
|
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
|
-
|
|
483
|
+
p1BaseParamType,
|
|
472
484
|
(0, hooks_ast_1.scopeTypeLiteral)(scopeTypeName),
|
|
473
485
|
])
|
|
474
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
629
|
-
|
|
630
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
68
|
-
*
|
|
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<
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
:
|
|
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
|
*/
|