@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.
- package/dist/index.d.ts +123 -24
- package/dist/index.js +342 -212
- 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
|
-
|
|
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/
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
498
|
-
function
|
|
499
|
-
const
|
|
500
|
-
|
|
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
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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.
|
|
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"
|