Skip to content

Commit 31541fc

Browse files
committed
test: tighten contract-ts runtime guard typing
1 parent cf579c4 commit 31541fc

3 files changed

Lines changed: 166 additions & 1 deletion

File tree

packages/2-sql/2-authoring/contract-ts/src/build-contract.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ function buildDomainField(
171171
export function buildSqlContractFromDefinition(
172172
definition: ContractDefinition,
173173
codecLookup?: CodecLookup,
174-
): Contract {
174+
): Contract<SqlStorage> {
175175
const target = definition.target.targetId;
176176
const targetFamily = 'sql';
177177
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));

packages/2-sql/2-authoring/contract-ts/test/contract-builder.contract-definition.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { CodecLookup } from '@prisma-next/framework-components/codec';
12
import type { TargetPackRef } from '@prisma-next/framework-components/components';
23
import { describe, expect, it } from 'vitest';
34
import { buildSqlContractFromDefinition } from '../src/contract-builder';
@@ -175,6 +176,59 @@ describe('shared contract definition lowering', () => {
175176
});
176177
});
177178

179+
it('encodes literal defaults through codecLookup during storage lowering', () => {
180+
const codecLookup: CodecLookup = {
181+
get: (id) => {
182+
if (id !== 'pg/timestamptz@1') {
183+
return undefined;
184+
}
185+
186+
return {
187+
id,
188+
targetTypes: ['timestamptz'],
189+
traits: ['equality', 'order'] as const,
190+
decode: (wire: unknown) => wire,
191+
encodeJson: (value: unknown) =>
192+
value instanceof Date ? value.toISOString() : (value as string),
193+
decodeJson: (json: unknown) => new Date(json as string),
194+
};
195+
},
196+
};
197+
198+
const contract = buildSqlContractFromDefinition(
199+
{
200+
target: postgresTargetPack,
201+
models: [
202+
{
203+
modelName: 'Event',
204+
tableName: 'event',
205+
fields: [
206+
{
207+
fieldName: 'scheduledAt',
208+
columnName: 'scheduled_at',
209+
descriptor: {
210+
codecId: 'pg/timestamptz@1',
211+
nativeType: 'timestamptz',
212+
},
213+
nullable: false,
214+
default: {
215+
kind: 'literal',
216+
value: new Date('2025-01-01T00:00:00.000Z'),
217+
},
218+
},
219+
],
220+
},
221+
],
222+
},
223+
codecLookup,
224+
);
225+
226+
expect(contract.storage.tables['event']?.columns['scheduled_at']?.default).toEqual({
227+
kind: 'literal',
228+
value: '2025-01-01T00:00:00.000Z',
229+
});
230+
});
231+
178232
it('rejects generated fields that also declare storage defaults', () => {
179233
expect(() =>
180234
buildSqlContractFromDefinition({
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import type {
2+
ExtensionPackRef,
3+
FamilyPackRef,
4+
TargetPackRef,
5+
} from '@prisma-next/framework-components/components';
6+
import { describe, expect, it } from 'vitest';
7+
import { defineContract } from '../src/contract-builder';
8+
9+
const sqlFamilyPack = {
10+
kind: 'family',
11+
id: 'sql',
12+
familyId: 'sql',
13+
version: '0.0.1',
14+
} as const satisfies FamilyPackRef<'sql'>;
15+
16+
const documentFamilyPack = {
17+
kind: 'family',
18+
id: 'document',
19+
familyId: 'document',
20+
version: '0.0.1',
21+
} as const satisfies FamilyPackRef<'document'>;
22+
23+
const postgresTargetPack = {
24+
kind: 'target',
25+
id: 'postgres',
26+
familyId: 'sql',
27+
targetId: 'postgres',
28+
version: '0.0.1',
29+
} as const satisfies TargetPackRef<'sql', 'postgres'>;
30+
31+
const pgvectorExtensionPack = {
32+
kind: 'extension',
33+
id: 'pgvector',
34+
familyId: 'sql',
35+
targetId: 'postgres',
36+
version: '0.0.1',
37+
} as const satisfies ExtensionPackRef<'sql', 'postgres'>;
38+
39+
const mysqlExtensionPack = {
40+
...pgvectorExtensionPack,
41+
targetId: 'mysql',
42+
} as const satisfies ExtensionPackRef<'sql', 'mysql'>;
43+
44+
function unsafeExtensionPackRefForRuntimeTest<FamilyId extends string, TargetId extends string>(
45+
pack: FamilyPackRef<string> | TargetPackRef<string, string> | ExtensionPackRef<string, string>,
46+
): ExtensionPackRef<FamilyId, TargetId> {
47+
// These runtime-guard tests intentionally bypass the static pack-ref contract so they can
48+
// assert the error paths for invalid inputs that well-typed authoring code cannot produce.
49+
return pack as unknown as ExtensionPackRef<FamilyId, TargetId>;
50+
}
51+
52+
describe('defineContract runtime guards', () => {
53+
it.each([
54+
{
55+
name: 'non-SQL family packs',
56+
run: () =>
57+
defineContract({
58+
family: documentFamilyPack,
59+
target: postgresTargetPack,
60+
models: {},
61+
}),
62+
error: 'defineContract only accepts SQL family packs. Received family "document".',
63+
},
64+
{
65+
name: 'non-extension pack refs in extensionPacks',
66+
run: () =>
67+
defineContract({
68+
family: sqlFamilyPack,
69+
target: postgresTargetPack,
70+
extensionPacks: {
71+
invalid: unsafeExtensionPackRefForRuntimeTest(postgresTargetPack),
72+
},
73+
models: {},
74+
}),
75+
error:
76+
'defineContract only accepts extension pack refs in extensionPacks. Received kind "target".',
77+
},
78+
{
79+
name: 'extension packs from another family',
80+
run: () =>
81+
defineContract({
82+
family: sqlFamilyPack,
83+
target: postgresTargetPack,
84+
extensionPacks: {
85+
invalid: unsafeExtensionPackRefForRuntimeTest({
86+
...pgvectorExtensionPack,
87+
familyId: 'document',
88+
}),
89+
},
90+
models: {},
91+
}),
92+
error:
93+
'extension pack "pgvector" targets family "document" but contract target family is "sql".',
94+
},
95+
{
96+
name: 'extension packs for another target',
97+
run: () =>
98+
defineContract({
99+
family: sqlFamilyPack,
100+
target: postgresTargetPack,
101+
extensionPacks: {
102+
invalid: mysqlExtensionPack,
103+
},
104+
models: {},
105+
}),
106+
error: 'extension pack "pgvector" targets "mysql" but contract target is "postgres".',
107+
},
108+
])('rejects $name', ({ run, error }) => {
109+
expect(run).toThrow(error);
110+
});
111+
});

0 commit comments

Comments
 (0)