@elysiumjs/moirai-drizzle 0.0.0-stage → 0.1.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/CHANGELOG.md +27 -0
- package/README.md +45 -2
- package/dist/index.d.ts +356 -0
- package/dist/index.js +1 -0
- package/package.json +67 -5
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# @elysiumjs/moirai-drizzle
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#21](https://github.com/na2axl/elysiumjs/pull/21) [`0035580`](https://github.com/na2axl/elysiumjs/commit/00355809c338a68e39d67335fb820ee098f36f89) Thanks [@na2axl](https://github.com/na2axl)! - Writes are actions and reads are queries. An action extends `Action` and is marked with `@Action.register()`; a query extends `Query` and is marked with `@Query.register()`. `Moirai.execute()` runs an action and `Moirai.ask()` runs a query, and their outcomes are `ActionOutcome` and `QueryOutcome`. Outbox handlers are marked with `@Outbox.handle(kind)`.
|
|
8
|
+
|
|
9
|
+
The options, retry policy and lock are `ActionOptions`, `ActionRetry`, `ActionLock` (`MemoryActionLock`, `MOIRAI_ACTION_LOCK`), and a refused `authorize` is a `CallForbiddenError`. Idempotency records name their `action`, and the `idempotency_keys` table of `moiraiTables()` has an `action` column.
|
|
10
|
+
|
|
11
|
+
- [#17](https://github.com/na2axl/elysiumjs/pull/17) [`03e062d`](https://github.com/na2axl/elysiumjs/commit/03e062d9f5d8ec761c63f05574ce96a1084fe3ea) Thanks [@na2axl](https://github.com/na2axl)! - Commands, outbox, authentication and multi-step flows, on Bun, Node.js and Cloudflare Workers.
|
|
12
|
+
|
|
13
|
+
Added:
|
|
14
|
+
|
|
15
|
+
- `@elysiumjs/moirai`: commands and queries declared with `@Moirai.command()` and `@Moirai.query()`, run with `Commands.execute` and `Queries.ask`. Inputs are validated (400 problem), `authorize` refuses with a 403 problem, commands run in a unit of work and retry transient failures, idempotent commands replay their response for a repeated key (428 without a key, 422 for a key reused with another input), and `serializeBy` runs calls with the same key one after the other. `Outbox.enqueue` writes effects that relay once the unit of work commits, to `@Moirai.effect()` handlers, with a backoff and dead rows; `Outbox.sweep` relays what is due; `Inbox.once` processes a message once per consumer. `@elysiumjs/moirai/heracles` adds `job()`, an effect that dispatches a Heracles job. Memory stores for tests.
|
|
16
|
+
- `@elysiumjs/moirai-drizzle`: Postgres stores for moirai through Drizzle ORM: `moiraiTables(schema?)`, `DrizzleIdempotencyStore`, `DrizzleOutboxStore` (claims with `FOR UPDATE SKIP LOCKED`), `DrizzleInboxStore` and `PgAdvisoryLock`.
|
|
17
|
+
- `@elysiumjs/themis`: password hashing (argon2id on Bun, PBKDF2 everywhere, scrypt on `@elysiumjs/themis/scrypt`), JWT through jose, API keys, signed URLs, sessions (memory, Redis, Workers KV, or an encrypted cookie), `@Auth()` with session, JWT and API key guards (401 problem), picked by name through `AuthMiddleware.guards()` of core middleware, CSRF protection, TOTP and backup codes, OAuth 2.0 and OpenID Connect clients with PKCE through oauth4webapi, and passkeys through SimpleWebAuthn.
|
|
18
|
+
- `@elysiumjs/ariadne`: multi-step flows as controllers: `@Flow.controller()` adds the start and state routes, each `@Flow.step()` method is a route that takes the `@Http.*()` parameter decorators, and `@Flow.state()` / `@Flow.context()` inject the flow. Replay of a repeated step id, refused steps that keep their state, back steps, guest and user owners, a flow that ends when `next` gives no step, and a memory store set by `AriadneModule.with({ store })`.
|
|
19
|
+
- `@elysiumjs/ariadne-redis`: `RedisFlowStore`, whose claims and commits are atomic Lua scripts run with `EVAL`, which Upstash runs too.
|
|
20
|
+
- `@elysiumjs/ariadne-workers`: `FlowStoreObject`, a Durable Object flow store, and `durableFlowStore(binding)`.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- Updated dependencies [[`6bc48fe`](https://github.com/na2axl/elysiumjs/commit/6bc48febd51f7090c582e73f349eaeff7261241c), [`e9c3d98`](https://github.com/na2axl/elysiumjs/commit/e9c3d98dd2714b69100215f7c8a80046da463148), [`557a3ca`](https://github.com/na2axl/elysiumjs/commit/557a3cab7af8053a8bab9944e492e5022d91aa82), [`ccc3f5b`](https://github.com/na2axl/elysiumjs/commit/ccc3f5bb904d1bf52d8c72ebf7ea8d10f617a8c4), [`6789879`](https://github.com/na2axl/elysiumjs/commit/678987976ecfd46b534b65276e0de3f448977562), [`87299fd`](https://github.com/na2axl/elysiumjs/commit/87299fd9da82d1a408ced1891c0c3d42991444e5), [`0035580`](https://github.com/na2axl/elysiumjs/commit/00355809c338a68e39d67335fb820ee098f36f89), [`a680a17`](https://github.com/na2axl/elysiumjs/commit/a680a177e2d82a6e85d04a9491ffc8fc05fb0010), [`ccc3f5b`](https://github.com/na2axl/elysiumjs/commit/ccc3f5bb904d1bf52d8c72ebf7ea8d10f617a8c4), [`bee66bd`](https://github.com/na2axl/elysiumjs/commit/bee66bdc874b8f23fea356f7d3adf7555a48ca64), [`0057b67`](https://github.com/na2axl/elysiumjs/commit/0057b67df0e3abecab8a9accff6b55a6a64c21df), [`1738567`](https://github.com/na2axl/elysiumjs/commit/17385674c14991f0ddb27eb6ee967afbcebcc307), [`632d77b`](https://github.com/na2axl/elysiumjs/commit/632d77b561fc43ade605fb69b06ac1b9b9a2ffc4), [`1ea417e`](https://github.com/na2axl/elysiumjs/commit/1ea417e3583250bc2f975d795f398b7ff736115d), [`0956186`](https://github.com/na2axl/elysiumjs/commit/0956186db0ca5a83c0a6636aeb2a4c81a0033de4), [`fbcc7da`](https://github.com/na2axl/elysiumjs/commit/fbcc7dad127db29da42e7a4a5c2124966731cd88), [`ac96e55`](https://github.com/na2axl/elysiumjs/commit/ac96e55874b0ffe350b287c4278cd85afecb7576), [`03e062d`](https://github.com/na2axl/elysiumjs/commit/03e062d9f5d8ec761c63f05574ce96a1084fe3ea), [`98a16aa`](https://github.com/na2axl/elysiumjs/commit/98a16aae889d46f7a9fb5573188f4cbe3629dd3b)]:
|
|
25
|
+
- @elysiumjs/core@0.11.0
|
|
26
|
+
- @elysiumjs/moirai@0.1.0
|
|
27
|
+
- @elysiumjs/mnemosyne-drizzle@0.4.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @elysiumjs/moirai-drizzle
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Postgres stores for [`@elysiumjs/moirai`](../moirai), through Drizzle ORM and the connections of [`@elysiumjs/mnemosyne-drizzle`](../mnemosyne-drizzle). They write in the transaction of the action, so an idempotency record and the outbox rows commit with the data.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @elysiumjs/moirai @elysiumjs/moirai-drizzle @elysiumjs/mnemosyne-drizzle
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Tables
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { moiraiTables } from '@elysiumjs/moirai-drizzle';
|
|
15
|
+
|
|
16
|
+
// In the Drizzle schema, so that migration:generate creates them.
|
|
17
|
+
export const billing = moiraiTables('billing');
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`moiraiTables(schema?)` builds the `idempotency_keys`, `outbox` and `inbox` tables, in a Postgres schema when one is given (one set of tables per domain schema, for example).
|
|
21
|
+
|
|
22
|
+
## Stores
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { MoiraiModule } from '@elysiumjs/moirai';
|
|
26
|
+
import {
|
|
27
|
+
DrizzleIdempotencyStore,
|
|
28
|
+
DrizzleInboxStore,
|
|
29
|
+
DrizzleOutboxStore,
|
|
30
|
+
PgAdvisoryLock
|
|
31
|
+
} from '@elysiumjs/moirai-drizzle';
|
|
32
|
+
|
|
33
|
+
MoiraiModule.with({
|
|
34
|
+
idempotency: () => new DrizzleIdempotencyStore({ table: billing.idempotencyKeys }),
|
|
35
|
+
outbox: () => new DrizzleOutboxStore({ table: billing.outbox }),
|
|
36
|
+
inbox: () => new DrizzleInboxStore({ table: billing.inbox }),
|
|
37
|
+
lock: () => new PgAdvisoryLock()
|
|
38
|
+
});
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `DrizzleIdempotencyStore` keeps the response of an idempotent action per key.
|
|
42
|
+
- `DrizzleOutboxStore` claims due rows with `FOR UPDATE SKIP LOCKED`, so that several relays never take the same row.
|
|
43
|
+
- `DrizzleInboxStore` records the messages a consumer has processed.
|
|
44
|
+
- `PgAdvisoryLock` serializes the actions that share a `serializeBy` key with a transaction advisory lock.
|
|
45
|
+
|
|
46
|
+
Each store takes `connection`, the name of a mnemosyne-drizzle connection, which defaults to `default`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import type { ActionLock, IdempotencyRecord, IdempotencyStore, InboxStore, OutboxClaim, OutboxRow, OutboxStore } from '@elysiumjs/moirai';
|
|
2
|
+
/**
|
|
3
|
+
* Builds the tables of the moirai stores: `idempotency_keys`, `outbox` and `inbox`, in a schema
|
|
4
|
+
* when one is given (one set of tables per domain schema, for example). Export them from the
|
|
5
|
+
* Drizzle schema so that `migration:generate` creates them.
|
|
6
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
7
|
+
* @param schema The Postgres schema of the tables. Defaults to the search path.
|
|
8
|
+
*/
|
|
9
|
+
export declare const moiraiTables: (schema?: string) => {
|
|
10
|
+
idempotencyKeys: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
11
|
+
name: "idempotency_keys";
|
|
12
|
+
schema: undefined;
|
|
13
|
+
columns: {
|
|
14
|
+
key: import("drizzle-orm/pg-core").PgBuildColumn<"idempotency_keys", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
15
|
+
name: string;
|
|
16
|
+
tableName: "idempotency_keys";
|
|
17
|
+
dataType: "string";
|
|
18
|
+
data: string;
|
|
19
|
+
driverParam: string;
|
|
20
|
+
notNull: true;
|
|
21
|
+
hasDefault: false;
|
|
22
|
+
isPrimaryKey: false;
|
|
23
|
+
isAutoincrement: false;
|
|
24
|
+
hasRuntimeDefault: false;
|
|
25
|
+
enumValues: undefined;
|
|
26
|
+
identity: undefined;
|
|
27
|
+
generated: undefined;
|
|
28
|
+
}>;
|
|
29
|
+
action: import("drizzle-orm/pg-core").PgBuildColumn<"idempotency_keys", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
30
|
+
name: string;
|
|
31
|
+
tableName: "idempotency_keys";
|
|
32
|
+
dataType: "string";
|
|
33
|
+
data: string;
|
|
34
|
+
driverParam: string;
|
|
35
|
+
notNull: true;
|
|
36
|
+
hasDefault: false;
|
|
37
|
+
isPrimaryKey: false;
|
|
38
|
+
isAutoincrement: false;
|
|
39
|
+
hasRuntimeDefault: false;
|
|
40
|
+
enumValues: undefined;
|
|
41
|
+
identity: undefined;
|
|
42
|
+
generated: undefined;
|
|
43
|
+
}>;
|
|
44
|
+
requestHash: import("drizzle-orm/pg-core").PgBuildColumn<"idempotency_keys", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
45
|
+
name: string;
|
|
46
|
+
tableName: "idempotency_keys";
|
|
47
|
+
dataType: "string";
|
|
48
|
+
data: string;
|
|
49
|
+
driverParam: string;
|
|
50
|
+
notNull: true;
|
|
51
|
+
hasDefault: false;
|
|
52
|
+
isPrimaryKey: false;
|
|
53
|
+
isAutoincrement: false;
|
|
54
|
+
hasRuntimeDefault: false;
|
|
55
|
+
enumValues: undefined;
|
|
56
|
+
identity: undefined;
|
|
57
|
+
generated: undefined;
|
|
58
|
+
}>;
|
|
59
|
+
response: import("drizzle-orm/pg-core").PgBuildColumn<"idempotency_keys", import("drizzle-orm/pg-core").PgJsonbBuilder, {
|
|
60
|
+
name: string;
|
|
61
|
+
tableName: "idempotency_keys";
|
|
62
|
+
dataType: "object json";
|
|
63
|
+
data: unknown;
|
|
64
|
+
driverParam: unknown;
|
|
65
|
+
notNull: false;
|
|
66
|
+
hasDefault: false;
|
|
67
|
+
isPrimaryKey: false;
|
|
68
|
+
isAutoincrement: false;
|
|
69
|
+
hasRuntimeDefault: false;
|
|
70
|
+
enumValues: undefined;
|
|
71
|
+
identity: undefined;
|
|
72
|
+
generated: undefined;
|
|
73
|
+
}>;
|
|
74
|
+
createdAt: import("drizzle-orm/pg-core").PgBuildColumn<"idempotency_keys", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
|
|
75
|
+
name: string;
|
|
76
|
+
tableName: "idempotency_keys";
|
|
77
|
+
dataType: "object date";
|
|
78
|
+
data: Date;
|
|
79
|
+
driverParam: string;
|
|
80
|
+
notNull: true;
|
|
81
|
+
hasDefault: true;
|
|
82
|
+
isPrimaryKey: false;
|
|
83
|
+
isAutoincrement: false;
|
|
84
|
+
hasRuntimeDefault: false;
|
|
85
|
+
enumValues: undefined;
|
|
86
|
+
identity: undefined;
|
|
87
|
+
generated: undefined;
|
|
88
|
+
}>;
|
|
89
|
+
};
|
|
90
|
+
dialect: 'pg';
|
|
91
|
+
}>;
|
|
92
|
+
outbox: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
93
|
+
name: "outbox";
|
|
94
|
+
schema: undefined;
|
|
95
|
+
columns: {
|
|
96
|
+
id: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetIsPrimaryKey<import("drizzle-orm/pg-core").PgUUIDBuilder>, {
|
|
97
|
+
name: string;
|
|
98
|
+
tableName: "outbox";
|
|
99
|
+
dataType: "string uuid";
|
|
100
|
+
data: string;
|
|
101
|
+
driverParam: string;
|
|
102
|
+
notNull: true;
|
|
103
|
+
hasDefault: false;
|
|
104
|
+
isPrimaryKey: false;
|
|
105
|
+
isAutoincrement: false;
|
|
106
|
+
hasRuntimeDefault: false;
|
|
107
|
+
enumValues: undefined;
|
|
108
|
+
identity: undefined;
|
|
109
|
+
generated: undefined;
|
|
110
|
+
}>;
|
|
111
|
+
kind: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
112
|
+
name: string;
|
|
113
|
+
tableName: "outbox";
|
|
114
|
+
dataType: "string";
|
|
115
|
+
data: string;
|
|
116
|
+
driverParam: string;
|
|
117
|
+
notNull: true;
|
|
118
|
+
hasDefault: false;
|
|
119
|
+
isPrimaryKey: false;
|
|
120
|
+
isAutoincrement: false;
|
|
121
|
+
hasRuntimeDefault: false;
|
|
122
|
+
enumValues: undefined;
|
|
123
|
+
identity: undefined;
|
|
124
|
+
generated: undefined;
|
|
125
|
+
}>;
|
|
126
|
+
payload: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgJsonbBuilder>, {
|
|
127
|
+
name: string;
|
|
128
|
+
tableName: "outbox";
|
|
129
|
+
dataType: "object json";
|
|
130
|
+
data: unknown;
|
|
131
|
+
driverParam: unknown;
|
|
132
|
+
notNull: true;
|
|
133
|
+
hasDefault: false;
|
|
134
|
+
isPrimaryKey: false;
|
|
135
|
+
isAutoincrement: false;
|
|
136
|
+
hasRuntimeDefault: false;
|
|
137
|
+
enumValues: undefined;
|
|
138
|
+
identity: undefined;
|
|
139
|
+
generated: undefined;
|
|
140
|
+
}>;
|
|
141
|
+
createdAt: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
|
|
142
|
+
name: string;
|
|
143
|
+
tableName: "outbox";
|
|
144
|
+
dataType: "object date";
|
|
145
|
+
data: Date;
|
|
146
|
+
driverParam: string;
|
|
147
|
+
notNull: true;
|
|
148
|
+
hasDefault: true;
|
|
149
|
+
isPrimaryKey: false;
|
|
150
|
+
isAutoincrement: false;
|
|
151
|
+
hasRuntimeDefault: false;
|
|
152
|
+
enumValues: undefined;
|
|
153
|
+
identity: undefined;
|
|
154
|
+
generated: undefined;
|
|
155
|
+
}>;
|
|
156
|
+
availableAt: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
|
|
157
|
+
name: string;
|
|
158
|
+
tableName: "outbox";
|
|
159
|
+
dataType: "object date";
|
|
160
|
+
data: Date;
|
|
161
|
+
driverParam: string;
|
|
162
|
+
notNull: true;
|
|
163
|
+
hasDefault: true;
|
|
164
|
+
isPrimaryKey: false;
|
|
165
|
+
isAutoincrement: false;
|
|
166
|
+
hasRuntimeDefault: false;
|
|
167
|
+
enumValues: undefined;
|
|
168
|
+
identity: undefined;
|
|
169
|
+
generated: undefined;
|
|
170
|
+
}>;
|
|
171
|
+
attempts: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgIntegerBuilder>>, {
|
|
172
|
+
name: string;
|
|
173
|
+
tableName: "outbox";
|
|
174
|
+
dataType: "number int32";
|
|
175
|
+
data: number;
|
|
176
|
+
driverParam: string | number;
|
|
177
|
+
notNull: true;
|
|
178
|
+
hasDefault: true;
|
|
179
|
+
isPrimaryKey: false;
|
|
180
|
+
isAutoincrement: false;
|
|
181
|
+
hasRuntimeDefault: false;
|
|
182
|
+
enumValues: undefined;
|
|
183
|
+
identity: undefined;
|
|
184
|
+
generated: undefined;
|
|
185
|
+
}>;
|
|
186
|
+
lastError: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>, {
|
|
187
|
+
name: string;
|
|
188
|
+
tableName: "outbox";
|
|
189
|
+
dataType: "string";
|
|
190
|
+
data: string;
|
|
191
|
+
driverParam: string;
|
|
192
|
+
notNull: false;
|
|
193
|
+
hasDefault: false;
|
|
194
|
+
isPrimaryKey: false;
|
|
195
|
+
isAutoincrement: false;
|
|
196
|
+
hasRuntimeDefault: false;
|
|
197
|
+
enumValues: undefined;
|
|
198
|
+
identity: undefined;
|
|
199
|
+
generated: undefined;
|
|
200
|
+
}>;
|
|
201
|
+
dispatchedAt: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").PgTimestampBuilder, {
|
|
202
|
+
name: string;
|
|
203
|
+
tableName: "outbox";
|
|
204
|
+
dataType: "object date";
|
|
205
|
+
data: Date;
|
|
206
|
+
driverParam: string;
|
|
207
|
+
notNull: false;
|
|
208
|
+
hasDefault: false;
|
|
209
|
+
isPrimaryKey: false;
|
|
210
|
+
isAutoincrement: false;
|
|
211
|
+
hasRuntimeDefault: false;
|
|
212
|
+
enumValues: undefined;
|
|
213
|
+
identity: undefined;
|
|
214
|
+
generated: undefined;
|
|
215
|
+
}>;
|
|
216
|
+
deadAt: import("drizzle-orm/pg-core").PgBuildColumn<"outbox", import("drizzle-orm/pg-core").PgTimestampBuilder, {
|
|
217
|
+
name: string;
|
|
218
|
+
tableName: "outbox";
|
|
219
|
+
dataType: "object date";
|
|
220
|
+
data: Date;
|
|
221
|
+
driverParam: string;
|
|
222
|
+
notNull: false;
|
|
223
|
+
hasDefault: false;
|
|
224
|
+
isPrimaryKey: false;
|
|
225
|
+
isAutoincrement: false;
|
|
226
|
+
hasRuntimeDefault: false;
|
|
227
|
+
enumValues: undefined;
|
|
228
|
+
identity: undefined;
|
|
229
|
+
generated: undefined;
|
|
230
|
+
}>;
|
|
231
|
+
};
|
|
232
|
+
dialect: 'pg';
|
|
233
|
+
}>;
|
|
234
|
+
inbox: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
235
|
+
name: "inbox";
|
|
236
|
+
schema: undefined;
|
|
237
|
+
columns: {
|
|
238
|
+
messageId: import("drizzle-orm/pg-core").PgBuildColumn<"inbox", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
239
|
+
name: string;
|
|
240
|
+
tableName: "inbox";
|
|
241
|
+
dataType: "string";
|
|
242
|
+
data: string;
|
|
243
|
+
driverParam: string;
|
|
244
|
+
notNull: true;
|
|
245
|
+
hasDefault: false;
|
|
246
|
+
isPrimaryKey: false;
|
|
247
|
+
isAutoincrement: false;
|
|
248
|
+
hasRuntimeDefault: false;
|
|
249
|
+
enumValues: undefined;
|
|
250
|
+
identity: undefined;
|
|
251
|
+
generated: undefined;
|
|
252
|
+
}>;
|
|
253
|
+
consumer: import("drizzle-orm/pg-core").PgBuildColumn<"inbox", import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTextBuilder<[string, ...string[]]>>, {
|
|
254
|
+
name: string;
|
|
255
|
+
tableName: "inbox";
|
|
256
|
+
dataType: "string";
|
|
257
|
+
data: string;
|
|
258
|
+
driverParam: string;
|
|
259
|
+
notNull: true;
|
|
260
|
+
hasDefault: false;
|
|
261
|
+
isPrimaryKey: false;
|
|
262
|
+
isAutoincrement: false;
|
|
263
|
+
hasRuntimeDefault: false;
|
|
264
|
+
enumValues: undefined;
|
|
265
|
+
identity: undefined;
|
|
266
|
+
generated: undefined;
|
|
267
|
+
}>;
|
|
268
|
+
recordedAt: import("drizzle-orm/pg-core").PgBuildColumn<"inbox", import("drizzle-orm/pg-core").SetHasDefault<import("drizzle-orm/pg-core").SetNotNull<import("drizzle-orm/pg-core").PgTimestampBuilder>>, {
|
|
269
|
+
name: string;
|
|
270
|
+
tableName: "inbox";
|
|
271
|
+
dataType: "object date";
|
|
272
|
+
data: Date;
|
|
273
|
+
driverParam: string;
|
|
274
|
+
notNull: true;
|
|
275
|
+
hasDefault: true;
|
|
276
|
+
isPrimaryKey: false;
|
|
277
|
+
isAutoincrement: false;
|
|
278
|
+
hasRuntimeDefault: false;
|
|
279
|
+
enumValues: undefined;
|
|
280
|
+
identity: undefined;
|
|
281
|
+
generated: undefined;
|
|
282
|
+
}>;
|
|
283
|
+
};
|
|
284
|
+
dialect: 'pg';
|
|
285
|
+
}>;
|
|
286
|
+
};
|
|
287
|
+
export type MoiraiTables = ReturnType<typeof moiraiTables>;
|
|
288
|
+
/**
|
|
289
|
+
* Where a Drizzle store works: its table, and the connection whose open transaction it joins.
|
|
290
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
291
|
+
*/
|
|
292
|
+
export type DrizzleStoreOptions<T> = {
|
|
293
|
+
table: T;
|
|
294
|
+
connection?: string;
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* Keeps idempotency records in a Postgres table, in the action's transaction.
|
|
298
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
299
|
+
*/
|
|
300
|
+
export declare class DrizzleIdempotencyStore implements IdempotencyStore {
|
|
301
|
+
private readonly options;
|
|
302
|
+
constructor(options: DrizzleStoreOptions<MoiraiTables['idempotencyKeys']>);
|
|
303
|
+
get(key: string): Promise<IdempotencyRecord | undefined>;
|
|
304
|
+
put(record: IdempotencyRecord): Promise<void>;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Keeps the outbox in a Postgres table. Claims lock the rows (`FOR UPDATE SKIP LOCKED`) until the
|
|
308
|
+
* relay's transaction ends, so that concurrent relays share the work.
|
|
309
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
310
|
+
*/
|
|
311
|
+
export declare class DrizzleOutboxStore implements OutboxStore {
|
|
312
|
+
#private;
|
|
313
|
+
private readonly options;
|
|
314
|
+
constructor(options: DrizzleStoreOptions<MoiraiTables['outbox']>);
|
|
315
|
+
private get db();
|
|
316
|
+
insert(rows: readonly OutboxRow[]): Promise<void>;
|
|
317
|
+
claimDue(claim: OutboxClaim): Promise<readonly OutboxRow[]>;
|
|
318
|
+
markDispatched(id: string, dispatchedAt: Date): Promise<void>;
|
|
319
|
+
markFailed(id: string, update: {
|
|
320
|
+
attempts: number;
|
|
321
|
+
lastError: string;
|
|
322
|
+
availableAt: Date;
|
|
323
|
+
}): Promise<void>;
|
|
324
|
+
markDead(id: string, update: {
|
|
325
|
+
attempts: number;
|
|
326
|
+
lastError: string;
|
|
327
|
+
deadAt: Date;
|
|
328
|
+
}): Promise<void>;
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Keeps the inbox in a Postgres table: one row per message and consumer.
|
|
332
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
333
|
+
*/
|
|
334
|
+
export declare class DrizzleInboxStore implements InboxStore {
|
|
335
|
+
private readonly options;
|
|
336
|
+
constructor(options: DrizzleStoreOptions<MoiraiTables['inbox']>);
|
|
337
|
+
record(messageId: string, consumer: string): Promise<boolean>;
|
|
338
|
+
/**
|
|
339
|
+
* Deletes the records older than a date, such as from a daily cron.
|
|
340
|
+
* @param cutoff The date before which records go.
|
|
341
|
+
* @returns The number of records deleted.
|
|
342
|
+
*/
|
|
343
|
+
pruneBefore(cutoff: Date): Promise<number>;
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Serializes actions with a Postgres advisory lock taken in the action's transaction, and
|
|
347
|
+
* released when it ends.
|
|
348
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
349
|
+
*/
|
|
350
|
+
export declare class PgAdvisoryLock implements ActionLock {
|
|
351
|
+
private readonly options;
|
|
352
|
+
constructor(options?: {
|
|
353
|
+
connection?: string;
|
|
354
|
+
});
|
|
355
|
+
lock(key: string): Promise<void>;
|
|
356
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{Database}from"@elysiumjs/mnemosyne-drizzle";import{IdempotencyConflictError}from"@elysiumjs/moirai";import{and,asc,eq,inArray,isNull,lt,lte,sql}from"drizzle-orm";import{index,integer,jsonb,pgSchema,pgTable,primaryKey,text,timestamp,uuid}from"drizzle-orm/pg-core";var at=(name)=>timestamp(name,{withTimezone:!0,mode:"date"}),moiraiTables=(schema)=>{let table=schema?pgSchema(schema).table:pgTable;return{idempotencyKeys:table("idempotency_keys",{key:text("key").primaryKey(),action:text("action").notNull(),requestHash:text("request_hash").notNull(),response:jsonb("response"),createdAt:at("created_at").notNull().defaultNow()}),outbox:table("outbox",{id:uuid("id").primaryKey(),kind:text("kind").notNull(),payload:jsonb("payload").notNull(),createdAt:at("created_at").notNull().defaultNow(),availableAt:at("available_at").notNull().defaultNow(),attempts:integer("attempts").notNull().default(0),lastError:text("last_error"),dispatchedAt:at("dispatched_at"),deadAt:at("dead_at")},(t)=>[index("outbox_due_idx").on(t.availableAt).where(sql`${t.dispatchedAt} is null and ${t.deadAt} is null`)]),inbox:table("inbox",{messageId:text("message_id").notNull(),consumer:text("consumer").notNull(),recordedAt:at("recorded_at").notNull().defaultNow()},(t)=>[primaryKey({columns:[t.messageId,t.consumer]})])}},isUniqueViolation=(error)=>{for(let e=error;e;e=e.cause){if(e.code==="23505"||e.errno==="23505")return!0;if(e.cause===e)break}return!1};class DrizzleIdempotencyStore{options;constructor(options){this.options=options}async get(key){let t=this.options.table,[row]=await Database.getConnection(this.options.connection).select().from(t).where(eq(t.key,key)).limit(1);return row?{...row,response:row.response??null}:void 0}async put(record){try{await Database.getConnection(this.options.connection).insert(this.options.table).values(record)}catch(error){if(isUniqueViolation(error))throw new IdempotencyConflictError(record.key);throw error}}}class DrizzleOutboxStore{options;constructor(options){this.options=options}get db(){return Database.getConnection(this.options.connection)}async insert(rows){if(rows.length===0)return;await this.db.insert(this.options.table).values(rows.map((row)=>({...row})))}async claimDue(claim){let{now,leaseUntil,limit,kinds,ids}=claim;if(kinds?.length===0||ids?.length===0)return[];let t=this.options.table,due=this.db.select({id:t.id}).from(t).where(and(isNull(t.dispatchedAt),isNull(t.deadAt),lte(t.availableAt,now),kinds?inArray(t.kind,[...kinds]):void 0,ids?inArray(t.id,[...ids]):void 0)).orderBy(asc(t.availableAt)).limit(limit).for("update",{skipLocked:!0});return(await this.db.update(t).set({availableAt:leaseUntil}).where(inArray(t.id,due)).returning()).sort((a,b)=>a.id<b.id?-1:a.id>b.id?1:0)}async markDispatched(id,dispatchedAt){await this.#update(id,{dispatchedAt})}async markFailed(id,update){await this.#update(id,update)}async markDead(id,update){await this.#update(id,update)}async#update(id,values){let t=this.options.table;await this.db.update(t).set(values).where(eq(t.id,id))}}class DrizzleInboxStore{options;constructor(options){this.options=options}async record(messageId,consumer){let t=this.options.table;return(await Database.getConnection(this.options.connection).insert(t).values({messageId,consumer}).onConflictDoNothing().returning({messageId:t.messageId})).length>0}async pruneBefore(cutoff){let t=this.options.table;return(await Database.getConnection(this.options.connection).delete(t).where(lt(t.recordedAt,cutoff)).returning({messageId:t.messageId})).length}}class PgAdvisoryLock{options;constructor(options={}){this.options=options}async lock(key){await Database.getConnection(this.options.connection).execute(sql`select pg_advisory_xact_lock(hashtextextended(${key}, 0))`)}}export{DrizzleIdempotencyStore,DrizzleInboxStore,DrizzleOutboxStore,PgAdvisoryLock,moiraiTables};
|
package/package.json
CHANGED
|
@@ -1,6 +1,68 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@elysiumjs/moirai-drizzle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"description": "Postgres stores for @elysiumjs/moirai through Drizzle ORM: idempotency keys, outbox, inbox and advisory locks.",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "Axel Nana",
|
|
9
|
+
"email": "axel.nana@workbud.com"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/na2axl/elysiumjs.git",
|
|
14
|
+
"directory": "@elysiumjs/moirai-drizzle"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/moirai-drizzle#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/na2axl/elysiumjs/issues"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"format": "prettier --write \"./src/**/*.ts\"",
|
|
22
|
+
"clean": "rimraf dist",
|
|
23
|
+
"build:js": "bun build ./src/index.ts --outdir ./dist --target node --packages external --root ./src --minify-syntax --minify-whitespace",
|
|
24
|
+
"build:ts": "tsc",
|
|
25
|
+
"build": "bun run clean && bun run build:js && bun run build:ts",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"prepublishOnly": "bun run build",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"test:coverage": "bun test --coverage"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"elysium",
|
|
33
|
+
"framework",
|
|
34
|
+
"typescript",
|
|
35
|
+
"commands",
|
|
36
|
+
"outbox",
|
|
37
|
+
"drizzle",
|
|
38
|
+
"postgresql"
|
|
39
|
+
],
|
|
40
|
+
"type": "module",
|
|
41
|
+
"module": "dist/index.js",
|
|
42
|
+
"types": "dist/index.d.ts",
|
|
43
|
+
"exports": {
|
|
44
|
+
".": {
|
|
45
|
+
"types": "./dist/index.d.ts",
|
|
46
|
+
"import": "./dist/index.js"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"files": [
|
|
50
|
+
"dist",
|
|
51
|
+
"CHANGELOG.md"
|
|
52
|
+
],
|
|
53
|
+
"sideEffects": false,
|
|
54
|
+
"dependencies": {
|
|
55
|
+
"drizzle-orm": "^1.0.0-rc.5"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"@elysiumjs/core": "0.11.0",
|
|
59
|
+
"@elysiumjs/mnemosyne-drizzle": "0.4.0",
|
|
60
|
+
"@elysiumjs/moirai": "0.1.0",
|
|
61
|
+
"@elysiumjs/runtime-bun": "0.1.0"
|
|
62
|
+
},
|
|
63
|
+
"peerDependencies": {
|
|
64
|
+
"@elysiumjs/core": "^0.11.0",
|
|
65
|
+
"@elysiumjs/mnemosyne-drizzle": "^0.4.0",
|
|
66
|
+
"@elysiumjs/moirai": "^0.1.0"
|
|
67
|
+
}
|
|
68
|
+
}
|