@constructive-io/graphql-codegen 4.19.1 → 4.21.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/core/codegen/barrel.d.ts +10 -0
- package/core/codegen/barrel.js +44 -0
- package/core/codegen/orm/input-types-generator.js +74 -6
- package/core/codegen/orm/model-generator.d.ts +1 -1
- package/core/codegen/orm/model-generator.js +148 -14
- package/core/codegen/templates/query-builder.ts +21 -5
- package/core/codegen/utils.d.ts +4 -2
- package/core/codegen/utils.js +8 -4
- package/core/generate.js +14 -2
- package/core/introspect/enrich-relations.d.ts +13 -0
- package/core/introspect/enrich-relations.js +26 -0
- package/core/pipeline/index.js +7 -1
- package/esm/core/codegen/barrel.d.ts +10 -0
- package/esm/core/codegen/barrel.js +43 -0
- package/esm/core/codegen/orm/input-types-generator.js +74 -6
- package/esm/core/codegen/orm/model-generator.d.ts +1 -1
- package/esm/core/codegen/orm/model-generator.js +149 -15
- package/esm/core/codegen/utils.d.ts +4 -2
- package/esm/core/codegen/utils.js +8 -4
- package/esm/core/generate.js +15 -3
- package/esm/core/introspect/enrich-relations.d.ts +13 -0
- package/esm/core/introspect/enrich-relations.js +23 -0
- package/esm/core/pipeline/index.js +7 -1
- package/esm/types/config.d.ts +2 -2
- package/package.json +3 -3
- package/types/config.d.ts +2 -2
package/core/codegen/barrel.d.ts
CHANGED
|
@@ -34,6 +34,16 @@ export interface RootBarrelOptions {
|
|
|
34
34
|
* Re-exports from subdirectories based on which generators are enabled.
|
|
35
35
|
*/
|
|
36
36
|
export declare function generateRootBarrel(options?: RootBarrelOptions): string;
|
|
37
|
+
/**
|
|
38
|
+
* Generate a root index.ts for multi-target output that re-exports each
|
|
39
|
+
* target as a namespace.
|
|
40
|
+
*
|
|
41
|
+
* Example output:
|
|
42
|
+
* export * as admin from './admin';
|
|
43
|
+
* export * as auth from './auth';
|
|
44
|
+
* export * as public_ from './public';
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateMultiTargetBarrel(targetNames: string[]): string;
|
|
37
47
|
/**
|
|
38
48
|
* Generate queries barrel including custom query operations
|
|
39
49
|
*/
|
package/core/codegen/barrel.js
CHANGED
|
@@ -37,6 +37,7 @@ exports.generateQueriesBarrel = generateQueriesBarrel;
|
|
|
37
37
|
exports.generateMutationsBarrel = generateMutationsBarrel;
|
|
38
38
|
exports.generateMainBarrel = generateMainBarrel;
|
|
39
39
|
exports.generateRootBarrel = generateRootBarrel;
|
|
40
|
+
exports.generateMultiTargetBarrel = generateMultiTargetBarrel;
|
|
40
41
|
exports.generateCustomQueriesBarrel = generateCustomQueriesBarrel;
|
|
41
42
|
exports.generateCustomMutationsBarrel = generateCustomMutationsBarrel;
|
|
42
43
|
/**
|
|
@@ -200,6 +201,49 @@ function generateRootBarrel(options = {}) {
|
|
|
200
201
|
return (0, babel_ast_1.generateCode)(statements);
|
|
201
202
|
}
|
|
202
203
|
// ============================================================================
|
|
204
|
+
// Multi-target root barrel (re-exports each target as a namespace)
|
|
205
|
+
// ============================================================================
|
|
206
|
+
/** JS reserved words that need an alias when used as export names */
|
|
207
|
+
const JS_RESERVED = new Set([
|
|
208
|
+
'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch',
|
|
209
|
+
'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
|
210
|
+
'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
|
|
211
|
+
'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import',
|
|
212
|
+
'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new',
|
|
213
|
+
'null', 'package', 'private', 'protected', 'public', 'return', 'short',
|
|
214
|
+
'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws',
|
|
215
|
+
'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while',
|
|
216
|
+
'with', 'yield',
|
|
217
|
+
]);
|
|
218
|
+
/**
|
|
219
|
+
* Generate a root index.ts for multi-target output that re-exports each
|
|
220
|
+
* target as a namespace.
|
|
221
|
+
*
|
|
222
|
+
* Example output:
|
|
223
|
+
* export * as admin from './admin';
|
|
224
|
+
* export * as auth from './auth';
|
|
225
|
+
* export * as public_ from './public';
|
|
226
|
+
*/
|
|
227
|
+
function generateMultiTargetBarrel(targetNames) {
|
|
228
|
+
const statements = [];
|
|
229
|
+
for (const name of targetNames) {
|
|
230
|
+
const alias = JS_RESERVED.has(name) ? `${name}_` : name;
|
|
231
|
+
const exportDecl = t.exportNamedDeclaration(null, [t.exportNamespaceSpecifier(t.identifier(alias))], t.stringLiteral(`./${name}`));
|
|
232
|
+
statements.push(exportDecl);
|
|
233
|
+
}
|
|
234
|
+
if (statements.length > 0) {
|
|
235
|
+
(0, babel_ast_1.addJSDocComment)(statements[0], [
|
|
236
|
+
'@constructive-io/sdk',
|
|
237
|
+
'',
|
|
238
|
+
'Auto-generated GraphQL types and ORM client.',
|
|
239
|
+
'Run `pnpm run generate` to populate this package from the schema files.',
|
|
240
|
+
'',
|
|
241
|
+
'@generated by @constructive-io/graphql-codegen',
|
|
242
|
+
]);
|
|
243
|
+
}
|
|
244
|
+
return (0, babel_ast_1.generateCode)(statements);
|
|
245
|
+
}
|
|
246
|
+
// ============================================================================
|
|
203
247
|
// Custom operation barrels (includes both table and custom hooks)
|
|
204
248
|
// ============================================================================
|
|
205
249
|
/**
|
|
@@ -718,10 +718,33 @@ function getFilterTypeForField(fieldType, isArray = false) {
|
|
|
718
718
|
return (0, scalars_1.scalarToFilterType)(fieldType, isArray) ?? 'StringFilter';
|
|
719
719
|
}
|
|
720
720
|
/**
|
|
721
|
-
* Build properties for a table filter interface
|
|
721
|
+
* Build properties for a table filter interface.
|
|
722
|
+
*
|
|
723
|
+
* When typeRegistry is available, uses the schema's filter input type as
|
|
724
|
+
* the sole source of truth — this captures plugin-injected filter fields
|
|
725
|
+
* (e.g., bm25Body, tsvTsv, trgmName, vectorEmbedding, geom) that are not
|
|
726
|
+
* present on the entity type itself. Same pattern as buildOrderByValues().
|
|
722
727
|
*/
|
|
723
|
-
function buildTableFilterProperties(table) {
|
|
728
|
+
function buildTableFilterProperties(table, typeRegistry) {
|
|
724
729
|
const filterName = (0, utils_1.getFilterTypeName)(table);
|
|
730
|
+
// When the schema's filter type is available, use it as the source of truth
|
|
731
|
+
if (typeRegistry) {
|
|
732
|
+
const filterType = typeRegistry.get(filterName);
|
|
733
|
+
if (filterType?.kind === 'INPUT_OBJECT' && filterType.inputFields) {
|
|
734
|
+
const properties = [];
|
|
735
|
+
for (const field of filterType.inputFields) {
|
|
736
|
+
const tsType = typeRefToTs(field.type);
|
|
737
|
+
properties.push({
|
|
738
|
+
name: field.name,
|
|
739
|
+
type: tsType,
|
|
740
|
+
optional: true,
|
|
741
|
+
description: (0, utils_1.stripSmartComments)(field.description, true),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
return properties;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
// Fallback: derive from table fields when schema filter type is not available
|
|
725
748
|
const properties = [];
|
|
726
749
|
for (const field of table.fields) {
|
|
727
750
|
const fieldType = typeof field.type === 'string' ? field.type : field.type.gqlType;
|
|
@@ -740,11 +763,11 @@ function buildTableFilterProperties(table) {
|
|
|
740
763
|
/**
|
|
741
764
|
* Generate table filter type statements
|
|
742
765
|
*/
|
|
743
|
-
function generateTableFilterTypes(tables) {
|
|
766
|
+
function generateTableFilterTypes(tables, typeRegistry) {
|
|
744
767
|
const statements = [];
|
|
745
768
|
for (const table of tables) {
|
|
746
769
|
const filterName = (0, utils_1.getFilterTypeName)(table);
|
|
747
|
-
statements.push(createExportedInterface(filterName, buildTableFilterProperties(table)));
|
|
770
|
+
statements.push(createExportedInterface(filterName, buildTableFilterProperties(table, typeRegistry)));
|
|
748
771
|
}
|
|
749
772
|
if (statements.length > 0) {
|
|
750
773
|
addSectionComment(statements, 'Table Filter Types');
|
|
@@ -1323,6 +1346,43 @@ function generateConnectionFieldsMap(tables, tableByName) {
|
|
|
1323
1346
|
// ============================================================================
|
|
1324
1347
|
// Plugin-Injected Type Collector
|
|
1325
1348
|
// ============================================================================
|
|
1349
|
+
/**
|
|
1350
|
+
* Collect extra input type names referenced by plugin-injected filter fields.
|
|
1351
|
+
*
|
|
1352
|
+
* When the schema's filter type is used as source of truth, plugin-injected
|
|
1353
|
+
* fields reference custom filter types (e.g., Bm25BodyFilter, TsvectorFilter,
|
|
1354
|
+
* GeometryFilter) that also need to be generated. This function discovers
|
|
1355
|
+
* those types by comparing the schema's filter type fields against the
|
|
1356
|
+
* standard scalar filter types.
|
|
1357
|
+
*/
|
|
1358
|
+
function collectFilterExtraInputTypes(tables, typeRegistry) {
|
|
1359
|
+
const extraTypes = new Set();
|
|
1360
|
+
for (const table of tables) {
|
|
1361
|
+
const filterTypeName = (0, utils_1.getFilterTypeName)(table);
|
|
1362
|
+
const filterType = typeRegistry.get(filterTypeName);
|
|
1363
|
+
if (!filterType ||
|
|
1364
|
+
filterType.kind !== 'INPUT_OBJECT' ||
|
|
1365
|
+
!filterType.inputFields) {
|
|
1366
|
+
continue;
|
|
1367
|
+
}
|
|
1368
|
+
const tableFieldNames = new Set(table.fields
|
|
1369
|
+
.filter((f) => !(0, utils_1.isRelationField)(f.name, table))
|
|
1370
|
+
.map((f) => f.name));
|
|
1371
|
+
for (const field of filterType.inputFields) {
|
|
1372
|
+
// Skip standard column-derived fields and logical operators
|
|
1373
|
+
if (tableFieldNames.has(field.name))
|
|
1374
|
+
continue;
|
|
1375
|
+
if (['and', 'or', 'not'].includes(field.name))
|
|
1376
|
+
continue;
|
|
1377
|
+
// Collect the base type name of this extra field
|
|
1378
|
+
const baseName = (0, type_resolver_1.getTypeBaseName)(field.type);
|
|
1379
|
+
if (baseName && !scalars_1.SCALAR_NAMES.has(baseName)) {
|
|
1380
|
+
extraTypes.add(baseName);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return extraTypes;
|
|
1385
|
+
}
|
|
1326
1386
|
/**
|
|
1327
1387
|
* Collect extra input type names referenced by plugin-injected condition fields.
|
|
1328
1388
|
*
|
|
@@ -1386,7 +1446,9 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
|
|
|
1386
1446
|
statements.push(...generateEntityWithRelations(tablesList));
|
|
1387
1447
|
statements.push(...generateEntitySelectTypes(tablesList, tableByName));
|
|
1388
1448
|
// 4. Table filter types
|
|
1389
|
-
|
|
1449
|
+
// Pass typeRegistry to use schema's filter type as source of truth,
|
|
1450
|
+
// capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
|
|
1451
|
+
statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
|
|
1390
1452
|
// 4b. Table condition types (simple equality filter)
|
|
1391
1453
|
// Pass typeRegistry to merge plugin-injected condition fields
|
|
1392
1454
|
// (e.g., vectorEmbedding from VectorSearchPlugin)
|
|
@@ -1404,8 +1466,14 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
|
|
|
1404
1466
|
// Always emit this export so generated model/custom-op imports stay valid.
|
|
1405
1467
|
statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
|
|
1406
1468
|
// 7. Custom input types from TypeRegistry
|
|
1407
|
-
// Also include any extra types referenced by plugin-injected condition fields
|
|
1469
|
+
// Also include any extra types referenced by plugin-injected filter/condition fields
|
|
1408
1470
|
const mergedUsedInputTypes = new Set(usedInputTypes);
|
|
1471
|
+
if (hasTables) {
|
|
1472
|
+
const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
|
|
1473
|
+
for (const typeName of filterExtraTypes) {
|
|
1474
|
+
mergedUsedInputTypes.add(typeName);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1409
1477
|
if (hasTables && conditionEnabled) {
|
|
1410
1478
|
const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
|
|
1411
1479
|
for (const typeName of conditionExtraTypes) {
|
|
@@ -7,7 +7,7 @@ export interface GeneratedModelFile {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function generateModelFile(table: Table, _useSharedTypes: boolean, options?: {
|
|
9
9
|
condition?: boolean;
|
|
10
|
-
}): GeneratedModelFile;
|
|
10
|
+
}, allTables?: Table[]): GeneratedModelFile;
|
|
11
11
|
export declare function generateAllModelFiles(tables: Table[], useSharedTypes: boolean, options?: {
|
|
12
12
|
condition?: boolean;
|
|
13
13
|
}): GeneratedModelFile[];
|
|
@@ -42,6 +42,7 @@ exports.generateAllModelFiles = generateAllModelFiles;
|
|
|
42
42
|
* Each method uses function overloads for IDE autocompletion of select objects.
|
|
43
43
|
*/
|
|
44
44
|
const t = __importStar(require("@babel/types"));
|
|
45
|
+
const inflekt_1 = require("inflekt");
|
|
45
46
|
const babel_ast_1 = require("../babel-ast");
|
|
46
47
|
const utils_1 = require("../utils");
|
|
47
48
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
@@ -102,7 +103,7 @@ function strictSelectGuard(selectTypeName) {
|
|
|
102
103
|
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
103
104
|
]));
|
|
104
105
|
}
|
|
105
|
-
function generateModelFile(table, _useSharedTypes, options) {
|
|
106
|
+
function generateModelFile(table, _useSharedTypes, options, allTables) {
|
|
106
107
|
const conditionEnabled = options?.condition !== false;
|
|
107
108
|
const { typeName, singularName, pluralName } = (0, utils_1.getTableNames)(table);
|
|
108
109
|
const modelName = `${typeName}Model`;
|
|
@@ -116,7 +117,6 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
116
117
|
const orderByTypeName = (0, utils_1.getOrderByTypeName)(table);
|
|
117
118
|
const createInputTypeName = `Create${typeName}Input`;
|
|
118
119
|
const updateInputTypeName = `Update${typeName}Input`;
|
|
119
|
-
const deleteInputTypeName = `Delete${typeName}Input`;
|
|
120
120
|
const patchTypeName = `${typeName}Patch`;
|
|
121
121
|
const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
|
|
122
122
|
const pkField = pkFields[0];
|
|
@@ -126,9 +126,16 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
126
126
|
const createMutationName = table.query?.create ?? `create${typeName}`;
|
|
127
127
|
const updateMutationName = table.query?.update;
|
|
128
128
|
const deleteMutationName = table.query?.delete;
|
|
129
|
+
const deleteInputTypeName = (0, utils_1.getDeleteInputTypeName)(table);
|
|
129
130
|
const statements = [];
|
|
130
131
|
statements.push(createImportDeclaration('../client', ['OrmClient']));
|
|
131
|
-
|
|
132
|
+
const m2nRels = table.relations.manyToMany.filter((r) => r.junctionLeftKeyFields?.length && r.junctionRightKeyFields?.length);
|
|
133
|
+
// Check if any remove methods will actually be generated (need junction table with delete mutation)
|
|
134
|
+
const needsJunctionRemove = m2nRels.some((r) => {
|
|
135
|
+
const jt = allTables?.find((tb) => tb.name === r.junctionTable);
|
|
136
|
+
return jt?.query?.delete != null;
|
|
137
|
+
});
|
|
138
|
+
const queryBuilderImports = [
|
|
132
139
|
'QueryBuilder',
|
|
133
140
|
'buildFindManyDocument',
|
|
134
141
|
'buildFindFirstDocument',
|
|
@@ -136,7 +143,9 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
136
143
|
'buildCreateDocument',
|
|
137
144
|
'buildUpdateByPkDocument',
|
|
138
145
|
'buildDeleteByPkDocument',
|
|
139
|
-
|
|
146
|
+
...(needsJunctionRemove ? ['buildJunctionRemoveDocument'] : []),
|
|
147
|
+
];
|
|
148
|
+
statements.push(createImportDeclaration('../query-builder', queryBuilderImports));
|
|
140
149
|
statements.push(createImportDeclaration('../select-types', [
|
|
141
150
|
'ConnectionResult',
|
|
142
151
|
'FindManyArgs',
|
|
@@ -429,13 +438,12 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
429
438
|
}
|
|
430
439
|
// ── delete ─────────────────────────────────────────────────────────────
|
|
431
440
|
if (deleteMutationName) {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
]);
|
|
441
|
+
// Build where type with ALL PK fields (supports composite PKs)
|
|
442
|
+
const whereLiteral = () => t.tsTypeLiteral(pkFields.map((pk) => {
|
|
443
|
+
const prop = t.tsPropertySignature(t.identifier(pk.name), t.tsTypeAnnotation(tsTypeFromPrimitive(pk.tsType ?? 'string')));
|
|
444
|
+
prop.optional = false;
|
|
445
|
+
return prop;
|
|
446
|
+
}));
|
|
439
447
|
const argsType = (sel) => t.tsTypeReference(t.identifier('DeleteArgs'), t.tsTypeParameterInstantiation([whereLiteral(), sel]));
|
|
440
448
|
const retType = (sel) => t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
|
|
441
449
|
t.tsTypeLiteral([
|
|
@@ -454,18 +462,144 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
454
462
|
strictSelectGuard(selectTypeName),
|
|
455
463
|
]));
|
|
456
464
|
const selectExpr = t.memberExpression(t.identifier('args'), t.identifier('select'));
|
|
465
|
+
// Build keys object: { field1: args.where.field1, field2: args.where.field2, ... }
|
|
466
|
+
const keysObj = t.objectExpression(pkFields.map((pk) => t.objectProperty(t.identifier(pk.name), t.memberExpression(t.memberExpression(t.identifier('args'), t.identifier('where')), t.identifier(pk.name)))));
|
|
457
467
|
const bodyArgs = [
|
|
458
468
|
t.stringLiteral(typeName),
|
|
459
469
|
t.stringLiteral(deleteMutationName),
|
|
460
470
|
t.stringLiteral(entityLower),
|
|
461
|
-
|
|
471
|
+
keysObj,
|
|
462
472
|
t.stringLiteral(deleteInputTypeName),
|
|
463
|
-
t.stringLiteral(pkField.name),
|
|
464
473
|
selectExpr,
|
|
465
474
|
t.identifier('connectionFieldsMap'),
|
|
466
475
|
];
|
|
467
476
|
classBody.push(createClassMethod('delete', createTypeParam(selectTypeName), [implParam], retType(sRef()), buildMethodBody('buildDeleteByPkDocument', bodyArgs, 'mutation', typeName, deleteMutationName)));
|
|
468
477
|
}
|
|
478
|
+
// ── M:N add/remove methods ────────────────────────────────────────────
|
|
479
|
+
for (const rel of m2nRels) {
|
|
480
|
+
if (!rel.fieldName)
|
|
481
|
+
continue;
|
|
482
|
+
const junctionTable = allTables?.find((tb) => tb.name === rel.junctionTable);
|
|
483
|
+
if (!junctionTable)
|
|
484
|
+
continue;
|
|
485
|
+
const junctionNames = (0, utils_1.getTableNames)(junctionTable);
|
|
486
|
+
const junctionCreateMutation = (0, utils_1.getCreateMutationName)(junctionTable);
|
|
487
|
+
const junctionCreateInputType = (0, utils_1.getCreateInputTypeName)(junctionTable);
|
|
488
|
+
const junctionDeleteMutation = junctionTable.query?.delete ?? (0, utils_1.getDeleteMutationName)(junctionTable);
|
|
489
|
+
const junctionDeleteInputType = (0, utils_1.getDeleteInputTypeName)(junctionTable);
|
|
490
|
+
const junctionSingular = junctionNames.singularName;
|
|
491
|
+
// Derive a friendly singular name from the fieldName (e.g., "tags" → "Tag", "categories" → "Category")
|
|
492
|
+
const relSingular = (0, utils_1.ucFirst)((0, inflekt_1.singularize)(rel.fieldName));
|
|
493
|
+
const leftKeys = rel.junctionLeftKeyFields;
|
|
494
|
+
const rightKeys = rel.junctionRightKeyFields;
|
|
495
|
+
const leftPkFields = rel.leftKeyFields ?? ['id'];
|
|
496
|
+
const rightPkFields = rel.rightKeyFields ?? ['id'];
|
|
497
|
+
// Resolve actual PK types from left (current) and right tables
|
|
498
|
+
const leftPkInfo = (0, utils_1.getPrimaryKeyInfo)(table);
|
|
499
|
+
const rightTable = allTables?.find((tb) => tb.name === rel.rightTable);
|
|
500
|
+
const rightPkInfo = rightTable ? (0, utils_1.getPrimaryKeyInfo)(rightTable) : [];
|
|
501
|
+
// ── add<Relation> ───────────────────────────────────────────────
|
|
502
|
+
{
|
|
503
|
+
// Parameters: one param per left PK + one param per right PK, with actual types
|
|
504
|
+
const params = [];
|
|
505
|
+
for (let i = 0; i < leftPkFields.length; i++) {
|
|
506
|
+
const p = t.identifier(leftPkFields[i]);
|
|
507
|
+
const pkInfo = leftPkInfo.find((pk) => pk.name === leftPkFields[i]);
|
|
508
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
509
|
+
params.push(p);
|
|
510
|
+
}
|
|
511
|
+
for (let i = 0; i < rightPkFields.length; i++) {
|
|
512
|
+
const rk = rightPkFields[i];
|
|
513
|
+
const p = t.identifier(rk === leftPkFields[0] ? `right${(0, utils_1.ucFirst)(rk)}` : rk);
|
|
514
|
+
const pkInfo = rightPkInfo.find((pk) => pk.name === rk);
|
|
515
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
516
|
+
params.push(p);
|
|
517
|
+
}
|
|
518
|
+
// Build the junction row data object: { junctionLeftKey: leftPk, junctionRightKey: rightPk }
|
|
519
|
+
const dataProps = [];
|
|
520
|
+
for (let i = 0; i < leftKeys.length; i++) {
|
|
521
|
+
dataProps.push(t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)));
|
|
522
|
+
}
|
|
523
|
+
for (let i = 0; i < rightKeys.length; i++) {
|
|
524
|
+
dataProps.push(t.objectProperty(t.identifier(rightKeys[i]), t.identifier(params[leftPkFields.length + i].name)));
|
|
525
|
+
}
|
|
526
|
+
const body = [
|
|
527
|
+
t.variableDeclaration('const', [
|
|
528
|
+
t.variableDeclarator(t.objectPattern([
|
|
529
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
530
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
531
|
+
]), t.callExpression(t.identifier('buildCreateDocument'), [
|
|
532
|
+
t.stringLiteral(junctionNames.typeName),
|
|
533
|
+
t.stringLiteral(junctionCreateMutation),
|
|
534
|
+
t.stringLiteral(junctionSingular),
|
|
535
|
+
t.objectExpression([t.objectProperty(t.identifier('id'), t.booleanLiteral(true))]),
|
|
536
|
+
t.objectExpression(dataProps),
|
|
537
|
+
t.stringLiteral(junctionCreateInputType),
|
|
538
|
+
])),
|
|
539
|
+
]),
|
|
540
|
+
t.returnStatement(t.newExpression(t.identifier('QueryBuilder'), [
|
|
541
|
+
t.objectExpression([
|
|
542
|
+
t.objectProperty(t.identifier('client'), t.memberExpression(t.thisExpression(), t.identifier('client'))),
|
|
543
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
|
|
544
|
+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
|
|
545
|
+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionCreateMutation)),
|
|
546
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
547
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
548
|
+
]),
|
|
549
|
+
])),
|
|
550
|
+
];
|
|
551
|
+
classBody.push(t.classMethod('method', t.identifier(`add${relSingular}`), params, t.blockStatement(body)));
|
|
552
|
+
}
|
|
553
|
+
// ── remove<Relation> ────────────────────────────────────────────
|
|
554
|
+
if (junctionTable.query?.delete) {
|
|
555
|
+
const params = [];
|
|
556
|
+
for (let i = 0; i < leftPkFields.length; i++) {
|
|
557
|
+
const p = t.identifier(leftPkFields[i]);
|
|
558
|
+
const pkInfo = leftPkInfo.find((pk) => pk.name === leftPkFields[i]);
|
|
559
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
560
|
+
params.push(p);
|
|
561
|
+
}
|
|
562
|
+
for (let i = 0; i < rightPkFields.length; i++) {
|
|
563
|
+
const rk = rightPkFields[i];
|
|
564
|
+
const p = t.identifier(rk === leftPkFields[0] ? `right${(0, utils_1.ucFirst)(rk)}` : rk);
|
|
565
|
+
const pkInfo = rightPkInfo.find((pk) => pk.name === rk);
|
|
566
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
567
|
+
params.push(p);
|
|
568
|
+
}
|
|
569
|
+
// Build the keys object for junction delete
|
|
570
|
+
const keysProps = [];
|
|
571
|
+
for (let i = 0; i < leftKeys.length; i++) {
|
|
572
|
+
keysProps.push(t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)));
|
|
573
|
+
}
|
|
574
|
+
for (let i = 0; i < rightKeys.length; i++) {
|
|
575
|
+
keysProps.push(t.objectProperty(t.identifier(rightKeys[i]), t.identifier(params[leftPkFields.length + i].name)));
|
|
576
|
+
}
|
|
577
|
+
const body = [
|
|
578
|
+
t.variableDeclaration('const', [
|
|
579
|
+
t.variableDeclarator(t.objectPattern([
|
|
580
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
581
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
582
|
+
]), t.callExpression(t.identifier('buildJunctionRemoveDocument'), [
|
|
583
|
+
t.stringLiteral(junctionNames.typeName),
|
|
584
|
+
t.stringLiteral(junctionDeleteMutation),
|
|
585
|
+
t.objectExpression(keysProps),
|
|
586
|
+
t.stringLiteral(junctionDeleteInputType),
|
|
587
|
+
])),
|
|
588
|
+
]),
|
|
589
|
+
t.returnStatement(t.newExpression(t.identifier('QueryBuilder'), [
|
|
590
|
+
t.objectExpression([
|
|
591
|
+
t.objectProperty(t.identifier('client'), t.memberExpression(t.thisExpression(), t.identifier('client'))),
|
|
592
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
|
|
593
|
+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
|
|
594
|
+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionDeleteMutation)),
|
|
595
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
596
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
597
|
+
]),
|
|
598
|
+
])),
|
|
599
|
+
];
|
|
600
|
+
classBody.push(t.classMethod('method', t.identifier(`remove${relSingular}`), params, t.blockStatement(body)));
|
|
601
|
+
}
|
|
602
|
+
}
|
|
469
603
|
const classDecl = t.classDeclaration(t.identifier(modelName), null, t.classBody(classBody));
|
|
470
604
|
statements.push(t.exportNamedDeclaration(classDecl));
|
|
471
605
|
const header = (0, utils_1.getGeneratedFileHeader)(`${typeName} model for ORM client`);
|
|
@@ -478,5 +612,5 @@ function generateModelFile(table, _useSharedTypes, options) {
|
|
|
478
612
|
};
|
|
479
613
|
}
|
|
480
614
|
function generateAllModelFiles(tables, useSharedTypes, options) {
|
|
481
|
-
return tables.map((table) => generateModelFile(table, useSharedTypes, options));
|
|
615
|
+
return tables.map((table) => generateModelFile(table, useSharedTypes, options, tables));
|
|
482
616
|
}
|
|
@@ -621,9 +621,8 @@ export function buildDeleteByPkDocument<TSelect = undefined>(
|
|
|
621
621
|
operationName: string,
|
|
622
622
|
mutationField: string,
|
|
623
623
|
entityField: string,
|
|
624
|
-
|
|
624
|
+
keys: Record<string, unknown>,
|
|
625
625
|
inputTypeName: string,
|
|
626
|
-
idFieldName: string,
|
|
627
626
|
select?: TSelect,
|
|
628
627
|
connectionFieldsMap?: Record<string, Record<string, string>>,
|
|
629
628
|
): { document: string; variables: Record<string, unknown> } {
|
|
@@ -648,9 +647,26 @@ export function buildDeleteByPkDocument<TSelect = undefined>(
|
|
|
648
647
|
],
|
|
649
648
|
}),
|
|
650
649
|
variables: {
|
|
651
|
-
input:
|
|
652
|
-
|
|
653
|
-
|
|
650
|
+
input: keys,
|
|
651
|
+
},
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
export function buildJunctionRemoveDocument(
|
|
656
|
+
operationName: string,
|
|
657
|
+
mutationField: string,
|
|
658
|
+
keys: Record<string, unknown>,
|
|
659
|
+
inputTypeName: string,
|
|
660
|
+
): { document: string; variables: Record<string, unknown> } {
|
|
661
|
+
return {
|
|
662
|
+
document: buildInputMutationDocument({
|
|
663
|
+
operationName,
|
|
664
|
+
mutationField,
|
|
665
|
+
inputTypeName,
|
|
666
|
+
resultSelections: [t.field({ name: 'clientMutationId' })],
|
|
667
|
+
}),
|
|
668
|
+
variables: {
|
|
669
|
+
input: keys,
|
|
654
670
|
},
|
|
655
671
|
};
|
|
656
672
|
}
|
package/core/codegen/utils.d.ts
CHANGED
|
@@ -118,12 +118,14 @@ export declare function getCreateInputTypeName(table: Table): string;
|
|
|
118
118
|
export declare function getPatchTypeName(table: Table): string;
|
|
119
119
|
/**
|
|
120
120
|
* Get PostGraphile update input type name
|
|
121
|
-
*
|
|
121
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
122
|
+
* like UpdatePostTagByPostIdAndTagIdInput), falls back to Update${Entity}Input.
|
|
122
123
|
*/
|
|
123
124
|
export declare function getUpdateInputTypeName(table: Table): string;
|
|
124
125
|
/**
|
|
125
126
|
* Get PostGraphile delete input type name
|
|
126
|
-
*
|
|
127
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
128
|
+
* like DeletePostTagByPostIdAndTagIdInput), falls back to Delete${Entity}Input.
|
|
127
129
|
*/
|
|
128
130
|
export declare function getDeleteInputTypeName(table: Table): string;
|
|
129
131
|
/**
|
package/core/codegen/utils.js
CHANGED
|
@@ -244,17 +244,21 @@ function getPatchTypeName(table) {
|
|
|
244
244
|
}
|
|
245
245
|
/**
|
|
246
246
|
* Get PostGraphile update input type name
|
|
247
|
-
*
|
|
247
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
248
|
+
* like UpdatePostTagByPostIdAndTagIdInput), falls back to Update${Entity}Input.
|
|
248
249
|
*/
|
|
249
250
|
function getUpdateInputTypeName(table) {
|
|
250
|
-
|
|
251
|
+
const mutationName = table.query?.update;
|
|
252
|
+
return mutationName ? ucFirst(mutationName) + 'Input' : `Update${table.name}Input`;
|
|
251
253
|
}
|
|
252
254
|
/**
|
|
253
255
|
* Get PostGraphile delete input type name
|
|
254
|
-
*
|
|
256
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
257
|
+
* like DeletePostTagByPostIdAndTagIdInput), falls back to Delete${Entity}Input.
|
|
255
258
|
*/
|
|
256
259
|
function getDeleteInputTypeName(table) {
|
|
257
|
-
|
|
260
|
+
const mutationName = table.query?.delete;
|
|
261
|
+
return mutationName ? ucFirst(mutationName) + 'Input' : `Delete${table.name}Input`;
|
|
258
262
|
}
|
|
259
263
|
// ============================================================================
|
|
260
264
|
// Type mapping: GraphQL → TypeScript
|
package/core/generate.js
CHANGED
|
@@ -675,11 +675,23 @@ async function generateMulti(options) {
|
|
|
675
675
|
await writeFiles(cliSkillsToWrite, skillsOutputDir, [], { pruneStaleFiles: false });
|
|
676
676
|
}
|
|
677
677
|
}
|
|
678
|
-
// Generate root-root README if multi-target
|
|
678
|
+
// Generate root-root README and barrel if multi-target
|
|
679
679
|
if (names.length > 1 && targetInfos.length > 0 && !dryRun) {
|
|
680
|
-
const rootReadme = (0, target_docs_generator_1.generateRootRootReadme)(targetInfos);
|
|
681
680
|
const { writeGeneratedFiles: writeFiles } = await Promise.resolve().then(() => __importStar(require('./output')));
|
|
681
|
+
const rootReadme = (0, target_docs_generator_1.generateRootRootReadme)(targetInfos);
|
|
682
682
|
await writeFiles([{ path: rootReadme.fileName, content: rootReadme.content }], '.', [], { pruneStaleFiles: false });
|
|
683
|
+
// Write a root barrel (index.ts) that re-exports each target as a
|
|
684
|
+
// namespace so the package has a single entry-point. Derive the
|
|
685
|
+
// common output root from the first target's output path.
|
|
686
|
+
const successfulNames = results
|
|
687
|
+
.filter((r) => r.result.success)
|
|
688
|
+
.map((r) => r.name);
|
|
689
|
+
if (successfulNames.length > 0) {
|
|
690
|
+
const firstOutput = (0, config_1.getConfigOptions)(configs[successfulNames[0]]).output;
|
|
691
|
+
const outputRoot = node_path_1.default.dirname(firstOutput);
|
|
692
|
+
const barrelContent = (0, barrel_1.generateMultiTargetBarrel)(successfulNames);
|
|
693
|
+
await writeFiles([{ path: 'index.ts', content: barrelContent }], outputRoot, [], { pruneStaleFiles: false });
|
|
694
|
+
}
|
|
683
695
|
}
|
|
684
696
|
}
|
|
685
697
|
finally {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M:N Relation Enrichment
|
|
3
|
+
*
|
|
4
|
+
* After table inference from introspection, enriches ManyToManyRelation objects
|
|
5
|
+
* with junction key field metadata from _cachedTablesMeta (MetaSchemaPlugin).
|
|
6
|
+
*/
|
|
7
|
+
import type { Table } from '../../types/schema';
|
|
8
|
+
import type { MetaTableInfo } from './source/types';
|
|
9
|
+
/**
|
|
10
|
+
* Enrich M:N relations with junction key field metadata from _meta.
|
|
11
|
+
* Mutates the tables array in-place.
|
|
12
|
+
*/
|
|
13
|
+
export declare function enrichManyToManyRelations(tables: Table[], tablesMeta?: MetaTableInfo[]): void;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.enrichManyToManyRelations = enrichManyToManyRelations;
|
|
4
|
+
/**
|
|
5
|
+
* Enrich M:N relations with junction key field metadata from _meta.
|
|
6
|
+
* Mutates the tables array in-place.
|
|
7
|
+
*/
|
|
8
|
+
function enrichManyToManyRelations(tables, tablesMeta) {
|
|
9
|
+
if (!tablesMeta?.length)
|
|
10
|
+
return;
|
|
11
|
+
const metaByName = new Map(tablesMeta.map((m) => [m.name, m]));
|
|
12
|
+
for (const table of tables) {
|
|
13
|
+
const meta = metaByName.get(table.name);
|
|
14
|
+
if (!meta?.relations.manyToMany.length)
|
|
15
|
+
continue;
|
|
16
|
+
for (const rel of table.relations.manyToMany) {
|
|
17
|
+
const metaRel = meta.relations.manyToMany.find((m) => m.fieldName === rel.fieldName);
|
|
18
|
+
if (!metaRel)
|
|
19
|
+
continue;
|
|
20
|
+
rel.junctionLeftKeyFields = metaRel.junctionLeftKeyAttributes.map((a) => a.name);
|
|
21
|
+
rel.junctionRightKeyFields = metaRel.junctionRightKeyAttributes.map((a) => a.name);
|
|
22
|
+
rel.leftKeyFields = metaRel.leftKeyAttributes.map((a) => a.name);
|
|
23
|
+
rel.rightKeyFields = metaRel.rightKeyAttributes.map((a) => a.name);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
package/core/pipeline/index.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validateSourceOptions = exports.createSchemaSource = void 0;
|
|
4
4
|
exports.runCodegenPipeline = runCodegenPipeline;
|
|
5
5
|
exports.validateTablesFound = validateTablesFound;
|
|
6
|
+
const enrich_relations_1 = require("../introspect/enrich-relations");
|
|
6
7
|
const infer_tables_1 = require("../introspect/infer-tables");
|
|
7
8
|
const transform_1 = require("../introspect/transform");
|
|
8
9
|
const transform_schema_1 = require("../introspect/transform-schema");
|
|
@@ -27,13 +28,18 @@ async function runCodegenPipeline(options) {
|
|
|
27
28
|
const log = verbose ? console.log : () => { };
|
|
28
29
|
// 1. Fetch introspection from source
|
|
29
30
|
log(`Fetching schema from ${source.describe()}...`);
|
|
30
|
-
const { introspection } = await source.fetch();
|
|
31
|
+
const { introspection, tablesMeta } = await source.fetch();
|
|
31
32
|
// 2. Infer tables from introspection (replaces _meta)
|
|
32
33
|
log('Inferring table metadata from schema...');
|
|
33
34
|
const commentsEnabled = config.codegen?.comments !== false;
|
|
34
35
|
let tables = (0, infer_tables_1.inferTablesFromIntrospection)(introspection, { comments: commentsEnabled });
|
|
35
36
|
const totalTables = tables.length;
|
|
36
37
|
log(` Found ${totalTables} tables`);
|
|
38
|
+
// 2a. Enrich M:N relations with junction key metadata from _meta
|
|
39
|
+
if (tablesMeta?.length) {
|
|
40
|
+
(0, enrich_relations_1.enrichManyToManyRelations)(tables, tablesMeta);
|
|
41
|
+
log(` Enriched M:N relations from _meta (${tablesMeta.length} tables)`);
|
|
42
|
+
}
|
|
37
43
|
// 3. Filter tables by config (combine exclude and systemExclude)
|
|
38
44
|
tables = (0, transform_1.filterTables)(tables, config.tables.include, [
|
|
39
45
|
...config.tables.exclude,
|
|
@@ -34,6 +34,16 @@ export interface RootBarrelOptions {
|
|
|
34
34
|
* Re-exports from subdirectories based on which generators are enabled.
|
|
35
35
|
*/
|
|
36
36
|
export declare function generateRootBarrel(options?: RootBarrelOptions): string;
|
|
37
|
+
/**
|
|
38
|
+
* Generate a root index.ts for multi-target output that re-exports each
|
|
39
|
+
* target as a namespace.
|
|
40
|
+
*
|
|
41
|
+
* Example output:
|
|
42
|
+
* export * as admin from './admin';
|
|
43
|
+
* export * as auth from './auth';
|
|
44
|
+
* export * as public_ from './public';
|
|
45
|
+
*/
|
|
46
|
+
export declare function generateMultiTargetBarrel(targetNames: string[]): string;
|
|
37
47
|
/**
|
|
38
48
|
* Generate queries barrel including custom query operations
|
|
39
49
|
*/
|
|
@@ -159,6 +159,49 @@ export function generateRootBarrel(options = {}) {
|
|
|
159
159
|
return generateCode(statements);
|
|
160
160
|
}
|
|
161
161
|
// ============================================================================
|
|
162
|
+
// Multi-target root barrel (re-exports each target as a namespace)
|
|
163
|
+
// ============================================================================
|
|
164
|
+
/** JS reserved words that need an alias when used as export names */
|
|
165
|
+
const JS_RESERVED = new Set([
|
|
166
|
+
'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch',
|
|
167
|
+
'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
|
|
168
|
+
'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
|
|
169
|
+
'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import',
|
|
170
|
+
'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new',
|
|
171
|
+
'null', 'package', 'private', 'protected', 'public', 'return', 'short',
|
|
172
|
+
'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws',
|
|
173
|
+
'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while',
|
|
174
|
+
'with', 'yield',
|
|
175
|
+
]);
|
|
176
|
+
/**
|
|
177
|
+
* Generate a root index.ts for multi-target output that re-exports each
|
|
178
|
+
* target as a namespace.
|
|
179
|
+
*
|
|
180
|
+
* Example output:
|
|
181
|
+
* export * as admin from './admin';
|
|
182
|
+
* export * as auth from './auth';
|
|
183
|
+
* export * as public_ from './public';
|
|
184
|
+
*/
|
|
185
|
+
export function generateMultiTargetBarrel(targetNames) {
|
|
186
|
+
const statements = [];
|
|
187
|
+
for (const name of targetNames) {
|
|
188
|
+
const alias = JS_RESERVED.has(name) ? `${name}_` : name;
|
|
189
|
+
const exportDecl = t.exportNamedDeclaration(null, [t.exportNamespaceSpecifier(t.identifier(alias))], t.stringLiteral(`./${name}`));
|
|
190
|
+
statements.push(exportDecl);
|
|
191
|
+
}
|
|
192
|
+
if (statements.length > 0) {
|
|
193
|
+
addJSDocComment(statements[0], [
|
|
194
|
+
'@constructive-io/sdk',
|
|
195
|
+
'',
|
|
196
|
+
'Auto-generated GraphQL types and ORM client.',
|
|
197
|
+
'Run `pnpm run generate` to populate this package from the schema files.',
|
|
198
|
+
'',
|
|
199
|
+
'@generated by @constructive-io/graphql-codegen',
|
|
200
|
+
]);
|
|
201
|
+
}
|
|
202
|
+
return generateCode(statements);
|
|
203
|
+
}
|
|
204
|
+
// ============================================================================
|
|
162
205
|
// Custom operation barrels (includes both table and custom hooks)
|
|
163
206
|
// ============================================================================
|
|
164
207
|
/**
|
|
@@ -680,10 +680,33 @@ function getFilterTypeForField(fieldType, isArray = false) {
|
|
|
680
680
|
return scalarToFilterType(fieldType, isArray) ?? 'StringFilter';
|
|
681
681
|
}
|
|
682
682
|
/**
|
|
683
|
-
* Build properties for a table filter interface
|
|
683
|
+
* Build properties for a table filter interface.
|
|
684
|
+
*
|
|
685
|
+
* When typeRegistry is available, uses the schema's filter input type as
|
|
686
|
+
* the sole source of truth — this captures plugin-injected filter fields
|
|
687
|
+
* (e.g., bm25Body, tsvTsv, trgmName, vectorEmbedding, geom) that are not
|
|
688
|
+
* present on the entity type itself. Same pattern as buildOrderByValues().
|
|
684
689
|
*/
|
|
685
|
-
function buildTableFilterProperties(table) {
|
|
690
|
+
function buildTableFilterProperties(table, typeRegistry) {
|
|
686
691
|
const filterName = getFilterTypeName(table);
|
|
692
|
+
// When the schema's filter type is available, use it as the source of truth
|
|
693
|
+
if (typeRegistry) {
|
|
694
|
+
const filterType = typeRegistry.get(filterName);
|
|
695
|
+
if (filterType?.kind === 'INPUT_OBJECT' && filterType.inputFields) {
|
|
696
|
+
const properties = [];
|
|
697
|
+
for (const field of filterType.inputFields) {
|
|
698
|
+
const tsType = typeRefToTs(field.type);
|
|
699
|
+
properties.push({
|
|
700
|
+
name: field.name,
|
|
701
|
+
type: tsType,
|
|
702
|
+
optional: true,
|
|
703
|
+
description: stripSmartComments(field.description, true),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
return properties;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Fallback: derive from table fields when schema filter type is not available
|
|
687
710
|
const properties = [];
|
|
688
711
|
for (const field of table.fields) {
|
|
689
712
|
const fieldType = typeof field.type === 'string' ? field.type : field.type.gqlType;
|
|
@@ -702,11 +725,11 @@ function buildTableFilterProperties(table) {
|
|
|
702
725
|
/**
|
|
703
726
|
* Generate table filter type statements
|
|
704
727
|
*/
|
|
705
|
-
function generateTableFilterTypes(tables) {
|
|
728
|
+
function generateTableFilterTypes(tables, typeRegistry) {
|
|
706
729
|
const statements = [];
|
|
707
730
|
for (const table of tables) {
|
|
708
731
|
const filterName = getFilterTypeName(table);
|
|
709
|
-
statements.push(createExportedInterface(filterName, buildTableFilterProperties(table)));
|
|
732
|
+
statements.push(createExportedInterface(filterName, buildTableFilterProperties(table, typeRegistry)));
|
|
710
733
|
}
|
|
711
734
|
if (statements.length > 0) {
|
|
712
735
|
addSectionComment(statements, 'Table Filter Types');
|
|
@@ -1285,6 +1308,43 @@ function generateConnectionFieldsMap(tables, tableByName) {
|
|
|
1285
1308
|
// ============================================================================
|
|
1286
1309
|
// Plugin-Injected Type Collector
|
|
1287
1310
|
// ============================================================================
|
|
1311
|
+
/**
|
|
1312
|
+
* Collect extra input type names referenced by plugin-injected filter fields.
|
|
1313
|
+
*
|
|
1314
|
+
* When the schema's filter type is used as source of truth, plugin-injected
|
|
1315
|
+
* fields reference custom filter types (e.g., Bm25BodyFilter, TsvectorFilter,
|
|
1316
|
+
* GeometryFilter) that also need to be generated. This function discovers
|
|
1317
|
+
* those types by comparing the schema's filter type fields against the
|
|
1318
|
+
* standard scalar filter types.
|
|
1319
|
+
*/
|
|
1320
|
+
function collectFilterExtraInputTypes(tables, typeRegistry) {
|
|
1321
|
+
const extraTypes = new Set();
|
|
1322
|
+
for (const table of tables) {
|
|
1323
|
+
const filterTypeName = getFilterTypeName(table);
|
|
1324
|
+
const filterType = typeRegistry.get(filterTypeName);
|
|
1325
|
+
if (!filterType ||
|
|
1326
|
+
filterType.kind !== 'INPUT_OBJECT' ||
|
|
1327
|
+
!filterType.inputFields) {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
const tableFieldNames = new Set(table.fields
|
|
1331
|
+
.filter((f) => !isRelationField(f.name, table))
|
|
1332
|
+
.map((f) => f.name));
|
|
1333
|
+
for (const field of filterType.inputFields) {
|
|
1334
|
+
// Skip standard column-derived fields and logical operators
|
|
1335
|
+
if (tableFieldNames.has(field.name))
|
|
1336
|
+
continue;
|
|
1337
|
+
if (['and', 'or', 'not'].includes(field.name))
|
|
1338
|
+
continue;
|
|
1339
|
+
// Collect the base type name of this extra field
|
|
1340
|
+
const baseName = getTypeBaseName(field.type);
|
|
1341
|
+
if (baseName && !SCALAR_NAMES.has(baseName)) {
|
|
1342
|
+
extraTypes.add(baseName);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return extraTypes;
|
|
1347
|
+
}
|
|
1288
1348
|
/**
|
|
1289
1349
|
* Collect extra input type names referenced by plugin-injected condition fields.
|
|
1290
1350
|
*
|
|
@@ -1348,7 +1408,9 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
|
|
|
1348
1408
|
statements.push(...generateEntityWithRelations(tablesList));
|
|
1349
1409
|
statements.push(...generateEntitySelectTypes(tablesList, tableByName));
|
|
1350
1410
|
// 4. Table filter types
|
|
1351
|
-
|
|
1411
|
+
// Pass typeRegistry to use schema's filter type as source of truth,
|
|
1412
|
+
// capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
|
|
1413
|
+
statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
|
|
1352
1414
|
// 4b. Table condition types (simple equality filter)
|
|
1353
1415
|
// Pass typeRegistry to merge plugin-injected condition fields
|
|
1354
1416
|
// (e.g., vectorEmbedding from VectorSearchPlugin)
|
|
@@ -1366,8 +1428,14 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
|
|
|
1366
1428
|
// Always emit this export so generated model/custom-op imports stay valid.
|
|
1367
1429
|
statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
|
|
1368
1430
|
// 7. Custom input types from TypeRegistry
|
|
1369
|
-
// Also include any extra types referenced by plugin-injected condition fields
|
|
1431
|
+
// Also include any extra types referenced by plugin-injected filter/condition fields
|
|
1370
1432
|
const mergedUsedInputTypes = new Set(usedInputTypes);
|
|
1433
|
+
if (hasTables) {
|
|
1434
|
+
const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
|
|
1435
|
+
for (const typeName of filterExtraTypes) {
|
|
1436
|
+
mergedUsedInputTypes.add(typeName);
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1371
1439
|
if (hasTables && conditionEnabled) {
|
|
1372
1440
|
const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
|
|
1373
1441
|
for (const typeName of conditionExtraTypes) {
|
|
@@ -7,7 +7,7 @@ export interface GeneratedModelFile {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function generateModelFile(table: Table, _useSharedTypes: boolean, options?: {
|
|
9
9
|
condition?: boolean;
|
|
10
|
-
}): GeneratedModelFile;
|
|
10
|
+
}, allTables?: Table[]): GeneratedModelFile;
|
|
11
11
|
export declare function generateAllModelFiles(tables: Table[], useSharedTypes: boolean, options?: {
|
|
12
12
|
condition?: boolean;
|
|
13
13
|
}): GeneratedModelFile[];
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* Each method uses function overloads for IDE autocompletion of select objects.
|
|
6
6
|
*/
|
|
7
7
|
import * as t from '@babel/types';
|
|
8
|
+
import { singularize } from 'inflekt';
|
|
8
9
|
import { generateCode } from '../babel-ast';
|
|
9
|
-
import { getFilterTypeName, getGeneratedFileHeader, getOrderByTypeName, getPrimaryKeyInfo, getSingleRowQueryName, getTableNames, hasValidPrimaryKey, lcFirst, } from '../utils';
|
|
10
|
+
import { getCreateInputTypeName, getCreateMutationName, getDeleteInputTypeName, getDeleteMutationName, getFilterTypeName, getGeneratedFileHeader, getOrderByTypeName, getPrimaryKeyInfo, getSingleRowQueryName, getTableNames, hasValidPrimaryKey, lcFirst, ucFirst, } from '../utils';
|
|
10
11
|
function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
|
|
11
12
|
const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
|
|
12
13
|
const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
|
|
@@ -65,7 +66,7 @@ function strictSelectGuard(selectTypeName) {
|
|
|
65
66
|
t.tsTypeReference(t.identifier(selectTypeName)),
|
|
66
67
|
]));
|
|
67
68
|
}
|
|
68
|
-
export function generateModelFile(table, _useSharedTypes, options) {
|
|
69
|
+
export function generateModelFile(table, _useSharedTypes, options, allTables) {
|
|
69
70
|
const conditionEnabled = options?.condition !== false;
|
|
70
71
|
const { typeName, singularName, pluralName } = getTableNames(table);
|
|
71
72
|
const modelName = `${typeName}Model`;
|
|
@@ -79,7 +80,6 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
79
80
|
const orderByTypeName = getOrderByTypeName(table);
|
|
80
81
|
const createInputTypeName = `Create${typeName}Input`;
|
|
81
82
|
const updateInputTypeName = `Update${typeName}Input`;
|
|
82
|
-
const deleteInputTypeName = `Delete${typeName}Input`;
|
|
83
83
|
const patchTypeName = `${typeName}Patch`;
|
|
84
84
|
const pkFields = getPrimaryKeyInfo(table);
|
|
85
85
|
const pkField = pkFields[0];
|
|
@@ -89,9 +89,16 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
89
89
|
const createMutationName = table.query?.create ?? `create${typeName}`;
|
|
90
90
|
const updateMutationName = table.query?.update;
|
|
91
91
|
const deleteMutationName = table.query?.delete;
|
|
92
|
+
const deleteInputTypeName = getDeleteInputTypeName(table);
|
|
92
93
|
const statements = [];
|
|
93
94
|
statements.push(createImportDeclaration('../client', ['OrmClient']));
|
|
94
|
-
|
|
95
|
+
const m2nRels = table.relations.manyToMany.filter((r) => r.junctionLeftKeyFields?.length && r.junctionRightKeyFields?.length);
|
|
96
|
+
// Check if any remove methods will actually be generated (need junction table with delete mutation)
|
|
97
|
+
const needsJunctionRemove = m2nRels.some((r) => {
|
|
98
|
+
const jt = allTables?.find((tb) => tb.name === r.junctionTable);
|
|
99
|
+
return jt?.query?.delete != null;
|
|
100
|
+
});
|
|
101
|
+
const queryBuilderImports = [
|
|
95
102
|
'QueryBuilder',
|
|
96
103
|
'buildFindManyDocument',
|
|
97
104
|
'buildFindFirstDocument',
|
|
@@ -99,7 +106,9 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
99
106
|
'buildCreateDocument',
|
|
100
107
|
'buildUpdateByPkDocument',
|
|
101
108
|
'buildDeleteByPkDocument',
|
|
102
|
-
|
|
109
|
+
...(needsJunctionRemove ? ['buildJunctionRemoveDocument'] : []),
|
|
110
|
+
];
|
|
111
|
+
statements.push(createImportDeclaration('../query-builder', queryBuilderImports));
|
|
103
112
|
statements.push(createImportDeclaration('../select-types', [
|
|
104
113
|
'ConnectionResult',
|
|
105
114
|
'FindManyArgs',
|
|
@@ -392,13 +401,12 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
392
401
|
}
|
|
393
402
|
// ── delete ─────────────────────────────────────────────────────────────
|
|
394
403
|
if (deleteMutationName) {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
]);
|
|
404
|
+
// Build where type with ALL PK fields (supports composite PKs)
|
|
405
|
+
const whereLiteral = () => t.tsTypeLiteral(pkFields.map((pk) => {
|
|
406
|
+
const prop = t.tsPropertySignature(t.identifier(pk.name), t.tsTypeAnnotation(tsTypeFromPrimitive(pk.tsType ?? 'string')));
|
|
407
|
+
prop.optional = false;
|
|
408
|
+
return prop;
|
|
409
|
+
}));
|
|
402
410
|
const argsType = (sel) => t.tsTypeReference(t.identifier('DeleteArgs'), t.tsTypeParameterInstantiation([whereLiteral(), sel]));
|
|
403
411
|
const retType = (sel) => t.tsTypeAnnotation(t.tsTypeReference(t.identifier('QueryBuilder'), t.tsTypeParameterInstantiation([
|
|
404
412
|
t.tsTypeLiteral([
|
|
@@ -417,18 +425,144 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
417
425
|
strictSelectGuard(selectTypeName),
|
|
418
426
|
]));
|
|
419
427
|
const selectExpr = t.memberExpression(t.identifier('args'), t.identifier('select'));
|
|
428
|
+
// Build keys object: { field1: args.where.field1, field2: args.where.field2, ... }
|
|
429
|
+
const keysObj = t.objectExpression(pkFields.map((pk) => t.objectProperty(t.identifier(pk.name), t.memberExpression(t.memberExpression(t.identifier('args'), t.identifier('where')), t.identifier(pk.name)))));
|
|
420
430
|
const bodyArgs = [
|
|
421
431
|
t.stringLiteral(typeName),
|
|
422
432
|
t.stringLiteral(deleteMutationName),
|
|
423
433
|
t.stringLiteral(entityLower),
|
|
424
|
-
|
|
434
|
+
keysObj,
|
|
425
435
|
t.stringLiteral(deleteInputTypeName),
|
|
426
|
-
t.stringLiteral(pkField.name),
|
|
427
436
|
selectExpr,
|
|
428
437
|
t.identifier('connectionFieldsMap'),
|
|
429
438
|
];
|
|
430
439
|
classBody.push(createClassMethod('delete', createTypeParam(selectTypeName), [implParam], retType(sRef()), buildMethodBody('buildDeleteByPkDocument', bodyArgs, 'mutation', typeName, deleteMutationName)));
|
|
431
440
|
}
|
|
441
|
+
// ── M:N add/remove methods ────────────────────────────────────────────
|
|
442
|
+
for (const rel of m2nRels) {
|
|
443
|
+
if (!rel.fieldName)
|
|
444
|
+
continue;
|
|
445
|
+
const junctionTable = allTables?.find((tb) => tb.name === rel.junctionTable);
|
|
446
|
+
if (!junctionTable)
|
|
447
|
+
continue;
|
|
448
|
+
const junctionNames = getTableNames(junctionTable);
|
|
449
|
+
const junctionCreateMutation = getCreateMutationName(junctionTable);
|
|
450
|
+
const junctionCreateInputType = getCreateInputTypeName(junctionTable);
|
|
451
|
+
const junctionDeleteMutation = junctionTable.query?.delete ?? getDeleteMutationName(junctionTable);
|
|
452
|
+
const junctionDeleteInputType = getDeleteInputTypeName(junctionTable);
|
|
453
|
+
const junctionSingular = junctionNames.singularName;
|
|
454
|
+
// Derive a friendly singular name from the fieldName (e.g., "tags" → "Tag", "categories" → "Category")
|
|
455
|
+
const relSingular = ucFirst(singularize(rel.fieldName));
|
|
456
|
+
const leftKeys = rel.junctionLeftKeyFields;
|
|
457
|
+
const rightKeys = rel.junctionRightKeyFields;
|
|
458
|
+
const leftPkFields = rel.leftKeyFields ?? ['id'];
|
|
459
|
+
const rightPkFields = rel.rightKeyFields ?? ['id'];
|
|
460
|
+
// Resolve actual PK types from left (current) and right tables
|
|
461
|
+
const leftPkInfo = getPrimaryKeyInfo(table);
|
|
462
|
+
const rightTable = allTables?.find((tb) => tb.name === rel.rightTable);
|
|
463
|
+
const rightPkInfo = rightTable ? getPrimaryKeyInfo(rightTable) : [];
|
|
464
|
+
// ── add<Relation> ───────────────────────────────────────────────
|
|
465
|
+
{
|
|
466
|
+
// Parameters: one param per left PK + one param per right PK, with actual types
|
|
467
|
+
const params = [];
|
|
468
|
+
for (let i = 0; i < leftPkFields.length; i++) {
|
|
469
|
+
const p = t.identifier(leftPkFields[i]);
|
|
470
|
+
const pkInfo = leftPkInfo.find((pk) => pk.name === leftPkFields[i]);
|
|
471
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
472
|
+
params.push(p);
|
|
473
|
+
}
|
|
474
|
+
for (let i = 0; i < rightPkFields.length; i++) {
|
|
475
|
+
const rk = rightPkFields[i];
|
|
476
|
+
const p = t.identifier(rk === leftPkFields[0] ? `right${ucFirst(rk)}` : rk);
|
|
477
|
+
const pkInfo = rightPkInfo.find((pk) => pk.name === rk);
|
|
478
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
479
|
+
params.push(p);
|
|
480
|
+
}
|
|
481
|
+
// Build the junction row data object: { junctionLeftKey: leftPk, junctionRightKey: rightPk }
|
|
482
|
+
const dataProps = [];
|
|
483
|
+
for (let i = 0; i < leftKeys.length; i++) {
|
|
484
|
+
dataProps.push(t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)));
|
|
485
|
+
}
|
|
486
|
+
for (let i = 0; i < rightKeys.length; i++) {
|
|
487
|
+
dataProps.push(t.objectProperty(t.identifier(rightKeys[i]), t.identifier(params[leftPkFields.length + i].name)));
|
|
488
|
+
}
|
|
489
|
+
const body = [
|
|
490
|
+
t.variableDeclaration('const', [
|
|
491
|
+
t.variableDeclarator(t.objectPattern([
|
|
492
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
493
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
494
|
+
]), t.callExpression(t.identifier('buildCreateDocument'), [
|
|
495
|
+
t.stringLiteral(junctionNames.typeName),
|
|
496
|
+
t.stringLiteral(junctionCreateMutation),
|
|
497
|
+
t.stringLiteral(junctionSingular),
|
|
498
|
+
t.objectExpression([t.objectProperty(t.identifier('id'), t.booleanLiteral(true))]),
|
|
499
|
+
t.objectExpression(dataProps),
|
|
500
|
+
t.stringLiteral(junctionCreateInputType),
|
|
501
|
+
])),
|
|
502
|
+
]),
|
|
503
|
+
t.returnStatement(t.newExpression(t.identifier('QueryBuilder'), [
|
|
504
|
+
t.objectExpression([
|
|
505
|
+
t.objectProperty(t.identifier('client'), t.memberExpression(t.thisExpression(), t.identifier('client'))),
|
|
506
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
|
|
507
|
+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
|
|
508
|
+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionCreateMutation)),
|
|
509
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
510
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
511
|
+
]),
|
|
512
|
+
])),
|
|
513
|
+
];
|
|
514
|
+
classBody.push(t.classMethod('method', t.identifier(`add${relSingular}`), params, t.blockStatement(body)));
|
|
515
|
+
}
|
|
516
|
+
// ── remove<Relation> ────────────────────────────────────────────
|
|
517
|
+
if (junctionTable.query?.delete) {
|
|
518
|
+
const params = [];
|
|
519
|
+
for (let i = 0; i < leftPkFields.length; i++) {
|
|
520
|
+
const p = t.identifier(leftPkFields[i]);
|
|
521
|
+
const pkInfo = leftPkInfo.find((pk) => pk.name === leftPkFields[i]);
|
|
522
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
523
|
+
params.push(p);
|
|
524
|
+
}
|
|
525
|
+
for (let i = 0; i < rightPkFields.length; i++) {
|
|
526
|
+
const rk = rightPkFields[i];
|
|
527
|
+
const p = t.identifier(rk === leftPkFields[0] ? `right${ucFirst(rk)}` : rk);
|
|
528
|
+
const pkInfo = rightPkInfo.find((pk) => pk.name === rk);
|
|
529
|
+
p.typeAnnotation = t.tsTypeAnnotation(tsTypeFromPrimitive(pkInfo?.tsType ?? 'string'));
|
|
530
|
+
params.push(p);
|
|
531
|
+
}
|
|
532
|
+
// Build the keys object for junction delete
|
|
533
|
+
const keysProps = [];
|
|
534
|
+
for (let i = 0; i < leftKeys.length; i++) {
|
|
535
|
+
keysProps.push(t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)));
|
|
536
|
+
}
|
|
537
|
+
for (let i = 0; i < rightKeys.length; i++) {
|
|
538
|
+
keysProps.push(t.objectProperty(t.identifier(rightKeys[i]), t.identifier(params[leftPkFields.length + i].name)));
|
|
539
|
+
}
|
|
540
|
+
const body = [
|
|
541
|
+
t.variableDeclaration('const', [
|
|
542
|
+
t.variableDeclarator(t.objectPattern([
|
|
543
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
544
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
545
|
+
]), t.callExpression(t.identifier('buildJunctionRemoveDocument'), [
|
|
546
|
+
t.stringLiteral(junctionNames.typeName),
|
|
547
|
+
t.stringLiteral(junctionDeleteMutation),
|
|
548
|
+
t.objectExpression(keysProps),
|
|
549
|
+
t.stringLiteral(junctionDeleteInputType),
|
|
550
|
+
])),
|
|
551
|
+
]),
|
|
552
|
+
t.returnStatement(t.newExpression(t.identifier('QueryBuilder'), [
|
|
553
|
+
t.objectExpression([
|
|
554
|
+
t.objectProperty(t.identifier('client'), t.memberExpression(t.thisExpression(), t.identifier('client'))),
|
|
555
|
+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
|
|
556
|
+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
|
|
557
|
+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionDeleteMutation)),
|
|
558
|
+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
|
|
559
|
+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
|
|
560
|
+
]),
|
|
561
|
+
])),
|
|
562
|
+
];
|
|
563
|
+
classBody.push(t.classMethod('method', t.identifier(`remove${relSingular}`), params, t.blockStatement(body)));
|
|
564
|
+
}
|
|
565
|
+
}
|
|
432
566
|
const classDecl = t.classDeclaration(t.identifier(modelName), null, t.classBody(classBody));
|
|
433
567
|
statements.push(t.exportNamedDeclaration(classDecl));
|
|
434
568
|
const header = getGeneratedFileHeader(`${typeName} model for ORM client`);
|
|
@@ -441,5 +575,5 @@ export function generateModelFile(table, _useSharedTypes, options) {
|
|
|
441
575
|
};
|
|
442
576
|
}
|
|
443
577
|
export function generateAllModelFiles(tables, useSharedTypes, options) {
|
|
444
|
-
return tables.map((table) => generateModelFile(table, useSharedTypes, options));
|
|
578
|
+
return tables.map((table) => generateModelFile(table, useSharedTypes, options, tables));
|
|
445
579
|
}
|
|
@@ -118,12 +118,14 @@ export declare function getCreateInputTypeName(table: Table): string;
|
|
|
118
118
|
export declare function getPatchTypeName(table: Table): string;
|
|
119
119
|
/**
|
|
120
120
|
* Get PostGraphile update input type name
|
|
121
|
-
*
|
|
121
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
122
|
+
* like UpdatePostTagByPostIdAndTagIdInput), falls back to Update${Entity}Input.
|
|
122
123
|
*/
|
|
123
124
|
export declare function getUpdateInputTypeName(table: Table): string;
|
|
124
125
|
/**
|
|
125
126
|
* Get PostGraphile delete input type name
|
|
126
|
-
*
|
|
127
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
128
|
+
* like DeletePostTagByPostIdAndTagIdInput), falls back to Delete${Entity}Input.
|
|
127
129
|
*/
|
|
128
130
|
export declare function getDeleteInputTypeName(table: Table): string;
|
|
129
131
|
/**
|
|
@@ -199,17 +199,21 @@ export function getPatchTypeName(table) {
|
|
|
199
199
|
}
|
|
200
200
|
/**
|
|
201
201
|
* Get PostGraphile update input type name
|
|
202
|
-
*
|
|
202
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
203
|
+
* like UpdatePostTagByPostIdAndTagIdInput), falls back to Update${Entity}Input.
|
|
203
204
|
*/
|
|
204
205
|
export function getUpdateInputTypeName(table) {
|
|
205
|
-
|
|
206
|
+
const mutationName = table.query?.update;
|
|
207
|
+
return mutationName ? ucFirst(mutationName) + 'Input' : `Update${table.name}Input`;
|
|
206
208
|
}
|
|
207
209
|
/**
|
|
208
210
|
* Get PostGraphile delete input type name
|
|
209
|
-
*
|
|
211
|
+
* Derives from actual mutation name when available (handles composite PK naming
|
|
212
|
+
* like DeletePostTagByPostIdAndTagIdInput), falls back to Delete${Entity}Input.
|
|
210
213
|
*/
|
|
211
214
|
export function getDeleteInputTypeName(table) {
|
|
212
|
-
|
|
215
|
+
const mutationName = table.query?.delete;
|
|
216
|
+
return mutationName ? ucFirst(mutationName) + 'Input' : `Delete${table.name}Input`;
|
|
213
217
|
}
|
|
214
218
|
// ============================================================================
|
|
215
219
|
// Type mapping: GraphQL → TypeScript
|
package/esm/core/generate.js
CHANGED
|
@@ -13,7 +13,7 @@ import { createEphemeralDb } from 'pgsql-client';
|
|
|
13
13
|
import { deployPgpm } from 'pgsql-seed';
|
|
14
14
|
import { getConfigOptions } from '../types/config';
|
|
15
15
|
import { generate as generateReactQueryFiles } from './codegen';
|
|
16
|
-
import { generateRootBarrel } from './codegen/barrel';
|
|
16
|
+
import { generateRootBarrel, generateMultiTargetBarrel } from './codegen/barrel';
|
|
17
17
|
import { generateCli as generateCliFiles, generateMultiTargetCli } from './codegen/cli';
|
|
18
18
|
import { generateReadme as generateCliReadme, generateAgentsDocs as generateCliAgentsDocs, getCliMcpTools, generateSkills as generateCliSkills, generateMultiTargetReadme, generateMultiTargetAgentsDocs, getMultiTargetCliMcpTools, generateMultiTargetSkills, } from './codegen/cli/docs-generator';
|
|
19
19
|
import { resolveDocsConfig } from './codegen/docs-utils';
|
|
@@ -633,11 +633,23 @@ export async function generateMulti(options) {
|
|
|
633
633
|
await writeFiles(cliSkillsToWrite, skillsOutputDir, [], { pruneStaleFiles: false });
|
|
634
634
|
}
|
|
635
635
|
}
|
|
636
|
-
// Generate root-root README if multi-target
|
|
636
|
+
// Generate root-root README and barrel if multi-target
|
|
637
637
|
if (names.length > 1 && targetInfos.length > 0 && !dryRun) {
|
|
638
|
-
const rootReadme = generateRootRootReadme(targetInfos);
|
|
639
638
|
const { writeGeneratedFiles: writeFiles } = await import('./output');
|
|
639
|
+
const rootReadme = generateRootRootReadme(targetInfos);
|
|
640
640
|
await writeFiles([{ path: rootReadme.fileName, content: rootReadme.content }], '.', [], { pruneStaleFiles: false });
|
|
641
|
+
// Write a root barrel (index.ts) that re-exports each target as a
|
|
642
|
+
// namespace so the package has a single entry-point. Derive the
|
|
643
|
+
// common output root from the first target's output path.
|
|
644
|
+
const successfulNames = results
|
|
645
|
+
.filter((r) => r.result.success)
|
|
646
|
+
.map((r) => r.name);
|
|
647
|
+
if (successfulNames.length > 0) {
|
|
648
|
+
const firstOutput = getConfigOptions(configs[successfulNames[0]]).output;
|
|
649
|
+
const outputRoot = path.dirname(firstOutput);
|
|
650
|
+
const barrelContent = generateMultiTargetBarrel(successfulNames);
|
|
651
|
+
await writeFiles([{ path: 'index.ts', content: barrelContent }], outputRoot, [], { pruneStaleFiles: false });
|
|
652
|
+
}
|
|
641
653
|
}
|
|
642
654
|
}
|
|
643
655
|
finally {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M:N Relation Enrichment
|
|
3
|
+
*
|
|
4
|
+
* After table inference from introspection, enriches ManyToManyRelation objects
|
|
5
|
+
* with junction key field metadata from _cachedTablesMeta (MetaSchemaPlugin).
|
|
6
|
+
*/
|
|
7
|
+
import type { Table } from '../../types/schema';
|
|
8
|
+
import type { MetaTableInfo } from './source/types';
|
|
9
|
+
/**
|
|
10
|
+
* Enrich M:N relations with junction key field metadata from _meta.
|
|
11
|
+
* Mutates the tables array in-place.
|
|
12
|
+
*/
|
|
13
|
+
export declare function enrichManyToManyRelations(tables: Table[], tablesMeta?: MetaTableInfo[]): void;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enrich M:N relations with junction key field metadata from _meta.
|
|
3
|
+
* Mutates the tables array in-place.
|
|
4
|
+
*/
|
|
5
|
+
export function enrichManyToManyRelations(tables, tablesMeta) {
|
|
6
|
+
if (!tablesMeta?.length)
|
|
7
|
+
return;
|
|
8
|
+
const metaByName = new Map(tablesMeta.map((m) => [m.name, m]));
|
|
9
|
+
for (const table of tables) {
|
|
10
|
+
const meta = metaByName.get(table.name);
|
|
11
|
+
if (!meta?.relations.manyToMany.length)
|
|
12
|
+
continue;
|
|
13
|
+
for (const rel of table.relations.manyToMany) {
|
|
14
|
+
const metaRel = meta.relations.manyToMany.find((m) => m.fieldName === rel.fieldName);
|
|
15
|
+
if (!metaRel)
|
|
16
|
+
continue;
|
|
17
|
+
rel.junctionLeftKeyFields = metaRel.junctionLeftKeyAttributes.map((a) => a.name);
|
|
18
|
+
rel.junctionRightKeyFields = metaRel.junctionRightKeyAttributes.map((a) => a.name);
|
|
19
|
+
rel.leftKeyFields = metaRel.leftKeyAttributes.map((a) => a.name);
|
|
20
|
+
rel.rightKeyFields = metaRel.rightKeyAttributes.map((a) => a.name);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { enrichManyToManyRelations } from '../introspect/enrich-relations';
|
|
1
2
|
import { inferTablesFromIntrospection } from '../introspect/infer-tables';
|
|
2
3
|
import { filterTables } from '../introspect/transform';
|
|
3
4
|
import { filterOperations, getCustomOperations, getTableOperationNames, transformSchemaToOperations, } from '../introspect/transform-schema';
|
|
@@ -20,13 +21,18 @@ export async function runCodegenPipeline(options) {
|
|
|
20
21
|
const log = verbose ? console.log : () => { };
|
|
21
22
|
// 1. Fetch introspection from source
|
|
22
23
|
log(`Fetching schema from ${source.describe()}...`);
|
|
23
|
-
const { introspection } = await source.fetch();
|
|
24
|
+
const { introspection, tablesMeta } = await source.fetch();
|
|
24
25
|
// 2. Infer tables from introspection (replaces _meta)
|
|
25
26
|
log('Inferring table metadata from schema...');
|
|
26
27
|
const commentsEnabled = config.codegen?.comments !== false;
|
|
27
28
|
let tables = inferTablesFromIntrospection(introspection, { comments: commentsEnabled });
|
|
28
29
|
const totalTables = tables.length;
|
|
29
30
|
log(` Found ${totalTables} tables`);
|
|
31
|
+
// 2a. Enrich M:N relations with junction key metadata from _meta
|
|
32
|
+
if (tablesMeta?.length) {
|
|
33
|
+
enrichManyToManyRelations(tables, tablesMeta);
|
|
34
|
+
log(` Enriched M:N relations from _meta (${tablesMeta.length} tables)`);
|
|
35
|
+
}
|
|
30
36
|
// 3. Filter tables by config (combine exclude and systemExclude)
|
|
31
37
|
tables = filterTables(tables, config.tables.include, [
|
|
32
38
|
...config.tables.exclude,
|
package/esm/types/config.d.ts
CHANGED
|
@@ -138,8 +138,8 @@ export interface DocsConfig {
|
|
|
138
138
|
*/
|
|
139
139
|
mcp?: boolean;
|
|
140
140
|
/**
|
|
141
|
-
* Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
|
|
142
|
-
* Skills are written to
|
|
141
|
+
* Generate .agents/skills/ directory — per-entity SKILL.md files with YAML frontmatter.
|
|
142
|
+
* Skills are written to {workspaceRoot}/.agents/skills/ (not nested in output).
|
|
143
143
|
* Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
|
|
144
144
|
* @default false
|
|
145
145
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.21.0",
|
|
4
4
|
"description": "GraphQL SDK generator for Constructive databases with React Query hooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"graphql",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"@0no-co/graphql.web": "^1.1.2",
|
|
57
57
|
"@babel/generator": "^7.29.1",
|
|
58
58
|
"@babel/types": "^7.29.0",
|
|
59
|
-
"@constructive-io/graphql-query": "^3.
|
|
59
|
+
"@constructive-io/graphql-query": "^3.9.0",
|
|
60
60
|
"@constructive-io/graphql-types": "^3.3.4",
|
|
61
61
|
"@inquirerer/utils": "^3.3.4",
|
|
62
62
|
"@pgpmjs/core": "^6.8.1",
|
|
@@ -101,5 +101,5 @@
|
|
|
101
101
|
"tsx": "^4.21.0",
|
|
102
102
|
"typescript": "^5.9.3"
|
|
103
103
|
},
|
|
104
|
-
"gitHead": "
|
|
104
|
+
"gitHead": "7b5d57e1d1aa274a2914cec3240a902d2b1020c6"
|
|
105
105
|
}
|
package/types/config.d.ts
CHANGED
|
@@ -138,8 +138,8 @@ export interface DocsConfig {
|
|
|
138
138
|
*/
|
|
139
139
|
mcp?: boolean;
|
|
140
140
|
/**
|
|
141
|
-
* Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
|
|
142
|
-
* Skills are written to
|
|
141
|
+
* Generate .agents/skills/ directory — per-entity SKILL.md files with YAML frontmatter.
|
|
142
|
+
* Skills are written to {workspaceRoot}/.agents/skills/ (not nested in output).
|
|
143
143
|
* Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
|
|
144
144
|
* @default false
|
|
145
145
|
*/
|