@constructive-io/graphql-codegen 4.20.0 → 4.21.1
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/orm/input-types-generator.js +71 -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/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/orm/input-types-generator.js +72 -7
- 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/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/package.json +3 -3
|
@@ -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,40 @@ 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
|
+
for (const field of filterType.inputFields) {
|
|
1369
|
+
// Skip logical operators (and/or/not reference the table's own filter type)
|
|
1370
|
+
if (['and', 'or', 'not'].includes(field.name))
|
|
1371
|
+
continue;
|
|
1372
|
+
// Collect any filter type that isn't already generated as a base scalar filter
|
|
1373
|
+
// This catches both plugin-injected fields (bm25Body, tsvTsv) AND regular columns
|
|
1374
|
+
// whose custom scalar types have their own filter types (e.g., ConstructiveInternalTypeEmailFilter)
|
|
1375
|
+
const baseName = (0, type_resolver_1.getTypeBaseName)(field.type);
|
|
1376
|
+
if (baseName && !scalars_1.SCALAR_NAMES.has(baseName) && !scalars_1.BASE_FILTER_TYPE_NAMES.has(baseName)) {
|
|
1377
|
+
extraTypes.add(baseName);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
return extraTypes;
|
|
1382
|
+
}
|
|
1326
1383
|
/**
|
|
1327
1384
|
* Collect extra input type names referenced by plugin-injected condition fields.
|
|
1328
1385
|
*
|
|
@@ -1386,7 +1443,9 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
|
|
|
1386
1443
|
statements.push(...generateEntityWithRelations(tablesList));
|
|
1387
1444
|
statements.push(...generateEntitySelectTypes(tablesList, tableByName));
|
|
1388
1445
|
// 4. Table filter types
|
|
1389
|
-
|
|
1446
|
+
// Pass typeRegistry to use schema's filter type as source of truth,
|
|
1447
|
+
// capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
|
|
1448
|
+
statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
|
|
1390
1449
|
// 4b. Table condition types (simple equality filter)
|
|
1391
1450
|
// Pass typeRegistry to merge plugin-injected condition fields
|
|
1392
1451
|
// (e.g., vectorEmbedding from VectorSearchPlugin)
|
|
@@ -1404,8 +1463,14 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
|
|
|
1404
1463
|
// Always emit this export so generated model/custom-op imports stay valid.
|
|
1405
1464
|
statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
|
|
1406
1465
|
// 7. Custom input types from TypeRegistry
|
|
1407
|
-
// Also include any extra types referenced by plugin-injected condition fields
|
|
1466
|
+
// Also include any extra types referenced by plugin-injected filter/condition fields
|
|
1408
1467
|
const mergedUsedInputTypes = new Set(usedInputTypes);
|
|
1468
|
+
if (hasTables) {
|
|
1469
|
+
const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
|
|
1470
|
+
for (const typeName of filterExtraTypes) {
|
|
1471
|
+
mergedUsedInputTypes.add(typeName);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1409
1474
|
if (hasTables && conditionEnabled) {
|
|
1410
1475
|
const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
|
|
1411
1476
|
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
|
|
@@ -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,
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import * as t from '@babel/types';
|
|
14
14
|
import { pluralize } from 'inflekt';
|
|
15
15
|
import { addJSDocComment, addLineComment, generateCode } from '../babel-ast';
|
|
16
|
-
import { SCALAR_NAMES, scalarToFilterType, scalarToTsType } from '../scalars';
|
|
16
|
+
import { BASE_FILTER_TYPE_NAMES, SCALAR_NAMES, scalarToFilterType, scalarToTsType } from '../scalars';
|
|
17
17
|
import { getTypeBaseName } from '../type-resolver';
|
|
18
18
|
import { getCreateInputTypeName, getConditionTypeName, getFilterTypeName, getGeneratedFileHeader, getOrderByTypeName, getPatchTypeName, getPrimaryKeyInfo, getTableNames, isRelationField, lcFirst, stripSmartComments, } from '../utils';
|
|
19
19
|
// ============================================================================
|
|
@@ -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,40 @@ 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
|
+
for (const field of filterType.inputFields) {
|
|
1331
|
+
// Skip logical operators (and/or/not reference the table's own filter type)
|
|
1332
|
+
if (['and', 'or', 'not'].includes(field.name))
|
|
1333
|
+
continue;
|
|
1334
|
+
// Collect any filter type that isn't already generated as a base scalar filter
|
|
1335
|
+
// This catches both plugin-injected fields (bm25Body, tsvTsv) AND regular columns
|
|
1336
|
+
// whose custom scalar types have their own filter types (e.g., ConstructiveInternalTypeEmailFilter)
|
|
1337
|
+
const baseName = getTypeBaseName(field.type);
|
|
1338
|
+
if (baseName && !SCALAR_NAMES.has(baseName) && !BASE_FILTER_TYPE_NAMES.has(baseName)) {
|
|
1339
|
+
extraTypes.add(baseName);
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return extraTypes;
|
|
1344
|
+
}
|
|
1288
1345
|
/**
|
|
1289
1346
|
* Collect extra input type names referenced by plugin-injected condition fields.
|
|
1290
1347
|
*
|
|
@@ -1348,7 +1405,9 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
|
|
|
1348
1405
|
statements.push(...generateEntityWithRelations(tablesList));
|
|
1349
1406
|
statements.push(...generateEntitySelectTypes(tablesList, tableByName));
|
|
1350
1407
|
// 4. Table filter types
|
|
1351
|
-
|
|
1408
|
+
// Pass typeRegistry to use schema's filter type as source of truth,
|
|
1409
|
+
// capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
|
|
1410
|
+
statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
|
|
1352
1411
|
// 4b. Table condition types (simple equality filter)
|
|
1353
1412
|
// Pass typeRegistry to merge plugin-injected condition fields
|
|
1354
1413
|
// (e.g., vectorEmbedding from VectorSearchPlugin)
|
|
@@ -1366,8 +1425,14 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
|
|
|
1366
1425
|
// Always emit this export so generated model/custom-op imports stay valid.
|
|
1367
1426
|
statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
|
|
1368
1427
|
// 7. Custom input types from TypeRegistry
|
|
1369
|
-
// Also include any extra types referenced by plugin-injected condition fields
|
|
1428
|
+
// Also include any extra types referenced by plugin-injected filter/condition fields
|
|
1370
1429
|
const mergedUsedInputTypes = new Set(usedInputTypes);
|
|
1430
|
+
if (hasTables) {
|
|
1431
|
+
const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
|
|
1432
|
+
for (const typeName of filterExtraTypes) {
|
|
1433
|
+
mergedUsedInputTypes.add(typeName);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1371
1436
|
if (hasTables && conditionEnabled) {
|
|
1372
1437
|
const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
|
|
1373
1438
|
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
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@constructive-io/graphql-codegen",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.21.1",
|
|
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": "67136efd789e8e038da7b3adf3af49bd7bb1a49d"
|
|
105
105
|
}
|