@voyant-travel/workflow-runs 0.107.10
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/LICENSE +201 -0
- package/README.md +175 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/recorder.d.ts +77 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +242 -0
- package/dist/routes.d.ts +43 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +311 -0
- package/dist/runner.d.ts +121 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +45 -0
- package/dist/schema.d.ts +515 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +97 -0
- package/dist/service.d.ts +32 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +55 -0
- package/dist/workflows.d.ts +38 -0
- package/dist/workflows.d.ts.map +1 -0
- package/dist/workflows.js +154 -0
- package/package.json +80 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyant-travel/workflow-runs` — schema for the lightweight observability
|
|
3
|
+
* surface that records in-process workflow lifecycles (saga steps the
|
|
4
|
+
* `@voyant-travel/core/workflows` primitive runs, plus any other "I'm a
|
|
5
|
+
* workflow" code path that opts in via `recordWorkflowRun`).
|
|
6
|
+
*
|
|
7
|
+
* Distinct from the durable `@voyant-travel/workflows` SDK's
|
|
8
|
+
* `voyant_snapshot_runs` schema — that one needs an orchestrator
|
|
9
|
+
* process to drive it, which doesn't fit the Cloudflare Workers
|
|
10
|
+
* deployment shape. This schema is edge-compatible (postgres-js or
|
|
11
|
+
* neon-http) so templates can register the routes inside their
|
|
12
|
+
* existing API surface.
|
|
13
|
+
*
|
|
14
|
+
* Tags as JSONB array — lets callers query "all runs for booking X"
|
|
15
|
+
* without hard-coding domain joins. The recorder's convention is to
|
|
16
|
+
* tag with `bookingId:<id>`, `paymentSessionId:<id>`, etc.
|
|
17
|
+
*/
|
|
18
|
+
import { typeId, typeIdRef } from "@voyant-travel/db/lib/typeid-column";
|
|
19
|
+
import { sql } from "drizzle-orm";
|
|
20
|
+
import { check, index, integer, jsonb, pgEnum, pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
21
|
+
export const workflowRunStatusEnum = pgEnum("workflow_run_status", [
|
|
22
|
+
"running",
|
|
23
|
+
"succeeded",
|
|
24
|
+
"failed",
|
|
25
|
+
"cancelled",
|
|
26
|
+
]);
|
|
27
|
+
export const workflowRunStepStatusEnum = pgEnum("workflow_run_step_status", [
|
|
28
|
+
"running",
|
|
29
|
+
"succeeded",
|
|
30
|
+
"failed",
|
|
31
|
+
"skipped",
|
|
32
|
+
"compensated",
|
|
33
|
+
]);
|
|
34
|
+
export const workflowRuns = pgTable("workflow_runs", {
|
|
35
|
+
id: typeId("workflow_runs"),
|
|
36
|
+
workflowName: text("workflow_name").notNull(),
|
|
37
|
+
/** Where the run came from — usually an event name or a trigger id. */
|
|
38
|
+
trigger: text("trigger").notNull().default("manual"),
|
|
39
|
+
correlationId: text("correlation_id"),
|
|
40
|
+
/** Free-form domain tags. Convention is `<key>:<value>` strings. */
|
|
41
|
+
tags: jsonb("tags").$type().notNull().default([]),
|
|
42
|
+
status: workflowRunStatusEnum("status").notNull().default("running"),
|
|
43
|
+
input: jsonb("input").$type(),
|
|
44
|
+
result: jsonb("result").$type(),
|
|
45
|
+
/** Compact error payload — message + optional code + step name. */
|
|
46
|
+
error: jsonb("error").$type(),
|
|
47
|
+
/**
|
|
48
|
+
* Parent run id when this run was triggered as a rerun/resume of
|
|
49
|
+
* another run. Self-references the same table; nullable because
|
|
50
|
+
* the original run has no parent.
|
|
51
|
+
*/
|
|
52
|
+
parentRunId: text("parent_run_id"),
|
|
53
|
+
/** User who triggered the rerun/resume (null for system runs). */
|
|
54
|
+
triggeredByUserId: text("triggered_by_user_id"),
|
|
55
|
+
/**
|
|
56
|
+
* When set, the run is a resume — the executor skips steps until
|
|
57
|
+
* (and not including) this step name, hydrating ctx.results from
|
|
58
|
+
* the parent run's recorded step outputs.
|
|
59
|
+
*/
|
|
60
|
+
resumeFromStep: text("resume_from_step"),
|
|
61
|
+
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
|
62
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
63
|
+
durationMs: integer("duration_ms"),
|
|
64
|
+
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
|
65
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
|
66
|
+
}, (table) => [
|
|
67
|
+
index("idx_workflow_runs_workflow").on(table.workflowName, table.startedAt),
|
|
68
|
+
index("idx_workflow_runs_status_started").on(table.status, table.startedAt),
|
|
69
|
+
index("idx_workflow_runs_correlation").on(table.correlationId),
|
|
70
|
+
index("idx_workflow_runs_parent").on(table.parentRunId),
|
|
71
|
+
// GIN index on tags so `tags @> '["bookingId:bk_…"]'::jsonb` is fast.
|
|
72
|
+
// agent-quality: raw-sql reviewed -- owner: workflow-runs; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
73
|
+
index("idx_workflow_runs_tags_gin").using("gin", sql `${table.tags}`),
|
|
74
|
+
check("ck_workflow_runs_completion", sql `(
|
|
75
|
+
${table.status} = 'running' AND ${table.completedAt} IS NULL
|
|
76
|
+
) OR (
|
|
77
|
+
${table.status} <> 'running' AND ${table.completedAt} IS NOT NULL
|
|
78
|
+
)`),
|
|
79
|
+
]);
|
|
80
|
+
export const workflowRunSteps = pgTable("workflow_run_steps", {
|
|
81
|
+
id: typeId("workflow_run_steps"),
|
|
82
|
+
runId: typeIdRef("run_id")
|
|
83
|
+
.notNull()
|
|
84
|
+
.references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
85
|
+
stepName: text("step_name").notNull(),
|
|
86
|
+
/** Order within the workflow — incremented as the step is reached. */
|
|
87
|
+
sequence: integer("sequence").notNull(),
|
|
88
|
+
status: workflowRunStepStatusEnum("status").notNull().default("running"),
|
|
89
|
+
output: jsonb("output").$type(),
|
|
90
|
+
error: jsonb("error").$type(),
|
|
91
|
+
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
|
92
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
93
|
+
durationMs: integer("duration_ms"),
|
|
94
|
+
}, (table) => [
|
|
95
|
+
index("idx_workflow_run_steps_run").on(table.runId, table.sequence),
|
|
96
|
+
index("idx_workflow_run_steps_status").on(table.status),
|
|
97
|
+
]);
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-side service for the workflow runs UI. The recorder writes
|
|
3
|
+
* rows; this is what the dashboard SPA / admin endpoints query.
|
|
4
|
+
*/
|
|
5
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
6
|
+
import { type WorkflowRun, type WorkflowRunStep } from "./schema.js";
|
|
7
|
+
export interface ListWorkflowRunsQuery {
|
|
8
|
+
/** Filter by workflow id (e.g. "checkout-finalize"). */
|
|
9
|
+
workflowName?: string;
|
|
10
|
+
/** Filter by status. */
|
|
11
|
+
status?: "running" | "succeeded" | "failed" | "cancelled";
|
|
12
|
+
/** Filter by tag membership — exact tag string match. */
|
|
13
|
+
tag?: string;
|
|
14
|
+
/** Filter to runs that descend from this parent (children only). */
|
|
15
|
+
parentRunId?: string;
|
|
16
|
+
limit?: number;
|
|
17
|
+
offset?: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ListWorkflowRunsResult {
|
|
20
|
+
data: WorkflowRun[];
|
|
21
|
+
total: number;
|
|
22
|
+
limit: number;
|
|
23
|
+
offset: number;
|
|
24
|
+
}
|
|
25
|
+
export declare const workflowRunsService: {
|
|
26
|
+
listRuns(db: PostgresJsDatabase, query: ListWorkflowRunsQuery): Promise<ListWorkflowRunsResult>;
|
|
27
|
+
getRunById(db: PostgresJsDatabase, id: string): Promise<{
|
|
28
|
+
run: WorkflowRun;
|
|
29
|
+
steps: WorkflowRunStep[];
|
|
30
|
+
} | null>;
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,OAAO,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAkC,MAAM,aAAa,CAAA;AAEpG,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,wBAAwB;IACxB,MAAM,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,WAAW,CAAA;IACzD,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,oEAAoE;IACpE,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,EAAE,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;CACf;AAED,eAAO,MAAM,mBAAmB;iBAExB,kBAAkB,SACf,qBAAqB,GAC3B,OAAO,CAAC,sBAAsB,CAAC;mBAkC5B,kBAAkB,MAClB,MAAM,GACT,OAAO,CAAC;QAAE,GAAG,EAAE,WAAW,CAAC;QAAC,KAAK,EAAE,eAAe,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC;CAUlE,CAAA"}
|
package/dist/service.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read-side service for the workflow runs UI. The recorder writes
|
|
3
|
+
* rows; this is what the dashboard SPA / admin endpoints query.
|
|
4
|
+
*/
|
|
5
|
+
import { and, desc, eq, sql } from "drizzle-orm";
|
|
6
|
+
import { workflowRunSteps, workflowRuns } from "./schema.js";
|
|
7
|
+
export const workflowRunsService = {
|
|
8
|
+
async listRuns(db, query) {
|
|
9
|
+
const limit = clamp(query.limit ?? 50, 1, 200);
|
|
10
|
+
const offset = Math.max(query.offset ?? 0, 0);
|
|
11
|
+
const conds = [];
|
|
12
|
+
if (query.workflowName)
|
|
13
|
+
conds.push(eq(workflowRuns.workflowName, query.workflowName));
|
|
14
|
+
if (query.status)
|
|
15
|
+
conds.push(eq(workflowRuns.status, query.status));
|
|
16
|
+
if (query.parentRunId)
|
|
17
|
+
conds.push(eq(workflowRuns.parentRunId, query.parentRunId));
|
|
18
|
+
if (query.tag) {
|
|
19
|
+
// jsonb contains — `tags @> '["bookingId:bk_…"]'::jsonb`
|
|
20
|
+
// agent-quality: raw-sql reviewed -- owner: workflow-runs; dynamic SQL interpolation uses Drizzle parameter binding or vetted SQL identifiers.
|
|
21
|
+
conds.push(sql `${workflowRuns.tags} @> ${JSON.stringify([query.tag])}::jsonb`);
|
|
22
|
+
}
|
|
23
|
+
const where = conds.length > 0 ? and(...conds) : undefined;
|
|
24
|
+
const [rows, countRow] = await Promise.all([
|
|
25
|
+
db
|
|
26
|
+
.select()
|
|
27
|
+
.from(workflowRuns)
|
|
28
|
+
.where(where)
|
|
29
|
+
.orderBy(desc(workflowRuns.startedAt))
|
|
30
|
+
.limit(limit)
|
|
31
|
+
.offset(offset),
|
|
32
|
+
db.select({ count: sql `count(*)::int` }).from(workflowRuns).where(where),
|
|
33
|
+
]);
|
|
34
|
+
return {
|
|
35
|
+
data: rows,
|
|
36
|
+
total: countRow[0]?.count ?? 0,
|
|
37
|
+
limit,
|
|
38
|
+
offset,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
async getRunById(db, id) {
|
|
42
|
+
const [run] = await db.select().from(workflowRuns).where(eq(workflowRuns.id, id)).limit(1);
|
|
43
|
+
if (!run)
|
|
44
|
+
return null;
|
|
45
|
+
const steps = await db
|
|
46
|
+
.select()
|
|
47
|
+
.from(workflowRunSteps)
|
|
48
|
+
.where(eq(workflowRunSteps.runId, id))
|
|
49
|
+
.orderBy(workflowRunSteps.sequence);
|
|
50
|
+
return { run, steps };
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
function clamp(n, min, max) {
|
|
54
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type WorkflowConfig, type WorkflowContext, type WorkflowDefinition } from "@voyant-travel/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 `@voyant-travel/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 "@voyant-travel/workflows";
|
|
2
|
+
import { beginWorkflowRun, } from "./recorder.js";
|
|
3
|
+
const WAITPOINT_PENDING = Symbol.for("voyant.workflows.waitpointPending");
|
|
4
|
+
/**
|
|
5
|
+
* Declare a `@voyant-travel/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
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyant-travel/workflow-runs",
|
|
3
|
+
"version": "0.107.10",
|
|
4
|
+
"description": "Workflow run recording, admin routes, and rerun/resume dispatch primitives for Voyant operator apps.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./schema": {
|
|
14
|
+
"types": "./dist/schema.d.ts",
|
|
15
|
+
"import": "./dist/schema.js",
|
|
16
|
+
"default": "./dist/schema.js"
|
|
17
|
+
},
|
|
18
|
+
"./service": {
|
|
19
|
+
"types": "./dist/service.d.ts",
|
|
20
|
+
"import": "./dist/service.js",
|
|
21
|
+
"default": "./dist/service.js"
|
|
22
|
+
},
|
|
23
|
+
"./routes": {
|
|
24
|
+
"types": "./dist/routes.d.ts",
|
|
25
|
+
"import": "./dist/routes.js",
|
|
26
|
+
"default": "./dist/routes.js"
|
|
27
|
+
},
|
|
28
|
+
"./recorder": {
|
|
29
|
+
"types": "./dist/recorder.d.ts",
|
|
30
|
+
"import": "./dist/recorder.js",
|
|
31
|
+
"default": "./dist/recorder.js"
|
|
32
|
+
},
|
|
33
|
+
"./workflows": {
|
|
34
|
+
"types": "./dist/workflows.d.ts",
|
|
35
|
+
"import": "./dist/workflows.js",
|
|
36
|
+
"default": "./dist/workflows.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"voyant": {
|
|
40
|
+
"schema": "./schema",
|
|
41
|
+
"requiresSchemas": [
|
|
42
|
+
"@voyant-travel/db"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"drizzle-orm": "^0.45.2",
|
|
47
|
+
"hono": "^4.12.10",
|
|
48
|
+
"zod": "^4.3.6",
|
|
49
|
+
"@voyant-travel/core": "^0.109.0",
|
|
50
|
+
"@voyant-travel/db": "^0.108.0",
|
|
51
|
+
"@voyant-travel/hono": "^0.109.1",
|
|
52
|
+
"@voyant-travel/workflows": "^0.107.10"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"typescript": "^6.0.2",
|
|
56
|
+
"vitest": "^4.1.2",
|
|
57
|
+
"@voyant-travel/voyant-typescript-config": "^0.1.0",
|
|
58
|
+
"@voyant-travel/workflows-orchestrator": "^0.107.10"
|
|
59
|
+
},
|
|
60
|
+
"files": [
|
|
61
|
+
"dist"
|
|
62
|
+
],
|
|
63
|
+
"publishConfig": {
|
|
64
|
+
"access": "public"
|
|
65
|
+
},
|
|
66
|
+
"repository": {
|
|
67
|
+
"type": "git",
|
|
68
|
+
"url": "https://github.com/voyant-travel/voyant.git",
|
|
69
|
+
"directory": "packages/workflow-runs"
|
|
70
|
+
},
|
|
71
|
+
"scripts": {
|
|
72
|
+
"typecheck": "tsc --noEmit",
|
|
73
|
+
"lint": "biome check src/",
|
|
74
|
+
"test": "vitest run --passWithNoTests",
|
|
75
|
+
"build": "tsc -p tsconfig.json",
|
|
76
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo"
|
|
77
|
+
},
|
|
78
|
+
"main": "./dist/index.js",
|
|
79
|
+
"types": "./dist/index.d.ts"
|
|
80
|
+
}
|