alepha 0.20.1 → 0.20.2

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 (232) hide show
  1. package/dist/api/files/index.js +2 -1
  2. package/dist/api/files/index.js.map +1 -1
  3. package/dist/api/jobs/index.browser.js +64 -148
  4. package/dist/api/jobs/index.browser.js.map +1 -1
  5. package/dist/api/jobs/index.d.ts +371 -573
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/jobs/index.js +605 -1012
  8. package/dist/api/jobs/index.js.map +1 -1
  9. package/dist/api/notifications/index.d.ts +78 -17
  10. package/dist/api/notifications/index.d.ts.map +1 -1
  11. package/dist/api/notifications/index.js +90 -23
  12. package/dist/api/notifications/index.js.map +1 -1
  13. package/dist/api/payments/index.d.ts +2 -1
  14. package/dist/api/payments/index.d.ts.map +1 -1
  15. package/dist/api/payments/index.js +4 -2
  16. package/dist/api/payments/index.js.map +1 -1
  17. package/dist/api/users/index.d.ts +34 -31
  18. package/dist/api/users/index.d.ts.map +1 -1
  19. package/dist/api/users/index.js +13 -7
  20. package/dist/api/users/index.js.map +1 -1
  21. package/dist/api/verifications/index.js +2 -1
  22. package/dist/api/verifications/index.js.map +1 -1
  23. package/dist/cli/core/index.d.ts +8 -34
  24. package/dist/cli/core/index.d.ts.map +1 -1
  25. package/dist/cli/core/index.js +43 -232
  26. package/dist/cli/core/index.js.map +1 -1
  27. package/dist/cli/platform/index.d.ts +36 -11
  28. package/dist/cli/platform/index.d.ts.map +1 -1
  29. package/dist/cli/platform/index.js +93 -27
  30. package/dist/cli/platform/index.js.map +1 -1
  31. package/dist/command/index.d.ts +1 -1
  32. package/dist/core/index.browser.js +6 -0
  33. package/dist/core/index.browser.js.map +1 -1
  34. package/dist/core/index.d.ts +6 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/index.native.js +6 -0
  39. package/dist/core/index.native.js.map +1 -1
  40. package/dist/core/index.workerd.js +6 -0
  41. package/dist/core/index.workerd.js.map +1 -1
  42. package/dist/react/form/index.d.ts +60 -1
  43. package/dist/react/form/index.d.ts.map +1 -1
  44. package/dist/react/form/index.js +86 -1
  45. package/dist/react/form/index.js.map +1 -1
  46. package/dist/react/head/index.browser.js +16 -1
  47. package/dist/react/head/index.browser.js.map +1 -1
  48. package/dist/react/head/index.d.ts +6 -0
  49. package/dist/react/head/index.d.ts.map +1 -1
  50. package/dist/react/head/index.js +16 -1
  51. package/dist/react/head/index.js.map +1 -1
  52. package/dist/react/router/index.browser.js +0 -10
  53. package/dist/react/router/index.browser.js.map +1 -1
  54. package/dist/react/router/index.d.ts +35 -12
  55. package/dist/react/router/index.d.ts.map +1 -1
  56. package/dist/react/router/index.js +0 -10
  57. package/dist/react/router/index.js.map +1 -1
  58. package/dist/react/ui/index.d.ts +124 -0
  59. package/dist/react/ui/index.d.ts.map +1 -0
  60. package/dist/react/ui/index.js +206 -0
  61. package/dist/react/ui/index.js.map +1 -0
  62. package/dist/router/index.d.ts +13 -13
  63. package/dist/router/index.d.ts.map +1 -1
  64. package/dist/router/index.js +45 -32
  65. package/dist/router/index.js.map +1 -1
  66. package/dist/system/index.d.ts.map +1 -1
  67. package/dist/system/index.js +1 -0
  68. package/dist/system/index.js.map +1 -1
  69. package/dist/topic/core/index.js +1 -1
  70. package/dist/topic/core/index.js.map +1 -1
  71. package/package.json +6 -23
  72. package/src/api/files/jobs/FileJobs.ts +2 -1
  73. package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
  74. package/src/api/jobs/controllers/AdminJobController.ts +29 -138
  75. package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
  76. package/src/api/jobs/index.browser.ts +5 -7
  77. package/src/api/jobs/index.ts +23 -51
  78. package/src/api/jobs/primitives/$job.ts +66 -58
  79. package/src/api/jobs/providers/JobProvider.ts +561 -566
  80. package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
  81. package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
  82. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
  83. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
  84. package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
  85. package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
  86. package/src/api/jobs/services/JobService.ts +90 -483
  87. package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
  88. package/src/api/notifications/index.ts +7 -4
  89. package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
  90. package/src/api/payments/services/PaymentService.ts +4 -2
  91. package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
  92. package/src/api/users/audits/UserAudits.ts +3 -1
  93. package/src/api/users/buckets/UserBuckets.ts +2 -1
  94. package/src/api/users/index.ts +1 -4
  95. package/src/api/users/jobs/UserJobs.ts +5 -4
  96. package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
  97. package/src/cli/core/__tests__/init.spec.ts +1 -1
  98. package/src/cli/core/commands/init.ts +0 -12
  99. package/src/cli/core/services/PackageManagerUtils.ts +2 -9
  100. package/src/cli/core/services/ProjectScaffolder.ts +17 -65
  101. package/src/cli/core/templates/agentMd.ts +2 -8
  102. package/src/cli/core/templates/apiIndexTs.ts +4 -18
  103. package/src/cli/core/templates/mainCss.ts +1 -36
  104. package/src/cli/core/templates/vitestConfigTs.ts +17 -0
  105. package/src/cli/core/templates/webAppRouterTs.ts +2 -85
  106. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
  107. package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
  108. package/src/cli/platform/atoms/platformOptions.ts +9 -0
  109. package/src/cli/platform/schemas/cloudflare.ts +3 -2
  110. package/src/cli/platform/services/CloudflareApi.ts +164 -25
  111. package/src/cli/platform/services/WranglerApi.ts +0 -17
  112. package/src/core/Alepha.ts +9 -0
  113. package/src/react/form/index.ts +2 -0
  114. package/src/react/form/services/parseField.ts +163 -0
  115. package/src/react/form/services/prettyName.ts +19 -0
  116. package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
  117. package/src/react/router/primitives/$page.ts +35 -12
  118. package/src/react/ui/atoms/uiAtom.ts +28 -0
  119. package/src/react/ui/components/ColorScheme.tsx +36 -0
  120. package/src/react/ui/hooks/useColorMode.ts +49 -0
  121. package/src/react/ui/hooks/useSidebarState.ts +26 -0
  122. package/src/react/ui/hooks/useTheme.ts +22 -0
  123. package/src/react/ui/index.ts +35 -0
  124. package/src/react/ui/services/UiPersistence.ts +41 -0
  125. package/src/router/TemplatedPathParser.ts +50 -51
  126. package/src/router/__tests__/RouterProvider.spec.ts +62 -0
  127. package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
  128. package/src/router/providers/RouterProvider.ts +10 -5
  129. package/src/system/providers/NodeShellProvider.ts +1 -0
  130. package/src/topic/core/providers/TopicProvider.ts +1 -1
  131. package/dist/api/invitations/index.d.ts +0 -790
  132. package/dist/api/invitations/index.d.ts.map +0 -1
  133. package/dist/api/invitations/index.js +0 -662
  134. package/dist/api/invitations/index.js.map +0 -1
  135. package/dist/api/issues/index.d.ts +0 -810
  136. package/dist/api/issues/index.d.ts.map +0 -1
  137. package/dist/api/issues/index.js +0 -444
  138. package/dist/api/issues/index.js.map +0 -1
  139. package/dist/api/subscriptions/index.d.ts +0 -1692
  140. package/dist/api/subscriptions/index.d.ts.map +0 -1
  141. package/dist/api/subscriptions/index.js +0 -1867
  142. package/dist/api/subscriptions/index.js.map +0 -1
  143. package/dist/api/workflows/index.browser.js +0 -246
  144. package/dist/api/workflows/index.browser.js.map +0 -1
  145. package/dist/api/workflows/index.d.ts +0 -1618
  146. package/dist/api/workflows/index.d.ts.map +0 -1
  147. package/dist/api/workflows/index.js +0 -1495
  148. package/dist/api/workflows/index.js.map +0 -1
  149. package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
  150. package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
  151. package/src/api/invitations/controllers/InvitationController.ts +0 -84
  152. package/src/api/invitations/entities/invitations.ts +0 -33
  153. package/src/api/invitations/index.ts +0 -58
  154. package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
  155. package/src/api/invitations/providers/InvitationProvider.ts +0 -45
  156. package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
  157. package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
  158. package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
  159. package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
  160. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
  161. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
  162. package/src/api/invitations/services/InvitationService.ts +0 -556
  163. package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
  164. package/src/api/issues/controllers/AdminIssueController.ts +0 -149
  165. package/src/api/issues/controllers/IssueController.ts +0 -44
  166. package/src/api/issues/entities/issues.ts +0 -49
  167. package/src/api/issues/index.ts +0 -50
  168. package/src/api/issues/schemas/createIssueSchema.ts +0 -13
  169. package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
  170. package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
  171. package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
  172. package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
  173. package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
  174. package/src/api/issues/services/IssueService.ts +0 -264
  175. package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
  176. package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
  177. package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
  178. package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
  179. package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
  180. package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
  181. package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
  182. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
  183. package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
  184. package/src/api/jobs/services/JobService-tests.ts +0 -157
  185. package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
  186. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
  187. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
  188. package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
  189. package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
  190. package/src/api/subscriptions/entities/subscriptions.ts +0 -68
  191. package/src/api/subscriptions/index.ts +0 -133
  192. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
  193. package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
  194. package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
  195. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
  196. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
  197. package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
  198. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
  199. package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
  200. package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
  201. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
  202. package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
  203. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
  204. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
  205. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
  206. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
  207. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
  208. package/src/api/subscriptions/services/BillingService.ts +0 -437
  209. package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
  210. package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
  211. package/src/api/subscriptions/services/UsageService.ts +0 -118
  212. package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
  213. package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
  214. package/src/api/workflows/entities/workflowExecutions.ts +0 -74
  215. package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
  216. package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
  217. package/src/api/workflows/index.browser.ts +0 -22
  218. package/src/api/workflows/index.ts +0 -115
  219. package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
  220. package/src/api/workflows/primitives/$workflow.ts +0 -202
  221. package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
  222. package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
  223. package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
  224. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
  225. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
  226. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
  227. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
  228. package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
  229. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
  230. package/src/api/workflows/services/WorkflowService.ts +0 -382
  231. package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
  232. package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
