alepha 0.19.2 → 0.19.4
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/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/audits/index.d.ts +8 -8
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +90 -34
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +267 -44
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +27 -21
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +167 -34
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/cli/config/index.d.ts +6 -28
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +5 -10
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +11669 -208
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +60 -69
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +5 -0
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +4 -0
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +69 -64
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +6 -2
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +38 -10
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +85 -26
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +25 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +25 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +25 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +25 -73
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +10 -32
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +25 -73
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +2 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/package.json +37 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +239 -25
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +51 -4
- package/src/api/users/services/RegistrationService.ts +38 -9
- package/src/api/users/services/SessionService.ts +62 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/cli/config/defineConfig.ts +17 -46
- package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
- package/src/cli/core/services/PackageManagerUtils.ts +3 -1
- package/src/cli/core/services/ProjectScaffolder.ts +5 -5
- package/src/cli/core/templates/agentMd.ts +14 -5
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/devtools/index.ts +21 -1
- package/src/cli/platform/index.ts +23 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
- package/src/cli/vendor/index.ts +20 -3
- package/src/cli/vendor/services/VendorService.ts +126 -27
- package/src/core/Alepha.ts +10 -0
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/logger/index.ts +6 -1
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$secure.ts +28 -0
- package/tsconfig.base.json +0 -1
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
|
@@ -0,0 +1,1504 @@
|
|
|
1
|
+
import { $atom, $hook, $inject, $module, $state, Alepha, AlephaError, KIND, Primitive, createPrimitive, t } from "alepha";
|
|
2
|
+
import { $job, AlephaApiJobs } from "alepha/api/jobs";
|
|
3
|
+
import { AlephaLock, LockProvider } from "alepha/lock";
|
|
4
|
+
import { $secure } from "alepha/security";
|
|
5
|
+
import { $action, NotFoundError, okSchema } from "alepha/server";
|
|
6
|
+
import { $entity, $repository, DatabaseProvider, db, pageQuerySchema, sql } from "alepha/orm";
|
|
7
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
8
|
+
import { $logger, logEntrySchema } from "alepha/logger";
|
|
9
|
+
//#region ../../src/api/workflows/schemas/workflowActivitySchema.ts
|
|
10
|
+
const workflowActivityPointSchema = t.object({
|
|
11
|
+
date: t.text(),
|
|
12
|
+
completed: t.integer(),
|
|
13
|
+
failed: t.integer()
|
|
14
|
+
});
|
|
15
|
+
const workflowActivityQuerySchema = t.object({ days: t.optional(t.integer({
|
|
16
|
+
minimum: 1,
|
|
17
|
+
maximum: 90
|
|
18
|
+
})) });
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region ../../src/api/workflows/entities/workflowExecutions.ts
|
|
21
|
+
const workflowExecutions = $entity({
|
|
22
|
+
name: "workflow_executions",
|
|
23
|
+
schema: t.object({
|
|
24
|
+
id: db.primaryKey(t.uuid()),
|
|
25
|
+
createdAt: db.createdAt(),
|
|
26
|
+
updatedAt: db.updatedAt(),
|
|
27
|
+
workflowName: t.text(),
|
|
28
|
+
tags: t.optional(t.array(t.text())),
|
|
29
|
+
payload: t.optional(t.record(t.text(), t.any())),
|
|
30
|
+
status: db.default(t.enum([
|
|
31
|
+
"pending",
|
|
32
|
+
"running",
|
|
33
|
+
"waiting_for_signal",
|
|
34
|
+
"completed",
|
|
35
|
+
"failed",
|
|
36
|
+
"timed_out",
|
|
37
|
+
"compensating",
|
|
38
|
+
"compensated",
|
|
39
|
+
"compensation_failed",
|
|
40
|
+
"cancelled"
|
|
41
|
+
]), "pending"),
|
|
42
|
+
currentStep: t.optional(t.text()),
|
|
43
|
+
startedAt: t.optional(t.datetime()),
|
|
44
|
+
completedAt: t.optional(t.datetime()),
|
|
45
|
+
deadlineAt: t.optional(t.datetime()),
|
|
46
|
+
error: t.optional(t.text()),
|
|
47
|
+
errorStep: t.optional(t.text()),
|
|
48
|
+
triggeredBy: t.optional(t.text()),
|
|
49
|
+
triggeredByName: t.optional(t.text()),
|
|
50
|
+
cancelledBy: t.optional(t.text()),
|
|
51
|
+
cancelledByName: t.optional(t.text()),
|
|
52
|
+
key: t.optional(t.nullable(t.text())),
|
|
53
|
+
priority: db.default(t.integer({
|
|
54
|
+
minimum: 0,
|
|
55
|
+
maximum: 3
|
|
56
|
+
}), 2)
|
|
57
|
+
}),
|
|
58
|
+
indexes: [
|
|
59
|
+
{ columns: ["workflowName", "status"] },
|
|
60
|
+
{ columns: [
|
|
61
|
+
"workflowName",
|
|
62
|
+
"status",
|
|
63
|
+
"createdAt"
|
|
64
|
+
] },
|
|
65
|
+
{
|
|
66
|
+
columns: ["workflowName", "key"],
|
|
67
|
+
unique: true,
|
|
68
|
+
where: sql`status NOT IN ('completed', 'failed', 'compensated', 'compensation_failed', 'cancelled')`
|
|
69
|
+
},
|
|
70
|
+
{ columns: ["status", "deadlineAt"] },
|
|
71
|
+
{ columns: ["completedAt"] }
|
|
72
|
+
]
|
|
73
|
+
});
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region ../../src/api/workflows/schemas/workflowExecutionResourceSchema.ts
|
|
76
|
+
const workflowExecutionCanSchema = t.object({
|
|
77
|
+
retry: t.boolean(),
|
|
78
|
+
cancel: t.boolean(),
|
|
79
|
+
compensate: t.boolean(),
|
|
80
|
+
restart: t.boolean(),
|
|
81
|
+
signal: t.optional(t.object({
|
|
82
|
+
stepName: t.text(),
|
|
83
|
+
schema: t.optional(t.any())
|
|
84
|
+
}))
|
|
85
|
+
});
|
|
86
|
+
const workflowExecutionResourceSchema = t.extend(workflowExecutions.schema, { can: workflowExecutionCanSchema }, {
|
|
87
|
+
title: "WorkflowExecutionResource",
|
|
88
|
+
description: "A workflow execution resource."
|
|
89
|
+
});
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region ../../src/api/workflows/entities/workflowStepExecutions.ts
|
|
92
|
+
const workflowStepExecutions = $entity({
|
|
93
|
+
name: "workflow_step_executions",
|
|
94
|
+
schema: t.object({
|
|
95
|
+
id: db.primaryKey(t.uuid()),
|
|
96
|
+
createdAt: db.createdAt(),
|
|
97
|
+
updatedAt: db.updatedAt(),
|
|
98
|
+
workflowExecutionId: db.ref(t.uuid(), () => workflowExecutions.cols.id, { onDelete: "cascade" }),
|
|
99
|
+
stepName: t.text(),
|
|
100
|
+
stepIndex: t.integer(),
|
|
101
|
+
stepType: db.default(t.enum([
|
|
102
|
+
"handler",
|
|
103
|
+
"signal",
|
|
104
|
+
"parallel"
|
|
105
|
+
]), "handler"),
|
|
106
|
+
parentStepId: t.optional(t.uuid()),
|
|
107
|
+
branchName: t.optional(t.text()),
|
|
108
|
+
status: db.default(t.enum([
|
|
109
|
+
"pending",
|
|
110
|
+
"running",
|
|
111
|
+
"completed",
|
|
112
|
+
"failed",
|
|
113
|
+
"skipped",
|
|
114
|
+
"waiting",
|
|
115
|
+
"compensating",
|
|
116
|
+
"compensated",
|
|
117
|
+
"compensation_failed",
|
|
118
|
+
"cancelled"
|
|
119
|
+
]), "pending"),
|
|
120
|
+
attempt: db.default(t.integer(), 0),
|
|
121
|
+
maxAttempts: db.default(t.integer(), 1),
|
|
122
|
+
result: t.optional(t.record(t.text(), t.any())),
|
|
123
|
+
error: t.optional(t.text()),
|
|
124
|
+
startedAt: t.optional(t.datetime()),
|
|
125
|
+
completedAt: t.optional(t.datetime()),
|
|
126
|
+
deadlineAt: t.optional(t.datetime()),
|
|
127
|
+
signalPayload: t.optional(t.record(t.text(), t.any())),
|
|
128
|
+
signalledBy: t.optional(t.text()),
|
|
129
|
+
signalledAt: t.optional(t.datetime())
|
|
130
|
+
}),
|
|
131
|
+
indexes: [
|
|
132
|
+
{ columns: ["workflowExecutionId", "stepName"] },
|
|
133
|
+
{ columns: ["workflowExecutionId", "stepIndex"] },
|
|
134
|
+
{ columns: ["workflowExecutionId", "status"] },
|
|
135
|
+
{ columns: ["status", "deadlineAt"] }
|
|
136
|
+
]
|
|
137
|
+
});
|
|
138
|
+
//#endregion
|
|
139
|
+
//#region ../../src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts
|
|
140
|
+
const workflowStepExecutionResourceSchema = t.extend(workflowStepExecutions.schema, {}, {
|
|
141
|
+
title: "WorkflowStepExecutionResource",
|
|
142
|
+
description: "A workflow step execution resource."
|
|
143
|
+
});
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region ../../src/api/workflows/schemas/workflowExecutionDetailSchema.ts
|
|
146
|
+
const workflowExecutionDetailSchema = t.extend(workflowExecutionResourceSchema, { steps: t.array(workflowStepExecutionResourceSchema) }, {
|
|
147
|
+
title: "WorkflowExecutionDetail",
|
|
148
|
+
description: "A workflow execution with step details."
|
|
149
|
+
});
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region ../../src/api/workflows/schemas/workflowExecutionQuerySchema.ts
|
|
152
|
+
const workflowExecutionQuerySchema = t.extend(pageQuerySchema, {
|
|
153
|
+
workflow: t.optional(t.text({ description: "Filter by workflow name" })),
|
|
154
|
+
status: t.optional(t.enum([
|
|
155
|
+
"pending",
|
|
156
|
+
"running",
|
|
157
|
+
"waiting_for_signal",
|
|
158
|
+
"completed",
|
|
159
|
+
"failed",
|
|
160
|
+
"timed_out",
|
|
161
|
+
"compensating",
|
|
162
|
+
"compensated",
|
|
163
|
+
"compensation_failed",
|
|
164
|
+
"cancelled"
|
|
165
|
+
])),
|
|
166
|
+
from: t.optional(t.datetime({ description: "From date (ISO)" })),
|
|
167
|
+
to: t.optional(t.datetime({ description: "To date (ISO)" }))
|
|
168
|
+
});
|
|
169
|
+
//#endregion
|
|
170
|
+
//#region ../../src/api/workflows/schemas/workflowRegistrationSchema.ts
|
|
171
|
+
const workflowRegistrationSchema = t.object({
|
|
172
|
+
name: t.text(),
|
|
173
|
+
stepCount: t.integer(),
|
|
174
|
+
steps: t.array(t.object({
|
|
175
|
+
name: t.text(),
|
|
176
|
+
type: t.enum([
|
|
177
|
+
"handler",
|
|
178
|
+
"signal",
|
|
179
|
+
"parallel"
|
|
180
|
+
]),
|
|
181
|
+
hasCompensate: t.boolean(),
|
|
182
|
+
hasRetry: t.boolean(),
|
|
183
|
+
timeout: t.optional(t.text())
|
|
184
|
+
})),
|
|
185
|
+
onError: t.enum(["compensate", "fail"]),
|
|
186
|
+
timeout: t.optional(t.text()),
|
|
187
|
+
priority: t.text(),
|
|
188
|
+
tags: t.optional(t.array(t.text())),
|
|
189
|
+
paused: t.boolean(),
|
|
190
|
+
running: t.integer(),
|
|
191
|
+
pending: t.integer(),
|
|
192
|
+
waiting: t.integer(),
|
|
193
|
+
failed: t.integer()
|
|
194
|
+
});
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region ../../src/api/workflows/schemas/workflowStatsSchema.ts
|
|
197
|
+
const workflowStatsSchema = t.object({
|
|
198
|
+
registered: t.integer(),
|
|
199
|
+
running: t.integer(),
|
|
200
|
+
pending: t.integer(),
|
|
201
|
+
waiting: t.integer(),
|
|
202
|
+
completed: t.integer(),
|
|
203
|
+
failed: t.integer(),
|
|
204
|
+
compensated: t.integer(),
|
|
205
|
+
compensationFailed: t.integer(),
|
|
206
|
+
cancelled: t.integer(),
|
|
207
|
+
timedOut: t.integer()
|
|
208
|
+
});
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region ../../src/api/workflows/entities/workflowStepLogs.ts
|
|
211
|
+
const workflowStepLogs = $entity({
|
|
212
|
+
name: "workflow_step_logs",
|
|
213
|
+
schema: t.object({
|
|
214
|
+
id: db.primaryKey(t.uuid()),
|
|
215
|
+
logs: t.array(logEntrySchema)
|
|
216
|
+
})
|
|
217
|
+
});
|
|
218
|
+
//#endregion
|
|
219
|
+
//#region ../../src/api/workflows/schemas/workflowConfigAtom.ts
|
|
220
|
+
const workflowConfig = $atom({
|
|
221
|
+
name: "alepha.workflows",
|
|
222
|
+
description: "Configuration for the workflow engine.",
|
|
223
|
+
schema: t.object({
|
|
224
|
+
defaultStepTimeout: t.integer({ description: "Default step timeout (ms). Used when no per-step timeout is set." }),
|
|
225
|
+
retentionDays: t.integer({ description: "Days to keep completed/failed workflow executions." }),
|
|
226
|
+
recovery: t.object({ staleThreshold: t.integer({ description: "Running step age (ms) before assumed crashed." }) }),
|
|
227
|
+
maxConcurrentWorkflows: t.integer({ description: "Max concurrent running instances per workflow name." }),
|
|
228
|
+
maxStepsPerWorkflow: t.integer({ description: "Safety limit on step count." }),
|
|
229
|
+
drainTimeout: t.integer({ description: "Max time (ms) to wait for in-flight steps during shutdown." }),
|
|
230
|
+
logMaxEntries: t.integer({ description: "Max log entries captured per step execution." })
|
|
231
|
+
}),
|
|
232
|
+
default: {
|
|
233
|
+
defaultStepTimeout: 3e5,
|
|
234
|
+
retentionDays: 30,
|
|
235
|
+
recovery: { staleThreshold: 18e5 },
|
|
236
|
+
maxConcurrentWorkflows: 50,
|
|
237
|
+
maxStepsPerWorkflow: 100,
|
|
238
|
+
drainTimeout: 3e4,
|
|
239
|
+
logMaxEntries: 100
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
//#endregion
|
|
243
|
+
//#region ../../src/api/workflows/providers/WorkflowProvider.ts
|
|
244
|
+
const PRIORITY_MAP = {
|
|
245
|
+
critical: 0,
|
|
246
|
+
high: 1,
|
|
247
|
+
normal: 2,
|
|
248
|
+
low: 3
|
|
249
|
+
};
|
|
250
|
+
var WorkflowProvider = class {
|
|
251
|
+
alepha = $inject(Alepha);
|
|
252
|
+
dt = $inject(DateTimeProvider);
|
|
253
|
+
lockProvider = $inject(LockProvider);
|
|
254
|
+
config = $state(workflowConfig);
|
|
255
|
+
log = $logger();
|
|
256
|
+
executions = $repository(workflowExecutions);
|
|
257
|
+
stepExecutions = $repository(workflowStepExecutions);
|
|
258
|
+
stepLogs = $repository(workflowStepLogs);
|
|
259
|
+
workflows = /* @__PURE__ */ new Map();
|
|
260
|
+
pausedWorkflows = /* @__PURE__ */ new Set();
|
|
261
|
+
inFlight = /* @__PURE__ */ new Set();
|
|
262
|
+
abortControllers = /* @__PURE__ */ new Map();
|
|
263
|
+
logs = /* @__PURE__ */ new Map();
|
|
264
|
+
stopping = false;
|
|
265
|
+
/**
|
|
266
|
+
* When set, step dispatches go through a queue.
|
|
267
|
+
* Set by WorkflowJobs on start.
|
|
268
|
+
*/
|
|
269
|
+
stepDispatch = null;
|
|
270
|
+
register(primitive) {
|
|
271
|
+
if (this.workflows.has(primitive.name)) throw new AlephaError(`Workflow already registered: ${primitive.name}`);
|
|
272
|
+
this.workflows.set(primitive.name, {
|
|
273
|
+
name: primitive.name,
|
|
274
|
+
options: primitive.options
|
|
275
|
+
});
|
|
276
|
+
this.log.debug(`Registered workflow '${primitive.name}'`, { steps: primitive.options.steps.length });
|
|
277
|
+
}
|
|
278
|
+
getRegisteredWorkflows() {
|
|
279
|
+
return this.workflows;
|
|
280
|
+
}
|
|
281
|
+
async start(workflowName, payload, options) {
|
|
282
|
+
const opts = this.getRegistration(workflowName).options;
|
|
283
|
+
const validated = this.alepha.codec.validate(opts.schema, payload);
|
|
284
|
+
const priority = PRIORITY_MAP[options?.priority ?? opts.priority ?? "normal"];
|
|
285
|
+
const status = options?.delay ? "pending" : "running";
|
|
286
|
+
let deadlineAt;
|
|
287
|
+
if (opts.timeout) deadlineAt = this.dt.now().add(this.dt.duration(opts.timeout)).toISOString();
|
|
288
|
+
if (options?.key) {
|
|
289
|
+
const existing = await this.executions.findMany({
|
|
290
|
+
where: {
|
|
291
|
+
workflowName: { eq: workflowName },
|
|
292
|
+
key: { eq: options.key },
|
|
293
|
+
status: { inArray: [
|
|
294
|
+
"pending",
|
|
295
|
+
"running",
|
|
296
|
+
"waiting_for_signal",
|
|
297
|
+
"compensating"
|
|
298
|
+
] }
|
|
299
|
+
},
|
|
300
|
+
limit: 1
|
|
301
|
+
});
|
|
302
|
+
if (existing.length > 0) return existing[0].id;
|
|
303
|
+
}
|
|
304
|
+
const execution = await this.executions.create({
|
|
305
|
+
workflowName,
|
|
306
|
+
payload: validated,
|
|
307
|
+
status,
|
|
308
|
+
priority,
|
|
309
|
+
deadlineAt,
|
|
310
|
+
key: options?.key,
|
|
311
|
+
triggeredBy: options?.triggeredBy,
|
|
312
|
+
triggeredByName: options?.triggeredByName,
|
|
313
|
+
tags: options?.tags ?? opts.tags,
|
|
314
|
+
startedAt: status === "running" ? this.dt.nowISOString() : void 0
|
|
315
|
+
});
|
|
316
|
+
for (let i = 0; i < opts.steps.length; i++) {
|
|
317
|
+
const step = opts.steps[i];
|
|
318
|
+
const retryOpts = step.retry;
|
|
319
|
+
await this.stepExecutions.create({
|
|
320
|
+
workflowExecutionId: execution.id,
|
|
321
|
+
stepName: step.name,
|
|
322
|
+
stepIndex: i,
|
|
323
|
+
stepType: step.type ?? "handler",
|
|
324
|
+
status: "pending",
|
|
325
|
+
maxAttempts: (retryOpts?.retries ?? 0) + 1
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
this.log.info(`Started workflow '${workflowName}'`, {
|
|
329
|
+
workflowId: execution.id,
|
|
330
|
+
steps: opts.steps.length
|
|
331
|
+
});
|
|
332
|
+
await this.alepha.events.emit("workflow:started", {
|
|
333
|
+
workflowName,
|
|
334
|
+
workflowId: execution.id
|
|
335
|
+
}, { catch: true });
|
|
336
|
+
if (status === "running" && !this.stopping) {
|
|
337
|
+
const firstStep = opts.steps[0];
|
|
338
|
+
if (firstStep) await this.dispatchStep(execution.id, firstStep.name, priority);
|
|
339
|
+
else await this.executions.updateById(execution.id, {
|
|
340
|
+
status: "completed",
|
|
341
|
+
completedAt: this.dt.nowISOString()
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
return execution.id;
|
|
345
|
+
}
|
|
346
|
+
async processStep(workflowId, stepName) {
|
|
347
|
+
const promise = this.processStepInner(workflowId, stepName);
|
|
348
|
+
this.inFlight.add(promise);
|
|
349
|
+
try {
|
|
350
|
+
await promise;
|
|
351
|
+
} finally {
|
|
352
|
+
this.inFlight.delete(promise);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
async processStepInner(workflowId, stepName) {
|
|
356
|
+
const lockKey = `workflow:${workflowId}`;
|
|
357
|
+
const lockValue = `${crypto.randomUUID()},${this.dt.nowISOString()}`;
|
|
358
|
+
const [lockId] = (await this.lockProvider.set(lockKey, lockValue, true, 6e5)).split(",");
|
|
359
|
+
if (lockId !== lockValue.split(",")[0]) {
|
|
360
|
+
this.log.debug(`Workflow ${workflowId} locked by another worker, skipping`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
try {
|
|
364
|
+
const workflow = await this.executions.findById(workflowId);
|
|
365
|
+
if (!workflow) return;
|
|
366
|
+
if (workflow.status !== "running" && workflow.status !== "pending") return;
|
|
367
|
+
if (workflow.status === "pending") await this.executions.updateById(workflowId, {
|
|
368
|
+
status: "running",
|
|
369
|
+
startedAt: this.dt.nowISOString()
|
|
370
|
+
});
|
|
371
|
+
const stepDef = this.getRegistration(workflow.workflowName).options.steps.find((s) => s.name === stepName);
|
|
372
|
+
if (!stepDef) return;
|
|
373
|
+
const stepExec = await this.findStepExecution(workflowId, stepName);
|
|
374
|
+
if (!stepExec) return;
|
|
375
|
+
if (stepExec.status !== "pending") return;
|
|
376
|
+
if (stepDef.when) {
|
|
377
|
+
const results = await this.assembleResults(workflowId);
|
|
378
|
+
if (!await stepDef.when({
|
|
379
|
+
payload: workflow.payload,
|
|
380
|
+
results
|
|
381
|
+
})) {
|
|
382
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
383
|
+
status: "skipped",
|
|
384
|
+
completedAt: this.dt.nowISOString()
|
|
385
|
+
});
|
|
386
|
+
await this.alepha.events.emit("workflow:step:skipped", {
|
|
387
|
+
workflowName: workflow.workflowName,
|
|
388
|
+
workflowId,
|
|
389
|
+
stepName
|
|
390
|
+
}, { catch: true });
|
|
391
|
+
await this.advance(workflowId);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
await this.executeHandlerStep(workflow, stepExec, stepDef);
|
|
396
|
+
} finally {
|
|
397
|
+
await this.lockProvider.del(lockKey);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
async executeHandlerStep(workflow, stepExec, stepDef) {
|
|
401
|
+
const workflowId = workflow.id;
|
|
402
|
+
const stepName = stepExec.stepName;
|
|
403
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
404
|
+
status: "running",
|
|
405
|
+
attempt: stepExec.attempt + 1,
|
|
406
|
+
startedAt: this.dt.nowISOString()
|
|
407
|
+
});
|
|
408
|
+
await this.executions.updateById(workflowId, { currentStep: stepName });
|
|
409
|
+
await this.alepha.events.emit("workflow:step:begin", {
|
|
410
|
+
workflowName: workflow.workflowName,
|
|
411
|
+
workflowId,
|
|
412
|
+
stepName
|
|
413
|
+
}, { catch: true });
|
|
414
|
+
const abortController = new AbortController();
|
|
415
|
+
const abortKey = `${workflowId}:${stepName}`;
|
|
416
|
+
this.abortControllers.set(abortKey, abortController);
|
|
417
|
+
const timeoutMs = stepDef.timeout ? this.dt.duration(stepDef.timeout).as("milliseconds") : this.config.defaultStepTimeout;
|
|
418
|
+
const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
|
|
419
|
+
const context = this.alepha.context.createContextId();
|
|
420
|
+
this.logs.set(context, []);
|
|
421
|
+
try {
|
|
422
|
+
await this.alepha.context.run(async () => {
|
|
423
|
+
const results = await this.assembleResults(workflowId);
|
|
424
|
+
const handlerResult = await stepDef.handler({
|
|
425
|
+
payload: workflow.payload,
|
|
426
|
+
results,
|
|
427
|
+
context: {
|
|
428
|
+
workflowId,
|
|
429
|
+
executionId: stepExec.id,
|
|
430
|
+
stepName,
|
|
431
|
+
attempt: stepExec.attempt + 1
|
|
432
|
+
},
|
|
433
|
+
signal: abortController.signal
|
|
434
|
+
});
|
|
435
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
436
|
+
status: "completed",
|
|
437
|
+
result: handlerResult != null ? handlerResult : void 0,
|
|
438
|
+
completedAt: this.dt.nowISOString()
|
|
439
|
+
});
|
|
440
|
+
await this.writeLogs(stepExec.id, context);
|
|
441
|
+
this.log.info(`Workflow step '${stepName}' completed`, { workflowId });
|
|
442
|
+
await this.alepha.events.emit("workflow:step:completed", {
|
|
443
|
+
workflowName: workflow.workflowName,
|
|
444
|
+
workflowId,
|
|
445
|
+
stepName,
|
|
446
|
+
result: handlerResult
|
|
447
|
+
}, { catch: true });
|
|
448
|
+
await this.advance(workflowId);
|
|
449
|
+
}, { context });
|
|
450
|
+
} catch (error) {
|
|
451
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
452
|
+
await this.writeLogs(stepExec.id, context);
|
|
453
|
+
if (abortController.signal.aborted) await this.handleStepFailure(workflow, stepExec, stepDef, /* @__PURE__ */ new Error("Step timed out"), context);
|
|
454
|
+
else await this.handleStepFailure(workflow, stepExec, stepDef, err, context);
|
|
455
|
+
} finally {
|
|
456
|
+
clearTimeout(timeoutId);
|
|
457
|
+
this.abortControllers.delete(abortKey);
|
|
458
|
+
this.logs.delete(context);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async handleStepFailure(workflow, stepExec, stepDef, error, _context) {
|
|
462
|
+
const retryOpts = stepDef.retry;
|
|
463
|
+
if (retryOpts && stepExec.attempt + 1 < stepExec.maxAttempts && (retryOpts.when ? retryOpts.when(error) : true)) {
|
|
464
|
+
const nextScheduledAt = this.computeBackoff(retryOpts, stepExec.attempt + 1);
|
|
465
|
+
this.log.info(`Workflow step '${stepExec.stepName}' failed, scheduling retry`, {
|
|
466
|
+
workflowId: workflow.id,
|
|
467
|
+
error: error.message
|
|
468
|
+
});
|
|
469
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
470
|
+
status: "pending",
|
|
471
|
+
error: error.message,
|
|
472
|
+
deadlineAt: nextScheduledAt
|
|
473
|
+
});
|
|
474
|
+
const delayMs = Math.max(0, new Date(nextScheduledAt).getTime() - this.dt.nowMillis());
|
|
475
|
+
this.dt.createTimeout(() => void this.dispatchStep(workflow.id, stepExec.stepName, workflow.priority), delayMs);
|
|
476
|
+
} else {
|
|
477
|
+
this.log.info(`Workflow step '${stepExec.stepName}' failed permanently`, {
|
|
478
|
+
workflowId: workflow.id,
|
|
479
|
+
error: error.message
|
|
480
|
+
});
|
|
481
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
482
|
+
status: "failed",
|
|
483
|
+
error: error.message,
|
|
484
|
+
completedAt: this.dt.nowISOString()
|
|
485
|
+
});
|
|
486
|
+
await this.alepha.events.emit("workflow:step:failed", {
|
|
487
|
+
workflowName: workflow.workflowName,
|
|
488
|
+
workflowId: workflow.id,
|
|
489
|
+
stepName: stepExec.stepName,
|
|
490
|
+
error
|
|
491
|
+
}, { catch: true });
|
|
492
|
+
if ((this.getRegistration(workflow.workflowName).options.onError ?? "compensate") === "compensate") await this.compensate(workflow.id, {
|
|
493
|
+
failedStep: stepExec.stepName,
|
|
494
|
+
error
|
|
495
|
+
});
|
|
496
|
+
else {
|
|
497
|
+
await this.executions.updateById(workflow.id, {
|
|
498
|
+
status: "failed",
|
|
499
|
+
error: error.message,
|
|
500
|
+
errorStep: stepExec.stepName,
|
|
501
|
+
completedAt: this.dt.nowISOString()
|
|
502
|
+
});
|
|
503
|
+
await this.alepha.events.emit("workflow:failed", {
|
|
504
|
+
workflowName: workflow.workflowName,
|
|
505
|
+
workflowId: workflow.id,
|
|
506
|
+
error,
|
|
507
|
+
stepName: stepExec.stepName
|
|
508
|
+
}, { catch: true });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
async advance(workflowId) {
|
|
513
|
+
const workflow = await this.executions.findById(workflowId);
|
|
514
|
+
if (!workflow || workflow.status !== "running") return;
|
|
515
|
+
this.getRegistration(workflow.workflowName);
|
|
516
|
+
const nextStep = (await this.stepExecutions.findMany({
|
|
517
|
+
where: { workflowExecutionId: { eq: workflowId } },
|
|
518
|
+
orderBy: {
|
|
519
|
+
column: "stepIndex",
|
|
520
|
+
direction: "asc"
|
|
521
|
+
}
|
|
522
|
+
})).find((s) => s.status === "pending");
|
|
523
|
+
if (nextStep) {
|
|
524
|
+
await this.executions.updateById(workflowId, { currentStep: nextStep.stepName });
|
|
525
|
+
await this.dispatchStep(workflowId, nextStep.stepName, workflow.priority);
|
|
526
|
+
} else {
|
|
527
|
+
await this.executions.updateById(workflowId, {
|
|
528
|
+
status: "completed",
|
|
529
|
+
currentStep: void 0,
|
|
530
|
+
completedAt: this.dt.nowISOString(),
|
|
531
|
+
key: null
|
|
532
|
+
});
|
|
533
|
+
this.log.info(`Workflow '${workflow.workflowName}' completed`, { workflowId });
|
|
534
|
+
await this.alepha.events.emit("workflow:completed", {
|
|
535
|
+
workflowName: workflow.workflowName,
|
|
536
|
+
workflowId
|
|
537
|
+
}, { catch: true });
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
async compensate(workflowId, context) {
|
|
541
|
+
const workflow = await this.executions.findById(workflowId);
|
|
542
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
543
|
+
const registration = this.getRegistration(workflow.workflowName);
|
|
544
|
+
await this.executions.updateById(workflowId, {
|
|
545
|
+
status: "compensating",
|
|
546
|
+
error: context?.error?.message,
|
|
547
|
+
errorStep: context?.failedStep
|
|
548
|
+
});
|
|
549
|
+
await this.alepha.events.emit("workflow:compensating", {
|
|
550
|
+
workflowName: workflow.workflowName,
|
|
551
|
+
workflowId,
|
|
552
|
+
stepName: context?.failedStep ?? ""
|
|
553
|
+
}, { catch: true });
|
|
554
|
+
const completedSteps = await this.stepExecutions.findMany({
|
|
555
|
+
where: {
|
|
556
|
+
workflowExecutionId: { eq: workflowId },
|
|
557
|
+
status: { eq: "completed" }
|
|
558
|
+
},
|
|
559
|
+
orderBy: {
|
|
560
|
+
column: "stepIndex",
|
|
561
|
+
direction: "desc"
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
const results = await this.assembleResults(workflowId);
|
|
565
|
+
for (const stepExec of completedSteps) {
|
|
566
|
+
const stepDef = registration.options.steps.find((s) => s.name === stepExec.stepName);
|
|
567
|
+
if (!stepDef?.compensate) continue;
|
|
568
|
+
await this.stepExecutions.updateById(stepExec.id, { status: "compensating" });
|
|
569
|
+
try {
|
|
570
|
+
await stepDef.compensate({
|
|
571
|
+
payload: workflow.payload,
|
|
572
|
+
result: stepExec.result,
|
|
573
|
+
results,
|
|
574
|
+
context: {
|
|
575
|
+
workflowId,
|
|
576
|
+
executionId: stepExec.id,
|
|
577
|
+
stepName: stepExec.stepName,
|
|
578
|
+
error: context?.error ?? /* @__PURE__ */ new Error("Compensation triggered")
|
|
579
|
+
}
|
|
580
|
+
});
|
|
581
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
582
|
+
status: "compensated",
|
|
583
|
+
completedAt: this.dt.nowISOString()
|
|
584
|
+
});
|
|
585
|
+
} catch (compError) {
|
|
586
|
+
const err = compError instanceof Error ? compError : new Error(String(compError));
|
|
587
|
+
this.log.error(`Compensation failed for step '${stepExec.stepName}'`, {
|
|
588
|
+
workflowId,
|
|
589
|
+
error: err.message
|
|
590
|
+
});
|
|
591
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
592
|
+
status: "compensation_failed",
|
|
593
|
+
error: err.message
|
|
594
|
+
});
|
|
595
|
+
await this.executions.updateById(workflowId, {
|
|
596
|
+
status: "compensation_failed",
|
|
597
|
+
completedAt: this.dt.nowISOString(),
|
|
598
|
+
key: null
|
|
599
|
+
});
|
|
600
|
+
await this.alepha.events.emit("workflow:compensation:failed", {
|
|
601
|
+
workflowName: workflow.workflowName,
|
|
602
|
+
workflowId,
|
|
603
|
+
stepName: stepExec.stepName,
|
|
604
|
+
error: err
|
|
605
|
+
}, { catch: true });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
await this.executions.updateById(workflowId, {
|
|
610
|
+
status: "compensated",
|
|
611
|
+
completedAt: this.dt.nowISOString(),
|
|
612
|
+
key: null
|
|
613
|
+
});
|
|
614
|
+
this.log.info(`Workflow '${workflow.workflowName}' compensated`, { workflowId });
|
|
615
|
+
await this.alepha.events.emit("workflow:compensated", {
|
|
616
|
+
workflowName: workflow.workflowName,
|
|
617
|
+
workflowId
|
|
618
|
+
}, { catch: true });
|
|
619
|
+
}
|
|
620
|
+
async cancel(workflowId, options) {
|
|
621
|
+
const workflow = await this.executions.findById(workflowId);
|
|
622
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
623
|
+
if (workflow.status !== "pending" && workflow.status !== "running" && workflow.status !== "waiting_for_signal") throw new AlephaError(`Cannot cancel workflow in '${workflow.status}' status`);
|
|
624
|
+
for (const [key, controller] of this.abortControllers) if (key.startsWith(`${workflowId}:`)) controller.abort();
|
|
625
|
+
const pendingSteps = await this.stepExecutions.findMany({ where: {
|
|
626
|
+
workflowExecutionId: { eq: workflowId },
|
|
627
|
+
status: { inArray: ["pending", "waiting"] }
|
|
628
|
+
} });
|
|
629
|
+
for (const step of pendingSteps) await this.stepExecutions.updateById(step.id, { status: "cancelled" });
|
|
630
|
+
if (options?.compensate) {
|
|
631
|
+
await this.compensate(workflowId, { error: /* @__PURE__ */ new Error("Cancelled with compensation") });
|
|
632
|
+
await this.executions.updateById(workflowId, {
|
|
633
|
+
status: "cancelled",
|
|
634
|
+
cancelledBy: options?.cancelledBy,
|
|
635
|
+
cancelledByName: options?.cancelledByName
|
|
636
|
+
});
|
|
637
|
+
} else await this.executions.updateById(workflowId, {
|
|
638
|
+
status: "cancelled",
|
|
639
|
+
cancelledBy: options?.cancelledBy,
|
|
640
|
+
cancelledByName: options?.cancelledByName,
|
|
641
|
+
completedAt: this.dt.nowISOString(),
|
|
642
|
+
key: null
|
|
643
|
+
});
|
|
644
|
+
this.log.info(`Workflow cancelled`, { workflowId });
|
|
645
|
+
await this.alepha.events.emit("workflow:cancelled", {
|
|
646
|
+
workflowName: workflow.workflowName,
|
|
647
|
+
workflowId
|
|
648
|
+
}, { catch: true });
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Send a signal to a waiting workflow step.
|
|
652
|
+
*/
|
|
653
|
+
async signal(workflowId, stepName, payload, signalledBy) {
|
|
654
|
+
const workflow = await this.executions.findById(workflowId);
|
|
655
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
656
|
+
if (workflow.status !== "waiting_for_signal") throw new AlephaError(`Cannot signal workflow in '${workflow.status}' status`);
|
|
657
|
+
const stepExec = await this.findStepExecution(workflowId, stepName);
|
|
658
|
+
if (!stepExec) throw new AlephaError(`Step '${stepName}' not found on workflow ${workflowId}`);
|
|
659
|
+
if (stepExec.status !== "waiting") throw new AlephaError(`Step '${stepName}' is in '${stepExec.status}' status, expected 'waiting'`);
|
|
660
|
+
await this.stepExecutions.updateById(stepExec.id, {
|
|
661
|
+
status: "completed",
|
|
662
|
+
signalPayload: payload != null ? payload : void 0,
|
|
663
|
+
signalledBy,
|
|
664
|
+
signalledAt: this.dt.nowISOString(),
|
|
665
|
+
completedAt: this.dt.nowISOString()
|
|
666
|
+
});
|
|
667
|
+
await this.executions.updateById(workflowId, { status: "running" });
|
|
668
|
+
this.log.info(`Workflow signalled step '${stepName}'`, { workflowId });
|
|
669
|
+
await this.advance(workflowId);
|
|
670
|
+
}
|
|
671
|
+
async retry(workflowId) {
|
|
672
|
+
const workflow = await this.executions.findById(workflowId);
|
|
673
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
674
|
+
if (workflow.status !== "failed" && workflow.status !== "timed_out") throw new AlephaError(`Cannot retry workflow in '${workflow.status}' status. Use restart() for compensated workflows.`);
|
|
675
|
+
const failedStep = await this.stepExecutions.findMany({
|
|
676
|
+
where: {
|
|
677
|
+
workflowExecutionId: { eq: workflowId },
|
|
678
|
+
status: { eq: "failed" }
|
|
679
|
+
},
|
|
680
|
+
limit: 1
|
|
681
|
+
});
|
|
682
|
+
if (failedStep.length === 0) throw new AlephaError("No failed step found to retry");
|
|
683
|
+
await this.stepExecutions.updateById(failedStep[0].id, {
|
|
684
|
+
status: "pending",
|
|
685
|
+
error: void 0,
|
|
686
|
+
startedAt: void 0,
|
|
687
|
+
completedAt: void 0
|
|
688
|
+
});
|
|
689
|
+
await this.executions.updateById(workflowId, {
|
|
690
|
+
status: "running",
|
|
691
|
+
error: void 0,
|
|
692
|
+
errorStep: void 0,
|
|
693
|
+
completedAt: void 0
|
|
694
|
+
});
|
|
695
|
+
await this.dispatchStep(workflowId, failedStep[0].stepName, workflow.priority);
|
|
696
|
+
}
|
|
697
|
+
async restart(workflowId) {
|
|
698
|
+
const workflow = await this.executions.findById(workflowId);
|
|
699
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
700
|
+
if (workflow.status !== "compensated" && workflow.status !== "compensation_failed" && workflow.status !== "failed") throw new AlephaError(`Cannot restart workflow in '${workflow.status}' status`);
|
|
701
|
+
return this.start(workflow.workflowName, workflow.payload);
|
|
702
|
+
}
|
|
703
|
+
async getExecution(workflowId) {
|
|
704
|
+
const workflow = await this.executions.findById(workflowId);
|
|
705
|
+
if (!workflow) throw new AlephaError(`Workflow not found: ${workflowId}`);
|
|
706
|
+
const steps = await this.stepExecutions.findMany({
|
|
707
|
+
where: { workflowExecutionId: { eq: workflowId } },
|
|
708
|
+
orderBy: {
|
|
709
|
+
column: "stepIndex",
|
|
710
|
+
direction: "asc"
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
return {
|
|
714
|
+
...workflow,
|
|
715
|
+
steps
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
pauseWorkflow(name) {
|
|
719
|
+
this.getRegistration(name);
|
|
720
|
+
this.pausedWorkflows.add(name);
|
|
721
|
+
this.log.info(`Paused workflow '${name}'`);
|
|
722
|
+
}
|
|
723
|
+
async resumeWorkflow(name) {
|
|
724
|
+
this.getRegistration(name);
|
|
725
|
+
this.pausedWorkflows.delete(name);
|
|
726
|
+
this.log.info(`Resumed workflow '${name}'`);
|
|
727
|
+
}
|
|
728
|
+
isWorkflowPaused(name) {
|
|
729
|
+
return this.pausedWorkflows.has(name);
|
|
730
|
+
}
|
|
731
|
+
getPausedWorkflows() {
|
|
732
|
+
return [...this.pausedWorkflows];
|
|
733
|
+
}
|
|
734
|
+
async dispatchStep(workflowId, stepName, priority) {
|
|
735
|
+
if (this.stopping) return;
|
|
736
|
+
if (this.stepDispatch) await this.stepDispatch(workflowId, stepName, priority);
|
|
737
|
+
else await this.processStep(workflowId, stepName);
|
|
738
|
+
}
|
|
739
|
+
async assembleResults(workflowId) {
|
|
740
|
+
const completed = await this.stepExecutions.findMany({
|
|
741
|
+
where: {
|
|
742
|
+
workflowExecutionId: { eq: workflowId },
|
|
743
|
+
status: { eq: "completed" }
|
|
744
|
+
},
|
|
745
|
+
orderBy: {
|
|
746
|
+
column: "stepIndex",
|
|
747
|
+
direction: "asc"
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
const results = {};
|
|
751
|
+
for (const step of completed) if (step.result) results[step.stepName] = step.result;
|
|
752
|
+
return results;
|
|
753
|
+
}
|
|
754
|
+
async findStepExecution(workflowId, stepName) {
|
|
755
|
+
return (await this.stepExecutions.findMany({
|
|
756
|
+
where: {
|
|
757
|
+
workflowExecutionId: { eq: workflowId },
|
|
758
|
+
stepName: { eq: stepName }
|
|
759
|
+
},
|
|
760
|
+
limit: 1
|
|
761
|
+
}))[0];
|
|
762
|
+
}
|
|
763
|
+
computeBackoff(retryOpts, attempt) {
|
|
764
|
+
const now = this.dt.now();
|
|
765
|
+
if (!retryOpts.backoff) return now.add(1, "second").toISOString();
|
|
766
|
+
if (Array.isArray(retryOpts.backoff)) {
|
|
767
|
+
const delay = this.dt.duration(retryOpts.backoff);
|
|
768
|
+
return now.add(delay).toISOString();
|
|
769
|
+
}
|
|
770
|
+
const backoff = retryOpts.backoff;
|
|
771
|
+
let delayMs = this.dt.duration(backoff.initial).as("milliseconds") * (backoff.factor ?? 2) ** (attempt - 1);
|
|
772
|
+
if (backoff.max) {
|
|
773
|
+
const maxMs = this.dt.duration(backoff.max).as("milliseconds");
|
|
774
|
+
delayMs = Math.min(delayMs, maxMs);
|
|
775
|
+
}
|
|
776
|
+
if (backoff.jitter) delayMs = delayMs * (.75 + Math.random() * .5);
|
|
777
|
+
return now.add(delayMs, "millisecond").toISOString();
|
|
778
|
+
}
|
|
779
|
+
async writeLogs(stepExecutionId, context) {
|
|
780
|
+
const entries = this.logs.get(context);
|
|
781
|
+
if (!entries || entries.length === 0) return;
|
|
782
|
+
const maxEntries = this.config.logMaxEntries;
|
|
783
|
+
if (maxEntries === 0) return;
|
|
784
|
+
let logs = entries;
|
|
785
|
+
if (logs.length > maxEntries) {
|
|
786
|
+
logs = logs.slice(0, maxEntries);
|
|
787
|
+
logs.push({
|
|
788
|
+
level: "WARN",
|
|
789
|
+
message: `Log entries truncated at ${maxEntries}`,
|
|
790
|
+
timestamp: this.dt.nowMillis(),
|
|
791
|
+
service: "alepha.workflows",
|
|
792
|
+
module: "WorkflowProvider"
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
await this.stepLogs.create({
|
|
797
|
+
id: stepExecutionId,
|
|
798
|
+
logs
|
|
799
|
+
});
|
|
800
|
+
} catch {
|
|
801
|
+
this.log.warn(`Failed to write logs for step ${stepExecutionId}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
getRegistration(name) {
|
|
805
|
+
const reg = this.workflows.get(name);
|
|
806
|
+
if (!reg) throw new AlephaError(`Workflow not registered: ${name}`);
|
|
807
|
+
return reg;
|
|
808
|
+
}
|
|
809
|
+
async recoverySweep() {
|
|
810
|
+
if (this.stopping) return;
|
|
811
|
+
const lockValue = `${crypto.randomUUID()},${this.dt.nowISOString()}`;
|
|
812
|
+
if ((await this.lockProvider.set("_alepha:workflows:recovery-lock", lockValue, true, 3e5)).split(",")[0] !== lockValue.split(",")[0]) return;
|
|
813
|
+
try {
|
|
814
|
+
const staleThreshold = this.dt.now().subtract(this.config.recovery.staleThreshold, "millisecond").toISOString();
|
|
815
|
+
const staleSteps = await this.stepExecutions.findMany({ where: {
|
|
816
|
+
status: { eq: "running" },
|
|
817
|
+
startedAt: { lte: staleThreshold }
|
|
818
|
+
} });
|
|
819
|
+
for (const step of staleSteps) {
|
|
820
|
+
if (this.abortControllers.has(`${step.workflowExecutionId}:${step.stepName}`)) continue;
|
|
821
|
+
this.log.warn(`Recovery sweep: marking stale step '${step.stepName}' as failed`, { workflowId: step.workflowExecutionId });
|
|
822
|
+
await this.stepExecutions.updateById(step.id, {
|
|
823
|
+
status: "failed",
|
|
824
|
+
error: "Step assumed crashed (recovered by sweep)",
|
|
825
|
+
completedAt: this.dt.nowISOString()
|
|
826
|
+
});
|
|
827
|
+
const workflow = await this.executions.findById(step.workflowExecutionId);
|
|
828
|
+
if (!workflow) continue;
|
|
829
|
+
const registration = this.workflows.get(workflow.workflowName);
|
|
830
|
+
if (!registration) continue;
|
|
831
|
+
if ((registration.options.onError ?? "compensate") === "compensate") await this.compensate(workflow.id, {
|
|
832
|
+
failedStep: step.stepName,
|
|
833
|
+
error: /* @__PURE__ */ new Error("Step assumed crashed")
|
|
834
|
+
});
|
|
835
|
+
else await this.executions.updateById(workflow.id, {
|
|
836
|
+
status: "failed",
|
|
837
|
+
error: "Step assumed crashed",
|
|
838
|
+
errorStep: step.stepName,
|
|
839
|
+
completedAt: this.dt.nowISOString()
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
const runningWorkflows = await this.executions.findMany({ where: { status: { eq: "running" } } });
|
|
843
|
+
for (const wf of runningWorkflows) if ((await this.stepExecutions.findMany({
|
|
844
|
+
where: {
|
|
845
|
+
workflowExecutionId: { eq: wf.id },
|
|
846
|
+
status: { inArray: ["running", "pending"] }
|
|
847
|
+
},
|
|
848
|
+
limit: 1
|
|
849
|
+
})).length === 0) {
|
|
850
|
+
this.log.warn("Recovery sweep: re-advancing inconsistent workflow", { workflowId: wf.id });
|
|
851
|
+
await this.advance(wf.id);
|
|
852
|
+
}
|
|
853
|
+
} catch (e) {
|
|
854
|
+
this.log.error("Recovery sweep failed", { error: e });
|
|
855
|
+
} finally {
|
|
856
|
+
await this.lockProvider.del("_alepha:workflows:recovery-lock");
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async timeoutSweep() {
|
|
860
|
+
if (this.stopping) return;
|
|
861
|
+
const lockValue = `${crypto.randomUUID()},${this.dt.nowISOString()}`;
|
|
862
|
+
if ((await this.lockProvider.set("_alepha:workflows:timeout-lock", lockValue, true, 6e4)).split(",")[0] !== lockValue.split(",")[0]) return;
|
|
863
|
+
try {
|
|
864
|
+
const now = this.dt.nowISOString();
|
|
865
|
+
const timedOutWorkflows = await this.executions.findMany({ where: {
|
|
866
|
+
status: { inArray: ["running", "waiting_for_signal"] },
|
|
867
|
+
deadlineAt: { lte: now }
|
|
868
|
+
} });
|
|
869
|
+
for (const wf of timedOutWorkflows) {
|
|
870
|
+
this.log.warn(`Timeout sweep: workflow timed out`, { workflowId: wf.id });
|
|
871
|
+
for (const [key, controller] of this.abortControllers) if (key.startsWith(`${wf.id}:`)) controller.abort();
|
|
872
|
+
await this.stepExecutions.updateMany({
|
|
873
|
+
workflowExecutionId: { eq: wf.id },
|
|
874
|
+
status: { inArray: ["running", "waiting"] }
|
|
875
|
+
}, {
|
|
876
|
+
status: "failed",
|
|
877
|
+
error: "Workflow timed out",
|
|
878
|
+
completedAt: now
|
|
879
|
+
});
|
|
880
|
+
await this.executions.updateById(wf.id, {
|
|
881
|
+
status: "timed_out",
|
|
882
|
+
completedAt: now
|
|
883
|
+
});
|
|
884
|
+
await this.alepha.events.emit("workflow:timed_out", {
|
|
885
|
+
workflowName: wf.workflowName,
|
|
886
|
+
workflowId: wf.id
|
|
887
|
+
}, { catch: true });
|
|
888
|
+
if (this.workflows.get(wf.workflowName)?.options.onError === "compensate") await this.compensate(wf.id, { error: /* @__PURE__ */ new Error("Workflow timed out") });
|
|
889
|
+
}
|
|
890
|
+
} catch (e) {
|
|
891
|
+
this.log.error("Timeout sweep failed", { error: e });
|
|
892
|
+
} finally {
|
|
893
|
+
await this.lockProvider.del("_alepha:workflows:timeout-lock");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
async purge() {
|
|
897
|
+
if (this.stopping) return;
|
|
898
|
+
try {
|
|
899
|
+
const cutoff = this.dt.now().subtract(this.config.retentionDays, "day").toISOString();
|
|
900
|
+
const old = await this.executions.findMany({ where: {
|
|
901
|
+
status: { inArray: [
|
|
902
|
+
"completed",
|
|
903
|
+
"failed",
|
|
904
|
+
"compensated",
|
|
905
|
+
"compensation_failed",
|
|
906
|
+
"cancelled",
|
|
907
|
+
"timed_out"
|
|
908
|
+
] },
|
|
909
|
+
completedAt: { lte: cutoff }
|
|
910
|
+
} });
|
|
911
|
+
if (old.length > 0) {
|
|
912
|
+
const ids = old.map((e) => e.id);
|
|
913
|
+
await this.executions.deleteMany({ id: { inArray: ids } });
|
|
914
|
+
this.log.info(`Purge: deleted ${ids.length} old workflow executions`);
|
|
915
|
+
}
|
|
916
|
+
} catch (e) {
|
|
917
|
+
this.log.error("Purge failed", { error: e });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
onStart = $hook({
|
|
921
|
+
on: "start",
|
|
922
|
+
handler: async () => {
|
|
923
|
+
this.log.info("Workflow engine OK", {
|
|
924
|
+
dispatch: this.stepDispatch ? "queue" : "inline",
|
|
925
|
+
workflows: this.workflows.size
|
|
926
|
+
});
|
|
927
|
+
this.alepha.events.on("log", ({ entry }) => {
|
|
928
|
+
const ctx = entry.context;
|
|
929
|
+
if (!ctx) return;
|
|
930
|
+
const entries = this.logs.get(ctx);
|
|
931
|
+
if (!entries) return;
|
|
932
|
+
entries.push(entry);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
onStop = $hook({
|
|
937
|
+
on: "stop",
|
|
938
|
+
handler: async () => {
|
|
939
|
+
this.stopping = true;
|
|
940
|
+
if (this.inFlight.size > 0) {
|
|
941
|
+
this.log.info(`Draining ${this.inFlight.size} in-flight step(s)...`);
|
|
942
|
+
await Promise.race([Promise.allSettled([...this.inFlight]), this.dt.wait([this.config.drainTimeout, "millisecond"])]);
|
|
943
|
+
}
|
|
944
|
+
if (this.abortControllers.size > 0) {
|
|
945
|
+
this.log.warn(`Aborting ${this.abortControllers.size} remaining step(s)`);
|
|
946
|
+
for (const controller of this.abortControllers.values()) controller.abort();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
};
|
|
951
|
+
//#endregion
|
|
952
|
+
//#region ../../src/api/workflows/services/WorkflowService.ts
|
|
953
|
+
var WorkflowService = class {
|
|
954
|
+
alepha = $inject(Alepha);
|
|
955
|
+
dt = $inject(DateTimeProvider);
|
|
956
|
+
log = $logger();
|
|
957
|
+
workflowProvider = $inject(WorkflowProvider);
|
|
958
|
+
database = $inject(DatabaseProvider);
|
|
959
|
+
executions = $repository(workflowExecutions);
|
|
960
|
+
stepExecutions = $repository(workflowStepExecutions);
|
|
961
|
+
/**
|
|
962
|
+
* Compute available actions for a workflow execution based on its status.
|
|
963
|
+
*/
|
|
964
|
+
computeCan(status, signalStepName) {
|
|
965
|
+
return {
|
|
966
|
+
retry: status === "failed" || status === "timed_out",
|
|
967
|
+
cancel: status === "pending" || status === "running" || status === "waiting_for_signal",
|
|
968
|
+
compensate: status === "failed" || status === "timed_out",
|
|
969
|
+
restart: status === "failed" || status === "compensated" || status === "compensation_failed",
|
|
970
|
+
signal: signalStepName ? { stepName: signalStepName } : void 0
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* Convert an ISO date string to the raw SQL parameter format
|
|
975
|
+
* expected by the current database dialect.
|
|
976
|
+
*
|
|
977
|
+
* - PostgreSQL: ISO string (timestamp comparison)
|
|
978
|
+
* - SQLite: epoch milliseconds (integer comparison)
|
|
979
|
+
*/
|
|
980
|
+
toRawDate(iso) {
|
|
981
|
+
return this.database.dialect === "sqlite" ? new Date(iso).getTime() : iso;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Get aggregate stats for the workflow engine.
|
|
985
|
+
*/
|
|
986
|
+
async getStats(days) {
|
|
987
|
+
const workflows = this.workflowProvider.getRegisteredWorkflows();
|
|
988
|
+
const periodAgo = this.toRawDate(this.dt.now().subtract(days ?? 1, "day").toISOString());
|
|
989
|
+
const row = (await this.executions.query((e) => sql`
|
|
990
|
+
SELECT
|
|
991
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'running') AS running,
|
|
992
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'pending') AS pending,
|
|
993
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'waiting_for_signal') AS waiting,
|
|
994
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'completed' AND ${e.completedAt} >= ${periodAgo}) AS completed,
|
|
995
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'failed' AND ${e.completedAt} >= ${periodAgo}) AS failed,
|
|
996
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'compensated' AND ${e.completedAt} >= ${periodAgo}) AS compensated,
|
|
997
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'compensation_failed' AND ${e.completedAt} >= ${periodAgo}) AS compensation_failed,
|
|
998
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'cancelled' AND ${e.completedAt} >= ${periodAgo}) AS cancelled,
|
|
999
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'timed_out' AND ${e.completedAt} >= ${periodAgo}) AS timed_out
|
|
1000
|
+
FROM ${e}
|
|
1001
|
+
`, t.object({
|
|
1002
|
+
running: t.string(),
|
|
1003
|
+
pending: t.string(),
|
|
1004
|
+
waiting: t.string(),
|
|
1005
|
+
completed: t.string(),
|
|
1006
|
+
failed: t.string(),
|
|
1007
|
+
compensated: t.string(),
|
|
1008
|
+
compensation_failed: t.string(),
|
|
1009
|
+
cancelled: t.string(),
|
|
1010
|
+
timed_out: t.string()
|
|
1011
|
+
})))[0];
|
|
1012
|
+
return {
|
|
1013
|
+
registered: workflows.size,
|
|
1014
|
+
running: Number(row.running),
|
|
1015
|
+
pending: Number(row.pending),
|
|
1016
|
+
waiting: Number(row.waiting),
|
|
1017
|
+
completed: Number(row.completed),
|
|
1018
|
+
failed: Number(row.failed),
|
|
1019
|
+
compensated: Number(row.compensated),
|
|
1020
|
+
compensationFailed: Number(row.compensation_failed),
|
|
1021
|
+
cancelled: Number(row.cancelled),
|
|
1022
|
+
timedOut: Number(row.timed_out)
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Get the full workflow registry with live counts.
|
|
1027
|
+
*/
|
|
1028
|
+
async getRegistry() {
|
|
1029
|
+
const workflows = this.workflowProvider.getRegisteredWorkflows();
|
|
1030
|
+
const names = [...workflows.keys()];
|
|
1031
|
+
const countRows = names.length > 0 ? await this.executions.query((e) => sql`
|
|
1032
|
+
SELECT
|
|
1033
|
+
${e.workflowName} AS workflow_name,
|
|
1034
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'running') AS running,
|
|
1035
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'pending') AS pending,
|
|
1036
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'waiting_for_signal') AS waiting,
|
|
1037
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'failed') AS failed
|
|
1038
|
+
FROM ${e}
|
|
1039
|
+
WHERE ${e.workflowName} IN (${sql.join(names.map((n) => sql`${n}`), sql`, `)})
|
|
1040
|
+
GROUP BY ${e.workflowName}
|
|
1041
|
+
`, t.object({
|
|
1042
|
+
workflow_name: t.string(),
|
|
1043
|
+
running: t.string(),
|
|
1044
|
+
pending: t.string(),
|
|
1045
|
+
waiting: t.string(),
|
|
1046
|
+
failed: t.string()
|
|
1047
|
+
})) : [];
|
|
1048
|
+
const countsByName = new Map(countRows.map((r) => [r.workflow_name, r]));
|
|
1049
|
+
const result = [];
|
|
1050
|
+
for (const [name, reg] of workflows) {
|
|
1051
|
+
const opts = reg.options;
|
|
1052
|
+
const counts = countsByName.get(name);
|
|
1053
|
+
result.push({
|
|
1054
|
+
name,
|
|
1055
|
+
stepCount: opts.steps.length,
|
|
1056
|
+
steps: opts.steps.map((step) => ({
|
|
1057
|
+
name: step.name,
|
|
1058
|
+
type: step.type ?? "handler",
|
|
1059
|
+
hasCompensate: Boolean(step.compensate),
|
|
1060
|
+
hasRetry: Boolean(step.retry),
|
|
1061
|
+
timeout: step.timeout ? String(step.timeout) : void 0
|
|
1062
|
+
})),
|
|
1063
|
+
onError: opts.onError ?? "compensate",
|
|
1064
|
+
timeout: opts.timeout ? String(opts.timeout) : void 0,
|
|
1065
|
+
priority: opts.priority ?? "normal",
|
|
1066
|
+
tags: opts.tags,
|
|
1067
|
+
paused: this.workflowProvider.isWorkflowPaused(name),
|
|
1068
|
+
running: Number(counts?.running ?? 0),
|
|
1069
|
+
pending: Number(counts?.pending ?? 0),
|
|
1070
|
+
waiting: Number(counts?.waiting ?? 0),
|
|
1071
|
+
failed: Number(counts?.failed ?? 0)
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
return result;
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Paginated query for workflow executions.
|
|
1078
|
+
*/
|
|
1079
|
+
async findExecutions(query = {}) {
|
|
1080
|
+
query.sort ??= "-createdAt";
|
|
1081
|
+
const where = this.executions.createQueryWhere();
|
|
1082
|
+
if (query.workflow) where.workflowName = { eq: query.workflow };
|
|
1083
|
+
if (query.status) where.status = { eq: query.status };
|
|
1084
|
+
if (query.from) where.createdAt = { gte: query.from };
|
|
1085
|
+
if (query.to) where.createdAt = {
|
|
1086
|
+
...where.createdAt,
|
|
1087
|
+
lte: query.to
|
|
1088
|
+
};
|
|
1089
|
+
const page = await this.executions.paginate(query, { where }, { count: true });
|
|
1090
|
+
return {
|
|
1091
|
+
...page,
|
|
1092
|
+
content: page.content.map((exec) => ({
|
|
1093
|
+
...exec,
|
|
1094
|
+
can: this.computeCan(exec.status)
|
|
1095
|
+
}))
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* Get a single workflow execution with step details.
|
|
1100
|
+
*/
|
|
1101
|
+
async getExecution(id) {
|
|
1102
|
+
const execution = await this.executions.findById(id);
|
|
1103
|
+
if (!execution) throw new NotFoundError(`Workflow execution not found: ${id}`);
|
|
1104
|
+
const steps = await this.stepExecutions.findMany({
|
|
1105
|
+
where: { workflowExecutionId: { eq: id } },
|
|
1106
|
+
orderBy: {
|
|
1107
|
+
column: "stepIndex",
|
|
1108
|
+
direction: "asc"
|
|
1109
|
+
}
|
|
1110
|
+
});
|
|
1111
|
+
let signalStepName;
|
|
1112
|
+
if (execution.status === "waiting_for_signal") {
|
|
1113
|
+
const waitingStep = steps.find((s) => s.status === "waiting");
|
|
1114
|
+
if (waitingStep) signalStepName = waitingStep.stepName;
|
|
1115
|
+
}
|
|
1116
|
+
return {
|
|
1117
|
+
...execution,
|
|
1118
|
+
can: this.computeCan(execution.status, signalStepName),
|
|
1119
|
+
steps
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
/**
|
|
1123
|
+
* Get daily activity (completed/failed) over a date range.
|
|
1124
|
+
*/
|
|
1125
|
+
async getActivity(days = 14) {
|
|
1126
|
+
return (await this.executions.query((e) => sql`
|
|
1127
|
+
WITH date_series AS (
|
|
1128
|
+
SELECT generate_series(
|
|
1129
|
+
CURRENT_DATE - ${days - 1}::int,
|
|
1130
|
+
CURRENT_DATE,
|
|
1131
|
+
'1 day'::interval
|
|
1132
|
+
)::date AS date
|
|
1133
|
+
)
|
|
1134
|
+
SELECT
|
|
1135
|
+
ds.date::text AS date,
|
|
1136
|
+
COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'completed'), 0) AS completed,
|
|
1137
|
+
COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'failed'), 0) AS failed
|
|
1138
|
+
FROM date_series ds
|
|
1139
|
+
LEFT JOIN ${e} ON DATE(${e.completedAt}) = ds.date
|
|
1140
|
+
AND ${e.status} IN ('completed', 'failed')
|
|
1141
|
+
GROUP BY ds.date
|
|
1142
|
+
ORDER BY ds.date ASC
|
|
1143
|
+
`, t.object({
|
|
1144
|
+
date: t.string(),
|
|
1145
|
+
completed: t.string(),
|
|
1146
|
+
failed: t.string()
|
|
1147
|
+
}))).map((row) => ({
|
|
1148
|
+
date: row.date,
|
|
1149
|
+
completed: Number(row.completed),
|
|
1150
|
+
failed: Number(row.failed)
|
|
1151
|
+
}));
|
|
1152
|
+
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Start a new workflow execution by name.
|
|
1155
|
+
*/
|
|
1156
|
+
async triggerWorkflow(name, payload, options) {
|
|
1157
|
+
this.log.info(`Triggering workflow '${name}'`, { triggeredBy: options?.triggeredByName ?? options?.triggeredBy });
|
|
1158
|
+
return { id: await this.workflowProvider.start(name, payload ?? {}, {
|
|
1159
|
+
key: options?.key,
|
|
1160
|
+
tags: options?.tags,
|
|
1161
|
+
triggeredBy: options?.triggeredBy,
|
|
1162
|
+
triggeredByName: options?.triggeredByName
|
|
1163
|
+
}) };
|
|
1164
|
+
}
|
|
1165
|
+
/**
|
|
1166
|
+
* Cancel a running workflow execution.
|
|
1167
|
+
*/
|
|
1168
|
+
async cancelExecution(id, context) {
|
|
1169
|
+
this.log.info(`Cancelling workflow execution ${id}`, { cancelledBy: context?.cancelledByName ?? context?.cancelledBy });
|
|
1170
|
+
await this.workflowProvider.cancel(id, {
|
|
1171
|
+
compensate: context?.compensate,
|
|
1172
|
+
cancelledBy: context?.cancelledBy,
|
|
1173
|
+
cancelledByName: context?.cancelledByName
|
|
1174
|
+
});
|
|
1175
|
+
return { ok: true };
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Retry a failed/timed-out workflow from the failed step.
|
|
1179
|
+
*/
|
|
1180
|
+
async retryExecution(id) {
|
|
1181
|
+
this.log.info(`Retrying workflow execution ${id}`);
|
|
1182
|
+
await this.workflowProvider.retry(id);
|
|
1183
|
+
return { ok: true };
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Restart a terminal workflow as a new execution.
|
|
1187
|
+
*/
|
|
1188
|
+
async restartExecution(id) {
|
|
1189
|
+
this.log.info(`Restarting workflow execution ${id}`);
|
|
1190
|
+
return { id: await this.workflowProvider.restart(id) };
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Trigger compensation on a failed/timed-out workflow.
|
|
1194
|
+
*/
|
|
1195
|
+
async compensateExecution(id) {
|
|
1196
|
+
this.log.info(`Compensating workflow execution ${id}`);
|
|
1197
|
+
await this.workflowProvider.compensate(id);
|
|
1198
|
+
return { ok: true };
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Send a signal to a waiting workflow step.
|
|
1202
|
+
*/
|
|
1203
|
+
async signalExecution(id, stepName, payload, signalledBy) {
|
|
1204
|
+
this.log.info(`Signalling workflow execution ${id} step '${stepName}'`, { signalledBy });
|
|
1205
|
+
await this.workflowProvider.signal(id, stepName, payload);
|
|
1206
|
+
return { ok: true };
|
|
1207
|
+
}
|
|
1208
|
+
};
|
|
1209
|
+
//#endregion
|
|
1210
|
+
//#region ../../src/api/workflows/controllers/AdminWorkflowController.ts
|
|
1211
|
+
var AdminWorkflowController = class {
|
|
1212
|
+
url = "/workflows";
|
|
1213
|
+
group = "admin:workflows";
|
|
1214
|
+
workflowService = $inject(WorkflowService);
|
|
1215
|
+
getRegistry = $action({
|
|
1216
|
+
path: this.url,
|
|
1217
|
+
group: this.group,
|
|
1218
|
+
use: [$secure({ permissions: ["admin:workflow:read"] })],
|
|
1219
|
+
schema: { response: t.array(workflowRegistrationSchema) },
|
|
1220
|
+
handler: () => this.workflowService.getRegistry()
|
|
1221
|
+
});
|
|
1222
|
+
getStats = $action({
|
|
1223
|
+
path: `${this.url}/stats`,
|
|
1224
|
+
group: this.group,
|
|
1225
|
+
use: [$secure({ permissions: ["admin:workflow:read"] })],
|
|
1226
|
+
schema: {
|
|
1227
|
+
query: workflowActivityQuerySchema,
|
|
1228
|
+
response: workflowStatsSchema
|
|
1229
|
+
},
|
|
1230
|
+
handler: ({ query }) => this.workflowService.getStats(query.days)
|
|
1231
|
+
});
|
|
1232
|
+
getActivity = $action({
|
|
1233
|
+
path: `${this.url}/activity`,
|
|
1234
|
+
group: this.group,
|
|
1235
|
+
use: [$secure({ permissions: ["admin:workflow:read"] })],
|
|
1236
|
+
schema: {
|
|
1237
|
+
query: workflowActivityQuerySchema,
|
|
1238
|
+
response: t.array(workflowActivityPointSchema)
|
|
1239
|
+
},
|
|
1240
|
+
handler: ({ query }) => this.workflowService.getActivity(query.days)
|
|
1241
|
+
});
|
|
1242
|
+
findExecutions = $action({
|
|
1243
|
+
path: `${this.url}/executions`,
|
|
1244
|
+
group: this.group,
|
|
1245
|
+
use: [$secure({ permissions: ["admin:workflow:read"] })],
|
|
1246
|
+
schema: {
|
|
1247
|
+
query: workflowExecutionQuerySchema,
|
|
1248
|
+
response: t.page(workflowExecutionResourceSchema)
|
|
1249
|
+
},
|
|
1250
|
+
handler: ({ query }) => this.workflowService.findExecutions(query)
|
|
1251
|
+
});
|
|
1252
|
+
getExecution = $action({
|
|
1253
|
+
path: `${this.url}/executions/:id`,
|
|
1254
|
+
group: this.group,
|
|
1255
|
+
use: [$secure({ permissions: ["admin:workflow:read"] })],
|
|
1256
|
+
schema: {
|
|
1257
|
+
params: t.object({ id: t.uuid() }),
|
|
1258
|
+
response: workflowExecutionDetailSchema
|
|
1259
|
+
},
|
|
1260
|
+
handler: ({ params }) => this.workflowService.getExecution(params.id)
|
|
1261
|
+
});
|
|
1262
|
+
startWorkflow = $action({
|
|
1263
|
+
method: "POST",
|
|
1264
|
+
path: `${this.url}/start`,
|
|
1265
|
+
group: this.group,
|
|
1266
|
+
use: [$secure({ permissions: ["admin:workflow:create"] })],
|
|
1267
|
+
schema: {
|
|
1268
|
+
body: t.object({
|
|
1269
|
+
name: t.text(),
|
|
1270
|
+
payload: t.optional(t.record(t.text(), t.any())),
|
|
1271
|
+
key: t.optional(t.text()),
|
|
1272
|
+
tags: t.optional(t.array(t.text()))
|
|
1273
|
+
}),
|
|
1274
|
+
response: t.object({ id: t.uuid() })
|
|
1275
|
+
},
|
|
1276
|
+
handler: async ({ body, user }) => {
|
|
1277
|
+
return this.workflowService.triggerWorkflow(body.name, body.payload, {
|
|
1278
|
+
key: body.key,
|
|
1279
|
+
tags: body.tags,
|
|
1280
|
+
triggeredBy: user?.id,
|
|
1281
|
+
triggeredByName: user?.name
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
});
|
|
1285
|
+
signalStep = $action({
|
|
1286
|
+
method: "POST",
|
|
1287
|
+
path: `${this.url}/executions/:id/signal`,
|
|
1288
|
+
group: this.group,
|
|
1289
|
+
use: [$secure({ permissions: ["admin:workflow:update"] })],
|
|
1290
|
+
schema: {
|
|
1291
|
+
params: t.object({ id: t.uuid() }),
|
|
1292
|
+
body: t.object({
|
|
1293
|
+
stepName: t.text(),
|
|
1294
|
+
payload: t.optional(t.record(t.text(), t.any()))
|
|
1295
|
+
}),
|
|
1296
|
+
response: okSchema
|
|
1297
|
+
},
|
|
1298
|
+
handler: async ({ params, body, user }) => {
|
|
1299
|
+
return this.workflowService.signalExecution(params.id, body.stepName, body.payload, user?.id);
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
cancelExecution = $action({
|
|
1303
|
+
method: "POST",
|
|
1304
|
+
path: `${this.url}/executions/:id/cancel`,
|
|
1305
|
+
group: this.group,
|
|
1306
|
+
use: [$secure({ permissions: ["admin:workflow:update"] })],
|
|
1307
|
+
schema: {
|
|
1308
|
+
params: t.object({ id: t.uuid() }),
|
|
1309
|
+
response: okSchema
|
|
1310
|
+
},
|
|
1311
|
+
handler: async ({ params, user }) => {
|
|
1312
|
+
return this.workflowService.cancelExecution(params.id, {
|
|
1313
|
+
cancelledBy: user?.id,
|
|
1314
|
+
cancelledByName: user?.name
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
});
|
|
1318
|
+
retryExecution = $action({
|
|
1319
|
+
method: "POST",
|
|
1320
|
+
path: `${this.url}/executions/:id/retry`,
|
|
1321
|
+
group: this.group,
|
|
1322
|
+
use: [$secure({ permissions: ["admin:workflow:update"] })],
|
|
1323
|
+
schema: {
|
|
1324
|
+
params: t.object({ id: t.uuid() }),
|
|
1325
|
+
response: okSchema
|
|
1326
|
+
},
|
|
1327
|
+
handler: async ({ params }) => {
|
|
1328
|
+
return this.workflowService.retryExecution(params.id);
|
|
1329
|
+
}
|
|
1330
|
+
});
|
|
1331
|
+
restartExecution = $action({
|
|
1332
|
+
method: "POST",
|
|
1333
|
+
path: `${this.url}/executions/:id/restart`,
|
|
1334
|
+
group: this.group,
|
|
1335
|
+
use: [$secure({ permissions: ["admin:workflow:create"] })],
|
|
1336
|
+
schema: {
|
|
1337
|
+
params: t.object({ id: t.uuid() }),
|
|
1338
|
+
response: t.object({ id: t.uuid() })
|
|
1339
|
+
},
|
|
1340
|
+
handler: async ({ params }) => {
|
|
1341
|
+
return this.workflowService.restartExecution(params.id);
|
|
1342
|
+
}
|
|
1343
|
+
});
|
|
1344
|
+
compensateExecution = $action({
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
path: `${this.url}/executions/:id/compensate`,
|
|
1347
|
+
group: this.group,
|
|
1348
|
+
use: [$secure({ permissions: ["admin:workflow:update"] })],
|
|
1349
|
+
schema: {
|
|
1350
|
+
params: t.object({ id: t.uuid() }),
|
|
1351
|
+
response: okSchema
|
|
1352
|
+
},
|
|
1353
|
+
handler: async ({ params }) => {
|
|
1354
|
+
return this.workflowService.compensateExecution(params.id);
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
};
|
|
1358
|
+
//#endregion
|
|
1359
|
+
//#region ../../src/api/workflows/jobs/WorkflowJobs.ts
|
|
1360
|
+
var WorkflowJobs = class {
|
|
1361
|
+
workflowProvider = $inject(WorkflowProvider);
|
|
1362
|
+
dispatchStep = $job({
|
|
1363
|
+
schema: t.object({
|
|
1364
|
+
workflowId: t.uuid(),
|
|
1365
|
+
stepName: t.text()
|
|
1366
|
+
}),
|
|
1367
|
+
retry: {
|
|
1368
|
+
retries: 2,
|
|
1369
|
+
backoff: [1, "second"]
|
|
1370
|
+
},
|
|
1371
|
+
timeout: [10, "minute"],
|
|
1372
|
+
concurrency: 10,
|
|
1373
|
+
handler: async ({ items }) => {
|
|
1374
|
+
for (const item of items) await this.workflowProvider.processStep(item.payload.workflowId, item.payload.stepName);
|
|
1375
|
+
}
|
|
1376
|
+
});
|
|
1377
|
+
timeoutSweep = $job({
|
|
1378
|
+
cron: "* * * * *",
|
|
1379
|
+
lock: true,
|
|
1380
|
+
handler: async () => {
|
|
1381
|
+
await this.workflowProvider.timeoutSweep();
|
|
1382
|
+
}
|
|
1383
|
+
});
|
|
1384
|
+
purge = $job({
|
|
1385
|
+
cron: "0 3 * * *",
|
|
1386
|
+
lock: true,
|
|
1387
|
+
handler: async () => {
|
|
1388
|
+
await this.workflowProvider.purge();
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
recoverySweep = $job({
|
|
1392
|
+
cron: "*/5 * * * *",
|
|
1393
|
+
lock: true,
|
|
1394
|
+
handler: async () => {
|
|
1395
|
+
await this.workflowProvider.recoverySweep();
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
onStart = $hook({
|
|
1399
|
+
on: "start",
|
|
1400
|
+
handler: async () => {
|
|
1401
|
+
this.workflowProvider.stepDispatch = async (workflowId, stepName, priority) => {
|
|
1402
|
+
await this.dispatchStep.push({
|
|
1403
|
+
workflowId,
|
|
1404
|
+
stepName
|
|
1405
|
+
}, { priority: {
|
|
1406
|
+
0: "critical",
|
|
1407
|
+
1: "high",
|
|
1408
|
+
2: "normal",
|
|
1409
|
+
3: "low"
|
|
1410
|
+
}[priority] ?? "normal" });
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
});
|
|
1414
|
+
};
|
|
1415
|
+
//#endregion
|
|
1416
|
+
//#region ../../src/api/workflows/primitives/$workflow.ts
|
|
1417
|
+
var WorkflowPrimitive = class extends Primitive {
|
|
1418
|
+
workflowProvider = $inject(WorkflowProvider);
|
|
1419
|
+
get name() {
|
|
1420
|
+
return `${this.config.service.name}.${this.config.propertyKey}`;
|
|
1421
|
+
}
|
|
1422
|
+
onInit() {
|
|
1423
|
+
this.workflowProvider.register(this);
|
|
1424
|
+
}
|
|
1425
|
+
/**
|
|
1426
|
+
* Start a new workflow execution.
|
|
1427
|
+
*/
|
|
1428
|
+
async start(payload, options) {
|
|
1429
|
+
return this.workflowProvider.start(this.name, payload, options);
|
|
1430
|
+
}
|
|
1431
|
+
/**
|
|
1432
|
+
* Send a signal to a waiting step on a specific execution.
|
|
1433
|
+
*/
|
|
1434
|
+
async signal(executionId, stepName, payload) {
|
|
1435
|
+
return this.workflowProvider.signal(executionId, stepName, payload);
|
|
1436
|
+
}
|
|
1437
|
+
/**
|
|
1438
|
+
* Cancel a running execution.
|
|
1439
|
+
*/
|
|
1440
|
+
async cancel(executionId, options) {
|
|
1441
|
+
return this.workflowProvider.cancel(executionId, { compensate: options?.compensate });
|
|
1442
|
+
}
|
|
1443
|
+
/**
|
|
1444
|
+
* Retry a failed/timed-out execution from the failed step.
|
|
1445
|
+
*/
|
|
1446
|
+
async retry(executionId) {
|
|
1447
|
+
return this.workflowProvider.retry(executionId);
|
|
1448
|
+
}
|
|
1449
|
+
/**
|
|
1450
|
+
* Restart a terminal execution from the beginning (new execution).
|
|
1451
|
+
*/
|
|
1452
|
+
async restart(executionId) {
|
|
1453
|
+
return this.workflowProvider.restart(executionId);
|
|
1454
|
+
}
|
|
1455
|
+
/**
|
|
1456
|
+
* Get the status of an execution.
|
|
1457
|
+
*/
|
|
1458
|
+
async status(executionId) {
|
|
1459
|
+
return this.workflowProvider.getExecution(executionId);
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
const $workflow = (options) => {
|
|
1463
|
+
return createPrimitive(WorkflowPrimitive, options);
|
|
1464
|
+
};
|
|
1465
|
+
$workflow[KIND] = WorkflowPrimitive;
|
|
1466
|
+
//#endregion
|
|
1467
|
+
//#region ../../src/api/workflows/index.ts
|
|
1468
|
+
/**
|
|
1469
|
+
* Durable workflow engine for long-running business processes.
|
|
1470
|
+
*
|
|
1471
|
+
* **Features:**
|
|
1472
|
+
* - Declarative, multi-step workflows with typed payloads
|
|
1473
|
+
* - Saga-pattern compensation for failure recovery
|
|
1474
|
+
* - Per-step retry with exponential backoff
|
|
1475
|
+
* - Workflow-level timeout and cancellation
|
|
1476
|
+
* - Deduplication via unique keys
|
|
1477
|
+
* - Per-execution log capture
|
|
1478
|
+
*
|
|
1479
|
+
* @module alepha.api.workflows
|
|
1480
|
+
*/
|
|
1481
|
+
const AlephaApiWorkflows = $module({
|
|
1482
|
+
name: "alepha.api.workflows",
|
|
1483
|
+
primitives: [$workflow],
|
|
1484
|
+
services: [
|
|
1485
|
+
AlephaApiJobs,
|
|
1486
|
+
AlephaLock,
|
|
1487
|
+
WorkflowProvider,
|
|
1488
|
+
WorkflowService,
|
|
1489
|
+
WorkflowJobs,
|
|
1490
|
+
AdminWorkflowController
|
|
1491
|
+
],
|
|
1492
|
+
register: (alepha) => {
|
|
1493
|
+
alepha.with(AlephaApiJobs);
|
|
1494
|
+
alepha.with(AlephaLock);
|
|
1495
|
+
alepha.with(WorkflowProvider);
|
|
1496
|
+
alepha.with(WorkflowService);
|
|
1497
|
+
alepha.with(WorkflowJobs);
|
|
1498
|
+
alepha.with(AdminWorkflowController);
|
|
1499
|
+
}
|
|
1500
|
+
});
|
|
1501
|
+
//#endregion
|
|
1502
|
+
export { $workflow, AdminWorkflowController, AlephaApiWorkflows, WorkflowPrimitive, WorkflowProvider, WorkflowService, workflowActivityPointSchema, workflowActivityQuerySchema, workflowConfig, workflowExecutionCanSchema, workflowExecutionDetailSchema, workflowExecutionQuerySchema, workflowExecutionResourceSchema, workflowExecutions, workflowRegistrationSchema, workflowStatsSchema, workflowStepExecutionResourceSchema, workflowStepExecutions, workflowStepLogs };
|
|
1503
|
+
|
|
1504
|
+
//# sourceMappingURL=index.js.map
|