@vibeorm/dsl 4.1.0 → 4.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.
- package/README.md +2 -2
- package/dist/column.d.ts +84 -5
- package/dist/index.d.ts +13 -7
- package/dist/index.js +797 -107
- package/dist/index.js.map +13 -12
- package/dist/predicate.d.ts +59 -2
- package/dist/relation-plan.d.ts +3 -1
- package/dist/row-rules.d.ts +63 -0
- package/dist/to-ir.d.ts +26 -2
- package/dist/types.d.ts +219 -10
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// packages/dsl/src/column.ts
|
|
3
|
-
import { VibeError, assertJsonSchemaRef, encodePrismaString } from "@vibeorm/schema";
|
|
3
|
+
import { VibeError, assertJsonSchemaRef, assertValidatorRef, encodePrismaString, generatedColumnFromSql } from "@vibeorm/schema";
|
|
4
4
|
function cloneColumnDef(params) {
|
|
5
5
|
const { column } = params;
|
|
6
6
|
return {
|
|
7
7
|
__type: "column",
|
|
8
8
|
__columnType: column.__columnType,
|
|
9
9
|
__name: column.__name,
|
|
10
|
-
__config: { ...column.__config, ...column.__config.jsonSchema === undefined ? {} : { jsonSchema: assertJsonSchemaRef({ value: column.__config.jsonSchema }) } },
|
|
10
|
+
__config: { ...column.__config, ...column.__config.jsonSchema === undefined ? {} : { jsonSchema: assertJsonSchemaRef({ value: column.__config.jsonSchema }) }, ...column.__config.validator === undefined ? {} : { validator: assertValidatorRef({ value: column.__config.validator }) }, ..."generated" in column.__config ? { generated: Object.freeze({ sql: generatedColumnFromSql({ value: column.__config.generated }).expression }) } : {} },
|
|
11
11
|
__enumValues: column.__enumValues,
|
|
12
12
|
__enumName: column.__enumName,
|
|
13
13
|
__enumStorage: column.__enumStorage
|
|
@@ -24,7 +24,7 @@ class ChainableColumn {
|
|
|
24
24
|
__enumStorage;
|
|
25
25
|
constructor(columnType, config = {}) {
|
|
26
26
|
this.__columnType = columnType;
|
|
27
|
-
this.__config = { ...config };
|
|
27
|
+
this.__config = { ...config, ...config.validator === undefined ? {} : { validator: assertValidatorRef({ value: config.validator }) }, ..."generated" in config ? { generated: Object.freeze({ sql: generatedColumnFromSql({ value: config.generated }).expression }) } : {} };
|
|
28
28
|
}
|
|
29
29
|
optional() {
|
|
30
30
|
this.__config.optional = true;
|
|
@@ -54,10 +54,62 @@ class ChainableColumn {
|
|
|
54
54
|
this.__config.dbgenerated = expression;
|
|
55
55
|
return this;
|
|
56
56
|
}
|
|
57
|
+
generated(options) {
|
|
58
|
+
if ("generated" in this.__config)
|
|
59
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare generation only once", meta: { issues: [{ rule: "generated-column-duplicate" }] } });
|
|
60
|
+
const generated = generatedColumnFromSql({ value: options });
|
|
61
|
+
this.__config.generated = Object.freeze({ sql: generated.expression });
|
|
62
|
+
return this;
|
|
63
|
+
}
|
|
64
|
+
validate(reference) {
|
|
65
|
+
if (this.__columnType === "json" || this.__columnType === "unsupported")
|
|
66
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "Validators are only valid on queryable non-JSON scalar or enum columns; use .schema() for Json", meta: { issues: [{ rule: "validator-non-scalar" }] } });
|
|
67
|
+
if (this.__config.jsonSchema !== undefined)
|
|
68
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field cannot combine a scalar validator with a JSON schema", meta: { issues: [{ rule: "conflicting-validator-ref" }] } });
|
|
69
|
+
if (this.__config.validator !== undefined)
|
|
70
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one validator reference", meta: { issues: [{ rule: "duplicate-validator-ref" }] } });
|
|
71
|
+
this.__config.validator = assertValidatorRef({ value: reference });
|
|
72
|
+
return this;
|
|
73
|
+
}
|
|
57
74
|
comment(text) {
|
|
58
75
|
this.__config.comment = text;
|
|
59
76
|
return this;
|
|
60
77
|
}
|
|
78
|
+
addCheckRule(params) {
|
|
79
|
+
const { options } = params;
|
|
80
|
+
const rule = Object.freeze({
|
|
81
|
+
rule: options?.rule ?? params.rule,
|
|
82
|
+
check: Object.freeze(params.check.kind === "oneOf" ? { ...params.check, values: Object.freeze([...params.check.values]) } : params.check),
|
|
83
|
+
...options?.name === undefined ? {} : { name: options.name }
|
|
84
|
+
});
|
|
85
|
+
this.__config.checks = Object.freeze([...this.__config.checks ?? [], rule]);
|
|
86
|
+
return this;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function numericCarrier(value) {
|
|
90
|
+
return typeof value === "bigint" ? value.toString() : value;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class ChainableNumericColumn extends ChainableColumn {
|
|
94
|
+
zero() {
|
|
95
|
+
return this.__columnType === "decimal" ? "0" : 0;
|
|
96
|
+
}
|
|
97
|
+
nonNegative(options) {
|
|
98
|
+
return this.addCheckRule({ rule: "nonNegative", check: { kind: "compare", op: "gte", value: this.zero() }, options });
|
|
99
|
+
}
|
|
100
|
+
positive(options) {
|
|
101
|
+
return this.addCheckRule({ rule: "positive", check: { kind: "compare", op: "gt", value: this.zero() }, options });
|
|
102
|
+
}
|
|
103
|
+
atLeast(value, options) {
|
|
104
|
+
return this.addCheckRule({ rule: "atLeast", check: { kind: "compare", op: "gte", value: numericCarrier(value) }, options });
|
|
105
|
+
}
|
|
106
|
+
atMost(value, options) {
|
|
107
|
+
return this.addCheckRule({ rule: "atMost", check: { kind: "compare", op: "lte", value: numericCarrier(value) }, options });
|
|
108
|
+
}
|
|
109
|
+
range(range) {
|
|
110
|
+
const { min, max, ...options } = range;
|
|
111
|
+
return this.addCheckRule({ rule: "range", check: { kind: "range", min: numericCarrier(min), max: numericCarrier(max) }, options });
|
|
112
|
+
}
|
|
61
113
|
}
|
|
62
114
|
|
|
63
115
|
class ChainableStringColumn extends ChainableColumn {
|
|
@@ -72,13 +124,23 @@ class ChainableStringColumn extends ChainableColumn {
|
|
|
72
124
|
this.__config.text = true;
|
|
73
125
|
return this;
|
|
74
126
|
}
|
|
127
|
+
uuid() {
|
|
128
|
+
this.__config.uuid = true;
|
|
129
|
+
return this;
|
|
130
|
+
}
|
|
75
131
|
collation(name) {
|
|
76
132
|
this.__config.collation = name;
|
|
77
133
|
return this;
|
|
78
134
|
}
|
|
135
|
+
oneOf(values, options) {
|
|
136
|
+
return this.addCheckRule({ rule: "allowed", check: { kind: "oneOf", values: [...values] }, options });
|
|
137
|
+
}
|
|
138
|
+
lengthAtMost(max, options) {
|
|
139
|
+
return this.addCheckRule({ rule: "length", check: { kind: "lengthAtMost", max }, options });
|
|
140
|
+
}
|
|
79
141
|
}
|
|
80
142
|
|
|
81
|
-
class ChainableDecimalColumn extends
|
|
143
|
+
class ChainableDecimalColumn extends ChainableNumericColumn {
|
|
82
144
|
constructor(config = {}) {
|
|
83
145
|
super("decimal", config);
|
|
84
146
|
}
|
|
@@ -120,6 +182,10 @@ class ChainableIdColumn extends ChainableColumn {
|
|
|
120
182
|
this.__config.type = idType;
|
|
121
183
|
return this;
|
|
122
184
|
}
|
|
185
|
+
uuid() {
|
|
186
|
+
this.__config.uuid = true;
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
123
189
|
}
|
|
124
190
|
|
|
125
191
|
class ChainableEnumColumn extends ChainableColumn {
|
|
@@ -137,22 +203,34 @@ class ChainableEnumColumn extends ChainableColumn {
|
|
|
137
203
|
this.__config.default = value;
|
|
138
204
|
return this;
|
|
139
205
|
}
|
|
206
|
+
oneOf(values, options) {
|
|
207
|
+
return this.addCheckRule({ rule: "allowed", check: { kind: "oneOf", values: [...values] }, options });
|
|
208
|
+
}
|
|
140
209
|
}
|
|
141
210
|
function string(config = {}) {
|
|
142
211
|
return new ChainableStringColumn(config);
|
|
143
212
|
}
|
|
144
213
|
|
|
145
|
-
class ChainableIntegerColumn extends
|
|
214
|
+
class ChainableIntegerColumn extends ChainableNumericColumn {
|
|
146
215
|
autoincrement() {
|
|
147
216
|
this.__config.autoincrement = true;
|
|
148
217
|
return this;
|
|
149
218
|
}
|
|
150
219
|
}
|
|
220
|
+
function localDate({ ...config } = {}) {
|
|
221
|
+
return new ChainableColumn("localDate", config);
|
|
222
|
+
}
|
|
151
223
|
function int(config = {}) {
|
|
152
224
|
return new ChainableIntegerColumn("int", config);
|
|
153
225
|
}
|
|
226
|
+
|
|
227
|
+
class ChainableFloatColumn extends ChainableNumericColumn {
|
|
228
|
+
constructor(config = {}) {
|
|
229
|
+
super("float", config);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
154
232
|
function float(config = {}) {
|
|
155
|
-
return new
|
|
233
|
+
return new ChainableFloatColumn(config);
|
|
156
234
|
}
|
|
157
235
|
function decimal(config = {}) {
|
|
158
236
|
return new ChainableDecimalColumn(config);
|
|
@@ -169,6 +247,8 @@ class ChainableJsonColumn extends ChainableColumn {
|
|
|
169
247
|
super("json", { ...config, ...config.jsonSchema === undefined ? {} : { jsonSchema: assertJsonSchemaRef({ value: config.jsonSchema }) } });
|
|
170
248
|
}
|
|
171
249
|
schema(reference) {
|
|
250
|
+
if (this.__config.validator !== undefined)
|
|
251
|
+
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field cannot combine a scalar validator with a JSON schema", meta: { issues: [{ rule: "conflicting-validator-ref" }] } });
|
|
172
252
|
if (this.__config.jsonSchema !== undefined)
|
|
173
253
|
throw new VibeError({ code: "VIBE_SCHEMA_INVALID", message: "A field may declare only one JSON schema reference", meta: { issues: [{ rule: "duplicate-json-schema-ref" }] } });
|
|
174
254
|
this.__config.jsonSchema = assertJsonSchemaRef({ value: reference });
|
|
@@ -235,11 +315,66 @@ function createEnum(config) {
|
|
|
235
315
|
}
|
|
236
316
|
|
|
237
317
|
// packages/dsl/src/generator/prisma.ts
|
|
238
|
-
import { VibeError as
|
|
318
|
+
import { VibeError as VibeError7, assertGeneratedColumns, assertRowRuleReferentialActions, encodePrismaString as encodePrismaString2, generatedColumnFromSql as generatedColumnFromSql3, internalError, normalizeJsonSchemaRef as normalizeJsonSchemaRef2, normalizeValidatorRef as normalizeValidatorRef2, providerToDialect as providerToDialect2, rowRulePrismaAttribute, indexKeyPrisma, indexKeyTerms } from "@vibeorm/schema";
|
|
239
319
|
import { getSqlDialect as getSqlDialect2 } from "@vibeorm/sql";
|
|
240
320
|
|
|
321
|
+
// packages/dsl/src/row-rules.ts
|
|
322
|
+
import { VibeError as VibeError2, normalizeRowRules } from "@vibeorm/schema";
|
|
323
|
+
function assertOptions(params) {
|
|
324
|
+
const { value } = params;
|
|
325
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || Reflect.ownKeys(value).some((key) => typeof key !== "string" || !params.allowed.includes(key))) {
|
|
326
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: `Row-rule options accept only ${params.allowed.join(", ")}` });
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function fieldArray(params) {
|
|
330
|
+
if (!Array.isArray(params.value) || params.value.some((entry) => typeof entry !== "string"))
|
|
331
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Row-rule fields must be an array of logical names" });
|
|
332
|
+
return Object.freeze([...params.value]);
|
|
333
|
+
}
|
|
334
|
+
function createRowRuleBuilder() {
|
|
335
|
+
return Object.freeze({
|
|
336
|
+
appendOnly(options = {}) {
|
|
337
|
+
assertOptions({ value: options, allowed: ["name"] });
|
|
338
|
+
return Object.freeze({ kind: "appendOnly", ...options });
|
|
339
|
+
},
|
|
340
|
+
fixed(options) {
|
|
341
|
+
assertOptions({ value: options, allowed: ["columns", "when", "name"] });
|
|
342
|
+
assertOptions({ value: options.when, allowed: ["fromCreation", "onceSet"] });
|
|
343
|
+
return Object.freeze({ kind: "fixed", ...options, columns: fieldArray({ value: options.columns }), when: Object.freeze({ ...options.when }) });
|
|
344
|
+
},
|
|
345
|
+
rowFixed(options) {
|
|
346
|
+
assertOptions({ value: options, allowed: ["except", "name"] });
|
|
347
|
+
return Object.freeze({ kind: "rowFixed", ...options, except: fieldArray({ value: options.except }) });
|
|
348
|
+
},
|
|
349
|
+
frozen(options) {
|
|
350
|
+
assertOptions({ value: options, allowed: ["from", "mutable", "name"] });
|
|
351
|
+
return Object.freeze({ kind: "frozen", ...options, mutable: fieldArray({ value: options.mutable }) });
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
function compileRowRules(params) {
|
|
356
|
+
if (params.model.__rowRules === undefined)
|
|
357
|
+
return [];
|
|
358
|
+
if (typeof params.model.__rowRules !== "function")
|
|
359
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "rowRules must be a model-bound callback" });
|
|
360
|
+
const declarations = params.model.__rowRules(createRowRuleBuilder());
|
|
361
|
+
if (!Array.isArray(declarations))
|
|
362
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "rowRules must return an array of declarations" });
|
|
363
|
+
const rules = declarations.map((declaration) => {
|
|
364
|
+
if (declaration === null || typeof declaration !== "object" || Array.isArray(declaration))
|
|
365
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Row-rule declarations must be data objects" });
|
|
366
|
+
const candidate = declaration;
|
|
367
|
+
if (candidate.kind !== "frozen")
|
|
368
|
+
return declaration;
|
|
369
|
+
if (typeof candidate.from !== "string" || candidate.from.length === 0)
|
|
370
|
+
throw new VibeError2({ code: "VIBE_SCHEMA_INVALID", message: "Frozen from must be create or a logical field name" });
|
|
371
|
+
return { ...candidate, kind: "frozen", from: candidate.from === "create" ? { create: true } : { field: candidate.from }, mutable: candidate.mutable };
|
|
372
|
+
});
|
|
373
|
+
return normalizeRowRules({ model: params.irModel, rules, dialect: params.dialect, schema: params.schema });
|
|
374
|
+
}
|
|
375
|
+
|
|
241
376
|
// packages/dsl/src/predicate.ts
|
|
242
|
-
import { VibeError as
|
|
377
|
+
import { IDENTIFIER_LIMITS, VibeError as VibeError3, checkRulePredicate, identifierLength, isEnumType } from "@vibeorm/schema";
|
|
243
378
|
import { compileExclusion, renderPredicate } from "@vibeorm/sql";
|
|
244
379
|
function isFieldExpr(operand) {
|
|
245
380
|
return typeof operand === "object" && operand !== null && "kind" in operand && operand.kind === "field";
|
|
@@ -254,7 +389,7 @@ function toValue(params) {
|
|
|
254
389
|
if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
|
|
255
390
|
return { kind: "value", value: input };
|
|
256
391
|
}
|
|
257
|
-
throw new
|
|
392
|
+
throw new VibeError3({
|
|
258
393
|
code: "VIBE_SCHEMA_INVALID",
|
|
259
394
|
message: `${at}: ${input === null ? "null" : typeof input} is not a comparable literal \u2014 use isNull()/isNotNull() for null, and a string, number, bigint or boolean otherwise`,
|
|
260
395
|
meta: { rule: "predicate-literal-invalid", at }
|
|
@@ -272,17 +407,28 @@ function toExpression(params) {
|
|
|
272
407
|
if (value.kind === "jsonText" && "field" in value && "path" in value && Array.isArray(value.path) && Array.from(value.path).every((segment) => typeof segment === "string" && !segment.includes("\x00"))) {
|
|
273
408
|
return { kind: "jsonText", field: toRef({ expr: requireFieldExpr({ value: value.field, at }) }), path: [...value.path] };
|
|
274
409
|
}
|
|
275
|
-
if (value.kind === "length" && "expression" in value) {
|
|
276
|
-
return { kind:
|
|
410
|
+
if ((value.kind === "length" || value.kind === "lower" || value.kind === "upper") && "expression" in value) {
|
|
411
|
+
return { kind: value.kind, expression: toExpression({ value: value.expression, at }) };
|
|
412
|
+
}
|
|
413
|
+
if ((value.kind === "add" || value.kind === "sub" || value.kind === "mul") && "left" in value && "right" in value) {
|
|
414
|
+
const right = value.right;
|
|
415
|
+
return {
|
|
416
|
+
kind: value.kind,
|
|
417
|
+
left: toExpression({ value: value.left, at }),
|
|
418
|
+
right: typeof right === "object" && right !== null && "kind" in right && right.kind === "value" && "value" in right ? toValue({ input: right.value, at }) : toOperand({ operand: right, at })
|
|
419
|
+
};
|
|
277
420
|
}
|
|
278
421
|
}
|
|
279
|
-
throw new
|
|
422
|
+
throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `${at}: expected a field, jsonText, length, add, sub, mul, lower or upper expression; JSON paths must contain only strings`, meta: { rule: "predicate-operand-invalid", at } });
|
|
423
|
+
}
|
|
424
|
+
function toPredicateExpression(params) {
|
|
425
|
+
return toExpression(params);
|
|
280
426
|
}
|
|
281
427
|
function requireFieldExpr(params) {
|
|
282
428
|
const { value, at } = params;
|
|
283
429
|
if (isFieldExpr(value))
|
|
284
430
|
return value;
|
|
285
|
-
throw new
|
|
431
|
+
throw new VibeError3({
|
|
286
432
|
code: "VIBE_SCHEMA_INVALID",
|
|
287
433
|
message: `${at}: the left side of a comparison must be c.field("\u2026"), not a bare value`,
|
|
288
434
|
meta: { rule: "predicate-operand-invalid", at }
|
|
@@ -291,7 +437,7 @@ function requireFieldExpr(params) {
|
|
|
291
437
|
function requireOperands(params) {
|
|
292
438
|
const { operands, at } = params;
|
|
293
439
|
if (operands.length === 0) {
|
|
294
|
-
throw new
|
|
440
|
+
throw new VibeError3({
|
|
295
441
|
code: "VIBE_SCHEMA_INVALID",
|
|
296
442
|
message: `${at}: an empty boolean group has no honest SQL spelling \u2014 pass at least one operand`,
|
|
297
443
|
meta: { rule: "predicate-empty", at }
|
|
@@ -310,7 +456,7 @@ function createPredicateBuilder() {
|
|
|
310
456
|
const at = negate ? "c.notInList()" : "c.inList()";
|
|
311
457
|
const ref = toRef({ expr: requireFieldExpr({ value: field, at }) });
|
|
312
458
|
if (values.length === 0) {
|
|
313
|
-
throw new
|
|
459
|
+
throw new VibeError3({
|
|
314
460
|
code: "VIBE_SCHEMA_INVALID",
|
|
315
461
|
message: `${at}: an empty IN list is UNKNOWN on every row and is never what an author means`,
|
|
316
462
|
meta: { rule: "predicate-empty", at, field: ref.name }
|
|
@@ -331,6 +477,11 @@ function createPredicateBuilder() {
|
|
|
331
477
|
gte: compare("gte"),
|
|
332
478
|
lt: compare("lt"),
|
|
333
479
|
lte: compare("lte"),
|
|
480
|
+
add: ({ left, right }) => ({ ...toExpression({ value: { kind: "add", left, right }, at: "c.add()" }), __kind: "Int" }),
|
|
481
|
+
sub: ({ left, right }) => ({ ...toExpression({ value: { kind: "sub", left, right }, at: "c.sub()" }), __kind: "Int" }),
|
|
482
|
+
mul: ({ left, right }) => ({ ...toExpression({ value: { kind: "mul", left, right }, at: "c.mul()" }), __kind: "Int" }),
|
|
483
|
+
lower: (expression) => ({ kind: "lower", expression: toExpression({ value: expression, at: "c.lower()" }), __kind: "String" }),
|
|
484
|
+
upper: (expression) => ({ kind: "upper", expression: toExpression({ value: expression, at: "c.upper()" }), __kind: "String" }),
|
|
334
485
|
jsonText: ({ field, path }) => ({ ...toExpression({ value: { kind: "jsonText", field: { kind: "field", name: field }, path }, at: "c.jsonText()" }), __kind: "String" }),
|
|
335
486
|
length: (expression) => ({ kind: "length", expression: toExpression({ value: expression, at: "c.length()" }), __kind: "Int" }),
|
|
336
487
|
isTrue: (operand) => ({ kind: "isTrue", operand }),
|
|
@@ -343,7 +494,7 @@ function createPredicateBuilder() {
|
|
|
343
494
|
not: (operand) => ({ kind: "not", operand }),
|
|
344
495
|
named: (name, predicate) => {
|
|
345
496
|
if (name.trim() === "") {
|
|
346
|
-
throw new
|
|
497
|
+
throw new VibeError3({
|
|
347
498
|
code: "VIBE_SCHEMA_INVALID",
|
|
348
499
|
message: "c.named(): a CHECK needs a non-empty name \u2014 the name is what keys the migration diff",
|
|
349
500
|
meta: { rule: "predicate-unnamed-check" }
|
|
@@ -353,6 +504,262 @@ function createPredicateBuilder() {
|
|
|
353
504
|
}
|
|
354
505
|
};
|
|
355
506
|
}
|
|
507
|
+
var MAX_LIST_PREVIEW = 6;
|
|
508
|
+
function preview(params) {
|
|
509
|
+
const { values } = params;
|
|
510
|
+
return values.length <= MAX_LIST_PREVIEW ? values.join(", ") : `${values.slice(0, MAX_LIST_PREVIEW).join(", ")}, \u2026`;
|
|
511
|
+
}
|
|
512
|
+
function ruleError(params) {
|
|
513
|
+
return new VibeError3({
|
|
514
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
515
|
+
message: `${params.site}: ${params.message}`,
|
|
516
|
+
meta: { rule: params.rule, site: params.site, ...params.meta ?? {} }
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
function physicalColumn(params) {
|
|
520
|
+
return params.field.dbName ?? params.field.name;
|
|
521
|
+
}
|
|
522
|
+
function mintedName(params) {
|
|
523
|
+
return `${params.context.table}_${physicalColumn({ field: params.field })}_${params.rule}_check`;
|
|
524
|
+
}
|
|
525
|
+
function assertRuleWord(params) {
|
|
526
|
+
const { site, options } = params;
|
|
527
|
+
if (options?.name !== undefined && (typeof options.name !== "string" || options.name.trim() === "")) {
|
|
528
|
+
throw ruleError({ site, rule: "predicate-unnamed-check", message: "{ name } must be a non-empty CHECK name" });
|
|
529
|
+
}
|
|
530
|
+
if (options?.rule !== undefined && (typeof options.rule !== "string" || options.rule.trim() === "")) {
|
|
531
|
+
throw ruleError({ site, rule: "check-rule-invalid", message: "{ rule } must be a non-empty rule word" });
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
function ruleField(params) {
|
|
535
|
+
const { context, name, site } = params;
|
|
536
|
+
const found = typeof name === "string" ? context.fields.find((field) => field.name === name) : undefined;
|
|
537
|
+
if (found === undefined) {
|
|
538
|
+
throw ruleError({ site, rule: "check-rule-unknown-field", message: `names ${JSON.stringify(name)}, which model "${context.model}" does not declare`, meta: { model: context.model, field: name } });
|
|
539
|
+
}
|
|
540
|
+
if (found.isList) {
|
|
541
|
+
throw ruleError({ site, rule: "check-rule-list-field", message: `"${found.name}" is a list column \u2014 a check rule compares scalar values`, meta: { model: context.model, field: found.name } });
|
|
542
|
+
}
|
|
543
|
+
return found;
|
|
544
|
+
}
|
|
545
|
+
function ruleCheck(params) {
|
|
546
|
+
return { __type: "check", name: params.name, predicate: params.predicate, __origin: params.origin, __minted: params.minted };
|
|
547
|
+
}
|
|
548
|
+
function createChecksBuilder(params) {
|
|
549
|
+
const { context } = params;
|
|
550
|
+
const group = (kind) => (...args) => {
|
|
551
|
+
const arrayForm = Array.isArray(args[0]);
|
|
552
|
+
const names = arrayForm ? [...args[0]] : args;
|
|
553
|
+
const options = arrayForm ? args[1] : undefined;
|
|
554
|
+
const site = `c.${kind}(${names.map((name) => JSON.stringify(name)).join(", ")}) on "${context.model}"`;
|
|
555
|
+
assertRuleWord({ site, options });
|
|
556
|
+
const fields = names.map((name) => ruleField({ context, name, site }));
|
|
557
|
+
if (fields.length < 2 || fields.length > 8) {
|
|
558
|
+
throw ruleError({ site, rule: "check-rule-group-arity", message: `takes 2 to 8 columns, not ${fields.length}` });
|
|
559
|
+
}
|
|
560
|
+
if (new Set(fields.map((field) => field.name)).size !== fields.length) {
|
|
561
|
+
throw ruleError({ site, rule: "check-rule-duplicate-field", message: "names a column twice \u2014 the expansion would contradict itself" });
|
|
562
|
+
}
|
|
563
|
+
const predicate = checkRulePredicate({ rule: { kind, fields: fields.map((field) => field.name) } });
|
|
564
|
+
const name = options?.name ?? mintedName({ context, field: fields[0], rule: options?.rule ?? kind });
|
|
565
|
+
return ruleCheck({ name, predicate, origin: site, minted: options?.name === undefined });
|
|
566
|
+
};
|
|
567
|
+
const order = (earlier, later, options) => {
|
|
568
|
+
const site = `c.order(${JSON.stringify(earlier)}, ${JSON.stringify(later)}) on "${context.model}"`;
|
|
569
|
+
assertRuleWord({ site, options });
|
|
570
|
+
const first = ruleField({ context, name: earlier, site });
|
|
571
|
+
const second = ruleField({ context, name: later, site });
|
|
572
|
+
if (first.name === second.name) {
|
|
573
|
+
throw ruleError({ site, rule: "check-rule-duplicate-field", message: "orders a column against itself" });
|
|
574
|
+
}
|
|
575
|
+
const nameColumn = options?.nameColumn ?? later;
|
|
576
|
+
if (nameColumn !== earlier && nameColumn !== later) {
|
|
577
|
+
throw ruleError({ site, rule: "check-rule-invalid", message: `nameColumn ${JSON.stringify(nameColumn)} must be one of the two ordered columns` });
|
|
578
|
+
}
|
|
579
|
+
const strict = options?.strict !== false;
|
|
580
|
+
const rule = nameColumn === later ? { kind: "order", left: second.name, op: strict ? "gt" : "gte", right: first.name } : { kind: "order", left: first.name, op: strict ? "lt" : "lte", right: second.name };
|
|
581
|
+
const name = options?.name ?? mintedName({ context, field: nameColumn === later ? second : first, rule: options?.rule ?? "order" });
|
|
582
|
+
return ruleCheck({ name, predicate: checkRulePredicate({ rule }), origin: site, minted: options?.name === undefined });
|
|
583
|
+
};
|
|
584
|
+
const presence = (config) => {
|
|
585
|
+
const site = `c.presence({ by: ${JSON.stringify(config?.by)} }) on "${context.model}"`;
|
|
586
|
+
if (config === null || typeof config !== "object")
|
|
587
|
+
throw ruleError({ site, rule: "presence-invalid", message: "takes { by, states }" });
|
|
588
|
+
assertRuleWord({ site, options: { ...config.rule === undefined ? {} : { rule: config.rule } } });
|
|
589
|
+
const by = ruleField({ context, name: config.by, site });
|
|
590
|
+
const byType = by.type;
|
|
591
|
+
if (!isEnumType(byType) || by.isOptional) {
|
|
592
|
+
throw ruleError({ site, rule: "presence-by-invalid", message: `"${by.name}" must be a REQUIRED enum column \u2014 a NULL state makes every IN unknown, and a CHECK passes on unknown`, meta: { model: context.model, field: by.name } });
|
|
593
|
+
}
|
|
594
|
+
const members = context.enums.find((entry) => entry.name === byType.enum)?.values;
|
|
595
|
+
if (members === undefined) {
|
|
596
|
+
throw ruleError({ site, rule: "presence-by-invalid", message: `enum "${byType.enum}" of "${by.name}" is not declared in the schema` });
|
|
597
|
+
}
|
|
598
|
+
const states = config.states;
|
|
599
|
+
if (states === null || typeof states !== "object")
|
|
600
|
+
throw ruleError({ site, rule: "presence-invalid", message: "states must map every enum member to { set, unset }" });
|
|
601
|
+
const missing = members.filter((member) => !Object.hasOwn(states, member));
|
|
602
|
+
const extra = Object.keys(states).filter((key) => !members.includes(key));
|
|
603
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
604
|
+
throw ruleError({
|
|
605
|
+
site,
|
|
606
|
+
rule: "presence-states-mismatch",
|
|
607
|
+
message: `states must list every member of enum "${byType.enum}" exactly once${missing.length === 0 ? "" : `; missing: ${preview({ values: missing })}`}${extra.length === 0 ? "" : `; not members: ${preview({ values: extra })}`}`,
|
|
608
|
+
meta: { missing, extra }
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const setIn = new Map;
|
|
612
|
+
const unsetIn = new Map;
|
|
613
|
+
for (const member of members) {
|
|
614
|
+
const state = states[member];
|
|
615
|
+
if (state === null || typeof state !== "object")
|
|
616
|
+
throw ruleError({ site, rule: "presence-invalid", message: `state "${member}" must be { set?, unset? }` });
|
|
617
|
+
const lists = { set: state.set ?? [], unset: state.unset ?? [] };
|
|
618
|
+
for (const [kind, list] of Object.entries(lists)) {
|
|
619
|
+
if (!Array.isArray(list))
|
|
620
|
+
throw ruleError({ site, rule: "presence-invalid", message: `state "${member}".${kind} must be a list of columns` });
|
|
621
|
+
if (new Set(list).size !== list.length)
|
|
622
|
+
throw ruleError({ site, rule: "check-rule-duplicate-field", message: `state "${member}".${kind} names a column twice` });
|
|
623
|
+
for (const name of list) {
|
|
624
|
+
const field = ruleField({ context, name, site });
|
|
625
|
+
if (field.name === by.name)
|
|
626
|
+
throw ruleError({ site, rule: "presence-invalid", message: `state "${member}" lists the state column "${by.name}" itself` });
|
|
627
|
+
if (!field.isOptional) {
|
|
628
|
+
throw ruleError({ site, rule: "presence-required-column", message: `"${field.name}" is required, so it is set in every state \u2014 a presence table lists optional columns only`, meta: { model: context.model, field: field.name } });
|
|
629
|
+
}
|
|
630
|
+
const target = kind === "set" ? setIn : unsetIn;
|
|
631
|
+
target.set(field.name, [...target.get(field.name) ?? [], member]);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
const both = lists.set.filter((name) => lists.unset.includes(name));
|
|
635
|
+
if (both.length > 0)
|
|
636
|
+
throw ruleError({ site, rule: "presence-conflict", message: `state "${member}" lists ${preview({ values: both })} as both set and unset` });
|
|
637
|
+
}
|
|
638
|
+
const columns = context.fields.filter((field) => setIn.has(field.name) || unsetIn.has(field.name));
|
|
639
|
+
if (columns.length === 0)
|
|
640
|
+
throw ruleError({ site, rule: "presence-empty", message: "names no column \u2014 it would hold nothing" });
|
|
641
|
+
const names = config.names ?? {};
|
|
642
|
+
for (const [column, name] of Object.entries(names)) {
|
|
643
|
+
if (!columns.some((field) => field.name === column))
|
|
644
|
+
throw ruleError({ site, rule: "presence-invalid", message: `names.${column} is not a column this presence table constrains` });
|
|
645
|
+
assertRuleWord({ site, options: { ...name === undefined ? {} : { name } } });
|
|
646
|
+
}
|
|
647
|
+
return columns.map((field) => {
|
|
648
|
+
const set = setIn.get(field.name) ?? [];
|
|
649
|
+
const unset = unsetIn.get(field.name) ?? [];
|
|
650
|
+
if (set.length === members.length) {
|
|
651
|
+
throw ruleError({ site, rule: "presence-constant-column", message: `"${field.name}" is set in every state \u2014 declare it required instead`, meta: { model: context.model, field: field.name } });
|
|
652
|
+
}
|
|
653
|
+
if (unset.length === members.length) {
|
|
654
|
+
throw ruleError({ site, rule: "presence-constant-column", message: `"${field.name}" is empty in every state \u2014 it can never hold a value`, meta: { model: context.model, field: field.name } });
|
|
655
|
+
}
|
|
656
|
+
const explicit = names[field.name];
|
|
657
|
+
return ruleCheck({
|
|
658
|
+
name: explicit ?? mintedName({ context, field, rule: config.rule ?? physicalColumn({ field: by }) }),
|
|
659
|
+
predicate: checkRulePredicate({ rule: { kind: "presence", by: by.name, field: field.name, set, unset } }),
|
|
660
|
+
origin: `presence table on "${context.model}.${by.name}" for column "${field.name}"`,
|
|
661
|
+
minted: explicit === undefined
|
|
662
|
+
});
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
...createPredicateBuilder(),
|
|
667
|
+
order,
|
|
668
|
+
allOrNone: group("allOrNone"),
|
|
669
|
+
exactlyOne: group("exactlyOne"),
|
|
670
|
+
atMostOne: group("atMostOne"),
|
|
671
|
+
anySet: group("anySet"),
|
|
672
|
+
presence
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
var EXACT_NUMBER = /^(-?)(\d+)(?:\.(\d+))?$/;
|
|
676
|
+
function compareBounds(params) {
|
|
677
|
+
const { left, right } = params;
|
|
678
|
+
if (typeof left === "number" && typeof right === "number")
|
|
679
|
+
return Number.isNaN(left - right) ? undefined : Math.sign(left - right);
|
|
680
|
+
const parse = (value) => {
|
|
681
|
+
const match = EXACT_NUMBER.exec(String(value));
|
|
682
|
+
if (match === null)
|
|
683
|
+
return;
|
|
684
|
+
const whole = match[2].replace(/^0+(?=\d)/, "");
|
|
685
|
+
const fraction = (match[3] ?? "").replace(/0+$/, "");
|
|
686
|
+
return { negative: match[1] === "-" && !(whole === "0" && fraction === ""), whole, fraction };
|
|
687
|
+
};
|
|
688
|
+
const a = parse(left);
|
|
689
|
+
const b = parse(right);
|
|
690
|
+
if (a === undefined || b === undefined)
|
|
691
|
+
return;
|
|
692
|
+
if (a.negative !== b.negative)
|
|
693
|
+
return a.negative ? -1 : 1;
|
|
694
|
+
let magnitude = a.whole.length - b.whole.length;
|
|
695
|
+
if (magnitude === 0 && a.whole !== b.whole)
|
|
696
|
+
magnitude = a.whole < b.whole ? -1 : 1;
|
|
697
|
+
if (magnitude === 0) {
|
|
698
|
+
const width = Math.max(a.fraction.length, b.fraction.length);
|
|
699
|
+
const x = a.fraction.padEnd(width, "0");
|
|
700
|
+
const y = b.fraction.padEnd(width, "0");
|
|
701
|
+
magnitude = x === y ? 0 : x < y ? -1 : 1;
|
|
702
|
+
}
|
|
703
|
+
return a.negative ? -Math.sign(magnitude) : Math.sign(magnitude);
|
|
704
|
+
}
|
|
705
|
+
function columnRuleChecks(params) {
|
|
706
|
+
const { model, context } = params;
|
|
707
|
+
const checks = [];
|
|
708
|
+
for (const [name, column] of model.__columns) {
|
|
709
|
+
const rules = column.__config.checks ?? [];
|
|
710
|
+
if (rules.length === 0)
|
|
711
|
+
continue;
|
|
712
|
+
for (const entry of rules) {
|
|
713
|
+
const site = `rule "${entry.rule}" on "${context.model}.${name}"`;
|
|
714
|
+
assertRuleWord({ site, options: { rule: entry.rule, ...entry.name === undefined ? {} : { name: entry.name } } });
|
|
715
|
+
const field = ruleField({ context, name, site });
|
|
716
|
+
const shape = entry.check;
|
|
717
|
+
let rule;
|
|
718
|
+
switch (shape.kind) {
|
|
719
|
+
case "compare":
|
|
720
|
+
rule = { kind: "compare", field: field.name, op: shape.op, value: shape.value };
|
|
721
|
+
break;
|
|
722
|
+
case "range":
|
|
723
|
+
if ((compareBounds({ left: shape.min, right: shape.max }) ?? 0) > 0) {
|
|
724
|
+
throw ruleError({ site, rule: "check-rule-range-bounds", message: `min ${String(shape.min)} is above max ${String(shape.max)} \u2014 no value could pass`, meta: { min: shape.min, max: shape.max } });
|
|
725
|
+
}
|
|
726
|
+
rule = { kind: "range", field: field.name, min: shape.min, max: shape.max };
|
|
727
|
+
break;
|
|
728
|
+
case "oneOf":
|
|
729
|
+
if (shape.values.length === 0)
|
|
730
|
+
throw ruleError({ site, rule: "check-rule-empty", message: "oneOf() has no values \u2014 an empty IN list is never what an author means" });
|
|
731
|
+
rule = { kind: "oneOf", field: field.name, values: shape.values };
|
|
732
|
+
break;
|
|
733
|
+
case "lengthAtMost":
|
|
734
|
+
if (!Number.isSafeInteger(shape.max) || shape.max < 0) {
|
|
735
|
+
throw ruleError({ site, rule: "check-rule-length-bound", message: `lengthAtMost(${String(shape.max)}) needs a non-negative whole number` });
|
|
736
|
+
}
|
|
737
|
+
rule = { kind: "lengthAtMost", field: field.name, max: shape.max };
|
|
738
|
+
break;
|
|
739
|
+
}
|
|
740
|
+
checks.push(ruleCheck({
|
|
741
|
+
name: entry.name ?? mintedName({ context, field, rule: entry.rule }),
|
|
742
|
+
predicate: checkRulePredicate({ rule }),
|
|
743
|
+
origin: site,
|
|
744
|
+
minted: entry.name === undefined
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return checks;
|
|
749
|
+
}
|
|
750
|
+
function isRuleCheck(entry) {
|
|
751
|
+
return entry !== null && typeof entry === "object" && "__type" in entry && entry.__type === "check";
|
|
752
|
+
}
|
|
753
|
+
function typedCheck(params) {
|
|
754
|
+
const { entry } = params;
|
|
755
|
+
return {
|
|
756
|
+
kind: "typed",
|
|
757
|
+
name: entry.name,
|
|
758
|
+
predicate: entry.predicate,
|
|
759
|
+
...entry.__origin === undefined ? {} : { origin: entry.__origin },
|
|
760
|
+
...entry.__minted === true ? { minted: true } : {}
|
|
761
|
+
};
|
|
762
|
+
}
|
|
356
763
|
function evaluateChecks(params) {
|
|
357
764
|
const { checks, model } = params;
|
|
358
765
|
if (checks === undefined)
|
|
@@ -360,28 +767,61 @@ function evaluateChecks(params) {
|
|
|
360
767
|
if (typeof checks !== "function") {
|
|
361
768
|
return checks.map((check) => ({ kind: "raw", name: check.name, expression: check.expression }));
|
|
362
769
|
}
|
|
363
|
-
const
|
|
770
|
+
const builder = params.context === undefined ? contextlessChecksBuilder({ model }) : createChecksBuilder({ context: params.context });
|
|
771
|
+
const declared = checks(builder);
|
|
364
772
|
return declared.map((entry, index) => {
|
|
365
|
-
if (entry !== null && typeof entry === "object" && !("__type" in entry) && typeof entry.name === "string" && "expression" in entry && typeof entry.expression === "string") {
|
|
773
|
+
if (entry !== null && typeof entry === "object" && !("__type" in entry) && "name" in entry && typeof entry.name === "string" && "expression" in entry && typeof entry.expression === "string") {
|
|
366
774
|
return { kind: "raw", name: entry.name, expression: entry.expression };
|
|
367
775
|
}
|
|
368
|
-
if (
|
|
369
|
-
throw new
|
|
776
|
+
if (Array.isArray(entry)) {
|
|
777
|
+
throw new VibeError3({
|
|
778
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
779
|
+
message: `Model "${model}": checks[${index}] is a list \u2014 spread c.presence(...) into the checks: [...c.presence({ \u2026 })]`,
|
|
780
|
+
meta: { rule: "predicate-unnamed-check", model, index }
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
if (!isRuleCheck(entry)) {
|
|
784
|
+
throw new VibeError3({
|
|
370
785
|
code: "VIBE_SCHEMA_INVALID",
|
|
371
|
-
message: `Model "${model}": checks[${index}] must be c.named("name", predicate) or a raw { name, expression } CHECK`,
|
|
786
|
+
message: `Model "${model}": checks[${index}] must be c.named("name", predicate), a check rule (c.order, c.allOrNone, \u2026) or a raw { name, expression } CHECK`,
|
|
372
787
|
meta: { rule: "predicate-unnamed-check", model, index }
|
|
373
788
|
});
|
|
374
789
|
}
|
|
375
|
-
return {
|
|
790
|
+
return typedCheck({ entry });
|
|
376
791
|
});
|
|
377
792
|
}
|
|
793
|
+
function contextlessChecksBuilder(params) {
|
|
794
|
+
const refuse = (name) => () => {
|
|
795
|
+
throw new VibeError3({
|
|
796
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
797
|
+
message: `Model "${params.model}": c.${name}() needs the model's fields \u2014 compile the schema with toIR (or compileModelChecks)`,
|
|
798
|
+
meta: { rule: "check-rule-context", model: params.model }
|
|
799
|
+
});
|
|
800
|
+
};
|
|
801
|
+
return {
|
|
802
|
+
...createPredicateBuilder(),
|
|
803
|
+
order: refuse("order"),
|
|
804
|
+
allOrNone: refuse("allOrNone"),
|
|
805
|
+
exactlyOne: refuse("exactlyOne"),
|
|
806
|
+
atMostOne: refuse("atMostOne"),
|
|
807
|
+
anySet: refuse("anySet"),
|
|
808
|
+
presence: refuse("presence")
|
|
809
|
+
};
|
|
810
|
+
}
|
|
378
811
|
function compileExclusions(params) {
|
|
379
812
|
const specs = typeof params.exclusions === "function" ? params.exclusions(createPredicateBuilder()) : params.exclusions ?? [];
|
|
380
813
|
return specs.map((exclusion) => compileExclusion({ exclusion, model: params.model, dialect: params.dialect }));
|
|
381
814
|
}
|
|
382
815
|
function compileChecks(params) {
|
|
383
816
|
const { checks, model, dialect, enums } = params;
|
|
384
|
-
|
|
817
|
+
const context = { model: model.name, table: params.table ?? model.name, fields: model.fields, enums };
|
|
818
|
+
const evaluated = evaluateChecks({ checks, model: model.name, context });
|
|
819
|
+
assertRuleNames({ checks: evaluated, model: model.name, dialect });
|
|
820
|
+
return renderChecks({ checks: evaluated, model, dialect, enums });
|
|
821
|
+
}
|
|
822
|
+
function renderChecks(params) {
|
|
823
|
+
const { model, dialect, enums } = params;
|
|
824
|
+
return params.checks.map((check) => check.kind === "raw" ? { name: check.name, expression: check.expression } : {
|
|
385
825
|
name: check.name,
|
|
386
826
|
expression: renderPredicate({
|
|
387
827
|
predicate: check.predicate,
|
|
@@ -389,15 +829,50 @@ function compileChecks(params) {
|
|
|
389
829
|
dialect,
|
|
390
830
|
enums,
|
|
391
831
|
usage: "check",
|
|
392
|
-
site: `CHECK "${check.name}"`
|
|
832
|
+
site: check.origin === undefined ? `CHECK "${check.name}"` : `CHECK "${check.name}" (${check.origin})`
|
|
393
833
|
}),
|
|
394
834
|
predicate: check.predicate
|
|
395
835
|
});
|
|
396
836
|
}
|
|
837
|
+
function assertRuleNames(params) {
|
|
838
|
+
const { checks, model, dialect } = params;
|
|
839
|
+
const seen = new Map;
|
|
840
|
+
const limit = IDENTIFIER_LIMITS[dialect.name];
|
|
841
|
+
for (const check of checks) {
|
|
842
|
+
const origin = check.kind === "typed" ? check.origin : undefined;
|
|
843
|
+
const previous = seen.get(check.name);
|
|
844
|
+
const previousOrigin = previous?.kind === "typed" ? previous.origin : undefined;
|
|
845
|
+
if (previous !== undefined && (origin !== undefined || previousOrigin !== undefined)) {
|
|
846
|
+
throw new VibeError3({
|
|
847
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
848
|
+
message: `Model "${model}": CHECK name "${check.name}" is produced twice (${previousOrigin ?? "a hand-written CHECK"}; ${origin ?? "a hand-written CHECK"}) \u2014 names key the migration diff. Pass { name: "\u2026" } to one of the rules`,
|
|
849
|
+
meta: { rule: "duplicate-check", model, name: check.name }
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
seen.set(check.name, check);
|
|
853
|
+
if (check.kind === "typed" && check.minted === true && limit !== undefined && identifierLength({ name: check.name, dialect: dialect.name }) > limit.max) {
|
|
854
|
+
throw new VibeError3({
|
|
855
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
856
|
+
message: `Model "${model}": generated CHECK name "${check.name}" (${origin ?? "check rule"}) is longer than ${dialect.name}'s ${limit.max}-${limit.unit} identifier limit \u2014 pass a shorter { name: "\u2026" } to the rule`,
|
|
857
|
+
meta: { rule: "identifier-too-long", model, name: check.name }
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function compileModelChecks(params) {
|
|
863
|
+
const { model, predicateModel, dialect, enums } = params;
|
|
864
|
+
const context = { model: model.__name, table: model.__map ?? model.__name, fields: predicateModel.fields, enums };
|
|
865
|
+
const checks = [
|
|
866
|
+
...evaluateChecks({ checks: model.__checks, model: model.__name, context }),
|
|
867
|
+
...columnRuleChecks({ model, context }).map((entry) => typedCheck({ entry }))
|
|
868
|
+
];
|
|
869
|
+
assertRuleNames({ checks, model: model.__name, dialect });
|
|
870
|
+
return renderChecks({ checks, model: predicateModel, dialect, enums });
|
|
871
|
+
}
|
|
397
872
|
function compileIndexPredicate(params) {
|
|
398
873
|
const { where, wherePredicate, columns, model, dialect, enums } = params;
|
|
399
874
|
if (where !== undefined && wherePredicate !== undefined) {
|
|
400
|
-
throw new
|
|
875
|
+
throw new VibeError3({
|
|
401
876
|
code: "VIBE_SCHEMA_INVALID",
|
|
402
877
|
message: `Index on "${model.name}" [${columns.join(", ")}] carries both a raw where: string and a typed .where() \u2014 an index has ONE predicate`,
|
|
403
878
|
meta: { rule: "predicate-duplicate-where", model: model.name, columns: [...columns] }
|
|
@@ -423,17 +898,22 @@ function compileIndexPredicate(params) {
|
|
|
423
898
|
// packages/dsl/src/to-ir.ts
|
|
424
899
|
import {
|
|
425
900
|
EMPTY_EXTENSION_REGISTRY,
|
|
426
|
-
VibeError as
|
|
901
|
+
VibeError as VibeError6,
|
|
427
902
|
assertValidSchemaIR,
|
|
903
|
+
assertIndexExpressions,
|
|
904
|
+
indexKeyFieldNames,
|
|
428
905
|
exactIntegerLiteral,
|
|
906
|
+
generatedColumnFromSql as generatedColumnFromSql2,
|
|
907
|
+
isCanonicalLocalDate,
|
|
429
908
|
providerToDialect,
|
|
430
909
|
scalarListDefault,
|
|
431
910
|
serializeJsonDefault,
|
|
432
911
|
mergeExtensionDeclarations,
|
|
433
912
|
normalizeJsonSchemaRef,
|
|
913
|
+
normalizeValidatorRef,
|
|
434
914
|
unsupportedCapability
|
|
435
915
|
} from "@vibeorm/schema";
|
|
436
|
-
import { getSqlDialect } from "@vibeorm/sql";
|
|
916
|
+
import { getSqlDialect, renderSchemaExpression } from "@vibeorm/sql";
|
|
437
917
|
|
|
438
918
|
// packages/dsl/src/field-type.ts
|
|
439
919
|
import { fieldTypeIdentity } from "@vibeorm/schema";
|
|
@@ -441,6 +921,7 @@ function columnFieldType(params) {
|
|
|
441
921
|
const { column } = params;
|
|
442
922
|
switch (column.__columnType) {
|
|
443
923
|
case "string":
|
|
924
|
+
case "localDate":
|
|
444
925
|
return "String";
|
|
445
926
|
case "int":
|
|
446
927
|
return "Int";
|
|
@@ -467,14 +948,17 @@ function columnFieldType(params) {
|
|
|
467
948
|
}
|
|
468
949
|
}
|
|
469
950
|
function fieldTypeKey(params) {
|
|
470
|
-
return fieldTypeIdentity({
|
|
951
|
+
return fieldTypeIdentity({
|
|
952
|
+
type: columnFieldType({ column: params.column }),
|
|
953
|
+
...params.column.__columnType === "localDate" ? { nativeType: "Date" } : {}
|
|
954
|
+
});
|
|
471
955
|
}
|
|
472
956
|
|
|
473
957
|
// packages/dsl/src/relation-plan.ts
|
|
474
|
-
import { VibeError as
|
|
958
|
+
import { VibeError as VibeError5, indexKeyDefinitionKey, isSqlObjectIdentity, predicateKey } from "@vibeorm/schema";
|
|
475
959
|
|
|
476
960
|
// packages/dsl/src/model.ts
|
|
477
|
-
import { VibeError as
|
|
961
|
+
import { VibeError as VibeError4 } from "@vibeorm/schema";
|
|
478
962
|
|
|
479
963
|
// packages/dsl/src/naming.ts
|
|
480
964
|
function capitalize(params) {
|
|
@@ -516,7 +1000,7 @@ function createIndexBuilder() {
|
|
|
516
1000
|
const partial = (index) => {
|
|
517
1001
|
const where = (predicate) => {
|
|
518
1002
|
if (index.__where !== undefined) {
|
|
519
|
-
throw new
|
|
1003
|
+
throw new VibeError4({
|
|
520
1004
|
code: "VIBE_SCHEMA_INVALID",
|
|
521
1005
|
message: `Index on [${index.__columns.join(", ")}] already carries a raw where: predicate \u2014 an index has ONE predicate, so use either the raw string or .where(), not both`,
|
|
522
1006
|
meta: { rule: "predicate-duplicate-where", columns: [...index.__columns] }
|
|
@@ -531,53 +1015,92 @@ function createIndexBuilder() {
|
|
|
531
1015
|
configurable: true
|
|
532
1016
|
});
|
|
533
1017
|
};
|
|
1018
|
+
const freeze = (value) => {
|
|
1019
|
+
if (value !== null && typeof value === "object") {
|
|
1020
|
+
for (const child of Object.values(value))
|
|
1021
|
+
freeze(child);
|
|
1022
|
+
Object.freeze(value);
|
|
1023
|
+
}
|
|
1024
|
+
return value;
|
|
1025
|
+
};
|
|
1026
|
+
const keyTerms = (params) => {
|
|
1027
|
+
if (params.keys.every((key) => typeof key === "string"))
|
|
1028
|
+
return { __columns: [...params.keys] };
|
|
1029
|
+
const terms = params.keys.map((entry) => {
|
|
1030
|
+
const sorted = typeof entry === "object" && entry !== null && "__type" in entry && entry.__type === "sortedIndexKey";
|
|
1031
|
+
const key = sorted ? entry.key : entry;
|
|
1032
|
+
const direction = sorted ? { sort: entry.sort } : {};
|
|
1033
|
+
if (typeof key === "string")
|
|
1034
|
+
return { kind: "field", field: { name: key, ...direction } };
|
|
1035
|
+
if (typeof key === "object" && key !== null && "__type" in key && key.__type === "rawIndexExpression") {
|
|
1036
|
+
if (typeof key.text !== "string" || key.text.trim() === "" || key.text.includes("\x00"))
|
|
1037
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "i.sql requires nonempty NUL-free scalar SQL", meta: { rule: "expression-index-raw-sql" } });
|
|
1038
|
+
return { kind: "raw", sql: key.text, ...direction };
|
|
1039
|
+
}
|
|
1040
|
+
return { kind: "expression", expression: toPredicateExpression({ value: key, at: "index key" }), ...direction };
|
|
1041
|
+
});
|
|
1042
|
+
const computed = terms.some((term) => term.kind !== "field");
|
|
1043
|
+
return { __columns: computed ? [] : terms.flatMap((term) => term.kind === "field" ? [term.field.name] : []), __keys: freeze(terms) };
|
|
1044
|
+
};
|
|
1045
|
+
const scalar = createPredicateBuilder();
|
|
534
1046
|
const createIndex = (params) => ({
|
|
535
1047
|
__type: "index",
|
|
536
|
-
|
|
1048
|
+
...keyTerms({ keys: params.keys }),
|
|
537
1049
|
__indexType: params.type,
|
|
538
1050
|
...params.map === undefined ? {} : { __map: params.map },
|
|
539
1051
|
...params.where === undefined ? {} : { __where: params.where },
|
|
540
1052
|
...params.ops === undefined ? {} : { __ops: params.ops }
|
|
541
1053
|
});
|
|
542
1054
|
return {
|
|
1055
|
+
field: scalar.field,
|
|
1056
|
+
add: scalar.add,
|
|
1057
|
+
sub: scalar.sub,
|
|
1058
|
+
mul: scalar.mul,
|
|
1059
|
+
lower: (value) => scalar.lower(typeof value === "string" ? scalar.field(value) : value),
|
|
1060
|
+
upper: (value) => scalar.upper(typeof value === "string" ? scalar.field(value) : value),
|
|
1061
|
+
asc: (key) => ({ __type: "sortedIndexKey", key, sort: "asc" }),
|
|
1062
|
+
desc: (key) => ({ __type: "sortedIndexKey", key, sort: "desc" }),
|
|
1063
|
+
sql: ({ text }) => ({ __type: "rawIndexExpression", text }),
|
|
543
1064
|
on(...args) {
|
|
544
1065
|
const [first, second] = args;
|
|
545
1066
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
546
|
-
|
|
1067
|
+
const options = second;
|
|
1068
|
+
return partial(createIndex({ keys: first, type: options.type, where: options.where, map: options.map, ops: options.ops }));
|
|
547
1069
|
}
|
|
548
|
-
return partial(createIndex({
|
|
1070
|
+
return partial(createIndex({ keys: args }));
|
|
549
1071
|
},
|
|
550
|
-
index(...
|
|
551
|
-
return partial(createIndex({
|
|
1072
|
+
index(...keys) {
|
|
1073
|
+
return partial(createIndex({ keys }));
|
|
552
1074
|
},
|
|
553
1075
|
unique(...args) {
|
|
554
1076
|
const [first, second] = args;
|
|
555
1077
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
556
1078
|
if ("ops" in second && second.ops !== undefined) {
|
|
557
|
-
throw new
|
|
1079
|
+
throw new VibeError4({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Custom unique operator classes can change client selector equality", meta: { feature: "uniqueIndexOperatorClasses" } });
|
|
558
1080
|
}
|
|
1081
|
+
const options = second;
|
|
559
1082
|
return partial({
|
|
560
1083
|
__type: "unique",
|
|
561
|
-
|
|
562
|
-
...
|
|
563
|
-
...
|
|
564
|
-
...
|
|
565
|
-
...
|
|
1084
|
+
...keyTerms({ keys: first }),
|
|
1085
|
+
...options.map === undefined ? {} : { __map: options.map },
|
|
1086
|
+
...options.where === undefined ? {} : { __where: options.where },
|
|
1087
|
+
...options.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
|
|
1088
|
+
...options.name === undefined ? {} : { __clientName: options.name }
|
|
566
1089
|
});
|
|
567
1090
|
}
|
|
568
|
-
return partial({ __type: "unique",
|
|
1091
|
+
return partial({ __type: "unique", ...keyTerms({ keys: args }) });
|
|
569
1092
|
},
|
|
570
1093
|
gin(...columns) {
|
|
571
|
-
return partial(createIndex({ columns, type: "Gin" }));
|
|
1094
|
+
return partial(createIndex({ keys: columns, type: "Gin" }));
|
|
572
1095
|
},
|
|
573
1096
|
gist(...columns) {
|
|
574
|
-
return partial(createIndex({ columns, type: "Gist" }));
|
|
1097
|
+
return partial(createIndex({ keys: columns, type: "Gist" }));
|
|
575
1098
|
},
|
|
576
1099
|
hash(...columns) {
|
|
577
|
-
return partial(createIndex({ columns, type: "Hash" }));
|
|
1100
|
+
return partial(createIndex({ keys: columns, type: "Hash" }));
|
|
578
1101
|
},
|
|
579
1102
|
brin(...columns) {
|
|
580
|
-
return partial(createIndex({ columns, type: "Brin" }));
|
|
1103
|
+
return partial(createIndex({ keys: columns, type: "Brin" }));
|
|
581
1104
|
}
|
|
582
1105
|
};
|
|
583
1106
|
}
|
|
@@ -621,6 +1144,7 @@ function create(config) {
|
|
|
621
1144
|
__primaryKey: config.primaryKey,
|
|
622
1145
|
__exclusions: config.exclusions,
|
|
623
1146
|
__checks: config.checks,
|
|
1147
|
+
__rowRules: config.rowRules,
|
|
624
1148
|
__extensions: config.extensions,
|
|
625
1149
|
__policy: config.policy,
|
|
626
1150
|
__comment: config.comment
|
|
@@ -649,7 +1173,7 @@ function detectOneToOnePairs(params) {
|
|
|
649
1173
|
for (const [modelName, modelRelations] of Object.entries(relations)) {
|
|
650
1174
|
for (const [relName, rel] of Object.entries(modelRelations)) {
|
|
651
1175
|
if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
|
|
652
|
-
throw new
|
|
1176
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
|
|
653
1177
|
}
|
|
654
1178
|
if (rel.__relationType === "one") {
|
|
655
1179
|
allOnes.push({ modelName, relName, rel });
|
|
@@ -724,7 +1248,7 @@ function sameColumnTuple(params) {
|
|
|
724
1248
|
function resolveFkOwner(params) {
|
|
725
1249
|
const { a, b, models } = params;
|
|
726
1250
|
const ownershipError = () => {
|
|
727
|
-
throw new
|
|
1251
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: `Ambiguous foreign-key ownership between ${a.modelName}.${a.relName} and ${b.modelName}.${b.relName}; set ownsForeignKey: true on the foreign-key side`, meta: { rule: "ambiguous-foreign-key-owner" } });
|
|
728
1252
|
};
|
|
729
1253
|
const aOwns = a.rel.__ownsForeignKey;
|
|
730
1254
|
const bOwns = b.rel.__ownsForeignKey;
|
|
@@ -893,28 +1417,31 @@ function planIndexes(params) {
|
|
|
893
1417
|
}
|
|
894
1418
|
}
|
|
895
1419
|
for (const index of evaluateIndexes({ model })) {
|
|
1420
|
+
if (index.__keys?.some((term) => term.kind !== "field") && index.__ops !== undefined)
|
|
1421
|
+
throw new VibeError5({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Custom operator classes are not admitted on expression-bearing keys", meta: { feature: "expressionIndexOperatorClasses", rule: "expression-index-operator-classes", model: model.__name } });
|
|
896
1422
|
if (index.__ops !== undefined) {
|
|
897
1423
|
if (index.__ops === null || typeof index.__ops !== "object" || Array.isArray(index.__ops))
|
|
898
|
-
throw new
|
|
1424
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: "Index ops must be a field-keyed qualified-name map", meta: { rule: "index-operator-class-map", model: model.__name } });
|
|
899
1425
|
if (index.__type === "unique")
|
|
900
|
-
throw new
|
|
1426
|
+
throw new VibeError5({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Custom unique operator classes can change client selector equality", meta: { feature: "uniqueIndexOperatorClasses", model: model.__name } });
|
|
901
1427
|
for (const [field, operatorClass] of Object.entries(index.__ops)) {
|
|
902
1428
|
if (!index.__columns.includes(field))
|
|
903
|
-
throw new
|
|
1429
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: "Operator class names a field outside the index", meta: { rule: "index-operator-class-field", model: model.__name, field } });
|
|
904
1430
|
if (operatorClass !== undefined && !isSqlObjectIdentity({ kind: "table", name: operatorClass }))
|
|
905
|
-
throw new
|
|
1431
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: "Index operator class must be a qualified SQL name { namespace, name }", meta: { rule: "index-operator-class-identity", model: model.__name, field } });
|
|
906
1432
|
}
|
|
907
1433
|
}
|
|
908
1434
|
const operatorClasses = index.__columns.map((field) => index.__ops?.[field]);
|
|
909
1435
|
const operatorSuffix = operatorClasses.some((operatorClass) => operatorClass !== undefined) ? `|ops:${JSON.stringify([index.__indexType ?? "BTree", operatorClasses.map((operatorClass) => operatorClass === undefined ? null : [operatorClass.namespace, operatorClass.name])])}` : "";
|
|
910
1436
|
const predicateSuffix = index.__where !== undefined ? `|where:${index.__where}` : index.__wherePredicate === undefined ? "" : `|where:${predicateKey({ predicate: index.__wherePredicate })}`;
|
|
911
|
-
const key = index.__columns.join(",") + predicateSuffix + operatorSuffix
|
|
1437
|
+
const key = index.__keys === undefined ? index.__columns.join(",") + predicateSuffix + operatorSuffix : index.__keys.some((term) => term.kind !== "field") ? indexKeyDefinitionKey({ index: { kind: index.__type === "unique" ? "unique" : (index.__indexType ?? "BTree").toLowerCase(), fields: [], expressions: index.__keys, ...index.__map === undefined ? {} : { name: index.__map }, ...index.__where === undefined ? {} : { where: index.__where }, ...index.__wherePredicate === undefined ? {} : { wherePredicate: index.__wherePredicate }, ...index.__nullsNotDistinct === undefined ? {} : { nullsNotDistinct: index.__nullsNotDistinct } } }) : `${index.__columns.join(",")}${predicateSuffix}${operatorSuffix}|sort:${JSON.stringify(index.__keys)}`;
|
|
912
1438
|
const existing = byKey.get(key);
|
|
913
1439
|
const isUnique = index.__type === "unique";
|
|
914
1440
|
if (existing === undefined || isUnique) {
|
|
915
1441
|
byKey.set(key, {
|
|
916
1442
|
isUnique,
|
|
917
1443
|
columns: [...index.__columns],
|
|
1444
|
+
...index.__keys === undefined ? {} : { keys: index.__keys },
|
|
918
1445
|
indexType: index.__indexType,
|
|
919
1446
|
...index.__ops === undefined ? {} : { ops: index.__ops },
|
|
920
1447
|
...index.__map === undefined ? {} : { map: index.__map },
|
|
@@ -941,7 +1468,7 @@ function referenceableKeys(params) {
|
|
|
941
1468
|
if (model.__primaryKey !== undefined && model.__primaryKey.length > 0)
|
|
942
1469
|
keys.push([...model.__primaryKey]);
|
|
943
1470
|
for (const index of evaluateIndexes({ model })) {
|
|
944
|
-
if (index.__type === "unique" && index.__where === undefined && index.__wherePredicate === undefined) {
|
|
1471
|
+
if (index.__type === "unique" && !index.__keys?.some((term) => term.kind !== "field") && index.__where === undefined && index.__wherePredicate === undefined) {
|
|
945
1472
|
keys.push([...index.__columns]);
|
|
946
1473
|
}
|
|
947
1474
|
}
|
|
@@ -953,7 +1480,7 @@ function assertForeignKeyIntegrity(params) {
|
|
|
953
1480
|
const references = relation.references ?? [];
|
|
954
1481
|
const at = `${relation.model}.${relation.name}`;
|
|
955
1482
|
const refuse = (message, rule, meta = {}) => {
|
|
956
|
-
throw new
|
|
1483
|
+
throw new VibeError5({
|
|
957
1484
|
code: "VIBE_SCHEMA_INVALID",
|
|
958
1485
|
message: `Relation ${at}: ${message}`,
|
|
959
1486
|
meta: { rule, model: relation.model, relation: relation.name, ...meta }
|
|
@@ -1003,12 +1530,14 @@ function assertForeignKeyIntegrity(params) {
|
|
|
1003
1530
|
}
|
|
1004
1531
|
}
|
|
1005
1532
|
function relationLookups(params) {
|
|
1533
|
+
const inputs = [];
|
|
1006
1534
|
const incoming = new Map;
|
|
1007
1535
|
for (const [modelName, relations] of Object.entries(params.schema.__relations)) {
|
|
1008
1536
|
for (const [relName, rel] of Object.entries(relations)) {
|
|
1009
1537
|
if (rel.__relationType !== "one")
|
|
1010
1538
|
continue;
|
|
1011
1539
|
const target = rel.__targetModel;
|
|
1540
|
+
inputs.push(modelName, relName, rel, target);
|
|
1012
1541
|
const entries = incoming.get(target);
|
|
1013
1542
|
const entry = { modelName, relName, rel };
|
|
1014
1543
|
if (entries === undefined)
|
|
@@ -1020,6 +1549,7 @@ function relationLookups(params) {
|
|
|
1020
1549
|
const parents = new Map;
|
|
1021
1550
|
for (const [name, model] of params.schema.__models) {
|
|
1022
1551
|
for (const child of model.__children.values()) {
|
|
1552
|
+
inputs.push(child.__name, name, model);
|
|
1023
1553
|
const entries = parents.get(child.__name);
|
|
1024
1554
|
if (entries === undefined)
|
|
1025
1555
|
parents.set(child.__name, [[name, model]]);
|
|
@@ -1027,7 +1557,35 @@ function relationLookups(params) {
|
|
|
1027
1557
|
entries.push([name, model]);
|
|
1028
1558
|
}
|
|
1029
1559
|
}
|
|
1030
|
-
return { incoming, parents };
|
|
1560
|
+
return { incoming, parents, inputs };
|
|
1561
|
+
}
|
|
1562
|
+
function lookupsCurrent(params) {
|
|
1563
|
+
const { schema, inputs } = params;
|
|
1564
|
+
let at = 0;
|
|
1565
|
+
const byModel = schema.__relations;
|
|
1566
|
+
for (const modelName in byModel) {
|
|
1567
|
+
if (!Object.hasOwn(byModel, modelName))
|
|
1568
|
+
continue;
|
|
1569
|
+
const relations = byModel[modelName];
|
|
1570
|
+
for (const relName in relations) {
|
|
1571
|
+
if (!Object.hasOwn(relations, relName))
|
|
1572
|
+
continue;
|
|
1573
|
+
const rel = relations[relName];
|
|
1574
|
+
if (rel.__relationType !== "one")
|
|
1575
|
+
continue;
|
|
1576
|
+
if (inputs[at] !== modelName || inputs[at + 1] !== relName || inputs[at + 2] !== rel || inputs[at + 3] !== rel.__targetModel)
|
|
1577
|
+
return false;
|
|
1578
|
+
at += 4;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
for (const [name, model] of schema.__models) {
|
|
1582
|
+
for (const child of model.__children.values()) {
|
|
1583
|
+
if (inputs[at] !== child.__name || inputs[at + 1] !== name || inputs[at + 2] !== model)
|
|
1584
|
+
return false;
|
|
1585
|
+
at += 3;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return at === inputs.length;
|
|
1031
1589
|
}
|
|
1032
1590
|
function planSchema(params) {
|
|
1033
1591
|
const { schema } = params;
|
|
@@ -1041,7 +1599,8 @@ function planSchema(params) {
|
|
|
1041
1599
|
let refreshLookups = false;
|
|
1042
1600
|
for (const [modelName, model] of schema.__models) {
|
|
1043
1601
|
if (refreshLookups) {
|
|
1044
|
-
|
|
1602
|
+
if (!lookupsCurrent({ schema, inputs: lookups.inputs }))
|
|
1603
|
+
lookups = relationLookups({ schema });
|
|
1045
1604
|
refreshLookups = false;
|
|
1046
1605
|
}
|
|
1047
1606
|
const explicit = planExplicitRelations({
|
|
@@ -1113,18 +1672,41 @@ function idDefault(params) {
|
|
|
1113
1672
|
}
|
|
1114
1673
|
function fieldDefault(params) {
|
|
1115
1674
|
const { column } = params;
|
|
1675
|
+
if (column.__columnType === "localDate") {
|
|
1676
|
+
const { default: value, dbgenerated, autoincrement, updatedAt, array, collation } = column.__config;
|
|
1677
|
+
if (array === true || updatedAt === true || autoincrement === true || dbgenerated !== undefined || collation !== undefined) {
|
|
1678
|
+
throw new VibeError6({
|
|
1679
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1680
|
+
message: `Local-date column "${column.__name ?? "localDate"}" requires a scalar calendar day and an optional canonical YYYY-MM-DD literal default; lists, collation, updatedAt and generated defaults are not supported`,
|
|
1681
|
+
meta: { issues: [{ rule: "local-date-declaration", field: column.__name }] }
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
if (value === undefined)
|
|
1685
|
+
return;
|
|
1686
|
+
const candidate = { value };
|
|
1687
|
+
if (!isCanonicalLocalDate(candidate))
|
|
1688
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `Local-date default on "${column.__name ?? "localDate"}" must be a canonical YYYY-MM-DD calendar-day literal in years 0001\u20139999` });
|
|
1689
|
+
return { kind: "literal", value: candidate.value };
|
|
1690
|
+
}
|
|
1116
1691
|
if (column.__columnType === "id") {
|
|
1117
|
-
|
|
1118
|
-
|
|
1692
|
+
const strategy = column.__config.type;
|
|
1693
|
+
const dbgenerated = column.__config.dbgenerated;
|
|
1694
|
+
if (column.__config.default !== undefined || column.__config.autoincrement !== undefined)
|
|
1695
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
|
|
1696
|
+
if (dbgenerated !== undefined) {
|
|
1697
|
+
if (strategy !== "none" || typeof dbgenerated !== "string")
|
|
1698
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `An id with a database default needs dsl.id({ type: "none" }); the "${String(strategy)}" generator strategy would conflict with it`, meta: { issues: [{ rule: "id-dbgenerated-strategy", column: column.__name, strategy }] } });
|
|
1699
|
+
return { kind: "dbgenerated", sql: dbgenerated };
|
|
1700
|
+
}
|
|
1119
1701
|
return idDefault({ column });
|
|
1120
1702
|
}
|
|
1121
1703
|
const { dbgenerated, default: value, autoincrement } = column.__config;
|
|
1122
1704
|
if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
|
|
1123
|
-
throw new
|
|
1705
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
|
|
1124
1706
|
}
|
|
1125
1707
|
if (autoincrement === true) {
|
|
1126
1708
|
if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
|
|
1127
|
-
throw new
|
|
1709
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
|
|
1128
1710
|
}
|
|
1129
1711
|
return { kind: "autoincrement" };
|
|
1130
1712
|
}
|
|
@@ -1134,7 +1716,7 @@ function fieldDefault(params) {
|
|
|
1134
1716
|
return;
|
|
1135
1717
|
if (column.__config.array === true) {
|
|
1136
1718
|
if (!Array.isArray(value))
|
|
1137
|
-
throw new
|
|
1719
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
|
|
1138
1720
|
return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
|
|
1139
1721
|
}
|
|
1140
1722
|
if (column.__columnType === "json" && typeof value !== "string") {
|
|
@@ -1149,20 +1731,20 @@ function fieldDefault(params) {
|
|
|
1149
1731
|
return { kind: "enumValue", value };
|
|
1150
1732
|
}
|
|
1151
1733
|
if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
|
|
1152
|
-
throw new
|
|
1734
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
|
|
1153
1735
|
}
|
|
1154
1736
|
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
1155
1737
|
return { kind: "literal", value };
|
|
1156
1738
|
}
|
|
1157
1739
|
if (typeof value === "bigint")
|
|
1158
1740
|
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
1159
|
-
throw new
|
|
1741
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
|
|
1160
1742
|
}
|
|
1161
1743
|
function datetimeNativeType(params) {
|
|
1162
1744
|
const { tz, precision } = params.config;
|
|
1163
1745
|
if (precision !== undefined && (tz !== true || !Number.isInteger(precision) || precision < 0 || precision > 6)) {
|
|
1164
1746
|
const at = params.column === undefined ? "A datetime column" : `Column "${params.column}"`;
|
|
1165
|
-
throw new
|
|
1747
|
+
throw new VibeError6({
|
|
1166
1748
|
code: "VIBE_SCHEMA_INVALID",
|
|
1167
1749
|
message: `${at}: a datetime precision must be an integer from 0 to 6 on a tz() column, as in dsl.datetime().tz({ precision: 3 })`,
|
|
1168
1750
|
meta: { issues: [{ rule: "datetime-precision", column: params.column, precision, tz: tz === true }] }
|
|
@@ -1172,14 +1754,51 @@ function datetimeNativeType(params) {
|
|
|
1172
1754
|
return;
|
|
1173
1755
|
return precision === undefined ? "Timestamptz" : `Timestamptz(${precision})`;
|
|
1174
1756
|
}
|
|
1757
|
+
function stringNativeType(params) {
|
|
1758
|
+
const { maxLength, text, uuid } = params.config;
|
|
1759
|
+
if (uuid === true) {
|
|
1760
|
+
if (maxLength !== undefined || text === true) {
|
|
1761
|
+
const at = params.column === undefined ? "A string column" : `Column "${params.column}"`;
|
|
1762
|
+
throw new VibeError6({
|
|
1763
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1764
|
+
message: `${at}: uuid() is the column's type and cannot be combined with maxLength or text`,
|
|
1765
|
+
meta: { issues: [{ rule: "string-native-type", column: params.column }] }
|
|
1766
|
+
});
|
|
1767
|
+
}
|
|
1768
|
+
return "Uuid";
|
|
1769
|
+
}
|
|
1770
|
+
if (typeof maxLength === "number")
|
|
1771
|
+
return `VarChar(${maxLength})`;
|
|
1772
|
+
if (text === true)
|
|
1773
|
+
return "Text";
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
function idNativeType(params) {
|
|
1777
|
+
if (params.config.uuid !== true)
|
|
1778
|
+
return;
|
|
1779
|
+
if (params.config.type === "autoincrement") {
|
|
1780
|
+
throw new VibeError6({
|
|
1781
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1782
|
+
message: `${params.column === undefined ? "An id" : `Id "${params.column}"`}: uuid() needs a String id; an autoincrement id is an Int`,
|
|
1783
|
+
meta: { issues: [{ rule: "id-native-type", column: params.column }] }
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
return "Uuid";
|
|
1787
|
+
}
|
|
1175
1788
|
function nativeType(params) {
|
|
1176
1789
|
const { column } = params;
|
|
1790
|
+
if (column.__columnType === "localDate")
|
|
1791
|
+
return "Date";
|
|
1792
|
+
if (column.__columnType === "string")
|
|
1793
|
+
return stringNativeType({ config: column.__config, column: column.__name });
|
|
1794
|
+
if (column.__columnType === "id")
|
|
1795
|
+
return idNativeType({ config: column.__config, column: column.__name });
|
|
1177
1796
|
if (column.__columnType === "enum") {
|
|
1178
1797
|
const { maxLength, text } = column.__config;
|
|
1179
1798
|
if (maxLength !== undefined || text !== undefined) {
|
|
1180
1799
|
const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
|
|
1181
1800
|
if (invalid) {
|
|
1182
|
-
throw new
|
|
1801
|
+
throw new VibeError6({
|
|
1183
1802
|
code: "VIBE_SCHEMA_INVALID",
|
|
1184
1803
|
message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
|
|
1185
1804
|
meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
|
|
@@ -1187,7 +1806,7 @@ function nativeType(params) {
|
|
|
1187
1806
|
}
|
|
1188
1807
|
}
|
|
1189
1808
|
}
|
|
1190
|
-
if (column.__columnType === "
|
|
1809
|
+
if (column.__columnType === "enum" && column.__enumStorage === "string") {
|
|
1191
1810
|
const { maxLength, text } = column.__config;
|
|
1192
1811
|
if (typeof maxLength === "number")
|
|
1193
1812
|
return `VarChar(${maxLength})`;
|
|
@@ -1216,7 +1835,8 @@ function buildField(params) {
|
|
|
1216
1835
|
const dbName = typeof config.map === "string" ? config.map : undefined;
|
|
1217
1836
|
const documentation = typeof config.comment === "string" ? config.comment : undefined;
|
|
1218
1837
|
const collation = typeof config.collation === "string" ? config.collation : undefined;
|
|
1219
|
-
const
|
|
1838
|
+
const validatorMetadata = normalizeValidatorRef({ documentation, reference: config.validator });
|
|
1839
|
+
const metadata = normalizeJsonSchemaRef({ documentation: validatorMetadata.documentation, reference: config.jsonSchema });
|
|
1220
1840
|
const defaultValue = fieldDefault({ column });
|
|
1221
1841
|
const native = extensionNativeType ?? nativeType({ column });
|
|
1222
1842
|
return {
|
|
@@ -1230,8 +1850,10 @@ function buildField(params) {
|
|
|
1230
1850
|
...collation === undefined ? {} : { collation },
|
|
1231
1851
|
...dbName === undefined ? {} : { dbName },
|
|
1232
1852
|
...defaultValue === undefined ? {} : { default: defaultValue },
|
|
1853
|
+
..."generated" in config ? { generated: generatedColumnFromSql2({ value: config.generated }) } : {},
|
|
1233
1854
|
...native === undefined ? {} : { nativeType: native },
|
|
1234
1855
|
...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
|
|
1856
|
+
...validatorMetadata.validator === undefined ? {} : { validator: validatorMetadata.validator },
|
|
1235
1857
|
...metadata
|
|
1236
1858
|
};
|
|
1237
1859
|
}
|
|
@@ -1265,17 +1887,26 @@ function buildIndex(params) {
|
|
|
1265
1887
|
if (index.ops !== undefined && Object.values(index.ops).some((operatorClass) => operatorClass !== undefined) && !dialect.capabilities.indexOperatorClasses)
|
|
1266
1888
|
throw unsupportedCapability({ dialect: dialect.name, feature: "indexOperatorClasses" });
|
|
1267
1889
|
const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
|
|
1890
|
+
const computed = index.keys?.some((term) => term.kind !== "field") === true;
|
|
1891
|
+
const keys = computed ? { fields: [], expressions: index.keys } : { fields: index.keys === undefined ? index.columns.map((name) => ({ name, ...index.ops?.[name] === undefined ? {} : { operatorClass: index.ops[name] } })) : index.keys.flatMap((term) => term.kind === "field" ? [{ ...term.field, ...index.ops?.[term.field.name] === undefined ? {} : { operatorClass: index.ops[term.field.name] } }] : []) };
|
|
1892
|
+
if (computed) {
|
|
1893
|
+
const declaration = { kind, ...keys, ...index.map === undefined ? {} : { name: index.map }, ...index.clientName === undefined ? {} : { clientName: index.clientName }, ...index.nullsNotDistinct === undefined ? {} : { nullsNotDistinct: index.nullsNotDistinct } };
|
|
1894
|
+
assertIndexExpressions({ schema: { datasource: { provider: dialect.name === "postgres" ? "postgresql" : dialect.name }, enums, extensions: [], models: [{ ...model, indexes: [declaration], relations: [], isView: false }] }, dialect: dialect.name });
|
|
1895
|
+
for (const term of index.keys)
|
|
1896
|
+
if (term.kind === "expression")
|
|
1897
|
+
renderSchemaExpression({ expression: term.expression, model, dialect, enums, usage: "indexExpression", site: `expression index on ${model.name}` });
|
|
1898
|
+
}
|
|
1268
1899
|
const predicate = compileIndexPredicate({
|
|
1269
1900
|
where: index.where,
|
|
1270
1901
|
wherePredicate: index.wherePredicate,
|
|
1271
|
-
columns: index.columns,
|
|
1902
|
+
columns: computed ? indexKeyFieldNames({ index: keys }) ?? [] : index.columns,
|
|
1272
1903
|
model,
|
|
1273
1904
|
dialect,
|
|
1274
1905
|
enums
|
|
1275
1906
|
});
|
|
1276
1907
|
return {
|
|
1277
1908
|
kind,
|
|
1278
|
-
|
|
1909
|
+
...keys,
|
|
1279
1910
|
...index.map === undefined ? {} : { name: index.map },
|
|
1280
1911
|
...predicate === undefined ? {} : { where: predicate.where },
|
|
1281
1912
|
...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
|
|
@@ -1362,8 +1993,8 @@ function buildModel(params) {
|
|
|
1362
1993
|
}
|
|
1363
1994
|
const predicateModel = { name: model.__name, fields };
|
|
1364
1995
|
const exclusions = compileExclusions({ exclusions: model.__exclusions, model: predicateModel, dialect });
|
|
1365
|
-
const checks =
|
|
1366
|
-
|
|
1996
|
+
const checks = compileModelChecks({ model, predicateModel, dialect, enums });
|
|
1997
|
+
const irModel = {
|
|
1367
1998
|
name: model.__name,
|
|
1368
1999
|
...model.__map === undefined ? {} : { dbName: model.__map },
|
|
1369
2000
|
fields,
|
|
@@ -1376,6 +2007,8 @@ function buildModel(params) {
|
|
|
1376
2007
|
isView: false,
|
|
1377
2008
|
...model.__comment === undefined ? {} : { documentation: model.__comment }
|
|
1378
2009
|
};
|
|
2010
|
+
const rowRules = compileRowRules({ model, irModel, dialect: dialect.name });
|
|
2011
|
+
return rowRules.length === 0 ? irModel : { ...irModel, rowRules };
|
|
1379
2012
|
}
|
|
1380
2013
|
|
|
1381
2014
|
class ExtensionDeclarationCollector {
|
|
@@ -1420,7 +2053,7 @@ function toIR(params) {
|
|
|
1420
2053
|
}
|
|
1421
2054
|
}
|
|
1422
2055
|
if (issues.length > 0) {
|
|
1423
|
-
throw new
|
|
2056
|
+
throw new VibeError6({
|
|
1424
2057
|
code: "VIBE_SCHEMA_INVALID",
|
|
1425
2058
|
message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
|
|
1426
2059
|
meta: { issues }
|
|
@@ -1444,11 +2077,36 @@ function predicateModel(params) {
|
|
|
1444
2077
|
fields: Array.from(params.model.__columns, ([name, column]) => buildField({ name, column }))
|
|
1445
2078
|
};
|
|
1446
2079
|
}
|
|
2080
|
+
function plannedAdmissionModel(params) {
|
|
2081
|
+
const { model, plan } = params;
|
|
2082
|
+
return {
|
|
2083
|
+
name: model.__name,
|
|
2084
|
+
...model.__map === undefined ? {} : { dbName: model.__map },
|
|
2085
|
+
fields: [...model.__columns].map(([name, column]) => buildField({ name, column })),
|
|
2086
|
+
relations: (plan.relations.get(model.__name) ?? []).map((relation) => ({
|
|
2087
|
+
name: relation.name,
|
|
2088
|
+
model: relation.model,
|
|
2089
|
+
targetModel: relation.targetModel,
|
|
2090
|
+
type: resolveRelationType({ relation, plan }),
|
|
2091
|
+
isList: relation.isList,
|
|
2092
|
+
isOptional: relation.isOptional,
|
|
2093
|
+
...relation.fields === undefined ? {} : { fields: relation.fields },
|
|
2094
|
+
...relation.references === undefined ? {} : { references: relation.references },
|
|
2095
|
+
...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
|
|
2096
|
+
...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
|
|
2097
|
+
})),
|
|
2098
|
+
...model.__primaryKey === undefined ? {} : { primaryKey: { fields: model.__primaryKey } },
|
|
2099
|
+
...model.__policy === undefined ? {} : { policy: { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field } },
|
|
2100
|
+
indexes: [],
|
|
2101
|
+
isView: false
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
1447
2104
|
function prismaType(params) {
|
|
1448
2105
|
const { column } = params;
|
|
1449
2106
|
const list = column.__config.array === true ? "[]" : "";
|
|
1450
2107
|
switch (column.__columnType) {
|
|
1451
2108
|
case "string":
|
|
2109
|
+
case "localDate":
|
|
1452
2110
|
return `String${list}`;
|
|
1453
2111
|
case "int":
|
|
1454
2112
|
return `Int${list}`;
|
|
@@ -1501,13 +2159,11 @@ function defaultAttribute(params) {
|
|
|
1501
2159
|
}
|
|
1502
2160
|
function nativeTypeAttribute(params) {
|
|
1503
2161
|
const { column } = params;
|
|
1504
|
-
if (column.__columnType === "
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
return "@db.Text";
|
|
1510
|
-
return;
|
|
2162
|
+
if (column.__columnType === "localDate")
|
|
2163
|
+
return "@db.Date";
|
|
2164
|
+
if (column.__columnType === "string" || column.__columnType === "id") {
|
|
2165
|
+
const native = column.__columnType === "string" ? stringNativeType({ config: column.__config, column: column.__name }) : idNativeType({ config: column.__config, column: column.__name });
|
|
2166
|
+
return native === undefined ? undefined : `@db.${native}`;
|
|
1511
2167
|
}
|
|
1512
2168
|
if (column.__columnType === "decimal") {
|
|
1513
2169
|
const { precision, scale } = column.__config;
|
|
@@ -1534,6 +2190,10 @@ function formatColumn(params) {
|
|
|
1534
2190
|
if (columnDefault !== undefined)
|
|
1535
2191
|
parts.push(columnDefault);
|
|
1536
2192
|
}
|
|
2193
|
+
if ("generated" in column.__config) {
|
|
2194
|
+
const generated = generatedColumnFromSql3({ value: column.__config.generated });
|
|
2195
|
+
parts.push(`@generated(sql: ${encodePrismaString2({ value: generated.expression })}, storage: "stored")`);
|
|
2196
|
+
}
|
|
1537
2197
|
if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
|
|
1538
2198
|
parts.push("@updatedAt");
|
|
1539
2199
|
}
|
|
@@ -1550,13 +2210,17 @@ function formatColumn(params) {
|
|
|
1550
2210
|
parts.push(`@collation(${encodePrismaString2({ value: column.__config.collation })})`);
|
|
1551
2211
|
}
|
|
1552
2212
|
const columnLine = parts.join(" ");
|
|
1553
|
-
const
|
|
2213
|
+
const validatorMetadata = normalizeValidatorRef2({ documentation: typeof column.__config.comment === "string" ? column.__config.comment : undefined, reference: column.__config.validator });
|
|
2214
|
+
const metadata = normalizeJsonSchemaRef2({ documentation: validatorMetadata.documentation, reference: column.__config.jsonSchema });
|
|
2215
|
+
if (validatorMetadata.validator !== undefined && (column.__columnType === "json" || column.__columnType === "unsupported" || metadata.jsonSchema !== undefined))
|
|
2216
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "Validators are only valid on queryable non-JSON scalar or enum columns; use .schema() for Json", meta: { issues: [{ rule: "validator-non-scalar", field: name }] } });
|
|
1554
2217
|
if (metadata.jsonSchema !== undefined && column.__columnType !== "json")
|
|
1555
|
-
throw new
|
|
2218
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "JSON schemas are only valid on Json fields", meta: { issues: [{ rule: "json-schema-non-json", field: name }] } });
|
|
1556
2219
|
const comments = [
|
|
1557
2220
|
...metadata.documentation === undefined ? [] : metadata.documentation.split(`
|
|
1558
2221
|
`),
|
|
1559
|
-
...metadata.jsonSchema === undefined ? [] : [`@schema(${metadata.jsonSchema.name} from ${encodePrismaString2({ value: metadata.jsonSchema.from })})`]
|
|
2222
|
+
...metadata.jsonSchema === undefined ? [] : [`@schema(${metadata.jsonSchema.name} from ${encodePrismaString2({ value: metadata.jsonSchema.from })})`],
|
|
2223
|
+
...validatorMetadata.validator === undefined ? [] : [`@validate(${validatorMetadata.validator.name} from ${encodePrismaString2({ value: validatorMetadata.validator.from })})`]
|
|
1560
2224
|
];
|
|
1561
2225
|
return [...comments.map((line) => ` /// ${line}`), columnLine].join(`
|
|
1562
2226
|
`);
|
|
@@ -1596,8 +2260,8 @@ function formatRelation(params) {
|
|
|
1596
2260
|
function formatIndex(params) {
|
|
1597
2261
|
const { index } = params;
|
|
1598
2262
|
if (index.ops !== undefined && Object.values(index.ops).some((operatorClass) => operatorClass !== undefined))
|
|
1599
|
-
throw new
|
|
1600
|
-
const columns = index.columns.join(", ");
|
|
2263
|
+
throw new VibeError7({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Prisma output cannot preserve qualified index operator classes; keep the DSL source", meta: { feature: "prismaIndexOperatorClasses", target: "prisma" } });
|
|
2264
|
+
const columns = index.keys === undefined ? index.columns.join(", ") : indexKeyTerms({ index: buildIndex({ index, model: predicateModel({ model: params.model }), dialect: params.dialect, enums: params.enums }) }).map((term) => indexKeyPrisma({ term })).join(", ");
|
|
1601
2265
|
const predicate = compileIndexPredicate({
|
|
1602
2266
|
where: index.where,
|
|
1603
2267
|
wherePredicate: index.wherePredicate,
|
|
@@ -1647,14 +2311,24 @@ function formatModel(params) {
|
|
|
1647
2311
|
for (const index of plan.indexes.get(model.__name) ?? []) {
|
|
1648
2312
|
lines.push(formatIndex({ index, model: params.model, dialect: params.dialect, enums: params.enums }));
|
|
1649
2313
|
}
|
|
1650
|
-
for (const check of
|
|
1651
|
-
|
|
1652
|
-
|
|
2314
|
+
for (const check of compileModelChecks({
|
|
2315
|
+
model: params.model,
|
|
2316
|
+
predicateModel: predicateModel({ model: params.model }),
|
|
1653
2317
|
dialect: params.dialect,
|
|
1654
2318
|
enums: params.enums
|
|
1655
2319
|
})) {
|
|
1656
2320
|
lines.push(` @@check(${encodePrismaString2({ value: check.expression })}, name: ${encodePrismaString2({ value: check.name })})`);
|
|
1657
2321
|
}
|
|
2322
|
+
const irModel = plannedAdmissionModel({ model, plan });
|
|
2323
|
+
const rowRules = compileRowRules({ model, irModel, dialect: params.dialect.name });
|
|
2324
|
+
assertRowRuleReferentialActions({ schema: {
|
|
2325
|
+
datasource: { provider: params.dialect.name === "postgres" ? "postgresql" : params.dialect.name },
|
|
2326
|
+
enums: params.enums,
|
|
2327
|
+
extensions: [],
|
|
2328
|
+
models: [{ ...irModel, rowRules }]
|
|
2329
|
+
}, dialect: params.dialect.name });
|
|
2330
|
+
for (const rule of rowRules)
|
|
2331
|
+
lines.push(` ${rowRulePrismaAttribute({ rule })}`);
|
|
1658
2332
|
for (const use of model.__extensions ?? []) {
|
|
1659
2333
|
const args = [];
|
|
1660
2334
|
if (use.fields !== undefined)
|
|
@@ -1688,24 +2362,31 @@ function generatePrisma(schema, config = {}) {
|
|
|
1688
2362
|
for (const model of schema.__models.values()) {
|
|
1689
2363
|
const exclusions = compileExclusions({ exclusions: model.__exclusions, model: { name: model.__name, fields: Array.from(model.__columns, ([name, column]) => buildField({ name, column })) }, dialect: getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) }) });
|
|
1690
2364
|
if (exclusions.length > 0)
|
|
1691
|
-
throw new
|
|
2365
|
+
throw new VibeError7({ code: "VIBE_CONFIG", message: "Prisma output cannot preserve exclusion constraints; keep the DSL source", meta: { feature: "exclusionConstraints", target: "prisma" } });
|
|
1692
2366
|
}
|
|
1693
2367
|
const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
|
|
1694
2368
|
if (stringEnums.length > 0) {
|
|
1695
|
-
throw new
|
|
2369
|
+
throw new VibeError7({
|
|
1696
2370
|
code: "VIBE_CONFIG",
|
|
1697
2371
|
message: `Cannot print to Prisma: enum(s) ${stringEnums.join(", ")} use storage: "string", which .prisma cannot express losslessly. Keep the DSL as the source or switch the enum to database storage.`,
|
|
1698
2372
|
meta: { enums: stringEnums, target: "prisma" }
|
|
1699
2373
|
});
|
|
1700
2374
|
}
|
|
1701
2375
|
if (schema.__rls !== undefined) {
|
|
1702
|
-
throw new
|
|
2376
|
+
throw new VibeError7({
|
|
1703
2377
|
code: "VIBE_CONFIG",
|
|
1704
2378
|
message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
|
|
1705
2379
|
meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
|
|
1706
2380
|
});
|
|
1707
2381
|
}
|
|
1708
2382
|
const plan = planSchema({ schema });
|
|
2383
|
+
if ([...schema.__models.values()].some((model) => [...model.__columns.values()].some((column) => ("generated" in column.__config))))
|
|
2384
|
+
assertGeneratedColumns({ schema: {
|
|
2385
|
+
datasource: { provider: config.provider ?? "postgresql" },
|
|
2386
|
+
enums: [],
|
|
2387
|
+
extensions: [],
|
|
2388
|
+
models: [...schema.__models.values()].map((model) => plannedAdmissionModel({ model, plan }))
|
|
2389
|
+
} });
|
|
1709
2390
|
const sections = [];
|
|
1710
2391
|
const dialect = getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) });
|
|
1711
2392
|
const enums = Array.from(schema.__enums, ([name, values]) => ({ name, values: [...values] }));
|
|
@@ -1732,9 +2413,10 @@ function generatePrisma(schema, config = {}) {
|
|
|
1732
2413
|
}
|
|
1733
2414
|
|
|
1734
2415
|
// packages/dsl/src/relations.ts
|
|
1735
|
-
import { VibeError as
|
|
2416
|
+
import { VibeError as VibeError8 } from "@vibeorm/schema";
|
|
1736
2417
|
var REF_KIND_BY_COLUMN_TYPE = {
|
|
1737
2418
|
string: "String",
|
|
2419
|
+
localDate: "String",
|
|
1738
2420
|
int: "Int",
|
|
1739
2421
|
bigint: "BigInt",
|
|
1740
2422
|
float: "Float",
|
|
@@ -1773,7 +2455,7 @@ function extractRefInfo(params) {
|
|
|
1773
2455
|
}
|
|
1774
2456
|
const members = ref;
|
|
1775
2457
|
if (members.length === 0) {
|
|
1776
|
-
throw new
|
|
2458
|
+
throw new VibeError8({
|
|
1777
2459
|
code: "VIBE_SCHEMA_INVALID",
|
|
1778
2460
|
message: `Relation to ${target}: "${side}" is an empty tuple \u2014 name at least one column`,
|
|
1779
2461
|
meta: { rule: "relation-empty-tuple", side, targetModel: target }
|
|
@@ -1781,14 +2463,14 @@ function extractRefInfo(params) {
|
|
|
1781
2463
|
}
|
|
1782
2464
|
const models = [...new Set(members.map((member) => member.__model))].sort();
|
|
1783
2465
|
if (models.length > 1) {
|
|
1784
|
-
throw new
|
|
2466
|
+
throw new VibeError8({
|
|
1785
2467
|
code: "VIBE_SCHEMA_INVALID",
|
|
1786
2468
|
message: `Relation to ${target}: "${side}" mixes models ${models.join(", ")} \u2014 every member of a composite key must sit on one model`,
|
|
1787
2469
|
meta: { rule: "relation-mixed-models", side, targetModel: target, models }
|
|
1788
2470
|
});
|
|
1789
2471
|
}
|
|
1790
2472
|
if (members.some((member) => member.__type !== "columnRef")) {
|
|
1791
|
-
throw new
|
|
2473
|
+
throw new VibeError8({
|
|
1792
2474
|
code: "VIBE_SCHEMA_INVALID",
|
|
1793
2475
|
message: `Relation to ${target}: "${side}" routes through a junction inside a tuple \u2014 implicit many-to-many is single-key; model the join with an explicit junction model`,
|
|
1794
2476
|
meta: { rule: "relation-through-in-tuple", side, targetModel: target }
|
|
@@ -1802,7 +2484,7 @@ function createRelationFactory(params) {
|
|
|
1802
2484
|
const from = extractRefInfo({ ref: config.from, side: "from", target: targetModelName });
|
|
1803
2485
|
const to = extractRefInfo({ ref: config.to, side: "to", target: targetModelName });
|
|
1804
2486
|
if (from.columns.length !== to.columns.length) {
|
|
1805
|
-
throw new
|
|
2487
|
+
throw new VibeError8({
|
|
1806
2488
|
code: "VIBE_SCHEMA_INVALID",
|
|
1807
2489
|
message: `Relation to ${targetModelName}: "from" names ${from.columns.length} column(s) but "to" names ${to.columns.length} \u2014 a composite key pairs member by member`,
|
|
1808
2490
|
meta: { rule: "relation-tuple-arity", targetModel: targetModelName, from: from.columns, to: to.columns }
|
|
@@ -1900,7 +2582,7 @@ function mergeRelations(...parts) {
|
|
|
1900
2582
|
}
|
|
1901
2583
|
|
|
1902
2584
|
// packages/dsl/src/schema.ts
|
|
1903
|
-
import { VibeError as
|
|
2585
|
+
import { VibeError as VibeError9, validateRlsContextIR } from "@vibeorm/schema";
|
|
1904
2586
|
function sameValues(left, right) {
|
|
1905
2587
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1906
2588
|
}
|
|
@@ -1919,7 +2601,7 @@ function registerEnum(params) {
|
|
|
1919
2601
|
...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
|
|
1920
2602
|
...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
|
|
1921
2603
|
].join("; ");
|
|
1922
|
-
throw new
|
|
2604
|
+
throw new VibeError9({
|
|
1923
2605
|
code: "VIBE_SCHEMA_INVALID",
|
|
1924
2606
|
message: `Enum "${name}" is defined more than once with conflicting definitions (${origin}): ${detail} \u2014 share one enum object across modules, or give the definitions different names`,
|
|
1925
2607
|
meta: {
|
|
@@ -1945,7 +2627,7 @@ function addModelTree(params) {
|
|
|
1945
2627
|
const { model, models, origin } = params;
|
|
1946
2628
|
const existing = models.get(model.__name);
|
|
1947
2629
|
if (existing !== undefined && existing !== model) {
|
|
1948
|
-
throw new
|
|
2630
|
+
throw new VibeError9({
|
|
1949
2631
|
code: "VIBE_SCHEMA_INVALID",
|
|
1950
2632
|
message: `Model "${model.__name}" is defined more than once with different definitions (${origin}) \u2014 ` + `share one model object across modules, or give the definitions different names`,
|
|
1951
2633
|
meta: {
|
|
@@ -2023,7 +2705,7 @@ var RLS_CONTEXT_TYPES = {
|
|
|
2023
2705
|
};
|
|
2024
2706
|
function defineRls(params) {
|
|
2025
2707
|
if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
|
|
2026
|
-
throw new
|
|
2708
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
2027
2709
|
}
|
|
2028
2710
|
const entries = [];
|
|
2029
2711
|
const refs = [];
|
|
@@ -2031,7 +2713,7 @@ function defineRls(params) {
|
|
|
2031
2713
|
const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
|
|
2032
2714
|
const column = descriptor?.value;
|
|
2033
2715
|
if (typeof key !== "string" || descriptor?.enumerable !== true || typeof column !== "object" || column === null || !("__columnType" in column) || !("__config" in column) || typeof column.__config !== "object" || column.__config === null || typeof column.__columnType !== "string" || !Object.hasOwn(RLS_CONTEXT_TYPES, column.__columnType) || "array" in column.__config && column.__config.array === true) {
|
|
2034
|
-
throw new
|
|
2716
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
2035
2717
|
}
|
|
2036
2718
|
entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
|
|
2037
2719
|
refs.push([key, Object.freeze({ context: key })]);
|
|
@@ -2039,22 +2721,22 @@ function defineRls(params) {
|
|
|
2039
2721
|
const context = Object.freeze(Object.fromEntries(entries));
|
|
2040
2722
|
const issues = validateRlsContextIR({ context });
|
|
2041
2723
|
if (issues.length > 0) {
|
|
2042
|
-
throw new
|
|
2724
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
|
|
2043
2725
|
}
|
|
2044
2726
|
const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
|
|
2045
2727
|
if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
|
|
2046
|
-
throw new
|
|
2728
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
2047
2729
|
}
|
|
2048
2730
|
const named = [];
|
|
2049
2731
|
const classified = new Set;
|
|
2050
2732
|
for (const key of Reflect.ownKeys(policies)) {
|
|
2051
2733
|
const descriptor = Object.getOwnPropertyDescriptor(policies, key);
|
|
2052
2734
|
if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
|
|
2053
|
-
throw new
|
|
2735
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
2054
2736
|
}
|
|
2055
2737
|
const name = params.models[key]?.__name ?? key;
|
|
2056
2738
|
if (classified.has(name)) {
|
|
2057
|
-
throw new
|
|
2739
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
|
|
2058
2740
|
}
|
|
2059
2741
|
classified.add(name);
|
|
2060
2742
|
named.push([name, descriptor.value]);
|
|
@@ -2297,6 +2979,7 @@ function enumFactory(config) {
|
|
|
2297
2979
|
var generate = Object.assign((schemaDef) => ({ prisma: generatePrisma(schemaDef) }), { prisma: generatePrisma });
|
|
2298
2980
|
var dsl = {
|
|
2299
2981
|
string,
|
|
2982
|
+
localDate,
|
|
2300
2983
|
int,
|
|
2301
2984
|
bigint,
|
|
2302
2985
|
float,
|
|
@@ -2326,9 +3009,11 @@ export {
|
|
|
2326
3009
|
ChainableDatetimeColumn,
|
|
2327
3010
|
ChainableDecimalColumn,
|
|
2328
3011
|
ChainableEnumColumn,
|
|
3012
|
+
ChainableFloatColumn,
|
|
2329
3013
|
ChainableIdColumn,
|
|
2330
3014
|
ChainableIntegerColumn,
|
|
2331
3015
|
ChainableJsonColumn,
|
|
3016
|
+
ChainableNumericColumn,
|
|
2332
3017
|
ChainableStringColumn,
|
|
2333
3018
|
base,
|
|
2334
3019
|
baseWithWorkspace,
|
|
@@ -2336,11 +3021,15 @@ export {
|
|
|
2336
3021
|
cloneColumnDef,
|
|
2337
3022
|
compileChecks,
|
|
2338
3023
|
compileIndexPredicate,
|
|
3024
|
+
compileModelChecks,
|
|
3025
|
+
compileRowRules,
|
|
2339
3026
|
create,
|
|
3027
|
+
createChecksBuilder,
|
|
2340
3028
|
createChildrenBuilder,
|
|
2341
3029
|
createEnum,
|
|
2342
3030
|
createIndexBuilder,
|
|
2343
3031
|
createPredicateBuilder,
|
|
3032
|
+
createRowRuleBuilder,
|
|
2344
3033
|
defineRelations,
|
|
2345
3034
|
defineRelationsPart,
|
|
2346
3035
|
defineRls,
|
|
@@ -2352,6 +3041,7 @@ export {
|
|
|
2352
3041
|
fields,
|
|
2353
3042
|
generatePrisma,
|
|
2354
3043
|
getColumnNames,
|
|
3044
|
+
localDate,
|
|
2355
3045
|
mergeRelations,
|
|
2356
3046
|
planSchema,
|
|
2357
3047
|
resolveRelationType,
|
|
@@ -2366,4 +3056,4 @@ export {
|
|
|
2366
3056
|
withNameDescription
|
|
2367
3057
|
};
|
|
2368
3058
|
|
|
2369
|
-
//# debugId=
|
|
3059
|
+
//# debugId=7EE10300C00F60DE64756E2164756E21
|