@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 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"]}
@@ -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 };
@@ -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":";;;;AA6BA,IAAM,aAAA,GAAgB,yBAAA;AAoBtB,IAAM,iBAAN,MAAyD;AAAA,EAIvD,WAAA,CACmB,IAAA,EACjB,OAAA,GAAiC,EAAC,EAClC;AAFiB,IAAA,IAAA,CAAA,IAAA,GAAA,IAAA;AAGjB,IAAA,IAAA,CAAK,SAAA,GAAY,QAAQ,SAAA,IAAa,aAAA;AACtC,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,OAAA,IAAW,aAAA;AAClC,IAAA,qBAAA,CAAsB,KAAK,SAAS,CAAA;AAAA,EACtC;AAAA,EANmB,IAAA;AAAA,EAJF,SAAA;AAAA,EACA,OAAA;AAAA,EAWjB,MAAM,gBAAmB,EAAA,EAAqD;AAC5E,IAAA,IAAI,oBAAA,CAAqB,IAAA,CAAK,IAAI,CAAA,EAAG;AACnC,MAAA,OAAO,KAAK,IAAA,CAAK,WAAA,CAAY,CAAC,EAAA,KAAwB,EAAA,CAAG,EAAE,CAAC,CAAA;AAAA,IAC9D;AAEA,IAAA,OAAO,EAAA,CAAG,KAAK,IAAI,CAAA;AAAA,EACrB;AAAA,EAEA,MAAM,WAAA,CACJ,EAAA,EACA,OAAA,EACA,SAAA,EACe;AACf,IAAA,IAAI,IAAA,CAAK,YAAY,YAAA,EAAc;AACnC,IAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,SAAS,CAAA,CAAA;AACnC,IAAA,IAAI;AAIF,MAAA,MAAM,EAAA,CAAG,OAAA,CAAQ,GAAA,CAAA,8CAAA,EAAoD,GAAG,CAAA,KAAA,CAAO,CAAA;AAAA,IACjF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,CAAA,EAAI,OAAO,CAAA,EAAA,EAAK,SAAS,CAAA,iCAAA,CAAA;AAAA,QACzB,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAA,CACJ,EAAA,EACA,OAAA,EACA,SAAA,EAC+B;AAC/B,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,OAAA;AAAA,QACtB,GAAA,CAAA,cAAA,EAAoB,GAAA,CAAI,UAAA,CAAW,IAAA,CAAK,SAAS,CAAC;AAAA,8BAAA,EAC1B,OAAO;AAAA,iCAAA,EACJ,SAAS;AAAA;AAAA,qBAAA;AAAA,OAGtC;AACA,MAAA,MAAM,IAAA,GAAO,YAA8B,MAAM,CAAA;AACjD,MAAA,MAAM,GAAA,GAAM,KAAK,CAAC,CAAA;AAClB,MAAA,OAAO,GAAA,GAAM,MAAA,CAAO,GAAG,CAAA,GAAI,IAAA;AAAA,IAC7B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,CAAA,EAAI,OAAO,CAAA,EAAA,EAAK,SAAS,CAAA,oBAAA,CAAA;AAAA,QACzB,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,gBAAA,CACJ,EAAA,EACA,GAAA,EACwB;AACxB,IAAA,IAAI;AAEF,MAAA,MAAM,EAAA,CAAG,OAAA;AAAA,QACP,GAAA,CAAA,OAAA,EAAa,GAAA,CAAI,UAAA,CAAW,IAAA,CAAK,SAAS,CAAC;AAAA;AAAA,6BAAA,EAEpB,IAAI,OAAO;AAAA,gCAAA,EACR,IAAI,SAAS;AAAA,qCAAA;AAAA,OAEzC;AAIA,MAAA,MAAM,eAAe,IAAA,CAAK,SAAA,CAAU,GAAA,CAAI,QAAA,IAAY,EAAE,CAAA;AACtD,MAAA,MAAM,MAAA,GAAS,MAAM,EAAA,CAAG,OAAA;AAAA,QACtB,GAAA,CAAA,YAAA,EAAkB,GAAA,CAAI,UAAA,CAAW,IAAA,CAAK,SAAS,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA,cAAA,EAIxC,GAAA,CAAI,OAAO,CAAA,EAAA,EAAK,GAAA,CAAI,SAAS,KAAK,GAAA,CAAI,SAAS,CAAA,EAAA,EAAK,GAAA,CAAI,OAAO,CAAA;AAAA,cAAA,EAC/D,GAAA,CAAI,OAAO,CAAA,EAAA,EAAK,GAAA,CAAI,UAAU,CAAA;AAAA,cAAA,EAC9B,GAAA,CAAI,OAAO,CAAA,EAAA,EAAK,GAAA,CAAI,SAAS,CAAA,EAAA,EAAK,YAAY,CAAA,SAAA,EAAY,GAAA,CAAI,cAAc;AAAA;AAAA,uBAAA;AAAA,OAGtF;AAEA,MAAA,MAAM,IAAA,GAAO,YAA8B,MAAM,CAAA;AACjD,MAAA,MAAM,OAAA,GAAU,KAAK,CAAC,CAAA;AACtB,MAAA,IAAI,CAAC,OAAA,EAAS;AACZ,QAAA,MAAM,IAAI,YAAA;AAAA,UACR,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAA,EAAA,EAAK,IAAI,SAAS,CAAA,yBAAA;AAAA,SACnC;AAAA,MACF;AACA,MAAA,OAAO,OAAO,OAAO,CAAA;AAAA,IACvB,SAAS,GAAA,EAAK;AAGZ,MAAA,IAAI,iBAAA,CAAkB,GAAG,CAAA,EAAG;AAC1B,QAAA,MAAM,IAAI,0BAAA,CAA2B,GAAA,CAAI,OAAA,EAAS,IAAI,SAAS,CAAA;AAAA,MACjE;AAEA,MAAA,IAAI,GAAA,YAAe,YAAA,IAAgB,GAAA,YAAe,0BAAA,EAA4B;AAC5E,QAAA,MAAM,GAAA;AAAA,MACR;AACA,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,CAAA,EAAI,GAAA,CAAI,OAAO,CAAA,EAAA,EAAK,IAAI,SAAS,CAAA,yBAAA,CAAA;AAAA,QACjC,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAA,CACJ,EAAA,EACA,OAAA,EACA,SAAA,EAC0B;AAC1B,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,IAAA;AAC1B,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,OAAA;AAAA,QAC1B,GAAA,CAAA,cAAA,EAAoB,GAAA,CAAI,UAAA,CAAW,IAAA,CAAK,SAAS,CAAC;AAAA,8BAAA,EAC1B,OAAO;AAAA,iCAAA,EACJ,SAAS;AAAA,mCAAA;AAAA,OAEtC;AACA,MAAA,OAAO,WAAA,CAA8B,MAAM,CAAA,CAAE,GAAA,CAAI,MAAM,CAAA;AAAA,IACzD,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,CAAA,EAAI,OAAO,CAAA,EAAA,EAAK,SAAS,CAAA,oBAAA,CAAA;AAAA,QACzB,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,WAAA,CACJ,EAAA,EACA,OAAA,EACA,WACA,EAAA,EAC+B;AAC/B,IAAA,MAAM,MAAA,GAAS,MAAM,IAAA,CAAK,IAAA;AAC1B,IAAA,IAAI;AAKF,MAAA,MAAM,MAAA,GAAS,MAAM,MAAA,CAAO,OAAA;AAAA,QAC1B,GAAA,CAAA,cAAA,EAAoB,GAAA,CAAI,UAAA,CAAW,IAAA,CAAK,SAAS,CAAC;AAAA,8BAAA,EAC1B,OAAO;AAAA,iCAAA,EACJ,SAAS;AAAA,kCAAA,EACR,EAAE;AAAA;AAAA,qBAAA;AAAA,OAGhC;AACA,MAAA,MAAM,IAAA,GAAO,YAA8B,MAAM,CAAA;AACjD,MAAA,MAAM,GAAA,GAAM,KAAK,CAAC,CAAA;AAClB,MAAA,OAAO,GAAA,GAAM,MAAA,CAAO,GAAG,CAAA,GAAI,IAAA;AAAA,IAC7B,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,CAAA,EAAI,OAAO,CAAA,EAAA,EAAK,SAAS,CAAA,oBAAA,CAAA;AAAA,QACzB,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAM,kBAAA,CACJ,EAAA,EACA,IAAA,EACA,QAAA,EACe;AACf,IAAA,MAAM,KAAA,GAAQ,WAAA,CAAY,IAAA,EAAM,OAAO,CAAA;AACvC,IAAA,MAAM,MAAA,GAAS,WAAA,CAAY,IAAA,EAAM,QAAQ,CAAA,IAAK,OAAA;AAC9C,IAAA,MAAM,KAAA,GAAQ,KAAK,OAAO,CAAA;AAE1B,IAAA,IAAI,CAAC,KAAA,EAAO;AACV,MAAA,MAAM,IAAI,YAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,IAAI,CAAC,KAAA,IAAS,OAAO,KAAA,KAAU,QAAA,EAAU;AACvC,MAAA,MAAM,IAAI,YAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AAEA,IAAA,qBAAA,CAAsB,KAAK,CAAA;AAC3B,IAAA,qBAAA,CAAsB,MAAM,CAAA;AAE5B,IAAA,MAAM,QAAA,GAAW,KAAA;AACjB,IAAA,MAAM,IAAA,GAAO,MAAA,CAAO,IAAA,CAAK,QAAQ,CAAA;AACjC,IAAA,IAAI,IAAA,CAAK,WAAW,CAAA,EAAG;AACrB,MAAA,MAAM,IAAI,YAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,KAAA,MAAW,GAAA,IAAO,IAAA,EAAM,qBAAA,CAAsB,GAAG,CAAA;AAKjD,IAAA,MAAM,iBAAiB,IAAA,CAAK,GAAA;AAAA,MAC1B,CAAC,CAAA,KAAM,GAAA,CAAA,EAAM,GAAA,CAAI,UAAA,CAAW,CAAC,CAAC,CAAA,GAAA,EAAM,QAAA,CAAS,CAAC,CAAC,CAAA;AAAA,KACjD;AACA,IAAA,MAAM,QAAA,GAAW,GAAA,CAAI,IAAA,CAAK,cAAA,EAAgB,GAAA,CAAA,KAAA,CAAU,CAAA;AAEpD,IAAA,IAAI;AACF,MAAA,MAAM,EAAA,CAAG,OAAA;AAAA,QACP,GAAA,CAAA,OAAA,EAAa,GAAA,CAAI,UAAA,CAAW,KAAK,CAAC;AAAA,mBAAA,EACrB,GAAA,CAAI,UAAA,CAAW,MAAM,CAAC,MAAM,QAAQ;AAAA,mBAAA,EACpC,QAAQ,CAAA;AAAA,OACvB;AAAA,IACF,SAAS,GAAA,EAAK;AACZ,MAAA,MAAM,IAAI,YAAA;AAAA,QACR,CAAA,8BAAA,EAAiC,KAAK,CAAA,CAAA,EAAI,MAAM,CAAA,CAAA;AAAA,QAChD,EAAE,OAAO,GAAA;AAAI,OACf;AAAA,IACF;AAAA,EACF;AACF,CAAA;AAUO,SAAS,oBAAA,CACd,QACA,OAAA,EAC0B;AAC1B,EAAA,OAAO,IAAI,cAAA,CAAe,MAAA,EAAQ,OAAO,CAAA;AAC3C;AAIA,SAAS,qBAAqB,MAAA,EAAyD;AACrF,EAAA,OAAO,OAAQ,OAAsC,WAAA,KAAgB,UAAA;AACvE;AAUA,SAAS,YAAe,MAAA,EAAsB;AAC5C,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,MAAA;AAClC,EAAA,IACE,MAAA,IACA,OAAO,MAAA,KAAW,QAAA,IAClB,MAAA,IAAU,UACV,KAAA,CAAM,OAAA,CAAS,MAAA,CAA8B,IAAI,CAAA,EACjD;AACA,IAAA,OAAQ,MAAA,CAAyB,IAAA;AAAA,EACnC;AACA,EAAA,MAAM,IAAI,YAAA;AAAA,IACR;AAAA,GAEF;AACF;AAQA,SAAS,kBAAkB,GAAA,EAAuB;AAChD,EAAA,IAAI,CAAC,GAAA,IAAO,OAAO,GAAA,KAAQ,UAAU,OAAO,KAAA;AAC5C,EAAA,MAAM,CAAA,GAAI,GAAA;AAKV,EAAA,IAAI,CAAA,CAAE,IAAA,KAAS,OAAA,EAAS,OAAO,IAAA;AAC/B,EAAA,IAAI,CAAA,CAAE,SAAS,OAAO,CAAA,CAAE,UAAU,QAAA,IAAa,CAAA,CAAE,KAAA,CAA6B,IAAA,KAAS,OAAA,EAAS;AAC9F,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IACE,CAAA,CAAE,YACF,OAAO,CAAA,CAAE,aAAa,QAAA,IACrB,CAAA,CAAE,QAAA,CAAgC,IAAA,KAAS,OAAA,EAC5C;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,OAAO,KAAA;AACT;AAEA,SAAS,OAAO,GAAA,EAAsC;AACpD,EAAA,OAAO;AAAA,IACL,IAAI,GAAA,CAAI,EAAA;AAAA,IACR,SAAS,GAAA,CAAI,OAAA;AAAA,IACb,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,SAAS,GAAA,CAAI,QAAA;AAAA,IACb,OAAA,EAAS,MAAA,CAAO,GAAA,CAAI,QAAQ,CAAA;AAAA,IAC5B,YAAY,GAAA,CAAI,WAAA;AAAA,IAChB,SAAS,GAAA,CAAI,QAAA;AAAA,IACb,WAAW,GAAA,CAAI,UAAA;AAAA,IACf,QAAA,EAAU,GAAA,CAAI,QAAA,IAAY,EAAC;AAAA,IAC3B,cAAA,EAAgB,MAAA,CAAO,GAAA,CAAI,eAAe,CAAA;AAAA,IAC1C,SAAA,EAAW,IAAI,UAAA,YAAsB,IAAA,GAAO,IAAI,UAAA,GAAa,IAAI,IAAA,CAAK,GAAA,CAAI,UAAU;AAAA,GACtF;AACF;AAEA,SAAS,WAAA,CAAY,KAA8B,GAAA,EAA4B;AAC7E,EAAA,MAAM,CAAA,GAAI,IAAI,GAAG,CAAA;AACjB,EAAA,OAAO,OAAO,CAAA,KAAM,QAAA,GAAW,CAAA,GAAI,IAAA;AACrC;AASA,SAAS,sBAAsB,IAAA,EAAoB;AACjD,EAAA,IAAI,CAAC,0BAAA,CAA2B,IAAA,CAAK,IAAI,CAAA,EAAG;AAC1C,IAAA,MAAM,IAAI,YAAA;AAAA,MACR,CAAA,wBAAA,EAA2B,IAAA,CAAK,SAAA,CAAU,IAAI,CAAC,CAAA,wCAAA;AAAA,KACjD;AAAA,EACF;AACF;;;AC5WO,IAAM,6BAAA,GAAmD;AAAA,EAC9D,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,GAAA,CAAA;AAAA;AAAA,EAgBA,CAAA;AAAA,gEAAA,CAAA;AAAA;AAAA;AAAA,EAKA,CAAA;AAAA;AAAA,4BAAA,CAAA;AAAA;AAAA,EAKA,CAAA;AAAA,qEAAA;AAEF;AAEO,IAAM,sBAAA,GAAiC,6BAAA,CAA8B,IAAA,CAAK,OAAO,CAAA,GAAI","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
+ }