@prisma-next/emitter 0.3.0-dev.135 → 0.3.0-dev.146

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +33 -30
  2. package/dist/domain-type-generation.d.mts +14 -2
  3. package/dist/domain-type-generation.d.mts.map +1 -1
  4. package/dist/domain-type-generation.mjs +139 -1
  5. package/dist/domain-type-generation.mjs.map +1 -1
  6. package/dist/exports/index.d.mts +39 -4
  7. package/dist/exports/index.d.mts.map +1 -0
  8. package/dist/exports/index.mjs +104 -3
  9. package/dist/exports/index.mjs.map +1 -0
  10. package/dist/test/utils.d.mts +16 -14
  11. package/dist/test/utils.d.mts.map +1 -1
  12. package/dist/test/utils.mjs +12 -51
  13. package/dist/test/utils.mjs.map +1 -1
  14. package/dist/type-expression-safety-7_1tfJXA.mjs +8 -0
  15. package/dist/type-expression-safety-7_1tfJXA.mjs.map +1 -0
  16. package/dist/type-expression-safety.d.mts +5 -0
  17. package/dist/type-expression-safety.d.mts.map +1 -0
  18. package/dist/type-expression-safety.mjs +3 -0
  19. package/package.json +12 -6
  20. package/src/domain-type-generation.ts +227 -1
  21. package/src/emit-types.ts +23 -0
  22. package/src/emit.ts +68 -0
  23. package/src/exports/index.ts +4 -9
  24. package/src/generate-contract-dts.ts +116 -0
  25. package/src/type-expression-safety.ts +3 -0
  26. package/test/canonicalization.test.ts +25 -28
  27. package/test/domain-type-generation.test.ts +509 -2
  28. package/test/emitter.integration.test.ts +81 -139
  29. package/test/emitter.roundtrip.test.ts +114 -184
  30. package/test/emitter.test.ts +82 -467
  31. package/test/hashing.test.ts +8 -30
  32. package/test/mock-spi.ts +18 -0
  33. package/test/type-expression-safety.test.ts +34 -0
  34. package/test/utils.ts +30 -156
  35. package/src/target-family.ts +0 -7
  36. package/test/factories.test.ts +0 -274
@@ -1,86 +1,18 @@
1
- import type { ContractIR } from '@prisma-next/contract/ir';
2
- import type {
3
- TargetFamilyHook,
4
- TypesImportSpec,
5
- ValidationContext,
6
- } from '@prisma-next/contract/types';
7
- import type { EmitOptions } from '@prisma-next/core-control-plane/emission';
8
- import { emit } from '@prisma-next/core-control-plane/emission';
9
- import { createOperationRegistry } from '@prisma-next/operations';
1
+ import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
10
2
  import { timeouts } from '@prisma-next/test-utils';
11
3
  import { describe, expect, it } from 'vitest';
12
- import { createContractIR } from './utils';
4
+ import type { EmitStackInput } from '../src/exports';
5
+ import { emit } from '../src/exports';
6
+ import { createMockSpi } from './mock-spi';
7
+ import { createTestContract } from './utils';
13
8
 
14
- const mockSqlHook: TargetFamilyHook = {
15
- id: 'sql',
16
- validateTypes: (ir: ContractIR, _ctx: ValidationContext) => {
17
- const storage = ir.storage as
18
- | { tables?: Record<string, { columns?: Record<string, { codecId?: string }> }> }
19
- | undefined;
20
- if (!storage?.tables) {
21
- return;
22
- }
23
-
24
- const referencedNamespaces = new Set<string>();
25
- const extensionPacks = ir.extensionPacks as Record<string, unknown> | undefined;
26
- if (extensionPacks) {
27
- for (const namespace of Object.keys(extensionPacks)) {
28
- referencedNamespaces.add(namespace);
29
- }
30
- }
31
-
32
- const typeIdRegex = /^([^/]+)\/([^@]+)@(\d+)$/;
33
-
34
- for (const [tableName, table] of Object.entries(storage.tables)) {
35
- if (!table.columns) continue;
36
- for (const [colName, col] of Object.entries(table.columns)) {
37
- const column = col as { codecId?: string };
38
- if (!column.codecId) {
39
- throw new Error(`Column "${colName}" in table "${tableName}" is missing codecId`);
40
- }
41
-
42
- if (!typeIdRegex.test(column.codecId)) {
43
- throw new Error(
44
- `Column "${colName}" in table "${tableName}" has invalid codecId format "${column.codecId}". Expected format: ns/name@version`,
45
- );
46
- }
47
-
48
- const match = column.codecId.match(typeIdRegex);
49
- if (match?.[1]) {
50
- const namespace = match[1];
51
- if (!referencedNamespaces.has(namespace)) {
52
- if (namespace === 'pg' && referencedNamespaces.has('postgres')) {
53
- continue;
54
- }
55
- throw new Error(
56
- `Column "${colName}" in table "${tableName}" uses codecId "${column.codecId}" from namespace "${namespace}" which is not referenced in contract.extensionPacks`,
57
- );
58
- }
59
- }
60
- }
61
- }
62
- },
63
- validateStructure: (ir: ContractIR) => {
64
- if (ir.targetFamily !== 'sql') {
65
- throw new Error(`Expected targetFamily "sql", got "${ir.targetFamily}"`);
66
- }
67
- },
68
- generateContractTypes: (_ir, _codecTypeImports, _operationTypeImports) => {
69
- void _codecTypeImports;
70
- void _operationTypeImports;
71
- return `// Generated contract types
72
- export type CodecTypes = Record<string, never>;
73
- export type LaneCodecTypes = CodecTypes;
74
- export type Contract = unknown;
75
- `;
76
- },
77
- };
9
+ const mockSqlHook = createMockSpi();
78
10
 
