@easy-cms/drizzle 1.15.0 → 1.16.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 -0
- package/dist/index.js +74 -13
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -100,6 +100,8 @@ interface DrizzleAdapterOptions {
|
|
|
100
100
|
/** A driver connection an adapter opened. */
|
|
101
101
|
interface Connection {
|
|
102
102
|
readonly db: DrizzleDb;
|
|
103
|
+
/** Connections kept for writes (`Database.writing`), when the adapter opened some. */
|
|
104
|
+
readonly writeDb?: DrizzleDb | undefined;
|
|
103
105
|
readonly runner: SqlRunner;
|
|
104
106
|
/**
|
|
105
107
|
* With a single-writer dialect: identifies the database (e.g. the SQLite file), so every
|
|
@@ -156,8 +158,22 @@ interface SchemaModel {
|
|
|
156
158
|
readonly tables: Readonly<Record<string, AnyTable>>;
|
|
157
159
|
/** Changes whenever the generated schema changes. Used to detect missing migrations. */
|
|
158
160
|
readonly hash: string;
|
|
161
|
+
/** The same for the same config on every database (`hash` adds what only one has): for copies. */
|
|
162
|
+
readonly configHash: string;
|
|
159
163
|
/** Locales of localized fields, recorded with snapshots so a new default locale keeps data. */
|
|
160
164
|
readonly localization: SchemaLocalization | null;
|
|
165
|
+
/** Statistics on columns that depend on each other (Postgres), recorded with snapshots. */
|
|
166
|
+
readonly statistics: readonly SchemaStatistics[];
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Which values of the columns go together (`CREATE STATISTICS … (mcv)`): a field indexed with
|
|
170
|
+
* `status`. Without them Postgres takes the two as independent, and for a value whose documents
|
|
171
|
+
* are all drafts it expects published ones and reads a whole index looking for them.
|
|
172
|
+
*/
|
|
173
|
+
interface SchemaStatistics {
|
|
174
|
+
readonly name: string;
|
|
175
|
+
readonly table: string;
|
|
176
|
+
readonly columns: readonly string[];
|
|
161
177
|
}
|
|
162
178
|
interface SchemaLocalization {
|
|
163
179
|
readonly defaultLocale: string;
|
|
@@ -190,6 +206,11 @@ declare class Migrator {
|
|
|
190
206
|
private snapshotOf;
|
|
191
207
|
/** SQL statements that turn `prev` into `cur`, with the extensions their indexes need. */
|
|
192
208
|
private diff;
|
|
209
|
+
/**
|
|
210
|
+
* Statistics that `cur` adds or `prev` had and `cur` doesn't, on Postgres: dropped before the
|
|
211
|
+
* tables change, made after, then the tables analyzed so the planner has them at once.
|
|
212
|
+
*/
|
|
213
|
+
private statistics;
|
|
193
214
|
/** Renames become drop + add unless interactive. */
|
|
194
215
|
private changes;
|
|
195
216
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/database.ts
|
|
2
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
2
3
|
import { resolve } from "path";
|
|
3
4
|
import { QueryError as QueryError2 } from "@easy-cms/core";
|
|
4
5
|
import { and as and3, asc as asc3, count, eq as eq3, getTableColumns, getTableName, inArray as inArray3, sql as sql2 } from "drizzle-orm";
|
|
@@ -15,6 +16,7 @@ function getAt(data, path) {
|
|
|
15
16
|
return current;
|
|
16
17
|
}
|
|
17
18
|
function setAt(data, path, value) {
|
|
19
|
+
if (path.includes("__proto__")) throw new Error(`Invalid field path: ${path.join(".")}`);
|
|
18
20
|
let current = data;
|
|
19
21
|
for (const key of path.slice(0, -1)) {
|
|
20
22
|
if (typeof current[key] !== "object" || current[key] === null) current[key] = {};
|
|
@@ -213,7 +215,8 @@ function buildSchema(config, prefix, dialect) {
|
|
|
213
215
|
tableNames: /* @__PURE__ */ new Set([migrationsTableName(prefix)]),
|
|
214
216
|
// The hash covers the logical schema only, so it stays stable across releases for the same config.
|
|
215
217
|
description: [],
|
|
216
|
-
localization: config.localization
|
|
218
|
+
localization: config.localization,
|
|
219
|
+
statistics: []
|
|
217
220
|
};
|
|
218
221
|
const collections = /* @__PURE__ */ new Map();
|
|
219
222
|
const tables = {};
|
|
@@ -243,9 +246,19 @@ function buildSchema(config, prefix, dialect) {
|
|
|
243
246
|
);
|
|
244
247
|
tables[globalsName] = globals;
|
|
245
248
|
builder.description.push(["globals", globalsName]);
|
|
246
|
-
const
|
|
249
|
+
const hashOf = (description) => createHash("sha256").update(JSON.stringify(description)).digest("hex").slice(0, 16);
|
|
250
|
+
const configHash = hashOf(builder.description);
|
|
251
|
+
const hash = builder.statistics.length > 0 ? hashOf([...builder.description, ["statistics", builder.statistics.map((s) => s.name)]]) : configHash;
|
|
247
252
|
const localization = config.localization ? { defaultLocale: config.localization.defaultLocale, locales: config.localization.locales } : null;
|
|
248
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
collections,
|
|
255
|
+
globals,
|
|
256
|
+
tables,
|
|
257
|
+
hash,
|
|
258
|
+
configHash,
|
|
259
|
+
localization,
|
|
260
|
+
statistics: builder.statistics
|
|
261
|
+
};
|
|
249
262
|
}
|
|
250
263
|
function collectTables(model, into) {
|
|
251
264
|
into[model.name] = model.table;
|
|
@@ -402,7 +415,9 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
402
415
|
} else if (field.index === "trigram") {
|
|
403
416
|
indexes.push({ column, unique: false, trigram: true });
|
|
404
417
|
} else if (field.index || field.unique || field.type === "slug" || field.type === "relationship" || field.type === "upload") {
|
|
405
|
-
indexes.push(
|
|
418
|
+
indexes.push(
|
|
419
|
+
kind === "root" && options.drafts ? { column, unique: false, withStatus: true } : { column, unique: false }
|
|
420
|
+
);
|
|
406
421
|
}
|
|
407
422
|
}
|
|
408
423
|
}
|
|
@@ -412,7 +427,7 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
412
427
|
const table = dialect.table(
|
|
413
428
|
name,
|
|
414
429
|
columns,
|
|
415
|
-
(t) => indexes.flatMap(({ column, unique, within, trigram }) => {
|
|
430
|
+
(t) => indexes.flatMap(({ column, unique, within, trigram, withStatus }) => {
|
|
416
431
|
const col3 = t[column];
|
|
417
432
|
if (trigram)
|
|
418
433
|
return dialect.trigramIndex ? [dialect.trigramIndex(`${name}_${column}_trgm`).on(col3)] : [];
|
|
@@ -420,10 +435,19 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
420
435
|
const columns2 = [within].flat();
|
|
421
436
|
return dialect.uniqueIndex(`${name}_${columns2.join("_")}_${column}_unique`).on(...columns2.map((w) => t[w]), col3);
|
|
422
437
|
}
|
|
438
|
+
if (withStatus)
|
|
439
|
+
return dialect.index(`${name}_${column}_status_idx`).on(col3, t.status);
|
|
423
440
|
return unique ? dialect.uniqueIndex(`${name}_${column}_unique`).on(col3) : dialect.index(`${name}_${column}_idx`).on(col3);
|
|
424
441
|
})
|
|
425
442
|
);
|
|
426
443
|
builder.description.push([name, kind, described, indexes]);
|
|
444
|
+
for (const { column, withStatus } of indexes)
|
|
445
|
+
if (withStatus && dialect.name === "postgres")
|
|
446
|
+
builder.statistics.push({
|
|
447
|
+
name: `${name}_${column}_status_stats`,
|
|
448
|
+
table: name,
|
|
449
|
+
columns: [column, "status"]
|
|
450
|
+
});
|
|
427
451
|
const model = { name, kind, table, fields, columns: columnModels, children };
|
|
428
452
|
return options.valueField ? { ...model, valueField: options.valueField } : model;
|
|
429
453
|
}
|
|
@@ -486,14 +510,40 @@ var Migrator = class {
|
|
|
486
510
|
}
|
|
487
511
|
async snapshotOf(schema) {
|
|
488
512
|
const snapshot = await this.options.dialect.snapshot(schema ? schema.tables : {});
|
|
489
|
-
const meta = {
|
|
513
|
+
const meta = {
|
|
514
|
+
localization: schema?.localization ?? null,
|
|
515
|
+
statistics: schema?.statistics ?? []
|
|
516
|
+
};
|
|
490
517
|
return { ...snapshot, [META]: meta };
|
|
491
518
|
}
|
|
492
519
|
/** SQL statements that turn `prev` into `cur`, with the extensions their indexes need. */
|
|
493
520
|
async diff(prev, cur, interactive) {
|
|
494
|
-
const statements =
|
|
521
|
+
const statements = [
|
|
522
|
+
...this.statistics(prev, cur, "drop"),
|
|
523
|
+
...await this.changes(prev, cur, interactive),
|
|
524
|
+
...this.statistics(prev, cur, "create")
|
|
525
|
+
];
|
|
495
526
|
return statements.some((s) => s.includes("gin_trgm_ops")) ? ["CREATE EXTENSION IF NOT EXISTS pg_trgm", ...statements] : statements;
|
|
496
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Statistics that `cur` adds or `prev` had and `cur` doesn't, on Postgres: dropped before the
|
|
530
|
+
* tables change, made after, then the tables analyzed so the planner has them at once.
|
|
531
|
+
*/
|
|
532
|
+
statistics(prev, cur, step) {
|
|
533
|
+
if (this.options.dialect.name !== "postgres") return [];
|
|
534
|
+
const before = metaOf(prev)?.statistics ?? [];
|
|
535
|
+
const after = metaOf(cur)?.statistics ?? [];
|
|
536
|
+
const q = (name) => this.q(name);
|
|
537
|
+
if (step === "drop")
|
|
538
|
+
return before.filter((s) => !after.some((a) => a.name === s.name)).map((s) => `DROP STATISTICS IF EXISTS ${q(s.name)}`);
|
|
539
|
+
const added = after.filter((s) => !before.some((b) => b.name === s.name));
|
|
540
|
+
return [
|
|
541
|
+
...added.map(
|
|
542
|
+
(s) => `CREATE STATISTICS IF NOT EXISTS ${q(s.name)} (mcv) ON ${s.columns.map(q).join(", ")} FROM ${q(s.table)}`
|
|
543
|
+
),
|
|
544
|
+
...[...new Set(added.map((s) => s.table))].map((table) => `ANALYZE ${q(table)}`)
|
|
545
|
+
];
|
|
546
|
+
}
|
|
497
547
|
/** Renames become drop + add unless interactive. */
|
|
498
548
|
async changes(prev, cur, interactive) {
|
|
499
549
|
const { dialect } = this.options;
|
|
@@ -714,7 +764,7 @@ function extensionHint(error) {
|
|
|
714
764
|
|
|
715
765
|
// src/query.ts
|
|
716
766
|
import { QueryError } from "@easy-cms/core";
|
|
717
|
-
import { parseId } from "@easy-cms/core/internal";
|
|
767
|
+
import { parseId, TRIGRAM_LIKE_MIN } from "@easy-cms/core/internal";
|
|
718
768
|
import {
|
|
719
769
|
and as and2,
|
|
720
770
|
asc as asc2,
|
|
@@ -907,6 +957,10 @@ function operatorSQL(dialect, column, field, op, raw, path) {
|
|
|
907
957
|
if (typeof raw !== "string") throw new QueryError(`"${path}.like" must be a string`);
|
|
908
958
|
if (field && !LIKE_TYPES.has(field.type))
|
|
909
959
|
throw new QueryError(`"like" works on text fields, not "${path}"`);
|
|
960
|
+
if (field?.index === "trigram" && dialect.trigramIndex && [...raw].length < TRIGRAM_LIKE_MIN)
|
|
961
|
+
throw new QueryError(
|
|
962
|
+
`"${path}.like" needs at least ${TRIGRAM_LIKE_MIN} characters: its trigram index can't find shorter text`
|
|
963
|
+
);
|
|
910
964
|
return dialect.like(column, `%${escapeLike(raw)}%`);
|
|
911
965
|
case "exists":
|
|
912
966
|
return raw === false ? isNull(column) : isNotNull(column);
|
|
@@ -1134,11 +1188,10 @@ var DrizzleDatabase = class {
|
|
|
1134
1188
|
this.migrator = migrator;
|
|
1135
1189
|
this.tablePrefix = tablePrefix;
|
|
1136
1190
|
this.countCacheMs = countCacheMs;
|
|
1137
|
-
this.db = connection.db;
|
|
1138
1191
|
for (const [slug, model] of schema.collections) {
|
|
1139
1192
|
this.builders.set(
|
|
1140
1193
|
slug,
|
|
1141
|
-
new WhereBuilder(
|
|
1194
|
+
new WhereBuilder(connection.db, dialect, model.config.drafts === true, schema.localization)
|
|
1142
1195
|
);
|
|
1143
1196
|
}
|
|
1144
1197
|
}
|
|
@@ -1155,7 +1208,15 @@ var DrizzleDatabase = class {
|
|
|
1155
1208
|
* site they would all the time), so a total is up to that old, on every instance alike.
|
|
1156
1209
|
*/
|
|
1157
1210
|
counts = /* @__PURE__ */ new Map();
|
|
1158
|
-
|
|
1211
|
+
/** Set inside `writing`: queries there use the connections for writes. */
|
|
1212
|
+
writes = new AsyncLocalStorage();
|
|
1213
|
+
get db() {
|
|
1214
|
+
const { writeDb } = this.connection;
|
|
1215
|
+
return writeDb && this.writes.getStore() ? writeDb : this.connection.db;
|
|
1216
|
+
}
|
|
1217
|
+
writing(fn) {
|
|
1218
|
+
return this.connection.writeDb ? this.writes.run(true, fn) : fn();
|
|
1219
|
+
}
|
|
1159
1220
|
model(slug) {
|
|
1160
1221
|
const model = this.schema.collections.get(slug);
|
|
1161
1222
|
if (!model) throw new QueryError2(`Unknown collection "${slug}"`);
|
|
@@ -1382,7 +1443,7 @@ var DrizzleDatabase = class {
|
|
|
1382
1443
|
});
|
|
1383
1444
|
}
|
|
1384
1445
|
get transfer() {
|
|
1385
|
-
const { tables,
|
|
1446
|
+
const { tables, configHash } = this.schema;
|
|
1386
1447
|
const table = (name) => {
|
|
1387
1448
|
const found = tables[name];
|
|
1388
1449
|
if (!found) throw new QueryError2(`Unknown table "${name}"`);
|
|
@@ -1393,7 +1454,7 @@ var DrizzleDatabase = class {
|
|
|
1393
1454
|
return columns.id ?? columns.slug;
|
|
1394
1455
|
};
|
|
1395
1456
|
return {
|
|
1396
|
-
schemaHash:
|
|
1457
|
+
schemaHash: configHash,
|
|
1397
1458
|
tables: () => Object.keys(tables),
|
|
1398
1459
|
count: async (name) => {
|
|
1399
1460
|
const [row] = await this.db.select({ n: count() }).from(table(name));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/drizzle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.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.4"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
|
-
"@easy-cms/core": "^1.
|
|
26
|
+
"@easy-cms/core": "^1.16.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@easy-cms/core": "^1.
|
|
29
|
+
"@easy-cms/core": "^1.16.0"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|