@saga-bus/store-dynamodb 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/LICENSE +21 -0
- package/README.md +102 -0
- package/dist/index.cjs +333 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +121 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +313 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dean Foran
|
|
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,102 @@
|
|
|
1
|
+
# @saga-bus/store-dynamodb
|
|
2
|
+
|
|
3
|
+
AWS DynamoDB saga store for saga-bus.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @saga-bus/store-dynamodb @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
15
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
16
|
+
import { DynamoDBSagaStore, createTable } from "@saga-bus/store-dynamodb";
|
|
17
|
+
import { createBus } from "@saga-bus/core";
|
|
18
|
+
|
|
19
|
+
const dynamoClient = new DynamoDBClient({ region: "us-east-1" });
|
|
20
|
+
const docClient = DynamoDBDocumentClient.from(dynamoClient);
|
|
21
|
+
|
|
22
|
+
// Create table (run once, or use CloudFormation/CDK)
|
|
23
|
+
await createTable(dynamoClient, { tableName: "saga-instances" });
|
|
24
|
+
|
|
25
|
+
const store = new DynamoDBSagaStore({
|
|
26
|
+
client: docClient,
|
|
27
|
+
tableName: "saga-instances",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const bus = createBus({
|
|
31
|
+
sagas: [{ definition: mySaga, store }],
|
|
32
|
+
transport,
|
|
33
|
+
});
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Table Schema
|
|
37
|
+
|
|
38
|
+
The store uses a single-table design:
|
|
39
|
+
|
|
40
|
+
| Key | Pattern | Description |
|
|
41
|
+
|-----|---------|-------------|
|
|
42
|
+
| `PK` | `SAGA#<sagaName>` | Partition key |
|
|
43
|
+
| `SK` | `ID#<sagaId>` | Sort key |
|
|
44
|
+
| `GSI1PK` | `SAGA#<sagaName>` | Correlation index PK |
|
|
45
|
+
| `GSI1SK` | `CORR#<correlationId>` | Correlation index SK |
|
|
46
|
+
| `GSI2PK` | `COMPLETED#<date>` | Cleanup index PK |
|
|
47
|
+
| `GSI2SK` | `<sagaName>#<sagaId>` | Cleanup index SK |
|
|
48
|
+
|
|
49
|
+
## Features
|
|
50
|
+
|
|
51
|
+
- Single-table design for cost efficiency
|
|
52
|
+
- Optimistic concurrency via conditional writes
|
|
53
|
+
- GSI for correlation ID lookups
|
|
54
|
+
- GSI for cleanup queries by completion date
|
|
55
|
+
- Automatic GSI index management
|
|
56
|
+
|
|
57
|
+
## Configuration
|
|
58
|
+
|
|
59
|
+
| Option | Type | Default | Description |
|
|
60
|
+
|--------|------|---------|-------------|
|
|
61
|
+
| `client` | `DynamoDBDocumentClient` | required | DynamoDB Document Client |
|
|
62
|
+
| `tableName` | `string` | required | Table name |
|
|
63
|
+
| `correlationIndexName` | `string` | `"GSI1"` | Correlation GSI name |
|
|
64
|
+
| `cleanupIndexName` | `string` | `"GSI2"` | Cleanup GSI name |
|
|
65
|
+
|
|
66
|
+
## Sharing Across Sagas
|
|
67
|
+
|
|
68
|
+
A single store instance can be shared across multiple sagas:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
const store = new DynamoDBSagaStore({ client, tableName: "saga-instances" });
|
|
72
|
+
|
|
73
|
+
const bus = createBus({
|
|
74
|
+
transport,
|
|
75
|
+
store, // shared by all sagas
|
|
76
|
+
sagas: [
|
|
77
|
+
{ definition: orderSaga },
|
|
78
|
+
{ definition: paymentSaga },
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Data is isolated by `sagaName` in the partition key, so different saga types won't conflict.
|
|
84
|
+
|
|
85
|
+
## Table Management
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
import { createTable, deleteTable, getTableSchema } from "@saga-bus/store-dynamodb";
|
|
89
|
+
|
|
90
|
+
// Create with defaults
|
|
91
|
+
await createTable(dynamoClient, { tableName: "saga-instances" });
|
|
92
|
+
|
|
93
|
+
// Get schema for CloudFormation/CDK
|
|
94
|
+
const schema = getTableSchema("saga-instances");
|
|
95
|
+
|
|
96
|
+
// Delete table
|
|
97
|
+
await deleteTable(dynamoClient, "saga-instances");
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DynamoDBSagaStore: () => DynamoDBSagaStore,
|
|
24
|
+
createTable: () => createTable,
|
|
25
|
+
deleteTable: () => deleteTable,
|
|
26
|
+
getTableSchema: () => getTableSchema
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/DynamoDBSagaStore.ts
|
|
31
|
+
var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
|
|
32
|
+
var import_core = require("@saga-bus/core");
|
|
33
|
+
var DynamoDBSagaStore = class {
|
|
34
|
+
client;
|
|
35
|
+
tableName;
|
|
36
|
+
correlationIndexName;
|
|
37
|
+
cleanupIndexName;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
this.client = options.client;
|
|
40
|
+
this.tableName = options.tableName;
|
|
41
|
+
this.correlationIndexName = options.correlationIndexName ?? "GSI1";
|
|
42
|
+
this.cleanupIndexName = options.cleanupIndexName ?? "GSI2";
|
|
43
|
+
}
|
|
44
|
+
async getById(sagaName, sagaId) {
|
|
45
|
+
const result = await this.client.send(
|
|
46
|
+
new import_lib_dynamodb.GetCommand({
|
|
47
|
+
TableName: this.tableName,
|
|
48
|
+
Key: {
|
|
49
|
+
PK: sagaName,
|
|
50
|
+
SK: sagaId
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
);
|
|
54
|
+
if (!result.Item) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return this.itemToState(result.Item);
|
|
58
|
+
}
|
|
59
|
+
async getByCorrelationId(sagaName, correlationId) {
|
|
60
|
+
const result = await this.client.send(
|
|
61
|
+
new import_lib_dynamodb.QueryCommand({
|
|
62
|
+
TableName: this.tableName,
|
|
63
|
+
IndexName: this.correlationIndexName,
|
|
64
|
+
KeyConditionExpression: "GSI1PK = :pk AND GSI1SK = :sk",
|
|
65
|
+
ExpressionAttributeValues: {
|
|
66
|
+
":pk": sagaName,
|
|
67
|
+
":sk": correlationId
|
|
68
|
+
},
|
|
69
|
+
Limit: 1
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
if (!result.Items || result.Items.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
return this.itemToState(result.Items[0]);
|
|
76
|
+
}
|
|
77
|
+
async insert(sagaName, correlationId, state) {
|
|
78
|
+
const { sagaId, version, isCompleted, createdAt, updatedAt } = state.metadata;
|
|
79
|
+
const serializedState = this.serializeState(state);
|
|
80
|
+
const item = {
|
|
81
|
+
PK: sagaName,
|
|
82
|
+
SK: sagaId,
|
|
83
|
+
sagaName,
|
|
84
|
+
sagaId,
|
|
85
|
+
correlationId,
|
|
86
|
+
version,
|
|
87
|
+
isCompleted,
|
|
88
|
+
state: serializedState,
|
|
89
|
+
createdAt: createdAt.toISOString(),
|
|
90
|
+
updatedAt: updatedAt.toISOString(),
|
|
91
|
+
GSI1PK: sagaName,
|
|
92
|
+
GSI1SK: correlationId
|
|
93
|
+
};
|
|
94
|
+
if (isCompleted) {
|
|
95
|
+
item.GSI2PK = sagaName;
|
|
96
|
+
item.GSI2SK = updatedAt.toISOString();
|
|
97
|
+
}
|
|
98
|
+
await this.client.send(
|
|
99
|
+
new import_lib_dynamodb.PutCommand({
|
|
100
|
+
TableName: this.tableName,
|
|
101
|
+
Item: item,
|
|
102
|
+
ConditionExpression: "attribute_not_exists(PK)"
|
|
103
|
+
})
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
async update(sagaName, state, expectedVersion) {
|
|
107
|
+
const { sagaId, version, isCompleted, updatedAt } = state.metadata;
|
|
108
|
+
try {
|
|
109
|
+
const updateExpression = isCompleted ? "SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt, GSI2PK = :gsi2pk, GSI2SK = :gsi2sk" : "SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt REMOVE GSI2PK, GSI2SK";
|
|
110
|
+
const serializedState = this.serializeState(state);
|
|
111
|
+
const expressionValues = {
|
|
112
|
+
":version": version,
|
|
113
|
+
":isCompleted": isCompleted,
|
|
114
|
+
":state": serializedState,
|
|
115
|
+
":updatedAt": updatedAt.toISOString(),
|
|
116
|
+
":expectedVersion": expectedVersion
|
|
117
|
+
};
|
|
118
|
+
if (isCompleted) {
|
|
119
|
+
expressionValues[":gsi2pk"] = sagaName;
|
|
120
|
+
expressionValues[":gsi2sk"] = updatedAt.toISOString();
|
|
121
|
+
}
|
|
122
|
+
await this.client.send(
|
|
123
|
+
new import_lib_dynamodb.UpdateCommand({
|
|
124
|
+
TableName: this.tableName,
|
|
125
|
+
Key: {
|
|
126
|
+
PK: sagaName,
|
|
127
|
+
SK: sagaId
|
|
128
|
+
},
|
|
129
|
+
UpdateExpression: updateExpression,
|
|
130
|
+
ConditionExpression: "version = :expectedVersion",
|
|
131
|
+
ExpressionAttributeNames: {
|
|
132
|
+
"#state": "state"
|
|
133
|
+
},
|
|
134
|
+
ExpressionAttributeValues: expressionValues
|
|
135
|
+
})
|
|
136
|
+
);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error instanceof Error && error.name === "ConditionalCheckFailedException") {
|
|
139
|
+
const existing = await this.getById(sagaName, sagaId);
|
|
140
|
+
if (existing) {
|
|
141
|
+
throw new import_core.ConcurrencyError(
|
|
142
|
+
sagaId,
|
|
143
|
+
expectedVersion,
|
|
144
|
+
existing.metadata.version
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error(`Saga ${sagaId} not found`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async delete(sagaName, sagaId) {
|
|
154
|
+
await this.client.send(
|
|
155
|
+
new import_lib_dynamodb.DeleteCommand({
|
|
156
|
+
TableName: this.tableName,
|
|
157
|
+
Key: {
|
|
158
|
+
PK: sagaName,
|
|
159
|
+
SK: sagaId
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
itemToState(item) {
|
|
165
|
+
const state = item.state;
|
|
166
|
+
return {
|
|
167
|
+
...state,
|
|
168
|
+
metadata: {
|
|
169
|
+
...state.metadata,
|
|
170
|
+
sagaId: item.sagaId,
|
|
171
|
+
version: item.version,
|
|
172
|
+
isCompleted: item.isCompleted,
|
|
173
|
+
createdAt: new Date(item.createdAt),
|
|
174
|
+
updatedAt: new Date(item.updatedAt)
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Serialize state for DynamoDB storage by converting Date objects to ISO strings.
|
|
180
|
+
*/
|
|
181
|
+
serializeState(state) {
|
|
182
|
+
return JSON.parse(JSON.stringify(state, (_, value) => {
|
|
183
|
+
if (value instanceof Date) {
|
|
184
|
+
return value.toISOString();
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
// ============ Query Helpers ============
|
|
190
|
+
/**
|
|
191
|
+
* Find sagas by name with pagination.
|
|
192
|
+
*/
|
|
193
|
+
async findByName(sagaName, options) {
|
|
194
|
+
let filterExpression;
|
|
195
|
+
const expressionValues = { ":pk": sagaName };
|
|
196
|
+
if (options?.completed !== void 0) {
|
|
197
|
+
filterExpression = "isCompleted = :isCompleted";
|
|
198
|
+
expressionValues[":isCompleted"] = options.completed;
|
|
199
|
+
}
|
|
200
|
+
const result = await this.client.send(
|
|
201
|
+
new import_lib_dynamodb.QueryCommand({
|
|
202
|
+
TableName: this.tableName,
|
|
203
|
+
KeyConditionExpression: "PK = :pk",
|
|
204
|
+
FilterExpression: filterExpression,
|
|
205
|
+
ExpressionAttributeValues: expressionValues,
|
|
206
|
+
Limit: options?.limit ?? 100,
|
|
207
|
+
ExclusiveStartKey: options?.startKey,
|
|
208
|
+
ScanIndexForward: false
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
const items = (result.Items ?? []).map(
|
|
212
|
+
(item) => this.itemToState(item)
|
|
213
|
+
);
|
|
214
|
+
return {
|
|
215
|
+
items,
|
|
216
|
+
lastKey: result.LastEvaluatedKey
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Delete completed sagas older than a given date.
|
|
221
|
+
* Note: This performs a query + batch delete which may require pagination.
|
|
222
|
+
*/
|
|
223
|
+
async deleteCompletedBefore(sagaName, before) {
|
|
224
|
+
let deletedCount = 0;
|
|
225
|
+
let lastKey;
|
|
226
|
+
do {
|
|
227
|
+
const result = await this.client.send(
|
|
228
|
+
new import_lib_dynamodb.QueryCommand({
|
|
229
|
+
TableName: this.tableName,
|
|
230
|
+
IndexName: this.cleanupIndexName,
|
|
231
|
+
KeyConditionExpression: "GSI2PK = :pk AND GSI2SK < :before",
|
|
232
|
+
ExpressionAttributeValues: {
|
|
233
|
+
":pk": sagaName,
|
|
234
|
+
":before": before.toISOString()
|
|
235
|
+
},
|
|
236
|
+
Limit: 25,
|
|
237
|
+
ExclusiveStartKey: lastKey
|
|
238
|
+
})
|
|
239
|
+
);
|
|
240
|
+
if (result.Items && result.Items.length > 0) {
|
|
241
|
+
for (const item of result.Items) {
|
|
242
|
+
await this.delete(sagaName, item.SK);
|
|
243
|
+
deletedCount++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
lastKey = result.LastEvaluatedKey;
|
|
247
|
+
} while (lastKey);
|
|
248
|
+
return deletedCount;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/schema.ts
|
|
253
|
+
var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
|
|
254
|
+
function getTableSchema(tableName, options) {
|
|
255
|
+
return {
|
|
256
|
+
tableName,
|
|
257
|
+
correlationIndexName: options?.correlationIndexName ?? "GSI1",
|
|
258
|
+
cleanupIndexName: options?.cleanupIndexName ?? "GSI2"
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
async function createTable(client, schema) {
|
|
262
|
+
const command = new import_client_dynamodb.CreateTableCommand({
|
|
263
|
+
TableName: schema.tableName,
|
|
264
|
+
KeySchema: [
|
|
265
|
+
{ AttributeName: "PK", KeyType: "HASH" },
|
|
266
|
+
{ AttributeName: "SK", KeyType: "RANGE" }
|
|
267
|
+
],
|
|
268
|
+
AttributeDefinitions: [
|
|
269
|
+
{ AttributeName: "PK", AttributeType: "S" },
|
|
270
|
+
{ AttributeName: "SK", AttributeType: "S" },
|
|
271
|
+
{ AttributeName: "GSI1PK", AttributeType: "S" },
|
|
272
|
+
{ AttributeName: "GSI1SK", AttributeType: "S" },
|
|
273
|
+
{ AttributeName: "GSI2PK", AttributeType: "S" },
|
|
274
|
+
{ AttributeName: "GSI2SK", AttributeType: "S" }
|
|
275
|
+
],
|
|
276
|
+
GlobalSecondaryIndexes: [
|
|
277
|
+
{
|
|
278
|
+
IndexName: schema.correlationIndexName,
|
|
279
|
+
KeySchema: [
|
|
280
|
+
{ AttributeName: "GSI1PK", KeyType: "HASH" },
|
|
281
|
+
{ AttributeName: "GSI1SK", KeyType: "RANGE" }
|
|
282
|
+
],
|
|
283
|
+
Projection: { ProjectionType: "ALL" },
|
|
284
|
+
ProvisionedThroughput: {
|
|
285
|
+
ReadCapacityUnits: 5,
|
|
286
|
+
WriteCapacityUnits: 5
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
IndexName: schema.cleanupIndexName,
|
|
291
|
+
KeySchema: [
|
|
292
|
+
{ AttributeName: "GSI2PK", KeyType: "HASH" },
|
|
293
|
+
{ AttributeName: "GSI2SK", KeyType: "RANGE" }
|
|
294
|
+
],
|
|
295
|
+
Projection: { ProjectionType: "KEYS_ONLY" },
|
|
296
|
+
ProvisionedThroughput: {
|
|
297
|
+
ReadCapacityUnits: 5,
|
|
298
|
+
WriteCapacityUnits: 5
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
],
|
|
302
|
+
BillingMode: "PROVISIONED",
|
|
303
|
+
ProvisionedThroughput: {
|
|
304
|
+
ReadCapacityUnits: 5,
|
|
305
|
+
WriteCapacityUnits: 5
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
await client.send(command);
|
|
309
|
+
await waitForTableActive(client, schema.tableName);
|
|
310
|
+
}
|
|
311
|
+
async function deleteTable(client, tableName) {
|
|
312
|
+
await client.send(new import_client_dynamodb.DeleteTableCommand({ TableName: tableName }));
|
|
313
|
+
}
|
|
314
|
+
async function waitForTableActive(client, tableName) {
|
|
315
|
+
for (let i = 0; i < 30; i++) {
|
|
316
|
+
const response = await client.send(
|
|
317
|
+
new import_client_dynamodb.DescribeTableCommand({ TableName: tableName })
|
|
318
|
+
);
|
|
319
|
+
if (response.Table?.TableStatus === "ACTIVE") {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Table ${tableName} did not become active`);
|
|
325
|
+
}
|
|
326
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
327
|
+
0 && (module.exports = {
|
|
328
|
+
DynamoDBSagaStore,
|
|
329
|
+
createTable,
|
|
330
|
+
deleteTable,
|
|
331
|
+
getTableSchema
|
|
332
|
+
});
|
|
333
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/DynamoDBSagaStore.ts","../src/schema.ts"],"sourcesContent":["export { DynamoDBSagaStore } from \"./DynamoDBSagaStore.js\";\nexport { createTable, deleteTable, getTableSchema } from \"./schema.js\";\nexport type {\n DynamoDBSagaStoreOptions,\n SagaInstanceItem,\n TableSchema,\n} from \"./types.js\";\n","import {\n GetCommand,\n PutCommand,\n UpdateCommand,\n DeleteCommand,\n QueryCommand,\n} from \"@aws-sdk/lib-dynamodb\";\nimport type { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { DynamoDBSagaStoreOptions, SagaInstanceItem } from \"./types.js\";\n\n/**\n * DynamoDB-backed saga store.\n *\n * @example\n * ```typescript\n * import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n * import { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\n *\n * const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));\n * const store = new DynamoDBSagaStore<OrderState>({\n * client,\n * tableName: \"saga_instances\",\n * });\n * ```\n */\nexport class DynamoDBSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private readonly client: DynamoDBDocumentClient;\n private readonly tableName: string;\n private readonly correlationIndexName: string;\n private readonly cleanupIndexName: string;\n\n constructor(options: DynamoDBSagaStoreOptions) {\n this.client = options.client;\n this.tableName = options.tableName;\n this.correlationIndexName = options.correlationIndexName ?? \"GSI1\";\n this.cleanupIndexName = options.cleanupIndexName ?? \"GSI2\";\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n const result = await this.client.send(\n new GetCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n })\n );\n\n if (!result.Item) {\n return null;\n }\n\n return this.itemToState(result.Item as SagaInstanceItem);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: this.correlationIndexName,\n KeyConditionExpression: \"GSI1PK = :pk AND GSI1SK = :sk\",\n ExpressionAttributeValues: {\n \":pk\": sagaName,\n \":sk\": correlationId,\n },\n Limit: 1,\n })\n );\n\n if (!result.Items || result.Items.length === 0) {\n return null;\n }\n\n return this.itemToState(result.Items[0] as SagaInstanceItem);\n }\n\n async insert(sagaName: string, correlationId: string, state: TState): Promise<void> {\n const { sagaId, version, isCompleted, createdAt, updatedAt } =\n state.metadata;\n\n // Serialize state with dates converted to ISO strings for DynamoDB compatibility\n const serializedState = this.serializeState(state);\n\n const item: SagaInstanceItem = {\n PK: sagaName,\n SK: sagaId,\n sagaName,\n sagaId,\n correlationId,\n version,\n isCompleted,\n state: serializedState,\n createdAt: createdAt.toISOString(),\n updatedAt: updatedAt.toISOString(),\n GSI1PK: sagaName,\n GSI1SK: correlationId,\n };\n\n // Only add GSI2 keys for completed sagas (for cleanup queries)\n if (isCompleted) {\n item.GSI2PK = sagaName;\n item.GSI2SK = updatedAt.toISOString();\n }\n\n await this.client.send(\n new PutCommand({\n TableName: this.tableName,\n Item: item,\n ConditionExpression: \"attribute_not_exists(PK)\",\n })\n );\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n const { sagaId, version, isCompleted, updatedAt } = state.metadata;\n\n try {\n const updateExpression = isCompleted\n ? \"SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt, GSI2PK = :gsi2pk, GSI2SK = :gsi2sk\"\n : \"SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt REMOVE GSI2PK, GSI2SK\";\n\n // Serialize state with dates converted to ISO strings for DynamoDB compatibility\n const serializedState = this.serializeState(state);\n\n const expressionValues: Record<string, unknown> = {\n \":version\": version,\n \":isCompleted\": isCompleted,\n \":state\": serializedState,\n \":updatedAt\": updatedAt.toISOString(),\n \":expectedVersion\": expectedVersion,\n };\n\n if (isCompleted) {\n expressionValues[\":gsi2pk\"] = sagaName;\n expressionValues[\":gsi2sk\"] = updatedAt.toISOString();\n }\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n UpdateExpression: updateExpression,\n ConditionExpression: \"version = :expectedVersion\",\n ExpressionAttributeNames: {\n \"#state\": \"state\",\n },\n ExpressionAttributeValues: expressionValues,\n })\n );\n } catch (error) {\n if (\n error instanceof Error &&\n error.name === \"ConditionalCheckFailedException\"\n ) {\n const existing = await this.getById(sagaName, sagaId);\n if (existing) {\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n } else {\n throw new Error(`Saga ${sagaId} not found`);\n }\n }\n throw error;\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n })\n );\n }\n\n private itemToState(item: SagaInstanceItem): TState {\n const state = item.state as TState;\n\n return {\n ...state,\n metadata: {\n ...state.metadata,\n sagaId: item.sagaId,\n version: item.version,\n isCompleted: item.isCompleted,\n createdAt: new Date(item.createdAt),\n updatedAt: new Date(item.updatedAt),\n },\n };\n }\n\n /**\n * Serialize state for DynamoDB storage by converting Date objects to ISO strings.\n */\n private serializeState(state: TState): Record<string, unknown> {\n return JSON.parse(JSON.stringify(state, (_, value) => {\n if (value instanceof Date) {\n return value.toISOString();\n }\n return value;\n }));\n }\n\n // ============ Query Helpers ============\n\n /**\n * Find sagas by name with pagination.\n */\n async findByName(\n sagaName: string,\n options?: {\n limit?: number;\n startKey?: Record<string, unknown>;\n completed?: boolean;\n }\n ): Promise<{ items: TState[]; lastKey?: Record<string, unknown> }> {\n let filterExpression: string | undefined;\n const expressionValues: Record<string, unknown> = { \":pk\": sagaName };\n\n if (options?.completed !== undefined) {\n filterExpression = \"isCompleted = :isCompleted\";\n expressionValues[\":isCompleted\"] = options.completed;\n }\n\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n KeyConditionExpression: \"PK = :pk\",\n FilterExpression: filterExpression,\n ExpressionAttributeValues: expressionValues,\n Limit: options?.limit ?? 100,\n ExclusiveStartKey: options?.startKey,\n ScanIndexForward: false,\n })\n );\n\n const items = (result.Items ?? []).map((item) =>\n this.itemToState(item as SagaInstanceItem)\n );\n\n return {\n items,\n lastKey: result.LastEvaluatedKey,\n };\n }\n\n /**\n * Delete completed sagas older than a given date.\n * Note: This performs a query + batch delete which may require pagination.\n */\n async deleteCompletedBefore(\n sagaName: string,\n before: Date\n ): Promise<number> {\n let deletedCount = 0;\n let lastKey: Record<string, unknown> | undefined;\n\n do {\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: this.cleanupIndexName,\n KeyConditionExpression: \"GSI2PK = :pk AND GSI2SK < :before\",\n ExpressionAttributeValues: {\n \":pk\": sagaName,\n \":before\": before.toISOString(),\n },\n Limit: 25,\n ExclusiveStartKey: lastKey,\n })\n );\n\n if (result.Items && result.Items.length > 0) {\n for (const item of result.Items) {\n await this.delete(sagaName, item.SK as string);\n deletedCount++;\n }\n }\n\n lastKey = result.LastEvaluatedKey;\n } while (lastKey);\n\n return deletedCount;\n }\n}\n","import {\n CreateTableCommand,\n DeleteTableCommand,\n DescribeTableCommand,\n type DynamoDBClient,\n} from \"@aws-sdk/client-dynamodb\";\nimport type { TableSchema } from \"./types.js\";\n\n/**\n * Get the table schema definition.\n */\nexport function getTableSchema(\n tableName: string,\n options?: {\n correlationIndexName?: string;\n cleanupIndexName?: string;\n }\n): TableSchema {\n return {\n tableName,\n correlationIndexName: options?.correlationIndexName ?? \"GSI1\",\n cleanupIndexName: options?.cleanupIndexName ?? \"GSI2\",\n };\n}\n\n/**\n * Create the saga instances table with required indexes.\n */\nexport async function createTable(\n client: DynamoDBClient,\n schema: TableSchema\n): Promise<void> {\n const command = new CreateTableCommand({\n TableName: schema.tableName,\n KeySchema: [\n { AttributeName: \"PK\", KeyType: \"HASH\" },\n { AttributeName: \"SK\", KeyType: \"RANGE\" },\n ],\n AttributeDefinitions: [\n { AttributeName: \"PK\", AttributeType: \"S\" },\n { AttributeName: \"SK\", AttributeType: \"S\" },\n { AttributeName: \"GSI1PK\", AttributeType: \"S\" },\n { AttributeName: \"GSI1SK\", AttributeType: \"S\" },\n { AttributeName: \"GSI2PK\", AttributeType: \"S\" },\n { AttributeName: \"GSI2SK\", AttributeType: \"S\" },\n ],\n GlobalSecondaryIndexes: [\n {\n IndexName: schema.correlationIndexName,\n KeySchema: [\n { AttributeName: \"GSI1PK\", KeyType: \"HASH\" },\n { AttributeName: \"GSI1SK\", KeyType: \"RANGE\" },\n ],\n Projection: { ProjectionType: \"ALL\" },\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n },\n {\n IndexName: schema.cleanupIndexName,\n KeySchema: [\n { AttributeName: \"GSI2PK\", KeyType: \"HASH\" },\n { AttributeName: \"GSI2SK\", KeyType: \"RANGE\" },\n ],\n Projection: { ProjectionType: \"KEYS_ONLY\" },\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n },\n ],\n BillingMode: \"PROVISIONED\",\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n });\n\n await client.send(command);\n\n // Wait for table to be active\n await waitForTableActive(client, schema.tableName);\n}\n\n/**\n * Delete the saga instances table.\n */\nexport async function deleteTable(\n client: DynamoDBClient,\n tableName: string\n): Promise<void> {\n await client.send(new DeleteTableCommand({ TableName: tableName }));\n}\n\n/**\n * Wait for table to be active.\n */\nasync function waitForTableActive(\n client: DynamoDBClient,\n tableName: string\n): Promise<void> {\n for (let i = 0; i < 30; i++) {\n const response = await client.send(\n new DescribeTableCommand({ TableName: tableName })\n );\n\n if (response.Table?.TableStatus === \"ACTIVE\") {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n\n throw new Error(`Table ${tableName} did not become active`);\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SagaState, SagaStore } from '@saga-bus/core';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DynamoDB saga store configuration options.
|
|
7
|
+
*/
|
|
8
|
+
interface DynamoDBSagaStoreOptions {
|
|
9
|
+
/**
|
|
10
|
+
* DynamoDB Document Client instance.
|
|
11
|
+
*/
|
|
12
|
+
client: DynamoDBDocumentClient;
|
|
13
|
+
/**
|
|
14
|
+
* Table name for saga instances.
|
|
15
|
+
*/
|
|
16
|
+
tableName: string;
|
|
17
|
+
/**
|
|
18
|
+
* Name of the correlation ID GSI.
|
|
19
|
+
* @default "GSI1"
|
|
20
|
+
*/
|
|
21
|
+
correlationIndexName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Name of the cleanup GSI (for querying completed sagas by date).
|
|
24
|
+
* @default "GSI2"
|
|
25
|
+
*/
|
|
26
|
+
cleanupIndexName?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Shape of a saga instance item in DynamoDB.
|
|
30
|
+
*/
|
|
31
|
+
interface SagaInstanceItem {
|
|
32
|
+
PK: string;
|
|
33
|
+
SK: string;
|
|
34
|
+
sagaName: string;
|
|
35
|
+
sagaId: string;
|
|
36
|
+
correlationId: string;
|
|
37
|
+
version: number;
|
|
38
|
+
isCompleted: boolean;
|
|
39
|
+
state: Record<string, unknown>;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
GSI1PK: string;
|
|
43
|
+
GSI1SK: string;
|
|
44
|
+
GSI2PK?: string;
|
|
45
|
+
GSI2SK?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Table schema definition for creating the table.
|
|
49
|
+
*/
|
|
50
|
+
interface TableSchema {
|
|
51
|
+
tableName: string;
|
|
52
|
+
correlationIndexName: string;
|
|
53
|
+
cleanupIndexName: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* DynamoDB-backed saga store.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
62
|
+
* import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
63
|
+
*
|
|
64
|
+
* const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
65
|
+
* const store = new DynamoDBSagaStore<OrderState>({
|
|
66
|
+
* client,
|
|
67
|
+
* tableName: "saga_instances",
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare class DynamoDBSagaStore<TState extends SagaState> implements SagaStore<TState> {
|
|
72
|
+
private readonly client;
|
|
73
|
+
private readonly tableName;
|
|
74
|
+
private readonly correlationIndexName;
|
|
75
|
+
private readonly cleanupIndexName;
|
|
76
|
+
constructor(options: DynamoDBSagaStoreOptions);
|
|
77
|
+
getById(sagaName: string, sagaId: string): Promise<TState | null>;
|
|
78
|
+
getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
|
|
79
|
+
insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
|
|
80
|
+
update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
|
|
81
|
+
delete(sagaName: string, sagaId: string): Promise<void>;
|
|
82
|
+
private itemToState;
|
|
83
|
+
/**
|
|
84
|
+
* Serialize state for DynamoDB storage by converting Date objects to ISO strings.
|
|
85
|
+
*/
|
|
86
|
+
private serializeState;
|
|
87
|
+
/**
|
|
88
|
+
* Find sagas by name with pagination.
|
|
89
|
+
*/
|
|
90
|
+
findByName(sagaName: string, options?: {
|
|
91
|
+
limit?: number;
|
|
92
|
+
startKey?: Record<string, unknown>;
|
|
93
|
+
completed?: boolean;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
items: TState[];
|
|
96
|
+
lastKey?: Record<string, unknown>;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* Delete completed sagas older than a given date.
|
|
100
|
+
* Note: This performs a query + batch delete which may require pagination.
|
|
101
|
+
*/
|
|
102
|
+
deleteCompletedBefore(sagaName: string, before: Date): Promise<number>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the table schema definition.
|
|
107
|
+
*/
|
|
108
|
+
declare function getTableSchema(tableName: string, options?: {
|
|
109
|
+
correlationIndexName?: string;
|
|
110
|
+
cleanupIndexName?: string;
|
|
111
|
+
}): TableSchema;
|
|
112
|
+
/**
|
|
113
|
+
* Create the saga instances table with required indexes.
|
|
114
|
+
*/
|
|
115
|
+
declare function createTable(client: DynamoDBClient, schema: TableSchema): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Delete the saga instances table.
|
|
118
|
+
*/
|
|
119
|
+
declare function deleteTable(client: DynamoDBClient, tableName: string): Promise<void>;
|
|
120
|
+
|
|
121
|
+
export { DynamoDBSagaStore, type DynamoDBSagaStoreOptions, type SagaInstanceItem, type TableSchema, createTable, deleteTable, getTableSchema };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SagaState, SagaStore } from '@saga-bus/core';
|
|
2
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
3
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DynamoDB saga store configuration options.
|
|
7
|
+
*/
|
|
8
|
+
interface DynamoDBSagaStoreOptions {
|
|
9
|
+
/**
|
|
10
|
+
* DynamoDB Document Client instance.
|
|
11
|
+
*/
|
|
12
|
+
client: DynamoDBDocumentClient;
|
|
13
|
+
/**
|
|
14
|
+
* Table name for saga instances.
|
|
15
|
+
*/
|
|
16
|
+
tableName: string;
|
|
17
|
+
/**
|
|
18
|
+
* Name of the correlation ID GSI.
|
|
19
|
+
* @default "GSI1"
|
|
20
|
+
*/
|
|
21
|
+
correlationIndexName?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Name of the cleanup GSI (for querying completed sagas by date).
|
|
24
|
+
* @default "GSI2"
|
|
25
|
+
*/
|
|
26
|
+
cleanupIndexName?: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Shape of a saga instance item in DynamoDB.
|
|
30
|
+
*/
|
|
31
|
+
interface SagaInstanceItem {
|
|
32
|
+
PK: string;
|
|
33
|
+
SK: string;
|
|
34
|
+
sagaName: string;
|
|
35
|
+
sagaId: string;
|
|
36
|
+
correlationId: string;
|
|
37
|
+
version: number;
|
|
38
|
+
isCompleted: boolean;
|
|
39
|
+
state: Record<string, unknown>;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
updatedAt: string;
|
|
42
|
+
GSI1PK: string;
|
|
43
|
+
GSI1SK: string;
|
|
44
|
+
GSI2PK?: string;
|
|
45
|
+
GSI2SK?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Table schema definition for creating the table.
|
|
49
|
+
*/
|
|
50
|
+
interface TableSchema {
|
|
51
|
+
tableName: string;
|
|
52
|
+
correlationIndexName: string;
|
|
53
|
+
cleanupIndexName: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* DynamoDB-backed saga store.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```typescript
|
|
61
|
+
* import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
62
|
+
* import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
63
|
+
*
|
|
64
|
+
* const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
65
|
+
* const store = new DynamoDBSagaStore<OrderState>({
|
|
66
|
+
* client,
|
|
67
|
+
* tableName: "saga_instances",
|
|
68
|
+
* });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare class DynamoDBSagaStore<TState extends SagaState> implements SagaStore<TState> {
|
|
72
|
+
private readonly client;
|
|
73
|
+
private readonly tableName;
|
|
74
|
+
private readonly correlationIndexName;
|
|
75
|
+
private readonly cleanupIndexName;
|
|
76
|
+
constructor(options: DynamoDBSagaStoreOptions);
|
|
77
|
+
getById(sagaName: string, sagaId: string): Promise<TState | null>;
|
|
78
|
+
getByCorrelationId(sagaName: string, correlationId: string): Promise<TState | null>;
|
|
79
|
+
insert(sagaName: string, correlationId: string, state: TState): Promise<void>;
|
|
80
|
+
update(sagaName: string, state: TState, expectedVersion: number): Promise<void>;
|
|
81
|
+
delete(sagaName: string, sagaId: string): Promise<void>;
|
|
82
|
+
private itemToState;
|
|
83
|
+
/**
|
|
84
|
+
* Serialize state for DynamoDB storage by converting Date objects to ISO strings.
|
|
85
|
+
*/
|
|
86
|
+
private serializeState;
|
|
87
|
+
/**
|
|
88
|
+
* Find sagas by name with pagination.
|
|
89
|
+
*/
|
|
90
|
+
findByName(sagaName: string, options?: {
|
|
91
|
+
limit?: number;
|
|
92
|
+
startKey?: Record<string, unknown>;
|
|
93
|
+
completed?: boolean;
|
|
94
|
+
}): Promise<{
|
|
95
|
+
items: TState[];
|
|
96
|
+
lastKey?: Record<string, unknown>;
|
|
97
|
+
}>;
|
|
98
|
+
/**
|
|
99
|
+
* Delete completed sagas older than a given date.
|
|
100
|
+
* Note: This performs a query + batch delete which may require pagination.
|
|
101
|
+
*/
|
|
102
|
+
deleteCompletedBefore(sagaName: string, before: Date): Promise<number>;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the table schema definition.
|
|
107
|
+
*/
|
|
108
|
+
declare function getTableSchema(tableName: string, options?: {
|
|
109
|
+
correlationIndexName?: string;
|
|
110
|
+
cleanupIndexName?: string;
|
|
111
|
+
}): TableSchema;
|
|
112
|
+
/**
|
|
113
|
+
* Create the saga instances table with required indexes.
|
|
114
|
+
*/
|
|
115
|
+
declare function createTable(client: DynamoDBClient, schema: TableSchema): Promise<void>;
|
|
116
|
+
/**
|
|
117
|
+
* Delete the saga instances table.
|
|
118
|
+
*/
|
|
119
|
+
declare function deleteTable(client: DynamoDBClient, tableName: string): Promise<void>;
|
|
120
|
+
|
|
121
|
+
export { DynamoDBSagaStore, type DynamoDBSagaStoreOptions, type SagaInstanceItem, type TableSchema, createTable, deleteTable, getTableSchema };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
// src/DynamoDBSagaStore.ts
|
|
2
|
+
import {
|
|
3
|
+
GetCommand,
|
|
4
|
+
PutCommand,
|
|
5
|
+
UpdateCommand,
|
|
6
|
+
DeleteCommand,
|
|
7
|
+
QueryCommand
|
|
8
|
+
} from "@aws-sdk/lib-dynamodb";
|
|
9
|
+
import { ConcurrencyError } from "@saga-bus/core";
|
|
10
|
+
var DynamoDBSagaStore = class {
|
|
11
|
+
client;
|
|
12
|
+
tableName;
|
|
13
|
+
correlationIndexName;
|
|
14
|
+
cleanupIndexName;
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.client = options.client;
|
|
17
|
+
this.tableName = options.tableName;
|
|
18
|
+
this.correlationIndexName = options.correlationIndexName ?? "GSI1";
|
|
19
|
+
this.cleanupIndexName = options.cleanupIndexName ?? "GSI2";
|
|
20
|
+
}
|
|
21
|
+
async getById(sagaName, sagaId) {
|
|
22
|
+
const result = await this.client.send(
|
|
23
|
+
new GetCommand({
|
|
24
|
+
TableName: this.tableName,
|
|
25
|
+
Key: {
|
|
26
|
+
PK: sagaName,
|
|
27
|
+
SK: sagaId
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
);
|
|
31
|
+
if (!result.Item) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return this.itemToState(result.Item);
|
|
35
|
+
}
|
|
36
|
+
async getByCorrelationId(sagaName, correlationId) {
|
|
37
|
+
const result = await this.client.send(
|
|
38
|
+
new QueryCommand({
|
|
39
|
+
TableName: this.tableName,
|
|
40
|
+
IndexName: this.correlationIndexName,
|
|
41
|
+
KeyConditionExpression: "GSI1PK = :pk AND GSI1SK = :sk",
|
|
42
|
+
ExpressionAttributeValues: {
|
|
43
|
+
":pk": sagaName,
|
|
44
|
+
":sk": correlationId
|
|
45
|
+
},
|
|
46
|
+
Limit: 1
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
if (!result.Items || result.Items.length === 0) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
return this.itemToState(result.Items[0]);
|
|
53
|
+
}
|
|
54
|
+
async insert(sagaName, correlationId, state) {
|
|
55
|
+
const { sagaId, version, isCompleted, createdAt, updatedAt } = state.metadata;
|
|
56
|
+
const serializedState = this.serializeState(state);
|
|
57
|
+
const item = {
|
|
58
|
+
PK: sagaName,
|
|
59
|
+
SK: sagaId,
|
|
60
|
+
sagaName,
|
|
61
|
+
sagaId,
|
|
62
|
+
correlationId,
|
|
63
|
+
version,
|
|
64
|
+
isCompleted,
|
|
65
|
+
state: serializedState,
|
|
66
|
+
createdAt: createdAt.toISOString(),
|
|
67
|
+
updatedAt: updatedAt.toISOString(),
|
|
68
|
+
GSI1PK: sagaName,
|
|
69
|
+
GSI1SK: correlationId
|
|
70
|
+
};
|
|
71
|
+
if (isCompleted) {
|
|
72
|
+
item.GSI2PK = sagaName;
|
|
73
|
+
item.GSI2SK = updatedAt.toISOString();
|
|
74
|
+
}
|
|
75
|
+
await this.client.send(
|
|
76
|
+
new PutCommand({
|
|
77
|
+
TableName: this.tableName,
|
|
78
|
+
Item: item,
|
|
79
|
+
ConditionExpression: "attribute_not_exists(PK)"
|
|
80
|
+
})
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async update(sagaName, state, expectedVersion) {
|
|
84
|
+
const { sagaId, version, isCompleted, updatedAt } = state.metadata;
|
|
85
|
+
try {
|
|
86
|
+
const updateExpression = isCompleted ? "SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt, GSI2PK = :gsi2pk, GSI2SK = :gsi2sk" : "SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt REMOVE GSI2PK, GSI2SK";
|
|
87
|
+
const serializedState = this.serializeState(state);
|
|
88
|
+
const expressionValues = {
|
|
89
|
+
":version": version,
|
|
90
|
+
":isCompleted": isCompleted,
|
|
91
|
+
":state": serializedState,
|
|
92
|
+
":updatedAt": updatedAt.toISOString(),
|
|
93
|
+
":expectedVersion": expectedVersion
|
|
94
|
+
};
|
|
95
|
+
if (isCompleted) {
|
|
96
|
+
expressionValues[":gsi2pk"] = sagaName;
|
|
97
|
+
expressionValues[":gsi2sk"] = updatedAt.toISOString();
|
|
98
|
+
}
|
|
99
|
+
await this.client.send(
|
|
100
|
+
new UpdateCommand({
|
|
101
|
+
TableName: this.tableName,
|
|
102
|
+
Key: {
|
|
103
|
+
PK: sagaName,
|
|
104
|
+
SK: sagaId
|
|
105
|
+
},
|
|
106
|
+
UpdateExpression: updateExpression,
|
|
107
|
+
ConditionExpression: "version = :expectedVersion",
|
|
108
|
+
ExpressionAttributeNames: {
|
|
109
|
+
"#state": "state"
|
|
110
|
+
},
|
|
111
|
+
ExpressionAttributeValues: expressionValues
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
} catch (error) {
|
|
115
|
+
if (error instanceof Error && error.name === "ConditionalCheckFailedException") {
|
|
116
|
+
const existing = await this.getById(sagaName, sagaId);
|
|
117
|
+
if (existing) {
|
|
118
|
+
throw new ConcurrencyError(
|
|
119
|
+
sagaId,
|
|
120
|
+
expectedVersion,
|
|
121
|
+
existing.metadata.version
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
throw new Error(`Saga ${sagaId} not found`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
async delete(sagaName, sagaId) {
|
|
131
|
+
await this.client.send(
|
|
132
|
+
new DeleteCommand({
|
|
133
|
+
TableName: this.tableName,
|
|
134
|
+
Key: {
|
|
135
|
+
PK: sagaName,
|
|
136
|
+
SK: sagaId
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
itemToState(item) {
|
|
142
|
+
const state = item.state;
|
|
143
|
+
return {
|
|
144
|
+
...state,
|
|
145
|
+
metadata: {
|
|
146
|
+
...state.metadata,
|
|
147
|
+
sagaId: item.sagaId,
|
|
148
|
+
version: item.version,
|
|
149
|
+
isCompleted: item.isCompleted,
|
|
150
|
+
createdAt: new Date(item.createdAt),
|
|
151
|
+
updatedAt: new Date(item.updatedAt)
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Serialize state for DynamoDB storage by converting Date objects to ISO strings.
|
|
157
|
+
*/
|
|
158
|
+
serializeState(state) {
|
|
159
|
+
return JSON.parse(JSON.stringify(state, (_, value) => {
|
|
160
|
+
if (value instanceof Date) {
|
|
161
|
+
return value.toISOString();
|
|
162
|
+
}
|
|
163
|
+
return value;
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
// ============ Query Helpers ============
|
|
167
|
+
/**
|
|
168
|
+
* Find sagas by name with pagination.
|
|
169
|
+
*/
|
|
170
|
+
async findByName(sagaName, options) {
|
|
171
|
+
let filterExpression;
|
|
172
|
+
const expressionValues = { ":pk": sagaName };
|
|
173
|
+
if (options?.completed !== void 0) {
|
|
174
|
+
filterExpression = "isCompleted = :isCompleted";
|
|
175
|
+
expressionValues[":isCompleted"] = options.completed;
|
|
176
|
+
}
|
|
177
|
+
const result = await this.client.send(
|
|
178
|
+
new QueryCommand({
|
|
179
|
+
TableName: this.tableName,
|
|
180
|
+
KeyConditionExpression: "PK = :pk",
|
|
181
|
+
FilterExpression: filterExpression,
|
|
182
|
+
ExpressionAttributeValues: expressionValues,
|
|
183
|
+
Limit: options?.limit ?? 100,
|
|
184
|
+
ExclusiveStartKey: options?.startKey,
|
|
185
|
+
ScanIndexForward: false
|
|
186
|
+
})
|
|
187
|
+
);
|
|
188
|
+
const items = (result.Items ?? []).map(
|
|
189
|
+
(item) => this.itemToState(item)
|
|
190
|
+
);
|
|
191
|
+
return {
|
|
192
|
+
items,
|
|
193
|
+
lastKey: result.LastEvaluatedKey
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Delete completed sagas older than a given date.
|
|
198
|
+
* Note: This performs a query + batch delete which may require pagination.
|
|
199
|
+
*/
|
|
200
|
+
async deleteCompletedBefore(sagaName, before) {
|
|
201
|
+
let deletedCount = 0;
|
|
202
|
+
let lastKey;
|
|
203
|
+
do {
|
|
204
|
+
const result = await this.client.send(
|
|
205
|
+
new QueryCommand({
|
|
206
|
+
TableName: this.tableName,
|
|
207
|
+
IndexName: this.cleanupIndexName,
|
|
208
|
+
KeyConditionExpression: "GSI2PK = :pk AND GSI2SK < :before",
|
|
209
|
+
ExpressionAttributeValues: {
|
|
210
|
+
":pk": sagaName,
|
|
211
|
+
":before": before.toISOString()
|
|
212
|
+
},
|
|
213
|
+
Limit: 25,
|
|
214
|
+
ExclusiveStartKey: lastKey
|
|
215
|
+
})
|
|
216
|
+
);
|
|
217
|
+
if (result.Items && result.Items.length > 0) {
|
|
218
|
+
for (const item of result.Items) {
|
|
219
|
+
await this.delete(sagaName, item.SK);
|
|
220
|
+
deletedCount++;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
lastKey = result.LastEvaluatedKey;
|
|
224
|
+
} while (lastKey);
|
|
225
|
+
return deletedCount;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
// src/schema.ts
|
|
230
|
+
import {
|
|
231
|
+
CreateTableCommand,
|
|
232
|
+
DeleteTableCommand,
|
|
233
|
+
DescribeTableCommand
|
|
234
|
+
} from "@aws-sdk/client-dynamodb";
|
|
235
|
+
function getTableSchema(tableName, options) {
|
|
236
|
+
return {
|
|
237
|
+
tableName,
|
|
238
|
+
correlationIndexName: options?.correlationIndexName ?? "GSI1",
|
|
239
|
+
cleanupIndexName: options?.cleanupIndexName ?? "GSI2"
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function createTable(client, schema) {
|
|
243
|
+
const command = new CreateTableCommand({
|
|
244
|
+
TableName: schema.tableName,
|
|
245
|
+
KeySchema: [
|
|
246
|
+
{ AttributeName: "PK", KeyType: "HASH" },
|
|
247
|
+
{ AttributeName: "SK", KeyType: "RANGE" }
|
|
248
|
+
],
|
|
249
|
+
AttributeDefinitions: [
|
|
250
|
+
{ AttributeName: "PK", AttributeType: "S" },
|
|
251
|
+
{ AttributeName: "SK", AttributeType: "S" },
|
|
252
|
+
{ AttributeName: "GSI1PK", AttributeType: "S" },
|
|
253
|
+
{ AttributeName: "GSI1SK", AttributeType: "S" },
|
|
254
|
+
{ AttributeName: "GSI2PK", AttributeType: "S" },
|
|
255
|
+
{ AttributeName: "GSI2SK", AttributeType: "S" }
|
|
256
|
+
],
|
|
257
|
+
GlobalSecondaryIndexes: [
|
|
258
|
+
{
|
|
259
|
+
IndexName: schema.correlationIndexName,
|
|
260
|
+
KeySchema: [
|
|
261
|
+
{ AttributeName: "GSI1PK", KeyType: "HASH" },
|
|
262
|
+
{ AttributeName: "GSI1SK", KeyType: "RANGE" }
|
|
263
|
+
],
|
|
264
|
+
Projection: { ProjectionType: "ALL" },
|
|
265
|
+
ProvisionedThroughput: {
|
|
266
|
+
ReadCapacityUnits: 5,
|
|
267
|
+
WriteCapacityUnits: 5
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
IndexName: schema.cleanupIndexName,
|
|
272
|
+
KeySchema: [
|
|
273
|
+
{ AttributeName: "GSI2PK", KeyType: "HASH" },
|
|
274
|
+
{ AttributeName: "GSI2SK", KeyType: "RANGE" }
|
|
275
|
+
],
|
|
276
|
+
Projection: { ProjectionType: "KEYS_ONLY" },
|
|
277
|
+
ProvisionedThroughput: {
|
|
278
|
+
ReadCapacityUnits: 5,
|
|
279
|
+
WriteCapacityUnits: 5
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
],
|
|
283
|
+
BillingMode: "PROVISIONED",
|
|
284
|
+
ProvisionedThroughput: {
|
|
285
|
+
ReadCapacityUnits: 5,
|
|
286
|
+
WriteCapacityUnits: 5
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
await client.send(command);
|
|
290
|
+
await waitForTableActive(client, schema.tableName);
|
|
291
|
+
}
|
|
292
|
+
async function deleteTable(client, tableName) {
|
|
293
|
+
await client.send(new DeleteTableCommand({ TableName: tableName }));
|
|
294
|
+
}
|
|
295
|
+
async function waitForTableActive(client, tableName) {
|
|
296
|
+
for (let i = 0; i < 30; i++) {
|
|
297
|
+
const response = await client.send(
|
|
298
|
+
new DescribeTableCommand({ TableName: tableName })
|
|
299
|
+
);
|
|
300
|
+
if (response.Table?.TableStatus === "ACTIVE") {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
304
|
+
}
|
|
305
|
+
throw new Error(`Table ${tableName} did not become active`);
|
|
306
|
+
}
|
|
307
|
+
export {
|
|
308
|
+
DynamoDBSagaStore,
|
|
309
|
+
createTable,
|
|
310
|
+
deleteTable,
|
|
311
|
+
getTableSchema
|
|
312
|
+
};
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/DynamoDBSagaStore.ts","../src/schema.ts"],"sourcesContent":["import {\n GetCommand,\n PutCommand,\n UpdateCommand,\n DeleteCommand,\n QueryCommand,\n} from \"@aws-sdk/lib-dynamodb\";\nimport type { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport type { SagaStore, SagaState } from \"@saga-bus/core\";\nimport { ConcurrencyError } from \"@saga-bus/core\";\nimport type { DynamoDBSagaStoreOptions, SagaInstanceItem } from \"./types.js\";\n\n/**\n * DynamoDB-backed saga store.\n *\n * @example\n * ```typescript\n * import { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n * import { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\n *\n * const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));\n * const store = new DynamoDBSagaStore<OrderState>({\n * client,\n * tableName: \"saga_instances\",\n * });\n * ```\n */\nexport class DynamoDBSagaStore<TState extends SagaState>\n implements SagaStore<TState>\n{\n private readonly client: DynamoDBDocumentClient;\n private readonly tableName: string;\n private readonly correlationIndexName: string;\n private readonly cleanupIndexName: string;\n\n constructor(options: DynamoDBSagaStoreOptions) {\n this.client = options.client;\n this.tableName = options.tableName;\n this.correlationIndexName = options.correlationIndexName ?? \"GSI1\";\n this.cleanupIndexName = options.cleanupIndexName ?? \"GSI2\";\n }\n\n async getById(sagaName: string, sagaId: string): Promise<TState | null> {\n const result = await this.client.send(\n new GetCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n })\n );\n\n if (!result.Item) {\n return null;\n }\n\n return this.itemToState(result.Item as SagaInstanceItem);\n }\n\n async getByCorrelationId(\n sagaName: string,\n correlationId: string\n ): Promise<TState | null> {\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: this.correlationIndexName,\n KeyConditionExpression: \"GSI1PK = :pk AND GSI1SK = :sk\",\n ExpressionAttributeValues: {\n \":pk\": sagaName,\n \":sk\": correlationId,\n },\n Limit: 1,\n })\n );\n\n if (!result.Items || result.Items.length === 0) {\n return null;\n }\n\n return this.itemToState(result.Items[0] as SagaInstanceItem);\n }\n\n async insert(sagaName: string, correlationId: string, state: TState): Promise<void> {\n const { sagaId, version, isCompleted, createdAt, updatedAt } =\n state.metadata;\n\n // Serialize state with dates converted to ISO strings for DynamoDB compatibility\n const serializedState = this.serializeState(state);\n\n const item: SagaInstanceItem = {\n PK: sagaName,\n SK: sagaId,\n sagaName,\n sagaId,\n correlationId,\n version,\n isCompleted,\n state: serializedState,\n createdAt: createdAt.toISOString(),\n updatedAt: updatedAt.toISOString(),\n GSI1PK: sagaName,\n GSI1SK: correlationId,\n };\n\n // Only add GSI2 keys for completed sagas (for cleanup queries)\n if (isCompleted) {\n item.GSI2PK = sagaName;\n item.GSI2SK = updatedAt.toISOString();\n }\n\n await this.client.send(\n new PutCommand({\n TableName: this.tableName,\n Item: item,\n ConditionExpression: \"attribute_not_exists(PK)\",\n })\n );\n }\n\n async update(\n sagaName: string,\n state: TState,\n expectedVersion: number\n ): Promise<void> {\n const { sagaId, version, isCompleted, updatedAt } = state.metadata;\n\n try {\n const updateExpression = isCompleted\n ? \"SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt, GSI2PK = :gsi2pk, GSI2SK = :gsi2sk\"\n : \"SET version = :version, isCompleted = :isCompleted, #state = :state, updatedAt = :updatedAt REMOVE GSI2PK, GSI2SK\";\n\n // Serialize state with dates converted to ISO strings for DynamoDB compatibility\n const serializedState = this.serializeState(state);\n\n const expressionValues: Record<string, unknown> = {\n \":version\": version,\n \":isCompleted\": isCompleted,\n \":state\": serializedState,\n \":updatedAt\": updatedAt.toISOString(),\n \":expectedVersion\": expectedVersion,\n };\n\n if (isCompleted) {\n expressionValues[\":gsi2pk\"] = sagaName;\n expressionValues[\":gsi2sk\"] = updatedAt.toISOString();\n }\n\n await this.client.send(\n new UpdateCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n UpdateExpression: updateExpression,\n ConditionExpression: \"version = :expectedVersion\",\n ExpressionAttributeNames: {\n \"#state\": \"state\",\n },\n ExpressionAttributeValues: expressionValues,\n })\n );\n } catch (error) {\n if (\n error instanceof Error &&\n error.name === \"ConditionalCheckFailedException\"\n ) {\n const existing = await this.getById(sagaName, sagaId);\n if (existing) {\n throw new ConcurrencyError(\n sagaId,\n expectedVersion,\n existing.metadata.version\n );\n } else {\n throw new Error(`Saga ${sagaId} not found`);\n }\n }\n throw error;\n }\n }\n\n async delete(sagaName: string, sagaId: string): Promise<void> {\n await this.client.send(\n new DeleteCommand({\n TableName: this.tableName,\n Key: {\n PK: sagaName,\n SK: sagaId,\n },\n })\n );\n }\n\n private itemToState(item: SagaInstanceItem): TState {\n const state = item.state as TState;\n\n return {\n ...state,\n metadata: {\n ...state.metadata,\n sagaId: item.sagaId,\n version: item.version,\n isCompleted: item.isCompleted,\n createdAt: new Date(item.createdAt),\n updatedAt: new Date(item.updatedAt),\n },\n };\n }\n\n /**\n * Serialize state for DynamoDB storage by converting Date objects to ISO strings.\n */\n private serializeState(state: TState): Record<string, unknown> {\n return JSON.parse(JSON.stringify(state, (_, value) => {\n if (value instanceof Date) {\n return value.toISOString();\n }\n return value;\n }));\n }\n\n // ============ Query Helpers ============\n\n /**\n * Find sagas by name with pagination.\n */\n async findByName(\n sagaName: string,\n options?: {\n limit?: number;\n startKey?: Record<string, unknown>;\n completed?: boolean;\n }\n ): Promise<{ items: TState[]; lastKey?: Record<string, unknown> }> {\n let filterExpression: string | undefined;\n const expressionValues: Record<string, unknown> = { \":pk\": sagaName };\n\n if (options?.completed !== undefined) {\n filterExpression = \"isCompleted = :isCompleted\";\n expressionValues[\":isCompleted\"] = options.completed;\n }\n\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n KeyConditionExpression: \"PK = :pk\",\n FilterExpression: filterExpression,\n ExpressionAttributeValues: expressionValues,\n Limit: options?.limit ?? 100,\n ExclusiveStartKey: options?.startKey,\n ScanIndexForward: false,\n })\n );\n\n const items = (result.Items ?? []).map((item) =>\n this.itemToState(item as SagaInstanceItem)\n );\n\n return {\n items,\n lastKey: result.LastEvaluatedKey,\n };\n }\n\n /**\n * Delete completed sagas older than a given date.\n * Note: This performs a query + batch delete which may require pagination.\n */\n async deleteCompletedBefore(\n sagaName: string,\n before: Date\n ): Promise<number> {\n let deletedCount = 0;\n let lastKey: Record<string, unknown> | undefined;\n\n do {\n const result = await this.client.send(\n new QueryCommand({\n TableName: this.tableName,\n IndexName: this.cleanupIndexName,\n KeyConditionExpression: \"GSI2PK = :pk AND GSI2SK < :before\",\n ExpressionAttributeValues: {\n \":pk\": sagaName,\n \":before\": before.toISOString(),\n },\n Limit: 25,\n ExclusiveStartKey: lastKey,\n })\n );\n\n if (result.Items && result.Items.length > 0) {\n for (const item of result.Items) {\n await this.delete(sagaName, item.SK as string);\n deletedCount++;\n }\n }\n\n lastKey = result.LastEvaluatedKey;\n } while (lastKey);\n\n return deletedCount;\n }\n}\n","import {\n CreateTableCommand,\n DeleteTableCommand,\n DescribeTableCommand,\n type DynamoDBClient,\n} from \"@aws-sdk/client-dynamodb\";\nimport type { TableSchema } from \"./types.js\";\n\n/**\n * Get the table schema definition.\n */\nexport function getTableSchema(\n tableName: string,\n options?: {\n correlationIndexName?: string;\n cleanupIndexName?: string;\n }\n): TableSchema {\n return {\n tableName,\n correlationIndexName: options?.correlationIndexName ?? \"GSI1\",\n cleanupIndexName: options?.cleanupIndexName ?? \"GSI2\",\n };\n}\n\n/**\n * Create the saga instances table with required indexes.\n */\nexport async function createTable(\n client: DynamoDBClient,\n schema: TableSchema\n): Promise<void> {\n const command = new CreateTableCommand({\n TableName: schema.tableName,\n KeySchema: [\n { AttributeName: \"PK\", KeyType: \"HASH\" },\n { AttributeName: \"SK\", KeyType: \"RANGE\" },\n ],\n AttributeDefinitions: [\n { AttributeName: \"PK\", AttributeType: \"S\" },\n { AttributeName: \"SK\", AttributeType: \"S\" },\n { AttributeName: \"GSI1PK\", AttributeType: \"S\" },\n { AttributeName: \"GSI1SK\", AttributeType: \"S\" },\n { AttributeName: \"GSI2PK\", AttributeType: \"S\" },\n { AttributeName: \"GSI2SK\", AttributeType: \"S\" },\n ],\n GlobalSecondaryIndexes: [\n {\n IndexName: schema.correlationIndexName,\n KeySchema: [\n { AttributeName: \"GSI1PK\", KeyType: \"HASH\" },\n { AttributeName: \"GSI1SK\", KeyType: \"RANGE\" },\n ],\n Projection: { ProjectionType: \"ALL\" },\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n },\n {\n IndexName: schema.cleanupIndexName,\n KeySchema: [\n { AttributeName: \"GSI2PK\", KeyType: \"HASH\" },\n { AttributeName: \"GSI2SK\", KeyType: \"RANGE\" },\n ],\n Projection: { ProjectionType: \"KEYS_ONLY\" },\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n },\n ],\n BillingMode: \"PROVISIONED\",\n ProvisionedThroughput: {\n ReadCapacityUnits: 5,\n WriteCapacityUnits: 5,\n },\n });\n\n await client.send(command);\n\n // Wait for table to be active\n await waitForTableActive(client, schema.tableName);\n}\n\n/**\n * Delete the saga instances table.\n */\nexport async function deleteTable(\n client: DynamoDBClient,\n tableName: string\n): Promise<void> {\n await client.send(new DeleteTableCommand({ TableName: tableName }));\n}\n\n/**\n * Wait for table to be active.\n */\nasync function waitForTableActive(\n client: DynamoDBClient,\n tableName: string\n): Promise<void> {\n for (let i = 0; i < 30; i++) {\n const response = await client.send(\n new DescribeTableCommand({ TableName: tableName })\n );\n\n if (response.Table?.TableStatus === \"ACTIVE\") {\n return;\n }\n\n await new Promise((resolve) => setTimeout(resolve, 1000));\n }\n\n throw new Error(`Table ${tableName} did not become active`);\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saga-bus/store-dynamodb",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DynamoDB saga store for saga-bus",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/deanforan/saga-bus.git",
|
|
26
|
+
"directory": "packages/store-dynamodb"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"saga",
|
|
30
|
+
"message-bus",
|
|
31
|
+
"store",
|
|
32
|
+
"dynamodb",
|
|
33
|
+
"aws"
|
|
34
|
+
],
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@aws-sdk/client-dynamodb": "^3.600.0",
|
|
37
|
+
"@aws-sdk/lib-dynamodb": "^3.600.0",
|
|
38
|
+
"@saga-bus/core": "0.1.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@testcontainers/localstack": "^10.0.0",
|
|
42
|
+
"tsup": "^8.0.0",
|
|
43
|
+
"typescript": "^5.9.2",
|
|
44
|
+
"vitest": "^3.0.0",
|
|
45
|
+
"@repo/eslint-config": "0.0.0",
|
|
46
|
+
"@repo/typescript-config": "0.0.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"@aws-sdk/client-dynamodb": ">=3.0.0",
|
|
50
|
+
"@aws-sdk/lib-dynamodb": ">=3.0.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsup",
|
|
54
|
+
"dev": "tsup --watch",
|
|
55
|
+
"lint": "eslint src/",
|
|
56
|
+
"check-types": "tsc --noEmit",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:watch": "vitest"
|
|
59
|
+
}
|
|
60
|
+
}
|