Skip to content

feat(prisma): consume @distilled.cloud/prisma-postgres instead of the hand-rolled client - #1290

Merged
sam-goodwin merged 13 commits into
alchemy-run:mainfrom
wmadden-electric:claude/distilled-effect-clients-refactor-697b6e
Sep 16, 2026
Merged

sam-goodwin merged 13 commits into
alchemy-run:mainfrom
wmadden-electric:claude/distilled-effect-clients-refactor-697b6e

Conversation

@wmadden

@wmadden wmadden commented Aug 21, 2026 •

Copy link
Copy Markdown
Contributor

Use @distilled.cloud/prisma-postgres for Prisma resource lifecycle calls instead of the hand-written Management API client.

yield* createProject({ payload }).pipe(
  Retry.none,
  Effect.catchTag("Conflict", recover),
);
  • Share Alchemy profile credentials with the SDK while keeping local-provider discovery credential-free.
  • Adapt the current /v1/services API to Alchemy's existing App and appId surface.
  • Preserve database branch attachments and the current Compute runtime bootstrap.
  • Retry Hyperdrive's typed origin-unavailable error while a new Prisma database becomes reachable.
  • Make live rollback and isolated-project tests verify resource deletion.
  • Correct diff-example indentation in the EC2 SecurityGroup and RDS DBInstance JSDoc.

Includes merged distilled#610, pinned at 2a62c51750623f0466c7b34b415fe489fb57d9e3. Standalone Management API helpers and CustomDomain retain their existing client implementation.

Pin the distilled submodule at 49ae23cb1 (the prisma-postgres management
coverage branch) and add the package as a workspace dependency plus a
TypeScript project reference, mirroring how @distilled.cloud/neon is
wired.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Credentials.ts resolves the distilled `Credentials` service lazily from the
Prisma auth profile, preserving PrismaEnvironment's PRISMA_API_URL override
and its validation (the resolved origin becomes `apiBaseUrl`). The service
holds an effect, so nothing resolves until the first API call and
`alchemy dev` still needs no token.

Providers.ts provides it alongside distilled's default retry policy, the
way src/Cloudflare/Providers.ts does. Both are merged inside the
management-client pipe so the auth layers below satisfy them; the fetch
transport is supplied with Layer.provide so it cannot override the ambient
HttpClient other providers depend on.

The retry envelope changes with this: Retry.makeDefault (8 retries, 250ms
base, honors Retry-After) replaces the client's 4x100ms idempotent-only
policy, so creates opt out with Retry.none.

Credentials.ts is internal wiring: null export-map entry plus the matching
PublicSurface expectations.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the claude/distilled-effect-clients-refactor-697b6e branch 6 times, most recently from 629fcf1 to 4c9f973 Compare August 21, 2026 13:00
Project.ts calls the generated management ops directly and handles failures
by typed tag (`Effect.catchTag("NotFound"|"Conflict")`) instead of the
raw-status helpers. Creates opt out of the retry policy with distilled's
`Retry.none`, as src/AWS/ApiGateway/common.ts does, because a retry policy
cannot see the request and a replayed create would duplicate the resource.
List operations walk the cursor inline, following src/Neon/Project.ts.

One PrismaClient reference remains, for the delete path that delegates to
destroyProjectApps — D3 migrates that with ComputeLifecycle.

Shared helpers widen to the structural minimum they read so both the
distilled shapes and Types.ts satisfy them (Internal/Observed.ts):
extractConnectionSecrets takes an ObservedConnection and normalizes
possibly-redacted secrets; recoverDatabaseConnectionSecrets is generic over
an ObservedDatabase.

Project's provider tests move from a fake PrismaManagementClient to a fake
HttpClient (test/Prisma/fixtures/FakeManagementApi.ts, lifted from the
harness in test/Prisma/Client.test.ts) serving the same in-memory state, so
they now exercise distilled's schema decoding and status-to-tag matching —
including a real 409 becoming the Conflict tag the create-recovery path
catches.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Database, Connection, Branch and Internal/DatabaseSecrets now call the
generated management ops directly and handle failures by typed tag, per the
pattern Project.ts froze in D1. Non-idempotent creates and credential
rotations opt out of the retry policy with Retry.none; list operations walk
the cursor inline.

Internal/Observed.ts grows the structural shapes these files share
(ObservedBranch, ObservedConnectionRecord, ObservedProjectDatabase) plus
narrowDatabaseSource, which recovers the discriminated source union from the
Management API's string-typed discriminant by checking fields rather than
asserting them.

Their hermetic tests move to the HTTP fake: ManagementLifecycle serves the
database and branch clouds over the wire alongside the project cloud, and
Connection/Resources adapt their existing client-shaped handlers onto the
same fixture. Test counts are unchanged; assertions gain route and body
pins, and the fixture's unhandled-route arm returns 400 rather than 500 so a
missing route fails fast instead of being retried eight times.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…rations

D2 group 2: Bucket, BucketAccessKey, EnvironmentVariable, and
SourceRepository consume @distilled.cloud/prisma-postgres operations
directly, replacing their PrismaClient calls. Cursor pagination is walked
inline at each list call site, every non-idempotent create opts out of the
retry policy with Retry.none, and the once-revealed bucket-key secret is
read through the possibly-redacted boundary helper.

Their suites now serve the Management API over the wire through the
client-shaped dispatch adapters (Bucket.test.ts, ManagementLifecycle.test.ts,
Resources.test.ts), exercising distilled's schema decode and typed-error
matching. Test names are preserved.

