@prisma-next/emitter 0.3.0-pr.99.5 → 0.3.0

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 (47) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +45 -35
  3. package/dist/domain-type-generation.d.mts +38 -0
  4. package/dist/domain-type-generation.d.mts.map +1 -0
  5. package/dist/domain-type-generation.mjs +255 -0
  6. package/dist/domain-type-generation.mjs.map +1 -0
  7. package/dist/exports/index.d.mts +39 -0
  8. package/dist/exports/index.d.mts.map +1 -0
  9. package/dist/exports/index.mjs +106 -0
  10. package/dist/exports/index.mjs.map +1 -0
  11. package/dist/test/utils.d.mts +21 -0
  12. package/dist/test/utils.d.mts.map +1 -0
  13. package/dist/test/utils.mjs +18 -0
  14. package/dist/test/utils.mjs.map +1 -0
  15. package/dist/type-expression-safety-7_1tfJXA.mjs +8 -0
  16. package/dist/type-expression-safety-7_1tfJXA.mjs.map +1 -0
  17. package/dist/type-expression-safety.d.mts +5 -0
  18. package/dist/type-expression-safety.d.mts.map +1 -0
  19. package/dist/type-expression-safety.mjs +3 -0
  20. package/package.json +27 -11
  21. package/src/domain-type-generation.ts +429 -0
  22. package/src/emit-types.ts +23 -0
  23. package/src/emit.ts +68 -0
  24. package/src/exports/index.ts +14 -9
  25. package/src/generate-contract-dts.ts +117 -0
  26. package/src/type-expression-safety.ts +3 -0
  27. package/test/canonicalization.test.ts +196 -19
  28. package/test/domain-type-generation.test.ts +997 -0
  29. package/test/emitter.integration.test.ts +132 -187
  30. package/test/emitter.roundtrip.test.ts +117 -191
  31. package/test/emitter.test.ts +123 -494
  32. package/test/hashing.test.ts +9 -34
  33. package/test/mock-spi.ts +18 -0
  34. package/test/type-expression-safety.test.ts +34 -0
  35. package/test/utils.ts +30 -165
  36. package/dist/exports/index.js +0 -6
  37. package/dist/exports/index.js.map +0 -1
  38. package/dist/src/exports/index.d.ts +0 -4
  39. package/dist/src/exports/index.d.ts.map +0 -1
  40. package/dist/src/target-family.d.ts +0 -2
  41. package/dist/src/target-family.d.ts.map +0 -1
  42. package/dist/test/utils.d.ts +0 -14
  43. package/dist/test/utils.d.ts.map +0 -1
  44. package/dist/test/utils.js +0 -78
  45. package/dist/test/utils.js.map +0 -1
  46. package/src/target-family.ts +0 -7
  47. package/test/factories.test.ts +0 -274
@@ -1,75 +1,30 @@
1
- import type { ContractIR } from '@prisma-next/contract/ir';
2
- import type {
3
- GenerateContractTypesOptions,
4
- TargetFamilyHook,
5
- TypeRenderEntry,
6
- TypesImportSpec,
7
- } from '@prisma-next/contract/types';
8
- import type { EmitOptions } from '@prisma-next/core-control-plane/emission';
9
- import { emit } from '@prisma-next/core-control-plane/emission';
10
- import { createOperationRegistry } from '@prisma-next/operations';
1
+ import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
11
2
  import { timeouts } from '@prisma-next/test-utils';
12
3
  import { describe, expect, it } from 'vitest';
