@constructive-io/graphql-query 3.2.5 → 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.
Files changed (88) hide show
  1. package/README.md +411 -65
  2. package/ast.d.ts +4 -4
  3. package/ast.js +24 -9
  4. package/client/error.d.ts +95 -0
  5. package/client/error.js +277 -0
  6. package/client/execute.d.ts +57 -0
  7. package/client/execute.js +124 -0
  8. package/client/index.d.ts +8 -0
  9. package/client/index.js +20 -0
  10. package/client/typed-document.d.ts +31 -0
  11. package/client/typed-document.js +44 -0
  12. package/custom-ast.d.ts +22 -8
  13. package/custom-ast.js +16 -1
  14. package/esm/ast.js +22 -7
  15. package/esm/client/error.js +271 -0
  16. package/esm/client/execute.js +120 -0
  17. package/esm/client/index.js +8 -0
  18. package/esm/client/typed-document.js +40 -0
  19. package/esm/custom-ast.js +16 -1
  20. package/esm/generators/field-selector.js +381 -0
  21. package/esm/generators/index.js +13 -0
  22. package/esm/generators/mutations.js +200 -0
  23. package/esm/generators/naming-helpers.js +154 -0
  24. package/esm/generators/select.js +661 -0
  25. package/esm/index.js +30 -0
  26. package/esm/introspect/index.js +9 -0
  27. package/esm/introspect/infer-tables.js +697 -0
  28. package/esm/introspect/schema-query.js +120 -0
  29. package/esm/introspect/transform-schema.js +271 -0
  30. package/esm/introspect/transform.js +38 -0
  31. package/esm/meta-object/convert.js +3 -0
  32. package/esm/meta-object/format.json +11 -41
  33. package/esm/meta-object/validate.js +20 -4
  34. package/esm/query-builder.js +14 -18
  35. package/esm/types/index.js +18 -0
  36. package/esm/types/introspection.js +54 -0
  37. package/esm/types/mutation.js +4 -0
  38. package/esm/types/query.js +4 -0
  39. package/esm/types/schema.js +5 -0
  40. package/esm/types/selection.js +4 -0
  41. package/esm/utils.js +69 -0
  42. package/generators/field-selector.d.ts +30 -0
  43. package/generators/field-selector.js +387 -0
  44. package/generators/index.d.ts +9 -0
  45. package/generators/index.js +42 -0
  46. package/generators/mutations.d.ts +30 -0
  47. package/generators/mutations.js +238 -0
  48. package/generators/naming-helpers.d.ts +48 -0
  49. package/generators/naming-helpers.js +169 -0
  50. package/generators/select.d.ts +39 -0
  51. package/generators/select.js +705 -0
  52. package/index.d.ts +19 -0
  53. package/index.js +34 -1
  54. package/introspect/index.d.ts +9 -0
  55. package/introspect/index.js +25 -0
  56. package/introspect/infer-tables.d.ts +42 -0
  57. package/introspect/infer-tables.js +700 -0
  58. package/introspect/schema-query.d.ts +20 -0
  59. package/introspect/schema-query.js +123 -0
  60. package/introspect/transform-schema.d.ts +86 -0
  61. package/introspect/transform-schema.js +281 -0
  62. package/introspect/transform.d.ts +20 -0
  63. package/introspect/transform.js +43 -0
  64. package/meta-object/convert.d.ts +3 -0
  65. package/meta-object/convert.js +3 -0
  66. package/meta-object/format.json +11 -41
  67. package/meta-object/validate.d.ts +8 -3
  68. package/meta-object/validate.js +20 -4
  69. package/package.json +4 -3
  70. package/query-builder.d.ts +11 -12
  71. package/query-builder.js +25 -29
  72. package/{types.d.ts → types/core.d.ts} +25 -18
  73. package/types/index.d.ts +12 -0
  74. package/types/index.js +34 -0
  75. package/types/introspection.d.ts +121 -0
  76. package/types/introspection.js +62 -0
  77. package/types/mutation.d.ts +45 -0
  78. package/types/mutation.js +5 -0
  79. package/types/query.d.ts +91 -0
  80. package/types/query.js +5 -0
  81. package/types/schema.d.ts +265 -0
  82. package/types/schema.js +6 -0
  83. package/types/selection.d.ts +43 -0
  84. package/types/selection.js +5 -0
  85. package/utils.d.ts +17 -0
  86. package/utils.js +72 -0
  87. /package/esm/{types.js → types/core.js} +0 -0
  88. /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';