@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.
- package/README.md +33 -30
- package/dist/domain-type-generation.d.mts +14 -2
- package/dist/domain-type-generation.d.mts.map +1 -1
- package/dist/domain-type-generation.mjs +139 -1
- package/dist/domain-type-generation.mjs.map +1 -1
- package/dist/exports/index.d.mts +39 -4
- package/dist/exports/index.d.mts.map +1 -0
- package/dist/exports/index.mjs +104 -3
- package/dist/exports/index.mjs.map +1 -0
- package/dist/test/utils.d.mts +16 -14
- package/dist/test/utils.d.mts.map +1 -1
- package/dist/test/utils.mjs +12 -51
- package/dist/test/utils.mjs.map +1 -1
- package/dist/type-expression-safety-7_1tfJXA.mjs +8 -0
- package/dist/type-expression-safety-7_1tfJXA.mjs.map +1 -0
- package/dist/type-expression-safety.d.mts +5 -0
- package/dist/type-expression-safety.d.mts.map +1 -0
- package/dist/type-expression-safety.mjs +3 -0
- package/package.json +12 -6
- package/src/domain-type-generation.ts +227 -1
- package/src/emit-types.ts +23 -0
- package/src/emit.ts +68 -0
- package/src/exports/index.ts +4 -9
- package/src/generate-contract-dts.ts +116 -0
- package/src/type-expression-safety.ts +3 -0
- package/test/canonicalization.test.ts +25 -28
- package/test/domain-type-generation.test.ts +509 -2
- package/test/emitter.integration.test.ts +81 -139
- package/test/emitter.roundtrip.test.ts +114 -184
- package/test/emitter.test.ts +82 -467
- package/test/hashing.test.ts +8 -30
- package/test/mock-spi.ts +18 -0
- package/test/type-expression-safety.test.ts +34 -0
- package/test/utils.ts +30 -156
- package/src/target-family.ts +0 -7
- package/test/factories.test.ts +0 -274
package/test/emitter.test.ts
CHANGED
|
@@ -1,76 +1,30 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 =
|
|
15
|
+
const ir = createTestContract({
|
|
68
16
|
models: {
|
|
69
17
|
User: {
|
|
70
|
-
storage: {
|
|
18
|
+
storage: {
|
|
19
|
+
table: 'user',
|
|
20
|
+
fields: {
|
|
21
|
+
id: { column: 'id' },
|
|
22
|
+
email: { column: 'email' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
71
25
|
fields: {
|
|
72
|
-
id: {
|
|
73
|
-
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:
|
|
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('
|
|
127
|
-
|
|
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: '
|
|
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
|
|
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('
|
|
160
|
-
const ir =
|
|
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
|
-
|
|
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
|
|
266
|
-
const options: EmitOptions = {
|
|
267
|
-
outputDir: '',
|
|
268
|
-
operationRegistry,
|
|
120
|
+
const options: EmitStackInput = {
|
|
269
121
|
codecTypeImports: [],
|
|
270
122
|
operationTypeImports: [],
|
|
271
|
-
extensionIds: [],
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
186
|
+
const ir = createTestContract({
|
|
543
187
|
sources: {
|
|
544
188
|
schema: { sourceId: 'schema.prisma' },
|
|
545
189
|
},
|
|
546
190
|
});
|
|
547
191
|
|
|
548
|
-
const
|
|
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 =
|
|
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:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
623
|
-
const ir = createContractIR({
|
|
259
|
+
const ir = createTestContract({
|
|
624
260
|
storage: {
|
|
625
261
|
tables: {},
|
|
626
262
|
},
|
|
627
263
|
});
|
|
628
264
|
|
|
629
|
-
|
|
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:
|
|
653
|
-
outputDir: '',
|
|
654
|
-
operationRegistry: createOperationRegistry(),
|
|
267
|
+
const options: EmitStackInput = {
|
|
655
268
|
codecTypeImports: [],
|
|
656
269
|
operationTypeImports: [],
|
|
657
|
-
extensionIds: ['postgres'],
|
|
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('
|
|
667
|
-
const ir =
|
|
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
|
-
|
|
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
|
|
690
|
-
|
|
691
|
-
|
|
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
|
|
695
|
-
|
|
297
|
+
const queryOperationTypeImports: TypesImportSpec[] = [
|
|
298
|
+
{ package: '@ext/query', named: 'QueryOperationTypes', alias: 'ExtQueryOpTypes' },
|
|
299
|
+
];
|
|
696
300
|
|
|
697
|
-
const options:
|
|
698
|
-
outputDir: '',
|
|
699
|
-
operationRegistry: createOperationRegistry(),
|
|
301
|
+
const options: EmitStackInput = {
|
|
700
302
|
codecTypeImports: [],
|
|
701
303
|
operationTypeImports: [],
|
|
702
304
|
extensionIds: [],
|
|
703
|
-
|
|
305
|
+
queryOperationTypeImports,
|
|
704
306
|
};
|
|
705
307
|
|
|
706
|
-
await emit(ir, options,
|
|
308
|
+
const result = await emit(ir, options, mockSqlHook);
|
|
309
|
+
expect(result.contractDts).toContain("from '@ext/query'");
|
|
310
|
+
});
|
|
707
311
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
});
|