@stateledger/drizzle 0.0.0
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/LICENSE +21 -0
- package/README.md +98 -0
- package/dist/index.cjs +266 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +262 -0
- package/dist/index.js.map +1 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Enow Divine
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# @stateledger/drizzle
|
|
2
|
+
|
|
3
|
+
Drizzle + Postgres adapter for [stateledger](https://github.com/enowdivine/stateledger) — persisted, audited, concurrency-safe state machines for Node/TypeScript.
|
|
4
|
+
|
|
5
|
+
Works with any Drizzle Postgres driver: `node-postgres`, `postgres-js`, `neon-serverless`, `vercel-postgres`.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @stateledger/core @stateledger/drizzle drizzle-orm
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Plus a Drizzle-compatible Postgres driver (`pg`, `postgres`, `@neondatabase/serverless`, or `@vercel/postgres`).
|
|
14
|
+
|
|
15
|
+
## Apply the schema
|
|
16
|
+
|
|
17
|
+
The library expects a `stateledger_transitions` table. Apply it however you migrate:
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { STATELEDGER_SCHEMA_SQL } from "@stateledger/drizzle";
|
|
21
|
+
// paste into a migration file, or:
|
|
22
|
+
import { sql } from "drizzle-orm";
|
|
23
|
+
import { STATELEDGER_SCHEMA_STATEMENTS } from "@stateledger/drizzle";
|
|
24
|
+
|
|
25
|
+
for (const stmt of STATELEDGER_SCHEMA_STATEMENTS) {
|
|
26
|
+
await db.execute(sql.raw(stmt));
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
The DDL is identical to what `@stateledger/prisma` ships — same table, same indexes, same locking guarantees.
|
|
31
|
+
|
|
32
|
+
## Use it
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { drizzle } from "drizzle-orm/node-postgres";
|
|
36
|
+
import { defineMachine } from "@stateledger/core";
|
|
37
|
+
import { createDrizzleAdapter } from "@stateledger/drizzle";
|
|
38
|
+
import { Pool } from "pg";
|
|
39
|
+
|
|
40
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
41
|
+
const db = drizzle(pool);
|
|
42
|
+
|
|
43
|
+
const adapter = createDrizzleAdapter(db);
|
|
44
|
+
|
|
45
|
+
const payment = defineMachine({
|
|
46
|
+
name: "payment",
|
|
47
|
+
states: ["pending", "authorized", "captured", "failed"] as const,
|
|
48
|
+
transitions: {
|
|
49
|
+
pending: { authorize: "authorized", fail: "failed" },
|
|
50
|
+
authorized: { capture: "captured", fail: "failed" },
|
|
51
|
+
},
|
|
52
|
+
adapter,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
await payment.transition("pay_123", "authorize");
|
|
56
|
+
const current = await payment.readState("pay_123");
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Options
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
createDrizzleAdapter(db, {
|
|
63
|
+
tableName: "stateledger_transitions", // default; override for multi-tenant setups
|
|
64
|
+
locking: "pessimistic", // default; use "optimistic" to skip pg_advisory_xact_lock
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **`pessimistic`** (default) — uses `pg_advisory_xact_lock(hashtextextended(machine:subject, 0))` per transition. Concurrent writers wait their turn.
|
|
69
|
+
- **`optimistic`** — skips the advisory lock and relies on the partial unique index on `most_recent`. Faster under low contention. Caller must catch `OptimisticConcurrencyError` and retry.
|
|
70
|
+
|
|
71
|
+
## Joining an existing transaction
|
|
72
|
+
|
|
73
|
+
Pass a Drizzle transaction context in place of the top-level client:
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await db.transaction(async (tx) => {
|
|
77
|
+
await stripe.paymentIntents.create(...); // your business logic
|
|
78
|
+
const adapter = createDrizzleAdapter(tx); // joins the outer tx
|
|
79
|
+
const machine = defineMachine({ ..., adapter });
|
|
80
|
+
await machine.transition("pay_123", "authorize");
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Contract-conformant
|
|
85
|
+
|
|
86
|
+
Passes the same 13-spec [`@stateledger/core/contract-tests`](https://github.com/enowdivine/stateledger/blob/main/packages/core/src/contract-tests.ts) suite as `@stateledger/prisma` — reads, appends, rollback, `most_recent` flip invariants, cross-subject independence, and time-travel semantics.
|
|
87
|
+
|
|
88
|
+
Run it locally:
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
pnpm --filter @stateledger/drizzle test:integration
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Requires Docker (uses `testcontainers` to boot a real Postgres).
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@stateledger/core');
|
|
4
|
+
var drizzleOrm = require('drizzle-orm');
|
|
5
|
+
|
|
6
|
+
// src/adapter.ts
|
|
7
|
+
var DEFAULT_TABLE = "stateledger_transitions";
|
|
8
|
+
var DrizzleAdapter = class {
|
|
9
|
+
constructor(root, options = {}) {
|
|
10
|
+
this.root = root;
|
|
11
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE;
|
|
12
|
+
this.locking = options.locking ?? "pessimistic";
|
|
13
|
+
assertValidIdentifier(this.tableName);
|
|
14
|
+
}
|
|
15
|
+
root;
|
|
16
|
+
tableName;
|
|
17
|
+
locking;
|
|
18
|
+
async withTransaction(fn) {
|
|
19
|
+
if (hasTransactionMethod(this.root)) {
|
|
20
|
+
return this.root.transaction((tx) => fn(tx));
|
|
21
|
+
}
|
|
22
|
+
return fn(this.root);
|
|
23
|
+
}
|
|
24
|
+
async acquireLock(tx, machine, subjectId) {
|
|
25
|
+
if (this.locking === "optimistic") return;
|
|
26
|
+
const key = `${machine}:${subjectId}`;
|
|
27
|
+
try {
|
|
28
|
+
await tx.execute(drizzleOrm.sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
throw new core.AdapterError(
|
|
31
|
+
`[${machine}] ${subjectId}: failed to acquire advisory lock`,
|
|
32
|
+
{ cause: err }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async readCurrent(tx, machine, subjectId) {
|
|
37
|
+
try {
|
|
38
|
+
const result = await tx.execute(
|
|
39
|
+
drizzleOrm.sql`SELECT * FROM ${drizzleOrm.sql.identifier(this.tableName)}
|
|
40
|
+
WHERE machine = ${machine}
|
|
41
|
+
AND subject_id = ${subjectId}
|
|
42
|
+
AND most_recent = TRUE
|
|
43
|
+
LIMIT 1`
|
|
44
|
+
);
|
|
45
|
+
const rows = extractRows(result);
|
|
46
|
+
const row = rows[0];
|
|
47
|
+
return row ? mapRow(row) : null;
|
|
48
|
+
} catch (err) {
|
|
49
|
+
throw new core.AdapterError(
|
|
50
|
+
`[${machine}] ${subjectId}: readCurrent failed`,
|
|
51
|
+
{ cause: err }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async appendTransition(tx, row) {
|
|
56
|
+
try {
|
|
57
|
+
await tx.execute(
|
|
58
|
+
drizzleOrm.sql`UPDATE ${drizzleOrm.sql.identifier(this.tableName)}
|
|
59
|
+
SET most_recent = FALSE
|
|
60
|
+
WHERE machine = ${row.machine}
|
|
61
|
+
AND subject_id = ${row.subjectId}
|
|
62
|
+
AND most_recent = TRUE`
|
|
63
|
+
);
|
|
64
|
+
const metadataJson = JSON.stringify(row.metadata ?? {});
|
|
65
|
+
const result = await tx.execute(
|
|
66
|
+
drizzleOrm.sql`INSERT INTO ${drizzleOrm.sql.identifier(this.tableName)} (
|
|
67
|
+
machine, subject_id, from_state, to_state, sort_key, most_recent,
|
|
68
|
+
actor_id, actor_type, metadata, machine_version
|
|
69
|
+
) VALUES (
|
|
70
|
+
${row.machine}, ${row.subjectId}, ${row.fromState}, ${row.toState},
|
|
71
|
+
${row.sortKey}, ${row.mostRecent},
|
|
72
|
+
${row.actorId}, ${row.actorType}, ${metadataJson}::jsonb, ${row.machineVersion}
|
|
73
|
+
)
|
|
74
|
+
RETURNING *`
|
|
75
|
+
);
|
|
76
|
+
const rows = extractRows(result);
|
|
77
|
+
const created = rows[0];
|
|
78
|
+
if (!created) {
|
|
79
|
+
throw new core.AdapterError(
|
|
80
|
+
`[${row.machine}] ${row.subjectId}: INSERT returned no rows`
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return mapRow(created);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (isUniqueViolation(err)) {
|
|
86
|
+
throw new core.OptimisticConcurrencyError(row.machine, row.subjectId);
|
|
87
|
+
}
|
|
88
|
+
if (err instanceof core.AdapterError || err instanceof core.OptimisticConcurrencyError) {
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
throw new core.AdapterError(
|
|
92
|
+
`[${row.machine}] ${row.subjectId}: appendTransition failed`,
|
|
93
|
+
{ cause: err }
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async readHistory(tx, machine, subjectId) {
|
|
98
|
+
const client = tx ?? this.root;
|
|
99
|
+
try {
|
|
100
|
+
const result = await client.execute(
|
|
101
|
+
drizzleOrm.sql`SELECT * FROM ${drizzleOrm.sql.identifier(this.tableName)}
|
|
102
|
+
WHERE machine = ${machine}
|
|
103
|
+
AND subject_id = ${subjectId}
|
|
104
|
+
ORDER BY sort_key ASC`
|
|
105
|
+
);
|
|
106
|
+
return extractRows(result).map(mapRow);
|
|
107
|
+
} catch (err) {
|
|
108
|
+
throw new core.AdapterError(
|
|
109
|
+
`[${machine}] ${subjectId}: readHistory failed`,
|
|
110
|
+
{ cause: err }
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async readStateAt(tx, machine, subjectId, at) {
|
|
115
|
+
const client = tx ?? this.root;
|
|
116
|
+
try {
|
|
117
|
+
const result = await client.execute(
|
|
118
|
+
drizzleOrm.sql`SELECT * FROM ${drizzleOrm.sql.identifier(this.tableName)}
|
|
119
|
+
WHERE machine = ${machine}
|
|
120
|
+
AND subject_id = ${subjectId}
|
|
121
|
+
AND created_at <= ${at}
|
|
122
|
+
ORDER BY sort_key DESC
|
|
123
|
+
LIMIT 1`
|
|
124
|
+
);
|
|
125
|
+
const rows = extractRows(result);
|
|
126
|
+
const row = rows[0];
|
|
127
|
+
return row ? mapRow(row) : null;
|
|
128
|
+
} catch (err) {
|
|
129
|
+
throw new core.AdapterError(
|
|
130
|
+
`[${machine}] ${subjectId}: readStateAt failed`,
|
|
131
|
+
{ cause: err }
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async updateSubjectState(tx, hint, newState) {
|
|
136
|
+
const model = stringField(hint, "model");
|
|
137
|
+
const column = stringField(hint, "column") ?? "state";
|
|
138
|
+
const where = hint["where"];
|
|
139
|
+
if (!model) {
|
|
140
|
+
throw new core.AdapterError(
|
|
141
|
+
"updateSubjectState: subjectStateColumn.model is required (Postgres table name)"
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (!where || typeof where !== "object") {
|
|
145
|
+
throw new core.AdapterError(
|
|
146
|
+
"updateSubjectState: subjectStateColumn.where is required (object of identifying columns)"
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
assertValidIdentifier(model);
|
|
150
|
+
assertValidIdentifier(column);
|
|
151
|
+
const whereObj = where;
|
|
152
|
+
const keys = Object.keys(whereObj);
|
|
153
|
+
if (keys.length === 0) {
|
|
154
|
+
throw new core.AdapterError(
|
|
155
|
+
"updateSubjectState: subjectStateColumn.where must have at least one column"
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
for (const key of keys) assertValidIdentifier(key);
|
|
159
|
+
const whereFragments = keys.map(
|
|
160
|
+
(k) => drizzleOrm.sql`${drizzleOrm.sql.identifier(k)} = ${whereObj[k]}`
|
|
161
|
+
);
|
|
162
|
+
const whereSql = drizzleOrm.sql.join(whereFragments, drizzleOrm.sql` AND `);
|
|
163
|
+
try {
|
|
164
|
+
await tx.execute(
|
|
165
|
+
drizzleOrm.sql`UPDATE ${drizzleOrm.sql.identifier(model)}
|
|
166
|
+
SET ${drizzleOrm.sql.identifier(column)} = ${newState}
|
|
167
|
+
WHERE ${whereSql}`
|
|
168
|
+
);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
throw new core.AdapterError(
|
|
171
|
+
`updateSubjectState failed for ${model}.${column}`,
|
|
172
|
+
{ cause: err }
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
function createDrizzleAdapter(client, options) {
|
|
178
|
+
return new DrizzleAdapter(client, options);
|
|
179
|
+
}
|
|
180
|
+
function hasTransactionMethod(client) {
|
|
181
|
+
return typeof client.transaction === "function";
|
|
182
|
+
}
|
|
183
|
+
function extractRows(result) {
|
|
184
|
+
if (Array.isArray(result)) return result;
|
|
185
|
+
if (result && typeof result === "object" && "rows" in result && Array.isArray(result.rows)) {
|
|
186
|
+
return result.rows;
|
|
187
|
+
}
|
|
188
|
+
throw new core.AdapterError(
|
|
189
|
+
"Unexpected execute() result shape: expected an array or an object with `.rows`. Is this a Drizzle Postgres client?"
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
function isUniqueViolation(err) {
|
|
193
|
+
if (!err || typeof err !== "object") return false;
|
|
194
|
+
const e = err;
|
|
195
|
+
if (e.code === "23505") return true;
|
|
196
|
+
if (e.cause && typeof e.cause === "object" && e.cause.code === "23505") {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
if (e.original && typeof e.original === "object" && e.original.code === "23505") {
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
function mapRow(raw) {
|
|
205
|
+
return {
|
|
206
|
+
id: raw.id,
|
|
207
|
+
machine: raw.machine,
|
|
208
|
+
subjectId: raw.subject_id,
|
|
209
|
+
fromState: raw.from_state,
|
|
210
|
+
toState: raw.to_state,
|
|
211
|
+
sortKey: Number(raw.sort_key),
|
|
212
|
+
mostRecent: raw.most_recent,
|
|
213
|
+
actorId: raw.actor_id,
|
|
214
|
+
actorType: raw.actor_type,
|
|
215
|
+
metadata: raw.metadata ?? {},
|
|
216
|
+
machineVersion: Number(raw.machine_version),
|
|
217
|
+
createdAt: raw.created_at instanceof Date ? raw.created_at : new Date(raw.created_at)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function stringField(obj, key) {
|
|
221
|
+
const v = obj[key];
|
|
222
|
+
return typeof v === "string" ? v : null;
|
|
223
|
+
}
|
|
224
|
+
function assertValidIdentifier(name) {
|
|
225
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
226
|
+
throw new core.AdapterError(
|
|
227
|
+
`invalid SQL identifier: ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/schema-sql.ts
|
|
233
|
+
var STATELEDGER_SCHEMA_STATEMENTS = [
|
|
234
|
+
`CREATE TABLE IF NOT EXISTS "stateledger_transitions" (
|
|
235
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
236
|
+
machine TEXT NOT NULL,
|
|
237
|
+
subject_id TEXT NOT NULL,
|
|
238
|
+
from_state TEXT,
|
|
239
|
+
to_state TEXT NOT NULL,
|
|
240
|
+
sort_key INTEGER NOT NULL,
|
|
241
|
+
most_recent BOOLEAN NOT NULL DEFAULT TRUE,
|
|
242
|
+
actor_id TEXT,
|
|
243
|
+
actor_type TEXT,
|
|
244
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
245
|
+
machine_version INTEGER NOT NULL DEFAULT 1,
|
|
246
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
247
|
+
)`,
|
|
248
|
+
// Deterministic ordering for history reads + the (machine, subject) lookup.
|
|
249
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_subject_sort_key"
|
|
250
|
+
ON "stateledger_transitions" (machine, subject_id, sort_key)`,
|
|
251
|
+
// Partial unique index: exactly one mostRecent row per (machine, subject).
|
|
252
|
+
// This is the optimistic-concurrency safety net + a hard correctness invariant.
|
|
253
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_one_most_recent"
|
|
254
|
+
ON "stateledger_transitions" (machine, subject_id)
|
|
255
|
+
WHERE most_recent = TRUE`,
|
|
256
|
+
// "Recent transitions for this subject, newest first" — covers history pagination.
|
|
257
|
+
`CREATE INDEX IF NOT EXISTS "stateledger_transitions_history"
|
|
258
|
+
ON "stateledger_transitions" (machine, subject_id, sort_key DESC)`
|
|
259
|
+
];
|
|
260
|
+
var STATELEDGER_SCHEMA_SQL = STATELEDGER_SCHEMA_STATEMENTS.join(";\n\n") + ";";
|
|
261
|
+
|
|
262
|
+
exports.STATELEDGER_SCHEMA_SQL = STATELEDGER_SCHEMA_SQL;
|
|
263
|
+
exports.STATELEDGER_SCHEMA_STATEMENTS = STATELEDGER_SCHEMA_STATEMENTS;
|
|
264
|
+
exports.createDrizzleAdapter = createDrizzleAdapter;
|
|
265
|
+
//# sourceMappingURL=index.cjs.map
|
|
266
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/schema-sql.ts"],"names":["sql","AdapterError","OptimisticConcurrencyError"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Drizzle adapter implementation for stateledger.\n *\n * All persistence goes through Drizzle's `execute()` + `sql`` `` template.\n * The adapter doesn't require the user to have added a `stateledger`\n * schema definition to their `drizzle` schema file — only the underlying\n * `stateledger_transitions` table (see `./schema-sql.ts`).\n *\n * Structural typing means any Drizzle Postgres client (node-postgres,\n * postgres.js, neon-serverless) satisfies the `DrizzleDb` contract via\n * duck typing.\n */\n\nimport type {\n Adapter,\n NewTransitionRow,\n SubjectStateHint,\n TransitionRow,\n} from \"@stateledger/core\";\nimport { AdapterError, OptimisticConcurrencyError } from \"@stateledger/core\";\nimport { sql } from \"drizzle-orm\";\n\nimport type {\n DrizzleAdapterOptions,\n DrizzleDb,\n DrizzleExecutor,\n DrizzleTransactionalClient,\n} from \"./types.js\";\n\nconst DEFAULT_TABLE = \"stateledger_transitions\";\n\n/**\n * Raw row shape returned by Postgres before we map to `TransitionRow`.\n */\ntype RawTransitionRow = {\n id: string;\n machine: string;\n subject_id: string;\n from_state: string | null;\n to_state: string;\n sort_key: number | bigint | string;\n most_recent: boolean;\n actor_id: string | null;\n actor_type: string | null;\n metadata: Record<string, unknown> | null;\n machine_version: number | bigint | string;\n created_at: Date | string;\n};\n\nclass DrizzleAdapter implements Adapter<DrizzleExecutor> {\n private readonly tableName: string;\n private readonly locking: \"pessimistic\" | \"optimistic\";\n\n constructor(\n private readonly root: DrizzleDb,\n options: DrizzleAdapterOptions = {},\n ) {\n this.tableName = options.tableName ?? DEFAULT_TABLE;\n this.locking = options.locking ?? \"pessimistic\";\n assertValidIdentifier(this.tableName);\n }\n\n async withTransaction<R>(fn: (tx: DrizzleExecutor) => Promise<R>): Promise<R> {\n if (hasTransactionMethod(this.root)) {\n return this.root.transaction((tx: DrizzleExecutor) => fn(tx));\n }\n // Already inside a transaction context — just call the fn with it.\n return fn(this.root);\n }\n\n async acquireLock(\n tx: DrizzleExecutor,\n machine: string,\n subjectId: string,\n ): Promise<void> {\n if (this.locking === \"optimistic\") return;\n const key = `${machine}:${subjectId}`;\n try {\n // `hashtextextended` returns bigint, which `pg_advisory_xact_lock`\n // accepts. The lock auto-releases when the surrounding transaction\n // ends — no explicit unlock is needed (or possible).\n await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: failed to acquire advisory lock`,\n { cause: err },\n );\n }\n }\n\n async readCurrent(\n tx: DrizzleExecutor,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow | null> {\n try {\n const result = await tx.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n AND most_recent = TRUE\n LIMIT 1`,\n );\n const rows = extractRows<RawTransitionRow>(result);\n const row = rows[0];\n return row ? mapRow(row) : null;\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readCurrent failed`,\n { cause: err },\n );\n }\n }\n\n async appendTransition(\n tx: DrizzleExecutor,\n row: NewTransitionRow,\n ): Promise<TransitionRow> {\n try {\n // Flip the previous mostRecent row (no-op for the bootstrap transition).\n await tx.execute(\n sql`UPDATE ${sql.identifier(this.tableName)}\n SET most_recent = FALSE\n WHERE machine = ${row.machine}\n AND subject_id = ${row.subjectId}\n AND most_recent = TRUE`,\n );\n\n // Insert the new row and return it. `metadata` is JSON-encoded here\n // because Drizzle's raw execute doesn't stringify objects for JSONB.\n const metadataJson = JSON.stringify(row.metadata ?? {});\n const result = await tx.execute<unknown>(\n sql`INSERT INTO ${sql.identifier(this.tableName)} (\n machine, subject_id, from_state, to_state, sort_key, most_recent,\n actor_id, actor_type, metadata, machine_version\n ) VALUES (\n ${row.machine}, ${row.subjectId}, ${row.fromState}, ${row.toState},\n ${row.sortKey}, ${row.mostRecent},\n ${row.actorId}, ${row.actorType}, ${metadataJson}::jsonb, ${row.machineVersion}\n )\n RETURNING *`,\n );\n\n const rows = extractRows<RawTransitionRow>(result);\n const created = rows[0];\n if (!created) {\n throw new AdapterError(\n `[${row.machine}] ${row.subjectId}: INSERT returned no rows`,\n );\n }\n return mapRow(created);\n } catch (err) {\n // The partial unique index on (machine, subject_id) WHERE most_recent = TRUE\n // surfaces lost optimistic races as a unique-constraint violation.\n if (isUniqueViolation(err)) {\n throw new OptimisticConcurrencyError(row.machine, row.subjectId);\n }\n // Re-throw our own errors as-is so callers can match on them.\n if (err instanceof AdapterError || err instanceof OptimisticConcurrencyError) {\n throw err;\n }\n throw new AdapterError(\n `[${row.machine}] ${row.subjectId}: appendTransition failed`,\n { cause: err },\n );\n }\n }\n\n async readHistory(\n tx: DrizzleExecutor | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow[]> {\n const client = tx ?? this.root;\n try {\n const result = await client.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n ORDER BY sort_key ASC`,\n );\n return extractRows<RawTransitionRow>(result).map(mapRow);\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readHistory failed`,\n { cause: err },\n );\n }\n }\n\n async readStateAt(\n tx: DrizzleExecutor | null,\n machine: string,\n subjectId: string,\n at: Date,\n ): Promise<TransitionRow | null> {\n const client = tx ?? this.root;\n try {\n // Most recent transition row whose created_at <= cutoff. Order by\n // sort_key DESC (not created_at DESC) for the tiebreak — sortKey is\n // monotonic per (machine, subjectId) and is the canonical ordering\n // used everywhere else in the library.\n const result = await client.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n AND created_at <= ${at}\n ORDER BY sort_key DESC\n LIMIT 1`,\n );\n const rows = extractRows<RawTransitionRow>(result);\n const row = rows[0];\n return row ? mapRow(row) : null;\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readStateAt failed`,\n { cause: err },\n );\n }\n }\n\n async updateSubjectState(\n tx: DrizzleExecutor,\n hint: SubjectStateHint,\n newState: string,\n ): Promise<void> {\n const model = stringField(hint, \"model\");\n const column = stringField(hint, \"column\") ?? \"state\";\n const where = hint[\"where\"];\n\n if (!model) {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.model is required (Postgres table name)\",\n );\n }\n if (!where || typeof where !== \"object\") {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.where is required (object of identifying columns)\",\n );\n }\n\n assertValidIdentifier(model);\n assertValidIdentifier(column);\n\n const whereObj = where as Record<string, unknown>;\n const keys = Object.keys(whereObj);\n if (keys.length === 0) {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.where must have at least one column\",\n );\n }\n for (const key of keys) assertValidIdentifier(key);\n\n // Build the parameterized WHERE fragment via sql.join so each column\n // stays parameterized (no string concatenation of user values into\n // raw SQL).\n const whereFragments = keys.map(\n (k) => sql`${sql.identifier(k)} = ${whereObj[k]}`,\n );\n const whereSql = sql.join(whereFragments, sql` AND `);\n\n try {\n await tx.execute(\n sql`UPDATE ${sql.identifier(model)}\n SET ${sql.identifier(column)} = ${newState}\n WHERE ${whereSql}`,\n );\n } catch (err) {\n throw new AdapterError(\n `updateSubjectState failed for ${model}.${column}`,\n { cause: err },\n );\n }\n }\n}\n\n/**\n * Create an adapter bound to a Drizzle client.\n *\n * Pass a top-level Drizzle client for typical use — the adapter will open\n * its own transactions via `db.transaction()`. Pass a Drizzle transaction\n * context to join an existing transaction the caller opened higher up\n * the stack.\n */\nexport function createDrizzleAdapter(\n client: DrizzleDb,\n options?: DrizzleAdapterOptions,\n): Adapter<DrizzleExecutor> {\n return new DrizzleAdapter(client, options);\n}\n\n// ---- internal helpers ----\n\nfunction hasTransactionMethod(client: DrizzleDb): client is DrizzleTransactionalClient {\n return typeof (client as DrizzleTransactionalClient).transaction === \"function\";\n}\n\n/**\n * Normalize `execute()` output across Drizzle's Postgres drivers.\n *\n * - `drizzle-orm/node-postgres`: returns pg's `QueryResult` — `{ rows: T[] }`.\n * - `drizzle-orm/postgres-js`: returns a `RowList<T>` that IS a `T[]` (with\n * extra properties like `.count`).\n * - `drizzle-orm/neon-serverless`: also `{ rows: T[] }` (uses pg protocol).\n */\nfunction extractRows<T>(result: unknown): T[] {\n if (Array.isArray(result)) return result as T[];\n if (\n result &&\n typeof result === \"object\" &&\n \"rows\" in result &&\n Array.isArray((result as { rows?: unknown }).rows)\n ) {\n return (result as { rows: T[] }).rows;\n }\n throw new AdapterError(\n \"Unexpected execute() result shape: expected an array or an object with `.rows`. \" +\n \"Is this a Drizzle Postgres client?\",\n );\n}\n\n/**\n * Detect \"unique constraint violation\" from the Postgres driver error.\n * All Postgres drivers surface SQLSTATE `23505`; some (pg) put it on\n * `.code`, some (postgres.js) on `.code` too, node-postgres nests it\n * further in some setups. Check every plausible location.\n */\nfunction isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as {\n code?: unknown;\n cause?: { code?: unknown } | null;\n original?: { code?: unknown } | null;\n };\n if (e.code === \"23505\") return true;\n if (e.cause && typeof e.cause === \"object\" && (e.cause as { code?: unknown }).code === \"23505\") {\n return true;\n }\n if (\n e.original &&\n typeof e.original === \"object\" &&\n (e.original as { code?: unknown }).code === \"23505\"\n ) {\n return true;\n }\n return false;\n}\n\nfunction mapRow(raw: RawTransitionRow): TransitionRow {\n return {\n id: raw.id,\n machine: raw.machine,\n subjectId: raw.subject_id,\n fromState: raw.from_state,\n toState: raw.to_state,\n sortKey: Number(raw.sort_key),\n mostRecent: raw.most_recent,\n actorId: raw.actor_id,\n actorType: raw.actor_type,\n metadata: raw.metadata ?? {},\n machineVersion: Number(raw.machine_version),\n createdAt: raw.created_at instanceof Date ? raw.created_at : new Date(raw.created_at),\n };\n}\n\nfunction stringField(obj: Record<string, unknown>, key: string): string | null {\n const v = obj[key];\n return typeof v === \"string\" ? v : null;\n}\n\n/**\n * Reject table/column names that aren't safe to interpolate into raw SQL.\n *\n * `sql.identifier()` already quotes identifiers to make injection hard,\n * but we still validate at the boundary so misconfigured hints fail loud\n * and early instead of at query time.\n */\nfunction assertValidIdentifier(name: string): void {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new AdapterError(\n `invalid SQL identifier: ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`,\n );\n }\n}\n","/**\n * Recommended Postgres schema for the transitions table.\n *\n * Exported in two shapes:\n *\n * - `STATELEDGER_SCHEMA_STATEMENTS`: an array of individual DDL statements.\n * Use this when applying the schema through a driver that runs one\n * statement at a time.\n * - `STATELEDGER_SCHEMA_SQL`: the same statements concatenated into one\n * multi-statement script. Use this when writing a Drizzle migration\n * (`drizzle-kit generate`) or any tool that accepts multi-statement input.\n *\n * The SQL here is identical to what `@stateledger/prisma` ships. It's\n * duplicated intentionally: the two adapters have separate release\n * cadences, and the SQL is ORM-agnostic Postgres DDL that shouldn't\n * change often. If it needs to change, both adapters must be bumped in\n * lockstep.\n */\n\nexport const STATELEDGER_SCHEMA_STATEMENTS: readonly string[] = [\n `CREATE TABLE IF NOT EXISTS \"stateledger_transitions\" (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n machine TEXT NOT NULL,\n subject_id TEXT NOT NULL,\n from_state TEXT,\n to_state TEXT NOT NULL,\n sort_key INTEGER NOT NULL,\n most_recent BOOLEAN NOT NULL DEFAULT TRUE,\n actor_id TEXT,\n actor_type TEXT,\n metadata JSONB NOT NULL DEFAULT '{}'::jsonb,\n machine_version INTEGER NOT NULL DEFAULT 1,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )`,\n\n // Deterministic ordering for history reads + the (machine, subject) lookup.\n `CREATE UNIQUE INDEX IF NOT EXISTS \"stateledger_transitions_subject_sort_key\"\n ON \"stateledger_transitions\" (machine, subject_id, sort_key)`,\n\n // Partial unique index: exactly one mostRecent row per (machine, subject).\n // This is the optimistic-concurrency safety net + a hard correctness invariant.\n `CREATE UNIQUE INDEX IF NOT EXISTS \"stateledger_transitions_one_most_recent\"\n ON \"stateledger_transitions\" (machine, subject_id)\n WHERE most_recent = TRUE`,\n\n // \"Recent transitions for this subject, newest first\" — covers history pagination.\n `CREATE INDEX IF NOT EXISTS \"stateledger_transitions_history\"\n ON \"stateledger_transitions\" (machine, subject_id, sort_key DESC)`,\n];\n\nexport const STATELEDGER_SCHEMA_SQL: string = STATELEDGER_SCHEMA_STATEMENTS.join(\";\\n\\n\") + \";\";\n"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Adapter } from '@stateledger/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public types for the Drizzle adapter.
|
|
5
|
+
*
|
|
6
|
+
* The adapter does NOT import Drizzle's driver-specific types at the type
|
|
7
|
+
* boundary — Drizzle has separate flavors (`drizzle-orm/node-postgres`,
|
|
8
|
+
* `drizzle-orm/postgres-js`, `drizzle-orm/neon-serverless`, etc.), each
|
|
9
|
+
* with a slightly different `execute()` return shape. We define the
|
|
10
|
+
* minimal structural surface the adapter actually uses; any Drizzle
|
|
11
|
+
* Postgres client satisfies it via duck typing.
|
|
12
|
+
*
|
|
13
|
+
* The `sql` template tag is imported from `drizzle-orm` at value level —
|
|
14
|
+
* users install `drizzle-orm` as a peer, so it's always available.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Minimal interface for any Drizzle Postgres client capable of executing
|
|
18
|
+
* a raw SQL fragment. `execute()`'s return shape varies by driver — for
|
|
19
|
+
* `drizzle-orm/node-postgres` it's `{ rows: T[] }` (pg's `QueryResult`),
|
|
20
|
+
* for `drizzle-orm/postgres-js` it's `T[]` directly. Both are unwrapped
|
|
21
|
+
* by the adapter internally.
|
|
22
|
+
*/
|
|
23
|
+
type DrizzleExecutor = {
|
|
24
|
+
execute<T = unknown>(query: any): Promise<T>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Minimal interface for a top-level Drizzle client (capable of opening a
|
|
28
|
+
* transaction). Drizzle's `db.transaction(async (tx) => …)` calls back with
|
|
29
|
+
* a scoped client that still exposes `execute()` — that's the `tx` we hand
|
|
30
|
+
* on to the adapter's methods.
|
|
31
|
+
*/
|
|
32
|
+
type DrizzleTransactionalClient = DrizzleExecutor & {
|
|
33
|
+
transaction<R>(fn: (tx: any) => Promise<R>): Promise<R>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What `createDrizzleAdapter` accepts: either a top-level Drizzle client
|
|
37
|
+
* (and we open our own transactions) or a Drizzle transaction context
|
|
38
|
+
* (and we join the caller's transaction).
|
|
39
|
+
*/
|
|
40
|
+
type DrizzleDb = DrizzleTransactionalClient | DrizzleExecutor;
|
|
41
|
+
type DrizzleAdapterOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Postgres table that holds the transition rows. Defaults to
|
|
44
|
+
* `stateledger_transitions`. Override if you've namespaced it (e.g. per
|
|
45
|
+
* tenant schema, or to avoid a collision in a legacy database).
|
|
46
|
+
*/
|
|
47
|
+
tableName?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Locking strategy.
|
|
50
|
+
* - `"pessimistic"` (default): `pg_advisory_xact_lock` per
|
|
51
|
+
* `(machine, subjectId)`. Concurrent writers wait their turn.
|
|
52
|
+
* - `"optimistic"`: skip the advisory lock; rely on the partial unique
|
|
53
|
+
* index on `most_recent` to detect lost races. Caller must catch
|
|
54
|
+
* `OptimisticConcurrencyError` and retry.
|
|
55
|
+
*/
|
|
56
|
+
locking?: "pessimistic" | "optimistic";
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Shape of the optional `subjectStateColumn` hint passed to
|
|
60
|
+
* `updateSubjectState`.
|
|
61
|
+
*
|
|
62
|
+
* Example (for a Drizzle `payments` table with `id` primary key):
|
|
63
|
+
* ```ts
|
|
64
|
+
* subjectStateColumn: {
|
|
65
|
+
* model: "payments", // Postgres table name
|
|
66
|
+
* where: { id: payment.id }, // identifying columns
|
|
67
|
+
* column: "state", // column to update
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
type DrizzleSubjectStateHint = {
|
|
72
|
+
/** Postgres table name. */
|
|
73
|
+
model: string;
|
|
74
|
+
/** Columns to identify the row. */
|
|
75
|
+
where: Record<string, string | number | boolean>;
|
|
76
|
+
/** Column to write the new state into. Defaults to `state`. */
|
|
77
|
+
column?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drizzle adapter implementation for stateledger.
|
|
82
|
+
*
|
|
83
|
+
* All persistence goes through Drizzle's `execute()` + `sql`` `` template.
|
|
84
|
+
* The adapter doesn't require the user to have added a `stateledger`
|
|
85
|
+
* schema definition to their `drizzle` schema file — only the underlying
|
|
86
|
+
* `stateledger_transitions` table (see `./schema-sql.ts`).
|
|
87
|
+
*
|
|
88
|
+
* Structural typing means any Drizzle Postgres client (node-postgres,
|
|
89
|
+
* postgres.js, neon-serverless) satisfies the `DrizzleDb` contract via
|
|
90
|
+
* duck typing.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create an adapter bound to a Drizzle client.
|
|
95
|
+
*
|
|
96
|
+
* Pass a top-level Drizzle client for typical use — the adapter will open
|
|
97
|
+
* its own transactions via `db.transaction()`. Pass a Drizzle transaction
|
|
98
|
+
* context to join an existing transaction the caller opened higher up
|
|
99
|
+
* the stack.
|
|
100
|
+
*/
|
|
101
|
+
declare function createDrizzleAdapter(client: DrizzleDb, options?: DrizzleAdapterOptions): Adapter<DrizzleExecutor>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recommended Postgres schema for the transitions table.
|
|
105
|
+
*
|
|
106
|
+
* Exported in two shapes:
|
|
107
|
+
*
|
|
108
|
+
* - `STATELEDGER_SCHEMA_STATEMENTS`: an array of individual DDL statements.
|
|
109
|
+
* Use this when applying the schema through a driver that runs one
|
|
110
|
+
* statement at a time.
|
|
111
|
+
* - `STATELEDGER_SCHEMA_SQL`: the same statements concatenated into one
|
|
112
|
+
* multi-statement script. Use this when writing a Drizzle migration
|
|
113
|
+
* (`drizzle-kit generate`) or any tool that accepts multi-statement input.
|
|
114
|
+
*
|
|
115
|
+
* The SQL here is identical to what `@stateledger/prisma` ships. It's
|
|
116
|
+
* duplicated intentionally: the two adapters have separate release
|
|
117
|
+
* cadences, and the SQL is ORM-agnostic Postgres DDL that shouldn't
|
|
118
|
+
* change often. If it needs to change, both adapters must be bumped in
|
|
119
|
+
* lockstep.
|
|
120
|
+
*/
|
|
121
|
+
declare const STATELEDGER_SCHEMA_STATEMENTS: readonly string[];
|
|
122
|
+
declare const STATELEDGER_SCHEMA_SQL: string;
|
|
123
|
+
|
|
124
|
+
export { type DrizzleAdapterOptions, type DrizzleDb, type DrizzleExecutor, type DrizzleSubjectStateHint, type DrizzleTransactionalClient, STATELEDGER_SCHEMA_SQL, STATELEDGER_SCHEMA_STATEMENTS, createDrizzleAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Adapter } from '@stateledger/core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Public types for the Drizzle adapter.
|
|
5
|
+
*
|
|
6
|
+
* The adapter does NOT import Drizzle's driver-specific types at the type
|
|
7
|
+
* boundary — Drizzle has separate flavors (`drizzle-orm/node-postgres`,
|
|
8
|
+
* `drizzle-orm/postgres-js`, `drizzle-orm/neon-serverless`, etc.), each
|
|
9
|
+
* with a slightly different `execute()` return shape. We define the
|
|
10
|
+
* minimal structural surface the adapter actually uses; any Drizzle
|
|
11
|
+
* Postgres client satisfies it via duck typing.
|
|
12
|
+
*
|
|
13
|
+
* The `sql` template tag is imported from `drizzle-orm` at value level —
|
|
14
|
+
* users install `drizzle-orm` as a peer, so it's always available.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Minimal interface for any Drizzle Postgres client capable of executing
|
|
18
|
+
* a raw SQL fragment. `execute()`'s return shape varies by driver — for
|
|
19
|
+
* `drizzle-orm/node-postgres` it's `{ rows: T[] }` (pg's `QueryResult`),
|
|
20
|
+
* for `drizzle-orm/postgres-js` it's `T[]` directly. Both are unwrapped
|
|
21
|
+
* by the adapter internally.
|
|
22
|
+
*/
|
|
23
|
+
type DrizzleExecutor = {
|
|
24
|
+
execute<T = unknown>(query: any): Promise<T>;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Minimal interface for a top-level Drizzle client (capable of opening a
|
|
28
|
+
* transaction). Drizzle's `db.transaction(async (tx) => …)` calls back with
|
|
29
|
+
* a scoped client that still exposes `execute()` — that's the `tx` we hand
|
|
30
|
+
* on to the adapter's methods.
|
|
31
|
+
*/
|
|
32
|
+
type DrizzleTransactionalClient = DrizzleExecutor & {
|
|
33
|
+
transaction<R>(fn: (tx: any) => Promise<R>): Promise<R>;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* What `createDrizzleAdapter` accepts: either a top-level Drizzle client
|
|
37
|
+
* (and we open our own transactions) or a Drizzle transaction context
|
|
38
|
+
* (and we join the caller's transaction).
|
|
39
|
+
*/
|
|
40
|
+
type DrizzleDb = DrizzleTransactionalClient | DrizzleExecutor;
|
|
41
|
+
type DrizzleAdapterOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* Postgres table that holds the transition rows. Defaults to
|
|
44
|
+
* `stateledger_transitions`. Override if you've namespaced it (e.g. per
|
|
45
|
+
* tenant schema, or to avoid a collision in a legacy database).
|
|
46
|
+
*/
|
|
47
|
+
tableName?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Locking strategy.
|
|
50
|
+
* - `"pessimistic"` (default): `pg_advisory_xact_lock` per
|
|
51
|
+
* `(machine, subjectId)`. Concurrent writers wait their turn.
|
|
52
|
+
* - `"optimistic"`: skip the advisory lock; rely on the partial unique
|
|
53
|
+
* index on `most_recent` to detect lost races. Caller must catch
|
|
54
|
+
* `OptimisticConcurrencyError` and retry.
|
|
55
|
+
*/
|
|
56
|
+
locking?: "pessimistic" | "optimistic";
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Shape of the optional `subjectStateColumn` hint passed to
|
|
60
|
+
* `updateSubjectState`.
|
|
61
|
+
*
|
|
62
|
+
* Example (for a Drizzle `payments` table with `id` primary key):
|
|
63
|
+
* ```ts
|
|
64
|
+
* subjectStateColumn: {
|
|
65
|
+
* model: "payments", // Postgres table name
|
|
66
|
+
* where: { id: payment.id }, // identifying columns
|
|
67
|
+
* column: "state", // column to update
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
type DrizzleSubjectStateHint = {
|
|
72
|
+
/** Postgres table name. */
|
|
73
|
+
model: string;
|
|
74
|
+
/** Columns to identify the row. */
|
|
75
|
+
where: Record<string, string | number | boolean>;
|
|
76
|
+
/** Column to write the new state into. Defaults to `state`. */
|
|
77
|
+
column?: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Drizzle adapter implementation for stateledger.
|
|
82
|
+
*
|
|
83
|
+
* All persistence goes through Drizzle's `execute()` + `sql`` `` template.
|
|
84
|
+
* The adapter doesn't require the user to have added a `stateledger`
|
|
85
|
+
* schema definition to their `drizzle` schema file — only the underlying
|
|
86
|
+
* `stateledger_transitions` table (see `./schema-sql.ts`).
|
|
87
|
+
*
|
|
88
|
+
* Structural typing means any Drizzle Postgres client (node-postgres,
|
|
89
|
+
* postgres.js, neon-serverless) satisfies the `DrizzleDb` contract via
|
|
90
|
+
* duck typing.
|
|
91
|
+
*/
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Create an adapter bound to a Drizzle client.
|
|
95
|
+
*
|
|
96
|
+
* Pass a top-level Drizzle client for typical use — the adapter will open
|
|
97
|
+
* its own transactions via `db.transaction()`. Pass a Drizzle transaction
|
|
98
|
+
* context to join an existing transaction the caller opened higher up
|
|
99
|
+
* the stack.
|
|
100
|
+
*/
|
|
101
|
+
declare function createDrizzleAdapter(client: DrizzleDb, options?: DrizzleAdapterOptions): Adapter<DrizzleExecutor>;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recommended Postgres schema for the transitions table.
|
|
105
|
+
*
|
|
106
|
+
* Exported in two shapes:
|
|
107
|
+
*
|
|
108
|
+
* - `STATELEDGER_SCHEMA_STATEMENTS`: an array of individual DDL statements.
|
|
109
|
+
* Use this when applying the schema through a driver that runs one
|
|
110
|
+
* statement at a time.
|
|
111
|
+
* - `STATELEDGER_SCHEMA_SQL`: the same statements concatenated into one
|
|
112
|
+
* multi-statement script. Use this when writing a Drizzle migration
|
|
113
|
+
* (`drizzle-kit generate`) or any tool that accepts multi-statement input.
|
|
114
|
+
*
|
|
115
|
+
* The SQL here is identical to what `@stateledger/prisma` ships. It's
|
|
116
|
+
* duplicated intentionally: the two adapters have separate release
|
|
117
|
+
* cadences, and the SQL is ORM-agnostic Postgres DDL that shouldn't
|
|
118
|
+
* change often. If it needs to change, both adapters must be bumped in
|
|
119
|
+
* lockstep.
|
|
120
|
+
*/
|
|
121
|
+
declare const STATELEDGER_SCHEMA_STATEMENTS: readonly string[];
|
|
122
|
+
declare const STATELEDGER_SCHEMA_SQL: string;
|
|
123
|
+
|
|
124
|
+
export { type DrizzleAdapterOptions, type DrizzleDb, type DrizzleExecutor, type DrizzleSubjectStateHint, type DrizzleTransactionalClient, STATELEDGER_SCHEMA_SQL, STATELEDGER_SCHEMA_STATEMENTS, createDrizzleAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { AdapterError, OptimisticConcurrencyError } from '@stateledger/core';
|
|
2
|
+
import { sql } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
// src/adapter.ts
|
|
5
|
+
var DEFAULT_TABLE = "stateledger_transitions";
|
|
6
|
+
var DrizzleAdapter = class {
|
|
7
|
+
constructor(root, options = {}) {
|
|
8
|
+
this.root = root;
|
|
9
|
+
this.tableName = options.tableName ?? DEFAULT_TABLE;
|
|
10
|
+
this.locking = options.locking ?? "pessimistic";
|
|
11
|
+
assertValidIdentifier(this.tableName);
|
|
12
|
+
}
|
|
13
|
+
root;
|
|
14
|
+
tableName;
|
|
15
|
+
locking;
|
|
16
|
+
async withTransaction(fn) {
|
|
17
|
+
if (hasTransactionMethod(this.root)) {
|
|
18
|
+
return this.root.transaction((tx) => fn(tx));
|
|
19
|
+
}
|
|
20
|
+
return fn(this.root);
|
|
21
|
+
}
|
|
22
|
+
async acquireLock(tx, machine, subjectId) {
|
|
23
|
+
if (this.locking === "optimistic") return;
|
|
24
|
+
const key = `${machine}:${subjectId}`;
|
|
25
|
+
try {
|
|
26
|
+
await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);
|
|
27
|
+
} catch (err) {
|
|
28
|
+
throw new AdapterError(
|
|
29
|
+
`[${machine}] ${subjectId}: failed to acquire advisory lock`,
|
|
30
|
+
{ cause: err }
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
async readCurrent(tx, machine, subjectId) {
|
|
35
|
+
try {
|
|
36
|
+
const result = await tx.execute(
|
|
37
|
+
sql`SELECT * FROM ${sql.identifier(this.tableName)}
|
|
38
|
+
WHERE machine = ${machine}
|
|
39
|
+
AND subject_id = ${subjectId}
|
|
40
|
+
AND most_recent = TRUE
|
|
41
|
+
LIMIT 1`
|
|
42
|
+
);
|
|
43
|
+
const rows = extractRows(result);
|
|
44
|
+
const row = rows[0];
|
|
45
|
+
return row ? mapRow(row) : null;
|
|
46
|
+
} catch (err) {
|
|
47
|
+
throw new AdapterError(
|
|
48
|
+
`[${machine}] ${subjectId}: readCurrent failed`,
|
|
49
|
+
{ cause: err }
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async appendTransition(tx, row) {
|
|
54
|
+
try {
|
|
55
|
+
await tx.execute(
|
|
56
|
+
sql`UPDATE ${sql.identifier(this.tableName)}
|
|
57
|
+
SET most_recent = FALSE
|
|
58
|
+
WHERE machine = ${row.machine}
|
|
59
|
+
AND subject_id = ${row.subjectId}
|
|
60
|
+
AND most_recent = TRUE`
|
|
61
|
+
);
|
|
62
|
+
const metadataJson = JSON.stringify(row.metadata ?? {});
|
|
63
|
+
const result = await tx.execute(
|
|
64
|
+
sql`INSERT INTO ${sql.identifier(this.tableName)} (
|
|
65
|
+
machine, subject_id, from_state, to_state, sort_key, most_recent,
|
|
66
|
+
actor_id, actor_type, metadata, machine_version
|
|
67
|
+
) VALUES (
|
|
68
|
+
${row.machine}, ${row.subjectId}, ${row.fromState}, ${row.toState},
|
|
69
|
+
${row.sortKey}, ${row.mostRecent},
|
|
70
|
+
${row.actorId}, ${row.actorType}, ${metadataJson}::jsonb, ${row.machineVersion}
|
|
71
|
+
)
|
|
72
|
+
RETURNING *`
|
|
73
|
+
);
|
|
74
|
+
const rows = extractRows(result);
|
|
75
|
+
const created = rows[0];
|
|
76
|
+
if (!created) {
|
|
77
|
+
throw new AdapterError(
|
|
78
|
+
`[${row.machine}] ${row.subjectId}: INSERT returned no rows`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return mapRow(created);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (isUniqueViolation(err)) {
|
|
84
|
+
throw new OptimisticConcurrencyError(row.machine, row.subjectId);
|
|
85
|
+
}
|
|
86
|
+
if (err instanceof AdapterError || err instanceof OptimisticConcurrencyError) {
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
throw new AdapterError(
|
|
90
|
+
`[${row.machine}] ${row.subjectId}: appendTransition failed`,
|
|
91
|
+
{ cause: err }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async readHistory(tx, machine, subjectId) {
|
|
96
|
+
const client = tx ?? this.root;
|
|
97
|
+
try {
|
|
98
|
+
const result = await client.execute(
|
|
99
|
+
sql`SELECT * FROM ${sql.identifier(this.tableName)}
|
|
100
|
+
WHERE machine = ${machine}
|
|
101
|
+
AND subject_id = ${subjectId}
|
|
102
|
+
ORDER BY sort_key ASC`
|
|
103
|
+
);
|
|
104
|
+
return extractRows(result).map(mapRow);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
throw new AdapterError(
|
|
107
|
+
`[${machine}] ${subjectId}: readHistory failed`,
|
|
108
|
+
{ cause: err }
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async readStateAt(tx, machine, subjectId, at) {
|
|
113
|
+
const client = tx ?? this.root;
|
|
114
|
+
try {
|
|
115
|
+
const result = await client.execute(
|
|
116
|
+
sql`SELECT * FROM ${sql.identifier(this.tableName)}
|
|
117
|
+
WHERE machine = ${machine}
|
|
118
|
+
AND subject_id = ${subjectId}
|
|
119
|
+
AND created_at <= ${at}
|
|
120
|
+
ORDER BY sort_key DESC
|
|
121
|
+
LIMIT 1`
|
|
122
|
+
);
|
|
123
|
+
const rows = extractRows(result);
|
|
124
|
+
const row = rows[0];
|
|
125
|
+
return row ? mapRow(row) : null;
|
|
126
|
+
} catch (err) {
|
|
127
|
+
throw new AdapterError(
|
|
128
|
+
`[${machine}] ${subjectId}: readStateAt failed`,
|
|
129
|
+
{ cause: err }
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async updateSubjectState(tx, hint, newState) {
|
|
134
|
+
const model = stringField(hint, "model");
|
|
135
|
+
const column = stringField(hint, "column") ?? "state";
|
|
136
|
+
const where = hint["where"];
|
|
137
|
+
if (!model) {
|
|
138
|
+
throw new AdapterError(
|
|
139
|
+
"updateSubjectState: subjectStateColumn.model is required (Postgres table name)"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (!where || typeof where !== "object") {
|
|
143
|
+
throw new AdapterError(
|
|
144
|
+
"updateSubjectState: subjectStateColumn.where is required (object of identifying columns)"
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
assertValidIdentifier(model);
|
|
148
|
+
assertValidIdentifier(column);
|
|
149
|
+
const whereObj = where;
|
|
150
|
+
const keys = Object.keys(whereObj);
|
|
151
|
+
if (keys.length === 0) {
|
|
152
|
+
throw new AdapterError(
|
|
153
|
+
"updateSubjectState: subjectStateColumn.where must have at least one column"
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
for (const key of keys) assertValidIdentifier(key);
|
|
157
|
+
const whereFragments = keys.map(
|
|
158
|
+
(k) => sql`${sql.identifier(k)} = ${whereObj[k]}`
|
|
159
|
+
);
|
|
160
|
+
const whereSql = sql.join(whereFragments, sql` AND `);
|
|
161
|
+
try {
|
|
162
|
+
await tx.execute(
|
|
163
|
+
sql`UPDATE ${sql.identifier(model)}
|
|
164
|
+
SET ${sql.identifier(column)} = ${newState}
|
|
165
|
+
WHERE ${whereSql}`
|
|
166
|
+
);
|
|
167
|
+
} catch (err) {
|
|
168
|
+
throw new AdapterError(
|
|
169
|
+
`updateSubjectState failed for ${model}.${column}`,
|
|
170
|
+
{ cause: err }
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
function createDrizzleAdapter(client, options) {
|
|
176
|
+
return new DrizzleAdapter(client, options);
|
|
177
|
+
}
|
|
178
|
+
function hasTransactionMethod(client) {
|
|
179
|
+
return typeof client.transaction === "function";
|
|
180
|
+
}
|
|
181
|
+
function extractRows(result) {
|
|
182
|
+
if (Array.isArray(result)) return result;
|
|
183
|
+
if (result && typeof result === "object" && "rows" in result && Array.isArray(result.rows)) {
|
|
184
|
+
return result.rows;
|
|
185
|
+
}
|
|
186
|
+
throw new AdapterError(
|
|
187
|
+
"Unexpected execute() result shape: expected an array or an object with `.rows`. Is this a Drizzle Postgres client?"
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
function isUniqueViolation(err) {
|
|
191
|
+
if (!err || typeof err !== "object") return false;
|
|
192
|
+
const e = err;
|
|
193
|
+
if (e.code === "23505") return true;
|
|
194
|
+
if (e.cause && typeof e.cause === "object" && e.cause.code === "23505") {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
if (e.original && typeof e.original === "object" && e.original.code === "23505") {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
function mapRow(raw) {
|
|
203
|
+
return {
|
|
204
|
+
id: raw.id,
|
|
205
|
+
machine: raw.machine,
|
|
206
|
+
subjectId: raw.subject_id,
|
|
207
|
+
fromState: raw.from_state,
|
|
208
|
+
toState: raw.to_state,
|
|
209
|
+
sortKey: Number(raw.sort_key),
|
|
210
|
+
mostRecent: raw.most_recent,
|
|
211
|
+
actorId: raw.actor_id,
|
|
212
|
+
actorType: raw.actor_type,
|
|
213
|
+
metadata: raw.metadata ?? {},
|
|
214
|
+
machineVersion: Number(raw.machine_version),
|
|
215
|
+
createdAt: raw.created_at instanceof Date ? raw.created_at : new Date(raw.created_at)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function stringField(obj, key) {
|
|
219
|
+
const v = obj[key];
|
|
220
|
+
return typeof v === "string" ? v : null;
|
|
221
|
+
}
|
|
222
|
+
function assertValidIdentifier(name) {
|
|
223
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {
|
|
224
|
+
throw new AdapterError(
|
|
225
|
+
`invalid SQL identifier: ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/schema-sql.ts
|
|
231
|
+
var STATELEDGER_SCHEMA_STATEMENTS = [
|
|
232
|
+
`CREATE TABLE IF NOT EXISTS "stateledger_transitions" (
|
|
233
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
234
|
+
machine TEXT NOT NULL,
|
|
235
|
+
subject_id TEXT NOT NULL,
|
|
236
|
+
from_state TEXT,
|
|
237
|
+
to_state TEXT NOT NULL,
|
|
238
|
+
sort_key INTEGER NOT NULL,
|
|
239
|
+
most_recent BOOLEAN NOT NULL DEFAULT TRUE,
|
|
240
|
+
actor_id TEXT,
|
|
241
|
+
actor_type TEXT,
|
|
242
|
+
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
243
|
+
machine_version INTEGER NOT NULL DEFAULT 1,
|
|
244
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
245
|
+
)`,
|
|
246
|
+
// Deterministic ordering for history reads + the (machine, subject) lookup.
|
|
247
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_subject_sort_key"
|
|
248
|
+
ON "stateledger_transitions" (machine, subject_id, sort_key)`,
|
|
249
|
+
// Partial unique index: exactly one mostRecent row per (machine, subject).
|
|
250
|
+
// This is the optimistic-concurrency safety net + a hard correctness invariant.
|
|
251
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS "stateledger_transitions_one_most_recent"
|
|
252
|
+
ON "stateledger_transitions" (machine, subject_id)
|
|
253
|
+
WHERE most_recent = TRUE`,
|
|
254
|
+
// "Recent transitions for this subject, newest first" — covers history pagination.
|
|
255
|
+
`CREATE INDEX IF NOT EXISTS "stateledger_transitions_history"
|
|
256
|
+
ON "stateledger_transitions" (machine, subject_id, sort_key DESC)`
|
|
257
|
+
];
|
|
258
|
+
var STATELEDGER_SCHEMA_SQL = STATELEDGER_SCHEMA_STATEMENTS.join(";\n\n") + ";";
|
|
259
|
+
|
|
260
|
+
export { STATELEDGER_SCHEMA_SQL, STATELEDGER_SCHEMA_STATEMENTS, createDrizzleAdapter };
|
|
261
|
+
//# sourceMappingURL=index.js.map
|
|
262
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/schema-sql.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Drizzle adapter implementation for stateledger.\n *\n * All persistence goes through Drizzle's `execute()` + `sql`` `` template.\n * The adapter doesn't require the user to have added a `stateledger`\n * schema definition to their `drizzle` schema file — only the underlying\n * `stateledger_transitions` table (see `./schema-sql.ts`).\n *\n * Structural typing means any Drizzle Postgres client (node-postgres,\n * postgres.js, neon-serverless) satisfies the `DrizzleDb` contract via\n * duck typing.\n */\n\nimport type {\n Adapter,\n NewTransitionRow,\n SubjectStateHint,\n TransitionRow,\n} from \"@stateledger/core\";\nimport { AdapterError, OptimisticConcurrencyError } from \"@stateledger/core\";\nimport { sql } from \"drizzle-orm\";\n\nimport type {\n DrizzleAdapterOptions,\n DrizzleDb,\n DrizzleExecutor,\n DrizzleTransactionalClient,\n} from \"./types.js\";\n\nconst DEFAULT_TABLE = \"stateledger_transitions\";\n\n/**\n * Raw row shape returned by Postgres before we map to `TransitionRow`.\n */\ntype RawTransitionRow = {\n id: string;\n machine: string;\n subject_id: string;\n from_state: string | null;\n to_state: string;\n sort_key: number | bigint | string;\n most_recent: boolean;\n actor_id: string | null;\n actor_type: string | null;\n metadata: Record<string, unknown> | null;\n machine_version: number | bigint | string;\n created_at: Date | string;\n};\n\nclass DrizzleAdapter implements Adapter<DrizzleExecutor> {\n private readonly tableName: string;\n private readonly locking: \"pessimistic\" | \"optimistic\";\n\n constructor(\n private readonly root: DrizzleDb,\n options: DrizzleAdapterOptions = {},\n ) {\n this.tableName = options.tableName ?? DEFAULT_TABLE;\n this.locking = options.locking ?? \"pessimistic\";\n assertValidIdentifier(this.tableName);\n }\n\n async withTransaction<R>(fn: (tx: DrizzleExecutor) => Promise<R>): Promise<R> {\n if (hasTransactionMethod(this.root)) {\n return this.root.transaction((tx: DrizzleExecutor) => fn(tx));\n }\n // Already inside a transaction context — just call the fn with it.\n return fn(this.root);\n }\n\n async acquireLock(\n tx: DrizzleExecutor,\n machine: string,\n subjectId: string,\n ): Promise<void> {\n if (this.locking === \"optimistic\") return;\n const key = `${machine}:${subjectId}`;\n try {\n // `hashtextextended` returns bigint, which `pg_advisory_xact_lock`\n // accepts. The lock auto-releases when the surrounding transaction\n // ends — no explicit unlock is needed (or possible).\n await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: failed to acquire advisory lock`,\n { cause: err },\n );\n }\n }\n\n async readCurrent(\n tx: DrizzleExecutor,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow | null> {\n try {\n const result = await tx.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n AND most_recent = TRUE\n LIMIT 1`,\n );\n const rows = extractRows<RawTransitionRow>(result);\n const row = rows[0];\n return row ? mapRow(row) : null;\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readCurrent failed`,\n { cause: err },\n );\n }\n }\n\n async appendTransition(\n tx: DrizzleExecutor,\n row: NewTransitionRow,\n ): Promise<TransitionRow> {\n try {\n // Flip the previous mostRecent row (no-op for the bootstrap transition).\n await tx.execute(\n sql`UPDATE ${sql.identifier(this.tableName)}\n SET most_recent = FALSE\n WHERE machine = ${row.machine}\n AND subject_id = ${row.subjectId}\n AND most_recent = TRUE`,\n );\n\n // Insert the new row and return it. `metadata` is JSON-encoded here\n // because Drizzle's raw execute doesn't stringify objects for JSONB.\n const metadataJson = JSON.stringify(row.metadata ?? {});\n const result = await tx.execute<unknown>(\n sql`INSERT INTO ${sql.identifier(this.tableName)} (\n machine, subject_id, from_state, to_state, sort_key, most_recent,\n actor_id, actor_type, metadata, machine_version\n ) VALUES (\n ${row.machine}, ${row.subjectId}, ${row.fromState}, ${row.toState},\n ${row.sortKey}, ${row.mostRecent},\n ${row.actorId}, ${row.actorType}, ${metadataJson}::jsonb, ${row.machineVersion}\n )\n RETURNING *`,\n );\n\n const rows = extractRows<RawTransitionRow>(result);\n const created = rows[0];\n if (!created) {\n throw new AdapterError(\n `[${row.machine}] ${row.subjectId}: INSERT returned no rows`,\n );\n }\n return mapRow(created);\n } catch (err) {\n // The partial unique index on (machine, subject_id) WHERE most_recent = TRUE\n // surfaces lost optimistic races as a unique-constraint violation.\n if (isUniqueViolation(err)) {\n throw new OptimisticConcurrencyError(row.machine, row.subjectId);\n }\n // Re-throw our own errors as-is so callers can match on them.\n if (err instanceof AdapterError || err instanceof OptimisticConcurrencyError) {\n throw err;\n }\n throw new AdapterError(\n `[${row.machine}] ${row.subjectId}: appendTransition failed`,\n { cause: err },\n );\n }\n }\n\n async readHistory(\n tx: DrizzleExecutor | null,\n machine: string,\n subjectId: string,\n ): Promise<TransitionRow[]> {\n const client = tx ?? this.root;\n try {\n const result = await client.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n ORDER BY sort_key ASC`,\n );\n return extractRows<RawTransitionRow>(result).map(mapRow);\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readHistory failed`,\n { cause: err },\n );\n }\n }\n\n async readStateAt(\n tx: DrizzleExecutor | null,\n machine: string,\n subjectId: string,\n at: Date,\n ): Promise<TransitionRow | null> {\n const client = tx ?? this.root;\n try {\n // Most recent transition row whose created_at <= cutoff. Order by\n // sort_key DESC (not created_at DESC) for the tiebreak — sortKey is\n // monotonic per (machine, subjectId) and is the canonical ordering\n // used everywhere else in the library.\n const result = await client.execute<unknown>(\n sql`SELECT * FROM ${sql.identifier(this.tableName)}\n WHERE machine = ${machine}\n AND subject_id = ${subjectId}\n AND created_at <= ${at}\n ORDER BY sort_key DESC\n LIMIT 1`,\n );\n const rows = extractRows<RawTransitionRow>(result);\n const row = rows[0];\n return row ? mapRow(row) : null;\n } catch (err) {\n throw new AdapterError(\n `[${machine}] ${subjectId}: readStateAt failed`,\n { cause: err },\n );\n }\n }\n\n async updateSubjectState(\n tx: DrizzleExecutor,\n hint: SubjectStateHint,\n newState: string,\n ): Promise<void> {\n const model = stringField(hint, \"model\");\n const column = stringField(hint, \"column\") ?? \"state\";\n const where = hint[\"where\"];\n\n if (!model) {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.model is required (Postgres table name)\",\n );\n }\n if (!where || typeof where !== \"object\") {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.where is required (object of identifying columns)\",\n );\n }\n\n assertValidIdentifier(model);\n assertValidIdentifier(column);\n\n const whereObj = where as Record<string, unknown>;\n const keys = Object.keys(whereObj);\n if (keys.length === 0) {\n throw new AdapterError(\n \"updateSubjectState: subjectStateColumn.where must have at least one column\",\n );\n }\n for (const key of keys) assertValidIdentifier(key);\n\n // Build the parameterized WHERE fragment via sql.join so each column\n // stays parameterized (no string concatenation of user values into\n // raw SQL).\n const whereFragments = keys.map(\n (k) => sql`${sql.identifier(k)} = ${whereObj[k]}`,\n );\n const whereSql = sql.join(whereFragments, sql` AND `);\n\n try {\n await tx.execute(\n sql`UPDATE ${sql.identifier(model)}\n SET ${sql.identifier(column)} = ${newState}\n WHERE ${whereSql}`,\n );\n } catch (err) {\n throw new AdapterError(\n `updateSubjectState failed for ${model}.${column}`,\n { cause: err },\n );\n }\n }\n}\n\n/**\n * Create an adapter bound to a Drizzle client.\n *\n * Pass a top-level Drizzle client for typical use — the adapter will open\n * its own transactions via `db.transaction()`. Pass a Drizzle transaction\n * context to join an existing transaction the caller opened higher up\n * the stack.\n */\nexport function createDrizzleAdapter(\n client: DrizzleDb,\n options?: DrizzleAdapterOptions,\n): Adapter<DrizzleExecutor> {\n return new DrizzleAdapter(client, options);\n}\n\n// ---- internal helpers ----\n\nfunction hasTransactionMethod(client: DrizzleDb): client is DrizzleTransactionalClient {\n return typeof (client as DrizzleTransactionalClient).transaction === \"function\";\n}\n\n/**\n * Normalize `execute()` output across Drizzle's Postgres drivers.\n *\n * - `drizzle-orm/node-postgres`: returns pg's `QueryResult` — `{ rows: T[] }`.\n * - `drizzle-orm/postgres-js`: returns a `RowList<T>` that IS a `T[]` (with\n * extra properties like `.count`).\n * - `drizzle-orm/neon-serverless`: also `{ rows: T[] }` (uses pg protocol).\n */\nfunction extractRows<T>(result: unknown): T[] {\n if (Array.isArray(result)) return result as T[];\n if (\n result &&\n typeof result === \"object\" &&\n \"rows\" in result &&\n Array.isArray((result as { rows?: unknown }).rows)\n ) {\n return (result as { rows: T[] }).rows;\n }\n throw new AdapterError(\n \"Unexpected execute() result shape: expected an array or an object with `.rows`. \" +\n \"Is this a Drizzle Postgres client?\",\n );\n}\n\n/**\n * Detect \"unique constraint violation\" from the Postgres driver error.\n * All Postgres drivers surface SQLSTATE `23505`; some (pg) put it on\n * `.code`, some (postgres.js) on `.code` too, node-postgres nests it\n * further in some setups. Check every plausible location.\n */\nfunction isUniqueViolation(err: unknown): boolean {\n if (!err || typeof err !== \"object\") return false;\n const e = err as {\n code?: unknown;\n cause?: { code?: unknown } | null;\n original?: { code?: unknown } | null;\n };\n if (e.code === \"23505\") return true;\n if (e.cause && typeof e.cause === \"object\" && (e.cause as { code?: unknown }).code === \"23505\") {\n return true;\n }\n if (\n e.original &&\n typeof e.original === \"object\" &&\n (e.original as { code?: unknown }).code === \"23505\"\n ) {\n return true;\n }\n return false;\n}\n\nfunction mapRow(raw: RawTransitionRow): TransitionRow {\n return {\n id: raw.id,\n machine: raw.machine,\n subjectId: raw.subject_id,\n fromState: raw.from_state,\n toState: raw.to_state,\n sortKey: Number(raw.sort_key),\n mostRecent: raw.most_recent,\n actorId: raw.actor_id,\n actorType: raw.actor_type,\n metadata: raw.metadata ?? {},\n machineVersion: Number(raw.machine_version),\n createdAt: raw.created_at instanceof Date ? raw.created_at : new Date(raw.created_at),\n };\n}\n\nfunction stringField(obj: Record<string, unknown>, key: string): string | null {\n const v = obj[key];\n return typeof v === \"string\" ? v : null;\n}\n\n/**\n * Reject table/column names that aren't safe to interpolate into raw SQL.\n *\n * `sql.identifier()` already quotes identifiers to make injection hard,\n * but we still validate at the boundary so misconfigured hints fail loud\n * and early instead of at query time.\n */\nfunction assertValidIdentifier(name: string): void {\n if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) {\n throw new AdapterError(\n `invalid SQL identifier: ${JSON.stringify(name)} (must match /^[A-Za-z_][A-Za-z0-9_]*$/)`,\n );\n }\n}\n","/**\n * Recommended Postgres schema for the transitions table.\n *\n * Exported in two shapes:\n *\n * - `STATELEDGER_SCHEMA_STATEMENTS`: an array of individual DDL statements.\n * Use this when applying the schema through a driver that runs one\n * statement at a time.\n * - `STATELEDGER_SCHEMA_SQL`: the same statements concatenated into one\n * multi-statement script. Use this when writing a Drizzle migration\n * (`drizzle-kit generate`) or any tool that accepts multi-statement input.\n *\n * The SQL here is identical to what `@stateledger/prisma` ships. It's\n * duplicated intentionally: the two adapters have separate release\n * cadences, and the SQL is ORM-agnostic Postgres DDL that shouldn't\n * change often. If it needs to change, both adapters must be bumped in\n * lockstep.\n */\n\nexport const STATELEDGER_SCHEMA_STATEMENTS: readonly string[] = [\n `CREATE TABLE IF NOT EXISTS \"stateledger_transitions\" (\n id UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n machine TEXT NOT NULL,\n subject_id TEXT NOT NULL,\n from_state TEXT,\n to_state TEXT NOT NULL,\n sort_key INTEGER NOT NULL,\n most_recent BOOLEAN NOT NULL DEFAULT TRUE,\n actor_id TEXT,\n actor_type TEXT,\n metadata JSONB NOT NULL DEFAULT '{}'::jsonb,\n machine_version INTEGER NOT NULL DEFAULT 1,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n )`,\n\n // Deterministic ordering for history reads + the (machine, subject) lookup.\n `CREATE UNIQUE INDEX IF NOT EXISTS \"stateledger_transitions_subject_sort_key\"\n ON \"stateledger_transitions\" (machine, subject_id, sort_key)`,\n\n // Partial unique index: exactly one mostRecent row per (machine, subject).\n // This is the optimistic-concurrency safety net + a hard correctness invariant.\n `CREATE UNIQUE INDEX IF NOT EXISTS \"stateledger_transitions_one_most_recent\"\n ON \"stateledger_transitions\" (machine, subject_id)\n WHERE most_recent = TRUE`,\n\n // \"Recent transitions for this subject, newest first\" — covers history pagination.\n `CREATE INDEX IF NOT EXISTS \"stateledger_transitions_history\"\n ON \"stateledger_transitions\" (machine, subject_id, sort_key DESC)`,\n];\n\nexport const STATELEDGER_SCHEMA_SQL: string = STATELEDGER_SCHEMA_STATEMENTS.join(\";\\n\\n\") + \";\";\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stateledger/drizzle",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "Drizzle + Postgres adapter for stateledger — persisted, audited, concurrency-safe state machines.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Enow Divine",
|
|
8
|
+
"email": "sirdivine16@gmail.com",
|
|
9
|
+
"url": "https://github.com/enowdivine"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/enowdivine/stateledger.git",
|
|
14
|
+
"directory": "packages/drizzle"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/enowdivine/stateledger#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/enowdivine/stateledger/issues"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"state-machine",
|
|
22
|
+
"drizzle",
|
|
23
|
+
"drizzle-orm",
|
|
24
|
+
"postgres",
|
|
25
|
+
"stateledger",
|
|
26
|
+
"audit-trail",
|
|
27
|
+
"fsm"
|
|
28
|
+
],
|
|
29
|
+
"type": "module",
|
|
30
|
+
"main": "./dist/index.cjs",
|
|
31
|
+
"module": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"import": "./dist/index.js",
|
|
37
|
+
"require": "./dist/index.cjs"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=20"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@stateledger/core": "0.2.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"drizzle-orm": ">=0.36"
|
|
55
|
+
},
|
|
56
|
+
"devDependencies": {
|
|
57
|
+
"drizzle-orm": "^0.36.4",
|
|
58
|
+
"pg": "^8.13.1",
|
|
59
|
+
"@types/pg": "^8.11.10",
|
|
60
|
+
"testcontainers": "^10.16.0"
|
|
61
|
+
},
|
|
62
|
+
"scripts": {
|
|
63
|
+
"build": "tsup",
|
|
64
|
+
"dev": "tsup --watch",
|
|
65
|
+
"typecheck": "tsc --noEmit",
|
|
66
|
+
"test": "vitest run --passWithNoTests",
|
|
67
|
+
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
68
|
+
"clean": "rm -rf dist"
|
|
69
|
+
}
|
|
70
|
+
}
|