@hozon/db 0.1.0 → 0.2.1

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
@@ -3,6 +3,7 @@ import { type Logger } from '@sozai/log';
3
3
  import { Kysely } from 'kysely';
4
4
  import { type Migration } from 'kysely/migration';
5
5
  export type MigrationContext = {
6
+ tablePrefix: string;
6
7
  kind: Adapter['kind'];
7
8
  types: ColumnTypes;
8
9
  functions: Functions;
package/lib/db.js CHANGED
@@ -4,6 +4,7 @@ import { Migrator } from 'kysely/migration';
4
4
  import { withTransactionalDDL } from './dialect.js';
5
5
  import { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError } from './errors.js';
6
6
  import { checkStore } from './preflight.js';
7
+ import { TablePrefixPlugin } from './table-prefix.js';
7
8
  export class HozonDB {
8
9
  #tablePrefix;
9
10
  #closed = false;
@@ -13,6 +14,7 @@ export class HozonDB {
13
14
  #migrations = new Map();
14
15
  #adapter;
15
16
  #db;
17
+ #storeDB;
16
18
  #logger;
17
19
  #migrated = new Map();
18
20
  #storeAPIs = new Map();
@@ -30,6 +32,7 @@ export class HozonDB {
30
32
  new ParseJSONResultsPlugin()
31
33
  ]
32
34
  });
35
+ this.#storeDB = this.#db.withPlugin(new TablePrefixPlugin(this.#tablePrefix));
33
36
  this.#logger = params.logger ?? getLogger([
34
37
  'hozon',
35
38
  'db'
@@ -60,7 +63,7 @@ export class HozonDB {
60
63
  if (store == null) {
61
64
  throw new Error(`Store "${name}" is not registered`);
62
65
  }
63
- this.#storeAPIs.set(name, store.createAPI(this.#db, this.#adapter));
66
+ this.#storeAPIs.set(name, store.createAPI(this.#storeDB, this.#adapter));
64
67
  }
65
68
  return this.#storeAPIs.get(name);
66
69
  }
