@kubun/db 0.10.0 → 0.12.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/lib/db.d.ts +19 -0
- package/lib/db.js +186 -1
- package/lib/index.js +1 -1
- package/package.json +13 -13
package/lib/db.d.ts
CHANGED
|
@@ -14,7 +14,24 @@ export type StoreDefinition<Tables, API> = {
|
|
|
14
14
|
};
|
|
15
15
|
export type StoreProvider<Stores extends Record<string, unknown> = Record<string, unknown>> = {
|
|
16
16
|
getStore<S extends keyof Stores & string>(name: S): Promise<Stores[S]>;
|
|
17
|
+
/**
|
|
18
|
+
* Returns whether a store definition is registered, synchronously and
|
|
19
|
+
* WITHOUT triggering migration. This distinguishes "plugin not installed"
|
|
20
|
+
* (registration absent) from "plugin installed but its store currently
|
|
21
|
+
* errors" — callers can fail closed on the latter instead of mistaking a
|
|
22
|
+
* transient store error for an uninstalled plugin.
|
|
23
|
+
*/
|
|
24
|
+
hasStore(name: string): boolean;
|
|
17
25
|
onCommit(fn: () => void): void;
|
|
26
|
+
/**
|
|
27
|
+
* Register a hook to run after the transaction rolls back. By the time the
|
|
28
|
+
* hook fires the DB is already reverted, so it is the correct place to drop
|
|
29
|
+
* any in-memory state that was eagerly advanced in anticipation of a commit
|
|
30
|
+
* (e.g. cache invalidation). Like `onCommit`, hooks are isolated and their
|
|
31
|
+
* errors logged, never re-thrown. On the non-transactional provider it is a
|
|
32
|
+
* no-op (each standalone op auto-commits; nothing to roll back).
|
|
33
|
+
*/
|
|
34
|
+
onRollback(fn: () => void): void;
|
|
18
35
|
/**
|
|
19
36
|
* Run `fn` against a `StoreProvider` that scopes its writes to a transaction.
|
|
20
37
|
*
|
|
@@ -40,7 +57,9 @@ export declare class KubunDB implements StoreProvider {
|
|
|
40
57
|
get adapter(): Adapter;
|
|
41
58
|
register<Tables, API>(store: StoreDefinition<Tables, API>): void;
|
|
42
59
|
getStore<T>(name: string): Promise<T>;
|
|
60
|
+
hasStore(name: string): boolean;
|
|
43
61
|
onCommit(fn: () => void): void;
|
|
62
|
+
onRollback(_fn: () => void): void;
|
|
44
63
|
/**
|
|
45
64
|
* Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
|
|
46
65
|
* Concurrent calls on the same `KubunDB` instance are supported — the
|
package/lib/db.js
CHANGED
|
@@ -1 +1,186 @@
|
|
|
1
|
-
import{getKubunLogger
|
|
1
|
+
import { getKubunLogger } from '@kubun/logger';
|
|
2
|
+
import { Kysely, ParseJSONResultsPlugin } from 'kysely';
|
|
3
|
+
import { Migrator } from 'kysely/migration';
|
|
4
|
+
export class KubunDB {
|
|
5
|
+
#adapter;
|
|
6
|
+
#db;
|
|
7
|
+
#logger;
|
|
8
|
+
#migrated = new Map();
|
|
9
|
+
#storeAPIs = new Map();
|
|
10
|
+
#stores = new Map();
|
|
11
|
+
constructor(params){
|
|
12
|
+
this.#adapter = params.adapter;
|
|
13
|
+
this.#db = new Kysely({
|
|
14
|
+
dialect: this.#adapter.dialect,
|
|
15
|
+
plugins: [
|
|
16
|
+
new ParseJSONResultsPlugin()
|
|
17
|
+
]
|
|
18
|
+
});
|
|
19
|
+
this.#logger = params.logger ?? getKubunLogger('db');
|
|
20
|
+
}
|
|
21
|
+
get adapter() {
|
|
22
|
+
return this.#adapter;
|
|
23
|
+
}
|
|
24
|
+
register(store) {
|
|
25
|
+
const existing = this.#stores.get(store.name);
|
|
26
|
+
if (existing != null) {
|
|
27
|
+
if (existing !== store) {
|
|
28
|
+
this.#logger.warn('store "{name}" registered with different definition, ignoring', {
|
|
29
|
+
name: store.name
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this.#stores.set(store.name, store);
|
|
35
|
+
}
|
|
36
|
+
async getStore(name) {
|
|
37
|
+
await this.#ensureMigrated(name);
|
|
38
|
+
if (!this.#storeAPIs.has(name)) {
|
|
39
|
+
const store = this.#stores.get(name);
|
|
40
|
+
if (store == null) {
|
|
41
|
+
throw new Error(`Store "${name}" is not registered`);
|
|
42
|
+
}
|
|
43
|
+
this.#storeAPIs.set(name, store.createAPI(this.#db, this.#adapter));
|
|
44
|
+
}
|
|
45
|
+
return this.#storeAPIs.get(name);
|
|
46
|
+
}
|
|
47
|
+
hasStore(name) {
|
|
48
|
+
return this.#stores.has(name);
|
|
49
|
+
}
|
|
50
|
+
onCommit(fn) {
|
|
51
|
+
fn();
|
|
52
|
+
}
|
|
53
|
+
// Each standalone op auto-commits, so there is nothing to roll back — no-op.
|
|
54
|
+
onRollback(_fn) {}
|
|
55
|
+
/**
|
|
56
|
+
* Runs `fn` inside a DB transaction and provides a scoped `StoreProvider`.
|
|
57
|
+
* Concurrent calls on the same `KubunDB` instance are supported — the
|
|
58
|
+
* underlying driver serializes them.
|
|
59
|
+
*
|
|
60
|
+
* Callers MUST use the provided `tx` inside the callback. Calling
|
|
61
|
+
* `db.withTransaction` on the same instance inside the callback opens a
|
|
62
|
+
* new independent transaction (no atomic nesting) and is a bug.
|
|
63
|
+
*/ async withTransaction(fn) {
|
|
64
|
+
// Migrate every registered store before opening the transaction. Stores
|
|
65
|
+
// are internal and known up front, so a broken migration should fail fast
|
|
66
|
+
// at startup rather than leave a runtime partition where one store's
|
|
67
|
+
// failure silently blocks transactions that never touch it.
|
|
68
|
+
await Promise.all(Array.from(this.#stores.keys()).map((name)=>this.#ensureMigrated(name)));
|
|
69
|
+
const commitHooks = [];
|
|
70
|
+
const rollbackHooks = [];
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
result = await this.#db.transaction().execute(async (trx)=>{
|
|
74
|
+
const txKysely = trx;
|
|
75
|
+
const txStoreAPIs = new Map();
|
|
76
|
+
const provider = {
|
|
77
|
+
getStore: async (name)=>{
|
|
78
|
+
if (!txStoreAPIs.has(name)) {
|
|
79
|
+
const store = this.#stores.get(name);
|
|
80
|
+
if (store == null) {
|
|
81
|
+
throw new Error(`Store "${name}" is not registered`);
|
|
82
|
+
}
|
|
83
|
+
txStoreAPIs.set(name, store.createAPI(txKysely, this.#adapter));
|
|
84
|
+
}
|
|
85
|
+
return txStoreAPIs.get(name);
|
|
86
|
+
},
|
|
87
|
+
hasStore: (name)=>this.#stores.has(name),
|
|
88
|
+
onCommit: (hook)=>{
|
|
89
|
+
commitHooks.push(hook);
|
|
90
|
+
},
|
|
91
|
+
onRollback: (hook)=>{
|
|
92
|
+
rollbackHooks.push(hook);
|
|
93
|
+
},
|
|
94
|
+
withTransaction: async (nestedFn)=>{
|
|
95
|
+
// Already inside a transaction — Kysely does not nest.
|
|
96
|
+
// Run `nestedFn` inline with the same provider so a throw rolls
|
|
97
|
+
// the outer transaction back.
|
|
98
|
+
return await nestedFn(provider);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
return await fn(provider);
|
|
102
|
+
});
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Kysely issues ROLLBACK and rejects `execute()` when the callback
|
|
105
|
+
// throws, so the DB is already reverted here. Run the rollback hooks to
|
|
106
|
+
// drop any in-memory state that was eagerly advanced expecting a commit
|
|
107
|
+
// (revert first, then drop the stale cache). Hooks are isolated/logged,
|
|
108
|
+
// never re-thrown; the original error is preserved so callers (e.g. the
|
|
109
|
+
// engine's `MutateGraphWriteRollback` sentinel) still observe it.
|
|
110
|
+
for(let index = 0; index < rollbackHooks.length; index++){
|
|
111
|
+
try {
|
|
112
|
+
rollbackHooks[index]();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.#logger.error('rollback hook {index}/{count} failed', {
|
|
115
|
+
index,
|
|
116
|
+
count: rollbackHooks.length,
|
|
117
|
+
error
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
// The transaction has committed; post-commit hooks are best-effort side
|
|
124
|
+
// effects (event emission, cache invalidation). A throwing hook must not
|
|
125
|
+
// skip the remaining hooks or make the caller observe a durable write as a
|
|
126
|
+
// failure, so each hook is isolated and its error logged, never re-thrown.
|
|
127
|
+
for(let index = 0; index < commitHooks.length; index++){
|
|
128
|
+
try {
|
|
129
|
+
commitHooks[index]();
|
|
130
|
+
} catch (error) {
|
|
131
|
+
this.#logger.error('commit hook {index}/{count} failed', {
|
|
132
|
+
index,
|
|
133
|
+
count: commitHooks.length,
|
|
134
|
+
error
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
async close() {
|
|
141
|
+
await this.#db.destroy();
|
|
142
|
+
this.#logger.info('closed');
|
|
143
|
+
}
|
|
144
|
+
async #ensureMigrated(name) {
|
|
145
|
+
let pending = this.#migrated.get(name);
|
|
146
|
+
if (pending == null) {
|
|
147
|
+
pending = this.#runStoreMigrations(name).catch((err)=>{
|
|
148
|
+
this.#migrated.delete(name);
|
|
149
|
+
throw err;
|
|
150
|
+
});
|
|
151
|
+
this.#migrated.set(name, pending);
|
|
152
|
+
}
|
|
153
|
+
return pending;
|
|
154
|
+
}
|
|
155
|
+
async #runStoreMigrations(name) {
|
|
156
|
+
const store = this.#stores.get(name);
|
|
157
|
+
if (store == null) {
|
|
158
|
+
throw new Error(`Store "${name}" is not registered`);
|
|
159
|
+
}
|
|
160
|
+
for (const dep of store.dependsOn ?? []){
|
|
161
|
+
await this.#ensureMigrated(dep);
|
|
162
|
+
}
|
|
163
|
+
const migrations = typeof store.migrations === 'function' ? store.migrations({
|
|
164
|
+
types: this.#adapter.types,
|
|
165
|
+
functions: this.#adapter.functions
|
|
166
|
+
}) : store.migrations;
|
|
167
|
+
if (Object.keys(migrations).length === 0) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const migrator = new Migrator({
|
|
171
|
+
db: this.#db,
|
|
172
|
+
provider: {
|
|
173
|
+
getMigrations: ()=>Promise.resolve(migrations)
|
|
174
|
+
},
|
|
175
|
+
migrationTableName: `kubun_${name}_migration`,
|
|
176
|
+
migrationLockTableName: `kubun_${name}_migration_lock`
|
|
177
|
+
});
|
|
178
|
+
const result = await migrator.migrateToLatest();
|
|
179
|
+
if (result.error != null) {
|
|
180
|
+
throw result.error;
|
|
181
|
+
}
|
|
182
|
+
this.#logger.info('store {name} migrations complete', {
|
|
183
|
+
name
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export{KubunDB}from
|
|
1
|
+
export { KubunDB } from './db.js';
|
package/package.json
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/db",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"license": "see LICENSE.md",
|
|
3
|
+
"version": "0.12.0",
|
|
5
4
|
"keywords": [],
|
|
5
|
+
"license": "see LICENSE.md",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"type": "module",
|
|
7
|
-
"main": "lib/index.js",
|
|
8
|
-
"types": "lib/index.d.ts",
|
|
9
8
|
"exports": {
|
|
10
9
|
".": "./lib/index.js"
|
|
11
10
|
},
|
|
11
|
+
"main": "lib/index.js",
|
|
12
|
+
"types": "lib/index.d.ts",
|
|
12
13
|
"files": [
|
|
13
14
|
"lib/*",
|
|
14
15
|
"LICENSE.md"
|
|
15
16
|
],
|
|
16
|
-
"sideEffects": false,
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"kysely": "^0.29.
|
|
19
|
-
"@kubun/
|
|
20
|
-
"@kubun/
|
|
18
|
+
"kysely": "^0.29.4",
|
|
19
|
+
"@kubun/logger": "^0.12.0",
|
|
20
|
+
"@kubun/db-adapter": "^0.12.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@kubun/db-better-sqlite": "^0.
|
|
23
|
+
"@kubun/db-better-sqlite": "^0.12.0"
|
|
24
24
|
},
|
|
25
25
|
"scripts": {
|
|
26
|
+
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
26
27
|
"build:clean": "del lib",
|
|
27
|
-
"build:js": "swc src -d ./lib --config-file ../../swc.json --strip-leading-paths",
|
|
28
|
+
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
28
29
|
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
29
30
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
30
|
-
"
|
|
31
|
+
"test": "pnpm run test:types && pnpm run test:unit",
|
|
31
32
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|
|
32
|
-
"test:unit": "vitest run"
|
|
33
|
-
"test": "pnpm run test:types && pnpm run test:unit"
|
|
33
|
+
"test:unit": "vitest run"
|
|
34
34
|
}
|
|
35
35
|
}
|