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