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
@@ -49,7 +49,6 @@ export class UserService {
49
49
  email: string,
50
50
  userRealmName?: string,
51
51
  method: "code" | "link" = "code",
52
- verifyUrl?: string,
53
52
  ): Promise<boolean> {
54
53
  this.log.trace("Requesting email verification", {
55
54
  email,
@@ -84,12 +83,15 @@ export class UserService {
84
83
  });
85
84
 
86
85
  if (method === "link") {
87
- // Build verification URL with token
88
- const url = new URL(verifyUrl || "/verify-email", "http://localhost");
86
+ // Build verification URL from realm settings (server-controlled, not user input)
87
+ const realm = this.realmProvider.getRealm(userRealmName);
88
+ const realmSettings = await realm.getSettings();
89
+ const baseUrl = realmSettings.verifyEmailUrl ?? "/verify-email";
90
+ const url = new URL(baseUrl, "http://localhost");
89
91
  url.searchParams.set("email", email);
90
92
  url.searchParams.set("token", verification.token);
91
- const fullVerifyUrl = verifyUrl
92
- ? `${verifyUrl}${url.search}`
93
+ const fullVerifyUrl = realmSettings.verifyEmailUrl
94
+ ? `${baseUrl}${url.search}`
93
95
  : url.pathname + url.search;
94
96
 
95
97
  await this.userNotifications(userRealmName)?.emailVerificationLink.push(
@@ -270,13 +272,12 @@ export class UserService {
270
272
  });
271
273
 
272
274
  const realm = this.realmProvider.getRealm(userRealmName);
275
+ const realmSettings = await realm.getSettings();
273
276
 
274
- // TODO: one query instead of 3
275
-
276
- // Check for existing user based on provided unique fields
277
+ // Check for existing user based on provided unique fields (scoped to realm)
277
278
  if (data.username) {
278
279
  const existingUser = await this.users(userRealmName).findOne({
279
- where: { username: { ilike: data.username } },
280
+ where: { realm: realm.name, username: { ilike: data.username } },
280
281
  });
281
282
 
282
283
  if (existingUser) {
@@ -287,7 +288,7 @@ export class UserService {
287
288
 
288
289
  if (data.email) {
289
290
  const existingUser = await this.users(userRealmName).findOne({
290
- where: { email: { eq: data.email } },
291
+ where: { realm: realm.name, email: { eq: data.email } },
291
292
  });
292
293
 
293
294
  if (existingUser) {
@@ -298,7 +299,7 @@ export class UserService {
298
299
 
299
300
  if (data.phoneNumber) {
300
301
  const existingUser = await this.users(userRealmName).findOne({
301
- where: { phoneNumber: { eq: data.phoneNumber } },
302
+ where: { realm: realm.name, phoneNumber: { eq: data.phoneNumber } },
302
303
  });
303
304
 
304
305
  if (existingUser) {
@@ -311,7 +312,7 @@ export class UserService {
311
312
 
312
313
  const user = await this.users(userRealmName).create({
313
314
  ...data,
314
- roles: data.roles ?? ["user"], // TODO: Default roles from realm settings
315
+ roles: data.roles ?? realmSettings.defaultRoles,
315
316
  realm: realm.name,
316
317
  });
317
318
 
@@ -386,6 +387,14 @@ export class UserService {
386
387
  this.log.trace("Deleting user", { id, userRealmName });
387
388
  const user = await this.getUserById(id, userRealmName);
388
389
 
390
+ // Clean up related sessions and identities before deleting the user
391
+ await this.realmProvider
392
+ .sessionRepository(userRealmName)
393
+ .deleteMany({ userId: { eq: id } });
394
+ await this.realmProvider
395
+ .identityRepository(userRealmName)
396
+ .deleteMany({ userId: { eq: id } });
397
+
389
398
  await this.users(userRealmName).deleteById(id);
390
399
  this.log.info("User deleted", { userId: id });
391
400
 
@@ -0,0 +1,616 @@
1
+ import { Alepha, t } from "alepha";
2
+ import { $repository } from "alepha/orm";
3
+ import { AlephaOrmPostgres } from "alepha/orm/postgres";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import {
6
+ $workflow,
7
+ AlephaApiWorkflows,
8
+ workflowExecutions,
9
+ workflowStepExecutions,
10
+ } from "../index.ts";
11
+
12
+ // -----------------------------------------------------------------------------------------------------------------
13
+
14
+ describe("$workflow", () => {
15
+ // ----- Basic functionality -----
16
+
17
+ describe("basic functionality", () => {
18
+ it("should start and complete a single-step workflow", async () => {
19
+ const handler = vi.fn();
20
+
21
+ class App {
22
+ repo = $repository(workflowExecutions);
23
+ stepRepo = $repository(workflowStepExecutions);
24
+ myWorkflow = $workflow({
25
+ schema: t.object({ orderId: t.uuid() }),
26
+ steps: [
27
+ {
28
+ name: "processOrder",
29
+ handler: async ({ payload }) => {
30
+ handler(payload);
31
+ return { processed: true };
32
+ },
33
+ },
34
+ ],
35
+ });
36
+ }
37
+
38
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
39
+ alepha.with(AlephaApiWorkflows);
40
+ alepha.with(App);
41
+ await alepha.start();
42
+
43
+ const app = alepha.inject(App);
44
+ const orderId = crypto.randomUUID();
45
+ const executionId = await app.myWorkflow.start({ orderId });
46
+
47
+ await vi.waitFor(async () => {
48
+ const exec = await app.repo.findById(executionId);
49
+ expect(exec?.status).toBe("completed");
50
+ });
51
+
52
+ expect(handler).toHaveBeenCalledTimes(1);
53
+ expect(handler).toHaveBeenCalledWith(
54
+ expect.objectContaining({ orderId }),
55
+ );
56
+
57
+ // Verify step is completed with result
58
+ const steps = await app.stepRepo.findMany({
59
+ where: { workflowExecutionId: { eq: executionId } },
60
+ });
61
+ expect(steps).toHaveLength(1);
62
+ expect(steps[0].status).toBe("completed");
63
+ expect(steps[0].result).toEqual({ processed: true });
64
+ });
65
+
66
+ it("should execute multi-step workflow in order", async () => {
67
+ const order: string[] = [];
68
+
69
+ class App {
70
+ repo = $repository(workflowExecutions);
71
+ myWorkflow = $workflow({
72
+ schema: t.object({ id: t.text() }),
73
+ steps: [
74
+ {
75
+ name: "step1",
76
+ handler: async () => {
77
+ order.push("step1");
78
+ return { a: 1 };
79
+ },
80
+ },
81
+ {
82
+ name: "step2",
83
+ handler: async ({ results }) => {
84
+ order.push("step2");
85
+ return { b: 2, fromStep1: results.step1 };
86
+ },
87
+ },
88
+ {
89
+ name: "step3",
90
+ handler: async ({ results }) => {
91
+ order.push("step3");
92
+ return { c: 3, fromStep2: results.step2 };
93
+ },
94
+ },
95
+ ],
96
+ });
97
+ }
98
+
99
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
100
+ alepha.with(AlephaApiWorkflows);
101
+ alepha.with(App);
102
+ await alepha.start();
103
+
104
+ const app = alepha.inject(App);
105
+ const executionId = await app.myWorkflow.start({ id: "test" });
106
+
107
+ await vi.waitFor(async () => {
108
+ const exec = await app.repo.findById(executionId);
109
+ expect(exec?.status).toBe("completed");
110
+ });
111
+
112
+ expect(order).toEqual(["step1", "step2", "step3"]);
113
+ });
114
+
115
+ it("should pass accumulated results between steps", async () => {
116
+ let step2Results: Record<string, unknown> = {};
117
+
118
+ class App {
119
+ repo = $repository(workflowExecutions);
120
+ myWorkflow = $workflow({
121
+ schema: t.object({ value: t.text() }),
122
+ steps: [
123
+ {
124
+ name: "first",
125
+ handler: async () => ({ key: "from-first" }),
126
+ },
127
+ {
128
+ name: "second",
129
+ handler: async ({ results }) => {
130
+ step2Results = results;
131
+ },
132
+ },
133
+ ],
134
+ });
135
+ }
136
+
137
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
138
+ alepha.with(AlephaApiWorkflows);
139
+ alepha.with(App);
140
+ await alepha.start();
141
+
142
+ const app = alepha.inject(App);
143
+ const executionId = await app.myWorkflow.start({ value: "test" });
144
+
145
+ await vi.waitFor(async () => {
146
+ const exec = await app.repo.findById(executionId);
147
+ expect(exec?.status).toBe("completed");
148
+ });
149
+
150
+ expect(step2Results.first).toEqual({ key: "from-first" });
151
+ });
152
+
153
+ it("should use ClassName.propertyKey as workflow name", async () => {
154
+ class OrderService {
155
+ processOrder = $workflow({
156
+ schema: t.object({ id: t.text() }),
157
+ steps: [{ name: "step1", handler: async () => {} }],
158
+ });
159
+ }
160
+
161
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
162
+ alepha.with(AlephaApiWorkflows);
163
+ alepha.with(OrderService);
164
+ await alepha.start();
165
+
166
+ const app = alepha.inject(OrderService);
167
+ expect(app.processOrder.name).toBe("OrderService.processOrder");
168
+ });
169
+ });
170
+
171
+ // ----- Compensation (saga pattern) -----
172
+
173
+ describe("compensation", () => {
174
+ it("should compensate completed steps in reverse order on failure", async () => {
175
+ const compensations: string[] = [];
176
+
177
+ class App {
178
+ repo = $repository(workflowExecutions);
179
+ myWorkflow = $workflow({
180
+ schema: t.object({ id: t.text() }),
181
+ onError: "compensate",
182
+ steps: [
183
+ {
184
+ name: "step1",
185
+ handler: async () => ({ id: "r1" }),
186
+ compensate: async () => {
187
+ compensations.push("step1");
188
+ },
189
+ },
190
+ {
191
+ name: "step2",
192
+ handler: async () => ({ id: "r2" }),
193
+ compensate: async () => {
194
+ compensations.push("step2");
195
+ },
196
+ },
197
+ {
198
+ name: "step3",
199
+ handler: async () => {
200
+ throw new Error("step3 failed");
201
+ },
202
+ compensate: async () => {
203
+ compensations.push("step3");
204
+ },
205
+ },
206
+ ],
207
+ });
208
+ }
209
+
210
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
211
+ alepha.with(AlephaApiWorkflows);
212
+ alepha.with(App);
213
+ await alepha.start();
214
+
215
+ const app = alepha.inject(App);
216
+ const executionId = await app.myWorkflow.start({ id: "test" });
217
+
218
+ await vi.waitFor(async () => {
219
+ const exec = await app.repo.findById(executionId);
220
+ expect(exec?.status).toBe("compensated");
221
+ });
222
+
223
+ // step3 never completed so no compensation for it
224
+ // step2 and step1 compensated in reverse order
225
+ expect(compensations).toEqual(["step2", "step1"]);
226
+ });
227
+
228
+ it("should mark as failed when onError is fail", async () => {
229
+ class App {
230
+ repo = $repository(workflowExecutions);
231
+ myWorkflow = $workflow({
232
+ schema: t.object({ id: t.text() }),
233
+ onError: "fail",
234
+ steps: [
235
+ {
236
+ name: "step1",
237
+ handler: async () => ({ ok: true }),
238
+ },
239
+ {
240
+ name: "step2",
241
+ handler: async () => {
242
+ throw new Error("boom");
243
+ },
244
+ },
245
+ ],
246
+ });
247
+ }
248
+
249
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
250
+ alepha.with(AlephaApiWorkflows);
251
+ alepha.with(App);
252
+ await alepha.start();
253
+
254
+ const app = alepha.inject(App);
255
+ const executionId = await app.myWorkflow.start({ id: "test" });
256
+
257
+ await vi.waitFor(async () => {
258
+ const exec = await app.repo.findById(executionId);
259
+ expect(exec?.status).toBe("failed");
260
+ });
261
+
262
+ const exec = await app.repo.findById(executionId);
263
+ expect(exec?.error).toBe("boom");
264
+ expect(exec?.errorStep).toBe("step2");
265
+ });
266
+ });
267
+
268
+ // ----- Retry -----
269
+
270
+ describe("retry", () => {
271
+ it("should retry a step on failure with retries configured", async () => {
272
+ let callCount = 0;
273
+
274
+ class App {
275
+ repo = $repository(workflowExecutions);
276
+ stepRepo = $repository(workflowStepExecutions);
277
+ myWorkflow = $workflow({
278
+ schema: t.object({ id: t.text() }),
279
+ steps: [
280
+ {
281
+ name: "flaky",
282
+ retry: { retries: 2, backoff: [10, "millisecond"] },
283
+ handler: async () => {
284
+ callCount++;
285
+ if (callCount < 3) throw new Error(`fail #${callCount}`);
286
+ return { ok: true };
287
+ },
288
+ },
289
+ ],
290
+ });
291
+ }
292
+
293
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
294
+ alepha.with(AlephaApiWorkflows);
295
+ alepha.with(App);
296
+ await alepha.start();
297
+
298
+ const app = alepha.inject(App);
299
+ const executionId = await app.myWorkflow.start({ id: "test" });
300
+
301
+ await vi.waitFor(
302
+ async () => {
303
+ const exec = await app.repo.findById(executionId);
304
+ expect(exec?.status).toBe("completed");
305
+ },
306
+ { timeout: 10_000 },
307
+ );
308
+
309
+ expect(callCount).toBe(3);
310
+ });
311
+ });
312
+
313
+ // ----- Conditional steps -----
314
+
315
+ describe("conditional steps", () => {
316
+ it("should skip steps when condition returns false", async () => {
317
+ const executed: string[] = [];
318
+
319
+ class App {
320
+ repo = $repository(workflowExecutions);
321
+ stepRepo = $repository(workflowStepExecutions);
322
+ myWorkflow = $workflow({
323
+ schema: t.object({ skipMiddle: t.boolean() }),
324
+ steps: [
325
+ {
326
+ name: "step1",
327
+ handler: async () => {
328
+ executed.push("step1");
329
+ return { value: 1 };
330
+ },
331
+ },
332
+ {
333
+ name: "step2",
334
+ when: ({ payload }) => !payload.skipMiddle,
335
+ handler: async () => {
336
+ executed.push("step2");
337
+ return { value: 2 };
338
+ },
339
+ },
340
+ {
341
+ name: "step3",
342
+ handler: async () => {
343
+ executed.push("step3");
344
+ return { value: 3 };
345
+ },
346
+ },
347
+ ],
348
+ });
349
+ }
350
+
351
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
352
+ alepha.with(AlephaApiWorkflows);
353
+ alepha.with(App);
354
+ await alepha.start();
355
+
356
+ const app = alepha.inject(App);
357
+ const executionId = await app.myWorkflow.start({ skipMiddle: true });
358
+
359
+ await vi.waitFor(async () => {
360
+ const exec = await app.repo.findById(executionId);
361
+ expect(exec?.status).toBe("completed");
362
+ });
363
+
364
+ expect(executed).toEqual(["step1", "step3"]);
365
+
366
+ // Verify step2 was skipped
367
+ const steps = await app.stepRepo.findMany({
368
+ where: { workflowExecutionId: { eq: executionId } },
369
+ orderBy: { column: "stepIndex", direction: "asc" },
370
+ });
371
+ expect(steps[1].status).toBe("skipped");
372
+ });
373
+ });
374
+
375
+ // ----- Cancel -----
376
+
377
+ describe("cancel", () => {
378
+ it("should cancel a pending workflow", async () => {
379
+ class App {
380
+ repo = $repository(workflowExecutions);
381
+ stepRepo = $repository(workflowStepExecutions);
382
+ myWorkflow = $workflow({
383
+ schema: t.object({ id: t.text() }),
384
+ steps: [
385
+ {
386
+ name: "long",
387
+ handler: async ({ signal }) => {
388
+ await new Promise<void>((resolve) => {
389
+ const check = () => {
390
+ if (signal.aborted) resolve();
391
+ else setTimeout(check, 10);
392
+ };
393
+ check();
394
+ });
395
+ },
396
+ },
397
+ ],
398
+ });
399
+ }
400
+
401
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
402
+ alepha.with(AlephaApiWorkflows);
403
+ alepha.with(App);
404
+ await alepha.start();
405
+
406
+ const app = alepha.inject(App);
407
+ const executionId = await app.myWorkflow.start({ id: "test" });
408
+
409
+ // Wait for it to be running
410
+ await vi.waitFor(async () => {
411
+ const steps = await app.stepRepo.findMany({
412
+ where: {
413
+ workflowExecutionId: { eq: executionId },
414
+ status: { eq: "running" },
415
+ },
416
+ });
417
+ expect(steps).toHaveLength(1);
418
+ });
419
+
420
+ await app.myWorkflow.cancel(executionId);
421
+
422
+ await vi.waitFor(async () => {
423
+ const exec = await app.repo.findById(executionId);
424
+ expect(exec?.status).toBe("cancelled");
425
+ });
426
+ });
427
+ });
428
+
429
+ // ----- Retry (admin action) -----
430
+
431
+ describe("admin retry", () => {
432
+ it("should retry a failed workflow from the failed step", async () => {
433
+ let callCount = 0;
434
+
435
+ class App {
436
+ repo = $repository(workflowExecutions);
437
+ myWorkflow = $workflow({
438
+ schema: t.object({ id: t.text() }),
439
+ onError: "fail",
440
+ steps: [
441
+ {
442
+ name: "step1",
443
+ handler: async () => ({ ok: true }),
444
+ },
445
+ {
446
+ name: "step2",
447
+ handler: async () => {
448
+ callCount++;
449
+ if (callCount === 1) throw new Error("transient");
450
+ return { ok: true };
451
+ },
452
+ },
453
+ ],
454
+ });
455
+ }
456
+
457
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
458
+ alepha.with(AlephaApiWorkflows);
459
+ alepha.with(App);
460
+ await alepha.start();
461
+
462
+ const app = alepha.inject(App);
463
+ const executionId = await app.myWorkflow.start({ id: "test" });
464
+
465
+ await vi.waitFor(async () => {
466
+ const exec = await app.repo.findById(executionId);
467
+ expect(exec?.status).toBe("failed");
468
+ });
469
+
470
+ // Retry from the failed step
471
+ await app.myWorkflow.retry(executionId);
472
+
473
+ await vi.waitFor(async () => {
474
+ const exec = await app.repo.findById(executionId);
475
+ expect(exec?.status).toBe("completed");
476
+ });
477
+
478
+ expect(callCount).toBe(2);
479
+ });
480
+ });
481
+
482
+ // ----- Restart -----
483
+
484
+ describe("restart", () => {
485
+ it("should create a new execution from the same payload", async () => {
486
+ let callCount = 0;
487
+
488
+ class App {
489
+ repo = $repository(workflowExecutions);
490
+ myWorkflow = $workflow({
491
+ schema: t.object({ id: t.text() }),
492
+ onError: "fail",
493
+ steps: [
494
+ {
495
+ name: "step1",
496
+ handler: async () => {
497
+ callCount++;
498
+ if (callCount === 1) throw new Error("fail first time");
499
+ return { ok: true };
500
+ },
501
+ },
502
+ ],
503
+ });
504
+ }
505
+
506
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
507
+ alepha.with(AlephaApiWorkflows);
508
+ alepha.with(App);
509
+ await alepha.start();
510
+
511
+ const app = alepha.inject(App);
512
+ const executionId = await app.myWorkflow.start({ id: "test" });
513
+
514
+ await vi.waitFor(async () => {
515
+ const exec = await app.repo.findById(executionId);
516
+ expect(exec?.status).toBe("failed");
517
+ });
518
+
519
+ const newId = await app.myWorkflow.restart(executionId);
520
+ expect(newId).not.toBe(executionId);
521
+
522
+ await vi.waitFor(async () => {
523
+ const exec = await app.repo.findById(newId);
524
+ expect(exec?.status).toBe("completed");
525
+ });
526
+
527
+ expect(callCount).toBe(2);
528
+ });
529
+ });
530
+
531
+ // ----- Events -----
532
+
533
+ describe("events", () => {
534
+ it("should emit workflow lifecycle events", async () => {
535
+ const events: string[] = [];
536
+
537
+ class App {
538
+ repo = $repository(workflowExecutions);
539
+ myWorkflow = $workflow({
540
+ schema: t.object({ id: t.text() }),
541
+ steps: [{ name: "step1", handler: async () => ({ ok: true }) }],
542
+ });
543
+ }
544
+
545
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
546
+ alepha.with(AlephaApiWorkflows);
547
+ alepha.with(App);
548
+ alepha.events.on("workflow:started", () => {
549
+ events.push("started");
550
+ });
551
+ alepha.events.on("workflow:step:begin", () => {
552
+ events.push("step:begin");
553
+ });
554
+ alepha.events.on("workflow:step:completed", () => {
555
+ events.push("step:completed");
556
+ });
557
+ alepha.events.on("workflow:completed", () => {
558
+ events.push("completed");
559
+ });
560
+ await alepha.start();
561
+
562
+ const app = alepha.inject(App);
563
+ const executionId = await app.myWorkflow.start({ id: "test" });
564
+
565
+ await vi.waitFor(async () => {
566
+ const exec = await app.repo.findById(executionId);
567
+ expect(exec?.status).toBe("completed");
568
+ });
569
+
570
+ expect(events).toContain("started");
571
+ expect(events).toContain("step:begin");
572
+ expect(events).toContain("step:completed");
573
+ expect(events).toContain("completed");
574
+ });
575
+ });
576
+
577
+ // ----- Deduplication -----
578
+
579
+ describe("deduplication", () => {
580
+ it("should return existing execution for same key", async () => {
581
+ class App {
582
+ repo = $repository(workflowExecutions);
583
+ myWorkflow = $workflow({
584
+ schema: t.object({ id: t.text() }),
585
+ steps: [{ name: "step1", handler: async () => ({ ok: true }) }],
586
+ });
587
+ }
588
+
589
+ const alepha = Alepha.create().with(AlephaOrmPostgres);
590
+ alepha.with(AlephaApiWorkflows);
591
+ alepha.with(App);
592
+ await alepha.start();
593
+
594
+ const app = alepha.inject(App);
595
+
596
+ // First start with key — creates a new execution
597
+ const id1 = await app.myWorkflow.start(
598
+ { id: "test" },
599
+ { key: "dedup-key" },
600
+ );
601
+
602
+ // Wait for completion (key is cleared on completion)
603
+ await vi.waitFor(async () => {
604
+ const exec = await app.repo.findById(id1);
605
+ expect(exec?.status).toBe("completed");
606
+ });
607
+
608
+ // Second start with same key after completion — creates NEW execution
609
+ const id2 = await app.myWorkflow.start(
610
+ { id: "test" },
611
+ { key: "dedup-key" },
612
+ );
613
+ expect(id2).not.toBe(id1);
614
+ });
615
+ });
616
+ });