@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 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.model.query();
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.model.query().whereIn(this.keyColumn, keys).whereNull(this.softDeletes).patch({ [this.softDeletes]: this.now() });
131
- return this.model.query().delete().whereIn(this.keyColumn, keys);
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.model.query().insertAndFetch((tenantKey !== null ? { ...data, [this.tenantScope.column]: tenantKey } : data)));
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.model.query().patchAndFetchById(key, data)) ?? null;
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.model.query().findById(key).whereNull(this.softDeletes).patch({ [this.softDeletes]: this.now() })) > 0;
194
- return (await this.model.query().deleteById(key)) > 0;
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.model.query().findById(key).whereNotNull(this.softDeletes).patch({ [this.softDeletes]: null })) > 0;
245
+ return (await this.query().findById(key).whereNotNull(this.softDeletes).patch({ [this.softDeletes]: null })) > 0;
202
246
  }
203
- async forceDelete(key) { return (await this.model.query().deleteById(key)) > 0; }
247
+ async forceDelete(key) { return (await this.query().deleteById(key)) > 0; }
204
248
  async exists(column, value, exceptKey = null) {
205
- const q = this.model.query().where(column, value);
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@navette-js/database",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.5",
4
4
  "description": "Data adapters (Objection/Knex) for Navette panels.",
5
5
  "license": "MIT",
6
6
  "type": "module",