@voyantjs/workflow-runs 0.41.1 → 0.41.2
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/README.md +48 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/recorder.d.ts +3 -1
- package/dist/recorder.d.ts.map +1 -1
- package/dist/recorder.js +27 -2
- package/dist/workflows.d.ts +38 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +154 -0
- package/package.json +12 -5
package/README.md
CHANGED
|
@@ -72,3 +72,51 @@ mountWorkflowRunsAdminRoutes(hono, {
|
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
For self-hosted workflow services, keep runner registration close to the code that mounts the workflow service. The registry should dispatch to your external workflow server instead of importing worker-only runtime code into the admin API process. The resume path sends `ctx.resumeFromStep` plus `ctx.seedResults`; the self-host server starts a new run, pre-populates the journal with the seeded step outputs, and executes from the failed step onward.
|
|
75
|
+
|
|
76
|
+
## Record `@voyantjs/workflows` executions
|
|
77
|
+
|
|
78
|
+
Use `recordedWorkflow` as a drop-in replacement for `workflow(...)` when a
|
|
79
|
+
workflow should appear in the workflow runs admin UI. The helper records start,
|
|
80
|
+
success, and failure rows in `workflow_runs` without repeating recorder
|
|
81
|
+
boilerplate in every workflow body.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { recordedWorkflow } from "@voyantjs/workflow-runs"
|
|
85
|
+
|
|
86
|
+
export const generatePdfWorkflow = recordedWorkflow({
|
|
87
|
+
id: "products.generate-pdf",
|
|
88
|
+
tags: ["products"],
|
|
89
|
+
async run(input, ctx) {
|
|
90
|
+
const renderer = ctx.services.resolve("products:pdf-renderer")
|
|
91
|
+
return renderer.generate(input)
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
By default, `recordedWorkflow` resolves a Drizzle database from
|
|
97
|
+
`ctx.services.resolve("db")`. It records the workflow id, trigger, run id as the
|
|
98
|
+
correlation id, configured/runtime tags, input, result, parent run id for child
|
|
99
|
+
workflow triggers, and errors. Recording is best-effort: database or serializer
|
|
100
|
+
failures do not fail the workflow execution.
|
|
101
|
+
|
|
102
|
+
You can customize the database service key or payload serializers:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
export const syncCatalogWorkflow = recordedWorkflow(
|
|
106
|
+
{
|
|
107
|
+
id: "catalog.sync",
|
|
108
|
+
async run(input, ctx) {
|
|
109
|
+
return ctx.services.resolve("catalog:sync").run(input)
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
dbServiceName: "postgres",
|
|
114
|
+
input: ({ input }) => ({ catalogId: input.catalogId }),
|
|
115
|
+
result: ({ output }) => ({ changed: output.changed }),
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
This helper only records observability data. Rerun and resume support still uses
|
|
121
|
+
`WorkflowRunnerRegistry` registration so apps can choose which workflows are
|
|
122
|
+
safe to dispatch from the admin UI.
|
package/dist/index.d.ts
CHANGED
|
@@ -21,4 +21,5 @@ export { mountWorkflowRunsAdminRoutes } from "./routes.js";
|
|
|
21
21
|
export { type WorkflowIdempotency, type WorkflowRerunContext, type WorkflowResumeContext, type WorkflowRunner, WorkflowRunnerRegistry, } from "./runner.js";
|
|
22
22
|
export { type NewWorkflowRun, type NewWorkflowRunStep, type WorkflowRun, type WorkflowRunErrorPayload, type WorkflowRunStep, workflowRunStatusEnum, workflowRunStepStatusEnum, workflowRunSteps, workflowRuns, } from "./schema.js";
|
|
23
23
|
export { type ListWorkflowRunsQuery, type ListWorkflowRunsResult, workflowRunsService, } from "./service.js";
|
|
24
|
+
export { type RecordedWorkflowOptions, type RecordedWorkflowResultContext, type RecordedWorkflowRunContext, recordedWorkflow, } from "./workflows.js";
|
|
24
25
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,mBAAmB,GACpB,MAAM,cAAc,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EACL,KAAK,qBAAqB,EAC1B,gBAAgB,EAChB,KAAK,mBAAmB,GACzB,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,4BAA4B,EAAE,MAAM,aAAa,CAAA;AAC1D,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,cAAc,EACnB,sBAAsB,GACvB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,WAAW,EAChB,KAAK,uBAAuB,EAC5B,KAAK,eAAe,EACpB,qBAAqB,EACrB,yBAAyB,EACzB,gBAAgB,EAChB,YAAY,GACb,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,EAC3B,mBAAmB,GACpB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,uBAAuB,EAC5B,KAAK,6BAA6B,EAClC,KAAK,0BAA0B,EAC/B,gBAAgB,GACjB,MAAM,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -21,3 +21,4 @@ export { mountWorkflowRunsAdminRoutes } from "./routes.js";
|
|
|
21
21
|
export { WorkflowRunnerRegistry, } from "./runner.js";
|
|
22
22
|
export { workflowRunStatusEnum, workflowRunStepStatusEnum, workflowRunSteps, workflowRuns, } from "./schema.js";
|
|
23
23
|
export { workflowRunsService, } from "./service.js";
|
|
24
|
+
export { recordedWorkflow, } from "./workflows.js";
|
package/dist/recorder.d.ts
CHANGED
|
@@ -71,5 +71,7 @@ export interface WorkflowRunRecorder {
|
|
|
71
71
|
* workflow keeps running. The whole point of this layer is
|
|
72
72
|
* observability, not durability.
|
|
73
73
|
*/
|
|
74
|
-
export declare function beginWorkflowRun(db: PostgresJsDatabase, input: BeginWorkflowRunInput
|
|
74
|
+
export declare function beginWorkflowRun(db: PostgresJsDatabase, input: BeginWorkflowRunInput, options?: {
|
|
75
|
+
reuseRunningRun?: boolean;
|
|
76
|
+
}): Promise<WorkflowRunRecorder>;
|
|
75
77
|
//# sourceMappingURL=recorder.d.ts.map
|
package/dist/recorder.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAUjE,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD;;;;;OAKG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,qBAAqB,
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAUjE,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC7B,IAAI,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACtC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC3B,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC,CAAA;IAC3D,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACrD;;;;;OAKG;IACH,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAChE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACjE,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACvC;AAED;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,EAAE,EAAE,kBAAkB,EACtB,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE;IAAE,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAC1C,OAAO,CAAC,mBAAmB,CAAC,CAgC9B"}
|
package/dist/recorder.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* persistence failures rather than rethrowing, so observability
|
|
25
25
|
* outages never break the underlying business operation.
|
|
26
26
|
*/
|
|
27
|
-
import { eq } from "drizzle-orm";
|
|
27
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
28
28
|
import { workflowRunSteps, workflowRuns, } from "./schema.js";
|
|
29
29
|
/**
|
|
30
30
|
* Start a run and return a recorder bound to its id. The row is
|
|
@@ -35,7 +35,12 @@ import { workflowRunSteps, workflowRuns, } from "./schema.js";
|
|
|
35
35
|
* workflow keeps running. The whole point of this layer is
|
|
36
36
|
* observability, not durability.
|
|
37
37
|
*/
|
|
38
|
-
export async function beginWorkflowRun(db, input) {
|
|
38
|
+
export async function beginWorkflowRun(db, input, options = {}) {
|
|
39
|
+
if (options.reuseRunningRun && input.correlationId) {
|
|
40
|
+
const existingRunId = await findRunningWorkflowRun(db, input);
|
|
41
|
+
if (existingRunId)
|
|
42
|
+
return workflowRunRecorder(db, existingRunId);
|
|
43
|
+
}
|
|
39
44
|
const insert = {
|
|
40
45
|
workflowName: input.workflowName,
|
|
41
46
|
trigger: input.trigger ?? "manual",
|
|
@@ -57,6 +62,26 @@ export async function beginWorkflowRun(db, input) {
|
|
|
57
62
|
}
|
|
58
63
|
if (!runId)
|
|
59
64
|
return noopRecorder();
|
|
65
|
+
return workflowRunRecorder(db, runId);
|
|
66
|
+
}
|
|
67
|
+
async function findRunningWorkflowRun(db, input) {
|
|
68
|
+
const correlationId = input.correlationId;
|
|
69
|
+
if (!correlationId)
|
|
70
|
+
return null;
|
|
71
|
+
try {
|
|
72
|
+
const [row] = await db
|
|
73
|
+
.select({ id: workflowRuns.id })
|
|
74
|
+
.from(workflowRuns)
|
|
75
|
+
.where(and(eq(workflowRuns.workflowName, input.workflowName), eq(workflowRuns.correlationId, correlationId), eq(workflowRuns.status, "running")))
|
|
76
|
+
.orderBy(desc(workflowRuns.startedAt))
|
|
77
|
+
.limit(1);
|
|
78
|
+
return row?.id ?? null;
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function workflowRunRecorder(db, runId) {
|
|
60
85
|
const stepStarts = new Map();
|
|
61
86
|
let nextSequence = 1;
|
|
62
87
|
return {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type WorkflowConfig, type WorkflowContext, type WorkflowDefinition } from "@voyantjs/workflows";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
4
|
+
type JsonRecord = Record<string, unknown>;
|
|
5
|
+
export interface RecordedWorkflowRunContext<TInput> {
|
|
6
|
+
input: TInput;
|
|
7
|
+
ctx: WorkflowContext<TInput>;
|
|
8
|
+
config: WorkflowConfig<TInput, unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface RecordedWorkflowResultContext<TInput, TOutput> extends RecordedWorkflowRunContext<TInput> {
|
|
11
|
+
output: TOutput;
|
|
12
|
+
}
|
|
13
|
+
export interface RecordedWorkflowOptions<TInput, TOutput> {
|
|
14
|
+
/**
|
|
15
|
+
* Database used by the workflow-runs recorder. When omitted, the helper
|
|
16
|
+
* resolves `ctx.services.resolve(dbServiceName)`.
|
|
17
|
+
*/
|
|
18
|
+
db?: PostgresJsDatabase | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<PostgresJsDatabase | null | undefined>);
|
|
19
|
+
/** Service-container key used when `db` is omitted. Defaults to `db`. */
|
|
20
|
+
dbServiceName?: string;
|
|
21
|
+
workflowName?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string>);
|
|
22
|
+
trigger?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
|
|
23
|
+
correlationId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
|
|
24
|
+
tags?: ReadonlyArray<string> | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<ReadonlyArray<string> | null | undefined>);
|
|
25
|
+
input?: false | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<JsonRecord | null | undefined>);
|
|
26
|
+
result?: false | ((args: RecordedWorkflowResultContext<TInput, TOutput>) => MaybePromise<JsonRecord | null | undefined>);
|
|
27
|
+
parentRunId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
|
|
28
|
+
triggeredByUserId?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
|
|
29
|
+
resumeFromStep?: string | ((args: RecordedWorkflowRunContext<TInput>) => MaybePromise<string | null | undefined>);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Declare a `@voyantjs/workflows` workflow whose run lifecycle is mirrored into
|
|
33
|
+
* the `workflow_runs` observability tables. Recording is best-effort: database,
|
|
34
|
+
* metadata, or serialization failures never change workflow execution behavior.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recordedWorkflow<TInput = unknown, TOutput = unknown>(config: WorkflowConfig<TInput, TOutput>, options?: RecordedWorkflowOptions<TInput, TOutput>): WorkflowDefinition<TInput, TOutput>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=workflows.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflows.d.ts","sourceRoot":"","sources":["../src/workflows.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { workflow, } from "@voyantjs/workflows";
|
|
2
|
+
import { beginWorkflowRun, } from "./recorder.js";
|
|
3
|
+
const WAITPOINT_PENDING = Symbol.for("voyant.workflows.waitpointPending");
|
|
4
|
+
/**
|
|
5
|
+
* Declare a `@voyantjs/workflows` workflow whose run lifecycle is mirrored into
|
|
6
|
+
* the `workflow_runs` observability tables. Recording is best-effort: database,
|
|
7
|
+
* metadata, or serialization failures never change workflow execution behavior.
|
|
8
|
+
*/
|
|
9
|
+
export function recordedWorkflow(config, options = {}) {
|
|
10
|
+
return workflow({
|
|
11
|
+
...config,
|
|
12
|
+
async run(input, ctx) {
|
|
13
|
+
const recorder = await startRecording(config, options, input, ctx);
|
|
14
|
+
try {
|
|
15
|
+
const output = await config.run(input, ctx);
|
|
16
|
+
await completeRecording(recorder, options, config, input, ctx, output);
|
|
17
|
+
return output;
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
if (isWaitpointPending(err))
|
|
21
|
+
throw err;
|
|
22
|
+
await failRecording(recorder, err);
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
async function startRecording(config, options, input, ctx) {
|
|
29
|
+
try {
|
|
30
|
+
const args = {
|
|
31
|
+
input,
|
|
32
|
+
ctx,
|
|
33
|
+
config: config,
|
|
34
|
+
};
|
|
35
|
+
const db = await resolveDb(options, args);
|
|
36
|
+
if (!db)
|
|
37
|
+
return null;
|
|
38
|
+
const beginInput = {
|
|
39
|
+
workflowName: await resolveValue(options.workflowName, args, config.id),
|
|
40
|
+
trigger: await resolveValue(options.trigger, args, triggerName(ctx)),
|
|
41
|
+
correlationId: await resolveValue(options.correlationId, args, ctx.run.id),
|
|
42
|
+
tags: [
|
|
43
|
+
...dedupe([...(config.tags ?? []), ...ctx.run.tags, ...(await resolveTags(options, args))]),
|
|
44
|
+
],
|
|
45
|
+
input: await resolveInput(options, args),
|
|
46
|
+
parentRunId: await resolveValue(options.parentRunId, args, parentRunId(ctx)),
|
|
47
|
+
triggeredByUserId: await resolveValue(options.triggeredByUserId, args, triggeredByUserId(ctx)),
|
|
48
|
+
resumeFromStep: await resolveValue(options.resumeFromStep, args, null),
|
|
49
|
+
};
|
|
50
|
+
return await beginWorkflowRun(db, beginInput, { reuseRunningRun: ctx.invocationCount > 1 });
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function completeRecording(recorder, options, config, input, ctx, output) {
|
|
57
|
+
if (!recorder)
|
|
58
|
+
return;
|
|
59
|
+
try {
|
|
60
|
+
await recorder.complete(await resolveResult(options, { input, ctx, config, output }));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// best-effort observability only
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function failRecording(recorder, err) {
|
|
67
|
+
if (!recorder)
|
|
68
|
+
return;
|
|
69
|
+
try {
|
|
70
|
+
await recorder.fail(err);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// best-effort observability only
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
async function resolveDb(options, args) {
|
|
77
|
+
if (typeof options.db === "function")
|
|
78
|
+
return options.db(args);
|
|
79
|
+
if (options.db)
|
|
80
|
+
return options.db;
|
|
81
|
+
const serviceName = options.dbServiceName ?? "db";
|
|
82
|
+
return args.ctx.services.resolve(serviceName);
|
|
83
|
+
}
|
|
84
|
+
async function resolveTags(options, args) {
|
|
85
|
+
if (Array.isArray(options.tags))
|
|
86
|
+
return options.tags;
|
|
87
|
+
if (typeof options.tags === "function")
|
|
88
|
+
return (await options.tags(args)) ?? [];
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
async function resolveInput(options, args) {
|
|
92
|
+
if (options.input === false)
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof options.input === "function")
|
|
95
|
+
return (await options.input(args)) ?? null;
|
|
96
|
+
return toJsonRecord(args.input);
|
|
97
|
+
}
|
|
98
|
+
async function resolveResult(options, args) {
|
|
99
|
+
try {
|
|
100
|
+
if (options.result === false)
|
|
101
|
+
return null;
|
|
102
|
+
if (typeof options.result === "function")
|
|
103
|
+
return (await options.result(args)) ?? null;
|
|
104
|
+
return toJsonRecord(args.output);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function resolveValue(value, args, fallback) {
|
|
111
|
+
if (typeof value === "function") {
|
|
112
|
+
return ((await value(args)) ?? fallback);
|
|
113
|
+
}
|
|
114
|
+
return (value ?? fallback);
|
|
115
|
+
}
|
|
116
|
+
function triggerName(ctx) {
|
|
117
|
+
const trigger = ctx.run.triggeredBy;
|
|
118
|
+
if (trigger.kind === "event")
|
|
119
|
+
return trigger.eventType;
|
|
120
|
+
if (trigger.kind === "schedule")
|
|
121
|
+
return `schedule:${trigger.scheduleId}`;
|
|
122
|
+
if (trigger.kind === "parent")
|
|
123
|
+
return "parent";
|
|
124
|
+
return "api";
|
|
125
|
+
}
|
|
126
|
+
function parentRunId(ctx) {
|
|
127
|
+
const trigger = ctx.run.triggeredBy;
|
|
128
|
+
return trigger.kind === "parent" ? trigger.parentRunId : null;
|
|
129
|
+
}
|
|
130
|
+
function triggeredByUserId(ctx) {
|
|
131
|
+
const trigger = ctx.run.triggeredBy;
|
|
132
|
+
return trigger.kind === "api" ? (trigger.actor ?? null) : null;
|
|
133
|
+
}
|
|
134
|
+
function toJsonRecord(value) {
|
|
135
|
+
if (value === undefined || value === null)
|
|
136
|
+
return null;
|
|
137
|
+
if (isPlainRecord(value))
|
|
138
|
+
return value;
|
|
139
|
+
return { value };
|
|
140
|
+
}
|
|
141
|
+
function isPlainRecord(value) {
|
|
142
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
143
|
+
return false;
|
|
144
|
+
const proto = Object.getPrototypeOf(value);
|
|
145
|
+
return proto === Object.prototype || proto === null;
|
|
146
|
+
}
|
|
147
|
+
function isWaitpointPending(err) {
|
|
148
|
+
return (typeof err === "object" &&
|
|
149
|
+
err !== null &&
|
|
150
|
+
err[WAITPOINT_PENDING] === true);
|
|
151
|
+
}
|
|
152
|
+
function dedupe(values) {
|
|
153
|
+
return [...new Set(values)];
|
|
154
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/workflow-runs",
|
|
3
|
-
"version": "0.41.
|
|
3
|
+
"version": "0.41.2",
|
|
4
4
|
"description": "Workflow run recording, admin routes, and rerun/resume dispatch primitives for Voyant operator apps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,20 +29,27 @@
|
|
|
29
29
|
"types": "./dist/recorder.d.ts",
|
|
30
30
|
"import": "./dist/recorder.js",
|
|
31
31
|
"default": "./dist/recorder.js"
|
|
32
|
+
},
|
|
33
|
+
"./workflows": {
|
|
34
|
+
"types": "./dist/workflows.d.ts",
|
|
35
|
+
"import": "./dist/workflows.js",
|
|
36
|
+
"default": "./dist/workflows.js"
|
|
32
37
|
}
|
|
33
38
|
},
|
|
34
39
|
"dependencies": {
|
|
35
40
|
"drizzle-orm": "^0.45.2",
|
|
36
41
|
"hono": "^4.12.10",
|
|
37
42
|
"zod": "^4.3.6",
|
|
38
|
-
"@voyantjs/core": "0.41.
|
|
39
|
-
"@voyantjs/db": "0.41.
|
|
40
|
-
"@voyantjs/hono": "0.41.
|
|
43
|
+
"@voyantjs/core": "0.41.2",
|
|
44
|
+
"@voyantjs/db": "0.41.2",
|
|
45
|
+
"@voyantjs/hono": "0.41.2",
|
|
46
|
+
"@voyantjs/workflows": "0.41.2"
|
|
41
47
|
},
|
|
42
48
|
"devDependencies": {
|
|
43
49
|
"typescript": "^6.0.2",
|
|
44
50
|
"vitest": "^4.1.2",
|
|
45
|
-
"@voyantjs/voyant-typescript-config": "0.1.0"
|
|
51
|
+
"@voyantjs/voyant-typescript-config": "0.1.0",
|
|
52
|
+
"@voyantjs/workflows-orchestrator": "0.41.2"
|
|
46
53
|
},
|
|
47
54
|
"files": [
|
|
48
55
|
"dist"
|