Skip to content

Commit 23a31e5

Browse files
committed
feat: wire tablesMeta through pipeline, enrich M:N relations, add ORM add/remove junction methods
- Destructure tablesMeta in runCodegenPipeline() alongside introspection - Create enrich-relations.ts to populate ManyToManyRelation junction key fields from _meta - Add buildJunctionRemoveDocument helper to query-builder template - Generate add<Relation>/remove<Relation> methods on ORM models for M:N relations - Methods delegate to junction table's existing create/delete CRUD mutations
1 parent b8d4ae2 commit 23a31e5

4 files changed

Lines changed: 249 additions & 11 deletions

File tree

graphql/codegen/src/core/codegen/orm/model-generator.ts

Lines changed: 180 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import {
1717
getTableNames,
1818
hasValidPrimaryKey,
1919
lcFirst,
20+
toPascalCase,
21+
ucFirst,
2022
} from '../utils';
2123

2224
export interface GeneratedModelFile {
@@ -165,6 +167,7 @@ export function generateModelFile(
165167
table: Table,
166168
_useSharedTypes: boolean,
167169
options?: { condition?: boolean },
170+
allTables?: Table[],
168171
): GeneratedModelFile {
169172
const conditionEnabled = options?.condition !== false;
170173
const { typeName, singularName, pluralName } = getTableNames(table);
@@ -196,16 +199,23 @@ export function generateModelFile(
196199
const statements: t.Statement[] = [];
197200

198201
statements.push(createImportDeclaration('../client', ['OrmClient']));
202+
const m2nRels = table.relations.manyToMany.filter(
203+
(r) => r.junctionLeftKeyFields?.length && r.junctionRightKeyFields?.length,
204+
);
205+
const hasM2n = m2nRels.length > 0;
206+
207+
const queryBuilderImports = [
208+
'QueryBuilder',
209+
'buildFindManyDocument',
210+
'buildFindFirstDocument',
211+
'buildFindOneDocument',
212+
'buildCreateDocument',
213+
'buildUpdateByPkDocument',
214+
'buildDeleteByPkDocument',
215+
...(hasM2n ? ['buildJunctionRemoveDocument'] : []),
216+
];
199217
statements.push(
200-
createImportDeclaration('../query-builder', [
201-
'QueryBuilder',
202-
'buildFindManyDocument',
203-
'buildFindFirstDocument',
204-
'buildFindOneDocument',
205-
'buildCreateDocument',
206-
'buildUpdateByPkDocument',
207-
'buildDeleteByPkDocument',
208-
]),
218+
createImportDeclaration('../query-builder', queryBuilderImports),
209219
);
210220
statements.push(
211221
createImportDeclaration(
@@ -982,6 +992,166 @@ export function generateModelFile(
982992
);
983993
}
984994

995+
// ── M:N add/remove methods ────────────────────────────────────────────
996+
for (const rel of m2nRels) {
997+
if (!rel.fieldName) continue;
998+
999+
const junctionTable = allTables?.find((tb) => tb.name === rel.junctionTable);
1000+
if (!junctionTable) continue;
1001+
1002+
const junctionNames = getTableNames(junctionTable);
1003+
const junctionCreateMutation = junctionTable.query?.create ?? `create${junctionNames.typeName}`;
1004+
const junctionCreateInputType = `Create${junctionNames.typeName}Input`;
1005+
const junctionDeleteMutation = junctionTable.query?.delete;
1006+
const junctionDeleteInputType = `Delete${junctionNames.typeName}Input`;
1007+
const junctionSingular = junctionNames.singularName;
1008+
1009+
// Derive a friendly singular name from the fieldName (e.g., "tags" → "Tag")
1010+
const relSingular = ucFirst(rel.fieldName.replace(/s$/, ''));
1011+
1012+
const leftKeys = rel.junctionLeftKeyFields!;
1013+
const rightKeys = rel.junctionRightKeyFields!;
1014+
const leftPkFields = rel.leftKeyFields ?? ['id'];
1015+
const rightPkFields = rel.rightKeyFields ?? ['id'];
1016+
1017+
// ── add<Relation> ───────────────────────────────────────────────
1018+
{
1019+
// Parameters: one param per left PK + one param per right PK
1020+
const params: t.Identifier[] = [];
1021+
for (const lk of leftPkFields) {
1022+
const p = t.identifier(lk);
1023+
p.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
1024+
params.push(p);
1025+
}
1026+
for (const rk of rightPkFields) {
1027+
const p = t.identifier(rk === leftPkFields[0] ? `right${ucFirst(rk)}` : rk);
1028+
p.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
1029+
params.push(p);
1030+
}
1031+
1032+
// Build the junction row data object: { junctionLeftKey: leftPk, junctionRightKey: rightPk }
1033+
const dataProps: t.ObjectProperty[] = [];
1034+
for (let i = 0; i < leftKeys.length; i++) {
1035+
dataProps.push(
1036+
t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)),
1037+
);
1038+
}
1039+
for (let i = 0; i < rightKeys.length; i++) {
1040+
dataProps.push(
1041+
t.objectProperty(
1042+
t.identifier(rightKeys[i]),
1043+
t.identifier(params[leftPkFields.length + i].name),
1044+
),
1045+
);
1046+
}
1047+
1048+
const body: t.Statement[] = [
1049+
t.variableDeclaration('const', [
1050+
t.variableDeclarator(
1051+
t.objectPattern([
1052+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
1053+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
1054+
]),
1055+
t.callExpression(t.identifier('buildCreateDocument'), [
1056+
t.stringLiteral(junctionNames.typeName),
1057+
t.stringLiteral(junctionCreateMutation),
1058+
t.stringLiteral(junctionSingular),
1059+
t.objectExpression([t.objectProperty(t.identifier('id'), t.booleanLiteral(true))]),
1060+
t.objectExpression(dataProps),
1061+
t.stringLiteral(junctionCreateInputType),
1062+
]),
1063+
),
1064+
]),
1065+
t.returnStatement(
1066+
t.newExpression(t.identifier('QueryBuilder'), [
1067+
t.objectExpression([
1068+
t.objectProperty(
1069+
t.identifier('client'),
1070+
t.memberExpression(t.thisExpression(), t.identifier('client')),
1071+
),
1072+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
1073+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
1074+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionCreateMutation)),
1075+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
1076+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
1077+
]),
1078+
]),
1079+
),
1080+
];
1081+
1082+
const method = t.classMethod('method', t.identifier(`add${relSingular}`), params, t.blockStatement(body));
1083+
method.async = true;
1084+
classBody.push(method);
1085+
}
1086+
1087+
// ── remove<Relation> ────────────────────────────────────────────
1088+
if (junctionDeleteMutation) {
1089+
const params: t.Identifier[] = [];
1090+
for (const lk of leftPkFields) {
1091+
const p = t.identifier(lk);
1092+
p.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
1093+
params.push(p);
1094+
}
1095+
for (const rk of rightPkFields) {
1096+
const p = t.identifier(rk === leftPkFields[0] ? `right${ucFirst(rk)}` : rk);
1097+
p.typeAnnotation = t.tsTypeAnnotation(t.tsStringKeyword());
1098+
params.push(p);
1099+
}
1100+
1101+
// Build the keys object for junction delete
1102+
const keysProps: t.ObjectProperty[] = [];
1103+
for (let i = 0; i < leftKeys.length; i++) {
1104+
keysProps.push(
1105+
t.objectProperty(t.identifier(leftKeys[i]), t.identifier(params[i].name)),
1106+
);
1107+
}
1108+
for (let i = 0; i < rightKeys.length; i++) {
1109+
keysProps.push(
1110+
t.objectProperty(
1111+
t.identifier(rightKeys[i]),
1112+
t.identifier(params[leftPkFields.length + i].name),
1113+
),
1114+
);
1115+
}
1116+
1117+
const body: t.Statement[] = [
1118+
t.variableDeclaration('const', [
1119+
t.variableDeclarator(
1120+
t.objectPattern([
1121+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
1122+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
1123+
]),
1124+
t.callExpression(t.identifier('buildJunctionRemoveDocument'), [
1125+
t.stringLiteral(junctionNames.typeName),
1126+
t.stringLiteral(junctionDeleteMutation),
1127+
t.objectExpression(keysProps),
1128+
t.stringLiteral(junctionDeleteInputType),
1129+
]),
1130+
),
1131+
]),
1132+
t.returnStatement(
1133+
t.newExpression(t.identifier('QueryBuilder'), [
1134+
t.objectExpression([
1135+
t.objectProperty(
1136+
t.identifier('client'),
1137+
t.memberExpression(t.thisExpression(), t.identifier('client')),
1138+
),
1139+
t.objectProperty(t.identifier('operation'), t.stringLiteral('mutation')),
1140+
t.objectProperty(t.identifier('operationName'), t.stringLiteral(junctionNames.typeName)),
1141+
t.objectProperty(t.identifier('fieldName'), t.stringLiteral(junctionDeleteMutation)),
1142+
t.objectProperty(t.identifier('document'), t.identifier('document'), false, true),
1143+
t.objectProperty(t.identifier('variables'), t.identifier('variables'), false, true),
1144+
]),
1145+
]),
1146+
),
1147+
];
1148+
1149+
const method = t.classMethod('method', t.identifier(`remove${relSingular}`), params, t.blockStatement(body));
1150+
method.async = true;
1151+
classBody.push(method);
1152+
}
1153+
}
1154+
9851155
const classDecl = t.classDeclaration(
9861156
t.identifier(modelName),
9871157
null,
@@ -1005,5 +1175,5 @@ export function generateAllModelFiles(
10051175
useSharedTypes: boolean,
10061176
options?: { condition?: boolean },
10071177
): GeneratedModelFile[] {
1008-
return tables.map((table) => generateModelFile(table, useSharedTypes, options));
1178+
return tables.map((table) => generateModelFile(table, useSharedTypes, options, tables));
10091179
}

graphql/codegen/src/core/codegen/templates/query-builder.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,25 @@ export function buildDeleteByPkDocument<TSelect = undefined>(
655655
};
656656
}
657657

