@brandboostinggmbh/observable-workflows 0.5.0 → 0.6.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.
Files changed (3) hide show
  1. package/dist/index.d.ts +123 -24
  2. package/dist/index.js +342 -212
  3. package/package.json +4 -2
package/dist/index.d.ts CHANGED
@@ -205,6 +205,105 @@ type WorkflowPropertyDefinition = {
205
205
  key: string;
206
206
  valueType: 'string' | 'number' | 'boolean' | 'object';
207
207
  };
208
+ /**
209
+ * Date range filter for filtering workflows by time periods
210
+ */
211
+ type DateRangeFilter = {
212
+ /** Greater than or equal to (inclusive) */
213
+ gte?: number;
214
+ /** Less than or equal to (inclusive) */
215
+ lte?: number;
216
+ /** Greater than (exclusive) */
217
+ gt?: number;
218
+ /** Less than (exclusive) */
219
+ lt?: number;
220
+ };
221
+ /**
222
+ * String filter supporting exact match or substring matching
223
+ */
224
+ type StringFilter = string | {
225
+ equals: string;
226
+ } | {
227
+ contains: string;
228
+ };
229
+ /**
230
+ * Property filter supporting various comparison operators for custom workflow properties
231
+ */
232
+ type PropertyFilter = {
233
+ /** Exact match */
234
+ equals?: any;
235
+ /** Substring match (for string values) */
236
+ contains?: string;
237
+ /** Greater than (for numeric values) */
238
+ gt?: number;
239
+ /** Greater than or equal to (for numeric values) */
240
+ gte?: number;
241
+ /** Less than (for numeric values) */
242
+ lt?: number;
243
+ /** Less than or equal to (for numeric values) */
244
+ lte?: number;
245
+ /** Match any value in the array */
246
+ in?: any[];
247
+ };
248
+ /**
249
+ * Comprehensive filter object for workflow listing with support for:
250
+ * - Text search across workflow names and types
251
+ * - Direct field filtering with various operators
252
+ * - Date range filtering
253
+ * - Custom property filtering with joins
254
+ *
255
+ * @example
256
+ * ```typescript
257
+ * // Text search
258
+ * const workflows = await listWorkflows(10, 0, { search: "payment" })
259
+ *
260
+ * // Status and type filtering
261
+ * const workflows = await listWorkflows(10, 0, {
262
+ * workflowStatus: ["completed", "failed"],
263
+ * workflowType: "PaymentWorkflow"
264
+ * })
265
+ *
266
+ * // Date range filtering
267
+ * const workflows = await listWorkflows(10, 0, {
268
+ * startTime: { gte: Date.now() - 86400000 } // Last 24 hours
269
+ * })
270
+ *
271
+ * // Property filtering
272
+ * const workflows = await listWorkflows(10, 0, {
273
+ * properties: {
274
+ * amount: { gt: 100, lte: 1000 },
275
+ * currency: { equals: "USD" },
276
+ * customer: { contains: "acme" }
277
+ * }
278
+ * })
279
+ *
280
+ * // Combined filtering
281
+ * const workflows = await listWorkflows(10, 0, {
282
+ * search: "payment",
283
+ * workflowStatus: "completed",
284
+ * startTime: { gte: Date.now() - 86400000 },
285
+ * properties: { amount: { gt: 100 } }
286
+ * })
287
+ * ```
288
+ */
289
+ type WorkflowFilter = {
290
+ /** Text search across workflow name and type */
291
+ search?: string;
292
+ /** Filter by workflow type (exact match) */
293
+ workflowType?: string | string[];
294
+ /** Filter by workflow status */
295
+ workflowStatus?: StepWorkflowStatus | StepWorkflowStatus[];
296
+ /** Filter by workflow name with string matching */
297
+ workflowName?: StringFilter;
298
+ /** Filter by workflow start time */
299
+ startTime?: DateRangeFilter;
300
+ /** Filter by workflow end time */
301
+ endTime?: DateRangeFilter;
302
+ /** Filter by custom workflow properties */
303
+ properties?: {
304
+ [key: string]: PropertyFilter;
305
+ };
306
+ };
208
307
  type StepContext = Awaited<ReturnType<typeof createStepContext>>;