13
- import { createContractIR } from './utils';
14
-
15
- const mockSqlHook: TargetFamilyHook = {
16
- id: 'sql',
17
- validateTypes: (ir: ContractIR) => {
18
- const storage = ir.storage as
19
- | { tables?: Record<string, { columns?: Record<string, { codecId?: string }> }> }
20
- | undefined;
21
- if (!storage?.tables) {
22
- return;
23
- }
24
-
25
- // Only validate codec ID format (ns/name@version)
26
- // Namespace validation removed - codecs can use any namespace
27
- const typeIdRegex = /^([^/]+)\/([^@]+)@(\d+)$/;
28
-
29
- for (const [tableName, table] of Object.entries(storage.tables)) {
30
- if (!table.columns) continue;
31
- for (const [colName, col] of Object.entries(table.columns)) {
32
- if (!col.codecId) {
33
- throw new Error(`Column "${colName}" in table "${tableName}" is missing codecId`);
34
- }
35
-
36
- if (!typeIdRegex.test(col.codecId)) {
37
- throw new Error(
38
- `Column "${colName}" in table "${tableName}" has invalid codecId format "${col.codecId}". Expected format: ns/name@version`,
39
- );
40
- }
41
- }
42
- }
43
- },
44
- validateStructure: (ir: ContractIR) => {
45
- if (ir.targetFamily !== 'sql') {
46
- throw new Error(`Expected targetFamily "sql", got "${ir.targetFamily}"`);
47
- }
48
- },
49
- generateContractTypes: (ir: ContractIR, _codecTypeImports, _operationTypeImports) => {
50
- // Access ir properties to satisfy lint rules, but we don't use them in the mock
51
- void ir;
52
- void _codecTypeImports;
53
- void _operationTypeImports;
54
- return `// Generated contract types
55
- export type CodecTypes = Record<string, never>;
56
- export type LaneCodecTypes = CodecTypes;
57
- export type Contract = unknown;
58
- `;
59
- },
60
- };
4
+ import type { EmitStackInput } from '../src/exports';
5
+ import { emit } from '../src/exports';
6
+ import { createMockSpi } from './mock-spi';
7
+ import { createTestContract } from './utils';
8
+
9
+ const mockSqlHook = createMockSpi();
61
10
 
