Skip to content

Commit b3bba1c

Browse files
committed
feat(emitter): emit through literal for N:M relations in contract.d.ts
Teach generateModelRelationsType to serialize the junction-table descriptor (through.table / parentColumns / childColumns / targetColumns) as a typed readonly literal when present in the relation object. Non-M:N relations (no through key) are byte-for-byte unchanged. Adds three unit tests: M:N with single-column keys, M:N with a composite parent key, and a regression guard that confirms through is absent on a plain N:1 relation that carries only an on block. Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 4f3db5a commit b3bba1c

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

packages/1-framework/3-tooling/emitter/src/domain-type-generation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,24 @@ export function generateModelRelationsType(relations: Record<string, unknown>):
127127
);
128128
}
129129

130+
const through = relObj['through'] as
131+
| {
132+
table?: string;
133+
parentColumns?: string[];
134+
childColumns?: string[];
135+
targetColumns?: string[];
136+
}
137+
| undefined;
138+
if (through?.table && through.parentColumns && through.childColumns && through.targetColumns) {
139+
const table = serializeValue(through.table);
140+
const parentColumns = through.parentColumns.map((c) => serializeValue(c)).join(', ');
141+
const childColumns = through.childColumns.map((c) => serializeValue(c)).join(', ');
142+
const targetColumns = through.targetColumns.map((c) => serializeValue(c)).join(', ');
143+
parts.push(
144+
`readonly through: { readonly table: ${table}; readonly parentColumns: readonly [${parentColumns}]; readonly childColumns: readonly [${childColumns}]; readonly targetColumns: readonly [${targetColumns}] }`,
145+
);
146+
}
147+
130148
if (parts.length > 0) {
131149
relationEntries.push(`readonly ${relName}: { ${parts.join('; ')} }`);
132150
}

packages/1-framework/3-tooling/emitter/test/domain-type-generation.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,56 @@ describe('generateModelRelationsType', () => {
343343
}),
344344
).toThrow('missing localFields or targetFields');
345345
});
346+
347+
it('emits through literal for N:M relations with junction metadata', () => {
348+
const result = generateModelRelationsType({
349+
tags: {
350+
to: crossRef('Tag'),
351+
cardinality: 'N:M',
352+
through: {
353+
table: 'post_tags',
354+
parentColumns: ['postId'],
355+
childColumns: ['tagId'],
356+
targetColumns: ['id'],
357+
},
358+
},
359+
});
360+
expect(result).toContain("readonly model: 'Tag'");
361+
expect(result).toContain("readonly cardinality: 'N:M'");
362+
expect(result).toContain('readonly through:');
363+
expect(result).toContain("readonly table: 'post_tags'");
364+
expect(result).toContain("readonly parentColumns: readonly ['postId']");
365+
expect(result).toContain("readonly childColumns: readonly ['tagId']");
366+
expect(result).toContain("readonly targetColumns: readonly ['id']");
367+
});
368+
369+
it('emits through with multi-column keys', () => {
370+
const result = generateModelRelationsType({
371+
roles: {
372+
to: crossRef('Role'),
373+
cardinality: 'N:M',
374+
through: {
375+
table: 'user_roles',
376+
parentColumns: ['userId', 'tenantId'],
377+
childColumns: ['roleId'],
378+
targetColumns: ['id'],
379+
},
380+
},
381+
});
382+
expect(result).toContain("readonly parentColumns: readonly ['userId', 'tenantId']");
383+
});
384+
385+
it('omits through when not present (non-N:M relations unchanged)', () => {
386+
const result = generateModelRelationsType({
387+
author: {
388+
to: crossRef('User'),
389+
cardinality: 'N:1',
390+
on: { localFields: ['authorId'], targetFields: ['id'] },
391+
},
392+
});
393+
expect(result).not.toContain('readonly through:');
394+
expect(result).toContain("readonly localFields: readonly ['authorId']");
395+
});
346396
});
347397

348398
describe('deduplicateImports', () => {

0 commit comments

Comments
 (0)