@vibeorm/dsl 2.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/column.d.ts +10 -2
- package/dist/index.js +128 -61
- package/dist/index.js.map +6 -6
- package/dist/to-ir.d.ts +13 -2
- package/dist/types.d.ts +5 -0
- package/package.json +6 -4
- package/dist/column.d.ts.map +0 -1
- package/dist/enum.d.ts.map +0 -1
- package/dist/field-type.d.ts.map +0 -1
- package/dist/generator/index.d.ts.map +0 -1
- package/dist/generator/prisma.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/model.d.ts.map +0 -1
- package/dist/naming.d.ts.map +0 -1
- package/dist/predicate.d.ts.map +0 -1
- package/dist/relation-plan.d.ts.map +0 -1
- package/dist/relation.d.ts.map +0 -1
- package/dist/relations.d.ts.map +0 -1
- package/dist/schema.d.ts.map +0 -1
- package/dist/templates.d.ts.map +0 -1
- package/dist/to-ir.d.ts.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/validate.d.ts.map +0 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 VibeORM contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -69,7 +69,7 @@ Id strategies are `dsl.id({ type: "ulid" | "uuid" | "cuid" | "nanoid" | "autoinc
|
|
|
69
69
|
|
|
70
70
|
On every column: `.optional()`, `.unique()`, `.index()`, `.array()`, `.default(value)`, `.map("db_col")`, `.dbgenerated("expr")`, `.comment("...")`.
|
|
71
71
|
|
|
72
|
-
Kind-specific: `.maxLength(n)` and `.text()` on strings, `.precision(p, s)` on decimals, `.defaultNow()`, `.updatedAt()` and `.tz()` on datetimes.
|
|
72
|
+
Kind-specific: `.maxLength(n)` and `.text()` on strings, `.precision(p, s)` on decimals, `.defaultNow()`, `.updatedAt()` and `.tz()` on datetimes. `.tz({ precision: 3 })` declares `@db.Timestamptz(3)`: it stores exactly what a JavaScript `Date` holds, so a database-written time reads back unchanged and `findPage` can order by the column without an assertion.
|
|
73
73
|
|
|
74
74
|
### Database defaults
|
|
75
75
|
|
package/dist/column.d.ts
CHANGED
|
@@ -77,8 +77,15 @@ export declare class ChainableDecimalColumn extends ChainableColumn<"decimal"> {
|
|
|
77
77
|
/** DateTime column with timestamp-specific helpers. */
|
|
78
78
|
export declare class ChainableDatetimeColumn extends ChainableColumn<"datetime"> {
|
|
79
79
|
constructor(config?: DatetimeColumnConfig);
|
|
80
|
-
/**
|
|
81
|
-
|
|
80
|
+
/**
|
|
81
|
+
* `@db.Timestamptz` — timezone-aware timestamp; `{ precision: n }` declares
|
|
82
|
+
* `@db.Timestamptz(n)` (0–6 fractional-seconds digits). Precision 3 stores
|
|
83
|
+
* exactly what a JavaScript `Date` holds, so a database-written value reads
|
|
84
|
+
* back unchanged and `findPage` may order by it without an assertion.
|
|
85
|
+
*/
|
|
86
|
+
tz(options?: {
|
|
87
|
+
readonly precision?: number;
|
|
88
|
+
}): this;
|
|
82
89
|
/** `@updatedAt` — refreshed on every update. */
|
|
83
90
|
updatedAt(): this;
|
|
84
91
|
/** `@default(now())` */
|
|
@@ -128,6 +135,7 @@ export declare function boolean(config?: BooleanColumnConfig): ChainableColumn<"
|
|
|
128
135
|
*
|
|
129
136
|
* @example
|
|
130
137
|
* dsl.datetime().defaultNow().updatedAt().tz()
|
|
138
|
+
* dsl.datetime().defaultNow().tz({ precision: 3 })
|
|
131
139
|
*/
|
|
132
140
|
export declare function datetime(config?: DatetimeColumnConfig): ChainableDatetimeColumn;
|
|
133
141
|
/** Json column with a consumer-declared payload type (typed JSON). */
|
package/dist/index.js
CHANGED
|
@@ -94,8 +94,11 @@ class ChainableDatetimeColumn extends ChainableColumn {
|
|
|
94
94
|
constructor(config = {}) {
|
|
95
95
|
super("datetime", config);
|
|
96
96
|
}
|
|
97
|
-
tz() {
|
|
97
|
+
tz(options) {
|
|
98
98
|
this.__config.tz = true;
|
|
99
|
+
if (options?.precision !== undefined) {
|
|
100
|
+
this.__config.precision = options.precision;
|
|
101
|
+
}
|
|
99
102
|
return this;
|
|
100
103
|
}
|
|
101
104
|
updatedAt() {
|
|
@@ -606,6 +609,10 @@ function evaluateIndexes(params) {
|
|
|
606
609
|
}
|
|
607
610
|
|
|
608
611
|
// packages/dsl/src/relation-plan.ts
|
|
612
|
+
function relationPairKey(params) {
|
|
613
|
+
const { relation, reverse } = params;
|
|
614
|
+
return JSON.stringify(reverse ? [relation.__toModel, relation.__toColumns, relation.__fromModel, relation.__fromColumns, relation.__alias ?? ""] : [relation.__fromModel, relation.__fromColumns, relation.__toModel, relation.__toColumns, relation.__alias ?? ""]);
|
|
615
|
+
}
|
|
609
616
|
function detectOneToOnePairs(params) {
|
|
610
617
|
const { relations, models } = params;
|
|
611
618
|
const mergeMap = new Map;
|
|
@@ -620,6 +627,15 @@ function detectOneToOnePairs(params) {
|
|
|
620
627
|
}
|
|
621
628
|
}
|
|
622
629
|
}
|
|
630
|
+
const candidatesByKey = new Map;
|
|
631
|
+
for (const [position, entry] of allOnes.entries()) {
|
|
632
|
+
const key = relationPairKey({ relation: entry.rel });
|
|
633
|
+
const bucket = candidatesByKey.get(key);
|
|
634
|
+
if (bucket === undefined)
|
|
635
|
+
candidatesByKey.set(key, { positions: [position], start: 0 });
|
|
636
|
+
else
|
|
637
|
+
bucket.positions.push(position);
|
|
638
|
+
}
|
|
623
639
|
const paired = new Set;
|
|
624
640
|
for (let i = 0;i < allOnes.length; i++) {
|
|
625
641
|
const a = allOnes[i];
|
|
@@ -628,13 +644,17 @@ function detectOneToOnePairs(params) {
|
|
|
628
644
|
const keyA = `${a.modelName}:${a.relName}`;
|
|
629
645
|
if (paired.has(keyA))
|
|
630
646
|
continue;
|
|
631
|
-
|
|
647
|
+
const bucket = candidatesByKey.get(relationPairKey({ relation: a.rel, reverse: true }));
|
|
648
|
+
if (bucket === undefined)
|
|
649
|
+
continue;
|
|
650
|
+
for (let candidate = bucket.start;candidate < bucket.positions.length; candidate++) {
|
|
651
|
+
const j = bucket.positions[candidate];
|
|
632
652
|
const b = allOnes[j];
|
|
633
|
-
if (b === undefined)
|
|
634
|
-
continue;
|
|
635
653
|
const keyB = `${b.modelName}:${b.relName}`;
|
|
636
|
-
if (paired.has(keyB))
|
|
654
|
+
if (j <= i || paired.has(keyB)) {
|
|
655
|
+
bucket.start = candidate + 1;
|
|
637
656
|
continue;
|
|
657
|
+
}
|
|
638
658
|
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 });
|
|
639
659
|
if (!sameColumnPair || (a.rel.__alias ?? "") !== (b.rel.__alias ?? ""))
|
|
640
660
|
continue;
|
|
@@ -662,6 +682,7 @@ function detectOneToOnePairs(params) {
|
|
|
662
682
|
});
|
|
663
683
|
paired.add(fkKey);
|
|
664
684
|
paired.add(backKey);
|
|
685
|
+
bucket.start = candidate + 1;
|
|
665
686
|
break;
|
|
666
687
|
}
|
|
667
688
|
}
|
|
@@ -762,7 +783,7 @@ function planExplicitRelations(params) {
|
|
|
762
783
|
return planned;
|
|
763
784
|
}
|
|
764
785
|
function planMissingReverseRelations(params) {
|
|
765
|
-
const { modelName, relations, mergeMap, existing } = params;
|
|
786
|
+
const { modelName, relations, incoming, mergeMap, existing } = params;
|
|
766
787
|
const planned = [];
|
|
767
788
|
const existingLists = new Set;
|
|
768
789
|
const existingNames = new Set;
|
|
@@ -772,44 +793,40 @@ function planMissingReverseRelations(params) {
|
|
|
772
793
|
existingLists.add(`${relation.targetModel}:${relation.relationName ?? ""}`);
|
|
773
794
|
}
|
|
774
795
|
}
|
|
775
|
-
for (const
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
796
|
+
for (const { modelName: sourceModelName, relName, rel } of incoming) {
|
|
797
|
+
if (mergeMap.has(`${sourceModelName}:${relName}`))
|
|
798
|
+
continue;
|
|
799
|
+
const reverseKey = `${sourceModelName}:${rel.__alias ?? ""}`;
|
|
800
|
+
if (existingLists.has(reverseKey))
|
|
801
|
+
continue;
|
|
802
|
+
if (sourceModelName === modelName) {
|
|
803
|
+
const hasExplicitReverse = Object.values(relations[sourceModelName] ?? {}).some((other) => other.__relationType === "many" && other.__targetModel === modelName && other.__alias === rel.__alias);
|
|
804
|
+
if (hasExplicitReverse)
|
|
783
805
|
continue;
|
|
784
|
-
if (sourceModelName === modelName) {
|
|
785
|
-
const hasExplicitReverse = Object.values(sourceRelations).some((other) => other.__relationType === "many" && other.__targetModel === modelName && other.__alias === rel.__alias);
|
|
786
|
-
if (hasExplicitReverse)
|
|
787
|
-
continue;
|
|
788
|
-
}
|
|
789
|
-
const baseName = rel.__alias ?? sourceModelName;
|
|
790
|
-
let name = baseName;
|
|
791
|
-
let counter = 1;
|
|
792
|
-
while (existingNames.has(name)) {
|
|
793
|
-
counter++;
|
|
794
|
-
name = `${baseName}${counter}`;
|
|
795
|
-
}
|
|
796
|
-
planned.push({
|
|
797
|
-
model: modelName,
|
|
798
|
-
name,
|
|
799
|
-
targetModel: sourceModelName,
|
|
800
|
-
kind: "list",
|
|
801
|
-
isList: true,
|
|
802
|
-
isOptional: false,
|
|
803
|
-
relationName: rel.__alias
|
|
804
|
-
});
|
|
805
|
-
existingLists.add(reverseKey);
|
|
806
|
-
existingNames.add(name);
|
|
807
806
|
}
|
|
807
|
+
const baseName = rel.__alias ?? sourceModelName;
|
|
808
|
+
let name = baseName;
|
|
809
|
+
let counter = 1;
|
|
810
|
+
while (existingNames.has(name)) {
|
|
811
|
+
counter++;
|
|
812
|
+
name = `${baseName}${counter}`;
|
|
813
|
+
}
|
|
814
|
+
planned.push({
|
|
815
|
+
model: modelName,
|
|
816
|
+
name,
|
|
817
|
+
targetModel: sourceModelName,
|
|
818
|
+
kind: "list",
|
|
819
|
+
isList: true,
|
|
820
|
+
isOptional: false,
|
|
821
|
+
relationName: rel.__alias
|
|
822
|
+
});
|
|
823
|
+
existingLists.add(reverseKey);
|
|
824
|
+
existingNames.add(name);
|
|
808
825
|
}
|
|
809
826
|
return planned;
|
|
810
827
|
}
|
|
811
828
|
function planChildRelations(params) {
|
|
812
|
-
const { modelName, model,
|
|
829
|
+
const { modelName, model, parents } = params;
|
|
813
830
|
const planned = [];
|
|
814
831
|
for (const [childKey, childModel] of model.__children) {
|
|
815
832
|
planned.push({
|
|
@@ -821,22 +838,18 @@ function planChildRelations(params) {
|
|
|
821
838
|
isOptional: false
|
|
822
839
|
});
|
|
823
840
|
}
|
|
824
|
-
for (const [parentModelName, parentModel] of
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
fields: [toFkColumnName({ modelName: parentModelName })],
|
|
837
|
-
references: [parentPk]
|
|
838
|
-
});
|
|
839
|
-
}
|
|
841
|
+
for (const [parentModelName, parentModel] of parents) {
|
|
842
|
+
const parentPk = parentModel.__primaryKey?.[0] ?? Array.from(parentModel.__columns.entries()).find(([, column]) => column.__columnType === "id")?.[0] ?? "id";
|
|
843
|
+
planned.push({
|
|
844
|
+
model: modelName,
|
|
845
|
+
name: parentModelName,
|
|
846
|
+
targetModel: parentModelName,
|
|
847
|
+
kind: "fkOwner",
|
|
848
|
+
isList: false,
|
|
849
|
+
isOptional: false,
|
|
850
|
+
fields: [toFkColumnName({ modelName: parentModelName })],
|
|
851
|
+
references: [parentPk]
|
|
852
|
+
});
|
|
840
853
|
}
|
|
841
854
|
return planned;
|
|
842
855
|
}
|
|
@@ -960,6 +973,33 @@ function assertForeignKeyIntegrity(params) {
|
|
|
960
973
|
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]) });
|
|
961
974
|
}
|
|
962
975
|
}
|
|
976
|
+
function relationLookups(params) {
|
|
977
|
+
const incoming = new Map;
|
|
978
|
+
for (const [modelName, relations] of Object.entries(params.schema.__relations)) {
|
|
979
|
+
for (const [relName, rel] of Object.entries(relations)) {
|
|
980
|
+
if (rel.__relationType !== "one")
|
|
981
|
+
continue;
|
|
982
|
+
const target = rel.__targetModel;
|
|
983
|
+
const entries = incoming.get(target);
|
|
984
|
+
const entry = { modelName, relName, rel };
|
|
985
|
+
if (entries === undefined)
|
|
986
|
+
incoming.set(target, [entry]);
|
|
987
|
+
else
|
|
988
|
+
entries.push(entry);
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
const parents = new Map;
|
|
992
|
+
for (const [name, model] of params.schema.__models) {
|
|
993
|
+
for (const child of model.__children.values()) {
|
|
994
|
+
const entries = parents.get(child.__name);
|
|
995
|
+
if (entries === undefined)
|
|
996
|
+
parents.set(child.__name, [[name, model]]);
|
|
997
|
+
else
|
|
998
|
+
entries.push([name, model]);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return { incoming, parents };
|
|
1002
|
+
}
|
|
963
1003
|
function planSchema(params) {
|
|
964
1004
|
const { schema } = params;
|
|
965
1005
|
const mergeMap = detectOneToOnePairs({
|
|
@@ -968,7 +1008,13 @@ function planSchema(params) {
|
|
|
968
1008
|
});
|
|
969
1009
|
const relations = new Map;
|
|
970
1010
|
const indexes = new Map;
|
|
1011
|
+
let lookups = relationLookups({ schema });
|
|
1012
|
+
let refreshLookups = false;
|
|
971
1013
|
for (const [modelName, model] of schema.__models) {
|
|
1014
|
+
if (refreshLookups) {
|
|
1015
|
+
lookups = relationLookups({ schema });
|
|
1016
|
+
refreshLookups = false;
|
|
1017
|
+
}
|
|
972
1018
|
const explicit = planExplicitRelations({
|
|
973
1019
|
modelName,
|
|
974
1020
|
model,
|
|
@@ -978,16 +1024,22 @@ function planSchema(params) {
|
|
|
978
1024
|
const reverses = planMissingReverseRelations({
|
|
979
1025
|
modelName,
|
|
980
1026
|
relations: schema.__relations,
|
|
1027
|
+
incoming: lookups.incoming.get(modelName) ?? [],
|
|
981
1028
|
mergeMap,
|
|
982
1029
|
existing: explicit
|
|
983
1030
|
});
|
|
984
|
-
const children = planChildRelations({ modelName, model,
|
|
1031
|
+
const children = planChildRelations({ modelName, model, parents: lookups.parents.get(modelName) ?? [] });
|
|
985
1032
|
const planned = [...explicit, ...reverses, ...children];
|
|
986
1033
|
for (const relation of planned) {
|
|
987
|
-
if (relation.kind === "fkOwner")
|
|
1034
|
+
if (relation.kind === "fkOwner") {
|
|
1035
|
+
if (schema.__models.get(relation.targetModel)?.__indexes !== undefined)
|
|
1036
|
+
refreshLookups = true;
|
|
988
1037
|
assertForeignKeyIntegrity({ relation, models: schema.__models });
|
|
1038
|
+
}
|
|
989
1039
|
}
|
|
990
1040
|
relations.set(modelName, planned);
|
|
1041
|
+
if (model.__indexes !== undefined)
|
|
1042
|
+
refreshLookups = true;
|
|
991
1043
|
indexes.set(modelName, planIndexes({ model }));
|
|
992
1044
|
}
|
|
993
1045
|
return { relations, indexes };
|
|
@@ -1077,6 +1129,20 @@ function fieldDefault(params) {
|
|
|
1077
1129
|
return { kind: "literal", value: exactIntegerLiteral({ text: value.toString() }) };
|
|
1078
1130
|
throw new VibeError4({ code: "VIBE_SCHEMA_INVALID", message: `Unsupported default for ${column.__columnType}` });
|
|
1079
1131
|
}
|
|
1132
|
+
function datetimeNativeType(params) {
|
|
1133
|
+
const { tz, precision } = params.config;
|
|
1134
|
+
if (precision !== undefined && (tz !== true || !Number.isInteger(precision) || precision < 0 || precision > 6)) {
|
|
1135
|
+
const at = params.column === undefined ? "A datetime column" : `Column "${params.column}"`;
|
|
1136
|
+
throw new VibeError4({
|
|
1137
|
+
code: "VIBE_SCHEMA_INVALID",
|
|
1138
|
+
message: `${at}: a datetime precision must be an integer from 0 to 6 on a tz() column, as in dsl.datetime().tz({ precision: 3 })`,
|
|
1139
|
+
meta: { issues: [{ rule: "datetime-precision", column: params.column, precision, tz: tz === true }] }
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
if (tz !== true)
|
|
1143
|
+
return;
|
|
1144
|
+
return precision === undefined ? "Timestamptz" : `Timestamptz(${precision})`;
|
|
1145
|
+
}
|
|
1080
1146
|
function nativeType(params) {
|
|
1081
1147
|
const { column } = params;
|
|
1082
1148
|
if (column.__columnType === "enum") {
|
|
@@ -1106,8 +1172,8 @@ function nativeType(params) {
|
|
|
1106
1172
|
return `Decimal(${precision}, ${scale ?? 0})`;
|
|
1107
1173
|
return;
|
|
1108
1174
|
}
|
|
1109
|
-
if (column.__columnType === "datetime"
|
|
1110
|
-
return
|
|
1175
|
+
if (column.__columnType === "datetime") {
|
|
1176
|
+
return datetimeNativeType({ config: column.__config, column: column.__name });
|
|
1111
1177
|
}
|
|
1112
1178
|
if (column.__columnType === "unsupported") {
|
|
1113
1179
|
return String(column.__config.dbType);
|
|
@@ -1419,8 +1485,9 @@ function nativeTypeAttribute(params) {
|
|
|
1419
1485
|
return `@db.Decimal(${precision}, ${scale ?? 0})`;
|
|
1420
1486
|
return;
|
|
1421
1487
|
}
|
|
1422
|
-
if (column.__columnType === "datetime"
|
|
1423
|
-
|
|
1488
|
+
if (column.__columnType === "datetime") {
|
|
1489
|
+
const native = datetimeNativeType({ config: column.__config, column: column.__name });
|
|
1490
|
+
return native === undefined ? undefined : `@db.${native}`;
|
|
1424
1491
|
}
|
|
1425
1492
|
return;
|
|
1426
1493
|
}
|
|
@@ -2267,4 +2334,4 @@ export {
|
|
|
2267
2334
|
withNameDescription
|
|
2268
2335
|
};
|
|
2269
2336
|
|
|
2270
|
-
//# debugId=
|
|
2337
|
+
//# debugId=AC91338749EBA17764756E2164756E21
|