Skip to content
This repository was archived by the owner on Aug 25, 2026. It is now read-only.

Commit e6fa3cc

Browse files
test(enums): acceptance guards + emit/no-emit agreement (TML-2952 D4)
Add the A1/A2 structural guard (enum-typing-codec-seam.guard.test.ts, modeled on rls-layer-invariant): the deleted direct-render helpers stay gone, and the only domain-enum typing reader repo-wide is the Mongo interim resolver. Add the A6 explicit assertion in the demo type test: the emitted Post.priority field type equals the no-emit db.enums value union. Update the mongo enum e2e mock lookup to supply renderValueTypeFor (mirrors production; shipped contract byte-identical). Full CI gate set green (build/typecheck force, whole Lint job, fixtures:check empty diff, test:packages/integration/e2e) except 3 config-loader tests that fail only on macOS (tmpdir /var vs /private/var), untouched by and unrelated to this slice, green on Linux CI. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent e5b744b commit e6fa3cc

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

examples/prisma-next-demo/test/demo-dx.types.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ test('emitted contract.d.ts types Post.priority as the value union, not string',
4646
expectTypeOf<PriorityOutput>().not.toEqualTypeOf<string>();
4747
});
4848

49+
// A6 (TML-2952): emit vs no-emit agreement. The emitted `FieldOutputTypes` enum
50+
// field type (produced on the emit path via the codec seam) must equal the enum's
51+
// authored value union carried by `db.enums` (the no-emit reference — the authored
52+
// member values propagated without serialization). Same enum, both paths, one union.
53+
test('A6: emitted Post.priority field type equals the no-emit db.enums value union', () => {
54+
type EmittedPriority = FieldOutputTypes['public']['Post']['priority'];
55+
type NoEmitPriorityValues = EnumValues<typeof db.enums.public.Priority>;
56+
expectTypeOf<EmittedPriority>().toEqualTypeOf<NoEmitPriorityValues>();
57+
});
58+
4959
test('emitted contract: db.sql.public.post SELECT priority yields the value union', () => {
5060
const plan = db.sql.public.post.select('id', 'priority').build();
5161
type Row = ResultType<typeof plan>;
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/**
2+
* Structural acceptance guards for the enum-typing-via-codec slice (TML-2952).
3+
*
4+
* A1 — every emit-path enum/value-set type goes through the codec seam
5+
* (`CodecLookup.renderValueTypeFor` / `CodecDescriptor.renderValueType`).
6+
* The pre-refactor helpers that rendered stored values straight to TS
7+
* literals (`renderValueSetUnionBase`, `renderValueSetLiteral`,
8+
* `renderEnumValueUnion`, `renderEnumMemberLiteral`) and the
9+
* `DomainEnumLookup` indirection stay deleted, and no emit-path source
10+
* reintroduces a direct value-to-literal render.
11+
*
12+
* A2 — no SQL or framework-emitter source reads `domain.enum` / `ns.enum` to
13+
* PRODUCE A TYPE. The single allowed `ns.enum` read in
14+
* `generate-contract-dts.ts` feeds `generateEnumBlockType`, which emits the
15+
* runtime `db.enums` dictionary block (not a field/column type). The only
16+
* domain-enum *typing* reader repo-wide is the Mongo interim resolver
17+
* (`packages/2-mongo-family/3-tooling/emitter/src/index.ts`), removed by
18+
* TML-2953.
19+
*
20+
* Comments are stripped before scanning so the doc comments naming the deleted
21+
* helpers (here and in the production sources) do not trip the guard.
22+
*/
23+
24+
import * as fs from 'node:fs';
25+
import * as path from 'node:path';
26+
import { describe, expect, it } from 'vitest';
27+
28+
const repoRoot = path.resolve(__dirname, '../../../../..');
29+
30+
function stripComments(source: string): string {
31+
return source.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/[^\n]*/g, '');
32+
}
33+
34+
function collectSourceFiles(dir: string): string[] {
35+
const files: string[] = [];
36+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
37+
const full = path.join(dir, entry.name);
38+
if (entry.isDirectory()) {
39+
if (entry.name === 'dist' || entry.name === 'node_modules' || entry.name === 'test') continue;
40+
files.push(...collectSourceFiles(full));
41+
} else if (entry.isFile() && entry.name.endsWith('.ts')) {
42+
files.push(full);
43+
}
44+
}
45+
return files;
46+
}
47+
48+
function scan(
49+
relDir: string,
50+
patterns: readonly RegExp[],
51+
): Array<{ file: string; line: number; text: string }> {
52+
const hits: Array<{ file: string; line: number; text: string }> = [];
53+
for (const file of collectSourceFiles(path.join(repoRoot, relDir))) {
54+
const lines = stripComments(fs.readFileSync(file, 'utf-8')).split('\n');
55+
for (let i = 0; i < lines.length; i++) {
56+
const line = lines[i];
57+
if (line === undefined) continue;
58+
for (const pattern of patterns) {
59+
if (pattern.test(line)) {
60+
hits.push({ file: path.relative(repoRoot, file), line: i + 1, text: line.trim() });
61+
break;
62+
}
63+
}
64+
}
65+
}
66+
return hits;
67+
}
68+
69+
// The deleted direct-render helpers and the retired domain-enum typing indirection.
70+
const DELETED_HELPER_PATTERNS = [
71+
/\brenderValueSetUnionBase\b/,
72+
/\brenderValueSetLiteral\b/,
73+
/\brenderEnumValueUnion\b/,
74+
/\brenderEnumMemberLiteral\b/,
75+
/\bDomainEnumLookup\b/,
76+
/\bdomainEnumLookup\b/,
77+
];
78+
79+
// An actual read of the domain enum entity: `.enum` immediately followed by an
80+
// index/optional-index/close-paren (`(ns.enum)`, `.enum[…]`, `.enum?.[…]`).
81+
// This deliberately ignores `.enum` appearing inside a string literal (e.g. a
82+
// `blindCast` reason describing the cast) — only access syntax counts.
83+
const DOMAIN_ENUM_READ = /\.enum\s*(\?\.\[|\[|\))/;
84+
85+
describe('A1: enum/value-set emit typing goes through the codec seam', () => {
86+
it('the deleted direct-render helpers do not reappear in the framework emitter source', () => {
87+
const hits = scan('packages/1-framework/3-tooling/emitter', DELETED_HELPER_PATTERNS);
88+
expect(hits).toEqual([]);
89+
});
90+
91+
it('the deleted direct-render helpers do not reappear in the SQL emit-path source', () => {
92+
const hits = scan('packages/2-sql', DELETED_HELPER_PATTERNS);
93+
expect(hits).toEqual([]);
94+
});
95+
96+
it('the deleted direct-render helpers do not reappear in the Mongo emitter source', () => {
97+
const hits = scan('packages/2-mongo-family/3-tooling/emitter', DELETED_HELPER_PATTERNS);
98+
expect(hits).toEqual([]);
99+
});
100+
});
101+
102+
describe('A2: no SQL/framework-emitter source reads domain.enum to produce a type', () => {
103+
it('the SQL emitter sources enum field/column types from storage, not domain.enum', () => {
104+
// The SQL emitter (3-tooling) is the emit-path typing reader; it must source
105+
// from the storage value set. Authoring layers legitimately build domain.enum,
106+
// so the guard is scoped to the emitter package.
107+
const hits = scan('packages/2-sql/3-tooling/emitter', [DOMAIN_ENUM_READ]);
108+
expect(hits).toEqual([]);
109+
});
110+
111+
it('the only ns.enum read in the framework emitter feeds the db.enums dictionary block', () => {
112+
const hits = scan('packages/1-framework/3-tooling/emitter', [DOMAIN_ENUM_READ]);
113+
// Exactly one allowed read: generate-contract-dts.ts → generateEnumBlockType.
114+
expect(hits).toHaveLength(1);
115+
expect(hits[0]?.file).toBe(
116+
'packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts',
117+
);
118+
});
119+
120+
it('the only repo-wide domain-enum typing reader is the Mongo interim resolver', () => {
121+
// Mongo has no storage value set yet (TML-2953), so its emitter reads
122+
// domain.enum on an interim basis. This read is expected; the test pins that
123+
// it is the single Mongo-emitter domain-enum read so its removal is noticed.
124+
const hits = scan('packages/2-mongo-family/3-tooling/emitter', [DOMAIN_ENUM_READ]);
125+
expect(hits.map((h) => h.file)).toEqual([
126+
'packages/2-mongo-family/3-tooling/emitter/src/index.ts',
127+
]);
128+
});
129+
});

packages/3-extensions/mongo/test/mongo.enum.e2e.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ const codecLookup: CodecLookup = {
6464
targetTypesFor: (id: string) => mongoTargetTypes[id],
6565
metaFor: () => undefined,
6666
renderOutputTypeFor: () => undefined,
67+
// Enum field types are produced through the codec seam (TML-2952): the emitter
68+
// renders each value-set value via `renderValueTypeFor`. `mongo/string@1` is an
69+
// identity codec, so it renders the encoded string straight to a quoted literal —
70+
// mirroring the real `mongo/string@1` descriptor's `renderValueType`.
71+
renderValueTypeFor: (id, value) =>
72+
id === 'mongo/string@1' && typeof value === 'string' ? `'${value}'` : undefined,
6773
};
6874

6975
// Derive the $jsonSchema validator from the contract via the production deriver.
@@ -287,6 +293,8 @@ describe('emit-then-consume: value-union narrowing through the emitted contract.
287293
mongoEmission,
288294
mongoCodecImports,
289295
testHashes,
296+
undefined,
297+
codecLookup,
290298
);
291299

292300
const outputMap = dts.slice(
@@ -311,6 +319,8 @@ describe('emit-then-consume: value-union narrowing through the emitted contract.
311319
mongoEmission,
312320
mongoCodecImports,
313321
testHashes,
322+
undefined,
323+
codecLookup,
314324
);
315325

316326
const outputMap = dts.slice(
@@ -329,6 +339,8 @@ describe('emit-then-consume: value-union narrowing through the emitted contract.
329339
mongoEmission,
330340
mongoCodecImports,
331341
testHashes,
342+
undefined,
343+
codecLookup,
332344
);
333345

334346
const outputMap = dts.slice(
@@ -347,6 +359,8 @@ describe('emit-then-consume: value-union narrowing through the emitted contract.
347359
mongoEmission,
348360
mongoCodecImports,
349361
testHashes,
362+
undefined,
363+
codecLookup,
350364
);
351365

352366
// The emitted contract.d.ts must carry the enum entity in the domain namespace

0 commit comments

Comments
 (0)