@vibeorm/dsl 2.0.0 → 2.1.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/field-type.d.ts +30 -0
- package/dist/field-type.d.ts.map +1 -0
- package/dist/generator/prisma.d.ts +1 -1
- package/dist/generator/prisma.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +531 -118
- package/dist/index.js.map +12 -10
- package/dist/model.d.ts +1 -1
- package/dist/model.d.ts.map +1 -1
- package/dist/predicate.d.ts +86 -0
- package/dist/predicate.d.ts.map +1 -0
- package/dist/relation-plan.d.ts +9 -1
- package/dist/relation-plan.d.ts.map +1 -1
- package/dist/relations.d.ts.map +1 -1
- package/dist/to-ir.d.ts +14 -1
- package/dist/to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +289 -30
- package/dist/types.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -224,21 +224,224 @@ function createEnum(config) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// packages/dsl/src/generator/prisma.ts
|
|
227
|
-
import { VibeError as
|
|
227
|
+
import { VibeError as VibeError5, encodePrismaString, internalError, providerToDialect as providerToDialect2 } from "@vibeorm/schema";
|
|
228
|
+
import { getSqlDialect as getSqlDialect2 } from "@vibeorm/sql";
|
|
229
|
+
|
|
230
|
+
// packages/dsl/src/predicate.ts
|
|
231
|
+
import { VibeError } from "@vibeorm/schema";
|
|
232
|
+
import { compileExclusion, renderPredicate } from "@vibeorm/sql";
|
|
233
|
+
function isFieldExpr(operand) {
|
|
234
|
+
return typeof operand === "object" && operand !== null && "kind" in operand && operand.kind === "field";
|
|
235
|
+
}
|
|
236
|
+
function toRef(params) {
|
|
237
|
+
return { kind: "field", name: params.expr.name };
|
|
238
|
+
}
|
|
239
|
+
function toValue(params) {
|
|
240
|
+
const { input, at } = params;
|
|
241
|
+
if (typeof input === "bigint")
|
|
242
|
+
return { kind: "value", value: input.toString() };
|
|
243
|
+
if (typeof input === "string" || typeof input === "number" || typeof input === "boolean") {
|
|
244
|
+
return { kind: "value", value: input };
|
|
245
|
+
}
|
|
246
|
+
throw new VibeError({
|
|
247
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
248
|
+
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`,
|
|
249
|
+
meta: { rule: "predicate-literal-invalid", at }
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
function toOperand(params) {
|
|
253
|
+
const { operand, at } = params;
|
|
254
|
+
return isFieldExpr(operand) ? toRef({ expr: operand }) : toValue({ input: operand, at });
|
|
255
|
+
}
|
|
256
|
+
function requireFieldExpr(params) {
|
|
257
|
+
const { value, at } = params;
|
|
258
|
+
if (isFieldExpr(value))
|
|
259
|
+
return value;
|
|
260
|
+
throw new VibeError({
|
|
261
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
262
|
+
message: `${at}: the left side of a comparison must be c.field("\u2026"), not a bare value`,
|
|
263
|
+
meta: { rule: "predicate-operand-invalid", at }
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function requireOperands(params) {
|
|
267
|
+
const { operands, at } = params;
|
|
268
|
+
if (operands.length === 0) {
|
|
269
|
+
throw new VibeError({
|
|
270
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
271
|
+
message: `${at}: an empty boolean group has no honest SQL spelling \u2014 pass at least one operand`,
|
|
272
|
+
meta: { rule: "predicate-empty", at }
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return operands;
|
|
276
|
+
}
|
|
277
|
+
function createPredicateBuilder() {
|
|
278
|
+
const compare = (op) => (left, right) => ({
|
|
279
|
+
kind: "compare",
|
|
280
|
+
op,
|
|
281
|
+
left: toRef({ expr: requireFieldExpr({ value: left, at: `c.${op}()` }) }),
|
|
282
|
+
right: toOperand({ operand: right, at: `c.${op}()` })
|
|
283
|
+
});
|
|
284
|
+
const membership = (negate) => (field, values) => {
|
|
285
|
+
const at = negate ? "c.notInList()" : "c.inList()";
|
|
286
|
+
const ref = toRef({ expr: requireFieldExpr({ value: field, at }) });
|
|
287
|
+
if (values.length === 0) {
|
|
288
|
+
throw new VibeError({
|
|
289
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
290
|
+
message: `${at}: an empty IN list is UNKNOWN on every row and is never what an author means`,
|
|
291
|
+
meta: { rule: "predicate-empty", at, field: ref.name }
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
kind: "inList",
|
|
296
|
+
field: ref,
|
|
297
|
+
values: values.map((value) => toValue({ input: value, at })),
|
|
298
|
+
...negate ? { negate: true } : {}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
return {
|
|
302
|
+
field: (name) => ({ kind: "field", name, __kind: "String" }),
|
|
303
|
+
eq: compare("eq"),
|
|
304
|
+
ne: compare("ne"),
|
|
305
|
+
gt: compare("gt"),
|
|
306
|
+
gte: compare("gte"),
|
|
307
|
+
lt: compare("lt"),
|
|
308
|
+
lte: compare("lte"),
|
|
309
|
+
inList: membership(false),
|
|
310
|
+
notInList: membership(true),
|
|
311
|
+
isNull: (name) => ({ kind: "isNull", field: { kind: "field", name } }),
|
|
312
|
+
isNotNull: (name) => ({ kind: "isNull", field: { kind: "field", name }, negate: true }),
|
|
313
|
+
and: (...operands) => ({ kind: "and", operands: requireOperands({ operands, at: "c.and()" }) }),
|
|
314
|
+
or: (...operands) => ({ kind: "or", operands: requireOperands({ operands, at: "c.or()" }) }),
|
|
315
|
+
not: (operand) => ({ kind: "not", operand }),
|
|
316
|
+
named: (name, predicate) => {
|
|
317
|
+
if (name.trim() === "") {
|
|
318
|
+
throw new VibeError({
|
|
319
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
320
|
+
message: "c.named(): a CHECK needs a non-empty name \u2014 the name is what keys the migration diff",
|
|
321
|
+
meta: { rule: "predicate-unnamed-check" }
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
return { __type: "check", name, predicate };
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function evaluateChecks(params) {
|
|
329
|
+
const { checks, model } = params;
|
|
330
|
+
if (checks === undefined)
|
|
331
|
+
return [];
|
|
332
|
+
if (typeof checks !== "function") {
|
|
333
|
+
return checks.map((check) => ({ kind: "raw", name: check.name, expression: check.expression }));
|
|
334
|
+
}
|
|
335
|
+
const declared = checks(createPredicateBuilder());
|
|
336
|
+
return declared.map((entry, index) => {
|
|
337
|
+
if (entry === null || typeof entry !== "object" || entry.__type !== "check") {
|
|
338
|
+
throw new VibeError({
|
|
339
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
340
|
+
message: `Model "${model}": checks[${index}] is not a named CHECK \u2014 wrap the expression in c.named("name", \u2026)`,
|
|
341
|
+
meta: { rule: "predicate-unnamed-check", model, index }
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return { kind: "typed", name: entry.name, predicate: entry.predicate };
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function compileExclusions(params) {
|
|
348
|
+
const specs = typeof params.exclusions === "function" ? params.exclusions(createPredicateBuilder()) : params.exclusions ?? [];
|
|
349
|
+
return specs.map((exclusion) => compileExclusion({ exclusion, model: params.model, dialect: params.dialect }));
|
|
350
|
+
}
|
|
351
|
+
function compileChecks(params) {
|
|
352
|
+
const { checks, model, dialect, enums } = params;
|
|
353
|
+
return evaluateChecks({ checks, model: model.name }).map((check) => check.kind === "raw" ? { name: check.name, expression: check.expression } : {
|
|
354
|
+
name: check.name,
|
|
355
|
+
expression: renderPredicate({
|
|
356
|
+
predicate: check.predicate,
|
|
357
|
+
model,
|
|
358
|
+
dialect,
|
|
359
|
+
enums,
|
|
360
|
+
usage: "check",
|
|
361
|
+
site: `CHECK "${check.name}"`
|
|
362
|
+
}),
|
|
363
|
+
predicate: check.predicate
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function compileIndexPredicate(params) {
|
|
367
|
+
const { where, wherePredicate, columns, model, dialect, enums } = params;
|
|
368
|
+
if (where !== undefined && wherePredicate !== undefined) {
|
|
369
|
+
throw new VibeError({
|
|
370
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
371
|
+
message: `Index on "${model.name}" [${columns.join(", ")}] carries both a raw where: string and a typed .where() \u2014 an index has ONE predicate`,
|
|
372
|
+
meta: { rule: "predicate-duplicate-where", model: model.name, columns: [...columns] }
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
if (where !== undefined)
|
|
376
|
+
return { where };
|
|
377
|
+
if (wherePredicate === undefined)
|
|
378
|
+
return;
|
|
379
|
+
return {
|
|
380
|
+
where: renderPredicate({
|
|
381
|
+
predicate: wherePredicate,
|
|
382
|
+
model,
|
|
383
|
+
dialect,
|
|
384
|
+
enums,
|
|
385
|
+
usage: "indexPredicate",
|
|
386
|
+
site: `partial index on [${columns.join(", ")}]`
|
|
387
|
+
}),
|
|
388
|
+
wherePredicate
|
|
389
|
+
};
|
|
390
|
+
}
|
|
228
391
|
|
|
229
392
|
// packages/dsl/src/to-ir.ts
|
|
230
393
|
import {
|
|
231
394
|
EMPTY_EXTENSION_REGISTRY,
|
|
232
|
-
VibeError as
|
|
395
|
+
VibeError as VibeError4,
|
|
233
396
|
assertValidSchemaIR,
|
|
234
397
|
exactIntegerLiteral,
|
|
398
|
+
providerToDialect,
|
|
235
399
|
scalarListDefault,
|
|
236
400
|
serializeJsonDefault,
|
|
237
401
|
mergeExtensionDeclarations
|
|
238
402
|
} from "@vibeorm/schema";
|
|
403
|
+
import { getSqlDialect } from "@vibeorm/sql";
|
|
404
|
+
|
|
405
|
+
// packages/dsl/src/field-type.ts
|
|
406
|
+
import { fieldTypeIdentity } from "@vibeorm/schema";
|
|
407
|
+
function columnFieldType(params) {
|
|
408
|
+
const { column } = params;
|
|
409
|
+
switch (column.__columnType) {
|
|
410
|
+
case "string":
|
|
411
|
+
return "String";
|
|
412
|
+
case "int":
|
|
413
|
+
return "Int";
|
|
414
|
+
case "bigint":
|
|
415
|
+
return "BigInt";
|
|
416
|
+
case "float":
|
|
417
|
+
return "Float";
|
|
418
|
+
case "decimal":
|
|
419
|
+
return "Decimal";
|
|
420
|
+
case "boolean":
|
|
421
|
+
return "Boolean";
|
|
422
|
+
case "datetime":
|
|
423
|
+
return "DateTime";
|
|
424
|
+
case "json":
|
|
425
|
+
return "Json";
|
|
426
|
+
case "bytes":
|
|
427
|
+
return "Bytes";
|
|
428
|
+
case "id":
|
|
429
|
+
return column.__config.type === "autoincrement" ? "Int" : "String";
|
|
430
|
+
case "enum":
|
|
431
|
+
return column.__enumName === undefined ? "String" : { enum: column.__enumName };
|
|
432
|
+
case "unsupported":
|
|
433
|
+
return "String";
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function fieldTypeKey(params) {
|
|
437
|
+
return fieldTypeIdentity({ type: columnFieldType({ column: params.column }) });
|
|
438
|
+
}
|
|
239
439
|
|
|
240
440
|
// packages/dsl/src/relation-plan.ts
|
|
241
|
-
import { VibeError } from "@vibeorm/schema";
|
|
441
|
+
import { VibeError as VibeError3, predicateKey } from "@vibeorm/schema";
|
|
442
|
+
|
|
443
|
+
// packages/dsl/src/model.ts
|
|
444
|
+
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
242
445
|
|
|
243
446
|
// packages/dsl/src/naming.ts
|
|
244
447
|
function capitalize(params) {
|
|
@@ -277,6 +480,24 @@ function isChildrenDef(value) {
|
|
|
277
480
|
return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
|
|
278
481
|
}
|
|
279
482
|
function createIndexBuilder() {
|
|
483
|
+
const partial = (index) => {
|
|
484
|
+
const where = (predicate) => {
|
|
485
|
+
if (index.__where !== undefined) {
|
|
486
|
+
throw new VibeError2({
|
|
487
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
488
|
+
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`,
|
|
489
|
+
meta: { rule: "predicate-duplicate-where", columns: [...index.__columns] }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return { ...index, __wherePredicate: predicate(createPredicateBuilder()) };
|
|
493
|
+
};
|
|
494
|
+
return Object.defineProperty({ ...index }, "where", {
|
|
495
|
+
value: where,
|
|
496
|
+
enumerable: false,
|
|
497
|
+
writable: false,
|
|
498
|
+
configurable: true
|
|
499
|
+
});
|
|
500
|
+
};
|
|
280
501
|
const createIndex = (columns, type, where, map) => ({
|
|
281
502
|
__type: "index",
|
|
282
503
|
__columns: [...columns],
|
|
@@ -288,38 +509,38 @@ function createIndexBuilder() {
|
|
|
288
509
|
on(...args) {
|
|
289
510
|
const [first, second] = args;
|
|
290
511
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
291
|
-
return createIndex(first, second.type, second.where, second.map);
|
|
512
|
+
return partial(createIndex(first, second.type, second.where, second.map));
|
|
292
513
|
}
|
|
293
|
-
return createIndex(args);
|
|
514
|
+
return partial(createIndex(args));
|
|
294
515
|
},
|
|
295
516
|
index(...columns) {
|
|
296
|
-
return createIndex(columns);
|
|
517
|
+
return partial(createIndex(columns));
|
|
297
518
|
},
|
|
298
519
|
unique(...args) {
|
|
299
520
|
const [first, second] = args;
|
|
300
521
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
301
|
-
return {
|
|
522
|
+
return partial({
|
|
302
523
|
__type: "unique",
|
|
303
524
|
__columns: [...first],
|
|
304
525
|
...second.map === undefined ? {} : { __map: second.map },
|
|
305
526
|
...second.where === undefined ? {} : { __where: second.where },
|
|
306
527
|
...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
|
|
307
528
|
...second.name === undefined ? {} : { __clientName: second.name }
|
|
308
|
-
};
|
|
529
|
+
});
|
|
309
530
|
}
|
|
310
|
-
return { __type: "unique", __columns: [...args] };
|
|
531
|
+
return partial({ __type: "unique", __columns: [...args] });
|
|
311
532
|
},
|
|
312
533
|
gin(...columns) {
|
|
313
|
-
return createIndex(columns, "Gin");
|
|
534
|
+
return partial(createIndex(columns, "Gin"));
|
|
314
535
|
},
|
|
315
536
|
gist(...columns) {
|
|
316
|
-
return createIndex(columns, "Gist");
|
|
537
|
+
return partial(createIndex(columns, "Gist"));
|
|
317
538
|
},
|
|
318
539
|
hash(...columns) {
|
|
319
|
-
return createIndex(columns, "Hash");
|
|
540
|
+
return partial(createIndex(columns, "Hash"));
|
|
320
541
|
},
|
|
321
542
|
brin(...columns) {
|
|
322
|
-
return createIndex(columns, "Brin");
|
|
543
|
+
return partial(createIndex(columns, "Brin"));
|
|
323
544
|
}
|
|
324
545
|
};
|
|
325
546
|
}
|
|
@@ -361,6 +582,7 @@ function create(config) {
|
|
|
361
582
|
__children: children2,
|
|
362
583
|
__map: config.map,
|
|
363
584
|
__primaryKey: config.primaryKey,
|
|
585
|
+
__exclusions: config.exclusions,
|
|
364
586
|
__checks: config.checks,
|
|
365
587
|
__extensions: config.extensions,
|
|
366
588
|
__policy: config.policy,
|
|
@@ -386,7 +608,7 @@ function detectOneToOnePairs(params) {
|
|
|
386
608
|
for (const [modelName, modelRelations] of Object.entries(relations)) {
|
|
387
609
|
for (const [relName, rel] of Object.entries(modelRelations)) {
|
|
388
610
|
if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
|
|
389
|
-
throw new
|
|
611
|
+
throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
|
|
390
612
|
}
|
|
391
613
|
if (rel.__relationType === "one") {
|
|
392
614
|
allOnes.push({ modelName, relName, rel });
|
|
@@ -408,12 +630,13 @@ function detectOneToOnePairs(params) {
|
|
|
408
630
|
const keyB = `${b.modelName}:${b.relName}`;
|
|
409
631
|
if (paired.has(keyB))
|
|
410
632
|
continue;
|
|
411
|
-
const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && a.rel.
|
|
633
|
+
const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && sameColumnTuple({ left: a.rel.__fromColumns, right: b.rel.__toColumns }) && a.rel.__toModel === b.rel.__fromModel && sameColumnTuple({ left: a.rel.__toColumns, right: b.rel.__fromColumns });
|
|
412
634
|
if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
|
|
413
635
|
continue;
|
|
414
636
|
const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
|
|
415
637
|
const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
|
|
416
638
|
const onUpdate = fkOwner.rel.__onUpdate ?? backRelation.rel.__onUpdate;
|
|
639
|
+
const constraintName = fkOwner.rel.__constraintName ?? backRelation.rel.__constraintName;
|
|
417
640
|
const fkKey = `${fkOwner.modelName}:${fkOwner.relName}`;
|
|
418
641
|
const backKey = `${backRelation.modelName}:${backRelation.relName}`;
|
|
419
642
|
mergeMap.set(fkKey, {
|
|
@@ -421,14 +644,16 @@ function detectOneToOnePairs(params) {
|
|
|
421
644
|
pairedRelName: backRelation.relName,
|
|
422
645
|
pairedModelName: backRelation.modelName,
|
|
423
646
|
onDelete,
|
|
424
|
-
onUpdate
|
|
647
|
+
onUpdate,
|
|
648
|
+
...constraintName === undefined ? {} : { constraintName }
|
|
425
649
|
});
|
|
426
650
|
mergeMap.set(backKey, {
|
|
427
651
|
role: "backRelation",
|
|
428
652
|
pairedRelName: fkOwner.relName,
|
|
429
653
|
pairedModelName: fkOwner.modelName,
|
|
430
654
|
onDelete,
|
|
431
|
-
onUpdate
|
|
655
|
+
onUpdate,
|
|
656
|
+
...constraintName === undefined ? {} : { constraintName }
|
|
432
657
|
});
|
|
433
658
|
paired.add(fkKey);
|
|
434
659
|
paired.add(backKey);
|
|
@@ -437,10 +662,14 @@ function detectOneToOnePairs(params) {
|
|
|
437
662
|
}
|
|
438
663
|
return mergeMap;
|
|
439
664
|
}
|
|
665
|
+
function sameColumnTuple(params) {
|
|
666
|
+
const { left, right } = params;
|
|
667
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
668
|
+
}
|
|
440
669
|
function resolveFkOwner(params) {
|
|
441
670
|
const { a, b, models } = params;
|
|
442
671
|
const ownershipError = () => {
|
|
443
|
-
throw new
|
|
672
|
+
throw new VibeError3({ 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" } });
|
|
444
673
|
};
|
|
445
674
|
const aOwns = a.rel.__ownsForeignKey;
|
|
446
675
|
const bOwns = b.rel.__ownsForeignKey;
|
|
@@ -449,8 +678,8 @@ function resolveFkOwner(params) {
|
|
|
449
678
|
return ownershipError();
|
|
450
679
|
return aOwns === true || bOwns === false ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
|
|
451
680
|
}
|
|
452
|
-
const aFromIsId =
|
|
453
|
-
const bFromIsId =
|
|
681
|
+
const aFromIsId = fromIsPrimaryKey({ model: models.get(a.modelName), columns: a.rel.__fromColumns });
|
|
682
|
+
const bFromIsId = fromIsPrimaryKey({ model: models.get(b.modelName), columns: b.rel.__fromColumns });
|
|
454
683
|
if (aFromIsId && !bFromIsId)
|
|
455
684
|
return { fkOwner: b, backRelation: a };
|
|
456
685
|
if (!aFromIsId && bFromIsId)
|
|
@@ -463,12 +692,25 @@ function resolveFkOwner(params) {
|
|
|
463
692
|
if (bActions && !aActions) {
|
|
464
693
|
return { fkOwner: b, backRelation: a };
|
|
465
694
|
}
|
|
466
|
-
const aLooksFk = looksLikeForeignKey({ column
|
|
467
|
-
const bLooksFk = looksLikeForeignKey({ column
|
|
695
|
+
const aLooksFk = a.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
|
|
696
|
+
const bLooksFk = b.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
|
|
468
697
|
if (aLooksFk === bLooksFk)
|
|
469
698
|
return ownershipError();
|
|
470
699
|
return aLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
|
|
471
700
|
}
|
|
701
|
+
function fromIsPrimaryKey(params) {
|
|
702
|
+
const { model, columns } = params;
|
|
703
|
+
if (model === undefined)
|
|
704
|
+
return false;
|
|
705
|
+
if (columns.length > 1) {
|
|
706
|
+
const primaryKey = model.__primaryKey;
|
|
707
|
+
return primaryKey !== undefined && primaryKey.length === columns.length && primaryKey.every((name) => columns.includes(name));
|
|
708
|
+
}
|
|
709
|
+
const column = columns[0];
|
|
710
|
+
if (column === undefined)
|
|
711
|
+
return false;
|
|
712
|
+
return model.__columns.get(column)?.__columnType === "id" || model.__primaryKey?.includes(column) === true;
|
|
713
|
+
}
|
|
472
714
|
function looksLikeForeignKey(params) {
|
|
473
715
|
const { column } = params;
|
|
474
716
|
return column.toLowerCase().includes("id") && column !== "id";
|
|
@@ -498,14 +740,16 @@ function planExplicitRelations(params) {
|
|
|
498
740
|
planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
|
|
499
741
|
continue;
|
|
500
742
|
}
|
|
501
|
-
const
|
|
743
|
+
const anyMemberOptional = rel.__fromColumns.some((name2) => model.__columns.get(name2)?.__config.optional === true);
|
|
744
|
+
const constraintName = mergeInfo?.constraintName ?? rel.__constraintName;
|
|
502
745
|
planned.push({
|
|
503
746
|
...base,
|
|
504
747
|
kind: "fkOwner",
|
|
505
748
|
isList: false,
|
|
506
|
-
isOptional:
|
|
507
|
-
fields: [rel.
|
|
508
|
-
references: [rel.
|
|
749
|
+
isOptional: anyMemberOptional || rel.__optional,
|
|
750
|
+
fields: [...rel.__fromColumns],
|
|
751
|
+
references: [...rel.__toColumns],
|
|
752
|
+
...constraintName === undefined ? {} : { constraintName },
|
|
509
753
|
onDelete: mergeInfo?.onDelete ?? rel.__onDelete,
|
|
510
754
|
onUpdate: mergeInfo?.onUpdate ?? rel.__onUpdate
|
|
511
755
|
});
|
|
@@ -602,7 +846,8 @@ function planIndexes(params) {
|
|
|
602
846
|
}
|
|
603
847
|
}
|
|
604
848
|
for (const index of evaluateIndexes({ model })) {
|
|
605
|
-
const
|
|
849
|
+
const predicateSuffix = index.__where !== undefined ? `|where:${index.__where}` : index.__wherePredicate === undefined ? "" : `|where:${predicateKey({ predicate: index.__wherePredicate })}`;
|
|
850
|
+
const key = index.__columns.join(",") + predicateSuffix;
|
|
606
851
|
const existing = byKey.get(key);
|
|
607
852
|
const isUnique = index.__type === "unique";
|
|
608
853
|
if (existing === undefined || isUnique) {
|
|
@@ -612,6 +857,7 @@ function planIndexes(params) {
|
|
|
612
857
|
indexType: index.__indexType,
|
|
613
858
|
...index.__map === undefined ? {} : { map: index.__map },
|
|
614
859
|
...index.__where === undefined ? {} : { where: index.__where },
|
|
860
|
+
...index.__wherePredicate === undefined ? {} : { wherePredicate: index.__wherePredicate },
|
|
615
861
|
...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
|
|
616
862
|
...index.__clientName === undefined ? {} : { clientName: index.__clientName }
|
|
617
863
|
});
|
|
@@ -623,6 +869,77 @@ function planIndexes(params) {
|
|
|
623
869
|
}
|
|
624
870
|
return Array.from(byKey.values());
|
|
625
871
|
}
|
|
872
|
+
function referenceableKeys(params) {
|
|
873
|
+
const { model } = params;
|
|
874
|
+
const keys = [];
|
|
875
|
+
for (const [name, column] of model.__columns) {
|
|
876
|
+
if (column.__columnType === "id" || column.__config.unique === true)
|
|
877
|
+
keys.push([name]);
|
|
878
|
+
}
|
|
879
|
+
if (model.__primaryKey !== undefined && model.__primaryKey.length > 0)
|
|
880
|
+
keys.push([...model.__primaryKey]);
|
|
881
|
+
for (const index of evaluateIndexes({ model })) {
|
|
882
|
+
if (index.__type === "unique" && index.__where === undefined && index.__wherePredicate === undefined) {
|
|
883
|
+
keys.push([...index.__columns]);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return keys;
|
|
887
|
+
}
|
|
888
|
+
function assertForeignKeyIntegrity(params) {
|
|
889
|
+
const { relation, models } = params;
|
|
890
|
+
const fields = relation.fields ?? [];
|
|
891
|
+
const references = relation.references ?? [];
|
|
892
|
+
const at = `${relation.model}.${relation.name}`;
|
|
893
|
+
const refuse = (message, rule, meta = {}) => {
|
|
894
|
+
throw new VibeError3({
|
|
895
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
896
|
+
message: `Relation ${at}: ${message}`,
|
|
897
|
+
meta: { rule, model: relation.model, relation: relation.name, ...meta }
|
|
898
|
+
});
|
|
899
|
+
};
|
|
900
|
+
if (fields.length !== references.length) {
|
|
901
|
+
refuse(`pairs ${fields.length} foreign-key column(s) with ${references.length} referenced column(s)`, "relation-tuple-arity", { fields, references });
|
|
902
|
+
}
|
|
903
|
+
if (new Set(fields).size !== fields.length) {
|
|
904
|
+
refuse(`repeats a foreign-key column (${fields.join(", ")})`, "relation-duplicate-column", { fields });
|
|
905
|
+
}
|
|
906
|
+
if (new Set(references).size !== references.length) {
|
|
907
|
+
refuse(`repeats a referenced column (${references.join(", ")})`, "relation-duplicate-column", { references });
|
|
908
|
+
}
|
|
909
|
+
const owner = models.get(relation.model);
|
|
910
|
+
const target = models.get(relation.targetModel);
|
|
911
|
+
if (owner === undefined || target === undefined)
|
|
912
|
+
return;
|
|
913
|
+
fields.forEach((name, index) => {
|
|
914
|
+
const referenced = references[index];
|
|
915
|
+
const column = owner.__columns.get(name);
|
|
916
|
+
const refColumn = target.__columns.get(referenced);
|
|
917
|
+
if (column === undefined) {
|
|
918
|
+
refuse(`uses foreign-key column "${name}", which ${relation.model} does not declare`, "unknown-relation-field", { field: name });
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
if (refColumn === undefined) {
|
|
922
|
+
refuse(`references "${relation.targetModel}.${referenced}", which does not exist`, "unknown-relation-reference", { reference: referenced });
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
925
|
+
if (column.__config.array === true || refColumn.__config.array === true) {
|
|
926
|
+
refuse(`pairs a scalar list (${name} \u2194 ${referenced}) \u2014 a foreign key compares single values`, "relation-list-column", { field: name, reference: referenced });
|
|
927
|
+
}
|
|
928
|
+
if (column.__columnType === "json" || refColumn.__columnType === "json" || column.__columnType === "unsupported" || refColumn.__columnType === "unsupported") {
|
|
929
|
+
refuse(`pairs ${name} \u2194 ${referenced}, and Json/unsupported columns cannot be a foreign key`, "relation-unsupported-column", { field: name, reference: referenced });
|
|
930
|
+
}
|
|
931
|
+
const ownType = fieldTypeKey({ column });
|
|
932
|
+
const refType = fieldTypeKey({ column: refColumn });
|
|
933
|
+
if (ownType !== refType) {
|
|
934
|
+
refuse(`pairs ${relation.model}.${name} (${ownType}) with ${relation.targetModel}.${referenced} (${refType}) \u2014 both ends of a foreign key need the same scalar type`, "relation-type-mismatch", { field: name, reference: referenced, fieldType: ownType, referenceType: refType });
|
|
935
|
+
}
|
|
936
|
+
});
|
|
937
|
+
const keys = referenceableKeys({ model: target });
|
|
938
|
+
const matched = keys.some((key) => key.length === references.length && key.every((name) => references.includes(name)));
|
|
939
|
+
if (!matched) {
|
|
940
|
+
refuse(`references ${relation.targetModel}.(${references.join(", ")}), which is not a unique key there \u2014 declare those columns as the primary key, a unique column, or a total @@unique index`, "relation-reference-not-unique", { references, candidates: keys.map((key) => [...key]) });
|
|
941
|
+
}
|
|
942
|
+
}
|
|
626
943
|
function planSchema(params) {
|
|
627
944
|
const { schema } = params;
|
|
628
945
|
const mergeMap = detectOneToOnePairs({
|
|
@@ -645,7 +962,12 @@ function planSchema(params) {
|
|
|
645
962
|
existing: explicit
|
|
646
963
|
});
|
|
647
964
|
const children2 = planChildRelations({ modelName, model, models: schema.__models });
|
|
648
|
-
|
|
965
|
+
const planned = [...explicit, ...reverses, ...children2];
|
|
966
|
+
for (const relation of planned) {
|
|
967
|
+
if (relation.kind === "fkOwner")
|
|
968
|
+
assertForeignKeyIntegrity({ relation, models: schema.__models });
|
|
969
|
+
}
|
|
970
|
+
relations.set(modelName, planned);
|
|
649
971
|
indexes.set(modelName, planIndexes({ model }));
|
|
650
972
|
}
|
|
651
973
|
return { relations, indexes };
|
|
@@ -671,35 +993,7 @@ function findCounterparts(params) {
|
|
|
671
993
|
}
|
|
672
994
|
|
|
673
995
|
// packages/dsl/src/to-ir.ts
|
|
674
|
-
|
|
675
|
-
const { column } = params;
|
|
676
|
-
switch (column.__columnType) {
|
|
677
|
-
case "string":
|
|
678
|
-
return "String";
|
|
679
|
-
case "int":
|
|
680
|
-
return "Int";
|
|
681
|
-
case "bigint":
|
|
682
|
-
return "BigInt";
|
|
683
|
-
case "float":
|
|
684
|
-
return "Float";
|
|
685
|
-
case "decimal":
|
|
686
|
-
return "Decimal";
|
|
687
|
-
case "boolean":
|
|
688
|
-
return "Boolean";
|
|
689
|
-
case "datetime":
|
|
690
|
-
return "DateTime";
|
|
691
|
-
case "json":
|
|
692
|
-
return "Json";
|
|
693
|
-
case "bytes":
|
|
694
|
-
return "Bytes";
|
|
695
|
-
case "id":
|
|
696
|
-
return column.__config.type === "autoincrement" ? "Int" : "String";
|
|
697
|
-
case "enum":
|
|
698
|
-
return column.__enumName === undefined ? "String" : { enum: column.__enumName };
|
|
699
|
-
case "unsupported":
|
|
700
|
-
return "String";
|
|
701
|
-
}
|
|
702
|
-
}
|
|
996
|
+
var fieldType = columnFieldType;
|
|
703
997
|
function idDefault(params) {
|
|
704
998
|
switch (params.column.__config.type) {
|
|
705
999
|
case "ulid":
|
|
@@ -720,16 +1014,16 @@ function fieldDefault(params) {
|
|
|
720
1014
|
const { column } = params;
|
|
721
1015
|
if (column.__columnType === "id") {
|
|
722
1016
|
if (column.__config.default !== undefined || column.__config.dbgenerated !== undefined || column.__config.autoincrement !== undefined)
|
|
723
|
-
throw new
|
|
1017
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
|
|
724
1018
|
return idDefault({ column });
|
|
725
1019
|
}
|
|
726
1020
|
const { dbgenerated, default: value, autoincrement } = column.__config;
|
|
727
1021
|
if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
|
|
728
|
-
throw new
|
|
1022
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
|
|
729
1023
|
}
|
|
730
1024
|
if (autoincrement === true) {
|
|
731
1025
|
if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
|
|
732
|
-
throw new
|
|
1026
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
|
|
733
1027
|
}
|
|
734
1028
|
return { kind: "autoincrement" };
|
|
735
1029
|
}
|
|
@@ -739,7 +1033,7 @@ function fieldDefault(params) {
|
|
|
739
1033
|
return;
|
|
740
1034
|
if (column.__config.array === true) {
|
|
741
1035
|
if (!Array.isArray(value))
|
|
742
|
-
throw new
|
|
1036
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
|
|
743
1037
|
return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
|
|
744
1038
|
}
|
|
745
1039
|
if (column.__columnType === "json" && typeof value !== "string") {
|
|
@@ -751,14 +1045,14 @@ function fieldDefault(params) {
|
|
|
751
1045
|
return { kind: "enumValue", value };
|
|
752
1046
|
}
|
|
753
1047
|
if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
|
|
754
|
-
throw new
|
|
1048
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
|
|
755
1049
|
}
|
|
756
1050
|
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
757
1051
|
return { kind: "literal", value };
|
|
758
1052
|
}
|
|
759
1053
|
if (typeof value === "bigint")
|
|
760
1054
|
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
761
|
-
throw new
|
|
1055
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
|
|
762
1056
|
}
|
|
763
1057
|
function nativeType(params) {
|
|
764
1058
|
const { column } = params;
|
|
@@ -767,7 +1061,7 @@ function nativeType(params) {
|
|
|
767
1061
|
if (maxLength !== undefined || text !== undefined) {
|
|
768
1062
|
const invalid = column.__enumStorage !== "string" || maxLength !== undefined && (typeof maxLength !== "number" || !Number.isSafeInteger(maxLength) || maxLength <= 0) || text !== undefined && typeof text !== "boolean" || text === true && maxLength !== undefined;
|
|
769
1063
|
if (invalid) {
|
|
770
|
-
throw new
|
|
1064
|
+
throw new VibeError4({
|
|
771
1065
|
code: "VIBE_SCHEMA_INVALID",
|
|
772
1066
|
message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
|
|
773
1067
|
meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
|
|
@@ -832,6 +1126,7 @@ function buildRelation(params) {
|
|
|
832
1126
|
isList: relation.isList,
|
|
833
1127
|
isOptional: relation.isOptional,
|
|
834
1128
|
...relation.relationName === undefined ? {} : { relationName: relation.relationName },
|
|
1129
|
+
...relation.constraintName === undefined ? {} : { constraintName: relation.constraintName },
|
|
835
1130
|
...relation.fields === undefined ? {} : { fields: [...relation.fields] },
|
|
836
1131
|
...relation.references === undefined ? {} : { references: [...relation.references] },
|
|
837
1132
|
...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
|
|
@@ -847,19 +1142,28 @@ var INDEX_KINDS = {
|
|
|
847
1142
|
Brin: "brin"
|
|
848
1143
|
};
|
|
849
1144
|
function buildIndex(params) {
|
|
850
|
-
const { index } = params;
|
|
1145
|
+
const { index, model, dialect, enums } = params;
|
|
851
1146
|
const kind = index.isUnique ? "unique" : index.indexType === undefined ? "btree" : INDEX_KINDS[index.indexType];
|
|
1147
|
+
const predicate = compileIndexPredicate({
|
|
1148
|
+
where: index.where,
|
|
1149
|
+
wherePredicate: index.wherePredicate,
|
|
1150
|
+
columns: index.columns,
|
|
1151
|
+
model,
|
|
1152
|
+
dialect,
|
|
1153
|
+
enums
|
|
1154
|
+
});
|
|
852
1155
|
return {
|
|
853
1156
|
kind,
|
|
854
1157
|
fields: index.columns.map((name) => ({ name })),
|
|
855
1158
|
...index.map === undefined ? {} : { name: index.map },
|
|
856
|
-
...
|
|
1159
|
+
...predicate === undefined ? {} : { where: predicate.where },
|
|
1160
|
+
...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
|
|
857
1161
|
...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
|
|
858
1162
|
...index.clientName === undefined ? {} : { clientName: index.clientName }
|
|
859
1163
|
};
|
|
860
1164
|
}
|
|
861
1165
|
function buildModel(params) {
|
|
862
|
-
const { model, plan, extensions, declarations, issues } = params;
|
|
1166
|
+
const { model, plan, extensions, declarations, issues, dialect, enums } = params;
|
|
863
1167
|
const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
|
|
864
1168
|
const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
|
|
865
1169
|
const fields = [];
|
|
@@ -935,13 +1239,17 @@ function buildModel(params) {
|
|
|
935
1239
|
declaration: parsed.declaration
|
|
936
1240
|
});
|
|
937
1241
|
}
|
|
1242
|
+
const predicateModel = { name: model.__name, fields };
|
|
1243
|
+
const exclusions = compileExclusions({ exclusions: model.__exclusions, model: predicateModel, dialect });
|
|
1244
|
+
const checks = compileChecks({ checks: model.__checks, model: predicateModel, dialect, enums });
|
|
938
1245
|
return {
|
|
939
1246
|
name: model.__name,
|
|
940
1247
|
...model.__map === undefined ? {} : { dbName: model.__map },
|
|
941
1248
|
fields,
|
|
942
1249
|
relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
|
|
943
|
-
indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index })),
|
|
944
|
-
...
|
|
1250
|
+
indexes: (plan.indexes.get(model.__name) ?? []).map((index) => buildIndex({ index, model: predicateModel, dialect, enums })),
|
|
1251
|
+
...exclusions.length === 0 ? {} : { exclusions },
|
|
1252
|
+
...checks.length === 0 ? {} : { checks },
|
|
945
1253
|
...policy === undefined ? {} : { policy },
|
|
946
1254
|
...primaryKey === undefined ? {} : { primaryKey },
|
|
947
1255
|
isView: false,
|
|
@@ -972,8 +1280,14 @@ function toIR(params) {
|
|
|
972
1280
|
const plan = planSchema({ schema });
|
|
973
1281
|
const declarations = new ExtensionDeclarationCollector;
|
|
974
1282
|
const issues = [];
|
|
1283
|
+
const enums = Array.from(schema.__enums, ([name, values]) => ({
|
|
1284
|
+
name,
|
|
1285
|
+
values: [...values],
|
|
1286
|
+
...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
|
|
1287
|
+
}));
|
|
1288
|
+
const dialect = getSqlDialect({ dialect: providerToDialect({ provider }) });
|
|
975
1289
|
const models = Array.from(schema.__models.values(), (model) => {
|
|
976
|
-
const built = buildModel({ model, plan, extensions, declarations, issues });
|
|
1290
|
+
const built = buildModel({ model, plan, extensions, declarations, issues, dialect, enums });
|
|
977
1291
|
const rls = schema.__rls?.policies[model.__name];
|
|
978
1292
|
return rls === undefined ? built : { ...built, rls };
|
|
979
1293
|
});
|
|
@@ -984,13 +1298,8 @@ function toIR(params) {
|
|
|
984
1298
|
}
|
|
985
1299
|
}
|
|
986
1300
|
}
|
|
987
|
-
const enums = Array.from(schema.__enums, ([name, values]) => ({
|
|
988
|
-
name,
|
|
989
|
-
values: [...values],
|
|
990
|
-
...schema.__enumStorage.get(name) === "string" ? { storage: "string" } : {}
|
|
991
|
-
}));
|
|
992
1301
|
if (issues.length > 0) {
|
|
993
|
-
throw new
|
|
1302
|
+
throw new VibeError4({
|
|
994
1303
|
code: "VIBE_SCHEMA_INVALID",
|
|
995
1304
|
message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
|
|
996
1305
|
meta: { issues }
|
|
@@ -1008,6 +1317,12 @@ function toIR(params) {
|
|
|
1008
1317
|
}
|
|
1009
1318
|
|
|
1010
1319
|
// packages/dsl/src/generator/prisma.ts
|
|
1320
|
+
function predicateModel(params) {
|
|
1321
|
+
return {
|
|
1322
|
+
name: params.model.__name,
|
|
1323
|
+
fields: Array.from(params.model.__columns, ([name, column]) => buildField({ name, column }))
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1011
1326
|
function prismaType(params) {
|
|
1012
1327
|
const { column } = params;
|
|
1013
1328
|
const list = column.__config.array === true ? "[]" : "";
|
|
@@ -1148,12 +1463,23 @@ function formatRelation(params) {
|
|
|
1148
1463
|
args.push(`onDelete: ${relation.onDelete}`);
|
|
1149
1464
|
if (relation.onUpdate !== undefined)
|
|
1150
1465
|
args.push(`onUpdate: ${relation.onUpdate}`);
|
|
1466
|
+
if (relation.constraintName !== undefined) {
|
|
1467
|
+
args.push(`map: ${encodePrismaString({ value: relation.constraintName })}`);
|
|
1468
|
+
}
|
|
1151
1469
|
return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
|
|
1152
1470
|
}
|
|
1153
1471
|
function formatIndex(params) {
|
|
1154
1472
|
const { index } = params;
|
|
1155
1473
|
const columns = index.columns.join(", ");
|
|
1156
|
-
const
|
|
1474
|
+
const predicate = compileIndexPredicate({
|
|
1475
|
+
where: index.where,
|
|
1476
|
+
wherePredicate: index.wherePredicate,
|
|
1477
|
+
columns: index.columns,
|
|
1478
|
+
model: predicateModel({ model: params.model }),
|
|
1479
|
+
dialect: params.dialect,
|
|
1480
|
+
enums: params.enums
|
|
1481
|
+
});
|
|
1482
|
+
const whereArg = predicate === undefined ? "" : `, where: ${encodePrismaString({ value: predicate.where })}`;
|
|
1157
1483
|
const mapArg = index.map === undefined ? "" : `, map: ${encodePrismaString({ value: index.map })}`;
|
|
1158
1484
|
const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
|
|
1159
1485
|
const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
|
|
@@ -1192,9 +1518,14 @@ function formatModel(params) {
|
|
|
1192
1518
|
lines.push(" //> Indexes");
|
|
1193
1519
|
lines.push("");
|
|
1194
1520
|
for (const index of plan.indexes.get(model.__name) ?? []) {
|
|
1195
|
-
lines.push(formatIndex({ index }));
|
|
1196
|
-
}
|
|
1197
|
-
for (const check of
|
|
1521
|
+
lines.push(formatIndex({ index, model: params.model, dialect: params.dialect, enums: params.enums }));
|
|
1522
|
+
}
|
|
1523
|
+
for (const check of compileChecks({
|
|
1524
|
+
checks: model.__checks,
|
|
1525
|
+
model: predicateModel({ model: params.model }),
|
|
1526
|
+
dialect: params.dialect,
|
|
1527
|
+
enums: params.enums
|
|
1528
|
+
})) {
|
|
1198
1529
|
lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
|
|
1199
1530
|
}
|
|
1200
1531
|
for (const use of model.__extensions ?? []) {
|
|
@@ -1226,17 +1557,22 @@ function formatEnum(params) {
|
|
|
1226
1557
|
return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
|
|
1227
1558
|
`);
|
|
1228
1559
|
}
|
|
1229
|
-
function generatePrisma(schema,
|
|
1560
|
+
function generatePrisma(schema, config = {}) {
|
|
1561
|
+
for (const model of schema.__models.values()) {
|
|
1562
|
+
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" }) }) });
|
|
1563
|
+
if (exclusions.length > 0)
|
|
1564
|
+
throw new VibeError5({ code: "VIBE_CONFIG", message: "Prisma output cannot preserve exclusion constraints; keep the DSL source", meta: { feature: "exclusionConstraints", target: "prisma" } });
|
|
1565
|
+
}
|
|
1230
1566
|
const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
|
|
1231
1567
|
if (stringEnums.length > 0) {
|
|
1232
|
-
throw new
|
|
1568
|
+
throw new VibeError5({
|
|
1233
1569
|
code: "VIBE_CONFIG",
|
|
1234
1570
|
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.`,
|
|
1235
1571
|
meta: { enums: stringEnums, target: "prisma" }
|
|
1236
1572
|
});
|
|
1237
1573
|
}
|
|
1238
1574
|
if (schema.__rls !== undefined) {
|
|
1239
|
-
throw new
|
|
1575
|
+
throw new VibeError5({
|
|
1240
1576
|
code: "VIBE_CONFIG",
|
|
1241
1577
|
message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
|
|
1242
1578
|
meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
|
|
@@ -1244,6 +1580,8 @@ function generatePrisma(schema, _config = {}) {
|
|
|
1244
1580
|
}
|
|
1245
1581
|
const plan = planSchema({ schema });
|
|
1246
1582
|
const sections = [];
|
|
1583
|
+
const dialect = getSqlDialect2({ dialect: providerToDialect2({ provider: config.provider ?? "postgresql" }) });
|
|
1584
|
+
const enums = Array.from(schema.__enums, ([name, values]) => ({ name, values: [...values] }));
|
|
1247
1585
|
if (schema.__enums.size > 0) {
|
|
1248
1586
|
sections.push("// ============================================");
|
|
1249
1587
|
sections.push("// Enums");
|
|
@@ -1259,7 +1597,7 @@ function generatePrisma(schema, _config = {}) {
|
|
|
1259
1597
|
sections.push("// ============================================");
|
|
1260
1598
|
sections.push("");
|
|
1261
1599
|
for (const model of schema.__models.values()) {
|
|
1262
|
-
sections.push(formatModel({ model, plan }));
|
|
1600
|
+
sections.push(formatModel({ model, plan, dialect, enums }));
|
|
1263
1601
|
sections.push("");
|
|
1264
1602
|
}
|
|
1265
1603
|
return sections.join(`
|
|
@@ -1267,12 +1605,28 @@ function generatePrisma(schema, _config = {}) {
|
|
|
1267
1605
|
}
|
|
1268
1606
|
|
|
1269
1607
|
// packages/dsl/src/relations.ts
|
|
1608
|
+
import { VibeError as VibeError6 } from "@vibeorm/schema";
|
|
1609
|
+
var REF_KIND_BY_COLUMN_TYPE = {
|
|
1610
|
+
string: "String",
|
|
1611
|
+
int: "Int",
|
|
1612
|
+
bigint: "BigInt",
|
|
1613
|
+
float: "Float",
|
|
1614
|
+
decimal: "Decimal",
|
|
1615
|
+
boolean: "Boolean",
|
|
1616
|
+
datetime: "DateTime",
|
|
1617
|
+
json: "Json",
|
|
1618
|
+
bytes: "Bytes",
|
|
1619
|
+
id: "Id",
|
|
1620
|
+
enum: "Enum",
|
|
1621
|
+
unsupported: "Unsupported"
|
|
1622
|
+
};
|
|
1270
1623
|
function createColumnRef(params) {
|
|
1271
|
-
const { model, column } = params;
|
|
1624
|
+
const { model, column, kind } = params;
|
|
1272
1625
|
return {
|
|
1273
1626
|
__type: "columnRef",
|
|
1274
1627
|
__model: model,
|
|
1275
1628
|
__column: column,
|
|
1629
|
+
__kind: kind,
|
|
1276
1630
|
through(junction) {
|
|
1277
1631
|
return {
|
|
1278
1632
|
__type: "throughRef",
|
|
@@ -1285,38 +1639,77 @@ function createColumnRef(params) {
|
|
|
1285
1639
|
};
|
|
1286
1640
|
}
|
|
1287
1641
|
function extractRefInfo(params) {
|
|
1288
|
-
const { ref } = params;
|
|
1289
|
-
|
|
1642
|
+
const { ref, side, target } = params;
|
|
1643
|
+
if (!Array.isArray(ref)) {
|
|
1644
|
+
const single = ref;
|
|
1645
|
+
return { model: single.__model, columns: [single.__column] };
|
|
1646
|
+
}
|
|
1647
|
+
const members = ref;
|
|
1648
|
+
if (members.length === 0) {
|
|
1649
|
+
throw new VibeError6({
|
|
1650
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1651
|
+
message: `Relation to ${target}: "${side}" is an empty tuple \u2014 name at least one column`,
|
|
1652
|
+
meta: { rule: "relation-empty-tuple", side, targetModel: target }
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1655
|
+
const models = [...new Set(members.map((member) => member.__model))].sort();
|
|
1656
|
+
if (models.length > 1) {
|
|
1657
|
+
throw new VibeError6({
|
|
1658
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1659
|
+
message: `Relation to ${target}: "${side}" mixes models ${models.join(", ")} \u2014 every member of a composite key must sit on one model`,
|
|
1660
|
+
meta: { rule: "relation-mixed-models", side, targetModel: target, models }
|
|
1661
|
+
});
|
|
1662
|
+
}
|
|
1663
|
+
if (members.some((member) => member.__type !== "columnRef")) {
|
|
1664
|
+
throw new VibeError6({
|
|
1665
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1666
|
+
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`,
|
|
1667
|
+
meta: { rule: "relation-through-in-tuple", side, targetModel: target }
|
|
1668
|
+
});
|
|
1669
|
+
}
|
|
1670
|
+
return { model: members[0].__model, columns: members.map((member) => member.__column) };
|
|
1290
1671
|
}
|
|
1291
1672
|
function createRelationFactory(params) {
|
|
1292
1673
|
const { relationType, targetModelName } = params;
|
|
1293
1674
|
return (config) => {
|
|
1294
|
-
const from = extractRefInfo({ ref: config.from });
|
|
1295
|
-
const to = extractRefInfo({ ref: config.to });
|
|
1675
|
+
const from = extractRefInfo({ ref: config.from, side: "from", target: targetModelName });
|
|
1676
|
+
const to = extractRefInfo({ ref: config.to, side: "to", target: targetModelName });
|
|
1677
|
+
if (from.columns.length !== to.columns.length) {
|
|
1678
|
+
throw new VibeError6({
|
|
1679
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1680
|
+
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`,
|
|
1681
|
+
meta: { rule: "relation-tuple-arity", targetModel: targetModelName, from: from.columns, to: to.columns }
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1296
1684
|
let throughJunction;
|
|
1297
1685
|
let throughFromCol;
|
|
1298
1686
|
let throughToCol;
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1687
|
+
const fromRef = Array.isArray(config.from) ? undefined : config.from;
|
|
1688
|
+
const toRef2 = Array.isArray(config.to) ? undefined : config.to;
|
|
1689
|
+
if (fromRef?.__type === "throughRef") {
|
|
1690
|
+
throughJunction = fromRef.__junction;
|
|
1691
|
+
throughFromCol = fromRef.__junctionColumn;
|
|
1302
1692
|
}
|
|
1303
|
-
if (
|
|
1304
|
-
throughJunction = throughJunction ??
|
|
1305
|
-
throughToCol =
|
|
1693
|
+
if (toRef2?.__type === "throughRef") {
|
|
1694
|
+
throughJunction = throughJunction ?? toRef2.__junction;
|
|
1695
|
+
throughToCol = toRef2.__junctionColumn;
|
|
1306
1696
|
}
|
|
1307
1697
|
return {
|
|
1308
1698
|
__type: "storedRelation",
|
|
1309
1699
|
__relationType: relationType,
|
|
1310
1700
|
__targetModel: targetModelName,
|
|
1311
1701
|
__fromModel: from.model,
|
|
1312
|
-
__fromColumn: from.
|
|
1702
|
+
__fromColumn: from.columns[0],
|
|
1703
|
+
__fromColumns: from.columns,
|
|
1313
1704
|
__toModel: to.model,
|
|
1314
|
-
__toColumn: to.
|
|
1705
|
+
__toColumn: to.columns[0],
|
|
1706
|
+
__toColumns: to.columns,
|
|
1315
1707
|
__throughJunction: throughJunction,
|
|
1316
1708
|
__throughFromCol: throughFromCol,
|
|
1317
1709
|
__throughToCol: throughToCol,
|
|
1318
1710
|
__optional: config.optional ?? relationType === "many",
|
|
1319
1711
|
__alias: config.alias,
|
|
1712
|
+
...config.constraintName === undefined ? {} : { __constraintName: config.constraintName },
|
|
1320
1713
|
..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
|
|
1321
1714
|
__onDelete: config.onDelete,
|
|
1322
1715
|
__onUpdate: config.onUpdate
|
|
@@ -1338,8 +1731,12 @@ function createRelationsBuilder(params) {
|
|
|
1338
1731
|
if (model === undefined)
|
|
1339
1732
|
continue;
|
|
1340
1733
|
const columnRefs = {};
|
|
1341
|
-
for (const columnName of model.__columns
|
|
1342
|
-
columnRefs[columnName] = createColumnRef({
|
|
1734
|
+
for (const [columnName, column] of model.__columns) {
|
|
1735
|
+
columnRefs[columnName] = createColumnRef({
|
|
1736
|
+
model: modelName,
|
|
1737
|
+
column: columnName,
|
|
1738
|
+
kind: REF_KIND_BY_COLUMN_TYPE[column.__columnType]
|
|
1739
|
+
});
|
|
1343
1740
|
}
|
|
1344
1741
|
builder[modelName] = columnRefs;
|
|
1345
1742
|
}
|
|
@@ -1376,7 +1773,7 @@ function mergeRelations(...parts) {
|
|
|
1376
1773
|
}
|
|
1377
1774
|
|
|
1378
1775
|
// packages/dsl/src/schema.ts
|
|
1379
|
-
import { VibeError as
|
|
1776
|
+
import { VibeError as VibeError7, validateRlsContextIR } from "@vibeorm/schema";
|
|
1380
1777
|
function sameValues(left, right) {
|
|
1381
1778
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1382
1779
|
}
|
|
@@ -1395,7 +1792,7 @@ function registerEnum(params) {
|
|
|
1395
1792
|
...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
|
|
1396
1793
|
...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
|
|
1397
1794
|
].join("; ");
|
|
1398
|
-
throw new
|
|
1795
|
+
throw new VibeError7({
|
|
1399
1796
|
code: "VIBE_SCHEMA_INVALID",
|
|
1400
1797
|
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`,
|
|
1401
1798
|
meta: {
|
|
@@ -1421,7 +1818,7 @@ function addModelTree(params) {
|
|
|
1421
1818
|
const { model, models, origin } = params;
|
|
1422
1819
|
const existing = models.get(model.__name);
|
|
1423
1820
|
if (existing !== undefined && existing !== model) {
|
|
1424
|
-
throw new
|
|
1821
|
+
throw new VibeError7({
|
|
1425
1822
|
code: "VIBE_SCHEMA_INVALID",
|
|
1426
1823
|
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`,
|
|
1427
1824
|
meta: {
|
|
@@ -1499,7 +1896,7 @@ var RLS_CONTEXT_TYPES = {
|
|
|
1499
1896
|
};
|
|
1500
1897
|
function defineRls(params) {
|
|
1501
1898
|
if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
|
|
1502
|
-
throw new
|
|
1899
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
1503
1900
|
}
|
|
1504
1901
|
const entries = [];
|
|
1505
1902
|
const refs = [];
|
|
@@ -1507,7 +1904,7 @@ function defineRls(params) {
|
|
|
1507
1904
|
const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
|
|
1508
1905
|
const column = descriptor?.value;
|
|
1509
1906
|
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) {
|
|
1510
|
-
throw new
|
|
1907
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
1511
1908
|
}
|
|
1512
1909
|
entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
|
|
1513
1910
|
refs.push([key, Object.freeze({ context: key })]);
|
|
@@ -1515,22 +1912,22 @@ function defineRls(params) {
|
|
|
1515
1912
|
const context = Object.freeze(Object.fromEntries(entries));
|
|
1516
1913
|
const issues = validateRlsContextIR({ context });
|
|
1517
1914
|
if (issues.length > 0) {
|
|
1518
|
-
throw new
|
|
1915
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
|
|
1519
1916
|
}
|
|
1520
1917
|
const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
|
|
1521
1918
|
if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
|
|
1522
|
-
throw new
|
|
1919
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
1523
1920
|
}
|
|
1524
1921
|
const named = [];
|
|
1525
1922
|
const classified = new Set;
|
|
1526
1923
|
for (const key of Reflect.ownKeys(policies)) {
|
|
1527
1924
|
const descriptor = Object.getOwnPropertyDescriptor(policies, key);
|
|
1528
1925
|
if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
|
|
1529
|
-
throw new
|
|
1926
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
1530
1927
|
}
|
|
1531
1928
|
const name = params.models[key]?.__name ?? key;
|
|
1532
1929
|
if (classified.has(name)) {
|
|
1533
|
-
throw new
|
|
1930
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
|
|
1534
1931
|
}
|
|
1535
1932
|
classified.add(name);
|
|
1536
1933
|
named.push([name, descriptor.value]);
|
|
@@ -1704,18 +2101,30 @@ function validateSchema(schema2) {
|
|
|
1704
2101
|
continue;
|
|
1705
2102
|
}
|
|
1706
2103
|
if (relation.__relationType === "one" && relation.__fromModel === modelName) {
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
2104
|
+
for (const column of relation.__fromColumns) {
|
|
2105
|
+
if (!model.__columns.has(column)) {
|
|
2106
|
+
errors.push({
|
|
2107
|
+
type: "error",
|
|
2108
|
+
message: `Relation "${relationName}" references FK column "${column}" which doesn't exist on model "${modelName}"`,
|
|
2109
|
+
model: modelName,
|
|
2110
|
+
relation: relationName
|
|
2111
|
+
});
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
for (const column of relation.__toColumns) {
|
|
2115
|
+
if (!targetModel.__columns.has(column)) {
|
|
2116
|
+
errors.push({
|
|
2117
|
+
type: "error",
|
|
2118
|
+
message: `Relation "${relationName}" references column "${column}" which doesn't exist on target model "${relation.__targetModel}"`,
|
|
2119
|
+
model: modelName,
|
|
2120
|
+
relation: relationName
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
1714
2123
|
}
|
|
1715
|
-
if (
|
|
2124
|
+
if (relation.__fromColumns.length !== relation.__toColumns.length) {
|
|
1716
2125
|
errors.push({
|
|
1717
2126
|
type: "error",
|
|
1718
|
-
message: `Relation "${relationName}"
|
|
2127
|
+
message: `Relation "${relationName}" pairs ${relation.__fromColumns.length} FK column(s) with ${relation.__toColumns.length} referenced column(s)`,
|
|
1719
2128
|
model: modelName,
|
|
1720
2129
|
relation: relationName
|
|
1721
2130
|
});
|
|
@@ -1802,16 +2211,20 @@ export {
|
|
|
1802
2211
|
generatePrisma,
|
|
1803
2212
|
fields,
|
|
1804
2213
|
evaluateIndexes,
|
|
2214
|
+
evaluateChecks,
|
|
1805
2215
|
dsl,
|
|
1806
2216
|
domain,
|
|
1807
2217
|
detectOneToOnePairs,
|
|
1808
2218
|
defineRls,
|
|
1809
2219
|
defineRelationsPart,
|
|
1810
2220
|
defineRelations,
|
|
2221
|
+
createPredicateBuilder,
|
|
1811
2222
|
createIndexBuilder,
|
|
1812
2223
|
createEnum,
|
|
1813
2224
|
createChildrenBuilder,
|
|
1814
2225
|
create,
|
|
2226
|
+
compileIndexPredicate,
|
|
2227
|
+
compileChecks,
|
|
1815
2228
|
cloneColumnDef,
|
|
1816
2229
|
children,
|
|
1817
2230
|
baseWithWorkspace,
|
|
@@ -1825,4 +2238,4 @@ export {
|
|
|
1825
2238
|
ChainableColumn
|
|
1826
2239
|
};
|
|
1827
2240
|
|
|
1828
|
-
//# debugId=
|
|
2241
|
+
//# debugId=13292A89548F16A064756E2164756E21
|