alepha 0.19.3 → 0.19.5
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/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/dist/api/invitations/index.d.ts +790 -0
- package/dist/api/invitations/index.d.ts.map +1 -0
- package/dist/api/invitations/index.js +665 -0
- package/dist/api/invitations/index.js.map +1 -0
- package/dist/api/issues/index.d.ts +810 -0
- package/dist/api/issues/index.d.ts.map +1 -0
- package/dist/api/issues/index.js +447 -0
- package/dist/api/issues/index.js.map +1 -0
- package/dist/api/jobs/index.browser.js +8 -9
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +99 -43
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +257 -40
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.browser.js +0 -1
- package/dist/api/notifications/index.browser.js.map +1 -1
- package/dist/api/notifications/index.d.ts +3 -3
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +0 -1
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.browser.js +112 -1
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +90 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +79 -12
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/{billing → api/payments}/index.d.ts +67 -49
- package/dist/api/payments/index.d.ts.map +1 -0
- package/dist/{billing → api/payments}/index.js +108 -74
- package/dist/api/payments/index.js.map +1 -0
- package/dist/api/subscriptions/index.d.ts +1692 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1870 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +24 -2
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +176 -36
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +13 -13
- package/dist/api/workflows/index.browser.js +246 -0
- package/dist/api/workflows/index.browser.js.map +1 -0
- package/dist/api/workflows/index.d.ts +1618 -0
- package/dist/api/workflows/index.d.ts.map +1 -0
- package/dist/api/workflows/index.js +1504 -0
- package/dist/api/workflows/index.js.map +1 -0
- package/dist/captcha/index.d.ts +142 -0
- package/dist/captcha/index.d.ts.map +1 -0
- package/dist/captcha/index.js +177 -0
- package/dist/captcha/index.js.map +1 -0
- package/dist/cli/core/index.d.ts +126 -30
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +106 -67
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +84 -10
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +92 -4
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +60 -10
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +177 -45
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2 -3
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +21 -2
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +33 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +21 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +21 -2
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +21 -2
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.js +24 -8
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +0 -18
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +6 -23
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +1 -13
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +6 -23
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +3 -3
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +3 -3
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +1 -0
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +8 -4
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/router/index.browser.js +25 -3
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +16 -1
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +25 -3
- package/dist/react/router/index.js.map +1 -1
- package/dist/security/index.d.ts +28 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +28 -0
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +145 -2
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +364 -63
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js.map +1 -1
- package/package.json +47 -20
- package/src/api/invitations/__tests__/InvitationService.spec.ts +439 -0
- package/src/api/invitations/controllers/AdminInvitationController.ts +86 -0
- package/src/api/invitations/controllers/InvitationController.ts +84 -0
- package/src/api/invitations/entities/invitations.ts +33 -0
- package/src/api/invitations/index.ts +65 -0
- package/src/api/invitations/jobs/InvitationJobs.ts +37 -0
- package/src/api/invitations/providers/InvitationProvider.ts +45 -0
- package/src/api/invitations/schemas/createInvitationSchema.ts +12 -0
- package/src/api/invitations/schemas/invitationConfigAtom.ts +20 -0
- package/src/api/invitations/schemas/invitationQuerySchema.ts +15 -0
- package/src/api/invitations/schemas/invitationResourceSchema.ts +6 -0
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +22 -0
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +10 -0
- package/src/api/invitations/services/InvitationService.ts +556 -0
- package/src/api/issues/__tests__/IssueService.spec.ts +263 -0
- package/src/api/issues/controllers/AdminIssueController.ts +149 -0
- package/src/api/issues/controllers/IssueController.ts +44 -0
- package/src/api/issues/entities/issues.ts +49 -0
- package/src/api/issues/index.ts +53 -0
- package/src/api/issues/schemas/createIssueSchema.ts +13 -0
- package/src/api/issues/schemas/issueConfigAtom.ts +13 -0
- package/src/api/issues/schemas/issueQuerySchema.ts +18 -0
- package/src/api/issues/schemas/issueResourceSchema.ts +6 -0
- package/src/api/issues/schemas/myIssueQuerySchema.ts +10 -0
- package/src/api/issues/schemas/updateIssueSchema.ts +13 -0
- package/src/api/issues/services/IssueService.ts +264 -0
- package/src/api/jobs/__tests__/$job.spec.ts +876 -0
- package/src/api/jobs/controllers/AdminJobController.ts +44 -0
- package/src/api/jobs/entities/jobExecutionEntity.ts +0 -2
- package/src/api/jobs/index.ts +0 -3
- package/src/api/jobs/primitives/$job.ts +22 -11
- package/src/api/jobs/providers/JobProvider.ts +229 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +4 -0
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +1 -0
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +0 -1
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +1 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +1 -6
- package/src/api/jobs/services/JobService.ts +51 -12
- package/src/api/notifications/schemas/notificationQuerySchema.ts +0 -1
- package/src/api/parameters/__tests__/$parameter.spec.ts +327 -0
- package/src/api/parameters/controllers/AdminParameterController.ts +29 -3
- package/src/api/parameters/index.browser.ts +12 -0
- package/src/api/parameters/primitives/$parameter.ts +20 -3
- package/src/api/parameters/services/ParameterProvider.ts +48 -7
- package/src/{billing → api/payments}/__tests__/PaymentMethodService.spec.ts +32 -6
- package/src/api/payments/__tests__/PaymentService.spec.ts +279 -0
- package/src/{billing/controllers/AdminBillingController.ts → api/payments/controllers/AdminPaymentController.ts} +26 -21
- package/src/{billing/controllers/BillingController.ts → api/payments/controllers/PaymentController.ts} +23 -11
- package/src/{billing → api/payments}/entities/paymentIntents.ts +1 -0
- package/src/{billing/errors/BillingError.ts → api/payments/errors/PaymentError.ts} +1 -1
- package/src/{billing → api/payments}/index.ts +31 -25
- package/src/{billing/providers/MemoryBillingProvider.ts → api/payments/providers/MemoryPaymentProvider.ts} +4 -4
- package/src/{billing/providers/BillingProvider.ts → api/payments/providers/PaymentProvider.ts} +9 -2
- package/src/{billing → api/payments}/services/PaymentMethodService.ts +5 -5
- package/src/{billing/services/BillingService.ts → api/payments/services/PaymentService.ts} +94 -18
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +144 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/api/users/__tests__/AdminUserController.spec.ts +80 -1
- package/src/api/users/__tests__/CredentialService.spec.ts +177 -0
- package/src/api/users/__tests__/EmailVerification.spec.ts +29 -18
- package/src/api/users/__tests__/PasswordReset.spec.ts +3 -0
- package/src/api/users/__tests__/RegistrationService.spec.ts +148 -1
- package/src/api/users/__tests__/SessionService.spec.ts +142 -1
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +10 -1
- package/src/api/users/controllers/UserController.ts +3 -8
- package/src/api/users/notifications/UserNotifications.ts +23 -0
- package/src/api/users/primitives/$realm.ts +24 -0
- package/src/api/users/schemas/loginSchema.ts +1 -1
- package/src/api/users/services/CredentialService.ts +57 -7
- package/src/api/users/services/RegistrationService.ts +50 -11
- package/src/api/users/services/SessionService.ts +64 -9
- package/src/api/users/services/UserService.ts +21 -12
- package/src/api/workflows/__tests__/$workflow.spec.ts +616 -0
- package/src/api/workflows/controllers/AdminWorkflowController.ts +191 -0
- package/src/api/workflows/entities/workflowExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepExecutions.ts +74 -0
- package/src/api/workflows/entities/workflowStepLogs.ts +13 -0
- package/src/api/workflows/index.browser.ts +22 -0
- package/src/api/workflows/index.ts +124 -0
- package/src/api/workflows/jobs/WorkflowJobs.ts +77 -0
- package/src/api/workflows/primitives/$workflow.ts +202 -0
- package/src/api/workflows/providers/WorkflowProvider.ts +1284 -0
- package/src/api/workflows/schemas/workflowActivitySchema.ts +15 -0
- package/src/api/workflows/schemas/workflowConfigAtom.ts +51 -0
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +18 -0
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +26 -0
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +30 -0
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +26 -0
- package/src/api/workflows/schemas/workflowStatsSchema.ts +16 -0
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +15 -0
- package/src/api/workflows/services/WorkflowService.ts +382 -0
- package/src/captcha/__tests__/MemoryCaptchaProvider.spec.ts +74 -0
- package/src/captcha/index.ts +33 -0
- package/src/captcha/providers/CaptchaProvider.ts +17 -0
- package/src/captcha/providers/MemoryCaptchaProvider.ts +65 -0
- package/src/captcha/providers/TurnstileCaptchaProvider.ts +125 -0
- package/src/cli/core/atoms/buildOptions.ts +57 -0
- package/src/cli/core/commands/build.ts +2 -0
- package/src/cli/core/providers/ViteDevServerProvider.ts +1 -1
- package/src/cli/core/services/ViteUtils.ts +5 -2
- package/src/cli/core/tasks/BuildClientTask.ts +3 -1
- package/src/cli/core/tasks/BuildCloudflareTask.ts +4 -0
- package/src/cli/core/tasks/BuildPwaTask.ts +81 -0
- package/src/cli/core/templates/webAppRouterTs.ts +5 -58
- package/src/cli/platform/adapters/CloudflareAdapter.ts +24 -0
- package/src/cli/platform/atoms/platformOptions.ts +19 -3
- package/src/cli/platform/hooks/PlatformHook.ts +51 -0
- package/src/cli/platform/index.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +22 -1
- package/src/cli/platform/services/PlatformOrchestrator.ts +67 -2
- package/src/cli/vendor/__tests__/VendorService.spec.ts +322 -178
- package/src/cli/vendor/commands/VendorCommand.ts +41 -38
- package/src/cli/vendor/services/VendorService.ts +234 -31
- package/src/command/__tests__/CliProvider.spec.ts +45 -0
- package/src/command/providers/CliProvider.ts +3 -4
- package/src/core/__tests__/TypeProvider.spec.ts +4 -2
- package/src/core/providers/SchemaValidator.ts +1 -1
- package/src/core/providers/TypeProvider.ts +46 -3
- package/src/orm/__tests__/enums.spec.ts +22 -29
- package/src/orm/__tests__/orm-showcase-tests.ts +430 -0
- package/src/orm/__tests__/orm-showcase.spec.ts +167 -0
- package/src/orm/core/providers/DatabaseTypeProvider.ts +0 -29
- package/src/orm/core/services/Repository.ts +20 -6
- package/src/orm/postgres/services/PostgresModelBuilder.ts +3 -6
- package/src/react/i18n/__tests__/I18nProvider.spec.ts +83 -0
- package/src/react/i18n/providers/I18nProvider.ts +12 -10
- package/src/react/router/__tests__/$page.browser.spec.tsx +157 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +39 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +22 -0
- package/src/security/__tests__/$secure-combinations.spec.ts +945 -0
- package/src/security/primitives/$issuer.ts +3 -1
- package/src/security/primitives/$secure.ts +28 -0
- package/src/server/auth/index.ts +7 -0
- package/src/server/auth/primitives/$auth.ts +37 -3
- package/src/server/auth/primitives/$authApple.ts +114 -4
- package/src/server/auth/primitives/$authFacebook.ts +98 -0
- package/src/server/auth/primitives/$authFranceConnect.ts +105 -0
- package/src/server/auth/primitives/$authGithub.ts +22 -16
- package/src/server/auth/primitives/$authMicrosoft.ts +88 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +197 -72
- package/src/server/cookies/providers/ServerCookiesProvider.ts +3 -0
- package/src/server/core/__tests__/ServerRouterProvider-errorHandler.spec.ts +1 -1
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -1
- package/dist/billing/index.d.ts.map +0 -1
- package/dist/billing/index.js.map +0 -1
- package/src/billing/__tests__/BillingService.spec.ts +0 -136
- /package/src/{billing → api/payments}/entities/paymentMethods.ts +0 -0
- /package/src/{billing → api/payments}/entities/refunds.ts +0 -0
- /package/src/{billing → api/payments}/schemas/intentSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/paymentMethodSchemas.ts +0 -0
- /package/src/{billing → api/payments}/schemas/refundSchemas.ts +0 -0
|
@@ -68,7 +68,7 @@ export class JobService {
|
|
|
68
68
|
COUNT(*) FILTER (WHERE ${e.status} = 'retrying') AS retrying,
|
|
69
69
|
COUNT(*) FILTER (WHERE ${e.status} = 'dead') AS dead,
|
|
70
70
|
COUNT(*) FILTER (WHERE ${e.status} = 'completed' AND ${e.completedAt} >= ${periodAgo}) AS completed_24h,
|
|
71
|
-
COUNT(*) FILTER (WHERE ${e.status}
|
|
71
|
+
COUNT(*) FILTER (WHERE ${e.status} = 'dead' AND ${e.completedAt} >= ${periodAgo}) AS failed_24h
|
|
72
72
|
FROM ${e}
|
|
73
73
|
`,
|
|
74
74
|
t.object({
|
|
@@ -127,12 +127,7 @@ export class JobService {
|
|
|
127
127
|
hasBackoff: Boolean(opts.retry.backoff),
|
|
128
128
|
}
|
|
129
129
|
: undefined,
|
|
130
|
-
|
|
131
|
-
? {
|
|
132
|
-
size: opts.batch.size,
|
|
133
|
-
window: String(opts.batch.window),
|
|
134
|
-
}
|
|
135
|
-
: undefined,
|
|
130
|
+
paused: this.jobProvider.isJobPaused(name),
|
|
136
131
|
};
|
|
137
132
|
|
|
138
133
|
result.push(registration);
|
|
@@ -278,6 +273,48 @@ export class JobService {
|
|
|
278
273
|
return { ok: true };
|
|
279
274
|
}
|
|
280
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
|
+
|
|
281
318
|
public async getCronJobs(): Promise<JobCronInfo[]> {
|
|
282
319
|
const jobs = this.jobProvider.getRegisteredJobs();
|
|
283
320
|
const cronJobNames: string[] = [];
|
|
@@ -301,6 +338,7 @@ export class JobService {
|
|
|
301
338
|
priority: (opts.priority ?? "normal") as JobCronInfo["priority"],
|
|
302
339
|
concurrency: opts.concurrency ?? 1,
|
|
303
340
|
hasSchema: Boolean(opts.schema),
|
|
341
|
+
paused: this.jobProvider.isJobPaused(name),
|
|
304
342
|
lastExecution: last
|
|
305
343
|
? {
|
|
306
344
|
id: last.id,
|
|
@@ -355,6 +393,7 @@ export class JobService {
|
|
|
355
393
|
retrying: Number(row?.retrying ?? 0),
|
|
356
394
|
dead: Number(row?.dead ?? 0),
|
|
357
395
|
concurrency: reg.options.concurrency ?? 1,
|
|
396
|
+
paused: this.jobProvider.isJobPaused(name),
|
|
358
397
|
});
|
|
359
398
|
}
|
|
360
399
|
|
|
@@ -378,10 +417,10 @@ export class JobService {
|
|
|
378
417
|
SELECT
|
|
379
418
|
ds.date::text AS date,
|
|
380
419
|
COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'completed'), 0) AS completed,
|
|
381
|
-
COALESCE(COUNT(*) FILTER (WHERE ${e.status}
|
|
420
|
+
COALESCE(COUNT(*) FILTER (WHERE ${e.status} = 'dead'), 0) AS failed
|
|
382
421
|
FROM date_series ds
|
|
383
422
|
LEFT JOIN ${e} ON DATE(${e.completedAt}) = ds.date
|
|
384
|
-
AND ${e.status} IN ('completed', 'dead'
|
|
423
|
+
AND ${e.status} IN ('completed', 'dead')
|
|
385
424
|
GROUP BY ds.date
|
|
386
425
|
ORDER BY ds.date ASC
|
|
387
426
|
`,
|
|
@@ -404,7 +443,7 @@ export class JobService {
|
|
|
404
443
|
const startDate = now.subtract(days - 1, "day");
|
|
405
444
|
|
|
406
445
|
const where = this.executions.createQueryWhere();
|
|
407
|
-
where.status = { inArray: ["completed", "dead"
|
|
446
|
+
where.status = { inArray: ["completed", "dead"] };
|
|
408
447
|
where.completedAt = { gte: startDate.startOf("day").toISOString() };
|
|
409
448
|
|
|
410
449
|
const executions = await this.executions.findMany({ where });
|
|
@@ -448,7 +487,7 @@ export class JobService {
|
|
|
448
487
|
COUNT(*) AS failures,
|
|
449
488
|
(ARRAY_AGG(${e.error} ORDER BY ${e.completedAt} DESC))[1] AS last_error
|
|
450
489
|
FROM ${e}
|
|
451
|
-
WHERE ${e.status}
|
|
490
|
+
WHERE ${e.status} = 'dead'
|
|
452
491
|
AND ${e.completedAt} >= ${periodAgoIso}
|
|
453
492
|
GROUP BY ${e.jobName}
|
|
454
493
|
ORDER BY failures DESC
|
|
@@ -471,7 +510,7 @@ export class JobService {
|
|
|
471
510
|
periodAgoIso: string,
|
|
472
511
|
): Promise<JobFailure[]> {
|
|
473
512
|
const where = this.executions.createQueryWhere();
|
|
474
|
-
where.status = {
|
|
513
|
+
where.status = { eq: "dead" };
|
|
475
514
|
where.completedAt = { gte: periodAgoIso };
|
|
476
515
|
|
|
477
516
|
const failures = await this.executions.findMany({
|
|
@@ -1902,3 +1902,330 @@ describe("ParameterTreeNode children structure", () => {
|
|
|
1902
1902
|
expect(aNode.children.map((c: any) => c.name).sort()).toEqual(["b", "c"]);
|
|
1903
1903
|
});
|
|
1904
1904
|
});
|
|
1905
|
+
|
|
1906
|
+
// ---------------------------------------------------------------------------
|
|
1907
|
+
// Content validation in save()
|
|
1908
|
+
// ---------------------------------------------------------------------------
|
|
1909
|
+
|
|
1910
|
+
describe("save validation", () => {
|
|
1911
|
+
it("should reject invalid content when schema hash matches", async () => {
|
|
1912
|
+
class AppConfig {
|
|
1913
|
+
features = $parameter({
|
|
1914
|
+
name: "validate.reject",
|
|
1915
|
+
schema: featureSchema,
|
|
1916
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
1921
|
+
alepha.with(AlephaApiParameters);
|
|
1922
|
+
alepha.with(AppConfig);
|
|
1923
|
+
await alepha.start();
|
|
1924
|
+
|
|
1925
|
+
const provider = alepha.inject(ParameterProvider);
|
|
1926
|
+
const schemaHash = (provider as any).schemaHashes.get("validate.reject");
|
|
1927
|
+
|
|
1928
|
+
// Invalid: maxUploadSize should be number, not string
|
|
1929
|
+
await expect(
|
|
1930
|
+
provider.save(
|
|
1931
|
+
"validate.reject",
|
|
1932
|
+
{ enableBeta: true, maxUploadSize: "not-a-number" } as any,
|
|
1933
|
+
schemaHash,
|
|
1934
|
+
),
|
|
1935
|
+
).rejects.toThrow();
|
|
1936
|
+
});
|
|
1937
|
+
|
|
1938
|
+
it("should accept valid content when schema hash matches", async () => {
|
|
1939
|
+
class AppConfig {
|
|
1940
|
+
features = $parameter({
|
|
1941
|
+
name: "validate.accept",
|
|
1942
|
+
schema: featureSchema,
|
|
1943
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
1944
|
+
});
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
1948
|
+
alepha.with(AlephaApiParameters);
|
|
1949
|
+
alepha.with(AppConfig);
|
|
1950
|
+
await alepha.start();
|
|
1951
|
+
|
|
1952
|
+
const provider = alepha.inject(ParameterProvider);
|
|
1953
|
+
const schemaHash = (provider as any).schemaHashes.get("validate.accept");
|
|
1954
|
+
|
|
1955
|
+
const result = await provider.save(
|
|
1956
|
+
"validate.accept",
|
|
1957
|
+
{ enableBeta: true, maxUploadSize: 999 },
|
|
1958
|
+
schemaHash,
|
|
1959
|
+
);
|
|
1960
|
+
|
|
1961
|
+
expect(result.content).toEqual({ enableBeta: true, maxUploadSize: 999 });
|
|
1962
|
+
});
|
|
1963
|
+
|
|
1964
|
+
it("should skip validation when schema hash differs (migration seed)", async () => {
|
|
1965
|
+
class AppConfig {
|
|
1966
|
+
features = $parameter({
|
|
1967
|
+
name: "validate.skip",
|
|
1968
|
+
schema: featureSchema,
|
|
1969
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
1974
|
+
alepha.with(AlephaApiParameters);
|
|
1975
|
+
alepha.with(AppConfig);
|
|
1976
|
+
await alepha.start();
|
|
1977
|
+
|
|
1978
|
+
const provider = alepha.inject(ParameterProvider);
|
|
1979
|
+
|
|
1980
|
+
// Save with mismatched hash — should NOT validate against current schema
|
|
1981
|
+
const result = await provider.save(
|
|
1982
|
+
"validate.skip",
|
|
1983
|
+
{ totally: "different", shape: 42 } as any,
|
|
1984
|
+
"old-hash",
|
|
1985
|
+
);
|
|
1986
|
+
|
|
1987
|
+
expect(result.content).toEqual({ totally: "different", shape: 42 });
|
|
1988
|
+
});
|
|
1989
|
+
});
|
|
1990
|
+
|
|
1991
|
+
// ---------------------------------------------------------------------------
|
|
1992
|
+
// $parameter.name fallback
|
|
1993
|
+
// ---------------------------------------------------------------------------
|
|
1994
|
+
|
|
1995
|
+
describe("$parameter.name fallback", () => {
|
|
1996
|
+
it("should use ClassName.propertyKey when name option is omitted", async () => {
|
|
1997
|
+
class MySettings {
|
|
1998
|
+
theme = $parameter({
|
|
1999
|
+
schema: t.object({ dark: t.boolean() }),
|
|
2000
|
+
default: { dark: false },
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2005
|
+
alepha.with(AlephaApiParameters);
|
|
2006
|
+
alepha.with(MySettings);
|
|
2007
|
+
await alepha.start();
|
|
2008
|
+
|
|
2009
|
+
const settings = alepha.inject(MySettings);
|
|
2010
|
+
expect(settings.theme.name).toBe("MySettings.theme");
|
|
2011
|
+
});
|
|
2012
|
+
|
|
2013
|
+
it("should use explicit name when provided", async () => {
|
|
2014
|
+
class MySettings {
|
|
2015
|
+
theme = $parameter({
|
|
2016
|
+
name: "app.ui.theme",
|
|
2017
|
+
schema: t.object({ dark: t.boolean() }),
|
|
2018
|
+
default: { dark: false },
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2023
|
+
alepha.with(AlephaApiParameters);
|
|
2024
|
+
alepha.with(MySettings);
|
|
2025
|
+
await alepha.start();
|
|
2026
|
+
|
|
2027
|
+
const settings = alepha.inject(MySettings);
|
|
2028
|
+
expect(settings.theme.name).toBe("app.ui.theme");
|
|
2029
|
+
});
|
|
2030
|
+
});
|
|
2031
|
+
|
|
2032
|
+
// ---------------------------------------------------------------------------
|
|
2033
|
+
// delete()
|
|
2034
|
+
// ---------------------------------------------------------------------------
|
|
2035
|
+
|
|
2036
|
+
describe("delete", () => {
|
|
2037
|
+
it("should delete all versions and clear cache", async () => {
|
|
2038
|
+
class AppConfig {
|
|
2039
|
+
features = $parameter({
|
|
2040
|
+
name: "delete.test",
|
|
2041
|
+
schema: featureSchema,
|
|
2042
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2047
|
+
alepha.with(AlephaApiParameters);
|
|
2048
|
+
alepha.with(AppConfig);
|
|
2049
|
+
await alepha.start();
|
|
2050
|
+
|
|
2051
|
+
const config = alepha.inject(AppConfig);
|
|
2052
|
+
const provider = alepha.inject(ParameterProvider);
|
|
2053
|
+
|
|
2054
|
+
await config.features.set({ enableBeta: true, maxUploadSize: 100 });
|
|
2055
|
+
await config.features.set({ enableBeta: false, maxUploadSize: 200 });
|
|
2056
|
+
|
|
2057
|
+
const historyBefore = await provider.getHistory("delete.test");
|
|
2058
|
+
expect(historyBefore.length).toBe(2);
|
|
2059
|
+
|
|
2060
|
+
await config.features.delete();
|
|
2061
|
+
|
|
2062
|
+
// All versions gone
|
|
2063
|
+
const historyAfter = await provider.getHistory("delete.test");
|
|
2064
|
+
expect(historyAfter.length).toBe(0);
|
|
2065
|
+
|
|
2066
|
+
// Cache cleared — falls back to default
|
|
2067
|
+
expect(await config.features.get()).toEqual({
|
|
2068
|
+
enableBeta: false,
|
|
2069
|
+
maxUploadSize: 10485760,
|
|
2070
|
+
});
|
|
2071
|
+
expect(config.features.isUsingDefault).toBe(true);
|
|
2072
|
+
});
|
|
2073
|
+
|
|
2074
|
+
it("should be a no-op for non-existent parameter", async () => {
|
|
2075
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2076
|
+
alepha.with(AlephaApiParameters);
|
|
2077
|
+
await alepha.start();
|
|
2078
|
+
|
|
2079
|
+
const provider = alepha.inject(ParameterProvider);
|
|
2080
|
+
|
|
2081
|
+
// Should not throw
|
|
2082
|
+
await provider.delete("nonexistent.param");
|
|
2083
|
+
});
|
|
2084
|
+
});
|
|
2085
|
+
|
|
2086
|
+
// ---------------------------------------------------------------------------
|
|
2087
|
+
// getHistory pagination
|
|
2088
|
+
// ---------------------------------------------------------------------------
|
|
2089
|
+
|
|
2090
|
+
describe("getHistory pagination", () => {
|
|
2091
|
+
it("should respect limit and offset", async () => {
|
|
2092
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2093
|
+
alepha.with(AlephaApiParameters);
|
|
2094
|
+
await alepha.start();
|
|
2095
|
+
|
|
2096
|
+
const provider = alepha.inject(ParameterProvider);
|
|
2097
|
+
|
|
2098
|
+
for (let i = 1; i <= 5; i++) {
|
|
2099
|
+
await provider.save("paginate.test", { v: i }, "h");
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
// Get first 2 (versions 5, 4 — desc order)
|
|
2103
|
+
const page1 = await provider.getHistory("paginate.test", {
|
|
2104
|
+
limit: 2,
|
|
2105
|
+
});
|
|
2106
|
+
expect(page1.length).toBe(2);
|
|
2107
|
+
expect(page1[0].version).toBe(5);
|
|
2108
|
+
expect(page1[1].version).toBe(4);
|
|
2109
|
+
|
|
2110
|
+
// Get next 2 (versions 3, 2)
|
|
2111
|
+
const page2 = await provider.getHistory("paginate.test", {
|
|
2112
|
+
limit: 2,
|
|
2113
|
+
offset: 2,
|
|
2114
|
+
});
|
|
2115
|
+
expect(page2.length).toBe(2);
|
|
2116
|
+
expect(page2[0].version).toBe(3);
|
|
2117
|
+
expect(page2[1].version).toBe(2);
|
|
2118
|
+
});
|
|
2119
|
+
|
|
2120
|
+
it("should return all when no limit specified", async () => {
|
|
2121
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2122
|
+
alepha.with(AlephaApiParameters);
|
|
2123
|
+
await alepha.start();
|
|
2124
|
+
|
|
2125
|
+
const provider = alepha.inject(ParameterProvider);
|
|
2126
|
+
|
|
2127
|
+
await provider.save("paginate.all", { v: 1 }, "h");
|
|
2128
|
+
await provider.save("paginate.all", { v: 2 }, "h");
|
|
2129
|
+
await provider.save("paginate.all", { v: 3 }, "h");
|
|
2130
|
+
|
|
2131
|
+
const all = await provider.getHistory("paginate.all");
|
|
2132
|
+
expect(all.length).toBe(3);
|
|
2133
|
+
});
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
// ---------------------------------------------------------------------------
|
|
2137
|
+
// getVersion on primitive
|
|
2138
|
+
// ---------------------------------------------------------------------------
|
|
2139
|
+
|
|
2140
|
+
describe("$parameter.getVersion", () => {
|
|
2141
|
+
it("should return a specific version via the primitive", async () => {
|
|
2142
|
+
class AppConfig {
|
|
2143
|
+
features = $parameter({
|
|
2144
|
+
name: "prim.getversion",
|
|
2145
|
+
schema: featureSchema,
|
|
2146
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
|
|
2150
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2151
|
+
alepha.with(AlephaApiParameters);
|
|
2152
|
+
alepha.with(AppConfig);
|
|
2153
|
+
await alepha.start();
|
|
2154
|
+
|
|
2155
|
+
const config = alepha.inject(AppConfig);
|
|
2156
|
+
|
|
2157
|
+
await config.features.set({ enableBeta: true, maxUploadSize: 1 });
|
|
2158
|
+
await config.features.set({ enableBeta: false, maxUploadSize: 2 });
|
|
2159
|
+
|
|
2160
|
+
const v1 = await config.features.getVersion(1);
|
|
2161
|
+
expect(v1).not.toBeNull();
|
|
2162
|
+
expect(v1!.content).toEqual({ enableBeta: true, maxUploadSize: 1 });
|
|
2163
|
+
|
|
2164
|
+
const missing = await config.features.getVersion(99);
|
|
2165
|
+
expect(missing).toBeNull();
|
|
2166
|
+
});
|
|
2167
|
+
});
|
|
2168
|
+
|
|
2169
|
+
// ---------------------------------------------------------------------------
|
|
2170
|
+
// Tags
|
|
2171
|
+
// ---------------------------------------------------------------------------
|
|
2172
|
+
|
|
2173
|
+
describe("tags", () => {
|
|
2174
|
+
it("should store and retrieve tags on parameter versions", async () => {
|
|
2175
|
+
class AppConfig {
|
|
2176
|
+
features = $parameter({
|
|
2177
|
+
name: "tags.test",
|
|
2178
|
+
schema: featureSchema,
|
|
2179
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
2180
|
+
});
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2184
|
+
alepha.with(AlephaApiParameters);
|
|
2185
|
+
alepha.with(AppConfig);
|
|
2186
|
+
await alepha.start();
|
|
2187
|
+
|
|
2188
|
+
const config = alepha.inject(AppConfig);
|
|
2189
|
+
|
|
2190
|
+
await config.features.set(
|
|
2191
|
+
{ enableBeta: true, maxUploadSize: 100 },
|
|
2192
|
+
{ tags: ["feature-flag", "beta"] },
|
|
2193
|
+
);
|
|
2194
|
+
|
|
2195
|
+
const history = await config.features.getHistory();
|
|
2196
|
+
expect(history[0].tags).toEqual(["feature-flag", "beta"]);
|
|
2197
|
+
});
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
// ---------------------------------------------------------------------------
|
|
2201
|
+
// Set idempotency
|
|
2202
|
+
// ---------------------------------------------------------------------------
|
|
2203
|
+
|
|
2204
|
+
describe("set idempotency", () => {
|
|
2205
|
+
it("should not notify subscribers when setting the same value", async () => {
|
|
2206
|
+
class AppConfig {
|
|
2207
|
+
features = $parameter({
|
|
2208
|
+
name: "idempotent.test",
|
|
2209
|
+
schema: featureSchema,
|
|
2210
|
+
default: { enableBeta: false, maxUploadSize: 10485760 },
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
|
|
2214
|
+
const alepha = Alepha.create().with(AlephaOrmPostgres);
|
|
2215
|
+
alepha.with(AlephaApiParameters);
|
|
2216
|
+
alepha.with(AppConfig);
|
|
2217
|
+
await alepha.start();
|
|
2218
|
+
|
|
2219
|
+
const config = alepha.inject(AppConfig);
|
|
2220
|
+
|
|
2221
|
+
await config.features.set({ enableBeta: true, maxUploadSize: 100 });
|
|
2222
|
+
|
|
2223
|
+
const received: unknown[] = [];
|
|
2224
|
+
config.features.sub((v) => received.push(v));
|
|
2225
|
+
|
|
2226
|
+
// Set the exact same value again
|
|
2227
|
+
await config.features.set({ enableBeta: true, maxUploadSize: 100 });
|
|
2228
|
+
|
|
2229
|
+
expect(received.length).toBe(0);
|
|
2230
|
+
});
|
|
2231
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { $inject, AlephaError, t } from "alepha";
|
|
2
2
|
import { $secure } from "alepha/security";
|
|
3
|
-
import { $action } from "alepha/server";
|
|
3
|
+
import { $action, okSchema } from "alepha/server";
|
|
4
4
|
import { activateParameterBodySchema } from "../schemas/activateParameterBodySchema.ts";
|
|
5
5
|
import { createParameterVersionBodySchema } from "../schemas/createParameterVersionBodySchema.ts";
|
|
6
6
|
import { parameterCurrentResponseSchema } from "../schemas/parameterCurrentResponseSchema.ts";
|
|
@@ -78,10 +78,17 @@ export class AdminParameterController {
|
|
|
78
78
|
method: "GET",
|
|
79
79
|
schema: {
|
|
80
80
|
params: parameterNameParamSchema,
|
|
81
|
+
query: t.object({
|
|
82
|
+
limit: t.optional(t.integer({ minimum: 1, maximum: 100 })),
|
|
83
|
+
offset: t.optional(t.integer({ minimum: 0 })),
|
|
84
|
+
}),
|
|
81
85
|
response: parameterHistoryResponseSchema,
|
|
82
86
|
},
|
|
83
|
-
handler: async ({ params }) => {
|
|
84
|
-
const rawVersions = await this.provider.getHistory(params.name
|
|
87
|
+
handler: async ({ params, query }) => {
|
|
88
|
+
const rawVersions = await this.provider.getHistory(params.name, {
|
|
89
|
+
limit: query.limit,
|
|
90
|
+
offset: query.offset,
|
|
91
|
+
});
|
|
85
92
|
const versions = this.provider.calculateStatuses(rawVersions);
|
|
86
93
|
return { versions };
|
|
87
94
|
},
|
|
@@ -241,4 +248,23 @@ export class AdminParameterController {
|
|
|
241
248
|
);
|
|
242
249
|
},
|
|
243
250
|
});
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Delete all versions of a parameter.
|
|
254
|
+
*/
|
|
255
|
+
deleteParameter = $action({
|
|
256
|
+
group: this.group,
|
|
257
|
+
use: [$secure({ permissions: ["admin:parameter:delete"] })],
|
|
258
|
+
description: "Delete all versions of a parameter.",
|
|
259
|
+
path: "/parameters/:name",
|
|
260
|
+
method: "DELETE",
|
|
261
|
+
schema: {
|
|
262
|
+
params: parameterNameParamSchema,
|
|
263
|
+
response: okSchema,
|
|
264
|
+
},
|
|
265
|
+
handler: async ({ params }) => {
|
|
266
|
+
await this.provider.delete(params.name);
|
|
267
|
+
return { ok: true };
|
|
268
|
+
},
|
|
269
|
+
});
|
|
244
270
|
}
|
|
@@ -3,6 +3,18 @@ import { $module } from "alepha";
|
|
|
3
3
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
4
4
|
|
|
5
5
|
export * from "./entities/parameters.ts";
|
|
6
|
+
export * from "./schemas/activateParameterBodySchema.ts";
|
|
7
|
+
export * from "./schemas/createParameterVersionBodySchema.ts";
|
|
8
|
+
export * from "./schemas/parameterCurrentResponseSchema.ts";
|
|
9
|
+
export * from "./schemas/parameterHistoryResponseSchema.ts";
|
|
10
|
+
export * from "./schemas/parameterNameParamSchema.ts";
|
|
11
|
+
export * from "./schemas/parameterNamesResponseSchema.ts";
|
|
12
|
+
export * from "./schemas/parameterResponseSchema.ts";
|
|
13
|
+
export * from "./schemas/parameterStatusSchema.ts";
|
|
14
|
+
export * from "./schemas/parameterTreeNodeSchema.ts";
|
|
15
|
+
export * from "./schemas/parameterVersionParamSchema.ts";
|
|
16
|
+
export * from "./schemas/parameterVersionResponseSchema.ts";
|
|
17
|
+
export * from "./schemas/rollbackParameterBodySchema.ts";
|
|
6
18
|
|
|
7
19
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
8
20
|
|
|
@@ -84,7 +84,10 @@ export class ParameterPrimitive<T extends TObject> extends Primitive<
|
|
|
84
84
|
* Parameter name (uses property key if not specified).
|
|
85
85
|
*/
|
|
86
86
|
public get name(): string {
|
|
87
|
-
return
|
|
87
|
+
return (
|
|
88
|
+
this.options.name ||
|
|
89
|
+
`${this.config.service.name}.${this.config.propertyKey}`
|
|
90
|
+
);
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
/**
|
|
@@ -163,8 +166,22 @@ export class ParameterPrimitive<T extends TObject> extends Primitive<
|
|
|
163
166
|
/**
|
|
164
167
|
* Get version history for this parameter.
|
|
165
168
|
*/
|
|
166
|
-
public async getHistory() {
|
|
167
|
-
return this.provider.getHistory(this.name);
|
|
169
|
+
public async getHistory(options?: { limit?: number; offset?: number }) {
|
|
170
|
+
return this.provider.getHistory(this.name, options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get a specific version of this parameter.
|
|
175
|
+
*/
|
|
176
|
+
public async getVersion(version: number) {
|
|
177
|
+
return this.provider.getVersion(this.name, version);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Delete all versions of this parameter.
|
|
182
|
+
*/
|
|
183
|
+
public async delete(): Promise<void> {
|
|
184
|
+
await this.provider.delete(this.name);
|
|
168
185
|
}
|
|
169
186
|
|
|
170
187
|
/**
|
|
@@ -89,6 +89,12 @@ export class ParameterProvider {
|
|
|
89
89
|
*/
|
|
90
90
|
protected readonly loadPromises = new Map<string, Promise<void>>();
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Generation counter per parameter — incremented on each doLoad call.
|
|
94
|
+
* Used to discard results from superseded loads.
|
|
95
|
+
*/
|
|
96
|
+
protected readonly loadGeneration = new Map<string, number>();
|
|
97
|
+
|
|
92
98
|
/**
|
|
93
99
|
* Subscriber callbacks per parameter name.
|
|
94
100
|
*/
|
|
@@ -379,6 +385,15 @@ export class ParameterProvider {
|
|
|
379
385
|
schemaHash = this.schemaHashes.get(name) ?? "";
|
|
380
386
|
}
|
|
381
387
|
|
|
388
|
+
// Validate content against the registered schema when the schema hash
|
|
389
|
+
// matches. A mismatched hash means the content is from a different
|
|
390
|
+
// schema version (e.g., migration seed) and should not be validated
|
|
391
|
+
// against the current schema.
|
|
392
|
+
const param = this.primitives.get(name);
|
|
393
|
+
if (param && schemaHash === this.schemaHashes.get(name)) {
|
|
394
|
+
content = this.alepha.codec.validate(param.schema, content) as Static<T>;
|
|
395
|
+
}
|
|
396
|
+
|
|
382
397
|
const now = this.dateTimeProvider.now().toDate();
|
|
383
398
|
const activationDate = options.activationDate ?? now;
|
|
384
399
|
const isImmediate = activationDate <= now;
|
|
@@ -444,13 +459,32 @@ export class ParameterProvider {
|
|
|
444
459
|
/**
|
|
445
460
|
* Get all versions of a parameter.
|
|
446
461
|
*/
|
|
447
|
-
public async getHistory(
|
|
462
|
+
public async getHistory(
|
|
463
|
+
name: string,
|
|
464
|
+
options?: { limit?: number; offset?: number },
|
|
465
|
+
): Promise<Parameter[]> {
|
|
448
466
|
return this.repo.findMany({
|
|
449
467
|
where: { name },
|
|
450
468
|
orderBy: { column: "version", direction: "desc" },
|
|
469
|
+
limit: options?.limit,
|
|
470
|
+
offset: options?.offset,
|
|
451
471
|
});
|
|
452
472
|
}
|
|
453
473
|
|
|
474
|
+
/**
|
|
475
|
+
* Delete all versions of a parameter.
|
|
476
|
+
*/
|
|
477
|
+
public async delete(name: string): Promise<void> {
|
|
478
|
+
await this.repo.deleteMany({ name: { eq: name } });
|
|
479
|
+
this.cachedCurrent.delete(name);
|
|
480
|
+
this.cachedNext.delete(name);
|
|
481
|
+
this.loaded.delete(name);
|
|
482
|
+
this.loadPromises.delete(name);
|
|
483
|
+
this.loadGeneration.delete(name);
|
|
484
|
+
this.migrationChecked.delete(name);
|
|
485
|
+
this.log.info("Parameter deleted", { name });
|
|
486
|
+
}
|
|
487
|
+
|
|
454
488
|
/**
|
|
455
489
|
* Get a specific version of a parameter.
|
|
456
490
|
*/
|
|
@@ -568,9 +602,15 @@ export class ParameterProvider {
|
|
|
568
602
|
* Fetches current and next from database, updates cache.
|
|
569
603
|
*/
|
|
570
604
|
protected async doLoad(name: string): Promise<void> {
|
|
605
|
+
const gen = (this.loadGeneration.get(name) ?? 0) + 1;
|
|
606
|
+
this.loadGeneration.set(name, gen);
|
|
607
|
+
|
|
571
608
|
const { current, next } = await this.loadCurrentAndNext(name);
|
|
572
609
|
const schemaHash = this.schemaHashes.get(name) ?? "";
|
|
573
610
|
|
|
611
|
+
// Superseded by a newer load — discard results
|
|
612
|
+
if (this.loadGeneration.get(name) !== gen) return;
|
|
613
|
+
|
|
574
614
|
// Check if migration is needed
|
|
575
615
|
if (current && !this.migrationChecked.has(name)) {
|
|
576
616
|
this.migrationChecked.add(name);
|
|
@@ -665,21 +705,22 @@ export class ParameterProvider {
|
|
|
665
705
|
}
|
|
666
706
|
|
|
667
707
|
/**
|
|
668
|
-
* Poll until a lock is released.
|
|
708
|
+
* Poll until a lock is released (or TTL expires).
|
|
709
|
+
* Uses a probe-only SET NX with minimal TTL to detect release
|
|
710
|
+
* without holding the lock longer than necessary.
|
|
669
711
|
*/
|
|
670
712
|
protected async waitForLock(lockKey: string): Promise<void> {
|
|
671
713
|
const maxWait = 30_000;
|
|
714
|
+
const probeId = crypto.randomUUID();
|
|
672
715
|
const start = this.dateTimeProvider.nowMillis();
|
|
673
716
|
while (this.dateTimeProvider.nowMillis() - start < maxWait) {
|
|
674
|
-
await this.dateTimeProvider.wait(
|
|
675
|
-
const
|
|
676
|
-
|
|
677
|
-
if (value === lockId) {
|
|
717
|
+
await this.dateTimeProvider.wait(500);
|
|
718
|
+
const value = await this.lockProvider.set(lockKey, probeId, true, 500);
|
|
719
|
+
if (value === probeId) {
|
|
678
720
|
await this.lockProvider.del(lockKey);
|
|
679
721
|
return;
|
|
680
722
|
}
|
|
681
723
|
}
|
|
682
|
-
// Timeout — proceed anyway (lock expired or will expire)
|
|
683
724
|
}
|
|
684
725
|
|
|
685
726
|
/**
|