@pithy-sh/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/address/address.d.ts +57 -0
- package/dist/address/address.d.ts.map +1 -0
- package/dist/address/address.js +71 -0
- package/dist/audit/auditEvent.d.ts +89 -0
- package/dist/audit/auditEvent.d.ts.map +1 -0
- package/dist/audit/auditEvent.js +69 -0
- package/dist/audit/recorder.d.ts +18 -0
- package/dist/audit/recorder.d.ts.map +1 -0
- package/dist/audit/recorder.js +8 -0
- package/dist/capability/bindings.d.ts +62 -0
- package/dist/capability/bindings.d.ts.map +1 -0
- package/dist/capability/bindings.js +97 -0
- package/dist/capability/capability.d.ts +501 -0
- package/dist/capability/capability.d.ts.map +1 -0
- package/dist/capability/capability.js +16 -0
- package/dist/capability/client.d.ts +70 -0
- package/dist/capability/client.d.ts.map +1 -0
- package/dist/capability/client.js +86 -0
- package/dist/capability/compose.d.ts +25 -0
- package/dist/capability/compose.d.ts.map +1 -0
- package/dist/capability/compose.js +54 -0
- package/dist/capability/composition.d.ts +26 -0
- package/dist/capability/composition.d.ts.map +1 -0
- package/dist/capability/composition.js +84 -0
- package/dist/capability/config.d.ts +7 -0
- package/dist/capability/config.d.ts.map +1 -0
- package/dist/capability/config.js +14 -0
- package/dist/capability/devSecret.d.ts +32 -0
- package/dist/capability/devSecret.d.ts.map +1 -0
- package/dist/capability/devSecret.js +28 -0
- package/dist/capability/manifest.d.ts +298 -0
- package/dist/capability/manifest.d.ts.map +1 -0
- package/dist/capability/manifest.js +356 -0
- package/dist/capability/secretOrigin.d.ts +121 -0
- package/dist/capability/secretOrigin.d.ts.map +1 -0
- package/dist/capability/secretOrigin.js +134 -0
- package/dist/capability/settings.d.ts +119 -0
- package/dist/capability/settings.d.ts.map +1 -0
- package/dist/capability/settings.js +66 -0
- package/dist/capability/validateBindings.d.ts +4 -0
- package/dist/capability/validateBindings.d.ts.map +1 -0
- package/dist/capability/validateBindings.js +34 -0
- package/dist/capability/vanishingKey.d.ts +31 -0
- package/dist/capability/vanishingKey.d.ts.map +1 -0
- package/dist/capability/vanishingKey.js +79 -0
- package/dist/controlPlane/audit/actions.d.ts +70 -0
- package/dist/controlPlane/audit/actions.d.ts.map +1 -0
- package/dist/controlPlane/audit/actions.js +76 -0
- package/dist/controlPlane/capability.d.ts +47 -0
- package/dist/controlPlane/capability.d.ts.map +1 -0
- package/dist/controlPlane/capability.js +138 -0
- package/dist/controlPlane/config/config.d.ts +19 -0
- package/dist/controlPlane/config/config.d.ts.map +1 -0
- package/dist/controlPlane/config/config.js +85 -0
- package/dist/controlPlane/context.d.ts +27 -0
- package/dist/controlPlane/context.d.ts.map +1 -0
- package/dist/controlPlane/context.js +29 -0
- package/dist/controlPlane/data/connection.d.ts +79 -0
- package/dist/controlPlane/data/connection.d.ts.map +1 -0
- package/dist/controlPlane/data/connection.js +55 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts +62 -0
- package/dist/controlPlane/data/keyLifecycle.d.ts.map +1 -0
- package/dist/controlPlane/data/keyLifecycle.js +126 -0
- package/dist/controlPlane/data/replay.d.ts +18 -0
- package/dist/controlPlane/data/replay.d.ts.map +1 -0
- package/dist/controlPlane/data/replay.js +20 -0
- package/dist/controlPlane/data/tables.d.ts +34 -0
- package/dist/controlPlane/data/tables.d.ts.map +1 -0
- package/dist/controlPlane/data/tables.js +37 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts +313 -0
- package/dist/controlPlane/discovery/adminRoute.d.ts.map +1 -0
- package/dist/controlPlane/discovery/adminRoute.js +136 -0
- package/dist/controlPlane/discovery/configuration.d.ts +108 -0
- package/dist/controlPlane/discovery/configuration.d.ts.map +1 -0
- package/dist/controlPlane/discovery/configuration.js +187 -0
- package/dist/controlPlane/discovery/drift.d.ts +64 -0
- package/dist/controlPlane/discovery/drift.d.ts.map +1 -0
- package/dist/controlPlane/discovery/drift.js +58 -0
- package/dist/controlPlane/discovery/health.d.ts +99 -0
- package/dist/controlPlane/discovery/health.d.ts.map +1 -0
- package/dist/controlPlane/discovery/health.js +151 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts +236 -0
- package/dist/controlPlane/discovery/healthSummary.d.ts.map +1 -0
- package/dist/controlPlane/discovery/healthSummary.js +278 -0
- package/dist/controlPlane/error/errors.d.ts +67 -0
- package/dist/controlPlane/error/errors.d.ts.map +1 -0
- package/dist/controlPlane/error/errors.js +79 -0
- package/dist/controlPlane/http/cors.d.ts +112 -0
- package/dist/controlPlane/http/cors.d.ts.map +1 -0
- package/dist/controlPlane/http/cors.js +192 -0
- package/dist/controlPlane/http/guard.d.ts +77 -0
- package/dist/controlPlane/http/guard.d.ts.map +1 -0
- package/dist/controlPlane/http/guard.js +142 -0
- package/dist/controlPlane/http/handlers.d.ts +179 -0
- package/dist/controlPlane/http/handlers.d.ts.map +1 -0
- package/dist/controlPlane/http/handlers.js +245 -0
- package/dist/controlPlane/http/responses.d.ts +99 -0
- package/dist/controlPlane/http/responses.d.ts.map +1 -0
- package/dist/controlPlane/http/responses.js +63 -0
- package/dist/controlPlane/http/routes.d.ts +40 -0
- package/dist/controlPlane/http/routes.d.ts.map +1 -0
- package/dist/controlPlane/http/routes.js +83 -0
- package/dist/controlPlane/http/schemas.d.ts +39 -0
- package/dist/controlPlane/http/schemas.d.ts.map +1 -0
- package/dist/controlPlane/http/schemas.js +31 -0
- package/dist/controlPlane/http/verify.d.ts +84 -0
- package/dist/controlPlane/http/verify.d.ts.map +1 -0
- package/dist/controlPlane/http/verify.js +65 -0
- package/dist/controlPlane/migrations/0001_init.d.ts +51 -0
- package/dist/controlPlane/migrations/0001_init.d.ts.map +1 -0
- package/dist/controlPlane/migrations/0001_init.js +65 -0
- package/dist/controlPlane/replay/d1Guard.d.ts +29 -0
- package/dist/controlPlane/replay/d1Guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/d1Guard.js +39 -0
- package/dist/controlPlane/replay/guard.d.ts +52 -0
- package/dist/controlPlane/replay/guard.d.ts.map +1 -0
- package/dist/controlPlane/replay/guard.js +0 -0
- package/dist/controlPlane/replay/kvGuard.d.ts +99 -0
- package/dist/controlPlane/replay/kvGuard.d.ts.map +1 -0
- package/dist/controlPlane/replay/kvGuard.js +101 -0
- package/dist/controlPlane/scope/scope.d.ts +68 -0
- package/dist/controlPlane/scope/scope.d.ts.map +1 -0
- package/dist/controlPlane/scope/scope.js +63 -0
- package/dist/controlPlane/token/base64url.d.ts +10 -0
- package/dist/controlPlane/token/base64url.d.ts.map +1 -0
- package/dist/controlPlane/token/base64url.js +48 -0
- package/dist/controlPlane/token/claims.d.ts +83 -0
- package/dist/controlPlane/token/claims.d.ts.map +1 -0
- package/dist/controlPlane/token/claims.js +86 -0
- package/dist/controlPlane/token/digest.d.ts +44 -0
- package/dist/controlPlane/token/digest.d.ts.map +1 -0
- package/dist/controlPlane/token/digest.js +56 -0
- package/dist/controlPlane/token/jws.d.ts +52 -0
- package/dist/controlPlane/token/jws.d.ts.map +1 -0
- package/dist/controlPlane/token/jws.js +65 -0
- package/dist/controlPlane/token/mint.d.ts +56 -0
- package/dist/controlPlane/token/mint.d.ts.map +1 -0
- package/dist/controlPlane/token/mint.js +55 -0
- package/dist/controlPlane/wire.d.ts +122 -0
- package/dist/controlPlane/wire.d.ts.map +1 -0
- package/dist/controlPlane/wire.js +123 -0
- package/dist/createBackend.d.ts +58 -0
- package/dist/createBackend.d.ts.map +1 -0
- package/dist/createBackend.js +170 -0
- package/dist/createEntrypoint.d.ts +53 -0
- package/dist/createEntrypoint.d.ts.map +1 -0
- package/dist/createEntrypoint.js +52 -0
- package/dist/data/boundParameters.d.ts +83 -0
- package/dist/data/boundParameters.d.ts.map +1 -0
- package/dist/data/boundParameters.js +162 -0
- package/dist/data/codecs.d.ts +34 -0
- package/dist/data/codecs.d.ts.map +1 -0
- package/dist/data/codecs.js +135 -0
- package/dist/data/cursor.d.ts +81 -0
- package/dist/data/cursor.d.ts.map +1 -0
- package/dist/data/cursor.js +104 -0
- package/dist/data/databases.d.ts +55 -0
- package/dist/data/databases.d.ts.map +1 -0
- package/dist/data/databases.js +32 -0
- package/dist/data/db.d.ts +42 -0
- package/dist/data/db.d.ts.map +1 -0
- package/dist/data/db.js +24 -0
- package/dist/data/withD1Retry.d.ts +91 -0
- package/dist/data/withD1Retry.d.ts.map +1 -0
- package/dist/data/withD1Retry.js +152 -0
- package/dist/entitlement/entitlement.d.ts +118 -0
- package/dist/entitlement/entitlement.d.ts.map +1 -0
- package/dist/entitlement/entitlement.js +59 -0
- package/dist/entitlement/gateScan.d.ts +26 -0
- package/dist/entitlement/gateScan.d.ts.map +1 -0
- package/dist/entitlement/gateScan.js +96 -0
- package/dist/entitlement/require.d.ts +12 -0
- package/dist/entitlement/require.d.ts.map +1 -0
- package/dist/entitlement/require.js +123 -0
- package/dist/env/ambient.d.ts +52 -0
- package/dist/env/ambient.d.ts.map +1 -0
- package/dist/env/ambient.js +35 -0
- package/dist/env/ci.d.ts +36 -0
- package/dist/env/ci.d.ts.map +1 -0
- package/dist/env/ci.js +40 -0
- package/dist/env/stem.d.ts +23 -0
- package/dist/env/stem.d.ts.map +1 -0
- package/dist/env/stem.js +29 -0
- package/dist/error/cause.d.ts +143 -0
- package/dist/error/cause.d.ts.map +1 -0
- package/dist/error/cause.js +192 -0
- package/dist/error/client.d.ts +35 -0
- package/dist/error/client.d.ts.map +1 -0
- package/dist/error/client.js +38 -0
- package/dist/error/extend.d.ts +101 -0
- package/dist/error/extend.d.ts.map +1 -0
- package/dist/error/extend.js +76 -0
- package/dist/error/http.d.ts +1516 -0
- package/dist/error/http.d.ts.map +1 -0
- package/dist/error/http.js +72 -0
- package/dist/error/payload.d.ts +3034 -0
- package/dist/error/payload.d.ts.map +1 -0
- package/dist/error/payload.js +1110 -0
- package/dist/error/pithyError.d.ts +154 -0
- package/dist/error/pithyError.d.ts.map +1 -0
- package/dist/error/pithyError.js +197 -0
- package/dist/error/terminal.d.ts +28 -0
- package/dist/error/terminal.d.ts.map +1 -0
- package/dist/error/terminal.js +33 -0
- package/dist/http/authContext.d.ts +21 -0
- package/dist/http/authContext.d.ts.map +1 -0
- package/dist/http/authContext.js +23 -0
- package/dist/http/routeContract.d.ts +39 -0
- package/dist/http/routeContract.d.ts.map +1 -0
- package/dist/http/routeContract.js +82 -0
- package/dist/http/sameOrigin.d.ts +47 -0
- package/dist/http/sameOrigin.d.ts.map +1 -0
- package/dist/http/sameOrigin.js +24 -0
- package/dist/http/signedWebhook.d.ts +225 -0
- package/dist/http/signedWebhook.d.ts.map +1 -0
- package/dist/http/signedWebhook.js +247 -0
- package/dist/http/validation.d.ts +37 -0
- package/dist/http/validation.d.ts.map +1 -0
- package/dist/http/validation.js +14 -0
- package/dist/http/verification.d.ts +10 -0
- package/dist/http/verification.d.ts.map +1 -0
- package/dist/http/verification.js +17 -0
- package/dist/i18n/acceptLanguage.d.ts +22 -0
- package/dist/i18n/acceptLanguage.d.ts.map +1 -0
- package/dist/i18n/acceptLanguage.js +55 -0
- package/dist/i18n/catalog.d.ts +39 -0
- package/dist/i18n/catalog.d.ts.map +1 -0
- package/dist/i18n/catalog.js +57 -0
- package/dist/i18n/locale.d.ts +74 -0
- package/dist/i18n/locale.d.ts.map +1 -0
- package/dist/i18n/locale.js +109 -0
- package/dist/i18n/localeMarker.d.ts +29 -0
- package/dist/i18n/localeMarker.d.ts.map +1 -0
- package/dist/i18n/localeMarker.js +112 -0
- package/dist/i18n/match.d.ts +26 -0
- package/dist/i18n/match.d.ts.map +1 -0
- package/dist/i18n/match.js +88 -0
- package/dist/i18n/registry.d.ts +25 -0
- package/dist/i18n/registry.d.ts.map +1 -0
- package/dist/i18n/registry.js +51 -0
- package/dist/i18n/translator.d.ts +107 -0
- package/dist/i18n/translator.d.ts.map +1 -0
- package/dist/i18n/translator.js +80 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/kv/kv.d.ts +166 -0
- package/dist/kv/kv.d.ts.map +1 -0
- package/dist/kv/kv.js +261 -0
- package/dist/kv/namespaces.d.ts +58 -0
- package/dist/kv/namespaces.d.ts.map +1 -0
- package/dist/kv/namespaces.js +40 -0
- package/dist/logger/local.d.ts +41 -0
- package/dist/logger/local.d.ts.map +1 -0
- package/dist/logger/local.js +45 -0
- package/dist/logger/logger.d.ts +56 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +83 -0
- package/dist/logger/record.d.ts +919 -0
- package/dist/logger/record.d.ts.map +1 -0
- package/dist/logger/record.js +79 -0
- package/dist/logger/worker.d.ts +74 -0
- package/dist/logger/worker.d.ts.map +1 -0
- package/dist/logger/worker.js +54 -0
- package/dist/migrations/batch.d.ts +10 -0
- package/dist/migrations/batch.d.ts.map +1 -0
- package/dist/migrations/batch.js +193 -0
- package/dist/migrations/bookkeeping.d.ts +24 -0
- package/dist/migrations/bookkeeping.d.ts.map +1 -0
- package/dist/migrations/bookkeeping.js +57 -0
- package/dist/migrations/owner.d.ts +41 -0
- package/dist/migrations/owner.d.ts.map +1 -0
- package/dist/migrations/owner.js +105 -0
- package/dist/migrations/registry.d.ts +36 -0
- package/dist/migrations/registry.d.ts.map +1 -0
- package/dist/migrations/registry.js +70 -0
- package/dist/migrations/runner.d.ts +67 -0
- package/dist/migrations/runner.d.ts.map +1 -0
- package/dist/migrations/runner.js +218 -0
- package/dist/naming/domains.d.ts +108 -0
- package/dist/naming/domains.d.ts.map +1 -0
- package/dist/naming/domains.js +156 -0
- package/dist/naming/environment.d.ts +118 -0
- package/dist/naming/environment.d.ts.map +1 -0
- package/dist/naming/environment.js +190 -0
- package/dist/naming/feature.d.ts +68 -0
- package/dist/naming/feature.d.ts.map +1 -0
- package/dist/naming/feature.js +106 -0
- package/dist/naming/limits.d.ts +229 -0
- package/dist/naming/limits.d.ts.map +1 -0
- package/dist/naming/limits.js +187 -0
- package/dist/naming/provisionScope.d.ts +91 -0
- package/dist/naming/provisionScope.d.ts.map +1 -0
- package/dist/naming/provisionScope.js +65 -0
- package/dist/naming/resource.d.ts +169 -0
- package/dist/naming/resource.d.ts.map +1 -0
- package/dist/naming/resource.js +220 -0
- package/dist/naming/resourceNames.d.ts +83 -0
- package/dist/naming/resourceNames.d.ts.map +1 -0
- package/dist/naming/resourceNames.js +102 -0
- package/dist/naming/segment.d.ts +29 -0
- package/dist/naming/segment.d.ts.map +1 -0
- package/dist/naming/segment.js +31 -0
- package/dist/projection/asRead.d.ts +72 -0
- package/dist/projection/asRead.d.ts.map +1 -0
- package/dist/projection/asRead.js +169 -0
- package/dist/projection/published.d.ts +95 -0
- package/dist/projection/published.d.ts.map +1 -0
- package/dist/projection/published.js +114 -0
- package/dist/schema/describedness.d.ts +55 -0
- package/dist/schema/describedness.d.ts.map +1 -0
- package/dist/schema/describedness.js +180 -0
- package/dist/seed/compose.d.ts +41 -0
- package/dist/seed/compose.d.ts.map +1 -0
- package/dist/seed/compose.js +51 -0
- package/dist/seed/devLogin.d.ts +50 -0
- package/dist/seed/devLogin.d.ts.map +1 -0
- package/dist/seed/devLogin.js +52 -0
- package/dist/seed/exampleIdentities.d.ts +58 -0
- package/dist/seed/exampleIdentities.d.ts.map +1 -0
- package/dist/seed/exampleIdentities.js +26 -0
- package/dist/seed/metadata.d.ts +20 -0
- package/dist/seed/metadata.d.ts.map +1 -0
- package/dist/seed/metadata.js +27 -0
- package/dist/seed/seed.d.ts +264 -0
- package/dist/seed/seed.d.ts.map +1 -0
- package/dist/seed/seed.js +37 -0
- package/dist/seed/seededRows.d.ts +26 -0
- package/dist/seed/seededRows.d.ts.map +1 -0
- package/dist/seed/seededRows.js +20 -0
- package/dist/seed/writeD1.d.ts +43 -0
- package/dist/seed/writeD1.d.ts.map +1 -0
- package/dist/seed/writeD1.js +73 -0
- package/dist/seed/writeKv.d.ts +29 -0
- package/dist/seed/writeKv.d.ts.map +1 -0
- package/dist/seed/writeKv.js +63 -0
- package/dist/semver/semver.d.ts +62 -0
- package/dist/semver/semver.d.ts.map +1 -0
- package/dist/semver/semver.js +137 -0
- package/dist/text/comments.d.ts +62 -0
- package/dist/text/comments.d.ts.map +1 -0
- package/dist/text/comments.js +161 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/dist/worker/health.d.ts +39 -0
- package/dist/worker/health.d.ts.map +1 -0
- package/dist/worker/health.js +41 -0
- package/dist/worker/identity.d.ts +191 -0
- package/dist/worker/identity.d.ts.map +1 -0
- package/dist/worker/identity.js +162 -0
- package/dist/workflow/bindings.d.ts +41 -0
- package/dist/workflow/bindings.d.ts.map +1 -0
- package/dist/workflow/bindings.js +53 -0
- package/dist/workflow/dispatch.d.ts +98 -0
- package/dist/workflow/dispatch.d.ts.map +1 -0
- package/dist/workflow/dispatch.js +148 -0
- package/dist/workflow/dispatchRoute.d.ts +80 -0
- package/dist/workflow/dispatchRoute.d.ts.map +1 -0
- package/dist/workflow/dispatchRoute.js +134 -0
- package/dist/workflow/faults.d.ts +97 -0
- package/dist/workflow/faults.d.ts.map +1 -0
- package/dist/workflow/faults.js +111 -0
- package/dist/workflow/host.d.ts +176 -0
- package/dist/workflow/host.d.ts.map +1 -0
- package/dist/workflow/host.js +116 -0
- package/dist/workflow/hostEntry.d.ts +50 -0
- package/dist/workflow/hostEntry.d.ts.map +1 -0
- package/dist/workflow/hostEntry.js +24 -0
- package/dist/workflow/hostEnv.d.ts +143 -0
- package/dist/workflow/hostEnv.d.ts.map +1 -0
- package/dist/workflow/hostEnv.js +170 -0
- package/dist/workflow/loopback.d.ts +52 -0
- package/dist/workflow/loopback.d.ts.map +1 -0
- package/dist/workflow/loopback.js +76 -0
- package/dist/workflow/naming.d.ts +79 -0
- package/dist/workflow/naming.d.ts.map +1 -0
- package/dist/workflow/naming.js +138 -0
- package/dist/workflow/register.d.ts +19 -0
- package/dist/workflow/register.d.ts.map +1 -0
- package/dist/workflow/register.js +40 -0
- package/dist/workflow/schemas.d.ts +44 -0
- package/dist/workflow/schemas.d.ts.map +1 -0
- package/dist/workflow/schemas.js +43 -0
- package/dist/workflow/spec.d.ts +81 -0
- package/dist/workflow/spec.d.ts.map +1 -0
- package/dist/workflow/spec.js +0 -0
- package/dist/workflow/stepMessage.d.ts +122 -0
- package/dist/workflow/stepMessage.d.ts.map +1 -0
- package/dist/workflow/stepMessage.js +152 -0
- package/package.json +20 -9
- package/src/capability/manifest.ts +21 -0
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ExecutionContext, ForwardableEmailMessage } from "@cloudflare/workers-types";
|
|
2
|
+
import type { Capability } from "./capability/capability";
|
|
3
|
+
import { type CreateBackendOptions } from "./createBackend";
|
|
4
|
+
/**
|
|
5
|
+
* A deployable Worker entrypoint: `fetch`, the single inbound `email` handler the runtime permits
|
|
6
|
+
* per Worker, and the single `scheduled` handler it permits for cron triggers. `createBackend`
|
|
7
|
+
* already produces the `fetch` side (a Hono app); this wraps it and composes every capability's
|
|
8
|
+
* inbound-email handler and every cron-carrying durable job behind those two entries, so a Worker
|
|
9
|
+
* that mounts `@pithy-sh/email` receives bounce/complaint mail — and fires its scheduler — without
|
|
10
|
+
* the user authoring a handler.
|
|
11
|
+
*/
|
|
12
|
+
export interface PithyEntrypoint {
|
|
13
|
+
/** The Worker `fetch` handler — the composed Hono app. */
|
|
14
|
+
fetch: (request: Request, env: Record<string, unknown>, ctx: ExecutionContext) => Response | Promise<Response>;
|
|
15
|
+
/**
|
|
16
|
+
* The Worker `email` handler — fans each incoming message to every capability that declares one.
|
|
17
|
+
* **Present only when at least one capability handles inbound mail.** Omitted otherwise, so a Worker
|
|
18
|
+
* that no capability wired for email doesn't expose a no-op `email()` that would silently *drop* any
|
|
19
|
+
* message routed to it (an exported-but-inert handler consumes the message without forwarding or
|
|
20
|
+
* rejecting it).
|
|
21
|
+
*/
|
|
22
|
+
email?: (message: ForwardableEmailMessage, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* The Worker `scheduled` handler — starts every registered job that declares a `schedule`.
|
|
25
|
+
*
|
|
26
|
+
* **Present only when at least one job carries a cron.** The reason differs from `email`'s: an
|
|
27
|
+
* inert `email()` silently *drops* mail, which is a correctness bug, whereas an inert `scheduled()`
|
|
28
|
+
* is merely harmless. It is omitted anyway so that the export tracks the composition: a Worker
|
|
29
|
+
* mounting no scheduled job does not advertise a schedule it has none of.
|
|
30
|
+
*
|
|
31
|
+
* The converse does happen and is deliberate. A capability whose cron belongs to its *prebuilt host*
|
|
32
|
+
* — `@pithy-sh/email`'s every-minute scheduler is the shipped example — still declares that schedule
|
|
33
|
+
* on its spec, so an app worker composing it exports a `scheduled` handler while its own
|
|
34
|
+
* `wrangler.jsonc` declares no `triggers.crons`. Nothing ever invokes it, and if something did, the
|
|
35
|
+
* job's binding lives only on the host, so dispatch degrades with a logged warning. Deriving the
|
|
36
|
+
* export from "does this deployment declare crons" is not available here: `createEntrypoint` sees
|
|
37
|
+
* the composed capabilities, never the wrangler config.
|
|
38
|
+
*
|
|
39
|
+
* A cron is an *additional* entry point, never the only one: every scheduled job stays dispatchable
|
|
40
|
+
* through `c.var.workflows.trigger(...)`, because a backfill nobody can run on demand cannot be
|
|
41
|
+
* tested in staging.
|
|
42
|
+
*/
|
|
43
|
+
scheduled?: (controller: unknown, env: Record<string, unknown>, ctx: ExecutionContext) => Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Assemble capabilities into a Worker entrypoint object — `{ fetch, email }`. Use this instead of
|
|
47
|
+
* `createBackend` directly when any capability handles inbound email (e.g. `@pithy-sh/email`'s bounce
|
|
48
|
+
* handler). `fetch` is the same Hono app `createBackend` returns; `email` fans each incoming message
|
|
49
|
+
* to every capability's `email` handler, replaying the single-use raw stream so each sees the full
|
|
50
|
+
* message. Handlers run sequentially in registration order.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createEntrypoint<const Caps extends readonly Capability[], const App extends Capability = Capability<Record<never, never>, Record<never, never>>>(options: CreateBackendOptions<Caps, App>): PithyEntrypoint;
|
|
53
|
+
//# sourceMappingURL=createEntrypoint.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createEntrypoint.d.ts","sourceRoot":"","sources":["../src/createEntrypoint.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,KAAK,EAAE,UAAU,EAA0B,MAAM,yBAAyB,CAAC;AAClF,OAAO,EAAE,KAAK,oBAAoB,EAAiB,MAAM,iBAAiB,CAAC;AAI3E;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC/G;;;;;;OAMG;IACH,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjH;;;;;;;;;;;;;;;;;;;OAmBG;IACH,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,EAAE,gBAAgB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CACzG;AAkBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,CAAC,IAAI,SAAS,SAAS,UAAU,EAAE,EACxC,KAAK,CAAC,GAAG,SAAS,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,EACrF,OAAO,EAAE,oBAAoB,CAAC,IAAI,EAAE,GAAG,CAAC,GAAG,eAAe,CA+C3D"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { triggerWorkflow } from "./workflow/dispatch.js";
|
|
2
|
+
import { composeWorkflows, scheduledWorkflows } from "./workflow/register.js";
|
|
3
|
+
import { createBackend } from "./createBackend.js";
|
|
4
|
+
//#region src/createEntrypoint.ts
|
|
5
|
+
/**
|
|
6
|
+
* The raw MIME stream of a `ForwardableEmailMessage` is **single-use** — once one handler reads it,
|
|
7
|
+
* the next sees an empty stream. When more than one capability handles inbound mail we buffer the
|
|
8
|
+
* body once and hand each handler a proxy whose `raw` yields a fresh stream over that buffer; every
|
|
9
|
+
* other property/method delegates to (and stays bound to) the real message.
|
|
10
|
+
*/
|
|
11
|
+
function replayable(message, raw) {
|
|
12
|
+
return new Proxy(message, { get(target, prop, receiver) {
|
|
13
|
+
if (prop === "raw") return new Response(raw.slice(0)).body;
|
|
14
|
+
const value = Reflect.get(target, prop, receiver);
|
|
15
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
16
|
+
} });
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Assemble capabilities into a Worker entrypoint object — `{ fetch, email }`. Use this instead of
|
|
20
|
+
* `createBackend` directly when any capability handles inbound email (e.g. `@pithy-sh/email`'s bounce
|
|
21
|
+
* handler). `fetch` is the same Hono app `createBackend` returns; `email` fans each incoming message
|
|
22
|
+
* to every capability's `email` handler, replaying the single-use raw stream so each sees the full
|
|
23
|
+
* message. Handlers run sequentially in registration order.
|
|
24
|
+
*/
|
|
25
|
+
function createEntrypoint(options) {
|
|
26
|
+
const app = createBackend(options);
|
|
27
|
+
const all = options.app ? [...options.capabilities, options.app] : [...options.capabilities];
|
|
28
|
+
const handlers = all.flatMap((cap) => cap.email ? [cap.email] : []);
|
|
29
|
+
const registry = composeWorkflows(all);
|
|
30
|
+
const scheduled = scheduledWorkflows(registry);
|
|
31
|
+
const entrypoint = { fetch: (request, env, ctx) => app.fetch(request, env, ctx) };
|
|
32
|
+
if (scheduled.length > 0) entrypoint.scheduled = async (_controller, env) => {
|
|
33
|
+
const failures = [];
|
|
34
|
+
for (const entry of scheduled) try {
|
|
35
|
+
await triggerWorkflow(env, registry, entry.key, {});
|
|
36
|
+
} catch (error) {
|
|
37
|
+
failures.push(error);
|
|
38
|
+
}
|
|
39
|
+
if (failures[0] !== void 0) throw failures[0];
|
|
40
|
+
};
|
|
41
|
+
if (handlers.length > 0) entrypoint.email = async (message, env, ctx) => {
|
|
42
|
+
if (handlers.length === 1) {
|
|
43
|
+
await handlers[0]?.(message, env, ctx);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const raw = await new Response(message.raw).arrayBuffer();
|
|
47
|
+
for (const handler of handlers) await handler(replayable(message, raw), env, ctx);
|
|
48
|
+
};
|
|
49
|
+
return entrypoint;
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
export { createEntrypoint };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
2
|
+
/**
|
|
3
|
+
* D1's bound-parameter ceiling, and the chunking every `in (...)` list needs to stay under it.
|
|
4
|
+
*
|
|
5
|
+
* D1 rejects a statement carrying more than 100 bound parameters. The failure mode is the nastiest kind:
|
|
6
|
+
* every small test passes, and the query only breaks once real data arrives. Worse, the naive fix —
|
|
7
|
+
* chunking the list at exactly 100 — is *also* wrong whenever the statement binds anything else. A
|
|
8
|
+
* `where("indexName", "=", name).where("id", "in", <100 ids>)` binds 101, and an `update … set(a, b)`
|
|
9
|
+
* ahead of the list binds two more before a single id is counted.
|
|
10
|
+
*
|
|
11
|
+
* So the unit here is not "the cap" but **the budget left after the statement's own fixed parameters**.
|
|
12
|
+
* A call site names how many parameters its statement binds besides the list, and the chunk size is
|
|
13
|
+
* derived. Adding a `where` to one of those queries is then a one-number edit next to it, not a silent
|
|
14
|
+
* re-break of a limit nobody re-derived.
|
|
15
|
+
*
|
|
16
|
+
* Lives in core rather than in each capability because the arithmetic is the part that was got wrong,
|
|
17
|
+
* and because `data/` is already where D1's quirks live (see `withD1Retry`).
|
|
18
|
+
*/
|
|
19
|
+
/** D1's hard cap on bound parameters in one statement. Cloudflare documents it; over it is an error, not a truncation. */
|
|
20
|
+
export declare const MAX_BOUND_PARAMETERS = 100;
|
|
21
|
+
/**
|
|
22
|
+
* How many list values one statement may still bind once its fixed parameters are paid for.
|
|
23
|
+
*
|
|
24
|
+
* `fixed` is everything the statement binds that is *not* part of the chunked list — each `where` value,
|
|
25
|
+
* each `set` column, each limit. Throws when a statement leaves no room at all: that is a bug in the
|
|
26
|
+
* query, not a runtime condition, and silently returning zero would spin the caller's loop forever.
|
|
27
|
+
*/
|
|
28
|
+
export declare function boundParameterBudget(fixed: number): number;
|
|
29
|
+
/**
|
|
30
|
+
* Split `values` into chunks, each small enough that one statement binding `fixed` other parameters
|
|
31
|
+
* stays under D1's cap. An empty input yields no chunks, so a caller's loop simply does not run.
|
|
32
|
+
*/
|
|
33
|
+
export declare function chunkByBoundParameters<T>(values: readonly T[], fixed: number): T[][];
|
|
34
|
+
/**
|
|
35
|
+
* Split `rows` into chunks small enough that one multi-row `insert` stays under D1's cap.
|
|
36
|
+
*
|
|
37
|
+
* The sibling of {@link chunkByBoundParameters}, and a separate function because the arithmetic is
|
|
38
|
+
* genuinely different: an `in (…)` list binds **one** parameter per value, while an insert binds one
|
|
39
|
+
* per *column* per row. Reusing the list chunker for an insert silently allows `columns` times too
|
|
40
|
+
* many rows, which is a bug that passes every small test and only appears once real data arrives —
|
|
41
|
+
* `@pithy-sh/support` shipped exactly that, capping attachments at 10 rows of 11 columns and losing
|
|
42
|
+
* every one of them to `too many SQL variables`.
|
|
43
|
+
*/
|
|
44
|
+
export declare function chunkRowsByBoundParameters<T>(rows: readonly T[], columnsPerRow: number, fixed?: number): T[][];
|
|
45
|
+
/**
|
|
46
|
+
* D1, wrapped so a statement over the cap fails as **the rule** rather than as SQLite's complaint.
|
|
47
|
+
*
|
|
48
|
+
* `createDatabase` applies this to every database it builds, which is the whole point: the rule lives at
|
|
49
|
+
* the thing being called, not at each of the query sites that call it. A capability written next month
|
|
50
|
+
* that has never heard of the cap is covered by it, and so is one written last year — which matters,
|
|
51
|
+
* because the list of sites that got this wrong has been wrong five times running. Four capabilities
|
|
52
|
+
* bound past the cap while the arithmetic to avoid it sat in this file, unimported (#250).
|
|
53
|
+
*
|
|
54
|
+
* The check is at `bind`, because that is the only place the number is real: it is what the driver hands
|
|
55
|
+
* the platform, after Kysely has compiled the statement and after any chunking a caller did. Anything
|
|
56
|
+
* measured earlier measures intent.
|
|
57
|
+
*
|
|
58
|
+
* It does not chunk. It cannot — splitting an arbitrary statement is the caller's arithmetic, and
|
|
59
|
+
* {@link chunkByBoundParameters} and {@link chunkRowsByBoundParameters} are where it belongs. What this
|
|
60
|
+
* buys is that the failure names the rule that was broken, one statement before the platform says
|
|
61
|
+
* `too many SQL variables` and leaves the author to work out which list was too long.
|
|
62
|
+
*/
|
|
63
|
+
export declare function guardBoundParameters(d1: D1Database): D1Database;
|
|
64
|
+
/** What one recorded run bound, and whatever it threw. */
|
|
65
|
+
export interface BoundParameterRecording {
|
|
66
|
+
/** Every parameter count D1 was asked to bind, in the order the statements ran. */
|
|
67
|
+
counts: number[];
|
|
68
|
+
/** What `run` threw, or `undefined`. Returned rather than propagated — see below. */
|
|
69
|
+
error: unknown;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Run `run` against a recording D1 and report every parameter count it bound.
|
|
73
|
+
*
|
|
74
|
+
* A gate wants to assert the ceiling *before* it rethrows, so the failure an author reads is the
|
|
75
|
+
* invariant rather than whatever the platform said about it. Hence the throw comes back in the result
|
|
76
|
+
* instead of propagating: the caller asserts the counts, then rethrows anything left.
|
|
77
|
+
*
|
|
78
|
+
* The counterpart to {@link guardBoundParameters} rather than a duplicate of it. The guard proves a
|
|
79
|
+
* statement was *refused*; this proves what was *bound* — that chunks fill the budget rather than
|
|
80
|
+
* creeping under it, and that chunking lost no rows on the way.
|
|
81
|
+
*/
|
|
82
|
+
export declare function recordBoundParameters(d1: D1Database, run: (d1: D1Database) => Promise<void>): Promise<BoundParameterRecording>;
|
|
83
|
+
//# sourceMappingURL=boundParameters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundParameters.d.ts","sourceRoot":"","sources":["../../src/data/boundParameters.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,2BAA2B,CAAC;AAGjF;;;;;;;;;;;;;;;;GAgBG;AAEH,0HAA0H;AAC1H,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAe1D;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,EAAE,EAAE,CAKpF;AAED;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,CAAC,EAAE,EAAE,CAiBzG;AAcD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,oBAAoB,CAAC,EAAE,EAAE,UAAU,GAAG,UAAU,CA0B/D;AAED,0DAA0D;AAC1D,MAAM,WAAW,uBAAuB;IACtC,mFAAmF;IACnF,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,qFAAqF;IACrF,KAAK,EAAE,OAAO,CAAC;CAChB;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,qBAAqB,CACzC,EAAE,EAAE,UAAU,EACd,GAAG,EAAE,CAAC,EAAE,EAAE,UAAU,KAAK,OAAO,CAAC,IAAI,CAAC,GACrC,OAAO,CAAC,uBAAuB,CAAC,CAyBlC"}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { InternalError } from "../error/pithyError.js";
|
|
2
|
+
//#region src/data/boundParameters.ts
|
|
3
|
+
/**
|
|
4
|
+
* D1's bound-parameter ceiling, and the chunking every `in (...)` list needs to stay under it.
|
|
5
|
+
*
|
|
6
|
+
* D1 rejects a statement carrying more than 100 bound parameters. The failure mode is the nastiest kind:
|
|
7
|
+
* every small test passes, and the query only breaks once real data arrives. Worse, the naive fix —
|
|
8
|
+
* chunking the list at exactly 100 — is *also* wrong whenever the statement binds anything else. A
|
|
9
|
+
* `where("indexName", "=", name).where("id", "in", <100 ids>)` binds 101, and an `update … set(a, b)`
|
|
10
|
+
* ahead of the list binds two more before a single id is counted.
|
|
11
|
+
*
|
|
12
|
+
* So the unit here is not "the cap" but **the budget left after the statement's own fixed parameters**.
|
|
13
|
+
* A call site names how many parameters its statement binds besides the list, and the chunk size is
|
|
14
|
+
* derived. Adding a `where` to one of those queries is then a one-number edit next to it, not a silent
|
|
15
|
+
* re-break of a limit nobody re-derived.
|
|
16
|
+
*
|
|
17
|
+
* Lives in core rather than in each capability because the arithmetic is the part that was got wrong,
|
|
18
|
+
* and because `data/` is already where D1's quirks live (see `withD1Retry`).
|
|
19
|
+
*/
|
|
20
|
+
/** D1's hard cap on bound parameters in one statement. Cloudflare documents it; over it is an error, not a truncation. */
|
|
21
|
+
const MAX_BOUND_PARAMETERS = 100;
|
|
22
|
+
/**
|
|
23
|
+
* How many list values one statement may still bind once its fixed parameters are paid for.
|
|
24
|
+
*
|
|
25
|
+
* `fixed` is everything the statement binds that is *not* part of the chunked list — each `where` value,
|
|
26
|
+
* each `set` column, each limit. Throws when a statement leaves no room at all: that is a bug in the
|
|
27
|
+
* query, not a runtime condition, and silently returning zero would spin the caller's loop forever.
|
|
28
|
+
*/
|
|
29
|
+
function boundParameterBudget(fixed) {
|
|
30
|
+
if (!Number.isInteger(fixed) || fixed < 0) throw new InternalError({
|
|
31
|
+
message: "Something unexpected happened.",
|
|
32
|
+
detail: `boundParameterBudget requires a non-negative integer count of fixed parameters; got ${fixed}.`
|
|
33
|
+
});
|
|
34
|
+
const budget = 100 - fixed;
|
|
35
|
+
if (budget < 1) throw new InternalError({
|
|
36
|
+
message: "Something unexpected happened.",
|
|
37
|
+
detail: `A statement binding ${fixed} fixed parameters leaves no room under D1's cap of 100.`
|
|
38
|
+
});
|
|
39
|
+
return budget;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Split `values` into chunks, each small enough that one statement binding `fixed` other parameters
|
|
43
|
+
* stays under D1's cap. An empty input yields no chunks, so a caller's loop simply does not run.
|
|
44
|
+
*/
|
|
45
|
+
function chunkByBoundParameters(values, fixed) {
|
|
46
|
+
const size = boundParameterBudget(fixed);
|
|
47
|
+
const chunks = [];
|
|
48
|
+
for (let start = 0; start < values.length; start += size) chunks.push(values.slice(start, start + size));
|
|
49
|
+
return chunks;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Split `rows` into chunks small enough that one multi-row `insert` stays under D1's cap.
|
|
53
|
+
*
|
|
54
|
+
* The sibling of {@link chunkByBoundParameters}, and a separate function because the arithmetic is
|
|
55
|
+
* genuinely different: an `in (…)` list binds **one** parameter per value, while an insert binds one
|
|
56
|
+
* per *column* per row. Reusing the list chunker for an insert silently allows `columns` times too
|
|
57
|
+
* many rows, which is a bug that passes every small test and only appears once real data arrives —
|
|
58
|
+
* `@pithy-sh/support` shipped exactly that, capping attachments at 10 rows of 11 columns and losing
|
|
59
|
+
* every one of them to `too many SQL variables`.
|
|
60
|
+
*/
|
|
61
|
+
function chunkRowsByBoundParameters(rows, columnsPerRow, fixed = 0) {
|
|
62
|
+
if (!Number.isInteger(columnsPerRow) || columnsPerRow < 1) throw new InternalError({
|
|
63
|
+
message: "Something unexpected happened.",
|
|
64
|
+
detail: `chunkRowsByBoundParameters requires a positive column count; got ${columnsPerRow}.`
|
|
65
|
+
});
|
|
66
|
+
const size = Math.floor(boundParameterBudget(fixed) / columnsPerRow);
|
|
67
|
+
if (size < 1) throw new InternalError({
|
|
68
|
+
message: "Something unexpected happened.",
|
|
69
|
+
detail: `A row of ${columnsPerRow} columns cannot fit under D1's cap of 100.`
|
|
70
|
+
});
|
|
71
|
+
const chunks = [];
|
|
72
|
+
for (let start = 0; start < rows.length; start += size) chunks.push(rows.slice(start, start + size));
|
|
73
|
+
return chunks;
|
|
74
|
+
}
|
|
75
|
+
/** Read a property off a platform object without losing its `this` — workerd's D1 objects need it. */
|
|
76
|
+
function passThrough(target, property) {
|
|
77
|
+
const value = Reflect.get(target, property);
|
|
78
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
79
|
+
}
|
|
80
|
+
/** Enough of a statement to name it in a failure, without carrying a query of unbounded length. */
|
|
81
|
+
function nameOf(query) {
|
|
82
|
+
const collapsed = query.replace(/\s+/g, " ").trim();
|
|
83
|
+
return collapsed.length > 160 ? `${collapsed.slice(0, 157)}…` : collapsed;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* D1, wrapped so a statement over the cap fails as **the rule** rather than as SQLite's complaint.
|
|
87
|
+
*
|
|
88
|
+
* `createDatabase` applies this to every database it builds, which is the whole point: the rule lives at
|
|
89
|
+
* the thing being called, not at each of the query sites that call it. A capability written next month
|
|
90
|
+
* that has never heard of the cap is covered by it, and so is one written last year — which matters,
|
|
91
|
+
* because the list of sites that got this wrong has been wrong five times running. Four capabilities
|
|
92
|
+
* bound past the cap while the arithmetic to avoid it sat in this file, unimported (#250).
|
|
93
|
+
*
|
|
94
|
+
* The check is at `bind`, because that is the only place the number is real: it is what the driver hands
|
|
95
|
+
* the platform, after Kysely has compiled the statement and after any chunking a caller did. Anything
|
|
96
|
+
* measured earlier measures intent.
|
|
97
|
+
*
|
|
98
|
+
* It does not chunk. It cannot — splitting an arbitrary statement is the caller's arithmetic, and
|
|
99
|
+
* {@link chunkByBoundParameters} and {@link chunkRowsByBoundParameters} are where it belongs. What this
|
|
100
|
+
* buys is that the failure names the rule that was broken, one statement before the platform says
|
|
101
|
+
* `too many SQL variables` and leaves the author to work out which list was too long.
|
|
102
|
+
*/
|
|
103
|
+
function guardBoundParameters(d1) {
|
|
104
|
+
return new Proxy(d1, { get(target, property) {
|
|
105
|
+
if (property !== "prepare") return passThrough(target, property);
|
|
106
|
+
return (query) => {
|
|
107
|
+
const statement = target.prepare(query);
|
|
108
|
+
return new Proxy(statement, { get(inner, key) {
|
|
109
|
+
if (key !== "bind") return passThrough(inner, key);
|
|
110
|
+
return (...values) => {
|
|
111
|
+
if (values.length > 100) throw new InternalError({
|
|
112
|
+
message: "Something unexpected happened.",
|
|
113
|
+
action: "Retry. If it persists, the query needs chunking.",
|
|
114
|
+
detail: `A statement bound ${values.length} parameters; D1 accepts 100. Chunk the list through chunkByBoundParameters (one parameter per value) or chunkRowsByBoundParameters (one per column per row). Statement: ${nameOf(query)}`
|
|
115
|
+
});
|
|
116
|
+
return inner.bind(...values);
|
|
117
|
+
};
|
|
118
|
+
} });
|
|
119
|
+
};
|
|
120
|
+
} });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Run `run` against a recording D1 and report every parameter count it bound.
|
|
124
|
+
*
|
|
125
|
+
* A gate wants to assert the ceiling *before* it rethrows, so the failure an author reads is the
|
|
126
|
+
* invariant rather than whatever the platform said about it. Hence the throw comes back in the result
|
|
127
|
+
* instead of propagating: the caller asserts the counts, then rethrows anything left.
|
|
128
|
+
*
|
|
129
|
+
* The counterpart to {@link guardBoundParameters} rather than a duplicate of it. The guard proves a
|
|
130
|
+
* statement was *refused*; this proves what was *bound* — that chunks fill the budget rather than
|
|
131
|
+
* creeping under it, and that chunking lost no rows on the way.
|
|
132
|
+
*/
|
|
133
|
+
async function recordBoundParameters(d1, run) {
|
|
134
|
+
const counts = [];
|
|
135
|
+
const recorder = new Proxy(d1, { get(target, property) {
|
|
136
|
+
if (property !== "prepare") return passThrough(target, property);
|
|
137
|
+
return (query) => {
|
|
138
|
+
const statement = target.prepare(query);
|
|
139
|
+
return new Proxy(statement, { get(inner, key) {
|
|
140
|
+
if (key !== "bind") return passThrough(inner, key);
|
|
141
|
+
return (...values) => {
|
|
142
|
+
counts.push(values.length);
|
|
143
|
+
return inner.bind(...values);
|
|
144
|
+
};
|
|
145
|
+
} });
|
|
146
|
+
};
|
|
147
|
+
} });
|
|
148
|
+
try {
|
|
149
|
+
await run(recorder);
|
|
150
|
+
return {
|
|
151
|
+
counts,
|
|
152
|
+
error: void 0
|
|
153
|
+
};
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return {
|
|
156
|
+
counts,
|
|
157
|
+
error
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
//#endregion
|
|
162
|
+
export { MAX_BOUND_PARAMETERS, boundParameterBudget, chunkByBoundParameters, chunkRowsByBoundParameters, guardBoundParameters, recordBoundParameters };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** SQLiteBoolean: boolean ↔ 0|1. Decode is lenient (number/boolean/string). */
|
|
3
|
+
export declare const SQLiteBoolean: z.ZodCodec<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>, z.ZodBoolean, z.ZodString]>, z.ZodBoolean>;
|
|
4
|
+
/** SQLiteDate: Date ↔ ms-epoch number. Decode accepts number/string/Date. */
|
|
5
|
+
export declare const SQLiteDate: z.ZodCodec<z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodDate]>, z.ZodDate>;
|
|
6
|
+
/** JsonDate: Date ↔ ISO string, for dates nested inside `sqliteJson` payloads. */
|
|
7
|
+
export declare const JsonDate: z.ZodCodec<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodDate]>, z.ZodDate>;
|
|
8
|
+
/**
|
|
9
|
+
* sqliteJson: T ↔ JSON string, validated by `schema` on both sides. Decode
|
|
10
|
+
* accepts a JSON string or an already-parsed value; the result is validated
|
|
11
|
+
* against `schema` before it reaches the app.
|
|
12
|
+
*
|
|
13
|
+
* **Both `JSON` functions throw**, and both were reachable. `JSON.parse` throws a
|
|
14
|
+
* `SyntaxError` on any string that is not a JSON document — which a column holding
|
|
15
|
+
* text an older writer put there, or a payload a customer's Worker returned, can be.
|
|
16
|
+
* `JSON.stringify` throws a `TypeError` on a `BigInt` and on a cycle, either of
|
|
17
|
+
* which a `schema` can legitimately admit. Both become an issue (#358).
|
|
18
|
+
*/
|
|
19
|
+
export declare function sqliteJson<T extends z.ZodType>(schema: T): z.ZodCodec<z.ZodUnion<readonly [z.ZodString, T]>, T>;
|
|
20
|
+
export declare const MAX_TIMEZONE_LENGTH = 64;
|
|
21
|
+
/**
|
|
22
|
+
* Trim, validate, and canonicalize an IANA timezone; anything invalid becomes
|
|
23
|
+
* `undefined`. Returns Intl's canonical casing (`america/new_york` →
|
|
24
|
+
* `America/New_York`) so a zone is stored and compared as one value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeIanaTimezone(value: unknown): string | undefined;
|
|
27
|
+
/** Whether `value` is a usable IANA timezone (bounded length, recognized by Intl). */
|
|
28
|
+
export declare function isValidIanaTimezone(value: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* IanaTimezone: validated/bounded IANA timezone string. Coerces invalid input
|
|
31
|
+
* to `undefined` on both decode and encode rather than throwing.
|
|
32
|
+
*/
|
|
33
|
+
export declare const IanaTimezone: z.ZodCodec<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodOptional<z.ZodString>>;
|
|
34
|
+
//# sourceMappingURL=codecs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codecs.d.ts","sourceRoot":"","sources":["../../src/data/codecs.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA+CxB,+EAA+E;AAC/E,eAAO,MAAM,aAAa,8GAGxB,CAAC;AAoBH,6EAA6E;AAC7E,eAAO,MAAM,UAAU,mFAGrB,CAAC;AAEH,kFAAkF;AAClF,eAAO,MAAM,QAAQ,mFAGnB,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,CAAC,wDA8BxD;AAED,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CASxE;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;GAGG;AACH,eAAO,MAAM,YAAY,mFAGvB,CAAC"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/data/codecs.ts
|
|
3
|
+
const TRUTHY = [
|
|
4
|
+
true,
|
|
5
|
+
1,
|
|
6
|
+
"1",
|
|
7
|
+
"true",
|
|
8
|
+
"True",
|
|
9
|
+
"TRUE"
|
|
10
|
+
];
|
|
11
|
+
function isTruthy(value) {
|
|
12
|
+
return TRUTHY.includes(value);
|
|
13
|
+
}
|
|
14
|
+
/** SQLiteBoolean: boolean ↔ 0|1. Decode is lenient (number/boolean/string). */
|
|
15
|
+
const SQLiteBoolean = z.codec(z.union([
|
|
16
|
+
z.literal(0),
|
|
17
|
+
z.literal(1),
|
|
18
|
+
z.boolean(),
|
|
19
|
+
z.string()
|
|
20
|
+
]), z.boolean(), {
|
|
21
|
+
decode: (input) => typeof input === "boolean" ? input : isTruthy(input),
|
|
22
|
+
encode: (value) => value ? 1 : 0
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* The shared decode for both date codecs. A string is parsed, a number is ms-epoch — what `encode`
|
|
26
|
+
* writes, so a caller ingesting seconds-epoch from elsewhere converts explicitly — and a `Date` passes
|
|
27
|
+
* through by identity.
|
|
28
|
+
*
|
|
29
|
+
* **Every branch can produce an unusable `Date`**, not only the string one: `new Date(8.64e15 + 1)` is
|
|
30
|
+
* as invalid as `new Date("not-a-date")`. So the check is on the result, once, rather than on the input
|
|
31
|
+
* shape — which is what stops the next accepted input type from arriving without it.
|
|
32
|
+
*/
|
|
33
|
+
function toDate(input, payload) {
|
|
34
|
+
const parsed = input instanceof Date ? input : new Date(input);
|
|
35
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
36
|
+
payload.issues.push({
|
|
37
|
+
code: "invalid_format",
|
|
38
|
+
format: "datetime",
|
|
39
|
+
input: String(input),
|
|
40
|
+
message: "Not a date."
|
|
41
|
+
});
|
|
42
|
+
return z.NEVER;
|
|
43
|
+
}
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
/** SQLiteDate: Date ↔ ms-epoch number. Decode accepts number/string/Date. */
|
|
47
|
+
const SQLiteDate = z.codec(z.union([
|
|
48
|
+
z.number(),
|
|
49
|
+
z.string(),
|
|
50
|
+
z.date()
|
|
51
|
+
]), z.date(), {
|
|
52
|
+
decode: toDate,
|
|
53
|
+
encode: (value) => value.getTime()
|
|
54
|
+
});
|
|
55
|
+
/** JsonDate: Date ↔ ISO string, for dates nested inside `sqliteJson` payloads. */
|
|
56
|
+
const JsonDate = z.codec(z.union([
|
|
57
|
+
z.string(),
|
|
58
|
+
z.number(),
|
|
59
|
+
z.date()
|
|
60
|
+
]), z.date(), {
|
|
61
|
+
decode: toDate,
|
|
62
|
+
encode: (value) => value.toISOString()
|
|
63
|
+
});
|
|
64
|
+
/**
|
|
65
|
+
* sqliteJson: T ↔ JSON string, validated by `schema` on both sides. Decode
|
|
66
|
+
* accepts a JSON string or an already-parsed value; the result is validated
|
|
67
|
+
* against `schema` before it reaches the app.
|
|
68
|
+
*
|
|
69
|
+
* **Both `JSON` functions throw**, and both were reachable. `JSON.parse` throws a
|
|
70
|
+
* `SyntaxError` on any string that is not a JSON document — which a column holding
|
|
71
|
+
* text an older writer put there, or a payload a customer's Worker returned, can be.
|
|
72
|
+
* `JSON.stringify` throws a `TypeError` on a `BigInt` and on a cycle, either of
|
|
73
|
+
* which a `schema` can legitimately admit. Both become an issue (#358).
|
|
74
|
+
*/
|
|
75
|
+
function sqliteJson(schema) {
|
|
76
|
+
return z.codec(z.union([z.string(), schema]), schema, {
|
|
77
|
+
decode: (value, payload) => {
|
|
78
|
+
if (typeof value !== "string") return value;
|
|
79
|
+
try {
|
|
80
|
+
return JSON.parse(value);
|
|
81
|
+
} catch {
|
|
82
|
+
payload.issues.push({
|
|
83
|
+
code: "invalid_format",
|
|
84
|
+
format: "json",
|
|
85
|
+
input: value,
|
|
86
|
+
message: "Not valid JSON."
|
|
87
|
+
});
|
|
88
|
+
return z.NEVER;
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
encode: (value, payload) => {
|
|
92
|
+
try {
|
|
93
|
+
const json = JSON.stringify(value);
|
|
94
|
+
if (typeof json === "string") return json;
|
|
95
|
+
} catch {}
|
|
96
|
+
payload.issues.push({
|
|
97
|
+
code: "invalid_format",
|
|
98
|
+
format: "json",
|
|
99
|
+
input: void 0,
|
|
100
|
+
message: "Not serializable as JSON."
|
|
101
|
+
});
|
|
102
|
+
return z.NEVER;
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
const MAX_TIMEZONE_LENGTH = 64;
|
|
107
|
+
/**
|
|
108
|
+
* Trim, validate, and canonicalize an IANA timezone; anything invalid becomes
|
|
109
|
+
* `undefined`. Returns Intl's canonical casing (`america/new_york` →
|
|
110
|
+
* `America/New_York`) so a zone is stored and compared as one value.
|
|
111
|
+
*/
|
|
112
|
+
function normalizeIanaTimezone(value) {
|
|
113
|
+
if (typeof value !== "string") return void 0;
|
|
114
|
+
const trimmed = value.trim();
|
|
115
|
+
if (trimmed.length === 0 || trimmed.length > 64) return void 0;
|
|
116
|
+
try {
|
|
117
|
+
return Intl.DateTimeFormat(void 0, { timeZone: trimmed }).resolvedOptions().timeZone;
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** Whether `value` is a usable IANA timezone (bounded length, recognized by Intl). */
|
|
123
|
+
function isValidIanaTimezone(value) {
|
|
124
|
+
return normalizeIanaTimezone(value) !== void 0;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* IanaTimezone: validated/bounded IANA timezone string. Coerces invalid input
|
|
128
|
+
* to `undefined` on both decode and encode rather than throwing.
|
|
129
|
+
*/
|
|
130
|
+
const IanaTimezone = z.codec(z.string().nullish(), z.string().optional(), {
|
|
131
|
+
decode: (input) => normalizeIanaTimezone(input),
|
|
132
|
+
encode: (value) => normalizeIanaTimezone(value)
|
|
133
|
+
});
|
|
134
|
+
//#endregion
|
|
135
|
+
export { IanaTimezone, JsonDate, MAX_TIMEZONE_LENGTH, SQLiteBoolean, SQLiteDate, isValidIanaTimezone, normalizeIanaTimezone, sqliteJson };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Keyset pagination, once, for every capability that lists an unbounded table.
|
|
4
|
+
*
|
|
5
|
+
* ## Why keyset and never offset
|
|
6
|
+
*
|
|
7
|
+
* `OFFSET` shifts under a client whenever a row is inserted, and every table these paginate — audit
|
|
8
|
+
* events, email jobs, users, ledger transactions — is written to constantly while somebody is reading
|
|
9
|
+
* it. With `OFFSET 25` a row inserted at the head pushes one row from page 1 onto page 2, so a client
|
|
10
|
+
* paging through sees it twice and misses nothing only by luck; a deletion drops a row entirely. A
|
|
11
|
+
* keyset cursor names the last row's sort position, so the next page starts exactly where the previous
|
|
12
|
+
* ended whatever happened in between.
|
|
13
|
+
*
|
|
14
|
+
* ## Why this is in core rather than copied
|
|
15
|
+
*
|
|
16
|
+
* `@pithy-sh/support` had the only implementation, and Part 3 of #89 needs the same thing in four more
|
|
17
|
+
* packages. Four copies of a security-adjacent decode — where the difference between "malformed cursor"
|
|
18
|
+
* and "500" is a caller's ability to probe — is four places for one of them to be wrong. Capabilities
|
|
19
|
+
* depend on core seams, which is exactly what this is.
|
|
20
|
+
*
|
|
21
|
+
* ## The shape of a page
|
|
22
|
+
*
|
|
23
|
+
* A caller asks for `limit`; the query fetches `limit + 1` and, when it gets them, drops the extra and
|
|
24
|
+
* returns a `nextCursor`. That is what makes "is there another page" answerable **without a count
|
|
25
|
+
* query**, which on an audit table is the difference between a page load and a table scan.
|
|
26
|
+
*/
|
|
27
|
+
/** A sensible page when a caller names none. Small enough to render, large enough to be one request. */
|
|
28
|
+
export declare const DEFAULT_PAGE_SIZE = 25;
|
|
29
|
+
/** The most a caller may ask for in one page, whatever they send. An unbounded page is a table scan. */
|
|
30
|
+
export declare const MAX_PAGE_SIZE = 100;
|
|
31
|
+
/**
|
|
32
|
+
* The `limit` a query should actually use, clamped into range.
|
|
33
|
+
*
|
|
34
|
+
* Clamped rather than rejected: a client asking for 1000 wants "as many as you'll give me", and failing
|
|
35
|
+
* the request teaches it nothing a capped page does not. A caller asking for 0 or a negative gets one
|
|
36
|
+
* row rather than an empty page that looks like the end of the list.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pageLimit(requested: number | undefined): number;
|
|
39
|
+
/**
|
|
40
|
+
* An opaque position in a descending `(sort, id)` ordering.
|
|
41
|
+
*
|
|
42
|
+
* `sort` is the primary column's value — a ms-epoch for a date, a number for a monotonic id. `id` is the
|
|
43
|
+
* tiebreak that makes the position exact: without it, two rows sharing a timestamp straddle a page
|
|
44
|
+
* boundary and one of them is skipped or repeated.
|
|
45
|
+
*/
|
|
46
|
+
export declare const PageCursor: z.ZodObject<{
|
|
47
|
+
sort: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
48
|
+
id: z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
export type PageCursor = z.infer<typeof PageCursor>;
|
|
51
|
+
/**
|
|
52
|
+
* Encode a cursor for the wire.
|
|
53
|
+
*
|
|
54
|
+
* Base64url over JSON, and **opaque by intent rather than by obscurity**: the point is that a client
|
|
55
|
+
* cannot construct one by hand and therefore cannot come to depend on its shape, so the ordering can
|
|
56
|
+
* change later without breaking every caller. It is not a secret — it holds a sort value and an id the
|
|
57
|
+
* caller was just given, both of which were in the page it came from.
|
|
58
|
+
*/
|
|
59
|
+
export declare function encodeCursor(cursor: PageCursor): string;
|
|
60
|
+
/**
|
|
61
|
+
* Decode a cursor. Returns undefined for anything malformed.
|
|
62
|
+
*
|
|
63
|
+
* **A bad cursor is a first page, not a 500.** Cursors travel through URLs, get truncated by clients,
|
|
64
|
+
* and outlive deploys; treating a malformed one as an error turns an ordinary client bug into a page
|
|
65
|
+
* that never loads, and hands anyone probing the endpoint a way to tell a parse failure from a
|
|
66
|
+
* not-found. Every failure mode here — bad base64, bad JSON, wrong shape — collapses to the same
|
|
67
|
+
* undefined.
|
|
68
|
+
*/
|
|
69
|
+
export declare function decodeCursor(value: string | undefined): PageCursor | undefined;
|
|
70
|
+
/**
|
|
71
|
+
* Split an over-fetched result into the page and the cursor that follows it.
|
|
72
|
+
*
|
|
73
|
+
* Pass the rows a query returned when asked for `limit + 1`. When there are more than `limit`, the extra
|
|
74
|
+
* is dropped and `nextCursor` names the last row of the page; otherwise `nextCursor` is null and the
|
|
75
|
+
* client knows it has reached the end without a second request.
|
|
76
|
+
*/
|
|
77
|
+
export declare function toPage<T>(rows: readonly T[], limit: number, position: (row: T) => PageCursor): {
|
|
78
|
+
items: T[];
|
|
79
|
+
nextCursor: string | null;
|
|
80
|
+
};
|
|
81
|
+
//# sourceMappingURL=cursor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cursor.d.ts","sourceRoot":"","sources":["../../src/data/cursor.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,wGAAwG;AACxG,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAEpC,wGAAwG;AACxG,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE/D;AAED;;;;;;GAMG;AACH,eAAO,MAAM,UAAU;;;iBAa4E,CAAC;AACpG,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAQvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAU9E;AAED;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,CAAC,EACtB,IAAI,EAAE,SAAS,CAAC,EAAE,EAClB,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,UAAU,GAC/B;IAAE,KAAK,EAAE,CAAC,EAAE,CAAC;IAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAK3C"}
|