658+
export function buildJunctionRemoveDocument(
659+
operationName: string,
660+
mutationField: string,
661+
keys: Record<string, unknown>,
662+
inputTypeName: string,
663+
): { document: string; variables: Record<string, unknown> } {
664+
return {
665+
document: buildInputMutationDocument({
666+
operationName,
667+
mutationField,
668+
inputTypeName,
669+
resultSelections: [t.field({ name: 'clientMutationId' })],
670+
}),
671+
variables: {
672+
input: keys,
673+
},
674+
};
675+
}
676+
658677
export function buildCustomDocument<TSelect, TArgs>(
659678
operationType: 'query' | 'mutation',
660679
operationName: string,
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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+
/**
11+
* Enrich M:N relations with junction key field metadata from _meta.
12+
* Mutates the tables array in-place.
13+
*/
14+
export function enrichManyToManyRelations(
15+
tables: Table[],
16+
tablesMeta?: MetaTableInfo[],
17+
): void {
18+
if (!tablesMeta?.length) return;
19+
20+
const metaByName = new Map(tablesMeta.map((m) => [m.name, m]));
21+
22+
for (const table of tables) {
23+
const meta = metaByName.get(table.name);
24+
if (!meta?.relations.manyToMany.length) continue;
25+
26+
for (const rel of table.relations.manyToMany) {
27+
const metaRel = meta.relations.manyToMany.find(
28+
(m) => m.fieldName === rel.fieldName,
29+
);
30+
if (!metaRel) continue;
31+
32+
rel.junctionLeftKeyFields = metaRel.junctionLeftKeyAttributes.map(
33+
(a) => a.name,
34+
);
35+
rel.junctionRightKeyFields = metaRel.junctionRightKeyAttributes.map(
36+
(a) => a.name,
37+
);
38+
rel.leftKeyFields = metaRel.leftKeyAttributes.map((a) => a.name);
39+
rel.rightKeyFields = metaRel.rightKeyAttributes.map((a) => a.name);
40+
}
41+
}
42+
}

graphql/codegen/src/core/pipeline/index.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import type {
1414
Table,
1515
TypeRegistry,
1616
} from '../../types/schema';
17+
import { enrichManyToManyRelations } from '../introspect/enrich-relations';
1718
import { inferTablesFromIntrospection } from '../introspect/infer-tables';
1819
import type { SchemaSource } from '../introspect/source';
1920
import { filterTables } from '../introspect/transform';
@@ -112,7 +113,7 @@ export async function runCodegenPipeline(
112113

113114
// 1. Fetch introspection from source
114115
log(`Fetching schema from ${source.describe()}...`);
115-
const { introspection } = await source.fetch();
116+
const { introspection, tablesMeta } = await source.fetch();
116117

117118
// 2. Infer tables from introspection (replaces _meta)
118119
log('Inferring table metadata from schema...');
@@ -121,6 +122,12 @@ export async function runCodegenPipeline(
121122
const totalTables = tables.length;
122123
log(` Found ${totalTables} tables`);
123124

125+
// 2a. Enrich M:N relations with junction key metadata from _meta
126+
if (tablesMeta?.length) {
127+
enrichManyToManyRelations(tables, tablesMeta);
128+
log(` Enriched M:N relations from _meta (${tablesMeta.length} tables)`);
129+
}
130+
124131
// 3. Filter tables by config (combine exclude and systemExclude)
125132
tables = filterTables(tables, config.tables.include, [
126133
...config.tables.exclude,

0 commit comments

Comments
 (0)