@relaybus/postgres 0.0.0-stage → 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 Berkcan Şavur
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 CHANGED
@@ -1,3 +1,210 @@
1
- # Temporary Holding Version
1
+ # @relaybus/postgres
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ PostgreSQL event store for [relaybus](https://github.com/berkcansavur/relaybus): a transactional outbox with
4
+ `FOR UPDATE SKIP LOCKED` leasing, per-key ordering and fenced state changes. PostgreSQL 13 or newer.
5
+ Works with `pg`, Kysely, Knex, TypeORM, Drizzle and Prisma, so events are written in the **same transaction as your
6
+ business data**.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install @relaybus/core @relaybus/postgres pg
12
+ npm install -D @types/pg
13
+ # plus the library you already use (optional peers): kysely | knex | typeorm | drizzle-orm
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ### `pg` (default)
19
+
20
+ ```ts
21
+ import { Pool } from 'pg';
22
+ import { RelayBus } from '@relaybus/core';
23
+ import { PostgresEventStore } from '@relaybus/postgres';
24
+
25
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
26
+ const bus = new RelayBus({ store: new PostgresEventStore({ pool }) });
27
+ bus.subscribe(
28
+ OrderPlaced,
29
+ async (event) => {
30
+ /* ... */
31
+ },
32
+ { id: 'send-confirmation' },
33
+ );
34
+ await bus.start(); // creates tables unless setupStore: false
35
+
36
+ const client = await pool.connect();
37
+ try {
38
+ await client.query('BEGIN');
39
+ await client.query('INSERT INTO orders ...');
40
+ await bus.publish(new OrderPlaced({ orderId }), { transaction: client, key: orderId });
41
+ await client.query('COMMIT');
42
+ } catch (error) {
43
+ await client.query('ROLLBACK');
44
+ throw error;
45
+ } finally {
46
+ client.release();
47
+ }
48
+ ```
49
+
50
+ The transaction handle is a `pg` `PoolClient`. If you pass `connectionString` instead of `pool`, the store creates and,
51
+ on `close()`, ends its own pool. A pool or executor you pass in is never closed by the store.
52
+
53
+ ### Other libraries
54
+
55
+ Each adapter lives in its own entry point, so `@relaybus/postgres` itself never imports an ORM.
56
+
57
+ | Library | Import | Transaction handle you pass to `publish` |
58
+ | ------------------------- | --------------------------------------------------------- | --------------------------------------------------------------- |
59
+ | Kysely | `createKyselyExecutor` from `@relaybus/postgres/kysely` | `trx` of `db.transaction().execute(trx => ...)` |
60
+ | Knex | `createKnexExecutor` from `@relaybus/postgres/knex` | `trx` of `knex.transaction(trx => ...)` |
61
+ | TypeORM | `createTypeOrmExecutor` from `@relaybus/postgres/typeorm` | the `EntityManager` of `dataSource.transaction(manager => ...)` |
62
+ | Drizzle (`node-postgres`) | `createDrizzleExecutor` from `@relaybus/postgres/drizzle` | `tx` of `db.transaction(tx => ...)` |
63
+ | Prisma | `createPrismaExecutor` from `@relaybus/postgres/prisma` | `tx` of `prisma.$transaction(async tx => ...)` |
64
+
65
+ ```ts
66
+ import { createKyselyExecutor } from '@relaybus/postgres/kysely';
67
+
68
+ const store = new PostgresEventStore({ executor: createKyselyExecutor(db) });
69
+ const bus = new RelayBus({ store });
70
+ await db.transaction().execute(async (trx) => {
71
+ await trx.insertInto('orders').values(order).execute();
72
+ await bus.publish(new OrderPlaced({ orderId: order.id }), { transaction: trx });
73
+ });
74
+ ```
75
+
76
+ Use `transactionResolver` (for example from `@nestjs-cls/transactional`) to avoid passing the handle explicitly.
77
+ For another client, implement `PostgresExecutor` (`transaction` and `query`; about 20 lines).
78
+
79
+ **Prisma is not integration-tested yet.** The adapter is typed structurally (no Prisma dependency) and unit-tested
80
+ against a fake client only. Prisma interactive transactions default to a 5 s timeout, so pass
81
+ `createPrismaExecutor(prisma, { timeout, maxWait })` if handlers run longer.
82
+
83
+ ## Options
84
+
85
+ | Option | Default | Meaning |
86
+ | ---------------------------------------- | --------------------- | --------------------------------------------------------------------------- |
87
+ | `pool` / `connectionString` / `executor` | required, exactly one | How to reach the database. |
88
+ | `poolConfig` | none | Extra `pg` pool settings when `connectionString` is used. |
89
+ | `schema` | `'public'` | Schema of the tables. Created by `setup()` when it is not `public`. |
90
+ | `tablePrefix` | `'relaybus_'` | Tables are `<prefix>events`, `<prefix>deliveries`, `<prefix>key_sequences`. |
91
+ | `purgeBatchSize` | `1000` | Deliveries deleted per transaction by `purge`. |
92
+ | `logger` | `console` | Receives idle-connection errors of a pool the store created itself. |
93
+
94
+ **Pool errors.** A pool created from `connectionString` gets an `error` listener that logs through `logger`
95
+ (`[PostgresEventStore](constructor) Idle connection failed ...`); `pg` then replaces the dead connection, so a failover
96
+ or `pg_terminate_backend` of an idle connection does not crash the process. A `pool` you pass in is yours: attach
97
+ `pool.on('error', ...)` to it yourself, because without a listener `pg` re-throws the error as an uncaught exception.
98
+
99
+ `schema` and `tablePrefix` are validated (`[a-z_][a-z0-9_]*`, names at most 63 characters) before they are placed in SQL;
100
+ anything else throws `ConfigurationError`.
101
+
102
+ ## Schema and migrations
103
+
104
+ `setup()` (called by `bus.start()`) creates everything with `IF NOT EXISTS` inside a transaction guarded by
105
+ `pg_advisory_xact_lock`, so any number of processes can start at once. The lock key is derived from the schema name, so
106
+ stores with different table prefixes in one schema (which race on `CREATE SCHEMA IF NOT EXISTS`) are serialized too. To own the schema yourself, set
107
+ `setupStore: false` and put the DDL into your migration tool:
108
+
109
+ ```ts
110
+ import { createPostgresSchemaSql, createPostgresSchemaStatements } from '@relaybus/postgres';
111
+
112
+ const migration = createPostgresSchemaSql({ schema: 'app', tablePrefix: 'bus_' });
113
+ ```
114
+
115
+ | Table | Purpose |
116
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
117
+ | `events` | One row per event: `id` (**text**, so any caller id works), `type`, `source`, `key`, `sequence`, `payload jsonb`, `headers jsonb`, correlation and causation ids, `occurred_at`, `created_at`. |
118
+ | `deliveries` | One row per (event, handler): status, attempts, `available_at`, lease owner/token/expiry, `last_error jsonb`, key and sequence copied from the event, timestamps. `unique (event_id, handler)`, foreign key to events with `ON DELETE CASCADE`. |
119
+ | `key_sequences` | One counter row per ordering key. |
120
+
121
+ Indexes on `deliveries`: a partial index on the due time of pending/processing rows (statistics), the same for unkeyed
122
+ rows only (claim), two partial indexes on `(handler, key, ...)` for the ordering check, `(status, created_at, id)` and
123
+ `(handler, created_at, id)` for `listDeliveries` filtered by status or handler, and partial indexes for purging
124
+ completed and dead rows; on `events`, `(created_at, id)` for the purge sweep. `listDeliveries({ eventType })` has no
125
+ index of its own (it filters through a join with `events`); combine it with `status` or `handler`, or add
126
+ `CREATE INDEX ON <prefix>events (type)` if you query by type often. `deliveries.id` uses the `C` collation so id tie-breaks are the
127
+ same on every database.
128
+
129
+ ## Guarantees
130
+
131
+ - **Atomic publish.** Events and deliveries are inserted in your transaction; roll back and nothing exists.
132
+ - **Per-key order.** On append, a key's counter row in `key_sequences` is upserted
133
+ (`INSERT ... ON CONFLICT (key) DO UPDATE SET sequence = sequence + n`). That row lock is held until the publishing
134
+ transaction ends, so two transactions publishing the same key are serialized and the sequence order equals the commit
135
+ order. Within one `append` (one `publishAll`) keys are locked in sorted order and events and deliveries are inserted in
136
+ primary-key order, so concurrent single calls cannot deadlock each other. Claiming hands out a keyed delivery only when no earlier (same handler and key) delivery
137
+ is pending or processing, so one key is processed one event at a time, in order. A retrying or leased head blocks its
138
+ successors; a dead-lettered head releases them. A `limit` of zero or less claims nothing.
139
+ - **Safe parallel claiming.** One statement chooses candidates with `FOR UPDATE SKIP LOCKED`, applies `LIMIT` after the
140
+ ordering filter, and leases them (`processing`, `attempts + 1`, a new `gen_random_uuid()` lease token). SKIP LOCKED
141
+ alone is sufficient for ordering: only the head of a key is ever a candidate, and a racing claimer either skips the
142
+ locked head or, if the other claim already committed, re-checks the row (now processing, not due) and drops it.
143
+ Followers stay blocked because the head remains pending or processing in every snapshot until it is completed or
144
+ buried. The stress test in `test/claim-concurrency.integration.test.ts` runs 12 claimers on 3 pools against 30 keys
145
+ and fails if a key is ever in flight twice or out of order. A keyed delivery is also not claimed while another
146
+ delivery of the same handler and key holds a **live** lease, which covers a requeued or newly attached delivery
147
+ re-entering behind a running one. An expired lease does not block: if the running successor's worker crashed, the
148
+ requeued head is claimed after the lease expires and the successor follows once it completes.
149
+ Claiming first looks at the 256 earliest due deliveries (one cheap statement). Only when those are mostly followers of
150
+ blocked keys and fewer than `limit` were leased does a second statement find the head of every key with
151
+ `DISTINCT ON (handler, key)`, so a deep backlog behind one busy key costs one index-only scan instead of a probe per
152
+ follower.
153
+ - **Fenced.** `complete`, `reschedule`, `bury`, `release` and `extendLease` apply only to
154
+ `status = 'processing' AND lease_token = <token>`; a worker that lost its lease changes nothing. `complete` takes the
155
+ handler's transaction, so a transactional handler's writes and its completion commit together.
156
+ - **App clock.** Every timestamp is the `now` the caller passes, never the database clock.
157
+ - **Driver independent reads.** Timestamps and JSON are read as text and parsed by the store, so custom `pg` type
158
+ parsers (or Drizzle's) cannot change results.
159
+
160
+ ## Operations
161
+
162
+ - **Isolation.** Claiming runs as autocommit statements and relies on `READ COMMITTED` (the PostgreSQL default). Do
163
+ not change `default_transaction_isolation` for the relaybus pool. Your own publishing transaction may use a stricter
164
+ level, but under `REPEATABLE READ` or `SERIALIZABLE` PostgreSQL can abort it with `40001` (serialization failure);
165
+ retry the whole transaction.
166
+ - **Deadlocks.** Publishing a keyed event locks that key's counter row until your transaction ends. Two transactions
167
+ that append keys in opposite order across **several** `append`/`publish` calls (A then B in one, B then A in the
168
+ other) can deadlock; PostgreSQL aborts one with `40P01`. The aborted transaction rolls back completely, nothing is
169
+ lost or half-written, and the caller should retry it. Publishing all keys of a transaction in one `publishAll` call
170
+ avoids the problem because the store locks them in sorted order.
171
+ - **Retention.** `purge` deletes completed (`completed_at < cutoff`) and dead (`updated_at < cutoff`) deliveries in
172
+ batches of `purgeBatchSize`, each batch in its own short transaction with `SKIP LOCKED`, then deletes the events left
173
+ without deliveries, including events created before the earliest cutoff you passed that never had one (a publish
174
+ whose delivery was purged in the same instant, or an event with no subscriber). Configure it through the bus `retention` option. Delete rows through `purge` only; events are
175
+ removed by cascade or by purge, never by hand while publishers are active.
176
+ - **Lock duration.** A publisher holds its key's counter row until commit. Keep transactions that publish keyed events
177
+ short; unkeyed events take no such lock.
178
+ - **Key counters.** `key_sequences` keeps one row per key ever published and rows are never deleted; the table grows
179
+ with the number of distinct keys (a bigint counter each). Pick keys with bounded cardinality, or prune counters of keys
180
+ you know are retired once nothing for them is pending.
181
+ - **Stored JSON.** Payloads and headers are stored as `jsonb`: object keys come back **reordered** (shorter keys first,
182
+ then bytewise) and duplicate keys collapse, values are otherwise identical. A string containing `\u0000` (NUL) or a
183
+ lone UTF-16 surrogate is rejected by PostgreSQL and fails the publish with a database error; strip or encode such
184
+ text before publishing.
185
+ - **Sequence gaps.** Republishing an existing event id consumes a sequence number. Gaps are normal; only order matters.
186
+ - **Pooling.** Claim, complete, extend and purge need a free connection besides your transactions; size the pool for
187
+ `dispatcher.concurrency` plus publishers. With PgBouncer use session or transaction pooling; no session state is used.
188
+ - **Handlers added later.** A delivery added to an old event (new handler, republish with the same id) keeps the
189
+ event's position in its key; if later events of that handler already finished, it runs after them.
190
+
191
+ ## Troubleshooting
192
+
193
+ - `relation "relaybus_events" does not exist`: `setup()` did not run; start the bus with `setupStore: true` or apply
194
+ `createPostgresSchemaSql()`.
195
+ - `permission denied for schema`: the role needs `CREATE` on the schema for `setup()`; with `setupStore: false` it needs
196
+ only DML rights.
197
+ - `invalid input syntax for type json` / `\u0000 cannot be converted to text`: payloads with NUL characters are not valid
198
+ `jsonb`.
199
+ - Foreign key error on append: the only known cause is republishing an event id in the same instant its last delivery
200
+ is purged. The publishing transaction rolls back and can simply be retried.
201
+ - A key looks stuck: a pending or processing head blocks it. Inspect `listDeliveries({ status: 'processing' })` and
202
+ `requeue` dead heads; expired leases are reclaimed automatically.
203
+ - `deadlock detected` (`40P01`) or `could not serialize access` (`40001`) while publishing: see Deadlocks and
204
+ Isolation under Operations; retry the transaction.
205
+
206
+ ## Testing
207
+
208
+ Integration tests use Testcontainers against `postgres:17-alpine` and `postgres:13-alpine` (override with
209
+ `RELAYBUS_POSTGRES_IMAGES`). Every adapter except Prisma runs the full `@relaybus/testing` event store contract plus
210
+ transaction tests against the real database.
@@ -0,0 +1,15 @@
1
+ // src/positional-sql.ts
2
+ function splitPositionalSql(sql) {
3
+ const parts = [];
4
+ let textStart = 0;
5
+ for (const match of sql.matchAll(/\$(\d+)/g)) {
6
+ parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });
7
+ textStart = match.index + match[0].length;
8
+ }
9
+ parts.push(sql.slice(textStart));
10
+ return parts;
11
+ }
12
+
13
+ export { splitPositionalSql };
14
+ //# sourceMappingURL=chunk-UUFSUL2M.js.map
15
+ //# sourceMappingURL=chunk-UUFSUL2M.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/positional-sql.ts"],"names":[],"mappings":";AASO,SAAS,mBAAmB,GAAA,EAAkC;AACnE,EAAA,MAAM,QAA6B,EAAC;AACpC,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,KAAA,MAAW,KAAA,IAAS,GAAA,CAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AAC5C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,MAAM,KAAK,CAAA,EAAG,EAAE,cAAA,EAAgB,OAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,GAAG,CAAA;AACtF,IAAA,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA;AAAA,EACrC;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAS,CAAC,CAAA;AAC/B,EAAA,OAAO,KAAA;AACT","file":"chunk-UUFSUL2M.js","sourcesContent":["/** A piece of SQL text, or a reference to the (zero-based) parameter that stands at this position. */\nexport type PositionalSqlPart = string | { readonly parameterIndex: number };\n\n/**\n * Splits SQL written with PostgreSQL `$1, $2, ...` placeholders into text and\n * parameter references, in order of appearance. A placeholder used twice\n * appears twice. Adapters for libraries with their own placeholder syntax\n * (Knex `?`, Drizzle parameter chunks) rebuild the statement from the parts.\n */\nexport function splitPositionalSql(sql: string): PositionalSqlPart[] {\n const parts: PositionalSqlPart[] = [];\n let textStart = 0;\n for (const match of sql.matchAll(/\\$(\\d+)/g)) {\n parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });\n textStart = match.index + match[0].length;\n }\n parts.push(sql.slice(textStart));\n return parts;\n}\n"]}
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ var drizzleOrm = require('drizzle-orm');
4
+
5
+ // src/drizzle/drizzle-executor.ts
6
+
7
+ // src/positional-sql.ts
8
+ function splitPositionalSql(sql) {
9
+ const parts = [];
10
+ let textStart = 0;
11
+ for (const match of sql.matchAll(/\$(\d+)/g)) {
12
+ parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });
13
+ textStart = match.index + match[0].length;
14
+ }
15
+ parts.push(sql.slice(textStart));
16
+ return parts;
17
+ }
18
+
19
+ // src/drizzle/drizzle-executor.ts
20
+ function createDrizzleExecutor(database) {
21
+ return {
22
+ transaction: (work) => database.transaction(work),
23
+ async query(sql, parameters, transaction) {
24
+ const chunks = splitPositionalSql(sql).map(
25
+ (part) => typeof part === "string" ? drizzleOrm.sql.raw(part) : drizzleOrm.sql.param(parameters[part.parameterIndex])
26
+ );
27
+ const result = await (transaction ?? database).execute(drizzleOrm.sql.join(chunks));
28
+ return result.rows;
29
+ }
30
+ };
31
+ }
32
+
33
+ exports.createDrizzleExecutor = createDrizzleExecutor;
34
+ //# sourceMappingURL=drizzle.cjs.map
35
+ //# sourceMappingURL=drizzle.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/positional-sql.ts","../src/drizzle/drizzle-executor.ts"],"names":["drizzleSql"],"mappings":";;;;;;;AASO,SAAS,mBAAmB,GAAA,EAAkC;AACnE,EAAA,MAAM,QAA6B,EAAC;AACpC,EAAA,IAAI,SAAA,GAAY,CAAA;AAChB,EAAA,KAAA,MAAW,KAAA,IAAS,GAAA,CAAI,QAAA,CAAS,UAAU,CAAA,EAAG;AAC5C,IAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAA,EAAW,MAAM,KAAK,CAAA,EAAG,EAAE,cAAA,EAAgB,OAAO,KAAA,CAAM,CAAC,CAAC,CAAA,GAAI,GAAG,CAAA;AACtF,IAAA,SAAA,GAAY,KAAA,CAAM,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,CAAE,MAAA;AAAA,EACrC;AACA,EAAA,KAAA,CAAM,IAAA,CAAK,GAAA,CAAI,KAAA,CAAM,SAAS,CAAC,CAAA;AAC/B,EAAA,OAAO,KAAA;AACT;;;ACAO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,KAAA,CAAM,GAAA,EAAK,UAAA,EAA0C,WAAA,EAAa;AACtE,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmB,GAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZA,cAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,cAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,MAAA,GAAS,OAAO,WAAA,IAAe,QAAA,EAAU,QAAQA,cAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9E,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACF;AACF","file":"drizzle.cjs","sourcesContent":["/** A piece of SQL text, or a reference to the (zero-based) parameter that stands at this position. */\nexport type PositionalSqlPart = string | { readonly parameterIndex: number };\n\n/**\n * Splits SQL written with PostgreSQL `$1, $2, ...` placeholders into text and\n * parameter references, in order of appearance. A placeholder used twice\n * appears twice. Adapters for libraries with their own placeholder syntax\n * (Knex `?`, Drizzle parameter chunks) rebuild the statement from the parts.\n */\nexport function splitPositionalSql(sql: string): PositionalSqlPart[] {\n const parts: PositionalSqlPart[] = [];\n let textStart = 0;\n for (const match of sql.matchAll(/\\$(\\d+)/g)) {\n parts.push(sql.slice(textStart, match.index), { parameterIndex: Number(match[1]) - 1 });\n textStart = match.index + match[0].length;\n }\n parts.push(sql.slice(textStart));\n return parts;\n}\n","import { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor, PostgresParameter } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return {\n transaction: (work) => database.transaction(work),\n async query(sql, parameters: readonly PostgresParameter[], transaction) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await (transaction ?? database).execute(drizzleSql.join(chunks));\n return result.rows;\n },\n };\n}\n"]}
@@ -0,0 +1,20 @@
1
+ import { SQL } from 'drizzle-orm';
2
+ import { P as PostgresExecutor } from './executor-CSM7ZOhz.cjs';
3
+
4
+ /** What a Drizzle database and a Drizzle transaction have in common. */
5
+ interface DrizzleQueryable {
6
+ execute(query: SQL): PromiseLike<{
7
+ readonly rows: Record<string, unknown>[];
8
+ }>;
9
+ }
10
+ /** The part of a Drizzle `node-postgres` database the executor uses. */
11
+ interface DrizzleLike extends DrizzleQueryable {
12
+ transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;
13
+ }
14
+ /**
15
+ * Executor for Drizzle with the `node-postgres` driver. The transaction handle
16
+ * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.
17
+ */
18
+ declare function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable>;
19
+
20
+ export { type DrizzleLike, type DrizzleQueryable, createDrizzleExecutor };
@@ -0,0 +1,20 @@
1
+ import { SQL } from 'drizzle-orm';
2
+ import { P as PostgresExecutor } from './executor-CSM7ZOhz.js';
3
+
4
+ /** What a Drizzle database and a Drizzle transaction have in common. */
5
+ interface DrizzleQueryable {
6
+ execute(query: SQL): PromiseLike<{
7
+ readonly rows: Record<string, unknown>[];
8
+ }>;
9
+ }
10
+ /** The part of a Drizzle `node-postgres` database the executor uses. */
11
+ interface DrizzleLike extends DrizzleQueryable {
12
+ transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;
13
+ }
14
+ /**
15
+ * Executor for Drizzle with the `node-postgres` driver. The transaction handle
16
+ * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.
17
+ */
18
+ declare function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable>;
19
+
20
+ export { type DrizzleLike, type DrizzleQueryable, createDrizzleExecutor };
@@ -0,0 +1,19 @@
1
+ import { splitPositionalSql } from './chunk-UUFSUL2M.js';
2
+ import { sql } from 'drizzle-orm';
3
+
4
+ function createDrizzleExecutor(database) {
5
+ return {
6
+ transaction: (work) => database.transaction(work),
7
+ async query(sql$1, parameters, transaction) {
8
+ const chunks = splitPositionalSql(sql$1).map(
9
+ (part) => typeof part === "string" ? sql.raw(part) : sql.param(parameters[part.parameterIndex])
10
+ );
11
+ const result = await (transaction ?? database).execute(sql.join(chunks));
12
+ return result.rows;
13
+ }
14
+ };
15
+ }
16
+
17
+ export { createDrizzleExecutor };
18
+ //# sourceMappingURL=drizzle.js.map
19
+ //# sourceMappingURL=drizzle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/drizzle/drizzle-executor.ts"],"names":["sql","drizzleSql"],"mappings":";;;AAkBO,SAAS,sBAAsB,QAAA,EAA2D;AAC/F,EAAA,OAAO;AAAA,IACL,WAAA,EAAa,CAAC,IAAA,KAAS,QAAA,CAAS,YAAY,IAAI,CAAA;AAAA,IAChD,MAAM,KAAA,CAAMA,KAAA,EAAK,UAAA,EAA0C,WAAA,EAAa;AACtE,MAAA,MAAM,MAAA,GAAS,kBAAA,CAAmBA,KAAG,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,IAAA,KAC1C,OAAO,IAAA,KAAS,WACZC,GAAA,CAAW,GAAA,CAAI,IAAI,CAAA,GACnBA,GAAA,CAAW,KAAA,CAAM,UAAA,CAAW,IAAA,CAAK,cAAc,CAAC;AAAA,OACtD;AACA,MAAA,MAAM,MAAA,GAAS,OAAO,WAAA,IAAe,QAAA,EAAU,QAAQA,GAAA,CAAW,IAAA,CAAK,MAAM,CAAC,CAAA;AAC9E,MAAA,OAAO,MAAA,CAAO,IAAA;AAAA,IAChB;AAAA,GACF;AACF","file":"drizzle.js","sourcesContent":["import { sql as drizzleSql, type SQL } from 'drizzle-orm';\nimport type { PostgresExecutor, PostgresParameter } from '../executor.js';\nimport { splitPositionalSql } from '../positional-sql.js';\n\n/** What a Drizzle database and a Drizzle transaction have in common. */\nexport interface DrizzleQueryable {\n execute(query: SQL): PromiseLike<{ readonly rows: Record<string, unknown>[] }>;\n}\n\n/** The part of a Drizzle `node-postgres` database the executor uses. */\nexport interface DrizzleLike extends DrizzleQueryable {\n transaction<T>(work: (transaction: DrizzleQueryable) => Promise<T>): Promise<T>;\n}\n\n/**\n * Executor for Drizzle with the `node-postgres` driver. The transaction handle\n * is a Drizzle transaction: pass `tx` from `db.transaction(async (tx) => ...)`.\n */\nexport function createDrizzleExecutor(database: DrizzleLike): PostgresExecutor<DrizzleQueryable> {\n return {\n transaction: (work) => database.transaction(work),\n async query(sql, parameters: readonly PostgresParameter[], transaction) {\n const chunks = splitPositionalSql(sql).map((part) =>\n typeof part === 'string'\n ? drizzleSql.raw(part)\n : drizzleSql.param(parameters[part.parameterIndex]),\n );\n const result = await (transaction ?? database).execute(drizzleSql.join(chunks));\n return result.rows;\n },\n };\n}\n"]}
@@ -0,0 +1,24 @@
1
+ /** Values the store binds: always text, numbers or null (documents travel as JSON text). */
2
+ type PostgresParameter = string | number | null;
3
+ type PostgresRow = Readonly<Record<string, unknown>>;
4
+ /**
5
+ * The only thing {@link PostgresEventStore} needs from a database library:
6
+ * run a parameterized statement, optionally on a transaction handle, and open
7
+ * a transaction. Implement it to support another client; adapters for `pg`,
8
+ * Kysely, Knex, TypeORM, Drizzle and Prisma ship with the package.
9
+ *
10
+ * Contract:
11
+ * - SQL uses PostgreSQL placeholders (`$1`, `$2`, ...). Adapters translate if
12
+ * their library needs another syntax.
13
+ * - Every statement the store sends returns its rows (or none), so `query`
14
+ * always resolves to an array of rows keyed by column name.
15
+ * - Without a `transaction`, a statement runs on its own connection in
16
+ * autocommit mode, in READ COMMITTED isolation.
17
+ * - `transaction` commits when `work` resolves and rolls back when it rejects.
18
+ */
19
+ interface PostgresExecutor<TTransaction> {
20
+ transaction<T>(work: (transaction: TTransaction) => Promise<T>): Promise<T>;
21
+ query(sql: string, parameters: readonly PostgresParameter[], transaction?: TTransaction): Promise<readonly PostgresRow[]>;
22
+ }
23
+
24
+ export type { PostgresExecutor as P, PostgresParameter as a, PostgresRow as b };
@@ -0,0 +1,24 @@
1
+ /** Values the store binds: always text, numbers or null (documents travel as JSON text). */
2
+ type PostgresParameter = string | number | null;
3
+ type PostgresRow = Readonly<Record<string, unknown>>;
4
+ /**
5
+ * The only thing {@link PostgresEventStore} needs from a database library:
6
+ * run a parameterized statement, optionally on a transaction handle, and open
7
+ * a transaction. Implement it to support another client; adapters for `pg`,
8
+ * Kysely, Knex, TypeORM, Drizzle and Prisma ship with the package.
9
+ *
10
+ * Contract:
11
+ * - SQL uses PostgreSQL placeholders (`$1`, `$2`, ...). Adapters translate if
12
+ * their library needs another syntax.
13
+ * - Every statement the store sends returns its rows (or none), so `query`
14
+ * always resolves to an array of rows keyed by column name.
15
+ * - Without a `transaction`, a statement runs on its own connection in
16
+ * autocommit mode, in READ COMMITTED isolation.
17
+ * - `transaction` commits when `work` resolves and rolls back when it rejects.
18
+ */
19
+ interface PostgresExecutor<TTransaction> {
20
+ transaction<T>(work: (transaction: TTransaction) => Promise<T>): Promise<T>;
21
+ query(sql: string, parameters: readonly PostgresParameter[], transaction?: TTransaction): Promise<readonly PostgresRow[]>;
22
+ }
23
+
24
+ export type { PostgresExecutor as P, PostgresParameter as a, PostgresRow as b };