@@ -1,7 +1,8 @@
1
1
  import { $inject, t } from "alepha";
2
- import { JobService } from "alepha/api/jobs";
2
+ import { jobExecutionEntity } from "alepha/api/jobs";
3
+ import { $repository } from "alepha/orm";
3
4
  import { $secure } from "alepha/security";
4
- import { $action } from "alepha/server";
5
+ import { $action, NotFoundError } from "alepha/server";
5
6
  import { NotificationJobs } from "../jobs/NotificationJobs.ts";
6
7
  import { notificationDetailResourceSchema } from "../schemas/notificationDetailResourceSchema.ts";
7
8
  import { notificationQuerySchema } from "../schemas/notificationQuerySchema.ts";
@@ -10,8 +11,8 @@ import { notificationResourceSchema } from "../schemas/notificationResourceSchem
10
11
  export class AdminNotificationController {
11
12
  protected readonly url: string = "/notifications";
12
13
  protected readonly group: string = "admin:notifications";
13
- protected readonly jobService = $inject(JobService);
14
14
  protected readonly notificationJobs = $inject(NotificationJobs);
15
+ protected readonly executions = $repository(jobExecutionEntity);
15
16
 
16
17
  protected get jobName(): string {
17
18
  return this.notificationJobs.sendNotification.name;
@@ -26,13 +27,17 @@ export class AdminNotificationController {
26
27
  response: t.page(notificationResourceSchema),
27
28
  },
28
29
  handler: async ({ query }) => {
29
- const result = await this.jobService.findExecutions({
30
- ...query,
31
- job: this.jobName,
32
- });
30
+ query.sort ??= "-createdAt";
31
+ const where = this.executions.createQueryWhere();
32
+ where.jobName = { eq: this.jobName };
33
+ const page = await this.executions.paginate(
34
+ query,
35
+ { where },
36
+ { count: true },
37
+ );
33
38
  return {
34
- ...result,
35
- content: result.content.map((exec) => this.toResource(exec)),
39
+ ...page,
40
+ content: page.content.map((exec) => this.toResource(exec)),
36
41
  } as any;
37
42
  },
38
43
  });
