@prisma-next/sql-contract-ts 0.3.0-dev.134 → 0.3.0-dev.135
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 +141 -4
- package/dist/contract-builder.d.mts +732 -15
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +1254 -131
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +7 -7
- package/src/authoring-helper-runtime.ts +139 -0
- package/src/authoring-type-utils.ts +168 -0
- package/src/composed-authoring-helpers.ts +254 -0
- package/src/contract-builder.ts +236 -335
- package/src/contract-ir-builder.ts +475 -0
- package/src/contract.ts +6 -0
- package/src/exports/contract-builder.ts +24 -2
- package/src/semantic-contract.ts +86 -0
- package/src/staged-contract-dsl.ts +1490 -0
- package/src/staged-contract-lowering.ts +705 -0
- package/src/staged-contract-types.ts +494 -0
- package/src/staged-contract-warnings.ts +245 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
import type { ContractIR } from '@prisma-next/contract/ir';
|
|
2
|
+
import type {
|
|
3
|
+
ColumnDefault,
|
|
4
|
+
ColumnDefaultLiteralInputValue,
|
|
5
|
+
ColumnDefaultLiteralValue,
|
|
6
|
+
ExecutionMutationDefault,
|
|
7
|
+
ExecutionMutationDefaultValue,
|
|
8
|
+
TaggedRaw,
|
|
9
|
+
} from '@prisma-next/contract/types';
|
|
10
|
+
import type {
|
|
11
|
+
ColumnBuilderState,
|
|
12
|
+
ContractBuilderState,
|
|
13
|
+
ModelBuilderState,
|
|
14
|
+
RelationDefinition,
|
|
15
|
+
TableBuilderState,
|
|
16
|
+
} from '@prisma-next/contract-authoring';
|
|
17
|
+
import {
|
|
18
|
+
applyFkDefaults,
|
|
19
|
+
type SqlStorage,
|
|
20
|
+
type StorageTypeInstance,
|
|
21
|
+
} from '@prisma-next/sql-contract/types';
|
|
22
|
+
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
23
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
24
|
+
import type { SqlSemanticContractDefinition, SqlSemanticModelNode } from './semantic-contract';
|
|
25
|
+
|
|
26
|
+
type RuntimeTableState = TableBuilderState<
|
|
27
|
+
string,
|
|
28
|
+
Record<string, ColumnBuilderState<string, boolean, string>>,
|
|
29
|
+
readonly string[] | undefined
|
|
30
|
+
>;
|
|
31
|
+
|
|
32
|
+
type RuntimeModelState = ModelBuilderState<
|
|
33
|
+
string,
|
|
34
|
+
string,
|
|
35
|
+
Record<string, string>,
|
|
36
|
+
Record<string, RelationDefinition>
|
|
37
|
+
>;
|
|
38
|
+
|
|
39
|
+
export type RuntimeBuilderState = ContractBuilderState<
|
|
40
|
+
string | undefined,
|
|
41
|
+
Record<string, RuntimeTableState>,
|
|
42
|
+
Record<string, RuntimeModelState>,
|
|
43
|
+
string | undefined,
|
|
44
|
+
Record<string, unknown> | undefined,
|
|
45
|
+
Record<string, Record<string, boolean>> | undefined
|
|
46
|
+
>;
|
|
47
|
+
|
|
48
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
49
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
50
|
+
const proto = Object.getPrototypeOf(value);
|
|
51
|
+
return proto === Object.prototype || proto === null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isJsonValue(value: unknown): value is ColumnDefaultLiteralValue {
|
|
55
|
+
if (value === null) return true;
|
|
56
|
+
const valueType = typeof value;
|
|
57
|
+
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') return true;
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.every((item) => isJsonValue(item));
|
|
60
|
+
}
|
|
61
|
+
if (isPlainObject(value)) {
|
|
62
|
+
return Object.values(value).every((item) => isJsonValue(item));
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function encodeDefaultLiteralValue(
|
|
68
|
+
value: ColumnDefaultLiteralInputValue,
|
|
69
|
+
): ColumnDefaultLiteralValue {
|
|
70
|
+
if (typeof value === 'bigint') {
|
|
71
|
+
return { $type: 'bigint', value: value.toString() };
|
|
72
|
+
}
|
|
73
|
+
if (value instanceof Date) {
|
|
74
|
+
return value.toISOString();
|
|
75
|
+
}
|
|
76
|
+
if (isJsonValue(value)) {
|
|
77
|
+
if (isPlainObject(value) && '$type' in value) {
|
|
78
|
+
return { $type: 'raw', value } satisfies TaggedRaw;
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
throw new Error(
|
|
83
|
+
'Unsupported column default literal value: expected JSON-safe value, bigint, or Date.',
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function encodeColumnDefault(defaultInput: ColumnDefault): ColumnDefault {
|
|
88
|
+
if (defaultInput.kind === 'function') {
|
|
89
|
+
return { kind: 'function', expression: defaultInput.expression };
|
|
90
|
+
}
|
|
91
|
+
return { kind: 'literal', value: encodeDefaultLiteralValue(defaultInput.value) };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertStorageSemantics(storage: SqlStorage): void {
|
|
95
|
+
const semanticErrors = validateStorageSemantics(storage);
|
|
96
|
+
if (semanticErrors.length > 0) {
|
|
97
|
+
throw new Error(`Contract semantic validation failed: ${semanticErrors.join('; ')}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function buildContractIR(state: RuntimeBuilderState): ContractIR {
|
|
102
|
+
if (!state.target) {
|
|
103
|
+
throw new Error('target is required. Call .target() before .build()');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const target = state.target;
|
|
107
|
+
|
|
108
|
+
const storageTables: Record<
|
|
109
|
+
string,
|
|
110
|
+
{
|
|
111
|
+
columns: Record<string, unknown>;
|
|
112
|
+
uniques: unknown[];
|
|
113
|
+
indexes: unknown[];
|
|
114
|
+
foreignKeys: unknown[];
|
|
115
|
+
primaryKey?: unknown;
|
|
116
|
+
}
|
|
117
|
+
> = {};
|
|
118
|
+
const executionDefaults: ExecutionMutationDefault[] = [];
|
|
119
|
+
|
|
120
|
+
for (const tableName of Object.keys(state.tables)) {
|
|
121
|
+
const tableState = state.tables[tableName];
|
|
122
|
+
if (!tableState) continue;
|
|
123
|
+
|
|
124
|
+
const columns: Record<string, unknown> = {};
|
|
125
|
+
|
|
126
|
+
for (const columnName in tableState.columns) {
|
|
127
|
+
const columnState = tableState.columns[columnName];
|
|
128
|
+
if (!columnState) continue;
|
|
129
|
+
|
|
130
|
+
const encodedDefault =
|
|
131
|
+
columnState.default !== undefined
|
|
132
|
+
? encodeColumnDefault(columnState.default as ColumnDefault)
|
|
133
|
+
: undefined;
|
|
134
|
+
|
|
135
|
+
columns[columnName] = {
|
|
136
|
+
nativeType: columnState.nativeType,
|
|
137
|
+
codecId: columnState.type,
|
|
138
|
+
nullable: columnState.nullable ?? false,
|
|
139
|
+
...ifDefined('typeParams', columnState.typeParams),
|
|
140
|
+
...ifDefined('default', encodedDefault),
|
|
141
|
+
...ifDefined('typeRef', columnState.typeRef),
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if ('executionDefault' in columnState && columnState.executionDefault) {
|
|
145
|
+
executionDefaults.push({
|
|
146
|
+
ref: { table: tableName, column: columnName },
|
|
147
|
+
onCreate: columnState.executionDefault as ExecutionMutationDefaultValue,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const uniques = (tableState.uniques ?? []).map((u) => ({
|
|
153
|
+
columns: u.columns,
|
|
154
|
+
...(u.name ? { name: u.name } : {}),
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
const indexes = (tableState.indexes ?? []).map((i) => ({
|
|
158
|
+
columns: i.columns,
|
|
159
|
+
...(i.name ? { name: i.name } : {}),
|
|
160
|
+
...(i.using ? { using: i.using } : {}),
|
|
161
|
+
...(i.config ? { config: i.config } : {}),
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
const foreignKeys = (tableState.foreignKeys ?? []).map((fk) => ({
|
|
165
|
+
columns: fk.columns,
|
|
166
|
+
references: fk.references,
|
|
167
|
+
...applyFkDefaults(fk, state.foreignKeyDefaults),
|
|
168
|
+
...(fk.name ? { name: fk.name } : {}),
|
|
169
|
+
...(fk.onDelete !== undefined ? { onDelete: fk.onDelete } : {}),
|
|
170
|
+
...(fk.onUpdate !== undefined ? { onUpdate: fk.onUpdate } : {}),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
storageTables[tableName] = {
|
|
174
|
+
columns,
|
|
175
|
+
uniques,
|
|
176
|
+
indexes,
|
|
177
|
+
foreignKeys,
|
|
178
|
+
...(tableState.primaryKey
|
|
179
|
+
? {
|
|
180
|
+
primaryKey: {
|
|
181
|
+
columns: tableState.primaryKey,
|
|
182
|
+
...(tableState.primaryKeyName ? { name: tableState.primaryKeyName } : {}),
|
|
183
|
+
},
|
|
184
|
+
}
|
|
185
|
+
: {}),
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const storageTypes = (state.storageTypes ?? {}) as Record<string, StorageTypeInstance>;
|
|
190
|
+
const storage = {
|
|
191
|
+
tables: storageTables,
|
|
192
|
+
types: storageTypes,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const execution =
|
|
196
|
+
executionDefaults.length > 0
|
|
197
|
+
? {
|
|
198
|
+
mutations: {
|
|
199
|
+
defaults: executionDefaults.sort((a, b) => {
|
|
200
|
+
const tableCompare = a.ref.table.localeCompare(b.ref.table);
|
|
201
|
+
if (tableCompare !== 0) {
|
|
202
|
+
return tableCompare;
|
|
203
|
+
}
|
|
204
|
+
return a.ref.column.localeCompare(b.ref.column);
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
: undefined;
|
|
209
|
+
|
|
210
|
+
const models: Record<string, Record<string, unknown>> = {};
|
|
211
|
+
|
|
212
|
+
for (const modelName in state.models) {
|
|
213
|
+
const modelState = state.models[modelName];
|
|
214
|
+
if (!modelState) continue;
|
|
215
|
+
|
|
216
|
+
const tableName = modelState.table;
|
|
217
|
+
const tableState = state.tables[tableName];
|
|
218
|
+
const tableColumns = tableState
|
|
219
|
+
? (tableState.columns as Record<string, { type: string; nullable?: boolean }>)
|
|
220
|
+
: {};
|
|
221
|
+
|
|
222
|
+
const storageFields: Record<string, { readonly column: string }> = {};
|
|
223
|
+
const domainFields: Record<string, Record<string, unknown>> = {};
|
|
224
|
+
|
|
225
|
+
for (const fieldName in modelState.fields) {
|
|
226
|
+
const columnName = modelState.fields[fieldName];
|
|
227
|
+
if (!columnName) continue;
|
|
228
|
+
|
|
229
|
+
storageFields[fieldName] = { column: columnName };
|
|
230
|
+
|
|
231
|
+
const column = tableColumns[columnName];
|
|
232
|
+
if (column) {
|
|
233
|
+
domainFields[fieldName] = {
|
|
234
|
+
codecId: column.type,
|
|
235
|
+
nullable: column.nullable ?? false,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const modelRelations: Record<string, Record<string, unknown>> = {};
|
|
241
|
+
if (modelState.relations) {
|
|
242
|
+
for (const relName in modelState.relations) {
|
|
243
|
+
const rel = modelState.relations[relName];
|
|
244
|
+
if (!rel) continue;
|
|
245
|
+
modelRelations[relName] = {
|
|
246
|
+
to: rel.to,
|
|
247
|
+
cardinality: rel.cardinality,
|
|
248
|
+
on: {
|
|
249
|
+
localFields: rel.on.parentCols,
|
|
250
|
+
targetFields: rel.on.childCols,
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
models[modelName] = {
|
|
257
|
+
storage: {
|
|
258
|
+
table: tableName,
|
|
259
|
+
fields: storageFields,
|
|
260
|
+
},
|
|
261
|
+
fields: domainFields,
|
|
262
|
+
relations: modelRelations,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const extensionNamespaces = state.extensionNamespaces ?? [];
|
|
267
|
+
const extensionPacks: Record<string, unknown> = { ...(state.extensionPacks || {}) };
|
|
268
|
+
for (const namespace of extensionNamespaces) {
|
|
269
|
+
if (!Object.hasOwn(extensionPacks, namespace)) {
|
|
270
|
+
extensionPacks[namespace] = {};
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const contract = {
|
|
275
|
+
schemaVersion: '1' as const,
|
|
276
|
+
target,
|
|
277
|
+
targetFamily: 'sql' as const,
|
|
278
|
+
storageHash: state.storageHash || 'sha256:ts-builder-placeholder',
|
|
279
|
+
models,
|
|
280
|
+
roots: {},
|
|
281
|
+
storage,
|
|
282
|
+
...(execution ? { execution } : {}),
|
|
283
|
+
extensionPacks,
|
|
284
|
+
capabilities: state.capabilities || {},
|
|
285
|
+
meta: {},
|
|
286
|
+
sources: {},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
assertStorageSemantics(contract.storage as SqlStorage);
|
|
290
|
+
|
|
291
|
+
return contract as ContractIR;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function assertKnownTargetModel(
|
|
295
|
+
modelsByName: ReadonlyMap<string, SqlSemanticModelNode>,
|
|
296
|
+
sourceModelName: string,
|
|
297
|
+
targetModelName: string,
|
|
298
|
+
context: string,
|
|
299
|
+
): SqlSemanticModelNode {
|
|
300
|
+
const targetModel = modelsByName.get(targetModelName);
|
|
301
|
+
if (!targetModel) {
|
|
302
|
+
throw new Error(
|
|
303
|
+
`${context} on model "${sourceModelName}" references unknown model "${targetModelName}"`,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
return targetModel;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function assertTargetTableMatches(
|
|
310
|
+
sourceModelName: string,
|
|
311
|
+
targetModel: SqlSemanticModelNode,
|
|
312
|
+
referencedTableName: string,
|
|
313
|
+
context: string,
|
|
314
|
+
): void {
|
|
315
|
+
if (targetModel.tableName !== referencedTableName) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function buildSqlContractFromSemanticDefinition(
|
|
323
|
+
definition: SqlSemanticContractDefinition,
|
|
324
|
+
): ContractIR {
|
|
325
|
+
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
326
|
+
|
|
327
|
+
const tables: Record<string, RuntimeTableState> = {};
|
|
328
|
+
for (const model of definition.models) {
|
|
329
|
+
const columns: Record<string, ColumnBuilderState<string, boolean, string>> = {};
|
|
330
|
+
|
|
331
|
+
for (const field of model.fields) {
|
|
332
|
+
if (field.executionDefault) {
|
|
333
|
+
if (field.default !== undefined) {
|
|
334
|
+
throw new Error(
|
|
335
|
+
`Field "${model.modelName}.${field.fieldName}" cannot define both default and executionDefault.`,
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
if (field.nullable) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`Field "${model.modelName}.${field.fieldName}" cannot be nullable when executionDefault is present.`,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
columns[field.columnName] = {
|
|
344
|
+
name: field.columnName,
|
|
345
|
+
type: field.descriptor.codecId,
|
|
346
|
+
nativeType: field.descriptor.nativeType,
|
|
347
|
+
nullable: false,
|
|
348
|
+
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
349
|
+
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
350
|
+
executionDefault: field.executionDefault,
|
|
351
|
+
} as ColumnBuilderState<string, false, string>;
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
columns[field.columnName] = {
|
|
356
|
+
name: field.columnName,
|
|
357
|
+
type: field.descriptor.codecId,
|
|
358
|
+
nativeType: field.descriptor.nativeType,
|
|
359
|
+
nullable: field.nullable,
|
|
360
|
+
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
361
|
+
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
362
|
+
...ifDefined('default', field.default),
|
|
363
|
+
} as ColumnBuilderState<string, boolean, string>;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (model.id) {
|
|
367
|
+
const fieldsByColumnName = new Map(model.fields.map((field) => [field.columnName, field]));
|
|
368
|
+
for (const columnName of model.id.columns) {
|
|
369
|
+
const field = fieldsByColumnName.get(columnName);
|
|
370
|
+
if (field?.nullable) {
|
|
371
|
+
throw new Error(
|
|
372
|
+
`Model "${model.modelName}" uses nullable field "${field.fieldName}" in its identity.`,
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const foreignKeys = (model.foreignKeys ?? []).map((fk) => {
|
|
379
|
+
const targetModel = assertKnownTargetModel(
|
|
380
|
+
modelsByName,
|
|
381
|
+
model.modelName,
|
|
382
|
+
fk.references.model,
|
|
383
|
+
'Foreign key',
|
|
384
|
+
);
|
|
385
|
+
assertTargetTableMatches(model.modelName, targetModel, fk.references.table, 'Foreign key');
|
|
386
|
+
return {
|
|
387
|
+
columns: fk.columns,
|
|
388
|
+
references: { table: fk.references.table, columns: fk.references.columns },
|
|
389
|
+
...ifDefined('name', fk.name),
|
|
390
|
+
...ifDefined('onDelete', fk.onDelete),
|
|
391
|
+
...ifDefined('onUpdate', fk.onUpdate),
|
|
392
|
+
...ifDefined('constraint', fk.constraint),
|
|
393
|
+
...ifDefined('index', fk.index),
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
tables[model.tableName] = {
|
|
398
|
+
name: model.tableName,
|
|
399
|
+
columns,
|
|
400
|
+
...(model.id ? { primaryKey: model.id.columns } : {}),
|
|
401
|
+
...(model.id?.name ? { primaryKeyName: model.id.name } : {}),
|
|
402
|
+
uniques: model.uniques ?? [],
|
|
403
|
+
indexes: model.indexes ?? [],
|
|
404
|
+
foreignKeys,
|
|
405
|
+
} as RuntimeTableState;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const modelStates: Record<string, RuntimeModelState> = {};
|
|
409
|
+
for (const model of definition.models) {
|
|
410
|
+
const fields: Record<string, string> = {};
|
|
411
|
+
for (const field of model.fields) {
|
|
412
|
+
fields[field.fieldName] = field.columnName;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const relations: Record<string, RelationDefinition> = {};
|
|
416
|
+
for (const relation of model.relations ?? []) {
|
|
417
|
+
const targetModel = assertKnownTargetModel(
|
|
418
|
+
modelsByName,
|
|
419
|
+
model.modelName,
|
|
420
|
+
relation.toModel,
|
|
421
|
+
'Relation',
|
|
422
|
+
);
|
|
423
|
+
assertTargetTableMatches(model.modelName, targetModel, relation.toTable, 'Relation');
|
|
424
|
+
|
|
425
|
+
if (relation.cardinality === 'N:M' && !relation.through) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Relation "${model.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
relations[relation.fieldName] = {
|
|
432
|
+
to: relation.toModel,
|
|
433
|
+
cardinality: relation.cardinality,
|
|
434
|
+
on: {
|
|
435
|
+
parentCols: relation.on.parentColumns,
|
|
436
|
+
childCols: relation.on.childColumns,
|
|
437
|
+
},
|
|
438
|
+
...(relation.through
|
|
439
|
+
? {
|
|
440
|
+
through: {
|
|
441
|
+
table: relation.through.table,
|
|
442
|
+
parentCols: relation.through.parentColumns,
|
|
443
|
+
childCols: relation.through.childColumns,
|
|
444
|
+
},
|
|
445
|
+
}
|
|
446
|
+
: undefined),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
modelStates[model.modelName] = {
|
|
451
|
+
name: model.modelName,
|
|
452
|
+
table: model.tableName,
|
|
453
|
+
fields,
|
|
454
|
+
relations,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const extensionNamespaces = definition.extensionPacks
|
|
459
|
+
? Object.values(definition.extensionPacks).map((pack) => pack.id)
|
|
460
|
+
: undefined;
|
|
461
|
+
|
|
462
|
+
const state: RuntimeBuilderState = {
|
|
463
|
+
target: definition.target.targetId,
|
|
464
|
+
tables,
|
|
465
|
+
models: modelStates,
|
|
466
|
+
...ifDefined('storageTypes', definition.storageTypes),
|
|
467
|
+
...ifDefined('storageHash', definition.storageHash),
|
|
468
|
+
...ifDefined('extensionPacks', definition.extensionPacks),
|
|
469
|
+
...ifDefined('capabilities', definition.capabilities),
|
|
470
|
+
...ifDefined('foreignKeyDefaults', definition.foreignKeyDefaults),
|
|
471
|
+
...ifDefined('extensionNamespaces', extensionNamespaces),
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
return buildContractIR(state);
|
|
475
|
+
}
|
package/src/contract.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
ColumnDefaultSchema,
|
|
11
11
|
ForeignKeySchema,
|
|
12
12
|
IndexSchema,
|
|
13
|
+
validateStorageSemantics,
|
|
13
14
|
} from '@prisma-next/sql-contract/validators';
|
|
14
15
|
import { type } from 'arktype';
|
|
15
16
|
import type { O } from 'ts-toolbelt';
|
|
@@ -465,5 +466,10 @@ export function validateContract<TContract extends SqlContract<SqlStorage>>(
|
|
|
465
466
|
|
|
466
467
|
validateContractLogic(contractForValidation);
|
|
467
468
|
|
|
469
|
+
const semanticErrors = validateStorageSemantics(contractForValidation.storage);
|
|
470
|
+
if (semanticErrors.length > 0) {
|
|
471
|
+
throw new Error(`Contract semantic validation failed: ${semanticErrors.join('; ')}`);
|
|
472
|
+
}
|
|
473
|
+
|
|
468
474
|
return decodeContractDefaults(contractForValidation) as TContract;
|
|
469
475
|
}
|
|
@@ -1,2 +1,24 @@
|
|
|
1
|
-
export type {
|
|
2
|
-
|
|
1
|
+
export type {
|
|
2
|
+
ColumnBuilder,
|
|
3
|
+
ComposedAuthoringHelpers,
|
|
4
|
+
ScalarFieldBuilder,
|
|
5
|
+
StagedContractInput,
|
|
6
|
+
StagedModelBuilder,
|
|
7
|
+
} from '../contract-builder';
|
|
8
|
+
export {
|
|
9
|
+
buildSqlContractFromSemanticDefinition,
|
|
10
|
+
defineContract,
|
|
11
|
+
field,
|
|
12
|
+
model,
|
|
13
|
+
rel,
|
|
14
|
+
} from '../contract-builder';
|
|
15
|
+
export type {
|
|
16
|
+
SqlSemanticContractDefinition,
|
|
17
|
+
SqlSemanticFieldNode,
|
|
18
|
+
SqlSemanticForeignKeyNode,
|
|
19
|
+
SqlSemanticIndexNode,
|
|
20
|
+
SqlSemanticModelNode,
|
|
21
|
+
SqlSemanticPrimaryKeyNode,
|
|
22
|
+
SqlSemanticRelationNode,
|
|
23
|
+
SqlSemanticUniqueConstraintNode,
|
|
24
|
+
} from '../semantic-contract';
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ExtensionPackRef, TargetPackRef } from '@prisma-next/contract/framework-components';
|
|
2
|
+
import type { ColumnDefault, ExecutionMutationDefaultValue } from '@prisma-next/contract/types';
|
|
3
|
+
import type {
|
|
4
|
+
ColumnTypeDescriptor,
|
|
5
|
+
ForeignKeyDefaultsState,
|
|
6
|
+
} from '@prisma-next/contract-authoring';
|
|
7
|
+
import type { ReferentialAction, StorageTypeInstance } from '@prisma-next/sql-contract/types';
|
|
8
|
+
|
|
9
|
+
export interface SqlSemanticFieldNode {
|
|
10
|
+
readonly fieldName: string;
|
|
11
|
+
readonly columnName: string;
|
|
12
|
+
readonly descriptor: ColumnTypeDescriptor;
|
|
13
|
+
readonly nullable: boolean;
|
|
14
|
+
readonly default?: ColumnDefault;
|
|
15
|
+
readonly executionDefault?: ExecutionMutationDefaultValue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SqlSemanticPrimaryKeyNode {
|
|
19
|
+
readonly columns: readonly string[];
|
|
20
|
+
readonly name?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SqlSemanticUniqueConstraintNode {
|
|
24
|
+
readonly columns: readonly string[];
|
|
25
|
+
readonly name?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface SqlSemanticIndexNode {
|
|
29
|
+
readonly columns: readonly string[];
|
|
30
|
+
readonly name?: string;
|
|
31
|
+
readonly using?: string;
|
|
32
|
+
readonly config?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SqlSemanticForeignKeyNode {
|
|
36
|
+
readonly columns: readonly string[];
|
|
37
|
+
readonly references: {
|
|
38
|
+
readonly model: string;
|
|
39
|
+
readonly table: string;
|
|
40
|
+
readonly columns: readonly string[];
|
|
41
|
+
};
|
|
42
|
+
readonly name?: string;
|
|
43
|
+
readonly onDelete?: ReferentialAction;
|
|
44
|
+
readonly onUpdate?: ReferentialAction;
|
|
45
|
+
readonly constraint?: boolean;
|
|
46
|
+
readonly index?: boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface SqlSemanticRelationNode {
|
|
50
|
+
readonly fieldName: string;
|
|
51
|
+
readonly toModel: string;
|
|
52
|
+
readonly toTable: string;
|
|
53
|
+
readonly cardinality: '1:1' | '1:N' | 'N:1' | 'N:M';
|
|
54
|
+
readonly on: {
|
|
55
|
+
readonly parentTable: string;
|
|
56
|
+
readonly parentColumns: readonly string[];
|
|
57
|
+
readonly childTable: string;
|
|
58
|
+
readonly childColumns: readonly string[];
|
|
59
|
+
};
|
|
60
|
+
readonly through?: {
|
|
61
|
+
readonly table: string;
|
|
62
|
+
readonly parentColumns: readonly string[];
|
|
63
|
+
readonly childColumns: readonly string[];
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SqlSemanticModelNode {
|
|
68
|
+
readonly modelName: string;
|
|
69
|
+
readonly tableName: string;
|
|
70
|
+
readonly fields: readonly SqlSemanticFieldNode[];
|
|
71
|
+
readonly id?: SqlSemanticPrimaryKeyNode;
|
|
72
|
+
readonly uniques?: readonly SqlSemanticUniqueConstraintNode[];
|
|
73
|
+
readonly indexes?: readonly SqlSemanticIndexNode[];
|
|
74
|
+
readonly foreignKeys?: readonly SqlSemanticForeignKeyNode[];
|
|
75
|
+
readonly relations?: readonly SqlSemanticRelationNode[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface SqlSemanticContractDefinition {
|
|
79
|
+
readonly target: TargetPackRef<'sql', string>;
|
|
80
|
+
readonly extensionPacks?: Record<string, ExtensionPackRef<'sql', string>>;
|
|
81
|
+
readonly capabilities?: Record<string, Record<string, boolean>>;
|
|
82
|
+
readonly storageHash?: string;
|
|
83
|
+
readonly foreignKeyDefaults?: ForeignKeyDefaultsState;
|
|
84
|
+
readonly storageTypes?: Record<string, StorageTypeInstance>;
|
|
85
|
+
readonly models: readonly SqlSemanticModelNode[];
|
|
86
|
+
}
|