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,7 +1,8 @@
|
|
|
1
1
|
import { $inject, t } from "alepha";
|
|
2
|
-
import {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
...
|
|
35
|
-
content:
|
|
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
|
|
52
|
-
|
|
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
|
-
* -
|
|
28
|
-
* -
|
|
29
|
-
* -
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
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
|
-
|
|
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();
|
|
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
|
-
|
|
56
|
-
const sessionsBefore = await repos.sessionRepository.findMany();
|
|
57
|
-
expect(sessionsBefore).toHaveLength(3);
|
|
51
|
+
expect(await repos.sessionRepository.findMany()).toHaveLength(3);
|
|
58
52
|
|
|
59
|
-
//
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
/**
|
package/src/api/users/index.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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", "--
|
|
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
|
|
406
|
-
|
|
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;
|