Skip to content

Commit 4d8ab61

Browse files
TML-2952: type SQL enum columns through the codec instead of printing their stored values (#896)
An enum column/field's TypeScript type is the union of its allowed values (`'low' | 'high' | 'urgent'`). Today that union is built by printing the stored, codec-encoded values straight to TS literals in two separate emit-path spots — which **bypasses the codec**, the one component that knows a stored value's real TypeScript type. It's correct only by coincidence: today's enums use text/int codecs where the encoded form happens to equal the output type. This routes emit typing through the codec, on one path, and removes the duplication. SQL only; Mongo is the sibling [TML-2953](https://linear.app/prisma-company/issue/TML-2953). No user-visible change for today's enums — correctness + de-duplication. ## What changed - **New codec seam.** `renderValueType(encodedValue, channel)` on `CodecDescriptor` and `renderValueTypeFor` on `CodecLookup` — the value-keyed counterpart of the existing `renderOutputType`. It renders a single codec-encoded value as its TS output literal, or `undefined` when the codec can't (e.g. a `Date`-output codec — no base-class default, so non-identity codecs opt out). Implemented for the Postgres and Mongo primitive codecs. - **SQL column emit** now renders each value-set value through the codec via a shared `renderValueSetType` helper, instead of printing the stored values directly (`renderValueSetUnionBase`/`renderValueSetLiteral` deleted). - **Framework field emit** no longer special-cases enums. The hardcoded domain-enum override (`DomainEnumLookup`, `renderEnumValueUnion`, `renderEnumMemberLiteral`) is deleted; a new **family-supplied** `EmissionSpi.resolveFieldValueSet` hook supplies a field's permitted values, rendered through the same codec helper. The domain `enum` is no longer a typing input — it keeps only the runtime `db.enums` dictionary. ## Keeping Mongo byte-identical (the one design call worth flagging) `FieldOutputTypes` is generated once for **both** families, and the framework emitter must not read `storage`. So deleting the shared override bare would regress Mongo's emitted enum fields (the demo `role: 'admin' | 'author' | 'reader'` would widen to `string`). The resolver hook resolves this cleanly: - **SQL** sources permitted values from the **storage value set** (field → column → value set) — the domain enum stops being a SQL typing input. - **Mongo** supplies an **interim** resolver reading `domain.enum` (it has no value-set entity yet), keeping its output byte-identical. This is removed by **TML-2953**, which brings Mongo onto the value-set model. So the acceptance criterion "domain enum is not a typing input" holds for SQL now, and repo-wide once TML-2953 lands. The no-emit (`typeof contract`) path is unchanged — it already propagated the authored, typed values, and is the reference the emit path now matches. ## Acceptance evidence - **A1** — every emit-path enum type goes through the codec; a structural guard keeps the deleted direct-render helpers gone. - **A2** — the guard pins the only domain-enum *typing* readers to the `db.enums` dictionary block and the Mongo interim resolver; the SQL emitter reads none. - **A4** — byte-identical for identity codecs, **both families**: empty `contract.json` diff, `fixtures:check` clean, the mongo-demo `contract.d.ts`/`.json` git-unchanged across every commit. - **A5** — a non-identity test codec (encodes `0|1|2`, outputs `'low'|'high'|'urgent'`) types as the codec **output**, not the raw encoded literal — on both the column and field surfaces. - **A6** — the emitted field type equals the no-emit `typeof contract` union. - **A7** — no new casts (`lint:casts` delta 0), `lint:deps` clean; build, typecheck, and the full test suites (`packages`/`integration`/`e2e`) green. ## Notes - Shaping artifacts (spec + dispatch plan) live under `projects/refactor-enum-typing-via-codec/`. The spec was corrected during shaping: the family-resolver design (the framework must not reach into storage) and the SQL-scoping of A2. - The change is additive at the framework layer (the new SPI hook is optional), so no downstream extension needed updating; one Mongo *test* harness updated its mock lookup to mirror production. - Verified on `main` including #880 (Mongo enums in retail-store), which re-emits byte-identically under this change. ## Upgrade instructions (0.14 → 0.15) - `skills/extension-author/prisma-next-extension-upgrade/upgrades/0.14-to-0.15/` — a real entry: an extension that types a value-set-restricted (enum) column via a custom `CodecDescriptorImpl` must implement `renderValueLiteral` (and expose `renderValueLiteralFor` on a hand-built `CodecLookup`), or the column widens to the codec output type. Framework-built lookups already supply it. Validated by execution (reverting `packages/3-extensions/` reddens the mongo enum e2e; re-applying reproduces the branch, tests green). - `skills/upgrade/prisma-next-upgrade/upgrades/0.14-to-0.15/` — user side is incidental (`changes: []`); the only `examples/` touch is a type test and the emitted contract is byte-identical. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Value-restricted fields now emit more precise literal unions instead of broad `string` types. * Type generation now uses codec-based literal rendering across supported databases, improving output for enums and similar restricted values. * **Bug Fixes** * Fixed cases where restricted field types could fall back to overly generic types. * Improved handling for non-identity codecs so emitted types stay aligned with stored values. * **Tests** * Added coverage for literal rendering, value-set narrowing, and fallback behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent 3a34e7c commit 4d8ab61

25 files changed

Lines changed: 1006 additions & 168 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+
// Emit vs no-emit agreement. The emitted `FieldOutputTypes` enum field type
50+
// (produced on the emit path via the codec seam) must equal the enum's authored
51+
// value union carried by `db.enums` (the no-emit reference — the authored member
52+
// values propagated without serialization). Same enum, both paths, one union.
53+
test('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>;

packages/1-framework/1-core/framework-components/src/control/control-stack.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { JsonValue } from '@prisma-next/contract/types';
12
import { blindCast } from '@prisma-next/utils/casts';
23
import type { Codec } from '../shared/codec';
34
import type { AnyCodecDescriptor } from '../shared/codec-descriptor';
@@ -310,6 +311,10 @@ export function extractCodecLookup(
310311
string,
311312
(params: Record<string, unknown>) => string | undefined
312313
>();
314+
const valueLiteralRenderersById = new Map<
315+
string,
316+
(value: JsonValue, side: 'output' | 'input') => string | undefined
317+
>();
313318
const owners = new Map<string, string>();
314319
for (const descriptor of descriptors) {
315320
const codecTypes = descriptor.types?.codecTypes;
@@ -337,6 +342,9 @@ export function extractCodecLookup(
337342
if (typeof codecDescriptor.renderInputType === 'function') {
338343
inputRenderersById.set(codecDescriptor.codecId, codecDescriptor.renderInputType);
339344
}
345+
if (typeof codecDescriptor.renderValueLiteral === 'function') {
346+
valueLiteralRenderersById.set(codecDescriptor.codecId, codecDescriptor.renderValueLiteral);
347+
}
340348
// Materialize a representative `Codec` instance for `byId.get()` so consumers reading the lookup's instance side (e.g. SQL renderer's cast-policy lookup, or the contract emitter's literal-default `encodeJson` resolver) keep finding the codec.
341349
//
342350
// Two cohorts:
@@ -376,6 +384,7 @@ export function extractCodecLookup(
376384
metaFor: (id) => metaById.get(id),
377385
renderOutputTypeFor: (id, params) => renderersById.get(id)?.(params),
378386
renderInputTypeFor: (id, params) => inputRenderersById.get(id)?.(params),
387+
renderValueLiteralFor: (id, value, side) => valueLiteralRenderersById.get(id)?.(value, side),
379388
};
380389
}
381390

packages/1-framework/1-core/framework-components/src/control/emission-types.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { Contract, ContractModelBase } from '@prisma-next/contract/types';
1+
import type { Contract, ContractModelBase, JsonValue } from '@prisma-next/contract/types';
22
import type { CodecLookup } from '../shared/codec-types';
33
import type { TypesImportSpec } from '../shared/types-import-spec';
44

@@ -33,5 +33,18 @@ export interface EmissionSpi {
3333
contract: Contract,
3434
): Record<string, unknown> | undefined;
3535

36+
/**
37+
* Resolves a field's permitted values (codec-encoded) plus the codec that types them, or
38+
* `undefined` for a field with no restricted value set. The framework renders the values into a TS
39+
* literal union through the codec seam. Each family decides where the values live — a value set in
40+
* its own storage plane, or another family-owned source.
41+
*/
42+
resolveFieldValueSet?(
43+
modelName: string,
44+
fieldName: string,
45+
model: ContractModelBase,
46+
contract: Contract,
47+
): { readonly encodedValues: readonly JsonValue[]; readonly codecId: string } | undefined;
48+
3649
getStorageTypeExports?(contract: Contract, codecLookup?: CodecLookup): string | undefined;
3750
}

packages/1-framework/1-core/framework-components/src/exports/codec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export type {
2727
ColumnTypeDescriptor,
2828
} from '../shared/column-spec';
2929
export { column } from '../shared/column-spec';
30+
export { renderTsLiteral } from '../shared/render-ts-literal';
3031
export {
3132
CONTRACT_CODEC_DESCRIPTOR_MISSING,
3233
materializeCodec,

packages/1-framework/1-core/framework-components/src/shared/codec-descriptor.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* The factory's method-level generic is the load-bearing piece for literal preservation: per-codec column helpers invoke `descriptor.factory(...)` *directly*, and the direct call binds the generic at its call site. Type extraction (`ReturnType<D['factory']>`, structural matching) widens method generics to their constraint — that's why the column-helper surface is per-codec, not polymorphic.
99
*/
1010

11+
import type { JsonValue } from '@prisma-next/contract/types';
1112
import type { StandardSchemaV1 } from '@standard-schema/spec';
1213
import type { Codec } from './codec';
1314
import {
@@ -45,6 +46,17 @@ export interface CodecDescriptor<P = void> {
4546
readonly renderOutputType?: (params: P) => string | undefined;
4647
/** Emit-path string renderer for the `contract.d.ts` *input* position (create/update values). Returns the TypeScript input type expression for given params. Optional; absent renderers fall back to the codec's base input type. A codec supplies this when its write type is narrower than the generic codec input — e.g. an enum whose input should be the literal member union, not `string`. */
4748
readonly renderInputType?: (params: P) => string | undefined;
49+
/**
50+
* Given one stored (codec-encoded) value, return the TypeScript literal type to print for it
51+
* (e.g. `'low'`, `1`), or `undefined` if this codec's output isn't literal-expressible (e.g. a
52+
* Date-output codec).
53+
*
54+
* `value` is the `encodeJson` form stored in the value set. `side` selects which type to print:
55+
* `output` = the read/SELECT type; `input` = the create/update type. Most codecs render the same
56+
* literal for both, but a codec whose read and write types differ can render per side. Called once
57+
* per permitted value; the caller joins the results with `|`.
58+
*/
59+
readonly renderValueLiteral?: (value: JsonValue, side: 'output' | 'input') => string | undefined;
4860
/** The curried higher-order codec. For non-parameterized codecs, the factory is constant — every call returns the same shared codec instance. For parameterized codecs, the factory is called once per `storage.types` instance (or once per inline-`typeParams` column), with `ctx` carrying the column set the resulting codec serves. */
4961
readonly factory: (params: P) => (ctx: CodecInstanceContext) => Codec;
5062
}
@@ -83,6 +95,9 @@ export abstract class CodecDescriptorImpl<TParams = void> implements CodecDescri
8395
/** Optional emit-path string renderer for the `contract.d.ts` input position. Returns the TypeScript input type expression for the given params; supplied when the write type is narrower than the generic codec input (e.g. an enum's literal member union). */
8496
renderInputType?(params: TParams): string | undefined;
8597

98+
/** Optional emit-path renderer for a single stored value. See {@link CodecDescriptor.renderValueLiteral}. */
99+
renderValueLiteral?(value: JsonValue, side: 'output' | 'input'): string | undefined;
100+
86101
/**
87102
* Materialize a curried codec factory for the given params. Concrete subclasses override with a typed return type (e.g. `factory<N>(params: { length: N }): (ctx) => VectorCodec<N>`); per-codec helpers read the typed return at the *direct* call site, which is what preserves method-level generics. Type extraction (e.g. `ReturnType<D['factory']>`) widens method generics to their constraint — that's why the column-helper surface is per-codec, not polymorphic.
88103
*/

packages/1-framework/1-core/framework-components/src/shared/codec-types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ export interface CodecLookup {
5151
renderOutputTypeFor(id: string, params: Record<string, unknown>): string | undefined;
5252
/** Codec-id-keyed `renderInputType` renderer for the `contract.d.ts` input position. Optional so existing lookups need not provide it; returns `undefined` when the codec renders no custom input type or the id is unknown. */
5353
renderInputTypeFor?(id: string, params: Record<string, unknown>): string | undefined;
54+
/** Codec-id-keyed `renderValueLiteral` renderer for the emit path (`side`: `output` = read type, `input` = create/update type). Optional so existing lookups need not provide it; returns `undefined` when the codec's output isn't literal-expressible or the id is unknown. */
55+
renderValueLiteralFor?(
56+
id: string,
57+
value: JsonValue,
58+
side: 'output' | 'input',
59+
): string | undefined;
5460
}
5561

5662
/**
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { JsonValue } from '@prisma-next/contract/types';
2+
3+
/**
4+
* Renders a codec-encoded value as a TypeScript literal (e.g. `'low'`, `1`, `true`), or `undefined`
5+
* when the value isn't literal-expressible (objects, arrays, null).
6+
*
7+
* Valid **only for identity codecs** whose `encodeJson` output equals their decoded output type
8+
* (text, int, float, bool). A non-identity codec (e.g. one that encodes to an int but decodes to a
9+
* string literal) must NOT use this: it has to `decodeJson` first, then render, in its own
10+
* `renderValueLiteral`.
11+
*
12+
* String values are fully escaped for a single-quoted `.d.ts` literal: backslash, single quote, and
13+
* every character a raw single-quoted TS literal cannot contain — newline, carriage return, and the
14+
* U+2028/U+2029 line/paragraph separators (which JS also treats as line terminators).
15+
*/
16+
export function renderTsLiteral(value: JsonValue): string | undefined {
17+
if (typeof value === 'string') {
18+
const escaped = value
19+
.replace(/\\/g, '\\\\')
20+
.replace(/'/g, "\\'")
21+
.replace(/\n/g, '\\n')
22+
.replace(/\r/g, '\\r')
23+
.replace(/\u2028/g, '\\u2028')
24+
.replace(/\u2029/g, '\\u2029');
25+
return `'${escaped}'`;
26+
}
27+
if (typeof value === 'number' || typeof value === 'boolean') {
28+
return String(value);
29+
}
30+
return undefined;
31+
}

packages/1-framework/1-core/framework-components/test/control-stack.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,33 @@ describe('extractCodecLookup', () => {
730730
expect.objectContaining({ code: 'CONTRACT.CODEC_DESCRIPTOR_MISSING' }),
731731
);
732732
});
733+
734+
it('renderValueLiteralFor delegates to the descriptor renderValueLiteral', () => {
735+
const descriptorWithValueRenderer: AnyCodecDescriptor = {
736+
...stubDescriptor('text@1'),
737+
renderValueLiteral: (value: unknown) =>
738+
typeof value === 'string' ? `'${value}'` : undefined,
739+
};
740+
const lookup = extractCodecLookup([
741+
{ id: 'desc', types: { codecTypes: { codecDescriptors: [descriptorWithValueRenderer] } } },
742+
]);
743+
expect(lookup.renderValueLiteralFor?.('text@1', 'low', 'output')).toBe("'low'");
744+
expect(lookup.renderValueLiteralFor?.('text@1', 42, 'output')).toBeUndefined();
745+
});
746+
747+
it('renderValueLiteralFor returns undefined for unknown codec ids', () => {
748+
const lookup = extractCodecLookup([
749+
{ id: 'desc', types: { codecTypes: { codecDescriptors: [stubDescriptor('a@1')] } } },
750+
]);
751+
expect(lookup.renderValueLiteralFor?.('unknown@1', 'val', 'output')).toBeUndefined();
752+
});
753+
754+
it('renderValueLiteralFor returns undefined when the codec has no renderValueLiteral', () => {
755+
const lookup = extractCodecLookup([
756+
{ id: 'desc', types: { codecTypes: { codecDescriptors: [stubDescriptor('a@1')] } } },
757+
]);
758+
expect(lookup.renderValueLiteralFor?.('a@1', 'val', 'output')).toBeUndefined();
759+
});
733760
});
734761

735762
describe('assembleScalarTypeDescriptors', () => {

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

Lines changed: 57 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import type {
2-
ContractEnum,
32
ContractField,
43
ContractManyToManyRelation,
54
ContractModelBase,
65
ContractValueObject,
76
CrossReference,
87
JsonValue,
9-
ValueSetRef,
108
} from '@prisma-next/contract/types';
119
import type { CodecLookup } from '@prisma-next/framework-components/codec';
1210
import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
@@ -291,20 +289,43 @@ export type FieldTypeParamsResolver = (
291289
model: ContractModelBase,
292290
) => Record<string, unknown> | undefined;
293291

294-
export type DomainEnumLookup = (ref: ValueSetRef) => ContractEnum | undefined;
295-
296-
function renderEnumMemberLiteral(value: JsonValue): string | undefined {
297-
if (typeof value === 'string') return serializeValue(value);
298-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
299-
return undefined;
300-
}
301-
302-
function renderEnumValueUnion(values: readonly JsonValue[]): string | undefined {
303-
if (values.length === 0) return undefined;
292+
/**
293+
* A field's permitted values (codec-encoded) plus the codec that types them, as supplied by the
294+
* family-specific {@link EmissionSpi.resolveFieldValueSet}. The framework renders these into a TS
295+
* literal union through the codec seam ({@link renderValueSetType}).
296+
*/
297+
export type ResolvedFieldValueSet = {
298+
readonly encodedValues: readonly JsonValue[];
299+
readonly codecId: string;
300+
};
301+
302+
export type FieldValueSetResolver = (
303+
modelName: string,
304+
fieldName: string,
305+
model: ContractModelBase,
306+
) => ResolvedFieldValueSet | undefined;
307+
308+
/**
309+
* Renders a value set (a field/column's permitted values, codec-encoded) into a TS literal union by
310+
* routing **each** value through the codec's `renderValueLiteral` — the seam owned by the codec, not
311+
* a generic serializer. `side`: `output` = read type, `input` = create/update type.
312+
*
313+
* Returns `undefined` — signalling the caller to fall back to the codec's full output type — when
314+
* the lookup is absent, has no `renderValueLiteralFor`, the value set is empty, or **any** value
315+
* isn't literal-expressible. A caller that needs column and field types to agree shares this so both
316+
* compute the union identically.
317+
*/
318+
export function renderValueSetType(
319+
values: readonly JsonValue[],
320+
codecId: string,
321+
side: 'output' | 'input',
322+
codecLookup: CodecLookup | undefined,
323+
): string | undefined {
324+
if (values.length === 0 || codecLookup?.renderValueLiteralFor === undefined) return undefined;
304325
const literals: string[] = [];
305326
for (const value of values) {
306-
const lit = renderEnumMemberLiteral(value);
307-
if (lit === undefined) return undefined;
327+
const lit = codecLookup.renderValueLiteralFor(codecId, value, side);
328+
if (lit === undefined || !isSafeTypeExpression(lit)) return undefined;
308329
literals.push(lit);
309330
}
310331
return literals.join(' | ');
@@ -314,21 +335,29 @@ export function resolveFieldType(
314335
field: ContractField,
315336
codecLookup?: CodecLookup,
316337
resolvedTypeParams?: Record<string, unknown>,
317-
domainEnumLookup?: DomainEnumLookup,
338+
resolvedValueSet?: ResolvedFieldValueSet,
318339
): ResolvedFieldType {
319340
const { type } = field;
320341

321342
switch (type.kind) {
322343
case 'scalar': {
323-
if (field.valueSet?.entityKind === 'enum' && domainEnumLookup) {
324-
const domainEnum = domainEnumLookup(field.valueSet);
325-
const union = domainEnum
326-
? renderEnumValueUnion(domainEnum.members.map((m) => m.value))
327-
: undefined;
328-
if (union !== undefined && isSafeTypeExpression(union)) {
344+
if (resolvedValueSet) {
345+
const output = renderValueSetType(
346+
resolvedValueSet.encodedValues,
347+
resolvedValueSet.codecId,
348+
'output',
349+
codecLookup,
350+
);
351+
const input = renderValueSetType(
352+
resolvedValueSet.encodedValues,
353+
resolvedValueSet.codecId,
354+
'input',
355+
codecLookup,
356+
);
357+
if (output !== undefined && input !== undefined) {
329358
return {
330-
output: applyModifiers(union, field),
331-
input: applyModifiers(union, field),
359+
output: applyModifiers(output, field),
360+
input: applyModifiers(input, field),
332361
};
333362
}
334363
}
@@ -394,7 +423,7 @@ export function generateBothFieldTypesMaps(
394423
models: Record<string, ContractModelBase> | undefined,
395424
codecLookup?: CodecLookup,
396425
resolveFieldTypeParams?: FieldTypeParamsResolver,
397-
domainEnumLookup?: DomainEnumLookup,
426+
resolveFieldValueSet?: FieldValueSetResolver,
398427
): ResolvedFieldType {
399428
if (!models || Object.keys(models).length === 0) {
400429
return { output: 'Record<string, never>', input: 'Record<string, never>' };
@@ -415,7 +444,8 @@ export function generateBothFieldTypesMaps(
415444
: undefined;
416445
const resolvedTypeParams =
417446
inlineTypeParams ?? resolveFieldTypeParams?.(modelName, fieldName, model);
418-
const resolved = resolveFieldType(field, codecLookup, resolvedTypeParams, domainEnumLookup);
447+
const resolvedValueSet = resolveFieldValueSet?.(modelName, fieldName, model);
448+
const resolved = resolveFieldType(field, codecLookup, resolvedTypeParams, resolvedValueSet);
419449
const key = `readonly ${serializeObjectKey(fieldName)}`;
420450
outputFieldEntries.push(`${key}: ${resolved.output}`);
421451
inputFieldEntries.push(`${key}: ${resolved.input}`);
@@ -443,7 +473,7 @@ export function generateFieldTypesMapsByNamespace(
443473
namespaceModels: ReadonlyArray<readonly [string, Record<string, ContractModelBase>]>,
444474
codecLookup?: CodecLookup,
445475
resolveFieldTypeParams?: FieldTypeParamsResolver,
446-
domainEnumLookup?: DomainEnumLookup,
476+
resolveFieldValueSet?: FieldValueSetResolver,
447477
): ResolvedFieldType {
448478
if (namespaceModels.length === 0) {
449479
return { output: 'Record<string, never>', input: 'Record<string, never>' };
@@ -456,7 +486,7 @@ export function generateFieldTypesMapsByNamespace(
456486
models,
457487
codecLookup,
458488
resolveFieldTypeParams,
459-
domainEnumLookup,
489+
resolveFieldValueSet,
460490
);
461491
const nsKey = `readonly ${serializeObjectKey(nsId)}`;
462492
outputNamespaceEntries.push(`${nsKey}: ${inner.output}`);

packages/1-framework/3-tooling/emitter/src/generate-contract-dts.ts

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type {
33
ContractEnum,
44
ContractModelBase,
55
ContractValueObject,
6-
ValueSetRef,
76
} from '@prisma-next/contract/types';
87
import { DomainNamespaceResolutionError } from '@prisma-next/contract/types';
98
import type { CodecLookup } from '@prisma-next/framework-components/codec';
@@ -14,7 +13,6 @@ import type {
1413
} from '@prisma-next/framework-components/emission';
1514
import { blindCast } from '@prisma-next/utils/casts';
1615
import {
17-
type DomainEnumLookup,
1816
deduplicateImports,
1917
generateCodecTypeIntersection,
2018
generateFieldTypesMapsByNamespace,
@@ -152,16 +150,10 @@ export function generateContractDts(
152150
emitter.resolveFieldTypeParams?.(modelName, fieldName, model, contract)
153151
: undefined;
154152

155-
const domainEnumLookup: DomainEnumLookup = (ref: ValueSetRef) => {
156-
if (ref.entityKind !== 'enum') return undefined;
157-
const ns = contract.domain.namespaces[ref.namespaceId];
158-
if (!ns) return undefined;
159-
const enumBlock = blindCast<
160-
Record<string, ContractEnum> | undefined,
161-
'ns.enum is an optional ContractEnum record in the emitted IR'
162-
>(ns.enum);
163-
return enumBlock?.[ref.entityName];
164-
};
153+
const resolveFieldValueSet = emitter.resolveFieldValueSet
154+
? (modelName: string, fieldName: string, model: ContractModelBase) =>
155+
emitter.resolveFieldValueSet?.(modelName, fieldName, model, contract)
156+
: undefined;
165157

166158
const namespaceModelsForFieldTypes = namespaceEntries.map(
167159
([nsId, ns]) => [nsId, ns.models] as const,
@@ -171,7 +163,7 @@ export function generateContractDts(
171163
namespaceModelsForFieldTypes,
172164
codecLookup,
173165
resolveFieldTypeParams,
174-
domainEnumLookup,
166+
resolveFieldValueSet,
175167
);
176168

177169
const extraTypeExports = emitter.getStorageTypeExports?.(contract, codecLookup);

0 commit comments

Comments
 (0)