62
11
  describe('emitter', () => {
63
12
  it(
64
13
  'emits contract.json and contract.d.ts',
65
14
  async () => {
66
- const ir = createContractIR({
15
+ const ir = createTestContract({
67
16
  models: {
68
17
  User: {
69
- storage: { table: 'user' },
18
+ storage: {
19
+ table: 'user',
20
+ fields: {
21
+ id: { column: 'id' },
22
+ email: { column: 'email' },
23
+ },
24
+ },
70
25
  fields: {
71
- id: { column: 'id' },
72
- email: { column: 'email' },
26
+ id: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
27
+ email: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: false },
73
28
  },
74
29
  relations: {},
75
30
  },
@@ -96,21 +51,17 @@ describe('emitter', () => {
96
51
  },
97
52
  });
98
53
 
99
- // Create empty registry and minimal test data (emitter tests don't load packs)
100
- const operationRegistry = createOperationRegistry();
101
54
  const codecTypeImports: TypesImportSpec[] = [];
102
55
  const operationTypeImports: TypesImportSpec[] = [];
103
56
  const extensionIds = ['postgres', 'pg'];
104
- const options: EmitOptions = {
105
- outputDir: '',
106
- operationRegistry,
57
+ const options: EmitStackInput = {
107
58
  codecTypeImports,
108
59
  operationTypeImports,
109
60
  extensionIds,
110
61
  };
111
62
 
112
63
  const result = await emit(ir, options, mockSqlHook);
113
- expect(result.coreHash).toMatch(/^sha256:[a-f0-9]{64}$/);
64
+ expect(result.storageHash).toMatch(/^sha256:[a-f0-9]{64}$/);
114
65
  expect(result.contractDts).toContain('export type Contract');
115
66
  expect(result.contractDts).toContain('CodecTypes');
116
67
 
@@ -122,16 +73,14 @@ describe('emitter', () => {
122
73
  timeouts.typeScriptCompilation,
123
74
  );
124
75
 
125
- it('does not validate codec namespaces against extensions', async () => {
126
- // Namespace validation removed - codecs can use any namespace
127
- const ir = createContractIR({
76
+ it('emits contract even when extension pack namespace does not match extensionIds', async () => {
77
+ const ir = createTestContract({
128
78
  storage: {
129
79
  tables: {
130
80
  user: {
131
81
  columns: {
132
- id: { codecId: 'unknown/type@1', nativeType: 'unknown_type', nullable: false },
82
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
133
83
  },
134
- primaryKey: { columns: ['id'] },
135
84
  uniques: [],
136
85
  indexes: [],
137
86
  foreignKeys: [],
@@ -140,30 +89,26 @@ describe('emitter', () => {
140
89
  },
141
90
  });
142
91
 
143
- const operationRegistry = createOperationRegistry();
144
- const options: EmitOptions = {
145
- outputDir: '',
146
- operationRegistry,
92
+ const options: EmitStackInput = {
147
93
  codecTypeImports: [],
148
94
  operationTypeImports: [],
149
95
  extensionIds: [],
150
96
  };
151
97
 
152
- // Should succeed - namespace validation removed
153
98
  const result = await emit(ir, options, mockSqlHook);
154
99
  expect(result.contractJson).toBeDefined();
155
100
  expect(result.contractDts).toBeDefined();
156
101
  });
157
102
 
158
- it('validates type ID format', async () => {
159
- const ir = createContractIR({
103
+ it('tolerates codec namespaces not registered in extensionIds', async () => {
104
+ const ir = createTestContract({
160
105
  storage: {
161
106
  tables: {
162
- user: {
107
+ data: {
163
108
  columns: {
164
- id: { codecId: 'invalid-format', nativeType: 'int4', nullable: false },
109
+ id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
110
+ value: { codecId: 'unknown/type@1', nativeType: 'custom', nullable: false },
165
111
  },
166
- primaryKey: { columns: ['id'] },
167
112
  uniques: [],
168
113
  indexes: [],
169
114
  foreignKeys: [],
@@ -172,21 +117,19 @@ describe('emitter', () => {
172
117
  },
173
118
  });
174
119
 
175
- const operationRegistry = createOperationRegistry();
176
- const options: EmitOptions = {
177
- outputDir: '',
178
- operationRegistry,
120
+ const options: EmitStackInput = {
179
121
  codecTypeImports: [],
180
122
  operationTypeImports: [],
181
- extensionIds: [],
123
+ extensionIds: ['some-other-extension'],
182
124
  };
183
125
 
184
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('invalid codecId format');
126
+ const result = await emit(ir, options, mockSqlHook);
127
+ expect(result.contractJson).toBeDefined();
128
+ expect(result.contractDts).toBeDefined();
185
129
  });
186
130
 
187
- it('throws error when targetFamily is missing', async () => {
188
- const ir = createContractIR({
189
- targetFamily: undefined as unknown as string,
131
+ it('handles missing extensionPacks field', async () => {
132
+ const ir = createTestContract({
190
133
  storage: {
191
134
  tables: {
192
135
  user: {
@@ -199,25 +142,21 @@ describe('emitter', () => {
199
142
  },
200
143
  },
201
144
  },
202
- }) as ContractIR;
145
+ });
203
146
 
204
- const operationRegistry = createOperationRegistry();
205
- const options: EmitOptions = {
206
- outputDir: '',
207
- operationRegistry,
147
+ const options: EmitStackInput = {
208
148
  codecTypeImports: [],
209
149
  operationTypeImports: [],
210
150
  extensionIds: [],
211
151
  };
212
152
 
213
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
214
- 'ContractIR must have targetFamily',
215
- );
153
+ const result = await emit(ir, options, mockSqlHook);
154
+ expect(result.contractJson).toBeDefined();
155
+ expect(result.contractDts).toBeDefined();
216
156
  });
217
157
 
218
- it('throws error when target is missing', async () => {
219
- const ir = createContractIR({
220
- target: undefined as unknown as string,
158
+ it('handles empty packs array', async () => {
159
+ const ir = createTestContract({
221
160
  storage: {
222
161
  tables: {
223
162
  user: {
@@ -230,474 +169,164 @@ describe('emitter', () => {
230
169
  },
231
170
  },
232
171
  },
233
- }) as ContractIR;
172
+ });
234
173
 
235
- const operationRegistry = createOperationRegistry();
236
- const options: EmitOptions = {
237
- outputDir: '',
238
- operationRegistry,
174
+ const options: EmitStackInput = {
239
175
  codecTypeImports: [],
240
176
  operationTypeImports: [],
241
177
  extensionIds: [],
242
178
  };
243
179
 
244
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have target');
180
+ const result = await emit(ir, options, mockSqlHook);
181
+ expect(result.contractJson).toBeDefined();
182
+ expect(result.contractDts).toBeDefined();
245
183
  });
246
184
 
247
- it('emits contract even when extension pack namespace does not match extensionIds', async () => {
248
- // Adapter-provided codecs (pg/int4@1) don't need to be in contract.extensionPacks
249
- const ir = createContractIR({
250
- storage: {
251
- tables: {
252
- user: {
253
- columns: {
254
- id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
255
- },
256
- uniques: [],
257
- indexes: [],
258
- foreignKeys: [],
259
- },
260
- },
185
+ it('omits sources from emitted contract artifact', async () => {
186
+ const ir = createTestContract({
187
+ sources: {
188
+ schema: { sourceId: 'schema.prisma' },
261
189
  },
262
190
  });
263
191
 
264
- const operationRegistry = createOperationRegistry();
265
- const options: EmitOptions = {
266
- outputDir: '',
267
- operationRegistry,
192
+ const options: EmitStackInput = {
268
193
  codecTypeImports: [],
269
194
  operationTypeImports: [],
270
- extensionIds: [], // No extensions, but codec still works
195
+ extensionIds: [],
271
196
  };
272
197
 
273
- // Should succeed - adapter-provided codecs don't need to be in contract.extensionPacks
274
198
  const result = await emit(ir, options, mockSqlHook);
275
- expect(result.contractJson).toBeDefined();
276
- expect(result.contractDts).toBeDefined();
199
+ const contractJson = JSON.parse(result.contractJson) as Record<string, unknown>;
200
+ expect(contractJson).not.toHaveProperty('sources');
277
201
  });
278
202
 
279
- it('handles missing extensionPacks field', async () => {
280
- // Namespace validation removed - codecs can use any namespace
281
- const ir = createContractIR({
282
- storage: {
283
- tables: {
284
- user: {
285
- columns: {
286
- id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
287
- },
288
- uniques: [],
289
- indexes: [],
290
- foreignKeys: [],
291
- },
292
- },
203
+ it('accepts meta keys when family validation allows them', async () => {
204
+ const ir = createTestContract({
205
+ meta: {
206
+ sourceId: 'schema.prisma',
207
+ schemaPath: '/tmp/schema.prisma',
208
+ source: 'psl',
293
209
  },
294
210
  });
295
211
 
296
- const operationRegistry = createOperationRegistry();
297
- const options: EmitOptions = {
298
- outputDir: '',
299
- operationRegistry,
212
+ const options: EmitStackInput = {
300
213
  codecTypeImports: [],
301
214
  operationTypeImports: [],
302
215
  extensionIds: [],
303
216
  };
304
217
 
305
- // Should succeed - namespace validation removed
306
- const result = await emit(ir, options, mockSqlHook);
307
- expect(result.contractJson).toBeDefined();
308
- expect(result.contractDts).toBeDefined();
218
+ await expect(emit(ir, options, mockSqlHook)).resolves.toMatchObject({
219
+ contractJson: expect.any(String),
220
+ contractDts: expect.any(String),
221
+ });
309
222
  });
310
223
 
311
- it('handles empty packs array', async () => {
312
- // Namespace validation removed - codecs can use any namespace
313
- const ir = createContractIR({
224
+ it('accepts canonical section keys when family validation allows them', async () => {
225
+ const ir = createTestContract({
314
226
  storage: {
315
227
  tables: {
316
228
  user: {
317
229
  columns: {
318
- id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
230
+ id: {
231
+ codecId: 'pg/int4@1',
232
+ nativeType: 'int4',
233
+ nullable: false,
234
+ sourceId: 'schema.prisma',
235
+ },
319
236
  },
237
+ primaryKey: { columns: ['id'] },
320
238
  uniques: [],
321
239
  indexes: [],
322
240
  foreignKeys: [],
323
241
  },
324
242
  },
325
- },
243
+ } as unknown as Record<string, unknown>,
326
244
  });
327
245
 
328
- const operationRegistry = createOperationRegistry();
329
- const options: EmitOptions = {
330
- outputDir: '',
331
- operationRegistry,
332
- codecTypeImports: [],
333
- operationTypeImports: [],
334
- extensionIds: [],
335
- };
336
-
337
- // Should succeed - namespace validation removed
338
- const result = await emit(ir, options, mockSqlHook);
339
- expect(result.contractJson).toBeDefined();
340
- expect(result.contractDts).toBeDefined();
341
- });
342
-
343
- it('throws error when schemaVersion is missing', async () => {
344
- const ir = createContractIR({
345
- schemaVersion: undefined as unknown as string,
346
- }) as ContractIR;
347
-
348
- const operationRegistry = createOperationRegistry();
349
- const options: EmitOptions = {
350
- outputDir: '',
351
- operationRegistry,
352
- codecTypeImports: [],
353
- operationTypeImports: [],
354
- extensionIds: [],
355
- };
356
-
357
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
358
- 'ContractIR must have schemaVersion',
359
- );
360
- });
361
-
362
- it('throws error when models is missing', async () => {
363
- const ir = createContractIR({
364
- models: undefined as unknown as Record<string, unknown>,
365
- }) as ContractIR;
366
-
367
- const operationRegistry = createOperationRegistry();
368
- const options: EmitOptions = {
369
- outputDir: '',
370
- operationRegistry,
371
- codecTypeImports: [],
372
- operationTypeImports: [],
373
- extensionIds: [],
374
- };
375
-
376
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have models');
377
- });
378
-
379
- it('throws error when models is not an object', async () => {
380
- const ir = createContractIR({
381
- models: 'not-an-object' as unknown as Record<string, unknown>,
382
- }) as ContractIR;
383
-
384
- const operationRegistry = createOperationRegistry();
385
- const options: EmitOptions = {
386
- outputDir: '',
387
- operationRegistry,
388
- codecTypeImports: [],
389
- operationTypeImports: [],
390
- extensionIds: [],
391
- };
392
-
393
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have models');
394
- });
395
-
396
- it('throws error when storage is missing', async () => {
397
- const ir = createContractIR({
398
- storage: undefined as unknown as Record<string, unknown>,
399
- }) as ContractIR;
400
-
401
- const operationRegistry = createOperationRegistry();
402
- const options: EmitOptions = {
403
- outputDir: '',
404
- operationRegistry,
405
- codecTypeImports: [],
406
- operationTypeImports: [],
407
- extensionIds: [],
408
- };
409
-
410
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have storage');
411
- });
412
-
413
- it('throws error when storage is not an object', async () => {
414
- const ir = createContractIR({
415
- storage: 'not-an-object' as unknown as Record<string, unknown>,
416
- }) as ContractIR;
417
-
418
- const operationRegistry = createOperationRegistry();
419
- const options: EmitOptions = {
420
- outputDir: '',
421
- operationRegistry,
246
+ const options: EmitStackInput = {
422
247
  codecTypeImports: [],
423
248
  operationTypeImports: [],
424
249
  extensionIds: [],
425
250
  };
426
251
 
427
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have storage');
428
- });
429
-
430
- it('throws error when relations is missing', async () => {
431
- const ir = createContractIR({
432
- relations: undefined as unknown as Record<string, unknown>,
433
- }) as ContractIR;
434
-
435
- const operationRegistry = createOperationRegistry();
436
- const options: EmitOptions = {
437
- outputDir: '',
438
- operationRegistry,
439
- codecTypeImports: [],
440
- operationTypeImports: [],
441
- extensionIds: [],
442
- };
443
-
444
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have relations');
445
- });
446
-
447
- it('throws error when relations is not an object', async () => {
448
- const ir = createContractIR({
449
- relations: 'not-an-object' as unknown as Record<string, unknown>,
450
- }) as ContractIR;
451
-
452
- const operationRegistry = createOperationRegistry();
453
- const options: EmitOptions = {
454
- outputDir: '',
455
- operationRegistry,
456
- codecTypeImports: [],
457
- operationTypeImports: [],
458
- extensionIds: [],
459
- };
460
-
461
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have relations');
462
- });
463
-
464
- it('throws error when extension packs are missing', async () => {
465
- const ir = createContractIR({
466
- extensionPacks: undefined as unknown as Record<string, unknown>,
467
- }) as ContractIR;
468
-
469
- const operationRegistry = createOperationRegistry();
470
- const options: EmitOptions = {
471
- outputDir: '',
472
- operationRegistry,
473
- codecTypeImports: [],
474
- operationTypeImports: [],
475
- extensionIds: [],
476
- };
477
-
478
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
479
- 'ContractIR must have extensionPacks',
480
- );
481
- });
482
-
483
- it('throws error when extension packs are not an object', async () => {
484
- const ir = createContractIR({
485
- extensionPacks: 'not-an-object' as unknown as Record<string, unknown>,
486
- }) as ContractIR;
487
-
488
- const operationRegistry = createOperationRegistry();
489
- const options: EmitOptions = {
490
- outputDir: '',
491
- operationRegistry,
492
- codecTypeImports: [],
493
- operationTypeImports: [],
494
- extensionIds: [],
495
- };
496
-
497
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
498
- 'ContractIR must have extensionPacks',
499
- );
500
- });
501
-
502
- it('throws error when capabilities is missing', async () => {
503
- const ir = createContractIR({
504
- capabilities: undefined as unknown as Record<string, Record<string, boolean>>,
505
- }) as ContractIR;
506
-
507
- const operationRegistry = createOperationRegistry();
508
- const options: EmitOptions = {
509
- outputDir: '',
510
- operationRegistry,
511
- codecTypeImports: [],
512
- operationTypeImports: [],
513
- extensionIds: [],
514
- };
515
-
516
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
517
- 'ContractIR must have capabilities',
518
- );
519
- });
520
-
521
- it('throws error when capabilities is not an object', async () => {
522
- const ir = createContractIR({
523
- capabilities: 'not-an-object' as unknown as Record<string, Record<string, boolean>>,
524
- }) as ContractIR;
525
-
526
- const operationRegistry = createOperationRegistry();
527
- const options: EmitOptions = {
528
- outputDir: '',
529
- operationRegistry,
530
- codecTypeImports: [],
531
- operationTypeImports: [],
532
- extensionIds: [],
533
- };
534
-
535
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow(
536
- 'ContractIR must have capabilities',
537
- );
538
- });
539
-
540
- it('throws error when meta is missing', async () => {
541
- const ir = createContractIR({
542
- meta: undefined as unknown as Record<string, unknown>,
543
- }) as ContractIR;
544
-
545
- const operationRegistry = createOperationRegistry();
546
- const options: EmitOptions = {
547
- outputDir: '',
548
- operationRegistry,
549
- codecTypeImports: [],
550
- operationTypeImports: [],
551
- extensionIds: [],
552
- };
553
-
554
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have meta');
252
+ await expect(emit(ir, options, mockSqlHook)).resolves.toMatchObject({
253
+ contractJson: expect.any(String),
254
+ contractDts: expect.any(String),
255
+ });
555
256
  });
556
257
 
557
- it('throws error when meta is not an object', async () => {
558
- const ir = createContractIR({
559
- meta: 'not-an-object' as unknown as Record<string, unknown>,
560
- }) as ContractIR;
561
-
562
- const operationRegistry = createOperationRegistry();
563
- const options: EmitOptions = {
564
- outputDir: '',
565
- operationRegistry,
566
- codecTypeImports: [],
567
- operationTypeImports: [],
568
- extensionIds: [],
569
- };
570
-
571
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have meta');
572
- });
258
+ it('emits contract even when extensionIds are not in contract.extensionPacks', async () => {
259
+ const ir = createTestContract({
260
+ storage: {
261
+ tables: {},
262
+ },
263
+ });
573
264
 
574
- it('throws error when sources is missing', async () => {
575
- const ir = createContractIR({
576
- sources: undefined as unknown as Record<string, unknown>,
577
- }) as ContractIR;
265
+ const mockHookNoTypeValidation = createMockSpi();
578
266
 
579
- const operationRegistry = createOperationRegistry();
580
- const options: EmitOptions = {
581
- outputDir: '',
582
- operationRegistry,
267
+ const options: EmitStackInput = {
583
268
  codecTypeImports: [],
584
269
  operationTypeImports: [],
585
- extensionIds: [],
270
+ extensionIds: ['postgres'],
586
271
  };
587
272
 
588
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have sources');
273
+ const result = await emit(ir, options, mockHookNoTypeValidation);
274
+ expect(result.contractJson).toBeDefined();
275
+ expect(result.contractDts).toBeDefined();
589
276
  });
590
277
 
591
- it('throws error when sources is not an object', async () => {
592
- const ir = createContractIR({
593
- sources: 'not-an-object' as unknown as Record<string, unknown>,
594
- }) as ContractIR;
278
+ it('defaults codecTypeImports and operationTypeImports to empty arrays when omitted', async () => {
279
+ const ir = createTestContract({
280
+ storage: { tables: {} },
281
+ });
595
282
 
596
- const operationRegistry = createOperationRegistry();
597
- const options: EmitOptions = {
598
- outputDir: '',
599
- operationRegistry,
600
- codecTypeImports: [],
601
- operationTypeImports: [],
283
+ const options: EmitStackInput = {
602
284
  extensionIds: [],
603
285
  };
604
286
 
605
- await expect(emit(ir, options, mockSqlHook)).rejects.toThrow('ContractIR must have sources');
287
+ const result = await emit(ir, options, mockSqlHook);
288
+ expect(result.contractDts).toContain('export type CodecTypes');
289
+ expect(result.contractDts).toContain('export type OperationTypes');
606
290
  });
607
291
 
608
- it('emits contract even when extensionIds are not in contract.extensionPacks', async () => {
609
- // extensionIds includes adapters/targets which are not in contract.extensionPacks
610
- const ir = createContractIR({
611
- storage: {
612
- tables: {},
613
- },
292
+ it('passes parameterizedTypeImports and queryOperationTypeImports to generateContractDts', async () => {
293
+ const ir = createTestContract({
294
+ storage: { tables: {} },
614
295
  });
615
296
 
616
- // Use a mock hook that doesn't validate types to avoid type validation errors
617
- const mockHookNoTypeValidation: TargetFamilyHook = {
618
- id: 'sql',
619
- validateTypes: () => {
620
- // Skip type validation
621
- },
622
- validateStructure: (ir: ContractIR) => {
623
- if (ir.targetFamily !== 'sql') {
624
- throw new Error(`Expected targetFamily "sql", got "${ir.targetFamily}"`);
625
- }
626
- },
627
- generateContractTypes: (_ir, _codecTypeImports, _operationTypeImports) => {
628
- void _codecTypeImports;
629
- void _operationTypeImports;
630
- return `// Generated contract types
631
- export type CodecTypes = Record<string, never>;
632
- export type LaneCodecTypes = CodecTypes;
633
- export type Contract = unknown;
634
- `;
635
- },
636
- };
297
+ const queryOperationTypeImports: TypesImportSpec[] = [
298
+ { package: '@ext/query', named: 'QueryOperationTypes', alias: 'ExtQueryOpTypes' },
299
+ ];
637
300
 
638
- const options: EmitOptions = {
639
- outputDir: '',
640
- operationRegistry: createOperationRegistry(),
301
+ const options: EmitStackInput = {
641
302
  codecTypeImports: [],
642
303
  operationTypeImports: [],
643
- extensionIds: ['postgres'], // Adapter ID, not an extension
304
+ extensionIds: [],
305
+ queryOperationTypeImports,
644
306
  };
645
307
 
646
- // Should succeed - extensionIds can include adapters/targets
647
- const result = await emit(ir, options, mockHookNoTypeValidation);
648
- expect(result.contractJson).toBeDefined();
649
- expect(result.contractDts).toBeDefined();
308
+ const result = await emit(ir, options, mockSqlHook);
309
+ expect(result.contractDts).toContain("from '@ext/query'");
650
310
  });
651
311
 
652
- it('passes parameterizedRenderers to generateContractTypes options', async () => {
653
- const ir = createContractIR({
654
- storage: {
655
- tables: {},
312
+ it('emits execution clause when contract has execution section', async () => {
313
+ const ir = createTestContract({
314
+ storage: { tables: {} },
315
+ execution: {
316
+ executionHash: 'sha256:abc123',
317
+ operations: {},
656
318
  },
657
319
  });
658
320
 
659
- let receivedOptions: GenerateContractTypesOptions | undefined;
660
-
661
- const mockHookCapturingOptions: TargetFamilyHook = {
662
- id: 'sql',
663
- validateTypes: () => {},
664
- validateStructure: () => {},
665
- generateContractTypes: (_ir, _codecTypeImports, _operationTypeImports, options) => {
666
- receivedOptions = options;
667
- return `// Generated contract types
668
- export type CodecTypes = Record<string, never>;
669
- export type LaneCodecTypes = CodecTypes;
670
- export type Contract = unknown;
671
- `;
672
- },
673
- };
674
-
675
- const vectorRenderer: TypeRenderEntry = {
676
- codecId: 'pg/vector@1',
677
- render: (params) => `Vector<${params['length']}>`,
678
- };
679
-
680
- const parameterizedRenderers = new Map<string, TypeRenderEntry>();
681
- parameterizedRenderers.set('pg/vector@1', vectorRenderer);
682
-
683
- const options: EmitOptions = {
684
- outputDir: '',
685
- operationRegistry: createOperationRegistry(),
321
+ const options: EmitStackInput = {
686
322
  codecTypeImports: [],
687
323
  operationTypeImports: [],
688
324
  extensionIds: [],
689
- parameterizedRenderers,
690
325
  };
691
326
 
692
- await emit(ir, options, mockHookCapturingOptions);
693
-
694
- expect(receivedOptions).toBeDefined();
695
- expect(receivedOptions?.parameterizedRenderers).toBeDefined();
696
- expect(receivedOptions?.parameterizedRenderers?.size).toBe(1);
697
-
698
- const entry = receivedOptions?.parameterizedRenderers?.get('pg/vector@1');
699
- expect(entry).toBeDefined();
700
- expect(entry?.codecId).toBe('pg/vector@1');
701
- expect(entry?.render({ length: 1536 }, { codecTypesName: 'CodecTypes' })).toBe('Vector<1536>');
327
+ const result = await emit(ir, options, mockSqlHook);
328
+ expect(result.contractDts).toContain('readonly execution:');
329
+ expect(result.contractDts).toContain('readonly executionHash: ExecutionHash');
330
+ expect(result.executionHash).toMatch(/^sha256:[a-f0-9]{64}$/);
702
331
  });
703
332
  });