feat: centralize application framework primitives
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/db",
|
||||
"version": "0.8.17",
|
||||
"version": "0.8.18",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
|
||||
@@ -63,6 +63,34 @@ export function withTransaction<T>(db: Db, callback: (tx: Db) => Promise<T>): Pr
|
||||
return db.tx(callback);
|
||||
}
|
||||
|
||||
/** Execute a parameterized compare-and-set update and report whether this caller won. */
|
||||
export async function compareAndSet(
|
||||
db: Db,
|
||||
options: {
|
||||
table: string;
|
||||
idColumn?: string;
|
||||
id: string | number;
|
||||
stateColumn?: string;
|
||||
from: string;
|
||||
to: string;
|
||||
extra?: Record<string, unknown>;
|
||||
},
|
||||
): Promise<boolean> {
|
||||
const table = identifier(options.table);
|
||||
const idColumn = identifier(options.idColumn ?? "id");
|
||||
const stateColumn = identifier(options.stateColumn ?? "status");
|
||||
const entries = Object.entries(options.extra ?? {});
|
||||
const assignments = [stateColumn, ...entries.map(([name]) => identifier(name))];
|
||||
const dialect = db.driver.dialect;
|
||||
const values = [options.to, ...entries.map(([, value]) => value), options.id, options.from];
|
||||
const sql = `UPDATE ${table} SET ${assignments
|
||||
.map((name, index) => `${name} = ${placeholder(dialect, index + 1)}`)
|
||||
.join(
|
||||
", ",
|
||||
)} WHERE ${idColumn} = ${placeholder(dialect, assignments.length + 1)} AND ${stateColumn} = ${placeholder(dialect, assignments.length + 2)}`;
|
||||
return (await db.exec(sql, values)).changes === 1;
|
||||
}
|
||||
|
||||
/** Conservative default classifier for deadlock/serialization retry errors. */
|
||||
export function isRetryableTransactionError(error: unknown): boolean {
|
||||
if (!error || typeof error !== "object") return false;
|
||||
|
||||
@@ -54,6 +54,7 @@ export {
|
||||
exists,
|
||||
countRows,
|
||||
withTransaction,
|
||||
compareAndSet,
|
||||
retryTransaction,
|
||||
batch,
|
||||
databaseHealth,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
createRepository,
|
||||
databaseHealth,
|
||||
retryTransaction,
|
||||
compareAndSet,
|
||||
} from "../src/index.ts";
|
||||
import type { Driver, Row } from "../src/index.ts";
|
||||
|
||||
@@ -28,6 +29,17 @@ function memoryDriver(): Driver {
|
||||
}
|
||||
|
||||
describe("database helper kit", () => {
|
||||
test("compareAndSet reports whether the guarded transition won", async () => {
|
||||
const db = createDb(memoryDriver());
|
||||
expect(
|
||||
await compareAndSet(db, {
|
||||
table: "messages",
|
||||
id: 1,
|
||||
from: "pending",
|
||||
to: "sending",
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
test("provides repository CRUD helpers and health checks", async () => {
|
||||
const db = createDb(memoryDriver());
|
||||
const repository = createRepository<{ id: number; name: string }>(db, {
|
||||
|
||||
Reference in New Issue
Block a user