@prisma-next/sql-contract-ts 0.3.0-pr.99.6 → 0.3.0

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.
Files changed (41) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +206 -73
  3. package/dist/config-types.d.mts +8 -0
  4. package/dist/config-types.d.mts.map +1 -0
  5. package/dist/config-types.mjs +14 -0
  6. package/dist/config-types.mjs.map +1 -0
  7. package/dist/contract-builder.d.mts +769 -0
  8. package/dist/contract-builder.d.mts.map +1 -0
  9. package/dist/contract-builder.mjs +1288 -0
  10. package/dist/contract-builder.mjs.map +1 -0
  11. package/package.json +20 -17
  12. package/schemas/data-contract-sql-v1.json +189 -23
  13. package/src/authoring-helper-runtime.ts +139 -0
  14. package/src/authoring-type-utils.ts +168 -0
  15. package/src/build-contract.ts +463 -0
  16. package/src/composed-authoring-helpers.ts +256 -0
  17. package/src/config-types.ts +11 -0
  18. package/src/contract-builder.ts +232 -551
  19. package/src/contract-definition.ts +103 -0
  20. package/src/contract-dsl.ts +1492 -0
  21. package/src/contract-lowering.ts +703 -0
  22. package/src/contract-types.ts +534 -0
  23. package/src/contract-warnings.ts +242 -0
  24. package/src/exports/config-types.ts +2 -0
  25. package/src/exports/contract-builder.ts +23 -2
  26. package/dist/chunk-HTNUNGA2.js +0 -346
  27. package/dist/chunk-HTNUNGA2.js.map +0 -1
  28. package/dist/contract-builder.d.ts +0 -101
  29. package/dist/contract-builder.d.ts.map +0 -1
  30. package/dist/contract.d.ts +0 -50
  31. package/dist/contract.d.ts.map +0 -1
  32. package/dist/exports/contract-builder.d.ts +0 -3
  33. package/dist/exports/contract-builder.d.ts.map +0 -1
  34. package/dist/exports/contract-builder.js +0 -231
  35. package/dist/exports/contract-builder.js.map +0 -1
  36. package/dist/exports/contract.d.ts +0 -2
  37. package/dist/exports/contract.d.ts.map +0 -1
  38. package/dist/exports/contract.js +0 -9
  39. package/dist/exports/contract.js.map +0 -1
  40. package/src/contract.ts +0 -582
  41. package/src/exports/contract.ts +0 -1
