@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
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ContractField,
|
|
3
|
+
ContractModel,
|
|
4
|
+
ContractValueObject,
|
|
5
|
+
} from '@prisma-next/contract/types';
|
|
6
|
+
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
7
|
+
import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
|
|
8
|
+
import { isSafeTypeExpression } from './type-expression-safety';
|
|
9
|
+
|
|
10
|
+
export function serializeValue(value: unknown): string {
|
|
11
|
+
if (value === null) {
|
|
12
|
+
return 'null';
|
|
13
|
+
}
|
|
14
|
+
if (value === undefined) {
|
|
15
|
+
return 'undefined';
|
|
16
|
+
}
|
|
17
|
+
if (typeof value === 'string') {
|
|
18
|
+
const escaped = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
19
|
+
return `'${escaped}'`;
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
22
|
+
return String(value);
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'bigint') {
|
|
25
|
+
return `${value}n`;
|
|
26
|
+
}
|
|
27
|
+
if (Array.isArray(value)) {
|
|
28
|
+
const items = value.map((v) => serializeValue(v)).join(', ');
|
|
29
|
+
return `readonly [${items}]`;
|
|
30
|
+
}
|
|
31
|
+
if (typeof value === 'object') {
|
|
32
|
+
const entries: string[] = [];
|
|
33
|
+
for (const [k, v] of Object.entries(value)) {
|
|
34
|
+
entries.push(`readonly ${serializeObjectKey(k)}: ${serializeValue(v)}`);
|
|
35
|
+
}
|
|
36
|
+
return `{ ${entries.join('; ')} }`;
|
|
37
|
+
}
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function serializeObjectKey(key: string): string {
|
|
42
|
+
if (/^[$A-Z_a-z][$\w]*$/.test(key)) {
|
|
43
|
+
return key;
|
|
44
|
+
}
|
|
45
|
+
return serializeValue(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function generateRootsType(roots: Record<string, string> | undefined): string {
|
|
49
|
+
if (!roots || Object.keys(roots).length === 0) {
|
|
50
|
+
return 'Record<string, string>';
|
|
51
|
+
}
|
|
52
|
+
const entries = Object.entries(roots)
|
|
53
|
+
.map(([key, value]) => `readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`)
|
|
54
|
+
.join('; ');
|
|
55
|
+
return `{ ${entries} }`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function contractFieldModifierSuffix(field: ContractField): string {
|
|
59
|
+
const many = field.many === true ? '; readonly many: true' : '';
|
|
60
|
+
const dict = field.dict === true ? '; readonly dict: true' : '';
|
|
61
|
+
return many + dict;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function generateModelFieldEntry(fieldName: string, field: ContractField): string {
|
|
65
|
+
const mods = contractFieldModifierSuffix(field);
|
|
66
|
+
const { nullable, type } = field;
|
|
67
|
+
if (type.kind === 'scalar') {
|
|
68
|
+
const typeParamsSpec =
|
|
69
|
+
type.typeParams && Object.keys(type.typeParams).length > 0
|
|
70
|
+
? `; readonly typeParams: ${serializeValue(type.typeParams)}`
|
|
71
|
+
: '';
|
|
72
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: { readonly kind: 'scalar'; readonly codecId: ${serializeValue(type.codecId)}${typeParamsSpec} }${mods} }`;
|
|
73
|
+
}
|
|
74
|
+
if (type.kind === 'valueObject') {
|
|
75
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: { readonly kind: 'valueObject'; readonly name: ${serializeValue(type.name)} }${mods} }`;
|
|
76
|
+
}
|
|
77
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${nullable}; readonly type: ${serializeValue(type)}${mods} }`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function generateModelFieldsType(fields: Record<string, ContractField>): string {
|
|
81
|
+
const fieldEntries: string[] = [];
|
|
82
|
+
for (const [fieldName, field] of Object.entries(fields)) {
|
|
83
|
+
fieldEntries.push(generateModelFieldEntry(fieldName, field));
|
|
84
|
+
}
|
|
85
|
+
return fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function generateModelRelationsType(relations: Record<string, unknown>): string {
|
|
89
|
+
const relationEntries: string[] = [];
|
|
90
|
+
|
|
91
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
92
|
+
if (typeof rel !== 'object' || rel === null) continue;
|
|
93
|
+
const relObj = rel as Record<string, unknown>;
|
|
94
|
+
const parts: string[] = [];
|
|
95
|
+
|
|
96
|
+
if (relObj['to']) parts.push(`readonly to: ${serializeValue(relObj['to'])}`);
|
|
97
|
+
if (relObj['cardinality'])
|
|
98
|
+
parts.push(`readonly cardinality: ${serializeValue(relObj['cardinality'])}`);
|
|
99
|
+
|
|
100
|
+
const on = relObj['on'] as { localFields?: string[]; targetFields?: string[] } | undefined;
|
|
101
|
+
if (on && (!on.localFields || !on.targetFields)) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Relation "${relName}" has an "on" block but is missing localFields or targetFields`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
if (on?.localFields && on.targetFields) {
|
|
107
|
+
const localFields = on.localFields.map((f) => serializeValue(f)).join(', ');
|
|
108
|
+
const targetFields = on.targetFields.map((f) => serializeValue(f)).join(', ');
|
|
109
|
+
parts.push(
|
|
110
|
+
`readonly on: { readonly localFields: readonly [${localFields}]; readonly targetFields: readonly [${targetFields}] }`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (parts.length > 0) {
|
|
115
|
+
relationEntries.push(`readonly ${relName}: { ${parts.join('; ')} }`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (relationEntries.length === 0) {
|
|
120
|
+
return 'Record<string, never>';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return `{ ${relationEntries.join('; ')} }`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function generateModelsType(
|
|
127
|
+
models: Record<string, ContractModel>,
|
|
128
|
+
generateModelStorage: (modelName: string, model: ContractModel) => string,
|
|
129
|
+
): string {
|
|
130
|
+
if (!models || Object.keys(models).length === 0) {
|
|
131
|
+
return 'Record<string, never>';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const modelTypes: string[] = [];
|
|
135
|
+
for (const [modelName, model] of Object.entries(models).sort(([a], [b]) => a.localeCompare(b))) {
|
|
136
|
+
const fieldsType = generateModelFieldsType(model.fields);
|
|
137
|
+
const relationsType = generateModelRelationsType(model.relations);
|
|
138
|
+
const storageType = generateModelStorage(modelName, model);
|
|
139
|
+
|
|
140
|
+
const modelParts: string[] = [
|
|
141
|
+
`readonly fields: ${fieldsType}`,
|
|
142
|
+
`readonly relations: ${relationsType}`,
|
|
143
|
+
`readonly storage: ${storageType}`,
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
if (model.owner) {
|
|
147
|
+
modelParts.push(`readonly owner: ${serializeValue(model.owner)}`);
|
|
148
|
+
}
|
|
149
|
+
if (model.discriminator) {
|
|
150
|
+
modelParts.push(`readonly discriminator: ${serializeValue(model.discriminator)}`);
|
|
151
|
+
}
|
|
152
|
+
if (model.variants) {
|
|
153
|
+
modelParts.push(`readonly variants: ${serializeValue(model.variants)}`);
|
|
154
|
+
}
|
|
155
|
+
if (model.base) {
|
|
156
|
+
modelParts.push(`readonly base: ${serializeValue(model.base)}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
modelTypes.push(`readonly ${modelName}: { ${modelParts.join('; ')} }`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return `{ ${modelTypes.join('; ')} }`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function deduplicateImports(imports: TypesImportSpec[]): TypesImportSpec[] {
|
|
166
|
+
const seenKeys = new Set<string>();
|
|
167
|
+
const result: TypesImportSpec[] = [];
|
|
168
|
+
for (const imp of imports) {
|
|
169
|
+
const key = `${imp.package}::${imp.named}`;
|
|
170
|
+
if (!seenKeys.has(key)) {
|
|
171
|
+
seenKeys.add(key);
|
|
172
|
+
result.push(imp);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function generateImportLines(imports: TypesImportSpec[]): string[] {
|
|
179
|
+
return imports.map((imp) => {
|
|
180
|
+
const importClause = imp.named === imp.alias ? imp.named : `${imp.named} as ${imp.alias}`;
|
|
181
|
+
return `import type { ${importClause} } from '${imp.package}';`;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function generateCodecTypeIntersection(
|
|
186
|
+
imports: ReadonlyArray<TypesImportSpec>,
|
|
187
|
+
named: string,
|
|
188
|
+
): string {
|
|
189
|
+
const aliases = imports.filter((imp) => imp.named === named).map((imp) => imp.alias);
|
|
190
|
+
return aliases.join(' & ') || 'Record<string, never>';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function serializeExecutionType(execution: Record<string, unknown>): string {
|
|
194
|
+
const parts: string[] = ['readonly executionHash: ExecutionHash'];
|
|
195
|
+
for (const [key, value] of Object.entries(execution)) {
|
|
196
|
+
if (key === 'executionHash') continue;
|
|
197
|
+
parts.push(`readonly ${serializeObjectKey(key)}: ${serializeValue(value)}`);
|
|
198
|
+
}
|
|
199
|
+
return `{ ${parts.join('; ')} }`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function generateHashTypeAliases(hashes: {
|
|
203
|
+
readonly storageHash: string;
|
|
204
|
+
readonly executionHash?: string;
|
|
205
|
+
readonly profileHash: string;
|
|
206
|
+
}): string {
|
|
207
|
+
const executionHashType = hashes.executionHash
|
|
208
|
+
? `ExecutionHashBase<'${hashes.executionHash}'>`
|
|
209
|
+
: 'ExecutionHashBase<string>';
|
|
210
|
+
|
|
211
|
+
return [
|
|
212
|
+
`export type StorageHash = StorageHashBase<'${hashes.storageHash}'>;`,
|
|
213
|
+
`export type ExecutionHash = ${executionHashType};`,
|
|
214
|
+
`export type ProfileHash = ProfileHashBase<'${hashes.profileHash}'>;`,
|
|
215
|
+
].join('\n');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export type ResolvedFieldType = { readonly input: string; readonly output: string };
|
|
219
|
+
|
|
220
|
+
function applyModifiers(base: string, field: ContractField): string {
|
|
221
|
+
let result = base;
|
|
222
|
+
if (field.many === true) result = `ReadonlyArray<${result}>`;
|
|
223
|
+
if (field.dict === true) result = `Readonly<Record<string, ${result}>>`;
|
|
224
|
+
if (field.nullable) result = `${result} | null`;
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function resolveFieldType(
|
|
229
|
+
field: ContractField,
|
|
230
|
+
codecLookup?: CodecLookup,
|
|
231
|
+
): ResolvedFieldType {
|
|
232
|
+
const { type } = field;
|
|
233
|
+
|
|
234
|
+
switch (type.kind) {
|
|
235
|
+
case 'scalar': {
|
|
236
|
+
let outputResolved: string | undefined;
|
|
237
|
+
if (codecLookup && type.typeParams && Object.keys(type.typeParams).length > 0) {
|
|
238
|
+
const codec = codecLookup.get(type.codecId);
|
|
239
|
+
if (codec?.renderOutputType) {
|
|
240
|
+
const rendered = codec.renderOutputType(type.typeParams);
|
|
241
|
+
if (rendered && isSafeTypeExpression(rendered)) {
|
|
242
|
+
outputResolved = rendered;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const codecAccessor = `CodecTypes[${serializeValue(type.codecId)}]`;
|
|
247
|
+
return {
|
|
248
|
+
output: applyModifiers(outputResolved ?? `${codecAccessor}['output']`, field),
|
|
249
|
+
input: applyModifiers(`${codecAccessor}['input']`, field),
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
case 'valueObject':
|
|
253
|
+
return {
|
|
254
|
+
output: applyModifiers(`${type.name}Output`, field),
|
|
255
|
+
input: applyModifiers(`${type.name}Input`, field),
|
|
256
|
+
};
|
|
257
|
+
case 'union': {
|
|
258
|
+
const outputMembers = type.members.map((m) =>
|
|
259
|
+
m.kind === 'scalar'
|
|
260
|
+
? `CodecTypes[${serializeValue(m.codecId)}]['output']`
|
|
261
|
+
: `${m.name}Output`,
|
|
262
|
+
);
|
|
263
|
+
const inputMembers = type.members.map((m) =>
|
|
264
|
+
m.kind === 'scalar'
|
|
265
|
+
? `CodecTypes[${serializeValue(m.codecId)}]['input']`
|
|
266
|
+
: `${m.name}Input`,
|
|
267
|
+
);
|
|
268
|
+
return {
|
|
269
|
+
output: applyModifiers(outputMembers.join(' | '), field),
|
|
270
|
+
input: applyModifiers(inputMembers.join(' | '), field),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
default:
|
|
274
|
+
return {
|
|
275
|
+
output: applyModifiers('unknown', field),
|
|
276
|
+
input: applyModifiers('unknown', field),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function generateFieldResolvedType(
|
|
282
|
+
field: ContractField,
|
|
283
|
+
codecLookup?: CodecLookup,
|
|
284
|
+
side: 'input' | 'output' = 'output',
|
|
285
|
+
): string {
|
|
286
|
+
return resolveFieldType(field, codecLookup)[side];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function generateBothFieldTypesMaps(
|
|
290
|
+
models: Record<string, ContractModel> | undefined,
|
|
291
|
+
codecLookup?: CodecLookup,
|
|
292
|
+
): ResolvedFieldType {
|
|
293
|
+
if (!models || Object.keys(models).length === 0) {
|
|
294
|
+
return { output: 'Record<string, never>', input: 'Record<string, never>' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const outputModelEntries: string[] = [];
|
|
298
|
+
const inputModelEntries: string[] = [];
|
|
299
|
+
for (const [modelName, model] of Object.entries(models).sort(([a], [b]) => a.localeCompare(b))) {
|
|
300
|
+
if (!model) continue;
|
|
301
|
+
const outputFieldEntries: string[] = [];
|
|
302
|
+
const inputFieldEntries: string[] = [];
|
|
303
|
+
for (const [fieldName, field] of Object.entries(model.fields)) {
|
|
304
|
+
const resolved = resolveFieldType(field, codecLookup);
|
|
305
|
+
const key = `readonly ${serializeObjectKey(fieldName)}`;
|
|
306
|
+
outputFieldEntries.push(`${key}: ${resolved.output}`);
|
|
307
|
+
inputFieldEntries.push(`${key}: ${resolved.input}`);
|
|
308
|
+
}
|
|
309
|
+
const outputFields =
|
|
310
|
+
outputFieldEntries.length > 0
|
|
311
|
+
? `{ ${outputFieldEntries.join('; ')} }`
|
|
312
|
+
: 'Record<string, never>';
|
|
313
|
+
const inputFields =
|
|
314
|
+
inputFieldEntries.length > 0
|
|
315
|
+
? `{ ${inputFieldEntries.join('; ')} }`
|
|
316
|
+
: 'Record<string, never>';
|
|
317
|
+
const modelKey = `readonly ${serializeObjectKey(modelName)}`;
|
|
318
|
+
outputModelEntries.push(`${modelKey}: ${outputFields}`);
|
|
319
|
+
inputModelEntries.push(`${modelKey}: ${inputFields}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
output: `{ ${outputModelEntries.join('; ')} }`,
|
|
324
|
+
input: `{ ${inputModelEntries.join('; ')} }`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function generateFieldOutputTypesMap(
|
|
329
|
+
models: Record<string, ContractModel> | undefined,
|
|
330
|
+
codecLookup?: CodecLookup,
|
|
331
|
+
): string {
|
|
332
|
+
return generateBothFieldTypesMaps(models, codecLookup).output;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function generateFieldInputTypesMap(
|
|
336
|
+
models: Record<string, ContractModel> | undefined,
|
|
337
|
+
codecLookup?: CodecLookup,
|
|
338
|
+
): string {
|
|
339
|
+
return generateBothFieldTypesMaps(models, codecLookup).input;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function generateValueObjectType(
|
|
343
|
+
_voName: string,
|
|
344
|
+
vo: ContractValueObject,
|
|
345
|
+
_valueObjects: Record<string, ContractValueObject>,
|
|
346
|
+
side: 'input' | 'output' = 'output',
|
|
347
|
+
codecLookup?: CodecLookup,
|
|
348
|
+
): string {
|
|
349
|
+
return resolveValueObjectType(_voName, vo, _valueObjects, codecLookup)[side];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function resolveValueObjectType(
|
|
353
|
+
_voName: string,
|
|
354
|
+
vo: ContractValueObject,
|
|
355
|
+
_valueObjects: Record<string, ContractValueObject>,
|
|
356
|
+
codecLookup?: CodecLookup,
|
|
357
|
+
): ResolvedFieldType {
|
|
358
|
+
const outputEntries: string[] = [];
|
|
359
|
+
const inputEntries: string[] = [];
|
|
360
|
+
for (const [fieldName, field] of Object.entries(vo.fields)) {
|
|
361
|
+
const resolved = resolveFieldType(field, codecLookup);
|
|
362
|
+
const key = `readonly ${serializeObjectKey(fieldName)}`;
|
|
363
|
+
outputEntries.push(`${key}: ${resolved.output}`);
|
|
364
|
+
inputEntries.push(`${key}: ${resolved.input}`);
|
|
365
|
+
}
|
|
366
|
+
const empty = 'Record<string, never>';
|
|
367
|
+
return {
|
|
368
|
+
output: outputEntries.length > 0 ? `{ ${outputEntries.join('; ')} }` : empty,
|
|
369
|
+
input: inputEntries.length > 0 ? `{ ${inputEntries.join('; ')} }` : empty,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function generateContractFieldDescriptor(fieldName: string, field: ContractField): string {
|
|
374
|
+
const mods: string[] = [];
|
|
375
|
+
if (field.many === true) mods.push('; readonly many: true');
|
|
376
|
+
if (field.dict === true) mods.push('; readonly dict: true');
|
|
377
|
+
const modStr = mods.join('');
|
|
378
|
+
|
|
379
|
+
const { type } = field;
|
|
380
|
+
if (type.kind === 'scalar') {
|
|
381
|
+
const typeParamsSpec =
|
|
382
|
+
type.typeParams && Object.keys(type.typeParams).length > 0
|
|
383
|
+
? `; readonly typeParams: ${serializeValue(type.typeParams)}`
|
|
384
|
+
: '';
|
|
385
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: { readonly kind: 'scalar'; readonly codecId: ${serializeValue(type.codecId)}${typeParamsSpec} }${modStr} }`;
|
|
386
|
+
}
|
|
387
|
+
if (type.kind === 'valueObject') {
|
|
388
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: { readonly kind: 'valueObject'; readonly name: ${serializeValue(type.name)} }${modStr} }`;
|
|
389
|
+
}
|
|
390
|
+
return `readonly ${serializeObjectKey(fieldName)}: { readonly nullable: ${field.nullable}; readonly type: ${serializeValue(type)}${modStr} }`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function generateValueObjectsDescriptorType(
|
|
394
|
+
valueObjects: Record<string, ContractValueObject> | undefined,
|
|
395
|
+
): string {
|
|
396
|
+
if (!valueObjects || Object.keys(valueObjects).length === 0) {
|
|
397
|
+
return 'Record<string, never>';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const voEntries: string[] = [];
|
|
401
|
+
for (const [voName, vo] of Object.entries(valueObjects)) {
|
|
402
|
+
const fieldEntries: string[] = [];
|
|
403
|
+
for (const [fieldName, field] of Object.entries(vo.fields)) {
|
|
404
|
+
fieldEntries.push(generateContractFieldDescriptor(fieldName, field));
|
|
405
|
+
}
|
|
406
|
+
const fieldsType =
|
|
407
|
+
fieldEntries.length > 0 ? `{ ${fieldEntries.join('; ')} }` : 'Record<string, never>';
|
|
408
|
+
voEntries.push(`readonly ${serializeObjectKey(voName)}: { readonly fields: ${fieldsType} }`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return `{ ${voEntries.join('; ')} }`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
export function generateValueObjectTypeAliases(
|
|
415
|
+
valueObjects: Record<string, ContractValueObject> | undefined,
|
|
416
|
+
codecLookup?: CodecLookup,
|
|
417
|
+
): string {
|
|
418
|
+
if (!valueObjects || Object.keys(valueObjects).length === 0) {
|
|
419
|
+
return '';
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const aliases: string[] = [];
|
|
423
|
+
for (const [voName, vo] of Object.entries(valueObjects)) {
|
|
424
|
+
const resolved = resolveValueObjectType(voName, vo, valueObjects, codecLookup);
|
|
425
|
+
aliases.push(`export type ${voName}Output = ${resolved.output};`);
|
|
426
|
+
aliases.push(`export type ${voName}Input = ${resolved.input};`);
|
|
427
|
+
}
|
|
428
|
+
return aliases.join('\n');
|
|
429
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
2
|
+
import type { TypesImportSpec } from '@prisma-next/framework-components/emission';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The subset of ControlStack that emit() reads.
|
|
6
|
+
* All fields are optional so tests can pass minimal objects.
|
|
7
|
+
* A full ControlStack satisfies this via structural typing.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmitStackInput {
|
|
10
|
+
readonly codecTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
11
|
+
readonly operationTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
12
|
+
readonly queryOperationTypeImports?: ReadonlyArray<TypesImportSpec>;
|
|
13
|
+
readonly extensionIds?: ReadonlyArray<string>;
|
|
14
|
+
readonly codecLookup?: CodecLookup;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface EmitResult {
|
|
18
|
+
readonly contractJson: string;
|
|
19
|
+
readonly contractDts: string;
|
|
20
|
+
readonly storageHash: string;
|
|
21
|
+
readonly executionHash?: string;
|
|
22
|
+
readonly profileHash: string;
|
|
23
|
+
}
|
package/src/emit.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { canonicalizeContractToObject } from '@prisma-next/contract/hashing';
|
|
2
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
3
|
+
import type { EmissionSpi } from '@prisma-next/framework-components/emission';
|
|
4
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
5
|
+
import { format } from 'prettier';
|
|
6
|
+
import type { EmitResult, EmitStackInput } from './emit-types';
|
|
7
|
+
import { generateContractDts } from './generate-contract-dts';
|
|
8
|
+
|
|
9
|
+
const SCHEMA_VERSION = '1';
|
|
10
|
+
|
|
11
|
+
export async function emit(
|
|
12
|
+
contract: Contract,
|
|
13
|
+
stack: EmitStackInput,
|
|
14
|
+
targetFamily: EmissionSpi,
|
|
15
|
+
): Promise<EmitResult> {
|
|
16
|
+
const { codecTypeImports, operationTypeImports, queryOperationTypeImports } = stack;
|
|
17
|
+
|
|
18
|
+
const { storageHash } = contract.storage;
|
|
19
|
+
const executionHash = contract.execution?.executionHash;
|
|
20
|
+
const { profileHash } = contract;
|
|
21
|
+
|
|
22
|
+
const canonicalized = canonicalizeContractToObject(contract, {
|
|
23
|
+
schemaVersion: SCHEMA_VERSION,
|
|
24
|
+
});
|
|
25
|
+
const contractJsonString = JSON.stringify(
|
|
26
|
+
{
|
|
27
|
+
...canonicalized,
|
|
28
|
+
_generated: {
|
|
29
|
+
warning: '⚠️ GENERATED FILE - DO NOT EDIT',
|
|
30
|
+
message: 'This file is automatically generated by "prisma-next contract emit".',
|
|
31
|
+
regenerate: 'To regenerate, run: prisma-next contract emit',
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
null,
|
|
35
|
+
2,
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const generateOptions = queryOperationTypeImports ? { queryOperationTypeImports } : undefined;
|
|
39
|
+
|
|
40
|
+
const contractTypeHashes = {
|
|
41
|
+
storageHash,
|
|
42
|
+
...ifDefined('executionHash', executionHash),
|
|
43
|
+
profileHash,
|
|
44
|
+
};
|
|
45
|
+
const contractDtsRaw = generateContractDts(
|
|
46
|
+
contract,
|
|
47
|
+
targetFamily,
|
|
48
|
+
codecTypeImports ?? [],
|
|
49
|
+
operationTypeImports ?? [],
|
|
50
|
+
contractTypeHashes,
|
|
51
|
+
generateOptions,
|
|
52
|
+
stack.codecLookup,
|
|
53
|
+
);
|
|
54
|
+
const contractDts = await format(contractDtsRaw, {
|
|
55
|
+
parser: 'typescript',
|
|
56
|
+
singleQuote: true,
|
|
57
|
+
semi: true,
|
|
58
|
+
printWidth: 100,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
contractJson: contractJsonString,
|
|
63
|
+
contractDts,
|
|
64
|
+
storageHash,
|
|
65
|
+
...ifDefined('executionHash', executionHash),
|
|
66
|
+
profileHash,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/exports/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
export {
|
|
2
|
+
deduplicateImports,
|
|
3
|
+
generateCodecTypeIntersection,
|
|
4
|
+
generateFieldOutputTypesMap,
|
|
5
|
+
generateHashTypeAliases,
|
|
6
|
+
generateImportLines,
|
|
7
|
+
generateModelRelationsType,
|
|
8
|
+
generateRootsType,
|
|
9
|
+
serializeObjectKey,
|
|
10
|
+
serializeValue,
|
|
11
|
+
} from '../domain-type-generation';
|
|
12
|
+
export { emit } from '../emit';
|
|
13
|
+
export type { EmitResult, EmitStackInput } from '../emit-types';
|
|
14
|
+
export { generateContractDts } from '../generate-contract-dts';
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { Contract, ContractModel, ContractValueObject } from '@prisma-next/contract/types';
|
|
2
|
+
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
3
|
+
import type {
|
|
4
|
+
EmissionSpi,
|
|
5
|
+
GenerateContractTypesOptions,
|
|
6
|
+
TypesImportSpec,
|
|
7
|
+
} from '@prisma-next/framework-components/emission';
|
|
8
|
+
import {
|
|
9
|
+
deduplicateImports,
|
|
10
|
+
generateBothFieldTypesMaps,
|
|
11
|
+
generateCodecTypeIntersection,
|
|
12
|
+
generateHashTypeAliases,
|
|
13
|
+
generateImportLines,
|
|
14
|
+
generateModelsType,
|
|
15
|
+
generateRootsType,
|
|
16
|
+
generateValueObjectsDescriptorType,
|
|
17
|
+
generateValueObjectTypeAliases,
|
|
18
|
+
serializeExecutionType,
|
|
19
|
+
serializeValue,
|
|
20
|
+
} from './domain-type-generation';
|
|
21
|
+
|
|
22
|
+
export function generateContractDts(
|
|
23
|
+
contract: Contract,
|
|
24
|
+
emitter: EmissionSpi,
|
|
25
|
+
codecTypeImports: ReadonlyArray<TypesImportSpec>,
|
|
26
|
+
operationTypeImports: ReadonlyArray<TypesImportSpec>,
|
|
27
|
+
hashes: {
|
|
28
|
+
readonly storageHash: string;
|
|
29
|
+
readonly executionHash?: string;
|
|
30
|
+
readonly profileHash: string;
|
|
31
|
+
},
|
|
32
|
+
options?: GenerateContractTypesOptions,
|
|
33
|
+
codecLookup?: CodecLookup,
|
|
34
|
+
): string {
|
|
35
|
+
const allImports: TypesImportSpec[] = [...codecTypeImports, ...operationTypeImports];
|
|
36
|
+
if (options?.queryOperationTypeImports) {
|
|
37
|
+
allImports.push(...options.queryOperationTypeImports);
|
|
38
|
+
}
|
|
39
|
+
const uniqueImports = deduplicateImports(allImports);
|
|
40
|
+
const importLines = generateImportLines(uniqueImports);
|
|
41
|
+
|
|
42
|
+
const familyImportLines = emitter.getFamilyImports();
|
|
43
|
+
|
|
44
|
+
const hashAliases = generateHashTypeAliases(hashes);
|
|
45
|
+
|
|
46
|
+
const codecTypes = generateCodecTypeIntersection(codecTypeImports, 'CodecTypes');
|
|
47
|
+
const operationTypes = generateCodecTypeIntersection(operationTypeImports, 'OperationTypes');
|
|
48
|
+
|
|
49
|
+
const familyTypeAliases = emitter.getFamilyTypeAliases(options);
|
|
50
|
+
|
|
51
|
+
const typeMapsExpr = emitter.getTypeMapsExpression();
|
|
52
|
+
|
|
53
|
+
const storageType = emitter.generateStorageType(contract, 'StorageHash');
|
|
54
|
+
|
|
55
|
+
const modelsType = generateModelsType(
|
|
56
|
+
contract.models as Record<string, ContractModel>,
|
|
57
|
+
(name, model) => emitter.generateModelStorageType(name, model),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const rootsType = generateRootsType(contract.roots);
|
|
61
|
+
|
|
62
|
+
const valueObjects = contract.valueObjects as Record<string, ContractValueObject> | undefined;
|
|
63
|
+
const valueObjectTypeAliases = generateValueObjectTypeAliases(valueObjects, codecLookup);
|
|
64
|
+
const valueObjectsDescriptor = generateValueObjectsDescriptorType(valueObjects);
|
|
65
|
+
|
|
66
|
+
const executionClause =
|
|
67
|
+
contract.execution !== undefined
|
|
68
|
+
? `\n readonly execution: ${serializeExecutionType(contract.execution)};`
|
|
69
|
+
: '';
|
|
70
|
+
|
|
71
|
+
const fieldTypesMaps = generateBothFieldTypesMaps(
|
|
72
|
+
contract.models as Record<string, ContractModel> | undefined,
|
|
73
|
+
codecLookup,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const contractWrapper = emitter.getContractWrapper('ContractBase', 'TypeMaps');
|
|
77
|
+
|
|
78
|
+
return `// ⚠️ GENERATED FILE - DO NOT EDIT
|
|
79
|
+
// This file is automatically generated by 'prisma-next contract emit'.
|
|
80
|
+
// To regenerate, run: prisma-next contract emit
|
|
81
|
+
${importLines.join('\n')}
|
|
82
|
+
|
|
83
|
+
${familyImportLines.join('\n')}
|
|
84
|
+
import type {
|
|
85
|
+
Contract as ContractType,
|
|
86
|
+
ExecutionHashBase,
|
|
87
|
+
ProfileHashBase,
|
|
88
|
+
StorageHashBase,
|
|
89
|
+
} from '@prisma-next/contract/types';
|
|
90
|
+
|
|
91
|
+
${hashAliases}
|
|
92
|
+
|
|
93
|
+
export type CodecTypes = ${codecTypes};
|
|
94
|
+
export type OperationTypes = ${operationTypes};
|
|
95
|
+
${familyTypeAliases}
|
|
96
|
+
${valueObjectTypeAliases}
|
|
97
|
+
export type FieldOutputTypes = ${fieldTypesMaps.output};
|
|
98
|
+
export type FieldInputTypes = ${fieldTypesMaps.input};
|
|
99
|
+
export type TypeMaps = ${typeMapsExpr};
|
|
100
|
+
|
|
101
|
+
type ContractBase = ContractType<
|
|
102
|
+
${storageType},
|
|
103
|
+
${modelsType}
|
|
104
|
+
> & {
|
|
105
|
+
readonly target: ${serializeValue(contract.target)};
|
|
106
|
+
readonly targetFamily: ${serializeValue(contract.targetFamily)};
|
|
107
|
+
readonly roots: ${rootsType};
|
|
108
|
+
readonly capabilities: ${serializeValue(contract.capabilities)};
|
|
109
|
+
readonly extensionPacks: ${serializeValue(contract.extensionPacks)};${executionClause}
|
|
110
|
+
readonly meta: ${serializeValue(contract.meta)};
|
|
111
|
+
${valueObjects ? `readonly valueObjects: ${valueObjectsDescriptor};` : ''}
|
|
112
|
+
readonly profileHash: ProfileHash;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
${contractWrapper}
|
|
116
|
+
`;
|
|
117
|
+
}
|