@suluk/drizzle 0.1.5 → 0.1.6
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/package.json +1 -1
- package/src/cas.ts +11 -1
- package/src/index.ts +1 -1
- package/test/cas.test.ts +13 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/drizzle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Drizzle ORM schema -> v4 'Suluk' contract: table -> Zod (drizzle-zod) -> v4 Schema Objects, DB metadata, and generated CRUD RouteContracts. CANDIDATE tooling.",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
package/src/cas.ts
CHANGED
|
@@ -18,7 +18,7 @@ export function rowsChanged(result: WriteResult): number {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/** Minimal drizzle handle for a conditional update (bun:sqlite sync or D1 async — both awaited). */
|
|
21
|
-
export interface ClaimDb { update: (table: unknown) => { set: (values: Record<string, unknown>) => { where: (cond: SQL) => { run: () => unknown | Promise<unknown> } } } }
|
|
21
|
+
export interface ClaimDb { update: (table: unknown) => { set: (values: Record<string, unknown>) => { where: (cond: SQL) => { run: () => unknown | Promise<unknown>; returning: () => unknown | Promise<unknown> } } } }
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Atomically CLAIM a transition: `UPDATE table SET set WHERE where`, returning true iff this call changed a row.
|
|
@@ -30,3 +30,13 @@ export async function claimOnce(db: ClaimDb, table: unknown, where: SQL, set: Re
|
|
|
30
30
|
const res = await db.update(table).set(set).where(where).run();
|
|
31
31
|
return rowsChanged(res) > 0;
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Atomically CLAIM a SET of rows and RETURN them: `UPDATE table SET set WHERE where RETURNING *`. The claim-then-act
|
|
36
|
+
* variant of {@link claimOnce} — for a batch sweep (mark a waitlist notified / a cart-recovery emailed) where each
|
|
37
|
+
* row must be handled exactly once even if the sweep overlaps: a concurrent run's UPDATE claims a DISJOINT set, so
|
|
38
|
+
* the side-effect (email, notify) fires once per row. Returns the rows THIS call won; act only on those.
|
|
39
|
+
*/
|
|
40
|
+
export async function claimRows<T = Record<string, unknown>>(db: ClaimDb, table: unknown, where: SQL, set: Record<string, unknown>): Promise<T[]> {
|
|
41
|
+
return (await db.update(table).set(set).where(where).returning()) as T[];
|
|
42
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -46,4 +46,4 @@ export { tableDDL, schemaDDL, type DdlOptions } from "./ddl";
|
|
|
46
46
|
export { crudHandlers, type CrudHandlers, type CrudHandlerOptions, type CrudDb } from "./handlers";
|
|
47
47
|
// once-only WRITE primitives — the race-safe compare-and-set skeleton for money/state-machine paths (normalize the
|
|
48
48
|
// affected-row count across drivers; claim a transition exactly once). The transitions/side-effects stay in the app.
|
|
49
|
-
export { rowsChanged, claimOnce, type WriteResult, type ClaimDb } from "./cas";
|
|
49
|
+
export { rowsChanged, claimOnce, claimRows, type WriteResult, type ClaimDb } from "./cas";
|
package/test/cas.test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { Database } from "bun:sqlite";
|
|
|
7
7
|
import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
|
|
8
8
|
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
|
|
9
9
|
import { and, eq } from "drizzle-orm";
|
|
10
|
-
import { rowsChanged, claimOnce, schemaDDL, type ClaimDb } from "../src/index";
|
|
10
|
+
import { rowsChanged, claimOnce, claimRows, schemaDDL, type ClaimDb } from "../src/index";
|
|
11
11
|
|
|
12
12
|
describe("rowsChanged", () => {
|
|
13
13
|
test("normalizes bun:sqlite .changes, D1 .meta.changes, others .rowsAffected; 0 when unknown", () => {
|
|
@@ -43,3 +43,15 @@ describe("claimOnce — atomic compare-and-set", () => {
|
|
|
43
43
|
expect(db.select().from(order).where(eq(order.id, 1)).get()!.status).toBe("pending"); // untouched
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
+
|
|
47
|
+
describe("claimRows — claim a set + return exactly the rows this call won", () => {
|
|
48
|
+
let db: BunSQLiteDatabase;
|
|
49
|
+
beforeEach(() => { const s = new Database(":memory:"); s.exec(schemaDDL([order])); db = drizzle(s); for (let i = 0; i < 3; i++) db.insert(order).values({ status: "pending" }).run(); });
|
|
50
|
+
|
|
51
|
+
test("claims matching rows once; a re-run claims a disjoint (empty) set", async () => {
|
|
52
|
+
const first = await claimRows<{ id: number }>(db as unknown as ClaimDb, order, eq(order.status, "pending"), { status: "paid" });
|
|
53
|
+
expect(first.map((r) => r.id).sort()).toEqual([1, 2, 3]); // returned the rows it flipped
|
|
54
|
+
const second = await claimRows(db as unknown as ClaimDb, order, eq(order.status, "pending"), { status: "paid" });
|
|
55
|
+
expect(second.length).toBe(0); // already paid → nothing left to claim (no double-handling)
|
|
56
|
+
});
|
|
57
|
+
});
|