@voyantjs/workflow-runs 0.21.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/README.md +44 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/recorder.d.ts +75 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +217 -0
- package/dist/routes.d.ts +33 -0
- package/dist/routes.d.ts.map +1 -0
- package/dist/routes.js +205 -0
- package/dist/runner.d.ts +103 -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 +96 -0
- package/dist/service.d.ts +32 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +54 -0
- package/package.json +75 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@voyantjs/workflow-runs` — schema for the lightweight observability
|
|
3
|
+
* surface that records in-process workflow lifecycles (saga steps the
|
|
4
|
+
* `@voyantjs/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 `@voyantjs/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 "@voyantjs/db";
|
|
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
|
+
index("idx_workflow_runs_tags_gin").using("gin", sql `${table.tags}`),
|
|
73
|
+
check("ck_workflow_runs_completion", sql `(
|
|
74
|
+
${table.status} = 'running' AND ${table.completedAt} IS NULL
|
|
75
|
+
) OR (
|
|
76
|
+
${table.status} <> 'running' AND ${table.completedAt} IS NOT NULL
|
|
77
|
+
)`),
|
|
78
|
+
]);
|
|
79
|
+
export const workflowRunSteps = pgTable("workflow_run_steps", {
|
|
80
|
+
id: typeId("workflow_run_steps"),
|
|
81
|
+
runId: typeIdRef("run_id")
|
|
82
|
+
.notNull()
|
|
83
|
+
.references(() => workflowRuns.id, { onDelete: "cascade" }),
|
|
84
|
+
stepName: text("step_name").notNull(),
|
|
85
|
+
/** Order within the workflow — incremented as the step is reached. */
|
|
86
|
+
sequence: integer("sequence").notNull(),
|
|
87
|
+
status: workflowRunStepStatusEnum("status").notNull().default("running"),
|
|
88
|
+
output: jsonb("output").$type(),
|
|
89
|
+
error: jsonb("error").$type(),
|
|
90
|
+
startedAt: timestamp("started_at", { withTimezone: true }).notNull().defaultNow(),
|
|
91
|
+
completedAt: timestamp("completed_at", { withTimezone: true }),
|
|
92
|
+
durationMs: integer("duration_ms"),
|
|
93
|
+
}, (table) => [
|
|
94
|
+
index("idx_workflow_run_steps_run").on(table.runId, table.sequence),
|
|
95
|
+
index("idx_workflow_run_steps_status").on(table.status),
|
|
96
|
+
]);
|
|
@@ -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;mBAiC5B,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,54 @@
|
|
|
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
|
+
conds.push(sql `${workflowRuns.tags} @> ${JSON.stringify([query.tag])}::jsonb`);
|
|
21
|
+
}
|
|
22
|
+
const where = conds.length > 0 ? and(...conds) : undefined;
|
|
23
|
+
const [rows, countRow] = await Promise.all([
|
|
24
|
+
db
|
|
25
|
+
.select()
|
|
26
|
+
.from(workflowRuns)
|
|
27
|
+
.where(where)
|
|
28
|
+
.orderBy(desc(workflowRuns.startedAt))
|
|
29
|
+
.limit(limit)
|
|
30
|
+
.offset(offset),
|
|
31
|
+
db.select({ count: sql `count(*)::int` }).from(workflowRuns).where(where),
|
|
32
|
+
]);
|
|
33
|
+
return {
|
|
34
|
+
data: rows,
|
|
35
|
+
total: countRow[0]?.count ?? 0,
|
|
36
|
+
limit,
|
|
37
|
+
offset,
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
async getRunById(db, id) {
|
|
41
|
+
const [run] = await db.select().from(workflowRuns).where(eq(workflowRuns.id, id)).limit(1);
|
|
42
|
+
if (!run)
|
|
43
|
+
return null;
|
|
44
|
+
const steps = await db
|
|
45
|
+
.select()
|
|
46
|
+
.from(workflowRunSteps)
|
|
47
|
+
.where(eq(workflowRunSteps.runId, id))
|
|
48
|
+
.orderBy(workflowRunSteps.sequence);
|
|
49
|
+
return { run, steps };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
function clamp(n, min, max) {
|
|
53
|
+
return Math.max(min, Math.min(max, Math.floor(n)));
|
|
54
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/workflow-runs",
|
|
3
|
+
"version": "0.21.0",
|
|
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
|
+
".": "./src/index.ts",
|
|
9
|
+
"./schema": "./src/schema.ts",
|
|
10
|
+
"./service": "./src/service.ts",
|
|
11
|
+
"./routes": "./src/routes.ts",
|
|
12
|
+
"./recorder": "./src/recorder.ts"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"lint": "biome check src/",
|
|
17
|
+
"test": "vitest run --passWithNoTests",
|
|
18
|
+
"build": "tsc -p tsconfig.json",
|
|
19
|
+
"clean": "rm -rf dist",
|
|
20
|
+
"prepack": "pnpm run build"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@voyantjs/core": "workspace:*",
|
|
24
|
+
"@voyantjs/db": "workspace:*",
|
|
25
|
+
"@voyantjs/hono": "workspace:*",
|
|
26
|
+
"drizzle-orm": "^0.45.2",
|
|
27
|
+
"hono": "^4.12.10",
|
|
28
|
+
"zod": "^4.3.6"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@voyantjs/voyant-typescript-config": "workspace:*",
|
|
32
|
+
"typescript": "^6.0.2",
|
|
33
|
+
"vitest": "^4.1.2"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public",
|
|
40
|
+
"main": "./dist/index.js",
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.ts",
|
|
45
|
+
"import": "./dist/index.js",
|
|
46
|
+
"default": "./dist/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./schema": {
|
|
49
|
+
"types": "./dist/schema.d.ts",
|
|
50
|
+
"import": "./dist/schema.js",
|
|
51
|
+
"default": "./dist/schema.js"
|
|
52
|
+
},
|
|
53
|
+
"./service": {
|
|
54
|
+
"types": "./dist/service.d.ts",
|
|
55
|
+
"import": "./dist/service.js",
|
|
56
|
+
"default": "./dist/service.js"
|
|
57
|
+
},
|
|
58
|
+
"./routes": {
|
|
59
|
+
"types": "./dist/routes.d.ts",
|
|
60
|
+
"import": "./dist/routes.js",
|
|
61
|
+
"default": "./dist/routes.js"
|
|
62
|
+
},
|
|
63
|
+
"./recorder": {
|
|
64
|
+
"types": "./dist/recorder.d.ts",
|
|
65
|
+
"import": "./dist/recorder.js",
|
|
66
|
+
"default": "./dist/recorder.js"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "https://github.com/voyantjs/voyant.git",
|
|
73
|
+
"directory": "packages/workflow-runs"
|
|
74
|
+
}
|
|
75
|
+
}
|