@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.
- package/README.md +403 -279
- package/cli/codegen/babel-ast.d.ts +7 -0
- package/cli/codegen/babel-ast.js +15 -0
- package/cli/codegen/barrel.js +43 -14
- package/cli/codegen/custom-mutations.js +4 -4
- package/cli/codegen/custom-queries.js +12 -22
- package/cli/codegen/gql-ast.js +22 -1
- package/cli/codegen/index.js +1 -0
- package/cli/codegen/mutations.d.ts +2 -0
- package/cli/codegen/mutations.js +26 -13
- package/cli/codegen/orm/client-generator.js +475 -136
- package/cli/codegen/orm/custom-ops-generator.js +8 -3
- package/cli/codegen/orm/input-types-generator.js +22 -0
- package/cli/codegen/orm/model-generator.js +18 -5
- package/cli/codegen/orm/select-types.d.ts +33 -0
- package/cli/codegen/queries.d.ts +1 -1
- package/cli/codegen/queries.js +112 -35
- package/cli/codegen/utils.d.ts +6 -0
- package/cli/codegen/utils.js +19 -0
- package/cli/commands/generate-orm.d.ts +14 -0
- package/cli/commands/generate-orm.js +160 -44
- package/cli/commands/generate.d.ts +22 -0
- package/cli/commands/generate.js +195 -55
- package/cli/commands/init.js +29 -9
- package/cli/index.js +133 -28
- package/cli/watch/orchestrator.d.ts +4 -0
- package/cli/watch/orchestrator.js +4 -0
- package/esm/cli/codegen/babel-ast.d.ts +7 -0
- package/esm/cli/codegen/babel-ast.js +14 -0
- package/esm/cli/codegen/barrel.js +44 -15
- package/esm/cli/codegen/custom-mutations.js +5 -5
- package/esm/cli/codegen/custom-queries.js +13 -23
- package/esm/cli/codegen/gql-ast.js +23 -2
- package/esm/cli/codegen/index.js +1 -0
- package/esm/cli/codegen/mutations.d.ts +2 -0
- package/esm/cli/codegen/mutations.js +27 -14
- package/esm/cli/codegen/orm/client-generator.js +475 -136
- package/esm/cli/codegen/orm/custom-ops-generator.js +8 -3
- package/esm/cli/codegen/orm/input-types-generator.js +22 -0
- package/esm/cli/codegen/orm/model-generator.js +18 -5
- package/esm/cli/codegen/orm/select-types.d.ts +33 -0
- package/esm/cli/codegen/queries.d.ts +1 -1
- package/esm/cli/codegen/queries.js +114 -37
- package/esm/cli/codegen/utils.d.ts +6 -0
- package/esm/cli/codegen/utils.js +18 -0
- package/esm/cli/commands/generate-orm.d.ts +14 -0
- package/esm/cli/commands/generate-orm.js +161 -45
- package/esm/cli/commands/generate.d.ts +22 -0
- package/esm/cli/commands/generate.js +195 -56
- package/esm/cli/commands/init.js +29 -9
- package/esm/cli/index.js +134 -29
- package/esm/cli/watch/orchestrator.d.ts +4 -0
- package/esm/cli/watch/orchestrator.js +5 -1
- package/esm/types/config.d.ts +39 -2
- package/esm/types/config.js +88 -4
- package/esm/types/index.d.ts +2 -2
- package/esm/types/index.js +1 -1
- package/package.json +10 -7
- package/types/config.d.ts +39 -2
- package/types/config.js +91 -4
- package/types/index.d.ts +2 -2
- package/types/index.js +2 -1
- package/cli/codegen/orm/query-builder.d.ts +0 -161
- package/cli/codegen/orm/query-builder.js +0 -366
- package/esm/cli/codegen/orm/query-builder.d.ts +0 -161
- package/esm/cli/codegen/orm/query-builder.js +0 -353
|
@@ -122,9 +122,14 @@ function buildOperationMethod(op, operationType) {
|
|
|
122
122
|
const optionsParam = t.identifier('options');
|
|
123
123
|
optionsParam.optional = true;
|
|
124
124
|
if (selectTypeName) {
|
|
125
|
+
// Use DeepExact<S, SelectType> to enforce strict field validation
|
|
126
|
+
// This catches invalid fields even when mixed with valid ones
|
|
125
127
|
optionsParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeLiteral([
|
|
126
128
|
(() => {
|
|
127
|
-
const prop = t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier('
|
|
129
|
+
const prop = t.tsPropertySignature(t.identifier('select'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
|
|
130
|
+
t.tsTypeReference(t.identifier('S')),
|
|
131
|
+
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
132
|
+
]))));
|
|
128
133
|
prop.optional = true;
|
|
129
134
|
return prop;
|
|
130
135
|
})(),
|
|
@@ -192,7 +197,7 @@ export function generateCustomQueryOpsFile(operations) {
|
|
|
192
197
|
// Add imports
|
|
193
198
|
statements.push(createImportDeclaration('../client', ['OrmClient']));
|
|
194
199
|
statements.push(createImportDeclaration('../query-builder', ['QueryBuilder', 'buildCustomDocument']));
|
|
195
|
-
statements.push(createImportDeclaration('../select-types', ['InferSelectResult'], true));
|
|
200
|
+
statements.push(createImportDeclaration('../select-types', ['InferSelectResult', 'DeepExact'], true));
|
|
196
201
|
if (allTypeImports.length > 0) {
|
|
197
202
|
statements.push(createImportDeclaration('../input-types', allTypeImports, true));
|
|
198
203
|
}
|
|
@@ -230,7 +235,7 @@ export function generateCustomMutationOpsFile(operations) {
|
|
|
230
235
|
// Add imports
|
|
231
236
|
statements.push(createImportDeclaration('../client', ['OrmClient']));
|
|
232
237
|
statements.push(createImportDeclaration('../query-builder', ['QueryBuilder', 'buildCustomDocument']));
|
|
233
|
-
statements.push(createImportDeclaration('../select-types', ['InferSelectResult'], true));
|
|
238
|
+
statements.push(createImportDeclaration('../select-types', ['InferSelectResult', 'DeepExact'], true));
|
|
234
239
|
if (allTypeImports.length > 0) {
|
|
235
240
|
statements.push(createImportDeclaration('../input-types', allTypeImports, true));
|
|
236
241
|
}
|
|
@@ -193,6 +193,22 @@ const SCALAR_FILTER_CONFIGS = [
|
|
|
193
193
|
operators: ['equality', 'distinct', 'inArray', 'comparison', 'inet'],
|
|
194
194
|
},
|
|
195
195
|
{ name: 'FullTextFilter', tsType: 'string', operators: ['fulltext'] },
|
|
196
|
+
// List filters (for array fields like string[], int[], uuid[])
|
|
197
|
+
{
|
|
198
|
+
name: 'StringListFilter',
|
|
199
|
+
tsType: 'string[]',
|
|
200
|
+
operators: ['equality', 'distinct', 'comparison', 'listArray'],
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'IntListFilter',
|
|
204
|
+
tsType: 'number[]',
|
|
205
|
+
operators: ['equality', 'distinct', 'comparison', 'listArray'],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'UUIDListFilter',
|
|
209
|
+
tsType: 'string[]',
|
|
210
|
+
operators: ['equality', 'distinct', 'comparison', 'listArray'],
|
|
211
|
+
},
|
|
196
212
|
];
|
|
197
213
|
/**
|
|
198
214
|
* Build filter properties based on operator sets
|
|
@@ -232,6 +248,12 @@ function buildScalarFilterProperties(config) {
|
|
|
232
248
|
if (operators.includes('fulltext')) {
|
|
233
249
|
props.push({ name: 'matches', type: 'string', optional: true });
|
|
234
250
|
}
|
|
251
|
+
// List/Array operators (contains, overlaps, anyEqualTo, etc.)
|
|
252
|
+
if (operators.includes('listArray')) {
|
|
253
|
+
// Extract base type from array type (e.g., 'string[]' -> 'string')
|
|
254
|
+
const baseType = tsType.replace('[]', '');
|
|
255
|
+
props.push({ name: 'contains', type: tsType, optional: true }, { name: 'containedBy', type: tsType, optional: true }, { name: 'overlaps', type: tsType, optional: true }, { name: 'anyEqualTo', type: baseType, optional: true }, { name: 'anyNotEqualTo', type: baseType, optional: true }, { name: 'anyLessThan', type: baseType, optional: true }, { name: 'anyLessThanOrEqualTo', type: baseType, optional: true }, { name: 'anyGreaterThan', type: baseType, optional: true }, { name: 'anyGreaterThanOrEqualTo', type: baseType, optional: true });
|
|
256
|
+
}
|
|
235
257
|
return props;
|
|
236
258
|
}
|
|
237
259
|
/**
|
|
@@ -63,7 +63,7 @@ export function generateModelFile(table, _useSharedTypes) {
|
|
|
63
63
|
]));
|
|
64
64
|
statements.push(createImportDeclaration('../select-types', [
|
|
65
65
|
'ConnectionResult', 'FindManyArgs', 'FindFirstArgs', 'CreateArgs',
|
|
66
|
-
'UpdateArgs', 'DeleteArgs', 'InferSelectResult',
|
|
66
|
+
'UpdateArgs', 'DeleteArgs', 'InferSelectResult', 'DeepExact',
|
|
67
67
|
], true));
|
|
68
68
|
statements.push(createImportDeclaration('../input-types', [
|
|
69
69
|
typeName, relationTypeName, selectTypeName, whereTypeName, orderByTypeName,
|
|
@@ -77,10 +77,14 @@ export function generateModelFile(table, _useSharedTypes) {
|
|
|
77
77
|
paramProp.accessibility = 'private';
|
|
78
78
|
classBody.push(t.classMethod('constructor', t.identifier('constructor'), [paramProp], t.blockStatement([])));
|
|
79
79
|
// findMany method
|
|
80
|
+
// Use DeepExact<S, SelectType> to enforce strict field validation
|
|
80
81
|
const findManyParam = t.identifier('args');
|
|
81
82
|
findManyParam.optional = true;
|
|
82
83
|
findManyParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FindManyArgs'), t.tsTypeParameterInstantiation([
|
|
83
|
-
t.tsTypeReference(t.identifier('
|
|
84
|
+
t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
|
|
85
|
+
t.tsTypeReference(t.identifier('S')),
|
|
86
|
+
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
87
|
+
])),
|
|
84
88
|
t.tsTypeReference(t.identifier(whereTypeName)),
|
|
85
89
|
t.tsTypeReference(t.identifier(orderByTypeName)),
|
|
86
90
|
])));
|
|
@@ -115,7 +119,10 @@ export function generateModelFile(table, _useSharedTypes) {
|
|
|
115
119
|
const findFirstParam = t.identifier('args');
|
|
116
120
|
findFirstParam.optional = true;
|
|
117
121
|
findFirstParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('FindFirstArgs'), t.tsTypeParameterInstantiation([
|
|
118
|
-
t.tsTypeReference(t.identifier('
|
|
122
|
+
t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
|
|
123
|
+
t.tsTypeReference(t.identifier('S')),
|
|
124
|
+
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
125
|
+
])),
|
|
119
126
|
t.tsTypeReference(t.identifier(whereTypeName)),
|
|
120
127
|
])));
|
|
121
128
|
const findFirstReturnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
|
|
@@ -141,7 +148,10 @@ export function generateModelFile(table, _useSharedTypes) {
|
|
|
141
148
|
// create method
|
|
142
149
|
const createParam = t.identifier('args');
|
|
143
150
|
createParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('CreateArgs'), t.tsTypeParameterInstantiation([
|
|
144
|
-
t.tsTypeReference(t.identifier('
|
|
151
|
+
t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
|
|
152
|
+
t.tsTypeReference(t.identifier('S')),
|
|
153
|
+
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
154
|
+
])),
|
|
145
155
|
t.tsIndexedAccessType(t.tsTypeReference(t.identifier(createInputTypeName)), t.tsLiteralType(t.stringLiteral(singularName))),
|
|
146
156
|
])));
|
|
147
157
|
const createReturnType = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
|
|
@@ -167,7 +177,10 @@ export function generateModelFile(table, _useSharedTypes) {
|
|
|
167
177
|
if (updateMutationName) {
|
|
168
178
|
const updateParam = t.identifier('args');
|
|
169
179
|
updateParam.typeAnnotation = t.tsTypeAnnotation(t.tsTypeReference(t.identifier('UpdateArgs'), t.tsTypeParameterInstantiation([
|
|
170
|
-
t.tsTypeReference(t.identifier('
|
|
180
|
+
t.tsTypeReference(t.identifier('DeepExact'), t.tsTypeParameterInstantiation([
|
|
181
|
+
t.tsTypeReference(t.identifier('S')),
|
|
182
|
+
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
183
|
+
])),
|
|
171
184
|
t.tsTypeLiteral([t.tsPropertySignature(t.identifier('id'), t.tsTypeAnnotation(t.tsStringKeyword()))]),
|
|
172
185
|
t.tsTypeReference(t.identifier(patchTypeName)),
|
|
173
186
|
])));
|
|
@@ -44,6 +44,39 @@ export interface NestedSelectConfig {
|
|
|
44
44
|
filter?: Record<string, unknown>;
|
|
45
45
|
orderBy?: string[];
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Recursively validates select objects, rejecting unknown keys.
|
|
49
|
+
*
|
|
50
|
+
* This type ensures that users can only select fields that actually exist
|
|
51
|
+
* in the GraphQL schema. It returns `never` if any excess keys are found
|
|
52
|
+
* at any nesting level, causing a TypeScript compile error.
|
|
53
|
+
*
|
|
54
|
+
* Why this is needed:
|
|
55
|
+
* TypeScript's excess property checking has a quirk where it only catches
|
|
56
|
+
* invalid fields when they are the ONLY fields. When mixed with valid fields
|
|
57
|
+
* (e.g., `{ id: true, invalidField: true }`), the structural typing allows
|
|
58
|
+
* the excess property through. This type explicitly checks for and rejects
|
|
59
|
+
* such cases.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* // This will cause a type error because 'invalid' doesn't exist:
|
|
63
|
+
* type Result = DeepExact<{ id: true, invalid: true }, { id?: boolean }>;
|
|
64
|
+
* // Result = never (causes assignment error)
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* // This works because all fields are valid:
|
|
68
|
+
* type Result = DeepExact<{ id: true }, { id?: boolean; name?: boolean }>;
|
|
69
|
+
* // Result = { id: true }
|
|
70
|
+
*/
|
|
71
|
+
export type DeepExact<T, Shape> = T extends Shape ? Exclude<keyof T, keyof Shape> extends never ? {
|
|
72
|
+
[K in keyof T]: K extends keyof Shape ? T[K] extends {
|
|
73
|
+
select: infer NS;
|
|
74
|
+
} ? Shape[K] extends {
|
|
75
|
+
select?: infer ShapeNS;
|
|
76
|
+
} ? {
|
|
77
|
+
select: DeepExact<NS, NonNullable<ShapeNS>>;
|
|
78
|
+
} : T[K] : T[K] : never;
|
|
79
|
+
} : never : never;
|
|
47
80
|
/**
|
|
48
81
|
* Infers the result type from a select configuration
|
|
49
82
|
*
|
|
@@ -17,5 +17,5 @@ export interface QueryGeneratorOptions {
|
|
|
17
17
|
hasRelationships?: boolean;
|
|
18
18
|
}
|
|
19
19
|
export declare function generateListQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile;
|
|
20
|
-
export declare function generateSingleQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile;
|
|
20
|
+
export declare function generateSingleQueryHook(table: CleanTable, options?: QueryGeneratorOptions): GeneratedQueryFile | null;
|
|
21
21
|
export declare function generateAllQueryHooks(tables: CleanTable[], options?: QueryGeneratorOptions): GeneratedQueryFile[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as t from '@babel/types';
|
|
2
|
-
import { generateCode, addJSDocComment, typedParam } from './babel-ast';
|
|
2
|
+
import { generateCode, addJSDocComment, typedParam, createTypedCallExpression } from './babel-ast';
|
|
3
3
|
import { buildListQueryAST, buildSingleQueryAST, printGraphQL, } from './gql-ast';
|
|
4
|
-
import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, toScreamingSnake, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
|
|
4
|
+
import { getTableNames, getListQueryHookName, getSingleQueryHookName, getListQueryFileName, getSingleQueryFileName, getAllRowsQueryName, getSingleRowQueryName, getFilterTypeName, getConditionTypeName, getOrderByTypeName, getScalarFields, getScalarFilterType, getPrimaryKeyInfo, hasValidPrimaryKey, fieldTypeToTs, toScreamingSnake, ucFirst, lcFirst, getGeneratedFileHeader, } from './utils';
|
|
5
5
|
function createUnionType(values) {
|
|
6
6
|
return t.tsUnionType(values.map((v) => t.tsLiteralType(t.stringLiteral(v))));
|
|
7
7
|
}
|
|
@@ -34,6 +34,7 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
34
34
|
const hookName = getListQueryHookName(table);
|
|
35
35
|
const queryName = getAllRowsQueryName(table);
|
|
36
36
|
const filterTypeName = getFilterTypeName(table);
|
|
37
|
+
const conditionTypeName = getConditionTypeName(table);
|
|
37
38
|
const orderByTypeName = getOrderByTypeName(table);
|
|
38
39
|
const scalarFields = getScalarFields(table);
|
|
39
40
|
const keysName = `${lcFirst(typeName)}Keys`;
|
|
@@ -98,6 +99,59 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
98
99
|
})
|
|
99
100
|
.filter((f) => f !== null);
|
|
100
101
|
statements.push(createFilterInterfaceDeclaration(filterTypeName, fieldFilters, false));
|
|
102
|
+
// Generate Condition interface (simple equality filter with scalar types)
|
|
103
|
+
// Track non-primitive types (enums) that need to be imported
|
|
104
|
+
const enumTypesUsed = new Set();
|
|
105
|
+
const conditionProperties = scalarFields.map((field) => {
|
|
106
|
+
const tsType = fieldTypeToTs(field.type);
|
|
107
|
+
const isPrimitive = tsType === 'string' ||
|
|
108
|
+
tsType === 'number' ||
|
|
109
|
+
tsType === 'boolean' ||
|
|
110
|
+
tsType === 'unknown' ||
|
|
111
|
+
tsType.endsWith('[]');
|
|
112
|
+
let typeAnnotation;
|
|
113
|
+
if (field.type.isArray) {
|
|
114
|
+
const baseType = tsType.replace('[]', '');
|
|
115
|
+
const isBasePrimitive = baseType === 'string' ||
|
|
116
|
+
baseType === 'number' ||
|
|
117
|
+
baseType === 'boolean' ||
|
|
118
|
+
baseType === 'unknown';
|
|
119
|
+
if (!isBasePrimitive) {
|
|
120
|
+
enumTypesUsed.add(baseType);
|
|
121
|
+
}
|
|
122
|
+
typeAnnotation = t.tsArrayType(baseType === 'string'
|
|
123
|
+
? t.tsStringKeyword()
|
|
124
|
+
: baseType === 'number'
|
|
125
|
+
? t.tsNumberKeyword()
|
|
126
|
+
: baseType === 'boolean'
|
|
127
|
+
? t.tsBooleanKeyword()
|
|
128
|
+
: t.tsTypeReference(t.identifier(baseType)));
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
if (!isPrimitive) {
|
|
132
|
+
enumTypesUsed.add(tsType);
|
|
133
|
+
}
|
|
134
|
+
typeAnnotation =
|
|
135
|
+
tsType === 'string'
|
|
136
|
+
? t.tsStringKeyword()
|
|
137
|
+
: tsType === 'number'
|
|
138
|
+
? t.tsNumberKeyword()
|
|
139
|
+
: tsType === 'boolean'
|
|
140
|
+
? t.tsBooleanKeyword()
|
|
141
|
+
: t.tsTypeReference(t.identifier(tsType));
|
|
142
|
+
}
|
|
143
|
+
const prop = t.tsPropertySignature(t.identifier(field.name), t.tsTypeAnnotation(typeAnnotation));
|
|
144
|
+
prop.optional = true;
|
|
145
|
+
return prop;
|
|
146
|
+
});
|
|
147
|
+
// Add import for enum types if any are used
|
|
148
|
+
if (enumTypesUsed.size > 0) {
|
|
149
|
+
const schemaTypesImport = t.importDeclaration(Array.from(enumTypesUsed).map((et) => t.importSpecifier(t.identifier(et), t.identifier(et))), t.stringLiteral('../schema-types'));
|
|
150
|
+
schemaTypesImport.importKind = 'type';
|
|
151
|
+
statements.push(schemaTypesImport);
|
|
152
|
+
}
|
|
153
|
+
const conditionInterface = t.tsInterfaceDeclaration(t.identifier(conditionTypeName), null, null, t.tsInterfaceBody(conditionProperties));
|
|
154
|
+
statements.push(conditionInterface);
|
|
101
155
|
const orderByValues = [
|
|
102
156
|
...scalarFields.flatMap((f) => [
|
|
103
157
|
`${toScreamingSnake(f.name)}_ASC`,
|
|
@@ -115,16 +169,36 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
115
169
|
p.optional = true;
|
|
116
170
|
return p;
|
|
117
171
|
})(),
|
|
172
|
+
(() => {
|
|
173
|
+
const p = t.tsPropertySignature(t.identifier('last'), t.tsTypeAnnotation(t.tsNumberKeyword()));
|
|
174
|
+
p.optional = true;
|
|
175
|
+
return p;
|
|
176
|
+
})(),
|
|
118
177
|
(() => {
|
|
119
178
|
const p = t.tsPropertySignature(t.identifier('offset'), t.tsTypeAnnotation(t.tsNumberKeyword()));
|
|
120
179
|
p.optional = true;
|
|
121
180
|
return p;
|
|
122
181
|
})(),
|
|
182
|
+
(() => {
|
|
183
|
+
const p = t.tsPropertySignature(t.identifier('before'), t.tsTypeAnnotation(t.tsStringKeyword()));
|
|
184
|
+
p.optional = true;
|
|
185
|
+
return p;
|
|
186
|
+
})(),
|
|
187
|
+
(() => {
|
|
188
|
+
const p = t.tsPropertySignature(t.identifier('after'), t.tsTypeAnnotation(t.tsStringKeyword()));
|
|
189
|
+
p.optional = true;
|
|
190
|
+
return p;
|
|
191
|
+
})(),
|
|
123
192
|
(() => {
|
|
124
193
|
const p = t.tsPropertySignature(t.identifier('filter'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(filterTypeName))));
|
|
125
194
|
p.optional = true;
|
|
126
195
|
return p;
|
|
127
196
|
})(),
|
|
197
|
+
(() => {
|
|
198
|
+
const p = t.tsPropertySignature(t.identifier('condition'), t.tsTypeAnnotation(t.tsTypeReference(t.identifier(conditionTypeName))));
|
|
199
|
+
p.optional = true;
|
|
200
|
+
return p;
|
|
201
|
+
})(),
|
|
128
202
|
(() => {
|
|
129
203
|
const p = t.tsPropertySignature(t.identifier('orderBy'), t.tsTypeAnnotation(t.tsArrayType(t.tsTypeReference(t.identifier(orderByTypeName)))));
|
|
130
204
|
p.optional = true;
|
|
@@ -184,9 +258,9 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
184
258
|
hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
|
|
185
259
|
t.objectExpression([
|
|
186
260
|
t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables'), t.identifier('scope')])),
|
|
187
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
188
|
-
t.identifier(`${
|
|
189
|
-
t.identifier(
|
|
261
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
262
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
|
|
263
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
|
|
190
264
|
]))),
|
|
191
265
|
t.spreadElement(t.identifier('queryOptions')),
|
|
192
266
|
]),
|
|
@@ -196,9 +270,9 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
196
270
|
hookBodyStatements.push(t.returnStatement(t.callExpression(t.identifier('useQuery'), [
|
|
197
271
|
t.objectExpression([
|
|
198
272
|
t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('list')), [t.identifier('variables')])),
|
|
199
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
200
|
-
t.identifier(`${
|
|
201
|
-
t.identifier(
|
|
273
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
274
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
|
|
275
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
|
|
202
276
|
]))),
|
|
203
277
|
t.spreadElement(t.identifier('options')),
|
|
204
278
|
]),
|
|
@@ -210,9 +284,9 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
210
284
|
t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
|
|
211
285
|
t.identifier('variables'),
|
|
212
286
|
])),
|
|
213
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
214
|
-
t.identifier(`${
|
|
215
|
-
t.identifier(
|
|
287
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
288
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
|
|
289
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
|
|
216
290
|
]))),
|
|
217
291
|
t.spreadElement(t.identifier('options')),
|
|
218
292
|
]),
|
|
@@ -260,10 +334,9 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
260
334
|
statements.push(hookExport);
|
|
261
335
|
}
|
|
262
336
|
const fetchFuncBody = t.blockStatement([
|
|
263
|
-
t.returnStatement(t.
|
|
264
|
-
t.identifier(`${
|
|
265
|
-
t.identifier(
|
|
266
|
-
t.identifier('options'),
|
|
337
|
+
t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
|
|
338
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
|
|
339
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
|
|
267
340
|
])),
|
|
268
341
|
]);
|
|
269
342
|
const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(pluralName)}Query`), [
|
|
@@ -314,10 +387,9 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
314
387
|
t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
|
|
315
388
|
t.objectExpression([
|
|
316
389
|
t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
|
|
317
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
318
|
-
t.identifier(`${
|
|
319
|
-
t.identifier(
|
|
320
|
-
t.identifier('options'),
|
|
390
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
|
|
391
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryResult`)),
|
|
392
|
+
t.tsTypeReference(t.identifier(`${ucFirst(pluralName)}QueryVariables`)),
|
|
321
393
|
]))),
|
|
322
394
|
]),
|
|
323
395
|
]))),
|
|
@@ -347,6 +419,10 @@ export function generateListQueryHook(table, options = {}) {
|
|
|
347
419
|
};
|
|
348
420
|
}
|
|
349
421
|
export function generateSingleQueryHook(table, options = {}) {
|
|
422
|
+
// Skip tables with composite keys - they are handled as custom queries
|
|
423
|
+
if (!hasValidPrimaryKey(table)) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
350
426
|
const { reactQueryEnabled = true, useCentralizedKeys = true, hasRelationships = false, } = options;
|
|
351
427
|
const { typeName, singularName } = getTableNames(table);
|
|
352
428
|
const hookName = getSingleQueryHookName(table);
|
|
@@ -454,9 +530,9 @@ export function generateSingleQueryHook(table, options = {}) {
|
|
|
454
530
|
t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
|
|
455
531
|
t.identifier('scope'),
|
|
456
532
|
])),
|
|
457
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
458
|
-
t.identifier(`${
|
|
459
|
-
t.identifier(
|
|
533
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
534
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
|
|
535
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
|
|
460
536
|
]))),
|
|
461
537
|
t.spreadElement(t.identifier('queryOptions')),
|
|
462
538
|
]),
|
|
@@ -468,9 +544,9 @@ export function generateSingleQueryHook(table, options = {}) {
|
|
|
468
544
|
t.objectProperty(t.identifier('queryKey'), t.callExpression(t.memberExpression(t.identifier(keysName), t.identifier('detail')), [
|
|
469
545
|
t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
|
|
470
546
|
])),
|
|
471
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
472
|
-
t.identifier(`${
|
|
473
|
-
t.identifier(
|
|
547
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
548
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
|
|
549
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
|
|
474
550
|
]))),
|
|
475
551
|
t.spreadElement(t.identifier('options')),
|
|
476
552
|
]),
|
|
@@ -482,9 +558,9 @@ export function generateSingleQueryHook(table, options = {}) {
|
|
|
482
558
|
t.objectProperty(t.identifier('queryKey'), t.callExpression(t.identifier(`${queryName}QueryKey`), [
|
|
483
559
|
t.memberExpression(t.identifier('variables'), t.identifier(pkName)),
|
|
484
560
|
])),
|
|
485
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
486
|
-
t.identifier(`${
|
|
487
|
-
t.identifier(
|
|
561
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables')], [
|
|
562
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
|
|
563
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
|
|
488
564
|
]))),
|
|
489
565
|
t.spreadElement(t.identifier('options')),
|
|
490
566
|
]),
|
|
@@ -528,10 +604,9 @@ export function generateSingleQueryHook(table, options = {}) {
|
|
|
528
604
|
statements.push(hookExport);
|
|
529
605
|
}
|
|
530
606
|
const fetchFuncBody = t.blockStatement([
|
|
531
|
-
t.returnStatement(t.
|
|
532
|
-
t.identifier(`${
|
|
533
|
-
t.identifier(
|
|
534
|
-
t.identifier('options'),
|
|
607
|
+
t.returnStatement(createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
|
|
608
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
|
|
609
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
|
|
535
610
|
])),
|
|
536
611
|
]);
|
|
537
612
|
const fetchFunc = t.functionDeclaration(t.identifier(`fetch${ucFirst(singularName)}Query`), [
|
|
@@ -578,10 +653,9 @@ export function generateSingleQueryHook(table, options = {}) {
|
|
|
578
653
|
t.expressionStatement(t.awaitExpression(t.callExpression(t.memberExpression(t.identifier('queryClient'), t.identifier('prefetchQuery')), [
|
|
579
654
|
t.objectExpression([
|
|
580
655
|
t.objectProperty(t.identifier('queryKey'), prefetchQueryKeyExpr),
|
|
581
|
-
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], t.
|
|
582
|
-
t.identifier(`${
|
|
583
|
-
t.identifier(
|
|
584
|
-
t.identifier('options'),
|
|
656
|
+
t.objectProperty(t.identifier('queryFn'), t.arrowFunctionExpression([], createTypedCallExpression(t.identifier('execute'), [t.identifier(`${queryName}QueryDocument`), t.identifier('variables'), t.identifier('options')], [
|
|
657
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryResult`)),
|
|
658
|
+
t.tsTypeReference(t.identifier(`${ucFirst(singularName)}QueryVariables`)),
|
|
585
659
|
]))),
|
|
586
660
|
]),
|
|
587
661
|
]))),
|
|
@@ -614,7 +688,10 @@ export function generateAllQueryHooks(tables, options = {}) {
|
|
|
614
688
|
const files = [];
|
|
615
689
|
for (const table of tables) {
|
|
616
690
|
files.push(generateListQueryHook(table, options));
|
|
617
|
-
|
|
691
|
+
const singleHook = generateSingleQueryHook(table, options);
|
|
692
|
+
if (singleHook) {
|
|
693
|
+
files.push(singleHook);
|
|
694
|
+
}
|
|
618
695
|
}
|
|
619
696
|
return files;
|
|
620
697
|
}
|
|
@@ -171,6 +171,12 @@ export declare function getPrimaryKeyInfo(table: CleanTable): PrimaryKeyField[];
|
|
|
171
171
|
* Get primary key field names (convenience wrapper)
|
|
172
172
|
*/
|
|
173
173
|
export declare function getPrimaryKeyFields(table: CleanTable): string[];
|
|
174
|
+
/**
|
|
175
|
+
* Check if table has a valid single-field primary key
|
|
176
|
+
* Used to determine if a single query hook can be generated
|
|
177
|
+
* Tables with composite keys return false (handled as custom queries)
|
|
178
|
+
*/
|
|
179
|
+
export declare function hasValidPrimaryKey(table: CleanTable): boolean;
|
|
174
180
|
/**
|
|
175
181
|
* Generate query key prefix for a table
|
|
176
182
|
* e.g., "cars" for list queries, "car" for detail queries
|
package/esm/cli/codegen/utils.js
CHANGED
|
@@ -291,6 +291,24 @@ export function getPrimaryKeyInfo(table) {
|
|
|
291
291
|
export function getPrimaryKeyFields(table) {
|
|
292
292
|
return getPrimaryKeyInfo(table).map((pk) => pk.name);
|
|
293
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Check if table has a valid single-field primary key
|
|
296
|
+
* Used to determine if a single query hook can be generated
|
|
297
|
+
* Tables with composite keys return false (handled as custom queries)
|
|
298
|
+
*/
|
|
299
|
+
export function hasValidPrimaryKey(table) {
|
|
300
|
+
// Check for explicit primary key constraint with single field
|
|
301
|
+
const pk = table.constraints?.primaryKey?.[0];
|
|
302
|
+
if (pk && pk.fields.length === 1) {
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
// Check for 'id' field as fallback
|
|
306
|
+
const idField = table.fields.find((f) => f.name.toLowerCase() === 'id');
|
|
307
|
+
if (idField) {
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
294
312
|
// ============================================================================
|
|
295
313
|
// Query key generation
|
|
296
314
|
// ============================================================================
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
export interface GenerateOrmOptions {
|
|
10
10
|
/** Path to config file */
|
|
11
11
|
config?: string;
|
|
12
|
+
/** Named target in a multi-target config */
|
|
13
|
+
target?: string;
|
|
12
14
|
/** GraphQL endpoint URL (overrides config) */
|
|
13
15
|
endpoint?: string;
|
|
14
16
|
/** Path to GraphQL schema file (.graphql) */
|
|
@@ -24,9 +26,21 @@ export interface GenerateOrmOptions {
|
|
|
24
26
|
/** Skip custom operations (only generate table CRUD) */
|
|
25
27
|
skipCustomOperations?: boolean;
|
|
26
28
|
}
|
|
29
|
+
export interface GenerateOrmTargetResult {
|
|
30
|
+
name: string;
|
|
31
|
+
output: string;
|
|
32
|
+
success: boolean;
|
|
33
|
+
message: string;
|
|
34
|
+
tables?: string[];
|
|
35
|
+
customQueries?: string[];
|
|
36
|
+
customMutations?: string[];
|
|
37
|
+
filesWritten?: string[];
|
|
38
|
+
errors?: string[];
|
|
39
|
+
}
|
|
27
40
|
export interface GenerateOrmResult {
|
|
28
41
|
success: boolean;
|
|
29
42
|
message: string;
|
|
43
|
+
targets?: GenerateOrmTargetResult[];
|
|
30
44
|
tables?: string[];
|
|
31
45
|
customQueries?: string[];
|
|
32
46
|
customMutations?: string[];
|