@prisma-next/emitter 0.3.0-pr.99.6 → 0.4.0-dev.1
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/LICENSE +201 -0
- package/README.md +45 -35
- package/dist/domain-type-generation.d.mts +38 -0
- package/dist/domain-type-generation.d.mts.map +1 -0
- package/dist/domain-type-generation.mjs +255 -0
- package/dist/domain-type-generation.mjs.map +1 -0
- package/dist/exports/index.d.mts +39 -0
- package/dist/exports/index.d.mts.map +1 -0
- package/dist/exports/index.mjs +106 -0
- package/dist/exports/index.mjs.map +1 -0
- package/dist/test/utils.d.mts +21 -0
- package/dist/test/utils.d.mts.map +1 -0
- package/dist/test/utils.mjs +18 -0
- package/dist/test/utils.mjs.map +1 -0
- 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 +28 -12
- package/src/domain-type-generation.ts +429 -0
- package/src/emit-types.ts +23 -0
- package/src/emit.ts +68 -0
- package/src/exports/index.ts +14 -9
- package/src/generate-contract-dts.ts +117 -0
- package/src/type-expression-safety.ts +3 -0
- package/test/canonicalization.test.ts +196 -19
- package/test/domain-type-generation.test.ts +997 -0
- package/test/emitter.integration.test.ts +132 -187
- package/test/emitter.roundtrip.test.ts +117 -191
- package/test/emitter.test.ts +123 -494
- package/test/hashing.test.ts +9 -34
- package/test/mock-spi.ts +18 -0
- package/test/type-expression-safety.test.ts +34 -0
- package/test/utils.ts +30 -165
- package/dist/exports/index.js +0 -6
- package/dist/exports/index.js.map +0 -1
- package/dist/src/exports/index.d.ts +0 -4
- package/dist/src/exports/index.d.ts.map +0 -1
- package/dist/src/target-family.d.ts +0 -2
- package/dist/src/target-family.d.ts.map +0 -1
- package/dist/test/utils.d.ts +0 -14
- package/dist/test/utils.d.ts.map +0 -1
- package/dist/test/utils.js +0 -78
- package/dist/test/utils.js.map +0 -1
- package/src/target-family.ts +0 -7
- package/test/factories.test.ts +0 -274
package/test/emitter.test.ts
CHANGED
|
@@ -1,75 +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) => {
|
|
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 =
|
|
15
|
+
const ir = createTestContract({
|
|
67
16
|
models: {
|
|
68
17
|
User: {
|
|
69
|
-
storage: {
|
|
18
|
+
storage: {
|
|
19
|
+
table: 'user',
|
|
20
|
+
fields: {
|
|
21
|
+
id: { column: 'id' },
|
|
22
|
+
email: { column: 'email' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
70
25
|
fields: {
|
|
71
|
-
id: {
|
|
72
|
-
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:
|
|
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.
|
|
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('
|
|
126
|
-
|
|
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: '
|
|
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
|
|
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('
|
|
159
|
-
const ir =
|
|
103
|
+
it('tolerates codec namespaces not registered in extensionIds', async () => {
|
|
104
|
+
const ir = createTestContract({
|
|
160
105
|
storage: {
|
|
161
106
|
tables: {
|
|
162
|
-
|
|
107
|
+
data: {
|
|
163
108
|
columns: {
|
|
164
|
-
id: { codecId: '
|
|
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
|
|
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
|
|
126
|
+
const result = await emit(ir, options, mockSqlHook);
|
|
127
|
+
expect(result.contractJson).toBeDefined();
|
|
128
|
+
expect(result.contractDts).toBeDefined();
|
|
185
129
|
});
|
|
186
130
|
|
|
187
|
-
it('
|
|
188
|
-
const ir =
|
|
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
|
-
})
|
|
145
|
+
});
|
|
203
146
|
|
|
204
|
-
const
|
|
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
|
|
214
|
-
|
|
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('
|
|
219
|
-
const ir =
|
|
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
|
-
})
|
|
172
|
+
});
|
|
234
173
|
|
|
235
|
-
const
|
|
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
|
|
180
|
+
const result = await emit(ir, options, mockSqlHook);
|
|
181
|
+
expect(result.contractJson).toBeDefined();
|
|
182
|
+
expect(result.contractDts).toBeDefined();
|
|
245
183
|
});
|
|
246
184
|
|
|
247
|
-
it('
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
|
265
|
-
const options: EmitOptions = {
|
|
266
|
-
outputDir: '',
|
|
267
|
-
operationRegistry,
|
|
192
|
+
const options: EmitStackInput = {
|
|
268
193
|
codecTypeImports: [],
|
|
269
194
|
operationTypeImports: [],
|
|
270
|
-
extensionIds: [],
|
|
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
|
-
|
|
276
|
-
expect(
|
|
199
|
+
const contractJson = JSON.parse(result.contractJson) as Record<string, unknown>;
|
|
200
|
+
expect(contractJson).not.toHaveProperty('sources');
|
|
277
201
|
});
|
|
278
202
|
|
|
279
|
-
it('
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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('
|
|
312
|
-
|
|
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: {
|
|
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
|
|
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)).
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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('
|
|
558
|
-
const ir =
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
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
|
-
|
|
575
|
-
const ir = createContractIR({
|
|
576
|
-
sources: undefined as unknown as Record<string, unknown>,
|
|
577
|
-
}) as ContractIR;
|
|
265
|
+
const mockHookNoTypeValidation = createMockSpi();
|
|
578
266
|
|
|
579
|
-
const
|
|
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
|
|
273
|
+
const result = await emit(ir, options, mockHookNoTypeValidation);
|
|
274
|
+
expect(result.contractJson).toBeDefined();
|
|
275
|
+
expect(result.contractDts).toBeDefined();
|
|
589
276
|
});
|
|
590
277
|
|
|
591
|
-
it('
|
|
592
|
-
const ir =
|
|
593
|
-
|
|
594
|
-
})
|
|
278
|
+
it('defaults codecTypeImports and operationTypeImports to empty arrays when omitted', async () => {
|
|
279
|
+
const ir = createTestContract({
|
|
280
|
+
storage: { tables: {} },
|
|
281
|
+
});
|
|
595
282
|
|
|
596
|
-
const
|
|
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
|
|
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('
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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:
|
|
639
|
-
outputDir: '',
|
|
640
|
-
operationRegistry: createOperationRegistry(),
|
|
301
|
+
const options: EmitStackInput = {
|
|
641
302
|
codecTypeImports: [],
|
|
642
303
|
operationTypeImports: [],
|
|
643
|
-
extensionIds: [
|
|
304
|
+
extensionIds: [],
|
|
305
|
+
queryOperationTypeImports,
|
|
644
306
|
};
|
|
645
307
|
|
|
646
|
-
|
|
647
|
-
|
|
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('
|
|
653
|
-
const ir =
|
|
654
|
-
storage: {
|
|
655
|
-
|
|
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
|
-
|
|
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,
|
|
693
|
-
|
|
694
|
-
expect(
|
|
695
|
-
expect(
|
|
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
|
});
|