@objectstack/driver-sql 6.1.1 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -12,6 +12,8 @@ var SqlDriver = class {
12
12
  this.version = "1.0.0";
13
13
  this.jsonFields = {};
14
14
  this.booleanFields = {};
15
+ this.dateFields = {};
16
+ this.datetimeFields = {};
15
17
  this.tablesWithTimestamps = /* @__PURE__ */ new Set();
16
18
  /**
17
19
  * Autonumber field configs per table, captured during initObjects.
@@ -746,6 +748,7 @@ var SqlDriver = class {
746
748
  * Batch-initialise tables from an array of object definitions.
747
749
  */
748
750
  async initObjects(objects) {
751
+ var _a, _b;
749
752
  await this.ensureDatabaseExists();
750
753
  for (const obj of objects) {
751
754
  const tableName = StorageNameMapping.resolveTableName(obj);
@@ -773,6 +776,12 @@ var SqlDriver = class {
773
776
  if (type === "boolean") {
774
777
  booleanCols.push(name);
775
778
  }
779
+ if (type === "date") {
780
+ ((_a = this.dateFields)[tableName] ?? (_a[tableName] = /* @__PURE__ */ new Set())).add(name);
781
+ }
782
+ if (type === "datetime") {
783
+ ((_b = this.datetimeFields)[tableName] ?? (_b[tableName] = /* @__PURE__ */ new Set())).add(name);
784
+ }
776
785
  if (type === "auto_number" || type === "autonumber") {
777
786
  const fmt = typeof field.format === "string" && field.format ? field.format : "{0000}";
778
787
  const m = fmt.match(/\{(0+)\}/);
@@ -960,19 +969,83 @@ var SqlDriver = class {
960
969
  );
961
970
  }
962
971
  // ── Filter helpers ──────────────────────────────────────────────────────────
972
+ /**
973
+ * Resolve the underlying table name for a Knex query builder so we can
974
+ * look up column type metadata (date/datetime maps populated during
975
+ * `initObjects`). Returns null when the builder is not table-scoped yet.
976
+ */
977
+ tableNameForBuilder(builder) {
978
+ const t = builder?._single?.table;
979
+ if (typeof t === "string") return t;
980
+ return null;
981
+ }
982
+ /**
983
+ * Normalise a filter value for a single column so the comparison the
984
+ * driver sends to SQLite matches the on-disk representation.
985
+ *
986
+ * The platform stores `Field.datetime()` values as INTEGER milliseconds
987
+ * (the result of passing a JS `Date` through better-sqlite3) but date
988
+ * macros like `{last_quarter_start}` expand to an ISO `YYYY-MM-DD` string
989
+ * client-side. Without coercion the SQL becomes `published_at >= '2026-…'`
990
+ * which collapses to a TEXT-vs-INTEGER affinity compare and never
991
+ * matches. We translate the ISO/Date/numeric inputs into the storage
992
+ * type so the comparison works.
993
+ *
994
+ * For `Field.date()` we keep ISO TEXT but normalise Date objects to
995
+ * `YYYY-MM-DD` for the same reason.
996
+ */
997
+ coerceFilterValue(table, field, value) {
998
+ if (value == null || !table) return value;
999
+ if (Array.isArray(value)) return value.map((v) => this.coerceFilterValue(table, field, v));
1000
+ const isDatetime = this.datetimeFields[table]?.has(field);
1001
+ const isDate = this.dateFields[table]?.has(field);
1002
+ if (!isDatetime && !isDate) return value;
1003
+ const toMs = (v) => {
1004
+ if (v instanceof Date) return v.getTime();
1005
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1006
+ if (typeof v === "string") {
1007
+ const trimmed = v.trim();
1008
+ if (trimmed === "") return null;
1009
+ if (/^-?\d+$/.test(trimmed)) {
1010
+ const n2 = Number(trimmed);
1011
+ if (Number.isFinite(n2)) return n2;
1012
+ }
1013
+ const iso = /^\d{4}-\d{2}-\d{2}$/.test(trimmed) ? `${trimmed}T00:00:00.000Z` : trimmed;
1014
+ const n = Date.parse(iso);
1015
+ return Number.isFinite(n) ? n : null;
1016
+ }
1017
+ return null;
1018
+ };
1019
+ if (isDatetime) {
1020
+ const ms = toMs(value);
1021
+ return ms == null ? value : ms;
1022
+ }
1023
+ if (value instanceof Date) {
1024
+ const y = value.getUTCFullYear();
1025
+ const m = String(value.getUTCMonth() + 1).padStart(2, "0");
1026
+ const d = String(value.getUTCDate()).padStart(2, "0");
1027
+ return `${y}-${m}-${d}`;
1028
+ }
1029
+ if (typeof value === "string") {
1030
+ const trimmed = value.trim();
1031
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) return trimmed.slice(0, 10);
1032
+ }
1033
+ return value;
1034
+ }
963
1035
  applyFilters(builder, filters) {
964
1036
  if (!filters) return;
1037
+ const table = this.tableNameForBuilder(builder);
965
1038
  if (!Array.isArray(filters) && typeof filters === "object") {
966
1039
  const hasMongoOperators = Object.keys(filters).some(
967
1040
  (k) => k.startsWith("$") || typeof filters[k] === "object" && filters[k] !== null && Object.keys(filters[k]).some((op) => op.startsWith("$"))
968
1041
  );
969
1042
  if (hasMongoOperators) {
970
- this.applyFilterCondition(builder, filters);
1043
+ this.applyFilterCondition(builder, filters, "and", table);
971
1044
  return;
972
1045
  }
973
1046
  for (const [key, value] of Object.entries(filters)) {
974
1047
  if (["limit", "offset", "fields", "orderBy"].includes(key)) continue;
975
- builder.where(key, value);
1048
+ builder.where(key, this.coerceFilterValue(table, key, value));
976
1049
  }
977
1050
  return;
978
1051
  }
@@ -989,6 +1062,7 @@ var SqlDriver = class {
989
1062
  const isCriterion = typeof fieldRaw === "string" && typeof op === "string";
990
1063
  if (isCriterion) {
991
1064
  const field = this.mapSortField(fieldRaw);
1065
+ const coerced = this.coerceFilterValue(table, field, value);
992
1066
  const apply = (b) => {
993
1067
  const method = nextJoin === "or" ? "orWhere" : "where";
994
1068
  const methodIn = nextJoin === "or" ? "orWhereIn" : "whereIn";
@@ -999,19 +1073,19 @@ var SqlDriver = class {
999
1073
  }
1000
1074
  switch (op) {
1001
1075
  case "=":
1002
- b[method](field, value);
1076
+ b[method](field, coerced);
1003
1077
  break;
1004
1078
  case "!=":
1005
- b[method](field, "<>", value);
1079
+ b[method](field, "<>", coerced);
1006
1080
  break;
1007
1081
  case "in":
1008
- b[methodIn](field, value);
1082
+ b[methodIn](field, coerced);
1009
1083
  break;
1010
1084
  case "nin":
1011
- b[methodNotIn](field, value);
1085
+ b[methodNotIn](field, coerced);
1012
1086
  break;
1013
1087
  default:
1014
- b[method](field, op, value);
1088
+ b[method](field, op, coerced);
1015
1089
  }
1016
1090
  };
1017
1091
  apply(builder);
@@ -1025,14 +1099,15 @@ var SqlDriver = class {
1025
1099
  }
1026
1100
  }
1027
1101
  }
1028
- applyFilterCondition(builder, condition, logicalOp = "and") {
1102
+ applyFilterCondition(builder, condition, logicalOp = "and", tableHint) {
1029
1103
  if (!condition || typeof condition !== "object") return;
1104
+ const table = tableHint ?? this.tableNameForBuilder(builder);
1030
1105
  for (const [key, value] of Object.entries(condition)) {
1031
1106
  if (key === "$and" && Array.isArray(value)) {
1032
1107
  builder.where((qb) => {
1033
1108
  for (const sub of value) {
1034
1109
  qb.where((subQb) => {
1035
- this.applyFilterCondition(subQb, sub, "and");
1110
+ this.applyFilterCondition(subQb, sub, "and", table);
1036
1111
  });
1037
1112
  }
1038
1113
  });
@@ -1041,7 +1116,7 @@ var SqlDriver = class {
1041
1116
  builder[method]((qb) => {
1042
1117
  for (const sub of value) {
1043
1118
  qb.orWhere((subQb) => {
1044
- this.applyFilterCondition(subQb, sub, "or");
1119
+ this.applyFilterCondition(subQb, sub, "or", table);
1045
1120
  });
1046
1121
  }
1047
1122
  });
@@ -1049,46 +1124,47 @@ var SqlDriver = class {
1049
1124
  const field = this.mapSortField(key);
1050
1125
  for (const [op, opValue] of Object.entries(value)) {
1051
1126
  const method = logicalOp === "or" ? "orWhere" : "where";
1127
+ const coerced = this.coerceFilterValue(table, field, opValue);
1052
1128
  switch (op) {
1053
1129
  case "$eq":
1054
- builder[method](field, opValue);
1130
+ builder[method](field, coerced);
1055
1131
  break;
1056
1132
  case "$ne":
1057
- builder[method](field, "<>", opValue);
1133
+ builder[method](field, "<>", coerced);
1058
1134
  break;
1059
1135
  case "$gt":
1060
- builder[method](field, ">", opValue);
1136
+ builder[method](field, ">", coerced);
1061
1137
  break;
1062
1138
  case "$gte":
1063
- builder[method](field, ">=", opValue);
1139
+ builder[method](field, ">=", coerced);
1064
1140
  break;
1065
1141
  case "$lt":
1066
- builder[method](field, "<", opValue);
1142
+ builder[method](field, "<", coerced);
1067
1143
  break;
1068
1144
  case "$lte":
1069
- builder[method](field, "<=", opValue);
1145
+ builder[method](field, "<=", coerced);
1070
1146
  break;
1071
1147
  case "$in": {
1072
1148
  const mIn = logicalOp === "or" ? "orWhereIn" : "whereIn";
1073
- builder[mIn](field, opValue);
1149
+ builder[mIn](field, coerced);
1074
1150
  break;
1075
1151
  }
1076
1152
  case "$nin": {
1077
1153
  const mNotIn = logicalOp === "or" ? "orWhereNotIn" : "whereNotIn";
1078
- builder[mNotIn](field, opValue);
1154
+ builder[mNotIn](field, coerced);
1079
1155
  break;
1080
1156
  }
1081
1157
  case "$contains":
1082
1158
  builder[method](field, "like", `%${opValue}%`);
1083
1159
  break;
1084
1160
  default:
1085
- builder[method](field, opValue);
1161
+ builder[method](field, coerced);
1086
1162
  }
1087
1163
  }
1088
1164
  } else {
1089
1165
  const field = this.mapSortField(key);
1090
1166
  const method = logicalOp === "or" ? "orWhere" : "where";
1091
- builder[method](field, value);
1167
+ builder[method](field, this.coerceFilterValue(table, field, value));
1092
1168
  }
1093
1169
  }
1094
1170
  }