@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 +1 -0
- package/lib/db.js +18 -3
- package/lib/index.d.ts +4 -0
- package/lib/index.js +2 -0
- package/lib/table-prefix.d.ts +7 -0
- package/lib/table-prefix.js +247 -0
- package/package.json +1 -1
package/lib/db.d.ts
CHANGED
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.#
|
|
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.#
|
|
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
|
-
|
|
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
|
+
}
|