@@ -48,8 +53,11 @@ export class AdminNotificationController {
48
53
  response: notificationDetailResourceSchema,
49
54
  },
50
55
  handler: async ({ params }) => {
51
- const detail = await this.jobService.getExecution(params.id);
52
- return this.toDetailResource(detail) as any;
56
+ const exec = await this.executions.findById(params.id);
57
+ if (!exec || exec.jobName !== this.jobName) {
58
+ throw new NotFoundError(`Notification not found: ${params.id}`);
59
+ }
60
+ return this.toDetailResource(exec) as any;
53
61
  },
54
62
  });
55
63
 
@@ -76,7 +84,6 @@ export class AdminNotificationController {
76
84
  return {
77
85
  ...this.toResource(exec),
78
86
  variables: payload.variables,
79
- rendered: exec.result,
80
87
  logs: exec.logs,
81
88
  };
82
89
  }
@@ -1,4 +1,6 @@
1
1
  import { $module } from "alepha";
2
+ import { AlephaApiJobsQueue } from "alepha/api/jobs";
3
+ import { AlephaApiParameters } from "alepha/api/parameters";
2
4
  import { AdminNotificationController } from "./controllers/AdminNotificationController.ts";
3
5
  import { NotificationJobs } from "./jobs/NotificationJobs.ts";
