alepha 0.20.5 → 0.20.7
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +701 -654
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +24 -1
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +193 -166
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +52 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +40 -14
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +639 -333
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +495 -162
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +222 -188
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +54 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +265 -236
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +55 -13
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/organizations/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +332 -314
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +1001 -844
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +237 -28
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +123 -122
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +21 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +47 -0
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +24 -0
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +134 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +156 -0
- package/dist/cache/database/index.d.ts.map +1 -0
- package/dist/cache/database/index.js +266 -0
- package/dist/cache/database/index.js.map +1 -0
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +142 -128
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +160 -13
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +106 -7
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +6 -5
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.browser.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/brevo/index.js.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/core/index.workerd.js.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js.map +1 -1
- package/dist/queue/core/index.workerd.js.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/queue/redis/index.js.map +1 -1
- package/dist/react/auth/index.browser.js.map +1 -1
- package/dist/react/auth/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +8 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.js.map +1 -1
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +25 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +12 -0
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +12 -0
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.browser.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/etag/index.js.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/dist/topic/redis/index.js.map +1 -1
- package/package.json +33 -39
- package/src/api/audits/controllers/AdminAuditController.ts +29 -0
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/controllers/FileController.ts +24 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/files/services/FileService.ts +41 -0
- package/src/api/jobs/__tests__/$job.spec.ts +501 -24
- package/src/api/jobs/entities/jobExecutionEntity.ts +4 -3
- package/src/api/jobs/index.ts +47 -10
- package/src/api/jobs/primitives/$job.ts +22 -9
- package/src/api/jobs/providers/DirectJobDispatcher.ts +71 -0
- package/src/api/jobs/providers/JobDispatcher.ts +49 -0
- package/src/api/jobs/providers/JobProvider.ts +385 -147
- package/src/api/jobs/providers/JobQueueProvider.ts +43 -18
- package/src/api/jobs/schemas/jobConfigAtom.ts +9 -3
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +11 -0
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +4 -2
- package/src/api/jobs/services/JobService.ts +21 -11
- package/src/api/keys/controllers/AdminApiKeyController.ts +23 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/keys/services/ApiKeyService.ts +42 -0
- package/src/api/notifications/__tests__/AlephaApiNotifications.spec.ts +63 -0
- package/src/api/notifications/controllers/AdminNotificationController.ts +48 -1
- package/src/api/notifications/index.ts +13 -3
- package/src/api/notifications/jobs/NotificationJobs.ts +0 -6
- package/src/api/parameters/controllers/AdminParameterController.ts +26 -0
- package/src/api/parameters/services/ParameterProvider.ts +18 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- 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 +133 -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__/Registration-emailMode.spec.ts +203 -0
- package/src/api/users/__tests__/UsernameSlugger.spec.ts +138 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +41 -3
- package/src/api/users/controllers/AdminSessionController.ts +29 -0
- package/src/api/users/controllers/AdminUserController.ts +32 -0
- package/src/api/users/index.ts +3 -0
- package/src/api/users/services/CredentialService.ts +5 -0
- package/src/api/users/services/RegistrationService.ts +49 -1
- package/src/api/users/services/SessionCrudService.ts +16 -0
- package/src/api/users/services/SessionService.ts +17 -59
- package/src/api/users/services/UsernameSlugger.ts +195 -0
- package/src/bucket/primitives/$bucket.ts +21 -0
- package/src/bucket/providers/CloudflareR2Provider.ts +15 -0
- package/src/bucket/providers/FileStorageProvider.ts +9 -0
- package/src/bucket/providers/LocalFileStorageProvider.ts +14 -0
- package/src/bucket/providers/MemoryFileStorageProvider.ts +9 -0
- package/src/bucket/providers/NodeS3BucketProvider.ts +35 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +367 -24
- package/src/cache/database/__tests__/DatabaseCacheProvider.behavior.spec.ts +203 -0
- package/src/cache/database/__tests__/DatabaseCacheProvider.spec.ts +110 -0
- package/src/cache/database/entities/cacheEntries.ts +55 -0
- package/src/cache/database/index.ts +36 -0
- package/src/cache/database/providers/DatabaseCacheProvider.ts +348 -0
- package/src/cli/core/services/ProjectScaffolder.ts +0 -2
- package/src/cli/core/tasks/BuildCloudflareTask.ts +33 -3
- package/src/cli/core/tasks/BuildSitemapTask.ts +7 -0
- package/src/cli/core/tasks/BuildVercelTask.ts +82 -3
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/__tests__/detectResources.spec.ts +96 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/commands/platform.ts +7 -1
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/scheduler/index.ts +14 -0
- package/src/scheduler/providers/CronProvider.ts +13 -0
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -844
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1175
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import * as _$alepha from "alepha";
|
|
2
|
+
import { Alepha, Static } from "alepha";
|
|
3
|
+
import { CacheProvider } from "alepha/cache";
|
|
4
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
5
|
+
import * as _$alepha_logger0 from "alepha/logger";
|
|
6
|
+
import * as _$alepha_orm0 from "alepha/orm";
|
|
7
|
+
import * as _$typebox from "typebox";
|
|
8
|
+
|
|
9
|
+
//#region ../../src/cache/database/entities/cacheEntries.d.ts
|
|
10
|
+
/**
|
|
11
|
+
* Storage table for the {@link DatabaseCacheProvider}.
|
|
12
|
+
*
|
|
13
|
+
* Each row represents one cache entry:
|
|
14
|
+
* - `(container, cacheKey)` is the logical key (uniqueness enforced by index).
|
|
15
|
+
* - `value` holds base64-encoded bytes for `set/get/setTyped/getTyped`.
|
|
16
|
+
* - `count` holds an integer counter for atomic `incr` operations.
|
|
17
|
+
* - `expiresAt` is null for entries that never expire, or a timestamp after
|
|
18
|
+
* which the entry is considered gone (filtered out at read time).
|
|
19
|
+
*/
|
|
20
|
+
declare const cacheEntries: _$alepha_orm0.EntityPrimitive<_$typebox.TObject<{
|
|
21
|
+
id: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$typebox.TString, typeof _$alepha_orm0.PG_PRIMARY_KEY>, typeof _$alepha_orm0.PG_DEFAULT>;
|
|
22
|
+
createdAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$typebox.TString, typeof _$alepha_orm0.PG_CREATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
|
|
23
|
+
container: _$typebox.TString;
|
|
24
|
+
cacheKey: _$typebox.TString;
|
|
25
|
+
value: _$typebox.TOptional<_$typebox.TString>;
|
|
26
|
+
count: _$typebox.TOptional<_$typebox.TInteger>;
|
|
27
|
+
expiresAt: _$typebox.TOptional<_$typebox.TString>;
|
|
28
|
+
}>>;
|
|
29
|
+
type CacheEntry = Static<typeof cacheEntries.schema>;
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region ../../src/cache/database/providers/DatabaseCacheProvider.d.ts
|
|
32
|
+
/**
|
|
33
|
+
* Configuration atom for {@link DatabaseCacheProvider}.
|
|
34
|
+
*/
|
|
35
|
+
declare const databaseCacheOptions: _$alepha.Atom<_$typebox.TObject<{
|
|
36
|
+
sweepProbability: _$typebox.TNumber;
|
|
37
|
+
sweepBatchSize: _$typebox.TInteger;
|
|
38
|
+
}>, "alepha.cache.database.options">;
|
|
39
|
+
type DatabaseCacheOptions = Static<typeof databaseCacheOptions.schema>;
|
|
40
|
+
declare module "alepha" {
|
|
41
|
+
interface State {
|
|
42
|
+
[databaseCacheOptions.key]: DatabaseCacheOptions;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Cache provider backed by the application's SQL database.
|
|
47
|
+
*
|
|
48
|
+
* Uses the `cache_entries` table as a generic key/value store with optional
|
|
49
|
+
* TTL. Works on every database supported by Alepha's ORM (Postgres, SQLite,
|
|
50
|
+
* Cloudflare D1, Bun SQLite).
|
|
51
|
+
*
|
|
52
|
+
* **Why use this over Cloudflare KV / Redis ?**
|
|
53
|
+
*
|
|
54
|
+
* - You already have a SQL database, no extra resource to provision/secure.
|
|
55
|
+
* - `incr()` is **atomic** through `INSERT ... ON CONFLICT DO UPDATE`.
|
|
56
|
+
* - Reads/writes are **strongly consistent** (KV is eventually consistent).
|
|
57
|
+
* - Phase-2 flows (registration, password reset) become transactional with
|
|
58
|
+
* the user-creation INSERT.
|
|
59
|
+
*
|
|
60
|
+
* **When to prefer Cloudflare KV / Redis instead ?**
|
|
61
|
+
*
|
|
62
|
+
* - Hot read paths where DB latency matters.
|
|
63
|
+
* - Very high write rates where DB pressure becomes a concern.
|
|
64
|
+
*
|
|
65
|
+
* **Storage layout**
|
|
66
|
+
*
|
|
67
|
+
* - `value` column: base64-encoded bytes for `set/get/setTyped/getTyped`.
|
|
68
|
+
* - `count` column: integer counter for atomic `incr()`.
|
|
69
|
+
* - `expiresAt` column: nullable timestamp; expired rows are filtered at read
|
|
70
|
+
* time and reaped opportunistically on writes.
|
|
71
|
+
*
|
|
72
|
+
* @see {@link CacheProvider}
|
|
73
|
+
*/
|
|
74
|
+
declare class DatabaseCacheProvider extends CacheProvider {
|
|
75
|
+
protected readonly log: _$alepha_logger0.Logger;
|
|
76
|
+
protected readonly alepha: Alepha;
|
|
77
|
+
protected readonly dateTimeProvider: DateTimeProvider;
|
|
78
|
+
protected readonly repository: _$alepha_orm0.Repository<_$typebox.TObject<{
|
|
79
|
+
id: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$typebox.TString, typeof _$alepha_orm0.PG_PRIMARY_KEY>, typeof _$alepha_orm0.PG_DEFAULT>;
|
|
80
|
+
createdAt: _$alepha_orm0.PgAttr<_$alepha_orm0.PgAttr<_$typebox.TString, typeof _$alepha_orm0.PG_CREATED_AT>, typeof _$alepha_orm0.PG_DEFAULT>;
|
|
81
|
+
container: _$typebox.TString;
|
|
82
|
+
cacheKey: _$typebox.TString;
|
|
83
|
+
value: _$typebox.TOptional<_$typebox.TString>;
|
|
84
|
+
count: _$typebox.TOptional<_$typebox.TInteger>;
|
|
85
|
+
expiresAt: _$typebox.TOptional<_$typebox.TString>;
|
|
86
|
+
}>>;
|
|
87
|
+
protected readonly options: Readonly<{
|
|
88
|
+
sweepProbability: number;
|
|
89
|
+
sweepBatchSize: number;
|
|
90
|
+
}>;
|
|
91
|
+
/**
|
|
92
|
+
* Total writes performed since startup. Useful for tests and metrics.
|
|
93
|
+
*/
|
|
94
|
+
writes: number;
|
|
95
|
+
/**
|
|
96
|
+
* Total opportunistic sweep cycles executed. Useful for tests.
|
|
97
|
+
*/
|
|
98
|
+
sweeps: number;
|
|
99
|
+
/**
|
|
100
|
+
* Last error caught while sweeping (sweeps must never throw).
|
|
101
|
+
*/
|
|
102
|
+
lastSweepError?: unknown;
|
|
103
|
+
protected readonly onStart: _$alepha.HookPrimitive<"start">;
|
|
104
|
+
get(name: string, key: string): Promise<Uint8Array | undefined>;
|
|
105
|
+
set(name: string, key: string, value: Uint8Array, ttl?: number): Promise<Uint8Array>;
|
|
106
|
+
del(name: string, ...keys: string[]): Promise<void>;
|
|
107
|
+
has(name: string, key: string): Promise<boolean>;
|
|
108
|
+
keys(name: string, filter?: string): Promise<string[]>;
|
|
109
|
+
clear(): Promise<void>;
|
|
110
|
+
incr(name: string, key: string, amount: number): Promise<number>;
|
|
111
|
+
/**
|
|
112
|
+
* Sweep all expired rows in one shot. Useful for tests and for users who
|
|
113
|
+
* want to schedule their own cleanup job.
|
|
114
|
+
*/
|
|
115
|
+
sweepExpired(): Promise<number>;
|
|
116
|
+
protected unexpiredOrClause(): Record<string, any>;
|
|
117
|
+
protected unexpiredWhere(name: string, key: string): Record<string, any>;
|
|
118
|
+
protected computeExpiresAt(ttl?: number): string | null;
|
|
119
|
+
protected toBase64(value: Uint8Array): string;
|
|
120
|
+
protected fromBase64(value: string): Uint8Array;
|
|
121
|
+
/**
|
|
122
|
+
* Run an opportunistic sweep with `sweepProbability` chance per write.
|
|
123
|
+
*
|
|
124
|
+
* Sweep failures are swallowed: cleanup is best-effort, lazy expiration on
|
|
125
|
+
* read keeps correctness regardless. Errors are logged once on `lastSweepError`
|
|
126
|
+
* so tests can assert on them.
|
|
127
|
+
*/
|
|
128
|
+
protected maybeSweep(): void;
|
|
129
|
+
protected runSweep(): Promise<void>;
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region ../../src/cache/database/index.d.ts
|
|
133
|
+
/**
|
|
134
|
+
* Plugin for Alepha Cache that stores entries in the application's SQL database.
|
|
135
|
+
*
|
|
136
|
+
* Adds a `cache_entries` table and a {@link DatabaseCacheProvider}
|
|
137
|
+
* implementation of {@link CacheProvider} that:
|
|
138
|
+
*
|
|
139
|
+
* - reads/writes through the framework's ORM (Postgres, SQLite, D1, Bun);
|
|
140
|
+
* - exposes an **atomic** `incr()` via `INSERT ... ON CONFLICT DO UPDATE`;
|
|
141
|
+
* - filters expired rows on every read (lazy expiration);
|
|
142
|
+
* - opportunistically sweeps a small batch of expired rows on writes
|
|
143
|
+
* (configurable via {@link databaseCacheOptions}).
|
|
144
|
+
*
|
|
145
|
+
* **Module is opt-in.** Importing this module does not change the default
|
|
146
|
+
* `CacheProvider` binding — pass `provider: DatabaseCacheProvider` explicitly
|
|
147
|
+
* to the relevant `$cache(...)` calls, or rebind globally via
|
|
148
|
+
* `alepha.with({ provide: CacheProvider, use: DatabaseCacheProvider })`.
|
|
149
|
+
*
|
|
150
|
+
* @see {@link DatabaseCacheProvider}
|
|
151
|
+
* @module alepha.cache.database
|
|
152
|
+
*/
|
|
153
|
+
declare const AlephaCacheDatabase: _$alepha.Service<_$alepha.Module>;
|
|
154
|
+
//#endregion
|
|
155
|
+
export { AlephaCacheDatabase, CacheEntry, DatabaseCacheOptions, DatabaseCacheProvider, cacheEntries, databaseCacheOptions };
|
|
156
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/cache/database/entities/cacheEntries.ts","../../../src/cache/database/providers/DatabaseCacheProvider.ts","../../../src/cache/database/index.ts"],"mappings":";;;;;;;;;;;;;;;;;AAaA;;cAAa,YAAA,EAAY,aAAA,CAAA,eAAA,WAAA,OAAA;gDAuCvB,SAAA,CAAA,OAAA;;;;;;;;KAEU,UAAA,GAAa,MAAA,QAAc,YAAA,CAAa,MAAA;;;;;;cC1CvC,oBAAA,EAAoB,QAAA,CAAA,IAAA,WAAA,OAAA;oBAqB/B,SAAA,CAAA,OAAA;;;KAEU,oBAAA,GAAuB,MAAA,QAAc,oBAAA,CAAqB,MAAA;AAAA;EAAA,UAG1D,KAAA;IAAA,CACP,oBAAA,CAAqB,GAAA,GAAM,oBAAA;EAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCnB,qBAAA,SAA8B,aAAA;EAAA,mBACtB,GAAA,EADc,gBAAA,CACX,MAAA;EAAA,mBACH,MAAA,EAAM,MAAA;EAAA,mBACN,gBAAA,EAAgB,gBAAA;EAAA,mBAChB,UAAA,EAAU,aAAA,CAAA,UAAA,WAAA,OAAA;kDADM,SAAA,CAAA,OAAA;;;;;;;;qBAEhB,OAAA,EAAO,QAAA;;;;;ADzB5B;;EC8BS,MAAA;ED9BsB;;;ECmCtB,MAAA;EDnCiD;;;ECwCjD,cAAA;EAAA,mBAIY,OAAA,EAnBO,QAAA,CAmBA,aAAA;EASb,GAAA,CAAI,IAAA,UAAc,GAAA,WAAc,OAAA,CAAQ,UAAA;EA2BxC,GAAA,CACX,IAAA,UACA,GAAA,UACA,KAAA,EAAO,UAAA,EACP,GAAA,YACC,OAAA,CAAQ,UAAA;EA+BE,GAAA,CAAI,IAAA,aAAiB,IAAA,aAAiB,OAAA;EActC,GAAA,CAAI,IAAA,UAAc,GAAA,WAAc,OAAA;EAOhC,IAAA,CAAK,IAAA,UAAc,MAAA,YAAkB,OAAA;EAgBrC,KAAA,CAAA,GAAS,OAAA;EAIT,IAAA,CACX,IAAA,UACA,GAAA,UACA,MAAA,WACC,OAAA;EA3M4B;;;;EAoPlB,YAAA,CAAA,GAAgB,OAAA;EAAA,UAUnB,iBAAA,CAAA,GAAqB,MAAA;EAAA,UAOrB,cAAA,CAAe,IAAA,UAAc,GAAA,WAAc,MAAA;EAAA,UAU3C,gBAAA,CAAiB,GAAA;EAAA,UASjB,QAAA,CAAS,KAAA,EAAO,UAAA;EAAA,UAQhB,UAAA,CAAW,KAAA,WAAgB,UAAA;EAzQ3B;;;;;;;EAAA,UAoRA,UAAA,CAAA;EAAA,UAgBM,QAAA,CAAA,GAAY,OAAA;AAAA;;;;;;;;;;AD1T9B;;;;;;;;;;;;;cEkBa,mBAAA,EAAmB,QAAA,CAAA,OAAA,CAI9B,QAAA,CAJ8B,MAAA"}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { $atom, $hook, $inject, $module, $state, Alepha, t } from "alepha";
|
|
2
|
+
import { AlephaCache, CacheProvider } from "alepha/cache";
|
|
3
|
+
import { DateTimeProvider } from "alepha/datetime";
|
|
4
|
+
import { $logger } from "alepha/logger";
|
|
5
|
+
import { $entity, $repository, db, sql } from "alepha/orm";
|
|
6
|
+
//#region ../../src/cache/database/entities/cacheEntries.ts
|
|
7
|
+
/**
|
|
8
|
+
* Storage table for the {@link DatabaseCacheProvider}.
|
|
9
|
+
*
|
|
10
|
+
* Each row represents one cache entry:
|
|
11
|
+
* - `(container, cacheKey)` is the logical key (uniqueness enforced by index).
|
|
12
|
+
* - `value` holds base64-encoded bytes for `set/get/setTyped/getTyped`.
|
|
13
|
+
* - `count` holds an integer counter for atomic `incr` operations.
|
|
14
|
+
* - `expiresAt` is null for entries that never expire, or a timestamp after
|
|
15
|
+
* which the entry is considered gone (filtered out at read time).
|
|
16
|
+
*/
|
|
17
|
+
const cacheEntries = $entity({
|
|
18
|
+
name: "cache_entries",
|
|
19
|
+
schema: t.object({
|
|
20
|
+
id: db.primaryKey(t.uuid()),
|
|
21
|
+
createdAt: db.createdAt(),
|
|
22
|
+
container: t.text({ description: "Cache container name, set by the $cache primitive." }),
|
|
23
|
+
cacheKey: t.text({ description: "Per-container key chosen by the caller." }),
|
|
24
|
+
value: t.optional(t.string({ description: "Base64-encoded bytes. Used by set/get." })),
|
|
25
|
+
count: t.optional(t.integer({ description: "Counter value. Used by atomic incr()." })),
|
|
26
|
+
expiresAt: t.optional(t.datetime({ description: "Null means no expiration." }))
|
|
27
|
+
}),
|
|
28
|
+
indexes: [{
|
|
29
|
+
columns: ["container", "cacheKey"],
|
|
30
|
+
unique: true
|
|
31
|
+
}, "expiresAt"]
|
|
32
|
+
});
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region ../../src/cache/database/providers/DatabaseCacheProvider.ts
|
|
35
|
+
/**
|
|
36
|
+
* Configuration atom for {@link DatabaseCacheProvider}.
|
|
37
|
+
*/
|
|
38
|
+
const databaseCacheOptions = $atom({
|
|
39
|
+
name: "alepha.cache.database.options",
|
|
40
|
+
schema: t.object({
|
|
41
|
+
sweepProbability: t.number({
|
|
42
|
+
description: "Probability (0..1) that a write operation triggers a sweep of expired rows. Set to 0 to disable opportunistic sweeping.",
|
|
43
|
+
default: .01,
|
|
44
|
+
minimum: 0,
|
|
45
|
+
maximum: 1
|
|
46
|
+
}),
|
|
47
|
+
sweepBatchSize: t.integer({
|
|
48
|
+
description: "Maximum number of expired rows deleted per opportunistic sweep.",
|
|
49
|
+
default: 100,
|
|
50
|
+
minimum: 1
|
|
51
|
+
})
|
|
52
|
+
}),
|
|
53
|
+
default: {
|
|
54
|
+
sweepProbability: .01,
|
|
55
|
+
sweepBatchSize: 100
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
/**
|
|
59
|
+
* Cache provider backed by the application's SQL database.
|
|
60
|
+
*
|
|
61
|
+
* Uses the `cache_entries` table as a generic key/value store with optional
|
|
62
|
+
* TTL. Works on every database supported by Alepha's ORM (Postgres, SQLite,
|
|
63
|
+
* Cloudflare D1, Bun SQLite).
|
|
64
|
+
*
|
|
65
|
+
* **Why use this over Cloudflare KV / Redis ?**
|
|
66
|
+
*
|
|
67
|
+
* - You already have a SQL database, no extra resource to provision/secure.
|
|
68
|
+
* - `incr()` is **atomic** through `INSERT ... ON CONFLICT DO UPDATE`.
|
|
69
|
+
* - Reads/writes are **strongly consistent** (KV is eventually consistent).
|
|
70
|
+
* - Phase-2 flows (registration, password reset) become transactional with
|
|
71
|
+
* the user-creation INSERT.
|
|
72
|
+
*
|
|
73
|
+
* **When to prefer Cloudflare KV / Redis instead ?**
|
|
74
|
+
*
|
|
75
|
+
* - Hot read paths where DB latency matters.
|
|
76
|
+
* - Very high write rates where DB pressure becomes a concern.
|
|
77
|
+
*
|
|
78
|
+
* **Storage layout**
|
|
79
|
+
*
|
|
80
|
+
* - `value` column: base64-encoded bytes for `set/get/setTyped/getTyped`.
|
|
81
|
+
* - `count` column: integer counter for atomic `incr()`.
|
|
82
|
+
* - `expiresAt` column: nullable timestamp; expired rows are filtered at read
|
|
83
|
+
* time and reaped opportunistically on writes.
|
|
84
|
+
*
|
|
85
|
+
* @see {@link CacheProvider}
|
|
86
|
+
*/
|
|
87
|
+
var DatabaseCacheProvider = class extends CacheProvider {
|
|
88
|
+
log = $logger();
|
|
89
|
+
alepha = $inject(Alepha);
|
|
90
|
+
dateTimeProvider = $inject(DateTimeProvider);
|
|
91
|
+
repository = $repository(cacheEntries);
|
|
92
|
+
options = $state(databaseCacheOptions);
|
|
93
|
+
/**
|
|
94
|
+
* Total writes performed since startup. Useful for tests and metrics.
|
|
95
|
+
*/
|
|
96
|
+
writes = 0;
|
|
97
|
+
/**
|
|
98
|
+
* Total opportunistic sweep cycles executed. Useful for tests.
|
|
99
|
+
*/
|
|
100
|
+
sweeps = 0;
|
|
101
|
+
/**
|
|
102
|
+
* Last error caught while sweeping (sweeps must never throw).
|
|
103
|
+
*/
|
|
104
|
+
lastSweepError;
|
|
105
|
+
onStart = $hook({
|
|
106
|
+
on: "start",
|
|
107
|
+
handler: () => {
|
|
108
|
+
this.log.debug("DatabaseCacheProvider ready");
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
async get(name, key) {
|
|
112
|
+
if (!this.alepha.isStarted()) return;
|
|
113
|
+
const row = await this.repository.findOne({ where: this.unexpiredWhere(name, key) });
|
|
114
|
+
if (!row) return;
|
|
115
|
+
if (row.value != null) return this.fromBase64(row.value);
|
|
116
|
+
if (row.count != null) return this.serialize(row.count);
|
|
117
|
+
}
|
|
118
|
+
async set(name, key, value, ttl) {
|
|
119
|
+
if (!this.alepha.isStarted()) return value;
|
|
120
|
+
const expiresAt = this.computeExpiresAt(ttl);
|
|
121
|
+
await this.repository.upsert({
|
|
122
|
+
container: name,
|
|
123
|
+
cacheKey: key,
|
|
124
|
+
value: this.toBase64(value),
|
|
125
|
+
count: null,
|
|
126
|
+
expiresAt
|
|
127
|
+
}, {
|
|
128
|
+
target: ["container", "cacheKey"],
|
|
129
|
+
set: {
|
|
130
|
+
value: this.toBase64(value),
|
|
131
|
+
count: null,
|
|
132
|
+
expiresAt
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
this.writes++;
|
|
136
|
+
this.maybeSweep();
|
|
137
|
+
return value;
|
|
138
|
+
}
|
|
139
|
+
async del(name, ...keys) {
|
|
140
|
+
if (keys.length === 0) {
|
|
141
|
+
await this.repository.deleteMany({ container: { eq: name } });
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
await this.repository.deleteMany({
|
|
145
|
+
container: { eq: name },
|
|
146
|
+
cacheKey: { inArray: keys }
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
async has(name, key) {
|
|
150
|
+
return await this.repository.findOne({ where: this.unexpiredWhere(name, key) }) != null;
|
|
151
|
+
}
|
|
152
|
+
async keys(name, filter) {
|
|
153
|
+
const baseAnd = [{ container: { eq: name } }, this.unexpiredOrClause()];
|
|
154
|
+
if (filter) baseAnd.push({ cacheKey: { startsWith: filter } });
|
|
155
|
+
return (await this.repository.findMany({ where: { and: baseAnd } })).map((row) => row.cacheKey);
|
|
156
|
+
}
|
|
157
|
+
async clear() {
|
|
158
|
+
await this.repository.deleteMany({});
|
|
159
|
+
}
|
|
160
|
+
async incr(name, key, amount) {
|
|
161
|
+
if (!this.alepha.isStarted()) return amount;
|
|
162
|
+
const table = this.repository.table;
|
|
163
|
+
const updated = await this.repository.upsert({
|
|
164
|
+
container: name,
|
|
165
|
+
cacheKey: key,
|
|
166
|
+
count: amount,
|
|
167
|
+
value: null,
|
|
168
|
+
expiresAt: null
|
|
169
|
+
}, {
|
|
170
|
+
target: ["container", "cacheKey"],
|
|
171
|
+
set: {
|
|
172
|
+
count: sql`coalesce(${table.count}, 0) + ${amount}`,
|
|
173
|
+
value: null
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
this.writes++;
|
|
177
|
+
this.maybeSweep();
|
|
178
|
+
return Number(updated.count ?? amount);
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Sweep all expired rows in one shot. Useful for tests and for users who
|
|
182
|
+
* want to schedule their own cleanup job.
|
|
183
|
+
*/
|
|
184
|
+
async sweepExpired() {
|
|
185
|
+
const nowIso = this.dateTimeProvider.nowISOString();
|
|
186
|
+
return (await this.repository.deleteMany({ expiresAt: { lt: nowIso } })).length;
|
|
187
|
+
}
|
|
188
|
+
unexpiredOrClause() {
|
|
189
|
+
return { or: [{ expiresAt: { isNull: true } }, { expiresAt: { gt: this.dateTimeProvider.nowISOString() } }] };
|
|
190
|
+
}
|
|
191
|
+
unexpiredWhere(name, key) {
|
|
192
|
+
return { and: [
|
|
193
|
+
{ container: { eq: name } },
|
|
194
|
+
{ cacheKey: { eq: key } },
|
|
195
|
+
this.unexpiredOrClause()
|
|
196
|
+
] };
|
|
197
|
+
}
|
|
198
|
+
computeExpiresAt(ttl) {
|
|
199
|
+
if (!ttl || ttl <= 0) return null;
|
|
200
|
+
return this.dateTimeProvider.of(this.dateTimeProvider.nowMillis() + ttl).toISOString();
|
|
201
|
+
}
|
|
202
|
+
toBase64(value) {
|
|
203
|
+
return Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString("base64");
|
|
204
|
+
}
|
|
205
|
+
fromBase64(value) {
|
|
206
|
+
return new Uint8Array(Buffer.from(value, "base64"));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Run an opportunistic sweep with `sweepProbability` chance per write.
|
|
210
|
+
*
|
|
211
|
+
* Sweep failures are swallowed: cleanup is best-effort, lazy expiration on
|
|
212
|
+
* read keeps correctness regardless. Errors are logged once on `lastSweepError`
|
|
213
|
+
* so tests can assert on them.
|
|
214
|
+
*/
|
|
215
|
+
maybeSweep() {
|
|
216
|
+
const probability = this.options.sweepProbability;
|
|
217
|
+
if (probability <= 0) return;
|
|
218
|
+
if (Math.random() >= probability) return;
|
|
219
|
+
this.runSweep().catch((err) => {
|
|
220
|
+
this.lastSweepError = err;
|
|
221
|
+
this.log.warn("DatabaseCacheProvider sweep failed", err);
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
async runSweep() {
|
|
225
|
+
this.sweeps++;
|
|
226
|
+
const nowIso = this.dateTimeProvider.nowISOString();
|
|
227
|
+
const expired = await this.repository.findMany({
|
|
228
|
+
where: { expiresAt: { lt: nowIso } },
|
|
229
|
+
columns: ["id"],
|
|
230
|
+
limit: this.options.sweepBatchSize
|
|
231
|
+
});
|
|
232
|
+
if (expired.length === 0) return;
|
|
233
|
+
await this.repository.deleteMany({ id: { inArray: expired.map((it) => it.id) } });
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
//#endregion
|
|
237
|
+
//#region ../../src/cache/database/index.ts
|
|
238
|
+
/**
|
|
239
|
+
* Plugin for Alepha Cache that stores entries in the application's SQL database.
|
|
240
|
+
*
|
|
241
|
+
* Adds a `cache_entries` table and a {@link DatabaseCacheProvider}
|
|
242
|
+
* implementation of {@link CacheProvider} that:
|
|
243
|
+
*
|
|
244
|
+
* - reads/writes through the framework's ORM (Postgres, SQLite, D1, Bun);
|
|
245
|
+
* - exposes an **atomic** `incr()` via `INSERT ... ON CONFLICT DO UPDATE`;
|
|
246
|
+
* - filters expired rows on every read (lazy expiration);
|
|
247
|
+
* - opportunistically sweeps a small batch of expired rows on writes
|
|
248
|
+
* (configurable via {@link databaseCacheOptions}).
|
|
249
|
+
*
|
|
250
|
+
* **Module is opt-in.** Importing this module does not change the default
|
|
251
|
+
* `CacheProvider` binding — pass `provider: DatabaseCacheProvider` explicitly
|
|
252
|
+
* to the relevant `$cache(...)` calls, or rebind globally via
|
|
253
|
+
* `alepha.with({ provide: CacheProvider, use: DatabaseCacheProvider })`.
|
|
254
|
+
*
|
|
255
|
+
* @see {@link DatabaseCacheProvider}
|
|
256
|
+
* @module alepha.cache.database
|
|
257
|
+
*/
|
|
258
|
+
const AlephaCacheDatabase = $module({
|
|
259
|
+
name: "alepha.cache.database",
|
|
260
|
+
services: [DatabaseCacheProvider],
|
|
261
|
+
register: (alepha) => alepha.with(AlephaCache)
|
|
262
|
+
});
|
|
263
|
+
//#endregion
|
|
264
|
+
export { AlephaCacheDatabase, DatabaseCacheProvider, cacheEntries, databaseCacheOptions };
|
|
265
|
+
|
|
266
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cache/database/entities/cacheEntries.ts","../../../src/cache/database/providers/DatabaseCacheProvider.ts","../../../src/cache/database/index.ts"],"sourcesContent":["import { type Static, t } from \"alepha\";\nimport { $entity, db } from \"alepha/orm\";\n\n/**\n * Storage table for the {@link DatabaseCacheProvider}.\n *\n * Each row represents one cache entry:\n * - `(container, cacheKey)` is the logical key (uniqueness enforced by index).\n * - `value` holds base64-encoded bytes for `set/get/setTyped/getTyped`.\n * - `count` holds an integer counter for atomic `incr` operations.\n * - `expiresAt` is null for entries that never expire, or a timestamp after\n * which the entry is considered gone (filtered out at read time).\n */\nexport const cacheEntries = $entity({\n name: \"cache_entries\",\n schema: t.object({\n id: db.primaryKey(t.uuid()),\n\n createdAt: db.createdAt(),\n\n container: t.text({\n description: \"Cache container name, set by the $cache primitive.\",\n }),\n\n cacheKey: t.text({\n description: \"Per-container key chosen by the caller.\",\n }),\n\n value: t.optional(\n // No maxLength: cache values are arbitrary-sized (especially when\n // `compress: true` is enabled on the $cache primitive, which can\n // produce blobs well above the default 255-char `t.text()` cap). This\n // resolves to TEXT in both Postgres and SQLite, which have no\n // practical length limit either.\n t.string({\n description: \"Base64-encoded bytes. Used by set/get.\",\n }),\n ),\n\n count: t.optional(\n t.integer({\n description: \"Counter value. Used by atomic incr().\",\n }),\n ),\n\n expiresAt: t.optional(\n t.datetime({\n description: \"Null means no expiration.\",\n }),\n ),\n }),\n indexes: [{ columns: [\"container\", \"cacheKey\"], unique: true }, \"expiresAt\"],\n});\n\nexport type CacheEntry = Static<typeof cacheEntries.schema>;\n","import { $atom, $hook, $inject, $state, Alepha, type Static, t } from \"alepha\";\nimport { CacheProvider } from \"alepha/cache\";\nimport { DateTimeProvider } from \"alepha/datetime\";\nimport { $logger } from \"alepha/logger\";\nimport { $repository, sql } from \"alepha/orm\";\nimport { cacheEntries } from \"../entities/cacheEntries.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Configuration atom for {@link DatabaseCacheProvider}.\n */\nexport const databaseCacheOptions = $atom({\n name: \"alepha.cache.database.options\",\n schema: t.object({\n sweepProbability: t.number({\n description:\n \"Probability (0..1) that a write operation triggers a sweep of expired rows. Set to 0 to disable opportunistic sweeping.\",\n default: 0.01,\n minimum: 0,\n maximum: 1,\n }),\n sweepBatchSize: t.integer({\n description:\n \"Maximum number of expired rows deleted per opportunistic sweep.\",\n default: 100,\n minimum: 1,\n }),\n }),\n default: {\n sweepProbability: 0.01,\n sweepBatchSize: 100,\n },\n});\n\nexport type DatabaseCacheOptions = Static<typeof databaseCacheOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [databaseCacheOptions.key]: DatabaseCacheOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Cache provider backed by the application's SQL database.\n *\n * Uses the `cache_entries` table as a generic key/value store with optional\n * TTL. Works on every database supported by Alepha's ORM (Postgres, SQLite,\n * Cloudflare D1, Bun SQLite).\n *\n * **Why use this over Cloudflare KV / Redis ?**\n *\n * - You already have a SQL database, no extra resource to provision/secure.\n * - `incr()` is **atomic** through `INSERT ... ON CONFLICT DO UPDATE`.\n * - Reads/writes are **strongly consistent** (KV is eventually consistent).\n * - Phase-2 flows (registration, password reset) become transactional with\n * the user-creation INSERT.\n *\n * **When to prefer Cloudflare KV / Redis instead ?**\n *\n * - Hot read paths where DB latency matters.\n * - Very high write rates where DB pressure becomes a concern.\n *\n * **Storage layout**\n *\n * - `value` column: base64-encoded bytes for `set/get/setTyped/getTyped`.\n * - `count` column: integer counter for atomic `incr()`.\n * - `expiresAt` column: nullable timestamp; expired rows are filtered at read\n * time and reaped opportunistically on writes.\n *\n * @see {@link CacheProvider}\n */\nexport class DatabaseCacheProvider extends CacheProvider {\n protected readonly log = $logger();\n protected readonly alepha = $inject(Alepha);\n protected readonly dateTimeProvider = $inject(DateTimeProvider);\n protected readonly repository = $repository(cacheEntries);\n protected readonly options = $state(databaseCacheOptions);\n\n /**\n * Total writes performed since startup. Useful for tests and metrics.\n */\n public writes = 0;\n\n /**\n * Total opportunistic sweep cycles executed. Useful for tests.\n */\n public sweeps = 0;\n\n /**\n * Last error caught while sweeping (sweeps must never throw).\n */\n public lastSweepError?: unknown;\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected readonly onStart = $hook({\n on: \"start\",\n handler: () => {\n this.log.debug(\"DatabaseCacheProvider ready\");\n },\n });\n\n // -------------------------------------------------------------------------------------------------------------------\n\n public async get(name: string, key: string): Promise<Uint8Array | undefined> {\n if (!this.alepha.isStarted()) {\n return undefined;\n }\n\n const row = await this.repository.findOne({\n where: this.unexpiredWhere(name, key) as any,\n });\n\n if (!row) {\n return undefined;\n }\n\n if (row.value != null) {\n return this.fromBase64(row.value);\n }\n\n if (row.count != null) {\n // The caller wrote the entry through `incr()`. Surface the count via\n // the same byte format that MemoryCacheProvider uses, so `getTyped`\n // returns the number transparently.\n return this.serialize(row.count);\n }\n\n return undefined;\n }\n\n public async set(\n name: string,\n key: string,\n value: Uint8Array,\n ttl?: number,\n ): Promise<Uint8Array> {\n if (!this.alepha.isStarted()) {\n return value;\n }\n\n const expiresAt = this.computeExpiresAt(ttl);\n\n await this.repository.upsert(\n {\n container: name,\n cacheKey: key,\n value: this.toBase64(value),\n count: null,\n expiresAt,\n } as any,\n {\n target: [\"container\", \"cacheKey\"],\n set: {\n value: this.toBase64(value),\n count: null,\n expiresAt,\n },\n },\n );\n\n this.writes++;\n this.maybeSweep();\n\n return value;\n }\n\n public async del(name: string, ...keys: string[]): Promise<void> {\n if (keys.length === 0) {\n await this.repository.deleteMany({\n container: { eq: name },\n });\n return;\n }\n\n await this.repository.deleteMany({\n container: { eq: name },\n cacheKey: { inArray: keys },\n });\n }\n\n public async has(name: string, key: string): Promise<boolean> {\n const row = await this.repository.findOne({\n where: this.unexpiredWhere(name, key) as any,\n });\n return row != null;\n }\n\n public async keys(name: string, filter?: string): Promise<string[]> {\n const baseAnd: any[] = [\n { container: { eq: name } },\n this.unexpiredOrClause(),\n ];\n if (filter) {\n baseAnd.push({ cacheKey: { startsWith: filter } });\n }\n\n const rows = await this.repository.findMany({\n where: { and: baseAnd } as any,\n });\n\n return rows.map((row) => row.cacheKey);\n }\n\n public async clear(): Promise<void> {\n await this.repository.deleteMany({});\n }\n\n public async incr(\n name: string,\n key: string,\n amount: number,\n ): Promise<number> {\n if (!this.alepha.isStarted()) {\n return amount;\n }\n\n // Atomic upsert via `INSERT ... ON CONFLICT (container, cacheKey) DO UPDATE\n // SET count = COALESCE(cache_entries.count, 0) + excluded.count`.\n // Both Postgres and SQLite (incl. D1) support this in a single statement,\n // so concurrent callers can never observe an interleaved read/write.\n const table = this.repository.table;\n\n const updated = await this.repository.upsert(\n {\n container: name,\n cacheKey: key,\n count: amount,\n value: null,\n expiresAt: null,\n } as any,\n {\n target: [\"container\", \"cacheKey\"],\n set: {\n // `excluded.count` references the value being inserted on conflict.\n count: sql`coalesce(${table.count}, 0) + ${amount}`,\n value: null,\n },\n },\n );\n\n this.writes++;\n this.maybeSweep();\n\n return Number(updated.count ?? amount);\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n\n /**\n * Sweep all expired rows in one shot. Useful for tests and for users who\n * want to schedule their own cleanup job.\n */\n public async sweepExpired(): Promise<number> {\n const nowIso = this.dateTimeProvider.nowISOString();\n const ids = await this.repository.deleteMany({\n expiresAt: { lt: nowIso },\n });\n return ids.length;\n }\n\n // -------------------------------------------------------------------------------------------------------------------\n\n protected unexpiredOrClause(): Record<string, any> {\n const nowIso = this.dateTimeProvider.nowISOString();\n return {\n or: [{ expiresAt: { isNull: true } }, { expiresAt: { gt: nowIso } }],\n };\n }\n\n protected unexpiredWhere(name: string, key: string): Record<string, any> {\n return {\n and: [\n { container: { eq: name } },\n { cacheKey: { eq: key } },\n this.unexpiredOrClause(),\n ],\n };\n }\n\n protected computeExpiresAt(ttl?: number): string | null {\n if (!ttl || ttl <= 0) {\n return null;\n }\n return this.dateTimeProvider\n .of(this.dateTimeProvider.nowMillis() + ttl)\n .toISOString();\n }\n\n protected toBase64(value: Uint8Array): string {\n return Buffer.from(\n value.buffer as ArrayBuffer,\n value.byteOffset,\n value.byteLength,\n ).toString(\"base64\");\n }\n\n protected fromBase64(value: string): Uint8Array {\n return new Uint8Array(Buffer.from(value, \"base64\"));\n }\n\n /**\n * Run an opportunistic sweep with `sweepProbability` chance per write.\n *\n * Sweep failures are swallowed: cleanup is best-effort, lazy expiration on\n * read keeps correctness regardless. Errors are logged once on `lastSweepError`\n * so tests can assert on them.\n */\n protected maybeSweep(): void {\n const probability = this.options.sweepProbability;\n if (probability <= 0) {\n return;\n }\n\n if (Math.random() >= probability) {\n return;\n }\n\n this.runSweep().catch((err) => {\n this.lastSweepError = err;\n this.log.warn(\"DatabaseCacheProvider sweep failed\", err);\n });\n }\n\n protected async runSweep(): Promise<void> {\n this.sweeps++;\n const nowIso = this.dateTimeProvider.nowISOString();\n\n // Drizzle does not expose a portable LIMIT on DELETE, so we select first\n // and then delete by ID. Two roundtrips, but only on the ~1% sweep path.\n const expired = await this.repository.findMany({\n where: { expiresAt: { lt: nowIso } },\n columns: [\"id\"],\n limit: this.options.sweepBatchSize,\n });\n\n if (expired.length === 0) {\n return;\n }\n\n await this.repository.deleteMany({\n id: { inArray: expired.map((it) => it.id) },\n });\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache } from \"alepha/cache\";\nimport { DatabaseCacheProvider } from \"./providers/DatabaseCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./entities/cacheEntries.ts\";\nexport * from \"./providers/DatabaseCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Cache that stores entries in the application's SQL database.\n *\n * Adds a `cache_entries` table and a {@link DatabaseCacheProvider}\n * implementation of {@link CacheProvider} that:\n *\n * - reads/writes through the framework's ORM (Postgres, SQLite, D1, Bun);\n * - exposes an **atomic** `incr()` via `INSERT ... ON CONFLICT DO UPDATE`;\n * - filters expired rows on every read (lazy expiration);\n * - opportunistically sweeps a small batch of expired rows on writes\n * (configurable via {@link databaseCacheOptions}).\n *\n * **Module is opt-in.** Importing this module does not change the default\n * `CacheProvider` binding — pass `provider: DatabaseCacheProvider` explicitly\n * to the relevant `$cache(...)` calls, or rebind globally via\n * `alepha.with({ provide: CacheProvider, use: DatabaseCacheProvider })`.\n *\n * @see {@link DatabaseCacheProvider}\n * @module alepha.cache.database\n */\nexport const AlephaCacheDatabase = $module({\n name: \"alepha.cache.database\",\n services: [DatabaseCacheProvider],\n register: (alepha) => alepha.with(AlephaCache),\n});\n"],"mappings":"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"}
|
|
@@ -3,13 +3,14 @@ import { Alepha, Static } from "alepha";
|
|
|
3
3
|
import { CacheProvider } from "alepha/cache";
|
|
4
4
|
import * as _$alepha_logger0 from "alepha/logger";
|
|
5
5
|
import { RedisProvider } from "alepha/redis";
|
|
6
|
+
import * as _$typebox from "typebox";
|
|
6
7
|
|
|
7
8
|
//#region ../../src/cache/redis/providers/RedisCacheProvider.d.ts
|
|
8
9
|
/**
|
|
9
10
|
* Redis cache configuration atom.
|
|
10
11
|
*/
|
|
11
|
-
declare const redisCacheOptions: _$alepha.Atom<_$
|
|
12
|
-
prefix: _$
|
|
12
|
+
declare const redisCacheOptions: _$alepha.Atom<_$typebox.TObject<{
|
|
13
|
+
prefix: _$typebox.TOptional<_$typebox.TString>;
|
|
13
14
|
}>, "alepha.cache.redis.options">;
|
|
14
15
|
type RedisCacheOptions = Static<typeof redisCacheOptions.schema>;
|
|
15
16
|
declare module "alepha" {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/cache/redis/providers/RedisCacheProvider.ts","../../../src/cache/redis/index.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/cache/redis/providers/RedisCacheProvider.ts","../../../src/cache/redis/index.ts"],"mappings":";;;;;;;;;;;cAUa,iBAAA,EAAiB,QAAA,CAAA,IAAA,WAAA,OAAA;8BAW5B,SAAA,CAAA,OAAA;AAAA;AAAA,KAEU,iBAAA,GAAoB,MAAA,QAAc,iBAAA,CAAkB,MAAA;AAAA;EAAA,UAGpD,KAAA;IAAA,CACP,iBAAA,CAAkB,GAAA,GAAM,iBAAA;EAAA;AAAA;AAAA,cAMhB,kBAAA,SAA2B,aAAA;EAAA,mBACnB,GAAA,EADW,gBAAA,CACR,MAAA;EAAA,mBACH,aAAA,EAAa,aAAA;EAAA,mBACb,OAAA,EAAO,QAAA;;;qBACP,MAAA,EAAM,MAAA;EAEZ,GAAA,CAAI,IAAA,UAAc,GAAA,WAAc,OAAA,CAAQ,UAAA;EAmBxC,GAAA,CACX,IAAA,UACA,GAAA,UACA,KAAA,EAAO,UAAA,WACP,GAAA,YACC,OAAA,CAAQ,UAAA;EAmBE,GAAA,CAAI,IAAA,aAAiB,IAAA,aAAiB,OAAA;EAmBtC,GAAA,CAAI,IAAA,UAAc,GAAA,WAAc,OAAA;EAIhC,IAAA,CAAK,IAAA,UAAc,MAAA,YAAkB,OAAA;EAOrC,KAAA,CAAA,GAAS,OAAA;EAST,IAAA,CACX,IAAA,UACA,GAAA,UACA,MAAA,WACC,OAAA;EAAA,UAKO,MAAA,CAAA,GAAU,IAAA;AAAA;;;;;;;;;cClHT,gBAAA,EAAgB,QAAA,CAAA,OAAA,CAW3B,QAAA,CAX2B,MAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cache/redis/providers/RedisCacheProvider.ts","../../../src/cache/redis/index.ts"],"sourcesContent":["import { $atom, $inject, $state, Alepha, type Static, t } from \"alepha\";\nimport { CacheProvider } from \"alepha/cache\";\nimport { $logger } from \"alepha/logger\";\nimport { RedisProvider } from \"alepha/redis\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Redis cache configuration atom.\n */\nexport const redisCacheOptions = $atom({\n name: \"alepha.cache.redis.options\",\n schema: t.object({\n prefix: t.optional(\n t.text({\n description:\n \"Prefix for all cache keys in Redis. Useful for testing or multi-tenant applications.\",\n }),\n ),\n }),\n default: {},\n});\n\nexport type RedisCacheOptions = Static<typeof redisCacheOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [redisCacheOptions.key]: RedisCacheOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class RedisCacheProvider extends CacheProvider {\n protected readonly log = $logger();\n protected readonly redisProvider = $inject(RedisProvider);\n protected readonly options = $state(redisCacheOptions);\n protected readonly alepha = $inject(Alepha);\n\n public async get(name: string, key: string): Promise<Uint8Array | undefined> {\n if (!this.alepha.isStarted()) {\n return;\n }\n\n const keyWithPrefix = this.prefix(name, key);\n const buffer = await this.redisProvider.get(keyWithPrefix);\n if (!buffer) {\n return;\n }\n\n this.log.debug(`Cache hit`, {\n size: buffer.byteLength,\n key: keyWithPrefix,\n });\n\n return new Uint8Array(buffer);\n }\n\n public async set(\n name: string,\n key: string,\n value: Uint8Array | string,\n ttl?: number,\n ): Promise<Uint8Array> {\n if (!this.alepha.isReady()) {\n return new Uint8Array(Buffer.from(value));\n }\n\n const buffer = Buffer.from(value);\n const prefix = this.prefix(name, key);\n\n if (ttl) {\n return new Uint8Array(\n await this.redisProvider.set(prefix, buffer, {\n expiration: { type: \"PX\", value: ttl },\n }),\n );\n }\n\n return new Uint8Array(await this.redisProvider.set(prefix, buffer));\n }\n\n public async del(name: string, ...keys: string[]): Promise<void> {\n const nameKey = this.prefix(name);\n\n if (keys.length === 0) {\n const matched = await this.redisProvider.keys(`${nameKey}:*`);\n if (matched.length > 0) {\n await this.redisProvider.del(matched);\n }\n return;\n }\n\n const prefixed = keys.map((key) =>\n key.startsWith(nameKey) ? key : this.prefix(name, key),\n );\n if (prefixed.length > 0) {\n await this.redisProvider.del(prefixed);\n }\n }\n\n public async has(name: string, key: string): Promise<boolean> {\n return this.redisProvider.has(this.prefix(name, key));\n }\n\n public async keys(name: string, filter?: string): Promise<string[]> {\n if (filter) {\n return await this.redisProvider.keys(`${this.prefix(name)}:${filter}*`);\n }\n return this.redisProvider.keys(`${this.prefix(name)}:*`);\n }\n\n public async clear(): Promise<void> {\n this.log.debug(\"Clearing all cache\");\n const pattern = `${this.prefix()}:*`;\n const keys = await this.redisProvider.keys(pattern);\n if (keys.length > 0) {\n await this.redisProvider.del(keys);\n }\n }\n\n public async incr(\n name: string,\n key: string,\n amount: number,\n ): Promise<number> {\n const keyWithPrefix = this.prefix(name, key);\n return this.redisProvider.incr(keyWithPrefix, amount);\n }\n\n protected prefix(...path: string[]): string {\n const parts = [\"cache\", ...path];\n\n if (this.options.prefix) {\n parts.unshift(this.options.prefix);\n }\n\n return parts.join(\":\");\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache, CacheProvider } from \"alepha/cache\";\nimport { RedisCacheProvider } from \"./providers/RedisCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/RedisCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Cache that provides Redis caching capabilities.\n *\n * @see {@link RedisCacheProvider}\n * @module alepha.cache.redis\n */\nexport const AlephaCacheRedis = $module({\n name: \"alepha.cache.redis\",\n services: [RedisCacheProvider],\n register: (alepha) =>\n alepha\n .with({\n provide: CacheProvider,\n use: RedisCacheProvider,\n optional: true,\n })\n .with(AlephaCache),\n});\n"],"mappings":";;;;;;;;AAUA,MAAa,oBAAoB,MAAM;CACrC,MAAM;CACN,QAAQ,EAAE,OAAO,EACf,QAAQ,EAAE,SACR,EAAE,KAAK,EACL,aACE,wFACH,CAAC,CACH,EACF,CAAC;CACF,SAAS,EAAE;CACZ,CAAC;AAYF,IAAa,qBAAb,cAAwC,cAAc;CACpD,MAAyB,SAAS;CAClC,gBAAmC,QAAQ,cAAc;CACzD,UAA6B,OAAO,kBAAkB;CACtD,SAA4B,QAAQ,OAAO;CAE3C,MAAa,IAAI,MAAc,KAA8C;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cache/redis/providers/RedisCacheProvider.ts","../../../src/cache/redis/index.ts"],"sourcesContent":["import { $atom, $inject, $state, Alepha, type Static, t } from \"alepha\";\nimport { CacheProvider } from \"alepha/cache\";\nimport { $logger } from \"alepha/logger\";\nimport { RedisProvider } from \"alepha/redis\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Redis cache configuration atom.\n */\nexport const redisCacheOptions = $atom({\n name: \"alepha.cache.redis.options\",\n schema: t.object({\n prefix: t.optional(\n t.text({\n description:\n \"Prefix for all cache keys in Redis. Useful for testing or multi-tenant applications.\",\n }),\n ),\n }),\n default: {},\n});\n\nexport type RedisCacheOptions = Static<typeof redisCacheOptions.schema>;\n\ndeclare module \"alepha\" {\n interface State {\n [redisCacheOptions.key]: RedisCacheOptions;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport class RedisCacheProvider extends CacheProvider {\n protected readonly log = $logger();\n protected readonly redisProvider = $inject(RedisProvider);\n protected readonly options = $state(redisCacheOptions);\n protected readonly alepha = $inject(Alepha);\n\n public async get(name: string, key: string): Promise<Uint8Array | undefined> {\n if (!this.alepha.isStarted()) {\n return;\n }\n\n const keyWithPrefix = this.prefix(name, key);\n const buffer = await this.redisProvider.get(keyWithPrefix);\n if (!buffer) {\n return;\n }\n\n this.log.debug(`Cache hit`, {\n size: buffer.byteLength,\n key: keyWithPrefix,\n });\n\n return new Uint8Array(buffer);\n }\n\n public async set(\n name: string,\n key: string,\n value: Uint8Array | string,\n ttl?: number,\n ): Promise<Uint8Array> {\n if (!this.alepha.isReady()) {\n return new Uint8Array(Buffer.from(value));\n }\n\n const buffer = Buffer.from(value);\n const prefix = this.prefix(name, key);\n\n if (ttl) {\n return new Uint8Array(\n await this.redisProvider.set(prefix, buffer, {\n expiration: { type: \"PX\", value: ttl },\n }),\n );\n }\n\n return new Uint8Array(await this.redisProvider.set(prefix, buffer));\n }\n\n public async del(name: string, ...keys: string[]): Promise<void> {\n const nameKey = this.prefix(name);\n\n if (keys.length === 0) {\n const matched = await this.redisProvider.keys(`${nameKey}:*`);\n if (matched.length > 0) {\n await this.redisProvider.del(matched);\n }\n return;\n }\n\n const prefixed = keys.map((key) =>\n key.startsWith(nameKey) ? key : this.prefix(name, key),\n );\n if (prefixed.length > 0) {\n await this.redisProvider.del(prefixed);\n }\n }\n\n public async has(name: string, key: string): Promise<boolean> {\n return this.redisProvider.has(this.prefix(name, key));\n }\n\n public async keys(name: string, filter?: string): Promise<string[]> {\n if (filter) {\n return await this.redisProvider.keys(`${this.prefix(name)}:${filter}*`);\n }\n return this.redisProvider.keys(`${this.prefix(name)}:*`);\n }\n\n public async clear(): Promise<void> {\n this.log.debug(\"Clearing all cache\");\n const pattern = `${this.prefix()}:*`;\n const keys = await this.redisProvider.keys(pattern);\n if (keys.length > 0) {\n await this.redisProvider.del(keys);\n }\n }\n\n public async incr(\n name: string,\n key: string,\n amount: number,\n ): Promise<number> {\n const keyWithPrefix = this.prefix(name, key);\n return this.redisProvider.incr(keyWithPrefix, amount);\n }\n\n protected prefix(...path: string[]): string {\n const parts = [\"cache\", ...path];\n\n if (this.options.prefix) {\n parts.unshift(this.options.prefix);\n }\n\n return parts.join(\":\");\n }\n}\n","import { $module } from \"alepha\";\nimport { AlephaCache, CacheProvider } from \"alepha/cache\";\nimport { RedisCacheProvider } from \"./providers/RedisCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/RedisCacheProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Plugin for Alepha Cache that provides Redis caching capabilities.\n *\n * @see {@link RedisCacheProvider}\n * @module alepha.cache.redis\n */\nexport const AlephaCacheRedis = $module({\n name: \"alepha.cache.redis\",\n services: [RedisCacheProvider],\n register: (alepha) =>\n alepha\n .with({\n provide: CacheProvider,\n use: RedisCacheProvider,\n optional: true,\n })\n .with(AlephaCache),\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/captcha/providers/CaptchaProvider.ts","../../src/captcha/providers/MemoryCaptchaProvider.ts","../../src/captcha/providers/TurnstileCaptchaProvider.ts","../../src/captcha/index.ts"],"sourcesContent":["/**\n * Captcha verification provider interface.\n *\n * Verifies that a user-submitted captcha token is valid. Implementations\n * call the relevant captcha service (Turnstile, reCAPTCHA, hCaptcha, etc.)\n * to validate the token server-side.\n */\nexport abstract class CaptchaProvider {\n /**\n * Verify a captcha token.\n *\n * @param token - The captcha response token submitted by the client.\n * @param ip - Optional client IP address for additional validation.\n * @returns Whether the token is valid.\n */\n public abstract verify(token: string, ip?: string): Promise<boolean>;\n\n /**\n * Public site/widget key to hand to the browser, when applicable.\n *\n * Returns `undefined` for providers that don't need a client key\n * (e.g. in-memory/test providers).\n */\n public getSiteKey(): string | undefined {\n return undefined;\n }\n}\n","import { $logger } from \"alepha/logger\";\nimport type { CaptchaProvider } from \"./CaptchaProvider.ts\";\n\nexport interface CaptchaRecord {\n token: string;\n ip?: string;\n verifiedAt: Date;\n}\n\n/**\n * In-memory captcha provider for testing.\n *\n * Accepts all tokens by default. Use `reject()` to make verification fail,\n * and `accept()` to restore. All verification attempts are recorded for assertions.\n */\nexport class MemoryCaptchaProvider implements CaptchaProvider {\n protected readonly log = $logger();\n\n /**\n * All verification attempts.\n */\n public records: CaptchaRecord[] = [];\n\n protected shouldAccept = true;\n\n public getSiteKey(): string | undefined {\n return undefined;\n }\n\n public async verify(token: string, ip?: string): Promise<boolean> {\n this.log.debug(\"Verifying captcha in memory store\", { token, ip });\n\n this.records.push({\n token,\n ip,\n verifiedAt: new Date(),\n });\n\n return this.shouldAccept;\n }\n\n /**\n * Make all subsequent verifications fail.\n */\n public reject(): void {\n this.shouldAccept = false;\n }\n\n /**\n * Make all subsequent verifications pass (default behavior).\n */\n public accept(): void {\n this.shouldAccept = true;\n }\n\n /**\n * Whether a token was verified.\n */\n public wasVerified(token: string): boolean {\n return this.records.some((r) => r.token === token);\n }\n\n /**\n * Get the last verification attempt.\n */\n public get last(): CaptchaRecord | undefined {\n return this.records[this.records.length - 1];\n }\n}\n","import { $context, AlephaError, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport type { CaptchaProvider } from \"./CaptchaProvider.ts\";\n\n/**\n * Cloudflare Turnstile captcha verification provider.\n *\n * Validates captcha tokens against the Cloudflare Turnstile siteverify API.\n * Free, privacy-friendly, and supports invisible mode.\n *\n * ## Setup\n *\n * 1. Create a Turnstile widget at https://dash.cloudflare.com/?to=/:account/turnstile\n * 2. Copy the **Site Key** (public, for the client) and **Secret Key** (private, for the server)\n * 3. Set `TURNSTILE_SECRET_KEY` in your environment\n *\n * ## Client-side integration\n *\n * Add the Turnstile script and widget to your form:\n *\n * ```html\n * <script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n * <form>\n * <div class=\"cf-turnstile\" data-sitekey=\"YOUR_SITE_KEY\"></div>\n * <button type=\"submit\">Submit</button>\n * </form>\n * ```\n *\n * The widget injects a hidden `cf-turnstile-response` input into the form.\n * Send this value as the `captchaToken` in your registration request.\n *\n * For explicit rendering (React, SPA):\n *\n * ```ts\n * turnstile.render(\"#container\", {\n * sitekey: \"YOUR_SITE_KEY\",\n * callback: (token) => setCaptchaToken(token),\n * });\n * ```\n *\n * ## Server-side usage\n *\n * Register the provider in your app:\n *\n * ```ts\n * import { CaptchaProvider } from \"alepha/captcha\";\n * import { TurnstileCaptchaProvider } from \"alepha/captcha\";\n *\n * alepha.with({ provide: CaptchaProvider, use: TurnstileCaptchaProvider });\n * ```\n *\n * ## Test keys (for development)\n *\n * - Always passes: site `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`\n * - Always blocks: site `2x00000000000000000000AB`, secret `2x0000000000000000000000000000000AB`\n * - Forces interactive: site `3x00000000000000000000FF`\n *\n * ## Environment Variables\n *\n * - `TURNSTILE_SECRET_KEY`: The secret key from the Cloudflare Turnstile dashboard.\n *\n * @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/\n */\nexport class TurnstileCaptchaProvider implements CaptchaProvider {\n protected readonly log = $logger();\n protected readonly secretKey: string;\n protected readonly siteKey: string;\n\n constructor() {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n TURNSTILE_SECRET_KEY: t.text({\n description:\n \"The secret key from the Cloudflare Turnstile dashboard.\",\n }),\n TURNSTILE_SITE_KEY: t.text({\n description:\n \"The public site key from the Cloudflare Turnstile dashboard, rendered on the client.\",\n }),\n }),\n );\n\n this.secretKey = env.TURNSTILE_SECRET_KEY;\n this.siteKey = env.TURNSTILE_SITE_KEY;\n }\n\n public getSiteKey(): string {\n return this.siteKey;\n }\n\n public async verify(token: string, ip?: string): Promise<boolean> {\n const body = new URLSearchParams();\n body.set(\"secret\", this.secretKey);\n body.set(\"response\", token);\n\n if (ip) {\n body.set(\"remoteip\", ip);\n }\n\n try {\n const res = await fetch(\n \"https://challenges.cloudflare.com/turnstile/v0/siteverify\",\n {\n method: \"POST\",\n body,\n },\n );\n\n const data = (await res.json()) as TurnstileResponse;\n\n if (!data.success) {\n this.log.debug(\"Turnstile verification failed\", {\n errorCodes: data[\"error-codes\"],\n });\n }\n\n return data.success;\n } catch (error) {\n throw new AlephaError(\"Failed to verify Turnstile captcha token\", {\n cause: error,\n });\n }\n }\n}\n\ninterface TurnstileResponse {\n success: boolean;\n \"error-codes\"?: string[];\n challenge_ts?: string;\n hostname?: string;\n action?: string;\n cdata?: string;\n}\n","import { $module } from \"alepha\";\nimport { CaptchaProvider } from \"./providers/CaptchaProvider.ts\";\nimport { MemoryCaptchaProvider } from \"./providers/MemoryCaptchaProvider.ts\";\nimport { TurnstileCaptchaProvider } from \"./providers/TurnstileCaptchaProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/CaptchaProvider.ts\";\nexport * from \"./providers/MemoryCaptchaProvider.ts\";\nexport * from \"./providers/TurnstileCaptchaProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Captcha verification for bot protection.\n *\n * **Features:**\n * - Provider abstraction for captcha services\n * - Cloudflare Turnstile support (free, privacy-friendly)\n * - In-memory provider for testing\n *\n * @module alepha.captcha\n */\nexport const AlephaCaptcha = $module({\n name: \"alepha.captcha\",\n services: [CaptchaProvider],\n variants: [MemoryCaptchaProvider, TurnstileCaptchaProvider],\n register: (alepha) =>\n alepha.with({\n optional: true,\n provide: CaptchaProvider,\n use: MemoryCaptchaProvider,\n }),\n});\n"],"mappings":";;;;;;;;;;AAOA,IAAsB,kBAAtB,MAAsC;;;;;;;CAgBpC,aAAwC;;;;;;;;;;ACR1C,IAAa,wBAAb,MAA8D;CAC5D,MAAyB,SAAS;;;;CAKlC,UAAkC,EAAE;CAEpC,eAAyB;CAEzB,aAAwC;CAIxC,MAAa,OAAO,OAAe,IAA+B;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/captcha/providers/CaptchaProvider.ts","../../src/captcha/providers/MemoryCaptchaProvider.ts","../../src/captcha/providers/TurnstileCaptchaProvider.ts","../../src/captcha/index.ts"],"sourcesContent":["/**\n * Captcha verification provider interface.\n *\n * Verifies that a user-submitted captcha token is valid. Implementations\n * call the relevant captcha service (Turnstile, reCAPTCHA, hCaptcha, etc.)\n * to validate the token server-side.\n */\nexport abstract class CaptchaProvider {\n /**\n * Verify a captcha token.\n *\n * @param token - The captcha response token submitted by the client.\n * @param ip - Optional client IP address for additional validation.\n * @returns Whether the token is valid.\n */\n public abstract verify(token: string, ip?: string): Promise<boolean>;\n\n /**\n * Public site/widget key to hand to the browser, when applicable.\n *\n * Returns `undefined` for providers that don't need a client key\n * (e.g. in-memory/test providers).\n */\n public getSiteKey(): string | undefined {\n return undefined;\n }\n}\n","import { $logger } from \"alepha/logger\";\nimport type { CaptchaProvider } from \"./CaptchaProvider.ts\";\n\nexport interface CaptchaRecord {\n token: string;\n ip?: string;\n verifiedAt: Date;\n}\n\n/**\n * In-memory captcha provider for testing.\n *\n * Accepts all tokens by default. Use `reject()` to make verification fail,\n * and `accept()` to restore. All verification attempts are recorded for assertions.\n */\nexport class MemoryCaptchaProvider implements CaptchaProvider {\n protected readonly log = $logger();\n\n /**\n * All verification attempts.\n */\n public records: CaptchaRecord[] = [];\n\n protected shouldAccept = true;\n\n public getSiteKey(): string | undefined {\n return undefined;\n }\n\n public async verify(token: string, ip?: string): Promise<boolean> {\n this.log.debug(\"Verifying captcha in memory store\", { token, ip });\n\n this.records.push({\n token,\n ip,\n verifiedAt: new Date(),\n });\n\n return this.shouldAccept;\n }\n\n /**\n * Make all subsequent verifications fail.\n */\n public reject(): void {\n this.shouldAccept = false;\n }\n\n /**\n * Make all subsequent verifications pass (default behavior).\n */\n public accept(): void {\n this.shouldAccept = true;\n }\n\n /**\n * Whether a token was verified.\n */\n public wasVerified(token: string): boolean {\n return this.records.some((r) => r.token === token);\n }\n\n /**\n * Get the last verification attempt.\n */\n public get last(): CaptchaRecord | undefined {\n return this.records[this.records.length - 1];\n }\n}\n","import { $context, AlephaError, t } from \"alepha\";\nimport { $logger } from \"alepha/logger\";\nimport type { CaptchaProvider } from \"./CaptchaProvider.ts\";\n\n/**\n * Cloudflare Turnstile captcha verification provider.\n *\n * Validates captcha tokens against the Cloudflare Turnstile siteverify API.\n * Free, privacy-friendly, and supports invisible mode.\n *\n * ## Setup\n *\n * 1. Create a Turnstile widget at https://dash.cloudflare.com/?to=/:account/turnstile\n * 2. Copy the **Site Key** (public, for the client) and **Secret Key** (private, for the server)\n * 3. Set `TURNSTILE_SECRET_KEY` in your environment\n *\n * ## Client-side integration\n *\n * Add the Turnstile script and widget to your form:\n *\n * ```html\n * <script src=\"https://challenges.cloudflare.com/turnstile/v0/api.js\" async defer></script>\n * <form>\n * <div class=\"cf-turnstile\" data-sitekey=\"YOUR_SITE_KEY\"></div>\n * <button type=\"submit\">Submit</button>\n * </form>\n * ```\n *\n * The widget injects a hidden `cf-turnstile-response` input into the form.\n * Send this value as the `captchaToken` in your registration request.\n *\n * For explicit rendering (React, SPA):\n *\n * ```ts\n * turnstile.render(\"#container\", {\n * sitekey: \"YOUR_SITE_KEY\",\n * callback: (token) => setCaptchaToken(token),\n * });\n * ```\n *\n * ## Server-side usage\n *\n * Register the provider in your app:\n *\n * ```ts\n * import { CaptchaProvider } from \"alepha/captcha\";\n * import { TurnstileCaptchaProvider } from \"alepha/captcha\";\n *\n * alepha.with({ provide: CaptchaProvider, use: TurnstileCaptchaProvider });\n * ```\n *\n * ## Test keys (for development)\n *\n * - Always passes: site `1x00000000000000000000AA`, secret `1x0000000000000000000000000000000AA`\n * - Always blocks: site `2x00000000000000000000AB`, secret `2x0000000000000000000000000000000AB`\n * - Forces interactive: site `3x00000000000000000000FF`\n *\n * ## Environment Variables\n *\n * - `TURNSTILE_SECRET_KEY`: The secret key from the Cloudflare Turnstile dashboard.\n *\n * @see https://developers.cloudflare.com/turnstile/get-started/server-side-validation/\n */\nexport class TurnstileCaptchaProvider implements CaptchaProvider {\n protected readonly log = $logger();\n protected readonly secretKey: string;\n protected readonly siteKey: string;\n\n constructor() {\n const { alepha } = $context();\n\n const env = alepha.parseEnv(\n t.object({\n TURNSTILE_SECRET_KEY: t.text({\n description:\n \"The secret key from the Cloudflare Turnstile dashboard.\",\n }),\n TURNSTILE_SITE_KEY: t.text({\n description:\n \"The public site key from the Cloudflare Turnstile dashboard, rendered on the client.\",\n }),\n }),\n );\n\n this.secretKey = env.TURNSTILE_SECRET_KEY;\n this.siteKey = env.TURNSTILE_SITE_KEY;\n }\n\n public getSiteKey(): string {\n return this.siteKey;\n }\n\n public async verify(token: string, ip?: string): Promise<boolean> {\n const body = new URLSearchParams();\n body.set(\"secret\", this.secretKey);\n body.set(\"response\", token);\n\n if (ip) {\n body.set(\"remoteip\", ip);\n }\n\n try {\n const res = await fetch(\n \"https://challenges.cloudflare.com/turnstile/v0/siteverify\",\n {\n method: \"POST\",\n body,\n },\n );\n\n const data = (await res.json()) as TurnstileResponse;\n\n if (!data.success) {\n this.log.debug(\"Turnstile verification failed\", {\n errorCodes: data[\"error-codes\"],\n });\n }\n\n return data.success;\n } catch (error) {\n throw new AlephaError(\"Failed to verify Turnstile captcha token\", {\n cause: error,\n });\n }\n }\n}\n\ninterface TurnstileResponse {\n success: boolean;\n \"error-codes\"?: string[];\n challenge_ts?: string;\n hostname?: string;\n action?: string;\n cdata?: string;\n}\n","import { $module } from \"alepha\";\nimport { CaptchaProvider } from \"./providers/CaptchaProvider.ts\";\nimport { MemoryCaptchaProvider } from \"./providers/MemoryCaptchaProvider.ts\";\nimport { TurnstileCaptchaProvider } from \"./providers/TurnstileCaptchaProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./providers/CaptchaProvider.ts\";\nexport * from \"./providers/MemoryCaptchaProvider.ts\";\nexport * from \"./providers/TurnstileCaptchaProvider.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Captcha verification for bot protection.\n *\n * **Features:**\n * - Provider abstraction for captcha services\n * - Cloudflare Turnstile support (free, privacy-friendly)\n * - In-memory provider for testing\n *\n * @module alepha.captcha\n */\nexport const AlephaCaptcha = $module({\n name: \"alepha.captcha\",\n services: [CaptchaProvider],\n variants: [MemoryCaptchaProvider, TurnstileCaptchaProvider],\n register: (alepha) =>\n alepha.with({\n optional: true,\n provide: CaptchaProvider,\n use: MemoryCaptchaProvider,\n }),\n});\n"],"mappings":";;;;;;;;;;AAOA,IAAsB,kBAAtB,MAAsC;;;;;;;CAgBpC,aAAwC;;;;;;;;;;ACR1C,IAAa,wBAAb,MAA8D;CAC5D,MAAyB,SAAS;;;;CAKlC,UAAkC,EAAE;CAEpC,eAAyB;CAEzB,aAAwC;CAIxC,MAAa,OAAO,OAAe,IAA+B;EAChE,KAAK,IAAI,MAAM,qCAAqC;GAAE;GAAO;GAAI,CAAC;EAElE,KAAK,QAAQ,KAAK;GAChB;GACA;GACA,4BAAY,IAAI,MAAM;GACvB,CAAC;EAEF,OAAO,KAAK;;;;;CAMd,SAAsB;EACpB,KAAK,eAAe;;;;;CAMtB,SAAsB;EACpB,KAAK,eAAe;;;;;CAMtB,YAAmB,OAAwB;EACzC,OAAO,KAAK,QAAQ,MAAM,MAAM,EAAE,UAAU,MAAM;;;;;CAMpD,IAAW,OAAkC;EAC3C,OAAO,KAAK,QAAQ,KAAK,QAAQ,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACH9C,IAAa,2BAAb,MAAiE;CAC/D,MAAyB,SAAS;CAClC;CACA;CAEA,cAAc;EACZ,MAAM,EAAE,WAAW,UAAU;EAE7B,MAAM,MAAM,OAAO,SACjB,EAAE,OAAO;GACP,sBAAsB,EAAE,KAAK,EAC3B,aACE,2DACH,CAAC;GACF,oBAAoB,EAAE,KAAK,EACzB,aACE,wFACH,CAAC;GACH,CAAC,CACH;EAED,KAAK,YAAY,IAAI;EACrB,KAAK,UAAU,IAAI;;CAGrB,aAA4B;EAC1B,OAAO,KAAK;;CAGd,MAAa,OAAO,OAAe,IAA+B;EAChE,MAAM,OAAO,IAAI,iBAAiB;EAClC,KAAK,IAAI,UAAU,KAAK,UAAU;EAClC,KAAK,IAAI,YAAY,MAAM;EAE3B,IAAI,IACF,KAAK,IAAI,YAAY,GAAG;EAG1B,IAAI;GASF,MAAM,OAAQ,OAAM,MARF,MAChB,6DACA;IACE,QAAQ;IACR;IACD,CACF,EAEuB,MAAM;GAE9B,IAAI,CAAC,KAAK,SACR,KAAK,IAAI,MAAM,iCAAiC,EAC9C,YAAY,KAAK,gBAClB,CAAC;GAGJ,OAAO,KAAK;WACL,OAAO;GACd,MAAM,IAAI,YAAY,4CAA4C,EAChE,OAAO,OACR,CAAC;;;;;;;;;;;;;;;;ACnGR,MAAa,gBAAgB,QAAQ;CACnC,MAAM;CACN,UAAU,CAAC,gBAAgB;CAC3B,UAAU,CAAC,uBAAuB,yBAAyB;CAC3D,WAAW,WACT,OAAO,KAAK;EACV,UAAU;EACV,SAAS;EACT,KAAK;EACN,CAAC;CACL,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cli/config/defineConfig.ts"],"sourcesContent":["import type { Alepha, Service } from \"alepha\";\nimport {\n type AppEntryOptions,\n appEntryOptions,\n type BuildOptions,\n buildOptions,\n type DevOptions,\n devOptions,\n} from \"alepha/cli\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface AlephaCliConfig {\n /**\n * Override entry paths.\n */\n entry?: AppEntryOptions;\n\n /**\n * Register more services to the Alepha CLI (enhancements, commands, etc.).\n */\n services?: Array<Service>;\n\n /**\n * @alias services Register more services to the Alepha CLI (enhancements, commands, etc.).\n */\n plugins?: Array<Service>;\n\n /**\n * Configure Alepha build command.\n */\n build?: BuildOptions;\n\n /**\n * Configure Alepha dev command.\n */\n dev?: DevOptions;\n\n /**\n * Environment variables to set before running commands.\n *\n * Always use .env files by default, this is only for dynamic values.\n */\n env?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const defineConfig = (config: AlephaCliConfig) => {\n return (alepha: Alepha) => {\n if (config.services) {\n for (const it of config.services) {\n alepha.with(it);\n }\n }\n\n if (config.plugins) {\n for (const it of config.plugins) {\n alepha.with(it);\n }\n }\n\n if (config.env) {\n for (const [key, value] of Object.entries(config.env)) {\n process.env[key] = String(value);\n }\n }\n\n if (config.build) {\n alepha.set(buildOptions, config.build);\n }\n\n if (config.dev) {\n alepha.set(devOptions, config.dev);\n }\n\n if (config.entry) {\n alepha.set(appEntryOptions, config.entry);\n }\n\n return {};\n };\n};\n"],"mappings":";;AAgDA,MAAa,gBAAgB,WAA4B;
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/cli/config/defineConfig.ts"],"sourcesContent":["import type { Alepha, Service } from \"alepha\";\nimport {\n type AppEntryOptions,\n appEntryOptions,\n type BuildOptions,\n buildOptions,\n type DevOptions,\n devOptions,\n} from \"alepha/cli\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport interface AlephaCliConfig {\n /**\n * Override entry paths.\n */\n entry?: AppEntryOptions;\n\n /**\n * Register more services to the Alepha CLI (enhancements, commands, etc.).\n */\n services?: Array<Service>;\n\n /**\n * @alias services Register more services to the Alepha CLI (enhancements, commands, etc.).\n */\n plugins?: Array<Service>;\n\n /**\n * Configure Alepha build command.\n */\n build?: BuildOptions;\n\n /**\n * Configure Alepha dev command.\n */\n dev?: DevOptions;\n\n /**\n * Environment variables to set before running commands.\n *\n * Always use .env files by default, this is only for dynamic values.\n */\n env?: Record<string, unknown>;\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport const defineConfig = (config: AlephaCliConfig) => {\n return (alepha: Alepha) => {\n if (config.services) {\n for (const it of config.services) {\n alepha.with(it);\n }\n }\n\n if (config.plugins) {\n for (const it of config.plugins) {\n alepha.with(it);\n }\n }\n\n if (config.env) {\n for (const [key, value] of Object.entries(config.env)) {\n process.env[key] = String(value);\n }\n }\n\n if (config.build) {\n alepha.set(buildOptions, config.build);\n }\n\n if (config.dev) {\n alepha.set(devOptions, config.dev);\n }\n\n if (config.entry) {\n alepha.set(appEntryOptions, config.entry);\n }\n\n return {};\n };\n};\n"],"mappings":";;AAgDA,MAAa,gBAAgB,WAA4B;CACvD,QAAQ,WAAmB;EACzB,IAAI,OAAO,UACT,KAAK,MAAM,MAAM,OAAO,UACtB,OAAO,KAAK,GAAG;EAInB,IAAI,OAAO,SACT,KAAK,MAAM,MAAM,OAAO,SACtB,OAAO,KAAK,GAAG;EAInB,IAAI,OAAO,KACT,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,IAAI,EACnD,QAAQ,IAAI,OAAO,OAAO,MAAM;EAIpC,IAAI,OAAO,OACT,OAAO,IAAI,cAAc,OAAO,MAAM;EAGxC,IAAI,OAAO,KACT,OAAO,IAAI,YAAY,OAAO,IAAI;EAGpC,IAAI,OAAO,OACT,OAAO,IAAI,iBAAiB,OAAO,MAAM;EAG3C,OAAO,EAAE"}
|