79
11
  describe('emitter round-trip', () => {
80
12
  it(
81
13
  'round-trip with minimal IR',
82
14
  async () => {
83
- const ir = createContractIR({
15
+ const ir = createTestContract({
84
16
  storage: {
85
17
  tables: {
86
18
  user: {
@@ -100,14 +32,10 @@ describe('emitter round-trip', () => {
100
32
  },
101
33
  });
102
34
 
103
- // Create minimal test data (emitter tests don't load packs)
104
- const operationRegistry = createOperationRegistry();
105
35
  const codecTypeImports: TypesImportSpec[] = [];
106
36
  const operationTypeImports: TypesImportSpec[] = [];
107
37
  const extensionIds = ['postgres', 'pg'];
108
- const options: EmitOptions = {
109
- outputDir: '',
110
- operationRegistry,
38
+ const options: EmitStackInput = {
111
39
  codecTypeImports,
112
40
  operationTypeImports,
113
41
  extensionIds,
@@ -116,17 +44,16 @@ describe('emitter round-trip', () => {
116
44
  const result1 = await emit(ir, options, mockSqlHook);
117
45
  const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
118
46
 
119
- const ir2 = createContractIR({
120
- schemaVersion: contractJson1['schemaVersion'] as string,
47
+ const ir2 = createTestContract({
121
48
  targetFamily: contractJson1['targetFamily'] as string,
122
49
  target: contractJson1['target'] as string,
50
+ roots: contractJson1['roots'] as Record<string, string>,
123
51
  models: contractJson1['models'] as Record<string, unknown>,
124
52
  storage: contractJson1['storage'] as Record<string, unknown>,
125
53
  extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
126
54
  capabilities:
127
55
  (contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
128
56
  meta: (contractJson1['meta'] as Record<string, unknown>) || {},
129
- sources: (contractJson1['sources'] as Record<string, unknown>) || {},
130
57
  });
131
58
 
132
59
  const result2 = await emit(ir2, options, mockSqlHook);
@@ -137,102 +64,115 @@ describe('emitter round-trip', () => {
137
64
  timeouts.typeScriptCompilation,
138
65
  );
139
66
 
140
- it('round-trip with complex IR', async () => {
141
- const ir = createContractIR({
142
- models: {
143
- User: {
144
- storage: { table: 'user' },
145
- fields: {
146
- id: { column: 'id' },
147
- email: { column: 'email' },
148
- name: { column: 'name' },
149
- },
150
- relations: {},
151
- },
152
- Post: {
153
- storage: { table: 'post' },
154
- fields: {
155
- id: { column: 'id' },
156
- title: { column: 'title' },
157
- userId: { column: 'user_id' },
67
+ it(
68
+ 'round-trip with complex IR',
69
+ async () => {
70
+ const ir = createTestContract({
71
+ models: {
72
+ User: {
73
+ storage: {
74
+ table: 'user',
75
+ fields: {
76
+ id: { column: 'id' },
77
+ email: { column: 'email' },
78
+ name: { column: 'name' },
79
+ },
80
+ },
81
+ fields: {
82
+ id: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
83
+ email: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: false },
84
+ name: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: true },
85
+ },
86
+ relations: {},
158
87
  },
159
- relations: {},
160
- },
161
- },
162
- storage: {
163
- tables: {
164
- user: {
165
- columns: {
166
- id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
167
- email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
168
- name: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
88
+ Post: {
89
+ storage: {
90
+ table: 'post',
91
+ fields: {
92
+ id: { column: 'id' },
93
+ title: { column: 'title' },
94
+ userId: { column: 'user_id' },
95
+ },
169
96
  },
170
- primaryKey: { columns: ['id'] },
171
- uniques: [{ columns: ['email'], name: 'user_email_key' }],
172
- indexes: [{ columns: ['name'], name: 'user_name_idx' }],
173
- foreignKeys: [],
97
+ fields: {
98
+ id: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
99
+ title: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: false },
100
+ userId: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
101
+ },
102
+ relations: {},
174
103
  },
175
- post: {
176
- columns: {
177
- id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
178
- title: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
179
- user_id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
104
+ },
105
+ storage: {
106
+ tables: {
107
+ user: {
108
+ columns: {
109
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
110
+ email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
111
+ name: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
112
+ },
113
+ primaryKey: { columns: ['id'] },
114
+ uniques: [{ columns: ['email'], name: 'user_email_key' }],
115
+ indexes: [{ columns: ['name'], name: 'user_name_idx' }],
116
+ foreignKeys: [],
180
117
  },
181
- primaryKey: { columns: ['id'] },
182
- uniques: [],
183
- indexes: [],
184
- foreignKeys: [
185
- {
186
- columns: ['user_id'],
187
- references: { table: 'user', columns: ['id'] },
188
- name: 'post_user_id_fkey',
118
+ post: {
119
+ columns: {
120
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
121
+ title: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
122
+ user_id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
189
123
  },
190
- ],
124
+ primaryKey: { columns: ['id'] },
125
+ uniques: [],
126
+ indexes: [],
127
+ foreignKeys: [
128
+ {
129
+ columns: ['user_id'],
130
+ references: { table: 'user', columns: ['id'] },
131
+ name: 'post_user_id_fkey',
132
+ },
133
+ ],
134
+ },
191
135
  },
192
136
  },
193
- },
194
- extensionPacks: {
195
- postgres: { version: '0.0.1' },
196
- },
197
- });
137
+ extensionPacks: {
138
+ postgres: { version: '0.0.1' },
139
+ },
140
+ });
198
141
 
199
- // Create minimal test data (emitter tests don't load packs)
200
- const operationRegistry = createOperationRegistry();
201
- const codecTypeImports: TypesImportSpec[] = [];
202
- const operationTypeImports: TypesImportSpec[] = [];
203
- const extensionIds = ['postgres'];
204
- const options: EmitOptions = {
205
- outputDir: '',
206
- operationRegistry,
207
- codecTypeImports,
208
- operationTypeImports,
209
- extensionIds,
210
- };
142
+ const codecTypeImports: TypesImportSpec[] = [];
143
+ const operationTypeImports: TypesImportSpec[] = [];
144
+ const extensionIds = ['postgres'];
145
+ const options: EmitStackInput = {
146
+ codecTypeImports,
147
+ operationTypeImports,
148
+ extensionIds,
149
+ };
211
150
 
212
- const result1 = await emit(ir, options, mockSqlHook);
213
- const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
151
+ const result1 = await emit(ir, options, mockSqlHook);
152
+ const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
214
153
 
215
- const ir2 = createContractIR({
216
- schemaVersion: contractJson1['schemaVersion'] as string,
217
- targetFamily: contractJson1['targetFamily'] as string,
218
- target: contractJson1['target'] as string,
219
- models: contractJson1['models'] as Record<string, unknown>,
220
- storage: contractJson1['storage'] as Record<string, unknown>,
221
- extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
222
- capabilities:
223
- (contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
224
- meta: (contractJson1['meta'] as Record<string, unknown>) || {},
225
- sources: (contractJson1['sources'] as Record<string, unknown>) || {},
226
- });
154
+ const ir2 = createTestContract({
155
+ targetFamily: contractJson1['targetFamily'] as string,
156
+ target: contractJson1['target'] as string,
157
+ roots: contractJson1['roots'] as Record<string, string>,
158
+ models: contractJson1['models'] as Record<string, unknown>,
159
+ storage: contractJson1['storage'] as Record<string, unknown>,
160
+ extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
161
+ capabilities:
162
+ (contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
163
+ meta: (contractJson1['meta'] as Record<string, unknown>) || {},
164
+ });
227
165
 
228
- const result2 = await emit(ir2, options, mockSqlHook);
166
+ const result2 = await emit(ir2, options, mockSqlHook);
229
167
 
230
- expect(result1.contractJson).toBe(result2.contractJson);
231
- expect(result1.storageHash).toBe(result2.storageHash);
232
- });
168
+ expect(result1.contractJson).toBe(result2.contractJson);
169
+ expect(result1.storageHash).toBe(result2.storageHash);
170
+ },
171
+ timeouts.typeScriptCompilation,
172
+ );
233
173
 
234
174
  it('round-trip with nullable fields', async () => {
235
- const ir = createContractIR({
175
+ const ir = createTestContract({
236
176
  storage: {
237
177
  tables: {
238
178
  user: {
@@ -254,14 +194,10 @@ describe('emitter round-trip', () => {
254
194
  },
255
195
  });
256
196
 
257
- // Create minimal test data (emitter tests don't load packs)
258
- const operationRegistry = createOperationRegistry();
259
197
  const codecTypeImports: TypesImportSpec[] = [];
260
198
  const operationTypeImports: TypesImportSpec[] = [];
261
199
  const extensionIds = ['postgres', 'pg'];
262
- const options: EmitOptions = {
263
- outputDir: '',
264
- operationRegistry,
200
+ const options: EmitStackInput = {
265
201
  codecTypeImports,
266
202
  operationTypeImports,
267
203
  extensionIds,
@@ -270,17 +206,16 @@ describe('emitter round-trip', () => {
270
206
  const result1 = await emit(ir, options, mockSqlHook);
271
207
  const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
272
208
 
273
- const ir2 = createContractIR({
274
- schemaVersion: contractJson1['schemaVersion'] as string,
209
+ const ir2 = createTestContract({
275
210
  targetFamily: contractJson1['targetFamily'] as string,
276
211
  target: contractJson1['target'] as string,
212
+ roots: contractJson1['roots'] as Record<string, string>,
277
213
  models: contractJson1['models'] as Record<string, unknown>,
278
214
  storage: contractJson1['storage'] as Record<string, unknown>,
279
215
  extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
280
216
  capabilities:
281
217
  (contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
282
218
  meta: (contractJson1['meta'] as Record<string, unknown>) || {},
283
- sources: (contractJson1['sources'] as Record<string, unknown>) || {},
284
219
  });
285
220
 
286
221
  const result2 = await emit(ir2, options, mockSqlHook);
@@ -296,13 +231,13 @@ describe('emitter round-trip', () => {
296
231
  const id = columns['id'] as Record<string, unknown>;
297
232
  const email = columns['email'] as Record<string, unknown>;
298
233
  const name = columns['name'] as Record<string, unknown>;
299
- expect(id['nullable']).toBeUndefined();
234
+ expect(id['nullable']).toBe(false);
300
235
  expect(email['nullable']).toBe(true);
301
- expect(name['nullable']).toBeUndefined();
236
+ expect(name['nullable']).toBe(false);
302
237
  });
303
238
 
304
239
  it('round-trip with capabilities', async () => {
305
- const ir = createContractIR({
240
+ const ir = createTestContract({
306
241
  storage: {
307
242
  tables: {
308
243
  user: {
@@ -328,14 +263,10 @@ describe('emitter round-trip', () => {
328
263
  },
329
264
  });
330
265
 
331
- // Create minimal test data (emitter tests don't load packs)
332
- const operationRegistry = createOperationRegistry();
333
266
  const codecTypeImports: TypesImportSpec[] = [];
334
267
  const operationTypeImports: TypesImportSpec[] = [];
335
268
  const extensionIds = ['postgres', 'pg'];
336
- const options: EmitOptions = {
337
- outputDir: '',
338
- operationRegistry,
269
+ const options: EmitStackInput = {
339
270
  codecTypeImports,
340
271
  operationTypeImports,
341
272
  extensionIds,
@@ -344,17 +275,16 @@ describe('emitter round-trip', () => {
344
275
  const result1 = await emit(ir, options, mockSqlHook);
345
276
  const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
346
277
 
347
- const ir2 = createContractIR({
348
- schemaVersion: contractJson1['schemaVersion'] as string,
278
+ const ir2 = createTestContract({
349
279
  targetFamily: contractJson1['targetFamily'] as string,
350
280
  target: contractJson1['target'] as string,
281
+ roots: contractJson1['roots'] as Record<string, string>,
351
282
  models: contractJson1['models'] as Record<string, unknown>,
352
283
  storage: contractJson1['storage'] as Record<string, unknown>,
353
284
  extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
354
285
  capabilities:
355
286
  (contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
356
287
  meta: (contractJson1['meta'] as Record<string, unknown>) || {},
357
- sources: (contractJson1['sources'] as Record<string, unknown>) || {},
358
288
  });
359
289
 
360
290
  const result2 = await emit(ir2, options, mockSqlHook);