CustomDomain stays on PrismaClient for now: its create relies on the
200-vs-201 status distinction of POST /v1/apps/{appId}/domains to refuse
silently adopting an already-registered domain, and distilled's REST
protocol does not surface the success status code. Migrating it needs a
distilled-side decision first.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
D2 group 3 (part 1): App.ts and Deployment.ts consume
@distilled.cloud/prisma-postgres operations directly for their own calls
(app list/get/create/update, branch lookup, deployment list/create).
Cursor pagination is walked inline at each list call site and both
non-idempotent creates opt out of the retry policy with Retry.none.

Both providers keep resolving PrismaClient for the paths that delegate to
still-unmigrated helpers, the same shim Project.ts uses for
destroyProjectApps: App.delete -> destroyApp, and Deployment's
observe/start/promote/destroy helpers. Those helpers migrate in D3 with
Compute and ComputeLifecycle. Internal/DeploymentActions.ts moves to D3 for
the same reason: dropping its client parameter changes the runtime context
of Compute.ts, which would drag Compute's suites into this commit.

App.test.ts, Deployment.test.ts, and the Resources.test.ts multi-resource
suites serve the migrated routes over the wire through the client-shaped
dispatch adapters; assertions follow wire truth (JSON drops undefined
members, query params arrive as strings). Test names are preserved. Tests
that stub artifact uploads now provide PrismaUploadClient, since the
ambient HttpClient is the management fake.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…lled operations

D3 (first unit): Compute.ts, ComputeLifecycle.ts, and the Internal helpers
(DeploymentObserve, DeploymentActions, DeploymentIdentity, AppPromotion)
consume @distilled.cloud/prisma-postgres operations directly and lose their
client parameters; App.ts and Project.ts drop the PrismaClient shims those
helpers required. Compute and Deployment each keep one shim for the log
tail, whose WebSocket transport is carved out of Client.ts separately.
CustomDomain stays fully on the hand-rolled client pending the 200-vs-201
question. Non-idempotent creates opt out of the retry policy with
Retry.none; start/stop/promote/rollback keep the default policy, matching
the old client's idempotent-POST list.

The gitlink moves to distilled 81cd422c7, whose patch round types the 404s
these lifecycle paths already handle on GET /v1/apps and
GET /v1/environment-variables, so their catches become typed tags. Two
raw-status predicates are replaced with their typed equivalents:
isProjectDeleteBlocked becomes catchTags on Conflict and BadRequest, and
isAppProvisioningNotFound retries on the NotFound tag. Where a helper's
error union mixes typed API errors with plain Error and TypeScript reduces
it to Error, the tag is matched with Predicate.isTagged — instanceof is
also unreliable across the workspace's two module instantiations of the
distilled error classes.

Suites serve the migrated routes over the wire through client-shaped
dispatch adapters; Compute.test.ts routes non-API URLs (uploads, health
probes) to each test's own transport beneath the management fake. Injected
5xx fixtures become 400s so the retry policy does not replay them, and
identity assertions on injected errors become tag/message assertions, since
errors are re-decoded at the wire. The stopped-deployment delete diagnostic
keeps its own coverage through an unmapped-status fixture with retries
disabled. Test names are preserved.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
D3 (second unit): Internal/LogsClient.ts is the hand-written home for the
Management API's log streams — the deployment WebSocket and the build
NDJSON request builders — consuming PrismaEnvironment the way the
hand-rolled client did, with the same path validation and URL building.
They stay in alchemy: neither stream fits distilled's request/response
operations (the question of a distilled home is deferred to the PR).

tailDeploymentLogs loses its client parameter and resolves the environment
on the caller's fiber before the callback stream starts, because the
reconnect loop forks with Effect.runFork, which cannot carry context. The
Compute and Deployment providers drop their last PrismaClient shims; the
Operations helpers re-point to the new module, so the delegation test now
excludes the two builders from the client-call inventory.

The builder assertions move with the code: LogsClient.test.ts pins the
wss swap, the from_start mapping, the NDJSON Accept header, and the
invalid-path refusal against the new module, so deleting Client.ts cannot
silently drop them. The tail suites provide PrismaEnvironment — the test
WebSocket server as an http:// API base, so the protocol swap is exercised
— and pin the requested path, instead of a client-shaped request fake.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…fake

Eight suites carried near-verbatim copies of the adapter that maps their
client-shaped handlers onto the wire, and the copies had drifted: one grew a
fourth `asResponse` variant, and every DELETE arm called its handler directly
instead of going through the missing-handler check, so an unstubbed delete
route threw a raw TypeError instead of the diagnosable `unhandled` 400.

`runHandler`, one `asResponse`/`voidResponse` pair, and a `dispatchTo(request)`
that binds `call`/`callVoid`/`list` now live in `fixtures/FakeManagementApi.ts`.
Every verb routes through it. Each suite keeps its own route table.

ManagementLifecycle also loses its two dead client fakes (the in-memory clouds
serve the wire directly now) and the five `Layer.succeed(PrismaClient, …)`
provides for resources that no longer resolve the client.

No test names change.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the claude/distilled-effect-clients-refactor-697b6e branch from 4c9f973 to 93ab75a Compare August 21, 2026 14:39
@sam-goodwin
sam-goodwin marked this pull request as ready for review September 16, 2026 22:52
@sam-goodwin
sam-goodwin merged commit f73403b into alchemy-run:main Sep 16, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants