@prisma-next/emitter 0.3.0-dev.16 → 0.3.0-dev.160

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