@prisma-next/sql-contract 0.13.0 → 0.14.0-dev.10
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/{types-DqhaAjCH.mjs → entity-kinds-Cl36zL5j.mjs} +121 -205
- package/dist/entity-kinds-Cl36zL5j.mjs.map +1 -0
- package/dist/entity-kinds.d.mts +18 -0
- package/dist/entity-kinds.d.mts.map +1 -0
- package/dist/entity-kinds.mjs +2 -0
- package/dist/factories.d.mts +2 -2
- package/dist/factories.mjs +2 -1
- package/dist/factories.mjs.map +1 -1
- package/dist/index-type-validation.d.mts +1 -1
- package/dist/index-type-validation.mjs +9 -12
- package/dist/index-type-validation.mjs.map +1 -1
- package/dist/resolve-storage-table.d.mts +2 -1
- package/dist/resolve-storage-table.d.mts.map +1 -1
- package/dist/resolve-storage-table.mjs +11 -8
- package/dist/resolve-storage-table.mjs.map +1 -1
- package/dist/sql-storage-Dga0jwP2.d.mts +128 -0
- package/dist/sql-storage-Dga0jwP2.d.mts.map +1 -0
- package/dist/{sql-storage-CXf9xjAL.d.mts → storage-value-set-WnYsIFM8.d.mts} +8 -120
- package/dist/storage-value-set-WnYsIFM8.d.mts.map +1 -0
- package/dist/types-B-eiQXff.mjs +191 -0
- package/dist/types-B-eiQXff.mjs.map +1 -0
- package/dist/{types-DEnWD3xB.d.mts → types-B1N8w0I2.d.mts} +11 -62
- package/dist/types-B1N8w0I2.d.mts.map +1 -0
- package/dist/types.d.mts +4 -3
- package/dist/types.mjs +3 -2
- package/dist/validators.d.mts +75 -40
- package/dist/validators.d.mts.map +1 -1
- package/dist/validators.mjs +156 -184
- package/dist/validators.mjs.map +1 -1
- package/package.json +8 -7
- package/src/entity-kinds.ts +45 -0
- package/src/exports/entity-kinds.ts +5 -0
- package/src/exports/types.ts +2 -4
- package/src/index-type-validation.ts +2 -3
- package/src/ir/build-sql-namespace.ts +39 -32
- package/src/ir/sql-node.ts +2 -2
- package/src/ir/sql-storage.ts +22 -24
- package/src/ir/sql-unbound-namespace.ts +15 -3
- package/src/ir/storage-entry-schemas.ts +128 -0
- package/src/ir/storage-type-instance.ts +3 -3
- package/src/ir/storage-value-set.ts +6 -5
- package/src/resolve-storage-table.ts +12 -17
- package/src/types.ts +10 -10
- package/src/validators.ts +288 -225
- package/dist/sql-storage-CXf9xjAL.d.mts.map +0 -1
- package/dist/types-DEnWD3xB.d.mts.map +0 -1
- package/dist/types-DqhaAjCH.mjs.map +0 -1
- package/src/ir/postgres-enum-storage-entry.ts +0 -57
package/dist/validators.mjs
CHANGED
|
@@ -1,32 +1,21 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { C as StorageTableSchema, S as ReferentialActionSchema, _ as ColumnDefaultSchema, b as ForeignKeySourceSchema, g as ColumnDefaultLiteralSchema, h as ColumnDefaultFunctionSchema, m as CheckConstraintSchema, t as composeSqlEntityKinds, v as ForeignKeyReferenceSchema, w as StorageValueSetSchema, x as IndexSchema, y as ForeignKeySchema } from "./entity-kinds-Cl36zL5j.mjs";
|
|
2
|
+
import { i as SqlStorage, l as buildSqlNamespaceMap, u as SqlUnboundNamespace } from "./types-B-eiQXff.mjs";
|
|
3
|
+
import { type } from "arktype";
|
|
4
|
+
import { UNBOUND_NAMESPACE_ID, isPlainRecord } from "@prisma-next/framework-components/ir";
|
|
4
5
|
import { CrossReferenceSchema } from "@prisma-next/contract/types";
|
|
6
|
+
import { blindCast } from "@prisma-next/utils/casts";
|
|
5
7
|
import { ContractValidationError } from "@prisma-next/contract/contract-validation-error";
|
|
6
8
|
import { validateContractDomain } from "@prisma-next/contract/validate-domain";
|
|
7
9
|
import { ifDefined } from "@prisma-next/utils/defined";
|
|
8
|
-
import { type } from "arktype";
|
|
9
10
|
//#region src/validators.ts
|
|
10
|
-
const literalKindSchema = type("'literal'");
|
|
11
|
-
const functionKindSchema = type("'function'");
|
|
12
11
|
const generatorKindSchema = type("'generator'");
|
|
13
12
|
const ControlPolicySchema = type("'managed' | 'tolerated' | 'external' | 'observed'");
|
|
14
|
-
const generatorIdSchema = type("string").narrow((value, ctx) => {
|
|
15
|
-
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe("a flat generator id");
|
|
16
|
-
});
|
|
17
|
-
const ColumnDefaultLiteralSchema = type.declare().type({
|
|
18
|
-
kind: literalKindSchema,
|
|
19
|
-
value: "string | number | boolean | null | unknown[] | Record<string, unknown>"
|
|
20
|
-
});
|
|
21
|
-
const ColumnDefaultFunctionSchema = type.declare().type({
|
|
22
|
-
kind: functionKindSchema,
|
|
23
|
-
expression: "string"
|
|
24
|
-
});
|
|
25
|
-
const ColumnDefaultSchema = ColumnDefaultLiteralSchema.or(ColumnDefaultFunctionSchema);
|
|
26
13
|
const ExecutionMutationDefaultValueSchema = type({
|
|
27
14
|
"+": "reject",
|
|
28
15
|
kind: generatorKindSchema,
|
|
29
|
-
id:
|
|
16
|
+
id: type("string").narrow((value, ctx) => {
|
|
17
|
+
return /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value) ? true : ctx.mustBe("a flat generator id");
|
|
18
|
+
}),
|
|
30
19
|
"params?": "Record<string, unknown>"
|
|
31
20
|
});
|
|
32
21
|
const ExecutionSchema = type({
|
|
@@ -38,6 +27,7 @@ const ExecutionSchema = type({
|
|
|
38
27
|
"+": "reject",
|
|
39
28
|
ref: {
|
|
40
29
|
"+": "reject",
|
|
30
|
+
namespace: "string",
|
|
41
31
|
table: "string",
|
|
42
32
|
column: "string"
|
|
43
33
|
},
|
|
@@ -46,63 +36,21 @@ const ExecutionSchema = type({
|
|
|
46
36
|
}).array().readonly()
|
|
47
37
|
}
|
|
48
38
|
});
|
|
49
|
-
const
|
|
50
|
-
plane: "'domain'
|
|
39
|
+
const DomainEnumRefSchema = type({
|
|
40
|
+
plane: "'domain'",
|
|
51
41
|
namespaceId: "string",
|
|
52
|
-
entityKind: "'enum'
|
|
53
|
-
|
|
42
|
+
entityKind: "'enum'",
|
|
43
|
+
entityName: "string",
|
|
54
44
|
"spaceId?": "string"
|
|
55
45
|
});
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
nativeType: "string",
|
|
59
|
-
codecId: "string",
|
|
60
|
-
nullable: "boolean",
|
|
61
|
-
"typeParams?": "Record<string, unknown>",
|
|
62
|
-
"typeRef?": "string",
|
|
63
|
-
"default?": ColumnDefaultSchema,
|
|
64
|
-
"control?": ControlPolicySchema,
|
|
65
|
-
"valueSet?": ValueSetRefSchema
|
|
66
|
-
}).narrow((col, ctx) => {
|
|
67
|
-
if (col.typeParams !== void 0 && col.typeRef !== void 0) return ctx.mustBe("a column with either typeParams or typeRef, not both");
|
|
68
|
-
return true;
|
|
69
|
-
});
|
|
70
|
-
/**
|
|
71
|
-
* Codec-triple entry persisted under `storage.types[name]`. Carries an
|
|
72
|
-
* enumerable literal `kind: 'codec-instance'` discriminator so the
|
|
73
|
-
* polymorphic slot dispatch can distinguish codec triples from
|
|
74
|
-
* class-instance kinds (e.g. `'postgres-enum'`) sharing the slot.
|
|
75
|
-
*/
|
|
76
|
-
const StorageTypeInstanceSchema = type.declare().type({
|
|
46
|
+
/** Document-scoped `storage.types`: codec triples only. */
|
|
47
|
+
const DocumentScopedStorageTypeSchema = type.declare().type({
|
|
77
48
|
kind: "'codec-instance'",
|
|
78
49
|
codecId: "string",
|
|
79
50
|
nativeType: "string",
|
|
80
51
|
"typeParams?": "Record<string, unknown>"
|
|
81
52
|
});
|
|
82
53
|
/**
|
|
83
|
-
* Postgres native enum entry under `storage.namespaces[namespaceId].entries.type[name]`.
|
|
84
|
-
* Document-scoped `storage.types` carries codec aliases only
|
|
85
|
-
* (`DocumentScopedStorageTypeSchema`).
|
|
86
|
-
*/
|
|
87
|
-
const PostgresEnumTypeSchema = type({
|
|
88
|
-
kind: "'postgres-enum'",
|
|
89
|
-
"name?": "string",
|
|
90
|
-
"nativeType?": "string",
|
|
91
|
-
values: type.string.array().readonly(),
|
|
92
|
-
"control?": ControlPolicySchema
|
|
93
|
-
});
|
|
94
|
-
/** Document-scoped `storage.types`: codec triples only. */
|
|
95
|
-
const DocumentScopedStorageTypeSchema = StorageTypeInstanceSchema;
|
|
96
|
-
/**
|
|
97
|
-
* Storage value-set entry under `storage.namespaces[id].entries.valueSet[name]`.
|
|
98
|
-
* Carries a `kind: 'value-set'` discriminator (enumerable, survives JSON) and an
|
|
99
|
-
* ordered `values` array of codec-encoded permitted values.
|
|
100
|
-
*/
|
|
101
|
-
const StorageValueSetSchema = type({
|
|
102
|
-
kind: "'value-set'",
|
|
103
|
-
values: type.string.array().readonly()
|
|
104
|
-
});
|
|
105
|
-
/**
|
|
106
54
|
* Domain enum entry under `domain.namespaces[id].enum[name]`.
|
|
107
55
|
* Carries the codec id and an ordered `members` array of `{name, value}` pairs.
|
|
108
56
|
*/
|
|
@@ -111,130 +59,55 @@ const ContractEnumSchema = type({
|
|
|
111
59
|
codecId: "string",
|
|
112
60
|
members: type({
|
|
113
61
|
name: "string",
|
|
114
|
-
value: "string"
|
|
62
|
+
value: "string | number | boolean | null | unknown[] | Record<string, unknown>"
|
|
115
63
|
}).array().readonly()
|
|
116
64
|
});
|
|
117
|
-
const PrimaryKeySchema = type.declare().type({
|
|
118
|
-
columns: type.string.array().readonly(),
|
|
119
|
-
"name?": "string"
|
|
120
|
-
});
|
|
121
|
-
const UniqueConstraintSchema = type.declare().type({
|
|
122
|
-
columns: type.string.array().readonly(),
|
|
123
|
-
"name?": "string"
|
|
124
|
-
});
|
|
125
|
-
const IndexSchema = type({
|
|
126
|
-
columns: type.string.array().readonly(),
|
|
127
|
-
"name?": "string",
|
|
128
|
-
"type?": "string",
|
|
129
|
-
"options?": "Record<string, unknown>"
|
|
130
|
-
});
|
|
131
|
-
const ForeignKeyReferenceSchema = type({
|
|
132
|
-
"+": "reject",
|
|
133
|
-
namespaceId: "string",
|
|
134
|
-
tableName: "string",
|
|
135
|
-
columns: type.string.array().readonly(),
|
|
136
|
-
"spaceId?": "string"
|
|
137
|
-
});
|
|
138
|
-
const ForeignKeySourceSchema = type({
|
|
139
|
-
"+": "reject",
|
|
140
|
-
namespaceId: "string",
|
|
141
|
-
tableName: "string",
|
|
142
|
-
columns: type.string.array().readonly()
|
|
143
|
-
});
|
|
144
|
-
const ReferentialActionSchema = type.declare().type("'noAction' | 'restrict' | 'cascade' | 'setNull' | 'setDefault'");
|
|
145
|
-
const ForeignKeySchema = type.declare().type({
|
|
146
|
-
source: ForeignKeySourceSchema,
|
|
147
|
-
target: ForeignKeyReferenceSchema,
|
|
148
|
-
"name?": "string",
|
|
149
|
-
"onDelete?": ReferentialActionSchema,
|
|
150
|
-
"onUpdate?": ReferentialActionSchema,
|
|
151
|
-
constraint: "boolean",
|
|
152
|
-
index: "boolean"
|
|
153
|
-
});
|
|
154
|
-
const CheckConstraintSchema = type({
|
|
155
|
-
"+": "reject",
|
|
156
|
-
name: "string",
|
|
157
|
-
column: "string",
|
|
158
|
-
valueSet: ValueSetRefSchema
|
|
159
|
-
});
|
|
160
|
-
const StorageTableSchema = type({
|
|
161
|
-
"+": "reject",
|
|
162
|
-
columns: type({ "[string]": StorageColumnSchema }),
|
|
163
|
-
"primaryKey?": PrimaryKeySchema,
|
|
164
|
-
uniques: UniqueConstraintSchema.array().readonly(),
|
|
165
|
-
indexes: IndexSchema.array().readonly(),
|
|
166
|
-
foreignKeys: ForeignKeySchema.array().readonly(),
|
|
167
|
-
"control?": ControlPolicySchema,
|
|
168
|
-
"checks?": CheckConstraintSchema.array().readonly()
|
|
169
|
-
});
|
|
170
65
|
/**
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
* discriminator. The composition is **additive**, not substitutive:
|
|
174
|
-
*
|
|
175
|
-
* - No fragments registered → entries are validated by `fallback`
|
|
176
|
-
* alone (the unchanged baseline).
|
|
177
|
-
* - An entry's `kind` matches `fallbackKind` AND a fragment for that
|
|
178
|
-
* kind is registered → the entry must pass **both** `fallback` and
|
|
179
|
-
* the fragment. This preserves family-owned invariants (e.g. the
|
|
180
|
-
* built-in `PostgresEnumType` shape) even when a pack contributes
|
|
181
|
-
* its own schema for the same kind.
|
|
182
|
-
* - An entry's `kind` matches a registered fragment for some
|
|
183
|
-
* non-fallback kind → the fragment alone validates the entry.
|
|
184
|
-
* `fallback` is family-specific (validates a single hardcoded kind)
|
|
185
|
-
* and would reject any other kind, so it does not apply here.
|
|
186
|
-
* - An entry's `kind` matches no fragment → fall through to
|
|
187
|
-
* `fallback`.
|
|
66
|
+
* Derives a schema map from a descriptor map: maps each kind's key to its
|
|
67
|
+
* `schema` field. Used by validation functions to validate entries.
|
|
188
68
|
*/
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
return type("unknown").narrow((entry, ctx) => {
|
|
192
|
-
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return ctx.mustBe("an object");
|
|
193
|
-
const kind = entry.kind;
|
|
194
|
-
if (typeof kind === "string") {
|
|
195
|
-
const fragment = fragments.get(kind);
|
|
196
|
-
if (fragment !== void 0) {
|
|
197
|
-
if (kind === fallbackKind) {
|
|
198
|
-
const baseParsed = fallback(entry);
|
|
199
|
-
if (baseParsed instanceof type.errors) return ctx.reject({ expected: baseParsed.summary });
|
|
200
|
-
}
|
|
201
|
-
const parsed = fragment(entry);
|
|
202
|
-
if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
|
|
203
|
-
return true;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
const parsed = fallback(entry);
|
|
207
|
-
if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
|
|
208
|
-
return true;
|
|
209
|
-
});
|
|
69
|
+
function schemaViewOf(kinds) {
|
|
70
|
+
return new Map([...kinds].map(([k, d]) => [k, d.schema]));
|
|
210
71
|
}
|
|
72
|
+
const DEFAULT_SQL_KINDS = composeSqlEntityKinds();
|
|
211
73
|
/**
|
|
212
74
|
* Builds the per-namespace entry schema for `storage.namespaces[id]`.
|
|
213
|
-
*
|
|
214
|
-
* descriptor
|
|
215
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* Validation is descriptor-driven: the `kinds` map carries both the schema
|
|
77
|
+
* (used here for structural validation) and the construct function (used at
|
|
78
|
+
* hydration time). An unregistered key fails validation naming the kind and
|
|
79
|
+
* the namespace id, so validation fails closed.
|
|
216
80
|
*/
|
|
217
|
-
function createNamespaceEntrySchema(
|
|
81
|
+
function createNamespaceEntrySchema(kinds) {
|
|
82
|
+
const schemas = schemaViewOf(kinds);
|
|
83
|
+
const knownKinds = new Set(kinds.keys());
|
|
218
84
|
return type({
|
|
219
85
|
"+": "reject",
|
|
220
86
|
id: "string",
|
|
221
87
|
"kind?": "string",
|
|
222
|
-
entries:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
88
|
+
entries: "object"
|
|
89
|
+
}).narrow((ns, ctx) => {
|
|
90
|
+
if (!isPlainRecord(ns.entries)) return ctx.mustBe("an entries object");
|
|
91
|
+
for (const [key, innerMap] of Object.entries(ns.entries)) {
|
|
92
|
+
if (!knownKinds.has(key)) return ctx.reject({ expected: `entries key "${key}" in namespace "${ns.id}" is not a registered entity kind` });
|
|
93
|
+
if (!isPlainRecord(innerMap)) return ctx.reject({ expected: `entries["${key}"] in namespace "${ns.id}" must be an object` });
|
|
94
|
+
const entrySchema = blindCast(schemas.get(key));
|
|
95
|
+
for (const [, value] of Object.entries(innerMap)) {
|
|
96
|
+
const parsed = entrySchema(value);
|
|
97
|
+
if (parsed instanceof type.errors) return ctx.reject({ expected: parsed.summary });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
228
101
|
});
|
|
229
102
|
}
|
|
230
103
|
/**
|
|
231
104
|
* Builds the storage schema. Pack contributions reach the per-namespace
|
|
232
105
|
* entry shape through {@link createNamespaceEntrySchema}; the
|
|
233
|
-
* document-scoped `storage.types`
|
|
106
|
+
* document-scoped `storage.types` field (codec triples only) and the
|
|
234
107
|
* storage hash stay family-shared.
|
|
235
108
|
*/
|
|
236
|
-
function createSqlStorageSchema(
|
|
237
|
-
const namespaceEntry = createNamespaceEntrySchema(
|
|
109
|
+
function createSqlStorageSchema(kinds) {
|
|
110
|
+
const namespaceEntry = createNamespaceEntrySchema(kinds);
|
|
238
111
|
return type({
|
|
239
112
|
"+": "reject",
|
|
240
113
|
storageHash: "string",
|
|
@@ -242,17 +115,14 @@ function createSqlStorageSchema(fragments) {
|
|
|
242
115
|
"namespaces?": type({ "[string]": namespaceEntry })
|
|
243
116
|
});
|
|
244
117
|
}
|
|
245
|
-
const StorageSchema = createSqlStorageSchema();
|
|
118
|
+
const StorageSchema = createSqlStorageSchema(DEFAULT_SQL_KINDS);
|
|
246
119
|
function eachStorageTable(storage) {
|
|
247
|
-
return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) => Object.entries(ns.entries
|
|
120
|
+
return Object.entries(storage.namespaces).flatMap(([namespaceId, ns]) => Object.entries(ns.entries["table"] ?? {}).map(([tableName, table]) => ({
|
|
248
121
|
namespaceId,
|
|
249
122
|
tableName,
|
|
250
123
|
table
|
|
251
124
|
})));
|
|
252
125
|
}
|
|
253
|
-
function isPlainRecord(value) {
|
|
254
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
255
|
-
}
|
|
256
126
|
function findDuplicateValue(values) {
|
|
257
127
|
const seen = /* @__PURE__ */ new Set();
|
|
258
128
|
for (const value of values) {
|
|
@@ -283,7 +153,7 @@ const ModelFieldSchema = type({
|
|
|
283
153
|
type: type("unknown").narrow((value, ctx) => isContractFieldType(value) ? true : ctx.mustBe("scalar, valueObject, or union field type")),
|
|
284
154
|
"many?": "true",
|
|
285
155
|
"dict?": "true",
|
|
286
|
-
"valueSet?":
|
|
156
|
+
"valueSet?": DomainEnumRefSchema
|
|
287
157
|
});
|
|
288
158
|
const ModelStorageSchema = type({
|
|
289
159
|
table: "string",
|
|
@@ -342,8 +212,8 @@ const ContractMetaSchema = type({ "[string]": "unknown" });
|
|
|
342
212
|
* pack contributions through {@link createSqlStorageSchema}; the rest
|
|
343
213
|
* of the contract envelope is family-shared.
|
|
344
214
|
*/
|
|
345
|
-
function createSqlContractSchema(
|
|
346
|
-
const storage = createSqlStorageSchema(
|
|
215
|
+
function createSqlContractSchema(kinds) {
|
|
216
|
+
const storage = createSqlStorageSchema(kinds);
|
|
347
217
|
return type({
|
|
348
218
|
"+": "reject",
|
|
349
219
|
target: "string",
|
|
@@ -364,7 +234,7 @@ function createSqlContractSchema(fragments) {
|
|
|
364
234
|
"execution?": ExecutionSchema
|
|
365
235
|
});
|
|
366
236
|
}
|
|
367
|
-
const SqlContractSchema = createSqlContractSchema();
|
|
237
|
+
const SqlContractSchema = createSqlContractSchema(DEFAULT_SQL_KINDS);
|
|
368
238
|
/**
|
|
369
239
|
* Validates the structural shape of SqlStorage using Arktype.
|
|
370
240
|
*
|
|
@@ -525,7 +395,7 @@ function validateModelStorageReferences(contract) {
|
|
|
525
395
|
const storageNamespaceId = model.storage.namespaceId;
|
|
526
396
|
if (storageNamespaceId !== namespaceId) throw new ContractValidationError(`Model "${qualifiedName}" storage.namespaceId "${storageNamespaceId}" does not match domain namespace "${namespaceId}"`, "storage");
|
|
527
397
|
const storageTable = model.storage.table;
|
|
528
|
-
const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table[storageTable];
|
|
398
|
+
const rawTable = contract.storage.namespaces[storageNamespaceId]?.entries.table?.[storageTable];
|
|
529
399
|
if (rawTable === void 0) throw new ContractValidationError(`Model "${qualifiedName}" references non-existent table "${storageNamespaceId}.${storageTable}"`, "storage");
|
|
530
400
|
const table = rawTable;
|
|
531
401
|
const columnNames = new Set(Object.keys(table.columns));
|
|
@@ -563,7 +433,7 @@ function validateSqlStorageConsistency(contract) {
|
|
|
563
433
|
if (fk.source.namespaceId !== namespaceId || fk.source.tableName !== tableName) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" contains foreignKey with mismatched source coordinates (${fk.source.namespaceId}.${fk.source.tableName})`, "storage");
|
|
564
434
|
for (const colName of fk.source.columns) if (!columnNames.has(colName)) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}"`, "storage");
|
|
565
435
|
if (fk.target.spaceId === void 0) {
|
|
566
|
-
const referencedRaw = contract.storage.namespaces[fk.target.namespaceId]?.entries.table[fk.target.tableName];
|
|
436
|
+
const referencedRaw = contract.storage.namespaces[fk.target.namespaceId]?.entries.table?.[fk.target.tableName];
|
|
567
437
|
if (referencedRaw === void 0) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent table "${fk.target.namespaceId}.${fk.target.tableName}"`, "storage");
|
|
568
438
|
const referencedColumnNames = new Set(Object.keys(referencedRaw.columns));
|
|
569
439
|
for (const colName of fk.target.columns) if (!referencedColumnNames.has(colName)) throw new ContractValidationError(`Namespace "${namespaceId}" table "${tableName}" foreignKey references non-existent column "${colName}" in table "${fk.target.tableName}"`, "storage");
|
|
@@ -593,9 +463,111 @@ function validateSqlContractFully(value, options) {
|
|
|
593
463
|
const semanticErrors = validateStorageSemantics(validated.storage);
|
|
594
464
|
if (semanticErrors.length > 0) throw new ContractValidationError(`Contract semantic validation failed: ${semanticErrors.join("; ")}`, "storage");
|
|
595
465
|
validateModelStorageReferences(validated);
|
|
466
|
+
validateRelationThroughConsistency(validated);
|
|
596
467
|
return validated;
|
|
597
468
|
}
|
|
469
|
+
/** Storage column lookup for through-consistency validation. */
|
|
470
|
+
function lookupStorageColumn(contract, namespaceId, tableName, columnName) {
|
|
471
|
+
const rawTable = contract.storage.namespaces[namespaceId]?.entries.table?.[tableName];
|
|
472
|
+
if (rawTable === void 0) return;
|
|
473
|
+
return blindCast(rawTable).columns[columnName];
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Two storage columns share a type when their `nativeType` and `typeParams`
|
|
477
|
+
* match. The contract is canonicalized, so `typeParams` key order is stable and
|
|
478
|
+
* a JSON comparison is exact. `codecId` and `nullable` are intentionally not
|
|
479
|
+
* compared: they do not change the database-level type that governs a join.
|
|
480
|
+
*/
|
|
481
|
+
function sameStorageType(a, b) {
|
|
482
|
+
return a.nativeType === b.nativeType && JSON.stringify(a.typeParams ?? null) === JSON.stringify(b.typeParams ?? null);
|
|
483
|
+
}
|
|
484
|
+
function describeColumnType(column) {
|
|
485
|
+
return column.typeParams === void 0 ? column.nativeType : `${column.nativeType} ${JSON.stringify(column.typeParams)}`;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Validates one side of an N:M join: the junction columns and the model
|
|
489
|
+
* columns they pair against positionally must be equal in number, exist in
|
|
490
|
+
* their tables, and share the same storage type (`nativeType` + `typeParams`).
|
|
491
|
+
* The junction's storage foreign keys already guarantee this for user-declared
|
|
492
|
+
* FK constraints, but `through` is a logical descriptor never tied to them by
|
|
493
|
+
* the rest of validation — and the TS builder accepts explicit join columns
|
|
494
|
+
* without requiring a junction FK at all — so this checks the columns directly
|
|
495
|
+
* against storage, one path regardless of how the junction was authored.
|
|
496
|
+
*
|
|
497
|
+
* Joined columns must be the *same* storage type, not merely compatible:
|
|
498
|
+
* relying on implicit conversion (e.g. `text`↔`character`) is unsafe on writes
|
|
499
|
+
* — `character(n)` space-padding makes such coercions non-associative — and no
|
|
500
|
+
* ADR sanctions heterogeneous junction columns. Equality is the conservative
|
|
501
|
+
* default; it can be relaxed deliberately if a real use case ever appears.
|
|
502
|
+
*/
|
|
503
|
+
function validateThroughJoinSide(input) {
|
|
504
|
+
const fail = (detail) => new ContractValidationError(`Many-to-many relation "${input.qualifiedName}" ${detail}`, "storage");
|
|
505
|
+
if (input.junctionColumns.length !== input.modelColumns.length) throw fail(`pairs ${input.junctionColumnsLabel} (${input.junctionColumns.length}) with ${input.modelColumnsLabel} (${input.modelColumns.length}) of differing length; they join positionally and must match.`);
|
|
506
|
+
for (const [index, junctionColumnName] of input.junctionColumns.entries()) {
|
|
507
|
+
const modelColumnRef = input.modelColumns[index];
|
|
508
|
+
if (modelColumnRef === void 0) continue;
|
|
509
|
+
const junctionColumn = lookupStorageColumn(input.contract, input.junctionNamespaceId, input.junctionTable, junctionColumnName);
|
|
510
|
+
if (junctionColumn === void 0) throw fail(`${input.junctionColumnsLabel} references column "${junctionColumnName}" absent from junction table "${input.junctionNamespaceId}.${input.junctionTable}".`);
|
|
511
|
+
const model = input.model;
|
|
512
|
+
if (model === void 0) continue;
|
|
513
|
+
let modelColumnName = modelColumnRef;
|
|
514
|
+
if (model.fieldToColumn !== void 0) {
|
|
515
|
+
const mapped = model.fieldToColumn[modelColumnRef];
|
|
516
|
+
if (mapped === void 0) throw fail(`${input.modelColumnsLabel} references field "${modelColumnRef}" absent from model on table "${model.namespaceId}.${model.table}".`);
|
|
517
|
+
modelColumnName = mapped.column;
|
|
518
|
+
}
|
|
519
|
+
const modelColumn = lookupStorageColumn(input.contract, model.namespaceId, model.table, modelColumnName);
|
|
520
|
+
if (modelColumn === void 0) throw fail(`${input.modelColumnsLabel} references column "${modelColumnName}" absent from table "${model.namespaceId}.${model.table}".`);
|
|
521
|
+
if (!sameStorageType(junctionColumn, modelColumn)) throw fail(`joins "${input.junctionTable}.${junctionColumnName}" (${describeColumnType(junctionColumn)}) with "${model.table}.${modelColumnName}" (${describeColumnType(modelColumn)}) of differing storage type; junction columns must match the type of the column they reference.`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Validates that every N:M relation's `through` descriptor is consistent with
|
|
526
|
+
* the storage columns it joins: both join sides match in column count,
|
|
527
|
+
* reference columns that exist in their tables, and pair columns of the same
|
|
528
|
+
* storage type. Without this, a `through` that disagrees with storage surfaces
|
|
529
|
+
* as a silently wrong JOIN at query time rather than a validation error here.
|
|
530
|
+
*/
|
|
531
|
+
function validateRelationThroughConsistency(contract) {
|
|
532
|
+
for (const [namespaceId, namespace] of Object.entries(contract.domain.namespaces)) for (const [modelName, model] of Object.entries(namespace.models)) for (const [relationName, relation] of Object.entries(model.relations)) {
|
|
533
|
+
if (relation.cardinality !== "N:M") continue;
|
|
534
|
+
const qualifiedName = `${namespaceId}.${modelName}.${relationName}`;
|
|
535
|
+
const { on, through } = relation;
|
|
536
|
+
const modelStorage = blindCast(model.storage);
|
|
537
|
+
validateThroughJoinSide({
|
|
538
|
+
contract,
|
|
539
|
+
qualifiedName,
|
|
540
|
+
modelColumns: on.localFields,
|
|
541
|
+
modelColumnsLabel: "on.localFields",
|
|
542
|
+
model: {
|
|
543
|
+
namespaceId,
|
|
544
|
+
table: modelStorage.table,
|
|
545
|
+
fieldToColumn: modelStorage.fields
|
|
546
|
+
},
|
|
547
|
+
junctionColumns: through.parentColumns,
|
|
548
|
+
junctionColumnsLabel: "through.parentColumns",
|
|
549
|
+
junctionNamespaceId: through.namespaceId,
|
|
550
|
+
junctionTable: through.table
|
|
551
|
+
});
|
|
552
|
+
const targetModel = relation.to.space === void 0 ? contract.domain.namespaces[relation.to.namespace]?.models[relation.to.model] : void 0;
|
|
553
|
+
const targetModelSide = targetModel === void 0 ? void 0 : {
|
|
554
|
+
namespaceId: relation.to.namespace,
|
|
555
|
+
table: blindCast(targetModel.storage).table
|
|
556
|
+
};
|
|
557
|
+
validateThroughJoinSide({
|
|
558
|
+
contract,
|
|
559
|
+
qualifiedName,
|
|
560
|
+
modelColumns: through.targetColumns,
|
|
561
|
+
modelColumnsLabel: "through.targetColumns",
|
|
562
|
+
...ifDefined("model", targetModelSide),
|
|
563
|
+
junctionColumns: through.childColumns,
|
|
564
|
+
junctionColumnsLabel: "through.childColumns",
|
|
565
|
+
junctionNamespaceId: through.namespaceId,
|
|
566
|
+
junctionTable: through.table
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
598
570
|
//#endregion
|
|
599
|
-
export { CheckConstraintSchema, ColumnDefaultFunctionSchema, ColumnDefaultLiteralSchema, ColumnDefaultSchema, ContractEnumSchema, ForeignKeyReferenceSchema, ForeignKeySchema, ForeignKeySourceSchema, IndexSchema,
|
|
571
|
+
export { CheckConstraintSchema, ColumnDefaultFunctionSchema, ColumnDefaultLiteralSchema, ColumnDefaultSchema, ContractEnumSchema, ForeignKeyReferenceSchema, ForeignKeySchema, ForeignKeySourceSchema, IndexSchema, ReferentialActionSchema, StorageTableSchema, StorageValueSetSchema, createNamespaceEntrySchema, createSqlContractSchema, createSqlStorageSchema, validateModel, validateModelStorageReferences, validateSqlContractFully, validateSqlStorageConsistency, validateStorage, validateStorageSemantics };
|
|
600
572
|
|
|
601
573
|
//# sourceMappingURL=validators.mjs.map
|