@nextlyhq/adapter-sqlite 0.0.2-alpha.65 → 0.0.2-alpha.67
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/README.md +5 -5
- package/dist/index.cjs +269 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +107 -6
- package/dist/index.d.ts +107 -6
- package/dist/index.mjs +269 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -81,11 +81,11 @@ Most users do not need this.
|
|
|
81
81
|
|
|
82
82
|
## Compatibility
|
|
83
83
|
|
|
84
|
-
| Tool | Version
|
|
85
|
-
| ---------------- |
|
|
86
|
-
| Node.js | 20+
|
|
87
|
-
| `better-sqlite3` | 11+
|
|
88
|
-
| `nextly` | 0.0.x
|
|
84
|
+
| Tool | Version |
|
|
85
|
+
| ---------------- | --------------------- |
|
|
86
|
+
| Node.js | 20.19+, 22.12+ or 24+ |
|
|
87
|
+
| `better-sqlite3` | 11+ |
|
|
88
|
+
| `nextly` | 0.0.x |
|
|
89
89
|
|
|
90
90
|
## Documentation
|
|
91
91
|
|
package/dist/index.cjs
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var async_hooks = require('async_hooks');
|
|
3
4
|
var adapterDrizzle = require('@nextlyhq/adapter-drizzle');
|
|
4
5
|
var types = require('@nextlyhq/adapter-drizzle/types');
|
|
5
6
|
var versionCheck = require('@nextlyhq/adapter-drizzle/version-check');
|
|
6
7
|
var betterSqlite3 = require('drizzle-orm/better-sqlite3');
|
|
8
|
+
var sqliteCore = require('drizzle-orm/sqlite-core');
|
|
7
9
|
|
|
8
10
|
// src/index.ts
|
|
9
11
|
var VERSION = "0.1.0";
|
|
@@ -35,6 +37,7 @@ var DEFAULT_CONFIG = {
|
|
|
35
37
|
function sanitizeSqliteValue(v) {
|
|
36
38
|
if (v === void 0) return null;
|
|
37
39
|
if (typeof v === "boolean") return v ? 1 : 0;
|
|
40
|
+
if (Buffer.isBuffer(v) || ArrayBuffer.isView(v)) return v;
|
|
38
41
|
if (v instanceof Date) return Math.floor(v.getTime() / 1e3);
|
|
39
42
|
if (v !== null && typeof v === "object") return JSON.stringify(v);
|
|
40
43
|
return v;
|
|
@@ -87,6 +90,18 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
87
90
|
* surface area without changing the achievable concurrency.
|
|
88
91
|
*/
|
|
89
92
|
transactionQueue = Promise.resolve();
|
|
93
|
+
/**
|
|
94
|
+
* The transaction the current async chain is running inside, if any.
|
|
95
|
+
*
|
|
96
|
+
* A `transaction()` call made from INSIDE a transaction's work — a store
|
|
97
|
+
* writing on behalf of the caller, a plugin's write inside its own
|
|
98
|
+
* `ctx.db.transaction` — used to queue behind the very transaction that
|
|
99
|
+
* was waiting for it, and neither ever settled: that hung every later
|
|
100
|
+
* transaction on the instance too. Such a call now runs as a SAVEPOINT of
|
|
101
|
+
* the active transaction instead, so it commits or rolls back with it and
|
|
102
|
+
* its own failure undoes only its own part.
|
|
103
|
+
*/
|
|
104
|
+
transactionScope = new async_hooks.AsyncLocalStorage();
|
|
90
105
|
/**
|
|
91
106
|
* Creates a new SQLite adapter instance.
|
|
92
107
|
*
|
|
@@ -249,19 +264,190 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
249
264
|
* @returns Result of the work function
|
|
250
265
|
*
|
|
251
266
|
* @remarks
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
* -
|
|
267
|
+
* The adapter manages the transaction itself, because better-sqlite3's
|
|
268
|
+
* `db.transaction()` does not take async work: it runs `BEGIN IMMEDIATE`,
|
|
269
|
+
* then `COMMIT` when `work` resolves or `ROLLBACK` when it throws.
|
|
270
|
+
* - Top-level calls run one at a time.
|
|
271
|
+
* - A call made inside an open transaction's work joins it as a savepoint
|
|
272
|
+
* (`SAVEPOINT` / `RELEASE` / `ROLLBACK TO`).
|
|
273
|
+
* - Nested calls of one scope run one at a time.
|
|
274
|
+
* - A call made later from a savepoint that has already released joins the
|
|
275
|
+
* innermost scope still open.
|
|
276
|
+
* - Nested work started and not awaited finishes before the enclosing scope
|
|
277
|
+
* commits or rolls back.
|
|
256
278
|
*
|
|
257
279
|
* Note: SQLite uses DEFERRED, IMMEDIATE, or EXCLUSIVE transaction modes
|
|
258
|
-
* rather than isolation levels. This adapter uses IMMEDIATE
|
|
280
|
+
* rather than isolation levels. This adapter uses IMMEDIATE.
|
|
259
281
|
*/
|
|
260
282
|
async transaction(work, _options) {
|
|
283
|
+
const own = this.transactionScope.getStore();
|
|
284
|
+
const parent = this.nestingScope();
|
|
285
|
+
if (own && parent !== own) own.writesAfterClosing += 1;
|
|
286
|
+
if (parent) {
|
|
287
|
+
return enqueueIn(parent, () => this.runSavepoint(work, parent));
|
|
288
|
+
}
|
|
261
289
|
const run = async () => this.runTransaction(work);
|
|
262
290
|
const next = this.transactionQueue.then(run, run);
|
|
263
291
|
this.transactionQueue = next.catch(() => void 0);
|
|
264
|
-
|
|
292
|
+
const { result, effects } = await next;
|
|
293
|
+
await this.runHeldEffects(effects);
|
|
294
|
+
return result;
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Hold `effect` until the transaction enclosing this async context commits,
|
|
298
|
+
* or run it now when none does.
|
|
299
|
+
*
|
|
300
|
+
* The effect belongs to the scope this async context runs in, even one
|
|
301
|
+
* that has already released or rolled back: a service whose own
|
|
302
|
+
* transaction nested as a savepoint registers from its caller's scope once
|
|
303
|
+
* that savepoint has released, and the change is undoable for as long as
|
|
304
|
+
* any scope around it is. The outermost transaction runs the effect once
|
|
305
|
+
* its COMMIT succeeds, unless a scope between the effect's scope and the
|
|
306
|
+
* outermost transaction rolled back, and drops every effect on ROLLBACK or
|
|
307
|
+
* a failed COMMIT. A context whose outermost transaction has already
|
|
308
|
+
* finished is outside any transaction, so its effect runs now, unless what
|
|
309
|
+
* it announces was undone and it has run no transaction since.
|
|
310
|
+
*/
|
|
311
|
+
async afterCommit(effect, onDeferredFailure) {
|
|
312
|
+
const scope = this.transactionScope.getStore();
|
|
313
|
+
if (!scope) {
|
|
314
|
+
await effect();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
const root = outermost(scope);
|
|
318
|
+
if (!root.finished) {
|
|
319
|
+
root.held.push({
|
|
320
|
+
effect: this.inRegisteringContext(effect),
|
|
321
|
+
onDeferredFailure,
|
|
322
|
+
scope,
|
|
323
|
+
writesBefore: scope.writesAfterClosing
|
|
324
|
+
});
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (!undone(scope, scope.writesAfterClosing)) await effect();
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* `effect`, bound to the async context it is registered from.
|
|
331
|
+
*
|
|
332
|
+
* A held effect runs after the outermost COMMIT, from the code that ran the
|
|
333
|
+
* outermost transaction, so without this it sees that code's context rather
|
|
334
|
+
* than its own: whatever its caller pinned around the write is gone. That
|
|
335
|
+
* includes who is calling. A scope pinned for a scoped API key lost there
|
|
336
|
+
* leaves a hook's nested call judged by the key's owner instead of the key,
|
|
337
|
+
* and a person's frame lost there leaves the hook told of nobody.
|
|
338
|
+
*
|
|
339
|
+
* The adapter's own transaction scope is the one store it leaves: the
|
|
340
|
+
* effect runs once that transaction has finished, outside it, as an effect
|
|
341
|
+
* held to the outermost commit always has.
|
|
342
|
+
*/
|
|
343
|
+
inRegisteringContext(effect) {
|
|
344
|
+
return async_hooks.AsyncResource.bind(() => this.transactionScope.exit(effect));
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Run the effects a committed transaction held, in order. A failure is
|
|
348
|
+
* reported, never thrown: the change is durable, and reporting the
|
|
349
|
+
* transaction as failed would invite a retry of something already done.
|
|
350
|
+
*/
|
|
351
|
+
async runHeldEffects(effects) {
|
|
352
|
+
for (const { effect, onDeferredFailure } of effects) {
|
|
353
|
+
try {
|
|
354
|
+
await effect();
|
|
355
|
+
} catch (error) {
|
|
356
|
+
this.reportHeldEffectFailure(error, onDeferredFailure);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/** Hand a held effect's failure to its reporter, or to the adapter's log. */
|
|
361
|
+
reportHeldEffectFailure(error, onDeferredFailure) {
|
|
362
|
+
try {
|
|
363
|
+
if (onDeferredFailure) {
|
|
364
|
+
onDeferredFailure(error);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
} catch {
|
|
368
|
+
}
|
|
369
|
+
const failure = error instanceof Error ? error : new Error(String(error));
|
|
370
|
+
if (this.config.logger?.error) {
|
|
371
|
+
this.config.logger.error(failure, { phase: "after-commit" });
|
|
372
|
+
} else {
|
|
373
|
+
console.error("An effect held until commit failed:", failure);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Whether a `transaction()` call made here would join an open transaction
|
|
378
|
+
* as a savepoint. On SQLite it would: the connection is the only one, so a
|
|
379
|
+
* transaction open on it encloses whatever the async chain that opened it
|
|
380
|
+
* starts.
|
|
381
|
+
*/
|
|
382
|
+
inTransaction() {
|
|
383
|
+
return this.nestingScope() !== void 0;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Whether the calling async flow is inside a transaction that has not
|
|
387
|
+
* finished, judged from the transaction this flow was started inside,
|
|
388
|
+
* never from the connection: another request's open transaction answers
|
|
389
|
+
* false here, where its own `transaction()` queues behind that one.
|
|
390
|
+
*
|
|
391
|
+
* A flow started inside a transaction, even inside a savepoint that has
|
|
392
|
+
* since released, stays inside it until its outermost transaction has
|
|
393
|
+
* committed or rolled back: until then whatever it writes on the one
|
|
394
|
+
* connection lands in that transaction.
|
|
395
|
+
*/
|
|
396
|
+
transactionOpenInFlow() {
|
|
397
|
+
const own = this.transactionScope.getStore();
|
|
398
|
+
return own !== void 0 && !outermost(own).finished;
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* The scope a `transaction()` call made here nests in, or none when it
|
|
402
|
+
* opens a transaction of its own.
|
|
403
|
+
*
|
|
404
|
+
* A call made later from inside a savepoint that has since released runs
|
|
405
|
+
* inside the innermost scope open on the connection. Queued on an enclosing
|
|
406
|
+
* scope, or on the instance, it would wait behind the scope running there,
|
|
407
|
+
* which may be the one awaiting it. Savepoints on one connection form a
|
|
408
|
+
* stack, so whatever opens now is inside that innermost scope anyway; when
|
|
409
|
+
* none is open the call queues on the instance.
|
|
410
|
+
*
|
|
411
|
+
* The innermost open scope need not be related to the call. When it is a
|
|
412
|
+
* sibling savepoint that later rolls back, the call's write rolls back with
|
|
413
|
+
* it, although the call itself already resolved. Waiting for an unrelated
|
|
414
|
+
* scope instead could wait on the very scope that awaits the call.
|
|
415
|
+
*/
|
|
416
|
+
nestingScope() {
|
|
417
|
+
const own = this.transactionScope.getStore();
|
|
418
|
+
return own?.active ? own : own?.open.at(-1);
|
|
419
|
+
}
|
|
420
|
+
/**
|
|
421
|
+
* Run `work` as a savepoint of the active transaction: released when it
|
|
422
|
+
* resolves, rolled back to when it throws.
|
|
423
|
+
*/
|
|
424
|
+
async runSavepoint(work, parent) {
|
|
425
|
+
const db = this.ensureDb();
|
|
426
|
+
const depth = parent.depth + 1;
|
|
427
|
+
const name = `nextly_sp_${depth}`;
|
|
428
|
+
try {
|
|
429
|
+
const ctx = this.createTransactionContext(db);
|
|
430
|
+
db.exec(`SAVEPOINT ${name}`);
|
|
431
|
+
const scope = openScope(parent);
|
|
432
|
+
try {
|
|
433
|
+
const result = await this.transactionScope.run(scope, () => work(ctx));
|
|
434
|
+
await settleNested(scope);
|
|
435
|
+
db.exec(`RELEASE ${name}`);
|
|
436
|
+
return result;
|
|
437
|
+
} catch (error) {
|
|
438
|
+
await settleNested(scope);
|
|
439
|
+
scope.rolledBack = true;
|
|
440
|
+
try {
|
|
441
|
+
db.exec(`ROLLBACK TO ${name}`);
|
|
442
|
+
db.exec(`RELEASE ${name}`);
|
|
443
|
+
} catch {
|
|
444
|
+
}
|
|
445
|
+
throw error;
|
|
446
|
+
}
|
|
447
|
+
} catch (error) {
|
|
448
|
+
if (types.isApplicationError(error)) throw error;
|
|
449
|
+
throw this.classifyError(error);
|
|
450
|
+
}
|
|
265
451
|
}
|
|
266
452
|
/**
|
|
267
453
|
* Inner transaction body. Kept private so callers always go through
|
|
@@ -274,17 +460,29 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
274
460
|
try {
|
|
275
461
|
const ctx = this.createTransactionContext(db);
|
|
276
462
|
db.exec("BEGIN IMMEDIATE");
|
|
463
|
+
const scope = openScope();
|
|
277
464
|
try {
|
|
278
|
-
const result = await work(ctx);
|
|
465
|
+
const result = await this.transactionScope.run(scope, () => work(ctx));
|
|
466
|
+
await settleNested(scope);
|
|
279
467
|
db.exec("COMMIT");
|
|
468
|
+
scope.finished = true;
|
|
280
469
|
if (this.config.logger?.debug) {
|
|
281
470
|
const durationMs = Date.now() - startTime;
|
|
282
471
|
this.config.logger.debug("Transaction committed", {
|
|
283
472
|
durationMs
|
|
284
473
|
});
|
|
285
474
|
}
|
|
286
|
-
return
|
|
475
|
+
return {
|
|
476
|
+
result,
|
|
477
|
+
effects: scope.held.filter(
|
|
478
|
+
(held) => !undone(held.scope, held.writesBefore)
|
|
479
|
+
)
|
|
480
|
+
};
|
|
287
481
|
} catch (error) {
|
|
482
|
+
await settleNested(scope);
|
|
483
|
+
scope.rolledBack = true;
|
|
484
|
+
scope.finished = true;
|
|
485
|
+
scope.held.length = 0;
|
|
288
486
|
try {
|
|
289
487
|
db.exec("ROLLBACK");
|
|
290
488
|
} catch {
|
|
@@ -430,11 +628,19 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
430
628
|
/**
|
|
431
629
|
* Creates a TransactionContext for the given database connection.
|
|
432
630
|
*/
|
|
631
|
+
/** A table-level `primaryKey({ columns })`, read through the SQLite table config. */
|
|
632
|
+
compositePrimaryKey(tableObj) {
|
|
633
|
+
return sqliteCore.getTableConfig(
|
|
634
|
+
tableObj
|
|
635
|
+
).primaryKeys.flatMap((key) => key.columns);
|
|
636
|
+
}
|
|
433
637
|
createTransactionContext(db) {
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
638
|
+
const txHandles = this.transactionDrizzleHandles(
|
|
639
|
+
(relations) => relations ? betterSqlite3.drizzle({ client: db, relations }) : betterSqlite3.drizzle({ client: db })
|
|
640
|
+
);
|
|
641
|
+
const txDb = txHandles.bare;
|
|
437
642
|
return {
|
|
643
|
+
drizzleWithRelations: txHandles.withRelations,
|
|
438
644
|
// SQLite has no row-level locking and needs none here: `withTransaction`
|
|
439
645
|
// opens SQLite transactions with BEGIN IMMEDIATE, which takes the write
|
|
440
646
|
// lock up front and serializes writers for the whole transaction.
|
|
@@ -536,6 +742,16 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
536
742
|
(r) => this.mapRowFromRawSql(tableObj, r)
|
|
537
743
|
);
|
|
538
744
|
},
|
|
745
|
+
// Adapter-built, as `insert` above is; `transactionUpdate` says why.
|
|
746
|
+
// A column the model does not declare binds as every other value on
|
|
747
|
+
// this path does, through `sanitizeSqliteValue`. `all` only when the
|
|
748
|
+
// statement carries RETURNING: better-sqlite3 throws on `all` for a
|
|
749
|
+
// statement that returns no rows, and on `run` for one that does.
|
|
750
|
+
update: this.transactionUpdate(
|
|
751
|
+
txDb,
|
|
752
|
+
(statement, returnsRows) => returnsRows ? txDb().all(statement) : (txDb().run(statement), void 0),
|
|
753
|
+
sanitizeSqliteValue
|
|
754
|
+
),
|
|
539
755
|
...this.createTransactionForwarders(txDb),
|
|
540
756
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
541
757
|
savepoint: async (name) => {
|
|
@@ -561,7 +777,10 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
561
777
|
// fallow-ignore-next-line complexity
|
|
562
778
|
classifyError(error, sql) {
|
|
563
779
|
if (types.isDatabaseError(error)) return error;
|
|
564
|
-
const sqliteError =
|
|
780
|
+
const sqliteError = this.driverErrorIn(
|
|
781
|
+
error,
|
|
782
|
+
(candidate) => typeof candidate.code === "string" && candidate.code !== ""
|
|
783
|
+
);
|
|
565
784
|
let kind = "unknown";
|
|
566
785
|
if (sqliteError.code) {
|
|
567
786
|
kind = SQLITE_ERROR_CODES[sqliteError.code] || "unknown";
|
|
@@ -593,21 +812,50 @@ var SqliteAdapter = class extends adapterDrizzle.DrizzleAdapter {
|
|
|
593
812
|
});
|
|
594
813
|
}
|
|
595
814
|
};
|
|
596
|
-
function findDriverError(error) {
|
|
597
|
-
let cursor = error;
|
|
598
|
-
for (let depth = 0; depth < 10 && cursor != null; depth++) {
|
|
599
|
-
if (typeof cursor.code === "string" && cursor.code.length > 0)
|
|
600
|
-
return cursor;
|
|
601
|
-
cursor = cursor.cause;
|
|
602
|
-
}
|
|
603
|
-
return error ?? {};
|
|
604
|
-
}
|
|
605
815
|
function createSqliteAdapter(config) {
|
|
606
816
|
return new SqliteAdapter(config);
|
|
607
817
|
}
|
|
608
818
|
function isSqliteAdapter(value) {
|
|
609
819
|
return value instanceof SqliteAdapter;
|
|
610
820
|
}
|
|
821
|
+
function openScope(parent) {
|
|
822
|
+
const scope = {
|
|
823
|
+
active: true,
|
|
824
|
+
depth: parent ? parent.depth + 1 : 0,
|
|
825
|
+
parent,
|
|
826
|
+
rolledBack: false,
|
|
827
|
+
writesAfterClosing: 0,
|
|
828
|
+
finished: false,
|
|
829
|
+
held: [],
|
|
830
|
+
queue: Promise.resolve(),
|
|
831
|
+
open: parent ? parent.open : []
|
|
832
|
+
};
|
|
833
|
+
scope.open.push(scope);
|
|
834
|
+
return scope;
|
|
835
|
+
}
|
|
836
|
+
function outermost(scope) {
|
|
837
|
+
let at = scope;
|
|
838
|
+
while (at.parent) at = at.parent;
|
|
839
|
+
return at;
|
|
840
|
+
}
|
|
841
|
+
function undone(scope, writesBefore) {
|
|
842
|
+
if (writesBefore > 0) return false;
|
|
843
|
+
for (let at = scope; at; at = at.parent) {
|
|
844
|
+
if (at.rolledBack) return true;
|
|
845
|
+
}
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
function enqueueIn(scope, run) {
|
|
849
|
+
const next = scope.queue.then(run, run);
|
|
850
|
+
scope.queue = next.catch(() => void 0);
|
|
851
|
+
return next;
|
|
852
|
+
}
|
|
853
|
+
async function settleNested(scope) {
|
|
854
|
+
scope.active = false;
|
|
855
|
+
const index = scope.open.indexOf(scope);
|
|
856
|
+
if (index !== -1) scope.open.splice(index, 1);
|
|
857
|
+
await scope.queue;
|
|
858
|
+
}
|
|
611
859
|
|
|
612
860
|
exports.SqliteAdapter = SqliteAdapter;
|
|
613
861
|
exports.VERSION = VERSION;
|