@brandboostinggmbh/observable-workflows 0.0.2 → 0.2.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/dist/index.d.ts CHANGED
@@ -1,6 +1,301 @@
1
- //#region src/index.d.ts
2
- declare const foo = "foo";
3
- declare function fn(): void;
1
+ //#region src/observableWorkflows/createStepContext.d.ts
2
+ declare function createStepContext(context: StepContextOptions): Promise<(<RESULT>(step: {
3
+ name: string;
4
+ metadata?: Record<string, any>;
5
+ }, callback: (ctx: StepCtx) => Promise<RESULT>) => Promise<RESULT>)>;
4
6
 
5
7
  //#endregion
6
- export { fn, foo };
8
+ //#region src/observableWorkflows/helperFunctions.d.ts
9
+ declare function finalizeWorkflowRecord(options: InternalWorkflowContextOptions, {
10
+ workflowStatus,
11
+ endTime,
12
+ instanceId
13
+ }: {
14
+ workflowStatus: StepWorkflowStatus;
15
+ endTime: number;
16
+ instanceId: string;
17
+ }): Promise<D1Result<Record<string, unknown>>>;
18
+ declare function insertWorkflowRecord(options: InternalWorkflowContextOptions, {
19
+ instanceId,
20
+ workflowType,
21
+ workflowName,
22
+ workflowMetadata,
23
+ input,
24
+ workflowStatus,
25
+ startTime,
26
+ endTime,
27
+ parentInstanceId,
28
+ tenantId
29
+ }: {
30
+ instanceId: string;
31
+ workflowType: string;
32
+ workflowName: string;
33
+ workflowMetadata: any;
34
+ input: any;
35
+ workflowStatus: StepWorkflowStatus;
36
+ startTime: number;
37
+ endTime?: number | null;
38
+ parentInstanceId?: string | null;
39
+ tenantId: string;
40
+ }): Promise<D1Result<Record<string, unknown>>>;
41
+ declare function insertStepRecordFull(context: StepContextOptions, {
42
+ instanceId,
43
+ name,
44
+ status,
45
+ metadata,
46
+ startTime,
47
+ endTime,
48
+ result,
49
+ error
50
+ }: {
51
+ instanceId: string;
52
+ name: string;
53
+ status: StepWorkflowStatus;
54
+ metadata: string;
55
+ startTime: number;
56
+ endTime: number | null;
57
+ result: string | null;
58
+ error: string | null;
59
+ }): Promise<D1Result<Record<string, unknown>>>;
60
+ declare function pushLogToDB(options: InternalWorkflowContextOptions, {
61
+ instanceId,
62
+ stepName,
63
+ message,
64
+ timestamp,
65
+ type,
66
+ logOrder,
67
+ tenantId
68
+ }: {
69
+ instanceId: string;
70
+ stepName: string | null;
71
+ message: string;
72
+ timestamp: number;
73
+ type: string;
74
+ logOrder: number;
75
+ tenantId: string;
76
+ }): Promise<D1Result<Record<string, unknown>>>;
77
+ declare function tryDeserializeObj(obj: string, serializer: Serializer): any;
78
+ /**
79
+ * We want to save steps to a databse, we are using D1, so SQLite is the database.
80
+ * Step Table:
81
+ * - instanceId: string
82
+ * - stepName: string
83
+ * - stepStatus: string (pending, completed, failed)
84
+ * - stepMetadata: string (JSON)
85
+ * - startTime: number (timestamp)
86
+ * - endTime: number (timestamp) (optional)
87
+ * - result: string (JSON) (optional)
88
+ * - error: string (JSON) (optional)
89
+ *
90
+ * Log Table:
91
+ * - instanceId: string
92
+ * - stepName: string
93
+ * - log: string
94
+ * - timestamp: number (timestamp)
95
+ * - type: string (info, error, warning)
96
+ */
97
+ /**
98
+ * Ensure that the required tables exist in D1 (SQLite).
99
+ */
100
+ declare function ensureTables(db: D1Database): Promise<void>;
101
+ declare function workflowTableRowToWorkflowRun(row: {
102
+ instanceId: string;
103
+ workflowType: string;
104
+ workflowName: string;
105
+ input: string;
106
+ tenantId: string;
107
+ workflowStatus: StepWorkflowStatus;
108
+ startTime: number;
109
+ endTime: number | null;
110
+ parentInstanceId: string | null;
111
+ }, serializer: Serializer): WorkflowRun;
112
+ declare function updateWorkflowName(context: {
113
+ D1: D1Database;
114
+ }, instanceId: string, newWorkflowName: string): Promise<D1Result<Record<string, unknown>>>;
115
+ type ValueTypeMap = {
116
+ string: string;
117
+ number: number;
118
+ boolean: boolean;
119
+ object: object;
120
+ };
121
+ type PossibleValueTypeNames = keyof ValueTypeMap;
122
+ type PossibleValueTypes = ValueTypeMap[PossibleValueTypeNames];
123
+ declare function upsertWorkflowProperty<T extends keyof ValueTypeMap>({
124
+ context,
125
+ instanceId,
126
+ key,
127
+ valueType,
128
+ value,
129
+ tenantId
130
+ }: {
131
+ context: InternalWorkflowContextOptions;
132
+ instanceId: string;
133
+ key: string;
134
+ valueType: T;
135
+ value: ValueTypeMap[T];
136
+ tenantId: string;
137
+ }): Promise<boolean>;
138
+
139
+ //#endregion
140
+ //#region src/observableWorkflows/types.d.ts
141
+ type StepContextOptions = {
142
+ D1: D1Database;
143
+ tenantId: string;
144
+ instanceId: string;
145
+ /** If the currect execution is a retry, this field references the original execution. */
146
+ parentInstanceId?: string;
147
+ serializer: Serializer;
148
+ idFactory: () => string;
149
+ };
150
+ type ConsoleWrapper = {
151
+ log: (message: string) => void;
152
+ info: (message: string) => void;
153
+ error: (message: string) => void;
154
+ warn: (message: string) => void;
155
+ };
156
+ type StepCtx = {
157
+ console: ConsoleWrapper;
158
+ };
159
+ type Log = {
160
+ instanceId: string;
161
+ stepName: string | null;
162
+ log: string;
163
+ timestamp: number;
164
+ type: 'info' | 'error' | 'warn';
165
+ };
166
+ type StepWorkflowStatus = 'pending' | 'completed' | 'failed';
167
+ type Step = {
168
+ instanceId: string;
169
+ name: string;
170
+ metadata: Record<string, any>;
171
+ status: StepWorkflowStatus;
172
+ startTime: number;
173
+ endTime: number | null;
174
+ result: any | null;
175
+ error: any | null;
176
+ logs?: Log[];
177
+ tenantId?: string;
178
+ };
179
+ type WorkflowRun = {
180
+ instanceId: string;
181
+ workflowType: string;
182
+ workflowName: string;
183
+ input: any;
184
+ tenantId: string;
185
+ workflowStatus: StepWorkflowStatus;
186
+ startTime: number;
187
+ endTime: number | null;
188
+ parentInstanceId: string | null;
189
+ steps?: Step[];
190
+ /** All Logs associated with the workflow. Includes logs that are part of steps */
191
+ logs?: Log[];
192
+ isRetryOf?: WorkflowRun | null;
193
+ retries?: WorkflowRun[] | null;
194
+ properties?: WorkflowProperty[];
195
+ };
196
+ type WorkflowProperty<T extends keyof ValueTypeMap = keyof ValueTypeMap> = {
197
+ key: string;
198
+ valueType: T;
199
+ value: ValueTypeMap[T];
200
+ };
201
+ type WorkflowPropertyDefinition = {
202
+ key: string;
203
+ valueType: 'string' | 'number' | 'boolean' | 'object';
204
+ };
205
+ type StepContext = Awaited<ReturnType<typeof createStepContext>>;
206
+ type Serializer = {
207
+ serialize: (data: any) => string;
208
+ deserialize: (data: string) => any;
209
+ };
210
+ type WorkflowContextOptions = {
211
+ D1: D1Database;
212
+ idFactory?: () => string;
213
+ serializer?: Serializer;
214
+ };
215
+ type InternalWorkflowContextOptions = WorkflowContextOptions & Required<Pick<WorkflowContextOptions, 'serializer' | 'idFactory'>>;
216
+ type QueueWorkflowContextOptions = {
217
+ QUEUE: Queue<WorkflowQueueMessage>;
218
+ serializer?: Serializer;
219
+ idFactory?: () => string;
220
+ };
221
+ type WorkflowContext = {
222
+ step: StepContext;
223
+ console: ConsoleWrapper;
224
+ setWorkflowName: (newName: string) => Promise<void>;
225
+ setWorkflowProperty: <T extends keyof ValueTypeMap>(key: string, valueType: T, value: ValueTypeMap[T]) => Promise<void>;
226
+ };
227
+ type WorkflowFunction<INPUT, TYPE extends string = string> = {
228
+ workflowType: TYPE;
229
+ metadata: Record<string, any>;
230
+ /**
231
+ * The code that will be executed.
232
+ * Do not call this directly.
233
+ * Use either the WorkflowContect, or the QueueWorkflowContext to run or enqueue this. */
234
+ _callback: (input: INPUT, ctx: WorkflowContext) => Promise<any>;
235
+ };
236
+ type WorkflowQueueMessage = {
237
+ type: 'workflow-run';
238
+ workflowType: string;
239
+ workflowName: string;
240
+ tenantId: string;
241
+ input: any;
242
+ } | {
243
+ type: 'workflow-retry';
244
+ workflowType: string;
245
+ tenantId: string;
246
+ retryInstanceId: string;
247
+ };
248
+
249
+ //#endregion
250
+ //#region src/observableWorkflows/defineWorkflow.d.ts
251
+ declare function defineWorkflow<I extends {} | null>(workflow: {
252
+ workflowType: string;
253
+ metadata?: Record<string, any>;
254
+ }, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
255
+
256
+ //#endregion
257
+ //#region src/observableWorkflows/createWorkflowContext.d.ts
258
+ declare function createWorkflowContext(options: WorkflowContextOptions): Promise<{
259
+ call: <I>({
260
+ workflow,
261
+ input,
262
+ workflowName,
263
+ tenantId,
264
+ parentInstanceId
265
+ }: {
266
+ workflow: WorkflowFunction<I>;
267
+ input: I;
268
+ workflowName: string;
269
+ tenantId: string;
270
+ parentInstanceId?: string | undefined;
271
+ }) => Promise<void>;
272
+ retry: <I>(workflow: WorkflowFunction<I>, retryInstanceId: string) => Promise<void>;
273
+ }>;
274
+
275
+ //#endregion
276
+ //#region src/observableWorkflows/createQueueWorkflowContext.d.ts
277
+ declare function createQueueWorkflowContext(options: QueueWorkflowContextOptions): {
278
+ enqueueWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, input: I, initialName: string) => Promise<void>;
279
+ enqueueRetryWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, oldInstanceId: string) => Promise<void>;
280
+ handleWorkflowQueueMessage: <T>(message: WorkflowQueueMessage, env: {
281
+ LOG_DB: D1Database;
282
+ }, workflowResolver: (workflowType: string) => WorkflowFunction<T> | undefined) => Promise<void>;
283
+ };
284
+
285
+ //#endregion
286
+ //#region src/observableWorkflows/createLogAccessor.d.ts
287
+ declare const createLogAccessor: (context: {
288
+ D1: D1Database;
289
+ tenantId: string;
290
+ serializer: Serializer;
291
+ }) => {
292
+ listSteps: (limit: number, offset: number, instanceId?: string | undefined) => Promise<Step[]>;
293
+ getStep: (instanceId: string, stepName: string) => Promise<Step | null>;
294
+ listWorkflows: (limit: number, offset: number) => Promise<WorkflowRun[]>;
295
+ getWorkflow: (instanceId: string) => Promise<WorkflowRun | null>;
296
+ getWorkflowTypesByTenantId: (tenantId: string) => Promise<string[]>;
297
+ getPropertiesKeys: (instanceId?: string) => Promise<WorkflowPropertyDefinition[]>;
298
+ };
299
+
300
+ //#endregion
301
+ export { ConsoleWrapper, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, QueueWorkflowContextOptions, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, ValueTypeMap, WorkflowContext, WorkflowContextOptions, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, createLogAccessor, createQueueWorkflowContext, createStepContext, createWorkflowContext, defineWorkflow, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, tryDeserializeObj, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
package/dist/index.js CHANGED
@@ -1,8 +1,633 @@
1
- //#region src/index.ts
2
- const foo = "foo";
3
- function fn() {
4
- return;
1
+ //#region src/observableWorkflows/helperFunctions.ts
2
+ function finalizeWorkflowRecord(options, { workflowStatus, endTime, instanceId }) {
3
+ return options.D1.prepare(
4
+ /* sql */
5
+ `UPDATE WorkflowTable
6
+ SET workflowStatus = ?, endTime = ?
7
+ WHERE instanceId = ?`
8
+ ).bind(workflowStatus, endTime, instanceId).run();
5
9
  }
10
+ function insertWorkflowRecord(options, { instanceId, workflowType, workflowName, workflowMetadata, input, workflowStatus, startTime, endTime, parentInstanceId, tenantId }) {
11
+ return options.D1.prepare(
12
+ /* sql */
13
+ `INSERT INTO WorkflowTable
14
+ (instanceId, workflowType, workflowName, workflowMetadata, input, tenantId, workflowStatus, startTime, endTime, parentInstanceId)
15
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
16
+ ).bind(instanceId, workflowType, workflowName, options.serializer.serialize(workflowMetadata), options.serializer.serialize(input), tenantId, workflowStatus, startTime, endTime ?? null, parentInstanceId ?? null).run();
17
+ }
18
+ function insertStepRecordFull(context, { instanceId, name, status, metadata, startTime, endTime, result, error }) {
19
+ return context.D1.prepare(
20
+ /* sql */
21
+ `INSERT INTO StepTable
22
+ (instanceId, stepName, stepStatus, stepMetadata, startTime, endTime, result, error, tenantId)
23
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
24
+ ).bind(instanceId, name, status, metadata, startTime, endTime, result, error, context.tenantId).run();
25
+ }
26
+ function pushLogToDB(options, { instanceId, stepName, message, timestamp, type, logOrder, tenantId }) {
27
+ return options.D1.prepare(
28
+ /* sql */
29
+ `INSERT INTO LogTable
30
+ (instanceId, stepName, log, timestamp, type, logOrder, tenantId)
31
+ VALUES (?, ?, ?, ?, ?, ?, ?)`
32
+ ).bind(instanceId, stepName, message, timestamp, type, logOrder, tenantId).run();
33
+ }
34
+ function tryDeserializeObj(obj, serializer) {
35
+ try {
36
+ return serializer.deserialize(obj);
37
+ } catch (error) {
38
+ console.error("Error deserializing object:", error);
39
+ return obj;
40
+ }
41
+ }
42
+ /**
43
+ * We want to save steps to a databse, we are using D1, so SQLite is the database.
44
+ * Step Table:
45
+ * - instanceId: string
46
+ * - stepName: string
47
+ * - stepStatus: string (pending, completed, failed)
48
+ * - stepMetadata: string (JSON)
49
+ * - startTime: number (timestamp)
50
+ * - endTime: number (timestamp) (optional)
51
+ * - result: string (JSON) (optional)
52
+ * - error: string (JSON) (optional)
53
+ *
54
+ * Log Table:
55
+ * - instanceId: string
56
+ * - stepName: string
57
+ * - log: string
58
+ * - timestamp: number (timestamp)
59
+ * - type: string (info, error, warning)
60
+ */
61
+ /**
62
+ * Ensure that the required tables exist in D1 (SQLite).
63
+ */
64
+ async function ensureTables(db) {
65
+ const s1 = db.prepare(
66
+ /* sql */
67
+ `CREATE TABLE IF NOT EXISTS WorkflowTable (
68
+ instanceId TEXT NOT NULL,
69
+ workflowType TEXT NOT NULL,
70
+ workflowName TEXT NOT NULL,
71
+ workflowMetadata TEXT NOT NULL,
72
+ input TEXT NOT NULL,
73
+ tenantId TEXT NOT NULL,
74
+ workflowStatus TEXT NOT NULL,
75
+ startTime INTEGER NOT NULL,
76
+ endTime INTEGER,
77
+ parentInstanceId TEXT,
78
+ PRIMARY KEY (instanceId)
79
+ )`
80
+ );
81
+ const s2 = db.prepare(
82
+ /* sql */
83
+ `CREATE TABLE IF NOT EXISTS StepTable (
84
+ instanceId TEXT NOT NULL,
85
+ stepName TEXT NOT NULL,
86
+ stepStatus TEXT NOT NULL,
87
+ stepMetadata TEXT NOT NULL,
88
+ startTime INTEGER NOT NULL,
89
+ endTime INTEGER,
90
+ result TEXT,
91
+ error TEXT,
92
+ tenantId TEXT NOT NULL,
93
+ PRIMARY KEY (instanceId, stepName)
94
+ )`
95
+ );
96
+ const s3 = db.prepare(
97
+ /* sql */
98
+ `CREATE TABLE IF NOT EXISTS LogTable (
99
+ instanceId TEXT NOT NULL,
100
+ stepName TEXT,
101
+ log TEXT NOT NULL,
102
+ timestamp INTEGER NOT NULL,
103
+ type TEXT NOT NULL,
104
+ logOrder INTEGER NOT NULL,
105
+ tenantId TEXT NOT NULL,
106
+ FOREIGN KEY (instanceId, stepName) REFERENCES StepTable(instanceId, stepName)
107
+ DEFERRABLE INITIALLY DEFERRED -- Makes constraint check at commit time, this is needed, since stepName is nullable in the log table.
108
+
109
+ )`
110
+ );
111
+ const s7 = db.prepare(
112
+ /* sql */
113
+ `
114
+ CREATE TABLE IF NOT EXISTS WorkflowProperties (
115
+ instanceId TEXT NOT NULL,
116
+ key TEXT NOT NULL,
117
+ value TEXT NOT NULL,
118
+ valueType TEXT NOT NULL,
119
+ tenantId TEXT NOT NULL,
120
+ PRIMARY KEY (instanceId, key),
121
+ FOREIGN KEY (instanceId) REFERENCES WorkflowTable(instanceId)
122
+ ON DELETE CASCADE
123
+ )`
124
+ );
125
+ const s6 = db.prepare(
126
+ /* sql */
127
+ `
128
+ CREATE INDEX IF NOT EXISTS idx_workflows_parent_instance_id ON WorkflowTable (parentInstanceId)
129
+ `
130
+ );
131
+ const p1 = db.prepare(`PRAGMA foreign_keys = ON`);
132
+ await db.batch([
133
+ s1,
134
+ s2,
135
+ s3,
136
+ s6,
137
+ s7,
138
+ p1
139
+ ]);
140
+ }
141
+ function workflowTableRowToWorkflowRun(row, serializer) {
142
+ return {
143
+ instanceId: row.instanceId,
144
+ workflowType: row.workflowType,
145
+ workflowName: row.workflowName,
146
+ input: tryDeserializeObj(row.input, serializer),
147
+ tenantId: row.tenantId,
148
+ workflowStatus: row.workflowStatus,
149
+ startTime: row.startTime,
150
+ endTime: row.endTime,
151
+ parentInstanceId: row.parentInstanceId
152
+ };
153
+ }
154
+ async function updateWorkflowName(context, instanceId, newWorkflowName) {
155
+ return await context.D1.prepare(
156
+ /* sql */
157
+ `UPDATE WorkflowTable
158
+ SET workflowName = ?
159
+ WHERE instanceId = ?`
160
+ ).bind(newWorkflowName, instanceId).run();
161
+ }
162
+ async function upsertWorkflowProperty({ context, instanceId, key, valueType, value, tenantId }) {
163
+ const serializedValue = valueType === "object" ? JSON.stringify(value) : String(value);
164
+ const res = await context.D1.prepare(
165
+ /* sql */
166
+ `INSERT OR REPLACE INTO WorkflowProperties
167
+ (instanceId, key, value, valueType, tenantId)
168
+ VALUES (?, ?, ?, ?, ?)`
169
+ ).bind(instanceId, key, serializedValue, valueType, tenantId).run();
170
+ if (res.error) {
171
+ console.error("Error inserting workflow property:", res.error);
172
+ return false;
173
+ }
174
+ return res.success;
175
+ }
176
+
177
+ //#endregion
178
+ //#region src/observableWorkflows/defineWorkflow.ts
179
+ function defineWorkflow(workflow, callback) {
180
+ return {
181
+ workflowType: workflow.workflowType,
182
+ metadata: workflow.metadata || {},
183
+ _callback: callback
184
+ };
185
+ }
186
+
187
+ //#endregion
188
+ //#region src/observableWorkflows/createStepContext.ts
189
+ async function createStepContext(context) {
190
+ const instanceId = context.instanceId;
191
+ await ensureTables(context.D1);
192
+ const step = async (step$1, callback) => {
193
+ if (context.parentInstanceId) {
194
+ const existingStep = await context.D1.prepare(
195
+ /* sql */
196
+ `SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`
197
+ ).bind(context.parentInstanceId, step$1.name, context.tenantId).first();
198
+ if (existingStep) {
199
+ const row = existingStep;
200
+ if (row.status === "completed") {
201
+ await insertStepRecordFull(context, {
202
+ instanceId,
203
+ name: row.name,
204
+ status: row.status,
205
+ metadata: row.metadata,
206
+ startTime: row.startTime,
207
+ endTime: row.endTime,
208
+ result: row.result,
209
+ error: row.error
210
+ });
211
+ return context.serializer.deserialize(row.result);
212
+ }
213
+ }
214
+ }
215
+ let waitFor = [];
216
+ const startTime = Date.now();
217
+ const stepStatus = "pending";
218
+ const stepMetadata = context.serializer.serialize(step$1.metadata || {});
219
+ const stepName = step$1.name;
220
+ const stepResult = null;
221
+ const stepError = null;
222
+ await insertStepRecordFull(context, {
223
+ instanceId,
224
+ name: stepName,
225
+ status: stepStatus,
226
+ metadata: stepMetadata,
227
+ startTime,
228
+ endTime: null,
229
+ result: stepResult,
230
+ error: stepError
231
+ });
232
+ let logOrder = 0;
233
+ const log = (message, type = "info") => {
234
+ logOrder++;
235
+ const timestamp = Date.now();
236
+ const logPromise = pushLogToDB(context, {
237
+ stepName: step$1.name,
238
+ instanceId,
239
+ message,
240
+ timestamp,
241
+ type,
242
+ logOrder,
243
+ tenantId: context.tenantId
244
+ });
245
+ waitFor.push(logPromise);
246
+ };
247
+ const ctx = { console: {
248
+ log: (message) => {
249
+ log(message, "info");
250
+ },
251
+ info: (message) => {
252
+ log(message, "info");
253
+ },
254
+ error: (message) => {
255
+ log(message, "error");
256
+ },
257
+ warn: (message) => {
258
+ log(message, "warn");
259
+ }
260
+ } };
261
+ try {
262
+ const result = await callback(ctx);
263
+ const endTime = Date.now();
264
+ const stepStatus$1 = "completed";
265
+ let stepResult$1 = null;
266
+ try {
267
+ stepResult$1 = result ? context.serializer.serialize(result) : null;
268
+ } catch {
269
+ stepResult$1 = String(result);
270
+ }
271
+ const stepError$1 = null;
272
+ await context.D1.prepare(
273
+ /* sql */
274
+ `UPDATE StepTable
275
+ SET stepStatus = ?, endTime = ?, result = ?, error = ?
276
+ WHERE instanceId = ? AND stepName = ?`
277
+ ).bind(stepStatus$1, endTime, stepResult$1, stepError$1, instanceId, stepName).run();
278
+ await Promise.allSettled(waitFor);
279
+ return result;
280
+ } catch (error) {
281
+ const endTime = Date.now();
282
+ const stepStatus$1 = "failed";
283
+ const stepResult$1 = null;
284
+ let stepError$1;
285
+ try {
286
+ stepError$1 = context.serializer.serialize(error);
287
+ } catch {
288
+ stepError$1 = String(error);
289
+ }
290
+ await context.D1.prepare(
291
+ /* sql */
292
+ `UPDATE StepTable
293
+ SET stepStatus = ?, endTime = ?, result = ?, error = ?
294
+ WHERE instanceId = ? AND stepName = ?`
295
+ ).bind(stepStatus$1, endTime, stepResult$1, stepError$1, instanceId, stepName).run();
296
+ await Promise.allSettled(waitFor);
297
+ throw error;
298
+ }
299
+ };
300
+ return step;
301
+ }
302
+
303
+ //#endregion
304
+ //#region src/observableWorkflows/createWorkflowContext.ts
305
+ async function createWorkflowContext(options) {
306
+ await ensureTables(options.D1);
307
+ const internalContext = {
308
+ ...options,
309
+ serializer: options.serializer ?? {
310
+ serialize: JSON.stringify,
311
+ deserialize: JSON.parse
312
+ },
313
+ idFactory: options.idFactory ?? (() => crypto.randomUUID())
314
+ };
315
+ const call = async ({ workflow, input, workflowName, tenantId, parentInstanceId }) => {
316
+ const instanceId = internalContext.idFactory();
317
+ const startTime = Date.now();
318
+ await insertWorkflowRecord(internalContext, {
319
+ instanceId,
320
+ workflowType: workflow.workflowType,
321
+ workflowName,
322
+ workflowMetadata: workflow.metadata,
323
+ input,
324
+ workflowStatus: "pending",
325
+ startTime,
326
+ endTime: null,
327
+ parentInstanceId,
328
+ tenantId
329
+ });
330
+ let waitFor = [];
331
+ let logOrder = 0;
332
+ const log = (message, type = "info") => {
333
+ logOrder++;
334
+ const timestamp = Date.now();
335
+ const logPromise = pushLogToDB(internalContext, {
336
+ instanceId,
337
+ stepName: null,
338
+ message,
339
+ timestamp,
340
+ type,
341
+ logOrder,
342
+ tenantId
343
+ });
344
+ waitFor.push(logPromise);
345
+ };
346
+ const stepContext = await createStepContext({
347
+ D1: options.D1,
348
+ tenantId,
349
+ instanceId,
350
+ serializer: internalContext.serializer,
351
+ idFactory: internalContext.idFactory
352
+ });
353
+ try {
354
+ await workflow._callback(input, {
355
+ step: stepContext,
356
+ console: {
357
+ log: (message) => {
358
+ log(message, "info");
359
+ },
360
+ info: (message) => {
361
+ log(message, "info");
362
+ },
363
+ error: (message) => {
364
+ log(message, "error");
365
+ },
366
+ warn: (message) => {
367
+ log(message, "warn");
368
+ }
369
+ },
370
+ async setWorkflowName(newName) {
371
+ await updateWorkflowName({ D1: options.D1 }, instanceId, newName);
372
+ },
373
+ async setWorkflowProperty(key, valueType, value) {
374
+ await upsertWorkflowProperty({
375
+ context: internalContext,
376
+ instanceId,
377
+ key,
378
+ valueType,
379
+ value,
380
+ tenantId
381
+ });
382
+ }
383
+ });
384
+ const endTime = Date.now();
385
+ const workflowStatus = "completed";
386
+ await finalizeWorkflowRecord(internalContext, {
387
+ workflowStatus,
388
+ endTime,
389
+ instanceId
390
+ });
391
+ await Promise.allSettled(waitFor);
392
+ } catch (error) {
393
+ const endTime = Date.now();
394
+ const workflowStatus = "failed";
395
+ await finalizeWorkflowRecord(internalContext, {
396
+ workflowStatus,
397
+ endTime,
398
+ instanceId
399
+ });
400
+ await Promise.allSettled(waitFor);
401
+ throw error;
402
+ }
403
+ };
404
+ const retry = async (workflow, retryInstanceId) => {
405
+ const oldRun = await options.D1.prepare(
406
+ /* sql */
407
+ `SELECT input, workflowName, tenantId FROM WorkflowTable WHERE instanceId = ? `
408
+ ).bind(retryInstanceId).first();
409
+ const oldWorkflowName = oldRun?.workflowName;
410
+ const tenantId = oldRun?.tenantId;
411
+ if (!tenantId) throw new Error(`No tenantId found for instanceId ${retryInstanceId}`);
412
+ const encodedInput = oldRun?.input;
413
+ if (!encodedInput) throw new Error(`No input found for instanceId ${retryInstanceId}`);
414
+ const input = JSON.parse(encodedInput);
415
+ await call({
416
+ workflow,
417
+ input,
418
+ workflowName: oldWorkflowName ?? "unknown",
419
+ parentInstanceId: retryInstanceId,
420
+ tenantId
421
+ });
422
+ };
423
+ return {
424
+ call,
425
+ retry
426
+ };
427
+ }
428
+
429
+ //#endregion
430
+ //#region src/observableWorkflows/createQueueWorkflowContext.ts
431
+ function createQueueWorkflowContext(options) {
432
+ const enqueueWorkflow = async (workflow, tenantId, input, initialName) => {
433
+ await options.QUEUE.send({
434
+ type: "workflow-run",
435
+ workflowType: workflow.workflowType,
436
+ workflowName: initialName,
437
+ input,
438
+ tenantId
439
+ });
440
+ };
441
+ const enqueueRetryWorkflow = async (workflow, tenantId, oldInstanceId) => {
442
+ await options.QUEUE.send({
443
+ type: "workflow-retry",
444
+ workflowType: workflow.workflowType,
445
+ retryInstanceId: oldInstanceId,
446
+ tenantId
447
+ });
448
+ };
449
+ const handleWorkflowQueueMessage = async (message, env, workflowResolver) => {
450
+ const workflowFunction = workflowResolver(message.workflowType);
451
+ if (!workflowFunction) throw new Error(`Workflow ${message.workflowType} not found`);
452
+ const wfc = await createWorkflowContext({
453
+ D1: env.LOG_DB,
454
+ serializer: options.serializer,
455
+ idFactory: options.idFactory
456
+ });
457
+ if (message.type === "workflow-run") {
458
+ console.log("running workflow", message.input);
459
+ await wfc.call({
460
+ input: message.input,
461
+ workflow: workflowFunction,
462
+ workflowName: message.workflowName,
463
+ tenantId: message.tenantId
464
+ });
465
+ } else if (message.type === "workflow-retry") {
466
+ console.log("retrying workflow", message.retryInstanceId);
467
+ await wfc.retry(workflowFunction, message.retryInstanceId);
468
+ }
469
+ };
470
+ return {
471
+ enqueueWorkflow,
472
+ enqueueRetryWorkflow,
473
+ handleWorkflowQueueMessage
474
+ };
475
+ }
476
+
477
+ //#endregion
478
+ //#region src/observableWorkflows/createLogAccessor.ts
479
+ const createLogAccessor = (context) => {
480
+ const listSteps = async (limit, offset, instanceId) => {
481
+ const result = await context.D1.prepare(
482
+ /* sql */
483
+ `SELECT * FROM StepTable
484
+ WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
485
+ ORDER BY startTime ASC LIMIT ? OFFSET ?`
486
+ ).bind(context.tenantId, instanceId ?? null, instanceId ?? null, limit, offset).all();
487
+ return result.results ? result.results.map((row) => ({
488
+ instanceId: row.instanceId,
489
+ name: row.stepName,
490
+ metadata: tryDeserializeObj(row.stepMetadata, context.serializer),
491
+ status: row.stepStatus,
492
+ startTime: row.startTime,
493
+ endTime: row.endTime,
494
+ result: row.result ? tryDeserializeObj(row.result, context.serializer) : null,
495
+ error: row.error ? tryDeserializeObj(row.error, context.serializer) : null
496
+ })) : [];
497
+ };
498
+ const listWorkflows = async (limit, offset) => {
499
+ const result = await context.D1.prepare(
500
+ /* sql */
501
+ `SELECT *
502
+ FROM WorkflowTable
503
+ WHERE tenantId = ?
504
+ ORDER BY startTime DESC LIMIT ? OFFSET ?`
505
+ ).bind(context.tenantId, limit, offset).all();
506
+ return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, context.serializer)) : [];
507
+ };
508
+ const getWorkflowByParentId = async (parentInstanceId) => {
509
+ const result = await context.D1.prepare(
510
+ /* sql */
511
+ `SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`
512
+ ).bind(parentInstanceId, context.tenantId).all();
513
+ return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, context.serializer)) : null;
514
+ };
515
+ const getWorkflowTypesByTenantId = async (tenantId) => {
516
+ const result = await context.D1.prepare(
517
+ /* sql */
518
+ `SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`
519
+ ).bind(tenantId).all();
520
+ if (!result.results) return [];
521
+ return result.results.map((row) => row.workflowType);
522
+ };
523
+ const getWorkflowProperties = async (instanceId) => {
524
+ const result = await context.D1.prepare(
525
+ /* sql */
526
+ `SELECT * FROM WorkflowProperties WHERE instanceId = ?`
527
+ ).bind(instanceId).all();
528
+ if (!result.results) return [];
529
+ return result.results.map((row) => ({
530
+ key: row.key,
531
+ valueType: row.valueType,
532
+ value: tryDeserializeObj(row.value, context.serializer)
533
+ }));
534
+ };
535
+ /** This function gets the basic data of a workflow, without populating any of it's complex fields */
536
+ const getWorkflowShallow = async (instanceId) => {
537
+ const result = await context.D1.prepare(
538
+ /* sql */
539
+ `SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`
540
+ ).bind(instanceId, context.tenantId).first();
541
+ if (!result) return null;
542
+ const workflow = workflowTableRowToWorkflowRun(result, context.serializer);
543
+ return workflow;
544
+ };
545
+ const getWorkflow = async (instanceId) => {
546
+ const workflow = await getWorkflowShallow(instanceId);
547
+ if (!workflow) return null;
548
+ const steps = await listSteps(100, 0, instanceId);
549
+ const retryWorkflows = await getWorkflowByParentId(instanceId);
550
+ const parentWorkflow = workflow.parentInstanceId ? await getWorkflowShallow(workflow.parentInstanceId) : null;
551
+ workflow.retries = retryWorkflows;
552
+ retryWorkflows?.forEach((retryWorkflow) => {
553
+ retryWorkflow.isRetryOf = workflow;
554
+ });
555
+ workflow.isRetryOf = parentWorkflow;
556
+ const allLogs = await context.D1.prepare(
557
+ /* sql */
558
+ `SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`
559
+ ).bind(instanceId).all();
560
+ const logs = allLogs.results?.map((logRow) => ({
561
+ instanceId: logRow.instanceId,
562
+ stepName: logRow.stepName,
563
+ log: logRow.log,
564
+ timestamp: logRow.timestamp,
565
+ type: logRow.type
566
+ }));
567
+ steps.forEach((step) => {
568
+ step.logs = logs?.filter((log) => log.stepName === step.name) ?? [];
569
+ });
570
+ workflow.steps = steps;
571
+ workflow.logs = logs;
572
+ const properties = await getWorkflowProperties(instanceId);
573
+ workflow.properties = properties;
574
+ return workflow;
575
+ };
576
+ const getStep = async (instanceId, stepName) => {
577
+ const result = await context.D1.prepare(
578
+ /* sql */
579
+ `SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`
580
+ ).bind(instanceId, stepName, context.tenantId).all();
581
+ if (!result.results || result.results.length === 0) return null;
582
+ const row = result.results[0];
583
+ if (!row) return null;
584
+ const step = {
585
+ instanceId: row.instanceId,
586
+ name: row.stepName,
587
+ metadata: tryDeserializeObj(row.stepMetadata, context.serializer),
588
+ status: row.stepStatus,
589
+ startTime: row.startTime,
590
+ endTime: row.endTime,
591
+ result: row.result ? tryDeserializeObj(row.result, context.serializer) : null,
592
+ error: row.error ? tryDeserializeObj(row.error, context.serializer) : null,
593
+ logs: []
594
+ };
595
+ const logResult = await context.D1.prepare(
596
+ /* sql */
597
+ `SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`
598
+ ).bind(instanceId, stepName).all();
599
+ if (logResult.results) step.logs = logResult.results.map((logRow) => ({
600
+ instanceId: logRow.instanceId,
601
+ stepName: logRow.stepName,
602
+ log: logRow.log,
603
+ timestamp: logRow.timestamp,
604
+ type: logRow.type
605
+ }));
606
+ return step;
607
+ };
608
+ const getPropertiesKeys = async (instanceId) => {
609
+ const result = await context.D1.prepare(
610
+ /* sql */
611
+ `
612
+ SELECT DISTINCT key, valueType FROM PropertiesTable
613
+ WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
614
+ `
615
+ ).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all();
616
+ if (!result.results) return [];
617
+ return result.results.map((row) => ({
618
+ key: row.key,
619
+ valueType: row.valueType
620
+ }));
621
+ };
622
+ return {
623
+ listSteps,
624
+ getStep,
625
+ listWorkflows,
626
+ getWorkflow,
627
+ getWorkflowTypesByTenantId,
628
+ getPropertiesKeys
629
+ };
630
+ };
6
631
 
7
632
  //#endregion
8
- export { fn, foo };
633
+ export { createLogAccessor, createQueueWorkflowContext, createStepContext, createWorkflowContext, defineWorkflow, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, tryDeserializeObj, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brandboostinggmbh/observable-workflows",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "My awesome typescript library",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -27,6 +27,7 @@
27
27
  "access": "public"
28
28
  },
29
29
  "devDependencies": {
30
+ "@cloudflare/workers-types": "^4.20250515.0",
30
31
  "@sxzz/eslint-config": "^7.0.1",
31
32
  "@sxzz/prettier-config": "^2.2.1",
32
33
  "@types/node": "^22.15.17",