@easy-cms/drizzle 1.15.1 → 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 +73 -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";
|
|
@@ -214,7 +215,8 @@ function buildSchema(config, prefix, dialect) {
|
|
|
214
215
|
tableNames: /* @__PURE__ */ new Set([migrationsTableName(prefix)]),
|
|
215
216
|
// The hash covers the logical schema only, so it stays stable across releases for the same config.
|
|
216
217
|
description: [],
|
|
217
|
-
localization: config.localization
|
|
218
|
+
localization: config.localization,
|
|
219
|
+
statistics: []
|
|
218
220
|
};
|
|
219
221
|
const collections = /* @__PURE__ */ new Map();
|
|
220
222
|
const tables = {};
|
|
@@ -244,9 +246,19 @@ function buildSchema(config, prefix, dialect) {
|
|
|
244
246
|
);
|
|
245
247
|
tables[globalsName] = globals;
|
|
246
248
|
builder.description.push(["globals", globalsName]);
|
|
247
|
-
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;
|
|
248
252
|
const localization = config.localization ? { defaultLocale: config.localization.defaultLocale, locales: config.localization.locales } : null;
|
|
249
|
-
return {
|
|
253
|
+
return {
|
|
254
|
+
collections,
|
|
255
|
+
globals,
|
|
256
|
+
tables,
|
|
257
|
+
hash,
|
|
258
|
+
configHash,
|
|
259
|
+
localization,
|
|
260
|
+
statistics: builder.statistics
|
|
261
|
+
};
|
|
250
262
|
}
|
|
251
263
|
function collectTables(model, into) {
|
|
252
264
|
into[model.name] = model.table;
|
|
@@ -403,7 +415,9 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
403
415
|
} else if (field.index === "trigram") {
|
|
404
416
|
indexes.push({ column, unique: false, trigram: true });
|
|
405
417
|
} else if (field.index || field.unique || field.type === "slug" || field.type === "relationship" || field.type === "upload") {
|
|
406
|
-
indexes.push(
|
|
418
|
+
indexes.push(
|
|
419
|
+
kind === "root" && options.drafts ? { column, unique: false, withStatus: true } : { column, unique: false }
|
|
420
|
+
);
|
|
407
421
|
}
|
|
408
422
|
}
|
|
409
423
|
}
|
|
@@ -413,7 +427,7 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
413
427
|
const table = dialect.table(
|
|
414
428
|
name,
|
|
415
429
|
columns,
|
|
416
|
-
(t) => indexes.flatMap(({ column, unique, within, trigram }) => {
|
|
430
|
+
(t) => indexes.flatMap(({ column, unique, within, trigram, withStatus }) => {
|
|
417
431
|
const col3 = t[column];
|
|
418
432
|
if (trigram)
|
|
419
433
|
return dialect.trigramIndex ? [dialect.trigramIndex(`${name}_${column}_trgm`).on(col3)] : [];
|
|
@@ -421,10 +435,19 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
421
435
|
const columns2 = [within].flat();
|
|
422
436
|
return dialect.uniqueIndex(`${name}_${columns2.join("_")}_${column}_unique`).on(...columns2.map((w) => t[w]), col3);
|
|
423
437
|
}
|
|
438
|
+
if (withStatus)
|
|
439
|
+
return dialect.index(`${name}_${column}_status_idx`).on(col3, t.status);
|
|
424
440
|
return unique ? dialect.uniqueIndex(`${name}_${column}_unique`).on(col3) : dialect.index(`${name}_${column}_idx`).on(col3);
|
|
425
441
|
})
|
|
426
442
|
);
|
|
427
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
|
+
});
|
|
428
451
|
const model = { name, kind, table, fields, columns: columnModels, children };
|
|
429
452
|
return options.valueField ? { ...model, valueField: options.valueField } : model;
|
|
430
453
|
}
|
|
@@ -487,14 +510,40 @@ var Migrator = class {
|
|
|
487
510
|
}
|
|
488
511
|
async snapshotOf(schema) {
|
|
489
512
|
const snapshot = await this.options.dialect.snapshot(schema ? schema.tables : {});
|
|
490
|
-
const meta = {
|
|
513
|
+
const meta = {
|
|
514
|
+
localization: schema?.localization ?? null,
|
|
515
|
+
statistics: schema?.statistics ?? []
|
|
516
|
+
};
|
|
491
517
|
return { ...snapshot, [META]: meta };
|
|
492
518
|
}
|
|
493
519
|
/** SQL statements that turn `prev` into `cur`, with the extensions their indexes need. */
|
|
494
520
|
async diff(prev, cur, interactive) {
|
|
495
|
-
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
|
+
];
|
|
496
526
|
return statements.some((s) => s.includes("gin_trgm_ops")) ? ["CREATE EXTENSION IF NOT EXISTS pg_trgm", ...statements] : statements;
|
|
497
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
|
+
}
|
|
498
547
|
/** Renames become drop + add unless interactive. */
|
|
499
548
|
async changes(prev, cur, interactive) {
|
|
500
549
|
const { dialect } = this.options;
|
|
@@ -715,7 +764,7 @@ function extensionHint(error) {
|
|
|
715
764
|
|
|
716
765
|
// src/query.ts
|
|
717
766
|
import { QueryError } from "@easy-cms/core";
|
|
718
|
-
import { parseId } from "@easy-cms/core/internal";
|
|
767
|
+
import { parseId, TRIGRAM_LIKE_MIN } from "@easy-cms/core/internal";
|
|
719
768
|
import {
|
|
720
769
|
and as and2,
|
|
721
770
|
asc as asc2,
|
|
@@ -908,6 +957,10 @@ function operatorSQL(dialect, column, field, op, raw, path) {
|
|
|
908
957
|
if (typeof raw !== "string") throw new QueryError(`"${path}.like" must be a string`);
|
|
909
958
|
if (field && !LIKE_TYPES.has(field.type))
|
|
910
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
|
+
);
|
|
911
964
|
return dialect.like(column, `%${escapeLike(raw)}%`);
|
|
912
965
|
case "exists":
|
|
913
966
|
return raw === false ? isNull(column) : isNotNull(column);
|
|
@@ -1135,11 +1188,10 @@ var DrizzleDatabase = class {
|
|
|
1135
1188
|
this.migrator = migrator;
|
|
1136
1189
|
this.tablePrefix = tablePrefix;
|
|
1137
1190
|
this.countCacheMs = countCacheMs;
|
|
1138
|
-
this.db = connection.db;
|
|
1139
1191
|
for (const [slug, model] of schema.collections) {
|
|
1140
1192
|
this.builders.set(
|
|
1141
1193
|
slug,
|
|
1142
|
-
new WhereBuilder(
|
|
1194
|
+
new WhereBuilder(connection.db, dialect, model.config.drafts === true, schema.localization)
|
|
1143
1195
|
);
|
|
1144
1196
|
}
|
|
1145
1197
|
}
|
|
@@ -1156,7 +1208,15 @@ var DrizzleDatabase = class {
|
|
|
1156
1208
|
* site they would all the time), so a total is up to that old, on every instance alike.
|
|
1157
1209
|
*/
|
|
1158
1210
|
counts = /* @__PURE__ */ new Map();
|
|
1159
|
-
|
|
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
|
+
}
|
|
1160
1220
|
model(slug) {
|
|
1161
1221
|
const model = this.schema.collections.get(slug);
|
|
1162
1222
|
if (!model) throw new QueryError2(`Unknown collection "${slug}"`);
|
|
@@ -1383,7 +1443,7 @@ var DrizzleDatabase = class {
|
|
|
1383
1443
|
});
|
|
1384
1444
|
}
|
|
1385
1445
|
get transfer() {
|
|
1386
|
-
const { tables,
|
|
1446
|
+
const { tables, configHash } = this.schema;
|
|
1387
1447
|
const table = (name) => {
|
|
1388
1448
|
const found = tables[name];
|
|
1389
1449
|
if (!found) throw new QueryError2(`Unknown table "${name}"`);
|
|
@@ -1394,7 +1454,7 @@ var DrizzleDatabase = class {
|
|
|
1394
1454
|
return columns.id ?? columns.slug;
|
|
1395
1455
|
};
|
|
1396
1456
|
return {
|
|
1397
|
-
schemaHash:
|
|
1457
|
+
schemaHash: configHash,
|
|
1398
1458
|
tables: () => Object.keys(tables),
|
|
1399
1459
|
count: async (name) => {
|
|
1400
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"
|