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,125 +1,95 @@
1
1
  import { $inject, Alepha, AlephaError, t } from "alepha";
2
- import { DateTimeProvider } from "alepha/datetime";
3
2
  import { $logger } from "alepha/logger";
4
- import { $repository, DatabaseProvider, sql } from "alepha/orm";
3
+ import { $repository, sql } from "alepha/orm";
5
4
  import { NotFoundError } from "alepha/server";
6
- import type { JobExecutionEntity } from "../entities/jobExecutionEntity.ts";
7
5
  import { jobExecutionEntity } from "../entities/jobExecutionEntity.ts";
8
- import { jobExecutionLogEntity } from "../entities/jobExecutionLogEntity.ts";
9
6
  import { $job } from "../primitives/$job.ts";
10
7
  import type { JobTriggerContext } from "../providers/JobProvider.ts";
11
8
  import { JobProvider } from "../providers/JobProvider.ts";
12
- import type { JobActivityPoint } from "../schemas/jobActivitySchema.ts";
13
- import type { JobCronInfo } from "../schemas/jobCronInfoSchema.ts";
14
9
  import type { JobExecutionQuery } from "../schemas/jobExecutionQuerySchema.ts";
15
- import type { JobFailure } from "../schemas/jobFailureSchema.ts";
16
- import type { JobQueueDepth } from "../schemas/jobQueueDepthSchema.ts";
17
10
  import type { JobRegistration } from "../schemas/jobRegistrationSchema.ts";
18
- import type { JobStats } from "../schemas/jobStatsSchema.ts";
19
-
20
- // -----------------------------------------------------------------------------------------------------------------
21
11
 
