@easy-cms/drizzle 0.9.0 → 0.10.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.d.ts CHANGED
@@ -58,11 +58,18 @@ interface Dialect {
58
58
  * (any element when it returns undefined). Anything but an array has no elements.
59
59
  */
60
60
  someElement(array: AnyColumn | SQL, condition: (el: JsonElement) => SQL | undefined): SQL;
61
+ /**
62
+ * True when a JSON column contains `pattern` (Postgres `@>`, which can skip unpacking arrays).
63
+ * Optional: without it, equality inside blocks uses `someElement`.
64
+ */
65
+ jsonContains?(column: AnyColumn, pattern: unknown): SQL;
61
66
  /**
62
67
  * For sorting: the value at `path` in the first element that has one, stepping through the
63
68
  * nested `lists` in each element (in order) like `someElement`.
64
69
  */
65
70
  firstElementValue(array: AnyColumn | SQL, lists: readonly (readonly string[])[], path: readonly string[], type: JsonValueType): SQL;
71
+ /** SQL that copies the whole database to a file while it is in use (SQLite: `VACUUM INTO`). */
72
+ backupSQL?(file: string): string;
66
73
  /** Placeholder for the n-th parameter (1-based) in raw SQL. */
67
74
  param(n: number): string;
68
75
  /** CREATE TABLE IF NOT EXISTS for the migrations table. */
@@ -85,6 +92,11 @@ interface DrizzleAdapterOptions {
85
92
  interface Connection {
86
93
  readonly db: DrizzleDb;
87
94
  readonly runner: SqlRunner;
95
+ /**
96
+ * With a single-writer dialect: identifies the database (e.g. the SQLite file), so every
97
+ * instance in this process that writes to it shares one queue.
98
+ */
99
+ readonly writeQueue?: string;
88
100
  close(): Promise<void>;
89
101
  }
90
102
  /**
package/dist/index.js CHANGED
@@ -300,7 +300,10 @@ function buildTable(builder, name, kind, fields, options) {
300
300
  add("id", dialect.serial("id"), "id:int");
301
301
  add("created_at", dialect.text("created_at").notNull(), "text!");
302
302
  add("updated_at", dialect.text("updated_at").notNull(), "text!");
303
- if (options.drafts) add("status", dialect.text("status").notNull().default("draft"), "status");
303
+ if (options.drafts) {
304
+ add("status", dialect.text("status").notNull().default("draft"), "status");
305
+ indexes.push({ column: "status", unique: false });
306
+ }
304
307
  indexes.push({ column: "created_at", unique: false });
305
308
  } else {
306
309
  const parentKind = options.parentIdKind ?? "integer";
@@ -937,9 +940,33 @@ var WhereBuilder = class {
937
940
  if (inner === null) return void 0;
938
941
  return operatorSQL(this.dialect, el.get(inner, type), field, op, value, path);
939
942
  };
943
+ const contains = this.containment(target, op, value);
944
+ if (contains) return contains;
940
945
  const some = this.dialect.someElement(target.column, (el) => match(el, 0));
941
946
  return inner === null && value === false ? not(some) : some;
942
947
  }
948
+ /**
949
+ * `equals` and `in` as JSON containment where the database has it: `layout.items.title = x`
950
+ * becomes `layout @> [{"items": [{"title": x}]}]`, which matches exactly the same documents
951
+ * without unpacking every array.
952
+ */
953
+ containment(target, op, value) {
954
+ const { jsonContains } = this.dialect;
955
+ const { lists, path, type, field } = target.value;
956
+ if (!jsonContains || !path) return void 0;
957
+ const values = op === "equals" ? [value] : op === "in" && Array.isArray(value) ? value : null;
958
+ if (!values || values.length === 0 || values.some((v) => v === null || v === void 0))
959
+ return void 0;
960
+ const nest = (segments, inner) => segments.reduceRight((acc, segment) => ({ [segment]: acc }), inner);
961
+ const patterns = values.map((raw) => {
962
+ const coerced = coerce(field, raw);
963
+ let pattern = type === "text" && typeof coerced !== "string" ? String(coerced) : coerced;
964
+ pattern = nest(path, pattern);
965
+ for (const list of [...lists].reverse()) pattern = nest(list, [pattern]);
966
+ return jsonContains(target.column, [pattern]);
967
+ });
968
+ return patterns.length === 1 ? patterns[0] : or(...patterns);
969
+ }
943
970
  /** Conditions on array rows or hasMany values become (NOT) EXISTS subqueries. */
944
971
  child(parent, child, rest, op, value, path, locale) {
945
972
  const table = child.table;
@@ -993,6 +1020,7 @@ var WhereBuilder = class {
993
1020
  // src/database.ts
994
1021
  var DEFAULT_TABLE_PREFIX = "ecms_";
995
1022
  var DEFAULT_MIGRATION_DIR = "easy-cms/migrations";
1023
+ var writeQueues = /* @__PURE__ */ new Map();
996
1024
  async function connectDatabase(dialect, connection, args, options = {}) {
997
1025
  const prefix = options.tablePrefix ?? DEFAULT_TABLE_PREFIX;
998
1026
  try {
@@ -1084,12 +1112,23 @@ var DrizzleDatabase = class {
1084
1112
  const [row] = await this.db.select({ total: count() }).from(model.root.table).where(this.where(args.collection, args.where));
1085
1113
  return Number(row?.total ?? 0);
1086
1114
  }
1087
- writing = Promise.resolve();
1088
- /** Runs a write; with a single-writer database, after the writes before it are done. */
1115
+ /**
1116
+ * Runs a write; with a single-writer database, after the writes before it in this process are
1117
+ * done. (Other processes are waited for by the database's busy timeout.)
1118
+ */
1089
1119
  write(run) {
1090
1120
  if (!this.dialect.singleWriter) return run();
1091
- const result = this.writing.then(() => retryWhileBusy(run));
1092
- this.writing = result.catch(() => {
1121
+ const key = this.connection.writeQueue ?? "";
1122
+ const result = (writeQueues.get(key) ?? Promise.resolve()).then(run);
1123
+ const settled = result.then(
1124
+ () => {
1125
+ },
1126
+ () => {
1127
+ }
1128
+ );
1129
+ writeQueues.set(key, settled);
1130
+ void settled.then(() => {
1131
+ if (writeQueues.get(key) === settled) writeQueues.delete(key);
1093
1132
  });
1094
1133
  return result;
1095
1134
  }
@@ -1150,30 +1189,17 @@ var DrizzleDatabase = class {
1150
1189
  migrationStatus() {
1151
1190
  return this.migrator.status();
1152
1191
  }
1192
+ get backup() {
1193
+ const { backupSQL } = this.dialect;
1194
+ if (!backupSQL) return void 0;
1195
+ return (file) => this.write(async () => {
1196
+ await this.connection.runner.query(backupSQL(file));
1197
+ });
1198
+ }
1153
1199
  async destroy() {
1154
1200
  await this.connection.close();
1155
1201
  }
1156
1202
  };
1157
- var BUSY_TIMEOUT = 1e4;
1158
- async function retryWhileBusy(run) {
1159
- const until = Date.now() + BUSY_TIMEOUT;
1160
- for (let delay = 5; ; delay = Math.min(delay * 2, 250)) {
1161
- try {
1162
- return await run();
1163
- } catch (error) {
1164
- if (!isBusy(error) || Date.now() + delay > until) throw error;
1165
- await new Promise((resolve2) => setTimeout(resolve2, delay + Math.random() * delay));
1166
- }
1167
- }
1168
- }
1169
- function isBusy(error) {
1170
- for (let e = error; e; e = e.cause) {
1171
- const code = e.code;
1172
- if (typeof code === "string" && code.startsWith("SQLITE_BUSY")) return true;
1173
- if (e === e.cause) break;
1174
- }
1175
- return false;
1176
- }
1177
1203
  export {
1178
1204
  DEFAULT_MIGRATION_DIR,
1179
1205
  DEFAULT_TABLE_PREFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/drizzle",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "Shared Drizzle ORM layer for Easy CMS database adapters",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,17 +23,17 @@
23
23
  "drizzle-orm": "0.45.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "@easy-cms/core": "^0.9.0"
26
+ "@easy-cms/core": "^0.10.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@easy-cms/core": "^0.9.0"
29
+ "@easy-cms/core": "^0.10.0"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"
33
33
  },
34
34
  "repository": {
35
35
  "type": "git",
36
- "url": "git+https://github.com/maritonx/easy-crm.git",
36
+ "url": "git+https://github.com/maritonx/easy-cms.git",
37
37
  "directory": "packages/drizzle"
38
38
  },
39
39
  "keywords": [