@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.
@@ -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
- statements.push(...generateTableFilterTypes(tablesList));
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
- statements.push(createImportDeclaration('../query-builder', [
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
- const whereLiteral = () => t.tsTypeLiteral([
433
- (() => {
434
- const prop = t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTsType()));
435
- prop.optional = false;
436
- return prop;
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
- t.memberExpression(t.memberExpression(t.identifier('args'), t.identifier('where')), t.identifier(pkField.name)),
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
- id: string | number,
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
- [idFieldName]: id,
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
  }
@@ -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
- * e.g., "UpdateCarInput"
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
- * e.g., "DeleteCarInput"
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
  /**
@@ -244,17 +244,21 @@ function getPatchTypeName(table) {
244
244
  }
245
245
  /**
246
246
  * Get PostGraphile update input type name
247
- * e.g., "UpdateCarInput"
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
- return `Update${table.name}Input`;
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
- * e.g., "DeleteCarInput"
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
- return `Delete${table.name}Input`;
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
+ }
@@ -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
- statements.push(...generateTableFilterTypes(tablesList));
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
- statements.push(createImportDeclaration('../query-builder', [
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
- const whereLiteral = () => t.tsTypeLiteral([
396
- (() => {
397
- const prop = t.tsPropertySignature(t.identifier(pkField.name), t.tsTypeAnnotation(pkTsType()));
398
- prop.optional = false;
399
- return prop;
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
- t.memberExpression(t.memberExpression(t.identifier('args'), t.identifier('where')), t.identifier(pkField.name)),
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
- * e.g., "UpdateCarInput"
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
- * e.g., "DeleteCarInput"
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
- * e.g., "UpdateCarInput"
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
- return `Update${table.name}Input`;
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
- * e.g., "DeleteCarInput"
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
- return `Delete${table.name}Input`;
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.20.0",
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.8.1",
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": "9bd7e1bb2eb2e66ac18dce50cc29b84ef667649e"
104
+ "gitHead": "67136efd789e8e038da7b3adf3af49bd7bb1a49d"
105
105
  }