@rikalabs/akter 0.1.0-alpha.0
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/CHANGELOG.md +15 -0
- package/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +116 -0
- package/dist/actor/codecs.d.ts +77 -0
- package/dist/actor/codecs.js +87 -0
- package/dist/actor/connections.d.ts +34 -0
- package/dist/actor/connections.js +145 -0
- package/dist/actor/definition.d.ts +893 -0
- package/dist/actor/definition.js +147 -0
- package/dist/actor/descriptor.d.ts +172 -0
- package/dist/actor/descriptor.js +367 -0
- package/dist/actor/handles.d.ts +77 -0
- package/dist/actor/handles.js +265 -0
- package/dist/actor/jobs.d.ts +10 -0
- package/dist/actor/jobs.js +137 -0
- package/dist/actor/layers.d.ts +41 -0
- package/dist/actor/layers.js +177 -0
- package/dist/actor/reads.d.ts +47 -0
- package/dist/actor/reads.js +224 -0
- package/dist/actor/served.d.ts +120 -0
- package/dist/actor/served.js +72 -0
- package/dist/actor/turns.d.ts +13 -0
- package/dist/actor/turns.js +332 -0
- package/dist/actor/workflows.d.ts +16 -0
- package/dist/actor/workflows.js +37 -0
- package/dist/client/calls.d.ts +47 -0
- package/dist/client/calls.js +425 -0
- package/dist/client/clock.d.ts +56 -0
- package/dist/client/clock.js +119 -0
- package/dist/client/fleet-page.d.ts +15 -0
- package/dist/client/fleet-page.js +13 -0
- package/dist/client/fleet.d.ts +29 -0
- package/dist/client/fleet.js +51 -0
- package/dist/client/index.d.ts +17 -0
- package/dist/client/index.js +6 -0
- package/dist/client/make.d.ts +157 -0
- package/dist/client/make.js +221 -0
- package/dist/client/offline/queue.d.ts +118 -0
- package/dist/client/offline/queue.js +292 -0
- package/dist/client/offline/store.d.ts +74 -0
- package/dist/client/offline/store.js +78 -0
- package/dist/client/optimistic.d.ts +57 -0
- package/dist/client/optimistic.js +136 -0
- package/dist/client/report.d.ts +2 -0
- package/dist/client/report.js +8 -0
- package/dist/client/sessions/connection.d.ts +118 -0
- package/dist/client/sessions/connection.js +162 -0
- package/dist/client/sessions/feed.d.ts +42 -0
- package/dist/client/sessions/feed.js +43 -0
- package/dist/client/sessions/sse.d.ts +47 -0
- package/dist/client/sessions/sse.js +112 -0
- package/dist/client/sessions/stream.d.ts +28 -0
- package/dist/client/sessions/stream.js +35 -0
- package/dist/client/sessions/watch.d.ts +34 -0
- package/dist/client/sessions/watch.js +40 -0
- package/dist/client/transport.d.ts +37 -0
- package/dist/client/transport.js +66 -0
- package/dist/contexts/command.d.ts +132 -0
- package/dist/contexts/command.js +20 -0
- package/dist/contexts/connection.d.ts +68 -0
- package/dist/contexts/connection.js +0 -0
- package/dist/contexts/job.d.ts +49 -0
- package/dist/contexts/job.js +0 -0
- package/dist/contexts/workflow.d.ts +147 -0
- package/dist/contexts/workflow.js +22 -0
- package/dist/errors/actor.d.ts +200 -0
- package/dist/errors/actor.js +271 -0
- package/dist/errors/content.d.ts +19 -0
- package/dist/errors/content.js +14 -0
- package/dist/errors/database.d.ts +22 -0
- package/dist/errors/database.js +23 -0
- package/dist/errors/events.d.ts +17 -0
- package/dist/errors/events.js +14 -0
- package/dist/errors/subscription.d.ts +12 -0
- package/dist/errors/subscription.js +8 -0
- package/dist/errors/workflow.d.ts +28 -0
- package/dist/errors/workflow.js +17 -0
- package/dist/handles/actors.d.ts +17 -0
- package/dist/handles/actors.js +7 -0
- package/dist/handles/content.d.ts +59 -0
- package/dist/handles/content.js +36 -0
- package/dist/handles/intents.d.ts +164 -0
- package/dist/handles/intents.js +214 -0
- package/dist/handles/run.d.ts +31 -0
- package/dist/handles/run.js +21 -0
- package/dist/handles/workflow.d.ts +27 -0
- package/dist/handles/workflow.js +23 -0
- package/dist/identity/caller.d.ts +105 -0
- package/dist/identity/caller.js +68 -0
- package/dist/identity/child.d.ts +18 -0
- package/dist/identity/child.js +39 -0
- package/dist/identity/command.d.ts +19 -0
- package/dist/identity/command.js +21 -0
- package/dist/identity/connection.d.ts +24 -0
- package/dist/identity/connection.js +29 -0
- package/dist/identity/content.d.ts +18 -0
- package/dist/identity/content.js +12 -0
- package/dist/identity/digest.d.ts +4 -0
- package/dist/identity/digest.js +5 -0
- package/dist/identity/execution.d.ts +32 -0
- package/dist/identity/execution.js +63 -0
- package/dist/identity/mint.d.ts +51 -0
- package/dist/identity/mint.js +76 -0
- package/dist/identity/version.d.ts +2 -0
- package/dist/identity/version.js +3 -0
- package/dist/index.d.ts +159 -0
- package/dist/index.js +97 -0
- package/dist/members/blob.d.ts +43 -0
- package/dist/members/blob.js +29 -0
- package/dist/members/command.d.ts +109 -0
- package/dist/members/command.js +61 -0
- package/dist/members/connection.d.ts +55 -0
- package/dist/members/connection.js +49 -0
- package/dist/members/event.d.ts +36 -0
- package/dist/members/event.js +24 -0
- package/dist/members/job.d.ts +157 -0
- package/dist/members/job.js +67 -0
- package/dist/members/payload.d.ts +93 -0
- package/dist/members/payload.js +145 -0
- package/dist/members/reducer.d.ts +64 -0
- package/dist/members/reducer.js +27 -0
- package/dist/members/stream.d.ts +36 -0
- package/dist/members/stream.js +14 -0
- package/dist/members/subscription.d.ts +182 -0
- package/dist/members/subscription.js +91 -0
- package/dist/members/workflow.d.ts +99 -0
- package/dist/members/workflow.js +2 -0
- package/dist/policies/access.d.ts +35 -0
- package/dist/policies/access.js +6 -0
- package/dist/policies/command.d.ts +127 -0
- package/dist/policies/command.js +45 -0
- package/dist/policies/job.d.ts +28 -0
- package/dist/policies/job.js +60 -0
- package/dist/policies/schedules.d.ts +33 -0
- package/dist/policies/schedules.js +131 -0
- package/dist/protocol/frames.d.ts +66 -0
- package/dist/protocol/frames.js +55 -0
- package/dist/protocol/inspection.d.ts +807 -0
- package/dist/protocol/inspection.js +243 -0
- package/dist/protocol/wire.d.ts +184 -0
- package/dist/protocol/wire.js +150 -0
- package/dist/runtime/actors.d.ts +155 -0
- package/dist/runtime/actors.js +4 -0
- package/dist/runtime/admission.d.ts +34 -0
- package/dist/runtime/admission.js +72 -0
- package/dist/runtime/adoption/backfill.d.ts +27 -0
- package/dist/runtime/adoption/backfill.js +144 -0
- package/dist/runtime/adoption/enforce.d.ts +55 -0
- package/dist/runtime/adoption/enforce.js +241 -0
- package/dist/runtime/adoption/observe.d.ts +43 -0
- package/dist/runtime/adoption/observe.js +139 -0
- package/dist/runtime/adoption/plan.d.ts +45 -0
- package/dist/runtime/adoption/plan.js +192 -0
- package/dist/runtime/adoption/startup.d.ts +32 -0
- package/dist/runtime/adoption/startup.js +76 -0
- package/dist/runtime/adoption/status.d.ts +23 -0
- package/dist/runtime/adoption/status.js +47 -0
- package/dist/runtime/adoption/target.d.ts +33 -0
- package/dist/runtime/adoption/target.js +49 -0
- package/dist/runtime/connections/holder.d.ts +221 -0
- package/dist/runtime/connections/holder.js +816 -0
- package/dist/runtime/connections/owner.d.ts +263 -0
- package/dist/runtime/connections/owner.js +896 -0
- package/dist/runtime/connections/protocol.d.ts +441 -0
- package/dist/runtime/connections/protocol.js +222 -0
- package/dist/runtime/connections/reads.d.ts +24 -0
- package/dist/runtime/connections/reads.js +19 -0
- package/dist/runtime/connections/streams.d.ts +62 -0
- package/dist/runtime/connections/streams.js +128 -0
- package/dist/runtime/connections/transport.d.ts +40 -0
- package/dist/runtime/connections/transport.js +68 -0
- package/dist/runtime/connections/watch.d.ts +52 -0
- package/dist/runtime/connections/watch.js +159 -0
- package/dist/runtime/content/grant.d.ts +34 -0
- package/dist/runtime/content/grant.js +58 -0
- package/dist/runtime/content/store.d.ts +63 -0
- package/dist/runtime/content/store.js +294 -0
- package/dist/runtime/cron/key.d.ts +2 -0
- package/dist/runtime/cron/key.js +2 -0
- package/dist/runtime/cron/schedule.d.ts +69 -0
- package/dist/runtime/cron/schedule.js +144 -0
- package/dist/runtime/database/bounded.d.ts +43 -0
- package/dist/runtime/database/bounded.js +108 -0
- package/dist/runtime/database/connection.d.ts +8 -0
- package/dist/runtime/database/connection.js +18 -0
- package/dist/runtime/database/coordination.d.ts +19 -0
- package/dist/runtime/database/coordination.js +72 -0
- package/dist/runtime/database/flock.d.ts +10 -0
- package/dist/runtime/database/flock.js +60 -0
- package/dist/runtime/database/gate.d.ts +26 -0
- package/dist/runtime/database/gate.js +54 -0
- package/dist/runtime/database/keepalive.d.ts +11 -0
- package/dist/runtime/database/keepalive.js +22 -0
- package/dist/runtime/database/migrations.d.ts +177 -0
- package/dist/runtime/database/migrations.js +1018 -0
- package/dist/runtime/database/neki/migrations.d.ts +31 -0
- package/dist/runtime/database/neki/migrations.js +276 -0
- package/dist/runtime/database/neki/session.d.ts +23 -0
- package/dist/runtime/database/neki/session.js +31 -0
- package/dist/runtime/database/pglite.d.ts +21 -0
- package/dist/runtime/database/pglite.js +74 -0
- package/dist/runtime/database/replica.d.ts +41 -0
- package/dist/runtime/database/replica.js +51 -0
- package/dist/runtime/database/shards.d.ts +21 -0
- package/dist/runtime/database/shards.js +37 -0
- package/dist/runtime/database/snapshot.d.ts +7 -0
- package/dist/runtime/database/snapshot.js +7 -0
- package/dist/runtime/database/tenancy.d.ts +69 -0
- package/dist/runtime/database/tenancy.js +145 -0
- package/dist/runtime/database/transient.d.ts +10 -0
- package/dist/runtime/database/transient.js +25 -0
- package/dist/runtime/drain.d.ts +132 -0
- package/dist/runtime/drain.js +109 -0
- package/dist/runtime/entity/mailbox.d.ts +86 -0
- package/dist/runtime/entity/mailbox.js +121 -0
- package/dist/runtime/entity/register.d.ts +424 -0
- package/dist/runtime/entity/register.js +669 -0
- package/dist/runtime/events/append.d.ts +192 -0
- package/dist/runtime/events/append.js +128 -0
- package/dist/runtime/events/cursor.d.ts +2 -0
- package/dist/runtime/events/cursor.js +5 -0
- package/dist/runtime/events/replay.d.ts +20 -0
- package/dist/runtime/events/replay.js +45 -0
- package/dist/runtime/feeds.d.ts +26 -0
- package/dist/runtime/feeds.js +100 -0
- package/dist/runtime/fleet/checks.d.ts +17 -0
- package/dist/runtime/fleet/checks.js +106 -0
- package/dist/runtime/fleet/maintainer.d.ts +38 -0
- package/dist/runtime/fleet/maintainer.js +383 -0
- package/dist/runtime/fleet/pgoutput.d.ts +41 -0
- package/dist/runtime/fleet/pgoutput.js +102 -0
- package/dist/runtime/fleet/setup.d.ts +36 -0
- package/dist/runtime/fleet/setup.js +58 -0
- package/dist/runtime/fleet/subscribe.d.ts +42 -0
- package/dist/runtime/fleet/subscribe.js +147 -0
- package/dist/runtime/index.d.ts +66 -0
- package/dist/runtime/index.js +37 -0
- package/dist/runtime/inspector/http.d.ts +19 -0
- package/dist/runtime/inspector/http.js +36 -0
- package/dist/runtime/inspector/layer.d.ts +46 -0
- package/dist/runtime/inspector/layer.js +180 -0
- package/dist/runtime/inspector/queries.d.ts +711 -0
- package/dist/runtime/inspector/queries.js +494 -0
- package/dist/runtime/jobs/attempt.d.ts +254 -0
- package/dist/runtime/jobs/attempt.js +413 -0
- package/dist/runtime/jobs/isolation.d.ts +20 -0
- package/dist/runtime/jobs/isolation.js +10 -0
- package/dist/runtime/jobs/progress.d.ts +95 -0
- package/dist/runtime/jobs/progress.js +144 -0
- package/dist/runtime/layer.d.ts +260 -0
- package/dist/runtime/layer.js +876 -0
- package/dist/runtime/members.d.ts +381 -0
- package/dist/runtime/members.js +8 -0
- package/dist/runtime/operators/audit.d.ts +64 -0
- package/dist/runtime/operators/audit.js +56 -0
- package/dist/runtime/operators/auth.d.ts +29 -0
- package/dist/runtime/operators/auth.js +42 -0
- package/dist/runtime/operators/export.d.ts +68 -0
- package/dist/runtime/operators/export.js +137 -0
- package/dist/runtime/operators/grants.d.ts +45 -0
- package/dist/runtime/operators/grants.js +40 -0
- package/dist/runtime/operators/repair.d.ts +206 -0
- package/dist/runtime/operators/repair.js +226 -0
- package/dist/runtime/operators/routes.d.ts +35 -0
- package/dist/runtime/operators/routes.js +190 -0
- package/dist/runtime/operators/seed.d.ts +123 -0
- package/dist/runtime/operators/seed.js +123 -0
- package/dist/runtime/payloads/versions.d.ts +65 -0
- package/dist/runtime/payloads/versions.js +240 -0
- package/dist/runtime/peering/authority.d.ts +42 -0
- package/dist/runtime/peering/authority.js +132 -0
- package/dist/runtime/peering/credentials.d.ts +21 -0
- package/dist/runtime/peering/credentials.js +13 -0
- package/dist/runtime/peering/transport.d.ts +51 -0
- package/dist/runtime/peering/transport.js +175 -0
- package/dist/runtime/queries.d.ts +18 -0
- package/dist/runtime/queries.js +169 -0
- package/dist/runtime/readiness.d.ts +36 -0
- package/dist/runtime/readiness.js +44 -0
- package/dist/runtime/registration.d.ts +58 -0
- package/dist/runtime/registration.js +186 -0
- package/dist/runtime/request.d.ts +158 -0
- package/dist/runtime/request.js +102 -0
- package/dist/runtime/retry.d.ts +8 -0
- package/dist/runtime/retry.js +11 -0
- package/dist/runtime/runner.d.ts +93 -0
- package/dist/runtime/runner.js +122 -0
- package/dist/runtime/storage/codec.d.ts +39 -0
- package/dist/runtime/storage/codec.js +41 -0
- package/dist/runtime/storage/generation.d.ts +69 -0
- package/dist/runtime/storage/generation.js +49 -0
- package/dist/runtime/storage/placements.d.ts +15 -0
- package/dist/runtime/storage/placements.js +38 -0
- package/dist/runtime/storage/platform.d.ts +6 -0
- package/dist/runtime/storage/platform.js +9 -0
- package/dist/runtime/storage/retention.d.ts +73 -0
- package/dist/runtime/storage/retention.js +158 -0
- package/dist/runtime/subscriptions/identity.d.ts +31 -0
- package/dist/runtime/subscriptions/identity.js +45 -0
- package/dist/runtime/subscriptions/relay.d.ts +175 -0
- package/dist/runtime/subscriptions/relay.js +763 -0
- package/dist/runtime/subscriptions/storage.d.ts +40 -0
- package/dist/runtime/subscriptions/storage.js +98 -0
- package/dist/runtime/sweeps.d.ts +39 -0
- package/dist/runtime/sweeps.js +54 -0
- package/dist/runtime/telemetry/defects.d.ts +57 -0
- package/dist/runtime/telemetry/defects.js +43 -0
- package/dist/runtime/telemetry/metrics.d.ts +52 -0
- package/dist/runtime/telemetry/metrics.js +138 -0
- package/dist/runtime/telemetry/routes.d.ts +19 -0
- package/dist/runtime/telemetry/routes.js +18 -0
- package/dist/runtime/telemetry/sampler.d.ts +26 -0
- package/dist/runtime/telemetry/sampler.js +101 -0
- package/dist/runtime/telemetry/spans.d.ts +38 -0
- package/dist/runtime/telemetry/spans.js +44 -0
- package/dist/runtime/telemetry/usage.d.ts +52 -0
- package/dist/runtime/telemetry/usage.js +30 -0
- package/dist/runtime/topology/admission.d.ts +13 -0
- package/dist/runtime/topology/admission.js +36 -0
- package/dist/runtime/topology/locks.d.ts +56 -0
- package/dist/runtime/topology/locks.js +81 -0
- package/dist/runtime/topology/messages.d.ts +15 -0
- package/dist/runtime/topology/messages.js +32 -0
- package/dist/runtime/turn/admission.d.ts +22 -0
- package/dist/runtime/turn/admission.js +41 -0
- package/dist/runtime/turn/blobs.d.ts +38 -0
- package/dist/runtime/turn/blobs.js +215 -0
- package/dist/runtime/turn/execute.d.ts +208 -0
- package/dist/runtime/turn/execute.js +972 -0
- package/dist/runtime/turn/group.d.ts +165 -0
- package/dist/runtime/turn/group.js +326 -0
- package/dist/runtime/turn/hooks.d.ts +44 -0
- package/dist/runtime/turn/hooks.js +28 -0
- package/dist/runtime/turn/outbox.d.ts +159 -0
- package/dist/runtime/turn/outbox.js +269 -0
- package/dist/runtime/turn/pipeline.d.ts +68 -0
- package/dist/runtime/turn/pipeline.js +83 -0
- package/dist/runtime/turn/receipt.d.ts +81 -0
- package/dist/runtime/turn/receipt.js +32 -0
- package/dist/runtime/turn/relay.d.ts +179 -0
- package/dist/runtime/turn/relay.js +685 -0
- package/dist/runtime/turn/rows.d.ts +43 -0
- package/dist/runtime/turn/rows.js +592 -0
- package/dist/runtime/workflows/compatibility.d.ts +125 -0
- package/dist/runtime/workflows/compatibility.js +285 -0
- package/dist/runtime/workflows/engine.d.ts +107 -0
- package/dist/runtime/workflows/engine.js +826 -0
- package/dist/runtime/workflows/manifest.d.ts +34 -0
- package/dist/runtime/workflows/manifest.js +39 -0
- package/dist/runtime/workflows/steps.d.ts +33 -0
- package/dist/runtime/workflows/steps.js +152 -0
- package/dist/serve/api.d.ts +99 -0
- package/dist/serve/api.js +403 -0
- package/dist/serve/assertion/binding.d.ts +41 -0
- package/dist/serve/assertion/binding.js +92 -0
- package/dist/serve/assertion/verify.d.ts +93 -0
- package/dist/serve/assertion/verify.js +189 -0
- package/dist/serve/auth.d.ts +200 -0
- package/dist/serve/auth.js +92 -0
- package/dist/serve/clock.d.ts +10 -0
- package/dist/serve/clock.js +25 -0
- package/dist/serve/jwt.d.ts +65 -0
- package/dist/serve/jwt.js +202 -0
- package/dist/serve/layer.d.ts +98 -0
- package/dist/serve/layer.js +809 -0
- package/dist/serve/mcp/endpoint.d.ts +39 -0
- package/dist/serve/mcp/endpoint.js +187 -0
- package/dist/serve/mcp/tools.d.ts +37 -0
- package/dist/serve/mcp/tools.js +125 -0
- package/dist/serve/origin.d.ts +6 -0
- package/dist/serve/origin.js +21 -0
- package/dist/serve/sessions/feed.d.ts +79 -0
- package/dist/serve/sessions/feed.js +115 -0
- package/dist/serve/sessions/socket.d.ts +56 -0
- package/dist/serve/sessions/socket.js +212 -0
- package/dist/serve/sessions/sse.d.ts +27 -0
- package/dist/serve/sessions/sse.js +26 -0
- package/dist/serve/sessions/stream.d.ts +9 -0
- package/dist/serve/sessions/stream.js +9 -0
- package/dist/serve/sessions/watch.d.ts +12 -0
- package/dist/serve/sessions/watch.js +11 -0
- package/dist/serve/wire.d.ts +16 -0
- package/dist/serve/wire.js +50 -0
- package/dist/state/blob.d.ts +71 -0
- package/dist/state/blob.js +0 -0
- package/dist/state/migration.d.ts +53 -0
- package/dist/state/migration.js +29 -0
- package/dist/tables/fleet.d.ts +170 -0
- package/dist/tables/fleet.js +214 -0
- package/dist/tables/owned.d.ts +250 -0
- package/dist/tables/owned.js +233 -0
- package/dist/testing/actor-test.d.ts +360 -0
- package/dist/testing/actor-test.js +337 -0
- package/dist/testing/cluster.d.ts +93 -0
- package/dist/testing/cluster.js +296 -0
- package/dist/testing/conformance/access.d.ts +5 -0
- package/dist/testing/conformance/access.js +531 -0
- package/dist/testing/conformance/admission.d.ts +11 -0
- package/dist/testing/conformance/admission.js +182 -0
- package/dist/testing/conformance/adoption.d.ts +2 -0
- package/dist/testing/conformance/adoption.js +1019 -0
- package/dist/testing/conformance/assertions.d.ts +202 -0
- package/dist/testing/conformance/assertions.js +849 -0
- package/dist/testing/conformance/batches.d.ts +41 -0
- package/dist/testing/conformance/batches.js +388 -0
- package/dist/testing/conformance/blobs.d.ts +14 -0
- package/dist/testing/conformance/blobs.js +405 -0
- package/dist/testing/conformance/capacity.d.ts +8 -0
- package/dist/testing/conformance/capacity.js +433 -0
- package/dist/testing/conformance/client.d.ts +5 -0
- package/dist/testing/conformance/client.js +719 -0
- package/dist/testing/conformance/cold-serve.d.ts +16 -0
- package/dist/testing/conformance/cold-serve.js +254 -0
- package/dist/testing/conformance/connections/actors.d.ts +1143 -0
- package/dist/testing/conformance/connections/actors.js +180 -0
- package/dist/testing/conformance/connections/harness.d.ts +1754 -0
- package/dist/testing/conformance/connections/harness.js +142 -0
- package/dist/testing/conformance/connections/holders.d.ts +4 -0
- package/dist/testing/conformance/connections/holders.js +165 -0
- package/dist/testing/conformance/connections/lifecycle.d.ts +4 -0
- package/dist/testing/conformance/connections/lifecycle.js +113 -0
- package/dist/testing/conformance/connections/limits.d.ts +4 -0
- package/dist/testing/conformance/connections/limits.js +127 -0
- package/dist/testing/conformance/connections/reauthorization.d.ts +4 -0
- package/dist/testing/conformance/connections/reauthorization.js +170 -0
- package/dist/testing/conformance/connections/resync.d.ts +4 -0
- package/dist/testing/conformance/connections/resync.js +360 -0
- package/dist/testing/conformance/connections/takeover.d.ts +4 -0
- package/dist/testing/conformance/connections/takeover.js +287 -0
- package/dist/testing/conformance/connections.d.ts +4 -0
- package/dist/testing/conformance/connections.js +15 -0
- package/dist/testing/conformance/content-blobs.d.ts +16 -0
- package/dist/testing/conformance/content-blobs.js +534 -0
- package/dist/testing/conformance/counter.d.ts +16 -0
- package/dist/testing/conformance/counter.js +813 -0
- package/dist/testing/conformance/cron.d.ts +5 -0
- package/dist/testing/conformance/cron.js +665 -0
- package/dist/testing/conformance/drain.d.ts +26 -0
- package/dist/testing/conformance/drain.js +540 -0
- package/dist/testing/conformance/effect-control.d.ts +38 -0
- package/dist/testing/conformance/effect-control.js +1059 -0
- package/dist/testing/conformance/effects.d.ts +23 -0
- package/dist/testing/conformance/effects.js +432 -0
- package/dist/testing/conformance/events.d.ts +28 -0
- package/dist/testing/conformance/events.js +335 -0
- package/dist/testing/conformance/export.d.ts +2 -0
- package/dist/testing/conformance/export.js +368 -0
- package/dist/testing/conformance/fleet.d.ts +444 -0
- package/dist/testing/conformance/fleet.js +1009 -0
- package/dist/testing/conformance/groups.d.ts +7 -0
- package/dist/testing/conformance/groups.js +353 -0
- package/dist/testing/conformance/heap.d.ts +3 -0
- package/dist/testing/conformance/heap.js +149 -0
- package/dist/testing/conformance/http.d.ts +2809 -0
- package/dist/testing/conformance/http.js +1234 -0
- package/dist/testing/conformance/inspection-views.d.ts +8 -0
- package/dist/testing/conformance/inspection-views.js +272 -0
- package/dist/testing/conformance/inspector.d.ts +7 -0
- package/dist/testing/conformance/inspector.js +650 -0
- package/dist/testing/conformance/mint.d.ts +58 -0
- package/dist/testing/conformance/mint.js +692 -0
- package/dist/testing/conformance/multi-runner.d.ts +8 -0
- package/dist/testing/conformance/multi-runner.js +403 -0
- package/dist/testing/conformance/neki/plan.d.ts +19 -0
- package/dist/testing/conformance/neki/plan.js +30 -0
- package/dist/testing/conformance/observability.d.ts +3 -0
- package/dist/testing/conformance/observability.js +343 -0
- package/dist/testing/conformance/offline.d.ts +4 -0
- package/dist/testing/conformance/offline.js +474 -0
- package/dist/testing/conformance/operator-harness.d.ts +57 -0
- package/dist/testing/conformance/operator-harness.js +78 -0
- package/dist/testing/conformance/operator.d.ts +3 -0
- package/dist/testing/conformance/operator.js +379 -0
- package/dist/testing/conformance/outbox-cross-shard.d.ts +14 -0
- package/dist/testing/conformance/outbox-cross-shard.js +160 -0
- package/dist/testing/conformance/outbox.d.ts +36 -0
- package/dist/testing/conformance/outbox.js +437 -0
- package/dist/testing/conformance/payload-migrations.d.ts +3 -0
- package/dist/testing/conformance/payload-migrations.js +612 -0
- package/dist/testing/conformance/pipeline.d.ts +3 -0
- package/dist/testing/conformance/pipeline.js +1040 -0
- package/dist/testing/conformance/placement.d.ts +22 -0
- package/dist/testing/conformance/placement.js +539 -0
- package/dist/testing/conformance/platform.d.ts +40 -0
- package/dist/testing/conformance/platform.js +101 -0
- package/dist/testing/conformance/progress.d.ts +39 -0
- package/dist/testing/conformance/progress.js +666 -0
- package/dist/testing/conformance/properties.d.ts +6 -0
- package/dist/testing/conformance/properties.js +295 -0
- package/dist/testing/conformance/protocols.d.ts +4 -0
- package/dist/testing/conformance/protocols.js +618 -0
- package/dist/testing/conformance/provider/advertise.d.ts +21 -0
- package/dist/testing/conformance/provider/advertise.js +44 -0
- package/dist/testing/conformance/read-your-writes.d.ts +20 -0
- package/dist/testing/conformance/read-your-writes.js +192 -0
- package/dist/testing/conformance/reducers.d.ts +8 -0
- package/dist/testing/conformance/reducers.js +258 -0
- package/dist/testing/conformance/relay.d.ts +42 -0
- package/dist/testing/conformance/relay.js +1115 -0
- package/dist/testing/conformance/restore.d.ts +43 -0
- package/dist/testing/conformance/restore.js +341 -0
- package/dist/testing/conformance/retention.d.ts +14 -0
- package/dist/testing/conformance/retention.js +463 -0
- package/dist/testing/conformance/rls.d.ts +24 -0
- package/dist/testing/conformance/rls.js +576 -0
- package/dist/testing/conformance/simulation.d.ts +31 -0
- package/dist/testing/conformance/simulation.js +167 -0
- package/dist/testing/conformance/single-shard.d.ts +2 -0
- package/dist/testing/conformance/single-shard.js +125 -0
- package/dist/testing/conformance/singleton.d.ts +3 -0
- package/dist/testing/conformance/singleton.js +293 -0
- package/dist/testing/conformance/statements.d.ts +60 -0
- package/dist/testing/conformance/statements.js +108 -0
- package/dist/testing/conformance/streams.d.ts +7 -0
- package/dist/testing/conformance/streams.js +280 -0
- package/dist/testing/conformance/subscriptions/access.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/access.js +98 -0
- package/dist/testing/conformance/subscriptions/actors.d.ts +4910 -0
- package/dist/testing/conformance/subscriptions/actors.js +372 -0
- package/dist/testing/conformance/subscriptions/cluster.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/cluster.js +89 -0
- package/dist/testing/conformance/subscriptions/delivery.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/delivery.js +253 -0
- package/dist/testing/conformance/subscriptions/deploys.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/deploys.js +162 -0
- package/dist/testing/conformance/subscriptions/epochs.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/epochs.js +169 -0
- package/dist/testing/conformance/subscriptions/failures.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/failures.js +108 -0
- package/dist/testing/conformance/subscriptions/feeds.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/feeds.js +218 -0
- package/dist/testing/conformance/subscriptions/harness.d.ts +124 -0
- package/dist/testing/conformance/subscriptions/harness.js +143 -0
- package/dist/testing/conformance/subscriptions/operator.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/operator.js +128 -0
- package/dist/testing/conformance/subscriptions/retention.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/retention.js +288 -0
- package/dist/testing/conformance/subscriptions/routing.d.ts +4 -0
- package/dist/testing/conformance/subscriptions/routing.js +179 -0
- package/dist/testing/conformance/subscriptions.d.ts +4 -0
- package/dist/testing/conformance/subscriptions.js +19 -0
- package/dist/testing/conformance/tables.d.ts +125 -0
- package/dist/testing/conformance/tables.js +664 -0
- package/dist/testing/conformance/transports/actors.d.ts +1907 -0
- package/dist/testing/conformance/transports/actors.js +163 -0
- package/dist/testing/conformance/transports/client.d.ts +3 -0
- package/dist/testing/conformance/transports/client.js +237 -0
- package/dist/testing/conformance/transports/feeds.d.ts +3 -0
- package/dist/testing/conformance/transports/feeds.js +232 -0
- package/dist/testing/conformance/transports/harness.d.ts +93 -0
- package/dist/testing/conformance/transports/harness.js +84 -0
- package/dist/testing/conformance/transports/streams.d.ts +3 -0
- package/dist/testing/conformance/transports/streams.js +96 -0
- package/dist/testing/conformance/transports/websocket.d.ts +3 -0
- package/dist/testing/conformance/transports/websocket.js +384 -0
- package/dist/testing/conformance/transports/wire.d.ts +141 -0
- package/dist/testing/conformance/transports/wire.js +204 -0
- package/dist/testing/conformance/transports.d.ts +3 -0
- package/dist/testing/conformance/transports.js +11 -0
- package/dist/testing/conformance/watch.d.ts +59 -0
- package/dist/testing/conformance/watch.js +879 -0
- package/dist/testing/conformance/workflow-engine.d.ts +97 -0
- package/dist/testing/conformance/workflow-engine.js +253 -0
- package/dist/testing/conformance/workflow-versions.d.ts +3 -0
- package/dist/testing/conformance/workflow-versions.js +545 -0
- package/dist/testing/conformance/workflows/access.d.ts +4 -0
- package/dist/testing/conformance/workflows/access.js +147 -0
- package/dist/testing/conformance/workflows/actors.d.ts +2317 -0
- package/dist/testing/conformance/workflows/actors.js +305 -0
- package/dist/testing/conformance/workflows/basics.d.ts +4 -0
- package/dist/testing/conformance/workflows/basics.js +144 -0
- package/dist/testing/conformance/workflows/engine.d.ts +4 -0
- package/dist/testing/conformance/workflows/engine.js +80 -0
- package/dist/testing/conformance/workflows/guards.d.ts +4 -0
- package/dist/testing/conformance/workflows/guards.js +172 -0
- package/dist/testing/conformance/workflows/harness.d.ts +14 -0
- package/dist/testing/conformance/workflows/harness.js +44 -0
- package/dist/testing/conformance/workflows/recovery.d.ts +4 -0
- package/dist/testing/conformance/workflows/recovery.js +181 -0
- package/dist/testing/conformance/workflows/starts.d.ts +4 -0
- package/dist/testing/conformance/workflows/starts.js +57 -0
- package/dist/testing/conformance/workflows/suspension.d.ts +4 -0
- package/dist/testing/conformance/workflows/suspension.js +106 -0
- package/dist/testing/conformance/workflows/waits.d.ts +4 -0
- package/dist/testing/conformance/workflows/waits.js +284 -0
- package/dist/testing/conformance/workflows.d.ts +4 -0
- package/dist/testing/conformance/workflows.js +19 -0
- package/dist/testing/conformance.d.ts +330 -0
- package/dist/testing/conformance.js +364 -0
- package/dist/testing/database.d.ts +31 -0
- package/dist/testing/database.js +70 -0
- package/dist/testing/foundation.d.ts +41 -0
- package/dist/testing/foundation.js +517 -0
- package/dist/testing/index.d.ts +20 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/property.d.ts +40 -0
- package/dist/testing/property.js +95 -0
- package/dist/testing/simulate-cluster.d.ts +112 -0
- package/dist/testing/simulate-cluster.js +257 -0
- package/dist/testing/simulate.d.ts +90 -0
- package/dist/testing/simulate.js +127 -0
- package/package.json +1 -0
|
@@ -0,0 +1,1019 @@
|
|
|
1
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
2
|
+
import { customType, integer, pgTable, text, uuid } from "drizzle-orm/pg-core";
|
|
3
|
+
import { Cause, Clock, Crypto, Effect, Exit, Layer, Redacted, Schema } from "effect";
|
|
4
|
+
import { SqlClient, SqlError } from "effect/sql";
|
|
5
|
+
import { Actor } from "../../index.js";
|
|
6
|
+
import { backfillAdoption } from "../../runtime/adoption/backfill.js";
|
|
7
|
+
import { enforceAdoption, releaseAdoption } from "../../runtime/adoption/enforce.js";
|
|
8
|
+
import { adoptionWriters, observeAdoption } from "../../runtime/adoption/observe.js";
|
|
9
|
+
import { ownerIndexSql, planAdoption } from "../../runtime/adoption/plan.js";
|
|
10
|
+
import { AdoptionRefused } from "../../runtime/adoption/target.js";
|
|
11
|
+
import { migrate } from "../../runtime/database/migrations.js";
|
|
12
|
+
import { Database } from "../../runtime/layer.js";
|
|
13
|
+
import { ActorTest } from "../actor-test.js";
|
|
14
|
+
const invoices = pgTable("conformance_invoices", {
|
|
15
|
+
id: text("id").primaryKey(),
|
|
16
|
+
orgId: text("org_id").notNull(),
|
|
17
|
+
accountId: text("account_id").notNull(),
|
|
18
|
+
amount: integer("amount").notNull().default(0),
|
|
19
|
+
note: text("note"),
|
|
20
|
+
});
|
|
21
|
+
const invoiceRows = Actor.table(invoices, {
|
|
22
|
+
owner: { tenant: invoices.orgId, actor: invoices.accountId },
|
|
23
|
+
});
|
|
24
|
+
const contacts = pgTable("conformance_contacts", {
|
|
25
|
+
id: text("id").primaryKey(),
|
|
26
|
+
tenant: text("tenant").notNull(),
|
|
27
|
+
owner: text("owner").notNull(),
|
|
28
|
+
name: text("name").notNull(),
|
|
29
|
+
});
|
|
30
|
+
const contactRows = Actor.table(contacts, {
|
|
31
|
+
owner: { tenant: contacts.tenant, actor: contacts.owner },
|
|
32
|
+
access: "read",
|
|
33
|
+
});
|
|
34
|
+
const shipments = pgTable("conformance_shipments", {
|
|
35
|
+
id: text("id").primaryKey(),
|
|
36
|
+
tenantUuid: uuid("tenant_uuid").notNull(),
|
|
37
|
+
actorUuid: uuid("actor_uuid").notNull(),
|
|
38
|
+
label: text("label").notNull(),
|
|
39
|
+
});
|
|
40
|
+
const shipmentRows = Actor.table(shipments, {
|
|
41
|
+
owner: { tenant: shipments.tenantUuid, actor: shipments.actorUuid },
|
|
42
|
+
});
|
|
43
|
+
const legacyDdl = [
|
|
44
|
+
`CREATE TABLE conformance_invoices (
|
|
45
|
+
id text PRIMARY KEY,
|
|
46
|
+
org_id text NOT NULL,
|
|
47
|
+
account_id text NOT NULL,
|
|
48
|
+
amount integer NOT NULL DEFAULT 0,
|
|
49
|
+
note text)`,
|
|
50
|
+
`CREATE TABLE conformance_contacts (
|
|
51
|
+
id text PRIMARY KEY,
|
|
52
|
+
tenant text NOT NULL,
|
|
53
|
+
owner text NOT NULL,
|
|
54
|
+
name text NOT NULL)`,
|
|
55
|
+
`CREATE INDEX conformance_contacts_owner ON conformance_contacts (tenant, owner)`,
|
|
56
|
+
`CREATE TABLE conformance_shipments (
|
|
57
|
+
id text PRIMARY KEY,
|
|
58
|
+
tenant_uuid uuid NOT NULL,
|
|
59
|
+
actor_uuid uuid NOT NULL,
|
|
60
|
+
label text NOT NULL)`,
|
|
61
|
+
];
|
|
62
|
+
const Line = Schema.Struct({
|
|
63
|
+
id: Schema.String,
|
|
64
|
+
amount: Schema.Int,
|
|
65
|
+
note: Schema.NullOr(Schema.String),
|
|
66
|
+
});
|
|
67
|
+
const Add = Actor.command("Add", {
|
|
68
|
+
payload: Schema.Struct({ id: Schema.String, amount: Schema.Int }),
|
|
69
|
+
});
|
|
70
|
+
const Put = Actor.command("Put", {
|
|
71
|
+
payload: Schema.Array(Schema.Struct({ id: Schema.String, amount: Schema.Int })),
|
|
72
|
+
});
|
|
73
|
+
const Bump = Actor.command("Bump", {
|
|
74
|
+
payload: Schema.Struct({ id: Schema.String, amount: Schema.Int }),
|
|
75
|
+
});
|
|
76
|
+
const Remove = Actor.command("Remove", { payload: Schema.String });
|
|
77
|
+
const Lines = Actor.query("Lines", { success: Schema.Array(Line) });
|
|
78
|
+
const Find = Actor.query("Find", { payload: Schema.String, success: Schema.Array(Line) });
|
|
79
|
+
const Account = Actor.make("Account", {
|
|
80
|
+
key: Schema.String,
|
|
81
|
+
tables: [invoiceRows],
|
|
82
|
+
api: { Add, Put, Bump, Remove, Lines, Find },
|
|
83
|
+
});
|
|
84
|
+
const Names = Actor.query("Names", { success: Schema.Array(Schema.String) });
|
|
85
|
+
const Peek = Actor.command("Peek", { success: Schema.Array(Schema.String) });
|
|
86
|
+
const Probe = Actor.command("Probe", { success: Schema.Boolean });
|
|
87
|
+
const Grouped = Actor.command("Grouped");
|
|
88
|
+
const Directory = Actor.make("Directory", {
|
|
89
|
+
key: Schema.String,
|
|
90
|
+
tables: [contactRows],
|
|
91
|
+
api: { Names, Peek, Probe, Grouped },
|
|
92
|
+
});
|
|
93
|
+
const Ship = Actor.command("Ship", {
|
|
94
|
+
payload: Schema.Struct({ id: Schema.String, label: Schema.String }),
|
|
95
|
+
});
|
|
96
|
+
const Labels = Actor.query("Labels", { success: Schema.Array(Schema.String) });
|
|
97
|
+
const Carrier = Actor.make("Carrier", {
|
|
98
|
+
key: Schema.String,
|
|
99
|
+
tables: [shipmentRows],
|
|
100
|
+
api: { Ship, Labels },
|
|
101
|
+
});
|
|
102
|
+
const adopting = [Account, Directory, Carrier];
|
|
103
|
+
const accountLayer = Layer.mergeAll(Account.toLayer(Effect.succeed({
|
|
104
|
+
Add: Effect.fnUntraced(function* (input) {
|
|
105
|
+
const turn = yield* Account.Turn;
|
|
106
|
+
yield* turn.rows(invoiceRows).insert(input);
|
|
107
|
+
}),
|
|
108
|
+
Put: Effect.fnUntraced(function* (input) {
|
|
109
|
+
const turn = yield* Account.Turn;
|
|
110
|
+
yield* turn.rows(invoiceRows).upsert(input);
|
|
111
|
+
}),
|
|
112
|
+
Bump: Effect.fnUntraced(function* (input) {
|
|
113
|
+
const turn = yield* Account.Turn;
|
|
114
|
+
yield* turn.rows(invoiceRows).update({ amount: input.amount }).where({ id: input.id });
|
|
115
|
+
}),
|
|
116
|
+
Remove: Effect.fnUntraced(function* (id) {
|
|
117
|
+
const turn = yield* Account.Turn;
|
|
118
|
+
yield* turn.rows(invoiceRows).delete().where({ id });
|
|
119
|
+
}),
|
|
120
|
+
})), Account.toQueryLayer(Effect.succeed({
|
|
121
|
+
Lines: Effect.fnUntraced(function* () {
|
|
122
|
+
const read = yield* Account.Read;
|
|
123
|
+
const found = yield* read.rows(invoiceRows).all({ orderBy: { id: "asc" } });
|
|
124
|
+
return found.map(({ id, amount, note }) => ({ id, amount, note }));
|
|
125
|
+
}),
|
|
126
|
+
Find: Effect.fnUntraced(function* (id) {
|
|
127
|
+
const read = yield* Account.Read;
|
|
128
|
+
const found = yield* read.rows(invoiceRows).all({ where: { id } });
|
|
129
|
+
return found.map(({ id, amount, note }) => ({ id, amount, note }));
|
|
130
|
+
}),
|
|
131
|
+
})));
|
|
132
|
+
const directoryLayer = Layer.mergeAll(Directory.toLayer(Effect.succeed({
|
|
133
|
+
Peek: Effect.fnUntraced(function* () {
|
|
134
|
+
const turn = yield* Directory.Turn;
|
|
135
|
+
const found = yield* turn.rows(contactRows).all({ orderBy: { id: "asc" } });
|
|
136
|
+
return found.map(({ name }) => name);
|
|
137
|
+
}),
|
|
138
|
+
Probe: Effect.fnUntraced(function* () {
|
|
139
|
+
const turn = yield* Directory.Turn;
|
|
140
|
+
return "insert" in turn.rows(contactRows) || "update" in turn.rows(contactRows);
|
|
141
|
+
}),
|
|
142
|
+
Grouped: Effect.fnUntraced(function* () {
|
|
143
|
+
const turn = yield* Directory.Turn;
|
|
144
|
+
yield* turn.group((database) => database.select().from(contacts));
|
|
145
|
+
}),
|
|
146
|
+
})), Directory.toQueryLayer(Effect.succeed({
|
|
147
|
+
Names: Effect.fnUntraced(function* () {
|
|
148
|
+
const read = yield* Directory.Read;
|
|
149
|
+
const found = yield* read.rows(contactRows).all({ orderBy: { id: "asc" } });
|
|
150
|
+
return found.map(({ name }) => name);
|
|
151
|
+
}),
|
|
152
|
+
})));
|
|
153
|
+
const carrierLayer = Layer.mergeAll(Carrier.toLayer(Effect.succeed({
|
|
154
|
+
Ship: Effect.fnUntraced(function* (input) {
|
|
155
|
+
const turn = yield* Carrier.Turn;
|
|
156
|
+
yield* turn.rows(shipmentRows).insert(input);
|
|
157
|
+
}),
|
|
158
|
+
})), Carrier.toQueryLayer(Effect.succeed({
|
|
159
|
+
Labels: Effect.fnUntraced(function* () {
|
|
160
|
+
const read = yield* Carrier.Read;
|
|
161
|
+
const found = yield* read.rows(shipmentRows).all({ orderBy: { id: "asc" } });
|
|
162
|
+
return found.map(({ label }) => label);
|
|
163
|
+
}),
|
|
164
|
+
})));
|
|
165
|
+
const live = Layer.mergeAll(accountLayer, directoryLayer, carrierLayer);
|
|
166
|
+
const databaseLayer = (target) => Redacted.isRedacted(target)
|
|
167
|
+
? Database.postgres({ url: target, maxConnections: 2, offTurnConnections: 2 })
|
|
168
|
+
: Database.pglite(target);
|
|
169
|
+
const onDatabase = (target, effect) => Layer.build(databaseLayer(target)).pipe(Effect.flatMap((context) => Effect.provideContext(effect, context)), Effect.scoped, Effect.orDie);
|
|
170
|
+
/** A migrated fresh database holding the application's existing tables, observed unless `observe` is false. */
|
|
171
|
+
const prepared = (environment, options = { observe: true }) => Effect.gen(function* () {
|
|
172
|
+
const fresh = yield* environment.freshDatabase;
|
|
173
|
+
const target = Redacted.isRedacted(fresh)
|
|
174
|
+
? fresh
|
|
175
|
+
: {
|
|
176
|
+
liveClient: yield* Effect.acquireRelease(Effect.sync(() => new PGlite()).pipe(Effect.tap((client) => Effect.promise(() => client.waitReady))), (client) => Effect.promise(() => client.query("SELECT 1")).pipe(Effect.andThen(Effect.promise(() => client.close())))),
|
|
177
|
+
};
|
|
178
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
179
|
+
const sql = yield* SqlClient.SqlClient;
|
|
180
|
+
yield* migrate;
|
|
181
|
+
for (const statement of legacyDdl)
|
|
182
|
+
yield* sql.unsafe(statement);
|
|
183
|
+
if (!options.observe)
|
|
184
|
+
return;
|
|
185
|
+
yield* observeAdoption(adopting);
|
|
186
|
+
if (options.index === false)
|
|
187
|
+
return;
|
|
188
|
+
for (const table of ["conformance_invoices", "conformance_shipments"])
|
|
189
|
+
yield* sql.unsafe(ownerIndexSql({
|
|
190
|
+
schema: "public",
|
|
191
|
+
table,
|
|
192
|
+
access: "write",
|
|
193
|
+
tenantColumn: table === "conformance_invoices" ? "org_id" : "tenant_uuid",
|
|
194
|
+
actorColumn: table === "conformance_invoices" ? "account_id" : "actor_uuid",
|
|
195
|
+
}).replace("CONCURRENTLY ", ""));
|
|
196
|
+
}));
|
|
197
|
+
return target;
|
|
198
|
+
});
|
|
199
|
+
const runtimeOn = (target, options = {}) => Effect.gen(function* () {
|
|
200
|
+
const crypto = yield* Crypto.Crypto;
|
|
201
|
+
return yield* Layer.build(Layer.fresh(live.pipe(Layer.provideMerge(ActorTest.layer({
|
|
202
|
+
database: Redacted.isRedacted(target) ? target : { liveClient: target.liveClient },
|
|
203
|
+
...options,
|
|
204
|
+
})), Layer.provide(Layer.succeed(Crypto.Crypto, crypto)))));
|
|
205
|
+
});
|
|
206
|
+
/** Runs `body` on a runtime over a fresh database that already holds the application's tables. */
|
|
207
|
+
const withAdoption = (environment, options, body) => environment.run(Effect.gen(function* () {
|
|
208
|
+
const target = yield* prepared(environment, options);
|
|
209
|
+
const context = yield* runtimeOn(target).pipe(Effect.orDie);
|
|
210
|
+
return yield* body({ target }).pipe(Effect.provideContext(context));
|
|
211
|
+
}));
|
|
212
|
+
const refusal = (declare) => {
|
|
213
|
+
try {
|
|
214
|
+
declare();
|
|
215
|
+
return "succeeded";
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
return error instanceof Error ? error.message : String(error);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
const defect = (exit) => Exit.isFailure(exit) ? Cause.pretty(exit.cause) : "succeeded";
|
|
222
|
+
const legacyInsert = (rows) => Effect.gen(function* () {
|
|
223
|
+
const sql = yield* SqlClient.SqlClient;
|
|
224
|
+
for (const row of rows)
|
|
225
|
+
yield* sql `INSERT INTO conformance_invoices (id, org_id, account_id, amount)
|
|
226
|
+
VALUES (${row.id}, ${row.org}, ${row.account}, ${row.amount ?? 0})`;
|
|
227
|
+
});
|
|
228
|
+
const invoiceRow = (id) => Effect.gen(function* () {
|
|
229
|
+
const sql = yield* SqlClient.SqlClient;
|
|
230
|
+
return yield* sql `SELECT org_id, account_id, amount, routing_key::text AS routing_key
|
|
231
|
+
FROM conformance_invoices WHERE id = ${id}`;
|
|
232
|
+
});
|
|
233
|
+
const actorRoutingKey = (tenant, actor, id) => Effect.gen(function* () {
|
|
234
|
+
const sql = yield* SqlClient.SqlClient;
|
|
235
|
+
return (yield* sql `
|
|
236
|
+
SELECT routing_key::text AS routing_key FROM actor_generations
|
|
237
|
+
WHERE tenant_id = ${tenant} AND actor_type = ${actor} AND actor_id = ${id}`)[0]?.routing_key;
|
|
238
|
+
});
|
|
239
|
+
const TENANT = "adopt-tenant";
|
|
240
|
+
const QUIET_MS = 7 * 86_400_000;
|
|
241
|
+
/** The login a legacy application would use: `role`, with an application name of its own. */
|
|
242
|
+
const loginAs = (target, role, effect) => {
|
|
243
|
+
const url = new URL(Redacted.value(target));
|
|
244
|
+
url.username = role;
|
|
245
|
+
url.password = "legacy";
|
|
246
|
+
url.searchParams.set("application_name", "legacy-app");
|
|
247
|
+
return onDatabase(Redacted.make(url.href), effect);
|
|
248
|
+
};
|
|
249
|
+
/**
|
|
250
|
+
* The roles of a brownfield deployment: an owner no login can act as, a writer
|
|
251
|
+
* the runtime takes, and two logins that write the table directly. The owner
|
|
252
|
+
* takes the invoices table, and every role is dropped when the scope closes.
|
|
253
|
+
*/
|
|
254
|
+
const provisionRoles = (target) => Effect.gen(function* () {
|
|
255
|
+
const crypto = yield* Crypto.Crypto;
|
|
256
|
+
const suffix = (yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "").slice(0, 12);
|
|
257
|
+
const roles = {
|
|
258
|
+
owner: `adopt_owner_${suffix}`,
|
|
259
|
+
writer: `adopt_writer_${suffix}`,
|
|
260
|
+
legacy: `adopt_legacy_${suffix}`,
|
|
261
|
+
batch: `adopt_batch_${suffix}`,
|
|
262
|
+
};
|
|
263
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
264
|
+
const sql = yield* SqlClient.SqlClient;
|
|
265
|
+
for (const statement of [
|
|
266
|
+
`CREATE ROLE ${roles.owner} NOLOGIN`,
|
|
267
|
+
`CREATE ROLE ${roles.writer} NOLOGIN`,
|
|
268
|
+
`CREATE ROLE ${roles.legacy} LOGIN PASSWORD 'legacy'`,
|
|
269
|
+
`CREATE ROLE ${roles.batch} LOGIN PASSWORD 'legacy'`,
|
|
270
|
+
`GRANT ${roles.writer} TO CURRENT_USER`,
|
|
271
|
+
`GRANT USAGE ON SCHEMA public TO ${roles.writer}, ${roles.legacy}, ${roles.batch}`,
|
|
272
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public TO ${roles.writer}`,
|
|
273
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE, TRUNCATE ON conformance_invoices TO ${roles.legacy}`,
|
|
274
|
+
`GRANT SELECT, INSERT, UPDATE, DELETE ON conformance_invoices TO ${roles.batch}`,
|
|
275
|
+
`ALTER TABLE conformance_invoices OWNER TO ${roles.owner}`,
|
|
276
|
+
])
|
|
277
|
+
yield* sql.unsafe(statement);
|
|
278
|
+
}));
|
|
279
|
+
yield* Effect.addFinalizer(() => onDatabase(target, Effect.gen(function* () {
|
|
280
|
+
const sql = yield* SqlClient.SqlClient;
|
|
281
|
+
for (const role of Object.values(roles)) {
|
|
282
|
+
yield* sql.unsafe(`REASSIGN OWNED BY ${role} TO CURRENT_USER`);
|
|
283
|
+
yield* sql.unsafe(`DROP OWNED BY ${role}`);
|
|
284
|
+
yield* sql.unsafe(`DROP ROLE ${role}`);
|
|
285
|
+
}
|
|
286
|
+
})));
|
|
287
|
+
return roles;
|
|
288
|
+
});
|
|
289
|
+
/** A Postgres database with the roles and two legacy rows written by the legacy login while the table was observed. */
|
|
290
|
+
const observedWithRoles = (environment) => Effect.gen(function* () {
|
|
291
|
+
const target = yield* prepared(environment, { observe: true });
|
|
292
|
+
if (!Redacted.isRedacted(target))
|
|
293
|
+
return yield* Effect.die(new Error("Enforcement cases need Postgres roles and logins"));
|
|
294
|
+
const roles = yield* provisionRoles(target);
|
|
295
|
+
yield* loginAs(target, roles.legacy, legacyInsert([
|
|
296
|
+
{ id: "inv-1", org: TENANT, account: "acct-a", amount: 10 },
|
|
297
|
+
{ id: "inv-2", org: TENANT, account: "acct-b", amount: 20 },
|
|
298
|
+
]));
|
|
299
|
+
return { target, roles };
|
|
300
|
+
});
|
|
301
|
+
/** Backfills, clears what the legacy login wrote, ages the observation past the quiet window, and enforces. */
|
|
302
|
+
const enforceTable = (roles, allow = []) => Effect.gen(function* () {
|
|
303
|
+
const sql = yield* SqlClient.SqlClient;
|
|
304
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
305
|
+
yield* adoptionWriters(adopting, { only: "conformance_invoices", clear: true });
|
|
306
|
+
yield* sql `UPDATE actor_adoptions SET changed_at_ms = changed_at_ms - ${QUIET_MS + 86_400_000}`;
|
|
307
|
+
return yield* enforceAdoption(adopting, {
|
|
308
|
+
only: "conformance_invoices",
|
|
309
|
+
writerRole: roles.writer,
|
|
310
|
+
allowedRoles: allow,
|
|
311
|
+
quietMs: QUIET_MS,
|
|
312
|
+
nowMs: yield* Clock.currentTimeMillis,
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
/** An enforced table, and a runtime whose turns take the writer role. */
|
|
316
|
+
const withEnforcement = (environment, options, body) => environment.run(Effect.gen(function* () {
|
|
317
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
318
|
+
yield* onDatabase(target, enforceTable(roles, options.allow));
|
|
319
|
+
const context = yield* runtimeOn(target, { adoption: { role: roles.writer } }).pipe(Effect.orDie);
|
|
320
|
+
return yield* body({ target, roles }).pipe(Effect.provideContext(context));
|
|
321
|
+
}));
|
|
322
|
+
/** What a statement did: `succeeded`, the SQL error class, or `other`, with the message the server gave. */
|
|
323
|
+
const outcome = (effect) => Effect.gen(function* () {
|
|
324
|
+
const exit = yield* Effect.exit(effect);
|
|
325
|
+
if (Exit.isSuccess(exit))
|
|
326
|
+
return { reason: "succeeded", text: "" };
|
|
327
|
+
const error = Cause.squash(exit.cause);
|
|
328
|
+
return {
|
|
329
|
+
reason: SqlError.isSqlError(error) ? error.reason._tag : "other",
|
|
330
|
+
text: Cause.pretty(exit.cause),
|
|
331
|
+
};
|
|
332
|
+
});
|
|
333
|
+
const account = (id) => Account.get(id).pipe(Actor.tenant(TENANT));
|
|
334
|
+
/** Runs `effect` inside a transaction as `role`, the way a turn takes the writer role. */
|
|
335
|
+
const asRole = (role, effect) => Effect.gen(function* () {
|
|
336
|
+
const sql = yield* SqlClient.SqlClient;
|
|
337
|
+
return yield* Effect.andThen(sql `SELECT set_config('role', ${role}, true)`, effect).pipe(sql.withTransaction);
|
|
338
|
+
});
|
|
339
|
+
const invoiceIds = Effect.gen(function* () {
|
|
340
|
+
const sql = yield* SqlClient.SqlClient;
|
|
341
|
+
return (yield* sql `SELECT id FROM conformance_invoices ORDER BY id`).map(({ id }) => id);
|
|
342
|
+
});
|
|
343
|
+
export const adoptionConformance = [
|
|
344
|
+
{
|
|
345
|
+
name: "maps existing tenant and actor columns, reads only the actor's rows, and never shows another tenant's or actor's rows for the same business key",
|
|
346
|
+
requiresFreshDatabase: true,
|
|
347
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, () => Effect.gen(function* () {
|
|
348
|
+
const test = yield* ActorTest;
|
|
349
|
+
const tenant = test.tenant;
|
|
350
|
+
const other = `${tenant}-other`;
|
|
351
|
+
yield* legacyInsert([
|
|
352
|
+
{ id: "inv-1", org: tenant, account: "acct-a", amount: 10 },
|
|
353
|
+
{ id: "inv-2", org: tenant, account: "acct-b", amount: 20 },
|
|
354
|
+
{ id: "inv-3", org: other, account: "acct-a", amount: 30 },
|
|
355
|
+
]);
|
|
356
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
357
|
+
const a = yield* Account.get("acct-a");
|
|
358
|
+
const b = yield* Account.get("acct-b");
|
|
359
|
+
const abroad = yield* Account.get("acct-a").pipe(Actor.tenant(other));
|
|
360
|
+
expect(yield* a.Lines()).toEqual([{ id: "inv-1", amount: 10, note: null }]);
|
|
361
|
+
expect(yield* b.Lines()).toEqual([{ id: "inv-2", amount: 20, note: null }]);
|
|
362
|
+
expect(yield* abroad.Lines()).toEqual([{ id: "inv-3", amount: 30, note: null }]);
|
|
363
|
+
expect(yield* b.Find("inv-1")).toEqual([]);
|
|
364
|
+
expect(yield* a.Find("inv-2")).toEqual([]);
|
|
365
|
+
expect(yield* a.Find("inv-3")).toEqual([]);
|
|
366
|
+
yield* a.Add({ id: "inv-4", amount: 40 });
|
|
367
|
+
yield* a.Bump({ id: "inv-1", amount: 11 });
|
|
368
|
+
yield* b.Bump({ id: "inv-1", amount: 99 });
|
|
369
|
+
yield* b.Remove("inv-1");
|
|
370
|
+
yield* a.Remove("inv-2");
|
|
371
|
+
expect((yield* a.Lines()).map(({ id, amount }) => [id, amount])).toEqual([
|
|
372
|
+
["inv-1", 11],
|
|
373
|
+
["inv-4", 40],
|
|
374
|
+
]);
|
|
375
|
+
expect((yield* invoiceRow("inv-2")).map(({ amount }) => amount)).toEqual([20]);
|
|
376
|
+
const written = yield* invoiceRow("inv-4");
|
|
377
|
+
const key = yield* actorRoutingKey(tenant, "Account", "acct-a");
|
|
378
|
+
expect(written).toEqual([
|
|
379
|
+
{ org_id: tenant, account_id: "acct-a", amount: 40, routing_key: key },
|
|
380
|
+
]);
|
|
381
|
+
expect(key).not.toBe(undefined);
|
|
382
|
+
})),
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: "a read-adopted table is readable in a turn and a query, has no mutation methods, and is refused by group",
|
|
386
|
+
requiresFreshDatabase: true,
|
|
387
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, () => Effect.gen(function* () {
|
|
388
|
+
const sql = yield* SqlClient.SqlClient;
|
|
389
|
+
const test = yield* ActorTest;
|
|
390
|
+
const tenant = test.tenant;
|
|
391
|
+
for (const [id, org, owner, name] of [
|
|
392
|
+
["c1", tenant, "owner-1", "Ada"],
|
|
393
|
+
["c2", tenant, "owner-1", "Grace"],
|
|
394
|
+
["c3", tenant, "owner-2", "Edsger"],
|
|
395
|
+
["c4", `${tenant}-other`, "owner-1", "Barbara"],
|
|
396
|
+
])
|
|
397
|
+
yield* sql `INSERT INTO conformance_contacts (id, tenant, owner, name)
|
|
398
|
+
VALUES (${id}, ${org}, ${owner}, ${name})`;
|
|
399
|
+
const directory = yield* Directory.get("owner-1");
|
|
400
|
+
expect(yield* directory.Names()).toEqual(["Ada", "Grace"]);
|
|
401
|
+
expect(yield* directory.Peek()).toEqual(["Ada", "Grace"]);
|
|
402
|
+
expect(yield* directory.Probe()).toBe(false);
|
|
403
|
+
expect(defect(yield* directory.Grouped().pipe(Effect.exit))).toContain("adopted for reading only");
|
|
404
|
+
expect(yield* (yield* Directory.get("owner-2")).Names()).toEqual(["Edsger"]);
|
|
405
|
+
const columns = yield* sql `
|
|
406
|
+
SELECT column_name AS name FROM information_schema.columns
|
|
407
|
+
WHERE table_name = 'conformance_contacts' ORDER BY ordinal_position`;
|
|
408
|
+
expect(columns.map(({ name }) => name)).toEqual(["id", "tenant", "owner", "name"]);
|
|
409
|
+
expect(yield* sql `SELECT 1 FROM actor_adoptions WHERE table_name = 'conformance_contacts'`).toEqual([]);
|
|
410
|
+
expect(yield* sql `SELECT tgname FROM pg_trigger WHERE tgrelid = 'conformance_contacts'::regclass AND NOT tgisinternal`).toEqual([]);
|
|
411
|
+
})),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "refuses an unsupported mapping: an integer or citext column, a missing column, one column mapped twice, a minted-id actor type",
|
|
415
|
+
run: ({ expect, environment }) => environment.run(Effect.sync(() => {
|
|
416
|
+
const numbers = pgTable("conformance_numbers", {
|
|
417
|
+
id: text("id").primaryKey(),
|
|
418
|
+
orgId: integer("org_id").notNull(),
|
|
419
|
+
accountId: text("account_id").notNull(),
|
|
420
|
+
});
|
|
421
|
+
const citext = customType({ dataType: () => "citext" });
|
|
422
|
+
const folded = pgTable("conformance_folded", {
|
|
423
|
+
id: text("id").primaryKey(),
|
|
424
|
+
orgId: citext("org_id").notNull(),
|
|
425
|
+
accountId: text("account_id").notNull(),
|
|
426
|
+
});
|
|
427
|
+
const elsewhere = pgTable("conformance_elsewhere", { other: text("other") });
|
|
428
|
+
const plain = pgTable("conformance_plain", {
|
|
429
|
+
id: text("id").primaryKey(),
|
|
430
|
+
orgId: text("org_id").notNull(),
|
|
431
|
+
accountId: text("account_id").notNull(),
|
|
432
|
+
});
|
|
433
|
+
expect(refusal(() => Actor.table(numbers, { owner: { tenant: numbers.orgId, actor: numbers.accountId } }))).toContain("only text, varchar, and uuid columns can be mapped");
|
|
434
|
+
expect(refusal(() => Actor.table(folded, { owner: { tenant: folded.orgId, actor: folded.accountId } }))).toContain("only text, varchar, and uuid columns can be mapped");
|
|
435
|
+
expect(refusal(() => Actor.table(plain, {
|
|
436
|
+
owner: { tenant: elsewhere.other, actor: plain.accountId },
|
|
437
|
+
}))).toContain("is not a column of conformance_plain");
|
|
438
|
+
expect(refusal(() => Actor.table(plain, { owner: { tenant: plain.orgId, actor: plain.orgId } }))).toContain("name the same column");
|
|
439
|
+
const routed = pgTable("conformance_routed", {
|
|
440
|
+
id: text("id").primaryKey(),
|
|
441
|
+
routingKey: text("routing_key"),
|
|
442
|
+
orgId: text("org_id"),
|
|
443
|
+
accountId: text("account_id"),
|
|
444
|
+
});
|
|
445
|
+
expect(refusal(() => Actor.table(routed, { owner: { tenant: routed.orgId, actor: routed.accountId } }))).toContain("reserved for ownership");
|
|
446
|
+
const minted = pgTable("conformance_minted", {
|
|
447
|
+
id: text("id").primaryKey(),
|
|
448
|
+
orgId: text("org_id").notNull(),
|
|
449
|
+
accountId: text("account_id").notNull(),
|
|
450
|
+
});
|
|
451
|
+
const mintedRows = Actor.table(minted, {
|
|
452
|
+
owner: { tenant: minted.orgId, actor: minted.accountId },
|
|
453
|
+
});
|
|
454
|
+
const Open = Actor.command("Open");
|
|
455
|
+
expect(refusal(() => Actor.make("Minted", {
|
|
456
|
+
tables: [mintedRows],
|
|
457
|
+
api: { Open },
|
|
458
|
+
createdBy: Open,
|
|
459
|
+
}))).toContain("mints its ids and cannot adopt table conformance_minted");
|
|
460
|
+
})),
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "refuses a mapping the database contradicts: plan reports it and observe changes nothing",
|
|
464
|
+
requiresFreshDatabase: true,
|
|
465
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
466
|
+
const target = yield* prepared(environment, { observe: false });
|
|
467
|
+
const mismatched = pgTable("conformance_mismatch", {
|
|
468
|
+
id: text("id").primaryKey(),
|
|
469
|
+
orgId: text("org_id").notNull(),
|
|
470
|
+
accountId: text("account_id").notNull(),
|
|
471
|
+
});
|
|
472
|
+
const rows = Actor.table(mismatched, {
|
|
473
|
+
owner: { tenant: mismatched.orgId, actor: mismatched.accountId },
|
|
474
|
+
});
|
|
475
|
+
const Mismatch = Actor.make("Mismatch", {
|
|
476
|
+
key: Schema.String,
|
|
477
|
+
tables: [rows],
|
|
478
|
+
api: { Add },
|
|
479
|
+
});
|
|
480
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
481
|
+
const sql = yield* SqlClient.SqlClient;
|
|
482
|
+
yield* sql `CREATE TABLE conformance_mismatch (
|
|
483
|
+
id text PRIMARY KEY, org_id integer NOT NULL, account_id text NOT NULL)`;
|
|
484
|
+
const [plan] = yield* planAdoption([Mismatch]);
|
|
485
|
+
expect(plan.problems).toEqual([
|
|
486
|
+
"tenant column org_id is int4; only text, varchar, and uuid columns can be mapped",
|
|
487
|
+
]);
|
|
488
|
+
const refused = yield* observeAdoption([Mismatch]).pipe(Effect.flip);
|
|
489
|
+
expect(refused).toBeInstanceOf(AdoptionRefused);
|
|
490
|
+
expect(refused.message).toContain("conformance_mismatch cannot be adopted");
|
|
491
|
+
expect(yield* sql `SELECT 1 FROM actor_adoptions`).toEqual([]);
|
|
492
|
+
expect(yield* sql `SELECT 1 FROM information_schema.columns
|
|
493
|
+
WHERE table_name = 'conformance_mismatch' AND column_name = 'routing_key'`).toEqual([]);
|
|
494
|
+
}));
|
|
495
|
+
})),
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
name: "refuses startup when an adopted table has no actor_adoptions row",
|
|
499
|
+
requiresFreshDatabase: true,
|
|
500
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
501
|
+
const target = yield* prepared(environment, { observe: false });
|
|
502
|
+
const exit = yield* runtimeOn(target).pipe(Effect.exit);
|
|
503
|
+
expect(defect(exit)).toContain("conformance_invoices has no adoption record; run durable adopt observe conformance_invoices before serving it");
|
|
504
|
+
})),
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "refuses startup when an adopted table has no index leading with its owner columns",
|
|
508
|
+
requiresFreshDatabase: true,
|
|
509
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
510
|
+
const target = yield* prepared(environment, { observe: true, index: false });
|
|
511
|
+
const exit = yield* runtimeOn(target).pipe(Effect.exit);
|
|
512
|
+
expect(defect(exit)).toContain(`needs an index that leads with its owner columns: CREATE INDEX CONCURRENTLY IF NOT EXISTS "conformance_invoices_durable_owner" ON "public"."conformance_invoices" ("routing_key", "org_id", "account_id")`);
|
|
513
|
+
})),
|
|
514
|
+
},
|
|
515
|
+
{
|
|
516
|
+
name: "maps uuid columns and refuses an actor id that is not a lowercase uuid",
|
|
517
|
+
requiresFreshDatabase: true,
|
|
518
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, () => Effect.gen(function* () {
|
|
519
|
+
const test = yield* ActorTest;
|
|
520
|
+
const sql = yield* SqlClient.SqlClient;
|
|
521
|
+
const actor = yield* (yield* Crypto.Crypto).randomUUIDv4.pipe(Effect.orDie);
|
|
522
|
+
yield* sql `INSERT INTO conformance_shipments (id, tenant_uuid, actor_uuid, label)
|
|
523
|
+
VALUES ('legacy', ${test.tenant}::uuid, ${actor}::uuid, 'legacy')`;
|
|
524
|
+
yield* backfillAdoption(adopting, { only: "conformance_shipments" });
|
|
525
|
+
const carrier = yield* Carrier.get(actor);
|
|
526
|
+
yield* carrier.Ship({ id: "new", label: "new" });
|
|
527
|
+
expect(yield* carrier.Labels()).toEqual(["legacy", "new"]);
|
|
528
|
+
const shouting = yield* Carrier.get("NOT-A-UUID");
|
|
529
|
+
expect(defect(yield* shouting.Ship({ id: "bad", label: "bad" }).pipe(Effect.exit))).toContain("is not a lowercase uuid");
|
|
530
|
+
expect(yield* sql `SELECT 1 FROM conformance_shipments WHERE id = 'bad'`).toEqual([]);
|
|
531
|
+
})),
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
name: "an insert or upsert on another actor's primary key fails the turn as a defect and changes nothing",
|
|
535
|
+
requiresFreshDatabase: true,
|
|
536
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, () => Effect.gen(function* () {
|
|
537
|
+
const test = yield* ActorTest;
|
|
538
|
+
const tenant = test.tenant;
|
|
539
|
+
yield* legacyInsert([{ id: "inv-1", org: tenant, account: "acct-a", amount: 10 }]);
|
|
540
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
541
|
+
const thief = yield* Account.get("acct-b");
|
|
542
|
+
const owner = yield* Account.get("acct-a");
|
|
543
|
+
expect(defect(yield* thief.Add({ id: "inv-1", amount: 99 }).pipe(Effect.exit))).toContain("conformance_invoices");
|
|
544
|
+
expect(defect(yield* thief
|
|
545
|
+
.Put([
|
|
546
|
+
{ id: "inv-fresh", amount: 1 },
|
|
547
|
+
{ id: "inv-1", amount: 99 },
|
|
548
|
+
])
|
|
549
|
+
.pipe(Effect.exit))).toContain("belongs to another actor");
|
|
550
|
+
expect(yield* invoiceRow("inv-1")).toMatchObject([
|
|
551
|
+
{ org_id: tenant, account_id: "acct-a", amount: 10 },
|
|
552
|
+
]);
|
|
553
|
+
expect(yield* invoiceRow("inv-fresh")).toEqual([]);
|
|
554
|
+
expect(yield* thief.Lines()).toEqual([]);
|
|
555
|
+
yield* owner.Put([
|
|
556
|
+
{ id: "inv-1", amount: 12 },
|
|
557
|
+
{ id: "inv-5", amount: 5 },
|
|
558
|
+
]);
|
|
559
|
+
expect((yield* owner.Lines()).map(({ id, amount }) => [id, amount])).toEqual([
|
|
560
|
+
["inv-1", 12],
|
|
561
|
+
["inv-5", 5],
|
|
562
|
+
]);
|
|
563
|
+
})),
|
|
564
|
+
},
|
|
565
|
+
{
|
|
566
|
+
name: "observing records a second pool's writes by role, application_name, operation and rows, classes the turn's writes in_turn, and changes no outcome",
|
|
567
|
+
requiresFreshDatabase: true,
|
|
568
|
+
requiresIndependentConnections: true,
|
|
569
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, ({ target }) => Effect.gen(function* () {
|
|
570
|
+
if (!Redacted.isRedacted(target))
|
|
571
|
+
return yield* Effect.die(new Error("Observation by a second pool needs Postgres"));
|
|
572
|
+
const sql = yield* SqlClient.SqlClient;
|
|
573
|
+
const test = yield* ActorTest;
|
|
574
|
+
const crypto = yield* Crypto.Crypto;
|
|
575
|
+
const role = `legacy_${(yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "")}`;
|
|
576
|
+
yield* sql.unsafe(`CREATE ROLE ${role} LOGIN PASSWORD 'legacy'`);
|
|
577
|
+
yield* sql.unsafe(`GRANT SELECT, INSERT, UPDATE, DELETE ON conformance_invoices TO ${role}`);
|
|
578
|
+
yield* Effect.addFinalizer(() => sql
|
|
579
|
+
.unsafe(`DROP OWNED BY ${role}`)
|
|
580
|
+
.pipe(Effect.andThen(sql.unsafe(`DROP ROLE ${role}`)), Effect.orDie));
|
|
581
|
+
const url = new URL(Redacted.value(target));
|
|
582
|
+
url.username = role;
|
|
583
|
+
url.password = "legacy";
|
|
584
|
+
url.searchParams.set("application_name", "legacy-app");
|
|
585
|
+
yield* onDatabase(Redacted.make(url.href), Effect.gen(function* () {
|
|
586
|
+
const legacy = yield* SqlClient.SqlClient;
|
|
587
|
+
yield* legacy `INSERT INTO conformance_invoices (id, org_id, account_id) VALUES
|
|
588
|
+
('l1', ${test.tenant}, 'acct-a'), ('l2', ${test.tenant}, 'acct-a')`;
|
|
589
|
+
yield* legacy `UPDATE conformance_invoices SET amount = 5 WHERE id = 'l1'`;
|
|
590
|
+
yield* legacy `DELETE FROM conformance_invoices WHERE id = 'l2'`;
|
|
591
|
+
}));
|
|
592
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
593
|
+
const account = yield* Account.get("acct-a");
|
|
594
|
+
yield* account.Add({ id: "t1", amount: 1 });
|
|
595
|
+
expect((yield* invoiceRow("l1")).map(({ amount }) => amount)).toEqual([5]);
|
|
596
|
+
expect(yield* invoiceRow("l2")).toEqual([]);
|
|
597
|
+
expect((yield* account.Lines()).map(({ id }) => id)).toEqual(["l1", "t1"]);
|
|
598
|
+
const writers = yield* adoptionWriters(adopting, { only: "conformance_invoices" });
|
|
599
|
+
const legacyWriters = writers.filter((writer) => writer.sessionUser === role);
|
|
600
|
+
expect(legacyWriters.map(({ operation, applicationName, inTurn, statements, rows }) => ({
|
|
601
|
+
operation,
|
|
602
|
+
applicationName,
|
|
603
|
+
inTurn,
|
|
604
|
+
statements,
|
|
605
|
+
rows,
|
|
606
|
+
}))).toEqual([
|
|
607
|
+
{
|
|
608
|
+
operation: "DELETE",
|
|
609
|
+
applicationName: "legacy-app",
|
|
610
|
+
inTurn: false,
|
|
611
|
+
statements: 1,
|
|
612
|
+
rows: 1,
|
|
613
|
+
},
|
|
614
|
+
{
|
|
615
|
+
operation: "INSERT",
|
|
616
|
+
applicationName: "legacy-app",
|
|
617
|
+
inTurn: false,
|
|
618
|
+
statements: 1,
|
|
619
|
+
rows: 2,
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
operation: "UPDATE",
|
|
623
|
+
applicationName: "legacy-app",
|
|
624
|
+
inTurn: false,
|
|
625
|
+
statements: 1,
|
|
626
|
+
rows: 1,
|
|
627
|
+
},
|
|
628
|
+
]);
|
|
629
|
+
const turns = writers.filter((writer) => writer.inTurn);
|
|
630
|
+
expect(turns.map(({ operation, rows, sessionUser }) => [
|
|
631
|
+
operation,
|
|
632
|
+
rows,
|
|
633
|
+
sessionUser === role,
|
|
634
|
+
])).toEqual([["INSERT", 1, false]]);
|
|
635
|
+
expect(writers.every((writer) => !writer.allowed)).toBe(true);
|
|
636
|
+
expect(writers.filter((writer) => !writer.inTurn && writer.sessionUser !== role)).toEqual([]);
|
|
637
|
+
const cleared = yield* adoptionWriters(adopting, {
|
|
638
|
+
only: "conformance_invoices",
|
|
639
|
+
clear: true,
|
|
640
|
+
});
|
|
641
|
+
expect(cleared.length).toBe(writers.length);
|
|
642
|
+
expect(yield* adoptionWriters(adopting, { only: "conformance_invoices" })).toEqual([]);
|
|
643
|
+
})),
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "observing still records a legacy role's write that sets durable.backfill, while the runtime's own backfill stays unrecorded",
|
|
647
|
+
requiresFreshDatabase: true,
|
|
648
|
+
requiresIndependentConnections: true,
|
|
649
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, ({ target }) => Effect.gen(function* () {
|
|
650
|
+
if (!Redacted.isRedacted(target))
|
|
651
|
+
return yield* Effect.die(new Error("Observation by a second pool needs Postgres"));
|
|
652
|
+
const sql = yield* SqlClient.SqlClient;
|
|
653
|
+
const test = yield* ActorTest;
|
|
654
|
+
const crypto = yield* Crypto.Crypto;
|
|
655
|
+
const role = `legacy_${(yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "")}`;
|
|
656
|
+
yield* sql.unsafe(`CREATE ROLE ${role} LOGIN PASSWORD 'legacy'`);
|
|
657
|
+
yield* sql.unsafe(`GRANT SELECT, INSERT, UPDATE, DELETE ON conformance_invoices TO ${role}`);
|
|
658
|
+
yield* Effect.addFinalizer(() => sql
|
|
659
|
+
.unsafe(`DROP OWNED BY ${role}`)
|
|
660
|
+
.pipe(Effect.andThen(sql.unsafe(`DROP ROLE ${role}`)), Effect.orDie));
|
|
661
|
+
yield* legacyInsert([{ id: "own", org: test.tenant, account: "acct-a" }]);
|
|
662
|
+
const url = new URL(Redacted.value(target));
|
|
663
|
+
url.username = role;
|
|
664
|
+
url.password = "legacy";
|
|
665
|
+
yield* onDatabase(Redacted.make(url.href), Effect.gen(function* () {
|
|
666
|
+
const legacy = yield* SqlClient.SqlClient;
|
|
667
|
+
yield* Effect.gen(function* () {
|
|
668
|
+
yield* legacy `SELECT set_config('durable.backfill', 'on', true)`;
|
|
669
|
+
yield* legacy `INSERT INTO conformance_invoices (id, org_id, account_id)
|
|
670
|
+
VALUES ('hidden', ${test.tenant}, 'acct-a')`;
|
|
671
|
+
}).pipe(legacy.withTransaction);
|
|
672
|
+
}));
|
|
673
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
674
|
+
const writers = yield* adoptionWriters(adopting, { only: "conformance_invoices" });
|
|
675
|
+
expect(writers
|
|
676
|
+
.filter((writer) => writer.sessionUser === role)
|
|
677
|
+
.map(({ operation, rows }) => [operation, rows])).toEqual([["INSERT", 1]]);
|
|
678
|
+
expect(writers.filter((writer) => writer.operation === "UPDATE")).toEqual([]);
|
|
679
|
+
expect(yield* sql `SELECT 1 FROM conformance_invoices WHERE routing_key IS NULL`).toEqual([]);
|
|
680
|
+
})),
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
name: "backfill fills every row with the key of the actor that writes it and refuses rows with NULL mapped columns",
|
|
684
|
+
requiresFreshDatabase: true,
|
|
685
|
+
run: ({ expect, environment }) => withAdoption(environment, { observe: true }, () => Effect.gen(function* () {
|
|
686
|
+
const sql = yield* SqlClient.SqlClient;
|
|
687
|
+
const test = yield* ActorTest;
|
|
688
|
+
const tenant = test.tenant;
|
|
689
|
+
yield* legacyInsert(Array.from({ length: 25 }, (_, index) => ({
|
|
690
|
+
id: `inv-${String(index).padStart(2, "0")}`,
|
|
691
|
+
org: tenant,
|
|
692
|
+
account: `acct-${index % 3}`,
|
|
693
|
+
})));
|
|
694
|
+
yield* sql `ALTER TABLE conformance_invoices ALTER COLUMN account_id DROP NOT NULL`;
|
|
695
|
+
yield* sql `INSERT INTO conformance_invoices (id, org_id, account_id) VALUES ('orphan', ${tenant}, NULL), ('blank', ${tenant}, '')`;
|
|
696
|
+
const refused = yield* backfillAdoption(adopting, {
|
|
697
|
+
only: "conformance_invoices",
|
|
698
|
+
batch: 10,
|
|
699
|
+
}).pipe(Effect.flip);
|
|
700
|
+
expect(refused.message).toContain("NULL or empty");
|
|
701
|
+
expect(refused.message).toContain("(blank) (orphan)");
|
|
702
|
+
expect(yield* sql `SELECT 1 FROM conformance_invoices WHERE routing_key IS NOT NULL`).toEqual([]);
|
|
703
|
+
yield* sql `DELETE FROM conformance_invoices WHERE id IN ('orphan', 'blank')`;
|
|
704
|
+
const [result] = yield* backfillAdoption(adopting, {
|
|
705
|
+
only: "conformance_invoices",
|
|
706
|
+
batch: 10,
|
|
707
|
+
});
|
|
708
|
+
expect(result).toEqual({ table: "public.conformance_invoices", filled: 25, passes: 2 });
|
|
709
|
+
for (const account of ["acct-0", "acct-1", "acct-2"]) {
|
|
710
|
+
const handle = yield* Account.get(account);
|
|
711
|
+
yield* handle.Add({ id: `turn-${account}`, amount: 1 });
|
|
712
|
+
const expected = yield* actorRoutingKey(tenant, "Account", account);
|
|
713
|
+
const keys = yield* sql `
|
|
714
|
+
SELECT DISTINCT routing_key::text AS routing_key FROM conformance_invoices
|
|
715
|
+
WHERE account_id = ${account}`;
|
|
716
|
+
expect(keys.map((row) => row.routing_key)).toEqual([expected]);
|
|
717
|
+
}
|
|
718
|
+
})),
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: "0024_adoption applies on PGlite and Postgres",
|
|
722
|
+
requiresFreshDatabase: true,
|
|
723
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
724
|
+
const target = yield* prepared(environment, { observe: false });
|
|
725
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
726
|
+
const sql = yield* SqlClient.SqlClient;
|
|
727
|
+
expect(yield* sql `SELECT to_regclass('actor_adoptions') IS NOT NULL AS adoptions,
|
|
728
|
+
to_regclass('actor_adoption_writes') IS NOT NULL AS writes,
|
|
729
|
+
to_regprocedure('actor_adoption_observe()') IS NOT NULL AS observe,
|
|
730
|
+
to_regprocedure('actor_adoption_guard()') IS NOT NULL AS guard`).toEqual([{ adoptions: true, writes: true, observe: true, guard: true }]);
|
|
731
|
+
expect(yield* migrate).toEqual([]);
|
|
732
|
+
expect(yield* sql `SELECT count(*)::int AS applied FROM actor_migrations WHERE migration_id = 24`).toEqual([{ applied: 1 }]);
|
|
733
|
+
}));
|
|
734
|
+
})),
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
name: "enforce refuses: unbackfilled rows, a legacy write inside the quiet window, a session_user seen both in and out of turns, an owner the legacy role can act as, and an incoming cascade",
|
|
738
|
+
requiresFreshDatabase: true,
|
|
739
|
+
requiresIndependentConnections: true,
|
|
740
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
741
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
742
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
743
|
+
const sql = yield* SqlClient.SqlClient;
|
|
744
|
+
const refusal = (condition) => Effect.gen(function* () {
|
|
745
|
+
const exit = yield* Effect.exit(enforceAdoption(adopting, {
|
|
746
|
+
only: "conformance_invoices",
|
|
747
|
+
writerRole: roles.writer,
|
|
748
|
+
quietMs: QUIET_MS,
|
|
749
|
+
nowMs: yield* Clock.currentTimeMillis,
|
|
750
|
+
}));
|
|
751
|
+
if (Exit.isSuccess(exit))
|
|
752
|
+
return yield* Effect.die(new Error(`enforce passed despite ${condition}`));
|
|
753
|
+
const refused = Cause.squash(exit.cause);
|
|
754
|
+
expect(Schema.is(AdoptionRefused)(refused)).toBe(true);
|
|
755
|
+
return Schema.is(AdoptionRefused)(refused) ? refused.message : "";
|
|
756
|
+
});
|
|
757
|
+
const reset = Effect.gen(function* () {
|
|
758
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
759
|
+
yield* adoptionWriters(adopting, { only: "conformance_invoices", clear: true });
|
|
760
|
+
yield* sql `UPDATE actor_adoptions SET changed_at_ms = changed_at_ms - ${QUIET_MS + 86_400_000}`;
|
|
761
|
+
});
|
|
762
|
+
const first = yield* refusal("unbackfilled rows");
|
|
763
|
+
expect(first).toContain("2 rows of public.conformance_invoices have no routing_key");
|
|
764
|
+
expect(first).toContain("less than the");
|
|
765
|
+
expect(first).toContain(`${roles.legacy} (legacy-app) wrote public.conformance_invoices`);
|
|
766
|
+
expect(yield* sql `SELECT mode FROM actor_adoptions WHERE table_name = 'conformance_invoices'`).toEqual([{ mode: "observe" }]);
|
|
767
|
+
yield* reset;
|
|
768
|
+
yield* loginAs(target, roles.legacy, legacyInsert([{ id: "inv-3", org: TENANT, account: "acct-a" }]));
|
|
769
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
770
|
+
const second = yield* refusal("a legacy write inside the quiet window");
|
|
771
|
+
expect(second).toContain(`${roles.legacy} (legacy-app) wrote`);
|
|
772
|
+
expect(second).toContain("1 INSERT statements inside the quiet window");
|
|
773
|
+
expect(second).not.toContain("no routing_key");
|
|
774
|
+
yield* reset;
|
|
775
|
+
yield* loginAs(target, roles.batch, Effect.gen(function* () {
|
|
776
|
+
const batch = yield* SqlClient.SqlClient;
|
|
777
|
+
yield* batch `SELECT set_config('durable.turn', 'on', false)`;
|
|
778
|
+
yield* batch `INSERT INTO conformance_invoices (id, org_id, account_id) VALUES ('inv-4', ${TENANT}, 'acct-a')`;
|
|
779
|
+
yield* batch `SELECT set_config('durable.turn', 'off', false)`;
|
|
780
|
+
yield* batch `INSERT INTO conformance_invoices (id, org_id, account_id) VALUES ('inv-5', ${TENANT}, 'acct-a')`;
|
|
781
|
+
}));
|
|
782
|
+
yield* backfillAdoption(adopting, { only: "conformance_invoices" });
|
|
783
|
+
yield* sql `UPDATE actor_adoption_writes SET observed_at_ms = observed_at_ms - ${QUIET_MS + 86_400_000}`;
|
|
784
|
+
expect(yield* refusal("a login that wrote in and out of turns")).toContain(`${roles.batch} wrote public.conformance_invoices both inside and outside runtime turns`);
|
|
785
|
+
yield* reset;
|
|
786
|
+
yield* sql.unsafe(`ALTER TABLE conformance_invoices OWNER TO ${roles.legacy}`);
|
|
787
|
+
expect(yield* refusal("an owner a login can act as")).toContain(`login ${roles.legacy} can act as public.conformance_invoices's owner ${roles.legacy}`);
|
|
788
|
+
yield* sql.unsafe(`ALTER TABLE conformance_invoices OWNER TO ${roles.owner}`);
|
|
789
|
+
yield* sql `CREATE TABLE conformance_parents (id text PRIMARY KEY)`;
|
|
790
|
+
yield* sql `INSERT INTO conformance_parents SELECT id FROM conformance_invoices`;
|
|
791
|
+
yield* sql `ALTER TABLE conformance_invoices ADD CONSTRAINT conformance_invoices_parent
|
|
792
|
+
FOREIGN KEY (id) REFERENCES conformance_parents (id) ON DELETE CASCADE`;
|
|
793
|
+
expect(yield* refusal("an incoming cascade")).toContain("foreign key conformance_invoices_parent of public.conformance_invoices references conformance_parents with ON DELETE CASCADE");
|
|
794
|
+
yield* sql `ALTER TABLE conformance_invoices DROP CONSTRAINT conformance_invoices_parent`;
|
|
795
|
+
yield* sql `DROP TABLE conformance_parents`;
|
|
796
|
+
yield* sql `UPDATE actor_adoption_writes SET observed_at_ms = observed_at_ms - ${QUIET_MS + 86_400_000}`;
|
|
797
|
+
yield* sql `DELETE FROM actor_adoption_writes`;
|
|
798
|
+
const [enforced] = yield* enforceAdoption(adopting, {
|
|
799
|
+
only: "conformance_invoices",
|
|
800
|
+
writerRole: roles.writer,
|
|
801
|
+
quietMs: QUIET_MS,
|
|
802
|
+
nowMs: yield* Clock.currentTimeMillis,
|
|
803
|
+
});
|
|
804
|
+
expect(enforced).toMatchObject({
|
|
805
|
+
table: "public.conformance_invoices",
|
|
806
|
+
writerRole: roles.writer,
|
|
807
|
+
allowedRoles: [],
|
|
808
|
+
});
|
|
809
|
+
expect(yield* sql `SELECT mode, writer_role FROM actor_adoptions WHERE table_name = 'conformance_invoices'`).toEqual([{ mode: "enforce", writer_role: roles.writer }]);
|
|
810
|
+
}));
|
|
811
|
+
})),
|
|
812
|
+
},
|
|
813
|
+
{
|
|
814
|
+
name: "enforced: a second pool, raw SQL, COPY, an updatable view, a SECURITY INVOKER function, a cascade, and TRUNCATE are rejected with 42501, and a mixed statement changes no row (A4)",
|
|
815
|
+
requiresFreshDatabase: true,
|
|
816
|
+
requiresIndependentConnections: true,
|
|
817
|
+
run: ({ expect, environment }) => withEnforcement(environment, {}, ({ target, roles }) => Effect.gen(function* () {
|
|
818
|
+
const sql = yield* SqlClient.SqlClient;
|
|
819
|
+
const a = yield* account("acct-a");
|
|
820
|
+
const b = yield* account("acct-b");
|
|
821
|
+
yield* a.Add({ id: "inv-3", amount: 30 });
|
|
822
|
+
yield* b.Put([{ id: "inv-2", amount: 21 }]);
|
|
823
|
+
expect((yield* a.Lines()).map(({ id }) => id)).toEqual(["inv-1", "inv-3"]);
|
|
824
|
+
expect((yield* b.Lines()).map(({ amount }) => amount)).toEqual([21]);
|
|
825
|
+
const before = yield* sql `
|
|
826
|
+
SELECT id, amount FROM conformance_invoices ORDER BY id`;
|
|
827
|
+
const rejected = (result) => {
|
|
828
|
+
expect(result.reason).toBe("AuthorizationError");
|
|
829
|
+
return result.text;
|
|
830
|
+
};
|
|
831
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
832
|
+
const owner = yield* SqlClient.SqlClient;
|
|
833
|
+
yield* owner.unsafe(`CREATE VIEW conformance_invoice_view AS SELECT * FROM conformance_invoices`);
|
|
834
|
+
yield* owner.unsafe(`GRANT ALL ON conformance_invoice_view TO ${roles.legacy}`);
|
|
835
|
+
yield* owner.unsafe(`CREATE FUNCTION conformance_invoice_write() RETURNS void LANGUAGE sql SECURITY INVOKER AS $$
|
|
836
|
+
INSERT INTO conformance_invoices (id, org_id, account_id, routing_key) VALUES ('fn', 't', 'a', 1) $$`);
|
|
837
|
+
yield* owner.unsafe(`GRANT EXECUTE ON FUNCTION conformance_invoice_write() TO ${roles.legacy}`);
|
|
838
|
+
yield* owner.unsafe(`CREATE TABLE conformance_parents (id text PRIMARY KEY)`);
|
|
839
|
+
yield* owner.unsafe(`INSERT INTO conformance_parents SELECT id FROM conformance_invoices`);
|
|
840
|
+
yield* owner.unsafe(`ALTER TABLE conformance_invoices ADD CONSTRAINT conformance_invoices_parent
|
|
841
|
+
FOREIGN KEY (id) REFERENCES conformance_parents (id) ON DELETE CASCADE`);
|
|
842
|
+
}));
|
|
843
|
+
yield* loginAs(target, roles.legacy, Effect.gen(function* () {
|
|
844
|
+
const legacy = yield* SqlClient.SqlClient;
|
|
845
|
+
for (const [statement, message] of [
|
|
846
|
+
[
|
|
847
|
+
`INSERT INTO conformance_invoices (id, org_id, account_id, routing_key) VALUES ('raw', 't', 'a', 1)`,
|
|
848
|
+
"permission denied",
|
|
849
|
+
],
|
|
850
|
+
[`UPDATE conformance_invoices SET amount = 0`, "permission denied"],
|
|
851
|
+
[`DELETE FROM conformance_invoices`, "permission denied"],
|
|
852
|
+
[`TRUNCATE conformance_invoices`, "permission denied"],
|
|
853
|
+
[
|
|
854
|
+
`INSERT INTO conformance_invoice_view (id, org_id, account_id, routing_key) VALUES ('view', 't', 'a', 1)`,
|
|
855
|
+
"belongs to actor Account",
|
|
856
|
+
],
|
|
857
|
+
[`SELECT conformance_invoice_write()`, "permission denied"],
|
|
858
|
+
])
|
|
859
|
+
expect(rejected(yield* outcome(legacy.unsafe(statement)))).toContain(message);
|
|
860
|
+
}));
|
|
861
|
+
expect(rejected(yield* outcome(sql `INSERT INTO conformance_invoices (id, org_id, account_id, routing_key) VALUES ('raw', ${TENANT}, 'acct-a', 1)`))).toContain("public.conformance_invoices belongs to actor Account");
|
|
862
|
+
expect(rejected(yield* outcome(sql `UPDATE conformance_invoices SET amount = 0`))).toContain("belongs to actor Account");
|
|
863
|
+
expect(rejected(yield* outcome(sql `INSERT INTO conformance_invoices (id, org_id, account_id, routing_key) VALUES
|
|
864
|
+
('mix-own', ${TENANT}, 'acct-a', 1), ('mix-other', ${TENANT}, 'acct-b', 1)`))).toContain("belongs to actor Account");
|
|
865
|
+
expect(rejected(yield* outcome(sql `COPY conformance_invoices (id, org_id, account_id, routing_key) FROM PROGRAM 'printf "copied\tt\ta\t1\n"'`))).toContain("belongs to actor Account");
|
|
866
|
+
expect(rejected(yield* outcome(asRole(roles.writer, sql `TRUNCATE conformance_invoices`)))).toContain("belongs to actor Account");
|
|
867
|
+
expect(rejected(yield* outcome(sql `DELETE FROM conformance_parents WHERE id = 'inv-1'`))).toContain("permission denied");
|
|
868
|
+
expect(yield* sql `SELECT id, amount FROM conformance_invoices ORDER BY id`).toEqual(before);
|
|
869
|
+
})),
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
name: "enforced: the table owner's write is rejected by the guard when privileges are granted back",
|
|
873
|
+
requiresFreshDatabase: true,
|
|
874
|
+
requiresIndependentConnections: true,
|
|
875
|
+
run: ({ expect, environment }) => withEnforcement(environment, {}, ({ target, roles }) => Effect.gen(function* () {
|
|
876
|
+
const sql = yield* SqlClient.SqlClient;
|
|
877
|
+
yield* onDatabase(target, SqlClient.SqlClient.pipe(Effect.flatMap((admin) => admin.unsafe(`GRANT INSERT, UPDATE, DELETE ON conformance_invoices TO ${roles.owner}, ${roles.legacy}`))));
|
|
878
|
+
const insert = sql `INSERT INTO conformance_invoices (id, org_id, account_id, routing_key)
|
|
879
|
+
VALUES ('granted', ${TENANT}, 'acct-a', 1)`;
|
|
880
|
+
for (const role of [roles.owner]) {
|
|
881
|
+
const result = yield* outcome(asRole(role, insert));
|
|
882
|
+
expect(result.reason).toBe("AuthorizationError");
|
|
883
|
+
expect(result.text).toContain("belongs to actor Account");
|
|
884
|
+
}
|
|
885
|
+
const legacy = yield* loginAs(target, roles.legacy, outcome(Effect.flatMap(SqlClient.SqlClient, (session) => session `INSERT INTO conformance_invoices
|
|
886
|
+
(id, org_id, account_id, routing_key) VALUES ('granted', ${TENANT}, 'acct-a', 1)`)));
|
|
887
|
+
expect(legacy.reason).toBe("AuthorizationError");
|
|
888
|
+
expect(legacy.text).toContain("belongs to actor Account");
|
|
889
|
+
expect(yield* invoiceIds).toEqual(["inv-1", "inv-2"]);
|
|
890
|
+
})),
|
|
891
|
+
},
|
|
892
|
+
{
|
|
893
|
+
name: "an --allow role's write passes, is recorded as allowed, and still needs routing_key",
|
|
894
|
+
requiresFreshDatabase: true,
|
|
895
|
+
requiresIndependentConnections: true,
|
|
896
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
897
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
898
|
+
yield* onDatabase(target, enforceTable(roles, [roles.batch]));
|
|
899
|
+
const context = yield* runtimeOn(target, { adoption: { role: roles.writer } }).pipe(Effect.orDie);
|
|
900
|
+
yield* Effect.gen(function* () {
|
|
901
|
+
const sql = yield* SqlClient.SqlClient;
|
|
902
|
+
const insert = (id, key) => loginAs(target, roles.batch, outcome(Effect.flatMap(SqlClient.SqlClient, (session) => session.unsafe(`INSERT INTO conformance_invoices (id, org_id, account_id, routing_key) VALUES ('${id}', '${TENANT}', 'acct-a', ${key})`))));
|
|
903
|
+
expect((yield* insert("batch-1", "7")).reason).toBe("succeeded");
|
|
904
|
+
const missing = yield* insert("batch-2", "NULL");
|
|
905
|
+
expect(missing.reason).toBe("ConstraintError");
|
|
906
|
+
expect(missing.text).toContain("needs routing_key");
|
|
907
|
+
expect(yield* invoiceIds).toEqual(["batch-1", "inv-1", "inv-2"]);
|
|
908
|
+
const writers = yield* adoptionWriters(adopting, { only: "conformance_invoices" });
|
|
909
|
+
expect(writers.map(({ sessionUser, operation, allowed, inTurn, statements }) => ({
|
|
910
|
+
sessionUser,
|
|
911
|
+
operation,
|
|
912
|
+
allowed,
|
|
913
|
+
inTurn,
|
|
914
|
+
statements,
|
|
915
|
+
}))).toEqual([
|
|
916
|
+
{
|
|
917
|
+
sessionUser: roles.batch,
|
|
918
|
+
operation: "INSERT",
|
|
919
|
+
allowed: true,
|
|
920
|
+
inTurn: false,
|
|
921
|
+
statements: 1,
|
|
922
|
+
},
|
|
923
|
+
]);
|
|
924
|
+
expect(yield* sql `SELECT allowed_roles FROM actor_adoptions WHERE table_name = 'conformance_invoices'`).toEqual([{ allowed_roles: [roles.batch] }]);
|
|
925
|
+
}).pipe(Effect.provideContext(context));
|
|
926
|
+
})),
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
name: "changing either mapped column of a row is rejected for every role",
|
|
930
|
+
requiresFreshDatabase: true,
|
|
931
|
+
requiresIndependentConnections: true,
|
|
932
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
933
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
934
|
+
yield* onDatabase(target, enforceTable(roles, [roles.batch]));
|
|
935
|
+
const context = yield* runtimeOn(target, { adoption: { role: roles.writer } }).pipe(Effect.orDie);
|
|
936
|
+
yield* Effect.gen(function* () {
|
|
937
|
+
const sql = yield* SqlClient.SqlClient;
|
|
938
|
+
for (const column of ["org_id", "account_id"]) {
|
|
939
|
+
const move = sql.unsafe(`UPDATE conformance_invoices SET ${column} = 'elsewhere' WHERE id = 'inv-1'`);
|
|
940
|
+
for (const result of [
|
|
941
|
+
yield* outcome(asRole(roles.writer, move)),
|
|
942
|
+
yield* outcome(move),
|
|
943
|
+
yield* loginAs(target, roles.batch, outcome(move)),
|
|
944
|
+
]) {
|
|
945
|
+
expect(result.reason).toBe("AuthorizationError");
|
|
946
|
+
expect(result.text).toContain("cannot move to another tenant or actor");
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
expect(yield* invoiceRow("inv-1")).toMatchObject([
|
|
950
|
+
{ org_id: TENANT, account_id: "acct-a" },
|
|
951
|
+
]);
|
|
952
|
+
}).pipe(Effect.provideContext(context));
|
|
953
|
+
})),
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
name: "startup refuses an enforced table whose trigger is disabled, whose privileges were granted back, or whose mapping changed",
|
|
957
|
+
requiresFreshDatabase: true,
|
|
958
|
+
requiresIndependentConnections: true,
|
|
959
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
960
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
961
|
+
yield* onDatabase(target, enforceTable(roles));
|
|
962
|
+
const start = (options) => runtimeOn(target, options).pipe(Effect.scoped, Effect.exit, Effect.map((exit) => defect(exit)));
|
|
963
|
+
const admin = (statement) => onDatabase(target, Effect.flatMap(SqlClient.SqlClient, (sql) => sql.unsafe(statement)));
|
|
964
|
+
expect(yield* start({})).toContain(`is enforced for writer role ${roles.writer}; start the runtime with adoption: { role: "${roles.writer}" }`);
|
|
965
|
+
expect(yield* start({ adoption: { role: roles.legacy } })).toContain(`is enforced for writer role ${roles.writer}, but the runtime takes ${roles.legacy}`);
|
|
966
|
+
yield* admin(`ALTER TABLE conformance_invoices DISABLE TRIGGER actor_adoption_guard`);
|
|
967
|
+
expect(yield* start({ adoption: { role: roles.writer } })).toContain("guard trigger actor_adoption_guard is not enabled always");
|
|
968
|
+
yield* admin(`ALTER TABLE conformance_invoices ENABLE ALWAYS TRIGGER actor_adoption_guard`);
|
|
969
|
+
yield* admin(`GRANT INSERT ON conformance_invoices TO ${roles.legacy}`);
|
|
970
|
+
expect(yield* start({ adoption: { role: roles.writer } })).toContain(`${roles.legacy} hold write privileges again`);
|
|
971
|
+
yield* admin(`REVOKE INSERT ON conformance_invoices FROM ${roles.legacy}`);
|
|
972
|
+
yield* admin(`UPDATE actor_adoptions SET actor_column = 'org_id', tenant_column = 'account_id' WHERE table_name = 'conformance_invoices'`);
|
|
973
|
+
expect(yield* start({ adoption: { role: roles.writer } })).toContain("was adopted with columns (account_id, org_id) but declares (org_id, account_id)");
|
|
974
|
+
yield* admin(`UPDATE actor_adoptions SET actor_column = 'account_id', tenant_column = 'org_id' WHERE table_name = 'conformance_invoices'`);
|
|
975
|
+
yield* admin(`DROP TRIGGER actor_adoption_guard_truncate ON conformance_invoices`);
|
|
976
|
+
expect(yield* start({ adoption: { role: roles.writer } })).toContain("guard trigger actor_adoption_guard_truncate is missing");
|
|
977
|
+
})),
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
name: "release returns an enforced table to observing: the guard is gone, privileges are back, and legacy writes are recorded again",
|
|
981
|
+
requiresFreshDatabase: true,
|
|
982
|
+
requiresIndependentConnections: true,
|
|
983
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
984
|
+
const { target, roles } = yield* observedWithRoles(environment);
|
|
985
|
+
yield* onDatabase(target, Effect.gen(function* () {
|
|
986
|
+
const sql = yield* SqlClient.SqlClient;
|
|
987
|
+
expect((yield* releaseAdoption(adopting, { only: "conformance_invoices" }).pipe(Effect.flip)).message).toBe("public.conformance_invoices is not enforced");
|
|
988
|
+
yield* enforceTable(roles);
|
|
989
|
+
expect(yield* releaseAdoption(adopting, { only: "conformance_invoices" })).toEqual([
|
|
990
|
+
"public.conformance_invoices",
|
|
991
|
+
]);
|
|
992
|
+
expect(yield* sql `SELECT mode, writer_role, revoked FROM actor_adoptions WHERE table_name = 'conformance_invoices'`).toEqual([{ mode: "observe", writer_role: null, revoked: [] }]);
|
|
993
|
+
expect(yield* sql `SELECT tgname FROM pg_trigger WHERE tgrelid = 'conformance_invoices'::regclass
|
|
994
|
+
AND NOT tgisinternal ORDER BY tgname`).toEqual([
|
|
995
|
+
{ tgname: "actor_adoption_observe_delete" },
|
|
996
|
+
{ tgname: "actor_adoption_observe_insert" },
|
|
997
|
+
{ tgname: "actor_adoption_observe_truncate" },
|
|
998
|
+
{ tgname: "actor_adoption_observe_update" },
|
|
999
|
+
]);
|
|
1000
|
+
}));
|
|
1001
|
+
const written = yield* loginAs(target, roles.legacy, outcome(Effect.flatMap(SqlClient.SqlClient, (legacy) => legacy `INSERT INTO conformance_invoices (id, org_id, account_id) VALUES ('after', ${TENANT}, 'acct-a')`)));
|
|
1002
|
+
expect(written.reason).toBe("succeeded");
|
|
1003
|
+
const writers = yield* onDatabase(target, adoptionWriters(adopting, { only: "conformance_invoices" }));
|
|
1004
|
+
expect(writers.map(({ sessionUser, operation }) => `${sessionUser} ${operation}`)).toContain(`${roles.legacy} INSERT`);
|
|
1005
|
+
})),
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
name: "refuses adoption.role and rowLevelSecurity.role when they name different roles",
|
|
1009
|
+
requiresFreshDatabase: true,
|
|
1010
|
+
run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
|
|
1011
|
+
const target = yield* prepared(environment, { observe: true });
|
|
1012
|
+
const exit = yield* runtimeOn(target, {
|
|
1013
|
+
adoption: { role: "one" },
|
|
1014
|
+
rowLevelSecurity: { role: "another" },
|
|
1015
|
+
}).pipe(Effect.scoped, Effect.exit);
|
|
1016
|
+
expect(defect(exit)).toContain("must name the same role: a turn takes one role");
|
|
1017
|
+
})),
|
|
1018
|
+
},
|
|
1019
|
+
];
|