@hozon/db 0.2.1 → 0.2.3

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/lib/db.d.ts CHANGED
@@ -12,6 +12,12 @@ export type StoreDefinition<Tables, API> = {
12
12
  name: string;
13
13
  migrations: Record<string, Migration> | ((ctx: MigrationContext) => Record<string, Migration>);
14
14
  dependsOn?: Array<string>;
15
+ /**
16
+ * Logical table names this store keeps outside the table prefix, for example
17
+ * dynamic tables whose names would exceed identifier length limits. Matching
18
+ * tables are not namespaced: the store must keep them collision-free.
19
+ */
20
+ unprefixedTables?: (name: string) => boolean;
15
21
  createAPI: (db: Kysely<Tables>, adapter: Adapter) => API;
16
22
  };
17
23
  export type StoreProvider<Stores extends Record<string, unknown> = Record<string, unknown>> = {
package/lib/db.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getLogger } from '@sozai/log';
2
- import { Kysely, ParseJSONResultsPlugin, sql } from 'kysely';
2
+ import { Kysely, sql } from 'kysely';
3
3
  import { Migrator } from 'kysely/migration';
4
4
  import { withTransactionalDDL } from './dialect.js';
5
5
  import { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError } from './errors.js';
@@ -7,6 +7,7 @@ import { checkStore } from './preflight.js';
7
7
  import { TablePrefixPlugin } from './table-prefix.js';
