@constructive-io/graphql-codegen 4.19.1 → 4.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,16 @@ export interface RootBarrelOptions {
34
34
  * Re-exports from subdirectories based on which generators are enabled.
35
35
  */
36
36
  export declare function generateRootBarrel(options?: RootBarrelOptions): string;
37
+ /**
38
+ * Generate a root index.ts for multi-target output that re-exports each
39
+ * target as a namespace.
40
+ *
41
+ * Example output:
42
+ * export * as admin from './admin';
43
+ * export * as auth from './auth';
44
+ * export * as public_ from './public';
45
+ */
46
+ export declare function generateMultiTargetBarrel(targetNames: string[]): string;
37
47
  /**
38
48
  * Generate queries barrel including custom query operations
39
49
  */
@@ -37,6 +37,7 @@ exports.generateQueriesBarrel = generateQueriesBarrel;
37
37
  exports.generateMutationsBarrel = generateMutationsBarrel;
38
38
  exports.generateMainBarrel = generateMainBarrel;
39
39
  exports.generateRootBarrel = generateRootBarrel;
40
+ exports.generateMultiTargetBarrel = generateMultiTargetBarrel;
40
41
  exports.generateCustomQueriesBarrel = generateCustomQueriesBarrel;
41
42
  exports.generateCustomMutationsBarrel = generateCustomMutationsBarrel;
42
43
  /**
@@ -200,6 +201,49 @@ function generateRootBarrel(options = {}) {
200
201
  return (0, babel_ast_1.generateCode)(statements);
201
202
  }
202
203
  // ============================================================================
204
+ // Multi-target root barrel (re-exports each target as a namespace)
205
+ // ============================================================================
206
+ /** JS reserved words that need an alias when used as export names */
207
+ const JS_RESERVED = new Set([
208
+ 'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch',
209
+ 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
210
+ 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
211
+ 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import',
212
+ 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new',
213
+ 'null', 'package', 'private', 'protected', 'public', 'return', 'short',
214
+ 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws',
215
+ 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while',
216
+ 'with', 'yield',
217
+ ]);
218
+ /**
219
+ * Generate a root index.ts for multi-target output that re-exports each
220
+ * target as a namespace.
221
+ *
222
+ * Example output:
223
+ * export * as admin from './admin';
224
+ * export * as auth from './auth';
225
+ * export * as public_ from './public';
226
+ */
227
+ function generateMultiTargetBarrel(targetNames) {
228
+ const statements = [];
229
+ for (const name of targetNames) {
230
+ const alias = JS_RESERVED.has(name) ? `${name}_` : name;
231
+ const exportDecl = t.exportNamedDeclaration(null, [t.exportNamespaceSpecifier(t.identifier(alias))], t.stringLiteral(`./${name}`));
232
+ statements.push(exportDecl);
233
+ }
234
+ if (statements.length > 0) {
235
+ (0, babel_ast_1.addJSDocComment)(statements[0], [
236
+ '@constructive-io/sdk',
237
+ '',
238
+ 'Auto-generated GraphQL types and ORM client.',
239
+ 'Run `pnpm run generate` to populate this package from the schema files.',
240
+ '',
241
+ '@generated by @constructive-io/graphql-codegen',
242
+ ]);
243
+ }
244
+ return (0, babel_ast_1.generateCode)(statements);
245
+ }
246
+ // ============================================================================
203
247
  // Custom operation barrels (includes both table and custom hooks)
204
248
  // ============================================================================
205
249
  /**
@@ -718,10 +718,33 @@ function getFilterTypeForField(fieldType, isArray = false) {
718
718
  return (0, scalars_1.scalarToFilterType)(fieldType, isArray) ?? 'StringFilter';
719
719
  }
720
720
  /**
721
- * Build properties for a table filter interface
721
+ * Build properties for a table filter interface.
722
+ *
723
+ * When typeRegistry is available, uses the schema's filter input type as
724
+ * the sole source of truth — this captures plugin-injected filter fields
725
+ * (e.g., bm25Body, tsvTsv, trgmName, vectorEmbedding, geom) that are not
726
+ * present on the entity type itself. Same pattern as buildOrderByValues().
722
727
  */
723
- function buildTableFilterProperties(table) {
728
+ function buildTableFilterProperties(table, typeRegistry) {
724
729
  const filterName = (0, utils_1.getFilterTypeName)(table);
730
+ // When the schema's filter type is available, use it as the source of truth
731
+ if (typeRegistry) {
732
+ const filterType = typeRegistry.get(filterName);
733
+ if (filterType?.kind === 'INPUT_OBJECT' && filterType.inputFields) {
734
+ const properties = [];
735
+ for (const field of filterType.inputFields) {
736
+ const tsType = typeRefToTs(field.type);
737
+ properties.push({
738
+ name: field.name,
739
+ type: tsType,
740
+ optional: true,
741
+ description: (0, utils_1.stripSmartComments)(field.description, true),
742
+ });
743
+ }
744
+ return properties;
745
+ }
746
+ }
747
+ // Fallback: derive from table fields when schema filter type is not available
725
748
  const properties = [];
726
749
  for (const field of table.fields) {
727
750
  const fieldType = typeof field.type === 'string' ? field.type : field.type.gqlType;
@@ -740,11 +763,11 @@ function buildTableFilterProperties(table) {
740
763
  /**
741
764
  * Generate table filter type statements
742
765
  */
743
- function generateTableFilterTypes(tables) {
766
+ function generateTableFilterTypes(tables, typeRegistry) {
744
767
  const statements = [];
745
768
  for (const table of tables) {
746
769
  const filterName = (0, utils_1.getFilterTypeName)(table);
747
- statements.push(createExportedInterface(filterName, buildTableFilterProperties(table)));
770
+ statements.push(createExportedInterface(filterName, buildTableFilterProperties(table, typeRegistry)));
748
771
  }
749
772
  if (statements.length > 0) {
750
773
  addSectionComment(statements, 'Table Filter Types');
@@ -1323,6 +1346,43 @@ function generateConnectionFieldsMap(tables, tableByName) {
1323
1346
  // ============================================================================
1324
1347
  // Plugin-Injected Type Collector
1325
1348
  // ============================================================================
1349
+ /**
1350
+ * Collect extra input type names referenced by plugin-injected filter fields.
1351
+ *
1352
+ * When the schema's filter type is used as source of truth, plugin-injected
1353
+ * fields reference custom filter types (e.g., Bm25BodyFilter, TsvectorFilter,
1354
+ * GeometryFilter) that also need to be generated. This function discovers
1355
+ * those types by comparing the schema's filter type fields against the
1356
+ * standard scalar filter types.
1357
+ */
1358
+ function collectFilterExtraInputTypes(tables, typeRegistry) {
1359
+ const extraTypes = new Set();
1360
+ for (const table of tables) {
1361
+ const filterTypeName = (0, utils_1.getFilterTypeName)(table);
1362
+ const filterType = typeRegistry.get(filterTypeName);
1363
+ if (!filterType ||
1364
+ filterType.kind !== 'INPUT_OBJECT' ||
1365
+ !filterType.inputFields) {
1366
+ continue;
1367
+ }
1368
+ const tableFieldNames = new Set(table.fields
1369
+ .filter((f) => !(0, utils_1.isRelationField)(f.name, table))
1370
+ .map((f) => f.name));
1371
+ for (const field of filterType.inputFields) {
1372
+ // Skip standard column-derived fields and logical operators
1373
+ if (tableFieldNames.has(field.name))
1374
+ continue;
1375
+ if (['and', 'or', 'not'].includes(field.name))
1376
+ continue;
1377
+ // Collect the base type name of this extra field
1378
+ const baseName = (0, type_resolver_1.getTypeBaseName)(field.type);
1379
+ if (baseName && !scalars_1.SCALAR_NAMES.has(baseName)) {
1380
+ extraTypes.add(baseName);
1381
+ }
1382
+ }
1383
+ }
1384
+ return extraTypes;
1385
+ }
1326
1386
  /**
1327
1387
  * Collect extra input type names referenced by plugin-injected condition fields.
1328
1388
  *
@@ -1386,7 +1446,9 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
1386
1446
  statements.push(...generateEntityWithRelations(tablesList));
1387
1447
  statements.push(...generateEntitySelectTypes(tablesList, tableByName));
1388
1448
  // 4. Table filter types
1389
- statements.push(...generateTableFilterTypes(tablesList));
1449
+ // Pass typeRegistry to use schema's filter type as source of truth,
1450
+ // capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
1451
+ statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
1390
1452
  // 4b. Table condition types (simple equality filter)
1391
1453
  // Pass typeRegistry to merge plugin-injected condition fields
1392
1454
  // (e.g., vectorEmbedding from VectorSearchPlugin)
@@ -1404,8 +1466,14 @@ function generateInputTypesFile(typeRegistry, usedInputTypes, tables, usedPayloa
1404
1466
  // Always emit this export so generated model/custom-op imports stay valid.
1405
1467
  statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
1406
1468
  // 7. Custom input types from TypeRegistry
1407
- // Also include any extra types referenced by plugin-injected condition fields
1469
+ // Also include any extra types referenced by plugin-injected filter/condition fields
1408
1470
  const mergedUsedInputTypes = new Set(usedInputTypes);
1471
+ if (hasTables) {
1472
+ const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
1473
+ for (const typeName of filterExtraTypes) {
1474
+ mergedUsedInputTypes.add(typeName);
1475
+ }
1476
+ }
1409
1477
  if (hasTables && conditionEnabled) {
1410
1478
  const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
1411
1479
  for (const typeName of conditionExtraTypes) {
@@ -7,7 +7,7 @@ export interface GeneratedModelFile {
7
7
  }
8
8
  export declare function generateModelFile(table: Table, _useSharedTypes: boolean, options?: {
9
9
  condition?: boolean;
10
- }): GeneratedModelFile;
10
+ }, allTables?: Table[]): GeneratedModelFile;
11
11
  export declare function generateAllModelFiles(tables: Table[], useSharedTypes: boolean, options?: {
12
12
  condition?: boolean;
13
13
  }): GeneratedModelFile[];
@@ -42,6 +42,7 @@ exports.generateAllModelFiles = generateAllModelFiles;
42
42
  * Each method uses function overloads for IDE autocompletion of select objects.
43
43
  */
44
44
  const t = __importStar(require("@babel/types"));
45
+ const inflekt_1 = require("inflekt");
45
46
  const babel_ast_1 = require("../babel-ast");
46
47
  const utils_1 = require("../utils");
47
48
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
@@ -102,7 +103,7 @@ function strictSelectGuard(selectTypeName) {
102
103
  t.tsTypeReference(t.identifier(selectTypeName)),
103
104
  ]));
104
105
  }
105
- function generateModelFile(table, _useSharedTypes, options) {
106
+ function generateModelFile(table, _useSharedTypes, options, allTables) {
106
107
  const conditionEnabled = options?.condition !== false;
107
108
  const { typeName, singularName, pluralName } = (0, utils_1.getTableNames)(table);
108
109
  const modelName = `${typeName}Model`;
@@ -116,7 +117,6 @@ function generateModelFile(table, _useSharedTypes, options) {
116
117
  const orderByTypeName = (0, utils_1.getOrderByTypeName)(table);
117
118
  const createInputTypeName = `Create${typeName}Input`;
118
119
  const updateInputTypeName = `Update${typeName}Input`;
119
- const deleteInputTypeName = `Delete${typeName}Input`;
120
120
  const patchTypeName = `${typeName}Patch`;
121
121
  const pkFields = (0, utils_1.getPrimaryKeyInfo)(table);
122
122
  const pkField = pkFields[0];
@@ -126,9 +126,16 @@ function generateModelFile(table, _useSharedTypes, options) {
126
126
  const createMutationName = table.query?.create ?? `create${typeName}`;
127
127
  const updateMutationName = table.query?.update;
128
128
  const deleteMutationName = table.query?.delete;
129
+ const deleteInputTypeName = (0, utils_1.getDeleteInputTypeName)(table);
129
130
  const statements = [];
130
131
  statements.push(createImportDeclaration('../client', ['OrmClient']));
131
- 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
package/core/generate.js CHANGED
@@ -675,11 +675,23 @@ async function generateMulti(options) {
675
675
  await writeFiles(cliSkillsToWrite, skillsOutputDir, [], { pruneStaleFiles: false });
676
676
  }
677
677
  }
678
- // Generate root-root README if multi-target
678
+ // Generate root-root README and barrel if multi-target
679
679
  if (names.length > 1 && targetInfos.length > 0 && !dryRun) {
680
- const rootReadme = (0, target_docs_generator_1.generateRootRootReadme)(targetInfos);
681
680
  const { writeGeneratedFiles: writeFiles } = await Promise.resolve().then(() => __importStar(require('./output')));
681
+ const rootReadme = (0, target_docs_generator_1.generateRootRootReadme)(targetInfos);
682
682
  await writeFiles([{ path: rootReadme.fileName, content: rootReadme.content }], '.', [], { pruneStaleFiles: false });
683
+ // Write a root barrel (index.ts) that re-exports each target as a
684
+ // namespace so the package has a single entry-point. Derive the
685
+ // common output root from the first target's output path.
686
+ const successfulNames = results
687
+ .filter((r) => r.result.success)
688
+ .map((r) => r.name);
689
+ if (successfulNames.length > 0) {
690
+ const firstOutput = (0, config_1.getConfigOptions)(configs[successfulNames[0]]).output;
691
+ const outputRoot = node_path_1.default.dirname(firstOutput);
692
+ const barrelContent = (0, barrel_1.generateMultiTargetBarrel)(successfulNames);
693
+ await writeFiles([{ path: 'index.ts', content: barrelContent }], outputRoot, [], { pruneStaleFiles: false });
694
+ }
683
695
  }
684
696
  }
685
697
  finally {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * M:N Relation Enrichment
3
+ *
4
+ * After table inference from introspection, enriches ManyToManyRelation objects
5
+ * with junction key field metadata from _cachedTablesMeta (MetaSchemaPlugin).
6
+ */
7
+ import type { Table } from '../../types/schema';
8
+ import type { MetaTableInfo } from './source/types';
9
+ /**
10
+ * Enrich M:N relations with junction key field metadata from _meta.
11
+ * Mutates the tables array in-place.
12
+ */
13
+ export declare function enrichManyToManyRelations(tables: Table[], tablesMeta?: MetaTableInfo[]): void;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.enrichManyToManyRelations = enrichManyToManyRelations;
4
+ /**
5
+ * Enrich M:N relations with junction key field metadata from _meta.
6
+ * Mutates the tables array in-place.
7
+ */
8
+ function enrichManyToManyRelations(tables, tablesMeta) {
9
+ if (!tablesMeta?.length)
10
+ return;
11
+ const metaByName = new Map(tablesMeta.map((m) => [m.name, m]));
12
+ for (const table of tables) {
13
+ const meta = metaByName.get(table.name);
14
+ if (!meta?.relations.manyToMany.length)
15
+ continue;
16
+ for (const rel of table.relations.manyToMany) {
17
+ const metaRel = meta.relations.manyToMany.find((m) => m.fieldName === rel.fieldName);
18
+ if (!metaRel)
19
+ continue;
20
+ rel.junctionLeftKeyFields = metaRel.junctionLeftKeyAttributes.map((a) => a.name);
21
+ rel.junctionRightKeyFields = metaRel.junctionRightKeyAttributes.map((a) => a.name);
22
+ rel.leftKeyFields = metaRel.leftKeyAttributes.map((a) => a.name);
23
+ rel.rightKeyFields = metaRel.rightKeyAttributes.map((a) => a.name);
24
+ }
25
+ }
26
+ }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateSourceOptions = exports.createSchemaSource = void 0;
4
4
  exports.runCodegenPipeline = runCodegenPipeline;
5
5
  exports.validateTablesFound = validateTablesFound;
6
+ const enrich_relations_1 = require("../introspect/enrich-relations");
6
7
  const infer_tables_1 = require("../introspect/infer-tables");
7
8
  const transform_1 = require("../introspect/transform");
8
9
  const transform_schema_1 = require("../introspect/transform-schema");
@@ -27,13 +28,18 @@ async function runCodegenPipeline(options) {
27
28
  const log = verbose ? console.log : () => { };
28
29
  // 1. Fetch introspection from source
29
30
  log(`Fetching schema from ${source.describe()}...`);
30
- const { introspection } = await source.fetch();
31
+ const { introspection, tablesMeta } = await source.fetch();
31
32
  // 2. Infer tables from introspection (replaces _meta)
32
33
  log('Inferring table metadata from schema...');
33
34
  const commentsEnabled = config.codegen?.comments !== false;
34
35
  let tables = (0, infer_tables_1.inferTablesFromIntrospection)(introspection, { comments: commentsEnabled });
35
36
  const totalTables = tables.length;
36
37
  log(` Found ${totalTables} tables`);
38
+ // 2a. Enrich M:N relations with junction key metadata from _meta
39
+ if (tablesMeta?.length) {
40
+ (0, enrich_relations_1.enrichManyToManyRelations)(tables, tablesMeta);
41
+ log(` Enriched M:N relations from _meta (${tablesMeta.length} tables)`);
42
+ }
37
43
  // 3. Filter tables by config (combine exclude and systemExclude)
38
44
  tables = (0, transform_1.filterTables)(tables, config.tables.include, [
39
45
  ...config.tables.exclude,
@@ -34,6 +34,16 @@ export interface RootBarrelOptions {
34
34
  * Re-exports from subdirectories based on which generators are enabled.
35
35
  */
36
36
  export declare function generateRootBarrel(options?: RootBarrelOptions): string;
37
+ /**
38
+ * Generate a root index.ts for multi-target output that re-exports each
39
+ * target as a namespace.
40
+ *
41
+ * Example output:
42
+ * export * as admin from './admin';
43
+ * export * as auth from './auth';
44
+ * export * as public_ from './public';
45
+ */
46
+ export declare function generateMultiTargetBarrel(targetNames: string[]): string;
37
47
  /**
38
48
  * Generate queries barrel including custom query operations
39
49
  */
@@ -159,6 +159,49 @@ export function generateRootBarrel(options = {}) {
159
159
  return generateCode(statements);
160
160
  }
161
161
  // ============================================================================
162
+ // Multi-target root barrel (re-exports each target as a namespace)
163
+ // ============================================================================
164
+ /** JS reserved words that need an alias when used as export names */
165
+ const JS_RESERVED = new Set([
166
+ 'abstract', 'arguments', 'await', 'boolean', 'break', 'byte', 'case', 'catch',
167
+ 'char', 'class', 'const', 'continue', 'debugger', 'default', 'delete', 'do',
168
+ 'double', 'else', 'enum', 'eval', 'export', 'extends', 'false', 'final',
169
+ 'finally', 'float', 'for', 'function', 'goto', 'if', 'implements', 'import',
170
+ 'in', 'instanceof', 'int', 'interface', 'let', 'long', 'native', 'new',
171
+ 'null', 'package', 'private', 'protected', 'public', 'return', 'short',
172
+ 'static', 'super', 'switch', 'synchronized', 'this', 'throw', 'throws',
173
+ 'transient', 'true', 'try', 'typeof', 'var', 'void', 'volatile', 'while',
174
+ 'with', 'yield',
175
+ ]);
176
+ /**
177
+ * Generate a root index.ts for multi-target output that re-exports each
178
+ * target as a namespace.
179
+ *
180
+ * Example output:
181
+ * export * as admin from './admin';
182
+ * export * as auth from './auth';
183
+ * export * as public_ from './public';
184
+ */
185
+ export function generateMultiTargetBarrel(targetNames) {
186
+ const statements = [];
187
+ for (const name of targetNames) {
188
+ const alias = JS_RESERVED.has(name) ? `${name}_` : name;
189
+ const exportDecl = t.exportNamedDeclaration(null, [t.exportNamespaceSpecifier(t.identifier(alias))], t.stringLiteral(`./${name}`));
190
+ statements.push(exportDecl);
191
+ }
192
+ if (statements.length > 0) {
193
+ addJSDocComment(statements[0], [
194
+ '@constructive-io/sdk',
195
+ '',
196
+ 'Auto-generated GraphQL types and ORM client.',
197
+ 'Run `pnpm run generate` to populate this package from the schema files.',
198
+ '',
199
+ '@generated by @constructive-io/graphql-codegen',
200
+ ]);
201
+ }
202
+ return generateCode(statements);
203
+ }
204
+ // ============================================================================
162
205
  // Custom operation barrels (includes both table and custom hooks)
163
206
  // ============================================================================
164
207
  /**
@@ -680,10 +680,33 @@ function getFilterTypeForField(fieldType, isArray = false) {
680
680
  return scalarToFilterType(fieldType, isArray) ?? 'StringFilter';
681
681
  }
682
682
  /**
683
- * Build properties for a table filter interface
683
+ * Build properties for a table filter interface.
684
+ *
685
+ * When typeRegistry is available, uses the schema's filter input type as
686
+ * the sole source of truth — this captures plugin-injected filter fields
687
+ * (e.g., bm25Body, tsvTsv, trgmName, vectorEmbedding, geom) that are not
688
+ * present on the entity type itself. Same pattern as buildOrderByValues().
684
689
  */
685
- function buildTableFilterProperties(table) {
690
+ function buildTableFilterProperties(table, typeRegistry) {
686
691
  const filterName = getFilterTypeName(table);
692
+ // When the schema's filter type is available, use it as the source of truth
693
+ if (typeRegistry) {
694
+ const filterType = typeRegistry.get(filterName);
695
+ if (filterType?.kind === 'INPUT_OBJECT' && filterType.inputFields) {
696
+ const properties = [];
697
+ for (const field of filterType.inputFields) {
698
+ const tsType = typeRefToTs(field.type);
699
+ properties.push({
700
+ name: field.name,
701
+ type: tsType,
702
+ optional: true,
703
+ description: stripSmartComments(field.description, true),
704
+ });
705
+ }
706
+ return properties;
707
+ }
708
+ }
709
+ // Fallback: derive from table fields when schema filter type is not available
687
710
  const properties = [];
688
711
  for (const field of table.fields) {
689
712
  const fieldType = typeof field.type === 'string' ? field.type : field.type.gqlType;
@@ -702,11 +725,11 @@ function buildTableFilterProperties(table) {
702
725
  /**
703
726
  * Generate table filter type statements
704
727
  */
705
- function generateTableFilterTypes(tables) {
728
+ function generateTableFilterTypes(tables, typeRegistry) {
706
729
  const statements = [];
707
730
  for (const table of tables) {
708
731
  const filterName = getFilterTypeName(table);
709
- statements.push(createExportedInterface(filterName, buildTableFilterProperties(table)));
732
+ statements.push(createExportedInterface(filterName, buildTableFilterProperties(table, typeRegistry)));
710
733
  }
711
734
  if (statements.length > 0) {
712
735
  addSectionComment(statements, 'Table Filter Types');
@@ -1285,6 +1308,43 @@ function generateConnectionFieldsMap(tables, tableByName) {
1285
1308
  // ============================================================================
1286
1309
  // Plugin-Injected Type Collector
1287
1310
  // ============================================================================
1311
+ /**
1312
+ * Collect extra input type names referenced by plugin-injected filter fields.
1313
+ *
1314
+ * When the schema's filter type is used as source of truth, plugin-injected
1315
+ * fields reference custom filter types (e.g., Bm25BodyFilter, TsvectorFilter,
1316
+ * GeometryFilter) that also need to be generated. This function discovers
1317
+ * those types by comparing the schema's filter type fields against the
1318
+ * standard scalar filter types.
1319
+ */
1320
+ function collectFilterExtraInputTypes(tables, typeRegistry) {
1321
+ const extraTypes = new Set();
1322
+ for (const table of tables) {
1323
+ const filterTypeName = getFilterTypeName(table);
1324
+ const filterType = typeRegistry.get(filterTypeName);
1325
+ if (!filterType ||
1326
+ filterType.kind !== 'INPUT_OBJECT' ||
1327
+ !filterType.inputFields) {
1328
+ continue;
1329
+ }
1330
+ const tableFieldNames = new Set(table.fields
1331
+ .filter((f) => !isRelationField(f.name, table))
1332
+ .map((f) => f.name));
1333
+ for (const field of filterType.inputFields) {
1334
+ // Skip standard column-derived fields and logical operators
1335
+ if (tableFieldNames.has(field.name))
1336
+ continue;
1337
+ if (['and', 'or', 'not'].includes(field.name))
1338
+ continue;
1339
+ // Collect the base type name of this extra field
1340
+ const baseName = getTypeBaseName(field.type);
1341
+ if (baseName && !SCALAR_NAMES.has(baseName)) {
1342
+ extraTypes.add(baseName);
1343
+ }
1344
+ }
1345
+ }
1346
+ return extraTypes;
1347
+ }
1288
1348
  /**
1289
1349
  * Collect extra input type names referenced by plugin-injected condition fields.
1290
1350
  *
@@ -1348,7 +1408,9 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
1348
1408
  statements.push(...generateEntityWithRelations(tablesList));
1349
1409
  statements.push(...generateEntitySelectTypes(tablesList, tableByName));
1350
1410
  // 4. Table filter types
1351
- statements.push(...generateTableFilterTypes(tablesList));
1411
+ // Pass typeRegistry to use schema's filter type as source of truth,
1412
+ // capturing plugin-injected filter fields (e.g., bm25, tsvector, trgm, vector, geom)
1413
+ statements.push(...generateTableFilterTypes(tablesList, typeRegistry));
1352
1414
  // 4b. Table condition types (simple equality filter)
1353
1415
  // Pass typeRegistry to merge plugin-injected condition fields
1354
1416
  // (e.g., vectorEmbedding from VectorSearchPlugin)
@@ -1366,8 +1428,14 @@ export function generateInputTypesFile(typeRegistry, usedInputTypes, tables, use
1366
1428
  // Always emit this export so generated model/custom-op imports stay valid.
1367
1429
  statements.push(...generateConnectionFieldsMap(tablesList, tableByName));
1368
1430
  // 7. Custom input types from TypeRegistry
1369
- // Also include any extra types referenced by plugin-injected condition fields
1431
+ // Also include any extra types referenced by plugin-injected filter/condition fields
1370
1432
  const mergedUsedInputTypes = new Set(usedInputTypes);
1433
+ if (hasTables) {
1434
+ const filterExtraTypes = collectFilterExtraInputTypes(tablesList, typeRegistry);
1435
+ for (const typeName of filterExtraTypes) {
1436
+ mergedUsedInputTypes.add(typeName);
1437
+ }
1438
+ }
1371
1439
  if (hasTables && conditionEnabled) {
1372
1440
  const conditionExtraTypes = collectConditionExtraInputTypes(tablesList, typeRegistry);
1373
1441
  for (const typeName of conditionExtraTypes) {
@@ -7,7 +7,7 @@ export interface GeneratedModelFile {
7
7
  }
8
8
  export declare function generateModelFile(table: Table, _useSharedTypes: boolean, options?: {
9
9
  condition?: boolean;
10
- }): GeneratedModelFile;
10
+ }, allTables?: Table[]): GeneratedModelFile;
11
11
  export declare function generateAllModelFiles(tables: Table[], useSharedTypes: boolean, options?: {
12
12
  condition?: boolean;
13
13
  }): GeneratedModelFile[];
@@ -5,8 +5,9 @@
5
5
  * Each method uses function overloads for IDE autocompletion of select objects.
6
6
  */
7
7
  import * as t from '@babel/types';
8
+ import { singularize } from 'inflekt';
8
9
  import { generateCode } from '../babel-ast';
9
- import { getFilterTypeName, getGeneratedFileHeader, getOrderByTypeName, getPrimaryKeyInfo, getSingleRowQueryName, getTableNames, hasValidPrimaryKey, lcFirst, } from '../utils';
10
+ import { getCreateInputTypeName, getCreateMutationName, getDeleteInputTypeName, getDeleteMutationName, getFilterTypeName, getGeneratedFileHeader, getOrderByTypeName, getPrimaryKeyInfo, getSingleRowQueryName, getTableNames, hasValidPrimaryKey, lcFirst, ucFirst, } from '../utils';
10
11
  function createImportDeclaration(moduleSpecifier, namedImports, typeOnly = false) {
11
12
  const specifiers = namedImports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name)));
12
13
  const decl = t.importDeclaration(specifiers, t.stringLiteral(moduleSpecifier));
@@ -65,7 +66,7 @@ function strictSelectGuard(selectTypeName) {
65
66
  t.tsTypeReference(t.identifier(selectTypeName)),
66
67
  ]));
67
68
  }
68
- export function generateModelFile(table, _useSharedTypes, options) {
69
+ export function generateModelFile(table, _useSharedTypes, options, allTables) {
69
70
  const conditionEnabled = options?.condition !== false;
70
71
  const { typeName, singularName, pluralName } = getTableNames(table);
71
72
  const modelName = `${typeName}Model`;
@@ -79,7 +80,6 @@ export function generateModelFile(table, _useSharedTypes, options) {
79
80
  const orderByTypeName = getOrderByTypeName(table);
80
81
  const createInputTypeName = `Create${typeName}Input`;
81
82
  const updateInputTypeName = `Update${typeName}Input`;
82
- const deleteInputTypeName = `Delete${typeName}Input`;
83
83
  const patchTypeName = `${typeName}Patch`;
84
84
  const pkFields = getPrimaryKeyInfo(table);
85
85
  const pkField = pkFields[0];
@@ -89,9 +89,16 @@ export function generateModelFile(table, _useSharedTypes, options) {
89
89
  const createMutationName = table.query?.create ?? `create${typeName}`;
90
90
  const updateMutationName = table.query?.update;
91
91
  const deleteMutationName = table.query?.delete;
92
+ const deleteInputTypeName = getDeleteInputTypeName(table);
92
93
  const statements = [];
93
94
  statements.push(createImportDeclaration('../client', ['OrmClient']));
94
- 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
@@ -13,7 +13,7 @@ import { createEphemeralDb } from 'pgsql-client';
13
13
  import { deployPgpm } from 'pgsql-seed';
14
14
  import { getConfigOptions } from '../types/config';
15
15
  import { generate as generateReactQueryFiles } from './codegen';
16
- import { generateRootBarrel } from './codegen/barrel';
16
+ import { generateRootBarrel, generateMultiTargetBarrel } from './codegen/barrel';
17
17
  import { generateCli as generateCliFiles, generateMultiTargetCli } from './codegen/cli';
18
18
  import { generateReadme as generateCliReadme, generateAgentsDocs as generateCliAgentsDocs, getCliMcpTools, generateSkills as generateCliSkills, generateMultiTargetReadme, generateMultiTargetAgentsDocs, getMultiTargetCliMcpTools, generateMultiTargetSkills, } from './codegen/cli/docs-generator';
19
19
  import { resolveDocsConfig } from './codegen/docs-utils';
@@ -633,11 +633,23 @@ export async function generateMulti(options) {
633
633
  await writeFiles(cliSkillsToWrite, skillsOutputDir, [], { pruneStaleFiles: false });
634
634
  }
635
635
  }
636
- // Generate root-root README if multi-target
636
+ // Generate root-root README and barrel if multi-target
637
637
  if (names.length > 1 && targetInfos.length > 0 && !dryRun) {
638
- const rootReadme = generateRootRootReadme(targetInfos);
639
638
  const { writeGeneratedFiles: writeFiles } = await import('./output');
639
+ const rootReadme = generateRootRootReadme(targetInfos);
640
640
  await writeFiles([{ path: rootReadme.fileName, content: rootReadme.content }], '.', [], { pruneStaleFiles: false });
641
+ // Write a root barrel (index.ts) that re-exports each target as a
642
+ // namespace so the package has a single entry-point. Derive the
643
+ // common output root from the first target's output path.
644
+ const successfulNames = results
645
+ .filter((r) => r.result.success)
646
+ .map((r) => r.name);
647
+ if (successfulNames.length > 0) {
648
+ const firstOutput = getConfigOptions(configs[successfulNames[0]]).output;
649
+ const outputRoot = path.dirname(firstOutput);
650
+ const barrelContent = generateMultiTargetBarrel(successfulNames);
651
+ await writeFiles([{ path: 'index.ts', content: barrelContent }], outputRoot, [], { pruneStaleFiles: false });
652
+ }
641
653
  }
642
654
  }
643
655
  finally {
@@ -0,0 +1,13 @@
1
+ /**
2
+ * M:N Relation Enrichment
3
+ *
4
+ * After table inference from introspection, enriches ManyToManyRelation objects
5
+ * with junction key field metadata from _cachedTablesMeta (MetaSchemaPlugin).
6
+ */
7
+ import type { Table } from '../../types/schema';
8
+ import type { MetaTableInfo } from './source/types';
9
+ /**
10
+ * Enrich M:N relations with junction key field metadata from _meta.
11
+ * Mutates the tables array in-place.
12
+ */
13
+ export declare function enrichManyToManyRelations(tables: Table[], tablesMeta?: MetaTableInfo[]): void;
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Enrich M:N relations with junction key field metadata from _meta.
3
+ * Mutates the tables array in-place.
4
+ */
5
+ export function enrichManyToManyRelations(tables, tablesMeta) {
6
+ if (!tablesMeta?.length)
7
+ return;
8
+ const metaByName = new Map(tablesMeta.map((m) => [m.name, m]));
9
+ for (const table of tables) {
10
+ const meta = metaByName.get(table.name);
11
+ if (!meta?.relations.manyToMany.length)
12
+ continue;
13
+ for (const rel of table.relations.manyToMany) {
14
+ const metaRel = meta.relations.manyToMany.find((m) => m.fieldName === rel.fieldName);
15
+ if (!metaRel)
16
+ continue;
17
+ rel.junctionLeftKeyFields = metaRel.junctionLeftKeyAttributes.map((a) => a.name);
18
+ rel.junctionRightKeyFields = metaRel.junctionRightKeyAttributes.map((a) => a.name);
19
+ rel.leftKeyFields = metaRel.leftKeyAttributes.map((a) => a.name);
20
+ rel.rightKeyFields = metaRel.rightKeyAttributes.map((a) => a.name);
21
+ }
22
+ }
23
+ }
@@ -1,3 +1,4 @@
1
+ import { enrichManyToManyRelations } from '../introspect/enrich-relations';
1
2
  import { inferTablesFromIntrospection } from '../introspect/infer-tables';
2
3
  import { filterTables } from '../introspect/transform';
3
4
  import { filterOperations, getCustomOperations, getTableOperationNames, transformSchemaToOperations, } from '../introspect/transform-schema';
@@ -20,13 +21,18 @@ export async function runCodegenPipeline(options) {
20
21
  const log = verbose ? console.log : () => { };
21
22
  // 1. Fetch introspection from source
22
23
  log(`Fetching schema from ${source.describe()}...`);
23
- const { introspection } = await source.fetch();
24
+ const { introspection, tablesMeta } = await source.fetch();
24
25
  // 2. Infer tables from introspection (replaces _meta)
25
26
  log('Inferring table metadata from schema...');
26
27
  const commentsEnabled = config.codegen?.comments !== false;
27
28
  let tables = inferTablesFromIntrospection(introspection, { comments: commentsEnabled });
28
29
  const totalTables = tables.length;
29
30
  log(` Found ${totalTables} tables`);
31
+ // 2a. Enrich M:N relations with junction key metadata from _meta
32
+ if (tablesMeta?.length) {
33
+ enrichManyToManyRelations(tables, tablesMeta);
34
+ log(` Enriched M:N relations from _meta (${tablesMeta.length} tables)`);
35
+ }
30
36
  // 3. Filter tables by config (combine exclude and systemExclude)
31
37
  tables = filterTables(tables, config.tables.include, [
32
38
  ...config.tables.exclude,
@@ -138,8 +138,8 @@ export interface DocsConfig {
138
138
  */
139
139
  mcp?: boolean;
140
140
  /**
141
- * Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
- * Skills are written to the workspace root skills/ directory (not nested in output).
141
+ * Generate .agents/skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
+ * Skills are written to {workspaceRoot}/.agents/skills/ (not nested in output).
143
143
  * Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
144
144
  * @default false
145
145
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@constructive-io/graphql-codegen",
3
- "version": "4.19.1",
3
+ "version": "4.21.0",
4
4
  "description": "GraphQL SDK generator for Constructive databases with React Query hooks",
5
5
  "keywords": [
6
6
  "graphql",
@@ -56,7 +56,7 @@
56
56
  "@0no-co/graphql.web": "^1.1.2",
57
57
  "@babel/generator": "^7.29.1",
58
58
  "@babel/types": "^7.29.0",
59
- "@constructive-io/graphql-query": "^3.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": "b0ff4ed0025af0e5df91d8e3535c347e4cde6438"
104
+ "gitHead": "7b5d57e1d1aa274a2914cec3240a902d2b1020c6"
105
105
  }
package/types/config.d.ts CHANGED
@@ -138,8 +138,8 @@ export interface DocsConfig {
138
138
  */
139
139
  mcp?: boolean;
140
140
  /**
141
- * Generate skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
- * Skills are written to the workspace root skills/ directory (not nested in output).
141
+ * Generate .agents/skills/ directory — per-entity SKILL.md files with YAML frontmatter.
142
+ * Skills are written to {workspaceRoot}/.agents/skills/ (not nested in output).
143
143
  * Uses composable naming: orm-{target}-{entity}, hooks-{target}-{entity}, cli-{target}-{entity}.
144
144
  * @default false
145
145
  */