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.
Files changed (241) hide show
  1. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  2. package/dist/api/audits/index.d.ts +8 -8
  3. package/dist/api/invitations/index.d.ts +790 -0
  4. package/dist/api/invitations/index.d.ts.map +1 -0
  5. package/dist/api/invitations/index.js +665 -0
  6. package/dist/api/invitations/index.js.map +1 -0
  7. package/dist/api/jobs/index.browser.js +8 -9
  8. package/dist/api/jobs/index.browser.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +90 -34
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js +267 -44
  12. package/dist/api/jobs/index.js.map +1 -1
  13. package/dist/api/notifications/index.browser.js +0 -1
  14. package/dist/api/notifications/index.browser.js.map +1 -1
  15. package/dist/api/notifications/index.d.ts +3 -3
  16. package/dist/api/notifications/index.d.ts.map +1 -1
  17. package/dist/api/notifications/index.js +0 -1
  18. package/dist/api/notifications/index.js.map +1 -1
  19. package/dist/api/parameters/index.browser.js +112 -1
  20. package/dist/api/parameters/index.browser.js.map +1 -1
  21. package/dist/api/parameters/index.d.ts +90 -3
  22. package/dist/api/parameters/index.d.ts.map +1 -1
  23. package/dist/api/parameters/index.js +79 -12
  24. package/dist/api/parameters/index.js.map +1 -1
  25. package/dist/{billing → api/payments}/index.d.ts +67 -49
  26. package/dist/api/payments/index.d.ts.map +1 -0
  27. package/dist/{billing → api/payments}/index.js +108 -74
  28. package/dist/api/payments/index.js.map +1 -0
  29. package/dist/api/subscriptions/index.d.ts +1692 -0
  30. package/dist/api/subscriptions/index.d.ts.map +1 -0
  31. package/dist/api/subscriptions/index.js +1870 -0
  32. package/dist/api/subscriptions/index.js.map +1 -0
  33. package/dist/api/users/index.d.ts +27 -21
  34. package/dist/api/users/index.d.ts.map +1 -1
  35. package/dist/api/users/index.js +167 -34
  36. package/dist/api/users/index.js.map +1 -1
  37. package/dist/api/workflows/index.browser.js +246 -0
  38. package/dist/api/workflows/index.browser.js.map +1 -0
  39. package/dist/api/workflows/index.d.ts +1618 -0
  40. package/dist/api/workflows/index.d.ts.map +1 -0
  41. package/dist/api/workflows/index.js +1504 -0
  42. package/dist/api/workflows/index.js.map +1 -0
  43. package/dist/cli/config/index.d.ts +6 -28
  44. package/dist/cli/config/index.d.ts.map +1 -1
  45. package/dist/cli/config/index.js +5 -10
  46. package/dist/cli/config/index.js.map +1 -1
  47. package/dist/cli/core/index.d.ts +11669 -208
  48. package/dist/cli/core/index.d.ts.map +1 -1
  49. package/dist/cli/core/index.js +60 -69
  50. package/dist/cli/core/index.js.map +1 -1
  51. package/dist/cli/devtools/index.d.ts +5 -0
  52. package/dist/cli/devtools/index.d.ts.map +1 -1
  53. package/dist/cli/devtools/index.js +4 -0
  54. package/dist/cli/devtools/index.js.map +1 -1
  55. package/dist/cli/platform/index.d.ts +69 -64
  56. package/dist/cli/platform/index.d.ts.map +1 -1
  57. package/dist/cli/platform/index.js +6 -2
  58. package/dist/cli/platform/index.js.map +1 -1
  59. package/dist/cli/vendor/index.d.ts +38 -10
  60. package/dist/cli/vendor/index.d.ts.map +1 -1
  61. package/dist/cli/vendor/index.js +85 -26
  62. package/dist/cli/vendor/index.js.map +1 -1
  63. package/dist/core/index.browser.js +21 -2
  64. package/dist/core/index.browser.js.map +1 -1
  65. package/dist/core/index.d.ts +33 -2
  66. package/dist/core/index.d.ts.map +1 -1
  67. package/dist/core/index.js +25 -2
  68. package/dist/core/index.js.map +1 -1
  69. package/dist/core/index.native.js +25 -2
  70. package/dist/core/index.native.js.map +1 -1
  71. package/dist/core/index.workerd.js +25 -2
  72. package/dist/core/index.workerd.js.map +1 -1
  73. package/dist/email/smtp/index.js +24 -8
  74. package/dist/email/smtp/index.js.map +1 -1
  75. package/dist/logger/index.d.ts.map +1 -1
  76. package/dist/logger/index.js +1 -1
  77. package/dist/logger/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +0 -18
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +25 -73
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +10 -32
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +25 -73
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.bun.js +3 -3
  87. package/dist/orm/postgres/index.bun.js.map +1 -1
  88. package/dist/orm/postgres/index.d.ts +2 -1
  89. package/dist/orm/postgres/index.d.ts.map +1 -1
  90. package/dist/orm/postgres/index.js +3 -3
  91. package/dist/orm/postgres/index.js.map +1 -1
  92. package/dist/react/router/index.browser.js +25 -3
  93. package/dist/react/router/index.browser.js.map +1 -1
  94. package/dist/react/router/index.d.ts +16 -1
  95. package/dist/react/router/index.d.ts.map +1 -1
  96. package/dist/react/router/index.js +25 -3
  97. package/dist/react/router/index.js.map +1 -1
  98. package/dist/security/index.d.ts +28 -0
  99. package/dist/security/index.d.ts.map +1 -1
  100. package/dist/security/index.js +28 -0
  101. package/dist/security/index.js.map +1 -1
  102. package/package.json +37 -20
  103. package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
  104. package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
  105. package/src/api/invitations/controllers/InvitationController.ts +84 -0
  106. package/src/api/invitations/entities/invitations.ts +33 -0
  107. package/src/api/invitations/index.ts +65 -0
  108. package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
  109. package/src/api/invitations/providers/InvitationProvider.ts +45 -0
  110. package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
  111. package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
  112. package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
  113. package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
  114. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
  115. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
  116. package/src/api/invitations/services/InvitationService.ts +556 -0
  117. package/src/api/jobs/__tests__/$job.spec.ts +876 -0
  118. package/src/api/jobs/controllers/AdminJobController.ts +44 -0
  119. package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
  120. package/src/api/jobs/index.ts +0 -3
  121. package/src/api/jobs/primitives/$job.ts +22 -11
  122. package/src/api/jobs/providers/JobProvider.ts +239 -25
  123. package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
  124. package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
  125. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
  126. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
  127. package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
  128. package/src/api/jobs/services/JobService.ts +51 -12
  129. package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
  130. package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
  131. package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
  132. package/src/api/parameters/index.browser.ts +12 -0
  133. package/src/api/parameters/primitives/$parameter.ts +20 -3
  134. package/src/api/parameters/services/ParameterProvider.ts +48 -7
  135. package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
  136. package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
  137. package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
  138. package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
  139. package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
  140. package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
  141. package/src/{billing → api/payments}/index.ts +31 -25
  142. package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
  143. package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
  144. package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
  145. package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
  146. package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
  147. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
  148. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
  149. package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
  150. package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
  151. package/src/api/subscriptions/entities/subscriptions.ts +68 -0
  152. package/src/api/subscriptions/index.ts +144 -0
  153. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
  154. package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
  155. package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
  156. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
  157. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
  158. package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
  159. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
  160. package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
  161. package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
  162. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
  163. package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
  164. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
  165. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
  166. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
  167. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
  168. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
  169. package/src/api/subscriptions/services/BillingService.ts +437 -0
  170. package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
  171. package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
  172. package/src/api/subscriptions/services/UsageService.ts +118 -0
  173. package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
  174. package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
  175. package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
  176. package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
  177. package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
  178. package/src/api/users/__tests__/SessionService.spec.ts +142 -1
  179. package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
  180. package/src/api/users/controllers/UserController.ts +3 -8
  181. package/src/api/users/notifications/UserNotifications.ts +23 -0
  182. package/src/api/users/schemas/loginSchema.ts +1 -1
  183. package/src/api/users/services/CredentialService.ts +51 -4
  184. package/src/api/users/services/RegistrationService.ts +38 -9
  185. package/src/api/users/services/SessionService.ts +62 -9
  186. package/src/api/users/services/UserService.ts +21 -12
  187. package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
  188. package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
  189. package/src/api/workflows/entities/workflowExecutions.ts +74 -0
  190. package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
  191. package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
  192. package/src/api/workflows/index.browser.ts +22 -0
  193. package/src/api/workflows/index.ts +124 -0
  194. package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
  195. package/src/api/workflows/primitives/$workflow.ts +202 -0
  196. package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
  197. package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
  198. package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
  199. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
  200. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
  201. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
  202. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
  203. package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
  204. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
  205. package/src/api/workflows/services/WorkflowService.ts +382 -0
  206. package/src/cli/config/defineConfig.ts +17 -46
  207. package/src/cli/core/providers/ViteDevServerProvider.ts +45 -3
  208. package/src/cli/core/services/PackageManagerUtils.ts +3 -1
  209. package/src/cli/core/services/ProjectScaffolder.ts +5 -5
  210. package/src/cli/core/templates/agentMd.ts +14 -5
  211. package/src/cli/core/templates/webAppRouterTs.ts +5 -58
  212. package/src/cli/devtools/index.ts +21 -1
  213. package/src/cli/platform/index.ts +23 -2
  214. package/src/cli/vendor/__tests__/VendorService.spec.ts +283 -178
  215. package/src/cli/vendor/index.ts +20 -3
  216. package/src/cli/vendor/services/VendorService.ts +126 -27
  217. package/src/core/Alepha.ts +10 -0
  218. package/src/core/__tests__/TypeProvider.spec.ts +4 -2
  219. package/src/core/providers/SchemaValidator.ts +1 -1
  220. package/src/core/providers/TypeProvider.ts +46 -3
  221. package/src/logger/index.ts +6 -1
  222. package/src/orm/__tests__/enums.spec.ts +22 -29
  223. package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
  224. package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
  225. package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
  226. package/src/orm/core/providers/DrizzleKitProvider.ts +56 -105
  227. package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
  228. package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
  229. package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
  230. package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
  231. package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
  232. package/src/security/primitives/$secure.ts +28 -0
  233. package/tsconfig.base.json +0 -1
  234. package/dist/billing/index.d.ts.map +0 -1
  235. package/dist/billing/index.js.map +0 -1
  236. package/src/billing/__tests__/BillingService.spec.ts +0 -136
  237. /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
  238. /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
  239. /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
  240. /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
  241. /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