| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import * as FetchHttpClient from "effect/http/FetchHttpClient"; |
| 5 | import { AuthProviders } from "../Auth/AuthProvider.ts"; |
| 6 | import { CredentialsStoreLive } from "../Auth/Credentials.ts"; |
| 7 | import { ProfileStore, ProfileStoreLive } from "../Auth/Profile.ts"; |
| 8 | import * as Provider from "../Provider.ts"; |
| 9 | import * as Command from "../Command/index.ts"; |
| 10 | import { PlatformServices } from "../Util/PlatformServices.ts"; |
| 11 | import { proxyChain } from "../Util/proxy-chain.ts"; |
| 12 | import { Server, ServerProvider } from "../Website/Server.ts"; |
| 13 | import { |
| 14 | WebsiteArtifact, |
| 15 | WebsiteArtifactProvider, |
| 16 | } from "./Website/Artifact.ts"; |
| 17 | import { PrismaAuth } from "./AuthProvider.ts"; |
| 18 | import { App, AppProvider } from "./App.ts"; |
| 19 | import { Branch, BranchProvider } from "./Branch.ts"; |
| 20 | import { Bucket, BucketProvider } from "./Bucket.ts"; |
| 21 | import { BucketAccessKey, BucketAccessKeyProvider } from "./BucketAccessKey.ts"; |
| 22 | import { |
| 23 | PrismaClient, |
| 24 | PrismaClientLive, |
| 25 | type PrismaManagementClient, |
| 26 | } from "./Client.ts"; |
| 27 | import { Connection, ConnectionProvider } from "./Connection.ts"; |
| 28 | import { Retry } from "@distilled.cloud/prisma"; |
| 29 | import * as Credentials from "./Credentials.ts"; |
| 30 | import { Compute, ComputeProvider } from "./Compute.ts"; |
| 31 | import { CustomDomain, CustomDomainProvider } from "./CustomDomain.ts"; |
| 32 | import { Database, DatabaseProvider } from "./Database.ts"; |
| 33 | import { Deployment, DeploymentProvider } from "./Deployment.ts"; |
| 34 | import { |
| 35 | EnvironmentVariable, |
| 36 | EnvironmentVariableProvider, |
| 37 | } from "./EnvironmentVariable.ts"; |
| 38 | import { |
| 39 | PrismaHttpClientLive, |
| 40 | PrismaUploadClientLive, |
| 41 | } from "./Internal/HttpClient.ts"; |
| 42 | import { fromProfile } from "./PrismaEnvironment.ts"; |
| 43 | import { Contract, ContractProvider } from "./ORM/Contract.ts"; |
| 44 | import { Migrate, MigrateProvider } from "./ORM/Migrate.ts"; |
| 45 | import { Project, ProjectProvider } from "./Project.ts"; |
| 46 | import { |
| 47 | SourceRepository, |
| 48 | SourceRepositoryProvider, |
| 49 | } from "./SourceRepository.ts"; |
| 50 | |
| 51 | export { PrismaEnvironment } from "./PrismaEnvironment.ts"; |
| 52 | |
| 53 | export class Providers extends Provider.ProviderCollection<Providers>()( |
| 54 | "Prisma", |
| 55 | ) {} |
| 56 | |
| 57 | export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>; |
| 58 | |
| 59 | /** |
| 60 | * Standalone operation helpers own a private auth registry because they run |
| 61 | * outside a Stack. Credential resolution stays eager here so constructing |
| 62 | * `managementApi()` preserves its existing fail-fast behavior — which also |
| 63 | * lets the distilled `Credentials` service read the already-resolved |
| 64 | * {@link PrismaEnvironment} rather than building its own. |
| 65 | */ |
| 66 | const standaloneManagementApiLayer = () => |
| 67 | PrismaClientLive.pipe( |
| 68 | Layer.provideMerge( |
| 69 | Layer.mergeAll( |
| 70 | Credentials.fromEnvironment(), |
| 71 | Layer.succeed(Retry.Retry, Retry.makeDefault), |
| 72 | ), |
| 73 | ), |
| 74 | Layer.provideMerge(fromProfile()), |
| 75 | Layer.provideMerge(PrismaAuth), |
| 76 | Layer.provideMerge( |
| 77 | Layer.mergeAll( |
| 78 | Layer.succeed(AuthProviders, {}), |
| 79 | // The Prisma-scoped upload client (node transport) rides the |
| 80 | // providers' output so artifact uploads can reach it at op time. |
| 81 | PrismaUploadClientLive, |
| 82 | Layer.provide(ProfileStoreLive, PlatformServices), |
| 83 | Layer.provide(CredentialsStoreLive, PlatformServices), |
| 84 | ), |
| 85 | ), |
| 86 | // Provide (NOT provideMerge) the node transport privately: it must serve |
| 87 | // only the Prisma management client. Exposing it from `providers()` would |
| 88 | // override the ambient HttpClient for every other provider in the stack — |
| 89 | // Cloudflare Worker script uploads then go out via node:http, which |
| 90 | // streams multipart bodies as `Transfer-Encoding: chunked`, and |
| 91 | // api.cloudflare.com never answers chunked uploads. |
| 92 | Layer.provide(PrismaHttpClientLive), |
| 93 | ); |
| 94 | |
| 95 | /** |
| 96 | * Stack provider discovery must register auth without requiring credentials. |
| 97 | * The management client is resolved on its first API operation, after |
| 98 | * `alchemy profile edit` has had a chance to configure the registered Prisma |
| 99 | * auth provider. The nested client layer shares the provider layer's lifetime. |
| 100 | */ |
| 101 | const stackManagementApiLayer = () => |
| 102 | Layer.effect( |
| 103 | PrismaClient, |
| 104 | Effect.gen(function* () { |
| 105 | const scope = yield* Effect.scope; |
| 106 | const authProviders = yield* AuthProviders; |
| 107 | const profileStore = yield* ProfileStore; |
| 108 | const client = Layer.buildWithScope( |
| 109 | PrismaClientLive.pipe( |
| 110 | Layer.provideMerge( |
| 111 | fromProfile().pipe( |
| 112 | Layer.provide( |
| 113 | Layer.mergeAll( |
| 114 | Layer.succeed(AuthProviders, authProviders), |
| 115 | Layer.succeed(ProfileStore, profileStore), |
| 116 | ), |
| 117 | ), |
| 118 | ), |
| 119 | ), |
| 120 | Layer.provide(PrismaHttpClientLive), |
| 121 | ), |
| 122 | scope, |
| 123 | ).pipe(Effect.map((context) => Context.get(context, PrismaClient))); |
| 124 | const cached = yield* Effect.cached(client); |
| 125 | return proxyChain(cached) as PrismaManagementClient; |
| 126 | }), |
| 127 | ).pipe( |
| 128 | Layer.provideMerge( |
| 129 | Layer.mergeAll( |
| 130 | Credentials.fromAuthProvider(), |
| 131 | Layer.succeed(Retry.Retry, Retry.makeDefault), |
| 132 | ), |
| 133 | ), |
| 134 | Layer.provideMerge(PrismaAuth), |
| 135 | Layer.provideMerge( |
| 136 | Layer.mergeAll( |
| 137 | // The Prisma-scoped upload client (node transport) rides the |
| 138 | // providers' output so artifact uploads can reach it at op time. |
| 139 | PrismaUploadClientLive, |
| 140 | Layer.provide(ProfileStoreLive, PlatformServices), |
| 141 | Layer.provide(CredentialsStoreLive, PlatformServices), |
| 142 | ), |
| 143 | ), |
| 144 | ); |
| 145 | |
| 146 | /** |
| 147 | * Build a layer for Prisma Management API operation helpers. |
| 148 | * |
| 149 | * Use this when calling helpers like `Prisma.listProjects()` outside an |
| 150 | * Alchemy stack or test. Inside a stack (or a `test.provider` body) deployed |
| 151 | * with `Prisma.providers()`, the management client is already in context, so |
| 152 | * operation helpers work without providing this layer. |
| 153 | * |
| 154 | * @example |
| 155 | * ```typescript |
| 156 | * const projects = yield* Prisma.listProjects().pipe( |
| 157 | * Effect.provide(Prisma.managementApi()), |
| 158 | * ); |
| 159 | * ``` |
| 160 | * |
| 161 | * This layer covers the operation helpers. The lifecycle helpers |
| 162 | * (`destroyApp`, `destroyDeployment`, `destroyProjectApps`, |
| 163 | * `waitForDeploymentStatus`, `syncComputeEnvironment`) also need an |
| 164 | * `HttpClient` from the caller, because the Prisma-scoped node transport is |
| 165 | * `Layer.provide`d privately here — see the comment on the private provide |
| 166 | * above for why it must not reach the surrounding stack. Add one, e.g.: |
| 167 | * |
| 168 | * ```typescript |
| 169 | * yield* Prisma.destroyApp(appId).pipe( |
| 170 | * Effect.provide(Prisma.managementApi()), |
| 171 | * Effect.provide(FetchHttpClient.layer), |
| 172 | * ); |
| 173 | * ``` |
| 174 | */ |
| 175 | export const managementApi = () => |
| 176 | standaloneManagementApiLayer().pipe( |
| 177 | Layer.provide(FetchHttpClient.layer), |
| 178 | Layer.orDie, |
| 179 | ); |
| 180 | |
| 181 | /** |
| 182 | * Build a layer that registers all Prisma resource providers, the Prisma |
| 183 | * auth provider, resolved credentials, and an HTTP client. |
| 184 | * |
| 185 | * Every resource is registered with {@link ../Local/ProviderLayer.ts |
| 186 | * ProviderLayer.dual}: a **live** implementation backed by the Prisma |
| 187 | * Management API and a **local** implementation used by `alchemy dev` |
| 188 | * (fabricated `dev:` identifiers; `Prisma.Database` boots a local |
| 189 | * `@prisma/dev` server). The engine picks the variant per run and per |
| 190 | * resource: |
| 191 | * |
| 192 | * - `alchemy deploy` resolves the live variant; `alchemy dev` resolves the |
| 193 | * local variant automatically. |
| 194 | * - Wrap a resource in `Alchemy.remote(...)` to keep it live even during |
| 195 | * `alchemy dev`. |
| 196 | * - To replace implementations wholesale, construct your own layer instead |
| 197 | * of this one: register the {@link Providers} collection and provide your |
| 198 | * own per-resource provider layers (compose the exported per-resource |
| 199 | * factories like {@link ProjectProvider} with your replacements, and |
| 200 | * {@link managementApi} when live operations are needed). |
| 201 | * |
| 202 | * @example |
| 203 | * ```typescript |
| 204 | * import * as Alchemy from "alchemy"; |
| 205 | * import * as Prisma from "alchemy/Prisma"; |
| 206 | * import * as Effect from "effect/Effect"; |
| 207 | * |
| 208 | * export default Alchemy.Stack( |
| 209 | * "MyStack", |
| 210 | * { providers: Prisma.providers(), state: Alchemy.localState() }, |
| 211 | * Effect.gen(function* () { |
| 212 | * const project = yield* Prisma.Project("app", { |
| 213 | * name: "app", |
| 214 | * region: "us-east-1", |
| 215 | * }); |
| 216 | * return { projectId: project.projectId }; |
| 217 | * }), |
| 218 | * ); |
| 219 | * ``` |
| 220 | */ |
| 221 | export const providers = () => |
| 222 | Layer.effect( |
| 223 | Providers, |
| 224 | Provider.collection([ |
| 225 | Project, |
| 226 | Database, |
| 227 | Connection, |
| 228 | Branch, |
| 229 | Bucket, |
| 230 | BucketAccessKey, |
| 231 | Compute, |
| 232 | App, |
| 233 | Deployment, |
| 234 | CustomDomain, |
| 235 | EnvironmentVariable, |
| 236 | SourceRepository, |
| 237 | Contract, |
| 238 | Migrate, |
| 239 | Server, |
| 240 | WebsiteArtifact, |
| 241 | ]), |
| 242 | ).pipe( |
| 243 | Layer.provideMerge( |
| 244 | Layer.mergeAll( |
| 245 | ProjectProvider(), |
| 246 | DatabaseProvider(), |
| 247 | ConnectionProvider(), |
| 248 | BranchProvider(), |
| 249 | BucketProvider(), |
| 250 | BucketAccessKeyProvider(), |
| 251 | ComputeProvider(), |
| 252 | AppProvider(), |
| 253 | DeploymentProvider(), |
| 254 | CustomDomainProvider(), |
| 255 | EnvironmentVariableProvider(), |
| 256 | SourceRepositoryProvider(), |
| 257 | ContractProvider(), |
| 258 | MigrateProvider(), |
| 259 | ServerProvider(), |
| 260 | WebsiteArtifactProvider(), |
| 261 | ), |
| 262 | ), |
| 263 | // The management client layer is shared by every live variant. It is |
| 264 | // built in both modes but stays inert until the first API operation: |
| 265 | // auth registers without resolving credentials, so `alchemy dev` never |
| 266 | // needs a Prisma token. |
| 267 | Layer.provideMerge(stackManagementApiLayer()), |
| 268 | Layer.provideMerge(Command.providers()), |
| 269 | Layer.provide(FetchHttpClient.layer), |
| 270 | Layer.orDie, |
| 271 | ); |