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.
- package/dist/api/files/index.js +2 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +64 -148
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +371 -573
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +605 -1012
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +78 -17
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +90 -23
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +2 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +4 -2
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +34 -31
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +13 -7
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +8 -34
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +43 -232
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +36 -11
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +93 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/core/index.browser.js +6 -0
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +6 -0
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +6 -0
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/react/form/index.d.ts +60 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +86 -1
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js +16 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +6 -0
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js +16 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/router/index.browser.js +0 -10
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +35 -12
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +0 -10
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +124 -0
- package/dist/react/ui/index.d.ts.map +1 -0
- package/dist/react/ui/index.js +206 -0
- package/dist/react/ui/index.js.map +1 -0
- package/dist/router/index.d.ts +13 -13
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -32
- package/dist/router/index.js.map +1 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/package.json +6 -23
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
- package/src/api/jobs/controllers/AdminJobController.ts +29 -138
- package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
- package/src/api/jobs/index.browser.ts +5 -7
- package/src/api/jobs/index.ts +23 -51
- package/src/api/jobs/primitives/$job.ts +66 -58
- package/src/api/jobs/providers/JobProvider.ts +561 -566
- package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
- package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
- package/src/api/jobs/services/JobService.ts +90 -483
- package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
- package/src/api/notifications/index.ts +7 -4
- package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
- package/src/api/payments/services/PaymentService.ts +4 -2
- package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
- package/src/api/users/audits/UserAudits.ts +3 -1
- package/src/api/users/buckets/UserBuckets.ts +2 -1
- package/src/api/users/index.ts +1 -4
- package/src/api/users/jobs/UserJobs.ts +5 -4
- package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/cli/core/__tests__/init.spec.ts +1 -1
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/services/PackageManagerUtils.ts +2 -9
- package/src/cli/core/services/ProjectScaffolder.ts +17 -65
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +4 -18
- package/src/cli/core/templates/mainCss.ts +1 -36
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +2 -85
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
- package/src/cli/platform/atoms/platformOptions.ts +9 -0
- package/src/cli/platform/schemas/cloudflare.ts +3 -2
- package/src/cli/platform/services/CloudflareApi.ts +164 -25
- package/src/cli/platform/services/WranglerApi.ts +0 -17
- package/src/core/Alepha.ts +9 -0
- package/src/react/form/index.ts +2 -0
- package/src/react/form/services/parseField.ts +163 -0
- package/src/react/form/services/prettyName.ts +19 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
- package/src/react/router/primitives/$page.ts +35 -12
- package/src/react/ui/atoms/uiAtom.ts +28 -0
- package/src/react/ui/components/ColorScheme.tsx +36 -0
- package/src/react/ui/hooks/useColorMode.ts +49 -0
- package/src/react/ui/hooks/useSidebarState.ts +26 -0
- package/src/react/ui/hooks/useTheme.ts +22 -0
- package/src/react/ui/index.ts +35 -0
- package/src/react/ui/services/UiPersistence.ts +41 -0
- package/src/router/TemplatedPathParser.ts +50 -51
- package/src/router/__tests__/RouterProvider.spec.ts +62 -0
- package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
- package/src/router/providers/RouterProvider.ts +10 -5
- package/src/system/providers/NodeShellProvider.ts +1 -0
- package/src/topic/core/providers/TopicProvider.ts +1 -1
- package/dist/api/invitations/index.d.ts +0 -790
- package/dist/api/invitations/index.d.ts.map +0 -1
- package/dist/api/invitations/index.js +0 -662
- package/dist/api/invitations/index.js.map +0 -1
- package/dist/api/issues/index.d.ts +0 -810
- package/dist/api/issues/index.d.ts.map +0 -1
- package/dist/api/issues/index.js +0 -444
- package/dist/api/issues/index.js.map +0 -1
- package/dist/api/subscriptions/index.d.ts +0 -1692
- package/dist/api/subscriptions/index.d.ts.map +0 -1
- package/dist/api/subscriptions/index.js +0 -1867
- package/dist/api/subscriptions/index.js.map +0 -1
- package/dist/api/workflows/index.browser.js +0 -246
- package/dist/api/workflows/index.browser.js.map +0 -1
- package/dist/api/workflows/index.d.ts +0 -1618
- package/dist/api/workflows/index.d.ts.map +0 -1
- package/dist/api/workflows/index.js +0 -1495
- package/dist/api/workflows/index.js.map +0 -1
- package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
- package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
- package/src/api/invitations/controllers/InvitationController.ts +0 -84
- package/src/api/invitations/entities/invitations.ts +0 -33
- package/src/api/invitations/index.ts +0 -58
- package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
- package/src/api/invitations/providers/InvitationProvider.ts +0 -45
- package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
- package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
- package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
- package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
- package/src/api/invitations/services/InvitationService.ts +0 -556
- package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
- package/src/api/issues/controllers/AdminIssueController.ts +0 -149
- package/src/api/issues/controllers/IssueController.ts +0 -44
- package/src/api/issues/entities/issues.ts +0 -49
- package/src/api/issues/index.ts +0 -50
- package/src/api/issues/schemas/createIssueSchema.ts +0 -13
- package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
- package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
- package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
- package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
- package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
- package/src/api/issues/services/IssueService.ts +0 -264
- package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
- package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
- package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
- package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
- package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
- package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
- package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
- package/src/api/jobs/services/JobService-tests.ts +0 -157
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
- package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
- package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
- package/src/api/subscriptions/entities/subscriptions.ts +0 -68
- package/src/api/subscriptions/index.ts +0 -133
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
- package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
- package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
- package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
- package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
- package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
- package/src/api/subscriptions/services/BillingService.ts +0 -437
- package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
- package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
- package/src/api/subscriptions/services/UsageService.ts +0 -118
- package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
- package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
- package/src/api/workflows/entities/workflowExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
- package/src/api/workflows/index.browser.ts +0 -22
- package/src/api/workflows/index.ts +0 -115
- package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
- package/src/api/workflows/primitives/$workflow.ts +0 -202
- package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
- package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
- package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
- package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
- package/src/api/workflows/services/WorkflowService.ts +0 -382
- package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
- 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,
|
|
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 === "
|
|
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
|
-
*
|
|
44
|
-
*
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
41
|
+
const aggRows = await this.executions.query(
|
|
63
42
|
(e) => sql`
|
|
64
43
|
SELECT
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
COUNT(*)
|
|
68
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
result.push(registration);
|
|
100
|
+
recent: counts,
|
|
101
|
+
});
|
|
134
102
|
}
|
|
135
|
-
|
|
136
103
|
return result;
|
|
137
104
|
}
|
|
138
105
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
}
|