209
308
  type Serializer = {
210
309
  serialize: (data: any) => string;
@@ -263,28 +362,6 @@ type RetryWorkflowOptions = {
263
362
  reuseSuccessfulSteps?: boolean;
264
363
  };
265
364
 
266
- //#endregion
267
- //#region src/observableWorkflows/defineWorkflow.d.ts
268
- declare function defineWorkflow<I extends {} | null>(workflow: {
269
- workflowType: string;
270
- metadata?: Record<string, any>;
271
- }, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
272
- declare function defineWorkflow<I extends {} | null>(workflowType: string, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
273
-
274
- //#endregion
275
- //#region src/observableWorkflows/createWorkflowContext.d.ts
276
- declare function createWorkflowContext(options: WorkflowContextOptions): WorkflowContextInstance;
277
-
278
- //#endregion
279
- //#region src/observableWorkflows/createQueueWorkflowContext.d.ts
280
- declare function createQueueWorkflowContext(options: QueueWorkflowContextOptions): {
281
- enqueueWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, input: I, initialName: string) => Promise<void>;
282
- enqueueRetryWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, oldInstanceId: string) => Promise<void>;
283
- handleWorkflowQueueMessage: (message: WorkflowQueueMessage, env: {
284
- LOG_DB: D1Database;
285
- }, workflowResolver: (workflowType: string) => WorkflowFunction<any> | undefined) => Promise<void>;
286
- };
287
-
288
365
  //#endregion
289
366
  //#region src/observableWorkflows/createLogAccessor.d.ts
290
367
  declare const createLogAccessor: (context: {
@@ -294,12 +371,26 @@ declare const createLogAccessor: (context: {
294
371
  }) => {
295
372
  listSteps: (limit: number, offset: number, instanceId?: string | undefined) => Promise<Step[]>;
296
373
  getStep: (instanceId: string, stepName: string) => Promise<Step | null>;
297
- listWorkflows: (limit: number, offset: number) => Promise<WorkflowRun[]>;
374
+ listWorkflows: (limit: number, offset: number, filter?: WorkflowFilter) => Promise<WorkflowRun[]>;
298
375
  getWorkflow: (instanceId: string) => Promise<WorkflowRun | null>;
299
376
  getWorkflowTypesByTenantId: (tenantId: string) => Promise<string[]>;
300
377
  getPropertiesKeys: (instanceId?: string) => Promise<WorkflowPropertyDefinition[]>;
301
378
  };
302
379
 
380
+ //#endregion
381
+ //#region src/observableWorkflows/createQueueWorkflowContext.d.ts
382
+ declare function createQueueWorkflowContext(options: QueueWorkflowContextOptions): {
383
+ enqueueWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, input: I, initialName: string) => Promise<void>;
384
+ enqueueRetryWorkflow: <I>(workflow: WorkflowFunction<I>, tenantId: string, oldInstanceId: string) => Promise<void>;
385
+ handleWorkflowQueueMessage: (message: WorkflowQueueMessage, env: {
386
+ LOG_DB: D1Database;
387
+ }, workflowResolver: (workflowType: string) => WorkflowFunction<any> | undefined) => Promise<void>;
388
+ };
389
+
390
+ //#endregion
391
+ //#region src/observableWorkflows/createWorkflowContext.d.ts
392
+ declare function createWorkflowContext(options: WorkflowContextOptions): WorkflowContextInstance;
393
+
303
394
  //#endregion
304
395
  //#region src/observableWorkflows/defaultImplementations.d.ts
305
396
  declare const defaultSerializer: {
@@ -312,4 +403,12 @@ declare const defaultSerializer: {
312
403
  declare const defaultIdFactory: () => string;
313
404
 
314
405
  //#endregion
315
- export { ConsoleWrapper, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, QueueWorkflowContextOptions, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, ValueTypeMap, WorkflowContext, WorkflowContextInstance, WorkflowContextOptions, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, createLogAccessor, createQueueWorkflowContext, createStepContext, createWorkflowContext, defaultIdFactory, defaultSerializer, defineWorkflow, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, tryDeserializeObj, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
406
+ //#region src/observableWorkflows/defineWorkflow.d.ts
407
+ declare function defineWorkflow<I extends {} | null>(workflow: {
408
+ workflowType: string;
409
+ metadata?: Record<string, any>;
410
+ }, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
411
+ declare function defineWorkflow<I extends {} | null>(workflowType: string, callback: (input: I, ctx: WorkflowContext) => Promise<any>): WorkflowFunction<I>;
412
+
413
+ //#endregion
414
+ export { ConsoleWrapper, DateRangeFilter, InternalWorkflowContextOptions, Log, PossibleValueTypeNames, PossibleValueTypes, PropertyFilter, QueueWorkflowContextOptions, Serializer, Step, StepContextOptions, StepCtx, StepWorkflowStatus, StringFilter, ValueTypeMap, WorkflowContext, WorkflowContextInstance, WorkflowContextOptions, WorkflowFilter, WorkflowFunction, WorkflowProperty, WorkflowPropertyDefinition, WorkflowQueueMessage, WorkflowRun, createLogAccessor, createQueueWorkflowContext, createStepContext, createWorkflowContext, defaultIdFactory, defaultSerializer, defineWorkflow, ensureTables, finalizeWorkflowRecord, insertStepRecordFull, insertWorkflowRecord, pushLogToDB, tryDeserializeObj, updateWorkflowName, upsertWorkflowProperty, workflowTableRowToWorkflowRun };
package/dist/index.js CHANGED
@@ -221,14 +221,340 @@ async function upsertWorkflowProperty({ context, instanceId, key, value, tenantI
221
221
  }
222
222
 
223
223
  //#endregion
224
- //#region src/observableWorkflows/defineWorkflow.ts
225
- function defineWorkflow(workflow, callback) {
226
- const metadata = typeof workflow === "string" ? void 0 : workflow.metadata;
227
- const workflowType = typeof workflow === "string" ? workflow : workflow.workflowType;
224
+ //#region src/observableWorkflows/defaultImplementations.ts
225
+ const defaultSerializer = {
226
+ serialize: JSON.stringify,
227
+ deserialize: JSON.parse
228
+ };
229
+ const defaultIdFactory = () => crypto.randomUUID();
230
+
231
+ //#endregion
232
+ //#region src/observableWorkflows/createLogAccessor.ts
233
+ const createLogAccessor = (context) => {
234
+ const internalSerializer = context.serializer ?? defaultSerializer;
235
+ const buildFilteredWorkflowQuery = (filter) => {
236
+ const bindings = [];
237
+ const whereConditions = [];
238
+ let joinClause = "";
239
+ const selectClause = "SELECT DISTINCT w.*";
240
+ const fromClause = "FROM WorkflowTable w";
241
+ const propertyWhereConditions = [];
242
+ if (filter) {
243
+ if (filter.properties && Object.keys(filter.properties).length > 0) {
244
+ let propIndex = 0;
245
+ for (const [key, propertyFilter] of Object.entries(filter.properties)) {
246
+ const propAlias = `p${propIndex}`;
247
+ joinClause += ` LEFT JOIN WorkflowProperties ${propAlias} ON w.instanceId = ${propAlias}.instanceId AND ${propAlias}.key = ? AND ${propAlias}.tenantId = ?`;
248
+ bindings.push(key, context.tenantId);
249
+ if (propertyFilter.equals !== void 0) propertyWhereConditions.push({
250
+ condition: `${propAlias}.value = ?`,
251
+ binding: String(propertyFilter.equals)
252
+ });
253
+ if (propertyFilter.contains !== void 0) propertyWhereConditions.push({
254
+ condition: `${propAlias}.value LIKE ?`,
255
+ binding: `%${propertyFilter.contains}%`
256
+ });
257
+ if (propertyFilter.gt !== void 0) propertyWhereConditions.push({
258
+ condition: `CAST(${propAlias}.value AS REAL) > ?`,
259
+ binding: propertyFilter.gt
260
+ });
261
+ if (propertyFilter.gte !== void 0) propertyWhereConditions.push({
262
+ condition: `CAST(${propAlias}.value AS REAL) >= ?`,
263
+ binding: propertyFilter.gte
264
+ });
265
+ if (propertyFilter.lt !== void 0) propertyWhereConditions.push({
266
+ condition: `CAST(${propAlias}.value AS REAL) < ?`,
267
+ binding: propertyFilter.lt
268
+ });
269
+ if (propertyFilter.lte !== void 0) propertyWhereConditions.push({
270
+ condition: `CAST(${propAlias}.value AS REAL) <= ?`,
271
+ binding: propertyFilter.lte
272
+ });
273
+ if (propertyFilter.in !== void 0 && Array.isArray(propertyFilter.in)) {
274
+ const placeholders = propertyFilter.in.map(() => "?").join(", ");
275
+ propertyWhereConditions.push({
276
+ condition: `${propAlias}.value IN (${placeholders})`,
277
+ bindings: propertyFilter.in.map(String)
278
+ });
279
+ }
280
+ propIndex++;
281
+ }
282
+ }
283
+ }
284
+ propertyWhereConditions.forEach((item) => {
285
+ whereConditions.push(item.condition);
286
+ if (item.bindings) bindings.push(...item.bindings);
287
+ else if (item.binding !== void 0) bindings.push(item.binding);
288
+ });
289
+ whereConditions.push("w.tenantId = ?");
290
+ bindings.push(context.tenantId);
291
+ if (filter) {
292
+ if (filter.search) {
293
+ whereConditions.push("(w.workflowName LIKE ? OR w.workflowType LIKE ?)");
294
+ bindings.push(`%${filter.search}%`, `%${filter.search}%`);
295
+ }
296
+ if (filter.workflowType) if (Array.isArray(filter.workflowType)) {
297
+ const placeholders = filter.workflowType.map(() => "?").join(", ");
298
+ whereConditions.push(`w.workflowType IN (${placeholders})`);
299
+ bindings.push(...filter.workflowType);
300
+ } else {
301
+ whereConditions.push("w.workflowType = ?");
302
+ bindings.push(filter.workflowType);
303
+ }
304
+ if (filter.workflowStatus) if (Array.isArray(filter.workflowStatus)) {
305
+ const placeholders = filter.workflowStatus.map(() => "?").join(", ");
306
+ whereConditions.push(`w.workflowStatus IN (${placeholders})`);
307
+ bindings.push(...filter.workflowStatus);
308
+ } else {
309
+ whereConditions.push("w.workflowStatus = ?");
310
+ bindings.push(filter.workflowStatus);
311
+ }
312
+ if (filter.workflowName) {
313
+ if (typeof filter.workflowName === "string") {
314
+ whereConditions.push("w.workflowName = ?");
315
+ bindings.push(filter.workflowName);
316
+ } else if (typeof filter.workflowName === "object") {
317
+ if ("equals" in filter.workflowName) {
318
+ whereConditions.push("w.workflowName = ?");
319
+ bindings.push(filter.workflowName.equals);
320
+ } else if ("contains" in filter.workflowName) {
321
+ whereConditions.push("w.workflowName LIKE ?");
322
+ bindings.push(`%${filter.workflowName.contains}%`);
323
+ }
324
+ }
325
+ }
326
+ if (filter.startTime) {
327
+ if (filter.startTime.gte !== void 0) {
328
+ whereConditions.push("w.startTime >= ?");
329
+ bindings.push(filter.startTime.gte);
330
+ }
331
+ if (filter.startTime.lte !== void 0) {
332
+ whereConditions.push("w.startTime <= ?");
333
+ bindings.push(filter.startTime.lte);
334
+ }
335
+ if (filter.startTime.gt !== void 0) {
336
+ whereConditions.push("w.startTime > ?");
337
+ bindings.push(filter.startTime.gt);
338
+ }
339
+ if (filter.startTime.lt !== void 0) {
340
+ whereConditions.push("w.startTime < ?");
341
+ bindings.push(filter.startTime.lt);
342
+ }
343
+ }
344
+ if (filter.endTime) {
345
+ if (filter.endTime.gte !== void 0) {
346
+ whereConditions.push("w.endTime >= ?");
347
+ bindings.push(filter.endTime.gte);
348
+ }
349
+ if (filter.endTime.lte !== void 0) {
350
+ whereConditions.push("w.endTime <= ?");
351
+ bindings.push(filter.endTime.lte);
352
+ }
353
+ if (filter.endTime.gt !== void 0) {
354
+ whereConditions.push("w.endTime > ?");
355
+ bindings.push(filter.endTime.gt);
356
+ }
357
+ if (filter.endTime.lt !== void 0) {
358
+ whereConditions.push("w.endTime < ?");
359
+ bindings.push(filter.endTime.lt);
360
+ }
361
+ }
362
+ }
363
+ const whereClause = whereConditions.length > 0 ? `WHERE ${whereConditions.join(" AND ")}` : "";
364
+ const sql = `${selectClause} ${fromClause}${joinClause} ${whereClause} ORDER BY w.startTime DESC LIMIT ? OFFSET ?`;
365
+ return {
366
+ sql,
367
+ bindings
368
+ };
369
+ };
370
+ const listSteps = async (limit, offset, instanceId) => {
371
+ const result = await context.D1.prepare(
372
+ /* sql */
373
+ `SELECT * FROM StepTable
374
+ WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
375
+ ORDER BY startTime ASC LIMIT ? OFFSET ?`
376
+ ).bind(context.tenantId, instanceId ?? null, instanceId ?? null, limit, offset).all();
377
+ return result.results ? result.results.map((row) => ({
378
+ instanceId: row.instanceId,
379
+ name: row.stepName,
380
+ metadata: tryDeserializeObj(row.stepMetadata, internalSerializer),
381
+ status: row.stepStatus,
382
+ startTime: row.startTime,
383
+ endTime: row.endTime,
384
+ result: row.result ? tryDeserializeObj(row.result, internalSerializer) : null,
385
+ error: row.error ? tryDeserializeObj(row.error, internalSerializer) : null
386
+ })) : [];
387
+ };
388
+ const listWorkflows = async (limit, offset, filter) => {
389
+ const { sql, bindings } = buildFilteredWorkflowQuery(filter);
390
+ const result = await context.D1.prepare(sql).bind(...bindings, limit, offset).all();
391
+ return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, internalSerializer)) : [];
392
+ };
393
+ const getWorkflowByParentId = async (parentInstanceId) => {
394
+ const result = await context.D1.prepare(
395
+ /* sql */
396
+ `SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`
397
+ ).bind(parentInstanceId, context.tenantId).all();
398
+ return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, internalSerializer)) : null;
399
+ };
400
+ const getWorkflowTypesByTenantId = async (tenantId) => {
401
+ const result = await context.D1.prepare(
402
+ /* sql */
403
+ `SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`
404
+ ).bind(tenantId).all();
405
+ if (!result.results) return [];
406
+ return result.results.map((row) => row.workflowType);
407
+ };
408
+ const getWorkflowProperties = async (instanceId) => {
409
+ const result = await context.D1.prepare(
410
+ /* sql */
411
+ `SELECT * FROM WorkflowProperties WHERE instanceId = ?`
412
+ ).bind(instanceId).all();
413
+ if (!result.results) return [];
414
+ return result.results.map((row) => ({
415
+ key: row.key,
416
+ valueType: row.valueType,
417
+ value: tryDeserializeObj(row.value, internalSerializer)
418
+ }));
419
+ };
420
+ /** This function gets the basic data of a workflow, without populating any of it's complex fields */
421
+ const getWorkflowShallow = async (instanceId) => {
422
+ const result = await context.D1.prepare(
423
+ /* sql */
424
+ `SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`
425
+ ).bind(instanceId, context.tenantId).first();
426
+ if (!result) return null;
427
+ const workflow = workflowTableRowToWorkflowRun(result, internalSerializer);
428
+ return workflow;
429
+ };
430
+ const getWorkflow = async (instanceId) => {
431
+ const workflow = await getWorkflowShallow(instanceId);
432
+ if (!workflow) return null;
433
+ const steps = await listSteps(100, 0, instanceId);
434
+ const retryWorkflows = await getWorkflowByParentId(instanceId);
435
+ const parentWorkflow = workflow.parentInstanceId ? await getWorkflowShallow(workflow.parentInstanceId) : null;
436
+ workflow.retries = retryWorkflows;
437
+ retryWorkflows?.forEach((retryWorkflow) => {
438
+ retryWorkflow.isRetryOf = workflow;
439
+ });
440
+ workflow.isRetryOf = parentWorkflow;
441
+ const allLogs = await context.D1.prepare(
442
+ /* sql */
443
+ `SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`
444
+ ).bind(instanceId).all();
445
+ const logs = allLogs.results?.map((logRow) => ({
446
+ instanceId: logRow.instanceId,
447
+ stepName: logRow.stepName,
448
+ log: logRow.log,
449
+ timestamp: logRow.timestamp,
450
+ type: logRow.type
451
+ }));
452
+ steps.forEach((step) => {
453
+ step.logs = logs?.filter((log) => log.stepName === step.name) ?? [];
454
+ });
455
+ workflow.steps = steps;
456
+ workflow.logs = logs;
457
+ const properties = await getWorkflowProperties(instanceId);
458
+ workflow.properties = properties;
459
+ return workflow;
460
+ };
461
+ const getStep = async (instanceId, stepName) => {
462
+ const result = await context.D1.prepare(
463
+ /* sql */
464
+ `SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`
465
+ ).bind(instanceId, stepName, context.tenantId).all();
466
+ if (!result.results || result.results.length === 0) return null;
467
+ const row = result.results[0];
468
+ if (!row) return null;
469
+ const step = {
470
+ instanceId: row.instanceId,
471
+ name: row.stepName,
472
+ metadata: tryDeserializeObj(row.stepMetadata, internalSerializer),
473
+ status: row.stepStatus,
474
+ startTime: row.startTime,
475
+ endTime: row.endTime,
476
+ result: row.result ? tryDeserializeObj(row.result, internalSerializer) : null,
477
+ error: row.error ? tryDeserializeObj(row.error, internalSerializer) : null,
478
+ logs: []
479
+ };
480
+ const logResult = await context.D1.prepare(
481
+ /* sql */
482
+ `SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`
483
+ ).bind(instanceId, stepName).all();
484
+ if (logResult.results) step.logs = logResult.results.map((logRow) => ({
485
+ instanceId: logRow.instanceId,
486
+ stepName: logRow.stepName,
487
+ log: logRow.log,
488
+ timestamp: logRow.timestamp,
489
+ type: logRow.type
490
+ }));
491
+ return step;
492
+ };
493
+ const getPropertiesKeys = async (instanceId) => {
494
+ const result = await context.D1.prepare(
495
+ /* sql */
496
+ `
497
+ SELECT DISTINCT key, valueType FROM PropertiesTable
498
+ WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
499
+ `
500
+ ).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all();
501
+ if (!result.results) return [];
502
+ return result.results.map((row) => ({
503
+ key: row.key,
504
+ valueType: row.valueType
505
+ }));
506
+ };
228
507
  return {
229
- workflowType,
230
- metadata: metadata || {},
231
- _callback: callback
508
+ listSteps,
509
+ getStep,
510
+ listWorkflows,
511
+ getWorkflow,
512
+ getWorkflowTypesByTenantId,
513
+ getPropertiesKeys
514
+ };
515
+ };
516
+
517
+ //#endregion
518
+ //#region src/observableWorkflows/createQueueWorkflowContext.ts
519
+ function createQueueWorkflowContext(options) {
520
+ const enqueueWorkflow = async (workflow, tenantId, input, initialName) => {
521
+ await options.QUEUE.send({
522
+ type: "workflow-run",
523
+ workflowType: workflow.workflowType,
524
+ workflowName: initialName,
525
+ input,
526
+ tenantId
527
+ });
528
+ };
529
+ const enqueueRetryWorkflow = async (workflow, tenantId, oldInstanceId) => {
530
+ await options.QUEUE.send({
531
+ type: "workflow-retry",
532
+ workflowType: workflow.workflowType,
533
+ retryInstanceId: oldInstanceId,
534
+ tenantId
535
+ });
536
+ };
537
+ const handleWorkflowQueueMessage = async (message, env, workflowResolver) => {
538
+ const workflowFunction = workflowResolver(message.workflowType);
539
+ if (!workflowFunction) throw new Error(`Workflow ${message.workflowType} not found`);
540
+ const wfc = options.workflowContext;
541
+ if (message.type === "workflow-run") {
542
+ console.log("running workflow", message.input);
543
+ await wfc.call({
544
+ input: message.input,
545
+ workflow: workflowFunction,
546
+ workflowName: message.workflowName,
547
+ tenantId: message.tenantId
548
+ });
549
+ } else if (message.type === "workflow-retry") {
550
+ console.log("retrying workflow", message.retryInstanceId);
551
+ await wfc.retry(workflowFunction, message.retryInstanceId);
552
+ }
553
+ };
554
+ return {
555
+ enqueueWorkflow,
556
+ enqueueRetryWorkflow,
557
+ handleWorkflowQueueMessage
232
558
  };
233
559
  }
234
560
 
@@ -263,7 +589,7 @@ async function createStepContext(context) {
263
589
  } else console.warn("temp: found existing step with different status", row);
264
590
  } else console.warn("temp: no existing step found");
265
591
  } else console.warn("temp: not trying to reuse successful steps");
266
- let waitFor = [];
592
+ const waitFor = [];
267
593
  const startTime = Date.now();
268
594
  const stepStatus = "pending";
269
595
  const stepMetadata = context.serializer.serialize(stepMetadataParam || {});
@@ -351,14 +677,6 @@ async function createStepContext(context) {
351
677
  return step;
352
678
  }
353
679
 
354
- //#endregion
355
- //#region src/observableWorkflows/defaultImplementations.ts
356
- const defaultSerializer = {
357
- serialize: JSON.stringify,
358
- deserialize: JSON.parse
359
- };
360
- const defaultIdFactory = () => crypto.randomUUID();
361
-
362
680
  //#endregion
363
681
  //#region src/observableWorkflows/createWorkflowContext.ts
364
682
  function createWorkflowContext(options) {
@@ -387,7 +705,7 @@ function createWorkflowContext(options) {
387
705
  parentInstanceId,
388
706
  tenantId
389
707
  });
390
- let waitFor = [];
708
+ const waitFor = [];
391
709
  let logOrder = 0;
392
710
  const log = (message, type = "info") => {
393
711
  logOrder++;
@@ -494,204 +812,16 @@ function createWorkflowContext(options) {
494
812
  }
495
813
 
496
814
  //#endregion
497
- //#region src/observableWorkflows/createQueueWorkflowContext.ts
498
- function createQueueWorkflowContext(options) {
499
- const enqueueWorkflow = async (workflow, tenantId, input, initialName) => {
500
- await options.QUEUE.send({
501
- type: "workflow-run",
502
- workflowType: workflow.workflowType,
503
- workflowName: initialName,
504
- input,
505
- tenantId
506
- });
507
- };
508
- const enqueueRetryWorkflow = async (workflow, tenantId, oldInstanceId) => {
509
- await options.QUEUE.send({
510
- type: "workflow-retry",
511
- workflowType: workflow.workflowType,
512
- retryInstanceId: oldInstanceId,
513
- tenantId
514
- });
515
- };
516
- const handleWorkflowQueueMessage = async (message, env, workflowResolver) => {
517
- const workflowFunction = workflowResolver(message.workflowType);
518
- if (!workflowFunction) throw new Error(`Workflow ${message.workflowType} not found`);
519
- const wfc = options.workflowContext;
520
- if (message.type === "workflow-run") {
521
- console.log("running workflow", message.input);
522
- await wfc.call({
523
- input: message.input,
524
- workflow: workflowFunction,
525
- workflowName: message.workflowName,
526
- tenantId: message.tenantId
527
- });
528
- } else if (message.type === "workflow-retry") {
529
- console.log("retrying workflow", message.retryInstanceId);
530
- await wfc.retry(workflowFunction, message.retryInstanceId);
531
- }
532
- };
815
+ //#region src/observableWorkflows/defineWorkflow.ts
816
+ function defineWorkflow(workflow, callback) {
817
+ const metadata = typeof workflow === "string" ? void 0 : workflow.metadata;
818
+ const workflowType = typeof workflow === "string" ? workflow : workflow.workflowType;
533
819
  return {
534
- enqueueWorkflow,
535
- enqueueRetryWorkflow,
536
- handleWorkflowQueueMessage
820
+ workflowType,
821
+ metadata: metadata || {},
822
+ _callback: callback
537
823
  };
538
824
  }
539
825
 
540
- //#endregion
541
- //#region src/observableWorkflows/createLogAccessor.ts
542
- const createLogAccessor = (context) => {
543
- const internalSerializer = context.serializer ?? defaultSerializer;
544
- const listSteps = async (limit, offset, instanceId) => {
545
- const result = await context.D1.prepare(
546
- /* sql */
547
- `SELECT * FROM StepTable
548
- WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
549
- ORDER BY startTime ASC LIMIT ? OFFSET ?`
550
- ).bind(context.tenantId, instanceId ?? null, instanceId ?? null, limit, offset).all();
551
- return result.results ? result.results.map((row) => ({
552
- instanceId: row.instanceId,
553
- name: row.stepName,
554
- metadata: tryDeserializeObj(row.stepMetadata, internalSerializer),
555
- status: row.stepStatus,
556
- startTime: row.startTime,
557
- endTime: row.endTime,
558
- result: row.result ? tryDeserializeObj(row.result, internalSerializer) : null,
559
- error: row.error ? tryDeserializeObj(row.error, internalSerializer) : null
560
- })) : [];
561
- };
562
- const listWorkflows = async (limit, offset) => {
563
- const result = await context.D1.prepare(
564
- /* sql */
565
- `SELECT *
566
- FROM WorkflowTable
567
- WHERE tenantId = ?
568
- ORDER BY startTime DESC LIMIT ? OFFSET ?`
569
- ).bind(context.tenantId, limit, offset).all();
570
- return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, internalSerializer)) : [];
571
- };
572
- const getWorkflowByParentId = async (parentInstanceId) => {
573
- const result = await context.D1.prepare(
574
- /* sql */
575
- `SELECT * FROM WorkflowTable WHERE parentInstanceId = ? AND tenantId = ?`
576
- ).bind(parentInstanceId, context.tenantId).all();
577
- return result.results ? result.results.map((row) => workflowTableRowToWorkflowRun(row, internalSerializer)) : null;
578
- };
579
- const getWorkflowTypesByTenantId = async (tenantId) => {
580
- const result = await context.D1.prepare(
581
- /* sql */
582
- `SELECT DISTINCT workflowType FROM WorkflowTable WHERE tenantId = ?`
583
- ).bind(tenantId).all();
584
- if (!result.results) return [];
585
- return result.results.map((row) => row.workflowType);
586
- };
587
- const getWorkflowProperties = async (instanceId) => {
588
- const result = await context.D1.prepare(
589
- /* sql */
590
- `SELECT * FROM WorkflowProperties WHERE instanceId = ?`
591
- ).bind(instanceId).all();
592
- if (!result.results) return [];
593
- return result.results.map((row) => ({
594
- key: row.key,
595
- valueType: row.valueType,
596
- value: tryDeserializeObj(row.value, internalSerializer)
597
- }));
598
- };
599
- /** This function gets the basic data of a workflow, without populating any of it's complex fields */
600
- const getWorkflowShallow = async (instanceId) => {
601
- const result = await context.D1.prepare(
602
- /* sql */
603
- `SELECT * FROM WorkflowTable WHERE instanceId = ? AND tenantId = ?`
604
- ).bind(instanceId, context.tenantId).first();
605
- if (!result) return null;
606
- const workflow = workflowTableRowToWorkflowRun(result, internalSerializer);
607
- return workflow;
608
- };
609
- const getWorkflow = async (instanceId) => {
610
- const workflow = await getWorkflowShallow(instanceId);
611
- if (!workflow) return null;
612
- const steps = await listSteps(100, 0, instanceId);
613
- const retryWorkflows = await getWorkflowByParentId(instanceId);
614
- const parentWorkflow = workflow.parentInstanceId ? await getWorkflowShallow(workflow.parentInstanceId) : null;
615
- workflow.retries = retryWorkflows;
616
- retryWorkflows?.forEach((retryWorkflow) => {
617
- retryWorkflow.isRetryOf = workflow;
618
- });
619
- workflow.isRetryOf = parentWorkflow;
620
- const allLogs = await context.D1.prepare(
621
- /* sql */
622
- `SELECT * FROM LogTable WHERE instanceId = ? ORDER BY timestamp ASC, logOrder ASC`
623
- ).bind(instanceId).all();
624
- const logs = allLogs.results?.map((logRow) => ({
625
- instanceId: logRow.instanceId,
626
- stepName: logRow.stepName,
627
- log: logRow.log,
628
- timestamp: logRow.timestamp,
629
- type: logRow.type
630
- }));
631
- steps.forEach((step) => {
632
- step.logs = logs?.filter((log) => log.stepName === step.name) ?? [];
633
- });
634
- workflow.steps = steps;
635
- workflow.logs = logs;
636
- const properties = await getWorkflowProperties(instanceId);
637
- workflow.properties = properties;
638
- return workflow;
639
- };
640
- const getStep = async (instanceId, stepName) => {
641
- const result = await context.D1.prepare(
642
- /* sql */
643
- `SELECT * FROM StepTable WHERE instanceId = ? AND stepName = ? AND tenantId = ?`
644
- ).bind(instanceId, stepName, context.tenantId).all();
645
- if (!result.results || result.results.length === 0) return null;
646
- const row = result.results[0];
647
- if (!row) return null;
648
- const step = {
649
- instanceId: row.instanceId,
650
- name: row.stepName,
651
- metadata: tryDeserializeObj(row.stepMetadata, internalSerializer),
652
- status: row.stepStatus,
653
- startTime: row.startTime,
654
- endTime: row.endTime,
655
- result: row.result ? tryDeserializeObj(row.result, internalSerializer) : null,
656
- error: row.error ? tryDeserializeObj(row.error, internalSerializer) : null,
657
- logs: []
658
- };
659
- const logResult = await context.D1.prepare(
660
- /* sql */
661
- `SELECT * FROM LogTable WHERE instanceId = ? AND stepName = ? ORDER BY logOrder ASC`
662
- ).bind(instanceId, stepName).all();
663
- if (logResult.results) step.logs = logResult.results.map((logRow) => ({
664
- instanceId: logRow.instanceId,
665
- stepName: logRow.stepName,
666
- log: logRow.log,
667
- timestamp: logRow.timestamp,
668
- type: logRow.type
669
- }));
670
- return step;
671
- };
672
- const getPropertiesKeys = async (instanceId) => {
673
- const result = await context.D1.prepare(
674
- /* sql */
675
- `
676
- SELECT DISTINCT key, valueType FROM PropertiesTable
677
- WHERE tenantId = ? AND (? IS NULL OR instanceId = ?)
678
- `
679
- ).bind(context.tenantId, instanceId ?? null, instanceId ?? null).all();
680
- if (!result.results) return [];
681
- return result.results.map((row) => ({
682
- key: row.key,
683
- valueType: row.valueType
684
- }));
685
- };
686
- return {
687
- listSteps,
688
- getStep,
689
- listWorkflows,
690
- getWorkflow,
691
- getWorkflowTypesByTenantId,
692
- getPropertiesKeys
693
- };
694
- };
695
-
696
826
  //#endregion
697
827
  export { createLogAccessor, createQueueWorkflowContext, createStepContext, createWorkflowContext, defaultIdFactory, defaultSerializer, 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.5.0",
3
+ "version": "0.6.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/vitest-pool-workers": "^0.8.36",
30
31
  "@cloudflare/workers-types": "^4.20250515.0",
31
32
  "@sxzz/eslint-config": "^7.0.1",
32
33
  "@sxzz/prettier-config": "^2.2.1",
@@ -37,7 +38,8 @@
37
38
  "tsdown": "^0.11.8",
38
39
  "tsx": "^4.19.4",
39
40
  "typescript": "^5.8.3",
40
- "vitest": "^3.1.3"
41
+ "vitest": "^3.1.3",
42
+ "wrangler": "^4.19.1"
41
43
  },
42
44
  "engines": {
43
45
  "node": ">=20.18.0"