@navette-js/database 1.0.0-beta.4 → 1.0.0-beta.5
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/index.d.ts +17 -0
- package/lib/index.js +55 -11
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
|
@@ -63,6 +63,8 @@ export interface DataAdapter<T = any> {
|
|
|
63
63
|
count(): Promise<number>;
|
|
64
64
|
paginate(query: ListQuery): Promise<Page<T>>;
|
|
65
65
|
find(key: string | number): Promise<T | null>;
|
|
66
|
+
/** Like `find()`, soft-deleted records included (tables that show deleted records). Optional. */
|
|
67
|
+
findWithTrashed?(key: string | number): Promise<T | null>;
|
|
66
68
|
create(data: Record<string, unknown>): Promise<T>;
|
|
67
69
|
update(key: string | number, data: Record<string, unknown>): Promise<T | null>;
|
|
68
70
|
delete(key: string | number): Promise<boolean>;
|
|
@@ -76,7 +78,17 @@ export interface DataAdapter<T = any> {
|
|
|
76
78
|
deleteMany(keys: (string | number)[]): Promise<number>;
|
|
77
79
|
/** True if another record already has this value (`unique` rule). */
|
|
78
80
|
exists(column: string, value: unknown, exceptKey?: string | number | null): Promise<boolean>;
|
|
81
|
+
/** Runs `fn` in a database transaction (`panel.databaseTransactions()`); without it, `fn` runs as is. */
|
|
82
|
+
transaction?<T>(fn: () => Promise<T>): Promise<T>;
|
|
79
83
|
}
|
|
84
|
+
/** The transaction open on this connection in the current async context (`transaction()`), or `null`. */
|
|
85
|
+
export declare function currentTransaction(knex?: Knex): Knex.Transaction | null;
|
|
86
|
+
/**
|
|
87
|
+
* Runs `fn` in a database transaction: committed when it resolves, rolled back when it throws. Every query made in the
|
|
88
|
+
* meantime through the connection (`createConnection()`, `sqlite()`: Objection models and `knex(table)` alike) joins it.
|
|
89
|
+
* Called inside an open transaction, `fn` simply joins it.
|
|
90
|
+
*/
|
|
91
|
+
export declare function transaction<T>(knex: Knex | (() => Promise<T>), fn?: () => Promise<T>): Promise<T>;
|
|
80
92
|
export declare function createConnection(config: Knex.Config): Knex;
|
|
81
93
|
/** SQLite connection (better-sqlite3 driver), for development and demos. */
|
|
82
94
|
export declare function sqlite(filename: string): Knex;
|
|
@@ -100,6 +112,10 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
100
112
|
* (the request's tenant; `null` means no filter). Set by the panel (`panel.tenant()`).
|
|
101
113
|
*/
|
|
102
114
|
scopeToTenant(column: string, key: () => string | number | null): this;
|
|
115
|
+
/** A query of the model, in the open transaction if any. */
|
|
116
|
+
private query;
|
|
117
|
+
/** Runs `fn` in a transaction of the model's connection. */
|
|
118
|
+
transaction<T>(fn: () => Promise<T>): Promise<T>;
|
|
103
119
|
private base;
|
|
104
120
|
private now;
|
|
105
121
|
private get keyColumn();
|
|
@@ -143,6 +159,7 @@ export declare class ObjectionAdapter<M extends Model> implements DataAdapter<M>
|
|
|
143
159
|
private filtered;
|
|
144
160
|
private finishPagination;
|
|
145
161
|
find(key: string | number): Promise<M | null>;
|
|
162
|
+
findWithTrashed(key: string | number): Promise<M | null>;
|
|
146
163
|
create(data: Record<string, unknown>): Promise<M>;
|
|
147
164
|
update(key: string | number, data: Record<string, unknown>): Promise<M | null>;
|
|
148
165
|
/** With soft deletes, the record is timestamped instead of removed. */
|
package/lib/index.js
CHANGED
|
@@ -1,9 +1,48 @@
|
|
|
1
1
|
// @navette-js/database — data access contract and its Knex + Objection adapter.
|
|
2
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
3
|
import knexFactory from 'knex';
|
|
3
4
|
import { Model } from 'objection';
|
|
4
5
|
export { Model };
|
|
6
|
+
/** Transactions open in the current async context, by connection. */
|
|
7
|
+
const openTransactions = new AsyncLocalStorage();
|
|
8
|
+
const RAW = Symbol('navette.rawKnex');
|
|
9
|
+
const rawOf = (knex) => knex[RAW] ?? knex;
|
|
10
|
+
/** The transaction open on this connection in the current async context (`transaction()`), or `null`. */
|
|
11
|
+
export function currentTransaction(knex = Model.knex()) {
|
|
12
|
+
return openTransactions.getStore()?.get(rawOf(knex)) ?? null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Runs `fn` in a database transaction: committed when it resolves, rolled back when it throws. Every query made in the
|
|
16
|
+
* meantime through the connection (`createConnection()`, `sqlite()`: Objection models and `knex(table)` alike) joins it.
|
|
17
|
+
* Called inside an open transaction, `fn` simply joins it.
|
|
18
|
+
*/
|
|
19
|
+
export async function transaction(knex, fn) {
|
|
20
|
+
if (typeof fn !== 'function') {
|
|
21
|
+
fn = knex;
|
|
22
|
+
knex = Model.knex();
|
|
23
|
+
}
|
|
24
|
+
const raw = rawOf(knex);
|
|
25
|
+
const open = openTransactions.getStore();
|
|
26
|
+
if (open?.has(raw))
|
|
27
|
+
return fn();
|
|
28
|
+
return raw.transaction((trx) => openTransactions.run(new Map(open ?? []).set(raw, trx), fn));
|
|
29
|
+
}
|
|
30
|
+
/** The connection, routed to the transaction open in the current async context (`transaction()`), if any. */
|
|
31
|
+
function transactionAware(knex) {
|
|
32
|
+
const target = () => openTransactions.getStore()?.get(knex) ?? knex;
|
|
33
|
+
return new Proxy(knex, {
|
|
34
|
+
apply: (_target, thisArg, args) => Reflect.apply(target(), thisArg, args),
|
|
35
|
+
get: (_target, property) => {
|
|
36
|
+
if (property === RAW)
|
|
37
|
+
return knex;
|
|
38
|
+
const current = target();
|
|
39
|
+
const value = Reflect.get(current, property);
|
|
40
|
+
return typeof value === 'function' && current !== knex && property !== 'constructor' ? value.bind(current) : value;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
5
44
|
export function createConnection(config) {
|
|
6
|
-
const knex = knexFactory(config);
|
|
45
|
+
const knex = transactionAware(knexFactory(config));
|
|
7
46
|
Model.knex(knex);
|
|
8
47
|
return knex;
|
|
9
48
|
}
|
|
@@ -33,8 +72,12 @@ export class ObjectionAdapter {
|
|
|
33
72
|
* (the request's tenant; `null` means no filter). Set by the panel (`panel.tenant()`).
|
|
34
73
|
*/
|
|
35
74
|
scopeToTenant(column, key) { this.tenantScope = { column, key }; return this; }
|
|
75
|
+
/** A query of the model, in the open transaction if any. */
|
|
76
|
+
query() { return this.model.query(currentTransaction(this.model.knex()) ?? undefined); }
|
|
77
|
+
/** Runs `fn` in a transaction of the model's connection. */
|
|
78
|
+
transaction(fn) { return transaction(this.model.knex(), fn); }
|
|
36
79
|
base(trashed = 'without') {
|
|
37
|
-
const q = this.
|
|
80
|
+
const q = this.query();
|
|
38
81
|
const tenantKey = this.tenantScope?.key() ?? null;
|
|
39
82
|
if (tenantKey !== null)
|
|
40
83
|
q.where(`${this.model.tableName}.${this.tenantScope.column}`, tenantKey);
|
|
@@ -127,8 +170,8 @@ export class ObjectionAdapter {
|
|
|
127
170
|
if (!keys.length)
|
|
128
171
|
return 0;
|
|
129
172
|
if (this.softDeletes)
|
|
130
|
-
return this.
|
|
131
|
-
return this.
|
|
173
|
+
return this.query().whereIn(this.keyColumn, keys).whereNull(this.softDeletes).patch({ [this.softDeletes]: this.now() });
|
|
174
|
+
return this.query().delete().whereIn(this.keyColumn, keys);
|
|
132
175
|
}
|
|
133
176
|
async paginate(query) {
|
|
134
177
|
return this.finishPagination(this.filtered(query), query);
|
|
@@ -174,9 +217,10 @@ export class ObjectionAdapter {
|
|
|
174
217
|
return { records, total, page, perPage, lastPage, from: records.length ? (page - 1) * perPage + 1 : null, to: records.length ? (page - 1) * perPage + records.length : null };
|
|
175
218
|
}
|
|
176
219
|
async find(key) { return (await this.base(this.findsTrashed ? 'with' : 'without').findById(key)) ?? null; }
|
|
220
|
+
async findWithTrashed(key) { return (await this.base('with').findById(key)) ?? null; }
|
|
177
221
|
async create(data) {
|
|
178
222
|
const tenantKey = this.tenantScope?.key() ?? null;
|
|
179
|
-
return (await this.
|
|
223
|
+
return (await this.query().insertAndFetch((tenantKey !== null ? { ...data, [this.tenantScope.column]: tenantKey } : data)));
|
|
180
224
|
}
|
|
181
225
|
async update(key, data) {
|
|
182
226
|
// Records outside the current tenant cannot be updated, and the tenant column never changes.
|
|
@@ -185,24 +229,24 @@ export class ObjectionAdapter {
|
|
|
185
229
|
return null;
|
|
186
230
|
data = Object.fromEntries(Object.entries(data).filter(([column]) => column !== this.tenantScope.column));
|
|
187
231
|
}
|
|
188
|
-
return (await this.
|
|
232
|
+
return (await this.query().patchAndFetchById(key, data)) ?? null;
|
|
189
233
|
}
|
|
190
234
|
/** With soft deletes, the record is timestamped instead of removed. */
|
|
191
235
|
async delete(key) {
|
|
192
236
|
if (this.softDeletes)
|
|
193
|
-
return (await this.
|
|
194
|
-
return (await this.
|
|
237
|
+
return (await this.query().findById(key).whereNull(this.softDeletes).patch({ [this.softDeletes]: this.now() })) > 0;
|
|
238
|
+
return (await this.query().deleteById(key)) > 0;
|
|
195
239
|
}
|
|
196
240
|
supportsSoftDeletes() { return this.softDeletes !== null; }
|
|
197
241
|
isTrashed(record) { return this.softDeletes !== null && record?.[this.softDeletes] != null; }
|
|
198
242
|
async restore(key) {
|
|
199
243
|
if (!this.softDeletes)
|
|
200
244
|
return false;
|
|
201
|
-
return (await this.
|
|
245
|
+
return (await this.query().findById(key).whereNotNull(this.softDeletes).patch({ [this.softDeletes]: null })) > 0;
|
|
202
246
|
}
|
|
203
|
-
async forceDelete(key) { return (await this.
|
|
247
|
+
async forceDelete(key) { return (await this.query().deleteById(key)) > 0; }
|
|
204
248
|
async exists(column, value, exceptKey = null) {
|
|
205
|
-
const q = this.
|
|
249
|
+
const q = this.query().where(column, value);
|
|
206
250
|
if (exceptKey !== null)
|
|
207
251
|
q.whereNot(this.keyColumn, exceptKey);
|
|
208
252
|
return (await q.resultSize()) > 0;
|