@prisma-next/sql-contract 0.12.0 → 0.13.0-dev.2
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/dist/canonicalization-hooks.d.mts.map +1 -1
- package/dist/canonicalization-hooks.mjs +10 -11
- package/dist/canonicalization-hooks.mjs.map +1 -1
- package/dist/factories.d.mts +4 -2
- package/dist/factories.d.mts.map +1 -1
- package/dist/factories.mjs +3 -2
- package/dist/factories.mjs.map +1 -1
- package/dist/foreign-key-BATxB95l.d.mts +121 -0
- package/dist/foreign-key-BATxB95l.d.mts.map +1 -0
- package/dist/index-type-validation.d.mts +2 -2
- package/dist/index-type-validation.mjs +1 -1
- package/dist/index-type-validation.mjs.map +1 -1
- package/dist/{index-types-B1cf5N0F.d.mts → index-types-Czsyu7Iw.d.mts} +1 -1
- package/dist/{index-types-B1cf5N0F.d.mts.map → index-types-Czsyu7Iw.d.mts.map} +1 -1
- package/dist/index-types.d.mts +1 -1
- package/dist/pack-types.d.mts +1 -1
- package/dist/referential-action-sql.d.mts +12 -0
- package/dist/referential-action-sql.d.mts.map +1 -0
- package/dist/referential-action-sql.mjs +17 -0
- package/dist/referential-action-sql.mjs.map +1 -0
- package/dist/resolve-storage-table.d.mts +20 -0
- package/dist/resolve-storage-table.d.mts.map +1 -0
- package/dist/resolve-storage-table.mjs +42 -0
- package/dist/resolve-storage-table.mjs.map +1 -0
- package/dist/sql-storage-CXf9xjAL.d.mts +314 -0
- package/dist/sql-storage-CXf9xjAL.d.mts.map +1 -0
- package/dist/types-DEnWD3xB.d.mts +208 -0
- package/dist/types-DEnWD3xB.d.mts.map +1 -0
- package/dist/{types-DPkj4y3_.mjs → types-DqhaAjCH.mjs} +109 -28
- package/dist/types-DqhaAjCH.mjs.map +1 -0
- package/dist/types.d.mts +4 -2
- package/dist/types.mjs +2 -2
- package/dist/validators.d.mts +51 -14
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +116 -32
- package/dist/validators.mjs.map +1 -1
- package/package.json +11 -9
- package/src/canonicalization-hooks.ts +5 -6
- package/src/exports/referential-action-sql.ts +1 -0
- package/src/exports/resolve-storage-table.ts +1 -0
- package/src/exports/types.ts +6 -0
- package/src/factories.ts +2 -1
- package/src/index-type-validation.ts +1 -1
- package/src/ir/build-sql-namespace.ts +33 -19
- package/src/ir/check-constraint.ts +42 -0
- package/src/ir/foreign-key-reference.ts +23 -0
- package/src/ir/postgres-enum-storage-entry.ts +2 -0
- package/src/ir/sql-storage.ts +53 -50
- package/src/ir/sql-unbound-namespace.ts +11 -4
- package/src/ir/storage-column.ts +7 -1
- package/src/ir/storage-table.ts +10 -0
- package/src/ir/storage-type-instance.ts +5 -3
- package/src/ir/storage-value-set.ts +42 -0
- package/src/referential-action-sql.ts +14 -0
- package/src/resolve-storage-table.ts +61 -0
- package/src/types.ts +13 -0
- package/src/validators.ts +156 -46
- package/dist/types-ChlHcJCu.d.mts +0 -508
- package/dist/types-ChlHcJCu.d.mts.map +0 -1
- package/dist/types-DPkj4y3_.mjs.map +0 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SqlNamespace, SqlStorage } from './ir/sql-storage';
|
|
2
|
+
import type { StorageTable } from './ir/storage-table';
|
|
3
|
+
|
|
4
|
+
export interface ResolvedStorageTable {
|
|
5
|
+
readonly namespaceId: string;
|
|
6
|
+
readonly table: StorageTable;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function tableInNamespace(
|
|
10
|
+
namespace: SqlNamespace | undefined,
|
|
11
|
+
tableName: string,
|
|
12
|
+
): StorageTable | undefined {
|
|
13
|
+
if (namespace === undefined) {
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const tables = namespace.entries.table;
|
|
17
|
+
if (!Object.hasOwn(tables, tableName)) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
return tables[tableName];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve a bare storage table name to its namespace coordinate and table IR.
|
|
25
|
+
*
|
|
26
|
+
* When `namespaceId` is supplied, the table is resolved strictly within that
|
|
27
|
+
* namespace (no scan). When omitted, a bare name unique across namespaces
|
|
28
|
+
* resolves to its sole namespace; a bare name declared in more than one
|
|
29
|
+
* namespace throws a fail-fast diagnostic naming the candidate namespaces
|
|
30
|
+
* rather than silently selecting the first match.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveStorageTable(
|
|
33
|
+
storage: SqlStorage,
|
|
34
|
+
tableName: string,
|
|
35
|
+
namespaceId?: string,
|
|
36
|
+
): ResolvedStorageTable | undefined {
|
|
37
|
+
if (namespaceId !== undefined) {
|
|
38
|
+
const table = tableInNamespace(storage.namespaces[namespaceId], tableName);
|
|
39
|
+
return table === undefined ? undefined : { namespaceId, table };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const matches: ResolvedStorageTable[] = [];
|
|
43
|
+
for (const candidateNamespaceId of Object.keys(storage.namespaces)) {
|
|
44
|
+
const table = tableInNamespace(storage.namespaces[candidateNamespaceId], tableName);
|
|
45
|
+
if (table !== undefined) {
|
|
46
|
+
matches.push({ namespaceId: candidateNamespaceId, table });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (matches.length > 1) {
|
|
51
|
+
const candidates = matches
|
|
52
|
+
.map((match) => match.namespaceId)
|
|
53
|
+
.sort()
|
|
54
|
+
.join(', ');
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Storage table "${tableName}" is ambiguous across namespaces [${candidates}]; qualify it with a namespace coordinate.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return matches[0];
|
|
61
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import type { CodecTrait } from '@prisma-next/framework-components/codec';
|
|
2
|
+
import type { ControlDriverInstance } from '@prisma-next/framework-components/control';
|
|
2
3
|
import type { ReferentialAction } from './ir/foreign-key';
|
|
3
4
|
|
|
5
|
+
export interface SqlControlDriverInstance<T extends string = string>
|
|
6
|
+
extends ControlDriverInstance<'sql', T> {
|
|
7
|
+
query<Row = Record<string, unknown>>(
|
|
8
|
+
sql: string,
|
|
9
|
+
params?: readonly unknown[],
|
|
10
|
+
): Promise<{ readonly rows: Row[] }>;
|
|
11
|
+
}
|
|
12
|
+
|
|
4
13
|
export { buildSqlNamespace, buildSqlNamespaceMap } from './ir/build-sql-namespace';
|
|
14
|
+
export { CheckConstraint, type CheckConstraintInput } from './ir/check-constraint';
|
|
5
15
|
export {
|
|
6
16
|
ForeignKey,
|
|
7
17
|
type ForeignKeyInput,
|
|
@@ -24,6 +34,7 @@ export {
|
|
|
24
34
|
SqlStorage,
|
|
25
35
|
type SqlStorageInput,
|
|
26
36
|
type SqlStorageTypeEntry,
|
|
37
|
+
storageTableAt,
|
|
27
38
|
} from './ir/sql-storage';
|
|
28
39
|
export { SqlUnboundNamespace } from './ir/sql-unbound-namespace';
|
|
29
40
|
export { StorageColumn, type StorageColumnInput } from './ir/storage-column';
|
|
@@ -35,6 +46,7 @@ export {
|
|
|
35
46
|
type StorageTypeInstanceInput,
|
|
36
47
|
toStorageTypeInstance,
|
|
37
48
|
} from './ir/storage-type-instance';
|
|
49
|
+
export { StorageValueSet, type StorageValueSetInput } from './ir/storage-value-set';
|
|
38
50
|
export {
|
|
39
51
|
UniqueConstraint,
|
|
40
52
|
type UniqueConstraintInput,
|
|
@@ -54,6 +66,7 @@ export type SqlModelFieldStorage = {
|
|
|
54
66
|
|
|
55
67
|
export type SqlModelStorage = {
|
|
56
68
|
readonly table: string;
|
|
69
|
+
readonly namespaceId: string;
|
|
57
70
|
readonly fields: Record<string, SqlModelFieldStorage>;
|
|
58
71
|
};
|
|
59
72
|
|
package/src/validators.ts
CHANGED
|
@@ -33,6 +33,7 @@ type ColumnDefaultFunction = { readonly kind: 'function'; readonly expression: s
|
|
|
33
33
|
const literalKindSchema = type("'literal'");
|
|
34
34
|
const functionKindSchema = type("'function'");
|
|
35
35
|
const generatorKindSchema = type("'generator'");
|
|
36
|
+
const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
|
|
36
37
|
const generatorIdSchema = type('string').narrow((value, ctx) => {
|
|
37
38
|
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe('a flat generator id');
|
|
38
39
|
});
|
|
@@ -76,6 +77,14 @@ const ExecutionSchema = type({
|
|
|
76
77
|
},
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
const ValueSetRefSchema = type({
|
|
81
|
+
plane: "'domain' | 'storage'",
|
|
82
|
+
namespaceId: 'string',
|
|
83
|
+
entityKind: "'enum' | 'value-set'",
|
|
84
|
+
name: 'string',
|
|
85
|
+
'spaceId?': 'string',
|
|
86
|
+
});
|
|
87
|
+
|
|
79
88
|
const StorageColumnSchema = type({
|
|
80
89
|
'+': 'reject',
|
|
81
90
|
nativeType: 'string',
|
|
@@ -84,6 +93,8 @@ const StorageColumnSchema = type({
|
|
|
84
93
|
'typeParams?': 'Record<string, unknown>',
|
|
85
94
|
'typeRef?': 'string',
|
|
86
95
|
'default?': ColumnDefaultSchema,
|
|
96
|
+
'control?': ControlPolicySchema,
|
|
97
|
+
'valueSet?': ValueSetRefSchema,
|
|
87
98
|
}).narrow((col, ctx) => {
|
|
88
99
|
if (col.typeParams !== undefined && col.typeRef !== undefined) {
|
|
89
100
|
return ctx.mustBe('a column with either typeParams or typeRef, not both');
|
|
@@ -103,11 +114,11 @@ const StorageTypeInstanceSchema = type
|
|
|
103
114
|
kind: "'codec-instance'",
|
|
104
115
|
codecId: 'string',
|
|
105
116
|
nativeType: 'string',
|
|
106
|
-
typeParams: 'Record<string, unknown>',
|
|
117
|
+
'typeParams?': 'Record<string, unknown>',
|
|
107
118
|
});
|
|
108
119
|
|
|
109
120
|
/**
|
|
110
|
-
* Postgres native enum entry under `storage.namespaces[namespaceId].
|
|
121
|
+
* Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
|
|
111
122
|
* Document-scoped `storage.types` carries codec aliases only
|
|
112
123
|
* (`DocumentScopedStorageTypeSchema`).
|
|
113
124
|
*/
|
|
@@ -116,11 +127,37 @@ const PostgresEnumTypeSchema = type({
|
|
|
116
127
|
'name?': 'string',
|
|
117
128
|
'nativeType?': 'string',
|
|
118
129
|
values: type.string.array().readonly(),
|
|
130
|
+
'control?': ControlPolicySchema,
|
|
119
131
|
});
|
|
120
132
|
|
|
121
133
|
/** Document-scoped `storage.types`: codec triples only. */
|
|
122
134
|
const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
|
|
123
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Storage value-set entry under `storage.namespaces[id].entries.valueSet[name]`.
|
|
138
|
+
* Carries a `kind: 'value-set'` discriminator (enumerable, survives JSON) and an
|
|
139
|
+
* ordered `values` array of codec-encoded permitted values.
|
|
140
|
+
*/
|
|
141
|
+
export const StorageValueSetSchema = type({
|
|
142
|
+
kind: "'value-set'",
|
|
143
|
+
values: type.string.array().readonly(),
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Domain enum entry under `domain.namespaces[id].enum[name]`.
|
|
148
|
+
* Carries the codec id and an ordered `members` array of `{name, value}` pairs.
|
|
149
|
+
*/
|
|
150
|
+
export const ContractEnumSchema = type({
|
|
151
|
+
'+': 'reject',
|
|
152
|
+
codecId: 'string',
|
|
153
|
+
members: type({
|
|
154
|
+
name: 'string',
|
|
155
|
+
value: 'string',
|
|
156
|
+
})
|
|
157
|
+
.array()
|
|
158
|
+
.readonly(),
|
|
159
|
+
});
|
|
160
|
+
|
|
124
161
|
const PrimaryKeySchema = type.declare<PrimaryKeyInput>().type({
|
|
125
162
|
columns: type.string.array().readonly(),
|
|
126
163
|
'name?': 'string',
|
|
@@ -143,6 +180,14 @@ export const ForeignKeyReferenceSchema = type({
|
|
|
143
180
|
namespaceId: 'string',
|
|
144
181
|
tableName: 'string',
|
|
145
182
|
columns: type.string.array().readonly(),
|
|
183
|
+
'spaceId?': 'string',
|
|
184
|
+
}) satisfies Type<ForeignKeyReferenceInput>;
|
|
185
|
+
|
|
186
|
+
export const ForeignKeySourceSchema = type({
|
|
187
|
+
'+': 'reject',
|
|
188
|
+
namespaceId: 'string',
|
|
189
|
+
tableName: 'string',
|
|
190
|
+
columns: type.string.array().readonly(),
|
|
146
191
|
}) satisfies Type<ForeignKeyReferenceInput>;
|
|
147
192
|
|
|
148
193
|
export const ReferentialActionSchema = type
|
|
@@ -150,7 +195,7 @@ export const ReferentialActionSchema = type
|
|
|
150
195
|
.type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
|
|
151
196
|
|
|
152
197
|
export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
|
|
153
|
-
source:
|
|
198
|
+
source: ForeignKeySourceSchema,
|
|
154
199
|
target: ForeignKeyReferenceSchema,
|
|
155
200
|
'name?': 'string',
|
|
156
201
|
'onDelete?': ReferentialActionSchema,
|
|
@@ -159,6 +204,13 @@ export const ForeignKeySchema = type.declare<ForeignKeyInput>().type({
|
|
|
159
204
|
index: 'boolean',
|
|
160
205
|
});
|
|
161
206
|
|
|
207
|
+
export const CheckConstraintSchema = type({
|
|
208
|
+
'+': 'reject',
|
|
209
|
+
name: 'string',
|
|
210
|
+
column: 'string',
|
|
211
|
+
valueSet: ValueSetRefSchema,
|
|
212
|
+
});
|
|
213
|
+
|
|
162
214
|
const StorageTableSchema = type({
|
|
163
215
|
'+': 'reject',
|
|
164
216
|
columns: type({ '[string]': StorageColumnSchema }),
|
|
@@ -166,6 +218,8 @@ const StorageTableSchema = type({
|
|
|
166
218
|
uniques: UniqueConstraintSchema.array().readonly(),
|
|
167
219
|
indexes: IndexSchema.array().readonly(),
|
|
168
220
|
foreignKeys: ForeignKeySchema.array().readonly(),
|
|
221
|
+
'control?': ControlPolicySchema,
|
|
222
|
+
'checks?': CheckConstraintSchema.array().readonly(),
|
|
169
223
|
});
|
|
170
224
|
|
|
171
225
|
/**
|
|
@@ -236,7 +290,7 @@ function namespaceSlotEntrySchema(
|
|
|
236
290
|
* Builds the per-namespace entry schema for `storage.namespaces[id]`.
|
|
237
291
|
* Pack-contributed `validatorSchema` fragments — keyed by the
|
|
238
292
|
* descriptor's `discriminator` — validate each entry by matching the
|
|
239
|
-
* entry's `kind` field on the `'
|
|
293
|
+
* entry's `kind` field on the `'entries.type'` slot.
|
|
240
294
|
*/
|
|
241
295
|
export function createNamespaceEntrySchema(
|
|
242
296
|
fragments?: ReadonlyMap<string, Type<unknown>>,
|
|
@@ -245,9 +299,13 @@ export function createNamespaceEntrySchema(
|
|
|
245
299
|
'+': 'reject',
|
|
246
300
|
id: 'string',
|
|
247
301
|
'kind?': 'string',
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
'
|
|
302
|
+
entries: type({
|
|
303
|
+
'+': 'reject',
|
|
304
|
+
'table?': type({ '[string]': StorageTableSchema }),
|
|
305
|
+
'type?': type({
|
|
306
|
+
'[string]': namespaceSlotEntrySchema(PostgresEnumTypeSchema, 'postgres-enum', fragments),
|
|
307
|
+
}),
|
|
308
|
+
'valueSet?': type({ '[string]': StorageValueSetSchema }),
|
|
251
309
|
}),
|
|
252
310
|
}) as Type<unknown>;
|
|
253
311
|
}
|
|
@@ -277,20 +335,22 @@ export function createSqlStorageSchema(
|
|
|
277
335
|
|
|
278
336
|
const StorageSchema = createSqlStorageSchema();
|
|
279
337
|
|
|
280
|
-
// SQL-specific namespace walk shape (`
|
|
281
|
-
//
|
|
282
|
-
// `
|
|
283
|
-
//
|
|
284
|
-
// envelope variants that lose class identity.
|
|
338
|
+
// SQL-specific namespace walk shape (`entries.table` is the SQL family's
|
|
339
|
+
// idiom). The wider `object` table value keeps this helper structurally
|
|
340
|
+
// compatible with `SqlNamespace` and JSON envelope variants that lose class
|
|
341
|
+
// identity.
|
|
285
342
|
type NamespacedStorageWalk = {
|
|
286
343
|
readonly namespaces: Readonly<
|
|
287
|
-
Record<
|
|
344
|
+
Record<
|
|
345
|
+
string,
|
|
346
|
+
Namespace & { readonly entries: { readonly table: Readonly<Record<string, object>> } }
|
|
347
|
+
>
|
|
288
348
|
>;
|
|
289
349
|
};
|
|
290
350
|
|
|
291
351
|
function eachStorageTable(storage: NamespacedStorageWalk) {
|
|
292
352
|
return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) =>
|
|
293
|
-
Object.entries(ns.
|
|
353
|
+
Object.entries(ns.entries.table).map(([tableName, table]) => ({
|
|
294
354
|
namespaceId,
|
|
295
355
|
tableName,
|
|
296
356
|
table,
|
|
@@ -298,16 +358,6 @@ function eachStorageTable(storage: NamespacedStorageWalk) {
|
|
|
298
358
|
);
|
|
299
359
|
}
|
|
300
360
|
|
|
301
|
-
function findStorageTableByTableName(storage: NamespacedStorageWalk, tableName: string): unknown {
|
|
302
|
-
for (const ns of Object.values(storage.namespaces)) {
|
|
303
|
-
const t = ns.tables?.[tableName];
|
|
304
|
-
if (t !== undefined) {
|
|
305
|
-
return t;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return undefined;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
361
|
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
312
362
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
313
363
|
}
|
|
@@ -353,6 +403,7 @@ const ModelFieldSchema = type({
|
|
|
353
403
|
type: ContractFieldTypeSchema,
|
|
354
404
|
'many?': 'true',
|
|
355
405
|
'dict?': 'true',
|
|
406
|
+
'valueSet?': ValueSetRefSchema,
|
|
356
407
|
});
|
|
357
408
|
|
|
358
409
|
const ModelStorageFieldSchema = type({
|
|
@@ -363,20 +414,44 @@ const ModelStorageFieldSchema = type({
|
|
|
363
414
|
|
|
364
415
|
const ModelStorageSchema = type({
|
|
365
416
|
table: 'string',
|
|
417
|
+
namespaceId: 'string',
|
|
366
418
|
fields: type({ '[string]': ModelStorageFieldSchema }),
|
|
367
419
|
});
|
|
368
420
|
|
|
369
|
-
const
|
|
421
|
+
const ContractRelationThroughSchema = type({
|
|
422
|
+
'+': 'reject',
|
|
423
|
+
table: 'string',
|
|
424
|
+
namespaceId: 'string',
|
|
425
|
+
parentColumns: type.string.array().readonly(),
|
|
426
|
+
childColumns: type.string.array().readonly(),
|
|
427
|
+
targetColumns: type.string.array().readonly(),
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
const ContractRelationOnSchema = type({
|
|
431
|
+
'+': 'reject',
|
|
432
|
+
localFields: type.string.array().readonly(),
|
|
433
|
+
targetFields: type.string.array().readonly(),
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
const ContractManyToManyRelationSchema = type({
|
|
437
|
+
'+': 'reject',
|
|
438
|
+
to: CrossReferenceSchema,
|
|
439
|
+
cardinality: "'N:M'",
|
|
440
|
+
on: ContractRelationOnSchema,
|
|
441
|
+
through: ContractRelationThroughSchema,
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
const ContractNonJunctionRelationSchema = type({
|
|
370
445
|
'+': 'reject',
|
|
371
446
|
to: CrossReferenceSchema,
|
|
372
447
|
cardinality: "'1:1' | '1:N' | 'N:1'",
|
|
373
|
-
on:
|
|
374
|
-
'+': 'reject',
|
|
375
|
-
localFields: type.string.array().readonly(),
|
|
376
|
-
targetFields: type.string.array().readonly(),
|
|
377
|
-
}),
|
|
448
|
+
on: ContractRelationOnSchema,
|
|
378
449
|
});
|
|
379
450
|
|
|
451
|
+
const ContractReferenceRelationSchema = ContractManyToManyRelationSchema.or(
|
|
452
|
+
ContractNonJunctionRelationSchema,
|
|
453
|
+
);
|
|
454
|
+
|
|
380
455
|
const ContractEmbedRelationSchema = type({
|
|
381
456
|
'+': 'reject',
|
|
382
457
|
to: CrossReferenceSchema,
|
|
@@ -417,12 +492,14 @@ export function createSqlContractSchema(
|
|
|
417
492
|
'capabilities?': 'Record<string, Record<string, boolean>>',
|
|
418
493
|
'extensionPacks?': 'Record<string, unknown>',
|
|
419
494
|
'meta?': ContractMetaSchema,
|
|
495
|
+
'defaultControlPolicy?': ControlPolicySchema,
|
|
420
496
|
'roots?': type({ '[string]': CrossReferenceSchema }),
|
|
421
497
|
domain: type({
|
|
422
498
|
namespaces: type({
|
|
423
499
|
'[string]': type({
|
|
424
500
|
models: type({ '[string]': ModelSchema }),
|
|
425
501
|
'valueObjects?': 'Record<string, unknown>',
|
|
502
|
+
'enum?': type({ '[string]': ContractEnumSchema }),
|
|
426
503
|
}),
|
|
427
504
|
}),
|
|
428
505
|
}),
|
|
@@ -557,6 +634,9 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
|
|
|
557
634
|
for (const fk of table.foreignKeys) {
|
|
558
635
|
registerNamedObject('foreign key', fk.name);
|
|
559
636
|
}
|
|
637
|
+
for (const check of table.checks ?? []) {
|
|
638
|
+
registerNamedObject('check constraint', check.name);
|
|
639
|
+
}
|
|
560
640
|
|
|
561
641
|
for (const [name, kinds] of namedObjects) {
|
|
562
642
|
if (kinds.length > 1) {
|
|
@@ -675,6 +755,18 @@ export function validateStorageSemantics(storage: SqlStorage): string[] {
|
|
|
675
755
|
}
|
|
676
756
|
}
|
|
677
757
|
}
|
|
758
|
+
|
|
759
|
+
const seenCheckDefinitions = new Set<string>();
|
|
760
|
+
for (const check of table.checks ?? []) {
|
|
761
|
+
const signature = JSON.stringify({ column: check.column, valueSet: check.valueSet });
|
|
762
|
+
if (seenCheckDefinitions.has(signature)) {
|
|
763
|
+
errors.push(
|
|
764
|
+
`Namespace "${namespaceId}" table "${tableName}": duplicate check constraint definition on column "${check.column}"`,
|
|
765
|
+
);
|
|
766
|
+
continue;
|
|
767
|
+
}
|
|
768
|
+
seenCheckDefinitions.add(signature);
|
|
769
|
+
}
|
|
678
770
|
}
|
|
679
771
|
|
|
680
772
|
return errors;
|
|
@@ -691,12 +783,19 @@ export function validateModelStorageReferences(contract: Contract<SqlStorage>):
|
|
|
691
783
|
const models = namespace.models as Record<string, ContractModel<SqlModelStorage>>;
|
|
692
784
|
for (const [modelName, model] of Object.entries(models)) {
|
|
693
785
|
const qualifiedName = `${namespaceId}:${modelName}`;
|
|
694
|
-
const
|
|
786
|
+
const storageNamespaceId = model.storage.namespaceId;
|
|
787
|
+
if (storageNamespaceId !== namespaceId) {
|
|
788
|
+
throw new ContractValidationError(
|
|
789
|
+
`Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`,
|
|
790
|
+
'storage',
|
|
791
|
+
);
|
|
792
|
+
}
|
|
695
793
|
|
|
696
|
-
const
|
|
794
|
+
const storageTable = model.storage.table;
|
|
795
|
+
const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
|
|
697
796
|
if (rawTable === undefined) {
|
|
698
797
|
throw new ContractValidationError(
|
|
699
|
-
`Model "${qualifiedName}" references non-existent table "${storageTable}"`,
|
|
798
|
+
`Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`,
|
|
700
799
|
'storage',
|
|
701
800
|
);
|
|
702
801
|
}
|
|
@@ -776,6 +875,15 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
|
|
|
776
875
|
}
|
|
777
876
|
}
|
|
778
877
|
|
|
878
|
+
for (const check of table.checks ?? []) {
|
|
879
|
+
if (!columnNames.has(check.column)) {
|
|
880
|
+
throw new ContractValidationError(
|
|
881
|
+
`Namespace "${namespaceId}" table "${tableName}" check constraint "${check.name}" references non-existent column "${check.column}"`,
|
|
882
|
+
'storage',
|
|
883
|
+
);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
779
887
|
for (const [colName, column] of Object.entries(table.columns)) {
|
|
780
888
|
if (!column.nullable && column.default?.kind === 'literal' && column.default.value === null) {
|
|
781
889
|
throw new ContractValidationError(
|
|
@@ -802,23 +910,25 @@ export function validateSqlStorageConsistency(contract: Contract<SqlStorage>): v
|
|
|
802
910
|
}
|
|
803
911
|
}
|
|
804
912
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
|
|
810
|
-
'storage',
|
|
811
|
-
);
|
|
812
|
-
}
|
|
813
|
-
const referencedTable = referencedRaw as StorageTable;
|
|
814
|
-
const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
|
|
815
|
-
for (const colName of fk.target.columns) {
|
|
816
|
-
if (!referencedColumnNames.has(colName)) {
|
|
913
|
+
if (fk.target.spaceId === undefined) {
|
|
914
|
+
const targetNamespace = contract.storage.namespaces[fk.target.namespaceId];
|
|
915
|
+
const referencedRaw = targetNamespace?.entries.table[fk.target.tableName];
|
|
916
|
+
if (referencedRaw === undefined) {
|
|
817
917
|
throw new ContractValidationError(
|
|
818
|
-
`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent
|
|
918
|
+
`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`,
|
|
819
919
|
'storage',
|
|
820
920
|
);
|
|
821
921
|
}
|
|
922
|
+
const referencedTable = referencedRaw as StorageTable;
|
|
923
|
+
const referencedColumnNames = new Set(Object.keys(referencedTable.columns));
|
|
924
|
+
for (const colName of fk.target.columns) {
|
|
925
|
+
if (!referencedColumnNames.has(colName)) {
|
|
926
|
+
throw new ContractValidationError(
|
|
927
|
+
`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`,
|
|
928
|
+
'storage',
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
822
932
|
}
|
|
823
933
|
|
|
824
934
|
if (fk.source.columns.length !== fk.target.columns.length) {
|