@vibeorm/dsl 2.0.0-alpha.8 → 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/README.md +23 -3
- package/dist/column.d.ts +8 -3
- package/dist/column.d.ts.map +1 -1
- 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 +7 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1281 -823
- package/dist/index.js.map +13 -11
- 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 +10 -1
- package/dist/relation-plan.d.ts.map +1 -1
- package/dist/relations.d.ts.map +1 -1
- package/dist/to-ir.d.ts +19 -2
- package/dist/to-ir.d.ts.map +1 -1
- package/dist/types.d.ts +307 -38
- 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
|
@@ -137,8 +137,15 @@ class ChainableEnumColumn extends ChainableColumn {
|
|
|
137
137
|
function string(config = {}) {
|
|
138
138
|
return new ChainableStringColumn(config);
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
class ChainableIntegerColumn extends ChainableColumn {
|
|
142
|
+
autoincrement() {
|
|
143
|
+
this.__config.autoincrement = true;
|
|
144
|
+
return this;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
140
147
|
function int(config = {}) {
|
|
141
|
-
return new
|
|
148
|
+
return new ChainableIntegerColumn("int", config);
|
|
142
149
|
}
|
|
143
150
|
function float(config = {}) {
|
|
144
151
|
return new ChainableColumn("float", config);
|
|
@@ -173,7 +180,7 @@ function bytes(config = {}) {
|
|
|
173
180
|
return new ChainableColumn("bytes", config);
|
|
174
181
|
}
|
|
175
182
|
function bigint(config = {}) {
|
|
176
|
-
return new
|
|
183
|
+
return new ChainableIntegerColumn("bigint", config);
|
|
177
184
|
}
|
|
178
185
|
function unsupported(dbType) {
|
|
179
186
|
const config = { dbType };
|
|
@@ -217,7 +224,224 @@ function createEnum(config) {
|
|
|
217
224
|
}
|
|
218
225
|
|
|
219
226
|
// packages/dsl/src/generator/prisma.ts
|
|
220
|
-
import { VibeError, encodePrismaString, internalError } from "@vibeorm/schema";
|
|
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
|
+
}
|
|
391
|
+
|
|
392
|
+
// packages/dsl/src/to-ir.ts
|
|
393
|
+
import {
|
|
394
|
+
EMPTY_EXTENSION_REGISTRY,
|
|
395
|
+
VibeError as VibeError4,
|
|
396
|
+
assertValidSchemaIR,
|
|
397
|
+
exactIntegerLiteral,
|
|
398
|
+
providerToDialect,
|
|
399
|
+
scalarListDefault,
|
|
400
|
+
serializeJsonDefault,
|
|
401
|
+
mergeExtensionDeclarations
|
|
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
|
+
}
|
|
439
|
+
|
|
440
|
+
// packages/dsl/src/relation-plan.ts
|
|
441
|
+
import { VibeError as VibeError3, predicateKey } from "@vibeorm/schema";
|
|
442
|
+
|
|
443
|
+
// packages/dsl/src/model.ts
|
|
444
|
+
import { VibeError as VibeError2 } from "@vibeorm/schema";
|
|
221
445
|
|
|
222
446
|
// packages/dsl/src/naming.ts
|
|
223
447
|
function capitalize(params) {
|
|
@@ -256,47 +480,67 @@ function isChildrenDef(value) {
|
|
|
256
480
|
return typeof value === "object" && value !== null && "__type" in value && value.__type === "children";
|
|
257
481
|
}
|
|
258
482
|
function createIndexBuilder() {
|
|
259
|
-
const
|
|
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
|
+
};
|
|
501
|
+
const createIndex = (columns, type, where, map) => ({
|
|
260
502
|
__type: "index",
|
|
261
503
|
__columns: [...columns],
|
|
262
504
|
__indexType: type,
|
|
505
|
+
...map === undefined ? {} : { __map: map },
|
|
263
506
|
...where === undefined ? {} : { __where: where }
|
|
264
507
|
});
|
|
265
508
|
return {
|
|
266
509
|
on(...args) {
|
|
267
510
|
const [first, second] = args;
|
|
268
511
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
269
|
-
return createIndex(first, second.type, second.where);
|
|
512
|
+
return partial(createIndex(first, second.type, second.where, second.map));
|
|
270
513
|
}
|
|
271
|
-
return createIndex(args);
|
|
514
|
+
return partial(createIndex(args));
|
|
272
515
|
},
|
|
273
516
|
index(...columns) {
|
|
274
|
-
return createIndex(columns);
|
|
517
|
+
return partial(createIndex(columns));
|
|
275
518
|
},
|
|
276
519
|
unique(...args) {
|
|
277
520
|
const [first, second] = args;
|
|
278
521
|
if (args.length === 2 && Array.isArray(first) && typeof second === "object" && !Array.isArray(second)) {
|
|
279
|
-
return {
|
|
522
|
+
return partial({
|
|
280
523
|
__type: "unique",
|
|
281
524
|
__columns: [...first],
|
|
525
|
+
...second.map === undefined ? {} : { __map: second.map },
|
|
282
526
|
...second.where === undefined ? {} : { __where: second.where },
|
|
283
527
|
...second.nullsNotDistinct === true ? { __nullsNotDistinct: true } : {},
|
|
284
528
|
...second.name === undefined ? {} : { __clientName: second.name }
|
|
285
|
-
};
|
|
529
|
+
});
|
|
286
530
|
}
|
|
287
|
-
return { __type: "unique", __columns: [...args] };
|
|
531
|
+
return partial({ __type: "unique", __columns: [...args] });
|
|
288
532
|
},
|
|
289
533
|
gin(...columns) {
|
|
290
|
-
return createIndex(columns, "Gin");
|
|
534
|
+
return partial(createIndex(columns, "Gin"));
|
|
291
535
|
},
|
|
292
536
|
gist(...columns) {
|
|
293
|
-
return createIndex(columns, "Gist");
|
|
537
|
+
return partial(createIndex(columns, "Gist"));
|
|
294
538
|
},
|
|
295
539
|
hash(...columns) {
|
|
296
|
-
return createIndex(columns, "Hash");
|
|
540
|
+
return partial(createIndex(columns, "Hash"));
|
|
297
541
|
},
|
|
298
542
|
brin(...columns) {
|
|
299
|
-
return createIndex(columns, "Brin");
|
|
543
|
+
return partial(createIndex(columns, "Brin"));
|
|
300
544
|
}
|
|
301
545
|
};
|
|
302
546
|
}
|
|
@@ -338,6 +582,7 @@ function create(config) {
|
|
|
338
582
|
__children: children2,
|
|
339
583
|
__map: config.map,
|
|
340
584
|
__primaryKey: config.primaryKey,
|
|
585
|
+
__exclusions: config.exclusions,
|
|
341
586
|
__checks: config.checks,
|
|
342
587
|
__extensions: config.extensions,
|
|
343
588
|
__policy: config.policy,
|
|
@@ -362,6 +607,9 @@ function detectOneToOnePairs(params) {
|
|
|
362
607
|
const allOnes = [];
|
|
363
608
|
for (const [modelName, modelRelations] of Object.entries(relations)) {
|
|
364
609
|
for (const [relName, rel] of Object.entries(modelRelations)) {
|
|
610
|
+
if (rel.__ownsForeignKey !== undefined && (typeof rel.__ownsForeignKey !== "boolean" || rel.__relationType !== "one" || rel.__ownsForeignKey && rel.__fromModel !== modelName)) {
|
|
611
|
+
throw new VibeError3({ code: "VIBE_SCHEMA_INVALID", message: `Invalid foreign-key ownership declaration on ${modelName}.${relName}` });
|
|
612
|
+
}
|
|
365
613
|
if (rel.__relationType === "one") {
|
|
366
614
|
allOnes.push({ modelName, relName, rel });
|
|
367
615
|
}
|
|
@@ -382,12 +630,13 @@ function detectOneToOnePairs(params) {
|
|
|
382
630
|
const keyB = `${b.modelName}:${b.relName}`;
|
|
383
631
|
if (paired.has(keyB))
|
|
384
632
|
continue;
|
|
385
|
-
const sameColumnPair = a.rel.__fromModel === b.rel.__toModel && a.rel.
|
|
386
|
-
if (!sameColumnPair)
|
|
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 });
|
|
634
|
+
if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
|
|
387
635
|
continue;
|
|
388
636
|
const { fkOwner, backRelation } = resolveFkOwner({ a, b, models });
|
|
389
637
|
const onDelete = fkOwner.rel.__onDelete ?? backRelation.rel.__onDelete;
|
|
390
638
|
const onUpdate = fkOwner.rel.__onUpdate ?? backRelation.rel.__onUpdate;
|
|
639
|
+
const constraintName = fkOwner.rel.__constraintName ?? backRelation.rel.__constraintName;
|
|
391
640
|
const fkKey = `${fkOwner.modelName}:${fkOwner.relName}`;
|
|
392
641
|
const backKey = `${backRelation.modelName}:${backRelation.relName}`;
|
|
393
642
|
mergeMap.set(fkKey, {
|
|
@@ -395,14 +644,16 @@ function detectOneToOnePairs(params) {
|
|
|
395
644
|
pairedRelName: backRelation.relName,
|
|
396
645
|
pairedModelName: backRelation.modelName,
|
|
397
646
|
onDelete,
|
|
398
|
-
onUpdate
|
|
647
|
+
onUpdate,
|
|
648
|
+
...constraintName === undefined ? {} : { constraintName }
|
|
399
649
|
});
|
|
400
650
|
mergeMap.set(backKey, {
|
|
401
651
|
role: "backRelation",
|
|
402
652
|
pairedRelName: fkOwner.relName,
|
|
403
653
|
pairedModelName: fkOwner.modelName,
|
|
404
654
|
onDelete,
|
|
405
|
-
onUpdate
|
|
655
|
+
onUpdate,
|
|
656
|
+
...constraintName === undefined ? {} : { constraintName }
|
|
406
657
|
});
|
|
407
658
|
paired.add(fkKey);
|
|
408
659
|
paired.add(backKey);
|
|
@@ -411,23 +662,54 @@ function detectOneToOnePairs(params) {
|
|
|
411
662
|
}
|
|
412
663
|
return mergeMap;
|
|
413
664
|
}
|
|
665
|
+
function sameColumnTuple(params) {
|
|
666
|
+
const { left, right } = params;
|
|
667
|
+
return left.length === right.length && left.every((name, index) => name === right[index]);
|
|
668
|
+
}
|
|
414
669
|
function resolveFkOwner(params) {
|
|
415
670
|
const { a, b, models } = params;
|
|
416
|
-
const
|
|
417
|
-
|
|
671
|
+
const ownershipError = () => {
|
|
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" } });
|
|
673
|
+
};
|
|
674
|
+
const aOwns = a.rel.__ownsForeignKey;
|
|
675
|
+
const bOwns = b.rel.__ownsForeignKey;
|
|
676
|
+
if (aOwns !== undefined || bOwns !== undefined) {
|
|
677
|
+
if (aOwns === bOwns)
|
|
678
|
+
return ownershipError();
|
|
679
|
+
return aOwns === true || bOwns === false ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
|
|
680
|
+
}
|
|
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 });
|
|
418
683
|
if (aFromIsId && !bFromIsId)
|
|
419
684
|
return { fkOwner: b, backRelation: a };
|
|
420
685
|
if (!aFromIsId && bFromIsId)
|
|
421
686
|
return { fkOwner: a, backRelation: b };
|
|
422
|
-
|
|
687
|
+
const aActions = a.rel.__onDelete !== undefined || a.rel.__onUpdate !== undefined;
|
|
688
|
+
const bActions = b.rel.__onDelete !== undefined || b.rel.__onUpdate !== undefined;
|
|
689
|
+
if (aActions && !bActions) {
|
|
423
690
|
return { fkOwner: a, backRelation: b };
|
|
424
691
|
}
|
|
425
|
-
if (
|
|
692
|
+
if (bActions && !aActions) {
|
|
426
693
|
return { fkOwner: b, backRelation: a };
|
|
427
694
|
}
|
|
428
|
-
const aLooksFk = looksLikeForeignKey({ column
|
|
429
|
-
const bLooksFk = looksLikeForeignKey({ column
|
|
430
|
-
|
|
695
|
+
const aLooksFk = a.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
|
|
696
|
+
const bLooksFk = b.rel.__fromColumns.some((column) => looksLikeForeignKey({ column }));
|
|
697
|
+
if (aLooksFk === bLooksFk)
|
|
698
|
+
return ownershipError();
|
|
699
|
+
return aLooksFk ? { fkOwner: a, backRelation: b } : { fkOwner: b, backRelation: a };
|
|
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;
|
|
431
713
|
}
|
|
432
714
|
function looksLikeForeignKey(params) {
|
|
433
715
|
const { column } = params;
|
|
@@ -453,19 +735,21 @@ function planExplicitRelations(params) {
|
|
|
453
735
|
planned.push({ ...base, kind: "backOne", isList: false, isOptional: true });
|
|
454
736
|
continue;
|
|
455
737
|
}
|
|
456
|
-
const ownsFk = mergeInfo?.role === "fkOwner" || rel.__fromModel === modelName;
|
|
738
|
+
const ownsFk = mergeInfo?.role === "fkOwner" || rel.__ownsForeignKey !== false && rel.__fromModel === modelName;
|
|
457
739
|
if (!ownsFk) {
|
|
458
740
|
planned.push({ ...base, kind: "backOne", isList: false, isOptional: rel.__optional });
|
|
459
741
|
continue;
|
|
460
742
|
}
|
|
461
|
-
const
|
|
743
|
+
const anyMemberOptional = rel.__fromColumns.some((name2) => model.__columns.get(name2)?.__config.optional === true);
|
|
744
|
+
const constraintName = mergeInfo?.constraintName ?? rel.__constraintName;
|
|
462
745
|
planned.push({
|
|
463
746
|
...base,
|
|
464
747
|
kind: "fkOwner",
|
|
465
748
|
isList: false,
|
|
466
|
-
isOptional:
|
|
467
|
-
fields: [rel.
|
|
468
|
-
references: [rel.
|
|
749
|
+
isOptional: anyMemberOptional || rel.__optional,
|
|
750
|
+
fields: [...rel.__fromColumns],
|
|
751
|
+
references: [...rel.__toColumns],
|
|
752
|
+
...constraintName === undefined ? {} : { constraintName },
|
|
469
753
|
onDelete: mergeInfo?.onDelete ?? rel.__onDelete,
|
|
470
754
|
onUpdate: mergeInfo?.onUpdate ?? rel.__onUpdate
|
|
471
755
|
});
|
|
@@ -562,7 +846,8 @@ function planIndexes(params) {
|
|
|
562
846
|
}
|
|
563
847
|
}
|
|
564
848
|
for (const index of evaluateIndexes({ model })) {
|
|
565
|
-
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;
|
|
566
851
|
const existing = byKey.get(key);
|
|
567
852
|
const isUnique = index.__type === "unique";
|
|
568
853
|
if (existing === undefined || isUnique) {
|
|
@@ -570,16 +855,91 @@ function planIndexes(params) {
|
|
|
570
855
|
isUnique,
|
|
571
856
|
columns: [...index.__columns],
|
|
572
857
|
indexType: index.__indexType,
|
|
858
|
+
...index.__map === undefined ? {} : { map: index.__map },
|
|
573
859
|
...index.__where === undefined ? {} : { where: index.__where },
|
|
860
|
+
...index.__wherePredicate === undefined ? {} : { wherePredicate: index.__wherePredicate },
|
|
574
861
|
...index.__nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
|
|
575
862
|
...index.__clientName === undefined ? {} : { clientName: index.__clientName }
|
|
576
863
|
});
|
|
864
|
+
} else if (index.__map !== undefined) {
|
|
865
|
+
byKey.set(key, { ...existing, map: index.__map, indexType: index.__indexType ?? existing.indexType });
|
|
577
866
|
} else if (index.__indexType !== undefined && existing.indexType === undefined) {
|
|
578
867
|
byKey.set(key, { ...existing, indexType: index.__indexType });
|
|
579
868
|
}
|
|
580
869
|
}
|
|
581
870
|
return Array.from(byKey.values());
|
|
582
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
|
+
}
|
|
583
943
|
function planSchema(params) {
|
|
584
944
|
const { schema } = params;
|
|
585
945
|
const mergeMap = detectOneToOnePairs({
|
|
@@ -602,7 +962,12 @@ function planSchema(params) {
|
|
|
602
962
|
existing: explicit
|
|
603
963
|
});
|
|
604
964
|
const children2 = planChildRelations({ modelName, model, models: schema.__models });
|
|
605
|
-
|
|
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);
|
|
606
971
|
indexes.set(modelName, planIndexes({ model }));
|
|
607
972
|
}
|
|
608
973
|
return { relations, indexes };
|
|
@@ -627,142 +992,454 @@ function findCounterparts(params) {
|
|
|
627
992
|
return (plan.relations.get(relation.targetModel) ?? []).filter((candidate) => candidate !== relation && candidate.targetModel === relation.model && (candidate.relationName ?? "") === nameKey);
|
|
628
993
|
}
|
|
629
994
|
|
|
630
|
-
// packages/dsl/src/
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
switch (column.__columnType) {
|
|
635
|
-
case "string":
|
|
636
|
-
return `String${list}`;
|
|
637
|
-
case "int":
|
|
638
|
-
return `Int${list}`;
|
|
639
|
-
case "bigint":
|
|
640
|
-
return `BigInt${list}`;
|
|
641
|
-
case "float":
|
|
642
|
-
return `Float${list}`;
|
|
643
|
-
case "decimal":
|
|
644
|
-
return `Decimal${list}`;
|
|
645
|
-
case "boolean":
|
|
646
|
-
return `Boolean${list}`;
|
|
647
|
-
case "datetime":
|
|
648
|
-
return `DateTime${list}`;
|
|
649
|
-
case "json":
|
|
650
|
-
return `Json${list}`;
|
|
651
|
-
case "bytes":
|
|
652
|
-
return `Bytes${list}`;
|
|
653
|
-
case "id":
|
|
654
|
-
return column.__config.type === "autoincrement" ? "Int" : "String";
|
|
655
|
-
case "enum":
|
|
656
|
-
return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
|
|
657
|
-
case "unsupported": {
|
|
658
|
-
const extensionTypeName = column.__config["extensionType"];
|
|
659
|
-
if (typeof extensionTypeName === "string") {
|
|
660
|
-
const rawArgs = column.__config["extensionArgs"];
|
|
661
|
-
const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
|
|
662
|
-
return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
|
|
663
|
-
}
|
|
664
|
-
return `Unsupported(${encodePrismaString({ value: String(column.__config.dbType) })})${list}`;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
function idDefaultAttribute(params) {
|
|
669
|
-
const idType = params.column.__config.type;
|
|
670
|
-
switch (idType) {
|
|
995
|
+
// packages/dsl/src/to-ir.ts
|
|
996
|
+
var fieldType = columnFieldType;
|
|
997
|
+
function idDefault(params) {
|
|
998
|
+
switch (params.column.__config.type) {
|
|
671
999
|
case "ulid":
|
|
1000
|
+
return { kind: "ulid" };
|
|
672
1001
|
case "cuid":
|
|
1002
|
+
return { kind: "cuid" };
|
|
673
1003
|
case "uuid":
|
|
1004
|
+
return { kind: "uuid" };
|
|
674
1005
|
case "nanoid":
|
|
1006
|
+
return { kind: "nanoid" };
|
|
675
1007
|
case "autoincrement":
|
|
676
|
-
return
|
|
1008
|
+
return { kind: "autoincrement" };
|
|
677
1009
|
default:
|
|
678
1010
|
return;
|
|
679
1011
|
}
|
|
680
1012
|
}
|
|
681
|
-
function
|
|
1013
|
+
function fieldDefault(params) {
|
|
682
1014
|
const { column } = params;
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
1015
|
+
if (column.__columnType === "id") {
|
|
1016
|
+
if (column.__config.default !== undefined || column.__config.dbgenerated !== undefined || column.__config.autoincrement !== undefined)
|
|
1017
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Use the id generator strategy instead of a conflicting id default" });
|
|
1018
|
+
return idDefault({ column });
|
|
1019
|
+
}
|
|
1020
|
+
const { dbgenerated, default: value, autoincrement } = column.__config;
|
|
1021
|
+
if ([dbgenerated !== undefined, value !== undefined, autoincrement === true].filter(Boolean).length > 1) {
|
|
1022
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Column "${column.__name ?? column.__columnType}" has conflicting defaults` });
|
|
1023
|
+
}
|
|
1024
|
+
if (autoincrement === true) {
|
|
1025
|
+
if (column.__columnType !== "int" && column.__columnType !== "bigint" || column.__config.array === true || column.__config.optional === true) {
|
|
1026
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "autoincrement requires a required scalar Int or BigInt column" });
|
|
1027
|
+
}
|
|
1028
|
+
return { kind: "autoincrement" };
|
|
686
1029
|
}
|
|
1030
|
+
if (typeof dbgenerated === "string")
|
|
1031
|
+
return { kind: "dbgenerated", sql: dbgenerated };
|
|
687
1032
|
if (value === undefined)
|
|
688
1033
|
return;
|
|
689
|
-
if (column.
|
|
690
|
-
|
|
1034
|
+
if (column.__config.array === true) {
|
|
1035
|
+
if (!Array.isArray(value))
|
|
1036
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "A scalar-list default must be an array" });
|
|
1037
|
+
return { kind: "list", values: scalarListDefault({ type: fieldType({ column }), values: column.__columnType === "json" ? value.map((entry) => typeof entry === "string" ? entry : serializeJsonDefault({ value: entry })) : value }) };
|
|
691
1038
|
}
|
|
1039
|
+
if (column.__columnType === "json" && typeof value !== "string") {
|
|
1040
|
+
return { kind: "literal", value: serializeJsonDefault({ value }) };
|
|
1041
|
+
}
|
|
1042
|
+
if (column.__columnType === "datetime" && value === "now")
|
|
1043
|
+
return { kind: "now" };
|
|
692
1044
|
if (column.__columnType === "enum" && typeof value === "string") {
|
|
693
|
-
return
|
|
1045
|
+
return { kind: "enumValue", value };
|
|
1046
|
+
}
|
|
1047
|
+
if (typeof value === "number" && (!Number.isFinite(value) || (column.__columnType === "int" || column.__columnType === "bigint") && !Number.isSafeInteger(value))) {
|
|
1048
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: "Numeric defaults must be finite; integer defaults must be exact" });
|
|
1049
|
+
}
|
|
1050
|
+
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
1051
|
+
return { kind: "literal", value };
|
|
694
1052
|
}
|
|
695
|
-
if (typeof value === "string")
|
|
696
|
-
return `@default(${encodePrismaString({ value })})`;
|
|
697
|
-
if (typeof value === "boolean" || typeof value === "number")
|
|
698
|
-
return `@default(${value})`;
|
|
699
1053
|
if (typeof value === "bigint")
|
|
700
|
-
return
|
|
701
|
-
|
|
1054
|
+
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
1055
|
+
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
|
|
702
1056
|
}
|
|
703
|
-
function
|
|
1057
|
+
function nativeType(params) {
|
|
704
1058
|
const { column } = params;
|
|
705
|
-
if (column.__columnType === "
|
|
1059
|
+
if (column.__columnType === "enum") {
|
|
1060
|
+
const { maxLength, text } = column.__config;
|
|
1061
|
+
if (maxLength !== undefined || text !== undefined) {
|
|
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;
|
|
1063
|
+
if (invalid) {
|
|
1064
|
+
throw new VibeError4({
|
|
1065
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1066
|
+
message: "Enum text/maxLength options require string storage and either a positive integer maxLength or text: true, not both",
|
|
1067
|
+
meta: { issues: [{ rule: "enum-string-storage-options", enum: column.__enumName }] }
|
|
1068
|
+
});
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
if (column.__columnType === "string" || column.__columnType === "enum" && column.__enumStorage === "string") {
|
|
706
1073
|
const { maxLength, text } = column.__config;
|
|
707
1074
|
if (typeof maxLength === "number")
|
|
708
|
-
return
|
|
1075
|
+
return `VarChar(${maxLength})`;
|
|
709
1076
|
if (text === true)
|
|
710
|
-
return "
|
|
1077
|
+
return "Text";
|
|
711
1078
|
return;
|
|
712
1079
|
}
|
|
713
1080
|
if (column.__columnType === "decimal") {
|
|
714
1081
|
const { precision, scale } = column.__config;
|
|
715
1082
|
if (typeof precision === "number")
|
|
716
|
-
return
|
|
1083
|
+
return `Decimal(${precision}, ${scale ?? 0})`;
|
|
717
1084
|
return;
|
|
718
1085
|
}
|
|
719
1086
|
if (column.__columnType === "datetime" && column.__config.tz === true) {
|
|
720
|
-
return "
|
|
721
|
-
}
|
|
722
|
-
return;
|
|
723
|
-
}
|
|
724
|
-
function formatColumn(params) {
|
|
725
|
-
const { name, column } = params;
|
|
726
|
-
const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
|
|
727
|
-
if (column.__columnType === "id") {
|
|
728
|
-
parts.push("@id");
|
|
729
|
-
const idDefault = idDefaultAttribute({ column });
|
|
730
|
-
if (idDefault !== undefined)
|
|
731
|
-
parts.push(idDefault);
|
|
732
|
-
} else {
|
|
733
|
-
const columnDefault = defaultAttribute({ column });
|
|
734
|
-
if (columnDefault !== undefined)
|
|
735
|
-
parts.push(columnDefault);
|
|
736
|
-
}
|
|
737
|
-
if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
|
|
738
|
-
parts.push("@updatedAt");
|
|
739
|
-
}
|
|
740
|
-
if (column.__config.unique === true) {
|
|
741
|
-
parts.push("@unique");
|
|
742
|
-
}
|
|
743
|
-
if (typeof column.__config.map === "string") {
|
|
744
|
-
parts.push(`@map(${encodePrismaString({ value: column.__config.map })})`);
|
|
745
|
-
}
|
|
746
|
-
const nativeType = nativeTypeAttribute({ column });
|
|
747
|
-
if (nativeType !== undefined)
|
|
748
|
-
parts.push(nativeType);
|
|
749
|
-
if (typeof column.__config.collation === "string") {
|
|
750
|
-
parts.push(`@collation(${encodePrismaString({ value: column.__config.collation })})`);
|
|
1087
|
+
return "Timestamptz";
|
|
751
1088
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
if (typeof comment === "string") {
|
|
755
|
-
const commentLines = comment.split(`
|
|
756
|
-
`).map((line) => ` /// ${line}`);
|
|
757
|
-
return [...commentLines, columnLine].join(`
|
|
758
|
-
`);
|
|
1089
|
+
if (column.__columnType === "unsupported") {
|
|
1090
|
+
return String(column.__config.dbType);
|
|
759
1091
|
}
|
|
760
|
-
return
|
|
1092
|
+
return;
|
|
761
1093
|
}
|
|
762
|
-
function
|
|
763
|
-
const {
|
|
764
|
-
const
|
|
765
|
-
|
|
1094
|
+
function buildField(params) {
|
|
1095
|
+
const { name, column, extensionNativeType } = params;
|
|
1096
|
+
const config = column.__config;
|
|
1097
|
+
const isList = column.__columnType !== "id" && config.array === true;
|
|
1098
|
+
const dbName = typeof config.map === "string" ? config.map : undefined;
|
|
1099
|
+
const documentation = typeof config.comment === "string" ? config.comment : undefined;
|
|
1100
|
+
const collation = typeof config.collation === "string" ? config.collation : undefined;
|
|
1101
|
+
const defaultValue = fieldDefault({ column });
|
|
1102
|
+
const native = extensionNativeType ?? nativeType({ column });
|
|
1103
|
+
return {
|
|
1104
|
+
name,
|
|
1105
|
+
type: fieldType({ column }),
|
|
1106
|
+
isList,
|
|
1107
|
+
isOptional: config.optional === true,
|
|
1108
|
+
isId: column.__columnType === "id",
|
|
1109
|
+
isUnique: config.unique === true,
|
|
1110
|
+
isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
|
|
1111
|
+
...collation === undefined ? {} : { collation },
|
|
1112
|
+
...dbName === undefined ? {} : { dbName },
|
|
1113
|
+
...defaultValue === undefined ? {} : { default: defaultValue },
|
|
1114
|
+
...native === undefined ? {} : { nativeType: native },
|
|
1115
|
+
...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
|
|
1116
|
+
...documentation === undefined ? {} : { documentation }
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function buildRelation(params) {
|
|
1120
|
+
const { relation, plan } = params;
|
|
1121
|
+
return {
|
|
1122
|
+
name: relation.name,
|
|
1123
|
+
model: relation.model,
|
|
1124
|
+
targetModel: relation.targetModel,
|
|
1125
|
+
type: resolveRelationType({ relation, plan }),
|
|
1126
|
+
isList: relation.isList,
|
|
1127
|
+
isOptional: relation.isOptional,
|
|
1128
|
+
...relation.relationName === undefined ? {} : { relationName: relation.relationName },
|
|
1129
|
+
...relation.constraintName === undefined ? {} : { constraintName: relation.constraintName },
|
|
1130
|
+
...relation.fields === undefined ? {} : { fields: [...relation.fields] },
|
|
1131
|
+
...relation.references === undefined ? {} : { references: [...relation.references] },
|
|
1132
|
+
...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
|
|
1133
|
+
...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
var INDEX_KINDS = {
|
|
1137
|
+
BTree: "btree",
|
|
1138
|
+
Hash: "hash",
|
|
1139
|
+
Gin: "gin",
|
|
1140
|
+
Gist: "gist",
|
|
1141
|
+
SpGist: "spgist",
|
|
1142
|
+
Brin: "brin"
|
|
1143
|
+
};
|
|
1144
|
+
function buildIndex(params) {
|
|
1145
|
+
const { index, model, dialect, enums } = params;
|
|
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
|
+
});
|
|
1155
|
+
return {
|
|
1156
|
+
kind,
|
|
1157
|
+
fields: index.columns.map((name) => ({ name })),
|
|
1158
|
+
...index.map === undefined ? {} : { name: index.map },
|
|
1159
|
+
...predicate === undefined ? {} : { where: predicate.where },
|
|
1160
|
+
...predicate?.wherePredicate === undefined ? {} : { wherePredicate: predicate.wherePredicate },
|
|
1161
|
+
...index.nullsNotDistinct === true ? { nullsNotDistinct: true } : {},
|
|
1162
|
+
...index.clientName === undefined ? {} : { clientName: index.clientName }
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
function buildModel(params) {
|
|
1166
|
+
const { model, plan, extensions, declarations, issues, dialect, enums } = params;
|
|
1167
|
+
const primaryKey = model.__primaryKey !== undefined && model.__primaryKey.length > 0 ? { fields: [...model.__primaryKey] } : undefined;
|
|
1168
|
+
const policy = model.__policy === undefined ? undefined : { field: model.__policy.field, context: model.__policy.context ?? model.__policy.field };
|
|
1169
|
+
const fields = [];
|
|
1170
|
+
for (const [name, column] of model.__columns) {
|
|
1171
|
+
const extensionTypeName = column.__config["extensionType"];
|
|
1172
|
+
if (typeof extensionTypeName !== "string") {
|
|
1173
|
+
fields.push(buildField({ name, column }));
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
const registered = extensions.fieldTypes.get(extensionTypeName);
|
|
1177
|
+
if (registered === undefined) {
|
|
1178
|
+
issues.push({
|
|
1179
|
+
rule: "extension-attribute",
|
|
1180
|
+
message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
|
|
1181
|
+
model: model.__name,
|
|
1182
|
+
field: name
|
|
1183
|
+
});
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
const rawArgs = column.__config["extensionArgs"];
|
|
1187
|
+
const lowered = registered.fieldType.lower({
|
|
1188
|
+
model: model.__name,
|
|
1189
|
+
field: name,
|
|
1190
|
+
args: Array.isArray(rawArgs) ? rawArgs.filter((entry) => typeof entry === "string" || typeof entry === "number") : []
|
|
1191
|
+
});
|
|
1192
|
+
if ("error" in lowered) {
|
|
1193
|
+
issues.push({
|
|
1194
|
+
rule: "extension-attribute",
|
|
1195
|
+
message: `Field "${model.__name}.${name}": ${lowered.error}`,
|
|
1196
|
+
model: model.__name,
|
|
1197
|
+
field: name
|
|
1198
|
+
});
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
fields.push(buildField({ name, column, extensionNativeType: lowered.nativeType }));
|
|
1202
|
+
if (lowered.declaration !== undefined) {
|
|
1203
|
+
declarations.add({
|
|
1204
|
+
extension: registered.extension.name,
|
|
1205
|
+
model: model.__name,
|
|
1206
|
+
declaration: lowered.declaration
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
for (const use of model.__extensions ?? []) {
|
|
1211
|
+
const registered = extensions.modelAttributes.get(use.attribute);
|
|
1212
|
+
if (registered === undefined) {
|
|
1213
|
+
issues.push({
|
|
1214
|
+
rule: "unknown-attribute",
|
|
1215
|
+
message: `Unknown block attribute "@@${use.attribute}" on model "${model.__name}" \u2014 no extension provides it`,
|
|
1216
|
+
model: model.__name
|
|
1217
|
+
});
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const parsed = registered.attribute.parse({
|
|
1221
|
+
model: model.__name,
|
|
1222
|
+
args: {
|
|
1223
|
+
named: { ...use.args ?? {} },
|
|
1224
|
+
...use.fields === undefined ? {} : { fields: use.fields }
|
|
1225
|
+
},
|
|
1226
|
+
fieldNames: fields.map((field) => field.name)
|
|
1227
|
+
});
|
|
1228
|
+
if ("error" in parsed) {
|
|
1229
|
+
issues.push({
|
|
1230
|
+
rule: "extension-attribute",
|
|
1231
|
+
message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
|
|
1232
|
+
model: model.__name
|
|
1233
|
+
});
|
|
1234
|
+
continue;
|
|
1235
|
+
}
|
|
1236
|
+
declarations.add({
|
|
1237
|
+
extension: registered.extension.name,
|
|
1238
|
+
model: model.__name,
|
|
1239
|
+
declaration: parsed.declaration
|
|
1240
|
+
});
|
|
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 });
|
|
1245
|
+
return {
|
|
1246
|
+
name: model.__name,
|
|
1247
|
+
...model.__map === undefined ? {} : { dbName: model.__map },
|
|
1248
|
+
fields,
|
|
1249
|
+
relations: (plan.relations.get(model.__name) ?? []).map((relation) => buildRelation({ relation, plan })),
|
|
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 },
|
|
1253
|
+
...policy === undefined ? {} : { policy },
|
|
1254
|
+
...primaryKey === undefined ? {} : { primaryKey },
|
|
1255
|
+
isView: false,
|
|
1256
|
+
...model.__comment === undefined ? {} : { documentation: model.__comment }
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
class ExtensionDeclarationCollector {
|
|
1261
|
+
byExtension = new Map;
|
|
1262
|
+
add(params) {
|
|
1263
|
+
const perModel = this.byExtension.get(params.extension) ?? new Map;
|
|
1264
|
+
this.byExtension.set(params.extension, perModel);
|
|
1265
|
+
const existing = perModel.get(params.model) ?? {};
|
|
1266
|
+
perModel.set(params.model, mergeExtensionDeclarations({ base: existing, next: params.declaration }));
|
|
1267
|
+
}
|
|
1268
|
+
toIR() {
|
|
1269
|
+
return [...this.byExtension.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([name, perModel]) => ({
|
|
1270
|
+
name,
|
|
1271
|
+
options: {
|
|
1272
|
+
models: Object.fromEntries([...perModel.entries()].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0))
|
|
1273
|
+
}
|
|
1274
|
+
}));
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
function toIR(params) {
|
|
1278
|
+
const { schema, provider = "postgresql" } = params;
|
|
1279
|
+
const extensions = params.extensions ?? EMPTY_EXTENSION_REGISTRY;
|
|
1280
|
+
const plan = planSchema({ schema });
|
|
1281
|
+
const declarations = new ExtensionDeclarationCollector;
|
|
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 }) });
|
|
1289
|
+
const models = Array.from(schema.__models.values(), (model) => {
|
|
1290
|
+
const built = buildModel({ model, plan, extensions, declarations, issues, dialect, enums });
|
|
1291
|
+
const rls = schema.__rls?.policies[model.__name];
|
|
1292
|
+
return rls === undefined ? built : { ...built, rls };
|
|
1293
|
+
});
|
|
1294
|
+
if (schema.__rls !== undefined) {
|
|
1295
|
+
for (const name of Object.keys(schema.__rls.policies)) {
|
|
1296
|
+
if (!schema.__models.has(name)) {
|
|
1297
|
+
issues.push({ rule: "rls-unknown-model", model: name, message: `RLS classification references model "${name}" absent from the composed schema` });
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (issues.length > 0) {
|
|
1302
|
+
throw new VibeError4({
|
|
1303
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1304
|
+
message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
|
|
1305
|
+
meta: { issues }
|
|
1306
|
+
});
|
|
1307
|
+
}
|
|
1308
|
+
const ir = {
|
|
1309
|
+
datasource: { provider },
|
|
1310
|
+
models,
|
|
1311
|
+
enums,
|
|
1312
|
+
extensions: declarations.toIR(),
|
|
1313
|
+
...schema.__rls === undefined ? {} : { rls: { context: schema.__rls.context } }
|
|
1314
|
+
};
|
|
1315
|
+
assertValidSchemaIR({ schema: ir });
|
|
1316
|
+
return ir;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
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
|
+
}
|
|
1326
|
+
function prismaType(params) {
|
|
1327
|
+
const { column } = params;
|
|
1328
|
+
const list = column.__config.array === true ? "[]" : "";
|
|
1329
|
+
switch (column.__columnType) {
|
|
1330
|
+
case "string":
|
|
1331
|
+
return `String${list}`;
|
|
1332
|
+
case "int":
|
|
1333
|
+
return `Int${list}`;
|
|
1334
|
+
case "bigint":
|
|
1335
|
+
return `BigInt${list}`;
|
|
1336
|
+
case "float":
|
|
1337
|
+
return `Float${list}`;
|
|
1338
|
+
case "decimal":
|
|
1339
|
+
return `Decimal${list}`;
|
|
1340
|
+
case "boolean":
|
|
1341
|
+
return `Boolean${list}`;
|
|
1342
|
+
case "datetime":
|
|
1343
|
+
return `DateTime${list}`;
|
|
1344
|
+
case "json":
|
|
1345
|
+
return `Json${list}`;
|
|
1346
|
+
case "bytes":
|
|
1347
|
+
return `Bytes${list}`;
|
|
1348
|
+
case "id":
|
|
1349
|
+
return column.__config.type === "autoincrement" ? "Int" : "String";
|
|
1350
|
+
case "enum":
|
|
1351
|
+
return column.__enumName === undefined ? `String${list}` : `${column.__enumName}${list}`;
|
|
1352
|
+
case "unsupported": {
|
|
1353
|
+
const extensionTypeName = column.__config["extensionType"];
|
|
1354
|
+
if (typeof extensionTypeName === "string") {
|
|
1355
|
+
const rawArgs = column.__config["extensionArgs"];
|
|
1356
|
+
const args = Array.isArray(rawArgs) ? rawArgs.map(String).join(", ") : "";
|
|
1357
|
+
return `${extensionTypeName}${args.length > 0 ? `(${args})` : ""}${list}`;
|
|
1358
|
+
}
|
|
1359
|
+
return `Unsupported(${encodePrismaString({ value: String(column.__config.dbType) })})${list}`;
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
function defaultAttribute(params) {
|
|
1364
|
+
const { column } = params;
|
|
1365
|
+
const value = fieldDefault({ column });
|
|
1366
|
+
if (value === undefined)
|
|
1367
|
+
return;
|
|
1368
|
+
if (value.kind === "list") {
|
|
1369
|
+
const values = value.values.map((entry) => column.__columnType === "enum" ? String(entry) : column.__columnType === "bigint" ? String(entry) : JSON.stringify(entry));
|
|
1370
|
+
return `@default([${values.join(", ")}])`;
|
|
1371
|
+
}
|
|
1372
|
+
if (value.kind === "literal")
|
|
1373
|
+
return `@default(${typeof value.value === "string" ? encodePrismaString({ value: value.value }) : String(value.value)})`;
|
|
1374
|
+
if (value.kind === "enumValue")
|
|
1375
|
+
return `@default(${value.value})`;
|
|
1376
|
+
if (value.kind === "dbgenerated")
|
|
1377
|
+
return `@default(dbgenerated(${encodePrismaString({ value: value.sql })}))`;
|
|
1378
|
+
return `@default(${value.kind}())`;
|
|
1379
|
+
}
|
|
1380
|
+
function nativeTypeAttribute(params) {
|
|
1381
|
+
const { column } = params;
|
|
1382
|
+
if (column.__columnType === "string") {
|
|
1383
|
+
const { maxLength, text } = column.__config;
|
|
1384
|
+
if (typeof maxLength === "number")
|
|
1385
|
+
return `@db.VarChar(${maxLength})`;
|
|
1386
|
+
if (text === true)
|
|
1387
|
+
return "@db.Text";
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
if (column.__columnType === "decimal") {
|
|
1391
|
+
const { precision, scale } = column.__config;
|
|
1392
|
+
if (typeof precision === "number")
|
|
1393
|
+
return `@db.Decimal(${precision}, ${scale ?? 0})`;
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
if (column.__columnType === "datetime" && column.__config.tz === true) {
|
|
1397
|
+
return "@db.Timestamptz";
|
|
1398
|
+
}
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
function formatColumn(params) {
|
|
1402
|
+
const { name, column } = params;
|
|
1403
|
+
const parts = [` ${name} ${prismaType({ column })}${column.__config.optional === true ? "?" : ""}`];
|
|
1404
|
+
if (column.__columnType === "id") {
|
|
1405
|
+
parts.push("@id");
|
|
1406
|
+
const idDefault2 = defaultAttribute({ column });
|
|
1407
|
+
if (idDefault2 !== undefined)
|
|
1408
|
+
parts.push(idDefault2);
|
|
1409
|
+
} else {
|
|
1410
|
+
const columnDefault = defaultAttribute({ column });
|
|
1411
|
+
if (columnDefault !== undefined)
|
|
1412
|
+
parts.push(columnDefault);
|
|
1413
|
+
}
|
|
1414
|
+
if (column.__columnType === "datetime" && column.__config.updatedAt === true) {
|
|
1415
|
+
parts.push("@updatedAt");
|
|
1416
|
+
}
|
|
1417
|
+
if (column.__config.unique === true) {
|
|
1418
|
+
parts.push("@unique");
|
|
1419
|
+
}
|
|
1420
|
+
if (typeof column.__config.map === "string") {
|
|
1421
|
+
parts.push(`@map(${encodePrismaString({ value: column.__config.map })})`);
|
|
1422
|
+
}
|
|
1423
|
+
const nativeType2 = nativeTypeAttribute({ column });
|
|
1424
|
+
if (nativeType2 !== undefined)
|
|
1425
|
+
parts.push(nativeType2);
|
|
1426
|
+
if (typeof column.__config.collation === "string") {
|
|
1427
|
+
parts.push(`@collation(${encodePrismaString({ value: column.__config.collation })})`);
|
|
1428
|
+
}
|
|
1429
|
+
const columnLine = parts.join(" ");
|
|
1430
|
+
const comment = column.__config.comment;
|
|
1431
|
+
if (typeof comment === "string") {
|
|
1432
|
+
const commentLines = comment.split(`
|
|
1433
|
+
`).map((line) => ` /// ${line}`);
|
|
1434
|
+
return [...commentLines, columnLine].join(`
|
|
1435
|
+
`);
|
|
1436
|
+
}
|
|
1437
|
+
return columnLine;
|
|
1438
|
+
}
|
|
1439
|
+
function formatRelation(params) {
|
|
1440
|
+
const { relation } = params;
|
|
1441
|
+
const { name, targetModel, relationName } = relation;
|
|
1442
|
+
if (relation.kind === "list") {
|
|
766
1443
|
const attribute = relationName === undefined ? "" : ` @relation(${encodePrismaString({ value: relationName })})`;
|
|
767
1444
|
return ` ${name} ${targetModel}[]${attribute}`;
|
|
768
1445
|
}
|
|
@@ -786,19 +1463,31 @@ function formatRelation(params) {
|
|
|
786
1463
|
args.push(`onDelete: ${relation.onDelete}`);
|
|
787
1464
|
if (relation.onUpdate !== undefined)
|
|
788
1465
|
args.push(`onUpdate: ${relation.onUpdate}`);
|
|
1466
|
+
if (relation.constraintName !== undefined) {
|
|
1467
|
+
args.push(`map: ${encodePrismaString({ value: relation.constraintName })}`);
|
|
1468
|
+
}
|
|
789
1469
|
return ` ${name} ${targetModel}${optional} @relation(${args.join(", ")})`;
|
|
790
1470
|
}
|
|
791
1471
|
function formatIndex(params) {
|
|
792
1472
|
const { index } = params;
|
|
793
1473
|
const columns = index.columns.join(", ");
|
|
794
|
-
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 })}`;
|
|
1483
|
+
const mapArg = index.map === undefined ? "" : `, map: ${encodePrismaString({ value: index.map })}`;
|
|
795
1484
|
const nndArg = index.nullsNotDistinct === true ? ", nullsNotDistinct: true" : "";
|
|
796
1485
|
const nameArg = index.clientName === undefined ? "" : `, name: ${encodePrismaString({ value: index.clientName })}`;
|
|
797
1486
|
if (index.isUnique) {
|
|
798
|
-
return ` @@unique([${columns}]${nameArg}${nndArg}${whereArg})`;
|
|
1487
|
+
return ` @@unique([${columns}]${nameArg}${mapArg}${nndArg}${whereArg})`;
|
|
799
1488
|
}
|
|
800
1489
|
const typeArg = index.indexType === undefined ? "" : `, type: ${index.indexType}`;
|
|
801
|
-
return ` @@index([${columns}]${typeArg}${whereArg})`;
|
|
1490
|
+
return ` @@index([${columns}]${typeArg}${mapArg}${whereArg})`;
|
|
802
1491
|
}
|
|
803
1492
|
function formatExtensionValue(params) {
|
|
804
1493
|
const { value } = params;
|
|
@@ -829,9 +1518,14 @@ function formatModel(params) {
|
|
|
829
1518
|
lines.push(" //> Indexes");
|
|
830
1519
|
lines.push("");
|
|
831
1520
|
for (const index of plan.indexes.get(model.__name) ?? []) {
|
|
832
|
-
lines.push(formatIndex({ index }));
|
|
833
|
-
}
|
|
834
|
-
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
|
+
})) {
|
|
835
1529
|
lines.push(` @@check(${encodePrismaString({ value: check.expression })}, name: ${encodePrismaString({ value: check.name })})`);
|
|
836
1530
|
}
|
|
837
1531
|
for (const use of model.__extensions ?? []) {
|
|
@@ -863,17 +1557,22 @@ function formatEnum(params) {
|
|
|
863
1557
|
return [`enum ${name} {`, ...values.map((value) => ` ${value}`), "}"].join(`
|
|
864
1558
|
`);
|
|
865
1559
|
}
|
|
866
|
-
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
|
+
}
|
|
867
1566
|
const stringEnums = [...schema.__enumStorage].filter(([, storage]) => storage === "string").map(([name]) => name);
|
|
868
1567
|
if (stringEnums.length > 0) {
|
|
869
|
-
throw new
|
|
1568
|
+
throw new VibeError5({
|
|
870
1569
|
code: "VIBE_CONFIG",
|
|
871
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.`,
|
|
872
1571
|
meta: { enums: stringEnums, target: "prisma" }
|
|
873
1572
|
});
|
|
874
1573
|
}
|
|
875
1574
|
if (schema.__rls !== undefined) {
|
|
876
|
-
throw new
|
|
1575
|
+
throw new VibeError5({
|
|
877
1576
|
code: "VIBE_CONFIG",
|
|
878
1577
|
message: "Cannot print to Prisma: this schema uses native RLS, which .prisma cannot express losslessly. Keep the DSL as the source.",
|
|
879
1578
|
meta: { feature: "nativeRowLevelSecurity", target: "prisma" }
|
|
@@ -881,6 +1580,8 @@ function generatePrisma(schema, _config = {}) {
|
|
|
881
1580
|
}
|
|
882
1581
|
const plan = planSchema({ schema });
|
|
883
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] }));
|
|
884
1585
|
if (schema.__enums.size > 0) {
|
|
885
1586
|
sections.push("// ============================================");
|
|
886
1587
|
sections.push("// Enums");
|
|
@@ -896,724 +1597,464 @@ function generatePrisma(schema, _config = {}) {
|
|
|
896
1597
|
sections.push("// ============================================");
|
|
897
1598
|
sections.push("");
|
|
898
1599
|
for (const model of schema.__models.values()) {
|
|
899
|
-
sections.push(formatModel({ model, plan }));
|
|
1600
|
+
sections.push(formatModel({ model, plan, dialect, enums }));
|
|
900
1601
|
sections.push("");
|
|
901
1602
|
}
|
|
902
1603
|
return sections.join(`
|
|
903
1604
|
`).trim();
|
|
904
1605
|
}
|
|
905
|
-
|
|
906
|
-
// packages/dsl/src/relations.ts
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
return {
|
|
910
|
-
__type: "columnRef",
|
|
911
|
-
__model: model,
|
|
912
|
-
__column: column,
|
|
913
|
-
through(junction) {
|
|
914
|
-
return {
|
|
915
|
-
__type: "throughRef",
|
|
916
|
-
__model: model,
|
|
917
|
-
__column: column,
|
|
918
|
-
__junction: junction.__model,
|
|
919
|
-
__junctionColumn: junction.__column
|
|
920
|
-
};
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
}
|
|
924
|
-
function extractRefInfo(params) {
|
|
925
|
-
const { ref } = params;
|
|
926
|
-
return { model: ref.__model, column: ref.__column };
|
|
927
|
-
}
|
|
928
|
-
function createRelationFactory(params) {
|
|
929
|
-
const { relationType, targetModelName } = params;
|
|
930
|
-
return (config) => {
|
|
931
|
-
const from = extractRefInfo({ ref: config.from });
|
|
932
|
-
const to = extractRefInfo({ ref: config.to });
|
|
933
|
-
let throughJunction;
|
|
934
|
-
let throughFromCol;
|
|
935
|
-
let throughToCol;
|
|
936
|
-
if (config.from.__type === "throughRef") {
|
|
937
|
-
throughJunction = config.from.__junction;
|
|
938
|
-
throughFromCol = config.from.__junctionColumn;
|
|
939
|
-
}
|
|
940
|
-
if (config.to.__type === "throughRef") {
|
|
941
|
-
throughJunction = throughJunction ?? config.to.__junction;
|
|
942
|
-
throughToCol = config.to.__junctionColumn;
|
|
943
|
-
}
|
|
944
|
-
return {
|
|
945
|
-
__type: "storedRelation",
|
|
946
|
-
__relationType: relationType,
|
|
947
|
-
__targetModel: targetModelName,
|
|
948
|
-
__fromModel: from.model,
|
|
949
|
-
__fromColumn: from.column,
|
|
950
|
-
__toModel: to.model,
|
|
951
|
-
__toColumn: to.column,
|
|
952
|
-
__throughJunction: throughJunction,
|
|
953
|
-
__throughFromCol: throughFromCol,
|
|
954
|
-
__throughToCol: throughToCol,
|
|
955
|
-
__optional: config.optional ?? relationType === "many",
|
|
956
|
-
__alias: config.alias,
|
|
957
|
-
__onDelete: config.onDelete,
|
|
958
|
-
__onUpdate: config.onUpdate
|
|
959
|
-
};
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
function createRelationsBuilder(params) {
|
|
963
|
-
const { models } = params;
|
|
964
|
-
const builder = {};
|
|
965
|
-
const oneObj = {};
|
|
966
|
-
const manyObj = {};
|
|
967
|
-
for (const modelName of Object.keys(models)) {
|
|
968
|
-
oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
|
|
969
|
-
manyObj[modelName] = createRelationFactory({
|
|
970
|
-
relationType: "many",
|
|
971
|
-
targetModelName: modelName
|
|
972
|
-
});
|
|
973
|
-
const model = models[modelName];
|
|
974
|
-
if (model === undefined)
|
|
975
|
-
continue;
|
|
976
|
-
const columnRefs = {};
|
|
977
|
-
for (const columnName of model.__columns.keys()) {
|
|
978
|
-
columnRefs[columnName] = createColumnRef({ model: modelName, column: columnName });
|
|
979
|
-
}
|
|
980
|
-
builder[modelName] = columnRefs;
|
|
981
|
-
}
|
|
982
|
-
builder.one = oneObj;
|
|
983
|
-
builder.many = manyObj;
|
|
984
|
-
return builder;
|
|
985
|
-
}
|
|
986
|
-
function processRelationsOutput(params) {
|
|
987
|
-
const result = {};
|
|
988
|
-
for (const [modelName, relations] of Object.entries(params.output)) {
|
|
989
|
-
if (relations) {
|
|
990
|
-
result[modelName] = relations;
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
return result;
|
|
994
|
-
}
|
|
995
|
-
function defineRelations(models, callback) {
|
|
996
|
-
const builder = createRelationsBuilder({ models });
|
|
997
|
-
return processRelationsOutput({ output: callback(builder) });
|
|
998
|
-
}
|
|
999
|
-
function defineRelationsPart(models, callback) {
|
|
1000
|
-
const builder = createRelationsBuilder({ models });
|
|
1001
|
-
return processRelationsOutput({ output: callback(builder) });
|
|
1002
|
-
}
|
|
1003
|
-
function mergeRelations(...parts) {
|
|
1004
|
-
const result = {};
|
|
1005
|
-
for (const part of parts) {
|
|
1006
|
-
for (const [modelName, relations] of Object.entries(part)) {
|
|
1007
|
-
const existing = result[modelName] ?? {};
|
|
1008
|
-
result[modelName] = { ...existing, ...relations };
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
return result;
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
// packages/dsl/src/schema.ts
|
|
1015
|
-
import { VibeError as VibeError2, validateRlsContextIR } from "@vibeorm/schema";
|
|
1016
|
-
function sameValues(left, right) {
|
|
1017
|
-
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1018
|
-
}
|
|
1019
|
-
function registerEnum(params) {
|
|
1020
|
-
const { registry, name, entry, origin } = params;
|
|
1021
|
-
const existing = registry.get(name);
|
|
1022
|
-
if (existing === undefined) {
|
|
1023
|
-
registry.set(name, entry);
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const valuesDiffer = !sameValues(existing.values, entry.values);
|
|
1027
|
-
const storageDiffers = existing.storage !== entry.storage;
|
|
1028
|
-
if (!valuesDiffer && !storageDiffers)
|
|
1029
|
-
return;
|
|
1030
|
-
const detail = [
|
|
1031
|
-
...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
|
|
1032
|
-
...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
|
|
1033
|
-
].join("; ");
|
|
1034
|
-
throw new VibeError2({
|
|
1035
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1036
|
-
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`,
|
|
1037
|
-
meta: {
|
|
1038
|
-
issues: [
|
|
1039
|
-
{
|
|
1040
|
-
rule: "enum-conflict",
|
|
1041
|
-
message: `Enum "${name}" reused with conflicting ${valuesDiffer ? "values" : "storage"} (${origin})`
|
|
1042
|
-
}
|
|
1043
|
-
],
|
|
1044
|
-
enum: name,
|
|
1045
|
-
existing,
|
|
1046
|
-
incoming: entry
|
|
1047
|
-
}
|
|
1048
|
-
});
|
|
1049
|
-
}
|
|
1050
|
-
function isModelDef(value) {
|
|
1051
|
-
return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
|
|
1052
|
-
}
|
|
1053
|
-
function isEnumDef(value) {
|
|
1054
|
-
return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
|
|
1055
|
-
}
|
|
1056
|
-
function addModelTree(params) {
|
|
1057
|
-
const { model, models, origin } = params;
|
|
1058
|
-
const existing = models.get(model.__name);
|
|
1059
|
-
if (existing !== undefined && existing !== model) {
|
|
1060
|
-
throw new VibeError2({
|
|
1061
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1062
|
-
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`,
|
|
1063
|
-
meta: {
|
|
1064
|
-
issues: [
|
|
1065
|
-
{
|
|
1066
|
-
rule: "model-conflict",
|
|
1067
|
-
message: `Model "${model.__name}" reused with a different definition (${origin})`,
|
|
1068
|
-
model: model.__name
|
|
1069
|
-
}
|
|
1070
|
-
],
|
|
1071
|
-
model: model.__name
|
|
1072
|
-
}
|
|
1073
|
-
});
|
|
1074
|
-
}
|
|
1075
|
-
models.set(model.__name, model);
|
|
1076
|
-
for (const childModel of model.__children.values()) {
|
|
1077
|
-
addModelTree({ model: childModel, models, origin: `inline child of ${model.__name}` });
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
function extractModelsAndEnums(params) {
|
|
1081
|
-
const { input, models, enums } = params;
|
|
1082
|
-
for (const value of Object.values(input)) {
|
|
1083
|
-
if (isModelDef(value)) {
|
|
1084
|
-
addModelTree({ model: value, models, origin: "listed in dsl.schema()" });
|
|
1085
|
-
} else if (isEnumDef(value)) {
|
|
1086
|
-
registerEnum({
|
|
1087
|
-
registry: enums,
|
|
1088
|
-
name: value.__name,
|
|
1089
|
-
entry: { values: value.__options, storage: value.__storage },
|
|
1090
|
-
origin: "standalone enum listed in dsl.schema()"
|
|
1091
|
-
});
|
|
1092
|
-
} else if (typeof value === "object" && value !== null) {
|
|
1093
|
-
extractModelsAndEnums({ input: value, models, enums });
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
function resolveEnums(params) {
|
|
1098
|
-
const { models, enums } = params;
|
|
1099
|
-
for (const [modelName, model] of models) {
|
|
1100
|
-
for (const [columnName, column] of model.__columns) {
|
|
1101
|
-
if (column.__columnType !== "enum" || column.__enumValues === undefined)
|
|
1102
|
-
continue;
|
|
1103
|
-
const entry = {
|
|
1104
|
-
values: column.__enumValues,
|
|
1105
|
-
storage: column.__enumStorage ?? "database"
|
|
1106
|
-
};
|
|
1107
|
-
if (column.__enumName !== undefined) {
|
|
1108
|
-
registerEnum({
|
|
1109
|
-
registry: enums,
|
|
1110
|
-
name: column.__enumName,
|
|
1111
|
-
entry,
|
|
1112
|
-
origin: `standalone enum used by ${modelName}.${columnName}`
|
|
1113
|
-
});
|
|
1114
|
-
continue;
|
|
1115
|
-
}
|
|
1116
|
-
const name = inlineEnumName({ modelName, columnName });
|
|
1117
|
-
registerEnum({
|
|
1118
|
-
registry: enums,
|
|
1119
|
-
name,
|
|
1120
|
-
entry,
|
|
1121
|
-
origin: `inline enum on ${modelName}.${columnName}, named ${name}`
|
|
1122
|
-
});
|
|
1123
|
-
column.__enumName = name;
|
|
1124
|
-
}
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
var RLS_CONTEXT_TYPES = {
|
|
1606
|
+
|
|
1607
|
+
// packages/dsl/src/relations.ts
|
|
1608
|
+
import { VibeError as VibeError6 } from "@vibeorm/schema";
|
|
1609
|
+
var REF_KIND_BY_COLUMN_TYPE = {
|
|
1128
1610
|
string: "String",
|
|
1129
|
-
boolean: "Boolean",
|
|
1130
1611
|
int: "Int",
|
|
1131
1612
|
bigint: "BigInt",
|
|
1132
1613
|
float: "Float",
|
|
1133
1614
|
decimal: "Decimal",
|
|
1134
|
-
|
|
1615
|
+
boolean: "Boolean",
|
|
1616
|
+
datetime: "DateTime",
|
|
1617
|
+
json: "Json",
|
|
1618
|
+
bytes: "Bytes",
|
|
1619
|
+
id: "Id",
|
|
1620
|
+
enum: "Enum",
|
|
1621
|
+
unsupported: "Unsupported"
|
|
1135
1622
|
};
|
|
1136
|
-
function
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1623
|
+
function createColumnRef(params) {
|
|
1624
|
+
const { model, column, kind } = params;
|
|
1625
|
+
return {
|
|
1626
|
+
__type: "columnRef",
|
|
1627
|
+
__model: model,
|
|
1628
|
+
__column: column,
|
|
1629
|
+
__kind: kind,
|
|
1630
|
+
through(junction) {
|
|
1631
|
+
return {
|
|
1632
|
+
__type: "throughRef",
|
|
1633
|
+
__model: model,
|
|
1634
|
+
__column: column,
|
|
1635
|
+
__junction: junction.__model,
|
|
1636
|
+
__junctionColumn: junction.__column
|
|
1637
|
+
};
|
|
1147
1638
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
function extractRefInfo(params) {
|
|
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
|
+
});
|
|
1150
1654
|
}
|
|
1151
|
-
const
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
+
});
|
|
1155
1662
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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
|
+
});
|
|
1159
1669
|
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1670
|
+
return { model: members[0].__model, columns: members.map((member) => member.__column) };
|
|
1671
|
+
}
|
|
1672
|
+
function createRelationFactory(params) {
|
|
1673
|
+
const { relationType, targetModelName } = params;
|
|
1674
|
+
return (config) => {
|
|
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
|
+
});
|
|
1166
1683
|
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1684
|
+
let throughJunction;
|
|
1685
|
+
let throughFromCol;
|
|
1686
|
+
let throughToCol;
|
|
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;
|
|
1170
1692
|
}
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1693
|
+
if (toRef2?.__type === "throughRef") {
|
|
1694
|
+
throughJunction = throughJunction ?? toRef2.__junction;
|
|
1695
|
+
throughToCol = toRef2.__junctionColumn;
|
|
1696
|
+
}
|
|
1697
|
+
return {
|
|
1698
|
+
__type: "storedRelation",
|
|
1699
|
+
__relationType: relationType,
|
|
1700
|
+
__targetModel: targetModelName,
|
|
1701
|
+
__fromModel: from.model,
|
|
1702
|
+
__fromColumn: from.columns[0],
|
|
1703
|
+
__fromColumns: from.columns,
|
|
1704
|
+
__toModel: to.model,
|
|
1705
|
+
__toColumn: to.columns[0],
|
|
1706
|
+
__toColumns: to.columns,
|
|
1707
|
+
__throughJunction: throughJunction,
|
|
1708
|
+
__throughFromCol: throughFromCol,
|
|
1709
|
+
__throughToCol: throughToCol,
|
|
1710
|
+
__optional: config.optional ?? relationType === "many",
|
|
1711
|
+
__alias: config.alias,
|
|
1712
|
+
...config.constraintName === undefined ? {} : { __constraintName: config.constraintName },
|
|
1713
|
+
..."ownsForeignKey" in config ? { __ownsForeignKey: config.ownsForeignKey } : {},
|
|
1714
|
+
__onDelete: config.onDelete,
|
|
1715
|
+
__onUpdate: config.onUpdate
|
|
1716
|
+
};
|
|
1188
1717
|
};
|
|
1189
1718
|
}
|
|
1190
|
-
function
|
|
1191
|
-
const {
|
|
1192
|
-
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
},
|
|
1214
|
-
get createdAt() {
|
|
1215
|
-
return datetime({ default: "now", tz: true, index: true });
|
|
1216
|
-
},
|
|
1217
|
-
get updatedAt() {
|
|
1218
|
-
return datetime({ default: "now", updatedAt: true, tz: true, index: true });
|
|
1219
|
-
},
|
|
1220
|
-
get deletedAt() {
|
|
1221
|
-
return datetime({ optional: true, index: true });
|
|
1222
|
-
},
|
|
1223
|
-
get workspaceId() {
|
|
1224
|
-
return string({ index: true });
|
|
1225
|
-
}
|
|
1226
|
-
};
|
|
1227
|
-
var withMetadata = {
|
|
1228
|
-
get metadata() {
|
|
1229
|
-
return json({ optional: true });
|
|
1230
|
-
},
|
|
1231
|
-
get settings() {
|
|
1232
|
-
return json({ optional: true });
|
|
1233
|
-
}
|
|
1234
|
-
};
|
|
1235
|
-
var withActive = {
|
|
1236
|
-
get isActive() {
|
|
1237
|
-
return boolean({ default: true });
|
|
1238
|
-
}
|
|
1239
|
-
};
|
|
1240
|
-
var withAudit = {
|
|
1241
|
-
get createdById() {
|
|
1242
|
-
return string({ optional: true, index: true });
|
|
1243
|
-
},
|
|
1244
|
-
get updatedById() {
|
|
1245
|
-
return string({ optional: true, index: true });
|
|
1246
|
-
}
|
|
1247
|
-
};
|
|
1248
|
-
var withExternalId = {
|
|
1249
|
-
get externalId() {
|
|
1250
|
-
return string({ index: true });
|
|
1251
|
-
}
|
|
1252
|
-
};
|
|
1253
|
-
var withNameDescription = {
|
|
1254
|
-
get name() {
|
|
1255
|
-
return string();
|
|
1256
|
-
},
|
|
1257
|
-
get description() {
|
|
1258
|
-
return string({ optional: true });
|
|
1259
|
-
}
|
|
1260
|
-
};
|
|
1261
|
-
var fields = {
|
|
1262
|
-
get datetime() {
|
|
1263
|
-
return datetime({ default: "now", updatedAt: true, tz: true, index: true });
|
|
1264
|
-
},
|
|
1265
|
-
get id() {
|
|
1266
|
-
return id({ type: "ulid" });
|
|
1267
|
-
},
|
|
1268
|
-
get title() {
|
|
1269
|
-
return string();
|
|
1270
|
-
},
|
|
1271
|
-
get description() {
|
|
1272
|
-
return string({ optional: true });
|
|
1273
|
-
},
|
|
1274
|
-
get externalId() {
|
|
1275
|
-
return string({ index: true });
|
|
1276
|
-
},
|
|
1277
|
-
get metadata() {
|
|
1278
|
-
return json({ optional: true });
|
|
1279
|
-
},
|
|
1280
|
-
get settings() {
|
|
1281
|
-
return json({ optional: true });
|
|
1282
|
-
},
|
|
1283
|
-
get isActive() {
|
|
1284
|
-
return boolean({ default: true });
|
|
1285
|
-
}
|
|
1286
|
-
};
|
|
1287
|
-
var templates = {
|
|
1288
|
-
base,
|
|
1289
|
-
baseWithWorkspace,
|
|
1290
|
-
withMetadata,
|
|
1291
|
-
withActive,
|
|
1292
|
-
withAudit,
|
|
1293
|
-
withExternalId,
|
|
1294
|
-
withNameDescription,
|
|
1295
|
-
fields
|
|
1296
|
-
};
|
|
1297
|
-
|
|
1298
|
-
// packages/dsl/src/to-ir.ts
|
|
1299
|
-
import {
|
|
1300
|
-
EMPTY_EXTENSION_REGISTRY,
|
|
1301
|
-
VibeError as VibeError3,
|
|
1302
|
-
assertValidSchemaIR,
|
|
1303
|
-
exactIntegerLiteral,
|
|
1304
|
-
mergeExtensionDeclarations
|
|
1305
|
-
} from "@vibeorm/schema";
|
|
1306
|
-
function fieldType(params) {
|
|
1307
|
-
const { column } = params;
|
|
1308
|
-
switch (column.__columnType) {
|
|
1309
|
-
case "string":
|
|
1310
|
-
return "String";
|
|
1311
|
-
case "int":
|
|
1312
|
-
return "Int";
|
|
1313
|
-
case "bigint":
|
|
1314
|
-
return "BigInt";
|
|
1315
|
-
case "float":
|
|
1316
|
-
return "Float";
|
|
1317
|
-
case "decimal":
|
|
1318
|
-
return "Decimal";
|
|
1319
|
-
case "boolean":
|
|
1320
|
-
return "Boolean";
|
|
1321
|
-
case "datetime":
|
|
1322
|
-
return "DateTime";
|
|
1323
|
-
case "json":
|
|
1324
|
-
return "Json";
|
|
1325
|
-
case "bytes":
|
|
1326
|
-
return "Bytes";
|
|
1327
|
-
case "id":
|
|
1328
|
-
return column.__config.type === "autoincrement" ? "Int" : "String";
|
|
1329
|
-
case "enum":
|
|
1330
|
-
return column.__enumName === undefined ? "String" : { enum: column.__enumName };
|
|
1331
|
-
case "unsupported":
|
|
1332
|
-
return "String";
|
|
1333
|
-
}
|
|
1334
|
-
}
|
|
1335
|
-
function idDefault(params) {
|
|
1336
|
-
switch (params.column.__config.type) {
|
|
1337
|
-
case "ulid":
|
|
1338
|
-
return { kind: "ulid" };
|
|
1339
|
-
case "cuid":
|
|
1340
|
-
return { kind: "cuid" };
|
|
1341
|
-
case "uuid":
|
|
1342
|
-
return { kind: "uuid" };
|
|
1343
|
-
case "nanoid":
|
|
1344
|
-
return { kind: "nanoid" };
|
|
1345
|
-
case "autoincrement":
|
|
1346
|
-
return { kind: "autoincrement" };
|
|
1347
|
-
default:
|
|
1348
|
-
return;
|
|
1719
|
+
function createRelationsBuilder(params) {
|
|
1720
|
+
const { models } = params;
|
|
1721
|
+
const builder = {};
|
|
1722
|
+
const oneObj = {};
|
|
1723
|
+
const manyObj = {};
|
|
1724
|
+
for (const modelName of Object.keys(models)) {
|
|
1725
|
+
oneObj[modelName] = createRelationFactory({ relationType: "one", targetModelName: modelName });
|
|
1726
|
+
manyObj[modelName] = createRelationFactory({
|
|
1727
|
+
relationType: "many",
|
|
1728
|
+
targetModelName: modelName
|
|
1729
|
+
});
|
|
1730
|
+
const model = models[modelName];
|
|
1731
|
+
if (model === undefined)
|
|
1732
|
+
continue;
|
|
1733
|
+
const columnRefs = {};
|
|
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
|
+
});
|
|
1740
|
+
}
|
|
1741
|
+
builder[modelName] = columnRefs;
|
|
1349
1742
|
}
|
|
1743
|
+
builder.one = oneObj;
|
|
1744
|
+
builder.many = manyObj;
|
|
1745
|
+
return builder;
|
|
1350
1746
|
}
|
|
1351
|
-
function
|
|
1352
|
-
const
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
return { kind: "dbgenerated", sql: dbgenerated };
|
|
1358
|
-
if (value === undefined)
|
|
1359
|
-
return;
|
|
1360
|
-
if (column.__columnType === "datetime" && value === "now")
|
|
1361
|
-
return { kind: "now" };
|
|
1362
|
-
if (column.__columnType === "enum" && typeof value === "string") {
|
|
1363
|
-
return { kind: "enumValue", value };
|
|
1364
|
-
}
|
|
1365
|
-
if (typeof value === "string" || typeof value === "boolean" || typeof value === "number") {
|
|
1366
|
-
return { kind: "literal", value };
|
|
1747
|
+
function processRelationsOutput(params) {
|
|
1748
|
+
const result = {};
|
|
1749
|
+
for (const [modelName, relations] of Object.entries(params.output)) {
|
|
1750
|
+
if (relations) {
|
|
1751
|
+
result[modelName] = relations;
|
|
1752
|
+
}
|
|
1367
1753
|
}
|
|
1368
|
-
|
|
1369
|
-
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
1370
|
-
return;
|
|
1754
|
+
return result;
|
|
1371
1755
|
}
|
|
1372
|
-
function
|
|
1373
|
-
const {
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
}
|
|
1756
|
+
function defineRelations(models, callback) {
|
|
1757
|
+
const builder = createRelationsBuilder({ models });
|
|
1758
|
+
return processRelationsOutput({ output: callback(builder) });
|
|
1759
|
+
}
|
|
1760
|
+
function defineRelationsPart(models, callback) {
|
|
1761
|
+
const builder = createRelationsBuilder({ models });
|
|
1762
|
+
return processRelationsOutput({ output: callback(builder) });
|
|
1763
|
+
}
|
|
1764
|
+
function mergeRelations(...parts) {
|
|
1765
|
+
const result = {};
|
|
1766
|
+
for (const part of parts) {
|
|
1767
|
+
for (const [modelName, relations] of Object.entries(part)) {
|
|
1768
|
+
const existing = result[modelName] ?? {};
|
|
1769
|
+
result[modelName] = { ...existing, ...relations };
|
|
1385
1770
|
}
|
|
1386
1771
|
}
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1772
|
+
return result;
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// packages/dsl/src/schema.ts
|
|
1776
|
+
import { VibeError as VibeError7, validateRlsContextIR } from "@vibeorm/schema";
|
|
1777
|
+
function sameValues(left, right) {
|
|
1778
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1779
|
+
}
|
|
1780
|
+
function registerEnum(params) {
|
|
1781
|
+
const { registry, name, entry, origin } = params;
|
|
1782
|
+
const existing = registry.get(name);
|
|
1783
|
+
if (existing === undefined) {
|
|
1784
|
+
registry.set(name, entry);
|
|
1393
1785
|
return;
|
|
1394
1786
|
}
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
return `Decimal(${precision}, ${scale ?? 0})`;
|
|
1787
|
+
const valuesDiffer = !sameValues(existing.values, entry.values);
|
|
1788
|
+
const storageDiffers = existing.storage !== entry.storage;
|
|
1789
|
+
if (!valuesDiffer && !storageDiffers)
|
|
1399
1790
|
return;
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1791
|
+
const detail = [
|
|
1792
|
+
...valuesDiffer ? [`values [${existing.values.join(", ")}] vs [${entry.values.join(", ")}]`] : [],
|
|
1793
|
+
...storageDiffers ? [`storage "${existing.storage}" vs "${entry.storage}"`] : []
|
|
1794
|
+
].join("; ");
|
|
1795
|
+
throw new VibeError7({
|
|
1796
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
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`,
|
|
1798
|
+
meta: {
|
|
1799
|
+
issues: [
|
|
1800
|
+
{
|
|
1801
|
+
rule: "enum-conflict",
|
|
1802
|
+
message: `Enum "${name}" reused with conflicting ${valuesDiffer ? "values" : "storage"} (${origin})`
|
|
1803
|
+
}
|
|
1804
|
+
],
|
|
1805
|
+
enum: name,
|
|
1806
|
+
existing,
|
|
1807
|
+
incoming: entry
|
|
1808
|
+
}
|
|
1809
|
+
});
|
|
1408
1810
|
}
|
|
1409
|
-
function
|
|
1410
|
-
|
|
1411
|
-
const config = column.__config;
|
|
1412
|
-
const isList = column.__columnType !== "id" && config.array === true;
|
|
1413
|
-
const dbName = typeof config.map === "string" ? config.map : undefined;
|
|
1414
|
-
const documentation = typeof config.comment === "string" ? config.comment : undefined;
|
|
1415
|
-
const collation = typeof config.collation === "string" ? config.collation : undefined;
|
|
1416
|
-
const defaultValue = fieldDefault({ column });
|
|
1417
|
-
const native = extensionNativeType ?? nativeType({ column });
|
|
1418
|
-
return {
|
|
1419
|
-
name,
|
|
1420
|
-
type: fieldType({ column }),
|
|
1421
|
-
isList,
|
|
1422
|
-
isOptional: config.optional === true,
|
|
1423
|
-
isId: column.__columnType === "id",
|
|
1424
|
-
isUnique: config.unique === true,
|
|
1425
|
-
isUpdatedAt: column.__columnType === "datetime" && config.updatedAt === true,
|
|
1426
|
-
...collation === undefined ? {} : { collation },
|
|
1427
|
-
...dbName === undefined ? {} : { dbName },
|
|
1428
|
-
...defaultValue === undefined ? {} : { default: defaultValue },
|
|
1429
|
-
...native === undefined ? {} : { nativeType: native },
|
|
1430
|
-
...column.__columnType === "unsupported" ? { isUnsupported: true } : {},
|
|
1431
|
-
...documentation === undefined ? {} : { documentation }
|
|
1432
|
-
};
|
|
1811
|
+
function isModelDef(value) {
|
|
1812
|
+
return typeof value === "object" && value !== null && "__type" in value && value.__type === "model";
|
|
1433
1813
|
}
|
|
1434
|
-
function
|
|
1435
|
-
|
|
1436
|
-
return {
|
|
1437
|
-
name: relation.name,
|
|
1438
|
-
model: relation.model,
|
|
1439
|
-
targetModel: relation.targetModel,
|
|
1440
|
-
type: resolveRelationType({ relation, plan }),
|
|
1441
|
-
isList: relation.isList,
|
|
1442
|
-
isOptional: relation.isOptional,
|
|
1443
|
-
...relation.relationName === undefined ? {} : { relationName: relation.relationName },
|
|
1444
|
-
...relation.fields === undefined ? {} : { fields: [...relation.fields] },
|
|
1445
|
-
...relation.references === undefined ? {} : { references: [...relation.references] },
|
|
1446
|
-
...relation.onDelete === undefined ? {} : { onDelete: relation.onDelete },
|
|
1447
|
-
...relation.onUpdate === undefined ? {} : { onUpdate: relation.onUpdate }
|
|
1448
|
-
};
|
|
1814
|
+
function isEnumDef(value) {
|
|
1815
|
+
return typeof value === "object" && value !== null && "__type" in value && value.__type === "enum";
|
|
1449
1816
|
}
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1817
|
+
function addModelTree(params) {
|
|
1818
|
+
const { model, models, origin } = params;
|
|
1819
|
+
const existing = models.get(model.__name);
|
|
1820
|
+
if (existing !== undefined && existing !== model) {
|
|
1821
|
+
throw new VibeError7({
|
|
1822
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
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`,
|
|
1824
|
+
meta: {
|
|
1825
|
+
issues: [
|
|
1826
|
+
{
|
|
1827
|
+
rule: "model-conflict",
|
|
1828
|
+
message: `Model "${model.__name}" reused with a different definition (${origin})`,
|
|
1829
|
+
model: model.__name
|
|
1830
|
+
}
|
|
1831
|
+
],
|
|
1832
|
+
model: model.__name
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
models.set(model.__name, model);
|
|
1837
|
+
for (const childModel of model.__children.values()) {
|
|
1838
|
+
addModelTree({ model: childModel, models, origin: `inline child of ${model.__name}` });
|
|
1839
|
+
}
|
|
1468
1840
|
}
|
|
1469
|
-
function
|
|
1470
|
-
const {
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
const registered = extensions.fieldTypes.get(extensionTypeName);
|
|
1481
|
-
if (registered === undefined) {
|
|
1482
|
-
issues.push({
|
|
1483
|
-
rule: "extension-attribute",
|
|
1484
|
-
message: `Field "${model.__name}.${name}" uses extension type "${extensionTypeName}" but no configured extension provides it`,
|
|
1485
|
-
model: model.__name,
|
|
1486
|
-
field: name
|
|
1841
|
+
function extractModelsAndEnums(params) {
|
|
1842
|
+
const { input, models, enums } = params;
|
|
1843
|
+
for (const value of Object.values(input)) {
|
|
1844
|
+
if (isModelDef(value)) {
|
|
1845
|
+
addModelTree({ model: value, models, origin: "listed in dsl.schema()" });
|
|
1846
|
+
} else if (isEnumDef(value)) {
|
|
1847
|
+
registerEnum({
|
|
1848
|
+
registry: enums,
|
|
1849
|
+
name: value.__name,
|
|
1850
|
+
entry: { values: value.__options, storage: value.__storage },
|
|
1851
|
+
origin: "standalone enum listed in dsl.schema()"
|
|
1487
1852
|
});
|
|
1488
|
-
|
|
1853
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1854
|
+
extractModelsAndEnums({ input: value, models, enums });
|
|
1489
1855
|
}
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function resolveEnums(params) {
|
|
1859
|
+
const { models, enums } = params;
|
|
1860
|
+
for (const [modelName, model] of models) {
|
|
1861
|
+
for (const [columnName, column] of model.__columns) {
|
|
1862
|
+
if (column.__columnType !== "enum" || column.__enumValues === undefined)
|
|
1863
|
+
continue;
|
|
1864
|
+
const entry = {
|
|
1865
|
+
values: column.__enumValues,
|
|
1866
|
+
storage: column.__enumStorage ?? "database"
|
|
1867
|
+
};
|
|
1868
|
+
if (column.__enumName !== undefined) {
|
|
1869
|
+
registerEnum({
|
|
1870
|
+
registry: enums,
|
|
1871
|
+
name: column.__enumName,
|
|
1872
|
+
entry,
|
|
1873
|
+
origin: `standalone enum used by ${modelName}.${columnName}`
|
|
1874
|
+
});
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const name = inlineEnumName({ modelName, columnName });
|
|
1878
|
+
registerEnum({
|
|
1879
|
+
registry: enums,
|
|
1880
|
+
name,
|
|
1881
|
+
entry,
|
|
1882
|
+
origin: `inline enum on ${modelName}.${columnName}, named ${name}`
|
|
1502
1883
|
});
|
|
1503
|
-
|
|
1884
|
+
column.__enumName = name;
|
|
1504
1885
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
var RLS_CONTEXT_TYPES = {
|
|
1889
|
+
string: "String",
|
|
1890
|
+
boolean: "Boolean",
|
|
1891
|
+
int: "Int",
|
|
1892
|
+
bigint: "BigInt",
|
|
1893
|
+
float: "Float",
|
|
1894
|
+
decimal: "Decimal",
|
|
1895
|
+
datetime: "DateTime"
|
|
1896
|
+
};
|
|
1897
|
+
function defineRls(params) {
|
|
1898
|
+
if (typeof params.context !== "object" || params.context === null || Array.isArray(params.context)) {
|
|
1899
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context must be a record of scalar declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
1900
|
+
}
|
|
1901
|
+
const entries = [];
|
|
1902
|
+
const refs = [];
|
|
1903
|
+
for (const key of Reflect.ownKeys(params.context)) {
|
|
1904
|
+
const descriptor = Object.getOwnPropertyDescriptor(params.context, key);
|
|
1905
|
+
const column = descriptor?.value;
|
|
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) {
|
|
1907
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS context slots must be named scalar column declarations", meta: { issues: [{ rule: "rls-invalid-context" }] } });
|
|
1512
1908
|
}
|
|
1909
|
+
entries.push([key, { type: RLS_CONTEXT_TYPES[column.__columnType], nullable: "optional" in column.__config && column.__config.optional === true }]);
|
|
1910
|
+
refs.push([key, Object.freeze({ context: key })]);
|
|
1513
1911
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1912
|
+
const context = Object.freeze(Object.fromEntries(entries));
|
|
1913
|
+
const issues = validateRlsContextIR({ context });
|
|
1914
|
+
if (issues.length > 0) {
|
|
1915
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "Invalid RLS context declaration", meta: { issues } });
|
|
1916
|
+
}
|
|
1917
|
+
const policies = params.policies({ context: Object.freeze(Object.fromEntries(refs)) });
|
|
1918
|
+
if (typeof policies !== "object" || policies === null || Array.isArray(policies)) {
|
|
1919
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be a record of model classifications", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
1920
|
+
}
|
|
1921
|
+
const named = [];
|
|
1922
|
+
const classified = new Set;
|
|
1923
|
+
for (const key of Reflect.ownKeys(policies)) {
|
|
1924
|
+
const descriptor = Object.getOwnPropertyDescriptor(policies, key);
|
|
1925
|
+
if (typeof key !== "string" || descriptor?.enumerable !== true || !("value" in descriptor)) {
|
|
1926
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: "RLS policies must be named data properties", meta: { issues: [{ rule: "rls-invalid-rule" }] } });
|
|
1523
1927
|
}
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
named: { ...use.args ?? {} },
|
|
1528
|
-
...use.fields === undefined ? {} : { fields: use.fields }
|
|
1529
|
-
},
|
|
1530
|
-
fieldNames: fields2.map((field) => field.name)
|
|
1531
|
-
});
|
|
1532
|
-
if ("error" in parsed) {
|
|
1533
|
-
issues.push({
|
|
1534
|
-
rule: "extension-attribute",
|
|
1535
|
-
message: `@@${use.attribute} on "${model.__name}": ${parsed.error}`,
|
|
1536
|
-
model: model.__name
|
|
1537
|
-
});
|
|
1538
|
-
continue;
|
|
1928
|
+
const name = params.models[key]?.__name ?? key;
|
|
1929
|
+
if (classified.has(name)) {
|
|
1930
|
+
throw new VibeError7({ code: "VIBE_SCHEMA_INVALID", message: `RLS classifies model "${name}" more than once`, meta: { issues: [{ rule: "rls-duplicate-classification", model: name }] } });
|
|
1539
1931
|
}
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
model: model.__name,
|
|
1543
|
-
declaration: parsed.declaration
|
|
1544
|
-
});
|
|
1932
|
+
classified.add(name);
|
|
1933
|
+
named.push([name, descriptor.value]);
|
|
1545
1934
|
}
|
|
1935
|
+
return { context, policies: Object.fromEntries(named) };
|
|
1936
|
+
}
|
|
1937
|
+
function schema(input, options) {
|
|
1938
|
+
const models = new Map;
|
|
1939
|
+
const enums = new Map;
|
|
1940
|
+
extractModelsAndEnums({ input, models, enums });
|
|
1941
|
+
resolveEnums({ models, enums });
|
|
1546
1942
|
return {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
...
|
|
1553
|
-
...policy === undefined ? {} : { policy },
|
|
1554
|
-
...primaryKey === undefined ? {} : { primaryKey },
|
|
1555
|
-
isView: false,
|
|
1556
|
-
...model.__comment === undefined ? {} : { documentation: model.__comment }
|
|
1943
|
+
__type: "schema",
|
|
1944
|
+
__models: models,
|
|
1945
|
+
__enums: new Map(Array.from(enums, ([name, entry]) => [name, entry.values])),
|
|
1946
|
+
__enumStorage: new Map(Array.from(enums, ([name, entry]) => [name, entry.storage])),
|
|
1947
|
+
__relations: options?.relations ?? {},
|
|
1948
|
+
...options?.rls === undefined ? {} : { __rls: options.rls }
|
|
1557
1949
|
};
|
|
1558
1950
|
}
|
|
1951
|
+
function domain(params) {
|
|
1952
|
+
const { name, models } = params;
|
|
1953
|
+
return { name, models };
|
|
1954
|
+
}
|
|
1559
1955
|
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1956
|
+
// packages/dsl/src/templates.ts
|
|
1957
|
+
var base = {
|
|
1958
|
+
get id() {
|
|
1959
|
+
return id({ type: "ulid" });
|
|
1960
|
+
},
|
|
1961
|
+
get createdAt() {
|
|
1962
|
+
return datetime({ default: "now", tz: true, index: true });
|
|
1963
|
+
},
|
|
1964
|
+
get updatedAt() {
|
|
1965
|
+
return datetime({ default: "now", updatedAt: true, tz: true, index: true });
|
|
1966
|
+
},
|
|
1967
|
+
get deletedAt() {
|
|
1968
|
+
return datetime({ optional: true, index: true });
|
|
1567
1969
|
}
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
})
|
|
1970
|
+
};
|
|
1971
|
+
var baseWithWorkspace = {
|
|
1972
|
+
get id() {
|
|
1973
|
+
return id({ type: "ulid" });
|
|
1974
|
+
},
|
|
1975
|
+
get createdAt() {
|
|
1976
|
+
return datetime({ default: "now", tz: true, index: true });
|
|
1977
|
+
},
|
|
1978
|
+
get updatedAt() {
|
|
1979
|
+
return datetime({ default: "now", updatedAt: true, tz: true, index: true });
|
|
1980
|
+
},
|
|
1981
|
+
get deletedAt() {
|
|
1982
|
+
return datetime({ optional: true, index: true });
|
|
1983
|
+
},
|
|
1984
|
+
get workspaceId() {
|
|
1985
|
+
return string({ index: true });
|
|
1575
1986
|
}
|
|
1576
|
-
}
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
const models = Array.from(schema2.__models.values(), (model) => {
|
|
1584
|
-
const built = buildModel({ model, plan, extensions, declarations, issues });
|
|
1585
|
-
const rls = schema2.__rls?.policies[model.__name];
|
|
1586
|
-
return rls === undefined ? built : { ...built, rls };
|
|
1587
|
-
});
|
|
1588
|
-
if (schema2.__rls !== undefined) {
|
|
1589
|
-
for (const name of Object.keys(schema2.__rls.policies)) {
|
|
1590
|
-
if (!schema2.__models.has(name)) {
|
|
1591
|
-
issues.push({ rule: "rls-unknown-model", model: name, message: `RLS classification references model "${name}" absent from the composed schema` });
|
|
1592
|
-
}
|
|
1593
|
-
}
|
|
1987
|
+
};
|
|
1988
|
+
var withMetadata = {
|
|
1989
|
+
get metadata() {
|
|
1990
|
+
return json({ optional: true });
|
|
1991
|
+
},
|
|
1992
|
+
get settings() {
|
|
1993
|
+
return json({ optional: true });
|
|
1594
1994
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
}));
|
|
1600
|
-
if (issues.length > 0) {
|
|
1601
|
-
throw new VibeError3({
|
|
1602
|
-
code: "VIBE_SCHEMA_INVALID",
|
|
1603
|
-
message: `Schema validation failed with ${issues.length} issue(s): ${issues.map((issue) => issue.message).join("; ")}`,
|
|
1604
|
-
meta: { issues }
|
|
1605
|
-
});
|
|
1995
|
+
};
|
|
1996
|
+
var withActive = {
|
|
1997
|
+
get isActive() {
|
|
1998
|
+
return boolean({ default: true });
|
|
1606
1999
|
}
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
2000
|
+
};
|
|
2001
|
+
var withAudit = {
|
|
2002
|
+
get createdById() {
|
|
2003
|
+
return string({ optional: true, index: true });
|
|
2004
|
+
},
|
|
2005
|
+
get updatedById() {
|
|
2006
|
+
return string({ optional: true, index: true });
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
var withExternalId = {
|
|
2010
|
+
get externalId() {
|
|
2011
|
+
return string({ index: true });
|
|
2012
|
+
}
|
|
2013
|
+
};
|
|
2014
|
+
var withNameDescription = {
|
|
2015
|
+
get name() {
|
|
2016
|
+
return string();
|
|
2017
|
+
},
|
|
2018
|
+
get description() {
|
|
2019
|
+
return string({ optional: true });
|
|
2020
|
+
}
|
|
2021
|
+
};
|
|
2022
|
+
var fields = {
|
|
2023
|
+
get datetime() {
|
|
2024
|
+
return datetime({ default: "now", updatedAt: true, tz: true, index: true });
|
|
2025
|
+
},
|
|
2026
|
+
get id() {
|
|
2027
|
+
return id({ type: "ulid" });
|
|
2028
|
+
},
|
|
2029
|
+
get title() {
|
|
2030
|
+
return string();
|
|
2031
|
+
},
|
|
2032
|
+
get description() {
|
|
2033
|
+
return string({ optional: true });
|
|
2034
|
+
},
|
|
2035
|
+
get externalId() {
|
|
2036
|
+
return string({ index: true });
|
|
2037
|
+
},
|
|
2038
|
+
get metadata() {
|
|
2039
|
+
return json({ optional: true });
|
|
2040
|
+
},
|
|
2041
|
+
get settings() {
|
|
2042
|
+
return json({ optional: true });
|
|
2043
|
+
},
|
|
2044
|
+
get isActive() {
|
|
2045
|
+
return boolean({ default: true });
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
var templates = {
|
|
2049
|
+
base,
|
|
2050
|
+
baseWithWorkspace,
|
|
2051
|
+
withMetadata,
|
|
2052
|
+
withActive,
|
|
2053
|
+
withAudit,
|
|
2054
|
+
withExternalId,
|
|
2055
|
+
withNameDescription,
|
|
2056
|
+
fields
|
|
2057
|
+
};
|
|
1617
2058
|
|
|
1618
2059
|
// packages/dsl/src/validate.ts
|
|
1619
2060
|
function validateSchema(schema2) {
|
|
@@ -1660,18 +2101,30 @@ function validateSchema(schema2) {
|
|
|
1660
2101
|
continue;
|
|
1661
2102
|
}
|
|
1662
2103
|
if (relation.__relationType === "one" && relation.__fromModel === modelName) {
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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
|
+
}
|
|
1670
2123
|
}
|
|
1671
|
-
if (
|
|
2124
|
+
if (relation.__fromColumns.length !== relation.__toColumns.length) {
|
|
1672
2125
|
errors.push({
|
|
1673
2126
|
type: "error",
|
|
1674
|
-
message: `Relation "${relationName}"
|
|
2127
|
+
message: `Relation "${relationName}" pairs ${relation.__fromColumns.length} FK column(s) with ${relation.__toColumns.length} referenced column(s)`,
|
|
1675
2128
|
model: modelName,
|
|
1676
2129
|
relation: relationName
|
|
1677
2130
|
});
|
|
@@ -1758,21 +2211,26 @@ export {
|
|
|
1758
2211
|
generatePrisma,
|
|
1759
2212
|
fields,
|
|
1760
2213
|
evaluateIndexes,
|
|
2214
|
+
evaluateChecks,
|
|
1761
2215
|
dsl,
|
|
1762
2216
|
domain,
|
|
1763
2217
|
detectOneToOnePairs,
|
|
1764
2218
|
defineRls,
|
|
1765
2219
|
defineRelationsPart,
|
|
1766
2220
|
defineRelations,
|
|
2221
|
+
createPredicateBuilder,
|
|
1767
2222
|
createIndexBuilder,
|
|
1768
2223
|
createEnum,
|
|
1769
2224
|
createChildrenBuilder,
|
|
1770
2225
|
create,
|
|
2226
|
+
compileIndexPredicate,
|
|
2227
|
+
compileChecks,
|
|
1771
2228
|
cloneColumnDef,
|
|
1772
2229
|
children,
|
|
1773
2230
|
baseWithWorkspace,
|
|
1774
2231
|
base,
|
|
1775
2232
|
ChainableStringColumn,
|
|
2233
|
+
ChainableIntegerColumn,
|
|
1776
2234
|
ChainableIdColumn,
|
|
1777
2235
|
ChainableEnumColumn,
|
|
1778
2236
|
ChainableDecimalColumn,
|
|
@@ -1780,4 +2238,4 @@ export {
|
|
|
1780
2238
|
ChainableColumn
|
|
1781
2239
|
};
|
|
1782
2240
|
|
|
1783
|
-
//# debugId=
|
|
2241
|
+
//# debugId=13292A89548F16A064756E2164756E21
|