4
6
  import { $notification } from "./primitives/$notification.ts";
@@ -23,15 +25,16 @@ export * from "./services/NotificationSenderService.ts";
23
25
  * User notification management.
24
26
  *
25
27
  * **Features:**
26
- * - Notification definitions
27
- * - Email/SMS notification sending
28
- * - Job-based delivery with retry and tracking
29
- * - User preferences
28
+ * - Notification definitions (email/SMS templates)
29
+ * - Queue-based delivery with retry and audit trail (`record: "all"` + no ring buffer trim)
30
+ * - Runtime-editable retention window via `$parameter` — purge cron respects it live
31
+ * - Admin API for inspecting sent notifications
30
32
  *
31
33
  * @module alepha.api.notifications
32
34
  */
33
35
  export const AlephaApiNotifications = $module({
34
36
  name: "alepha.api.notifications",
37
+ imports: [AlephaApiJobsQueue, AlephaApiParameters],
35
38
  primitives: [$notification],
36
39
  services: [
37
40
  NotificationSenderService,
@@ -1,16 +1,57 @@
1
- import { $inject } from "alepha";
1
+ import { $inject, t } from "alepha";
2
2
  import { $job, jobExecutionEntity } from "alepha/api/jobs";
3
+ import { $parameter } from "alepha/api/parameters";
4
+ import { DateTimeProvider } from "alepha/datetime";
5
+ import { $logger } from "alepha/logger";
3
6
  import { $repository } from "alepha/orm";
4
7
  import { notificationPayloadSchema } from "../schemas/notificationPayloadSchema.ts";
5
8
  import { NotificationSenderService } from "../services/NotificationSenderService.ts";
6
9
 
10
+ /**
11
+ * Notification jobs + runtime-editable retention.
12
+ *
13
+ * - `settings` — a `$parameter` exposing `retentionDays` that admins can
14
+ * update at runtime. Changes propagate across instances via the parameter
15
+ * pub/sub; the next purge run picks up the new value with no restart.
16
+ * - `sendNotification` — queue-mode, audit-oriented. Every execution is kept
17
+ * (`record: "all"`, `keep: { ok: 0, error: 0 }` disables the ring-buffer
18
+ * trim) so the audit trail survives even under heavy volume.
19
+ * - `purgeOldNotifications` — cron sweep that deletes notification execution
20
+ * rows whose `completedAt` is older than the current `retentionDays`.
21
+ *
22
+ * Cron expression note: the purge cron is declared statically (`0 3 * * *`)
23
+ * because some runtimes (Cloudflare Workers) freeze cron triggers at deploy
24
+ * time. The *retention window* is fully runtime-editable — that's the knob
25
+ * that actually matters for operators.
26
+ */
7
27
  export class NotificationJobs {
28
+ protected readonly log = $logger();
29
+ protected readonly dt = $inject(DateTimeProvider);
8
30
  protected readonly notificationSenderService = $inject(
9
31
  NotificationSenderService,
10
32
  );
11
33
  protected readonly executions = $repository(jobExecutionEntity);
12
34
 
35
+ /** Runtime-editable config. Admins can change retentionDays without deploy. */
36
+ public readonly settings = $parameter({
37
+ name: "alepha.api.notifications",
38
+ description: "Notification delivery & retention settings.",
39
+ schema: t.object({
40
+ retentionDays: t.integer({
41
+ description:
42
+ "Days to keep notification execution rows before the purge sweep removes them.",
43
+ minimum: 1,
44
+ }),
45
+ }),
46
+ default: {
47
+ retentionDays: 7,
48
+ },
49
+ });
50
+
13
51
  public readonly sendNotification = $job({
52
+ name: "api:notifications:sendNotification",
53
+ description:
54
+ "Sends a notification (email/SMS) and keeps every execution for audit.",
14
55
  schema: notificationPayloadSchema,
15
56
  retry: {
16
57
  retries: 3,
@@ -22,18 +63,48 @@ export class NotificationJobs {
22
63
  },
23
64
  },
24
65
  timeout: [30, "seconds"],
25
- concurrency: 5,
26
- handler: async ({ items }) => {
27
- for (const item of items) {
28
- const rendered = await this.notificationSenderService.send(
29
- item.payload,
30
- );
31
- if (rendered) {
32
- await this.executions.updateById(item.id, {
33
- result: rendered as Record<string, unknown>,
34
- });
35
- }
66
+ record: "all",
67
+ keep: { ok: 0, error: 0 },
68
+ handler: async ({ payload }) => {
69
+ await this.notificationSenderService.send(payload);
70
+ },
71
+ });
72
+
73
+ public readonly purgeOldNotifications = $job({
74
+ name: "api:notifications:purgeOldNotifications",
75
+ description:
76
+ "Hourly sweep that deletes notification execution rows older than the configured retention window.",
77
+ cron: "0 * * * *",
78
+ handler: async ({ now }) => {
79
+ const { retentionDays } = this.settings.cachedCurrentContent;
80
+ const cutoff = now.subtract(retentionDays, "day").toISOString();
81
+ const jobName = this.sendNotification.name;
82
+
83
+ const expired = await this.executions.findMany({
84
+ where: {
85
+ jobName: { eq: jobName },
86
+ status: { inArray: ["ok", "error", "cancelled"] },
87
+ completedAt: { lt: cutoff },
88
+ },
89
+ columns: ["id"] as any,
90
+ limit: 5_000,
91
+ });
92
+
93
+ if (expired.length === 0) {
94
+ this.log.debug("Notification purge: nothing to delete", {
95
+ cutoff,
96
+ retentionDays,
97
+ });
98
+ return;
36
99
  }
100
+
101
+ await this.executions.deleteMany({
102
+ id: { inArray: expired.map((r) => r.id) },
103
+ });
104
+ this.log.info(
105
+ `Notification purge: deleted ${expired.length} row(s) older than ${retentionDays} days`,
106
+ { cutoff },
107
+ );
37
108
  },
38
109
  });
39
110
  }
@@ -21,10 +21,12 @@ export class PaymentService {
21
21
 
22
22
  /**
23
23
  * Expires stale payment intents that have been in "processing" status
24
- * for more than 30 minutes. Runs every 15 minutes.
24
+ * for more than 30 minutes. Runs every 5 minutes — shares the CF wrangler
25
+ * trigger with the jobs sweep so no extra binding is consumed.
25
26
  */
26
27
  protected readonly expireStaleIntents = $job({
27
- cron: "*/15 * * * *",
28
+ name: "api:payments:expireStaleIntents",
29
+ cron: "*/5 * * * *",
28
30
  handler: async () => {
29
31
  const cutoff = this.dateTime.now().subtract(30, "minutes").toISOString();
30
32
 
@@ -1,15 +1,15 @@
1
1
  import { Alepha } from "alepha";
2
- import { AlephaApiJobs, jobExecutionEntity } from "alepha/api/jobs";
2
+ import { AlephaApiJobs } from "alepha/api/jobs";
3
3
  import { $repository } from "alepha/orm";
4
4
  import { AlephaOrmPostgres } from "alepha/orm/postgres";
5
- import { describe, test, vi } from "vitest";
5
+ import { describe, test } from "vitest";
6
6
  import { sessions } from "../entities/sessions.ts";
7
7
  import { users } from "../entities/users.ts";
8
8
  import { UserJobs } from "../jobs/UserJobs.ts";
9
9
 
10
10
  describe("UserJobs", () => {
11
11
  describe("purgeExpiredSessions", () => {
12
- test("should delete expired sessions", async ({ expect }) => {
12
+ test("deletes expired sessions", async ({ expect }) => {
13
13
  const alepha = Alepha.create()
14
14
  .with(AlephaOrmPostgres)
15
15
  .with(AlephaApiJobs);
@@ -17,20 +17,17 @@ describe("UserJobs", () => {
17
17
  class TestRepositories {
18
18
  userRepository = $repository(users);
19
19
  sessionRepository = $repository(sessions);
20
- executions = $repository(jobExecutionEntity);
21
20
  }
22
21
 
23
22
  const userJobs = alepha.inject(UserJobs);
24
23
  const repos = alepha.inject(TestRepositories);
25
24
  await alepha.start();
26
25
 
27
- // Create a test user
28
26
  const user = await repos.userRepository.create({
29
27
  email: "test@example.com",
30
28
  });
31
29
 
32
- // Create expired sessions (expiresAt in the past)
33
- const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); // 1 day ago
30
+ const pastDate = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
34
31
  await repos.sessionRepository.create({
35
32
  userId: user.id,
36
33
  refreshToken: crypto.randomUUID(),
@@ -42,35 +39,20 @@ describe("UserJobs", () => {
42
39
  expiresAt: pastDate,
43
40
  });
44
41
 
45
- // Create a valid session (expiresAt in the future)
46
42
  const futureDate = new Date(
47
43
  Date.now() + 24 * 60 * 60 * 1000,
48
- ).toISOString(); // 1 day from now
44
+ ).toISOString();
49
45
  await repos.sessionRepository.create({
50
46
  userId: user.id,
51
47
  refreshToken: crypto.randomUUID(),
52
48
  expiresAt: futureDate,
53
49
  });
54
50
 
55
- // Verify we have 3 sessions
56
- const sessionsBefore = await repos.sessionRepository.findMany();
57
- expect(sessionsBefore).toHaveLength(3);
51
+ expect(await repos.sessionRepository.findMany()).toHaveLength(3);
58
52
 
59
- // Trigger the job
53
+ // Cron jobs run inline — trigger awaits the handler synchronously.
60
54
  await userJobs.purgeExpiredSessions.trigger();
61
55
 
62
- // Wait for async job processing to complete
63
- await vi.waitFor(async () => {
64
- const executions = await repos.executions.findMany({
65
- where: {
66
- jobName: "UserJobs.purgeExpiredSessions",
67
- status: "completed",
68
- },
69
- });
70
- expect(executions).toHaveLength(1);
71
- });
72
-
73
- // Verify only the valid session remains
74
56
  const sessionsAfter = await repos.sessionRepository.findMany();
75
57
  expect(sessionsAfter).toHaveLength(1);
76
58
  expect(new Date(sessionsAfter[0].expiresAt).getTime()).toBeGreaterThan(
@@ -78,9 +60,7 @@ describe("UserJobs", () => {
78
60
  );
79
61
  });
80
62
 
81
- test("should handle case when no expired sessions exist", async ({
82
- expect,
83
- }) => {
63
+ test("no-op when no expired sessions exist", async ({ expect }) => {
84
64
  const alepha = Alepha.create()
85
65
  .with(AlephaOrmPostgres)
86
66
  .with(AlephaApiJobs);
@@ -88,19 +68,16 @@ describe("UserJobs", () => {
88
68
  class TestRepositories {
89
69
  userRepository = $repository(users);
90
70
  sessionRepository = $repository(sessions);
91
- executions = $repository(jobExecutionEntity);
92
71
  }
93
72
 
94
73
  const userJobs = alepha.inject(UserJobs);
95
74
  const repos = alepha.inject(TestRepositories);
96
75
  await alepha.start();
97
76
 
98
- // Create a test user
99
77
  const user = await repos.userRepository.create({
100
78
  email: "test2@example.com",
101
79
  });
102
80
 
103
- // Create only valid sessions
104
81
  const futureDate = new Date(
105
82
  Date.now() + 24 * 60 * 60 * 1000,
106
83
  ).toISOString();
@@ -110,27 +87,11 @@ describe("UserJobs", () => {
110
87
  expiresAt: futureDate,
111
88
  });
112
89
 
113
- // Verify we have 1 session
114
- const sessionsBefore = await repos.sessionRepository.findMany();
115
- expect(sessionsBefore).toHaveLength(1);
90
+ expect(await repos.sessionRepository.findMany()).toHaveLength(1);
116
91
 
117
- // Trigger the job - should not throw
118
92
  await userJobs.purgeExpiredSessions.trigger();
119
93
 
120
- // Wait for async job processing to complete
121
- await vi.waitFor(async () => {
122
- const executions = await repos.executions.findMany({
123
- where: {
124
- jobName: "UserJobs.purgeExpiredSessions",
125
- status: "completed",
126
- },
127
- });
128
- expect(executions).toHaveLength(1);
129
- });
130
-
131
- // Session should still exist
132
- const sessionsAfter = await repos.sessionRepository.findMany();
133
- expect(sessionsAfter).toHaveLength(1);
94
+ expect(await repos.sessionRepository.findMany()).toHaveLength(1);
134
95
  });
135
96
  });
136
97
  });
@@ -7,7 +7,9 @@ type AuditContext = Omit<CreateAudit, "type" | "action">;
7
7
  * User-specific audit wrapper service.
8
8
  *
9
9
  * This service wraps the core AuditService to provide user-related audit logging.
10
- * It is lazy-loaded when the `audits` feature is enabled in the realm.
10
+ *
11
+ * Declared as a module variant — not auto-injected. It is instantiated
12
+ * lazily the first time something calls `alepha.inject(UserAudits)`.
11
13
  */
12
14
  export class UserAudits {
13
15
  protected readonly auditService = $inject(AuditService);
@@ -6,7 +6,8 @@ import { $bucket } from "alepha/bucket";
6
6
  * This service provides file storage for user-related files such as:
7
7
  * - User avatars/profile pictures
8
8
  *
9
- * It is lazy-loaded when the `avatars` feature is enabled in the realm.
9
+ * Declared as a module variant not auto-injected. It is instantiated
10
+ * lazily the first time something calls `alepha.inject(UserBuckets)`.
10
11
  */
11
12
  export class UserBuckets {
12
13
  /**
@@ -88,9 +88,6 @@ export const AlephaApiUsers = $module({
88
88
  AdminSessionController,
89
89
  AdminIdentityController,
90
90
  RealmController,
91
- UserJobs,
92
- UserNotifications,
93
- UserAudits,
94
- UserBuckets,
95
91
  ],
92
+ variants: [UserJobs, UserNotifications, UserAudits, UserBuckets],
96
93
  });
@@ -13,7 +13,8 @@ import { sessions } from "../entities/sessions.ts";
13
13
  * - Verification code cleanup
14
14
  * - Inactive user notifications
15
15
  *
16
- * It is lazy-loaded when the `sessionPurge` feature is enabled in the realm.
16
+ * Declared as a module variant not auto-injected. It is instantiated
17
+ * lazily the first time something calls `alepha.inject(UserJobs)`.
17
18
  */
18
19
  export class UserJobs {
19
20
  protected readonly log = $logger();
@@ -23,11 +24,11 @@ export class UserJobs {
23
24
  /**
24
25
  * Purge expired sessions from the database.
25
26
  *
26
- * This job runs daily at 3:00 AM and removes all sessions
27
- * where the `expiresAt` timestamp has passed.
27
+ * Runs hourly (at :00) and deletes sessions whose `expiresAt` has passed.
28
28
  */
29
29
  public readonly purgeExpiredSessions = $job({
30
- cron: "0 0 * * *", // Daily at 3:00 AM
30
+ name: "api:users:purgeExpiredSessions",
31
+ cron: "0 * * * *", // Hourly at minute 0
31
32
  handler: async () => {
32
33
  const now = this.dateTimeProvider.nowISOString();
33
34
 
@@ -11,7 +11,8 @@ export class VerificationJobs {
11
11
  protected readonly dateTimeProvider = $inject(DateTimeProvider);
12
12
 
13
13
  public readonly cleanExpired = $scheduler({
14
- cron: "0 0 * * *", // Every day at midnight
14
+ name: "api:verifications:cleanExpired",
15
+ cron: "0 * * * *", // Hourly at minute 0
15
16
  description: "Clean expired verifications",
16
17
  handler: async () => {
17
18
  const purgeDays = this.verificationParameters.get("purgeDays");
@@ -542,7 +542,7 @@ describe("alepha init", () => {
542
542
  });
543
543
 
544
544
  it("should check each codegen flag independently", async () => {
545
- for (const flag of ["--react", "--ui", "--saas", "--tailwind"]) {
545
+ for (const flag of ["--react", "--tailwind"]) {
546
546
  const { fs, cli, cmd, json } = createTestEnv();
547
547
  await setupProject(fs, json);
548
548
  await fs.writeFile("/project/src/existing.ts", "export {}");
@@ -37,18 +37,6 @@ export class InitCommand {
37
37
  description: "Include React dependencies and web module (src/web/)",
38
38
  }),
39
39
  ),
40
- ui: t.optional(
41
- t.boolean({
42
- description:
43
- "Include @alepha/ui (components, auth portal, admin portal)",
44
- }),
45
- ),
46
- saas: t.optional(
47
- t.boolean({
48
- description:
49
- "Include authentication, admin portal, API, UI, and React. Everything you need for a SaaS app.",
50
- }),
51
- ),
52
40
  tailwind: t.optional(
53
41
  t.boolean({
54
42
  description: "Include Tailwind CSS with Vite plugin. Implies --react",
@@ -402,9 +402,8 @@ export class PackageManagerUtils {
402
402
  };
403
403
 
404
404
  // Only include drizzle-kit when the project uses a database.
405
- // React-only projects (--react without --api/--saas) don't need it.
406
- const isReactOnly = modes.react && !modes.ui;
407
- if (!isReactOnly) {
405
+ // React-only projects don't need it.
406
+ if (!modes.react) {
408
407
  devDependencies["drizzle-kit"] = alephaDeps["drizzle-kit"];
409
408
  }
410
409
 
@@ -428,11 +427,6 @@ export class PackageManagerUtils {
428
427
  scripts.test = "vitest run";
429
428
  }
430
429
 
431
- if (modes.ui) {
432
- dependencies["@alepha/ui"] = `^${version}`;
433
- modes.react = true;
434
- }
435
-
436
430
  if (modes.tailwind) {
437
431
  devDependencies.tailwindcss = "^4.2.0";
438
432
  devDependencies["@tailwindcss/vite"] = "^4.2.0";
@@ -467,7 +461,6 @@ export class PackageManagerUtils {
467
461
 
468
462
  export interface DependencyModes {
469
463
  react?: boolean;
470
- ui?: boolean;
471
464
  expo?: boolean;
472
465
  tailwind?: boolean;
473
466
  test?: boolean;