@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
|
@@ -1,86 +1,18 @@
|
|
|
1
|
-
import type {
|
|
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 {
|
|
4
|
+
import type { EmitStackInput } from '../src/exports';
|
|
5
|
+
import { emit } from '../src/exports';
|
|
6
|
+
import { createMockSpi } from './mock-spi';
|
|
7
|
+
import { createTestContract } from './utils';
|
|
13
8
|
|
|
14
|
-
const mockSqlHook
|
|
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
|
-
const referencedNamespaces = new Set<string>();
|
|
25
|
-
const extensionPacks = ir.extensionPacks as Record<string, unknown> | undefined;
|
|
26
|
-
if (extensionPacks) {
|
|
27
|
-
for (const namespace of Object.keys(extensionPacks)) {
|
|
28
|
-
referencedNamespaces.add(namespace);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const typeIdRegex = /^([^/]+)\/([^@]+)@(\d+)$/;
|
|
33
|
-
|
|
34
|
-
for (const [tableName, table] of Object.entries(storage.tables)) {
|
|
35
|
-
if (!table.columns) continue;
|
|
36
|
-
for (const [colName, col] of Object.entries(table.columns)) {
|
|
37
|
-
const column = col as { codecId?: string };
|
|
38
|
-
if (!column.codecId) {
|
|
39
|
-
throw new Error(`Column "${colName}" in table "${tableName}" is missing codecId`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (!typeIdRegex.test(column.codecId)) {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`Column "${colName}" in table "${tableName}" has invalid codecId format "${column.codecId}". Expected format: ns/name@version`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const match = column.codecId.match(typeIdRegex);
|
|
49
|
-
if (match?.[1]) {
|
|
50
|
-
const namespace = match[1];
|
|
51
|
-
if (!referencedNamespaces.has(namespace)) {
|
|
52
|
-
if (namespace === 'pg' && referencedNamespaces.has('postgres')) {
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
throw new Error(
|
|
56
|
-
`Column "${colName}" in table "${tableName}" uses codecId "${column.codecId}" from namespace "${namespace}" which is not referenced in contract.extensionPacks`,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
},
|
|
63
|
-
validateStructure: (ir: ContractIR) => {
|
|
64
|
-
if (ir.targetFamily !== 'sql') {
|
|
65
|
-
throw new Error(`Expected targetFamily "sql", got "${ir.targetFamily}"`);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
generateContractTypes: (_ir, _codecTypeImports, _operationTypeImports) => {
|
|
69
|
-
void _codecTypeImports;
|
|
70
|
-
void _operationTypeImports;
|
|
71
|
-
return `// Generated contract types
|
|
72
|
-
export type CodecTypes = Record<string, never>;
|
|
73
|
-
export type LaneCodecTypes = CodecTypes;
|
|
74
|
-
export type Contract = unknown;
|
|
75
|
-
`;
|
|
76
|
-
},
|
|
77
|
-
};
|
|
9
|
+
const mockSqlHook = createMockSpi();
|
|
78
10
|
|
|
79
11
|
describe('emitter round-trip', () => {
|
|
80
12
|
it(
|
|
81
13
|
'round-trip with minimal IR',
|
|
82
14
|
async () => {
|
|
83
|
-
const ir =
|
|
15
|
+
const ir = createTestContract({
|
|
84
16
|
storage: {
|
|
85
17
|
tables: {
|
|
86
18
|
user: {
|
|
@@ -100,14 +32,10 @@ describe('emitter round-trip', () => {
|
|
|
100
32
|
},
|
|
101
33
|
});
|
|
102
34
|
|
|
103
|
-
// Create minimal test data (emitter tests don't load packs)
|
|
104
|
-
const operationRegistry = createOperationRegistry();
|
|
105
35
|
const codecTypeImports: TypesImportSpec[] = [];
|
|
106
36
|
const operationTypeImports: TypesImportSpec[] = [];
|
|
107
37
|
const extensionIds = ['postgres', 'pg'];
|
|
108
|
-
const options:
|
|
109
|
-
outputDir: '',
|
|
110
|
-
operationRegistry,
|
|
38
|
+
const options: EmitStackInput = {
|
|
111
39
|
codecTypeImports,
|
|
112
40
|
operationTypeImports,
|
|
113
41
|
extensionIds,
|
|
@@ -116,17 +44,16 @@ describe('emitter round-trip', () => {
|
|
|
116
44
|
const result1 = await emit(ir, options, mockSqlHook);
|
|
117
45
|
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
|
|
118
46
|
|
|
119
|
-
const ir2 =
|
|
120
|
-
schemaVersion: contractJson1['schemaVersion'] as string,
|
|
47
|
+
const ir2 = createTestContract({
|
|
121
48
|
targetFamily: contractJson1['targetFamily'] as string,
|
|
122
49
|
target: contractJson1['target'] as string,
|
|
50
|
+
roots: contractJson1['roots'] as Record<string, string>,
|
|
123
51
|
models: contractJson1['models'] as Record<string, unknown>,
|
|
124
52
|
storage: contractJson1['storage'] as Record<string, unknown>,
|
|
125
53
|
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
|
|
126
54
|
capabilities:
|
|
127
55
|
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
|
|
128
56
|
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
|
|
129
|
-
sources: (contractJson1['sources'] as Record<string, unknown>) || {},
|
|
130
57
|
});
|
|
131
58
|
|
|
132
59
|
const result2 = await emit(ir2, options, mockSqlHook);
|
|
@@ -137,102 +64,115 @@ describe('emitter round-trip', () => {
|
|
|
137
64
|
timeouts.typeScriptCompilation,
|
|
138
65
|
);
|
|
139
66
|
|
|
140
|
-
it(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
67
|
+
it(
|
|
68
|
+
'round-trip with complex IR',
|
|
69
|
+
async () => {
|
|
70
|
+
const ir = createTestContract({
|
|
71
|
+
models: {
|
|
72
|
+
User: {
|
|
73
|
+
storage: {
|
|
74
|
+
table: 'user',
|
|
75
|
+
fields: {
|
|
76
|
+
id: { column: 'id' },
|
|
77
|
+
email: { column: 'email' },
|
|
78
|
+
name: { column: 'name' },
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
fields: {
|
|
82
|
+
id: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
|
|
83
|
+
email: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: false },
|
|
84
|
+
name: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: true },
|
|
85
|
+
},
|
|
86
|
+
relations: {},
|
|
158
87
|
},
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
168
|
-
name: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
|
|
88
|
+
Post: {
|
|
89
|
+
storage: {
|
|
90
|
+
table: 'post',
|
|
91
|
+
fields: {
|
|
92
|
+
id: { column: 'id' },
|
|
93
|
+
title: { column: 'title' },
|
|
94
|
+
userId: { column: 'user_id' },
|
|
95
|
+
},
|
|
169
96
|
},
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
97
|
+
fields: {
|
|
98
|
+
id: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
|
|
99
|
+
title: { type: { kind: 'scalar', codecId: 'pg/text@1' }, nullable: false },
|
|
100
|
+
userId: { type: { kind: 'scalar', codecId: 'pg/int4@1' }, nullable: false },
|
|
101
|
+
},
|
|
102
|
+
relations: {},
|
|
174
103
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
104
|
+
},
|
|
105
|
+
storage: {
|
|
106
|
+
tables: {
|
|
107
|
+
user: {
|
|
108
|
+
columns: {
|
|
109
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
110
|
+
email: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
111
|
+
name: { codecId: 'pg/text@1', nativeType: 'text', nullable: true },
|
|
112
|
+
},
|
|
113
|
+
primaryKey: { columns: ['id'] },
|
|
114
|
+
uniques: [{ columns: ['email'], name: 'user_email_key' }],
|
|
115
|
+
indexes: [{ columns: ['name'], name: 'user_name_idx' }],
|
|
116
|
+
foreignKeys: [],
|
|
180
117
|
},
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
columns: ['user_id'],
|
|
187
|
-
references: { table: 'user', columns: ['id'] },
|
|
188
|
-
name: 'post_user_id_fkey',
|
|
118
|
+
post: {
|
|
119
|
+
columns: {
|
|
120
|
+
id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
121
|
+
title: { codecId: 'pg/text@1', nativeType: 'text', nullable: false },
|
|
122
|
+
user_id: { codecId: 'pg/int4@1', nativeType: 'int4', nullable: false },
|
|
189
123
|
},
|
|
190
|
-
|
|
124
|
+
primaryKey: { columns: ['id'] },
|
|
125
|
+
uniques: [],
|
|
126
|
+
indexes: [],
|
|
127
|
+
foreignKeys: [
|
|
128
|
+
{
|
|
129
|
+
columns: ['user_id'],
|
|
130
|
+
references: { table: 'user', columns: ['id'] },
|
|
131
|
+
name: 'post_user_id_fkey',
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
191
135
|
},
|
|
192
136
|
},
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
197
|
-
});
|
|
137
|
+
extensionPacks: {
|
|
138
|
+
postgres: { version: '0.0.1' },
|
|
139
|
+
},
|
|
140
|
+
});
|
|
198
141
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
codecTypeImports,
|
|
208
|
-
operationTypeImports,
|
|
209
|
-
extensionIds,
|
|
210
|
-
};
|
|
142
|
+
const codecTypeImports: TypesImportSpec[] = [];
|
|
143
|
+
const operationTypeImports: TypesImportSpec[] = [];
|
|
144
|
+
const extensionIds = ['postgres'];
|
|
145
|
+
const options: EmitStackInput = {
|
|
146
|
+
codecTypeImports,
|
|
147
|
+
operationTypeImports,
|
|
148
|
+
extensionIds,
|
|
149
|
+
};
|
|
211
150
|
|
|
212
|
-
|
|
213
|
-
|
|
151
|
+
const result1 = await emit(ir, options, mockSqlHook);
|
|
152
|
+
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
|
|
214
153
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
});
|
|
154
|
+
const ir2 = createTestContract({
|
|
155
|
+
targetFamily: contractJson1['targetFamily'] as string,
|
|
156
|
+
target: contractJson1['target'] as string,
|
|
157
|
+
roots: contractJson1['roots'] as Record<string, string>,
|
|
158
|
+
models: contractJson1['models'] as Record<string, unknown>,
|
|
159
|
+
storage: contractJson1['storage'] as Record<string, unknown>,
|
|
160
|
+
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
|
|
161
|
+
capabilities:
|
|
162
|
+
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
|
|
163
|
+
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
|
|
164
|
+
});
|
|
227
165
|
|
|
228
|
-
|
|
166
|
+
const result2 = await emit(ir2, options, mockSqlHook);
|
|
229
167
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
168
|
+
expect(result1.contractJson).toBe(result2.contractJson);
|
|
169
|
+
expect(result1.storageHash).toBe(result2.storageHash);
|
|
170
|
+
},
|
|
171
|
+
timeouts.typeScriptCompilation,
|
|
172
|
+
);
|
|
233
173
|
|
|
234
174
|
it('round-trip with nullable fields', async () => {
|
|
235
|
-
const ir =
|
|
175
|
+
const ir = createTestContract({
|
|
236
176
|
storage: {
|
|
237
177
|
tables: {
|
|
238
178
|
user: {
|
|
@@ -254,14 +194,10 @@ describe('emitter round-trip', () => {
|
|
|
254
194
|
},
|
|
255
195
|
});
|
|
256
196
|
|
|
257
|
-
// Create minimal test data (emitter tests don't load packs)
|
|
258
|
-
const operationRegistry = createOperationRegistry();
|
|
259
197
|
const codecTypeImports: TypesImportSpec[] = [];
|
|
260
198
|
const operationTypeImports: TypesImportSpec[] = [];
|
|
261
199
|
const extensionIds = ['postgres', 'pg'];
|
|
262
|
-
const options:
|
|
263
|
-
outputDir: '',
|
|
264
|
-
operationRegistry,
|
|
200
|
+
const options: EmitStackInput = {
|
|
265
201
|
codecTypeImports,
|
|
266
202
|
operationTypeImports,
|
|
267
203
|
extensionIds,
|
|
@@ -270,17 +206,16 @@ describe('emitter round-trip', () => {
|
|
|
270
206
|
const result1 = await emit(ir, options, mockSqlHook);
|
|
271
207
|
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
|
|
272
208
|
|
|
273
|
-
const ir2 =
|
|
274
|
-
schemaVersion: contractJson1['schemaVersion'] as string,
|
|
209
|
+
const ir2 = createTestContract({
|
|
275
210
|
targetFamily: contractJson1['targetFamily'] as string,
|
|
276
211
|
target: contractJson1['target'] as string,
|
|
212
|
+
roots: contractJson1['roots'] as Record<string, string>,
|
|
277
213
|
models: contractJson1['models'] as Record<string, unknown>,
|
|
278
214
|
storage: contractJson1['storage'] as Record<string, unknown>,
|
|
279
215
|
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
|
|
280
216
|
capabilities:
|
|
281
217
|
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
|
|
282
218
|
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
|
|
283
|
-
sources: (contractJson1['sources'] as Record<string, unknown>) || {},
|
|
284
219
|
});
|
|
285
220
|
|
|
286
221
|
const result2 = await emit(ir2, options, mockSqlHook);
|
|
@@ -296,13 +231,13 @@ describe('emitter round-trip', () => {
|
|
|
296
231
|
const id = columns['id'] as Record<string, unknown>;
|
|
297
232
|
const email = columns['email'] as Record<string, unknown>;
|
|
298
233
|
const name = columns['name'] as Record<string, unknown>;
|
|
299
|
-
expect(id['nullable']).
|
|
234
|
+
expect(id['nullable']).toBe(false);
|
|
300
235
|
expect(email['nullable']).toBe(true);
|
|
301
|
-
expect(name['nullable']).
|
|
236
|
+
expect(name['nullable']).toBe(false);
|
|
302
237
|
});
|
|
303
238
|
|
|
304
239
|
it('round-trip with capabilities', async () => {
|
|
305
|
-
const ir =
|
|
240
|
+
const ir = createTestContract({
|
|
306
241
|
storage: {
|
|
307
242
|
tables: {
|
|
308
243
|
user: {
|
|
@@ -328,14 +263,10 @@ describe('emitter round-trip', () => {
|
|
|
328
263
|
},
|
|
329
264
|
});
|
|
330
265
|
|
|
331
|
-
// Create minimal test data (emitter tests don't load packs)
|
|
332
|
-
const operationRegistry = createOperationRegistry();
|
|
333
266
|
const codecTypeImports: TypesImportSpec[] = [];
|
|
334
267
|
const operationTypeImports: TypesImportSpec[] = [];
|
|
335
268
|
const extensionIds = ['postgres', 'pg'];
|
|
336
|
-
const options:
|
|
337
|
-
outputDir: '',
|
|
338
|
-
operationRegistry,
|
|
269
|
+
const options: EmitStackInput = {
|
|
339
270
|
codecTypeImports,
|
|
340
271
|
operationTypeImports,
|
|
341
272
|
extensionIds,
|
|
@@ -344,17 +275,16 @@ describe('emitter round-trip', () => {
|
|
|
344
275
|
const result1 = await emit(ir, options, mockSqlHook);
|
|
345
276
|
const contractJson1 = JSON.parse(result1.contractJson) as Record<string, unknown>;
|
|
346
277
|
|
|
347
|
-
const ir2 =
|
|
348
|
-
schemaVersion: contractJson1['schemaVersion'] as string,
|
|
278
|
+
const ir2 = createTestContract({
|
|
349
279
|
targetFamily: contractJson1['targetFamily'] as string,
|
|
350
280
|
target: contractJson1['target'] as string,
|
|
281
|
+
roots: contractJson1['roots'] as Record<string, string>,
|
|
351
282
|
models: contractJson1['models'] as Record<string, unknown>,
|
|
352
283
|
storage: contractJson1['storage'] as Record<string, unknown>,
|
|
353
284
|
extensionPacks: contractJson1['extensionPacks'] as Record<string, unknown>,
|
|
354
285
|
capabilities:
|
|
355
286
|
(contractJson1['capabilities'] as Record<string, Record<string, boolean>>) || {},
|
|
356
287
|
meta: (contractJson1['meta'] as Record<string, unknown>) || {},
|
|
357
|
-
sources: (contractJson1['sources'] as Record<string, unknown>) || {},
|
|
358
288
|
});
|
|
359
289
|
|
|
360
290
|
const result2 = await emit(ir2, options, mockSqlHook);
|