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