12
+ /**
13
+ * Admin surface for the job system.
14
+ *
15
+ * Six methods: list jobs, list executions, get execution,
16
+ * trigger, retry, cancel. Everything else lives in events — any
17
+ * analytics/observability is an external concern that subscribes
18
+ * to `job:begin` / `job:success` / `job:error`.
19
+ */
22
20
  export class JobService {
23
21
  protected readonly alepha = $inject(Alepha);
24
- protected readonly dt = $inject(DateTimeProvider);
25
22
  protected readonly log = $logger();
26
23
  protected readonly jobProvider = $inject(JobProvider);
27
- protected readonly database = $inject(DatabaseProvider);
28
24
  protected readonly executions = $repository(jobExecutionEntity);
29
- protected readonly executionLogs = $repository(jobExecutionLogEntity);
30
25
 
31
26
  protected computeCan(status: string) {
32
27
  return {
33
- retry: status === "dead" || status === "cancelled",
28
+ retry: status === "error" || status === "cancelled",
34
29
  cancel:
35
- status === "pending" ||
36
- status === "running" ||
37
- status === "scheduled" ||
38
- status === "retrying",
30
+ status === "pending" || status === "running" || status === "scheduled",
39
31
  };
40
32
  }
41
33
 
42
34
  /**
43
- * Convert an ISO date string to the raw SQL parameter format
44
- * expected by the current database dialect.
45
- *
46
- * - PostgreSQL: ISO string (timestamp comparison)
47
- * - SQLite: epoch milliseconds (integer comparison)
35
+ * List every registered job with recent ok/error counts and lastRun.
36
+ * One aggregate query covers all jobs.
48
37
  */
49
- protected toRawDate(iso: string): string | number {
50
- return this.database.dialect === "sqlite" ? new Date(iso).getTime() : iso;
51
- }
52
-
53
- public async getStats(days?: number): Promise<JobStats> {
54
- const jobs = this.jobProvider.getRegisteredJobs();
55
- const periodAgo = this.toRawDate(
56
- this.dt
57
- .now()
58
- .subtract(days ?? 1, "day")
59
- .toISOString(),
60
- );
38
+ public async listJobs(): Promise<JobRegistration[]> {
39
+ const registry = this.jobProvider.getRegisteredJobs();
61
40
 
62
- const rows = await this.executions.query(
41
+ const aggRows = await this.executions.query(
63
42
  (e) => sql`
64
43
  SELECT
65
- COUNT(*) FILTER (WHERE ${e.status} = 'running') AS running,
66
- COUNT(*) FILTER (WHERE ${e.status} = 'pending') AS pending,
67
- COUNT(*) FILTER (WHERE ${e.status} = 'scheduled') AS scheduled,
68
- COUNT(*) FILTER (WHERE ${e.status} = 'retrying') AS retrying,
69
- COUNT(*) FILTER (WHERE ${e.status} = 'dead') AS dead,
70
- COUNT(*) FILTER (WHERE ${e.status} = 'completed' AND ${e.completedAt} >= ${periodAgo}) AS completed_24h,
71
- COUNT(*) FILTER (WHERE ${e.status} = 'dead' AND ${e.completedAt} >= ${periodAgo}) AS failed_24h
44
+ ${e.jobName} AS job_name,
45
+ ${e.status} AS status,
46
+ COUNT(*) AS count,
47
+ MAX(${e.completedAt}) AS last_run
72
48
  FROM ${e}
49
+ WHERE ${e.status} IN ('ok', 'error')
50
+ GROUP BY ${e.jobName}, ${e.status}
73
51
  `,
74
52
  t.object({
75
- running: t.string(),
76
- pending: t.string(),
77
- scheduled: t.string(),
78
- retrying: t.string(),
79
- dead: t.string(),
80
- completed_24h: t.string(),
81
- failed_24h: t.string(),
53
+ job_name: t.string(),
54
+ status: t.string(),
55
+ count: t.string(),
56
+ last_run: t.optional(t.nullable(t.union([t.string(), t.number()]))),
82
57
  }),
83
58
  );
84
59
 
85
- const row = rows[0];
86
- return {
87
- registered: jobs.size,
88
- running: Number(row.running),
89
- pending: Number(row.pending),
90
- scheduled: Number(row.scheduled),
91
- retrying: Number(row.retrying),
92
- dead: Number(row.dead),
93
- completed: Number(row.completed_24h),
94
- failed: Number(row.failed_24h),
60
+ const toIso = (
61
+ v: string | number | null | undefined,
62
+ ): string | undefined => {
63
+ if (v === null || v === undefined) return undefined;
64
+ if (typeof v === "number") return new Date(v).toISOString();
65
+ return v;
95
66
  };
96
- }
97
-
98
- public getRegistry(): JobRegistration[] {
99
- const jobs = this.jobProvider.getRegisteredJobs();
100
- const result: JobRegistration[] = [];
101
67
 
102
- for (const [name, reg] of jobs) {
103
- const opts = reg.options;
104
- const hasCron = Boolean(opts.cron);
105
- const hasSchema = Boolean(opts.schema);
106
-
107
- let type: "cron" | "push" | "both";
108
- if (hasCron && hasSchema) {
109
- type = "both";
110
- } else if (hasCron) {
111
- type = "cron";
112
- } else {
113
- type = "push";
68
+ const byJob = new Map<
69
+ string,
70
+ { ok: number; error: number; lastRun?: string }
71
+ >();
72
+ for (const row of aggRows) {
73
+ const entry = byJob.get(row.job_name) ?? { ok: 0, error: 0 };
74
+ if (row.status === "ok") entry.ok = Number(row.count);
75
+ if (row.status === "error") entry.error = Number(row.count);
76
+ const iso = toIso(row.last_run);
77
+ if (iso && (!entry.lastRun || iso > entry.lastRun)) {
78
+ entry.lastRun = iso;
114
79
  }
80
+ byJob.set(row.job_name, entry);
81
+ }
115
82
 
116
- const registration: JobRegistration = {
83
+ const result: JobRegistration[] = [];
84
+ for (const [name, reg] of registry) {
85
+ const opts = reg.options;
86
+ const counts = byJob.get(name) ?? { ok: 0, error: 0 };
87
+ result.push({
117
88
  name,
118
- type,
119
- priority: (opts.priority ?? "normal") as JobRegistration["priority"],
120
- concurrency: opts.concurrency ?? 1,
121
- hasSchema,
89
+ description: opts.description,
90
+ type: reg.type,
122
91
  cron: opts.cron,
92
+ priority: (opts.priority ?? "normal") as JobRegistration["priority"],
123
93
  timeout: opts.timeout ? String(opts.timeout) : undefined,
124
94
  retry: opts.retry
125
95
  ? {
@@ -127,97 +97,72 @@ export class JobService {
127
97
  hasBackoff: Boolean(opts.retry.backoff),
128
98
  }
129
99
  : undefined,
130
- paused: this.jobProvider.isJobPaused(name),
131
- };
132
-
133
- result.push(registration);
100
+ recent: counts,
101
+ });
134
102
  }
135
-
136
103
  return result;
137
104
  }
138
105
 
139
- public async findExecutions(query: JobExecutionQuery = {}) {
140
- query.sort ??= "-createdAt";
141
-
142
- const where = this.executions.createQueryWhere();
143
-
144
- if (query.job) {
145
- where.jobName = { eq: query.job };
106
+ /**
107
+ * Recent executions for a single job, ORDER BY startedAt DESC.
108
+ */
109
+ public async getExecutions(jobName: string, query: JobExecutionQuery = {}) {
110
+ const registry = this.jobProvider.getRegisteredJobs();
111
+ if (!registry.has(jobName)) {
112
+ throw new NotFoundError(`Job not found: ${jobName}`);
146
113
  }
147
-
114
+ const where = this.executions.createQueryWhere();
115
+ where.jobName = { eq: jobName };
148
116
  if (query.status) {
149
117
  where.status = { eq: query.status };
150
118
  }
151
-
152
- if (query.priority) {
153
- const priorityMap: Record<string, number> = {
154
- critical: 0,
155
- high: 1,
156
- normal: 2,
157
- low: 3,
158
- };
159
- where.priority = { eq: priorityMap[query.priority] };
160
- }
161
-
162
- if (query.from) {
163
- where.createdAt = { gte: query.from };
164
- }
165
-
166
- if (query.to) {
167
- where.createdAt = {
168
- ...(where.createdAt as object),
169
- lte: query.to,
170
- };
171
- }
172
-
173
- const page = await this.executions.paginate(
174
- query,
175
- { where },
176
- { count: true },
177
- );
178
- return {
179
- ...page,
180
- content: page.content.map((exec: JobExecutionEntity) => ({
181
- ...exec,
182
- can: this.computeCan(exec.status),
183
- })),
184
- };
119
+ const rows = await this.executions.findMany({
120
+ where,
121
+ orderBy: { column: "startedAt", direction: "desc" },
122
+ limit: query.limit ?? 20,
123
+ });
124
+ return rows.map((row) => ({
125
+ ...row,
126
+ can: this.computeCan(row.status),
127
+ }));
185
128
  }
186
129
 
130
+ /**
131
+ * Full execution detail (includes captured logs).
132
+ */
187
133
  public async getExecution(id: string) {
188
134
  const execution = await this.executions.findById(id);
189
135
  if (!execution) {
190
136
  throw new NotFoundError(`Execution not found: ${id}`);
191
137
  }
192
-
193
- const logRecord = await this.executionLogs.findById(id);
194
-
195
138
  return {
196
139
  ...execution,
197
140
  can: this.computeCan(execution.status),
198
- logs: logRecord?.logs,
199
141
  };
200
142
  }
201
143
 
144
+ /**
145
+ * Manual trigger (cron jobs) or push-with-payload (queue jobs).
146
+ */
202
147
  public async triggerJob(
203
148
  name: string,
204
149
  context?: JobTriggerContext,
205
150
  ): Promise<{ ok: boolean }> {
206
151
  const jobPrimitives = this.alepha.primitives($job);
207
152
  const job = jobPrimitives.find((j) => j.name === name);
208
-
209
153
  if (!job) {
210
154
  throw new NotFoundError(`Job not found: ${name}`);
211
155
  }
212
-
213
156
  this.log.info(`Triggering job '${name}'`, {
214
157
  triggeredBy: context?.triggeredByName ?? context?.triggeredBy,
215
158
  });
216
-
217
159
  await job.trigger(context);
218
160
  return { ok: true };
219
161
  }
220
162
 
163
+ /**
164
+ * Retry a dead or cancelled execution by re-pushing with the original payload.
165
+ */
221
166
  public async retryExecution(
222
167
  id: string,
223
168
  context?: { triggeredBy?: string; triggeredByName?: string },
@@ -226,35 +171,32 @@ export class JobService {
226
171
  if (!execution) {
227
172
  throw new NotFoundError(`Execution not found: ${id}`);
228
173
  }
229
-
230
- if (execution.status !== "dead" && execution.status !== "cancelled") {
174
+ if (execution.status !== "error" && execution.status !== "cancelled") {
231
175
  throw new AlephaError(
232
176
  `Cannot retry execution in '${execution.status}' status`,
233
177
  );
234
178
  }
235
179
 
236
- this.log.info(`Retrying execution ${id}`, {
237
- jobName: execution.jobName,
238
- previousStatus: execution.status,
239
- triggeredBy: context?.triggeredByName ?? context?.triggeredBy,
240
- });
241
-
242
180
  const jobPrimitives = this.alepha.primitives($job);
243
181
  const job = jobPrimitives.find((j) => j.name === execution.jobName);
244
-
245
182
  if (!job) {
246
183
  throw new NotFoundError(`Job not found: ${execution.jobName}`);
247
184
  }
248
185
 
186
+ this.log.info(`Retrying execution ${id}`, {
187
+ jobName: execution.jobName,
188
+ previousStatus: execution.status,
189
+ triggeredBy: context?.triggeredByName ?? context?.triggeredBy,
190
+ });
191
+
249
192
  if (execution.payload) {
250
- await job.push(execution.payload, {});
193
+ await job.push(execution.payload as any);
251
194
  } else {
252
195
  await job.trigger({
253
196
  triggeredBy: context?.triggeredBy,
254
197
  triggeredByName: context?.triggeredByName,
255
198
  });
256
199
  }
257
-
258
200
  return { ok: true };
259
201
  }
260
202
 
@@ -265,345 +207,10 @@ export class JobService {
265
207
  this.log.info(`Cancelling execution ${id}`, {
266
208
  cancelledBy: context?.cancelledByName ?? context?.cancelledBy,
267
209
  });
268
-
269
210
  await this.jobProvider.cancel(id, {
270
211
  cancelledBy: context?.cancelledBy,
271
212
  cancelledByName: context?.cancelledByName,
272
213
  });
273
214
  return { ok: true };
274
215
  }
275
-
276
- public pauseJob(
277
- name: string,
278
- context?: { pausedBy?: string; pausedByName?: string },
279
- ): { ok: boolean } {
280
- const jobPrimitives = this.alepha.primitives($job);
281
- const job = jobPrimitives.find((j) => j.name === name);
282
-
283
- if (!job) {
284
- throw new NotFoundError(`Job not found: ${name}`);
285
- }
286
-
287
- this.log.info(`Pausing job '${name}'`, {
288
- pausedBy: context?.pausedByName ?? context?.pausedBy,
289
- });
290
-
291
- job.pause();
292
- return { ok: true };
293
- }
294
-
295
- public async resumeJob(
296
- name: string,
297
- context?: { resumedBy?: string; resumedByName?: string },
298
- ): Promise<{ ok: boolean }> {
299
- const jobPrimitives = this.alepha.primitives($job);
300
- const job = jobPrimitives.find((j) => j.name === name);
301
-
302
- if (!job) {
303
- throw new NotFoundError(`Job not found: ${name}`);
304
- }
305
-
306
- this.log.info(`Resuming job '${name}'`, {
307
- resumedBy: context?.resumedByName ?? context?.resumedBy,
308
- });
309
-
310
- await job.resume();
311
- return { ok: true };
312
- }
313
-
314
- public getPausedJobs(): string[] {
315
- return this.jobProvider.getPausedJobs();
316
- }
317
-
318
- public async getCronJobs(): Promise<JobCronInfo[]> {
319
- const jobs = this.jobProvider.getRegisteredJobs();
320
- const cronJobNames: string[] = [];
321
-
322
- for (const [name, reg] of jobs) {
323
- if (reg.options.cron) cronJobNames.push(name);
324
- }
325
-
326
- const lastByJob = await this.getLastExecutionPerJob(cronJobNames);
327
-
328
- const result: JobCronInfo[] = [];
329
- for (const name of cronJobNames) {
330
- const reg = jobs.get(name)!;
331
- const opts = reg.options;
332
- const last = lastByJob.get(name);
333
-
334
- result.push({
335
- name,
336
- cron: opts.cron!,
337
- lock: opts.lock !== false,
338
- priority: (opts.priority ?? "normal") as JobCronInfo["priority"],
339
- concurrency: opts.concurrency ?? 1,
340
- hasSchema: Boolean(opts.schema),
341
- paused: this.jobProvider.isJobPaused(name),
342
- lastExecution: last
343
- ? {
344
- id: last.id,
345
- status: last.status,
346
- startedAt: last.started_at ?? undefined,
347
- completedAt: last.completed_at ?? undefined,
348
- error: last.error ?? undefined,
349
- }
350
- : undefined,
351
- });
352
- }
353
-
354
- return result;
355
- }
356
-
357
- public async getQueueDepth(): Promise<JobQueueDepth[]> {
358
- const jobs = this.jobProvider.getRegisteredJobs();
359
-
360
- const rows = await this.executions.query(
361
- (e) => sql`
362
- SELECT
363
- ${e.jobName} AS job_name,
364
- COUNT(*) FILTER (WHERE ${e.status} = 'pending') AS pending,
365
- COUNT(*) FILTER (WHERE ${e.status} = 'running') AS running,
366
- COUNT(*) FILTER (WHERE ${e.status} = 'scheduled') AS scheduled,
367
- COUNT(*) FILTER (WHERE ${e.status} = 'retrying') AS retrying,
368
- COUNT(*) FILTER (WHERE ${e.status} = 'dead') AS dead
369
- FROM ${e}
370
- WHERE ${e.status} IN ('pending', 'running', 'scheduled', 'retrying', 'dead')
371
- GROUP BY ${e.jobName}
372
- `,
373
- t.object({
374
- job_name: t.string(),
375
- pending: t.string(),
376
- running: t.string(),
377
- scheduled: t.string(),
378
- retrying: t.string(),
379
- dead: t.string(),
380
- }),
381
- );
382
-
383
- const counts = new Map(rows.map((r) => [r.job_name, r]));
384
-
385
- const result: JobQueueDepth[] = [];
386
- for (const [name, reg] of jobs) {
387
- const row = counts.get(name);
388
- result.push({
389
- jobName: name,
390
- pending: Number(row?.pending ?? 0),
391
- running: Number(row?.running ?? 0),
392
- scheduled: Number(row?.scheduled ?? 0),
393
- retrying: Number(row?.retrying ?? 0),
394
- dead: Number(row?.dead ?? 0),
395
- concurrency: reg.options.concurrency ?? 1,
396
- paused: this.jobProvider.isJobPaused(name),
397
- });
398
- }
399
-
400
- return result;
401
- }
402
-
403
- public async getActivity(days = 14): Promise<JobActivityPoint[]> {
404
- if (this.database.dialect === "sqlite") {
405
- return this.getActivitySqlite(days);
406
- }
407
-
408
- const rows = await this.executions.query(
409
- (e) => sql`
410
- WITH date_series AS (
411
- SELECT generate_series(
412
- CURRENT_DATE - ${days - 1}::int,
413
- CURRENT_DATE,
414
- '1 day'::interval
415
- )::date AS date
416
- )
417
- SELECT
418
- ds.date::text AS date,
419
- COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'completed'), 0) AS completed,
420
- COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'dead'), 0) AS failed
421
- FROM date_series ds
422
- LEFT JOIN ${e} ON DATE(${e.completedAt}) = ds.date
423
- AND ${e.status} IN ('completed', 'dead')
424
- GROUP BY ds.date
425
- ORDER BY ds.date ASC
426
- `,
427
- t.object({
428
- date: t.string(),
429
- completed: t.string(),
430
- failed: t.string(),
431
- }),
432
- );
433
-
434
- return rows.map((row) => ({
435
- date: row.date,
436
- completed: Number(row.completed),
437
- failed: Number(row.failed),
438
- }));
439
- }
440
-
441
- protected async getActivitySqlite(days = 14): Promise<JobActivityPoint[]> {
442
- const now = this.dt.now();
443
- const startDate = now.subtract(days - 1, "day");
444
-
445
- const where = this.executions.createQueryWhere();
446
- where.status = { inArray: ["completed", "dead"] };
447
- where.completedAt = { gte: startDate.startOf("day").toISOString() };
448
-
449
- const executions = await this.executions.findMany({ where });
450
-
451
- // Build date → counts map
452
- const byDate = new Map<string, { completed: number; failed: number }>();
453
- for (let i = 0; i < days; i++) {
454
- const date = startDate.add(i, "day").format("YYYY-MM-DD");
455
- byDate.set(date, { completed: 0, failed: 0 });
456
- }
457
-
458
- for (const exec of executions) {
459
- if (!exec.completedAt) continue;
460
- const date = this.dt.of(exec.completedAt).format("YYYY-MM-DD");
461
- const entry = byDate.get(date);
462
- if (!entry) continue;
463
- if (exec.status === "completed") entry.completed++;
464
- else entry.failed++;
465
- }
466
-
467
- return [...byDate.entries()].map(([date, counts]) => ({
468
- date,
469
- ...counts,
470
- }));
471
- }
472
-
473
- public async getTopFailures(days?: number): Promise<JobFailure[]> {
474
- const periodAgoIso = this.dt
475
- .now()
476
- .subtract(days ?? 7, "day")
477
- .toISOString();
478
-
479
- if (this.database.dialect === "sqlite") {
480
- return this.getTopFailuresSqlite(periodAgoIso);
481
- }
482
-
483
- const rows = await this.executions.query(
484
- (e) => sql`
485
- SELECT
486
- ${e.jobName} AS job_name,
487
- COUNT(*) AS failures,
488
- (ARRAY_AGG(${e.error} ORDER BY ${e.completedAt} DESC))[1] AS last_error
489
- FROM ${e}
490
- WHERE ${e.status} = 'dead'
491
- AND ${e.completedAt} >= ${periodAgoIso}
492
- GROUP BY ${e.jobName}
493
- ORDER BY failures DESC
494
- `,
495
- t.object({
496
- job_name: t.string(),
497
- failures: t.string(),
498
- last_error: t.optional(t.nullable(t.string())),
499
- }),
500
- );
501
-
502
- return rows.map((row) => ({
503
- jobName: row.job_name,
504
- failures: Number(row.failures),
505
- lastError: row.last_error ?? undefined,
506
- }));
507
- }
508
-
509
- protected async getTopFailuresSqlite(
510
- periodAgoIso: string,
511
- ): Promise<JobFailure[]> {
512
- const where = this.executions.createQueryWhere();
513
- where.status = { eq: "dead" };
514
- where.completedAt = { gte: periodAgoIso };
515
-
516
- const failures = await this.executions.findMany({
517
- where,
518
- orderBy: { column: "completedAt", direction: "desc" },
519
- });
520
-
521
- const byJob = new Map<string, { failures: number; lastError?: string }>();
522
- for (const exec of failures) {
523
- const entry = byJob.get(exec.jobName) ?? { failures: 0 };
524
- entry.failures++;
525
- if (!entry.lastError) entry.lastError = exec.error ?? undefined;
526
- byJob.set(exec.jobName, entry);
527
- }
528
-
529
- return [...byJob.entries()]
530
- .map(([jobName, data]) => ({
531
- jobName,
532
- failures: data.failures,
533
- lastError: data.lastError,
534
- }))
535
- .sort((a, b) => b.failures - a.failures);
536
- }
537
-
538
- /**
539
- * Fetch the most recent execution per job name.
540
- *
541
- * - PostgreSQL: uses `DISTINCT ON` for a single-pass query
542
- * - SQLite: uses ORM queries (one per job name) since `DISTINCT ON` is not supported
543
- */
544
- protected async getLastExecutionPerJob(jobNames: string[]): Promise<
545
- Map<
546
- string,
547
- {
548
- id: string;
549
- job_name: string;
550
- status: string;
551
- started_at?: string | null;
552
- completed_at?: string | null;
553
- error?: string | null;
554
- }
555
- >
556
- > {
557
- if (jobNames.length === 0) {
558
- return new Map();
559
- }
560
-
561
- if (this.database.dialect === "sqlite") {
562
- const result = new Map<string, any>();
563
- for (const name of jobNames) {
564
- const rows = await this.executions.findMany({
565
- where: { jobName: { eq: name } },
566
- orderBy: { column: "createdAt", direction: "desc" },
567
- limit: 1,
568
- });
569
- if (rows[0]) {
570
- result.set(name, {
571
- id: rows[0].id,
572
- job_name: rows[0].jobName,
573
- status: rows[0].status,
574
- started_at: rows[0].startedAt,
575
- completed_at: rows[0].completedAt,
576
- error: rows[0].error,
577
- });
578
- }
579
- }
580
- return result;
581
- }
582
-
583
- const schema = t.object({
584
- id: t.string(),
585
- job_name: t.string(),
586
- status: t.string(),
587
- started_at: t.optional(t.nullable(t.string())),
588
- completed_at: t.optional(t.nullable(t.string())),
589
- error: t.optional(t.nullable(t.string())),
590
- });
591
-
592
- const rows = await this.executions.query(
593
- (e) => sql`
594
- SELECT DISTINCT ON (${e.jobName})
595
- ${e.id}, ${e.jobName} AS job_name, ${e.status},
596
- ${e.startedAt} AS started_at, ${e.completedAt} AS completed_at, ${e.error}
597
- FROM ${e}
598
- WHERE ${e.jobName} IN (${sql.join(
599
- jobNames.map((n) => sql`${n}`),
600
- sql`, `,
601
- )})
602
- ORDER BY ${e.jobName}, ${e.createdAt} DESC
603
- `,
604
- schema,
605
- );
606
-
607
- return new Map(rows.map((r) => [r.job_name, r]));
608
- }
609
216
  }