@storyshelf/db-sqlite 0.10.0-next-7fcea06 → 0.10.0-next-a8124f3
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/dist/better-sqlite3.mjs +1 -1
- package/dist/bun-sqlite.mjs +1 -1
- package/dist/d1.mjs +1 -1
- package/dist/drizzle-factory.d.mts +2 -1
- package/dist/drizzle-factory.mjs +31 -25
- package/dist/drizzle-factory.mjs.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/nesting-guard.mjs +33 -0
- package/dist/nesting-guard.mjs.map +1 -0
- package/dist/transaction-mutex.mjs +61 -0
- package/dist/transaction-mutex.mjs.map +1 -0
- package/dist/turso.mjs +1 -1
- package/package.json +2 -2
package/dist/better-sqlite3.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function createBetterSqlite3Database(options) {
|
|
|
37
37
|
const adapter = createDrizzleAdapter(db, {
|
|
38
38
|
metadata: {
|
|
39
39
|
name: "better-sqlite3",
|
|
40
|
-
version: "0.10.0-next-
|
|
40
|
+
version: "0.10.0-next-a8124f3",
|
|
41
41
|
description: "better-sqlite3 database adapter (native + Drizzle)",
|
|
42
42
|
kind: "better-sqlite3",
|
|
43
43
|
category: "database"
|
package/dist/bun-sqlite.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function createBunSqliteDatabase(options) {
|
|
|
37
37
|
const adapter = createDrizzleAdapter(db, {
|
|
38
38
|
metadata: {
|
|
39
39
|
name: "Bun SQLite",
|
|
40
|
-
version: "0.10.0-next-
|
|
40
|
+
version: "0.10.0-next-a8124f3",
|
|
41
41
|
description: "Bun SQLite database adapter (bun:sqlite + Drizzle)",
|
|
42
42
|
kind: "bun-sqlite",
|
|
43
43
|
category: "database"
|
package/dist/d1.mjs
CHANGED
|
@@ -22,7 +22,7 @@ function createD1Database(options) {
|
|
|
22
22
|
return createDrizzleAdapter(db, {
|
|
23
23
|
metadata: {
|
|
24
24
|
name: "Cloudflare D1",
|
|
25
|
-
version: "0.10.0-next-
|
|
25
|
+
version: "0.10.0-next-a8124f3",
|
|
26
26
|
description: "Cloudflare D1 database adapter (D1 binding + Drizzle)",
|
|
27
27
|
kind: "d1",
|
|
28
28
|
category: "database",
|
|
@@ -5,7 +5,8 @@ import { DatabaseAdapter, DrizzleAdapterOptions } from "@storyshelf/core/adapter
|
|
|
5
5
|
* Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous
|
|
6
6
|
* SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`
|
|
7
7
|
* rejects async callbacks. Statements run on the adapter's own connection,
|
|
8
|
-
* so the open transaction covers them; rollback runs on throw.
|
|
8
|
+
* so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one
|
|
9
|
+
* transaction at a time (see `transaction-mutex.ts`).
|
|
9
10
|
*
|
|
10
11
|
* @param adapter - Adapter built by {@link createDrizzleAdapter}.
|
|
11
12
|
* @param exec - Synchronous statement runner on the same connection.
|
package/dist/drizzle-factory.mjs
CHANGED
|
@@ -1,45 +1,50 @@
|
|
|
1
1
|
import "node:path";
|
|
2
2
|
import "node:url";
|
|
3
3
|
import.meta.url;
|
|
4
|
+
import { createNestingGuard } from "./nesting-guard.mjs";
|
|
5
|
+
import { createTransactionMutex } from "./transaction-mutex.mjs";
|
|
4
6
|
import { and, eq, getTableColumns } from "drizzle-orm";
|
|
5
7
|
// src/drizzle-factory.ts
|
|
6
8
|
/**
|
|
7
9
|
* Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous
|
|
8
10
|
* SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`
|
|
9
11
|
* rejects async callbacks. Statements run on the adapter's own connection,
|
|
10
|
-
* so the open transaction covers them; rollback runs on throw.
|
|
12
|
+
* so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one
|
|
13
|
+
* transaction at a time (see `transaction-mutex.ts`).
|
|
11
14
|
*
|
|
12
15
|
* @param adapter - Adapter built by {@link createDrizzleAdapter}.
|
|
13
16
|
* @param exec - Synchronous statement runner on the same connection.
|
|
14
17
|
* @returns The adapter with `transact` replaced.
|
|
15
18
|
*/
|
|
16
19
|
function withManualTransactions(adapter, exec) {
|
|
20
|
+
const mutex = createTransactionMutex(createNestingGuard());
|
|
17
21
|
return {
|
|
18
22
|
...adapter,
|
|
19
|
-
transact: async (fn) =>
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const result = await fn(tx);
|
|
33
|
-
exec("COMMIT");
|
|
34
|
-
return result;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
try {
|
|
37
|
-
exec("ROLLBACK");
|
|
38
|
-
} catch {}
|
|
39
|
-
throw error;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
23
|
+
transact: async (fn) => await mutex.run(async () => await runManualTransaction(adapter, exec, fn))
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function runManualTransaction(adapter, exec, fn) {
|
|
27
|
+
const tx = {
|
|
28
|
+
insert: async (table, values) => await adapter.insert(table, values),
|
|
29
|
+
update: async (table, id, values) => await adapter.update(table, id, values),
|
|
30
|
+
get: async (table, id) => await adapter.get(table, id),
|
|
31
|
+
remove: async (table, id) => {
|
|
32
|
+
await adapter.remove(table, id);
|
|
33
|
+
},
|
|
34
|
+
list: async (table, opts) => await adapter.list(table, opts),
|
|
35
|
+
count: async (table, where) => await adapter.count(table, where)
|
|
42
36
|
};
|
|
37
|
+
exec("BEGIN");
|
|
38
|
+
try {
|
|
39
|
+
const result = await fn(tx);
|
|
40
|
+
exec("COMMIT");
|
|
41
|
+
return result;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
try {
|
|
44
|
+
exec("ROLLBACK");
|
|
45
|
+
} catch {}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
43
48
|
}
|
|
44
49
|
/** Setup, teardown (once) and health for the adapter; teardown closes the driver a single time. */
|
|
45
50
|
function buildLifecycle(options) {
|
|
@@ -65,6 +70,7 @@ function buildLifecycle(options) {
|
|
|
65
70
|
}
|
|
66
71
|
function createDrizzleAdapter(db, options) {
|
|
67
72
|
const drizzle = db;
|
|
73
|
+
const mutex = createTransactionMutex(createNestingGuard());
|
|
68
74
|
return {
|
|
69
75
|
metadata: options.metadata,
|
|
70
76
|
tables: options.tables,
|
|
@@ -79,7 +85,7 @@ function createDrizzleAdapter(db, options) {
|
|
|
79
85
|
list: async (table, opts = {}) => await listMany(drizzle, table, opts),
|
|
80
86
|
count: async (table, where) => await drizzle.$count(table, where),
|
|
81
87
|
all: async (query) => await drizzle.all(query),
|
|
82
|
-
transact: async (fn) => await drizzle.transaction(async (tx) => await fn(buildTxStore(tx)))
|
|
88
|
+
transact: async (fn) => await mutex.run(async () => await drizzle.transaction(async (tx) => await fn(buildTxStore(tx))))
|
|
83
89
|
};
|
|
84
90
|
}
|
|
85
91
|
async function updateWhereOne(drizzle, table, id, values, where) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drizzle-factory.mjs","names":[],"sources":["../src/drizzle-factory.ts"],"sourcesContent":["/**\n * SQLite Drizzle adapter factory.\n *\n * Builds a {@link DatabaseAdapter} over any SQLite-compatible Drizzle dialect.\n */\n\nimport type {\n DatabaseAdapter,\n DrizzleAdapterOptions,\n ListOptions,\n TxStore,\n} from \"@storyshelf/core/adapter/database\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { and, eq, getTableColumns } from \"drizzle-orm\";\nimport type { AnySQLiteTable, SQLiteColumn } from \"drizzle-orm/sqlite-core\";\n\n/**\n * Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous\n * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`\n * rejects async callbacks. Statements run on the adapter's own connection,\n * so the open transaction covers them; rollback runs on throw.\n *\n * @param adapter - Adapter built by {@link createDrizzleAdapter}.\n * @param exec - Synchronous statement runner on the same connection.\n * @returns The adapter with `transact` replaced.\n */\nexport function withManualTransactions(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n): DatabaseAdapter {\n return {\n ...adapter,\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> => {\n const tx: TxStore = {\n insert: async (table, values) => await adapter.insert(table, values),\n update: async (table, id, values) => await adapter.update(table, id, values),\n get: async (table, id) => await adapter.get(table, id),\n remove: async (table, id) => {\n await adapter.remove(table, id);\n },\n list: async (table, opts) => await adapter.list(table, opts),\n count: async (table, where) => await adapter.count(table, where),\n };\n exec(\"BEGIN\");\n try {\n const result = await fn(tx);\n exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n exec(\"ROLLBACK\");\n } catch {\n // already rolled back (e.g. constraint abort) — report the cause\n }\n throw error;\n }\n },\n };\n}\n/** Setup, teardown (once) and health for the adapter; teardown closes the driver a single time. */\nfunction buildLifecycle(options: DrizzleAdapterOptions): NonNullable<DatabaseAdapter[\"lifecycle\"]> {\n let closed = false;\n return {\n setup: async () => {\n await options.migrate();\n },\n teardown: async () => {\n if (closed) {\n return;\n }\n closed = true;\n await options.close();\n },\n health: async () => {\n if (closed) {\n return { ok: false, detail: \"database closed\" };\n }\n if (options.ping) {\n await options.ping();\n }\n return { ok: true };\n },\n };\n}\n\nexport function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter {\n const drizzle = db as DrizzleLike;\n return {\n metadata: options.metadata,\n tables: options.tables,\n lifecycle: buildLifecycle(options),\n insert: async <T extends AnySQLiteTable>(\n table: T,\n values: T[\"$inferInsert\"],\n ): Promise<T[\"$inferSelect\"]> => await insertOne(drizzle, table, values),\n update: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n // Homomorphic equivalent of Partial<T[\"$inferInsert\"]>; written out so\n // the type stays deferred instead of resolving to `{}` under generics.\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n ): Promise<T[\"$inferSelect\"]> => await updateOne(drizzle, table, id, values),\n updateWhere: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n ): Promise<T[\"$inferSelect\"] | null> => await updateWhereOne(drizzle, table, id, values, where),\n get: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n ): Promise<T[\"$inferSelect\"] | null> => await getOne(drizzle, table, id),\n remove: async (table: AnySQLiteTable, id: string): Promise<void> => {\n await removeOne(drizzle, table, id);\n },\n list: async <T extends AnySQLiteTable>(\n table: T,\n opts: ListOptions = {},\n ): Promise<T[\"$inferSelect\"][]> => await listMany(drizzle, table, opts),\n count: async (table: AnySQLiteTable, where?: SQL): Promise<number> =>\n await drizzle.$count(table, where),\n all: async <T>(query: SQL): Promise<T[]> => await drizzle.all(query),\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await drizzle.transaction(async (tx) => await fn(buildTxStore(tx as DrizzleLike))),\n };\n}\n\nasync function updateWhereOne<T extends AnySQLiteTable>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n): Promise<T[\"$inferSelect\"] | null> {\n const updated = (await drizzle\n .update(table)\n .set(values)\n .where(and(eq(idOf(table), id), where) ?? where)\n .returning()\n .get()) as T[\"$inferSelect\"] | undefined;\n return updated ?? (await getOne(drizzle, table, id));\n}\n\nasync function insertOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n values: T[\"$inferInsert\"],\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle.insert(table).values(values).returning().get()) as T[\"$inferSelect\"];\n}\n\nasync function updateOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle\n .update(table)\n .set(values)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .returning()\n .get()) as T[\"$inferSelect\"];\n}\n\nasync function getOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n): Promise<T[\"$inferSelect\"] | null> {\n return (\n ((await drizzle\n .select()\n .from(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .limit(1)\n .get()) as T[\"$inferSelect\"] | undefined) ?? null\n );\n}\n\nasync function removeOne(drizzle: DrizzleLike, table: Table, id: string): Promise<void> {\n await drizzle\n .delete(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .run();\n}\n\nasync function listMany<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n opts: ListOptions = {},\n): Promise<T[\"$inferSelect\"][]> {\n const query = applyListOptions(drizzle.select().from(table), opts);\n const rows: unknown = await query.all();\n return rows as T[\"$inferSelect\"][];\n}\n\n/** The `where`, `orderBy`, `limit` and `offset` the caller asked for, in that order. */\nfunction applyListOptions(query: DrizzleSelectChain, opts: ListOptions): DrizzleSelectChain {\n let next = query;\n if (opts.where) {\n next = next.where(opts.where);\n }\n if (opts.orderBy) {\n next = next.orderBy(opts.orderBy);\n }\n if (opts.limit !== undefined) {\n next = next.limit(opts.limit);\n }\n if (opts.offset !== undefined) {\n next = next.offset(opts.offset);\n }\n return next;\n}\n\n/** CRUD subset bound to a transaction handle. */\nfunction buildTxStore(drizzle: DrizzleLike): TxStore {\n return {\n insert: async (table, values) => await insertOne(drizzle, table, values as never),\n update: async (table, id, values) => await updateOne(drizzle, table, id, values as never),\n get: async (table, id) => await getOne(drizzle, table, id),\n remove: async (table, id) => {\n await removeOne(drizzle, table, id);\n },\n list: async (table, opts) => await listMany(drizzle, table, opts),\n count: async (table, where) => await drizzle.$count(table as AnySQLiteTable, where),\n };\n}\n\n/** A value or a promise of one (sync node:sqlite vs async libSQL drivers). */\ntype MaybePromise<T> = T | Promise<T>;\n\n/** Chainable select surface shared by the sync and async Drizzle dialects. */\ninterface DrizzleSelectChain {\n where(where: SQL): DrizzleSelectChain;\n orderBy(order: SQL): DrizzleSelectChain;\n limit(n: number): DrizzleSelectChain;\n offset(n: number): DrizzleSelectChain;\n get(): MaybePromise<unknown>;\n all(): MaybePromise<unknown[]>;\n}\n\n/**\n * Minimal Drizzle surface consumed by {@link createDrizzleAdapter}. Drivers\n * pass their dialect instance as `unknown`; the cast lives here, once, so\n * driver packages stay fully typed. Covered by the adapter contract tests.\n */\ninterface DrizzleLike {\n insert(table: AnySQLiteTable): {\n values(values: unknown): { returning(): { get(): MaybePromise<unknown> } };\n };\n update(table: AnySQLiteTable): {\n set(values: unknown): { where(where: SQL): { returning(): { get(): MaybePromise<unknown> } } };\n };\n select(): { from(table: AnySQLiteTable): DrizzleSelectChain };\n delete(table: AnySQLiteTable): { where(where: SQL): { run(): MaybePromise<unknown> } };\n $count(table: AnySQLiteTable, where?: SQL): MaybePromise<number>;\n all<T>(query: SQL): MaybePromise<T[]>;\n transaction<R>(fn: (tx: unknown) => Promise<R>): Promise<R>;\n}\n\nfunction idOf(table: AnySQLiteTable): SQLiteColumn {\n const id = getTableColumns(table)[\"id\"];\n if (!id) {\n throw new Error(\"Table has no id column\");\n }\n return id;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"drizzle-factory.mjs","names":[],"sources":["../src/drizzle-factory.ts"],"sourcesContent":["/**\n * SQLite Drizzle adapter factory.\n *\n * Builds a {@link DatabaseAdapter} over any SQLite-compatible Drizzle dialect.\n */\n\nimport type {\n DatabaseAdapter,\n DrizzleAdapterOptions,\n ListOptions,\n TxStore,\n} from \"@storyshelf/core/adapter/database\";\nimport type { SQL, Table } from \"drizzle-orm\";\nimport { and, eq, getTableColumns } from \"drizzle-orm\";\nimport type { AnySQLiteTable, SQLiteColumn } from \"drizzle-orm/sqlite-core\";\nimport { createNestingGuard } from \"./nesting-guard.ts\";\nimport { createTransactionMutex } from \"./transaction-mutex.ts\";\n\n/**\n * Wrap an adapter with manual BEGIN/COMMIT transactions for synchronous\n * SQLite drivers (better-sqlite3, `bun:sqlite`), whose `transaction()`\n * rejects async callbacks. Statements run on the adapter's own connection,\n * so the open transaction covers them; rollback runs on throw. Overlapping calls queue, one\n * transaction at a time (see `transaction-mutex.ts`).\n *\n * @param adapter - Adapter built by {@link createDrizzleAdapter}.\n * @param exec - Synchronous statement runner on the same connection.\n * @returns The adapter with `transact` replaced.\n */\nexport function withManualTransactions(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n): DatabaseAdapter {\n const mutex = createTransactionMutex(createNestingGuard());\n return {\n ...adapter,\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(async () => await runManualTransaction(adapter, exec, fn)),\n };\n}\n\nasync function runManualTransaction<R>(\n adapter: DatabaseAdapter,\n exec: (sql: string) => void,\n fn: (tx: TxStore) => Promise<R>,\n): Promise<R> {\n const tx: TxStore = {\n insert: async (table, values) => await adapter.insert(table, values),\n update: async (table, id, values) => await adapter.update(table, id, values),\n get: async (table, id) => await adapter.get(table, id),\n remove: async (table, id) => {\n await adapter.remove(table, id);\n },\n list: async (table, opts) => await adapter.list(table, opts),\n count: async (table, where) => await adapter.count(table, where),\n };\n exec(\"BEGIN\");\n try {\n const result = await fn(tx);\n exec(\"COMMIT\");\n return result;\n } catch (error) {\n try {\n exec(\"ROLLBACK\");\n } catch {\n // already rolled back (e.g. constraint abort) — report the cause\n }\n throw error;\n }\n}\n\n/** Setup, teardown (once) and health for the adapter; teardown closes the driver a single time. */\nfunction buildLifecycle(options: DrizzleAdapterOptions): NonNullable<DatabaseAdapter[\"lifecycle\"]> {\n let closed = false;\n return {\n setup: async () => {\n await options.migrate();\n },\n teardown: async () => {\n if (closed) {\n return;\n }\n closed = true;\n await options.close();\n },\n health: async () => {\n if (closed) {\n return { ok: false, detail: \"database closed\" };\n }\n if (options.ping) {\n await options.ping();\n }\n return { ok: true };\n },\n };\n}\n\nexport function createDrizzleAdapter(db: unknown, options: DrizzleAdapterOptions): DatabaseAdapter {\n const drizzle = db as DrizzleLike;\n // The shared connection (or, for a libSQL file, its connection per transaction) cannot overlap\n // transactions: they queue (see `transaction-mutex.ts`).\n const mutex = createTransactionMutex(createNestingGuard());\n return {\n metadata: options.metadata,\n tables: options.tables,\n lifecycle: buildLifecycle(options),\n insert: async <T extends AnySQLiteTable>(\n table: T,\n values: T[\"$inferInsert\"],\n ): Promise<T[\"$inferSelect\"]> => await insertOne(drizzle, table, values),\n update: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n // Homomorphic equivalent of Partial<T[\"$inferInsert\"]>; written out so\n // the type stays deferred instead of resolving to `{}` under generics.\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n ): Promise<T[\"$inferSelect\"]> => await updateOne(drizzle, table, id, values),\n updateWhere: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n ): Promise<T[\"$inferSelect\"] | null> => await updateWhereOne(drizzle, table, id, values, where),\n get: async <T extends AnySQLiteTable>(\n table: T,\n id: string,\n ): Promise<T[\"$inferSelect\"] | null> => await getOne(drizzle, table, id),\n remove: async (table: AnySQLiteTable, id: string): Promise<void> => {\n await removeOne(drizzle, table, id);\n },\n list: async <T extends AnySQLiteTable>(\n table: T,\n opts: ListOptions = {},\n ): Promise<T[\"$inferSelect\"][]> => await listMany(drizzle, table, opts),\n count: async (table: AnySQLiteTable, where?: SQL): Promise<number> =>\n await drizzle.$count(table, where),\n all: async <T>(query: SQL): Promise<T[]> => await drizzle.all(query),\n transact: async <R>(fn: (tx: TxStore) => Promise<R>): Promise<R> =>\n await mutex.run(\n async () =>\n await drizzle.transaction(async (tx) => await fn(buildTxStore(tx as DrizzleLike))),\n ),\n };\n}\n\nasync function updateWhereOne<T extends AnySQLiteTable>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: { [K in keyof T[\"$inferInsert\"]]?: T[\"$inferInsert\"][K] },\n where: SQL,\n): Promise<T[\"$inferSelect\"] | null> {\n const updated = (await drizzle\n .update(table)\n .set(values)\n .where(and(eq(idOf(table), id), where) ?? where)\n .returning()\n .get()) as T[\"$inferSelect\"] | undefined;\n return updated ?? (await getOne(drizzle, table, id));\n}\n\nasync function insertOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n values: T[\"$inferInsert\"],\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle.insert(table).values(values).returning().get()) as T[\"$inferSelect\"];\n}\n\nasync function updateOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n values: Partial<T[\"$inferInsert\"]>,\n): Promise<T[\"$inferSelect\"]> {\n return (await drizzle\n .update(table)\n .set(values)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .returning()\n .get()) as T[\"$inferSelect\"];\n}\n\nasync function getOne<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n id: string,\n): Promise<T[\"$inferSelect\"] | null> {\n return (\n ((await drizzle\n .select()\n .from(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .limit(1)\n .get()) as T[\"$inferSelect\"] | undefined) ?? null\n );\n}\n\nasync function removeOne(drizzle: DrizzleLike, table: Table, id: string): Promise<void> {\n await drizzle\n .delete(table)\n .where(eq(idOf(table as AnySQLiteTable), id))\n .run();\n}\n\nasync function listMany<T extends Table>(\n drizzle: DrizzleLike,\n table: T,\n opts: ListOptions = {},\n): Promise<T[\"$inferSelect\"][]> {\n const query = applyListOptions(drizzle.select().from(table), opts);\n const rows: unknown = await query.all();\n return rows as T[\"$inferSelect\"][];\n}\n\n/** The `where`, `orderBy`, `limit` and `offset` the caller asked for, in that order. */\nfunction applyListOptions(query: DrizzleSelectChain, opts: ListOptions): DrizzleSelectChain {\n let next = query;\n if (opts.where) {\n next = next.where(opts.where);\n }\n if (opts.orderBy) {\n next = next.orderBy(opts.orderBy);\n }\n if (opts.limit !== undefined) {\n next = next.limit(opts.limit);\n }\n if (opts.offset !== undefined) {\n next = next.offset(opts.offset);\n }\n return next;\n}\n\n/** CRUD subset bound to a transaction handle. */\nfunction buildTxStore(drizzle: DrizzleLike): TxStore {\n return {\n insert: async (table, values) => await insertOne(drizzle, table, values as never),\n update: async (table, id, values) => await updateOne(drizzle, table, id, values as never),\n get: async (table, id) => await getOne(drizzle, table, id),\n remove: async (table, id) => {\n await removeOne(drizzle, table, id);\n },\n list: async (table, opts) => await listMany(drizzle, table, opts),\n count: async (table, where) => await drizzle.$count(table as AnySQLiteTable, where),\n };\n}\n\n/** A value or a promise of one (sync node:sqlite vs async libSQL drivers). */\ntype MaybePromise<T> = T | Promise<T>;\n\n/** Chainable select surface shared by the sync and async Drizzle dialects. */\ninterface DrizzleSelectChain {\n where(where: SQL): DrizzleSelectChain;\n orderBy(order: SQL): DrizzleSelectChain;\n limit(n: number): DrizzleSelectChain;\n offset(n: number): DrizzleSelectChain;\n get(): MaybePromise<unknown>;\n all(): MaybePromise<unknown[]>;\n}\n\n/**\n * Minimal Drizzle surface consumed by {@link createDrizzleAdapter}. Drivers\n * pass their dialect instance as `unknown`; the cast lives here, once, so\n * driver packages stay fully typed. Covered by the adapter contract tests.\n */\ninterface DrizzleLike {\n insert(table: AnySQLiteTable): {\n values(values: unknown): { returning(): { get(): MaybePromise<unknown> } };\n };\n update(table: AnySQLiteTable): {\n set(values: unknown): { where(where: SQL): { returning(): { get(): MaybePromise<unknown> } } };\n };\n select(): { from(table: AnySQLiteTable): DrizzleSelectChain };\n delete(table: AnySQLiteTable): { where(where: SQL): { run(): MaybePromise<unknown> } };\n $count(table: AnySQLiteTable, where?: SQL): MaybePromise<number>;\n all<T>(query: SQL): MaybePromise<T[]>;\n transaction<R>(fn: (tx: unknown) => Promise<R>): Promise<R>;\n}\n\nfunction idOf(table: AnySQLiteTable): SQLiteColumn {\n const id = getTableColumns(table)[\"id\"];\n if (!id) {\n throw new Error(\"Table has no id column\");\n }\n return id;\n}\n"],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -29,7 +29,7 @@ function createSqliteDatabase(path) {
|
|
|
29
29
|
const adapter = createDrizzleAdapter(db, {
|
|
30
30
|
metadata: {
|
|
31
31
|
name: "SQLite",
|
|
32
|
-
version: "0.10.0-next-
|
|
32
|
+
version: "0.10.0-next-a8124f3",
|
|
33
33
|
description: "SQLite database adapter (node:sqlite + Drizzle)",
|
|
34
34
|
kind: "sqlite",
|
|
35
35
|
category: "database"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/nesting-guard.ts
|
|
5
|
+
/** `AsyncLocalStorage` where the runtime has it, else undefined. */
|
|
6
|
+
function findScopeStorage() {
|
|
7
|
+
return (globalThis.process?.getBuiltinModule?.("node:async_hooks"))?.AsyncLocalStorage;
|
|
8
|
+
}
|
|
9
|
+
/** A guard for one adapter; it detects nothing where the runtime has no async context tracking. */
|
|
10
|
+
function createNestingGuard() {
|
|
11
|
+
const Storage = findScopeStorage();
|
|
12
|
+
if (Storage === void 0) return {
|
|
13
|
+
detects: false,
|
|
14
|
+
inside: () => false,
|
|
15
|
+
enter: async (body) => await body()
|
|
16
|
+
};
|
|
17
|
+
const scopes = new Storage();
|
|
18
|
+
return {
|
|
19
|
+
detects: true,
|
|
20
|
+
inside: () => scopes.getStore()?.open === true,
|
|
21
|
+
enter: async (body) => {
|
|
22
|
+
const scope = { open: true };
|
|
23
|
+
try {
|
|
24
|
+
return await scopes.run(scope, body);
|
|
25
|
+
} finally {
|
|
26
|
+
scope.open = false;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export { createNestingGuard };
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=nesting-guard.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nesting-guard.mjs","names":[],"sources":["../src/nesting-guard.ts"],"sourcesContent":["/**\n * Nested-transaction detection (see `transaction-mutex.ts`). It needs async context tracking,\n * which Node and Bun have as `node:async_hooks` and a Cloudflare Worker may not, so the module is\n * looked up at run time with `process.getBuiltinModule` and never imported: where it is missing\n * the guard detects nothing and a nested `transact` waits for the mutex's wait limit instead of failing at once. Importing it\n * statically would stop the D1 entry point from loading in a Worker without Node compatibility.\n */\nimport type { NestingGuard } from \"./transaction-mutex.ts\";\n\n/** The part of `AsyncLocalStorage` the guard uses. */\ninterface ScopeStorage {\n getStore(): { open: boolean } | undefined;\n run<R>(store: { open: boolean }, body: () => R): R;\n}\n\ntype ScopeStorageConstructor = new () => ScopeStorage;\n\n/** `AsyncLocalStorage` where the runtime has it, else undefined. */\nfunction findScopeStorage(): ScopeStorageConstructor | undefined {\n const runtime = (\n globalThis as {\n process?: { getBuiltinModule?: (id: string) => unknown };\n }\n ).process;\n const loaded = runtime?.getBuiltinModule?.(\"node:async_hooks\") as\n | { AsyncLocalStorage?: ScopeStorageConstructor }\n | undefined;\n return loaded?.AsyncLocalStorage;\n}\n\n/** A guard for one adapter; it detects nothing where the runtime has no async context tracking. */\nexport function createNestingGuard(): NestingGuard {\n const Storage = findScopeStorage();\n if (Storage === undefined) {\n return { detects: false, inside: () => false, enter: async (body) => await body() };\n }\n // Work started inside a body keeps this store after the body ends, so what marks a nested call\n // is the transaction still being open, not the store being present.\n const scopes = new Storage();\n return {\n detects: true,\n inside: () => scopes.getStore()?.open === true,\n enter: async <R>(body: () => Promise<R>): Promise<R> => {\n const scope = { open: true };\n try {\n return await scopes.run(scope, body);\n } finally {\n scope.open = false;\n }\n },\n };\n}\n"],"mappings":";;;;;AAkBA,SAAS,mBAAwD;CAS/D,QAPE,WAGA,SACsB,mBAAmB,kBAAkB,EAAA,EAG9C;AACjB;;AAGA,SAAgB,qBAAmC;CACjD,MAAM,UAAU,iBAAiB;CACjC,IAAI,YAAY,KAAA,GACd,OAAO;EAAE,SAAS;EAAO,cAAc;EAAO,OAAO,OAAO,SAAS,MAAM,KAAK;CAAE;CAIpF,MAAM,SAAS,IAAI,QAAQ;CAC3B,OAAO;EACL,SAAS;EACT,cAAc,OAAO,SAAS,CAAC,EAAE,SAAS;EAC1C,OAAO,OAAU,SAAuC;GACtD,MAAM,QAAQ,EAAE,MAAM,KAAK;GAC3B,IAAI;IACF,OAAO,MAAM,OAAO,IAAI,OAAO,IAAI;GACrC,UAAU;IACR,MAAM,OAAO;GACf;EACF;CACF;AACF"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import "node:path";
|
|
2
|
+
import "node:url";
|
|
3
|
+
import.meta.url;
|
|
4
|
+
// src/transaction-mutex.ts
|
|
5
|
+
/**
|
|
6
|
+
* How long a transaction waits for its turn where nesting cannot be detected. A nested call there
|
|
7
|
+
* waits for the transaction it sits in, which waits for it; the limit turns that into an error for
|
|
8
|
+
* the nested call, which ends the outer transaction and frees the queue. It only counts the wait
|
|
9
|
+
* for the turn, never a transaction's own run time.
|
|
10
|
+
*/
|
|
11
|
+
const UNGUARDED_WAIT_LIMIT_MS = 3e4;
|
|
12
|
+
const globalSchedule = (fire, ms) => {
|
|
13
|
+
const timer = setTimeout(fire, ms);
|
|
14
|
+
return () => {
|
|
15
|
+
clearTimeout(timer);
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** A mutex for one adapter; each adapter instance owns its own. */
|
|
19
|
+
function createTransactionMutex(guard, options = {}) {
|
|
20
|
+
const limit = options.waitLimitMs ?? (guard.detects ? void 0 : 3e4);
|
|
21
|
+
let tail = Promise.resolve();
|
|
22
|
+
return { run: async (body) => {
|
|
23
|
+
if (guard.inside()) throw new Error("transact was called inside a transaction of the same database; use `tx`");
|
|
24
|
+
const turn = new Turn();
|
|
25
|
+
const done = tail.then(async () => {
|
|
26
|
+
turn.begin();
|
|
27
|
+
return await guard.enter(body);
|
|
28
|
+
});
|
|
29
|
+
tail = done.catch(() => {});
|
|
30
|
+
return await (limit === void 0 ? done : turn.within(done, limit, options.schedule ?? globalSchedule));
|
|
31
|
+
} };
|
|
32
|
+
}
|
|
33
|
+
/** One caller's place in the queue: whether its turn has come, and a limit on how long to wait for it. */
|
|
34
|
+
var Turn = class {
|
|
35
|
+
#started = false;
|
|
36
|
+
#abandoned = false;
|
|
37
|
+
/** The turn came: stop if the caller already gave up (the wait limit passed), else go on. */
|
|
38
|
+
begin() {
|
|
39
|
+
if (this.#abandoned) throw new Error("transaction gave up waiting for its turn");
|
|
40
|
+
this.#started = true;
|
|
41
|
+
}
|
|
42
|
+
/** `done`, or an error if the turn has not come within `limitMs`. */
|
|
43
|
+
async within(done, limitMs, schedule) {
|
|
44
|
+
let cancel;
|
|
45
|
+
const expired = new Promise((_resolve, reject) => {
|
|
46
|
+
cancel = schedule(() => {
|
|
47
|
+
if (this.#started) return;
|
|
48
|
+
this.#abandoned = true;
|
|
49
|
+
reject(/* @__PURE__ */ new Error(`transaction waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \`tx\` instead`));
|
|
50
|
+
}, limitMs);
|
|
51
|
+
});
|
|
52
|
+
try {
|
|
53
|
+
return await Promise.race([done, expired]);
|
|
54
|
+
} finally {
|
|
55
|
+
cancel?.();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
export { UNGUARDED_WAIT_LIMIT_MS, createTransactionMutex };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=transaction-mutex.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transaction-mutex.mjs","names":[],"sources":["../src/transaction-mutex.ts"],"sourcesContent":["/**\n * One transaction at a time per adapter. The built-in SQLite adapters run on a single connection,\n * and SQLite refuses a `BEGIN` inside an open transaction, so two overlapping `transact` calls\n * (the queue's acks, approvals under load) would fail the second one. The mutex makes the second\n * wait for the first to commit or roll back instead.\n *\n * A callback that calls `transact` on the same adapter again could never get the mutex it already\n * holds, which would hang, so every mutex takes a {@link NestingGuard} (`nesting-guard.ts`) that\n * turns that into an immediate error saying to use the `tx` the callback was given, where the\n * runtime can track async context.\n */\n\n/** Marks the code running inside a transaction, so a nested `transact` can be told apart. */\nexport interface NestingGuard {\n /** Whether the runtime can tell a nested call from an independent one at all. */\n detects: boolean;\n /** Run `body` as a transaction; `inside()` is true within it and anything it starts, until it ends. */\n enter<R>(body: () => Promise<R>): Promise<R>;\n /** Whether the caller runs inside a transaction that has not ended. */\n inside(): boolean;\n}\n\n/** Runs transaction bodies one at a time, in the order they asked. */\nexport interface TransactionMutex {\n run<R>(body: () => Promise<R>): Promise<R>;\n}\n\n/**\n * How long a transaction waits for its turn where nesting cannot be detected. A nested call there\n * waits for the transaction it sits in, which waits for it; the limit turns that into an error for\n * the nested call, which ends the outer transaction and frees the queue. It only counts the wait\n * for the turn, never a transaction's own run time.\n */\nexport const UNGUARDED_WAIT_LIMIT_MS = 30_000;\n\n/** Options of {@link createTransactionMutex}. */\nexport interface TransactionMutexOptions {\n /** Wait limit in ms; defaults to {@link UNGUARDED_WAIT_LIMIT_MS} when the guard cannot detect, else none. */\n waitLimitMs?: number;\n /** Runs `fire` after `ms` and returns what cancels it (default the global timers); tests inject a manual one. */\n schedule?: (fire: () => void, ms: number) => () => void;\n}\n\nconst globalSchedule = (fire: () => void, ms: number): (() => void) => {\n const timer = setTimeout(fire, ms);\n return () => {\n clearTimeout(timer);\n };\n};\n\n/** A mutex for one adapter; each adapter instance owns its own. */\nexport function createTransactionMutex(\n guard: NestingGuard,\n options: TransactionMutexOptions = {},\n): TransactionMutex {\n const limit = options.waitLimitMs ?? (guard.detects ? undefined : UNGUARDED_WAIT_LIMIT_MS);\n let tail: Promise<unknown> = Promise.resolve();\n return {\n run: async <R>(body: () => Promise<R>): Promise<R> => {\n if (guard.inside()) {\n throw new Error(\"transact was called inside a transaction of the same database; use `tx`\");\n }\n const turn = new Turn();\n const done = tail.then(async () => {\n turn.begin();\n return await guard.enter(body);\n });\n // The next transaction waits for this one to end either way, never for its error.\n tail = done.catch(() => {});\n return await (limit === undefined\n ? done\n : turn.within(done, limit, options.schedule ?? globalSchedule));\n },\n };\n}\n\n/** One caller's place in the queue: whether its turn has come, and a limit on how long to wait for it. */\nclass Turn {\n #started = false;\n #abandoned = false;\n\n /** The turn came: stop if the caller already gave up (the wait limit passed), else go on. */\n begin(): void {\n if (this.#abandoned) {\n throw new Error(\"transaction gave up waiting for its turn\");\n }\n this.#started = true;\n }\n\n /** `done`, or an error if the turn has not come within `limitMs`. */\n async within<R>(\n done: Promise<R>,\n limitMs: number,\n schedule: (fire: () => void, ms: number) => () => void,\n ): Promise<R> {\n let cancel: (() => void) | undefined;\n const expired = new Promise<never>((_resolve, reject) => {\n cancel = schedule(() => {\n if (this.#started) return;\n this.#abandoned = true;\n reject(\n new Error(\n `transaction waited ${String(limitMs)} ms for the previous one to end; a transaction may have opened another on the same database, use \\`tx\\` instead`,\n ),\n );\n }, limitMs);\n });\n try {\n return await Promise.race([done, expired]);\n } finally {\n cancel?.();\n }\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,MAAa,0BAA0B;AAUvC,MAAM,kBAAkB,MAAkB,OAA6B;CACrE,MAAM,QAAQ,WAAW,MAAM,EAAE;CACjC,aAAa;EACX,aAAa,KAAK;CACpB;AACF;;AAGA,SAAgB,uBACd,OACA,UAAmC,CAAC,GAClB;CAClB,MAAM,QAAQ,QAAQ,gBAAgB,MAAM,UAAU,KAAA,IAAA;CACtD,IAAI,OAAyB,QAAQ,QAAQ;CAC7C,OAAO,EACL,KAAK,OAAU,SAAuC;EACpD,IAAI,MAAM,OAAO,GACf,MAAM,IAAI,MAAM,yEAAyE;EAE3F,MAAM,OAAO,IAAI,KAAK;EACtB,MAAM,OAAO,KAAK,KAAK,YAAY;GACjC,KAAK,MAAM;GACX,OAAO,MAAM,MAAM,MAAM,IAAI;EAC/B,CAAC;EAED,OAAO,KAAK,YAAY,CAAC,CAAC;EAC1B,OAAO,OAAO,UAAU,KAAA,IACpB,OACA,KAAK,OAAO,MAAM,OAAO,QAAQ,YAAY,cAAc;CACjE,EACF;AACF;;AAGA,IAAM,OAAN,MAAW;CACT,WAAW;CACX,aAAa;;CAGb,QAAc;EACZ,IAAI,KAAK,YACP,MAAM,IAAI,MAAM,0CAA0C;EAE5D,KAAK,WAAW;CAClB;;CAGA,MAAM,OACJ,MACA,SACA,UACY;EACZ,IAAI;EACJ,MAAM,UAAU,IAAI,SAAgB,UAAU,WAAW;GACvD,SAAS,eAAe;IACtB,IAAI,KAAK,UAAU;IACnB,KAAK,aAAa;IAClB,uBACE,IAAI,MACF,sBAAsB,OAAO,OAAO,EAAE,gHACxC,CACF;GACF,GAAG,OAAO;EACZ,CAAC;EACD,IAAI;GACF,OAAO,MAAM,QAAQ,KAAK,CAAC,MAAM,OAAO,CAAC;EAC3C,UAAU;GACR,SAAS;EACX;CACF;AACF"}
|
package/dist/turso.mjs
CHANGED
|
@@ -32,7 +32,7 @@ function createTursoDatabase(options) {
|
|
|
32
32
|
return createDrizzleAdapter(db, {
|
|
33
33
|
metadata: {
|
|
34
34
|
name: "Turso",
|
|
35
|
-
version: "0.10.0-next-
|
|
35
|
+
version: "0.10.0-next-a8124f3",
|
|
36
36
|
description: "Turso/libSQL database adapter",
|
|
37
37
|
kind: "turso",
|
|
38
38
|
category: "database"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@storyshelf/db-sqlite",
|
|
3
|
-
"version": "0.10.0-next-
|
|
3
|
+
"version": "0.10.0-next-a8124f3",
|
|
4
4
|
"description": "SQLite database adapter for StoryShelf (node:sqlite + Drizzle).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"database",
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"prepublishOnly": "nub run build"
|
|
88
88
|
},
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"@storyshelf/core": "0.10.0-next-
|
|
90
|
+
"@storyshelf/core": "0.10.0-next-a8124f3",
|
|
91
91
|
"drizzle-orm": "^0.45.4"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|