@vibeorm/dsl 4.2.0 → 5.0.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/dist/column.d.ts +72 -5
- package/dist/index.d.ts +13 -7
- package/dist/index.js +745 -101
- 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 +11 -1
- package/dist/types.d.ts +212 -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 {
|
|
@@ -80,9 +132,15 @@ class ChainableStringColumn extends ChainableColumn {
|
|
|
80
132
|
this.__config.collation = name;
|
|
81
133
|
return this;
|
|
82
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
|
+
}
|
|
83
141
|
}
|
|
84
142
|
|
|
85
|
-
class ChainableDecimalColumn extends
|
|
143
|
+
class ChainableDecimalColumn extends ChainableNumericColumn {
|
|
86
144
|
constructor(config = {}) {
|
|
87
145
|
super("decimal", config);
|
|
88
146
|
}
|
|
@@ -145,22 +203,34 @@ class ChainableEnumColumn extends ChainableColumn {
|
|
|
145
203
|
this.__config.default = value;
|
|
146
204
|
return this;
|
|
147
205
|
}
|
|
206
|
+
oneOf(values, options) {
|
|
207
|
+
return this.addCheckRule({ rule: "allowed", check: { kind: "oneOf", values: [...values] }, options });
|
|
208
|
+
}
|
|
148
209
|
}
|
|
149
210
|
function string(config = {}) {
|
|
150
211
|
return new ChainableStringColumn(config);
|
|
151
212
|
}
|
|
152
213
|
|
|
153
|
-
class ChainableIntegerColumn extends
|
|
214
|
+
class ChainableIntegerColumn extends ChainableNumericColumn {
|
|
154
215
|
autoincrement() {
|
|
155
216
|
this.__config.autoincrement = true;
|
|
156
217
|
return this;
|
|
157
218
|
}
|
|
158
219
|
}
|
|
220
|
+
function localDate({ ...config } = {}) {
|
|
221
|
+
return new ChainableColumn("localDate", config);
|
|
222
|
+
}
|
|
159
223
|
function int(config = {}) {
|
|
160
224
|
return new ChainableIntegerColumn("int", config);
|
|
161
225
|
}
|
|
226
|
+
|
|
227
|
+
class ChainableFloatColumn extends ChainableNumericColumn {
|
|
228
|
+
constructor(config = {}) {
|
|
229
|
+
super("float", config);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
162
232
|
function float(config = {}) {
|
|
163
|
-
return new
|
|
233
|
+
return new ChainableFloatColumn(config);
|
|
164
234
|
}
|
|
165
235
|
function decimal(config = {}) {
|
|
166
236
|
return new ChainableDecimalColumn(config);
|
|
@@ -177,6 +247,8 @@ class ChainableJsonColumn extends ChainableColumn {
|
|
|
177
247
|
super("json", { ...config, ...config.jsonSchema === undefined ? {} : { jsonSchema: assertJsonSchemaRef({ value: config.jsonSchema }) } });
|
|
178
248
|
}
|
|
179
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" }] } });
|
|
180
252
|
if (this.__config.jsonSchema !== undefined)
|
|
181
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" }] } });
|
|
182
254
|
this.__config.jsonSchema = assertJsonSchemaRef({ value: reference });
|
|
@@ -243,11 +315,66 @@ function createEnum(config) {
|
|
|
243
315
|
}
|
|
244
316
|
|
|
245
317
|
// packages/dsl/src/generator/prisma.ts
|
|
246
|
-
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";
|
|
247
319
|
import { getSqlDialect as getSqlDialect2 } from "@vibeorm/sql";
|
|
248
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
|
+
|
|
249
376
|
// packages/dsl/src/predicate.ts
|
|
250
|
-
import { VibeError as
|
|
377
|
+
import { IDENTIFIER_LIMITS, VibeError as VibeError3, checkRulePredicate, identifierLength, isEnumType } from "@vibeorm/schema";
|
|
251
378
|
import { compileExclusion, renderPredicate } from "@vibeorm/sql";
|
|
252
379
|
function isFieldExpr(operand) {
|
|
253
380
|
return typeof operand === "object" && operand !== null && "kind" in operand && operand.kind === "field";
|
|
@@ -262,7 +389,7 @@ function toValue(params) {
|
|
|
262
389
|
if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
|
|
263
390
|
return { kind: "value", value: input };
|
|
264
391
|
}
|
|
265
|
-
throw new
|
|
392
|
+
throw new VibeError3({
|
|
266
393
|
code: "VIBE_SCHEMA_INVALID",
|
|
267
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`,
|
|
268
395
|
meta: { rule: "predicate-literal-invalid", at }
|
|
@@ -280,17 +407,28 @@ function toExpression(params) {
|
|
|
280
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"))) {
|
|
281
408
|
return { kind: "jsonText", field: toRef({ expr: requireFieldExpr({ value: value.field, at }) }), path: [...value.path] };
|
|
282
409
|
}
|
|
283
|
-
if (value.kind === "length" && "expression" in value) {
|
|
284
|
-
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
|
+
};
|
|
285
420
|
}
|
|
286
421
|
}
|
|
287
|
-
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);
|
|
288
426
|
}
|
|
289
427
|
function requireFieldExpr(params) {
|
|
290
428
|
const { value, at } = params;
|
|
291
429
|
if (isFieldExpr(value))
|
|
292
430
|
return value;
|
|
293
|
-
throw new
|
|
431
|
+
throw new VibeError3({
|
|
294
432
|
code: "VIBE_SCHEMA_INVALID",
|
|
295
433
|
message: `${at}: the left side of a comparison must be c.field("\u2026"), not a bare value`,
|
|
296
434
|
meta: { rule: "predicate-operand-invalid", at }
|
|
@@ -299,7 +437,7 @@ function requireFieldExpr(params) {
|
|
|
299
437
|
function requireOperands(params) {
|
|
300
438
|
const { operands, at } = params;
|
|
301
439
|
if (operands.length === 0) {
|
|
302
|
-
throw new
|
|
440
|
+
throw new VibeError3({
|
|
303
441
|
code: "VIBE_SCHEMA_INVALID",
|
|
304
442
|
message: `${at}: an empty boolean group has no honest SQL spelling \u2014 pass at least one operand`,
|
|
305
443
|
meta: { rule: "predicate-empty", at }
|
|
@@ -318,7 +456,7 @@ function createPredicateBuilder() {
|
|
|
318
456
|
const at = negate ? "c.notInList()" : "c.inList()";
|
|
319
457
|
const ref = toRef({ expr: requireFieldExpr({ value: field, at }) });
|
|
320
458
|
if (values.length === 0) {
|
|
321
|
-
throw new
|
|
459
|
+
throw new VibeError3({
|
|
322
460
|
code: "VIBE_SCHEMA_INVALID",
|
|
323
461
|
message: `${at}: an empty IN list is UNKNOWN on every row and is never what an author means`,
|
|
324
462
|
meta: { rule: "predicate-empty", at, field: ref.name }
|
|
@@ -339,6 +477,11 @@ function createPredicateBuilder() {
|
|
|
339
477
|
gte: compare("gte"),
|
|
340
478
|
lt: compare("lt"),
|
|
341
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" }),
|
|
342
485
|
jsonText: ({ field, path }) => ({ ...toExpression({ value: { kind: "jsonText", field: { kind: "field", name: field }, path }, at: "c.jsonText()" }), __kind: "String" }),
|
|
343
486
|
length: (expression) => ({ kind: "length", expression: toExpression({ value: expression, at: "c.length()" }), __kind: "Int" }),
|
|
344
487
|
isTrue: (operand) => ({ kind: "isTrue", operand }),
|
|
@@ -351,7 +494,7 @@ function createPredicateBuilder() {
|
|
|
351
494
|
not: (operand) => ({ kind: "not", operand }),
|
|
352
495
|
named: (name, predicate) => {
|
|
353
496
|
if (name.trim() === "") {
|
|
354
|
-
throw new
|
|
497
|
+
throw new VibeError3({
|
|
355
498
|
code: "VIBE_SCHEMA_INVALID",
|
|
356
499
|
message: "c.named(): a CHECK needs a non-empty name \u2014 the name is what keys the migration diff",
|
|
357
500
|
meta: { rule: "predicate-unnamed-check" }
|
|
@@ -361,6 +504,262 @@ function createPredicateBuilder() {
|
|
|
361
504
|
}
|
|
362
505
|
};
|
|
363
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
|
+
}
|
|
364
763
|
function evaluateChecks(params) {
|
|
365
764
|
const { checks, model } = params;
|
|
366
765
|
if (checks === undefined)
|
|
@@ -368,28 +767,61 @@ function evaluateChecks(params) {
|
|
|
368
767
|
if (typeof checks !== "function") {
|
|
369
768
|
return checks.map((check) => ({ kind: "raw", name: check.name, expression: check.expression }));
|
|
370
769
|
}
|
|
371
|
-
const
|
|
770
|
+
const builder = params.context === undefined ? contextlessChecksBuilder({ model }) : createChecksBuilder({ context: params.context });
|
|
771
|
+
const declared = checks(builder);
|
|
372
772
|
return declared.map((entry, index) => {
|
|
373
|
-
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") {
|
|
374
774
|
return { kind: "raw", name: entry.name, expression: entry.expression };
|
|
375
775
|
}
|
|
376
|
-
if (
|
|
377
|
-
throw new
|
|
776
|
+
if (Array.isArray(entry)) {
|
|
777
|
+
throw new VibeError3({
|
|
378
778
|
code: "VIBE_SCHEMA_INVALID",
|
|
379
|
-
message: `Model "${model}": checks[${index}]
|
|
779
|
+
message: `Model "${model}": checks[${index}] is a list \u2014 spread c.presence(...) into the checks: [...c.presence({ \u2026 })]`,
|
|
380
780
|
meta: { rule: "predicate-unnamed-check", model, index }
|
|
381
781
|
});
|
|
382
782
|
}
|
|
383
|
-
|
|
783
|
+
if (!isRuleCheck(entry)) {
|
|
784
|
+
throw new VibeError3({
|
|
785
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
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`,
|
|
787
|
+
meta: { rule: "predicate-unnamed-check", model, index }
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return typedCheck({ entry });
|
|
384
791
|
});
|
|
385
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
|
+
}
|
|
386
811
|
function compileExclusions(params) {
|
|
387
812
|
const specs = typeof params.exclusions === "function" ? params.exclusions(createPredicateBuilder()) : params.exclusions ?? [];
|
|
388
813
|
return specs.map((exclusion) => compileExclusion({ exclusion, model: params.model, dialect: params.dialect }));
|
|
389
814
|
}
|
|
390
815
|
function compileChecks(params) {
|
|
391
816
|
const { checks, model, dialect, enums } = params;
|
|
392
|
-
|
|
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 } : {
|
|
393
825
|
name: check.name,
|
|
394
826
|
expression: renderPredicate({
|
|
395
827
|
predicate: check.predicate,
|
|
@@ -397,15 +829,50 @@ function compileChecks(params) {
|
|
|
397
829
|
dialect,
|
|
398
830
|
enums,
|
|
399
831
|
usage: "check",
|
|
400
|
-
site: `CHECK "${check.name}"`
|
|
832
|
+
site: check.origin === undefined ? `CHECK "${check.name}"` : `CHECK "${check.name}" (${check.origin})`
|
|
401
833
|
}),
|
|
402
834
|
predicate: check.predicate
|
|
403
835
|
});
|
|
404
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
|
+
}
|
|
405
872
|
function compileIndexPredicate(params) {
|
|
406
873
|
const { where, wherePredicate, columns, model, dialect, enums } = params;
|
|
407
874
|
if (where !== undefined && wherePredicate !== undefined) {
|
|
408
|
-
throw new
|
|
875
|
+
throw new VibeError3({
|
|
409
876
|
code: "VIBE_SCHEMA_INVALID",
|
|
410
877
|
message: `Index on "${model.name}" [${columns.join(", ")}] carries both a raw where: string and a typed .where() \u2014 an index has ONE predicate`,
|
|
411
878
|
meta: { rule: "predicate-duplicate-where", model: model.name, columns: [...columns] }
|
|
@@ -431,17 +898,22 @@ function compileIndexPredicate(params) {
|
|
|
431
898
|
// packages/dsl/src/to-ir.ts
|
|
432
899
|
import {
|
|
433
900
|
EMPTY_EXTENSION_REGISTRY,
|
|
434
|
-
VibeError as
|
|
901
|
+
VibeError as VibeError6,
|
|
435
902
|
assertValidSchemaIR,
|
|
903
|
+
assertIndexExpressions,
|
|
904
|
+
indexKeyFieldNames,
|
|
436
905
|
exactIntegerLiteral,
|
|
906
|
+
generatedColumnFromSql as generatedColumnFromSql2,
|
|
907
|
+
isCanonicalLocalDate,
|
|
437
908
|
providerToDialect,
|
|
438
909
|
scalarListDefault,
|
|
439
910
|
serializeJsonDefault,
|
|
440
911
|
mergeExtensionDeclarations,
|
|
441
912
|
normalizeJsonSchemaRef,
|
|
913
|
+
normalizeValidatorRef,
|
|
442
914
|
unsupportedCapability
|
|
443
915
|
} from "@vibeorm/schema";
|
|
444
|
-
import { getSqlDialect } from "@vibeorm/sql";
|
|
916
|
+
import { getSqlDialect, renderSchemaExpression } from "@vibeorm/sql";
|
|
445
917
|
|
|
446
918
|
// packages/dsl/src/field-type.ts
|
|
447
919
|
import { fieldTypeIdentity } from "@vibeorm/schema";
|
|
@@ -449,6 +921,7 @@ function columnFieldType(params) {
|
|
|
449
921
|
const { column } = params;
|
|
450
922
|
switch (column.__columnType) {
|
|
451
923
|
case "string":
|
|
924
|
+
case "localDate":
|
|
452
925
|
return "String";
|
|
453
926
|
case "int":
|
|
454
927
|
return "Int";
|
|
@@ -475,14 +948,17 @@ function columnFieldType(params) {
|
|
|
475
948
|
}
|
|
476
949
|
}
|
|
477
950
|
function fieldTypeKey(params) {
|
|
478
|
-
return fieldTypeIdentity({
|
|
951
|
+
return fieldTypeIdentity({
|
|
952
|
+
type: columnFieldType({ column: params.column }),
|
|
953
|
+
...params.column.__columnType === "localDate" ? { nativeType: "Date" } : {}
|
|
954
|
+
});
|
|
479
955
|
}
|
|
480
956
|
|
|
481
957
|
// packages/dsl/src/relation-plan.ts
|
|
482
|
-
import { VibeError as
|
|
958
|
+
import { VibeError as VibeError5, indexKeyDefinitionKey, isSqlObjectIdentity, predicateKey } from "@vibeorm/schema";
|
|
483
959
|
|
|
484
960
|
// packages/dsl/src/model.ts
|
|
485
|
-
import { VibeError as
|
|
961
|
+
import { VibeError as VibeError4 } from "@vibeorm/schema";
|
|
486
962
|
|
|
487
963
|
// packages/dsl/src/naming.ts
|
|
488
964
|
function capitalize(params) {
|
|
@@ -524,7 +1000,7 @@ function createIndexBuilder() {
|
|
|
524
1000
|
const partial = (index) => {
|
|
525
1001
|
const where = (predicate) => {
|
|
526
1002
|
if (index.__where !== undefined) {
|
|
527
|
-
throw new
|
|
1003
|
+
throw new VibeError4({
|
|
528
1004
|
code: "VIBE_SCHEMA_INVALID",
|
|
529
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`,
|
|
530
1006
|
meta: { rule: "predicate-duplicate-where", columns: [...index.__columns] }
|
|
@@ -539,53 +1015,92 @@ function createIndexBuilder() {
|
|
|
539
1015
|
configurable: true
|
|
540
1016
|
});
|
|
541
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();
|
|
542
1046
|
const createIndex = (params) => ({
|
|
543
1047
|
__type: "index",
|
|
544
|
-
|
|
1048
|
+
...keyTerms({ keys: params.keys }),
|
|
545
1049
|
__indexType: params.type,
|
|
546
1050
|
...params.map === undefined ? {} : { __map: params.map },
|
|
547
1051
|
...params.where === undefined ? {} : { __where: params.where },
|
|
548
1052
|
...params.ops === undefined ? {} : { __ops: params.ops }
|
|
549
1053
|
});
|
|
550
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 }),
|
|
551
1064
|
on(...args) {
|
|
552
1065
|
const [first, second] = args;
|
|
553
1066
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
554
|
-
|
|
1067
|
+
const options = second;
|
|
1068
|
+
return partial(createIndex({ keys: first, type: options.type, where: options.where, map: options.map, ops: options.ops }));
|
|
555
1069
|
}
|
|
556
|
-
return partial(createIndex({
|
|
1070
|
+
return partial(createIndex({ keys: args }));
|
|
557
1071
|
},
|
|
558
|
-
index(...
|
|
559
|
-
return partial(createIndex({
|
|
1072
|
+
index(...keys) {
|
|
1073
|
+
return partial(createIndex({ keys }));
|
|
560
1074
|
},
|
|
561
1075
|
unique(...args) {
|
|
562
1076
|
const [first, second] = args;
|
|
563
1077
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
564
1078
|
if ("ops" in second && second.ops !== undefined) {
|
|
565
|
-
throw new
|
|
1079
|
+
throw new VibeError4({ code: "VIBE_UNSUPPORTED_CAPABILITY", message: "Custom unique operator classes can change client selector equality", meta: { feature: "uniqueIndexOperatorClasses" } });
|
|
566
1080
|
}
|
|
1081
|
+
const options = second;
|
|
567
1082
|
return partial({
|
|
568
1083
|
__type: "unique",
|
|
569
|
-
|
|
570
|
-
...
|
|
571
|
-
...
|
|
572
|
-
...
|
|
573
|
-
...
|
|
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 }
|
|
574
1089
|
});
|
|
575
1090
|
}
|
|
576
|
-
return partial({ __type: "unique",
|
|
1091
|
+
return partial({ __type: "unique", ...keyTerms({ keys: args }) });
|
|
577
1092
|
},
|
|
578
1093
|
gin(...columns) {
|
|
579
|
-
return partial(createIndex({ columns, type: "Gin" }));
|
|
1094
|
+
return partial(createIndex({ keys: columns, type: "Gin" }));
|
|
580
1095
|
},
|
|
581
1096
|
gist(...columns) {
|
|
582
|
-
return partial(createIndex({ columns, type: "Gist" }));
|
|
1097
|
+
return partial(createIndex({ keys: columns, type: "Gist" }));
|
|
583
1098
|
},
|
|
584
1099
|
hash(...columns) {
|
|
585
|
-
return partial(createIndex({ columns, type: "Hash" }));
|
|
1100
|
+
return partial(createIndex({ keys: columns, type: "Hash" }));
|
|
586
1101
|
},
|
|
587
1102
|
brin(...columns) {
|
|
588
|
-
return partial(createIndex({ columns, type: "Brin" }));
|
|
1103
|
+
return partial(createIndex({ keys: columns, type: "Brin" }));
|
|
589
1104
|
}
|
|
590
1105
|
};
|
|
591
1106
|
}
|
|
@@ -629,6 +1144,7 @@ function create(config) {
|
|
|
629
1144
|
__primaryKey: config.primaryKey,
|
|
630
1145
|
__exclusions: config.exclusions,
|
|
631
1146
|
__checks: config.checks,
|
|
1147
|
+
__rowRules: config.rowRules,
|
|
632
1148
|
__extensions: config.extensions,
|
|
633
1149
|
__policy: config.policy,
|
|
634
1150
|
__comment: config.comment
|
|
@@ -657,7 +1173,7 @@ function detectOneToOnePairs(params) {
|
|
|
657
1173
|
for (const [modelName, modelRelations] of Object.entries(relations)) {
|
|
658
1174
|
for (const [relName, rel] of Object.entries(modelRelations)) {
|
|
659
1175
|
if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
|
|
660
|
-
throw new
|
|
1176
|
+
throw new VibeError5({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
|
|
661
1177
|
}
|
|
662
1178
|
if (rel.__relationType === "one") {
|
|
663
1179
|
allOnes.push({ modelName, relName, rel });
|
|
@@ -732,7 +1248,7 @@ function sameColumnTuple(params) {
|
|
|
732
1248
|
function resolveFkOwner(params) {
|
|
733
1249
|
const { a, b, models } = params;
|
|
734
1250
|
const ownershipError = () => {
|
|
735
|
-
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" } });
|
|
736
1252
|
};
|
|
737
1253
|
const aOwns = a.rel.__ownsForeignKey;
|
|
738
1254
|
const bOwns = b.rel.__ownsForeignKey;
|
|
@@ -901,28 +1417,31 @@ function planIndexes(params) {
|
|
|
901
1417
|
}
|
|
902
1418
|
}
|
|
903
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 } });
|
|
904
1422
|
if (index.__ops !== undefined) {
|
|
905
1423
|
if (index.__ops === null || typeof index.__ops !== "object" || Array.isArray(index.__ops))
|
|
906
|
-
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 } });
|
|
907
1425
|
if (index.__type === "unique")
|
|
908
|
-
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 } });
|
|
909
1427
|
for (const [field, operatorClass] of Object.entries(index.__ops)) {
|
|
910
1428
|
if (!index.__columns.includes(field))
|
|
911
|
-
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 } });
|
|
912
1430
|
if (operatorClass !== undefined && !isSqlObjectIdentity({ kind: "table", name: operatorClass }))
|
|
913
|
-
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 } });
|
|
914
1432
|
}
|
|
915
1433
|
}
|
|
916
1434
|
const operatorClasses = index.__columns.map((field) => index.__ops?.[field]);
|
|
917
1435
|
const operatorSuffix = operatorClasses.some((operatorClass) => operatorClass !== undefined) ? `|ops:${JSON.stringify([index.__indexType ?? "BTree", operatorClasses.map((operatorClass) => operatorClass === undefined ? null : [operatorClass.namespace, operatorClass.name])])}` : "";
|
|
918
1436
|
const predicateSuffix = index.__where !== undefined ? `|where:${index.__where}` : index.__wherePredicate === undefined ? "" : `|where:${predicateKey({ predicate: index.__wherePredicate })}`;
|
|
919
|
-
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)}`;
|
|
920
1438
|
const existing = byKey.get(key);
|
|
921
1439
|
const isUnique = index.__type === "unique";
|
|
922
1440
|
if (existing === undefined || isUnique) {
|
|
923
1441
|
byKey.set(key, {
|
|
924
1442
|
isUnique,
|
|
925
1443
|
columns: [...index.__columns],
|
|
1444
|
+
...index.__keys === undefined ? {} : { keys: index.__keys },
|
|
926
1445
|
indexType: index.__indexType,
|
|
927
1446
|
...index.__ops === undefined ? {} : { ops: index.__ops },
|
|
928
1447
|
...index.__map === undefined ? {} : { map: index.__map },
|
|
@@ -949,7 +1468,7 @@ function referenceableKeys(params) {
|
|
|
949
1468
|
if (model.__primaryKey !== undefined && model.__primaryKey.length > 0)
|
|
950
1469
|
keys.push([...model.__primaryKey]);
|
|
951
1470
|
for (const index of evaluateIndexes({ model })) {
|
|
952
|
-
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) {
|
|
953
1472
|
keys.push([...index.__columns]);
|
|
954
1473
|
}
|
|
955
1474
|
}
|
|
@@ -961,7 +1480,7 @@ function assertForeignKeyIntegrity(params) {
|
|
|
961
1480
|
const references = relation.references ?? [];
|
|
962
1481
|
const at = `${relation.model}.${relation.name}`;
|
|
963
1482
|
const refuse = (message, rule, meta = {}) => {
|
|
964
|
-
throw new
|
|
1483
|
+
throw new VibeError5({
|
|
965
1484
|
code: "VIBE_SCHEMA_INVALID",
|
|
966
1485
|
message: `Relation ${at}: ${message}`,
|
|
967
1486
|
meta: { rule, model: relation.model, relation: relation.name, ...meta }
|
|
@@ -1011,12 +1530,14 @@ function assertForeignKeyIntegrity(params) {
|
|
|
1011
1530
|
}
|
|
1012
1531
|
}
|
|
1013
1532
|
function relationLookups(params) {
|
|
1533
|
+
const inputs = [];
|
|
1014
1534
|
const incoming = new Map;
|
|
1015
1535
|
for (const [modelName, relations] of Object.entries(params.schema.__relations)) {
|
|
1016
1536
|
for (const [relName, rel] of Object.entries(relations)) {
|
|
1017
1537
|
if (rel.__relationType !== "one")
|
|
1018
1538
|
continue;
|
|
1019
1539
|
const target = rel.__targetModel;
|
|
1540
|
+
inputs.push(modelName, relName, rel, target);
|
|
1020
1541
|
const entries = incoming.get(target);
|
|
1021
1542
|
const entry = { modelName, relName, rel };
|
|
1022
1543
|
if (entries === undefined)
|
|
@@ -1028,6 +1549,7 @@ function relationLookups(params) {
|
|
|
1028
1549
|
const parents = new Map;
|
|
1029
1550
|
for (const [name, model] of params.schema.__models) {
|
|
1030
1551
|
for (const child of model.__children.values()) {
|
|
1552
|
+
inputs.push(child.__name, name, model);
|
|
1031
1553
|
const entries = parents.get(child.__name);
|
|
1032
1554
|
if (entries === undefined)
|
|
1033
1555
|
parents.set(child.__name, [[name, model]]);
|
|
@@ -1035,7 +1557,35 @@ function relationLookups(params) {
|
|
|
1035
1557
|
entries.push([name, model]);
|
|
1036
1558
|
}
|
|
1037
1559
|
}
|
|
1038
|
-
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;
|
|
1039
1589
|
}
|
|
1040
1590
|
function planSchema(params) {
|
|
1041
1591
|
const { schema } = params;
|
|
@@ -1049,7 +1599,8 @@ function planSchema(params) {
|
|
|
1049
1599
|
let refreshLookups = false;
|
|
1050
1600
|
for (const [modelName, model] of schema.__models) {
|
|
1051
1601
|
if (refreshLookups) {
|
|
1052
|
-
|
|
1602
|
+
if (!lookupsCurrent({ schema, inputs: lookups.inputs }))
|
|
1603
|
+
lookups = relationLookups({ schema });
|
|
1053
1604
|
refreshLookups = false;
|
|
1054
1605
|
}
|
|
1055
1606
|
const explicit = planExplicitRelations({
|
|
@@ -1121,25 +1672,41 @@ function idDefault(params) {
|
|
|
1121
1672
|
}
|
|
1122
1673
|
function fieldDefault(params) {
|
|
1123
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
|
+
}
|
|
1124
1691
|
if (column.__columnType === "id") {
|
|
1125
1692
|
const strategy = column.__config.type;
|
|
1126
1693
|
const dbgenerated = column.__config.dbgenerated;
|
|
1127
1694
|
if (column.__config.default !== undefined || column.__config.autoincrement !== undefined)
|
|
1128
|
-
throw new
|
|
1695
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
|
|
1129
1696
|
if (dbgenerated !== undefined) {
|
|
1130
1697
|
if (strategy !== "none" || typeof dbgenerated !== "string")
|
|
1131
|
-
throw new
|
|
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 }] } });
|
|
1132
1699
|
return { kind: "dbgenerated", sql: dbgenerated };
|
|
1133
1700
|
}
|
|
1134
1701
|
return idDefault({ column });
|
|
1135
1702
|
}
|
|
1136
1703
|
const { dbgenerated, default: value, autoincrement } = column.__config;
|
|
1137
1704
|
if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
|
|
1138
|
-
throw new
|
|
1705
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
|
|
1139
1706
|
}
|
|
1140
1707
|
if (autoincrement === true) {
|
|
1141
1708
|
if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
|
|
1142
|
-
throw new
|
|
1709
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
|
|
1143
1710
|
}
|
|
1144
1711
|
return { kind: "autoincrement" };
|
|
1145
1712
|
}
|
|
@@ -1149,7 +1716,7 @@ function fieldDefault(params) {
|
|
|
1149
1716
|
return;
|
|
1150
1717
|
if (column.__config.array === true) {
|
|
1151
1718
|
if (!Array.isArray(value))
|
|
1152
|
-
throw new
|
|
1719
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
|
|
1153
1720
|
return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
|
|
1154
1721
|
}
|
|
1155
1722
|
if (column.__columnType === "json" && typeof value !== "string") {
|
|
@@ -1164,20 +1731,20 @@ function fieldDefault(params) {
|
|
|
1164
1731
|
return { kind: "enumValue", value };
|
|
1165
1732
|
}
|
|
1166
1733
|
if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
|
|
1167
|
-
throw new
|
|
1734
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
|
|
1168
1735
|
}
|
|
1169
1736
|
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
1170
1737
|
return { kind: "literal", value };
|
|
1171
1738
|
}
|
|
1172
1739
|
if (typeof value === "bigint")
|
|
1173
1740
|
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
1174
|
-
throw new
|
|
1741
|
+
throw new VibeError6({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
|
|
1175
1742
|
}
|
|
1176
1743
|
function datetimeNativeType(params) {
|
|
1177
1744
|
const { tz, precision } = params.config;
|
|
1178
1745
|
if (precision !== undefined && (tz !== true || !Number.isInteger(precision) || precision < 0 || precision > 6)) {
|
|
1179
1746
|
const at = params.column === undefined ? "A datetime column" : `Column "${params.column}"`;
|
|
1180
|
-
throw new
|
|
1747
|
+
throw new VibeError6({
|
|
1181
1748
|
code: "VIBE_SCHEMA_INVALID",
|
|
1182
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 })`,
|
|
1183
1750
|
meta: { issues: [{ rule: "datetime-precision", column: params.column, precision, tz: tz === true }] }
|
|
@@ -1192,7 +1759,7 @@ function stringNativeType(params) {
|
|
|
1192
1759
|
if (uuid === true) {
|
|
1193
1760
|
if (maxLength !== undefined || text === true) {
|
|
1194
1761
|
const at = params.column === undefined ? "A string column" : `Column "${params.column}"`;
|
|
1195
|
-
throw new
|
|
1762
|
+
throw new VibeError6({
|
|
1196
1763
|
code: "VIBE_SCHEMA_INVALID",
|
|
1197
1764
|
message: `${at}: uuid() is the column's type and cannot be combined with maxLength or text`,
|
|
1198
1765
|
meta: { issues: [{ rule: "string-native-type", column: params.column }] }
|
|
@@ -1210,7 +1777,7 @@ function idNativeType(params) {
|
|
|
1210
1777
|
if (params.config.uuid !== true)
|
|
1211
1778
|
return;
|
|
1212
1779
|
if (params.config.type === "autoincrement") {
|
|
1213
|
-
throw new
|
|
1780
|
+
throw new VibeError6({
|
|
1214
1781
|
code: "VIBE_SCHEMA_INVALID",
|
|
1215
1782
|
message: `${params.column === undefined ? "An id" : `Id "${params.column}"`}: uuid() needs a String id; an autoincrement id is an Int`,
|
|
1216
1783
|
meta: { issues: [{ rule: "id-native-type", column: params.column }] }
|
|
@@ -1220,6 +1787,8 @@ function idNativeType(params) {
|
|
|
1220
1787
|
}
|
|
1221
1788
|
function nativeType(params) {
|
|
1222
1789
|
const { column } = params;
|
|
1790
|
+
if (column.__columnType === "localDate")
|
|
1791
|
+
return "Date";
|
|
1223
1792
|
if (column.__columnType === "string")
|
|
1224
1793
|
return stringNativeType({ config: column.__config, column: column.__name });
|
|
1225
1794
|
if (column.__columnType === "id")
|
|
@@ -1229,7 +1798,7 @@ function nativeType(params) {
|
|
|
1229
1798
|
if (maxLength !== undefined || text !== undefined) {
|
|
1230
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;
|
|
1231
1800
|
if (invalid) {
|
|
1232
|
-
throw new
|
|
1801
|
+
throw new VibeError6({
|
|
1233
1802
|
code: "VIBE_SCHEMA_INVALID",
|
|
1234
1803
|
message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
|
|
1235
1804
|
meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
|
|
@@ -1266,7 +1835,8 @@ function buildField(params) {
|
|
|
1266
1835
|
const dbName = typeof config.map === "string" ? config.map : undefined;
|
|
1267
1836
|
const documentation = typeof config.comment === "string" ? config.comment : undefined;
|
|
1268
1837
|
const collation = typeof config.collation === "string" ? config.collation : undefined;
|
|
1269
|
-
const
|
|
1838
|
+
const validatorMetadata = normalizeValidatorRef({ documentation, reference: config.validator });
|
|
1839
|
+
const metadata = normalizeJsonSchemaRef({ documentation: validatorMetadata.documentation, reference: config.jsonSchema });
|
|
1270
1840
|
const defaultValue = fieldDefault({ column });
|
|
1271
1841
|
const native = extensionNativeType ?? nativeType({ column });
|
|
1272
1842
|
return {
|
|
@@ -1280,8 +1850,10 @@ function buildField(params) {
|
|
|
1280
1850
|
...collation === undefined ? {} : { collation },
|
|
1281
1851
|
...dbName === undefined ? {} : { dbName },
|
|
1282
1852
|
...defaultValue === undefined ? {} : { default: defaultValue },
|
|
1853
|
+
..."generated" in config ? { generated: generatedColumnFromSql2({ value: config.generated }) } : {},
|
|
1283
1854
|
...native === undefined ? {} : { nativeType: native },
|
|
1284
1855
|
...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
|
|
1856
|
+
...validatorMetadata.validator === undefined ? {} : { validator: validatorMetadata.validator },
|
|
1285
1857
|
...metadata
|
|
1286
1858
|
};
|
|
1287
1859
|
}
|
|
@@ -1315,17 +1887,26 @@ function buildIndex(params) {
|
|
|
1315
1887
|
if (index.ops !== undefined && Object.values(index.ops).some((operatorClass) => operatorClass !== undefined) && !dialect.capabilities.indexOperatorClasses)
|
|
1316
1888
|
throw unsupportedCapability({ dialect: dialect.name, feature: "indexOperatorClasses" });
|
|
1317
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
|
+
}
|
|
1318
1899
|
const predicate = compileIndexPredicate({
|
|
1319
1900
|
where: index.where,
|
|
1320
1901
|
wherePredicate: index.wherePredicate,
|
|
1321
|
-
columns: index.columns,
|
|
1902
|
+
columns: computed ? indexKeyFieldNames({ index: keys }) ?? [] : index.columns,
|
|
1322
1903
|
model,
|
|
1323
1904
|
dialect,
|
|
1324
1905
|
enums
|
|
1325
1906
|
});
|
|
1326
1907
|
return {
|
|
1327
1908
|
kind,
|
|
1328
|
-
|
|
1909
|
+
...keys,
|
|
1329
1910
|
...index.map === undefined ? {} : { name: index.map },
|
|
1330
1911
|
...predicate === undefined ? {} : { where: predicate.where },
|
|
1331
1912
|
...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
|
|
@@ -1412,8 +1993,8 @@ function buildModel(params) {
|
|
|
1412
1993
|
}
|
|
1413
1994
|
const predicateModel = { name: model.__name, fields };
|
|
1414
1995
|
const exclusions = compileExclusions({ exclusions: model.__exclusions, model: predicateModel, dialect });
|
|
1415
|
-
const checks =
|
|
1416
|
-
|
|
1996
|
+
const checks = compileModelChecks({ model, predicateModel, dialect, enums });
|
|
1997
|
+
const irModel = {
|
|
1417
1998
|
name: model.__name,
|
|
1418
1999
|
...model.__map === undefined ? {} : { dbName: model.__map },
|
|
1419
2000
|
fields,
|
|
@@ -1426,6 +2007,8 @@ function buildModel(params) {
|
|
|
1426
2007
|
isView: false,
|
|
1427
2008
|
...model.__comment === undefined ? {} : { documentation: model.__comment }
|
|
1428
2009
|
};
|
|
2010
|
+
const rowRules = compileRowRules({ model, irModel, dialect: dialect.name });
|
|
2011
|
+
return rowRules.length === 0 ? irModel : { ...irModel, rowRules };
|
|
1429
2012
|
}
|
|
1430
2013
|
|
|
1431
2014
|
class ExtensionDeclarationCollector {
|
|
@@ -1470,7 +2053,7 @@ function toIR(params) {
|
|
|
1470
2053
|
}
|
|
1471
2054
|
}
|
|
1472
2055
|
if (issues.length > 0) {
|
|
1473
|
-
throw new
|
|
2056
|
+
throw new VibeError6({
|
|
1474
2057
|
code: "VIBE_SCHEMA_INVALID",
|
|
1475
2058
|
message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
|
|
1476
2059
|
meta: { issues }
|
|
@@ -1494,11 +2077,36 @@ function predicateModel(params) {
|
|
|
1494
2077
|
fields: Array.from(params.model.__columns, ([name, column]) => buildField({ name, column }))
|
|
1495
2078
|
};
|
|
1496
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
|
+
}
|
|
1497
2104
|
function prismaType(params) {
|
|
1498
2105
|
const { column } = params;
|
|
1499
2106
|
const list = column.__config.array === true ? "[]" : "";
|
|
1500
2107
|
switch (column.__columnType) {
|
|
1501
2108
|
case "string":
|
|
2109
|
+
case "localDate":
|
|
1502
2110
|
return `String${list}`;
|
|
1503
2111
|
case "int":
|
|
1504
2112
|
return `Int${list}`;
|
|
@@ -1551,6 +2159,8 @@ function defaultAttribute(params) {
|
|
|
1551
2159
|
}
|
|
1552
2160
|
function nativeTypeAttribute(params) {
|
|
1553
2161
|
const { column } = params;
|
|
2162
|
+
if (column.__columnType === "localDate")
|
|
2163
|
+
return "@db.Date";
|
|
1554
2164
|
if (column.__columnType === "string" || column.__columnType === "id") {
|
|
1555
2165
|
const native = column.__columnType === "string" ? stringNativeType({ config: column.__config, column: column.__name }) : idNativeType({ config: column.__config, column: column.__name });
|
|
1556
2166
|
return native === undefined ? undefined : `@db.${native}`;
|
|
@@ -1580,6 +2190,10 @@ function formatColumn(params) {
|
|
|
1580
2190
|
if (columnDefault !== undefined)
|
|
1581
2191
|
parts.push(columnDefault);
|
|
1582
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
|
+
}
|
|
1583
2197
|
if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
|
|
1584
2198
|
parts.push("@updatedAt");
|
|
1585
2199
|
}
|
|
@@ -1596,13 +2210,17 @@ function formatColumn(params) {
|
|
|
1596
2210
|
parts.push(`@collation(${encodePrismaString2({ value: column.__config.collation })})`);
|
|
1597
2211
|
}
|
|
1598
2212
|
const columnLine = parts.join(" ");
|
|
1599
|
-
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 }] } });
|
|
1600
2217
|
if (metadata.jsonSchema !== undefined && column.__columnType !== "json")
|
|
1601
|
-
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 }] } });
|
|
1602
2219
|
const comments = [
|
|
1603
2220
|
...metadata.documentation === undefined ? [] : metadata.documentation.split(`
|
|
1604
2221
|
`),
|
|
1605
|
-
...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 })})`]
|
|
1606
2224
|
];
|
|
1607
2225
|
return [...comments.map((line) => ` /// ${line}`), columnLine].join(`
|
|
1608
2226
|
`);
|
|
@@ -1642,8 +2260,8 @@ function formatRelation(params) {
|
|
|
1642
2260
|
function formatIndex(params) {
|
|
1643
2261
|
const { index } = params;
|
|
1644
2262
|
if (index.ops !== undefined && Object.values(index.ops).some((operatorClass) => operatorClass !== undefined))
|
|
1645
|
-
throw new
|
|
1646
|
-
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(", ");
|
|
1647
2265
|
const predicate = compileIndexPredicate({
|
|
1648
2266
|
where: index.where,
|
|
1649
2267
|
wherePredicate: index.wherePredicate,
|
|
@@ -1693,14 +2311,24 @@ function formatModel(params) {
|
|
|
1693
2311
|
for (const index of plan.indexes.get(model.__name) ?? []) {
|
|
1694
2312
|
lines.push(formatIndex({ index, model: params.model, dialect: params.dialect, enums: params.enums }));
|
|
1695
2313
|
}
|
|
1696
|
-
for (const check of
|
|
1697
|
-
|
|
1698
|
-
|
|
2314
|
+
for (const check of compileModelChecks({
|
|
2315
|
+
model: params.model,
|
|
2316
|
+
predicateModel: predicateModel({ model: params.model }),
|
|
1699
2317
|
dialect: params.dialect,
|
|
1700
2318
|
enums: params.enums
|
|
1701
2319
|
})) {
|
|
1702
2320
|
lines.push(` @@check(${encodePrismaString2({ value: check.expression })}, name: ${encodePrismaString2({ value: check.name })})`);
|
|
1703
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 })}`);
|
|
1704
2332
|
for (const use of model.__extensions ?? []) {
|
|
1705
2333
|
const args = [];
|
|
1706
2334
|
if (use.fields !== undefined)
|
|
@@ -1734,24 +2362,31 @@ function generatePrisma(schema, config = {}) {
|
|
|
1734
2362
|
for (const model of schema.__models.values()) {
|
|
1735
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" }) }) });
|
|
1736
2364
|
if (exclusions.length > 0)
|
|
1737
|
-
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" } });
|
|
1738
2366
|
}
|
|
1739
2367
|
const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
|
|
1740
2368
|
if (stringEnums.length > 0) {
|
|
1741
|
-
throw new
|
|
2369
|
+
throw new VibeError7({
|
|
1742
2370
|
code: "VIBE_CONFIG",
|
|
1743
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.`,
|
|
1744
2372
|
meta: { enums: stringEnums, target: "prisma" }
|
|
1745
2373
|
});
|
|
1746
2374
|
}
|
|
1747
2375
|
if (schema.__rls !== undefined) {
|
|
1748
|
-
throw new
|
|
2376
|
+
throw new VibeError7({
|
|
1749
2377
|
code: "VIBE_CONFIG",
|
|
1750
2378
|
message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
|
|
1751
2379
|
meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
|
|
1752
2380
|
});
|
|
1753
2381
|
}
|
|
1754
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
|
+
} });
|
|
1755
2390
|
const sections = [];
|
|
1756
2391
|
const dialect = getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) });
|
|
1757
2392
|
const enums = Array.from(schema.__enums, ([name, values]) => ({ name, values: [...values] }));
|
|
@@ -1778,9 +2413,10 @@ function generatePrisma(schema, config = {}) {
|
|
|
1778
2413
|
}
|
|
1779
2414
|
|
|
1780
2415
|
// packages/dsl/src/relations.ts
|
|
1781
|
-
import { VibeError as
|
|
2416
|
+
import { VibeError as VibeError8 } from "@vibeorm/schema";
|
|
1782
2417
|
var REF_KIND_BY_COLUMN_TYPE = {
|
|
1783
2418
|
string: "String",
|
|
2419
|
+
localDate: "String",
|
|
1784
2420
|
int: "Int",
|
|
1785
2421
|
bigint: "BigInt",
|
|
1786
2422
|
float: "Float",
|
|
@@ -1819,7 +2455,7 @@ function extractRefInfo(params) {
|
|
|
1819
2455
|
}
|
|
1820
2456
|
const members = ref;
|
|
1821
2457
|
if (members.length === 0) {
|
|
1822
|
-
throw new
|
|
2458
|
+
throw new VibeError8({
|
|
1823
2459
|
code: "VIBE_SCHEMA_INVALID",
|
|
1824
2460
|
message: `Relation to ${target}: "${side}" is an empty tuple \u2014 name at least one column`,
|
|
1825
2461
|
meta: { rule: "relation-empty-tuple", side, targetModel: target }
|
|
@@ -1827,14 +2463,14 @@ function extractRefInfo(params) {
|
|
|
1827
2463
|
}
|
|
1828
2464
|
const models = [...new Set(members.map((member) => member.__model))].sort();
|
|
1829
2465
|
if (models.length > 1) {
|
|
1830
|
-
throw new
|
|
2466
|
+
throw new VibeError8({
|
|
1831
2467
|
code: "VIBE_SCHEMA_INVALID",
|
|
1832
2468
|
message: `Relation to ${target}: "${side}" mixes models ${models.join(", ")} \u2014 every member of a composite key must sit on one model`,
|
|
1833
2469
|
meta: { rule: "relation-mixed-models", side, targetModel: target, models }
|
|
1834
2470
|
});
|
|
1835
2471
|
}
|
|
1836
2472
|
if (members.some((member) => member.__type !== "columnRef")) {
|
|
1837
|
-
throw new
|
|
2473
|
+
throw new VibeError8({
|
|
1838
2474
|
code: "VIBE_SCHEMA_INVALID",
|
|
1839
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`,
|
|
1840
2476
|
meta: { rule: "relation-through-in-tuple", side, targetModel: target }
|
|
@@ -1848,7 +2484,7 @@ function createRelationFactory(params) {
|
|
|
1848
2484
|
const from = extractRefInfo({ ref: config.from, side: "from", target: targetModelName });
|
|
1849
2485
|
const to = extractRefInfo({ ref: config.to, side: "to", target: targetModelName });
|
|
1850
2486
|
if (from.columns.length !== to.columns.length) {
|
|
1851
|
-
throw new
|
|
2487
|
+
throw new VibeError8({
|
|
1852
2488
|
code: "VIBE_SCHEMA_INVALID",
|
|
1853
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`,
|
|
1854
2490
|
meta: { rule: "relation-tuple-arity", targetModel: targetModelName, from: from.columns, to: to.columns }
|
|
@@ -1946,7 +2582,7 @@ function mergeRelations(...parts) {
|
|
|
1946
2582
|
}
|
|
1947
2583
|
|
|
1948
2584
|
// packages/dsl/src/schema.ts
|
|
1949
|
-
import { VibeError as
|
|
2585
|
+
import { VibeError as VibeError9, validateRlsContextIR } from "@vibeorm/schema";
|
|
1950
2586
|
function sameValues(left, right) {
|
|
1951
2587
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1952
2588
|
}
|
|
@@ -1965,7 +2601,7 @@ function registerEnum(params) {
|
|
|
1965
2601
|
...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
|
|
1966
2602
|
...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
|
|
1967
2603
|
].join("; ");
|
|
1968
|
-
throw new
|
|
2604
|
+
throw new VibeError9({
|
|
1969
2605
|
code: "VIBE_SCHEMA_INVALID",
|
|
1970
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`,
|
|
1971
2607
|
meta: {
|
|
@@ -1991,7 +2627,7 @@ function addModelTree(params) {
|
|
|
1991
2627
|
const { model, models, origin } = params;
|
|
1992
2628
|
const existing = models.get(model.__name);
|
|
1993
2629
|
if (existing !== undefined && existing !== model) {
|
|
1994
|
-
throw new
|
|
2630
|
+
throw new VibeError9({
|
|
1995
2631
|
code: "VIBE_SCHEMA_INVALID",
|
|
1996
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`,
|
|
1997
2633
|
meta: {
|
|
@@ -2069,7 +2705,7 @@ var RLS_CONTEXT_TYPES = {
|
|
|
2069
2705
|
};
|
|
2070
2706
|
function defineRls(params) {
|
|
2071
2707
|
if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
|
|
2072
|
-
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" }] } });
|
|
2073
2709
|
}
|
|
2074
2710
|
const entries = [];
|
|
2075
2711
|
const refs = [];
|
|
@@ -2077,7 +2713,7 @@ function defineRls(params) {
|
|
|
2077
2713
|
const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
|
|
2078
2714
|
const column = descriptor?.value;
|
|
2079
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) {
|
|
2080
|
-
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" }] } });
|
|
2081
2717
|
}
|
|
2082
2718
|
entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
|
|
2083
2719
|
refs.push([key, Object.freeze({ context: key })]);
|
|
@@ -2085,22 +2721,22 @@ function defineRls(params) {
|
|
|
2085
2721
|
const context = Object.freeze(Object.fromEntries(entries));
|
|
2086
2722
|
const issues = validateRlsContextIR({ context });
|
|
2087
2723
|
if (issues.length > 0) {
|
|
2088
|
-
throw new
|
|
2724
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
|
|
2089
2725
|
}
|
|
2090
2726
|
const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
|
|
2091
2727
|
if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
|
|
2092
|
-
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" }] } });
|
|
2093
2729
|
}
|
|
2094
2730
|
const named = [];
|
|
2095
2731
|
const classified = new Set;
|
|
2096
2732
|
for (const key of Reflect.ownKeys(policies)) {
|
|
2097
2733
|
const descriptor = Object.getOwnPropertyDescriptor(policies, key);
|
|
2098
2734
|
if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
|
|
2099
|
-
throw new
|
|
2735
|
+
throw new VibeError9({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
2100
2736
|
}
|
|
2101
2737
|
const name = params.models[key]?.__name ?? key;
|
|
2102
2738
|
if (classified.has(name)) {
|
|
2103
|
-
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 }] } });
|
|
2104
2740
|
}
|
|
2105
2741
|
classified.add(name);
|
|
2106
2742
|
named.push([name, descriptor.value]);
|
|
@@ -2343,6 +2979,7 @@ function enumFactory(config) {
|
|
|
2343
2979
|
var generate = Object.assign((schemaDef) => ({ prisma: generatePrisma(schemaDef) }), { prisma: generatePrisma });
|
|
2344
2980
|
var dsl = {
|
|
2345
2981
|
string,
|
|
2982
|
+
localDate,
|
|
2346
2983
|
int,
|
|
2347
2984
|
bigint,
|
|
2348
2985
|
float,
|
|
@@ -2372,9 +3009,11 @@ export {
|
|
|
2372
3009
|
ChainableDatetimeColumn,
|
|
2373
3010
|
ChainableDecimalColumn,
|
|
2374
3011
|
ChainableEnumColumn,
|
|
3012
|
+
ChainableFloatColumn,
|
|
2375
3013
|
ChainableIdColumn,
|
|
2376
3014
|
ChainableIntegerColumn,
|
|
2377
3015
|
ChainableJsonColumn,
|
|
3016
|
+
ChainableNumericColumn,
|
|
2378
3017
|
ChainableStringColumn,
|
|
2379
3018
|
base,
|
|
2380
3019
|
baseWithWorkspace,
|
|
@@ -2382,11 +3021,15 @@ export {
|
|
|
2382
3021
|
cloneColumnDef,
|
|
2383
3022
|
compileChecks,
|
|
2384
3023
|
compileIndexPredicate,
|
|
3024
|
+
compileModelChecks,
|
|
3025
|
+
compileRowRules,
|
|
2385
3026
|
create,
|
|
3027
|
+
createChecksBuilder,
|
|
2386
3028
|
createChildrenBuilder,
|
|
2387
3029
|
createEnum,
|
|
2388
3030
|
createIndexBuilder,
|
|
2389
3031
|
createPredicateBuilder,
|
|
3032
|
+
createRowRuleBuilder,
|
|
2390
3033
|
defineRelations,
|
|
2391
3034
|
defineRelationsPart,
|
|
2392
3035
|
defineRls,
|
|
@@ -2398,6 +3041,7 @@ export {
|
|
|
2398
3041
|
fields,
|
|
2399
3042
|
generatePrisma,
|
|
2400
3043
|
getColumnNames,
|
|
3044
|
+
localDate,
|
|
2401
3045
|
mergeRelations,
|
|
2402
3046
|
planSchema,
|
|
2403
3047
|
resolveRelationType,
|
|
@@ -2412,4 +3056,4 @@ export {
|
|
|
2412
3056
|
withNameDescription
|
|
2413
3057
|
};
|
|
2414
3058
|
|
|
2415
|
-
//# debugId=
|
|
3059
|
+
//# debugId=7EE10300C00F60DE64756E2164756E21
|