8
8
  export class HozonDB {
9
9
  #tablePrefix;
10
+ #defaultPlugin;
10
11
  #closed = false;
11
12
  #closing = null;
12
13
  #preflight = null;
@@ -15,6 +16,7 @@ export class HozonDB {
15
16
  #adapter;
16
17
  #db;
17
18
  #storeDB;
19
+ #storePlugins = new Map();
18
20
  #logger;
19
21
  #migrated = new Map();
20
22
  #storeAPIs = new Map();
@@ -24,15 +26,13 @@ export class HozonDB {
24
26
  if (!/^[a-z][a-z0-9_]{0,30}$/.test(this.#tablePrefix)) {
25
27
  throw new InvalidTablePrefixError(this.#tablePrefix);
26
28
  }
29
+ this.#defaultPlugin = new TablePrefixPlugin(this.#tablePrefix);
27
30
  this.#adapter = params.adapter;
28
31
  this.#db = new Kysely({
29
32
  // SQLite supports transactional DDL, but Kysely's SQLite adapters do not report it.
30
- dialect: this.#adapter.kind === 'sqlite' ? withTransactionalDDL(this.#adapter.dialect) : this.#adapter.dialect,
31
- plugins: [
32
- new ParseJSONResultsPlugin()
33
- ]
33
+ dialect: this.#adapter.kind === 'sqlite' ? withTransactionalDDL(this.#adapter.dialect) : this.#adapter.dialect
34
34
  });
35
- this.#storeDB = this.#db.withPlugin(new TablePrefixPlugin(this.#tablePrefix));
35
+ this.#storeDB = this.#db.withPlugin(this.#defaultPlugin);
36
36
  this.#logger = params.logger ?? getLogger([
37
37
  'hozon',
38
38
  'db'
@@ -63,7 +63,8 @@ export class HozonDB {
63
63
  if (store == null) {
64
64
  throw new Error(`Store "${name}" is not registered`);
65
65
  }
66
- this.#storeAPIs.set(name, store.createAPI(this.#storeDB, this.#adapter));
66
+ const storeDB = store.unprefixedTables == null ? this.#storeDB : this.#db.withPlugin(this.#getStorePlugin(store));
67
+ this.#storeAPIs.set(name, store.createAPI(storeDB, this.#adapter));
67
68
  }
68
69
  return this.#storeAPIs.get(name);
69
70
  }
@@ -95,8 +96,10 @@ export class HozonDB {
95
96
  let result;
96
97
  let savepointID = 0;
97
98
  try {
98
- result = await this.#storeDB.transaction().execute(async (trx)=>{
99
+ // The transaction runs on the unprefixed base so each store applies its own plugin.
100
+ result = await this.#db.transaction().execute(async (trx)=>{
99
101
  const txKysely = trx;
102
+ const txStoreDB = txKysely.withPlugin(this.#defaultPlugin);
100
103
  const txStoreAPIs = new Map();
101
104
  const createScopedProvider = (scopedCommitHooks, scopedRollbackHooks)=>{
102
105
  // Savepoints on one connection form a stack: a scope holds at most
@@ -110,7 +113,8 @@ export class HozonDB {
110
113
  if (store == null) {
111
114
  throw new Error(`Store "${name}" is not registered`);
112
115
  }
113
- txStoreAPIs.set(name, store.createAPI(txKysely, this.#adapter));
116
+ const storeDB = store.unprefixedTables == null ? txStoreDB : txKysely.withPlugin(this.#getStorePlugin(store));
117
+ txStoreAPIs.set(name, store.createAPI(storeDB, this.#adapter));
114
118
  }
115
119
  return txStoreAPIs.get(name);
116
120
  },
@@ -217,7 +221,7 @@ export class HozonDB {
217
221
  types: this.#adapter.types,
218
222
  functions: this.#adapter.functions
219
223
  }) : store.migrations;
220
- const plugin = new TablePrefixPlugin(this.#tablePrefix);
224
+ const plugin = this.#getStorePlugin(store);
221
225
  migrations = Object.fromEntries(Object.entries(storeMigrations).map(([name, migration])=>{
222
226
  const down = migration.down;
223
227
  return [
@@ -232,6 +236,17 @@ export class HozonDB {
232
236
  }
233
237
  return migrations;
234
238
  }
239
+ #getStorePlugin(store) {
240
+ if (store.unprefixedTables == null) return this.#defaultPlugin;
241
+ let plugin = this.#storePlugins.get(store.name);
242
+ if (plugin == null) {
243
+ plugin = new TablePrefixPlugin(this.#tablePrefix, {
244
+ unprefixed: store.unprefixedTables
245
+ });
246
+ this.#storePlugins.set(store.name, plugin);
247
+ }
248
+ return plugin;
249
+ }
235
250
  #ensurePreflight() {
236
251
  // Snapshot before awaiting so concurrent triggers share one read-only gate.
237
252
  // A failed attempt clears the gate so the next caller reruns the read-only checks,
package/lib/index.d.ts CHANGED
@@ -6,4 +6,4 @@ export { HozonDB } from './db.js';
6
6
  export { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError, } from './errors.js';
7
7
  export { chunk, withKeepSet } from './keep-set.js';
8
8
  export { withStoreTransaction } from './store-transaction.js';
9
- export { TablePrefixPlugin } from './table-prefix.js';
9
+ export { type TablePrefixOptions, TablePrefixPlugin } from './table-prefix.js';
@@ -1,7 +1,11 @@
1
1
  import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs, QueryResult, RootOperationNode, UnknownRow } from 'kysely';
2
+ export type TablePrefixOptions = {
3
+ /** Logical table names to leave unprefixed, e.g. dynamic tables near identifier length limits. */
4
+ unprefixed?: (name: string) => boolean;
5
+ };
2
6
  export declare class TablePrefixPlugin implements KyselyPlugin {
3
7
  #private;
4
- constructor(prefix: string);
8
+ constructor(prefix: string, options?: TablePrefixOptions);
5
9
  transformQuery({ node, queryId }: PluginTransformQueryArgs): RootOperationNode;
6
10
  transformResult({ result }: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
7
11
  }
@@ -26,15 +26,17 @@ function isRootOperationNode(node) {
26
26
  }
27
27
  class TablePrefixTransformer extends OperationNodeTransformer {
28
28
  #prefix;
29
+ #unprefixed;
29
30
  #tables = new Set();
30
31
  #ctes = new Set();
31
32
  #aliases = new Set();
32
33
  #writeTargets = new Set();
33
34
  #writeNames = new Set();
34
35
  #prefixedIdentifiers = new WeakSet();
35
- constructor(prefix){
36
+ constructor(prefix, options = {}){
36
37
  super();
37
38
  this.#prefix = prefix;
39
+ this.#unprefixed = options.unprefixed;
38
40
  }
39
41
  transformNodeImpl(node, queryId) {
40
42
  if (!isRootOperationNode(node) || node.kind === 'RawNode' && this.nodeStack.length > 1) {
@@ -90,6 +92,7 @@ class TablePrefixTransformer extends OperationNodeTransformer {
90
92
  #prefixTable(node) {
91
93
  if (node.table.schema !== undefined || this.#prefixedIdentifiers.has(node.table.identifier)) return node;
92
94
  const name = node.table.identifier.name;
95
+ if (this.#unprefixed?.(name)) return node;
93
96
  const transformed = TableNode.create(`${this.#prefix}_${name}`);
94
97
  this.#prefixedIdentifiers.add(transformed.table.identifier);
95
98
  return transformed;
@@ -235,8 +238,8 @@ class TablePrefixTransformer extends OperationNodeTransformer {
235
238
  }
236
239
  export class TablePrefixPlugin {
237
240
  #transformer;
238
- constructor(prefix){
239
- this.#transformer = new TablePrefixTransformer(prefix);
241
+ constructor(prefix, options){
242
+ this.#transformer = new TablePrefixTransformer(prefix, options);
240
243
  }
241
244
  transformQuery({ node, queryId }) {
242
245
  return this.#transformer.transformNode(node, queryId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hozon/db",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Database registry, migrations and transactions for Hozon",
5
5
  "repository": {
6
6
  "type": "git",