@@ -0,0 +1,463 @@
1
+ import {
2
+ computeExecutionHash,
3
+ computeProfileHash,
4
+ computeStorageHash,
5
+ } from '@prisma-next/contract/hashing';
6
+ import {
7
+ type ColumnDefault,
8
+ type ColumnDefaultLiteralInputValue,
9
+ type Contract,
10
+ type ContractField,
11
+ type ContractModel,
12
+ type ContractRelation,
13
+ type ContractValueObject,
14
+ coreHash,
15
+ type ExecutionMutationDefault,
16
+ type ExecutionMutationDefaultValue,
17
+ type JsonValue,
18
+ type StorageHashBase,
19
+ } from '@prisma-next/contract/types';
20
+ import type { CodecLookup } from '@prisma-next/framework-components/codec';
21
+ import {
22
+ applyFkDefaults,
23
+ type SqlStorage,
24
+ type StorageColumn,
25
+ type StorageTable,
26
+ type StorageTypeInstance,
27
+ } from '@prisma-next/sql-contract/types';
28
+ import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
29
+ import { ifDefined } from '@prisma-next/utils/defined';
30
+ import type {
31
+ ContractDefinition,
32
+ FieldNode,
33
+ ModelNode,
34
+ ValueObjectFieldNode,
35
+ } from './contract-definition';
36
+
37
+ type DomainFieldRef =
38
+ | { readonly kind: 'scalar'; readonly many?: boolean }
39
+ | { readonly kind: 'valueObject'; readonly name: string; readonly many?: boolean };
40
+
41
+ function encodeDefaultLiteralValue(
42
+ value: ColumnDefaultLiteralInputValue,
43
+ codecId: string,
44
+ codecLookup?: CodecLookup,
45
+ ): JsonValue {
46
+ const codec = codecLookup?.get(codecId);
47
+ if (codec) {
48
+ return codec.encodeJson(value);
49
+ }
50
+ return value as JsonValue;
51
+ }
52
+
53
+ function encodeColumnDefault(
54
+ defaultInput: ColumnDefault,
55
+ codecId: string,
56
+ codecLookup?: CodecLookup,
57
+ ): ColumnDefault {
58
+ if (defaultInput.kind === 'function') {
59
+ return { kind: 'function', expression: defaultInput.expression };
60
+ }
61
+ return {
62
+ kind: 'literal',
63
+ value: encodeDefaultLiteralValue(defaultInput.value, codecId, codecLookup),
64
+ };
65
+ }
66
+
67
+ function assertStorageSemantics(storage: SqlStorage): void {
68
+ const semanticErrors = validateStorageSemantics(storage);
69
+ if (semanticErrors.length > 0) {
70
+ throw new Error(`Contract semantic validation failed: ${semanticErrors.join('; ')}`);
71
+ }
72
+ }
73
+
74
+ function assertKnownTargetModel(
75
+ modelsByName: ReadonlyMap<string, ModelNode>,
76
+ sourceModelName: string,
77
+ targetModelName: string,
78
+ context: string,
79
+ ): ModelNode {
80
+ const targetModel = modelsByName.get(targetModelName);
81
+ if (!targetModel) {
82
+ throw new Error(
83
+ `${context} on model "${sourceModelName}" references unknown model "${targetModelName}"`,
84
+ );
85
+ }
86
+ return targetModel;
87
+ }
88
+
89
+ function assertTargetTableMatches(
90
+ sourceModelName: string,
91
+ targetModel: ModelNode,
92
+ referencedTableName: string,
93
+ context: string,
94
+ ): void {
95
+ if (targetModel.tableName !== referencedTableName) {
96
+ throw new Error(
97
+ `${context} on model "${sourceModelName}" references table "${referencedTableName}" but model "${targetModel.modelName}" maps to "${targetModel.tableName}"`,
98
+ );
99
+ }
100
+ }
101
+
102
+ function isValueObjectField(
103
+ field: FieldNode | ValueObjectFieldNode,
104
+ ): field is ValueObjectFieldNode {
105
+ return 'valueObjectName' in field;
106
+ }
107
+
108
+ const JSONB_CODEC_ID = 'pg/jsonb@1';
109
+ const JSONB_NATIVE_TYPE = 'jsonb';
110
+
111
+ function buildStorageColumn(
112
+ field: FieldNode | ValueObjectFieldNode,
113
+ codecLookup?: CodecLookup,
114
+ ): StorageColumn {
115
+ if (isValueObjectField(field)) {
116
+ const encodedDefault =
117
+ field.default !== undefined
118
+ ? encodeColumnDefault(field.default, JSONB_CODEC_ID, codecLookup)
119
+ : undefined;
120
+
121
+ return {
122
+ nativeType: JSONB_NATIVE_TYPE,
123
+ codecId: JSONB_CODEC_ID,
124
+ nullable: field.nullable,
125
+ ...ifDefined('default', encodedDefault),
126
+ };
127
+ }
128
+
129
+ if (field.many) {
130
+ return {
131
+ nativeType: JSONB_NATIVE_TYPE,
132
+ codecId: JSONB_CODEC_ID,
133
+ nullable: field.nullable,
134
+ };
135
+ }
136
+
137
+ const codecId = field.descriptor.codecId;
138
+ const encodedDefault =
139
+ field.default !== undefined
140
+ ? encodeColumnDefault(field.default, codecId, codecLookup)
141
+ : undefined;
142
+
143
+ return {
144
+ nativeType: field.descriptor.nativeType,
145
+ codecId,
146
+ nullable: field.nullable,
147
+ ...ifDefined('typeParams', field.descriptor.typeParams),
148
+ ...ifDefined('default', encodedDefault),
149
+ ...ifDefined('typeRef', field.descriptor.typeRef),
150
+ };
151
+ }
152
+
153
+ function buildDomainField(
154
+ field: FieldNode | ValueObjectFieldNode,
155
+ column: StorageColumn,
156
+ ): ContractField {
157
+ if (isValueObjectField(field)) {
158
+ return {
159
+ type: { kind: 'valueObject', name: field.valueObjectName },
160
+ nullable: field.nullable,
161
+ ...(field.many ? { many: true } : {}),
162
+ };
163
+ }
164
+
165
+ return {
166
+ type: {
167
+ kind: 'scalar',
168
+ codecId: column.codecId,
169
+ ...ifDefined('typeParams', column.typeParams),
170
+ },
171
+ nullable: column.nullable,
172
+ ...(field.many ? { many: true } : {}),
173
+ };
174
+ }
175
+
176
+ export function buildSqlContractFromDefinition(
177
+ definition: ContractDefinition,
178
+ codecLookup?: CodecLookup,
179
+ ): Contract<SqlStorage> {
180
+ const target = definition.target.targetId;
181
+ const targetFamily = 'sql';
182
+ const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
183
+
184
+ const storageTables: Record<string, StorageTable> = {};
185
+ const executionDefaults: ExecutionMutationDefault[] = [];
186
+ const models: Record<string, ContractModel> = {};
187
+ const roots: Record<string, string> = {};
188
+
189
+ for (const semanticModel of definition.models) {
190
+ const tableName = semanticModel.tableName;
191
+ roots[tableName] = semanticModel.modelName;
192
+
193
+ // --- Build storage table ---
194
+
195
+ const columns: Record<string, StorageColumn> = {};
196
+ const fieldToColumn: Record<string, string> = {};
197
+ const domainFields: Record<string, ContractField> = {};
198
+ const domainFieldRefs: Record<string, DomainFieldRef> = {};
199
+
200
+ for (const field of semanticModel.fields) {
201
+ if (field.executionDefault) {
202
+ if (field.default !== undefined) {
203
+ throw new Error(
204
+ `Field "${semanticModel.modelName}.${field.fieldName}" cannot define both default and executionDefault.`,
205
+ );
206
+ }
207
+ if (field.nullable) {
208
+ throw new Error(
209
+ `Field "${semanticModel.modelName}.${field.fieldName}" cannot be nullable when executionDefault is present.`,
210
+ );
211
+ }
212
+ }
213
+
214
+ const column = buildStorageColumn(field, codecLookup);
215
+ columns[field.columnName] = column;
216
+ fieldToColumn[field.fieldName] = field.columnName;
217
+
218
+ domainFields[field.fieldName] = buildDomainField(field, column);
219
+
220
+ if (isValueObjectField(field)) {
221
+ domainFieldRefs[field.fieldName] = {
222
+ kind: 'valueObject',
223
+ name: field.valueObjectName,
224
+ ...(field.many ? { many: true } : {}),
225
+ };
226
+ } else if (field.many) {
227
+ domainFieldRefs[field.fieldName] = { kind: 'scalar', many: true };
228
+ }
229
+
230
+ if ('executionDefault' in field && field.executionDefault) {
231
+ executionDefaults.push({
232
+ ref: { table: tableName, column: field.columnName },
233
+ onCreate: field.executionDefault as ExecutionMutationDefaultValue,
234
+ });
235
+ }
236
+ }
237
+
238
+ if (semanticModel.id) {
239
+ const fieldsByColumnName = new Map(
240
+ semanticModel.fields.map((field) => [field.columnName, field]),
241
+ );
242
+ for (const columnName of semanticModel.id.columns) {
243
+ const field = fieldsByColumnName.get(columnName);
244
+ if (field?.nullable) {
245
+ throw new Error(
246
+ `Model "${semanticModel.modelName}" uses nullable field "${field.fieldName}" in its identity.`,
247
+ );
248
+ }
249
+ }
250
+ }
251
+
252
+ const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
253
+ const targetModel = assertKnownTargetModel(
254
+ modelsByName,
255
+ semanticModel.modelName,
256
+ fk.references.model,
257
+ 'Foreign key',
258
+ );
259
+ assertTargetTableMatches(
260
+ semanticModel.modelName,
261
+ targetModel,
262
+ fk.references.table,
263
+ 'Foreign key',
264
+ );
265
+ return {
266
+ columns: fk.columns,
267
+ references: { table: fk.references.table, columns: fk.references.columns },
268
+ ...applyFkDefaults(
269
+ {
270
+ ...ifDefined('constraint', fk.constraint),
271
+ ...ifDefined('index', fk.index),
272
+ },
273
+ definition.foreignKeyDefaults,
274
+ ),
275
+ ...ifDefined('name', fk.name),
276
+ ...ifDefined('onDelete', fk.onDelete),
277
+ ...ifDefined('onUpdate', fk.onUpdate),
278
+ };
279
+ });
280
+
281
+ storageTables[tableName] = {
282
+ columns,
283
+ uniques: (semanticModel.uniques ?? []).map((u) => ({
284
+ columns: u.columns,
285
+ ...ifDefined('name', u.name),
286
+ })),
287
+ indexes: (semanticModel.indexes ?? []).map((i) => ({
288
+ columns: i.columns,
289
+ ...ifDefined('name', i.name),
290
+ ...ifDefined('using', i.using),
291
+ ...ifDefined('config', i.config),
292
+ })),
293
+ foreignKeys,
294
+ ...(semanticModel.id
295
+ ? {
296
+ primaryKey: {
297
+ columns: semanticModel.id.columns,
298
+ ...ifDefined('name', semanticModel.id.name),
299
+ },
300
+ }
301
+ : {}),
302
+ };
303
+
304
+ // --- Build contract model ---
305
+
306
+ const storageFields: Record<string, { readonly column: string }> = {};
307
+ for (const [fieldName, columnName] of Object.entries(fieldToColumn)) {
308
+ storageFields[fieldName] = { column: columnName };
309
+ }
310
+
311
+ const columnToField = new Map(
312
+ Object.entries(fieldToColumn).map(([field, col]) => [col, field]),
313
+ );
314
+ const modelRelations: Record<string, ContractRelation> = {};
315
+ for (const relation of semanticModel.relations ?? []) {
316
+ const targetModel = assertKnownTargetModel(
317
+ modelsByName,
318
+ semanticModel.modelName,
319
+ relation.toModel,
320
+ 'Relation',
321
+ );
322
+ assertTargetTableMatches(semanticModel.modelName, targetModel, relation.toTable, 'Relation');
323
+
324
+ if (relation.cardinality === 'N:M' && !relation.through) {
325
+ throw new Error(
326
+ `Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
327
+ );
328
+ }
329
+
330
+ const targetColumnToField = new Map(
331
+ targetModel.fields.map((f) => [f.columnName, f.fieldName]),
332
+ );
333
+
334
+ modelRelations[relation.fieldName] = {
335
+ to: relation.toModel,
336
+ // RelationDefinition.cardinality includes 'N:M' which isn't in
337
+ // ContractReferenceRelation yet — cast is needed until the contract
338
+ // type is extended to cover many-to-many.
339
+ cardinality: relation.cardinality as ContractRelation['cardinality'],
340
+ on: {
341
+ localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
342
+ targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col),
343
+ },
344
+ ...(relation.through
345
+ ? {
346
+ through: {
347
+ table: relation.through.table,
348
+ parentCols: relation.through.parentColumns,
349
+ childCols: relation.through.childColumns,
350
+ },
351
+ }
352
+ : undefined),
353
+ };
354
+ }
355
+
356
+ models[semanticModel.modelName] = {
357
+ storage: {
358
+ table: tableName,
359
+ fields: storageFields,
360
+ },
361
+ fields: domainFields,
362
+ relations: modelRelations,
363
+ };
364
+ }
365
+
366
+ // --- Assemble contract ---
367
+
368
+ const storageTypes = (definition.storageTypes ?? {}) as Record<string, StorageTypeInstance>;
369
+ const storageWithoutHash = {
370
+ tables: storageTables,
371
+ types: storageTypes,
372
+ };
373
+ const storageHash: StorageHashBase<string> = definition.storageHash
374
+ ? coreHash(definition.storageHash)
375
+ : computeStorageHash({ target, targetFamily, storage: storageWithoutHash });
376
+ const storage: SqlStorage = { ...storageWithoutHash, storageHash };
377
+
378
+ const executionSection =
379
+ executionDefaults.length > 0
380
+ ? {
381
+ mutations: {
382
+ defaults: executionDefaults.sort((a, b) => {
383
+ const tableCompare = a.ref.table.localeCompare(b.ref.table);
384
+ if (tableCompare !== 0) {
385
+ return tableCompare;
386
+ }
387
+ return a.ref.column.localeCompare(b.ref.column);
388
+ }),
389
+ },
390
+ }
391
+ : undefined;
392
+
393
+ const extensionNamespaces = definition.extensionPacks
394
+ ? Object.values(definition.extensionPacks).map((pack) => pack.id)
395
+ : undefined;
396
+
397
+ const extensionPacks: Record<string, unknown> = { ...(definition.extensionPacks || {}) };
398
+ if (extensionNamespaces) {
399
+ for (const namespace of extensionNamespaces) {
400
+ if (!Object.hasOwn(extensionPacks, namespace)) {
401
+ extensionPacks[namespace] = {};
402
+ }
403
+ }
404
+ }
405
+
406
+ const capabilities: Record<string, Record<string, boolean>> = definition.capabilities || {};
407
+ const profileHash = computeProfileHash({ target, targetFamily, capabilities });
408
+
409
+ const executionWithHash = executionSection
410
+ ? {
411
+ ...executionSection,
412
+ executionHash: computeExecutionHash({ target, targetFamily, execution: executionSection }),
413
+ }
414
+ : undefined;
415
+
416
+ const valueObjects: Record<string, ContractValueObject> | undefined =
417
+ definition.valueObjects && definition.valueObjects.length > 0
418
+ ? Object.fromEntries(
419
+ definition.valueObjects.map((vo) => [
420
+ vo.name,
421
+ {
422
+ fields: Object.fromEntries(
423
+ vo.fields.map((f) => [
424
+ f.fieldName,
425
+ isValueObjectField(f)
426
+ ? {
427
+ type: { kind: 'valueObject' as const, name: f.valueObjectName },
428
+ nullable: f.nullable,
429
+ ...(f.many ? { many: true } : {}),
430
+ }
431
+ : {
432
+ type: {
433
+ kind: 'scalar' as const,
434
+ codecId: f.descriptor.codecId,
435
+ ...ifDefined('typeParams', f.descriptor.typeParams),
436
+ },
437
+ nullable: f.nullable,
438
+ },
439
+ ]),
440
+ ),
441
+ },
442
+ ]),
443
+ )
444
+ : undefined;
445
+
446
+ const contract: Contract<SqlStorage> = {
447
+ target,
448
+ targetFamily,
449
+ models,
450
+ roots,
451
+ storage,
452
+ ...(executionWithHash ? { execution: executionWithHash } : {}),
453
+ ...ifDefined('valueObjects', valueObjects),
454
+ extensionPacks,
455
+ capabilities,
456
+ profileHash,
457
+ meta: {},
458
+ };
459
+
460
+ assertStorageSemantics(contract.storage);
461
+
462
+ return contract;
463
+ }