@@ -92,7 +95,7 @@ export class HozonDB {
92
95
  let result;
93
96
  let savepointID = 0;
94
97
  try {
95
- result = await this.#db.transaction().execute(async (trx)=>{
98
+ result = await this.#storeDB.transaction().execute(async (trx)=>{
96
99
  const txKysely = trx;
97
100
  const txStoreAPIs = new Map();
98
101
  const createScopedProvider = (scopedCommitHooks, scopedRollbackHooks)=>{
@@ -208,11 +211,23 @@ export class HozonDB {
208
211
  #getMigrations(store) {
209
212
  let migrations = this.#migrations.get(store.name);
210
213
  if (migrations == null) {
211
- migrations = typeof store.migrations === 'function' ? store.migrations({
214
+ const storeMigrations = typeof store.migrations === 'function' ? store.migrations({
215
+ tablePrefix: this.#tablePrefix,
212
216
  kind: this.#adapter.kind,
213
217
  types: this.#adapter.types,
214
218
  functions: this.#adapter.functions
215
219
  }) : store.migrations;
220
+ const plugin = new TablePrefixPlugin(this.#tablePrefix);
221
+ migrations = Object.fromEntries(Object.entries(storeMigrations).map(([name, migration])=>{
222
+ const down = migration.down;
223
+ return [
224
+ name,
225
+ {
226
+ up: (db)=>migration.up(db.withPlugin(plugin)),
227
+ down: down && ((db)=>down.call(migration, db.withPlugin(plugin)))
228
+ }
229
+ ];
230
+ }));
216
231
  this.#migrations.set(store.name, migrations);
217
232
  }
218
233
  return migrations;
package/lib/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
+ export type { ColumnType, Expression, Generated, Insertable, RawBuilder, Selectable, SelectQueryBuilder, Transaction, Updateable, } from 'kysely';
2
+ export { Kysely, sql } from 'kysely';
3
+ export type { Migration } from 'kysely/migration';
1
4
  export type { HozonDBParams, MigrationContext, StoreDefinition, StoreProvider } from './db.js';
2
5
  export { HozonDB } from './db.js';
3
6
  export { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError, } from './errors.js';
4
7
  export { chunk, withKeepSet } from './keep-set.js';
5
8
  export { withStoreTransaction } from './store-transaction.js';
9
+ export { TablePrefixPlugin } from './table-prefix.js';
package/lib/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ export { Kysely, sql } from 'kysely';
1
2
  export { HozonDB } from './db.js';
2
3
  export { HozonDBClosedError, InvalidTablePrefixError, SavepointOverlapError, SchemaVersionError } from './errors.js';
3
4
  export { chunk, withKeepSet } from './keep-set.js';
4
5
  export { withStoreTransaction } from './store-transaction.js';
6
+ export { TablePrefixPlugin } from './table-prefix.js';
@@ -0,0 +1,7 @@
1
+ import type { KyselyPlugin, PluginTransformQueryArgs, PluginTransformResultArgs, QueryResult, RootOperationNode, UnknownRow } from 'kysely';
2
+ export declare class TablePrefixPlugin implements KyselyPlugin {
3
+ #private;
4
+ constructor(prefix: string);
5
+ transformQuery({ node, queryId }: PluginTransformQueryArgs): RootOperationNode;
6
+ transformResult({ result }: PluginTransformResultArgs): Promise<QueryResult<UnknownRow>>;
7
+ }
@@ -0,0 +1,247 @@
1
+ import { AliasNode, IdentifierNode, JoinNode, ListNode, OperationNodeTransformer, SchemableIdentifierNode, TableNode, UsingNode } from 'kysely';
2
+ // Kysely's root-node guard is internal; keep this list checked against its public root union.
3
+ const ROOT_NODE_KINDS = {
4
+ AlterTableNode: true,
5
+ AlterTypeNode: true,
6
+ CreateIndexNode: true,
7
+ CreateSchemaNode: true,
8
+ CreateTableNode: true,
9
+ CreateTypeNode: true,
10
+ CreateViewNode: true,
11
+ DeleteQueryNode: true,
12
+ DropIndexNode: true,
13
+ DropSchemaNode: true,
14
+ DropTableNode: true,
15
+ DropTypeNode: true,
16
+ DropViewNode: true,
17
+ InsertQueryNode: true,
18
+ MergeQueryNode: true,
19
+ RawNode: true,
20
+ RefreshMaterializedViewNode: true,
21
+ SelectQueryNode: true,
22
+ UpdateQueryNode: true
23
+ };
24
+ function isRootOperationNode(node) {
25
+ return Object.hasOwn(ROOT_NODE_KINDS, node.kind);
26
+ }
27
+ class TablePrefixTransformer extends OperationNodeTransformer {
28
+ #prefix;
29
+ #tables = new Set();
30
+ #ctes = new Set();
31
+ #aliases = new Set();
32
+ #writeTargets = new Set();
33
+ #writeNames = new Set();
34
+ #prefixedIdentifiers = new WeakSet();
35
+ constructor(prefix){
36
+ super();
37
+ this.#prefix = prefix;
38
+ }
39
+ transformNodeImpl(node, queryId) {
40
+ if (!isRootOperationNode(node) || node.kind === 'RawNode' && this.nodeStack.length > 1) {
41
+ return super.transformNodeImpl(node, queryId);
42
+ }
43
+ return this.#transformScope(node, queryId);
44
+ }
45
+ #transformScope(node, queryId) {
46
+ const previousTables = this.#tables;
47
+ const previousCTEs = this.#ctes;
48
+ const previousAliases = this.#aliases;
49
+ const previousWriteTargets = this.#writeTargets;
50
+ const previousWriteNames = this.#writeNames;
51
+ this.#tables = new Set(previousTables);
52
+ this.#ctes = new Set(previousCTEs);
53
+ this.#aliases = new Set(previousAliases);
54
+ this.#writeTargets = new Set();
55
+ this.#writeNames = new Set(previousWriteNames);
56
+ try {
57
+ let withNode;
58
+ if ('with' in node && node.with) {
59
+ withNode = this.#transformWithScope(node.with, queryId);
60
+ }
61
+ this.#collectTables(node);
62
+ this.#collectWriteTargets(node);
63
+ const transformed = super.transformNodeImpl(withNode ? {
64
+ ...node,
65
+ with: undefined
66
+ } : node, queryId);
67
+ return withNode ? {
68
+ ...transformed,
69
+ with: withNode
70
+ } : transformed;
71
+ } finally{
72
+ // Nested queries inherit visible names without leaking them back into the outer query.
73
+ this.#tables = previousTables;
74
+ this.#ctes = previousCTEs;
75
+ this.#aliases = previousAliases;
76
+ this.#writeTargets = previousWriteTargets;
77
+ this.#writeNames = previousWriteNames;
78
+ }
79
+ }
80
+ transformTable(node, queryId) {
81
+ // Revisit plugged subqueries for correlated references, but keep their physical tables.
82
+ // Kysely transformers clone containers but retain immutable identifier leaves.
83
+ if (this.#prefixedIdentifiers.has(node.table.identifier)) return node;
84
+ const name = node.table.identifier.name;
85
+ if (this.#ctes.has(name) && !this.#writeTargets.has(node.table.identifier)) {
86
+ return super.transformTable(node, queryId);
87
+ }
88
+ return this.#prefixTable(node);
89
+ }
90
+ #prefixTable(node) {
91
+ if (node.table.schema !== undefined || this.#prefixedIdentifiers.has(node.table.identifier)) return node;
92
+ const name = node.table.identifier.name;
93
+ const transformed = TableNode.create(`${this.#prefix}_${name}`);
94
+ this.#prefixedIdentifiers.add(transformed.table.identifier);
95
+ return transformed;
96
+ }
97
+ transformReference(node, queryId) {
98
+ return {
99
+ ...node,
100
+ column: this.transformNode(node.column, queryId),
101
+ table: node.table && this.#tables.has(node.table.table.identifier.name) && !this.#aliases.has(node.table.table.identifier.name) ? this.#writeNames.has(node.table.table.identifier.name) ? this.#prefixTable(node.table) : this.transformNode(node.table, queryId) : node.table
102
+ };
103
+ }
104
+ transformSelectModifier(node, queryId) {
105
+ return {
106
+ ...super.transformSelectModifier({
107
+ ...node,
108
+ of: undefined
109
+ }, queryId),
110
+ of: node.of?.map((item)=>this.#transformTableReference(item, queryId))
111
+ };
112
+ }
113
+ transformAggregateFunction(node, queryId) {
114
+ return {
115
+ ...super.transformAggregateFunction({
116
+ ...node,
117
+ aggregated: []
118
+ }, queryId),
119
+ aggregated: this.#transformTableArgs(node.func, node.aggregated, queryId)
120
+ };
121
+ }
122
+ transformFunction(node, queryId) {
123
+ return {
124
+ ...super.transformFunction({
125
+ ...node,
126
+ arguments: []
127
+ }, queryId),
128
+ arguments: this.#transformTableArgs(node.func, node.arguments, queryId)
129
+ };
130
+ }
131
+ #transformTableArgs(func, args, queryId) {
132
+ return func === 'json_agg' || func === 'to_json' ? args.map((arg)=>this.#transformTableReference(arg, queryId)) : this.transformNodeList(args, queryId);
133
+ }
134
+ #transformTableReference(node, queryId) {
135
+ // Only direct table references can be aliases; raw sql.table() nodes still name physical tables.
136
+ return TableNode.is(node) && node.table.schema === undefined && this.#aliases.has(node.table.identifier.name) ? node : this.transformNode(node, queryId);
137
+ }
138
+ #collectTables(node) {
139
+ this.#collectRawTables(node);
140
+ if ('name' in node && node.name && SchemableIdentifierNode.is(node.name)) {
141
+ this.#tables.add(node.name.identifier.name);
142
+ }
143
+ if ('from' in node && node.from) {
144
+ for (const table of node.from.froms)this.#collectTableExpression(table);
145
+ }
146
+ if ('into' in node && node.into) this.#collectTableExpression(node.into);
147
+ if ('table' in node && node.table) this.#collectTableExpression(node.table);
148
+ if ('joins' in node && node.joins) {
149
+ for (const join of node.joins)this.#collectTableExpression(join.table);
150
+ }
151
+ if ('using' in node && node.using) {
152
+ this.#collectTableExpression(JoinNode.is(node.using) ? node.using.table : node.using);
153
+ }
154
+ }
155
+ #transformWithScope(node, queryId) {
156
+ // Nonrecursive definitions see preceding CTEs, not their own name or later names.
157
+ if (node.recursive) {
158
+ for (const expression of node.expressions){
159
+ this.#ctes.add(expression.name.table.table.identifier.name);
160
+ }
161
+ }
162
+ const expressions = [];
163
+ for (const expression of node.expressions){
164
+ expressions.push({
165
+ ...expression,
166
+ expression: expression.expression.kind === 'RawNode' ? Object.freeze(this.#transformScope(expression.expression, queryId)) : this.transformNode(expression.expression, queryId)
167
+ });
168
+ this.#ctes.add(expression.name.table.table.identifier.name);
169
+ }
170
+ return {
171
+ ...node,
172
+ expressions
173
+ };
174
+ }
175
+ #collectWriteTargets(node) {
176
+ const collect = (table)=>{
177
+ if (TableNode.is(table)) {
178
+ this.#writeTargets.add(table.table.identifier);
179
+ this.#writeNames.add(table.table.identifier.name);
180
+ } else if (AliasNode.is(table)) {
181
+ collect(table.node);
182
+ } else if (ListNode.is(table)) {
183
+ for (const item of table.items)collect(item);
184
+ }
185
+ };
186
+ if (node.kind === 'InsertQueryNode' || node.kind === 'MergeQueryNode') {
187
+ if (node.into) collect(node.into);
188
+ } else if (node.kind === 'UpdateQueryNode') {
189
+ if (node.table) collect(node.table);
190
+ } else if (node.kind === 'DeleteQueryNode') {
191
+ for (const table of node.from.froms)collect(table);
192
+ }
193
+ }
194
+ #collectRawTables(node) {
195
+ // Qualifiers are references, not declarations. Nested queries collect their own scope.
196
+ if (node.kind === 'ReferenceNode' || node.kind === 'WithNode') return;
197
+ if (node.kind === 'RawNode') {
198
+ for (const parameter of node.parameters){
199
+ if (AliasNode.is(parameter)) {
200
+ if (!isRootOperationNode(parameter.node) || parameter.node.kind === 'RawNode') this.#collectRawTables(parameter.node);
201
+ this.#collectTableExpression(parameter);
202
+ continue;
203
+ }
204
+ this.#collectTableExpression(parameter);
205
+ if (!isRootOperationNode(parameter) || parameter.kind === 'RawNode') this.#collectRawTables(parameter);
206
+ }
207
+ return;
208
+ }
209
+ for (const value of Object.values(node)){
210
+ for (const child of Array.isArray(value) ? value : [
211
+ value
212
+ ]){
213
+ if (child && typeof child === 'object' && 'kind' in child) {
214
+ const operation = child;
215
+ if (!isRootOperationNode(operation) || operation.kind === 'RawNode') this.#collectRawTables(operation);
216
+ }
217
+ }
218
+ }
219
+ }
220
+ #collectTableExpression(node) {
221
+ if (TableNode.is(node)) {
222
+ this.#tables.add(node.table.identifier.name);
223
+ this.#aliases.delete(node.table.identifier.name);
224
+ // Local read bindings shadow enclosing write qualifiers in correlated subqueries.
225
+ this.#writeNames.delete(node.table.identifier.name);
226
+ } else if (AliasNode.is(node)) {
227
+ if (TableNode.is(node.node)) this.#tables.add(node.node.table.identifier.name);
228
+ if (IdentifierNode.is(node.alias)) this.#aliases.add(node.alias.name);
229
+ } else if (ListNode.is(node)) {
230
+ for (const table of node.items)this.#collectTableExpression(table);
231
+ } else if (UsingNode.is(node)) {
232
+ for (const table of node.tables)this.#collectTableExpression(table);
233
+ }
234
+ }
235
+ }
236
+ export class TablePrefixPlugin {
237
+ #transformer;
238
+ constructor(prefix){
239
+ this.#transformer = new TablePrefixTransformer(prefix);
240
+ }
241
+ transformQuery({ node, queryId }) {
242
+ return this.#transformer.transformNode(node, queryId);
243
+ }
244
+ async transformResult({ result }) {
245
+ return result;
246
+ }
247
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hozon/db",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Database registry, migrations and transactions for Hozon",
5
5
  "repository": {
6
6
  "type": "git",