@constructive-io/graphql-query 3.2.4 → 3.3.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 +411 -65
- package/ast.d.ts +4 -4
- package/ast.js +24 -9
- package/client/error.d.ts +95 -0
- package/client/error.js +277 -0
- package/client/execute.d.ts +57 -0
- package/client/execute.js +124 -0
- package/client/index.d.ts +8 -0
- package/client/index.js +20 -0
- package/client/typed-document.d.ts +31 -0
- package/client/typed-document.js +44 -0
- package/custom-ast.d.ts +22 -8
- package/custom-ast.js +16 -1
- package/esm/ast.js +22 -7
- package/esm/client/error.js +271 -0
- package/esm/client/execute.js +120 -0
- package/esm/client/index.js +8 -0
- package/esm/client/typed-document.js +40 -0
- package/esm/custom-ast.js +16 -1
- package/esm/generators/field-selector.js +381 -0
- package/esm/generators/index.js +13 -0
- package/esm/generators/mutations.js +200 -0
- package/esm/generators/naming-helpers.js +154 -0
- package/esm/generators/select.js +661 -0
- package/esm/index.js +30 -0
- package/esm/introspect/index.js +9 -0
- package/esm/introspect/infer-tables.js +697 -0
- package/esm/introspect/schema-query.js +120 -0
- package/esm/introspect/transform-schema.js +271 -0
- package/esm/introspect/transform.js +38 -0
- package/esm/meta-object/convert.js +3 -0
- package/esm/meta-object/format.json +11 -41
- package/esm/meta-object/validate.js +20 -4
- package/esm/query-builder.js +14 -18
- package/esm/types/index.js +18 -0
- package/esm/types/introspection.js +54 -0
- package/esm/types/mutation.js +4 -0
- package/esm/types/query.js +4 -0
- package/esm/types/schema.js +5 -0
- package/esm/types/selection.js +4 -0
- package/esm/utils.js +69 -0
- package/generators/field-selector.d.ts +30 -0
- package/generators/field-selector.js +387 -0
- package/generators/index.d.ts +9 -0
- package/generators/index.js +42 -0
- package/generators/mutations.d.ts +30 -0
- package/generators/mutations.js +238 -0
- package/generators/naming-helpers.d.ts +48 -0
- package/generators/naming-helpers.js +169 -0
- package/generators/select.d.ts +39 -0
- package/generators/select.js +705 -0
- package/index.d.ts +19 -0
- package/index.js +34 -1
- package/introspect/index.d.ts +9 -0
- package/introspect/index.js +25 -0
- package/introspect/infer-tables.d.ts +42 -0
- package/introspect/infer-tables.js +700 -0
- package/introspect/schema-query.d.ts +20 -0
- package/introspect/schema-query.js +123 -0
- package/introspect/transform-schema.d.ts +86 -0
- package/introspect/transform-schema.js +281 -0
- package/introspect/transform.d.ts +20 -0
- package/introspect/transform.js +43 -0
- package/meta-object/convert.d.ts +3 -0
- package/meta-object/convert.js +3 -0
- package/meta-object/format.json +11 -41
- package/meta-object/validate.d.ts +8 -3
- package/meta-object/validate.js +20 -4
- package/package.json +4 -3
- package/query-builder.d.ts +11 -12
- package/query-builder.js +25 -29
- package/{types.d.ts → types/core.d.ts} +25 -18
- package/types/index.d.ts +12 -0
- package/types/index.js +34 -0
- package/types/introspection.d.ts +121 -0
- package/types/introspection.js +62 -0
- package/types/mutation.d.ts +45 -0
- package/types/mutation.js +5 -0
- package/types/query.d.ts +91 -0
- package/types/query.js +5 -0
- package/types/schema.d.ts +265 -0
- package/types/schema.js +6 -0
- package/types/selection.d.ts +43 -0
- package/types/selection.js +5 -0
- package/utils.d.ts +17 -0
- package/utils.js +72 -0
- /package/esm/{types.js → types/core.js} +0 -0
- /package/{types.js → types/core.js} +0 -0
|
@@ -0,0 +1,661 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query generators for SELECT, FindOne, and Count operations
|
|
3
|
+
* Uses AST-based approach for all query generation
|
|
4
|
+
*/
|
|
5
|
+
import * as t from 'gql-ast';
|
|
6
|
+
import { Kind, OperationTypeNode, print } from 'graphql';
|
|
7
|
+
import { TypedDocumentString } from '../client/typed-document';
|
|
8
|
+
import { getCustomAstForCleanField, requiresSubfieldSelection, } from '../custom-ast';
|
|
9
|
+
import { QueryBuilder } from '../query-builder';
|
|
10
|
+
import { convertToSelectionOptions, isRelationalField } from './field-selector';
|
|
11
|
+
import { normalizeInflectionValue, toCamelCasePlural, toCamelCaseSingular, toCreateInputTypeName, toCreateMutationName, toDeleteInputTypeName, toDeleteMutationName, toFilterTypeName, toOrderByTypeName, toPatchFieldName, toUpdateInputTypeName, toUpdateMutationName, } from './naming-helpers';
|
|
12
|
+
// Re-export naming helpers for backwards compatibility
|
|
13
|
+
export { toCamelCasePlural, toOrderByTypeName } from './naming-helpers';
|
|
14
|
+
/**
|
|
15
|
+
* Convert CleanTable to MetaObject format for QueryBuilder
|
|
16
|
+
*/
|
|
17
|
+
export function cleanTableToMetaObject(tables) {
|
|
18
|
+
return {
|
|
19
|
+
tables: tables.map((table) => ({
|
|
20
|
+
name: table.name,
|
|
21
|
+
fields: table.fields.map((field) => ({
|
|
22
|
+
name: field.name,
|
|
23
|
+
type: {
|
|
24
|
+
gqlType: field.type.gqlType,
|
|
25
|
+
isArray: field.type.isArray,
|
|
26
|
+
modifier: field.type.modifier,
|
|
27
|
+
pgAlias: field.type.pgAlias,
|
|
28
|
+
pgType: field.type.pgType,
|
|
29
|
+
subtype: field.type.subtype,
|
|
30
|
+
typmod: field.type.typmod,
|
|
31
|
+
},
|
|
32
|
+
})),
|
|
33
|
+
primaryConstraints: [], // Would need to be derived from schema
|
|
34
|
+
uniqueConstraints: [], // Would need to be derived from schema
|
|
35
|
+
foreignConstraints: table.relations.belongsTo.map((rel) => ({
|
|
36
|
+
refTable: rel.referencesTable,
|
|
37
|
+
fromKey: {
|
|
38
|
+
name: rel.fieldName || '',
|
|
39
|
+
type: {
|
|
40
|
+
gqlType: 'UUID', // Default, should be derived from actual field
|
|
41
|
+
isArray: false,
|
|
42
|
+
modifier: null,
|
|
43
|
+
pgAlias: null,
|
|
44
|
+
pgType: null,
|
|
45
|
+
subtype: null,
|
|
46
|
+
typmod: null,
|
|
47
|
+
},
|
|
48
|
+
alias: rel.fieldName || '',
|
|
49
|
+
},
|
|
50
|
+
toKey: {
|
|
51
|
+
name: 'id',
|
|
52
|
+
type: {
|
|
53
|
+
gqlType: 'UUID',
|
|
54
|
+
isArray: false,
|
|
55
|
+
modifier: null,
|
|
56
|
+
pgAlias: null,
|
|
57
|
+
pgType: null,
|
|
58
|
+
subtype: null,
|
|
59
|
+
typmod: null,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
})),
|
|
63
|
+
})),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Generate basic IntrospectionSchema from CleanTable array
|
|
68
|
+
* This creates a minimal schema for AST generation
|
|
69
|
+
*/
|
|
70
|
+
export function generateIntrospectionSchema(tables) {
|
|
71
|
+
const schema = {};
|
|
72
|
+
for (const table of tables) {
|
|
73
|
+
const modelName = table.name;
|
|
74
|
+
const pluralName = toCamelCasePlural(modelName, table);
|
|
75
|
+
// Basic field selection for the model
|
|
76
|
+
const selection = table.fields.map((field) => field.name);
|
|
77
|
+
// Add getMany query
|
|
78
|
+
schema[pluralName] = {
|
|
79
|
+
qtype: 'getMany',
|
|
80
|
+
model: modelName,
|
|
81
|
+
selection,
|
|
82
|
+
properties: convertFieldsToProperties(table.fields),
|
|
83
|
+
};
|
|
84
|
+
// Add getOne query (by ID)
|
|
85
|
+
const singularName = toCamelCaseSingular(modelName, table);
|
|
86
|
+
schema[singularName] = {
|
|
87
|
+
qtype: 'getOne',
|
|
88
|
+
model: modelName,
|
|
89
|
+
selection,
|
|
90
|
+
properties: convertFieldsToProperties(table.fields),
|
|
91
|
+
};
|
|
92
|
+
// Derive entity-specific names from introspection data
|
|
93
|
+
const patchFieldName = toPatchFieldName(modelName, table);
|
|
94
|
+
const createMutationName = toCreateMutationName(modelName, table);
|
|
95
|
+
const updateMutationName = toUpdateMutationName(modelName, table);
|
|
96
|
+
const deleteMutationName = toDeleteMutationName(modelName, table);
|
|
97
|
+
const createInputType = toCreateInputTypeName(modelName, table);
|
|
98
|
+
const patchType = normalizeInflectionValue(table.inflection?.patchType) ??
|
|
99
|
+
`${modelName}Patch`;
|
|
100
|
+
// Add create mutation
|
|
101
|
+
schema[createMutationName] = {
|
|
102
|
+
qtype: 'mutation',
|
|
103
|
+
mutationType: 'create',
|
|
104
|
+
model: modelName,
|
|
105
|
+
selection,
|
|
106
|
+
properties: {
|
|
107
|
+
input: {
|
|
108
|
+
name: 'input',
|
|
109
|
+
type: createInputType,
|
|
110
|
+
isNotNull: true,
|
|
111
|
+
isArray: false,
|
|
112
|
+
isArrayNotNull: false,
|
|
113
|
+
properties: {
|
|
114
|
+
[singularName]: {
|
|
115
|
+
name: singularName,
|
|
116
|
+
type: `${modelName}Input`,
|
|
117
|
+
isNotNull: true,
|
|
118
|
+
isArray: false,
|
|
119
|
+
isArrayNotNull: false,
|
|
120
|
+
properties: convertFieldsToNestedProperties(table.fields),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
// Add update mutation
|
|
127
|
+
schema[updateMutationName] = {
|
|
128
|
+
qtype: 'mutation',
|
|
129
|
+
mutationType: 'patch',
|
|
130
|
+
model: modelName,
|
|
131
|
+
selection,
|
|
132
|
+
properties: {
|
|
133
|
+
input: {
|
|
134
|
+
name: 'input',
|
|
135
|
+
type: toUpdateInputTypeName(modelName),
|
|
136
|
+
isNotNull: true,
|
|
137
|
+
isArray: false,
|
|
138
|
+
isArrayNotNull: false,
|
|
139
|
+
properties: {
|
|
140
|
+
[patchFieldName]: {
|
|
141
|
+
name: patchFieldName,
|
|
142
|
+
type: patchType,
|
|
143
|
+
isNotNull: true,
|
|
144
|
+
isArray: false,
|
|
145
|
+
isArrayNotNull: false,
|
|
146
|
+
properties: convertFieldsToNestedProperties(table.fields),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
// Add delete mutation
|
|
153
|
+
schema[deleteMutationName] = {
|
|
154
|
+
qtype: 'mutation',
|
|
155
|
+
mutationType: 'delete',
|
|
156
|
+
model: modelName,
|
|
157
|
+
selection,
|
|
158
|
+
properties: {
|
|
159
|
+
input: {
|
|
160
|
+
name: 'input',
|
|
161
|
+
type: toDeleteInputTypeName(modelName),
|
|
162
|
+
isNotNull: true,
|
|
163
|
+
isArray: false,
|
|
164
|
+
isArrayNotNull: false,
|
|
165
|
+
properties: {
|
|
166
|
+
id: {
|
|
167
|
+
name: 'id',
|
|
168
|
+
type: 'UUID',
|
|
169
|
+
isNotNull: true,
|
|
170
|
+
isArray: false,
|
|
171
|
+
isArrayNotNull: false,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
return schema;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Convert CleanTable fields to QueryBuilder properties
|
|
182
|
+
*/
|
|
183
|
+
function convertFieldsToProperties(fields) {
|
|
184
|
+
const properties = {};
|
|
185
|
+
fields.forEach((field) => {
|
|
186
|
+
properties[field.name] = {
|
|
187
|
+
name: field.name,
|
|
188
|
+
type: field.type.gqlType,
|
|
189
|
+
isNotNull: !field.type.gqlType.endsWith('!'),
|
|
190
|
+
isArray: field.type.isArray,
|
|
191
|
+
isArrayNotNull: false,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
return properties;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Convert fields to nested properties for mutations
|
|
198
|
+
*/
|
|
199
|
+
function convertFieldsToNestedProperties(fields) {
|
|
200
|
+
const properties = {};
|
|
201
|
+
fields.forEach((field) => {
|
|
202
|
+
properties[field.name] = {
|
|
203
|
+
name: field.name,
|
|
204
|
+
type: field.type.gqlType,
|
|
205
|
+
isNotNull: false, // Mutations typically allow optional fields
|
|
206
|
+
isArray: field.type.isArray,
|
|
207
|
+
isArrayNotNull: false,
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
return properties;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Create AST-based query builder for a table
|
|
214
|
+
*/
|
|
215
|
+
export function createASTQueryBuilder(tables) {
|
|
216
|
+
const metaObject = cleanTableToMetaObject(tables);
|
|
217
|
+
const introspectionSchema = generateIntrospectionSchema(tables);
|
|
218
|
+
return new QueryBuilder({
|
|
219
|
+
meta: metaObject,
|
|
220
|
+
introspection: introspectionSchema,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Build a SELECT query for a table with optional filtering, sorting, and pagination
|
|
225
|
+
* Uses direct AST generation without intermediate conversions
|
|
226
|
+
*/
|
|
227
|
+
export function buildSelect(table, allTables, options = {}) {
|
|
228
|
+
const tableList = Array.from(allTables);
|
|
229
|
+
const selection = convertFieldSelectionToSelectionOptions(table, tableList, options.fieldSelection);
|
|
230
|
+
// Generate query directly using AST
|
|
231
|
+
const queryString = generateSelectQueryAST(table, tableList, selection, options, options.relationFieldMap);
|
|
232
|
+
return new TypedDocumentString(queryString, {});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Build a single row query by primary key or unique field
|
|
236
|
+
*/
|
|
237
|
+
export function buildFindOne(table, _pkField = 'id') {
|
|
238
|
+
const queryString = generateFindOneQueryAST(table);
|
|
239
|
+
return new TypedDocumentString(queryString, {});
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Build a count query for a table
|
|
243
|
+
*/
|
|
244
|
+
export function buildCount(table) {
|
|
245
|
+
const queryString = generateCountQueryAST(table);
|
|
246
|
+
return new TypedDocumentString(queryString, {});
|
|
247
|
+
}
|
|
248
|
+
function convertFieldSelectionToSelectionOptions(table, allTables, options) {
|
|
249
|
+
return convertToSelectionOptions(table, allTables, options);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Generate SELECT query AST directly from CleanTable
|
|
253
|
+
*/
|
|
254
|
+
function generateSelectQueryAST(table, allTables, selection, options, relationFieldMap) {
|
|
255
|
+
const pluralName = toCamelCasePlural(table.name, table);
|
|
256
|
+
// Generate field selections
|
|
257
|
+
const fieldSelections = generateFieldSelectionsFromOptions(table, allTables, selection, relationFieldMap);
|
|
258
|
+
// Build the query AST
|
|
259
|
+
const variableDefinitions = [];
|
|
260
|
+
const queryArgs = [];
|
|
261
|
+
// Add pagination variables if needed
|
|
262
|
+
const limitValue = options.limit ?? options.first;
|
|
263
|
+
if (limitValue !== undefined) {
|
|
264
|
+
variableDefinitions.push(t.variableDefinition({
|
|
265
|
+
variable: t.variable({ name: 'first' }),
|
|
266
|
+
type: t.namedType({ type: 'Int' }),
|
|
267
|
+
}));
|
|
268
|
+
queryArgs.push(t.argument({
|
|
269
|
+
name: 'first',
|
|
270
|
+
value: t.variable({ name: 'first' }),
|
|
271
|
+
}));
|
|
272
|
+
}
|
|
273
|
+
if (options.offset !== undefined) {
|
|
274
|
+
variableDefinitions.push(t.variableDefinition({
|
|
275
|
+
variable: t.variable({ name: 'offset' }),
|
|
276
|
+
type: t.namedType({ type: 'Int' }),
|
|
277
|
+
}));
|
|
278
|
+
queryArgs.push(t.argument({
|
|
279
|
+
name: 'offset',
|
|
280
|
+
value: t.variable({ name: 'offset' }),
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
// Add cursor-based pagination variables if needed (for infinite scroll)
|
|
284
|
+
if (options.after !== undefined) {
|
|
285
|
+
variableDefinitions.push(t.variableDefinition({
|
|
286
|
+
variable: t.variable({ name: 'after' }),
|
|
287
|
+
type: t.namedType({ type: 'Cursor' }),
|
|
288
|
+
}));
|
|
289
|
+
queryArgs.push(t.argument({
|
|
290
|
+
name: 'after',
|
|
291
|
+
value: t.variable({ name: 'after' }),
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
if (options.before !== undefined) {
|
|
295
|
+
variableDefinitions.push(t.variableDefinition({
|
|
296
|
+
variable: t.variable({ name: 'before' }),
|
|
297
|
+
type: t.namedType({ type: 'Cursor' }),
|
|
298
|
+
}));
|
|
299
|
+
queryArgs.push(t.argument({
|
|
300
|
+
name: 'before',
|
|
301
|
+
value: t.variable({ name: 'before' }),
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
// Add filter variables if needed
|
|
305
|
+
if (options.where) {
|
|
306
|
+
variableDefinitions.push(t.variableDefinition({
|
|
307
|
+
variable: t.variable({ name: 'filter' }),
|
|
308
|
+
type: t.namedType({ type: toFilterTypeName(table.name, table) }),
|
|
309
|
+
}));
|
|
310
|
+
queryArgs.push(t.argument({
|
|
311
|
+
name: 'filter',
|
|
312
|
+
value: t.variable({ name: 'filter' }),
|
|
313
|
+
}));
|
|
314
|
+
}
|
|
315
|
+
// Add orderBy variables if needed
|
|
316
|
+
if (options.orderBy && options.orderBy.length > 0) {
|
|
317
|
+
variableDefinitions.push(t.variableDefinition({
|
|
318
|
+
variable: t.variable({ name: 'orderBy' }),
|
|
319
|
+
// PostGraphile expects [ProductsOrderBy!] - list of non-null enum values
|
|
320
|
+
type: t.listType({
|
|
321
|
+
type: t.nonNullType({
|
|
322
|
+
type: t.namedType({ type: toOrderByTypeName(table.name, table) }),
|
|
323
|
+
}),
|
|
324
|
+
}),
|
|
325
|
+
}));
|
|
326
|
+
queryArgs.push(t.argument({
|
|
327
|
+
name: 'orderBy',
|
|
328
|
+
value: t.variable({ name: 'orderBy' }),
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
// Build connection selections: totalCount, nodes, and optionally pageInfo
|
|
332
|
+
const connectionSelections = [
|
|
333
|
+
t.field({ name: 'totalCount' }),
|
|
334
|
+
t.field({
|
|
335
|
+
name: 'nodes',
|
|
336
|
+
selectionSet: t.selectionSet({
|
|
337
|
+
selections: fieldSelections,
|
|
338
|
+
}),
|
|
339
|
+
}),
|
|
340
|
+
];
|
|
341
|
+
// Add pageInfo if requested (for cursor-based pagination / infinite scroll)
|
|
342
|
+
if (options.includePageInfo ||
|
|
343
|
+
options.after !== undefined ||
|
|
344
|
+
options.before !== undefined) {
|
|
345
|
+
connectionSelections.push(t.field({
|
|
346
|
+
name: 'pageInfo',
|
|
347
|
+
selectionSet: t.selectionSet({
|
|
348
|
+
selections: [
|
|
349
|
+
t.field({ name: 'hasNextPage' }),
|
|
350
|
+
t.field({ name: 'hasPreviousPage' }),
|
|
351
|
+
t.field({ name: 'startCursor' }),
|
|
352
|
+
t.field({ name: 'endCursor' }),
|
|
353
|
+
],
|
|
354
|
+
}),
|
|
355
|
+
}));
|
|
356
|
+
}
|
|
357
|
+
const ast = t.document({
|
|
358
|
+
definitions: [
|
|
359
|
+
t.operationDefinition({
|
|
360
|
+
operation: OperationTypeNode.QUERY,
|
|
361
|
+
name: `${pluralName}Query`,
|
|
362
|
+
variableDefinitions,
|
|
363
|
+
selectionSet: t.selectionSet({
|
|
364
|
+
selections: [
|
|
365
|
+
t.field({
|
|
366
|
+
name: pluralName,
|
|
367
|
+
args: queryArgs,
|
|
368
|
+
selectionSet: t.selectionSet({
|
|
369
|
+
selections: connectionSelections,
|
|
370
|
+
}),
|
|
371
|
+
}),
|
|
372
|
+
],
|
|
373
|
+
}),
|
|
374
|
+
}),
|
|
375
|
+
],
|
|
376
|
+
});
|
|
377
|
+
return print(ast);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Generate field selections from SelectionOptions
|
|
381
|
+
*/
|
|
382
|
+
function generateFieldSelectionsFromOptions(table, allTables, selection, relationFieldMap) {
|
|
383
|
+
const DEFAULT_NESTED_RELATION_FIRST = 20;
|
|
384
|
+
if (!selection) {
|
|
385
|
+
// Default to all non-relational fields (includes complex fields like JSON, geometry, etc.)
|
|
386
|
+
return table.fields
|
|
387
|
+
.filter((field) => !isRelationalField(field.name, table))
|
|
388
|
+
.map((field) => {
|
|
389
|
+
if (requiresSubfieldSelection(field)) {
|
|
390
|
+
// For complex fields that require subfield selection, use custom AST generation
|
|
391
|
+
return getCustomAstForCleanField(field);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
// For simple fields, use basic field selection
|
|
395
|
+
return t.field({ name: field.name });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
const fieldSelections = [];
|
|
400
|
+
Object.entries(selection).forEach(([fieldName, fieldOptions]) => {
|
|
401
|
+
const resolvedField = resolveSelectionFieldName(fieldName, relationFieldMap);
|
|
402
|
+
if (!resolvedField) {
|
|
403
|
+
return; // Field mapped to null — omit it
|
|
404
|
+
}
|
|
405
|
+
if (fieldOptions === true) {
|
|
406
|
+
// Check if this field requires subfield selection
|
|
407
|
+
const field = table.fields.find((f) => f.name === fieldName);
|
|
408
|
+
if (field && requiresSubfieldSelection(field)) {
|
|
409
|
+
// Use custom AST generation for complex fields
|
|
410
|
+
fieldSelections.push(getCustomAstForCleanField(field));
|
|
411
|
+
}
|
|
412
|
+
else {
|
|
413
|
+
// Simple field selection for scalar fields
|
|
414
|
+
fieldSelections.push(createFieldSelectionNode(resolvedField.name, resolvedField.alias));
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else if (typeof fieldOptions === 'object' && fieldOptions.select) {
|
|
418
|
+
// Nested field selection (for relation fields)
|
|
419
|
+
const nestedSelections = [];
|
|
420
|
+
// Find the related table to check for complex fields
|
|
421
|
+
const relatedTable = findRelatedTable(fieldName, table, allTables);
|
|
422
|
+
Object.entries(fieldOptions.select).forEach(([nestedField, include]) => {
|
|
423
|
+
if (include) {
|
|
424
|
+
// Check if this nested field requires subfield selection
|
|
425
|
+
const nestedFieldDef = relatedTable?.fields.find((f) => f.name === nestedField);
|
|
426
|
+
if (nestedFieldDef && requiresSubfieldSelection(nestedFieldDef)) {
|
|
427
|
+
// Use custom AST generation for complex nested fields
|
|
428
|
+
nestedSelections.push(getCustomAstForCleanField(nestedFieldDef));
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
// Simple field selection for scalar nested fields
|
|
432
|
+
nestedSelections.push(t.field({ name: nestedField }));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// Check if this is a hasMany relation that uses Connection pattern
|
|
437
|
+
const relationInfo = getRelationInfo(fieldName, table);
|
|
438
|
+
if (relationInfo &&
|
|
439
|
+
(relationInfo.type === 'hasMany' || relationInfo.type === 'manyToMany')) {
|
|
440
|
+
// For hasMany/manyToMany relations, wrap selections in nodes { ... }
|
|
441
|
+
fieldSelections.push(createFieldSelectionNode(resolvedField.name, resolvedField.alias, [
|
|
442
|
+
t.argument({
|
|
443
|
+
name: 'first',
|
|
444
|
+
value: t.intValue({
|
|
445
|
+
value: DEFAULT_NESTED_RELATION_FIRST.toString(),
|
|
446
|
+
}),
|
|
447
|
+
}),
|
|
448
|
+
], t.selectionSet({
|
|
449
|
+
selections: [
|
|
450
|
+
t.field({
|
|
451
|
+
name: 'nodes',
|
|
452
|
+
selectionSet: t.selectionSet({
|
|
453
|
+
selections: nestedSelections,
|
|
454
|
+
}),
|
|
455
|
+
}),
|
|
456
|
+
],
|
|
457
|
+
})));
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// For belongsTo/hasOne relations, use direct selection
|
|
461
|
+
fieldSelections.push(createFieldSelectionNode(resolvedField.name, resolvedField.alias, undefined, t.selectionSet({
|
|
462
|
+
selections: nestedSelections,
|
|
463
|
+
})));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
return fieldSelections;
|
|
468
|
+
}
|
|
469
|
+
// ---------------------------------------------------------------------------
|
|
470
|
+
// Field aliasing helpers (back-ported from Dashboard query-generator.ts)
|
|
471
|
+
// ---------------------------------------------------------------------------
|
|
472
|
+
/**
|
|
473
|
+
* Resolve a field name through the optional relationFieldMap.
|
|
474
|
+
* Returns `null` if the field should be omitted (mapped to null).
|
|
475
|
+
* Returns `{ name, alias? }` where alias is set when the mapped name differs.
|
|
476
|
+
*/
|
|
477
|
+
function resolveSelectionFieldName(fieldName, relationFieldMap) {
|
|
478
|
+
if (!relationFieldMap || !(fieldName in relationFieldMap)) {
|
|
479
|
+
return { name: fieldName };
|
|
480
|
+
}
|
|
481
|
+
const mappedFieldName = relationFieldMap[fieldName];
|
|
482
|
+
if (!mappedFieldName) {
|
|
483
|
+
return null; // mapped to null → omit
|
|
484
|
+
}
|
|
485
|
+
if (mappedFieldName === fieldName) {
|
|
486
|
+
return { name: fieldName };
|
|
487
|
+
}
|
|
488
|
+
return { name: mappedFieldName, alias: fieldName };
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Create a field AST node with optional alias support.
|
|
492
|
+
* When alias is provided and differs from name, a GraphQL alias is emitted:
|
|
493
|
+
* `alias: name { … }` instead of `name { … }`
|
|
494
|
+
*/
|
|
495
|
+
function createFieldSelectionNode(name, alias, args, selectionSet) {
|
|
496
|
+
const node = t.field({ name, args, selectionSet });
|
|
497
|
+
if (!alias || alias === name) {
|
|
498
|
+
return node;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
...node,
|
|
502
|
+
alias: {
|
|
503
|
+
kind: Kind.NAME,
|
|
504
|
+
value: alias,
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Get relation information for a field
|
|
510
|
+
*/
|
|
511
|
+
function getRelationInfo(fieldName, table) {
|
|
512
|
+
const { belongsTo, hasOne, hasMany, manyToMany } = table.relations;
|
|
513
|
+
// Check belongsTo relations
|
|
514
|
+
const belongsToRel = belongsTo.find((rel) => rel.fieldName === fieldName);
|
|
515
|
+
if (belongsToRel) {
|
|
516
|
+
return { type: 'belongsTo', relation: belongsToRel };
|
|
517
|
+
}
|
|
518
|
+
// Check hasOne relations
|
|
519
|
+
const hasOneRel = hasOne.find((rel) => rel.fieldName === fieldName);
|
|
520
|
+
if (hasOneRel) {
|
|
521
|
+
return { type: 'hasOne', relation: hasOneRel };
|
|
522
|
+
}
|
|
523
|
+
// Check hasMany relations
|
|
524
|
+
const hasManyRel = hasMany.find((rel) => rel.fieldName === fieldName);
|
|
525
|
+
if (hasManyRel) {
|
|
526
|
+
return { type: 'hasMany', relation: hasManyRel };
|
|
527
|
+
}
|
|
528
|
+
// Check manyToMany relations
|
|
529
|
+
const manyToManyRel = manyToMany.find((rel) => rel.fieldName === fieldName);
|
|
530
|
+
if (manyToManyRel) {
|
|
531
|
+
return { type: 'manyToMany', relation: manyToManyRel };
|
|
532
|
+
}
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
/**
|
|
536
|
+
* Find the related table for a given relation field
|
|
537
|
+
*/
|
|
538
|
+
function findRelatedTable(relationField, table, allTables) {
|
|
539
|
+
// Find the related table name
|
|
540
|
+
let referencedTableName;
|
|
541
|
+
// Check belongsTo relations
|
|
542
|
+
const belongsToRel = table.relations.belongsTo.find((rel) => rel.fieldName === relationField);
|
|
543
|
+
if (belongsToRel) {
|
|
544
|
+
referencedTableName = belongsToRel.referencesTable;
|
|
545
|
+
}
|
|
546
|
+
// Check hasOne relations
|
|
547
|
+
if (!referencedTableName) {
|
|
548
|
+
const hasOneRel = table.relations.hasOne.find((rel) => rel.fieldName === relationField);
|
|
549
|
+
if (hasOneRel) {
|
|
550
|
+
referencedTableName = hasOneRel.referencedByTable;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Check hasMany relations
|
|
554
|
+
if (!referencedTableName) {
|
|
555
|
+
const hasManyRel = table.relations.hasMany.find((rel) => rel.fieldName === relationField);
|
|
556
|
+
if (hasManyRel) {
|
|
557
|
+
referencedTableName = hasManyRel.referencedByTable;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Check manyToMany relations
|
|
561
|
+
if (!referencedTableName) {
|
|
562
|
+
const manyToManyRel = table.relations.manyToMany.find((rel) => rel.fieldName === relationField);
|
|
563
|
+
if (manyToManyRel) {
|
|
564
|
+
referencedTableName = manyToManyRel.rightTable;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (!referencedTableName) {
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
// Find the related table in allTables
|
|
571
|
+
return allTables.find((tbl) => tbl.name === referencedTableName) || null;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Generate FindOne query AST directly from CleanTable
|
|
575
|
+
*/
|
|
576
|
+
function generateFindOneQueryAST(table) {
|
|
577
|
+
const singularName = toCamelCaseSingular(table.name, table);
|
|
578
|
+
// Generate field selections (include all non-relational fields, including complex types)
|
|
579
|
+
const fieldSelections = table.fields
|
|
580
|
+
.filter((field) => !isRelationalField(field.name, table))
|
|
581
|
+
.map((field) => {
|
|
582
|
+
if (requiresSubfieldSelection(field)) {
|
|
583
|
+
// For complex fields that require subfield selection, use custom AST generation
|
|
584
|
+
return getCustomAstForCleanField(field);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
// For simple fields, use basic field selection
|
|
588
|
+
return t.field({ name: field.name });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
const ast = t.document({
|
|
592
|
+
definitions: [
|
|
593
|
+
t.operationDefinition({
|
|
594
|
+
operation: OperationTypeNode.QUERY,
|
|
595
|
+
name: `${singularName}Query`,
|
|
596
|
+
variableDefinitions: [
|
|
597
|
+
t.variableDefinition({
|
|
598
|
+
variable: t.variable({ name: 'id' }),
|
|
599
|
+
type: t.nonNullType({
|
|
600
|
+
type: t.namedType({ type: 'UUID' }),
|
|
601
|
+
}),
|
|
602
|
+
}),
|
|
603
|
+
],
|
|
604
|
+
selectionSet: t.selectionSet({
|
|
605
|
+
selections: [
|
|
606
|
+
t.field({
|
|
607
|
+
name: singularName,
|
|
608
|
+
args: [
|
|
609
|
+
t.argument({
|
|
610
|
+
name: 'id',
|
|
611
|
+
value: t.variable({ name: 'id' }),
|
|
612
|
+
}),
|
|
613
|
+
],
|
|
614
|
+
selectionSet: t.selectionSet({
|
|
615
|
+
selections: fieldSelections,
|
|
616
|
+
}),
|
|
617
|
+
}),
|
|
618
|
+
],
|
|
619
|
+
}),
|
|
620
|
+
}),
|
|
621
|
+
],
|
|
622
|
+
});
|
|
623
|
+
return print(ast);
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Generate Count query AST directly from CleanTable
|
|
627
|
+
*/
|
|
628
|
+
function generateCountQueryAST(table) {
|
|
629
|
+
const pluralName = toCamelCasePlural(table.name, table);
|
|
630
|
+
const ast = t.document({
|
|
631
|
+
definitions: [
|
|
632
|
+
t.operationDefinition({
|
|
633
|
+
operation: OperationTypeNode.QUERY,
|
|
634
|
+
name: `${pluralName}CountQuery`,
|
|
635
|
+
variableDefinitions: [
|
|
636
|
+
t.variableDefinition({
|
|
637
|
+
variable: t.variable({ name: 'filter' }),
|
|
638
|
+
type: t.namedType({ type: toFilterTypeName(table.name, table) }),
|
|
639
|
+
}),
|
|
640
|
+
],
|
|
641
|
+
selectionSet: t.selectionSet({
|
|
642
|
+
selections: [
|
|
643
|
+
t.field({
|
|
644
|
+
name: pluralName,
|
|
645
|
+
args: [
|
|
646
|
+
t.argument({
|
|
647
|
+
name: 'filter',
|
|
648
|
+
value: t.variable({ name: 'filter' }),
|
|
649
|
+
}),
|
|
650
|
+
],
|
|
651
|
+
selectionSet: t.selectionSet({
|
|
652
|
+
selections: [t.field({ name: 'totalCount' })],
|
|
653
|
+
}),
|
|
654
|
+
}),
|
|
655
|
+
],
|
|
656
|
+
}),
|
|
657
|
+
}),
|
|
658
|
+
],
|
|
659
|
+
});
|
|
660
|
+
return print(ast);
|
|
661
|
+
}
|
package/esm/index.js
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @constructive-io/graphql-query
|
|
3
|
+
*
|
|
4
|
+
* Browser-safe GraphQL query generation core.
|
|
5
|
+
* Contains the pure-function query builders, AST generators, naming helpers,
|
|
6
|
+
* introspection utilities, and client execution layer.
|
|
7
|
+
*
|
|
8
|
+
* This package is the canonical source for runtime query generation logic.
|
|
9
|
+
* @constructive-io/graphql-codegen depends on this for the core and adds
|
|
10
|
+
* Node.js-only build-time features (CLI, file output, watch mode, etc.).
|
|
11
|
+
*/
|
|
12
|
+
// QueryBuilder class (runtime query builder)
|
|
1
13
|
export { QueryBuilder } from './query-builder';
|
|
14
|
+
// QueryExecutor (server-side execution via PostGraphile)
|
|
2
15
|
export { QueryExecutor, createExecutor } from './executor';
|
|
16
|
+
// AST builders (getAll, getMany, getOne, createOne, patchOne, deleteOne)
|
|
17
|
+
export * from './ast';
|
|
18
|
+
// Custom AST (geometry, interval, etc.)
|
|
19
|
+
export * from './custom-ast';
|
|
20
|
+
// All types (core + codegen-style schema/introspection/query/mutation/selection)
|
|
3
21
|
export * from './types';
|
|
22
|
+
// Meta object utilities (convert, validate)
|
|
4
23
|
export * as MetaObject from './meta-object';
|
|
24
|
+
// Also export meta-object functions directly for codegen backward compatibility
|
|
25
|
+
export { convertFromMetaSchema } from './meta-object/convert';
|
|
26
|
+
export { validateMetaObject } from './meta-object/validate';
|
|
27
|
+
// Generators (buildSelect, buildFindOne, buildCount, mutations, field-selector, naming-helpers)
|
|
28
|
+
export * from './generators';
|
|
29
|
+
// Client utilities (TypedDocumentString, error handling, execute)
|
|
30
|
+
export * from './client';
|
|
31
|
+
// Introspection utilities (infer-tables, transform, transform-schema, schema-query)
|
|
32
|
+
export * from './introspect';
|
|
33
|
+
// Utility functions
|
|
34
|
+
export { stripSmartComments } from './utils';
|