@easy-cms/drizzle 1.8.0 → 1.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 +7 -1
- package/dist/index.js +111 -7
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -43,6 +43,10 @@ interface Dialect {
|
|
|
43
43
|
uniqueIndex(name: string): {
|
|
44
44
|
on(...columns: AnyColumn[]): IndexBuilder;
|
|
45
45
|
};
|
|
46
|
+
/** An index for `like` on any substring (`index: 'trigram'`); without it, none is made. */
|
|
47
|
+
trigramIndex?(name: string): {
|
|
48
|
+
on(column: AnyColumn): IndexBuilder;
|
|
49
|
+
};
|
|
46
50
|
/** Auto-incrementing integer primary key. */
|
|
47
51
|
serial(name: string): ColumnBuilder;
|
|
48
52
|
text(name: string): ColumnBuilder;
|
|
@@ -184,8 +188,10 @@ declare class Migrator {
|
|
|
184
188
|
private get runner();
|
|
185
189
|
private q;
|
|
186
190
|
private snapshotOf;
|
|
187
|
-
/** SQL statements that turn `prev` into `cur
|
|
191
|
+
/** SQL statements that turn `prev` into `cur`, with the extensions their indexes need. */
|
|
188
192
|
private diff;
|
|
193
|
+
/** Renames become drop + add unless interactive. */
|
|
194
|
+
private changes;
|
|
189
195
|
/**
|
|
190
196
|
* A new default locale changes which locale the plain column holds (`title` is the default
|
|
191
197
|
* locale's, `title__en` another's). Moves the values so every locale keeps its own.
|
package/dist/index.js
CHANGED
|
@@ -399,6 +399,8 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
399
399
|
indexes.push({ column, unique: false });
|
|
400
400
|
} else if (topLevel && kind === "root" && (field.unique || field.type === "slug")) {
|
|
401
401
|
indexes.push({ column, unique: true });
|
|
402
|
+
} else if (field.index === "trigram") {
|
|
403
|
+
indexes.push({ column, unique: false, trigram: true });
|
|
402
404
|
} else if (field.index || field.unique || field.type === "slug" || field.type === "relationship" || field.type === "upload") {
|
|
403
405
|
indexes.push({ column, unique: false });
|
|
404
406
|
}
|
|
@@ -410,8 +412,10 @@ function buildTable(builder, name, kind, fields, options) {
|
|
|
410
412
|
const table = dialect.table(
|
|
411
413
|
name,
|
|
412
414
|
columns,
|
|
413
|
-
(t) => indexes.
|
|
415
|
+
(t) => indexes.flatMap(({ column, unique, within, trigram }) => {
|
|
414
416
|
const col3 = t[column];
|
|
417
|
+
if (trigram)
|
|
418
|
+
return dialect.trigramIndex ? [dialect.trigramIndex(`${name}_${column}_trgm`).on(col3)] : [];
|
|
415
419
|
if (within) {
|
|
416
420
|
const columns2 = [within].flat();
|
|
417
421
|
return dialect.uniqueIndex(`${name}_${columns2.join("_")}_${column}_unique`).on(...columns2.map((w) => t[w]), col3);
|
|
@@ -485,8 +489,13 @@ var Migrator = class {
|
|
|
485
489
|
const meta = { localization: schema?.localization ?? null };
|
|
486
490
|
return { ...snapshot, [META]: meta };
|
|
487
491
|
}
|
|
488
|
-
/** SQL statements that turn `prev` into `cur
|
|
492
|
+
/** SQL statements that turn `prev` into `cur`, with the extensions their indexes need. */
|
|
489
493
|
async diff(prev, cur, interactive) {
|
|
494
|
+
const statements = await this.changes(prev, cur, interactive);
|
|
495
|
+
return statements.some((s) => s.includes("gin_trgm_ops")) ? ["CREATE EXTENSION IF NOT EXISTS pg_trgm", ...statements] : statements;
|
|
496
|
+
}
|
|
497
|
+
/** Renames become drop + add unless interactive. */
|
|
498
|
+
async changes(prev, cur, interactive) {
|
|
490
499
|
const { dialect } = this.options;
|
|
491
500
|
const moves = this.localeMoves(prev, metaOf(prev), metaOf(cur));
|
|
492
501
|
const [from, to] = [withoutMeta(prev), withoutMeta(cur)];
|
|
@@ -572,10 +581,16 @@ var Migrator = class {
|
|
|
572
581
|
if (statements.some((s) => DESTRUCTIVE.test(s))) {
|
|
573
582
|
this.options.logger.warn("Schema push drops tables or columns; data in them is lost.");
|
|
574
583
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
584
|
+
try {
|
|
585
|
+
await this.runner.transaction([
|
|
586
|
+
...statements.map((sql3) => ({ sql: sql3 })),
|
|
587
|
+
this.record(DEV_PUSH, this.options.schema.hash, cur, true)
|
|
588
|
+
]);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
const hint = extensionHint(error);
|
|
591
|
+
if (hint) throw new SchemaError(`${error.message}${hint}`);
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
579
594
|
if (statements.length > 0)
|
|
580
595
|
this.options.logger.info(`Schema pushed (${statements.length} statements).`);
|
|
581
596
|
}
|
|
@@ -647,7 +662,7 @@ ${BREAKPOINT}
|
|
|
647
662
|
]);
|
|
648
663
|
} catch (error) {
|
|
649
664
|
throw new SchemaError(
|
|
650
|
-
`Migration ${migration.name} failed and was rolled back: ${error.message}`
|
|
665
|
+
`Migration ${migration.name} failed and was rolled back: ${error.message}${extensionHint(error)}`
|
|
651
666
|
);
|
|
652
667
|
}
|
|
653
668
|
this.options.logger.info(`Applied ${migration.name}`);
|
|
@@ -689,6 +704,13 @@ function timestamp() {
|
|
|
689
704
|
const pad = (n) => String(n).padStart(2, "0");
|
|
690
705
|
return `${d.getUTCFullYear()}${pad(d.getUTCMonth() + 1)}${pad(d.getUTCDate())}${pad(d.getUTCHours())}${pad(d.getUTCMinutes())}${pad(d.getUTCSeconds())}`;
|
|
691
706
|
}
|
|
707
|
+
function extensionHint(error) {
|
|
708
|
+
const match = /permission denied to create extension "([^"]+)"/.exec(
|
|
709
|
+
error?.message ?? ""
|
|
710
|
+
);
|
|
711
|
+
if (!match) return "";
|
|
712
|
+
return `. Ask the database's owner to run, once: CREATE EXTENSION IF NOT EXISTS ${match[1]} (with SCHEMA <yours> when this role's search_path is a schema of its own).`;
|
|
713
|
+
}
|
|
692
714
|
|
|
693
715
|
// src/query.ts
|
|
694
716
|
import { QueryError } from "@easy-cms/core";
|
|
@@ -1031,6 +1053,49 @@ var WhereBuilder = class {
|
|
|
1031
1053
|
order.push(desc(col2(model, "id")));
|
|
1032
1054
|
return order;
|
|
1033
1055
|
}
|
|
1056
|
+
/**
|
|
1057
|
+
* The columns a cursor goes by, in `orderBy`'s order: the sort's, then `id`. Only plain
|
|
1058
|
+
* columns: a sort on a value inside blocks has nothing to compare a cursor with.
|
|
1059
|
+
*/
|
|
1060
|
+
keyset(model, sort) {
|
|
1061
|
+
const keys = sort.map((entry) => {
|
|
1062
|
+
const descending = entry.startsWith("-");
|
|
1063
|
+
const path = descending ? entry.slice(1) : entry;
|
|
1064
|
+
const target = resolvePath(model, path.split("."), this.drafts, this.localization);
|
|
1065
|
+
if (target.kind !== "column")
|
|
1066
|
+
throw new QueryError(
|
|
1067
|
+
`A cursor can't follow a sort by "${path}"; sort by a field of the document, or use page`
|
|
1068
|
+
);
|
|
1069
|
+
return { column: target.column, descending };
|
|
1070
|
+
});
|
|
1071
|
+
keys.push({ column: col2(model, "id"), descending: true });
|
|
1072
|
+
return keys;
|
|
1073
|
+
}
|
|
1074
|
+
/**
|
|
1075
|
+
* Rows after `values` (the keys of the last row of the page before) in the keyset's order.
|
|
1076
|
+
* Where nulls go follows the database's default order, as `orderBy` leaves it: last in
|
|
1077
|
+
* ascending order on Postgres, first on SQLite.
|
|
1078
|
+
*/
|
|
1079
|
+
after(keys, values) {
|
|
1080
|
+
const nullsHigh = this.dialect.name === "postgres";
|
|
1081
|
+
const equal = (key, value) => value === null ? isNull(key.column) : eq2(key.column, value);
|
|
1082
|
+
const later = (key, value) => {
|
|
1083
|
+
const nullsLater = nullsHigh !== key.descending;
|
|
1084
|
+
if (value === null) return nullsLater ? sql`1 = 0` : isNotNull(key.column);
|
|
1085
|
+
const beyond = key.descending ? lt(key.column, value) : gt(key.column, value);
|
|
1086
|
+
return nullsLater ? or(beyond, isNull(key.column)) : beyond;
|
|
1087
|
+
};
|
|
1088
|
+
const branches = keys.map(
|
|
1089
|
+
(key, i) => and2(...keys.slice(0, i).map((k, j) => equal(k, values[j])), later(key, values[i]))
|
|
1090
|
+
);
|
|
1091
|
+
const rest = or(...branches);
|
|
1092
|
+
const [first] = keys;
|
|
1093
|
+
const start = values[0];
|
|
1094
|
+
if (!first || start === null || start === void 0) return rest;
|
|
1095
|
+
const from = first.descending ? lte(first.column, start) : gte(first.column, start);
|
|
1096
|
+
const bound = nullsHigh !== first.descending ? or(from, isNull(first.column)) : from;
|
|
1097
|
+
return and2(bound, rest);
|
|
1098
|
+
}
|
|
1034
1099
|
};
|
|
1035
1100
|
|
|
1036
1101
|
// src/database.ts
|
|
@@ -1052,6 +1117,7 @@ async function connectDatabase(dialect, connection, args, options = {}) {
|
|
|
1052
1117
|
});
|
|
1053
1118
|
if (args.schema === "push") await migrator.push();
|
|
1054
1119
|
else if (args.schema === "verify") await migrator.verify();
|
|
1120
|
+
if (dialect.name === "postgres" && usesTrigram(args.config)) await checkCtype(connection, args);
|
|
1055
1121
|
const countCacheMs = (args.config.countCacheSeconds ?? 5) * 1e3;
|
|
1056
1122
|
return new DrizzleDatabase(dialect, connection, schema, migrator, prefix, countCacheMs);
|
|
1057
1123
|
} catch (error) {
|
|
@@ -1132,6 +1198,24 @@ var DrizzleDatabase = class {
|
|
|
1132
1198
|
hasPrevPage: args.page > 1
|
|
1133
1199
|
};
|
|
1134
1200
|
}
|
|
1201
|
+
async findAfter(args) {
|
|
1202
|
+
const model = this.model(args.collection);
|
|
1203
|
+
const builder = this.builder(args.collection);
|
|
1204
|
+
const keys = builder.keyset(model.root, args.sort);
|
|
1205
|
+
const condition = this.where(args.collection, args.where);
|
|
1206
|
+
const after = args.after ? builder.after(keys, args.after) : void 0;
|
|
1207
|
+
const columns = Object.entries(getTableColumns(model.root.table));
|
|
1208
|
+
const names = keys.map(({ column }) => columns.find(([, c]) => c === column)?.[0]);
|
|
1209
|
+
const rows = await this.db.select().from(model.root.table).where(and3(condition, after)).orderBy(...builder.orderBy(model.root, args.sort)).limit(args.limit + 1);
|
|
1210
|
+
const more = rows.length > args.limit;
|
|
1211
|
+
const page = more ? rows.slice(0, args.limit) : rows;
|
|
1212
|
+
const last = page.at(-1);
|
|
1213
|
+
return {
|
|
1214
|
+
docs: await this.load(model, page),
|
|
1215
|
+
hasNextPage: more,
|
|
1216
|
+
next: last ? names.map((name) => last[name] ?? null) : null
|
|
1217
|
+
};
|
|
1218
|
+
}
|
|
1135
1219
|
async findRows(args, take) {
|
|
1136
1220
|
const model = this.model(args.collection);
|
|
1137
1221
|
const condition = this.where(args.collection, args.where);
|
|
@@ -1326,6 +1410,26 @@ var DrizzleDatabase = class {
|
|
|
1326
1410
|
await this.connection.close();
|
|
1327
1411
|
}
|
|
1328
1412
|
};
|
|
1413
|
+
function usesTrigram(config) {
|
|
1414
|
+
const search = (fields) => fields.some((field) => {
|
|
1415
|
+
const f = field;
|
|
1416
|
+
return f.index === "trigram" || (f.fields ? search(f.fields) : false) || (f.blocks ? f.blocks.some((b) => search(b.fields)) : false) || (f.tabs ? f.tabs.some((t) => search(t.fields)) : false);
|
|
1417
|
+
});
|
|
1418
|
+
return [...config.collections, ...config.globals].some((c) => search(c.fields));
|
|
1419
|
+
}
|
|
1420
|
+
async function checkCtype(connection, args) {
|
|
1421
|
+
try {
|
|
1422
|
+
const [row] = await connection.runner.query(
|
|
1423
|
+
"SELECT datctype FROM pg_database WHERE datname = current_database()"
|
|
1424
|
+
);
|
|
1425
|
+
const ctype = String(row?.datctype ?? "");
|
|
1426
|
+
if (ctype === "C" || ctype === "POSIX")
|
|
1427
|
+
args.logger.warn(
|
|
1428
|
+
`index: 'trigram' can't help searching Thai or other non-ASCII text in this database: its LC_CTYPE is ${ctype}. Create the database with LC_CTYPE 'C.UTF-8', 'en_US.UTF-8' or 'th_TH.UTF-8'.`
|
|
1429
|
+
);
|
|
1430
|
+
} catch {
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1329
1433
|
export {
|
|
1330
1434
|
DEFAULT_MIGRATION_DIR,
|
|
1331
1435
|
DEFAULT_TABLE_PREFIX,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@easy-cms/drizzle",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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.10.0"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@easy-cms/core": "^1.
|
|
29
|
+
"@easy-cms/core": "^1.10.0"
|
|
30
30
|
},
|
|
31
31
|
"publishConfig": {
|
|
32
32
|
"access": "public"
|