@easy-cms/drizzle 0.7.0 → 0.9.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 +21 -6
- package/dist/index.js +99 -38
- package/package.json +3 -3
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
|
-
/**
|
|
25
|
-
|
|
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,15 @@ 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`
|
|
48
|
-
* returns undefined).
|
|
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.
|
|
59
|
+
*/
|
|
60
|
+
someElement(array: AnyColumn | SQL, condition: (el: JsonElement) => SQL | undefined): SQL;
|
|
61
|
+
/**
|
|
62
|
+
* For sorting: the value at `path` in the first element that has one, stepping through the
|
|
63
|
+
* nested `lists` in each element (in order) like `someElement`.
|
|
49
64
|
*/
|
|
50
|
-
|
|
65
|
+
firstElementValue(array: AnyColumn | SQL, lists: readonly (readonly string[])[], path: readonly string[], type: JsonValueType): SQL;
|
|
51
66
|
/** Placeholder for the n-th parameter (1-based) in raw SQL. */
|
|
52
67
|
param(n: number): string;
|
|
53
68
|
/** CREATE TABLE IF NOT EXISTS for the migrations table. */
|
|
@@ -182,4 +197,4 @@ declare class Migrator {
|
|
|
182
197
|
private files;
|
|
183
198
|
}
|
|
184
199
|
|
|
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
|
|
200
|
+
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
|
|
723
|
-
if (rest.length ===
|
|
724
|
-
|
|
725
|
-
|
|
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 =
|
|
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
|
-
|
|
735
|
+
list = field.fields;
|
|
735
736
|
continue;
|
|
736
737
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
|
744
|
-
|
|
745
|
-
|
|
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++;
|
|
747
745
|
}
|
|
748
|
-
|
|
749
|
-
|
|
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] };
|
|
756
|
+
}
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
|
919
|
-
if (
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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,22 @@ var WhereBuilder = class {
|
|
|
955
968
|
}
|
|
956
969
|
return subquery(this.build(table, { [rest.join(".")]: { [op]: value } }));
|
|
957
970
|
}
|
|
971
|
+
/** A column, or the first value in blocks (and the lists in them, in order) that has it. */
|
|
972
|
+
sortValue(target, path) {
|
|
973
|
+
if (target.kind === "column") return target.column;
|
|
974
|
+
if (target.kind === "blocks" && target.value.path) {
|
|
975
|
+
const { lists, path: inner, type } = target.value;
|
|
976
|
+
return this.dialect.firstElementValue(target.column, lists, inner, type);
|
|
977
|
+
}
|
|
978
|
+
throw new QueryError(`Cannot sort by "${path}"`);
|
|
979
|
+
}
|
|
958
980
|
orderBy(model, sort) {
|
|
959
981
|
const order = sort.map((entry) => {
|
|
960
982
|
const descending = entry.startsWith("-");
|
|
961
983
|
const path = descending ? entry.slice(1) : entry;
|
|
962
984
|
const target = resolvePath(model, path.split("."), this.drafts, this.localization);
|
|
963
|
-
|
|
964
|
-
return descending ? desc(
|
|
985
|
+
const value = this.sortValue(target, path);
|
|
986
|
+
return descending ? desc(value) : asc2(value);
|
|
965
987
|
});
|
|
966
988
|
order.push(desc(col2(model, "id")));
|
|
967
989
|
return order;
|
|
@@ -995,6 +1017,7 @@ async function connectDatabase(dialect, connection, args, options = {}) {
|
|
|
995
1017
|
}
|
|
996
1018
|
var DrizzleDatabase = class {
|
|
997
1019
|
constructor(dialect, connection, schema, migrator) {
|
|
1020
|
+
this.dialect = dialect;
|
|
998
1021
|
this.connection = connection;
|
|
999
1022
|
this.schema = schema;
|
|
1000
1023
|
this.migrator = migrator;
|
|
@@ -1006,6 +1029,7 @@ var DrizzleDatabase = class {
|
|
|
1006
1029
|
);
|
|
1007
1030
|
}
|
|
1008
1031
|
}
|
|
1032
|
+
dialect;
|
|
1009
1033
|
connection;
|
|
1010
1034
|
schema;
|
|
1011
1035
|
migrator;
|
|
@@ -1060,23 +1084,38 @@ var DrizzleDatabase = class {
|
|
|
1060
1084
|
const [row] = await this.db.select({ total: count() }).from(model.root.table).where(this.where(args.collection, args.where));
|
|
1061
1085
|
return Number(row?.total ?? 0);
|
|
1062
1086
|
}
|
|
1087
|
+
writing = Promise.resolve();
|
|
1088
|
+
/** Runs a write; with a single-writer database, after the writes before it are done. */
|
|
1089
|
+
write(run) {
|
|
1090
|
+
if (!this.dialect.singleWriter) return run();
|
|
1091
|
+
const result = this.writing.then(() => retryWhileBusy(run));
|
|
1092
|
+
this.writing = result.catch(() => {
|
|
1093
|
+
});
|
|
1094
|
+
return result;
|
|
1095
|
+
}
|
|
1063
1096
|
async create(args) {
|
|
1064
1097
|
const model = this.model(args.collection);
|
|
1065
|
-
const id = await this.
|
|
1066
|
-
(
|
|
1098
|
+
const id = await this.write(
|
|
1099
|
+
() => this.db.transaction(
|
|
1100
|
+
(tx) => insertDocument(tx, model.root, args.data, model.config.drafts === true)
|
|
1101
|
+
)
|
|
1067
1102
|
);
|
|
1068
1103
|
return await this.findById({ collection: args.collection, id });
|
|
1069
1104
|
}
|
|
1070
1105
|
async update(args) {
|
|
1071
1106
|
const model = this.model(args.collection);
|
|
1072
|
-
await this.
|
|
1073
|
-
(
|
|
1107
|
+
await this.write(
|
|
1108
|
+
() => this.db.transaction(
|
|
1109
|
+
(tx) => replaceDocument(tx, model.root, args.id, args.data, model.config.drafts === true)
|
|
1110
|
+
)
|
|
1074
1111
|
);
|
|
1075
1112
|
return await this.findById({ collection: args.collection, id: args.id });
|
|
1076
1113
|
}
|
|
1077
1114
|
async delete(args) {
|
|
1078
1115
|
const model = this.model(args.collection);
|
|
1079
|
-
await this.
|
|
1116
|
+
await this.write(
|
|
1117
|
+
() => this.db.transaction((tx) => deleteDocument(tx, model.root, args.id))
|
|
1118
|
+
);
|
|
1080
1119
|
}
|
|
1081
1120
|
async findGlobal(args) {
|
|
1082
1121
|
const table = this.schema.globals;
|
|
@@ -1097,7 +1136,9 @@ var DrizzleDatabase = class {
|
|
|
1097
1136
|
status: status ?? null,
|
|
1098
1137
|
updated_at: updatedAt
|
|
1099
1138
|
};
|
|
1100
|
-
await this.
|
|
1139
|
+
await this.write(
|
|
1140
|
+
() => this.db.insert(table).values(values).onConflictDoUpdate({ target: table.slug, set: values })
|
|
1141
|
+
);
|
|
1101
1142
|
return await this.findGlobal({ slug: args.slug });
|
|
1102
1143
|
}
|
|
1103
1144
|
createMigration(args) {
|
|
@@ -1113,6 +1154,26 @@ var DrizzleDatabase = class {
|
|
|
1113
1154
|
await this.connection.close();
|
|
1114
1155
|
}
|
|
1115
1156
|
};
|
|
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
|
+
}
|
|
1116
1177
|
export {
|
|
1117
1178
|
DEFAULT_MIGRATION_DIR,
|
|
1118
1179
|
DEFAULT_TABLE_PREFIX,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/drizzle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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.
|
|
26
|
+
"@easy-cms/core": "^0.9.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@easy-cms/core": "^0.
|
|
29
|
+
"@easy-cms/core": "^0.9.0"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|