@omega-flow/store-aws 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) omega-flow contributors
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.
@@ -0,0 +1,125 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { WorkflowStore, WorkflowMemory, WorkflowScheduler } from '@omega-flow/engine';
3
+ import { Workflow, Context, Event } from '@omega-flow/types';
4
+ import { SchedulerClient } from '@aws-sdk/client-scheduler';
5
+
6
+ interface DynamoDBWorkflowStoreConfig {
7
+ client: DynamoDBClient;
8
+ tableName: string;
9
+ }
10
+ /**
11
+ * DynamoDB-backed implementation of WorkflowStore.
12
+ *
13
+ * Table layout (dedicated workflows table):
14
+ * domain (pk) = tenant identifier (e.g. organization id, company id)
15
+ * workflowId (sk) = workflow id
16
+ * data = full Workflow JSON
17
+ * createdAt, updatedAt = epoch ms
18
+ */
19
+ declare class DynamoDBWorkflowStore implements WorkflowStore {
20
+ private docClient;
21
+ private tableName;
22
+ constructor(config: DynamoDBWorkflowStoreConfig);
23
+ getWorkflow(domain: string, workflowId: string): Promise<Workflow | null>;
24
+ getAllWorkflows(domain: string): Promise<Workflow[]>;
25
+ setWorkflow(domain: string, workflow: Workflow): Promise<void>;
26
+ createWorkflow(domain: string, workflowData: Omit<Workflow, "id">): Promise<Workflow>;
27
+ deleteWorkflow(domain: string, workflowId: string): Promise<boolean>;
28
+ }
29
+
30
+ interface DynamoDBWorkflowMemoryConfig {
31
+ client: DynamoDBClient;
32
+ tableName: string;
33
+ /**
34
+ * Name of the GSI with `domain` as partition key and `subjectId` as sort key.
35
+ * Required for `getAllContexts` and `getAllContextsForSubject`.
36
+ * Defaults to `domain-subjectId-index`.
37
+ */
38
+ gsiName?: string;
39
+ }
40
+ /**
41
+ * DynamoDB-backed implementation of WorkflowMemory.
42
+ *
43
+ * Table layout (dedicated contexts table):
44
+ * contextKey (pk) = `${domain}#${workflowId}#${subjectId}`
45
+ * instanceId (sk) = workflow instance id
46
+ * domain = tenant identifier (denormalised for GSI)
47
+ * subjectId = subject identifier (denormalised for GSI)
48
+ * data = full Context JSON
49
+ * isCompleted, startedAt = mirrored from Context for filtering/sorting
50
+ * updatedAt = epoch ms, set on every save
51
+ *
52
+ * GSI (domain-subjectId-index):
53
+ * domain (pk), subjectId (sk) — enables getAllContexts and getAllContextsForSubject queries.
54
+ *
55
+ * Assumes domain / workflowId / subjectId do not contain '#'.
56
+ */
57
+ declare class DynamoDBWorkflowMemory implements WorkflowMemory {
58
+ private docClient;
59
+ private tableName;
60
+ private gsiName;
61
+ constructor(config: DynamoDBWorkflowMemoryConfig);
62
+ private buildContextKey;
63
+ getContexts(domain: string, workflowId: string, subjectId: string): Promise<Context[]>;
64
+ saveContext(domain: string, workflowId: string, subjectId: string, context: Context): Promise<void>;
65
+ deleteContext(domain: string, workflowId: string, subjectId: string, instanceId: string): Promise<void>;
66
+ /**
67
+ * Fetch a single Context by instance id.
68
+ */
69
+ getContext(domain: string, workflowId: string, subjectId: string, instanceId: string): Promise<Context | null>;
70
+ /**
71
+ * List all contexts for a given subject across all workflows in a domain.
72
+ * Uses the `domain-subjectId-index` GSI.
73
+ */
74
+ getAllContextsForSubject(domain: string, subjectId: string): Promise<Context[]>;
75
+ /**
76
+ * List all contexts across all subjects and workflows in a domain.
77
+ * Uses the `domain-subjectId-index` GSI.
78
+ * Returns contexts annotated with `subjectId`.
79
+ */
80
+ getAllContexts(domain: string): Promise<Array<Context & {
81
+ subjectId: string;
82
+ }>>;
83
+ }
84
+
85
+ interface EventBusWorkflowSchedulerConfig {
86
+ client: SchedulerClient;
87
+ /** ARN of the EventBridge bus that receives the scheduled event. */
88
+ eventBusArn: string;
89
+ /** IAM role assumed by EventBridge Scheduler to invoke the bus target. */
90
+ roleArn: string;
91
+ /** Schedule group name. Defaults to `default`. */
92
+ scheduleGroupName?: string;
93
+ /** EventBridge `Source` for the published event. Defaults to `omega-flow`. */
94
+ source?: string;
95
+ /** EventBridge `DetailType` for the published event. Defaults to `workflow.event`. */
96
+ detailType?: string;
97
+ }
98
+ /**
99
+ * EventBridge Scheduler-backed implementation of WorkflowScheduler.
100
+ *
101
+ * Creates one-time schedules that publish the workflow event to an EventBridge
102
+ * bus when they fire. A downstream consumer (e.g. a Lambda subscribed to the
103
+ * bus) is expected to deserialize the event and call `WorkflowManager.processEvent`.
104
+ *
105
+ * Schedules use `ActionAfterCompletion: DELETE` so AWS removes them once fired.
106
+ */
107
+ declare class EventBusWorkflowScheduler implements WorkflowScheduler {
108
+ private client;
109
+ private eventBusArn;
110
+ private roleArn;
111
+ private scheduleGroupName;
112
+ private source;
113
+ private detailType;
114
+ constructor(config: EventBusWorkflowSchedulerConfig);
115
+ schedule(event: Event, delayMs: number): Promise<string>;
116
+ cancel(scheduleId: string): Promise<boolean>;
117
+ }
118
+
119
+ declare class WorkflowAlreadyExistsError extends Error {
120
+ readonly domain: string;
121
+ readonly workflowId: string;
122
+ constructor(domain: string, workflowId: string);
123
+ }
124
+
125
+ export { DynamoDBWorkflowMemory, type DynamoDBWorkflowMemoryConfig, DynamoDBWorkflowStore, type DynamoDBWorkflowStoreConfig, EventBusWorkflowScheduler, type EventBusWorkflowSchedulerConfig, WorkflowAlreadyExistsError };
@@ -0,0 +1,125 @@
1
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
2
+ import { WorkflowStore, WorkflowMemory, WorkflowScheduler } from '@omega-flow/engine';
3
+ import { Workflow, Context, Event } from '@omega-flow/types';
4
+ import { SchedulerClient } from '@aws-sdk/client-scheduler';
5
+
6
+ interface DynamoDBWorkflowStoreConfig {
7
+ client: DynamoDBClient;
8
+ tableName: string;
9
+ }
10
+ /**
11
+ * DynamoDB-backed implementation of WorkflowStore.
12
+ *
13
+ * Table layout (dedicated workflows table):
14
+ * domain (pk) = tenant identifier (e.g. organization id, company id)
15
+ * workflowId (sk) = workflow id
16
+ * data = full Workflow JSON
17
+ * createdAt, updatedAt = epoch ms
18
+ */
19
+ declare class DynamoDBWorkflowStore implements WorkflowStore {
20
+ private docClient;
21
+ private tableName;
22
+ constructor(config: DynamoDBWorkflowStoreConfig);
23
+ getWorkflow(domain: string, workflowId: string): Promise<Workflow | null>;
24
+ getAllWorkflows(domain: string): Promise<Workflow[]>;
25
+ setWorkflow(domain: string, workflow: Workflow): Promise<void>;
26
+ createWorkflow(domain: string, workflowData: Omit<Workflow, "id">): Promise<Workflow>;
27
+ deleteWorkflow(domain: string, workflowId: string): Promise<boolean>;
28
+ }
29
+
30
+ interface DynamoDBWorkflowMemoryConfig {
31
+ client: DynamoDBClient;
32
+ tableName: string;
33
+ /**
34
+ * Name of the GSI with `domain` as partition key and `subjectId` as sort key.
35
+ * Required for `getAllContexts` and `getAllContextsForSubject`.
36
+ * Defaults to `domain-subjectId-index`.
37
+ */
38
+ gsiName?: string;
39
+ }
40
+ /**
41
+ * DynamoDB-backed implementation of WorkflowMemory.
42
+ *
43
+ * Table layout (dedicated contexts table):
44
+ * contextKey (pk) = `${domain}#${workflowId}#${subjectId}`
45
+ * instanceId (sk) = workflow instance id
46
+ * domain = tenant identifier (denormalised for GSI)
47
+ * subjectId = subject identifier (denormalised for GSI)
48
+ * data = full Context JSON
49
+ * isCompleted, startedAt = mirrored from Context for filtering/sorting
50
+ * updatedAt = epoch ms, set on every save
51
+ *
52
+ * GSI (domain-subjectId-index):
53
+ * domain (pk), subjectId (sk) — enables getAllContexts and getAllContextsForSubject queries.
54
+ *
55
+ * Assumes domain / workflowId / subjectId do not contain '#'.
56
+ */
57
+ declare class DynamoDBWorkflowMemory implements WorkflowMemory {
58
+ private docClient;
59
+ private tableName;
60
+ private gsiName;
61
+ constructor(config: DynamoDBWorkflowMemoryConfig);
62
+ private buildContextKey;
63
+ getContexts(domain: string, workflowId: string, subjectId: string): Promise<Context[]>;
64
+ saveContext(domain: string, workflowId: string, subjectId: string, context: Context): Promise<void>;
65
+ deleteContext(domain: string, workflowId: string, subjectId: string, instanceId: string): Promise<void>;
66
+ /**
67
+ * Fetch a single Context by instance id.
68
+ */
69
+ getContext(domain: string, workflowId: string, subjectId: string, instanceId: string): Promise<Context | null>;
70
+ /**
71
+ * List all contexts for a given subject across all workflows in a domain.
72
+ * Uses the `domain-subjectId-index` GSI.
73
+ */
74
+ getAllContextsForSubject(domain: string, subjectId: string): Promise<Context[]>;
75
+ /**
76
+ * List all contexts across all subjects and workflows in a domain.
77
+ * Uses the `domain-subjectId-index` GSI.
78
+ * Returns contexts annotated with `subjectId`.
79
+ */
80
+ getAllContexts(domain: string): Promise<Array<Context & {
81
+ subjectId: string;
82
+ }>>;
83
+ }
84
+
85
+ interface EventBusWorkflowSchedulerConfig {
86
+ client: SchedulerClient;
87
+ /** ARN of the EventBridge bus that receives the scheduled event. */
88
+ eventBusArn: string;
89
+ /** IAM role assumed by EventBridge Scheduler to invoke the bus target. */
90
+ roleArn: string;
91
+ /** Schedule group name. Defaults to `default`. */
92
+ scheduleGroupName?: string;
93
+ /** EventBridge `Source` for the published event. Defaults to `omega-flow`. */
94
+ source?: string;
95
+ /** EventBridge `DetailType` for the published event. Defaults to `workflow.event`. */
96
+ detailType?: string;
97
+ }
98
+ /**
99
+ * EventBridge Scheduler-backed implementation of WorkflowScheduler.
100
+ *
101
+ * Creates one-time schedules that publish the workflow event to an EventBridge
102
+ * bus when they fire. A downstream consumer (e.g. a Lambda subscribed to the
103
+ * bus) is expected to deserialize the event and call `WorkflowManager.processEvent`.
104
+ *
105
+ * Schedules use `ActionAfterCompletion: DELETE` so AWS removes them once fired.
106
+ */
107
+ declare class EventBusWorkflowScheduler implements WorkflowScheduler {
108
+ private client;
109
+ private eventBusArn;
110
+ private roleArn;
111
+ private scheduleGroupName;
112
+ private source;
113
+ private detailType;
114
+ constructor(config: EventBusWorkflowSchedulerConfig);
115
+ schedule(event: Event, delayMs: number): Promise<string>;
116
+ cancel(scheduleId: string): Promise<boolean>;
117
+ }
118
+
119
+ declare class WorkflowAlreadyExistsError extends Error {
120
+ readonly domain: string;
121
+ readonly workflowId: string;
122
+ constructor(domain: string, workflowId: string);
123
+ }
124
+
125
+ export { DynamoDBWorkflowMemory, type DynamoDBWorkflowMemoryConfig, DynamoDBWorkflowStore, type DynamoDBWorkflowStoreConfig, EventBusWorkflowScheduler, type EventBusWorkflowSchedulerConfig, WorkflowAlreadyExistsError };
package/dist/index.js ADDED
@@ -0,0 +1,329 @@
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
+ DynamoDBWorkflowMemory: () => DynamoDBWorkflowMemory,
24
+ DynamoDBWorkflowStore: () => DynamoDBWorkflowStore,
25
+ EventBusWorkflowScheduler: () => EventBusWorkflowScheduler,
26
+ WorkflowAlreadyExistsError: () => WorkflowAlreadyExistsError
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/stores/DynamoDBWorkflowStore.ts
31
+ var import_client_dynamodb = require("@aws-sdk/client-dynamodb");
32
+ var import_lib_dynamodb = require("@aws-sdk/lib-dynamodb");
33
+ var import_nanoid = require("nanoid");
34
+
35
+ // src/errors.ts
36
+ var WorkflowAlreadyExistsError = class extends Error {
37
+ constructor(domain, workflowId) {
38
+ super(
39
+ `Workflow already exists: domain=${domain} workflowId=${workflowId}`
40
+ );
41
+ this.domain = domain;
42
+ this.workflowId = workflowId;
43
+ this.name = "WorkflowAlreadyExistsError";
44
+ }
45
+ };
46
+
47
+ // src/stores/DynamoDBWorkflowStore.ts
48
+ var DynamoDBWorkflowStore = class {
49
+ constructor(config) {
50
+ this.docClient = import_lib_dynamodb.DynamoDBDocumentClient.from(config.client);
51
+ this.tableName = config.tableName;
52
+ }
53
+ async getWorkflow(domain, workflowId) {
54
+ const result = await this.docClient.send(
55
+ new import_lib_dynamodb.GetCommand({
56
+ TableName: this.tableName,
57
+ Key: { domain, workflowId }
58
+ })
59
+ );
60
+ if (!result.Item) {
61
+ return null;
62
+ }
63
+ return result.Item.data;
64
+ }
65
+ async getAllWorkflows(domain) {
66
+ const workflows = [];
67
+ let lastKey;
68
+ do {
69
+ const result = await this.docClient.send(
70
+ new import_lib_dynamodb.QueryCommand({
71
+ TableName: this.tableName,
72
+ KeyConditionExpression: "#domain = :domain",
73
+ ExpressionAttributeNames: { "#domain": "domain" },
74
+ ExpressionAttributeValues: { ":domain": domain },
75
+ ExclusiveStartKey: lastKey
76
+ })
77
+ );
78
+ for (const item of result.Items ?? []) {
79
+ workflows.push(item.data);
80
+ }
81
+ lastKey = result.LastEvaluatedKey;
82
+ } while (lastKey);
83
+ return workflows;
84
+ }
85
+ async setWorkflow(domain, workflow) {
86
+ const now = Date.now();
87
+ await this.docClient.send(
88
+ new import_lib_dynamodb.UpdateCommand({
89
+ TableName: this.tableName,
90
+ Key: { domain, workflowId: workflow.id },
91
+ UpdateExpression: "SET #data = :data, createdAt = if_not_exists(createdAt, :now), updatedAt = :now",
92
+ ExpressionAttributeNames: { "#data": "data" },
93
+ ExpressionAttributeValues: { ":data": workflow, ":now": now }
94
+ })
95
+ );
96
+ }
97
+ async createWorkflow(domain, workflowData) {
98
+ const id = (0, import_nanoid.nanoid)(8);
99
+ const workflow = { ...workflowData, id };
100
+ const now = Date.now();
101
+ const item = {
102
+ domain,
103
+ workflowId: id,
104
+ data: workflow,
105
+ createdAt: now,
106
+ updatedAt: now
107
+ };
108
+ try {
109
+ await this.docClient.send(
110
+ new import_lib_dynamodb.PutCommand({
111
+ TableName: this.tableName,
112
+ Item: item,
113
+ ConditionExpression: "attribute_not_exists(#domain)",
114
+ ExpressionAttributeNames: { "#domain": "domain" }
115
+ })
116
+ );
117
+ } catch (err) {
118
+ if (err instanceof import_client_dynamodb.ConditionalCheckFailedException) {
119
+ throw new WorkflowAlreadyExistsError(domain, id);
120
+ }
121
+ throw err;
122
+ }
123
+ return workflow;
124
+ }
125
+ async deleteWorkflow(domain, workflowId) {
126
+ const result = await this.docClient.send(
127
+ new import_lib_dynamodb.DeleteCommand({
128
+ TableName: this.tableName,
129
+ Key: { domain, workflowId },
130
+ ReturnValues: "ALL_OLD"
131
+ })
132
+ );
133
+ return result.Attributes !== void 0;
134
+ }
135
+ };
136
+
137
+ // src/memories/DynamoDBWorkflowMemory.ts
138
+ var import_lib_dynamodb2 = require("@aws-sdk/lib-dynamodb");
139
+ var DynamoDBWorkflowMemory = class {
140
+ constructor(config) {
141
+ this.docClient = import_lib_dynamodb2.DynamoDBDocumentClient.from(config.client);
142
+ this.tableName = config.tableName;
143
+ this.gsiName = config.gsiName ?? "domain-subjectId-index";
144
+ }
145
+ buildContextKey(domain, workflowId, subjectId) {
146
+ return `${domain}#${workflowId}#${subjectId}`;
147
+ }
148
+ async getContexts(domain, workflowId, subjectId) {
149
+ const contextKey = this.buildContextKey(domain, workflowId, subjectId);
150
+ const contexts = [];
151
+ let lastKey;
152
+ do {
153
+ const result = await this.docClient.send(
154
+ new import_lib_dynamodb2.QueryCommand({
155
+ TableName: this.tableName,
156
+ KeyConditionExpression: "contextKey = :contextKey",
157
+ ExpressionAttributeValues: { ":contextKey": contextKey },
158
+ ExclusiveStartKey: lastKey
159
+ })
160
+ );
161
+ for (const item of result.Items ?? []) {
162
+ contexts.push(item.data);
163
+ }
164
+ lastKey = result.LastEvaluatedKey;
165
+ } while (lastKey);
166
+ return contexts;
167
+ }
168
+ async saveContext(domain, workflowId, subjectId, context) {
169
+ const item = {
170
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
171
+ instanceId: context.instanceId,
172
+ domain,
173
+ subjectId,
174
+ data: context,
175
+ isCompleted: !!context.isCompleted,
176
+ startedAt: context.startedAt,
177
+ updatedAt: Date.now()
178
+ };
179
+ await this.docClient.send(
180
+ new import_lib_dynamodb2.PutCommand({
181
+ TableName: this.tableName,
182
+ Item: item
183
+ })
184
+ );
185
+ }
186
+ async deleteContext(domain, workflowId, subjectId, instanceId) {
187
+ await this.docClient.send(
188
+ new import_lib_dynamodb2.DeleteCommand({
189
+ TableName: this.tableName,
190
+ Key: {
191
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
192
+ instanceId
193
+ }
194
+ })
195
+ );
196
+ }
197
+ /**
198
+ * Fetch a single Context by instance id.
199
+ */
200
+ async getContext(domain, workflowId, subjectId, instanceId) {
201
+ const result = await this.docClient.send(
202
+ new import_lib_dynamodb2.GetCommand({
203
+ TableName: this.tableName,
204
+ Key: {
205
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
206
+ instanceId
207
+ }
208
+ })
209
+ );
210
+ if (!result.Item) {
211
+ return null;
212
+ }
213
+ return result.Item.data;
214
+ }
215
+ /**
216
+ * List all contexts for a given subject across all workflows in a domain.
217
+ * Uses the `domain-subjectId-index` GSI.
218
+ */
219
+ async getAllContextsForSubject(domain, subjectId) {
220
+ const contexts = [];
221
+ let lastKey;
222
+ do {
223
+ const result = await this.docClient.send(
224
+ new import_lib_dynamodb2.QueryCommand({
225
+ TableName: this.tableName,
226
+ IndexName: this.gsiName,
227
+ KeyConditionExpression: "#domain = :domain AND subjectId = :subjectId",
228
+ ExpressionAttributeNames: { "#domain": "domain" },
229
+ ExpressionAttributeValues: { ":domain": domain, ":subjectId": subjectId },
230
+ ExclusiveStartKey: lastKey
231
+ })
232
+ );
233
+ for (const item of result.Items ?? []) {
234
+ contexts.push(item.data);
235
+ }
236
+ lastKey = result.LastEvaluatedKey;
237
+ } while (lastKey);
238
+ return contexts;
239
+ }
240
+ /**
241
+ * List all contexts across all subjects and workflows in a domain.
242
+ * Uses the `domain-subjectId-index` GSI.
243
+ * Returns contexts annotated with `subjectId`.
244
+ */
245
+ async getAllContexts(domain) {
246
+ const contexts = [];
247
+ let lastKey;
248
+ do {
249
+ const result = await this.docClient.send(
250
+ new import_lib_dynamodb2.QueryCommand({
251
+ TableName: this.tableName,
252
+ IndexName: this.gsiName,
253
+ KeyConditionExpression: "#domain = :domain",
254
+ ExpressionAttributeNames: { "#domain": "domain" },
255
+ ExpressionAttributeValues: { ":domain": domain },
256
+ ExclusiveStartKey: lastKey
257
+ })
258
+ );
259
+ for (const item of result.Items ?? []) {
260
+ const ci = item;
261
+ contexts.push({ ...ci.data, subjectId: ci.subjectId });
262
+ }
263
+ lastKey = result.LastEvaluatedKey;
264
+ } while (lastKey);
265
+ return contexts;
266
+ }
267
+ };
268
+
269
+ // src/schedulers/EventBusWorkflowScheduler.ts
270
+ var import_client_scheduler = require("@aws-sdk/client-scheduler");
271
+ var import_nanoid2 = require("nanoid");
272
+ var EventBusWorkflowScheduler = class {
273
+ constructor(config) {
274
+ this.client = config.client;
275
+ this.eventBusArn = config.eventBusArn;
276
+ this.roleArn = config.roleArn;
277
+ this.scheduleGroupName = config.scheduleGroupName ?? "default";
278
+ this.source = config.source ?? "omega-flow";
279
+ this.detailType = config.detailType ?? "workflow.event";
280
+ }
281
+ async schedule(event, delayMs) {
282
+ const fireAt = new Date(Date.now() + delayMs);
283
+ const scheduleExpression = `at(${fireAt.toISOString().slice(0, 19)})`;
284
+ const name = `omf-${(0, import_nanoid2.nanoid)(16)}`;
285
+ await this.client.send(
286
+ new import_client_scheduler.CreateScheduleCommand({
287
+ Name: name,
288
+ GroupName: this.scheduleGroupName,
289
+ ScheduleExpression: scheduleExpression,
290
+ ScheduleExpressionTimezone: "UTC",
291
+ FlexibleTimeWindow: { Mode: "OFF" },
292
+ ActionAfterCompletion: "DELETE",
293
+ Target: {
294
+ Arn: this.eventBusArn,
295
+ RoleArn: this.roleArn,
296
+ Input: JSON.stringify(event),
297
+ EventBridgeParameters: {
298
+ Source: this.source,
299
+ DetailType: this.detailType
300
+ }
301
+ }
302
+ })
303
+ );
304
+ return name;
305
+ }
306
+ async cancel(scheduleId) {
307
+ try {
308
+ await this.client.send(
309
+ new import_client_scheduler.DeleteScheduleCommand({
310
+ Name: scheduleId,
311
+ GroupName: this.scheduleGroupName
312
+ })
313
+ );
314
+ return true;
315
+ } catch (err) {
316
+ if (err instanceof import_client_scheduler.ResourceNotFoundException) {
317
+ return false;
318
+ }
319
+ throw err;
320
+ }
321
+ }
322
+ };
323
+ // Annotate the CommonJS export names for ESM import in node:
324
+ 0 && (module.exports = {
325
+ DynamoDBWorkflowMemory,
326
+ DynamoDBWorkflowStore,
327
+ EventBusWorkflowScheduler,
328
+ WorkflowAlreadyExistsError
329
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,318 @@
1
+ // src/stores/DynamoDBWorkflowStore.ts
2
+ import {
3
+ ConditionalCheckFailedException
4
+ } from "@aws-sdk/client-dynamodb";
5
+ import {
6
+ DeleteCommand,
7
+ DynamoDBDocumentClient,
8
+ GetCommand,
9
+ PutCommand,
10
+ QueryCommand,
11
+ UpdateCommand
12
+ } from "@aws-sdk/lib-dynamodb";
13
+ import { nanoid } from "nanoid";
14
+
15
+ // src/errors.ts
16
+ var WorkflowAlreadyExistsError = class extends Error {
17
+ constructor(domain, workflowId) {
18
+ super(
19
+ `Workflow already exists: domain=${domain} workflowId=${workflowId}`
20
+ );
21
+ this.domain = domain;
22
+ this.workflowId = workflowId;
23
+ this.name = "WorkflowAlreadyExistsError";
24
+ }
25
+ };
26
+
27
+ // src/stores/DynamoDBWorkflowStore.ts
28
+ var DynamoDBWorkflowStore = class {
29
+ constructor(config) {
30
+ this.docClient = DynamoDBDocumentClient.from(config.client);
31
+ this.tableName = config.tableName;
32
+ }
33
+ async getWorkflow(domain, workflowId) {
34
+ const result = await this.docClient.send(
35
+ new GetCommand({
36
+ TableName: this.tableName,
37
+ Key: { domain, workflowId }
38
+ })
39
+ );
40
+ if (!result.Item) {
41
+ return null;
42
+ }
43
+ return result.Item.data;
44
+ }
45
+ async getAllWorkflows(domain) {
46
+ const workflows = [];
47
+ let lastKey;
48
+ do {
49
+ const result = await this.docClient.send(
50
+ new QueryCommand({
51
+ TableName: this.tableName,
52
+ KeyConditionExpression: "#domain = :domain",
53
+ ExpressionAttributeNames: { "#domain": "domain" },
54
+ ExpressionAttributeValues: { ":domain": domain },
55
+ ExclusiveStartKey: lastKey
56
+ })
57
+ );
58
+ for (const item of result.Items ?? []) {
59
+ workflows.push(item.data);
60
+ }
61
+ lastKey = result.LastEvaluatedKey;
62
+ } while (lastKey);
63
+ return workflows;
64
+ }
65
+ async setWorkflow(domain, workflow) {
66
+ const now = Date.now();
67
+ await this.docClient.send(
68
+ new UpdateCommand({
69
+ TableName: this.tableName,
70
+ Key: { domain, workflowId: workflow.id },
71
+ UpdateExpression: "SET #data = :data, createdAt = if_not_exists(createdAt, :now), updatedAt = :now",
72
+ ExpressionAttributeNames: { "#data": "data" },
73
+ ExpressionAttributeValues: { ":data": workflow, ":now": now }
74
+ })
75
+ );
76
+ }
77
+ async createWorkflow(domain, workflowData) {
78
+ const id = nanoid(8);
79
+ const workflow = { ...workflowData, id };
80
+ const now = Date.now();
81
+ const item = {
82
+ domain,
83
+ workflowId: id,
84
+ data: workflow,
85
+ createdAt: now,
86
+ updatedAt: now
87
+ };
88
+ try {
89
+ await this.docClient.send(
90
+ new PutCommand({
91
+ TableName: this.tableName,
92
+ Item: item,
93
+ ConditionExpression: "attribute_not_exists(#domain)",
94
+ ExpressionAttributeNames: { "#domain": "domain" }
95
+ })
96
+ );
97
+ } catch (err) {
98
+ if (err instanceof ConditionalCheckFailedException) {
99
+ throw new WorkflowAlreadyExistsError(domain, id);
100
+ }
101
+ throw err;
102
+ }
103
+ return workflow;
104
+ }
105
+ async deleteWorkflow(domain, workflowId) {
106
+ const result = await this.docClient.send(
107
+ new DeleteCommand({
108
+ TableName: this.tableName,
109
+ Key: { domain, workflowId },
110
+ ReturnValues: "ALL_OLD"
111
+ })
112
+ );
113
+ return result.Attributes !== void 0;
114
+ }
115
+ };
116
+
117
+ // src/memories/DynamoDBWorkflowMemory.ts
118
+ import {
119
+ DeleteCommand as DeleteCommand2,
120
+ DynamoDBDocumentClient as DynamoDBDocumentClient2,
121
+ GetCommand as GetCommand2,
122
+ PutCommand as PutCommand2,
123
+ QueryCommand as QueryCommand2
124
+ } from "@aws-sdk/lib-dynamodb";
125
+ var DynamoDBWorkflowMemory = class {
126
+ constructor(config) {
127
+ this.docClient = DynamoDBDocumentClient2.from(config.client);
128
+ this.tableName = config.tableName;
129
+ this.gsiName = config.gsiName ?? "domain-subjectId-index";
130
+ }
131
+ buildContextKey(domain, workflowId, subjectId) {
132
+ return `${domain}#${workflowId}#${subjectId}`;
133
+ }
134
+ async getContexts(domain, workflowId, subjectId) {
135
+ const contextKey = this.buildContextKey(domain, workflowId, subjectId);
136
+ const contexts = [];
137
+ let lastKey;
138
+ do {
139
+ const result = await this.docClient.send(
140
+ new QueryCommand2({
141
+ TableName: this.tableName,
142
+ KeyConditionExpression: "contextKey = :contextKey",
143
+ ExpressionAttributeValues: { ":contextKey": contextKey },
144
+ ExclusiveStartKey: lastKey
145
+ })
146
+ );
147
+ for (const item of result.Items ?? []) {
148
+ contexts.push(item.data);
149
+ }
150
+ lastKey = result.LastEvaluatedKey;
151
+ } while (lastKey);
152
+ return contexts;
153
+ }
154
+ async saveContext(domain, workflowId, subjectId, context) {
155
+ const item = {
156
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
157
+ instanceId: context.instanceId,
158
+ domain,
159
+ subjectId,
160
+ data: context,
161
+ isCompleted: !!context.isCompleted,
162
+ startedAt: context.startedAt,
163
+ updatedAt: Date.now()
164
+ };
165
+ await this.docClient.send(
166
+ new PutCommand2({
167
+ TableName: this.tableName,
168
+ Item: item
169
+ })
170
+ );
171
+ }
172
+ async deleteContext(domain, workflowId, subjectId, instanceId) {
173
+ await this.docClient.send(
174
+ new DeleteCommand2({
175
+ TableName: this.tableName,
176
+ Key: {
177
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
178
+ instanceId
179
+ }
180
+ })
181
+ );
182
+ }
183
+ /**
184
+ * Fetch a single Context by instance id.
185
+ */
186
+ async getContext(domain, workflowId, subjectId, instanceId) {
187
+ const result = await this.docClient.send(
188
+ new GetCommand2({
189
+ TableName: this.tableName,
190
+ Key: {
191
+ contextKey: this.buildContextKey(domain, workflowId, subjectId),
192
+ instanceId
193
+ }
194
+ })
195
+ );
196
+ if (!result.Item) {
197
+ return null;
198
+ }
199
+ return result.Item.data;
200
+ }
201
+ /**
202
+ * List all contexts for a given subject across all workflows in a domain.
203
+ * Uses the `domain-subjectId-index` GSI.
204
+ */
205
+ async getAllContextsForSubject(domain, subjectId) {
206
+ const contexts = [];
207
+ let lastKey;
208
+ do {
209
+ const result = await this.docClient.send(
210
+ new QueryCommand2({
211
+ TableName: this.tableName,
212
+ IndexName: this.gsiName,
213
+ KeyConditionExpression: "#domain = :domain AND subjectId = :subjectId",
214
+ ExpressionAttributeNames: { "#domain": "domain" },
215
+ ExpressionAttributeValues: { ":domain": domain, ":subjectId": subjectId },
216
+ ExclusiveStartKey: lastKey
217
+ })
218
+ );
219
+ for (const item of result.Items ?? []) {
220
+ contexts.push(item.data);
221
+ }
222
+ lastKey = result.LastEvaluatedKey;
223
+ } while (lastKey);
224
+ return contexts;
225
+ }
226
+ /**
227
+ * List all contexts across all subjects and workflows in a domain.
228
+ * Uses the `domain-subjectId-index` GSI.
229
+ * Returns contexts annotated with `subjectId`.
230
+ */
231
+ async getAllContexts(domain) {
232
+ const contexts = [];
233
+ let lastKey;
234
+ do {
235
+ const result = await this.docClient.send(
236
+ new QueryCommand2({
237
+ TableName: this.tableName,
238
+ IndexName: this.gsiName,
239
+ KeyConditionExpression: "#domain = :domain",
240
+ ExpressionAttributeNames: { "#domain": "domain" },
241
+ ExpressionAttributeValues: { ":domain": domain },
242
+ ExclusiveStartKey: lastKey
243
+ })
244
+ );
245
+ for (const item of result.Items ?? []) {
246
+ const ci = item;
247
+ contexts.push({ ...ci.data, subjectId: ci.subjectId });
248
+ }
249
+ lastKey = result.LastEvaluatedKey;
250
+ } while (lastKey);
251
+ return contexts;
252
+ }
253
+ };
254
+
255
+ // src/schedulers/EventBusWorkflowScheduler.ts
256
+ import {
257
+ CreateScheduleCommand,
258
+ DeleteScheduleCommand,
259
+ ResourceNotFoundException
260
+ } from "@aws-sdk/client-scheduler";
261
+ import { nanoid as nanoid2 } from "nanoid";
262
+ var EventBusWorkflowScheduler = class {
263
+ constructor(config) {
264
+ this.client = config.client;
265
+ this.eventBusArn = config.eventBusArn;
266
+ this.roleArn = config.roleArn;
267
+ this.scheduleGroupName = config.scheduleGroupName ?? "default";
268
+ this.source = config.source ?? "omega-flow";
269
+ this.detailType = config.detailType ?? "workflow.event";
270
+ }
271
+ async schedule(event, delayMs) {
272
+ const fireAt = new Date(Date.now() + delayMs);
273
+ const scheduleExpression = `at(${fireAt.toISOString().slice(0, 19)})`;
274
+ const name = `omf-${nanoid2(16)}`;
275
+ await this.client.send(
276
+ new CreateScheduleCommand({
277
+ Name: name,
278
+ GroupName: this.scheduleGroupName,
279
+ ScheduleExpression: scheduleExpression,
280
+ ScheduleExpressionTimezone: "UTC",
281
+ FlexibleTimeWindow: { Mode: "OFF" },
282
+ ActionAfterCompletion: "DELETE",
283
+ Target: {
284
+ Arn: this.eventBusArn,
285
+ RoleArn: this.roleArn,
286
+ Input: JSON.stringify(event),
287
+ EventBridgeParameters: {
288
+ Source: this.source,
289
+ DetailType: this.detailType
290
+ }
291
+ }
292
+ })
293
+ );
294
+ return name;
295
+ }
296
+ async cancel(scheduleId) {
297
+ try {
298
+ await this.client.send(
299
+ new DeleteScheduleCommand({
300
+ Name: scheduleId,
301
+ GroupName: this.scheduleGroupName
302
+ })
303
+ );
304
+ return true;
305
+ } catch (err) {
306
+ if (err instanceof ResourceNotFoundException) {
307
+ return false;
308
+ }
309
+ throw err;
310
+ }
311
+ }
312
+ };
313
+ export {
314
+ DynamoDBWorkflowMemory,
315
+ DynamoDBWorkflowStore,
316
+ EventBusWorkflowScheduler,
317
+ WorkflowAlreadyExistsError
318
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@omega-flow/store-aws",
3
+ "version": "0.1.0",
4
+ "description": "AWS-backed storage and scheduler implementations for Omega Flow",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/omega-flow/omega-flow.git",
12
+ "directory": "packages/store-aws"
13
+ },
14
+ "homepage": "https://omega-flow.github.io/omega-flow/",
15
+ "bugs": "https://github.com/omega-flow/omega-flow/issues",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "devDependencies": {
23
+ "aws-sdk-client-mock": "^4.1.0"
24
+ },
25
+ "dependencies": {
26
+ "@aws-sdk/client-dynamodb": "^3.700.0",
27
+ "@aws-sdk/client-scheduler": "^3.1045.0",
28
+ "@aws-sdk/lib-dynamodb": "^3.700.0",
29
+ "nanoid": "^3.3.7",
30
+ "@omega-flow/types": "0.1.0",
31
+ "@omega-flow/engine": "0.1.0"
32
+ },
33
+ "scripts": {
34
+ "build": "tsup src/index.ts --format cjs,esm --dts",
35
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
36
+ "test": "jest --verbose --passWithNoTests",
37
+ "test:watch": "jest --watchAll --verbose --passWithNoTests --detectOpenHandles",
38
+ "test:coverage": "jest --verbose --coverage"
39
+ }
40
+ }