@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 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
- /** `@db.Timestamptz` — timezone-aware timestamp. */
81
- tz(): this;
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
- for (let j = i + 1;j < allOnes.length; j++) {
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 [sourceModelName, sourceRelations] of Object.entries(relations)) {
776
- for (const [relName, rel] of Object.entries(sourceRelations)) {
777
- if (rel.__targetModel !== modelName || rel.__relationType !== "one")
778
- continue;
779
- if (mergeMap.has(`${sourceModelName}:${relName}`))
780
- continue;
781
- const reverseKey = `${sourceModelName}:${rel.__alias ?? ""}`;
782
- if (existingLists.has(reverseKey))
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, models } = params;
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 models) {
825
- for (const childModel of parentModel.__children.values()) {
826
- if (childModel.__name !== modelName)
827
- continue;
828
- const parentPk = parentModel.__primaryKey?.[0] ?? Array.from(parentModel.__columns.entries()).find(([, column]) => column.__columnType === "id")?.[0] ?? "id";
829
- planned.push({
830
- model: modelName,
831
- name: parentModelName,
832
- targetModel: parentModelName,
833
- kind: "fkOwner",
834
- isList: false,
835
- isOptional: false,
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, models: schema.__models });
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" && column.__config.tz === true) {
1110
- return "Timestamptz";
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" && column.__config.tz === true) {
1423
- return "@db.Timestamptz";
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=9B3711B7350090C264756E2164756E21
2337
+ //# debugId=AC91338749EBA17764756E2164756E21