@easy-cms/drizzle 0.7.0 → 0.8.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
@@ -21,11 +21,21 @@ interface SqlRunner {
21
21
  }
22
22
  /** How to read a value out of a JSON element for comparing. */
23
23
  type JsonValueType = 'text' | 'number' | 'integer' | 'boolean';
24
- /** A value inside the JSON element being tested, e.g. `['heading', 'en']`. */
25
- type JsonGetter = (path: readonly string[], type: JsonValueType) => SQL;
24
+ /** The JSON element being tested. Paths are field names, already checked against the config. */
25
+ interface JsonElement {
26
+ /** A value inside the element, e.g. `['heading', 'en']`; `[]` for the element itself. */
27
+ get(path: readonly string[], type: JsonValueType): SQL;
28
+ /** A JSON array inside the element, to test with `someElement`. */
29
+ list(path: readonly string[]): SQL;
30
+ }
26
31
  /** What differs between SQL dialects. */
27
32
  interface Dialect {
28
33
  readonly name: 'sqlite' | 'postgres';
34
+ /**
35
+ * One writer at a time (SQLite): writes from this process wait for each other instead of
36
+ * failing with "database is locked".
37
+ */
38
+ readonly singleWriter?: boolean;
29
39
  table(name: string, columns: Record<string, ColumnBuilder>, indexes: (t: Record<string, AnyColumn>) => IndexBuilder[]): AnyTable;
30
40
  index(name: string): {
31
41
  on(column: AnyColumn): IndexBuilder;
@@ -44,10 +54,12 @@ interface Dialect {
44
54
  /** Case-insensitive substring match; `pattern` is already escaped and wrapped in `%`. */
45
55
  like(column: AnyColumn, pattern: string): SQL;
46
56
  /**
47
- * True when some element of a JSON array column matches `condition` (any element when it
48
- * returns undefined). Path segments are field names, already checked against the config.
57
+ * True when some element of a JSON array (a column, or `JsonElement.list`) matches `condition`
58
+ * (any element when it returns undefined). Anything but an array has no elements.
49
59
  */
50
- someElement(column: AnyColumn, condition: (get: JsonGetter) => SQL | undefined): SQL;
60
+ someElement(array: AnyColumn | SQL, condition: (el: JsonElement) => SQL | undefined): SQL;
61
+ /** The value at `path` in the first element of a JSON array that has one, for sorting. */
62
+ firstElementValue(array: AnyColumn | SQL, path: readonly string[], type: JsonValueType): SQL;
51
63
  /** Placeholder for the n-th parameter (1-based) in raw SQL. */
52
64
  param(n: number): string;
53
65
  /** CREATE TABLE IF NOT EXISTS for the migrations table. */
@@ -182,4 +194,4 @@ declare class Migrator {
182
194
  private files;
183
195
  }
184
196
 
185
- export { type AnyColumn, type AnyTable, type ColumnBuilder, type Connection, DEFAULT_MIGRATION_DIR, DEFAULT_TABLE_PREFIX, type Dialect, type DrizzleAdapterOptions, type DrizzleDb, type IndexBuilder, type JsonGetter, type JsonValueType, Migrator, type SchemaModel, type Snapshot, type SqlRunner, type Statement, buildSchema, connectDatabase, snake, unionSnapshot };
197
+ export { type AnyColumn, type AnyTable, type ColumnBuilder, type Connection, DEFAULT_MIGRATION_DIR, DEFAULT_TABLE_PREFIX, type Dialect, type DrizzleAdapterOptions, type DrizzleDb, type IndexBuilder, type JsonElement, type JsonValueType, Migrator, type SchemaModel, type Snapshot, type SqlRunner, type Statement, buildSchema, connectDatabase, snake, unionSnapshot };
package/dist/index.js CHANGED
@@ -719,34 +719,43 @@ var JSON_TYPES = {
719
719
  relationship: "integer",
720
720
  upload: "integer"
721
721
  };
722
- function resolveBlockValue(blocks, rest, shown, localization) {
723
- if (rest.length === 1 && rest[0] === "blockType")
724
- return { path: ["blockType"], type: "text", field: void 0 };
725
- let fields = blocks.blocks.flatMap((block) => block.fields);
722
+ function resolveJsonValue(fields, rest, shown, localization, blocks) {
723
+ if (rest.length === 0) return { lists: [], path: null, type: "text", field: void 0 };
724
+ if (blocks && rest.length === 1 && rest[0] === "blockType")
725
+ return { lists: [], path: ["blockType"], type: "text", field: void 0 };
726
+ let list = fields;
726
727
  const path = [];
727
728
  for (let i = 0; i < rest.length; i++) {
728
729
  const segment = rest[i];
729
- const field = fields.find((f) => f.name === segment);
730
+ const field = list.find((f) => f.name === segment);
730
731
  const at = [shown, ...rest.slice(0, i + 1)].join(".");
731
732
  if (!field) throw new QueryError(`Unknown field "${at}"`);
732
733
  path.push(segment);
733
734
  if (field.type === "group") {
734
- fields = field.fields;
735
+ list = field.fields;
735
736
  continue;
736
737
  }
737
- const hasMany = "hasMany" in field && field.hasMany === true;
738
- if (hasMany || field.type === "array" || field.type === "blocks" || field.type === "json" || field.type === "richText") {
739
- throw new QueryError(`Cannot query "${at}" inside blocks`);
740
- }
741
- let last = i;
738
+ if (field.type === "json" || field.type === "richText")
739
+ throw new QueryError(`Cannot query "${at}": ${field.type} fields are not queryable`);
740
+ let next = i + 1;
742
741
  if (field.localized && localization) {
743
- const next = rest[i + 1];
744
- const named = next !== void 0 && localization.locales.includes(next);
745
- path.push(named ? next : localization.defaultLocale);
746
- if (named) last = i + 1;
742
+ const named = rest[next] !== void 0 && localization.locales.includes(rest[next]);
743
+ path.push(named ? rest[next] : localization.defaultLocale);
744
+ if (named) next++;
745
+ }
746
+ const inner = rest.slice(next);
747
+ const shownHere = [shown, ...rest.slice(0, next)].join(".");
748
+ if ("hasMany" in field && field.hasMany === true) {
749
+ if (inner.length > 0) throw new QueryError(`Cannot query inside "${shownHere}"`);
750
+ return { lists: [path], path: [], type: JSON_TYPES[field.type] ?? "text", field };
751
+ }
752
+ if (field.type === "array" || field.type === "blocks") {
753
+ const rows = field.type === "array" ? field.fields : field.blocks.flatMap((block) => block.fields);
754
+ const value = resolveJsonValue(rows, inner, shownHere, localization, field.type === "blocks");
755
+ return { ...value, lists: [path, ...value.lists] };
747
756
  }
748
- if (last < rest.length - 1) throw new QueryError(`Cannot query inside "${at}"`);
749
- return { path, type: JSON_TYPES[field.type] ?? "text", field };
757
+ if (inner.length > 0) throw new QueryError(`Cannot query inside "${at}"`);
758
+ return { lists: [], path, type: JSON_TYPES[field.type] ?? "text", field };
750
759
  }
751
760
  throw new QueryError(`"${[shown, ...rest].join(".")}" is a group; query one of its fields`);
752
761
  }
@@ -799,7 +808,8 @@ function resolvePath(model, segments, drafts, localization = null) {
799
808
  const column2 = columnOf();
800
809
  if (!column2) throw new QueryError(`Unknown locale "${locale}" for "${shown}"`);
801
810
  const rest = segments.slice(last + 1);
802
- const value = rest.length === 0 ? null : resolveBlockValue(field, rest, shown, localization);
811
+ const rows = field.blocks.flatMap((block) => block.fields);
812
+ const value = resolveJsonValue(rows, rest, shown, localization, true);
803
813
  return { kind: "blocks", column: col2(model, column2.column), value };
804
814
  }
805
815
  if (last < segments.length - 1) {
@@ -913,19 +923,22 @@ var WhereBuilder = class {
913
923
  });
914
924
  return parts.length === 1 ? parts[0] : and(...parts);
915
925
  }
916
- /** Conditions inside blocks test each block of the JSON column: some block must match. */
926
+ /**
927
+ * Conditions inside blocks test the JSON rows: some block (and, for nested lists, some row in
928
+ * it) must match.
929
+ */
917
930
  blocks(target, op, value, path) {
918
- const { value: inner } = target;
919
- if (!inner) {
920
- if (op !== "exists")
921
- throw new QueryError(`"${path}" is a blocks field; query one of its fields`);
922
- const any = this.dialect.someElement(target.column, () => void 0);
923
- return value === false ? not(any) : any;
924
- }
925
- return this.dialect.someElement(
926
- target.column,
927
- (get) => operatorSQL(this.dialect, get(inner.path, inner.type), inner.field, op, value, path)
928
- );
931
+ const { lists, path: inner, type, field } = target.value;
932
+ if (inner === null && op !== "exists")
933
+ throw new QueryError(`"${path}" is a list; query one of its fields`);
934
+ const match = (el, level) => {
935
+ const list = lists[level];
936
+ if (list) return this.dialect.someElement(el.list(list), (row) => match(row, level + 1));
937
+ if (inner === null) return void 0;
938
+ return operatorSQL(this.dialect, el.get(inner, type), field, op, value, path);
939
+ };
940
+ const some = this.dialect.someElement(target.column, (el) => match(el, 0));
941
+ return inner === null && value === false ? not(some) : some;
929
942
  }
930
943
  /** Conditions on array rows or hasMany values become (NOT) EXISTS subqueries. */
931
944
  child(parent, child, rest, op, value, path, locale) {
@@ -955,13 +968,21 @@ var WhereBuilder = class {
955
968
  }
956
969
  return subquery(this.build(table, { [rest.join(".")]: { [op]: value } }));
957
970
  }
971
+ /** A column, or the value in the first block that has it. */
972
+ sortValue(target, path) {
973
+ if (target.kind === "column") return target.column;
974
+ if (target.kind === "blocks" && target.value.lists.length === 0 && target.value.path) {
975
+ return this.dialect.firstElementValue(target.column, target.value.path, target.value.type);
976
+ }
977
+ throw new QueryError(`Cannot sort by "${path}"`);
978
+ }
958
979
  orderBy(model, sort) {
959
980
  const order = sort.map((entry) => {
960
981
  const descending = entry.startsWith("-");
961
982
  const path = descending ? entry.slice(1) : entry;
962
983
  const target = resolvePath(model, path.split("."), this.drafts, this.localization);
963
- if (target.kind !== "column") throw new QueryError(`Cannot sort by "${path}"`);
964
- return descending ? desc(target.column) : asc2(target.column);
984
+ const value = this.sortValue(target, path);
985
+ return descending ? desc(value) : asc2(value);
965
986
  });
966
987
  order.push(desc(col2(model, "id")));
967
988
  return order;
@@ -995,6 +1016,7 @@ async function connectDatabase(dialect, connection, args, options = {}) {
995
1016
  }
996
1017
  var DrizzleDatabase = class {
997
1018
  constructor(dialect, connection, schema, migrator) {
1019
+ this.dialect = dialect;
998
1020
  this.connection = connection;
999
1021
  this.schema = schema;
1000
1022
  this.migrator = migrator;
@@ -1006,6 +1028,7 @@ var DrizzleDatabase = class {
1006
1028
  );
1007
1029
  }
1008
1030
  }
1031
+ dialect;
1009
1032
  connection;
1010
1033
  schema;
1011
1034
  migrator;
@@ -1060,23 +1083,38 @@ var DrizzleDatabase = class {
1060
1083
  const [row] = await this.db.select({ total: count() }).from(model.root.table).where(this.where(args.collection, args.where));
1061
1084
  return Number(row?.total ?? 0);
1062
1085
  }
1086
+ writing = Promise.resolve();
1087
+ /** Runs a write; with a single-writer database, after the writes before it are done. */
1088
+ write(run) {
1089
+ if (!this.dialect.singleWriter) return run();
1090
+ const result = this.writing.then(run);
1091
+ this.writing = result.catch(() => {
1092
+ });
1093
+ return result;
1094
+ }
1063
1095
  async create(args) {
1064
1096
  const model = this.model(args.collection);
1065
- const id = await this.db.transaction(
1066
- (tx) => insertDocument(tx, model.root, args.data, model.config.drafts === true)
1097
+ const id = await this.write(
1098
+ () => this.db.transaction(
1099
+ (tx) => insertDocument(tx, model.root, args.data, model.config.drafts === true)
1100
+ )
1067
1101
  );
1068
1102
  return await this.findById({ collection: args.collection, id });
1069
1103
  }
1070
1104
  async update(args) {
1071
1105
  const model = this.model(args.collection);
1072
- await this.db.transaction(
1073
- (tx) => replaceDocument(tx, model.root, args.id, args.data, model.config.drafts === true)
1106
+ await this.write(
1107
+ () => this.db.transaction(
1108
+ (tx) => replaceDocument(tx, model.root, args.id, args.data, model.config.drafts === true)
1109
+ )
1074
1110
  );
1075
1111
  return await this.findById({ collection: args.collection, id: args.id });
1076
1112
  }
1077
1113
  async delete(args) {
1078
1114
  const model = this.model(args.collection);
1079
- await this.db.transaction((tx) => deleteDocument(tx, model.root, args.id));
1115
+ await this.write(
1116
+ () => this.db.transaction((tx) => deleteDocument(tx, model.root, args.id))
1117
+ );
1080
1118
  }
1081
1119
  async findGlobal(args) {
1082
1120
  const table = this.schema.globals;
@@ -1097,7 +1135,9 @@ var DrizzleDatabase = class {
1097
1135
  status: status ?? null,
1098
1136
  updated_at: updatedAt
1099
1137
  };
1100
- await this.db.insert(table).values(values).onConflictDoUpdate({ target: table.slug, set: values });
1138
+ await this.write(
1139
+ () => this.db.insert(table).values(values).onConflictDoUpdate({ target: table.slug, set: values })
1140
+ );
1101
1141
  return await this.findGlobal({ slug: args.slug });
1102
1142
  }
1103
1143
  createMigration(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/drizzle",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Shared Drizzle ORM layer for Easy CMS database adapters",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -23,10 +23,10 @@
23
23
  "drizzle-orm": "0.45.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "@easy-cms/core": "^0.7.0"
26
+ "@easy-cms/core": "^0.8.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@easy-cms/core": "^0.7.0"
29
+ "@easy-cms/core": "^0.8.0"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"