@prisma-next/sql-contract-ts 0.12.0-dev.9 → 0.13.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/README.md +8 -0
- package/dist/{build-contract-BCYW3_wE.mjs → build-contract-C-x2pfu4.mjs} +214 -76
- package/dist/build-contract-C-x2pfu4.mjs.map +1 -0
- package/dist/config-types.d.mts +7 -3
- package/dist/config-types.d.mts.map +1 -1
- package/dist/config-types.mjs +12 -9
- package/dist/config-types.mjs.map +1 -1
- package/dist/contract-builder.d.mts +319 -23
- package/dist/contract-builder.d.mts.map +1 -1
- package/dist/contract-builder.mjs +280 -44
- package/dist/contract-builder.mjs.map +1 -1
- package/package.json +13 -13
- package/schemas/data-contract-sql-v1.json +31 -0
- package/src/build-contract.ts +340 -95
- package/src/config-types.ts +24 -6
- package/src/contract-builder.ts +137 -16
- package/src/contract-definition.ts +57 -3
- package/src/contract-dsl.ts +346 -18
- package/src/contract-lowering.ts +188 -15
- package/src/contract-types.ts +18 -21
- package/src/enum-type.ts +236 -0
- package/src/exports/contract-builder.ts +5 -0
- package/dist/build-contract-BCYW3_wE.mjs.map +0 -1
package/src/build-contract.ts
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
type ColumnDefault,
|
|
9
9
|
type ColumnDefaultLiteralInputValue,
|
|
10
10
|
type Contract,
|
|
11
|
+
type ContractEnum,
|
|
11
12
|
type ContractField,
|
|
12
13
|
type ContractModel,
|
|
13
14
|
type ContractRelation,
|
|
15
|
+
type ContractRelationThrough,
|
|
14
16
|
type ContractValueObject,
|
|
15
17
|
type CrossReference,
|
|
16
18
|
coreHash,
|
|
@@ -18,6 +20,7 @@ import {
|
|
|
18
20
|
type ExecutionMutationDefault,
|
|
19
21
|
type JsonValue,
|
|
20
22
|
type StorageHashBase,
|
|
23
|
+
type ValueSetRef,
|
|
21
24
|
} from '@prisma-next/contract/types';
|
|
22
25
|
import { type CapabilityMatrix, mergeCapabilityMatrices } from '@prisma-next/contract-authoring';
|
|
23
26
|
import type { CodecLookup } from '@prisma-next/framework-components/codec';
|
|
@@ -32,6 +35,7 @@ import {
|
|
|
32
35
|
import {
|
|
33
36
|
applyFkDefaults,
|
|
34
37
|
buildSqlNamespace,
|
|
38
|
+
type CheckConstraintInput,
|
|
35
39
|
isPostgresEnumStorageEntry,
|
|
36
40
|
type PostgresEnumStorageEntry,
|
|
37
41
|
type SqlNamespaceTablesInput,
|
|
@@ -41,6 +45,7 @@ import {
|
|
|
41
45
|
StorageTable,
|
|
42
46
|
type StorageTableInput,
|
|
43
47
|
type StorageTypeInstance,
|
|
48
|
+
type StorageValueSetInput,
|
|
44
49
|
toStorageTypeInstance,
|
|
45
50
|
} from '@prisma-next/sql-contract/types';
|
|
46
51
|
import { validateStorageSemantics } from '@prisma-next/sql-contract/validators';
|
|
@@ -50,6 +55,7 @@ import type {
|
|
|
50
55
|
ContractDefinition,
|
|
51
56
|
FieldNode,
|
|
52
57
|
ModelNode,
|
|
58
|
+
RelationNode,
|
|
53
59
|
ValueObjectFieldNode,
|
|
54
60
|
} from './contract-definition';
|
|
55
61
|
|
|
@@ -118,14 +124,23 @@ function assertStorageSemantics(
|
|
|
118
124
|
|
|
119
125
|
function assertKnownTargetModel(
|
|
120
126
|
modelsByName: ReadonlyMap<string, ModelNode>,
|
|
127
|
+
modelsByCoordinate: ReadonlyMap<string, ModelNode>,
|
|
121
128
|
sourceModelName: string,
|
|
122
129
|
targetModelName: string,
|
|
130
|
+
targetNamespaceId: string | undefined,
|
|
123
131
|
context: string,
|
|
124
132
|
): ModelNode {
|
|
125
|
-
const targetModel =
|
|
133
|
+
const targetModel =
|
|
134
|
+
targetNamespaceId !== undefined && targetNamespaceId.length > 0
|
|
135
|
+
? modelsByCoordinate.get(`${targetNamespaceId}:${targetModelName}`)
|
|
136
|
+
: modelsByName.get(targetModelName);
|
|
126
137
|
if (!targetModel) {
|
|
138
|
+
const qualified =
|
|
139
|
+
targetNamespaceId !== undefined && targetNamespaceId.length > 0
|
|
140
|
+
? `${targetNamespaceId}.${targetModelName}`
|
|
141
|
+
: targetModelName;
|
|
127
142
|
throw new Error(
|
|
128
|
-
`${context} on model "${sourceModelName}" references unknown model "${
|
|
143
|
+
`${context} on model "${sourceModelName}" references unknown model "${qualified}"`,
|
|
129
144
|
);
|
|
130
145
|
}
|
|
131
146
|
return targetModel;
|
|
@@ -156,16 +171,54 @@ const JSONB_NATIVE_TYPE = 'jsonb';
|
|
|
156
171
|
function resolveModelNamespaceId(
|
|
157
172
|
model: ModelNode,
|
|
158
173
|
modelNameToNamespaceId: ReadonlyMap<string, string>,
|
|
159
|
-
|
|
174
|
+
defaultNamespaceId: string,
|
|
160
175
|
): string {
|
|
161
176
|
if (model.namespaceId !== undefined && model.namespaceId.length > 0) {
|
|
162
177
|
return model.namespaceId;
|
|
163
178
|
}
|
|
164
|
-
return modelNameToNamespaceId.get(model.modelName) ??
|
|
179
|
+
return modelNameToNamespaceId.get(model.modelName) ?? defaultNamespaceId;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildThroughDescriptor(
|
|
183
|
+
through: NonNullable<RelationNode['through']>,
|
|
184
|
+
tableNamespaceByName: ReadonlyMap<string, string>,
|
|
185
|
+
targetModel: ModelNode,
|
|
186
|
+
modelName: string,
|
|
187
|
+
fieldName: string,
|
|
188
|
+
): ContractRelationThrough {
|
|
189
|
+
const namespaceId = tableNamespaceByName.get(through.table);
|
|
190
|
+
if (namespaceId === undefined) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`buildSqlContractFromDefinition: junction table "${through.table}" for relation "${modelName}.${fieldName}" is not a declared model.`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
table: through.table,
|
|
198
|
+
namespaceId,
|
|
199
|
+
parentColumns: through.parentColumns,
|
|
200
|
+
childColumns: through.childColumns,
|
|
201
|
+
targetColumns: targetColumnsForJunction(targetModel, fieldName),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function targetColumnsForJunction(targetModel: ModelNode, fieldName: string): readonly string[] {
|
|
206
|
+
const primaryKeyColumns = targetModel.id?.columns;
|
|
207
|
+
if (primaryKeyColumns && primaryKeyColumns.length > 0) {
|
|
208
|
+
return primaryKeyColumns;
|
|
209
|
+
}
|
|
210
|
+
const firstUnique = targetModel.uniques?.find((u) => u.columns.length > 0);
|
|
211
|
+
if (firstUnique) {
|
|
212
|
+
return firstUnique.columns;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(
|
|
215
|
+
`M:N target model "${targetModel.modelName}" (relation field "${fieldName}") has no primary id or unique key to derive junction targetColumns.`,
|
|
216
|
+
);
|
|
165
217
|
}
|
|
166
218
|
|
|
167
219
|
function buildStorageColumn(
|
|
168
220
|
field: FieldNode | ValueObjectFieldNode,
|
|
221
|
+
storageValueSetRef: ValueSetRef | undefined,
|
|
169
222
|
codecLookup?: CodecLookup,
|
|
170
223
|
): StorageColumn {
|
|
171
224
|
if (isValueObjectField(field)) {
|
|
@@ -203,12 +256,14 @@ function buildStorageColumn(
|
|
|
203
256
|
...ifDefined('typeParams', field.descriptor.typeParams),
|
|
204
257
|
...ifDefined('default', encodedDefault),
|
|
205
258
|
...ifDefined('typeRef', field.descriptor.typeRef),
|
|
259
|
+
...ifDefined('valueSet', storageValueSetRef),
|
|
206
260
|
};
|
|
207
261
|
}
|
|
208
262
|
|
|
209
263
|
function buildDomainField(
|
|
210
264
|
field: FieldNode | ValueObjectFieldNode,
|
|
211
265
|
column: StorageColumn,
|
|
266
|
+
domainValueSetRef: ValueSetRef | undefined,
|
|
212
267
|
): ContractField {
|
|
213
268
|
if (isValueObjectField(field)) {
|
|
214
269
|
return {
|
|
@@ -226,12 +281,13 @@ function buildDomainField(
|
|
|
226
281
|
},
|
|
227
282
|
nullable: column.nullable,
|
|
228
283
|
...(field.many ? { many: true } : {}),
|
|
284
|
+
...ifDefined('valueSet', domainValueSetRef),
|
|
229
285
|
};
|
|
230
286
|
}
|
|
231
287
|
|
|
232
288
|
function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): Set<string> {
|
|
233
289
|
const ids = new Set<string>();
|
|
234
|
-
ids.add(
|
|
290
|
+
ids.add(definition.target.defaultNamespaceId);
|
|
235
291
|
for (const id of definition.namespaces ?? []) {
|
|
236
292
|
if (id.length > 0) {
|
|
237
293
|
ids.add(id);
|
|
@@ -245,22 +301,38 @@ function collectStorageNamespaceCoordinateIds(definition: ContractDefinition): S
|
|
|
245
301
|
return ids;
|
|
246
302
|
}
|
|
247
303
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
304
|
+
function ensureUnboundNamespaceSlot(
|
|
305
|
+
namespaces: SqlStorageInput['namespaces'],
|
|
306
|
+
createNamespace: ContractDefinition['createNamespace'],
|
|
307
|
+
): SqlStorageInput['namespaces'] {
|
|
308
|
+
if (Object.hasOwn(namespaces, UNBOUND_NAMESPACE_ID)) {
|
|
309
|
+
return namespaces;
|
|
310
|
+
}
|
|
311
|
+
const unboundInput: SqlNamespaceTablesInput = {
|
|
312
|
+
id: UNBOUND_NAMESPACE_ID,
|
|
313
|
+
entries: { table: {} },
|
|
314
|
+
};
|
|
315
|
+
const unbound = createNamespace ? createNamespace(unboundInput) : buildSqlNamespace(unboundInput);
|
|
316
|
+
return blindCast<
|
|
317
|
+
SqlStorageInput['namespaces'],
|
|
318
|
+
'createNamespace may return a target namespace concretion; the unbound slot matches SqlNamespace at runtime'
|
|
319
|
+
>({
|
|
320
|
+
[UNBOUND_NAMESPACE_ID]: unbound,
|
|
321
|
+
...namespaces,
|
|
322
|
+
});
|
|
253
323
|
}
|
|
254
324
|
|
|
325
|
+
const POSTGRES_ENUM_NAMESPACE_ID = 'public';
|
|
326
|
+
|
|
255
327
|
function partitionStorageTypesForTarget(
|
|
256
328
|
targetId: string,
|
|
257
329
|
types: Record<string, StorageTypeInstance | PostgresEnumStorageEntry>,
|
|
258
330
|
namespaceTypes?: Readonly<Record<string, Readonly<Record<string, PostgresEnumStorageEntry>>>>,
|
|
259
331
|
): {
|
|
260
|
-
readonly documentTypes: Record<string, StorageTypeInstance
|
|
332
|
+
readonly documentTypes: Record<string, StorageTypeInstance>;
|
|
261
333
|
readonly namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>>;
|
|
262
334
|
} {
|
|
263
|
-
const documentTypes: Record<string, StorageTypeInstance
|
|
335
|
+
const documentTypes: Record<string, StorageTypeInstance> = {};
|
|
264
336
|
const namespaceEnumTypesById: Record<string, Record<string, PostgresEnumStorageEntry>> = {};
|
|
265
337
|
for (const [name, entry] of Object.entries(types)) {
|
|
266
338
|
if (isPostgresEnumStorageEntry(entry)) {
|
|
@@ -304,24 +376,47 @@ export function buildSqlContractFromDefinition(
|
|
|
304
376
|
codecLookup?: CodecLookup,
|
|
305
377
|
): Contract<SqlStorage> {
|
|
306
378
|
const target = definition.target.targetId;
|
|
379
|
+
const defaultNamespaceId = definition.target.defaultNamespaceId;
|
|
307
380
|
const targetFamily = 'sql';
|
|
381
|
+
const resolveNamespaceId = (m: ModelNode): string =>
|
|
382
|
+
m.namespaceId !== undefined && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId;
|
|
308
383
|
const modelsByName = new Map(definition.models.map((m) => [m.modelName, m]));
|
|
384
|
+
const tableNamespaceByName = new Map(
|
|
385
|
+
definition.models.map((m) => [
|
|
386
|
+
m.tableName,
|
|
387
|
+
m.namespaceId !== undefined && m.namespaceId.length > 0 ? m.namespaceId : defaultNamespaceId,
|
|
388
|
+
]),
|
|
389
|
+
);
|
|
390
|
+
const modelsByCoordinate = new Map(
|
|
391
|
+
definition.models.map((m) => [`${resolveNamespaceId(m)}:${m.modelName}`, m]),
|
|
392
|
+
);
|
|
309
393
|
|
|
310
394
|
const tablesByNamespace: Record<string, Record<string, StorageTable>> = {};
|
|
311
|
-
const tableNameToNamespaceId = new Map<string, string>();
|
|
312
395
|
const modelNameToNamespaceId = new Map<string, string>();
|
|
313
396
|
const executionDefaults: ExecutionMutationDefault[] = [];
|
|
314
397
|
const modelsByNamespace: Record<string, Record<string, ContractModel>> = {};
|
|
315
|
-
const
|
|
398
|
+
const rootEntries: Array<{
|
|
399
|
+
readonly tableName: string;
|
|
400
|
+
readonly namespaceId: string;
|
|
401
|
+
readonly ref: CrossReference;
|
|
402
|
+
}> = [];
|
|
316
403
|
|
|
317
404
|
for (const semanticModel of definition.models) {
|
|
318
405
|
const tableName = semanticModel.tableName;
|
|
319
406
|
const namespaceId =
|
|
320
407
|
semanticModel.namespaceId !== undefined && semanticModel.namespaceId.length > 0
|
|
321
408
|
? semanticModel.namespaceId
|
|
322
|
-
:
|
|
409
|
+
: defaultNamespaceId;
|
|
323
410
|
modelNameToNamespaceId.set(semanticModel.modelName, namespaceId);
|
|
324
|
-
|
|
411
|
+
// STI variants share the base table; the base model already owns this
|
|
412
|
+
// table name and its root, so the variant contributes neither.
|
|
413
|
+
if (!semanticModel.sharesBaseTable) {
|
|
414
|
+
rootEntries.push({
|
|
415
|
+
tableName,
|
|
416
|
+
namespaceId,
|
|
417
|
+
ref: crossRef(semanticModel.modelName, namespaceId),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
325
420
|
|
|
326
421
|
// --- Build storage table ---
|
|
327
422
|
|
|
@@ -348,11 +443,34 @@ export function buildSqlContractFromDefinition(
|
|
|
348
443
|
}
|
|
349
444
|
}
|
|
350
445
|
|
|
351
|
-
const
|
|
446
|
+
const enumHandle = !isValueObjectField(field) ? field.enumTypeHandle : undefined;
|
|
447
|
+
// Authored enums are always registered under the contract's defaultNamespaceId
|
|
448
|
+
// (see the enum registration loop below), so refs must point there regardless
|
|
449
|
+
// of which namespace the consuming model lives in.
|
|
450
|
+
const storageValueSetRef: ValueSetRef | undefined =
|
|
451
|
+
enumHandle !== undefined
|
|
452
|
+
? {
|
|
453
|
+
plane: 'storage',
|
|
454
|
+
entityKind: 'value-set',
|
|
455
|
+
namespaceId: defaultNamespaceId,
|
|
456
|
+
name: enumHandle.enumName,
|
|
457
|
+
}
|
|
458
|
+
: undefined;
|
|
459
|
+
const domainValueSetRef: ValueSetRef | undefined =
|
|
460
|
+
enumHandle !== undefined
|
|
461
|
+
? {
|
|
462
|
+
plane: 'domain',
|
|
463
|
+
entityKind: 'enum',
|
|
464
|
+
namespaceId: defaultNamespaceId,
|
|
465
|
+
name: enumHandle.enumName,
|
|
466
|
+
}
|
|
467
|
+
: undefined;
|
|
468
|
+
|
|
469
|
+
const column = buildStorageColumn(field, storageValueSetRef, codecLookup);
|
|
352
470
|
columns[field.columnName] = column;
|
|
353
471
|
fieldToColumn[field.fieldName] = field.columnName;
|
|
354
472
|
|
|
355
|
-
domainFields[field.fieldName] = buildDomainField(field, column);
|
|
473
|
+
domainFields[field.fieldName] = buildDomainField(field, column, domainValueSetRef);
|
|
356
474
|
|
|
357
475
|
if (isValueObjectField(field)) {
|
|
358
476
|
domainFieldRefs[field.fieldName] = {
|
|
@@ -374,10 +492,37 @@ export function buildSqlContractFromDefinition(
|
|
|
374
492
|
}
|
|
375
493
|
|
|
376
494
|
const foreignKeys = (semanticModel.foreignKeys ?? []).map((fk) => {
|
|
495
|
+
if (fk.references.spaceId !== undefined) {
|
|
496
|
+
// Cross-space FK: the target lives in a different contract space.
|
|
497
|
+
// Skip local model lookup and carry the spaceId coordinate through.
|
|
498
|
+
const targetNamespaceId = fk.references.namespaceId ?? defaultNamespaceId;
|
|
499
|
+
return {
|
|
500
|
+
source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
|
|
501
|
+
target: {
|
|
502
|
+
namespaceId: asNamespaceId(targetNamespaceId),
|
|
503
|
+
tableName: fk.references.table,
|
|
504
|
+
columns: fk.references.columns,
|
|
505
|
+
spaceId: fk.references.spaceId,
|
|
506
|
+
},
|
|
507
|
+
...applyFkDefaults(
|
|
508
|
+
{
|
|
509
|
+
...ifDefined('constraint', fk.constraint),
|
|
510
|
+
...ifDefined('index', fk.index),
|
|
511
|
+
},
|
|
512
|
+
definition.foreignKeyDefaults,
|
|
513
|
+
),
|
|
514
|
+
...ifDefined('name', fk.name),
|
|
515
|
+
...ifDefined('onDelete', fk.onDelete),
|
|
516
|
+
...ifDefined('onUpdate', fk.onUpdate),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
377
520
|
const targetModel = assertKnownTargetModel(
|
|
378
521
|
modelsByName,
|
|
522
|
+
modelsByCoordinate,
|
|
379
523
|
semanticModel.modelName,
|
|
380
524
|
fk.references.model,
|
|
525
|
+
fk.references.namespaceId,
|
|
381
526
|
'Foreign key',
|
|
382
527
|
);
|
|
383
528
|
assertTargetTableMatches(
|
|
@@ -390,7 +535,7 @@ export function buildSqlContractFromDefinition(
|
|
|
390
535
|
fk.references.namespaceId ??
|
|
391
536
|
(targetModel.namespaceId !== undefined && targetModel.namespaceId.length > 0
|
|
392
537
|
? targetModel.namespaceId
|
|
393
|
-
:
|
|
538
|
+
: defaultNamespaceId);
|
|
394
539
|
return {
|
|
395
540
|
source: { namespaceId: asNamespaceId(namespaceId), tableName, columns: fk.columns },
|
|
396
541
|
target: {
|
|
@@ -411,48 +556,56 @@ export function buildSqlContractFromDefinition(
|
|
|
411
556
|
};
|
|
412
557
|
});
|
|
413
558
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
559
|
+
// STI variants share the base table: their columns are already
|
|
560
|
+
// materialised onto the base `ModelNode`, so the variant builds a domain
|
|
561
|
+
// model (below) but no storage table of its own.
|
|
562
|
+
if (!semanticModel.sharesBaseTable) {
|
|
563
|
+
const checksForTable: CheckConstraintInput[] = Object.entries(columns).flatMap(
|
|
564
|
+
([columnName, col]) => {
|
|
565
|
+
const valueSet = col.valueSet;
|
|
566
|
+
return valueSet === undefined
|
|
567
|
+
? []
|
|
568
|
+
: [{ name: `${tableName}_${columnName}_check`, column: columnName, valueSet }];
|
|
569
|
+
},
|
|
418
570
|
);
|
|
419
|
-
}
|
|
420
|
-
tableNameToNamespaceId.set(tableName, namespaceId);
|
|
421
|
-
|
|
422
|
-
const tableInput: StorageTableInput = {
|
|
423
|
-
columns,
|
|
424
|
-
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
425
|
-
columns: u.columns,
|
|
426
|
-
...ifDefined('name', u.name),
|
|
427
|
-
})),
|
|
428
|
-
indexes: (semanticModel.indexes ?? []).map((i) => ({
|
|
429
|
-
columns: i.columns,
|
|
430
|
-
...ifDefined('name', i.name),
|
|
431
|
-
...ifDefined('type', i.type),
|
|
432
|
-
...ifDefined('options', i.options),
|
|
433
|
-
})),
|
|
434
|
-
foreignKeys,
|
|
435
|
-
...(semanticModel.id
|
|
436
|
-
? {
|
|
437
|
-
primaryKey: {
|
|
438
|
-
columns: semanticModel.id.columns,
|
|
439
|
-
...ifDefined('name', semanticModel.id.name),
|
|
440
|
-
},
|
|
441
|
-
}
|
|
442
|
-
: {}),
|
|
443
|
-
};
|
|
444
571
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
572
|
+
const tableInput: StorageTableInput = {
|
|
573
|
+
columns,
|
|
574
|
+
...ifDefined('control', semanticModel.control),
|
|
575
|
+
uniques: (semanticModel.uniques ?? []).map((u) => ({
|
|
576
|
+
columns: u.columns,
|
|
577
|
+
...ifDefined('name', u.name),
|
|
578
|
+
})),
|
|
579
|
+
indexes: (semanticModel.indexes ?? []).map((i) => ({
|
|
580
|
+
columns: i.columns,
|
|
581
|
+
...ifDefined('name', i.name),
|
|
582
|
+
...ifDefined('type', i.type),
|
|
583
|
+
...ifDefined('options', i.options),
|
|
584
|
+
})),
|
|
585
|
+
foreignKeys,
|
|
586
|
+
...(semanticModel.id
|
|
587
|
+
? {
|
|
588
|
+
primaryKey: {
|
|
589
|
+
columns: semanticModel.id.columns,
|
|
590
|
+
...ifDefined('name', semanticModel.id.name),
|
|
591
|
+
},
|
|
592
|
+
}
|
|
593
|
+
: {}),
|
|
594
|
+
...(checksForTable.length > 0 ? { checks: checksForTable } : {}),
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
let nsTables = tablesByNamespace[namespaceId];
|
|
598
|
+
if (nsTables === undefined) {
|
|
599
|
+
nsTables = {};
|
|
600
|
+
tablesByNamespace[namespaceId] = nsTables;
|
|
601
|
+
}
|
|
602
|
+
if (nsTables[tableName] !== undefined) {
|
|
603
|
+
throw new Error(
|
|
604
|
+
`buildSqlContractFromDefinition: duplicate table "${tableName}" in namespace "${namespaceId}".`,
|
|
605
|
+
);
|
|
606
|
+
}
|
|
607
|
+
nsTables[tableName] = new StorageTable(tableInput);
|
|
454
608
|
}
|
|
455
|
-
nsTables[tableName] = new StorageTable(tableInput);
|
|
456
609
|
|
|
457
610
|
// --- Build contract model ---
|
|
458
611
|
|
|
@@ -466,47 +619,70 @@ export function buildSqlContractFromDefinition(
|
|
|
466
619
|
);
|
|
467
620
|
const modelRelations: Record<string, ContractRelation> = {};
|
|
468
621
|
for (const relation of semanticModel.relations ?? []) {
|
|
622
|
+
// Cross-space relations have `spaceId` set — the target model lives in
|
|
623
|
+
// a different contract space, so skip local model lookup and validation.
|
|
624
|
+
if (relation.spaceId !== undefined) {
|
|
625
|
+
const targetNamespaceId = relation.namespaceId ?? defaultNamespaceId;
|
|
626
|
+
modelRelations[relation.fieldName] = {
|
|
627
|
+
to: crossRef(relation.toModel, targetNamespaceId, relation.spaceId),
|
|
628
|
+
// Cross-space belongsTo relations are always N:1 (the FK-owning side).
|
|
629
|
+
cardinality: 'N:1',
|
|
630
|
+
on: {
|
|
631
|
+
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
632
|
+
// For cross-space targets the lowering carries field names directly
|
|
633
|
+
// (no fieldToColumn map available for the remote model).
|
|
634
|
+
targetFields: relation.on.childColumns,
|
|
635
|
+
},
|
|
636
|
+
};
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
|
|
469
640
|
const targetModel = assertKnownTargetModel(
|
|
470
641
|
modelsByName,
|
|
642
|
+
modelsByCoordinate,
|
|
471
643
|
semanticModel.modelName,
|
|
472
644
|
relation.toModel,
|
|
645
|
+
relation.toNamespaceId,
|
|
473
646
|
'Relation',
|
|
474
647
|
);
|
|
475
648
|
assertTargetTableMatches(semanticModel.modelName, targetModel, relation.toTable, 'Relation');
|
|
476
649
|
|
|
477
|
-
if (relation.cardinality === 'N:M' && !relation.through) {
|
|
478
|
-
throw new Error(
|
|
479
|
-
`Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
|
|
480
|
-
);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
650
|
const targetColumnToField = new Map(
|
|
484
651
|
targetModel.fields.map((f) => [f.columnName, f.fieldName]),
|
|
485
652
|
);
|
|
486
653
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
on: {
|
|
497
|
-
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
498
|
-
targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col),
|
|
499
|
-
},
|
|
500
|
-
...(relation.through
|
|
501
|
-
? {
|
|
502
|
-
through: {
|
|
503
|
-
table: relation.through.table,
|
|
504
|
-
parentCols: relation.through.parentColumns,
|
|
505
|
-
childCols: relation.through.childColumns,
|
|
506
|
-
},
|
|
507
|
-
}
|
|
508
|
-
: undefined),
|
|
654
|
+
const to = crossRef(
|
|
655
|
+
relation.toModel,
|
|
656
|
+
relation.toNamespaceId !== undefined && relation.toNamespaceId.length > 0
|
|
657
|
+
? relation.toNamespaceId
|
|
658
|
+
: resolveModelNamespaceId(targetModel, modelNameToNamespaceId, defaultNamespaceId),
|
|
659
|
+
);
|
|
660
|
+
const on = {
|
|
661
|
+
localFields: relation.on.parentColumns.map((col) => columnToField.get(col) ?? col),
|
|
662
|
+
targetFields: relation.on.childColumns.map((col) => targetColumnToField.get(col) ?? col),
|
|
509
663
|
};
|
|
664
|
+
|
|
665
|
+
if (relation.cardinality === 'N:M') {
|
|
666
|
+
if (!relation.through) {
|
|
667
|
+
throw new Error(
|
|
668
|
+
`Relation "${semanticModel.modelName}.${relation.fieldName}" with cardinality "N:M" requires through metadata`,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
modelRelations[relation.fieldName] = {
|
|
672
|
+
to,
|
|
673
|
+
cardinality: 'N:M',
|
|
674
|
+
on,
|
|
675
|
+
through: buildThroughDescriptor(
|
|
676
|
+
relation.through,
|
|
677
|
+
tableNamespaceByName,
|
|
678
|
+
targetModel,
|
|
679
|
+
semanticModel.modelName,
|
|
680
|
+
relation.fieldName,
|
|
681
|
+
),
|
|
682
|
+
};
|
|
683
|
+
} else {
|
|
684
|
+
modelRelations[relation.fieldName] = { to, cardinality: relation.cardinality, on };
|
|
685
|
+
}
|
|
510
686
|
}
|
|
511
687
|
|
|
512
688
|
let namespaceModels = modelsByNamespace[namespaceId];
|
|
@@ -517,6 +693,7 @@ export function buildSqlContractFromDefinition(
|
|
|
517
693
|
namespaceModels[semanticModel.modelName] = {
|
|
518
694
|
storage: {
|
|
519
695
|
table: tableName,
|
|
696
|
+
namespaceId,
|
|
520
697
|
fields: storageFields,
|
|
521
698
|
},
|
|
522
699
|
fields: domainFields,
|
|
@@ -526,6 +703,24 @@ export function buildSqlContractFromDefinition(
|
|
|
526
703
|
|
|
527
704
|
// --- Assemble contract ---
|
|
528
705
|
|
|
706
|
+
// Aggregate roots are keyed by bare storage table name. When two models in
|
|
707
|
+
// different namespaces map to the same bare table name, the bare key would
|
|
708
|
+
// collide (last write wins, silently dropping a root), so those entries fall
|
|
709
|
+
// back to a namespace-qualified key. Single-namespace contracts never
|
|
710
|
+
// collide and keep their bare keys unchanged.
|
|
711
|
+
const rootTableNameCounts = new Map<string, number>();
|
|
712
|
+
for (const entry of rootEntries) {
|
|
713
|
+
rootTableNameCounts.set(entry.tableName, (rootTableNameCounts.get(entry.tableName) ?? 0) + 1);
|
|
714
|
+
}
|
|
715
|
+
const roots: Record<string, CrossReference> = {};
|
|
716
|
+
for (const entry of rootEntries) {
|
|
717
|
+
const key =
|
|
718
|
+
(rootTableNameCounts.get(entry.tableName) ?? 0) > 1
|
|
719
|
+
? `${entry.namespaceId}.${entry.tableName}`
|
|
720
|
+
: entry.tableName;
|
|
721
|
+
roots[key] = entry.ref;
|
|
722
|
+
}
|
|
723
|
+
|
|
529
724
|
// Normalise raw codec-triple inputs to the `kind: 'codec-instance'`
|
|
530
725
|
// discriminator shape before hashing so the storageHash matches the
|
|
531
726
|
// persisted JSON envelope produced from the SqlStorage class instance
|
|
@@ -557,6 +752,39 @@ export function buildSqlContractFromDefinition(
|
|
|
557
752
|
for (const id of Object.keys(namespaceEnumTypesById)) {
|
|
558
753
|
namespaceCoordinateIds.add(id);
|
|
559
754
|
}
|
|
755
|
+
|
|
756
|
+
// Build per-namespace registries for `enumType()` handles.
|
|
757
|
+
// All authored enums target the contract's default namespace.
|
|
758
|
+
const domainEnumsByNs: Record<string, Record<string, ContractEnum>> = {};
|
|
759
|
+
const storageValueSetsByNs: Record<string, Record<string, StorageValueSetInput>> = {};
|
|
760
|
+
for (const [enumName, handle] of Object.entries(definition.enums ?? {})) {
|
|
761
|
+
if (enumName !== handle.enumName) {
|
|
762
|
+
throw new Error(
|
|
763
|
+
`enum declaration key "${enumName}" must match enumType name "${handle.enumName}". Aliases are not supported.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
const nsId = defaultNamespaceId;
|
|
767
|
+
let domainSlot = domainEnumsByNs[nsId];
|
|
768
|
+
if (domainSlot === undefined) {
|
|
769
|
+
domainSlot = {};
|
|
770
|
+
domainEnumsByNs[nsId] = domainSlot;
|
|
771
|
+
}
|
|
772
|
+
domainSlot[enumName] = {
|
|
773
|
+
codecId: handle.codecId,
|
|
774
|
+
members: handle.enumMembers,
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
let storageSlot = storageValueSetsByNs[nsId];
|
|
778
|
+
if (storageSlot === undefined) {
|
|
779
|
+
storageSlot = {};
|
|
780
|
+
storageValueSetsByNs[nsId] = storageSlot;
|
|
781
|
+
}
|
|
782
|
+
storageSlot[enumName] = {
|
|
783
|
+
kind: 'value-set',
|
|
784
|
+
values: handle.values,
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
|
|
560
788
|
const { createNamespace } = definition;
|
|
561
789
|
const namespaces = blindCast<
|
|
562
790
|
SqlStorageInput['namespaces'],
|
|
@@ -565,18 +793,26 @@ export function buildSqlContractFromDefinition(
|
|
|
565
793
|
Object.fromEntries(
|
|
566
794
|
[...namespaceCoordinateIds].sort().map((id) => {
|
|
567
795
|
const enumTypes = namespaceEnumTypesById[id];
|
|
796
|
+
const valueSetEntries = storageValueSetsByNs[id];
|
|
568
797
|
const nsInput: SqlNamespaceTablesInput = {
|
|
569
798
|
id,
|
|
570
|
-
|
|
571
|
-
|
|
799
|
+
entries: {
|
|
800
|
+
table: tablesByNamespace[id] ?? {},
|
|
801
|
+
...(valueSetEntries !== undefined && Object.keys(valueSetEntries).length > 0
|
|
802
|
+
? { valueSet: valueSetEntries }
|
|
803
|
+
: {}),
|
|
804
|
+
},
|
|
572
805
|
};
|
|
573
|
-
return [
|
|
806
|
+
return [
|
|
807
|
+
id,
|
|
808
|
+
createNamespace ? createNamespace(nsInput, enumTypes) : buildSqlNamespace(nsInput),
|
|
809
|
+
];
|
|
574
810
|
}),
|
|
575
811
|
),
|
|
576
812
|
);
|
|
577
813
|
const storageWithoutHash = {
|
|
578
814
|
...(Object.keys(documentTypes).length > 0 ? { types: documentTypes } : {}),
|
|
579
|
-
namespaces,
|
|
815
|
+
namespaces: ensureUnboundNamespaceSlot(namespaces, createNamespace),
|
|
580
816
|
};
|
|
581
817
|
const storageHash: StorageHashBase<string> = definition.storageHash
|
|
582
818
|
? coreHash(definition.storageHash)
|
|
@@ -672,7 +908,6 @@ export function buildSqlContractFromDefinition(
|
|
|
672
908
|
)
|
|
673
909
|
: undefined;
|
|
674
910
|
|
|
675
|
-
const defaultNamespaceId = defaultModelNamespaceId(target);
|
|
676
911
|
const domainNamespaceIds = new Set(Object.keys(modelsByNamespace));
|
|
677
912
|
if (domainNamespaceIds.size === 0) {
|
|
678
913
|
domainNamespaceIds.add(defaultNamespaceId);
|
|
@@ -680,13 +915,22 @@ export function buildSqlContractFromDefinition(
|
|
|
680
915
|
if (valueObjects !== undefined) {
|
|
681
916
|
domainNamespaceIds.add(defaultNamespaceId);
|
|
682
917
|
}
|
|
918
|
+
for (const nsId of Object.keys(domainEnumsByNs)) {
|
|
919
|
+
domainNamespaceIds.add(nsId);
|
|
920
|
+
}
|
|
683
921
|
const domainNamespaces = Object.fromEntries(
|
|
684
922
|
[...domainNamespaceIds].sort().map((namespaceId) => {
|
|
685
923
|
const modelsInNs = modelsByNamespace[namespaceId] ?? {};
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
924
|
+
const enumsInNs = domainEnumsByNs[namespaceId];
|
|
925
|
+
const namespaceSlice = {
|
|
926
|
+
models: modelsInNs,
|
|
927
|
+
...(namespaceId === defaultNamespaceId && valueObjects !== undefined
|
|
928
|
+
? { valueObjects }
|
|
929
|
+
: {}),
|
|
930
|
+
...(enumsInNs !== undefined && Object.keys(enumsInNs).length > 0
|
|
931
|
+
? { enum: enumsInNs }
|
|
932
|
+
: {}),
|
|
933
|
+
};
|
|
690
934
|
return [namespaceId, namespaceSlice];
|
|
691
935
|
}),
|
|
692
936
|
);
|
|
@@ -694,6 +938,7 @@ export function buildSqlContractFromDefinition(
|
|
|
694
938
|
const contract: Contract<SqlStorage> = {
|
|
695
939
|
target,
|
|
696
940
|
targetFamily,
|
|
941
|
+
...ifDefined('defaultControlPolicy', definition.defaultControlPolicy),
|
|
697
942
|
domain: { namespaces: domainNamespaces },
|
|
698
943
|
roots,
|
|
699
944
|
storage,
|