@hozon/db 0.2.1 → 0.2.2

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
@@ -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,6 +26,7 @@ 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.
@@ -32,7 +35,7 @@ export class HozonDB {
32
35
  new ParseJSONResultsPlugin()
33
36
  ]
34
37
  });
35
- this.#storeDB = this.#db.withPlugin(new TablePrefixPlugin(this.#tablePrefix));
38
+ this.#storeDB = this.#db.withPlugin(this.#defaultPlugin);
36
39
  this.#logger = params.logger ?? getLogger([
37
40
  'hozon',
38
41
  'db'
@@ -63,7 +66,8 @@ export class HozonDB {
63
66
  if (store == null) {
64
67
  throw new Error(`Store "${name}" is not registered`);
65
68
  }
66
- this.#storeAPIs.set(name, store.createAPI(this.#storeDB, this.#adapter));
69
+ const storeDB = store.unprefixedTables == null ? this.#storeDB : this.#db.withPlugin(this.#getStorePlugin(store));
70
+ this.#storeAPIs.set(name, store.createAPI(storeDB, this.#adapter));
67
71
  }
68
72
  return this.#storeAPIs.get(name);
69
73
  }
@@ -95,8 +99,10 @@ export class HozonDB {
95
99
  let result;
96
100
  let savepointID = 0;
97
101
  try {
98
- result = await this.#storeDB.transaction().execute(async (trx)=>{
102
+ // The transaction runs on the unprefixed base so each store applies its own plugin.
103
+ result = await this.#db.transaction().execute(async (trx)=>{
99
104
  const txKysely = trx;
105
+ const txStoreDB = txKysely.withPlugin(this.#defaultPlugin);
100
106
  const txStoreAPIs = new Map();
101
107
  const createScopedProvider = (scopedCommitHooks, scopedRollbackHooks)=>{
102
108
  // Savepoints on one connection form a stack: a scope holds at most
@@ -110,7 +116,8 @@ export class HozonDB {
110
116
  if (store == null) {
111
117
  throw new Error(`Store "${name}" is not registered`);
112
118
  }
113
- txStoreAPIs.set(name, store.createAPI(txKysely, this.#adapter));
119
+ const storeDB = store.unprefixedTables == null ? txStoreDB : txKysely.withPlugin(this.#getStorePlugin(store));
120
+ txStoreAPIs.set(name, store.createAPI(storeDB, this.#adapter));
114
121
  }
115
122
  return txStoreAPIs.get(name);
116
123
  },
@@ -217,7 +224,7 @@ export class HozonDB {
217
224
  types: this.#adapter.types,
218
225
  functions: this.#adapter.functions
219
226
  }) : store.migrations;
220
- const plugin = new TablePrefixPlugin(this.#tablePrefix);
227
+ const plugin = this.#getStorePlugin(store);
221
228
  migrations = Object.fromEntries(Object.entries(storeMigrations).map(([name, migration])=>{
222
229
  const down = migration.down;
223
230
  return [
@@ -232,6 +239,17 @@ export class HozonDB {
232
239
  }
233
240
  return migrations;
234
241
  }
242
+ #getStorePlugin(store) {
243
+ if (store.unprefixedTables == null) return this.#defaultPlugin;
244
+ let plugin = this.#storePlugins.get(store.name);
245
+ if (plugin == null) {
246
+ plugin = new TablePrefixPlugin(this.#tablePrefix, {
247
+ unprefixed: store.unprefixedTables
248
+ });
249
+ this.#storePlugins.set(store.name, plugin);
250
+ }
251
+ return plugin;
252
+ }
235
253
  #ensurePreflight() {
236
254
  // Snapshot before awaiting so concurrent triggers share one read-only gate.
237
255
  // 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.2",
4
4
  "description": "Database registry, migrations and transactions for Hozon",
5
5
  "repository": {
6
6
  "type": "git",