@easy-cms/drizzle 1.9.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 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`. Renames become drop + add unless interactive. */
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.map(({ column, unique, within }) => {
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`. Renames become drop + add unless interactive. */
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
- await this.runner.transaction([
576
- ...statements.map((sql3) => ({ sql: sql3 })),
577
- this.record(DEV_PUSH, this.options.schema.hash, cur, true)
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";
@@ -1095,6 +1117,7 @@ async function connectDatabase(dialect, connection, args, options = {}) {
1095
1117
  });
1096
1118
  if (args.schema === "push") await migrator.push();
1097
1119
  else if (args.schema === "verify") await migrator.verify();
1120
+ if (dialect.name === "postgres" && usesTrigram(args.config)) await checkCtype(connection, args);
1098
1121
  const countCacheMs = (args.config.countCacheSeconds ?? 5) * 1e3;
1099
1122
  return new DrizzleDatabase(dialect, connection, schema, migrator, prefix, countCacheMs);
1100
1123
  } catch (error) {
@@ -1387,6 +1410,26 @@ var DrizzleDatabase = class {
1387
1410
  await this.connection.close();
1388
1411
  }
1389
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
+ }
1390
1433
  export {
1391
1434
  DEFAULT_MIGRATION_DIR,
1392
1435
  DEFAULT_TABLE_PREFIX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@easy-cms/drizzle",
3
- "version": "1.9.0",
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.9.0"
26
+ "@easy-cms/core": "^1.10.0"
27
27
  },
28
28
  "devDependencies": {
29
- "@easy-cms/core": "^1.9.0"
29
+ "@easy-cms/core": "^1.10.0"
30
30
  },
31
31
  "publishConfig": {
32
32
  "access": "public"