1import * as Provider from "@/Provider";
2import { Unowned } from "@/AdoptPolicy";
3import { InstanceId } from "@/InstanceId";
4import { Branch as PrismaBranch, BranchProvider } from "@/Prisma/Branch";
5import {
6 PrismaApiError,
7 PrismaClient,
8 type DatabaseCreateResult,
9} from "@/Prisma/Client";
10import { Compute as PrismaCompute } from "@/Prisma/Compute";
11import { App as PrismaApp, AppProvider } from "@/Prisma/App";
12import {
13 Deployment as PrismaDeployment,
14 DeploymentProvider,
15} from "@/Prisma/Deployment";
16import { Connect, ConnectBinding, connectEnvKeys } from "@/Prisma/Connect";
17import {
18 Connection as PrismaConnection,
19 ConnectionProvider,
20} from "@/Prisma/Connection";
21import {
22 CustomDomain as PrismaCustomDomain,
23 CustomDomainProvider,
24} from "@/Prisma/CustomDomain";
25import {
26 Database as PrismaDatabase,
27 DatabaseProvider,
28} from "@/Prisma/Database";
29import {
30 EnvironmentVariable as PrismaEnvironmentVariable,
31 EnvironmentVariableProvider,
32} from "@/Prisma/EnvironmentVariable";
33import { Project as PrismaProject, ProjectProvider } from "@/Prisma/Project";
34import { Providers as PrismaProviderCollection } from "@/Prisma/Providers";
35import {
36 SourceRepository as PrismaSourceRepository,
37 SourceRepositoryProvider,
38} from "@/Prisma/SourceRepository";
39import * as Output from "@/Output";
40import type { PrismaManagementClient } from "@/Prisma/Client";
41import type { Database as ApiDatabase } from "@/Prisma/Types";
42import { RuntimeContext } from "@/RuntimeContext";
43import { Self } from "@/Self";
44import { Stack, type StackSpec } from "@/Stack";
45import { inMemoryState } from "@/State/InMemoryState";
46import { Stage } from "@/Stage";
47import { describe, expect, it } from "alchemy-test";
48import {
49 type Captured,
50 conflict,
51 data as envelope,
52 dispatchTo,
53 failure,
54 json,
55 makeFakeManagementApi,
56 notFound,
57 page,
58 routesOf,
59 unhandled,
60 wireConnection,
61 wireCreatedDatabase,
62 wireDatabase,
63 wireProject,
64} from "./fixtures/FakeManagementApi.ts";
65import { testStackContext } from "./fixtures/StackContext.ts";
66import { PlatformServices } from "@/Util/PlatformServices";
67import * as ConfigProvider from "effect/ConfigProvider";
68import * as Effect from "effect/Effect";
69import * as Layer from "effect/Layer";
70import * as Redacted from "effect/Redacted";
71import * as Result from "effect/Result";
72import * as FetchHttpClient from "effect/http/FetchHttpClient";
73import { AlchemyContext } from "@/AlchemyContext";
74
75type Call = [operation: string, input?: unknown];
76type Equal<A, B> =
77 (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
78 ? true
79 : false;
80type Expect<T extends true> = T;
81
82const createdAt = "2026-01-01T00:00:00Z";
83const updatedAt = "2026-01-01T00:00:01Z";
84
85const redactedValue = (
86 value: string | Redacted.Redacted<string> | undefined,
87) => {
88 if (!Redacted.isRedacted(value)) {
89 throw new Error("Expected a redacted value");
90 }
91 return Redacted.value(value);
92};
93
94const expectJsonNotToContain = (value: unknown, ...secrets: string[]) => {
95 const json = JSON.stringify(value);
96 for (const secret of secrets) {
97 expect(json).not.toContain(secret);
98 }
99};
100
101const resourceRef = (kind: string, id: string, name = id) => ({
102 id,
103 url: `https://api.prisma.test/v1/${kind}/${id}`,
104 name,
105});
106
107const databaseConnection = (
108 databaseId: string,
109 connectionId = `connection-${databaseId}`,
110) => ({
111 id: connectionId,
112 type: "connection" as const,
113 url: `https://api.prisma.test/v1/connections/${connectionId}`,
114 name: "default",
115 createdAt,
116 kind: "postgres" as const,
117 endpoints: {
118 direct: {
119 host: "db.prisma.test",
120 port: 5432,
121 connectionString: `postgres://user:password@db.prisma.test/${databaseId}`,
122 },
123 },
124 database: resourceRef("databases", databaseId),
125});
126
127const makeClient = () => {
128 const calls: Call[] = [];
129 const client = {
130 listProjects: () => {
131 calls.push(["listProjects"]);
132 return Effect.succeed([]);
133 },
134 createProject: (input: unknown) => {
135 calls.push(["createProject", input]);
136 return Effect.succeed({
137 id: "project-1",
138 type: "project",
139 url: "https://api.prisma.test/v1/projects/project-1",
140 name: "app",
141 createdAt,
142 defaultRegion: "us-east-1",
143 workspace: resourceRef("workspaces", "workspace-1", "team"),
144 database: null,
145 });
146 },
147 listProjectDatabases: (projectId: string, query: unknown) => {
148 calls.push(["listProjectDatabases", { projectId, query }]);
149 return Effect.succeed([]);
150 },
151 createDatabase: (input: unknown) => {
152 calls.push(["createDatabase", input]);
153 return Effect.succeed({
154 id: "database-1",
155 type: "database",
156 url: "https://api.prisma.test/v1/databases/database-1",
157 name: "main",
158 status: "ready",
159 createdAt,
160 isDefault: false,
161 defaultConnectionId: "connection-1",
162 connections: [
163 {
164 id: "connection-1",
165 type: "connection",
166 url: "https://api.prisma.test/v1/connections/connection-1",
167 name: "default",
168 createdAt,
169 kind: "postgres",
170 endpoints: {
171 direct: {
172 host: "db.prisma.test",
173 port: 5432,
174 connectionString: "postgres://direct",
175 },
176 pooled: {
177 host: "pool.prisma.test",
178 port: 5432,
179 connectionString: "postgres://pooled",
180 },
181 },
182 database: resourceRef("databases", "database-1", "main"),
183 },
184 ],
185 project: resourceRef("projects", "project-1", "app"),
186 region: { id: "us-east-1", name: "US East" },
187 source: { type: "empty" },
188 branchId: null,
189 });
190 },
191 listDatabaseConnections: (databaseId: string, query: unknown) => {
192 calls.push(["listDatabaseConnections", { databaseId, query }]);
193 return Effect.succeed([]);
194 },
195 createConnection: (input: unknown) => {
196 calls.push(["createConnection", input]);
197 return Effect.succeed({
198 id: "connection-2",
199 type: "connection",
200 url: "https://api.prisma.test/v1/connections/connection-2",
201 name: "api",
202 createdAt,
203 kind: "postgres",
204 endpoints: {
205 direct: {
206 host: "db.prisma.test",
207 port: 5432,
208 connectionString: "postgres://api-direct",
209 },
210 },
211 database: resourceRef("databases", "database-1", "main"),
212 });
213 },
214 listBranches: (projectId: string, query: unknown) => {
215 calls.push(["listBranches", { projectId, query }]);
216 const gitName = (query as { gitName?: string } | undefined)?.gitName;
217 return Effect.succeed(
218 gitName !== undefined && gitName !== "main"
219 ? []
220 : [
221 {
222 id: "branch-1",
223 type: "branch",
224 url: "https://api.prisma.test/v1/branches/branch-1",
225 gitName: "main",
226 isDefault: true,
227 role: "production",
228 createdAt,
229 updatedAt,
230 project: resourceRef("projects", "project-1", "app"),
231 },
232 ],
233 );
234 },
235 getBranch: (id: string) => {
236 calls.push(["getBranch", id]);
237 return Effect.succeed({
238 id,
239 type: "branch",
240 url: `https://api.prisma.test/v1/branches/${id}`,
241 gitName: "main",
242 isDefault: true,
243 role: "production",
244 createdAt,
245 updatedAt,
246 project: resourceRef("projects", "project-1", "app"),
247 });
248 },
249 createBranch: (
250 projectId: string,
251 input: { gitName: string; isDefault?: boolean },
252 ) => {
253 calls.push(["createBranch", { projectId, input }]);
254 return Effect.succeed({
255 id: "branch-2",
256 type: "branch",
257 url: "https://api.prisma.test/v1/branches/branch-2",
258 gitName: input.gitName,
259 isDefault: input.isDefault ?? false,
260 role: "preview",
261 createdAt,
262 updatedAt,
263 project: resourceRef("projects", "project-1", "app"),
264 });
265 },
266 listApps: (query: unknown) => {
267 calls.push(["listApps", query]);
268 return Effect.succeed([]);
269 },
270 createApp: (input: { projectId: string }) => {
271 calls.push(["createApp", input]);
272 return Effect.succeed({
273 id: "service-1",
274 type: "app",
275 url: "https://api.prisma.test/v1/services/service-1",
276 name: "api",
277 region: { id: "us-east-1", name: "US East" },
278 projectId: "project-1",
279 branchId: "branch-1",
280 latestDeploymentId: null,
281 appEndpointDomain: "service-1.prisma.build",
282 createdAt,
283 });
284 },
285 getApp: (id: string) => {
286 calls.push(["getApp", id]);
287 return Effect.succeed({
288 id,
289 type: "app",
290 url: `https://api.prisma.test/v1/services/${id}`,
291 name: "api",
292 region: { id: "us-east-1", name: "US East" },
293 projectId: "project-1",
294 branchId: "branch-1",
295 latestDeploymentId: "version-1",
296 appEndpointDomain: "service-1.prisma.build",
297 createdAt,
298 });
299 },
300 createAppDeployment: (appId: string, input: unknown) => {
301 calls.push(["createAppDeployment", { appId, input }]);
302 return Effect.succeed({
303 id: "version-1",
304 type: "deployment",
305 url: "https://api.prisma.test/v1/deployments/version-1",
306 foundryVersionId: "foundry-1",
307 uploadUrl: null,
308 });
309 },
310 listAppDomains: (appId: string) => {
311 calls.push(["listAppDomains", appId]);
312 return Effect.succeed([]);
313 },
314 createAppDomain: (appId: string, input: unknown) => {
315 calls.push(["createAppDomain", { appId, input }]);
316 return Effect.succeed({
317 status: 201 as const,
318 domain: {
319 id: "domain-1",
320 type: "custom-domain" as const,
321 url: "https://api.prisma.test/v1/domains/domain-1",
322 hostname: "api.example.com",
323 appId: appId,
324 status: "pending_dns" as const,
325 foundryStatus: "pending_dns",
326 failureReason: null,
327 failureCategory: null,
328 certExpiresAt: null,
329 dnsRecords: [
330 {
331 type: "CNAME" as const,
332 name: "api.example.com",
333 value: "service-1.prisma.build",
334 ttl: null,
335 },
336 ],
337 createdAt,
338 updatedAt,
339 },
340 });
341 },
342 getDeployment: (id: string) => {
343 calls.push(["getDeployment", id]);
344 return Effect.succeed({
345 id,
346 type: "deployment",
347 serviceId: "service-1",
348 url: `https://api.prisma.test/v1/deployments/${id}`,
349 foundryVersionId: "foundry-1",
350 status: "new",
351 previewDomain: null,
352 createdAt,
353 });
354 },
355 listEnvironmentVariables: (query: unknown) => {
356 calls.push(["listEnvironmentVariables", query]);
357 return Effect.succeed([]);
358 },
359 createEnvironmentVariable: (input: unknown) => {
360 calls.push(["createEnvironmentVariable", input]);
361 return Effect.succeed({
362 id: "env-1",
363 type: "environment-variable",
364 url: "https://api.prisma.test/v1/environment-variables/env-1",
365 projectId: "project-1",
366 branchId: null,
367 class: "production",
368 key: "TOKEN",
369 valueKid: "kid-1",
370 isManagedBySystem: false,
371 createdAt,
372 updatedAt,
373 });
374 },
375 listSourceRepositories: (query: unknown) => {
376 calls.push(["listSourceRepositories", query]);
377 return Effect.succeed([]);
378 },
379 createSourceRepository: (input: unknown) => {
380 calls.push(["createSourceRepository", input]);
381 return Effect.succeed({
382 id: "repo-1",
383 type: "source-repository",
384 url: "https://api.prisma.test/v1/source-repositories/repo-1",
385 repoId: 123,
386 provider: "github",
387 repoFullName: "acme/api",
388 defaultBranch: "main",
389 isPrivate: true,
390 status: "active",
391 projectId: "project-1",
392 installationId: "installation-1",
393 createdAt,
394 updatedAt,
395 });
396 },
397 getSourceRepository: (id: string) => {
398 calls.push(["getSourceRepository", id]);
399 return Effect.succeed({
400 id,
401 type: "source-repository",
402 url: `https://api.prisma.test/v1/source-repositories/${id}`,
403 repoId: 123,
404 provider: "github",
405 repoFullName: "acme/api",
406 defaultBranch: "main",
407 isPrivate: true,
408 status: "active",
409 projectId: "project-1",
410 installationId: "installation-1",
411 createdAt,
412 updatedAt,
413 });
414 },
415 } as unknown as PrismaManagementClient;
416 return { client, calls };
417};
418
419const liveProviderContext = Layer.succeed(AlchemyContext, {
420 dotAlchemy: ".alchemy-test",
421 dev: false,
422 adopt: false,
423});
424
425/**
426 * Serve the Management API from the same hermetic client-shaped handlers
427 * these suites already declare, for the resources that now call distilled
428 * operations. `dispatchTo` maps each handler's result onto the wire (see the
429 * fixture).
430 */
431const dispatchManagement = (client: any, request: Captured): Response => {
432 {
433 const segments = request.pathname.split("/").filter((s) => s.length > 0);
434 const body = request.bodyJson as any;
435 const { call, callVoid, list } = dispatchTo(request);
436
437 // segments[0] is the "v1" prefix.
438 const [head, id, tail] = segments.slice(1) as [
439 string | undefined,
440 string | undefined,
441 string | undefined,
442 ];
443
444 if (head === "projects") {
445 if (id === undefined) {
446 return request.method === "GET"
447 ? call(client.listProjects, [], list)
448 : call(client.createProject, [body]);
449 }
450 if (tail === "databases") {
451 return request.method === "GET"
452 ? call(client.listProjectDatabases, [id, { limit: 100 }], list)
453 : call(client.createProjectDatabase, [id, body]);
454 }
455 if (tail === "branches") {
456 return request.method === "GET"
457 ? call(
458 client.listBranches,
459 [id, Object.fromEntries(new URLSearchParams(request.search))],
460 list,
461 )
462 : call(client.createBranch, [id, body]);
463 }
464 if (request.method === "GET") return call(client.getProject, [id]);
465 if (request.method === "PATCH")
466 return call(client.updateProject, [id, body]);
467 if (request.method === "DELETE") {
468 return callVoid(client.deleteProject, [id]);
469 }
470 }
471
472 if (head === "databases") {
473 if (id === undefined) {
474 return request.method === "GET"
475 ? call(client.listDatabases, [], list)
476 : call(client.createDatabase, [body]);
477 }
478 if (tail === "connections") {
479 return request.method === "GET"
480 ? call(client.listDatabaseConnections, [id, { limit: 100 }], list)
481 : call(client.createDatabaseConnection, [id, body]);
482 }
483 if (request.method === "GET") return call(client.getDatabase, [id]);
484 if (request.method === "PATCH")
485 return call(client.updateDatabase, [id, body]);
486 if (request.method === "DELETE") {
487 return callVoid(client.deleteDatabase, [id]);
488 }
489 }
490
491 if (head === "connections") {
492 if (id === undefined) {
493 return request.method === "GET"
494 ? call(client.listConnections, [], list)
495 : call(client.createConnection, [body]);
496 }
497 if (tail === "rotate") return call(client.rotateConnection, [id]);
498 if (request.method === "GET") return call(client.getConnection, [id]);
499 if (request.method === "DELETE") {
500 return callVoid(client.deleteConnection, [id]);
501 }
502 }
503
504 if (head === "environment-variables") {
505 if (id === undefined) {
506 return request.method === "GET"
507 ? call(
508 client.listEnvironmentVariables,
509 [Object.fromEntries(new URLSearchParams(request.search))],
510 list,
511 )
512 : call(client.createEnvironmentVariable, [body]);
513 }
514 if (request.method === "GET") {
515 return call(client.getEnvironmentVariable, [id]);
516 }
517 if (request.method === "PATCH") {
518 return call(client.updateEnvironmentVariable, [id, body]);
519 }
520 if (request.method === "DELETE") {
521 return callVoid(client.deleteEnvironmentVariable, [id]);
522 }
523 }
524
525 if (head === "source-repositories") {
526 if (id === undefined) {
527 return request.method === "GET"
528 ? call(
529 client.listSourceRepositories,
530 [Object.fromEntries(new URLSearchParams(request.search))],
531 list,
532 )
533 : call(client.createSourceRepository, [body]);
534 }
535 if (request.method === "GET") {
536 return call(client.getSourceRepository, [id]);
537 }
538 if (request.method === "DELETE") {
539 return callVoid(client.deleteSourceRepository, [id]);
540 }
541 }
542
543 if (head === "buckets") {
544 if (id === undefined) {
545 return request.method === "GET"
546 ? call(client.listBuckets, [], list)
547 : call(client.createBucket, [body]);
548 }
549 if (tail === "keys") {
550 return request.method === "GET"
551 ? call(client.listBucketKeys, [id, { limit: 100 }], list)
552 : call(client.createBucketKey, [id, body]);
553 }
554 if (request.method === "GET") return call(client.getBucket, [id]);
555 if (request.method === "DELETE") {
556 return callVoid(client.deleteBucket, [id]);
557 }
558 }
559
560 if (head === "services") {
561 if (id === undefined) {
562 return request.method === "GET"
563 ? call(
564 client.listApps,
565 [Object.fromEntries(new URLSearchParams(request.search))],
566 list,
567 )
568 : call(client.createApp, [body]);
569 }
570 if (tail === "deployments") {
571 return request.method === "GET"
572 ? call(
573 client.listAppDeployments,
574 [id, Object.fromEntries(new URLSearchParams(request.search))],
575 list,
576 )
577 : call(client.createAppDeployment, [id, body]);
578 }
579 if (tail === undefined) {
580 if (request.method === "GET") return call(client.getApp, [id]);
581 if (request.method === "PATCH")
582 return call(client.updateApp, [id, body]);
583 }
584 }
585
586 if (head === "deployments" && id !== undefined) {
587 if (tail === "start") return call(client.startDeployment, [id]);
588 if (tail === "stop") {
589 return callVoid(client.stopDeployment, [id]);
590 }
591 if (request.method === "GET") return call(client.getDeployment, [id]);
592 if (request.method === "DELETE") {
593 return callVoid(client.deleteDeployment, [id]);
594 }
595 }
596
597 if (head === "services" && id !== undefined && tail === "promote") {
598 return call(client.promoteApp, [id, body]);
599 }
600 if (head === "services" && id !== undefined && tail === "rollback") {
601 return call(client.rollbackApp, [id, body]);
602 }
603 if (
604 head === "services" &&
605 id !== undefined &&
606 tail === undefined &&
607 request.method === "DELETE"
608 ) {
609 return callVoid(client.deleteApp, [id]);
610 }
611
612 if (head === "branches" && id !== undefined) {
613 if (request.method === "GET") return call(client.getBranch, [id]);
614 if (request.method === "PATCH")
615 return call(client.updateBranch, [id, body]);
616 if (request.method === "DELETE") {
617 return callVoid(client.deleteBranch, [id]);
618 }
619 }
620
621 return unhandled(request);
622 }
623};
624
625const managementApi = (client: any) =>
626 makeFakeManagementApi((request) => dispatchManagement(client, request));
627
628const providerLayer = (client: PrismaManagementClient) =>
629 Layer.mergeAll(
630 ProjectProvider(),
631 DatabaseProvider(),
632 ConnectionProvider(),
633 BranchProvider(),
634 AppProvider(),
635 DeploymentProvider(),
636 CustomDomainProvider(),
637 EnvironmentVariableProvider(),
638 SourceRepositoryProvider(),
639 ).pipe(
640 Layer.provide(Layer.succeed(PrismaClient, client)),
641 Layer.provide(liveProviderContext),
642 Layer.provide(testStackContext),
643 Layer.provide(PlatformServices),
644 );
645
646const reconcileInput = <Props, Attrs>(
647 id: string,
648 news: Props,
649 output?: Attrs,
650 olds?: Props,
651) => ({
652 id,
653 fqn: id,
654 instanceId: "00000000000000000000000000000000",
655 news,
656 olds,
657 output,
658 session: undefined as never,
659 bindings: [],
660});
661
662const deleteInput = (id: string, output: unknown) =>
663 ({
664 id,
665 fqn: id,
666 instanceId: "00000000000000000000000000000000",
667 olds: {} as never,
668 output,
669 session: undefined as never,
670 bindings: [],
671 }) as never;
672
673const readInput = <Props, Attrs>(id: string, olds: Props, output?: Attrs) =>
674 ({
675 id,
676 fqn: id,
677 instanceId: "00000000000000000000000000000000",
678 olds,
679 output,
680 }) as never;
681
682const diffInput = <Props, Attrs>(olds: Props, news: Props, output?: Attrs) =>
683 ({
684 id: "Resource",
685 fqn: "Resource",
686 instanceId: "00000000000000000000000000000000",
687 olds,
688 news,
689 oldBindings: [],
690 newBindings: [],
691 output,
692 }) as never;
693
694describe(
695 "Prisma resource providers",
696 { tags: ["unit", "provider:prisma", "local"] },
697 () => {
698 it(
699 "derives namespaced-safe env keys for connection bindings",
700 () => {
701 expect(
702 connectEnvKeys({
703 FQN: "Connection",
704 LogicalId: "Connection",
705 }).directConnectionString,
706 ).toBe("PRISMA_CONNECTION_DIRECT_CONNECTION_STRING");
707 expect(
708 connectEnvKeys({
709 FQN: "Api/Connection",
710 LogicalId: "Connection",
711 }).directConnectionString,
712 ).toBe("PRISMA_API_CONNECTION_DIRECT_CONNECTION_STRING");
713 },
714 { tags: ["provider:prisma:connect"] },
715 );
716
717 it.effect(
718 "ConnectBinding resolves bound connection outputs at runtime",
719 () => {
720 const stored: Record<string, Output.Output> = {};
721 let capturedBindingEnv: Record<string, Output.Output> | undefined;
722 const runtime = {
723 Type: "Prisma.Compute",
724 id: "App",
725 env: stored,
726 set: (id: string, output: Output.Output) =>
727 Effect.sync(() => {
728 const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
729 stored[key] = output;
730 return key;
731 }),
732 get: <T>(key: string): Effect.Effect<T> => {
733 const output = stored[key];
734 if (!output) return Effect.die(`missing runtime binding ${key}`);
735 return Output.evaluate(output, {}) as Effect.Effect<T>;
736 },
737 };
738 const host = {
739 Type: "Prisma.Compute",
740 LogicalId: "App",
741 FQN: "App",
742 bind: (...args: unknown[]) =>
743 args[0] instanceof Array
744 ? (binding: { env?: Record<string, Output.Output> }) =>
745 Effect.sync(() => {
746 capturedBindingEnv = binding.env;
747 })
748 : Effect.void,
749 };
750 const escapedPooledConnectionString =
751 "__ALCHEMY_PRISMA_CONNECTION_VALUE__:prisma://pooled";
752 const connection = {
753 Type: "Prisma.Connection",
754 LogicalId: "Connection",
755 FQN: "Api/Connection",
756 connectionId: Output.asOutput("connection-1"),
757 databaseId: Output.asOutput("database-1"),
758 directConnectionString: Output.asOutput(
759 Redacted.make("postgres://direct"),
760 ),
761 pooledConnectionString: Output.asOutput(
762 Redacted.make(escapedPooledConnectionString),
763 ),
764 accelerateConnectionString: Output.asOutput(undefined),
765 host: Output.asOutput("db.example.test"),
766 user: Output.asOutput(null),
767 password: Output.asOutput(Redacted.make("password")),
768 } as PrismaConnection;
769
770 return Effect.gen(function* () {
771 const db = yield* Connect(connection);
772 const keys = connectEnvKeys(connection);
773 const encodedEnv = yield* Output.evaluate(
774 capturedBindingEnv ?? {},
775 {},
776 ) as Effect.Effect<Record<string, unknown>>;
777
778 expect(Object.keys(stored)).toEqual([]);
779 expect(encodedEnv[keys.accelerateConnectionString]).toEqual(
780 expect.any(String),
781 );
782 expect(encodedEnv[keys.user]).toEqual(expect.any(String));
783 expect(yield* db.connectionId).toBe("connection-1");
784 expect(Redacted.value(yield* db.databaseUrl)).toBe(
785 escapedPooledConnectionString,
786 );
787 expect(Redacted.value((yield* db.directConnectionString)!)).toBe(
788 "postgres://direct",
789 );
790 expect(Redacted.value((yield* db.pooledConnectionString)!)).toBe(
791 escapedPooledConnectionString,
792 );
793 expect(yield* db.accelerateConnectionString).toBeUndefined();
794 expect(yield* db.user).toBeNull();
795 expect(Redacted.value((yield* db.password)!)).toBe("password");
796 expect(Object.keys(stored)).toEqual(
797 expect.arrayContaining([
798 "PRISMA_API_CONNECTION_CONNECTION_ID",
799 "PRISMA_API_CONNECTION_DIRECT_CONNECTION_STRING",
800 "PRISMA_API_CONNECTION_POOLED_CONNECTION_STRING",
801 "PRISMA_API_CONNECTION_ACCELERATE_CONNECTION_STRING",
802 "PRISMA_API_CONNECTION_USER",
803 "PRISMA_API_CONNECTION_PASSWORD",
804 ]),
805 );
806 }).pipe(
807 Effect.provide(ConnectBinding),
808 Effect.provide(Layer.succeed(RuntimeContext, runtime)),
809 Effect.provide(Layer.succeed(Self, host)),
810 Effect.provide(
811 Layer.succeed(
812 ConfigProvider.ConfigProvider,
813 ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
814 ),
815 ),
816 );
817 },
818 { tags: ["provider:prisma:connect", "provider:prisma:connection"] },
819 );
820
821 it.effect(
822 "ConnectBinding does not require the deploy-time host at runtime",
823 () => {
824 const stored: Record<string, Output.Output> = {};
825 const runtime = {
826 Type: "Prisma.Compute",
827 id: "App",
828 env: stored,
829 set: (id: string, output: Output.Output) =>
830 Effect.sync(() => {
831 const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
832 stored[key] = output;
833 return key;
834 }),
835 get: <T>(key: string): Effect.Effect<T> => {
836 const output = stored[key];
837 if (!output) return Effect.die(`missing runtime binding ${key}`);
838 return Output.evaluate(output, {}) as Effect.Effect<T>;
839 },
840 };
841 const connection = {
842 Type: "Prisma.Connection",
843 LogicalId: "Connection",
844 FQN: "Connection",
845 connectionId: Output.asOutput("connection-1"),
846 databaseId: Output.asOutput("database-1"),
847 directConnectionString: Output.asOutput(
848 Redacted.make("postgres://runtime"),
849 ),
850 pooledConnectionString: Output.asOutput(undefined),
851 accelerateConnectionString: Output.asOutput(undefined),
852 host: Output.asOutput("db.example.test"),
853 user: Output.asOutput("api"),
854 password: Output.asOutput(Redacted.make("password")),
855 } as PrismaConnection;
856
857 // The deploy-time host dispatch is guarded by `__ALCHEMY_RUNTIME__`,
858 // which bundles fold to `true` — simulate that so no Self is needed.
859 const wasRuntime = globalThis.__ALCHEMY_RUNTIME__;
860 globalThis.__ALCHEMY_RUNTIME__ = true;
861 return Effect.gen(function* () {
862 const db = yield* Connect(connection);
863
864 expect(yield* db.connectionId).toBe("connection-1");
865 expect(Redacted.value(yield* db.databaseUrl)).toBe(
866 "postgres://runtime",
867 );
868 }).pipe(
869 Effect.provide(ConnectBinding),
870 Effect.provide(Layer.succeed(RuntimeContext, runtime)),
871 Effect.provide(
872 Layer.succeed(
873 ConfigProvider.ConfigProvider,
874 ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
875 ),
876 ),
877 Effect.ensuring(
878 Effect.sync(() => {
879 globalThis.__ALCHEMY_RUNTIME__ = wasRuntime;
880 }),
881 ),
882 );
883 },
884 { tags: ["provider:prisma:connect", "provider:prisma:connection"] },
885 );
886
887 it.effect(
888 "Prisma.Compute records Connection.bind env on platform bindings",
889 () => {
890 const stack: Omit<StackSpec, "output"> = {
891 name: "prisma-compute-binding-test",
892 stage: "test",
893 resources: {},
894 bindings: {},
895 actions: {},
896 };
897 const connection = {
898 Type: "Prisma.Connection",
899 LogicalId: "Connection",
900 FQN: "Api/Connection",
901 connectionId: Output.asOutput("connection-1"),
902 databaseId: Output.asOutput("database-1"),
903 directConnectionString: Output.asOutput(
904 Redacted.make("postgres://api"),
905 ),
906 pooledConnectionString: Output.asOutput(
907 Redacted.make("prisma+postgres://api"),
908 ),
909 accelerateConnectionString: Output.asOutput(undefined),
910 host: Output.asOutput("db.example.test"),
911 user: Output.asOutput("api"),
912 password: Output.asOutput(Redacted.make("password")),
913 } as PrismaConnection;
914
915 return Effect.gen(function* () {
916 const app = yield* PrismaCompute(
917 "App",
918 {
919 project: "project-1",
920 appName: "api",
921 main: "app.ts",
922 },
923 Effect.gen(function* () {
924 yield* Connect(connection);
925 }).pipe(Effect.provide(ConnectBinding)),
926 );
927
928 const keys = connectEnvKeys(connection);
929 const binding = stack.bindings[app.FQN]?.[0];
930 const env = yield* Output.evaluate(binding?.data.env ?? {}, {});
931
932 expect(binding?.sid).toBe("Connection");
933 expect(Object.keys(env)).toEqual(
934 expect.arrayContaining([
935 keys.connectionId,
936 keys.databaseId,
937 keys.directConnectionString,
938 keys.pooledConnectionString,
939 keys.password,
940 ]),
941 );
942 expect(env[keys.connectionId]).toBe("connection-1");
943 expect(env[keys.databaseId]).toBe("database-1");
944 expect(
945 redactedValue(env[keys.directConnectionString] ?? undefined),
946 ).toBe("postgres://api");
947 expect(
948 redactedValue(env[keys.pooledConnectionString] ?? undefined),
949 ).toBe("prisma+postgres://api");
950 expect(redactedValue(env[keys.password] ?? undefined)).toBe(
951 "password",
952 );
953 }).pipe(
954 Effect.provide(inMemoryState()),
955 Effect.provide(
956 Layer.succeed(PrismaProviderCollection, {
957 kind: "ProviderCollection" as const,
958 get: () => undefined,
959 providers: {},
960 }),
961 ),
962 Effect.provideService(Stack, stack),
963 Effect.provideService(Stage, "test"),
964 Effect.provide(
965 Layer.succeed(
966 ConfigProvider.ConfigProvider,
967 ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "plan" }),
968 ),
969 ),
970 );
971 },
972 {
973 tags: [
974 "provider:prisma:compute",
975 "provider:prisma:connect",
976 "provider:prisma:connection",
977 ],
978 },
979 );
980
981 it.effect(
982 "Connection.bind records env for AWS Lambda function hosts",
983 () => {
984 const stored: Record<string, Output.Output> = {};
985 let capturedBindingEnv: Record<string, Output.Output> | undefined;
986 const runtime = {
987 Type: "AWS.Lambda.Function",
988 id: "Api",
989 env: stored,
990 set: (id: string, output: Output.Output) =>
991 Effect.sync(() => {
992 const key = id.replaceAll(/[^a-zA-Z0-9]/g, "_");
993 stored[key] = output;
994 return key;
995 }),
996 get: <T>(key: string): Effect.Effect<T> => {
997 const output = stored[key];
998 if (!output) return Effect.die(`missing runtime binding ${key}`);
999 return Output.evaluate(output, {}) as Effect.Effect<T>;
1000 },
1001 };
1002 const host = {
1003 Type: "AWS.Lambda.Function",
1004 LogicalId: "Api",
1005 FQN: "Api",
1006 bind: (...args: unknown[]) =>
1007 args[0] instanceof Array
1008 ? (binding: { env?: Record<string, Output.Output> }) =>
1009 Effect.sync(() => {
1010 capturedBindingEnv = binding.env;
1011 })
1012 : Effect.void,
1013 };
1014 const connection = {
1015 Type: "Prisma.Connection",
1016 LogicalId: "Connection",
1017 FQN: "Connection",
1018 connectionId: Output.asOutput("connection-1"),
1019 databaseId: Output.asOutput("database-1"),
1020 directConnectionString: Output.asOutput(
1021 Redacted.make("postgres://api"),
1022 ),
1023 pooledConnectionString: Output.asOutput(undefined),
1024 accelerateConnectionString: Output.asOutput(undefined),
1025 host: Output.asOutput("db.example.test"),
1026 user: Output.asOutput("api"),
1027 password: Output.asOutput(Redacted.make("password")),
1028 } as PrismaConnection;
1029
1030 return Effect.gen(function* () {
1031 const db = yield* Connect(connection);
1032 const keys = connectEnvKeys(connection);
1033 const env = yield* Output.evaluate(
1034 capturedBindingEnv ?? {},
1035 {},
1036 ) as Effect.Effect<Record<string, unknown>>;
1037
1038 expect(Object.keys(env)).toEqual(
1039 expect.arrayContaining([
1040 keys.connectionId,
1041 keys.databaseId,
1042 keys.directConnectionString,
1043 keys.password,
1044 ]),
1045 );
1046 expect(env[keys.connectionId]).toBe("connection-1");
1047 expect(env[keys.databaseId]).toBe("database-1");
1048 expect(
1049 redactedValue(
1050 env[keys.directConnectionString] as
1051 | string
1052 | Redacted.Redacted<string>
1053 | undefined,
1054 ),
1055 ).toBe("postgres://api");
1056 expect(
1057 redactedValue(
1058 env[keys.password] as
1059 | string
1060 | Redacted.Redacted<string>
1061 | undefined,
1062 ),
1063 ).toBe("password");
1064 expect(Redacted.value(yield* db.databaseUrl)).toBe("postgres://api");
1065 }).pipe(
1066 Effect.provide(ConnectBinding),
1067 Effect.provide(inMemoryState()),
1068 Effect.provide(Layer.succeed(RuntimeContext, runtime)),
1069 Effect.provide(Layer.succeed(Self, host)),
1070 Effect.provide(
1071 Layer.succeed(
1072 ConfigProvider.ConfigProvider,
1073 ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
1074 ),
1075 ),
1076 );
1077 },
1078 { tags: ["provider:prisma:connect", "provider:prisma:connection"] },
1079 );
1080
1081 it.effect(
1082 "Connection.bind records native text bindings for Workers",
1083 () => {
1084 const workerEnv: Record<string, string> = {};
1085 let capturedBindings: unknown[] | undefined;
1086 const runtime = {
1087 Type: "Cloudflare.Worker",
1088 id: "Worker",
1089 env: {},
1090 set: (id: string) =>
1091 Effect.succeed(id.replaceAll(/[^a-zA-Z0-9]/g, "_")),
1092 get: <T>(key: string): Effect.Effect<T> => {
1093 const value = workerEnv[key];
1094 if (value === undefined) {
1095 return Effect.die(`missing worker binding ${key}`);
1096 }
1097 return Effect.succeed(value as T);
1098 },
1099 };
1100 const host = {
1101 Type: "Cloudflare.Worker",
1102 LogicalId: "Worker",
1103 FQN: "Worker",
1104 bind: (...args: unknown[]) =>
1105 args[0] instanceof Array
1106 ? (binding: {
1107 bindings?: Output.Output<{
1108 type: string;
1109 name: string;
1110 text: string;
1111 }>[];
1112 }) =>
1113 Effect.sync(() => {
1114 capturedBindings = binding.bindings;
1115 })
1116 : Effect.void,
1117 };
1118 const connection = {
1119 Type: "Prisma.Connection",
1120 LogicalId: "Connection",
1121 FQN: "Connection",
1122 connectionId: Output.asOutput("connection-1"),
1123 databaseId: Output.asOutput("database-1"),
1124 directConnectionString: Output.asOutput(
1125 Redacted.make("postgres://api"),
1126 ),
1127 pooledConnectionString: Output.asOutput(undefined),
1128 accelerateConnectionString: Output.asOutput(undefined),
1129 host: Output.asOutput("db.example.test"),
1130 user: Output.asOutput("api"),
1131 password: Output.asOutput(Redacted.make("password")),
1132 } as PrismaConnection;
1133
1134 return Effect.gen(function* () {
1135 const db = yield* Connect(connection);
1136 const keys = connectEnvKeys(connection);
1137 const bindings = (yield* Output.evaluate(
1138 capturedBindings ?? [],
1139 {},
1140 )) as Array<{ type: string; name: string; text: string }>;
1141
1142 for (const binding of bindings) {
1143 workerEnv[binding.name] = binding.text;
1144 }
1145
1146 expect(bindings).toEqual(
1147 expect.arrayContaining([
1148 {
1149 type: "plain_text",
1150 name: keys.connectionId,
1151 text: "connection-1",
1152 },
1153 {
1154 type: "plain_text",
1155 name: keys.databaseId,
1156 text: "database-1",
1157 },
1158 {
1159 type: "secret_text",
1160 name: keys.directConnectionString,
1161 text: "postgres://api",
1162 },
1163 {
1164 type: "secret_text",
1165 name: keys.password,
1166 text: "password",
1167 },
1168 ]),
1169 );
1170 expect("connectionString" in db).toBe(false);
1171 expect(Redacted.value(yield* db.databaseUrl)).toBe("postgres://api");
1172 expect(Redacted.value((yield* db.password)!)).toBe("password");
1173 }).pipe(
1174 Effect.provide(ConnectBinding),
1175 Effect.provide(inMemoryState()),
1176 Effect.provide(Layer.succeed(RuntimeContext, runtime)),
1177 Effect.provide(Layer.succeed(Self, host)),
1178 Effect.provide(
1179 Layer.succeed(
1180 ConfigProvider.ConfigProvider,
1181 ConfigProvider.fromUnknown({ ALCHEMY_PHASE: "runtime" }),
1182 ),
1183 ),
1184 );
1185 },
1186 { tags: ["provider:prisma:connect", "provider:prisma:connection"] },
1187 );
1188
1189 it.effect(
1190 "rejects conflicting App branch inputs",
1191 () => {
1192 const { client } = makeClient();
1193
1194 return Effect.gen(function* () {
1195 const serviceProvider = yield* PrismaApp.Provider;
1196 const error = yield* serviceProvider
1197 .reconcile(
1198 reconcileInput("App", {
1199 project: "project-1",
1200 displayName: "api",
1201 branchId: "branch-1",
1202 branchGitName: "main",
1203 }),
1204 )
1205 .pipe(Effect.flip);
1206
1207 expect(error).toBeInstanceOf(Error);
1208 expect((error as Error).message).toContain(
1209 "branchId and branchGitName are mutually exclusive",
1210 );
1211 }).pipe(
1212 Effect.provide(providerLayer(client)),
1213 Effect.provide(managementApi(client).layer),
1214 );
1215 },
1216 {
1217 tags: [
1218 "provider:prisma:app",
1219 "provider:prisma:branch",
1220 "provider:prisma:connection",
1221 "provider:prisma:customdomain",
1222 "provider:prisma:database",
1223 "provider:prisma:deployment",
1224 "provider:prisma:environmentvariable",
1225 "provider:prisma:project",
1226 "provider:prisma:sourcerepository",
1227 ],
1228 },
1229 );
1230
1231 it.effect(
1232 "rejects conflicting Database branch inputs",
1233 () => {
1234 const { client } = makeClient();
1235
1236 return Effect.gen(function* () {
1237 const databaseProvider = yield* PrismaDatabase.Provider;
1238 const error = yield* databaseProvider
1239 .reconcile(
1240 reconcileInput("Database", {
1241 project: "project-1",
1242 name: "main",
1243 branchId: "branch-1",
1244 branchGitName: "main",
1245 }),
1246 )
1247 .pipe(Effect.flip);
1248
1249 expect(error).toBeInstanceOf(Error);
1250 expect((error as Error).message).toContain(
1251 "branchId and branchGitName are mutually exclusive",
1252 );
1253 }).pipe(
1254 Effect.provide(providerLayer(client)),
1255 Effect.provide(managementApi(client).layer),
1256 );
1257 },
1258 {
1259 tags: [
1260 "provider:prisma:app",
1261 "provider:prisma:branch",
1262 "provider:prisma:connection",
1263 "provider:prisma:customdomain",
1264 "provider:prisma:database",
1265 "provider:prisma:deployment",
1266 "provider:prisma:environmentvariable",
1267 "provider:prisma:project",
1268 "provider:prisma:sourcerepository",
1269 ],
1270 },
1271 );
1272
1273 it.effect(
1274 "classifies Prisma resource diffs as updates or replacements",
1275 () => {
1276 const { client } = makeClient();
1277
1278 return Effect.gen(function* () {
1279 const projectProvider = yield* PrismaProject.Provider;
1280 const databaseProvider = yield* PrismaDatabase.Provider;
1281 const connectionProvider = yield* PrismaConnection.Provider;
1282 const branchProvider = yield* PrismaBranch.Provider;
1283 const serviceProvider = yield* PrismaApp.Provider;
1284 const versionProvider = yield* PrismaDeployment.Provider;
1285 const envProvider = yield* PrismaEnvironmentVariable.Provider;
1286 const repoProvider = yield* PrismaSourceRepository.Provider;
1287
1288 expect(
1289 yield* projectProvider.diff!(
1290 diffInput(
1291 { name: "app", region: "us-east-1", createDatabase: false },
1292 { name: "renamed", region: "us-east-1", createDatabase: false },
1293 ),
1294 ),
1295 ).toEqual({ action: "update" });
1296 expect(
1297 yield* projectProvider.diff!(
1298 diffInput(
1299 { name: "app", region: "us-east-1", createDatabase: false },
1300 { name: "app", region: "us-west-2", createDatabase: false },
1301 ),
1302 ),
1303 ).toBeUndefined();
1304 expect(
1305 yield* projectProvider.diff!(
1306 diffInput(
1307 { name: "app", region: "us-east-1", createDatabase: false },
1308 {
1309 name: "app",
1310 region: "us-west-2",
1311 createDatabase: Output.asOutput(false),
1312 },
1313 ),
1314 ),
1315 ).toBeUndefined();
1316 expect(
1317 yield* projectProvider.diff!(
1318 diffInput(
1319 { name: "app", region: "us-east-1", createDatabase: false },
1320 {
1321 name: "app",
1322 region: "us-west-2",
1323 createDatabase: false,
1324 settings: Output.asOutput({}),
1325 },
1326 ),
1327 ),
1328 ).toBeUndefined();
1329 expect(
1330 yield* projectProvider.diff!(
1331 diffInput(
1332 {
1333 name: "app",
1334 region: "us-east-1",
1335 createDatabase: false,
1336 settings: { preview: true, tier: "dev" },
1337 },
1338 {
1339 name: "app",
1340 region: "us-east-1",
1341 createDatabase: false,
1342 settings: { tier: "dev", preview: true },
1343 },
1344 ),
1345 ),
1346 ).toEqual({ action: "update" });
1347
1348 expect(
1349 yield* databaseProvider.diff!(
1350 diffInput(
1351 { project: "project-1", name: "main", region: "us-east-1" },
1352 { project: "project-1", name: "primary", region: "us-east-1" },
1353 ),
1354 ),
1355 ).toEqual({ action: "update" });
1356 expect(
1357 yield* databaseProvider.diff!(
1358 diffInput(
1359 {
1360 project: "project-1",
1361 name: "main",
1362 region: "us-east-1",
1363 source: {
1364 type: "backup",
1365 databaseId: "database-source",
1366 backupId: "backup-1",
1367 },
1368 },
1369 {
1370 project: "project-1",
1371 name: "main",
1372 region: "us-east-1",
1373 source: {
1374 backupId: "backup-1",
1375 databaseId: "database-source",
1376 type: "backup",
1377 },
1378 },
1379 ),
1380 ),
1381 ).toBeUndefined();
1382 expect(
1383 yield* databaseProvider.diff!(
1384 diffInput(
1385 { project: "project-1", name: "main", region: "us-east-1" },
1386 {
1387 project: Output.asOutput("project-1"),
1388 name: "main",
1389 region: "us-west-2",
1390 },
1391 ),
1392 ),
1393 ).toEqual({ action: "replace" });
1394 expect(
1395 yield* databaseProvider.diff!(
1396 diffInput(
1397 {
1398 project: "project-1",
1399 name: "main",
1400 region: "us-east-1",
1401 branchId: "branch-1",
1402 },
1403 {
1404 project: "project-1",
1405 name: "main",
1406 region: "us-west-2",
1407 branchId: Output.asOutput("branch-1"),
1408 },
1409 ),
1410 ),
1411 ).toEqual({ action: "replace" });
1412
1413 expect(
1414 yield* connectionProvider.diff!(
1415 diffInput(
1416 { database: "database-1", name: "api", rotate: false },
1417 { database: "database-1", name: "api", rotate: true },
1418 ),
1419 ),
1420 ).toEqual({ action: "update" });
1421 expect(
1422 yield* connectionProvider.diff!(
1423 diffInput(
1424 { database: "database-1", name: "api" },
1425 { database: "database-1", name: "worker" },
1426 ),
1427 ),
1428 ).toEqual({ action: "replace" });
1429 expect(
1430 yield* connectionProvider.diff!(
1431 diffInput(
1432 { database: "database-1", name: "api" },
1433 { database: Output.asOutput("database-1"), name: "worker" },
1434 ),
1435 ),
1436 ).toEqual({ action: "replace" });
1437 expect(
1438 yield* connectionProvider.diff!(
1439 diffInput(
1440 { database: "database-1", name: "api", rotate: false },
1441 {
1442 database: "database-1",
1443 name: "worker",
1444 rotate: Output.asOutput(false),
1445 },
1446 ),
1447 ),
1448 ).toEqual({ action: "replace" });
1449
1450 expect(
1451 yield* branchProvider.diff!(
1452 diffInput(
1453 { project: "project-1", gitName: "main", isDefault: false },
1454 { project: "project-1", gitName: "main", isDefault: true },
1455 ),
1456 ),
1457 ).toEqual({ action: "update" });
1458 expect(
1459 yield* branchProvider.diff!(
1460 diffInput(
1461 { project: "project-1", gitName: "main", isDefault: true },
1462 { project: "project-1", gitName: "main", isDefault: false },
1463 ),
1464 ),
1465 ).toBeUndefined();
1466 expect(
1467 yield* branchProvider.diff!(
1468 diffInput(
1469 { project: "project-1", gitName: "main" },
1470 { project: "project-1", gitName: "release" },
1471 ),
1472 ),
1473 ).toEqual({ action: "replace" });
1474 expect(
1475 yield* branchProvider.diff!(
1476 diffInput(
1477 { project: "project-1", gitName: "main" },
1478 { project: Output.asOutput("project-1"), gitName: "release" },
1479 ),
1480 ),
1481 ).toEqual({ action: "replace" });
1482 expect(
1483 yield* branchProvider.diff!(
1484 diffInput(
1485 { project: "project-1", gitName: "main", isDefault: false },
1486 {
1487 project: "project-1",
1488 gitName: "release",
1489 isDefault: Output.asOutput(false),
1490 },
1491 ),
1492 ),
1493 ).toEqual({ action: "replace" });
1494
1495 expect(
1496 yield* serviceProvider.diff!(
1497 diffInput(
1498 {
1499 project: "project-1",
1500 displayName: "api",
1501 regionId: "us-east-1",
1502 },
1503 {
1504 project: "project-1",
1505 displayName: "web",
1506 regionId: "us-east-1",
1507 },
1508 ),
1509 ),
1510 ).toEqual({ action: "update" });
1511 const appRegionChanges = yield* Effect.forEach(
1512 [
1513 serviceProvider.diff!(
1514 diffInput(
1515 {
1516 project: "project-1",
1517 displayName: "api",
1518 regionId: "us-east-1",
1519 },
1520 {
1521 project: "project-1",
1522 displayName: "api",
1523 regionId: "us-west-2",
1524 },
1525 ),
1526 ),
1527 serviceProvider.diff!(
1528 diffInput(
1529 {
1530 project: "project-1",
1531 displayName: "api",
1532 regionId: "us-east-1",
1533 },
1534 {
1535 project: Output.asOutput("project-1"),
1536 displayName: "api",
1537 regionId: "us-west-2",
1538 },
1539 ),
1540 ),
1541 serviceProvider.diff!(
1542 diffInput(
1543 {
1544 project: "project-1",
1545 displayName: "api",
1546 regionId: "us-east-1",
1547 branchId: "branch-1",
1548 },
1549 {
1550 project: "project-1",
1551 displayName: "api",
1552 regionId: "us-west-2",
1553 branchId: Output.asOutput("branch-1"),
1554 },
1555 ),
1556 ),
1557 ],
1558 (change) => change.pipe(Effect.result),
1559 );
1560 for (const change of appRegionChanges) {
1561 expect(Result.isFailure(change)).toBe(true);
1562 if (Result.isFailure(change)) {
1563 expect(String(change.failure)).toContain(
1564 "cannot atomically move",
1565 );
1566 }
1567 }
1568
1569 expect(
1570 yield* versionProvider.diff!(
1571 diffInput(
1572 { app: "service-1", start: false },
1573 { app: "service-1", start: true },
1574 ),
1575 ),
1576 ).toEqual({ action: "update" });
1577 expect(
1578 yield* versionProvider.diff!(
1579 diffInput(
1580 { app: "service-1", portMapping: { http: 3000 } },
1581 { app: "service-1", portMapping: { http: 8080 } },
1582 ),
1583 ),
1584 ).toEqual({ action: "replace" });
1585 expect(
1586 yield* versionProvider.diff!(
1587 diffInput(
1588 { app: "service-1", portMapping: { http: 3000 } },
1589 {
1590 app: Output.asOutput("service-1"),
1591 portMapping: { http: 8080 },
1592 },
1593 ),
1594 ),
1595 ).toEqual({ action: "replace" });
1596 expect(
1597 yield* versionProvider.diff!(
1598 diffInput(
1599 {
1600 app: "service-1",
1601 portMapping: { http: 3000 },
1602 start: false,
1603 },
1604 {
1605 app: "service-1",
1606 portMapping: { http: 8080 },
1607 start: Output.asOutput(false),
1608 },
1609 ),
1610 ),
1611 ).toEqual({ action: "replace" });
1612
1613 expect(
1614 yield* envProvider.diff!(
1615 diffInput(
1616 {
1617 project: "project-1",
1618 class: "production" as const,
1619 key: "TOKEN",
1620 value: Redacted.make("old"),
1621 },
1622 {
1623 project: "project-1",
1624 class: "production" as const,
1625 key: "TOKEN",
1626 value: Redacted.make("new"),
1627 },
1628 ),
1629 ),
1630 ).toEqual({ action: "update" });
1631 expect(
1632 yield* envProvider.diff!(
1633 diffInput(
1634 {
1635 project: "project-1",
1636 class: "production" as const,
1637 key: "TOKEN",
1638 value: Redacted.make("same"),
1639 },
1640 {
1641 project: "project-1",
1642 class: "production" as const,
1643 key: "TOKEN",
1644 value: Redacted.make("same"),
1645 },
1646 ),
1647 ),
1648 ).toEqual({ action: "update" });
1649 expect(
1650 yield* envProvider.diff!(
1651 diffInput(
1652 {
1653 project: "project-1",
1654 class: "production" as const,
1655 key: "TOKEN",
1656 value: Redacted.make("secret"),
1657 },
1658 {
1659 project: "project-1",
1660 class: "preview" as const,
1661 key: "TOKEN",
1662 value: Redacted.make("secret"),
1663 },
1664 ),
1665 ),
1666 ).toEqual({ action: "replace" });
1667 expect(
1668 yield* envProvider.diff!(
1669 diffInput(
1670 {
1671 project: "project-1",
1672 class: "production" as const,
1673 key: "TOKEN",
1674 value: Redacted.make("secret"),
1675 },
1676 {
1677 project: Output.asOutput("project-1"),
1678 class: "preview" as const,
1679 key: "TOKEN",
1680 value: Output.asOutput(Redacted.make("secret")),
1681 },
1682 ),
1683 ),
1684 ).toEqual({ action: "replace" });
1685 expect(
1686 yield* envProvider.diff!(
1687 diffInput(
1688 {
1689 project: "project-1",
1690 class: "production" as const,
1691 key: "TOKEN",
1692 value: Redacted.make("secret"),
1693 },
1694 {
1695 project: "project-1",
1696 class: "preview" as const,
1697 key: Output.asOutput("TOKEN"),
1698 value: Redacted.make("secret"),
1699 },
1700 ),
1701 ),
1702 ).toEqual({ action: "replace" });
1703
1704 const repositoryChanges = yield* Effect.forEach(
1705 [
1706 repoProvider.diff!(
1707 diffInput(
1708 { project: "project-1", providerRepositoryId: 123 },
1709 { project: "project-1", providerRepositoryId: 456 },
1710 ),
1711 ),
1712 repoProvider.diff!(
1713 diffInput(
1714 { project: "project-1", providerRepositoryId: 123 },
1715 {
1716 project: Output.asOutput("project-1"),
1717 providerRepositoryId: 456,
1718 },
1719 ),
1720 ),
1721 repoProvider.diff!(
1722 diffInput(
1723 {
1724 project: "project-1",
1725 providerRepositoryId: 123,
1726 installationId: "install-1",
1727 },
1728 {
1729 project: "project-1",
1730 providerRepositoryId: 456,
1731 installationId: Output.asOutput("install-1"),
1732 },
1733 ),
1734 ),
1735 ],
1736 (change) => change.pipe(Effect.result),
1737 );
1738 for (const change of repositoryChanges) {
1739 expect(Result.isFailure(change)).toBe(true);
1740 if (Result.isFailure(change)) {
1741 expect(String(change.failure)).toContain(
1742 "cannot be replaced atomically",
1743 );
1744 }
1745 }
1746 }).pipe(
1747 Effect.provide(providerLayer(client)),
1748 Effect.provide(managementApi(client).layer),
1749 Effect.provide(FetchHttpClient.layer),
1750 Effect.provideService(Stack, {
1751 name: "prisma-provider-diff-test",
1752 stage: "test",
1753 resources: {},
1754 bindings: {},
1755 actions: {},
1756 }),
1757 Effect.provideService(Stage, "test"),
1758 );
1759 },
1760 {
1761 tags: [
1762 "provider:prisma:app",
1763 "provider:prisma:branch",
1764 "provider:prisma:connection",
1765 "provider:prisma:customdomain",
1766 "provider:prisma:database",
1767 "provider:prisma:deployment",
1768 "provider:prisma:environmentvariable",
1769 "provider:prisma:project",
1770 "provider:prisma:sourcerepository",
1771 ],
1772 },
1773 );
1774
1775 it.effect(
1776 "reads existing Prisma resources for adoption and refresh",
1777 () => {
1778 const calls: Call[] = [];
1779 const database = {
1780 id: "database-1",
1781 type: "database" as const,
1782 url: "https://api.prisma.test/v1/databases/database-1",
1783 name: "main",
1784 status: "ready" as const,
1785 createdAt,
1786 isDefault: true,
1787 defaultConnectionId: "connection-1",
1788 connections: [databaseConnection("database-1", "connection-1")],
1789 project: resourceRef("projects", "project-1", "app"),
1790 region: { id: "us-east-1", name: "US East" },
1791 source: { type: "empty" },
1792 branchId: null,
1793 };
1794 const fake = makeFakeManagementApi((request) => {
1795 if (request.pathname === "/v1/projects" && request.method === "GET") {
1796 return page([wireProject()]);
1797 }
1798 if (
1799 request.pathname === "/v1/projects/project-1" &&
1800 request.method === "GET"
1801 ) {
1802 return envelope(wireProject());
1803 }
1804 if (
1805 request.pathname === "/v1/projects/project-1/databases" &&
1806 request.method === "GET"
1807 ) {
1808 return page([
1809 wireDatabase({
1810 isDefault: true,
1811 defaultConnectionId: "connection-1",
1812 connections: [wireConnection()],
1813 }),
1814 ]);
1815 }
1816 return dispatchManagement(client, request);
1817 });
1818
1819 const client = {
1820 listProjectDatabases: (projectId: string, query: unknown) =>
1821 Effect.sync(() => {
1822 calls.push(["listProjectDatabases", { projectId, query }]);
1823 return [database];
1824 }),
1825 listDatabaseConnections: (databaseId: string, query: unknown) =>
1826 Effect.sync(() => {
1827 calls.push(["listDatabaseConnections", { databaseId, query }]);
1828 return [
1829 {
1830 id: "connection-1",
1831 type: "connection" as const,
1832 url: "https://api.prisma.test/v1/connections/connection-1",
1833 name: "api",
1834 createdAt,
1835 kind: "postgres" as const,
1836 endpoints: {
1837 direct: {
1838 host: "db.prisma.test",
1839 port: 5432,
1840 },
1841 },
1842 database: resourceRef("databases", "database-1", "main"),
1843 },
1844 ];
1845 }),
1846 listBranches: (projectId: string, query: unknown) =>
1847 Effect.sync(() => {
1848 calls.push(["listBranches", { projectId, query }]);
1849 return [
1850 {
1851 id: "branch-1",
1852 type: "branch" as const,
1853 url: "https://api.prisma.test/v1/branches/branch-1",
1854 gitName: "main",
1855 isDefault: true,
1856 createdAt,
1857 updatedAt,
1858 project: resourceRef("projects", "project-1", "app"),
1859 },
1860 ];
1861 }),
1862 listApps: (query: { projectId: string; limit?: number }) =>
1863 Effect.sync(() => {
1864 calls.push(["listApps", query]);
1865 return [
1866 {
1867 id: "service-1",
1868 type: "app" as const,
1869 url: "https://api.prisma.test/v1/services/service-1",
1870 name: "api",
1871 region: { id: "us-east-1", name: "US East" },
1872 projectId: query.projectId,
1873 branchId: "branch-1",
1874 latestDeploymentId: "version-1",
1875 appEndpointDomain: "api.prisma.build",
1876 createdAt,
1877 },
1878 ];
1879 }),
1880 listAppDeployments: (appId: string, query: unknown) =>
1881 Effect.sync(() => {
1882 calls.push(["listAppDeployments", { appId, query }]);
1883 return [
1884 {
1885 id: "version-1",
1886 type: "deployment" as const,
1887 serviceId: "service-1",
1888 url: "https://api.prisma.test/v1/deployments/version-1",
1889 foundryVersionId: "foundry-1",
1890 createdAt,
1891 },
1892 ];
1893 }),
1894 getDeployment: (id: string) =>
1895 Effect.sync(() => {
1896 calls.push(["getDeployment", id]);
1897 return {
1898 id,
1899 type: "deployment" as const,
1900 serviceId: "service-1",
1901 url: `https://api.prisma.test/v1/deployments/${id}`,
1902 foundryVersionId: "foundry-1",
1903 status: "running",
1904 previewDomain: "version-1.preview.prisma.build",
1905 createdAt,
1906 };
1907 }),
1908 listEnvironmentVariables: (query: unknown) =>
1909 Effect.sync(() => {
1910 calls.push(["listEnvironmentVariables", query]);
1911 return [
1912 {
1913 id: "env-1",
1914 type: "environment-variable" as const,
1915 url: "https://api.prisma.test/v1/environment-variables/env-1",
1916 projectId: "project-1",
1917 branchId: null,
1918 class: "production" as const,
1919 key: "TOKEN",
1920 valueKid: "kid-1",
1921 isManagedBySystem: false,
1922 createdAt,
1923 updatedAt,
1924 },
1925 ];
1926 }),
1927 listSourceRepositories: (query: unknown) =>
1928 Effect.sync(() => {
1929 calls.push(["listSourceRepositories", query]);
1930 return [
1931 {
1932 id: "repo-1",
1933 type: "source-repository" as const,
1934 url: "https://api.prisma.test/v1/source-repositories/repo-1",
1935 repoId: 123,
1936 provider: "github" as const,
1937 repoFullName: "acme/api",
1938 defaultBranch: "main",
1939 isPrivate: true,
1940 status: "active" as const,
1941 projectId: "project-1",
1942 installationId: "installation-1",
1943 createdAt,
1944 updatedAt,
1945 },
1946 ];
1947 }),
1948 } as unknown as PrismaManagementClient;
1949
1950 return Effect.gen(function* () {
1951 const projectProvider = yield* Provider.findProvider(PrismaProject);
1952 const databaseProvider = yield* Provider.findProvider(PrismaDatabase);
1953 const connectionProvider =
1954 yield* Provider.findProvider(PrismaConnection);
1955 const branchProvider = yield* Provider.findProvider(PrismaBranch);
1956 const serviceProvider = yield* Provider.findProvider(PrismaApp);
1957 const versionProvider =
1958 yield* Provider.findProvider(PrismaDeployment);
1959 const envProvider = yield* Provider.findProvider(
1960 PrismaEnvironmentVariable,
1961 );
1962 const repoProvider = yield* Provider.findProvider(
1963 PrismaSourceRepository,
1964 );
1965
1966 const project = yield* projectProvider.read!(
1967 readInput("Project", { name: "app" }),
1968 );
1969 const database = yield* databaseProvider.read!(
1970 readInput("Database", { project: "project-1", name: "main" }),
1971 );
1972 const connection = yield* connectionProvider.read!(
1973 readInput("Connection", { database: "database-1", name: "api" }),
1974 );
1975 const branch = yield* branchProvider.read!(
1976 readInput("Branch", { project: "project-1", gitName: "main" }),
1977 );
1978 const service = yield* serviceProvider.read!(
1979 readInput("App", {
1980 project: "project-1",
1981 displayName: "api",
1982 }),
1983 );
1984 const version = yield* versionProvider.read!(
1985 readInput(
1986 "Deployment",
1987 { app: "service-1" },
1988 {
1989 deploymentId: "version-1",
1990 appId: "service-1",
1991 foundryVersionId: "foundry-1",
1992 status: "new",
1993 previewDomain: null,
1994 uploadUrl: null,
1995 appEndpointDomain: undefined,
1996 createdAt,
1997 },
1998 ),
1999 );
2000 const env = yield* envProvider.read!(
2001 readInput("EnvironmentVariable", {
2002 project: "project-1",
2003 class: "production" as const,
2004 key: "TOKEN",
2005 value: Redacted.make("secret"),
2006 }),
2007 );
2008 const repo = yield* repoProvider.read!(
2009 readInput("SourceRepository", {
2010 project: "project-1",
2011 providerRepositoryId: 123,
2012 }),
2013 );
2014
2015 expect(project?.projectId).toBe("project-1");
2016 expect(project?.databaseId).toBe("database-1");
2017 expect(database?.databaseId).toBe("database-1");
2018 expect(connection?.connectionId).toBe("connection-1");
2019 expect(branch?.branchId).toBe("branch-1");
2020 expect(service?.appId).toBe("service-1");
2021 expect(version?.deploymentId).toBe("version-1");
2022 expect(version?.status).toBe("running");
2023 expect(env?.environmentVariableId).toBe("env-1");
2024 expect(env?.valueKid).toBe("kid-1");
2025 expect(env?.value && Redacted.value(env.value)).toBe("");
2026 expect(repo?.sourceRepositoryId).toBe("repo-1");
2027 expect(Unowned.is(project!)).toBe(true);
2028 expect(Unowned.is(database!)).toBe(true);
2029 expect(Unowned.is(branch!)).toBe(true);
2030 expect(Unowned.is(env!)).toBe(true);
2031 expect(Unowned.is(repo!)).toBe(true);
2032 expect(routesOf(fake.captured)).toEqual([
2033 "GET /v1/projects",
2034 "GET /v1/projects/project-1/databases",
2035 "GET /v1/projects/project-1/databases",
2036 "GET /v1/databases/database-1/connections",
2037 "GET /v1/databases/database-1/connections",
2038 "GET /v1/databases/database-1/connections",
2039 "GET /v1/projects/project-1/branches",
2040 "GET /v1/services",
2041 "GET /v1/projects/project-1/branches",
2042 "GET /v1/deployments/version-1",
2043 "GET /v1/services/service-1/deployments",
2044 "GET /v1/environment-variables",
2045 "GET /v1/source-repositories",
2046 ]);
2047 expect(calls.map(([operation]) => operation)).toEqual([
2048 "listDatabaseConnections",
2049 "listDatabaseConnections",
2050 "listDatabaseConnections",
2051 "listBranches",
2052 "listApps",
2053 "listBranches",
2054 "getDeployment",
2055 "listAppDeployments",
2056 "listEnvironmentVariables",
2057 "listSourceRepositories",
2058 ]);
2059 expect(routesOf(fake.captured)).not.toContain("POST /v1/projects");
2060 }).pipe(
2061 Effect.provide(providerLayer(client)),
2062 Effect.provide(fake.layer),
2063 Effect.provide(FetchHttpClient.layer),
2064 Effect.provideService(Stack, {
2065 name: "prisma-provider-read-test",
2066 stage: "test",
2067 resources: {},
2068 bindings: {},
2069 actions: {},
2070 }),
2071 Effect.provideService(Stage, "test"),
2072 );
2073 },
2074 {
2075 tags: [
2076 "provider:prisma:app",
2077 "provider:prisma:branch",
2078 "provider:prisma:connection",
2079 "provider:prisma:customdomain",
2080 "provider:prisma:database",
2081 "provider:prisma:deployment",
2082 "provider:prisma:environmentvariable",
2083 "provider:prisma:project",
2084 "provider:prisma:sourcerepository",
2085 ],
2086 },
2087 );
2088
2089 it.effect(
2090 "treats Prisma 404 during delete as already gone",
2091 () => {
2092 const calls: Call[] = [];
2093 const notFound = (method: "GET" | "DELETE", path: string) =>
2094 new PrismaApiError({
2095 method,
2096 path,
2097 status: 404,
2098 message: "not found",
2099 });
2100 const failNotFound = (
2101 operation: string,
2102 id: string,
2103 method: "GET" | "DELETE",
2104 path: string,
2105 ) =>
2106 Effect.gen(function* () {
2107 calls.push([operation, id]);
2108 return yield* Effect.fail(notFound(method, path));
2109 });
2110
2111 const client = {
2112 listApps: (query: unknown) =>
2113 Effect.sync(() => {
2114 calls.push(["listApps", query]);
2115 return [];
2116 }),
2117 deleteProject: (id: string) =>
2118 failNotFound("deleteProject", id, "DELETE", `/v1/projects/${id}`),
2119 getDatabase: (id: string) =>
2120 failNotFound("getDatabase", id, "GET", `/v1/databases/${id}`),
2121 deleteDatabase: (id: string) =>
2122 failNotFound("deleteDatabase", id, "DELETE", `/v1/databases/${id}`),
2123 getConnection: (id: string) =>
2124 failNotFound("getConnection", id, "GET", `/v1/connections/${id}`),
2125 deleteConnection: (id: string) =>
2126 failNotFound(
2127 "deleteConnection",
2128 id,
2129 "DELETE",
2130 `/v1/connections/${id}`,
2131 ),
2132 getBranch: (id: string) =>
2133 failNotFound("getBranch", id, "GET", `/v1/branches/${id}`),
2134 deleteBranch: (id: string) =>
2135 failNotFound("deleteBranch", id, "DELETE", `/v1/branches/${id}`),
2136 getEnvironmentVariable: (id: string) =>
2137 failNotFound(
2138 "getEnvironmentVariable",
2139 id,
2140 "GET",
2141 `/v1/environment-variables/${id}`,
2142 ),
2143 deleteEnvironmentVariable: (id: string) =>
2144 failNotFound(
2145 "deleteEnvironmentVariable",
2146 id,
2147 "DELETE",
2148 `/v1/environment-variables/${id}`,
2149 ),
2150 deleteSourceRepository: (id: string) =>
2151 failNotFound(
2152 "deleteSourceRepository",
2153 id,
2154 "DELETE",
2155 `/v1/source-repositories/${id}`,
2156 ),
2157 getSourceRepository: (id: string) =>
2158 failNotFound(
2159 "getSourceRepository",
2160 id,
2161 "GET",
2162 `/v1/source-repositories/${id}`,
2163 ),
2164 listAppDeployments: (appId: string) =>
2165 failNotFound(
2166 "listAppDeployments",
2167 appId,
2168 "GET",
2169 `/v1/services/${appId}/deployments`,
2170 ),
2171 deleteApp: (id: string) =>
2172 failNotFound("deleteApp", id, "DELETE", `/v1/services/${id}`),
2173 getApp: (id: string) =>
2174 failNotFound("getApp", id, "GET", `/v1/services/${id}`),
2175 getDeployment: (id: string) =>
2176 failNotFound("getDeployment", id, "GET", `/v1/deployments/${id}`),
2177 } as unknown as PrismaManagementClient;
2178
2179 return Effect.gen(function* () {
2180 const projectProvider = yield* PrismaProject.Provider;
2181 const databaseProvider = yield* PrismaDatabase.Provider;
2182 const connectionProvider = yield* PrismaConnection.Provider;
2183 const branchProvider = yield* PrismaBranch.Provider;
2184 const serviceProvider = yield* PrismaApp.Provider;
2185 const versionProvider = yield* PrismaDeployment.Provider;
2186 const envProvider = yield* PrismaEnvironmentVariable.Provider;
2187 const repoProvider = yield* PrismaSourceRepository.Provider;
2188
2189 yield* projectProvider.delete!(
2190 deleteInput("Project", { projectId: "project-1" }),
2191 );
2192 yield* databaseProvider.delete!(
2193 deleteInput("Database", { databaseId: "database-1" }),
2194 );
2195 yield* connectionProvider.delete!(
2196 deleteInput("Connection", { connectionId: "connection-1" }),
2197 );
2198 yield* branchProvider.delete!(
2199 deleteInput("Branch", { branchId: "branch-1" }),
2200 );
2201 yield* envProvider.delete!(
2202 deleteInput("EnvironmentVariable", {
2203 environmentVariableId: "env-1",
2204 }),
2205 );
2206 yield* repoProvider.delete!(
2207 deleteInput("SourceRepository", { sourceRepositoryId: "repo-1" }),
2208 );
2209 yield* serviceProvider.delete!(
2210 deleteInput("App", { appId: "service-1" }),
2211 );
2212 yield* versionProvider.delete!(
2213 deleteInput("Deployment", { deploymentId: "version-1" }),
2214 );
2215
2216 expect(calls).toEqual([
2217 // Over the wire, query params arrive as strings.
2218 ["listApps", { limit: "100", projectId: "project-1" }],
2219 ["deleteProject", "project-1"],
2220 ["getDatabase", "database-1"],
2221 ["getConnection", "connection-1"],
2222 ["getBranch", "branch-1"],
2223 ["getEnvironmentVariable", "env-1"],
2224 ["getSourceRepository", "repo-1"],
2225 ["getApp", "service-1"],
2226 ["getDeployment", "version-1"],
2227 ]);
2228 }).pipe(
2229 Effect.provide(providerLayer(client)),
2230 Effect.provide(managementApi(client).layer),
2231 );
2232 },
2233 {
2234 tags: [
2235 "provider:prisma:app",
2236 "provider:prisma:branch",
2237 "provider:prisma:connection",
2238 "provider:prisma:customdomain",
2239 "provider:prisma:database",
2240 "provider:prisma:deployment",
2241 "provider:prisma:environmentvariable",
2242 "provider:prisma:project",
2243 "provider:prisma:sourcerepository",
2244 ],
2245 },
2246 );
2247
2248 it.effect(
2249 "does not delete cloud projects for dev placeholder project IDs",
2250 () => {
2251 const calls: Call[] = [];
2252 const client = {
2253 listProjects: () =>
2254 Effect.sync(() => {
2255 calls.push(["listProjects"]);
2256 return [
2257 {
2258 id: "project-cloud",
2259 type: "project",
2260 url: "https://api.prisma.test/v1/projects/project-cloud",
2261 name: "local-project",
2262 createdAt,
2263 defaultRegion: "us-east-1",
2264 workspace: resourceRef("workspaces", "workspace-1", "team"),
2265 database: null,
2266 },
2267 ];
2268 }),
2269 deleteProject: (id: string) =>
2270 Effect.sync(() => {
2271 calls.push(["deleteProject", id]);
2272 }),
2273 } as unknown as PrismaManagementClient;
2274
2275 return Effect.gen(function* () {
2276 const projectProvider = yield* PrismaProject.Provider;
2277
2278 yield* projectProvider.delete!(
2279 deleteInput("Project", {
2280 projectId: "dev:project:Project",
2281 projectName: "local-project",
2282 }),
2283 );
2284
2285 expect(calls).toEqual([]);
2286 }).pipe(
2287 Effect.provide(providerLayer(client)),
2288 Effect.provide(managementApi(client).layer),
2289 );
2290 },
2291 {
2292 tags: [
2293 "provider:prisma:app",
2294 "provider:prisma:branch",
2295 "provider:prisma:connection",
2296 "provider:prisma:customdomain",
2297 "provider:prisma:database",
2298 "provider:prisma:deployment",
2299 "provider:prisma:environmentvariable",
2300 "provider:prisma:project",
2301 "provider:prisma:sourcerepository",
2302 ],
2303 },
2304 );
2305
2306 it.effect(
2307 "reconciles each greenfield Prisma resource through the client",
2308 () => {
2309 const { client, calls } = makeClient();
2310 const fake = makeFakeManagementApi((request) => {
2311 if (
2312 request.pathname === "/v1/projects" &&
2313 request.method === "POST"
2314 ) {
2315 return envelope(
2316 { ...wireProject(), database: null },
2317 {
2318 status: 201,
2319 },
2320 );
2321 }
2322 return dispatchManagement(client, request);
2323 });
2324
2325 return Effect.gen(function* () {
2326 const projectProvider = yield* PrismaProject.Provider;
2327 const databaseProvider = yield* PrismaDatabase.Provider;
2328 const connectionProvider = yield* PrismaConnection.Provider;
2329 const branchProvider = yield* PrismaBranch.Provider;
2330 const serviceProvider = yield* PrismaApp.Provider;
2331 const versionProvider = yield* PrismaDeployment.Provider;
2332 const envProvider = yield* PrismaEnvironmentVariable.Provider;
2333 const repoProvider = yield* PrismaSourceRepository.Provider;
2334
2335 const project = yield* projectProvider.reconcile(
2336 reconcileInput("Project", {
2337 name: "app",
2338 createDatabase: false,
2339 region: "us-east-1",
2340 }),
2341 );
2342 const database = yield* databaseProvider.reconcile(
2343 reconcileInput("Database", {
2344 project: project.projectId,
2345 name: "main",
2346 region: "us-east-1",
2347 }),
2348 );
2349 const connection = yield* connectionProvider.reconcile(
2350 reconcileInput("Connection", {
2351 database: database.databaseId,
2352 name: "api",
2353 }),
2354 );
2355 const branch = yield* branchProvider.reconcile(
2356 reconcileInput("Branch", {
2357 project: project.projectId,
2358 gitName: "preview",
2359 isDefault: false,
2360 }),
2361 );
2362 const service = yield* serviceProvider.reconcile(
2363 reconcileInput("App", {
2364 project: project.projectId,
2365 displayName: "api",
2366 regionId: "us-east-1",
2367 }),
2368 );
2369 const version = yield* versionProvider.reconcile(
2370 reconcileInput("Deployment", {
2371 app: service.appId,
2372 portMapping: { http: 3000 },
2373 skipCodeUpload: true,
2374 }),
2375 );
2376 const env = yield* envProvider.reconcile(
2377 reconcileInput("EnvironmentVariable", {
2378 project: project.projectId,
2379 class: "production" as const,
2380 key: "TOKEN",
2381 value: Redacted.make("secret"),
2382 }),
2383 );
2384 const repo = yield* repoProvider.reconcile(
2385 reconcileInput("SourceRepository", {
2386 project: project.projectId,
2387 providerRepositoryId: 123,
2388 }),
2389 );
2390
2391 expect(project.projectId).toBe("project-1");
2392 expect(database.databaseId).toBe("database-1");
2393 expect(Redacted.value(database.directConnectionString!)).toBe(
2394 "postgres://direct",
2395 );
2396 expect(connection.connectionId).toBe("connection-2");
2397 expectJsonNotToContain(
2398 database,
2399 "postgres://direct",
2400 "postgres://pooled",
2401 );
2402 expectJsonNotToContain(connection, "postgres://api-direct");
2403 expect(branch.branchId).toBe("branch-2");
2404 expect(service.appId).toBe("service-1");
2405 expect(version.deploymentId).toBe("version-1");
2406 expect(env.environmentVariableId).toBe("env-1");
2407 expect(Redacted.value(env.value)).toBe("secret");
2408 expectJsonNotToContain(env, "secret");
2409 expect(repo.sourceRepositoryId).toBe("repo-1");
2410
2411 expect(routesOf(fake.captured)).toEqual([
2412 "POST /v1/projects",
2413 "POST /v1/databases",
2414 "GET /v1/databases/database-1/connections",
2415 "POST /v1/connections",
2416 "GET /v1/projects/project-1/branches",
2417 "POST /v1/projects/project-1/branches",
2418 "GET /v1/projects/project-1/branches",
2419 "POST /v1/services",
2420 "GET /v1/projects/project-1/branches",
2421 "POST /v1/services/service-1/deployments",
2422 "GET /v1/deployments/version-1",
2423 "POST /v1/environment-variables",
2424 "GET /v1/services",
2425 "GET /v1/projects/project-1/databases",
2426 "POST /v1/source-repositories",
2427 "GET /v1/source-repositories/repo-1",
2428 "GET /v1/projects/project-1/branches",
2429 ]);
2430 expect(fake.captured[0]?.bodyJson).toEqual({
2431 name: "app",
2432 createDatabase: false,
2433 region: "us-east-1",
2434 });
2435 expect(calls).toEqual([
2436 [
2437 "createDatabase",
2438 // JSON transport drops `undefined` members.
2439 {
2440 projectId: "project-1",
2441 region: "us-east-1",
2442 name: "main",
2443 isDefault: false,
2444 },
2445 ],
2446 [
2447 "listDatabaseConnections",
2448 { databaseId: "database-1", query: { limit: 100 } },
2449 ],
2450 [
2451 "createConnection",
2452 { databaseId: "database-1", name: "api-000000000000" },
2453 ],
2454 ["listBranches", { projectId: "project-1", query: {} }],
2455 [
2456 "createBranch",
2457 {
2458 projectId: "project-1",
2459 input: { gitName: "preview", isDefault: false },
2460 },
2461 ],
2462 [
2463 "listBranches",
2464 { projectId: "project-1", query: { limit: "100" } },
2465 ],
2466 [
2467 "createApp",
2468 // JSON transport drops `undefined` members.
2469 {
2470 projectId: "project-1",
2471 displayName: "api",
2472 branchId: "branch-1",
2473 regionId: "us-east-1",
2474 },
2475 ],
2476 [
2477 "listBranches",
2478 { projectId: "project-1", query: { limit: "100" } },
2479 ],
2480 [
2481 "createAppDeployment",
2482 {
2483 appId: "service-1",
2484 input: {
2485 portMapping: { http: 3000 },
2486 skipCodeUpload: true,
2487 },
2488 },
2489 ],
2490 ["getDeployment", "version-1"],
2491 [
2492 "createEnvironmentVariable",
2493 {
2494 projectId: "project-1",
2495 class: "production",
2496 key: "TOKEN",
2497 value: "secret",
2498 },
2499 ],
2500 [
2501 "listApps",
2502 // Query parameters arrive as strings over the wire.
2503 { limit: "100", projectId: "project-1", branchId: "unassigned" },
2504 ],
2505 [
2506 "listProjectDatabases",
2507 { projectId: "project-1", query: { limit: 100 } },
2508 ],
2509 [
2510 "createSourceRepository",
2511 // JSON transport drops `undefined` members.
2512 {
2513 projectId: "project-1",
2514 provider: "github",
2515 providerRepositoryId: 123,
2516 },
2517 ],
2518 ["getSourceRepository", "repo-1"],
2519 [
2520 "listBranches",
2521 {
2522 projectId: "project-1",
2523 query: { limit: "100", gitName: "main" },
2524 },
2525 ],
2526 ]);
2527 }).pipe(
2528 Effect.provide(providerLayer(client)),
2529 Effect.provide(fake.layer),
2530 Effect.provide(FetchHttpClient.layer),
2531 Effect.provideService(Stack, {
2532 name: "prisma-provider-test",
2533 stage: "test",
2534 resources: {},
2535 bindings: {},
2536 actions: {},
2537 }),
2538 Effect.provideService(Stage, "test"),
2539 );
2540 },
2541 {
2542 tags: [
2543 "provider:prisma:app",
2544 "provider:prisma:branch",
2545 "provider:prisma:connection",
2546 "provider:prisma:customdomain",
2547 "provider:prisma:database",
2548 "provider:prisma:deployment",
2549 "provider:prisma:environmentvariable",
2550 "provider:prisma:project",
2551 "provider:prisma:sourcerepository",
2552 ],
2553 },
2554 );
2555
2556 it.effect(
2557 "reconciles a Prisma custom domain through the client",
2558 () => {
2559 const { client, calls } = makeClient();
2560
2561 return Effect.gen(function* () {
2562 const domainProvider = yield* PrismaCustomDomain.Provider;
2563 const domain = yield* domainProvider.reconcile(
2564 reconcileInput("CustomDomain", {
2565 app: "service-1",
2566 hostname: "api.example.com",
2567 }),
2568 );
2569
2570 expect(domain.customDomainId).toBe("domain-1");
2571 expect(domain.hostname).toBe("api.example.com");
2572 expect(domain.appId).toBe("service-1");
2573 expect(domain.foundryStatus).toBe("pending_dns");
2574 expect(domain.dnsRecords[0]?.type).toBe("CNAME");
2575 expect(calls).toEqual([
2576 ["listAppDomains", "service-1"],
2577 ["getApp", "service-1"],
2578 ["getBranch", "branch-1"],
2579 [
2580 "createAppDomain",
2581 {
2582 appId: "service-1",
2583 input: { hostname: "api.example.com" },
2584 },
2585 ],
2586 ]);
2587 }).pipe(
2588 Effect.provide(providerLayer(client)),
2589 Effect.provide(managementApi(client).layer),
2590 );
2591 },
2592 {
2593 tags: [
2594 "provider:prisma:app",
2595 "provider:prisma:branch",
2596 "provider:prisma:connection",
2597 "provider:prisma:customdomain",
2598 "provider:prisma:database",
2599 "provider:prisma:deployment",
2600 "provider:prisma:environmentvariable",
2601 "provider:prisma:project",
2602 "provider:prisma:sourcerepository",
2603 ],
2604 },
2605 );
2606
2607 it.effect(
2608 "refuses to claim an existing custom domain during reconcile",
2609 () => {
2610 const { client, calls } = makeClient();
2611 const existing = {
2612 id: "domain-existing",
2613 type: "custom-domain" as const,
2614 url: "https://api.prisma.test/v1/domains/domain-existing",
2615 hostname: "api.example.com",
2616 appId: "service-1",
2617 status: "active" as const,
2618 foundryStatus: "ready",
2619 failureReason: null,
2620 failureCategory: null,
2621 certExpiresAt: null,
2622 dnsRecords: [],
2623 createdAt,
2624 updatedAt,
2625 };
2626 Object.assign(client, {
2627 listAppDomains: (appId: string) =>
2628 Effect.sync(() => {
2629 calls.push(["listAppDomains", appId]);
2630 return [existing];
2631 }),
2632 createAppDomain: () =>
2633 Effect.die("must not claim an existing domain"),
2634 });
2635
2636 return Effect.gen(function* () {
2637 const domainProvider = yield* PrismaCustomDomain.Provider;
2638 const error = yield* domainProvider
2639 .reconcile(
2640 reconcileInput("CustomDomain", {
2641 app: "service-1",
2642 hostname: "api.example.com",
2643 }),
2644 )
2645 .pipe(Effect.flip);
2646
2647 expect((error as Error).message).toContain("explicit adoption");
2648 expect(calls).toEqual([["listAppDomains", "service-1"]]);
2649 }).pipe(
2650 Effect.provide(providerLayer(client)),
2651 Effect.provide(managementApi(client).layer),
2652 );
2653 },
2654 {
2655 tags: [
2656 "provider:prisma:app",
2657 "provider:prisma:branch",
2658 "provider:prisma:connection",
2659 "provider:prisma:customdomain",
2660 "provider:prisma:database",
2661 "provider:prisma:deployment",
2662 "provider:prisma:environmentvariable",
2663 "provider:prisma:project",
2664 "provider:prisma:sourcerepository",
2665 ],
2666 },
2667 );
2668
2669 it.effect(
2670 "routes a 200 custom-domain create race through explicit adoption",
2671 () => {
2672 const { client, calls } = makeClient();
2673 let visible = false;
2674 const raced = {
2675 id: "domain-raced",
2676 type: "custom-domain" as const,
2677 url: "https://api.prisma.test/v1/domains/domain-raced",
2678 hostname: "api.example.com",
2679 appId: "service-1",
2680 status: "pending_dns" as const,
2681 foundryStatus: "pending",
2682 failureReason: null,
2683 failureCategory: null,
2684 certExpiresAt: null,
2685 dnsRecords: [],
2686 createdAt,
2687 updatedAt,
2688 };
2689 Object.assign(client, {
2690 listAppDomains: (appId: string) =>
2691 Effect.sync(() => {
2692 calls.push(["listAppDomains", appId]);
2693 return visible ? [raced] : [];
2694 }),
2695 createAppDomain: (appId: string, input: unknown) =>
2696 Effect.sync(() => {
2697 calls.push(["createAppDomain", { appId, input }]);
2698 visible = true;
2699 return { status: 200 as const, domain: raced };
2700 }),
2701 });
2702
2703 return Effect.gen(function* () {
2704 const domainProvider =
2705 yield* Provider.findProvider(PrismaCustomDomain);
2706 const error = yield* domainProvider
2707 .reconcile(
2708 reconcileInput("CustomDomain", {
2709 app: "service-1",
2710 hostname: raced.hostname,
2711 }),
2712 )
2713 .pipe(Effect.flip);
2714
2715 expect((error as Error).message).toContain("explicit adoption");
2716 expect(calls).toContainEqual([
2717 "createAppDomain",
2718 {
2719 appId: "service-1",
2720 input: { hostname: raced.hostname },
2721 },
2722 ]);
2723
2724 const observed = yield* domainProvider.read!(
2725 readInput("CustomDomain", {
2726 app: "service-1",
2727 hostname: raced.hostname,
2728 }),
2729 );
2730 expect(observed?.customDomainId).toBe(raced.id);
2731 expect(Unowned.is(observed!)).toBe(true);
2732 }).pipe(
2733 Effect.provide(providerLayer(client)),
2734 Effect.provide(managementApi(client).layer),
2735 );
2736 },
2737 {
2738 tags: [
2739 "provider:prisma:app",
2740 "provider:prisma:branch",
2741 "provider:prisma:connection",
2742 "provider:prisma:customdomain",
2743 "provider:prisma:database",
2744 "provider:prisma:deployment",
2745 "provider:prisma:environmentvariable",
2746 "provider:prisma:project",
2747 "provider:prisma:sourcerepository",
2748 ],
2749 },
2750 );
2751
2752 it.effect(
2753 "normalizes Prisma custom domain hostnames when matching",
2754 () => {
2755 const { client, calls } = makeClient();
2756 Object.assign(client, {
2757 listAppDomains: (appId: string) =>
2758 Effect.sync(() => {
2759 calls.push(["listAppDomains", appId]);
2760 return [
2761 {
2762 id: "domain-1",
2763 type: "custom-domain" as const,
2764 url: "https://api.prisma.test/v1/domains/domain-1",
2765 hostname: "api.example.com",
2766 appId: appId,
2767 status: "pending_dns" as const,
2768 foundryStatus: "pending",
2769 failureReason: null,
2770 failureCategory: null,
2771 certExpiresAt: null,
2772 dnsRecords: [
2773 {
2774 type: "CNAME" as const,
2775 name: "api.example.com",
2776 value: "service-1.prisma.build",
2777 ttl: null,
2778 },
2779 ],
2780 createdAt,
2781 updatedAt,
2782 },
2783 ];
2784 }),
2785 });
2786
2787 return Effect.gen(function* () {
2788 const domainProvider =
2789 yield* Provider.findProvider(PrismaCustomDomain);
2790 const domain = yield* domainProvider.read!(
2791 readInput("CustomDomain", {
2792 app: "service-1",
2793 hostname: "API.EXAMPLE.COM.",
2794 }),
2795 );
2796
2797 expect(domain?.customDomainId).toBe("domain-1");
2798 expect(domain?.hostname).toBe("api.example.com");
2799 expect(Unowned.is(domain!)).toBe(true);
2800 expect(calls).toEqual([["listAppDomains", "service-1"]]);
2801 }).pipe(
2802 Effect.provide(providerLayer(client)),
2803 Effect.provide(managementApi(client).layer),
2804 );
2805 },
2806 {
2807 tags: [
2808 "provider:prisma:app",
2809 "provider:prisma:branch",
2810 "provider:prisma:connection",
2811 "provider:prisma:customdomain",
2812 "provider:prisma:database",
2813 "provider:prisma:deployment",
2814 "provider:prisma:environmentvariable",
2815 "provider:prisma:project",
2816 "provider:prisma:sourcerepository",
2817 ],
2818 },
2819 );
2820
2821 it.effect(
2822 "rejects Prisma custom domains on non-default branches",
2823 () => {
2824 const { client, calls } = makeClient();
2825 Object.assign(client, {
2826 getBranch: (id: string) =>
2827 Effect.sync(() => {
2828 calls.push(["getBranch", id]);
2829 return {
2830 id,
2831 type: "branch" as const,
2832 url: `https://api.prisma.test/v1/branches/${id}`,
2833 gitName: "preview",
2834 isDefault: false,
2835 createdAt,
2836 updatedAt,
2837 project: resourceRef("projects", "project-1", "app"),
2838 };
2839 }),
2840 });
2841
2842 return Effect.gen(function* () {
2843 const domainProvider = yield* PrismaCustomDomain.Provider;
2844 const error = yield* domainProvider
2845 .reconcile(
2846 reconcileInput("CustomDomain", {
2847 app: "service-1",
2848 hostname: "api.example.com",
2849 }),
2850 )
2851 .pipe(Effect.flip);
2852
2853 expect(error).toBeInstanceOf(Error);
2854 expect((error as Error).message).toContain(
2855 "custom domains can only be attached to apps on the default Branch",
2856 );
2857 expect(calls).toEqual([
2858 ["listAppDomains", "service-1"],
2859 ["getApp", "service-1"],
2860 ["getBranch", "branch-1"],
2861 ]);
2862 }).pipe(
2863 Effect.provide(providerLayer(client)),
2864 Effect.provide(managementApi(client).layer),
2865 );
2866 },
2867 {
2868 tags: [
2869 "provider:prisma:app",
2870 "provider:prisma:branch",
2871 "provider:prisma:connection",
2872 "provider:prisma:customdomain",
2873 "provider:prisma:database",
2874 "provider:prisma:deployment",
2875 "provider:prisma:environmentvariable",
2876 "provider:prisma:project",
2877 "provider:prisma:sourcerepository",
2878 ],
2879 },
2880 );
2881
2882 it.effect(
2883 "starts a direct deployment only after observing status",
2884 () => {
2885 const calls: Call[] = [];
2886 let status = "new";
2887 let latestDeploymentId: string | null = null;
2888 const client = {
2889 createAppDeployment: (appId: string, input: unknown) =>
2890 Effect.sync(() => {
2891 calls.push(["createAppDeployment", { appId, input }]);
2892 return {
2893 id: "version-1",
2894 type: "deployment" as const,
2895 url: "https://api.prisma.test/v1/deployments/version-1",
2896 foundryVersionId: "foundry-1",
2897 uploadUrl: null,
2898 };
2899 }),
2900 getDeployment: (id: string) =>
2901 Effect.sync(() => {
2902 calls.push(["getDeployment", id]);
2903 return {
2904 id,
2905 type: "deployment" as const,
2906 serviceId: "service-1",
2907 url: `https://api.prisma.test/v1/deployments/${id}`,
2908 foundryVersionId: "foundry-1",
2909 status,
2910 previewDomain:
2911 status === "running"
2912 ? "version-1.preview.prisma.build"
2913 : null,
2914 createdAt,
2915 };
2916 }),
2917 startDeployment: (id: string) =>
2918 Effect.sync(() => {
2919 calls.push(["startDeployment", id]);
2920 status = "running";
2921 return { previewDomain: "version-1.preview.prisma.build" };
2922 }),
2923 getApp: (id: string) =>
2924 Effect.sync(() => {
2925 calls.push(["getApp", id]);
2926 return {
2927 id,
2928 type: "app" as const,
2929 url: `https://api.prisma.test/v1/services/${id}`,
2930 name: "api-000000000000",
2931 region: { id: "us-east-1", name: "US East" },
2932 projectId: "project-1",
2933 branchId: null,
2934 latestDeploymentId,
2935 appEndpointDomain: "api.prisma.build",
2936 createdAt,
2937 };
2938 }),
2939 promoteApp: (
2940 appId: string,
2941 { deploymentId }: { deploymentId: string },
2942 ) =>
2943 Effect.sync(() => {
2944 calls.push(["promoteApp", { appId, deploymentId }]);
2945 latestDeploymentId = deploymentId;
2946 return { appEndpointDomain: "api.prisma.build" };
2947 }),
2948 } as unknown as PrismaManagementClient;
2949
2950 return Effect.gen(function* () {
2951 const provider = yield* PrismaDeployment.Provider;
2952 const output = yield* provider.reconcile(
2953 reconcileInput("Deployment", {
2954 app: "service-1",
2955 skipCodeUpload: true,
2956 start: true,
2957 promote: true,
2958 }),
2959 );
2960
2961 expect(output.status).toBe("running");
2962 expect(output.previewDomain).toBe("version-1.preview.prisma.build");
2963 expect(output.appEndpointDomain).toBe("api.prisma.build");
2964 expect(calls).toEqual([
2965 [
2966 "createAppDeployment",
2967 {
2968 appId: "service-1",
2969 input: {
2970 portMapping: undefined,
2971 skipCodeUpload: true,
2972 },
2973 },
2974 ],
2975 ["getDeployment", "version-1"],
2976 ["startDeployment", "version-1"],
2977 ["getDeployment", "version-1"],
2978 ["promoteApp", { appId: "service-1", deploymentId: "version-1" }],
2979 ["getApp", "service-1"],
2980 ]);
2981 }).pipe(
2982 Effect.provide(providerLayer(client)),
2983 Effect.provide(managementApi(client).layer),
2984 Effect.provide(FetchHttpClient.layer),
2985 Effect.provideService(Stack, {
2986 name: "prisma-deployment-test",
2987 stage: "test",
2988 resources: {},
2989 bindings: {},
2990 actions: {},
2991 }),
2992 Effect.provideService(Stage, "test"),
2993 );
2994 },
2995 {
2996 tags: [
2997 "provider:prisma:app",
2998 "provider:prisma:branch",
2999 "provider:prisma:connection",
3000 "provider:prisma:customdomain",
3001 "provider:prisma:database",
3002 "provider:prisma:deployment",
3003 "provider:prisma:environmentvariable",
3004 "provider:prisma:project",
3005 "provider:prisma:sourcerepository",
3006 ],
3007 },
3008 );
3009
3010 it.effect(
3011 "ignores branch env overrides when reconciling project env vars",
3012 () => {
3013 const calls: Call[] = [];
3014 const branchVariable = {
3015 id: "env-branch",
3016 type: "environment-variable" as const,
3017 url: "https://api.prisma.test/v1/environment-variables/env-branch",
3018 projectId: "project-1",
3019 branchId: "branch-1",
3020 class: "production" as const,
3021 key: "TOKEN",
3022 valueKid: "kid-branch",
3023 isManagedBySystem: false,
3024 createdAt,
3025 updatedAt,
3026 };
3027 const projectVariable = {
3028 id: "env-project",
3029 type: "environment-variable" as const,
3030 url: "https://api.prisma.test/v1/environment-variables/env-project",
3031 projectId: "project-1",
3032 branchId: null,
3033 class: "production" as const,
3034 key: "TOKEN",
3035 valueKid: "kid-old",
3036 isManagedBySystem: false,
3037 createdAt,
3038 updatedAt,
3039 };
3040 const client = {
3041 listEnvironmentVariables: (query: unknown) =>
3042 Effect.sync(() => {
3043 calls.push(["listEnvironmentVariables", query]);
3044 return [
3045 branchVariable,
3046 { ...branchVariable, id: "env-branch-2", branchId: "branch-2" },
3047 projectVariable,
3048 ];
3049 }),
3050 getEnvironmentVariable: (id: string) =>
3051 Effect.sync(() => {
3052 calls.push(["getEnvironmentVariable", id]);
3053 return projectVariable;
3054 }),
3055 updateEnvironmentVariable: (id: string, input: unknown) =>
3056 Effect.sync(() => {
3057 calls.push(["updateEnvironmentVariable", { id, input }]);
3058 return {
3059 ...projectVariable,
3060 id,
3061 valueKid: "kid-new",
3062 updatedAt: "2026-01-01T00:00:02Z",
3063 };
3064 }),
3065 } as unknown as PrismaManagementClient;
3066
3067 return Effect.gen(function* () {
3068 const envProvider = yield* Provider.findProvider(
3069 PrismaEnvironmentVariable,
3070 );
3071 const observed = yield* envProvider.read!(
3072 readInput("EnvironmentVariable", {
3073 project: "project-1",
3074 class: "production" as const,
3075 key: "TOKEN",
3076 value: Redacted.make("secret"),
3077 }),
3078 );
3079 expect(Unowned.is(observed!)).toBe(true);
3080 const env = yield* envProvider.reconcile(
3081 reconcileInput(
3082 "EnvironmentVariable",
3083 {
3084 project: "project-1",
3085 class: "production" as const,
3086 key: "TOKEN",
3087 value: Redacted.make("secret"),
3088 },
3089 observed,
3090 ),
3091 );
3092
3093 expect(env.environmentVariableId).toBe("env-project");
3094 expect(env.branchId).toBeNull();
3095 expect(redactedValue(env.value)).toBe("secret");
3096 expect(calls).toEqual([
3097 [
3098 "listEnvironmentVariables",
3099 // Query parameters arrive as strings over the wire.
3100 {
3101 limit: "100",
3102 projectId: "project-1",
3103 class: "production",
3104 key: "TOKEN",
3105 },
3106 ],
3107 ["getEnvironmentVariable", "env-project"],
3108 [
3109 "updateEnvironmentVariable",
3110 { id: "env-project", input: { value: "secret" } },
3111 ],
3112 ]);
3113 }).pipe(
3114 Effect.provide(providerLayer(client)),
3115 Effect.provide(managementApi(client).layer),
3116 );
3117 },
3118 {
3119 tags: [
3120 "provider:prisma:app",
3121 "provider:prisma:branch",
3122 "provider:prisma:connection",
3123 "provider:prisma:customdomain",
3124 "provider:prisma:database",
3125 "provider:prisma:deployment",
3126 "provider:prisma:environmentvariable",
3127 "provider:prisma:project",
3128 "provider:prisma:sourcerepository",
3129 ],
3130 },
3131 );
3132
3133 it.effect(
3134 "refuses to mutate system-managed environment variables",
3135 () => {
3136 const calls: Call[] = [];
3137 const systemVariable = {
3138 id: "env-system",
3139 type: "environment-variable" as const,
3140 url: "https://api.prisma.test/v1/environment-variables/env-system",
3141 projectId: "project-1",
3142 branchId: null,
3143 class: "production" as const,
3144 key: "PRISMA_INTERNAL_URL",
3145 valueKid: "kid-system",
3146 isManagedBySystem: true,
3147 createdAt,
3148 updatedAt,
3149 };
3150 const client = {
3151 listEnvironmentVariables: (query: unknown) =>
3152 Effect.sync(() => {
3153 calls.push(["listEnvironmentVariables", query]);
3154 return [systemVariable];
3155 }),
3156 getEnvironmentVariable: (id: string) =>
3157 Effect.sync(() => {
3158 calls.push(["getEnvironmentVariable", id]);
3159 return systemVariable;
3160 }),
3161 updateEnvironmentVariable: (id: string, input: unknown) =>
3162 Effect.sync(() => {
3163 calls.push(["updateEnvironmentVariable", { id, input }]);
3164 return systemVariable;
3165 }),
3166 } as unknown as PrismaManagementClient;
3167
3168 return Effect.gen(function* () {
3169 const envProvider = yield* Provider.findProvider(
3170 PrismaEnvironmentVariable,
3171 );
3172 const observed = yield* envProvider.read!(
3173 readInput("EnvironmentVariable", {
3174 project: "project-1",
3175 class: "production" as const,
3176 key: "PRISMA_INTERNAL_URL",
3177 value: Redacted.make("secret"),
3178 }),
3179 );
3180 const error = yield* envProvider
3181 .reconcile(
3182 reconcileInput(
3183 "EnvironmentVariable",
3184 {
3185 project: "project-1",
3186 class: "production" as const,
3187 key: "PRISMA_INTERNAL_URL",
3188 value: Redacted.make("secret"),
3189 },
3190 observed,
3191 ),
3192 )
3193 .pipe(Effect.flip);
3194
3195 expect(error).toBeInstanceOf(Error);
3196 expect((error as Error).message).toContain(
3197 "is managed by Prisma and cannot be managed by Alchemy",
3198 );
3199 expect(calls).toEqual([
3200 [
3201 "listEnvironmentVariables",
3202 // Query parameters arrive as strings over the wire.
3203 {
3204 limit: "100",
3205 projectId: "project-1",
3206 class: "production",
3207 key: "PRISMA_INTERNAL_URL",
3208 },
3209 ],
3210 ["getEnvironmentVariable", "env-system"],
3211 ]);
3212 }).pipe(
3213 Effect.provide(providerLayer(client)),
3214 Effect.provide(managementApi(client).layer),
3215 );
3216 },
3217 {
3218 tags: [
3219 "provider:prisma:app",
3220 "provider:prisma:branch",
3221 "provider:prisma:connection",
3222 "provider:prisma:customdomain",
3223 "provider:prisma:database",
3224 "provider:prisma:deployment",
3225 "provider:prisma:environmentvariable",
3226 "provider:prisma:project",
3227 "provider:prisma:sourcerepository",
3228 ],
3229 },
3230 );
3231
3232 it.effect(
3233 "skips direct delete for system-managed environment variables",
3234 () => {
3235 const calls: Call[] = [];
3236 const notes: string[] = [];
3237 const client = {
3238 getEnvironmentVariable: (id: string) =>
3239 Effect.sync(() => {
3240 calls.push(["getEnvironmentVariable", id]);
3241 return {
3242 id,
3243 type: "environment-variable" as const,
3244 url: `https://api.prisma.test/v1/environment-variables/${id}`,
3245 projectId: "project-1",
3246 branchId: null,
3247 class: "production" as const,
3248 key: "PRISMA_INTERNAL_URL",
3249 valueKid: "kid-system",
3250 isManagedBySystem: true,
3251 createdAt,
3252 updatedAt,
3253 };
3254 }),
3255 deleteEnvironmentVariable: (id: string) =>
3256 Effect.sync(() => {
3257 calls.push(["deleteEnvironmentVariable", id]);
3258 }),
3259 } as unknown as PrismaManagementClient;
3260
3261 return Effect.gen(function* () {
3262 const envProvider = yield* PrismaEnvironmentVariable.Provider;
3263 yield* envProvider.delete!({
3264 id: "EnvironmentVariable",
3265 fqn: "EnvironmentVariable",
3266 instanceId: "00000000000000000000000000000000",
3267 olds: {} as never,
3268 output: {
3269 environmentVariableId: "env-system",
3270 projectId: "project-1",
3271 branchId: null,
3272 class: "production",
3273 key: "PRISMA_INTERNAL_URL",
3274 value: Redacted.make("secret"),
3275 valueKid: "kid-system",
3276 isManagedBySystem: true,
3277 createdAt,
3278 updatedAt,
3279 },
3280 session: {
3281 note: (message: string) =>
3282 Effect.sync(() => {
3283 notes.push(message);
3284 }),
3285 } as never,
3286 bindings: [],
3287 });
3288
3289 expect(calls).toEqual([["getEnvironmentVariable", "env-system"]]);
3290 expect(notes).toEqual([
3291 "Skipping direct delete for system-managed Prisma environment variable 'PRISMA_INTERNAL_URL'.",
3292 ]);
3293 }).pipe(
3294 Effect.provide(providerLayer(client)),
3295 Effect.provide(managementApi(client).layer),
3296 );
3297 },
3298 {
3299 tags: [
3300 "provider:prisma:app",
3301 "provider:prisma:branch",
3302 "provider:prisma:connection",
3303 "provider:prisma:customdomain",
3304 "provider:prisma:database",
3305 "provider:prisma:deployment",
3306 "provider:prisma:environmentvariable",
3307 "provider:prisma:project",
3308 "provider:prisma:sourcerepository",
3309 ],
3310 },
3311 );
3312
3313 it.effect(
3314 "checks live environment variable ownership before deleting",
3315 () => {
3316 const calls: Call[] = [];
3317 const client = {
3318 getEnvironmentVariable: (id: string) =>
3319 Effect.sync(() => {
3320 calls.push(["getEnvironmentVariable", id]);
3321 return {
3322 id,
3323 type: "environment-variable" as const,
3324 url: `https://api.prisma.test/v1/environment-variables/${id}`,
3325 projectId: "project-1",
3326 branchId: null,
3327 class: "production" as const,
3328 key: "PRISMA_INTERNAL_URL",
3329 valueKid: "kid-system",
3330 isManagedBySystem: true,
3331 createdAt,
3332 updatedAt,
3333 };
3334 }),
3335 deleteEnvironmentVariable: (id: string) =>
3336 Effect.sync(() => {
3337 calls.push(["deleteEnvironmentVariable", id]);
3338 }),
3339 } as unknown as PrismaManagementClient;
3340
3341 const session = {
3342 note: (message: string) =>
3343 Effect.sync(() => {
3344 calls.push(["note", message]);
3345 }),
3346 };
3347
3348 return Effect.gen(function* () {
3349 const envProvider = yield* PrismaEnvironmentVariable.Provider;
3350 yield* envProvider.delete!({
3351 id: "EnvironmentVariable",
3352 fqn: "EnvironmentVariable",
3353 instanceId: "00000000000000000000000000000000",
3354 olds: {} as never,
3355 output: {
3356 environmentVariableId: "env-system",
3357 projectId: "project-1",
3358 branchId: null,
3359 class: "production",
3360 key: "PRISMA_INTERNAL_URL",
3361 value: Redacted.make("secret"),
3362 valueKid: "kid-user",
3363 isManagedBySystem: false,
3364 createdAt,
3365 updatedAt,
3366 },
3367 session: session as never,
3368 bindings: [],
3369 });
3370
3371 expect(calls).toEqual([
3372 ["getEnvironmentVariable", "env-system"],
3373 [
3374 "note",
3375 "Skipping direct delete for system-managed Prisma environment variable 'PRISMA_INTERNAL_URL'.",
3376 ],
3377 ]);
3378 }).pipe(
3379 Effect.provide(providerLayer(client)),
3380 Effect.provide(managementApi(client).layer),
3381 );
3382 },
3383 {
3384 tags: [
3385 "provider:prisma:app",
3386 "provider:prisma:branch",
3387 "provider:prisma:connection",
3388 "provider:prisma:customdomain",
3389 "provider:prisma:database",
3390 "provider:prisma:deployment",
3391 "provider:prisma:environmentvariable",
3392 "provider:prisma:project",
3393 "provider:prisma:sourcerepository",
3394 ],
3395 },
3396 );
3397
3398 it.effect(
3399 "deletes an environment variable when stale state says system-managed",
3400 () => {
3401 const calls: Call[] = [];
3402 const client = {
3403 getEnvironmentVariable: (id: string) =>
3404 Effect.sync(() => {
3405 calls.push(["getEnvironmentVariable", id]);
3406 return {
3407 id,
3408 type: "environment-variable" as const,
3409 url: `https://api.prisma.test/v1/environment-variables/${id}`,
3410 projectId: "project-1",
3411 branchId: null,
3412 class: "production" as const,
3413 key: "PRISMA_INTERNAL_URL",
3414 valueKid: "kid-user",
3415 isManagedBySystem: false,
3416 createdAt,
3417 updatedAt,
3418 };
3419 }),
3420 deleteEnvironmentVariable: (id: string) =>
3421 Effect.sync(() => {
3422 calls.push(["deleteEnvironmentVariable", id]);
3423 }),
3424 } as unknown as PrismaManagementClient;
3425
3426 return Effect.gen(function* () {
3427 const envProvider = yield* PrismaEnvironmentVariable.Provider;
3428 yield* envProvider.delete!({
3429 id: "EnvironmentVariable",
3430 fqn: "EnvironmentVariable",
3431 instanceId: "00000000000000000000000000000000",
3432 olds: {} as never,
3433 output: {
3434 environmentVariableId: "env-1",
3435 projectId: "project-1",
3436 branchId: null,
3437 class: "production",
3438 key: "PRISMA_INTERNAL_URL",
3439 value: Redacted.make("secret"),
3440 valueKid: "kid-system",
3441 isManagedBySystem: true,
3442 createdAt,
3443 updatedAt,
3444 },
3445 session: undefined as never,
3446 bindings: [],
3447 });
3448
3449 expect(calls).toEqual([
3450 ["getEnvironmentVariable", "env-1"],
3451 ["deleteEnvironmentVariable", "env-1"],
3452 ]);
3453 }).pipe(
3454 Effect.provide(providerLayer(client)),
3455 Effect.provide(managementApi(client).layer),
3456 );
3457 },
3458 {
3459 tags: [
3460 "provider:prisma:app",
3461 "provider:prisma:branch",
3462 "provider:prisma:connection",
3463 "provider:prisma:customdomain",
3464 "provider:prisma:database",
3465 "provider:prisma:deployment",
3466 "provider:prisma:environmentvariable",
3467 "provider:prisma:project",
3468 "provider:prisma:sourcerepository",
3469 ],
3470 },
3471 );
3472
3473 it.effect(
3474 "validates Prisma environment variable writes locally",
3475 () => {
3476 const calls: Call[] = [];
3477 const client = {
3478 listEnvironmentVariables: (query: unknown) =>
3479 Effect.sync(() => {
3480 calls.push(["listEnvironmentVariables", query]);
3481 return [];
3482 }),
3483 createEnvironmentVariable: (input: unknown) =>
3484 Effect.sync(() => {
3485 calls.push(["createEnvironmentVariable", input]);
3486 return {
3487 id: "env-1",
3488 type: "environment-variable" as const,
3489 url: "https://api.prisma.test/v1/environment-variables/env-1",
3490 projectId: "project-1",
3491 branchId: null,
3492 class: "production" as const,
3493 key: "TOKEN",
3494 valueKid: "kid-1",
3495 isManagedBySystem: false,
3496 createdAt,
3497 updatedAt,
3498 };
3499 }),
3500 } as unknown as PrismaManagementClient;
3501
3502 return Effect.gen(function* () {
3503 const envProvider = yield* PrismaEnvironmentVariable.Provider;
3504 const invalidKey = yield* envProvider
3505 .reconcile(
3506 reconcileInput("EnvironmentVariable", {
3507 project: "project-1",
3508 class: "production" as const,
3509 key: "bad-key",
3510 value: Redacted.make("secret"),
3511 }),
3512 )
3513 .pipe(Effect.flip);
3514 const emptyValue = yield* envProvider
3515 .reconcile(
3516 reconcileInput("EnvironmentVariable", {
3517 project: "project-1",
3518 class: "production" as const,
3519 key: "TOKEN",
3520 value: Redacted.make(""),
3521 }),
3522 )
3523 .pipe(Effect.flip);
3524
3525 expect(invalidKey).toBeInstanceOf(Error);
3526 expect((invalidKey as Error).message).toContain(
3527 "must match POSIX env-var key shape",
3528 );
3529 expect(emptyValue).toBeInstanceOf(Error);
3530 expect((emptyValue as Error).message).toContain(
3531 "value must be non-empty",
3532 );
3533 expect(calls).toEqual([]);
3534 }).pipe(
3535 Effect.provide(providerLayer(client)),
3536 Effect.provide(managementApi(client).layer),
3537 );
3538 },
3539 {
3540 tags: [
3541 "provider:prisma:app",
3542 "provider:prisma:branch",
3543 "provider:prisma:connection",
3544 "provider:prisma:customdomain",
3545 "provider:prisma:database",
3546 "provider:prisma:deployment",
3547 "provider:prisma:environmentvariable",
3548 "provider:prisma:project",
3549 "provider:prisma:sourcerepository",
3550 ],
3551 },
3552 );
3553
3554 it.effect(
3555 "updates mutable Prisma resources from observed state",
3556 () => {
3557 const calls: Call[] = [];
3558 const client = {
3559 getDatabase: (id: string) =>
3560 Effect.sync(() => {
3561 calls.push(["getDatabase", id]);
3562 return {
3563 id,
3564 type: "database" as const,
3565 url: `https://api.prisma.test/v1/databases/${id}`,
3566 name: "main",
3567 status: "ready" as const,
3568 createdAt,
3569 isDefault: false,
3570 defaultConnectionId: "connection-1",
3571 connections: [databaseConnection(id, "connection-1")],
3572 project: resourceRef("projects", "project-1", "app"),
3573 region: { id: "us-east-1", name: "US East" },
3574 source: { type: "empty" },
3575 branchId: null,
3576 };
3577 }),
3578 updateDatabase: (id: string, input: unknown) =>
3579 Effect.sync(() => {
3580 calls.push(["updateDatabase", { id, input }]);
3581 return {
3582 id,
3583 type: "database" as const,
3584 url: `https://api.prisma.test/v1/databases/${id}`,
3585 name: "primary",
3586 status: "ready" as const,
3587 createdAt,
3588 isDefault: false,
3589 defaultConnectionId: "connection-1",
3590 connections: [databaseConnection(id, "connection-1")],
3591 project: resourceRef("projects", "project-1", "app"),
3592 region: { id: "us-east-1", name: "US East" },
3593 source: { type: "empty" },
3594 branchId: "branch-1",
3595 };
3596 }),
3597 getConnection: (id: string) =>
3598 Effect.sync(() => {
3599 calls.push(["getConnection", id]);
3600 return {
3601 id,
3602 type: "connection" as const,
3603 url: `https://api.prisma.test/v1/connections/${id}`,
3604 name: "api-000000000000",
3605 createdAt,
3606 kind: "postgres" as const,
3607 endpoints: {
3608 direct: {
3609 host: "db.prisma.test",
3610 port: 5432,
3611 connectionString: "postgres://old-direct",
3612 },
3613 },
3614 database: resourceRef("databases", "database-1", "main"),
3615 };
3616 }),
3617 rotateConnection: (id: string) =>
3618 Effect.sync(() => {
3619 calls.push(["rotateConnection", id]);
3620 return {
3621 id,
3622 type: "connection" as const,
3623 url: `https://api.prisma.test/v1/connections/${id}`,
3624 name: "api-000000000000",
3625 createdAt,
3626 kind: "postgres" as const,
3627 endpoints: {
3628 direct: {
3629 host: "db.prisma.test",
3630 port: 5432,
3631 connectionString:
3632 "postgres://app:new-password@db.prisma.test/database-1",
3633 },
3634 },
3635 database: resourceRef("databases", "database-1", "main"),
3636 };
3637 }),
3638 getBranch: (id: string) =>
3639 Effect.sync(() => {
3640 calls.push(["getBranch", id]);
3641 return {
3642 id,
3643 type: "branch" as const,
3644 url: `https://api.prisma.test/v1/branches/${id}`,
3645 gitName: "main",
3646 isDefault: false,
3647 role: "preview" as const,
3648 createdAt,
3649 updatedAt,
3650 project: resourceRef("projects", "project-1", "app"),
3651 };
3652 }),
3653 updateBranch: (id: string, input: unknown) =>
3654 Effect.sync(() => {
3655 calls.push(["updateBranch", { id, input }]);
3656 return {
3657 id,
3658 type: "branch" as const,
3659 url: `https://api.prisma.test/v1/branches/${id}`,
3660 gitName: "main",
3661 isDefault: true,
3662 role: "preview" as const,
3663 createdAt,
3664 updatedAt: "2026-01-01T00:00:02Z",
3665 project: resourceRef("projects", "project-1", "app"),
3666 };
3667 }),
3668 listBranches: (projectId: string, query: unknown) =>
3669 Effect.sync(() => {
3670 calls.push(["listBranches", { projectId, query }]);
3671 return [
3672 {
3673 id: "branch-default",
3674 type: "branch" as const,
3675 url: "https://api.prisma.test/v1/branches/branch-default",
3676 gitName: "production",
3677 isDefault: true,
3678 role: "production" as const,
3679 createdAt,
3680 updatedAt,
3681 project: resourceRef("projects", projectId, "app"),
3682 },
3683 ];
3684 }),
3685 getApp: (id: string) =>
3686 Effect.sync(() => {
3687 calls.push(["getApp", id]);
3688 return {
3689 id,
3690 type: "app" as const,
3691 url: `https://api.prisma.test/v1/services/${id}`,
3692 name: "api",
3693 region: { id: "us-east-1", name: "US East" },
3694 projectId: "project-1",
3695 branchId: null,
3696 latestDeploymentId: null,
3697 appEndpointDomain: "service-1.prisma.build",
3698 createdAt,
3699 };
3700 }),
3701 updateApp: (id: string, input: unknown) =>
3702 Effect.sync(() => {
3703 calls.push(["updateApp", { id, input }]);
3704 return {
3705 id,
3706 type: "app" as const,
3707 url: `https://api.prisma.test/v1/services/${id}`,
3708 name: "web",
3709 region: { id: "us-east-1", name: "US East" },
3710 projectId: "project-1",
3711 branchId: "branch-1",
3712 latestDeploymentId: null,
3713 appEndpointDomain: "service-1.prisma.build",
3714 createdAt,
3715 };
3716 }),
3717 getEnvironmentVariable: (id: string) =>
3718 Effect.sync(() => {
3719 calls.push(["getEnvironmentVariable", id]);
3720 return {
3721 id,
3722 type: "environment-variable" as const,
3723 url: `https://api.prisma.test/v1/environment-variables/${id}`,
3724 projectId: "project-1",
3725 branchId: null,
3726 class: "production" as const,
3727 key: "TOKEN",
3728 valueKid: "kid-old",
3729 isManagedBySystem: false,
3730 createdAt,
3731 updatedAt,
3732 };
3733 }),
3734 updateEnvironmentVariable: (id: string, input: unknown) =>
3735 Effect.sync(() => {
3736 calls.push(["updateEnvironmentVariable", { id, input }]);
3737 return {
3738 id,
3739 type: "environment-variable" as const,
3740 url: `https://api.prisma.test/v1/environment-variables/${id}`,
3741 projectId: "project-1",
3742 branchId: null,
3743 class: "production" as const,
3744 key: "TOKEN",
3745 valueKid: "kid-new",
3746 isManagedBySystem: false,
3747 createdAt,
3748 updatedAt: "2026-01-01T00:00:02Z",
3749 };
3750 }),
3751 } as unknown as PrismaManagementClient;
3752
3753 return Effect.gen(function* () {
3754 const databaseProvider = yield* PrismaDatabase.Provider;
3755 const connectionProvider = yield* PrismaConnection.Provider;
3756 const branchProvider = yield* PrismaBranch.Provider;
3757 const serviceProvider = yield* PrismaApp.Provider;
3758 const envProvider = yield* PrismaEnvironmentVariable.Provider;
3759
3760 const database = yield* databaseProvider.reconcile(
3761 reconcileInput(
3762 "Database",
3763 {
3764 project: "project-1",
3765 name: "primary",
3766 region: "us-east-1",
3767 branchId: "branch-1",
3768 },
3769 {
3770 databaseId: "database-1",
3771 databaseName: "main",
3772 projectId: "project-1",
3773 status: "ready",
3774 region: "us-east-1",
3775 isDefault: false,
3776 branchId: null,
3777 defaultConnectionId: "connection-1",
3778 createdAt,
3779 directConnectionString: undefined,
3780 pooledConnectionString: undefined,
3781 accelerateConnectionString: undefined,
3782 host: undefined,
3783 user: undefined,
3784 password: undefined,
3785 },
3786 ),
3787 );
3788 const connection = yield* connectionProvider.reconcile(
3789 reconcileInput(
3790 "Connection",
3791 {
3792 database: "database-1",
3793 name: "api",
3794 rotate: true,
3795 },
3796 {
3797 connectionId: "connection-1",
3798 connectionName: "api-000000000000",
3799 databaseId: "database-1",
3800 kind: "postgres" as const,
3801 createdAt,
3802 directConnectionString: Redacted.make("postgres://old-direct"),
3803 pooledConnectionString: undefined,
3804 accelerateConnectionString: undefined,
3805 host: "db.prisma.test",
3806 user: undefined,
3807 password: undefined,
3808 databaseUrl: Redacted.make("postgres://old-direct"),
3809 origin: undefined,
3810 pooledOrigin: undefined,
3811 },
3812 {
3813 database: "database-1",
3814 name: "api",
3815 rotate: false,
3816 },
3817 ),
3818 );
3819 const branch = yield* branchProvider.reconcile(
3820 reconcileInput(
3821 "Branch",
3822 {
3823 project: "project-1",
3824 gitName: "main",
3825 isDefault: true,
3826 },
3827 {
3828 branchId: "branch-1",
3829 gitName: "main",
3830 projectId: "project-1",
3831 isDefault: false,
3832 role: "preview",
3833 createdAt,
3834 updatedAt,
3835 },
3836 ),
3837 );
3838 const service = yield* serviceProvider.reconcile(
3839 reconcileInput(
3840 "App",
3841 {
3842 project: "project-1",
3843 displayName: "web",
3844 regionId: "us-east-1",
3845 branchId: "branch-1",
3846 },
3847 {
3848 appId: "service-1",
3849 name: "api",
3850 projectId: "project-1",
3851 regionId: "us-east-1",
3852 branchId: null,
3853 latestDeploymentId: null,
3854 appEndpointDomain: "service-1.prisma.build",
3855 createdAt,
3856 },
3857 ),
3858 );
3859 const env = yield* envProvider.reconcile(
3860 reconcileInput(
3861 "EnvironmentVariable",
3862 {
3863 project: "project-1",
3864 class: "production" as const,
3865 key: "TOKEN",
3866 value: Redacted.make("new-secret"),
3867 },
3868 {
3869 environmentVariableId: "env-1",
3870 projectId: "project-1",
3871 branchId: null,
3872 class: "production" as const,
3873 key: "TOKEN",
3874 value: Redacted.make("old-secret"),
3875 valueKid: "kid-old",
3876 isManagedBySystem: false,
3877 createdAt,
3878 updatedAt,
3879 },
3880 ),
3881 );
3882
3883 expect(database.databaseName).toBe("primary");
3884 expect(database.branchId).toBe("branch-1");
3885 expect(Redacted.value(connection.directConnectionString!)).toBe(
3886 "postgres://app:new-password@db.prisma.test/database-1",
3887 );
3888 expect(connection.user).toBe("app");
3889 expect(Redacted.value(connection.password!)).toBe("new-password");
3890 expectJsonNotToContain(
3891 connection,
3892 "postgres://app:new-password@db.prisma.test/database-1",
3893 "new-password",
3894 );
3895 expect(branch.isDefault).toBe(true);
3896 expect(service.name).toBe("web");
3897 expect(service.branchId).toBe("branch-1");
3898 expect(env.valueKid).toBe("kid-new");
3899 expect(Redacted.value(env.value)).toBe("new-secret");
3900 expectJsonNotToContain(env, "new-secret");
3901 expect(calls).toEqual([
3902 ["getDatabase", "database-1"],
3903 [
3904 "updateDatabase",
3905 {
3906 id: "database-1",
3907 // JSON transport drops `undefined` members.
3908 input: { name: "primary", branchId: "branch-1" },
3909 },
3910 ],
3911 ["getConnection", "connection-1"],
3912 ["rotateConnection", "connection-1"],
3913 ["getBranch", "branch-1"],
3914 ["listBranches", { projectId: "project-1", query: {} }],
3915 ["updateBranch", { id: "branch-1", input: { isDefault: true } }],
3916 ["getApp", "service-1"],
3917 [
3918 "updateApp",
3919 {
3920 id: "service-1",
3921 input: {
3922 displayName: "web",
3923 branchId: "branch-1",
3924 branchGitName: undefined,
3925 },
3926 },
3927 ],
3928 ["getEnvironmentVariable", "env-1"],
3929 [
3930 "updateEnvironmentVariable",
3931 { id: "env-1", input: { value: "new-secret" } },
3932 ],
3933 ]);
3934 }).pipe(
3935 Effect.provide(providerLayer(client)),
3936 Effect.provide(managementApi(client).layer),
3937 Effect.provide(FetchHttpClient.layer),
3938 Effect.provideService(Stack, {
3939 name: "prisma-provider-update-test",
3940 stage: "test",
3941 resources: {},
3942 bindings: {},
3943 actions: {},
3944 }),
3945 Effect.provideService(Stage, "test"),
3946 );
3947 },
3948 {
3949 tags: [
3950 "provider:prisma:app",
3951 "provider:prisma:branch",
3952 "provider:prisma:connection",
3953 "provider:prisma:customdomain",
3954 "provider:prisma:database",
3955 "provider:prisma:deployment",
3956 "provider:prisma:environmentvariable",
3957 "provider:prisma:project",
3958 "provider:prisma:sourcerepository",
3959 ],
3960 },
3961 );
3962
3963 it.effect(
3964 "skips branchGitName updates when observed branch ids already match",
3965 () => {
3966 const calls: Call[] = [];
3967 const branch = {
3968 id: "branch-main",
3969 type: "branch" as const,
3970 url: "https://api.prisma.test/v1/branches/branch-main",
3971 gitName: "main",
3972 isDefault: true,
3973 createdAt,
3974 updatedAt,
3975 project: resourceRef("projects", "project-1", "app"),
3976 };
3977 const client = {
3978 getDatabase: (id: string) =>
3979 Effect.sync(() => {
3980 calls.push(["getDatabase", id]);
3981 return {
3982 id,
3983 type: "database" as const,
3984 url: `https://api.prisma.test/v1/databases/${id}`,
3985 name: "main",
3986 status: "ready" as const,
3987 createdAt,
3988 isDefault: false,
3989 defaultConnectionId: "connection-1",
3990 connections: [databaseConnection(id, "connection-1")],
3991 project: resourceRef("projects", "project-1", "app"),
3992 region: { id: "us-east-1", name: "US East" },
3993 source: { type: "empty" },
3994 branchId: "branch-main",
3995 };
3996 }),
3997 getApp: (id: string) =>
3998 Effect.sync(() => {
3999 calls.push(["getApp", id]);
4000 return {
4001 id,
4002 type: "app" as const,
4003 url: `https://api.prisma.test/v1/services/${id}`,
4004 name: "api",
4005 region: { id: "us-east-1", name: "US East" },
4006 projectId: "project-1",
4007 branchId: "branch-main",
4008 latestDeploymentId: null,
4009 appEndpointDomain: "service-1.prisma.build",
4010 createdAt,
4011 };
4012 }),
4013 listBranches: (projectId: string, query: unknown) =>
4014 Effect.sync(() => {
4015 calls.push(["listBranches", { projectId, query }]);
4016 return [branch];
4017 }),
4018 updateDatabase: (id: string, input: unknown) =>
4019 Effect.sync(() => {
4020 calls.push(["updateDatabase", { id, input }]);
4021 throw new Error("updateDatabase should not be called");
4022 }),
4023 updateApp: (id: string, input: unknown) =>
4024 Effect.sync(() => {
4025 calls.push(["updateApp", { id, input }]);
4026 throw new Error("updateApp should not be called");
4027 }),
4028 } as unknown as PrismaManagementClient;
4029
4030 return Effect.gen(function* () {
4031 const databaseProvider = yield* PrismaDatabase.Provider;
4032 const serviceProvider = yield* PrismaApp.Provider;
4033
4034 const database = yield* databaseProvider.reconcile(
4035 reconcileInput(
4036 "Database",
4037 {
4038 project: "project-1",
4039 name: "main",
4040 region: "us-east-1",
4041 branchGitName: "main",
4042 },
4043 {
4044 databaseId: "database-1",
4045 databaseName: "main",
4046 projectId: "project-1",
4047 status: "ready",
4048 region: "us-east-1",
4049 isDefault: false,
4050 branchId: "branch-main",
4051 defaultConnectionId: "connection-1",
4052 createdAt,
4053 directConnectionString: undefined,
4054 pooledConnectionString: undefined,
4055 accelerateConnectionString: undefined,
4056 host: undefined,
4057 user: undefined,
4058 password: undefined,
4059 },
4060 ),
4061 );
4062 const service = yield* serviceProvider.reconcile(
4063 reconcileInput(
4064 "App",
4065 {
4066 project: "project-1",
4067 displayName: "api",
4068 regionId: "us-east-1",
4069 branchGitName: "main",
4070 },
4071 {
4072 appId: "service-1",
4073 name: "api",
4074 projectId: "project-1",
4075 regionId: "us-east-1",
4076 branchId: "branch-main",
4077 latestDeploymentId: null,
4078 appEndpointDomain: "service-1.prisma.build",
4079 createdAt,
4080 },
4081 ),
4082 );
4083
4084 expect(database.branchId).toBe("branch-main");
4085 expect(service.branchId).toBe("branch-main");
4086 expect(calls).toEqual([
4087 ["getDatabase", "database-1"],
4088 [
4089 "listBranches",
4090 // Query parameters arrive as strings over the wire.
4091 {
4092 projectId: "project-1",
4093 query: { limit: "2", gitName: "main" },
4094 },
4095 ],
4096 [
4097 "listBranches",
4098 {
4099 projectId: "project-1",
4100 query: { limit: "100", gitName: "main" },
4101 },
4102 ],
4103 ["getApp", "service-1"],
4104 [
4105 "listBranches",
4106 {
4107 projectId: "project-1",
4108 query: { limit: "100", gitName: "main" },
4109 },
4110 ],
4111 ]);
4112 }).pipe(
4113 Effect.provide(providerLayer(client)),
4114 Effect.provide(managementApi(client).layer),
4115 Effect.provide(FetchHttpClient.layer),
4116 Effect.provideService(Stack, {
4117 name: "prisma-branch-noop-test",
4118 stage: "test",
4119 resources: {},
4120 bindings: {},
4121 actions: {},
4122 }),
4123 Effect.provideService(Stage, "test"),
4124 );
4125 },
4126 {
4127 tags: [
4128 "provider:prisma:app",
4129 "provider:prisma:branch",
4130 "provider:prisma:connection",
4131 "provider:prisma:customdomain",
4132 "provider:prisma:database",
4133 "provider:prisma:deployment",
4134 "provider:prisma:environmentvariable",
4135 "provider:prisma:project",
4136 "provider:prisma:sourcerepository",
4137 ],
4138 },
4139 );
4140
4141 it.effect(
4142 "normalizes Management API clone source ids before comparing immutable state",
4143 () => {
4144 const calls: Call[] = [];
4145 const database = {
4146 id: "database-clone",
4147 type: "database" as const,
4148 url: "https://api.prisma.test/v1/databases/database-clone",
4149 name: "clone",
4150 status: "ready" as const,
4151 createdAt,
4152 isDefault: false,
4153 defaultConnectionId: "connection-clone",
4154 connections: [
4155 databaseConnection("database-clone", "connection-clone"),
4156 ],
4157 project: resourceRef("projects", "project-1", "app"),
4158 region: { id: "us-east-1", name: "US East" },
4159 source: { type: "database" as const, databaseId: "source" },
4160 branchId: null,
4161 };
4162 const client = {
4163 getDatabase: (id: string) =>
4164 Effect.sync(() => {
4165 calls.push(["getDatabase", id]);
4166 return database;
4167 }),
4168 updateDatabase: () =>
4169 Effect.die("normalized clone source must not trigger an update"),
4170 } as unknown as PrismaManagementClient;
4171
4172 return Effect.gen(function* () {
4173 const provider = yield* PrismaDatabase.Provider;
4174 const result = yield* provider.reconcile(
4175 reconcileInput(
4176 "Database",
4177 {
4178 project: "project-1",
4179 name: "clone",
4180 region: "us-east-1",
4181 source: { type: "database" as const, databaseId: "db_source" },
4182 },
4183 {
4184 databaseId: "database-clone",
4185 databaseName: "clone",
4186 projectId: "project-1",
4187 status: "ready" as const,
4188 region: "us-east-1",
4189 isDefault: false,
4190 branchId: null,
4191 defaultConnectionId: "connection-clone",
4192 createdAt,
4193 directConnectionString: undefined,
4194 pooledConnectionString: undefined,
4195 accelerateConnectionString: undefined,
4196 host: undefined,
4197 user: undefined,
4198 password: undefined,
4199 },
4200 ),
4201 );
4202
4203 expect(result.databaseId).toBe("database-clone");
4204 expect(calls).toEqual([["getDatabase", "database-clone"]]);
4205 }).pipe(
4206 Effect.provide(providerLayer(client)),
4207 Effect.provide(managementApi(client).layer),
4208 );
4209 },
4210 {
4211 tags: [
4212 "provider:prisma:app",
4213 "provider:prisma:branch",
4214 "provider:prisma:connection",
4215 "provider:prisma:customdomain",
4216 "provider:prisma:database",
4217 "provider:prisma:deployment",
4218 "provider:prisma:environmentvariable",
4219 "provider:prisma:project",
4220 "provider:prisma:sourcerepository",
4221 ],
4222 },
4223 );
4224
4225 it.effect(
4226 "refuses convergence when the observed source shape is unrecognized",
4227 () => {
4228 const database = {
4229 id: "database-exotic",
4230 type: "database" as const,
4231 url: "https://api.prisma.test/v1/databases/database-exotic",
4232 name: "exotic",
4233 status: "ready" as const,
4234 createdAt,
4235 isDefault: false,
4236 defaultConnectionId: "connection-exotic",
4237 connections: [
4238 databaseConnection("database-exotic", "connection-exotic"),
4239 ],
4240 project: resourceRef("projects", "project-1", "app"),
4241 region: { id: "us-east-1", name: "US East" },
4242 source: { type: "replica", replicaOf: "db_source" },
4243 branchId: null,
4244 };
4245 const client = {
4246 getDatabase: () => Effect.succeed(database),
4247 updateDatabase: () =>
4248 Effect.die("an unrecognized source must refuse before updating"),
4249 } as unknown as PrismaManagementClient;
4250
4251 return Effect.gen(function* () {
4252 const provider = yield* PrismaDatabase.Provider;
4253 const error = yield* provider
4254 .reconcile(
4255 reconcileInput(
4256 "Database",
4257 {
4258 project: "project-1",
4259 name: "exotic",
4260 region: "us-east-1",
4261 },
4262 {
4263 databaseId: "database-exotic",
4264 databaseName: "exotic",
4265 projectId: "project-1",
4266 status: "ready" as const,
4267 region: "us-east-1",
4268 isDefault: false,
4269 branchId: null,
4270 defaultConnectionId: "connection-exotic",
4271 createdAt,
4272 directConnectionString: undefined,
4273 pooledConnectionString: undefined,
4274 accelerateConnectionString: undefined,
4275 host: undefined,
4276 user: undefined,
4277 password: undefined,
4278 },
4279 ),
4280 )
4281 .pipe(Effect.flip);
4282
4283 expect((error as Error).message).toContain("immutable source");
4284 expect((error as Error).message).toContain('"replica"');
4285 }).pipe(
4286 Effect.provide(providerLayer(client)),
4287 Effect.provide(managementApi(client).layer),
4288 );
4289 },
4290 {
4291 tags: [
4292 "provider:prisma:app",
4293 "provider:prisma:branch",
4294 "provider:prisma:connection",
4295 "provider:prisma:customdomain",
4296 "provider:prisma:database",
4297 "provider:prisma:deployment",
4298 "provider:prisma:environmentvariable",
4299 "provider:prisma:project",
4300 "provider:prisma:sourcerepository",
4301 ],
4302 },
4303 );
4304
4305 it.effect(
4306 "leaves the observed branch alone when branch props are omitted",
4307 () => {
4308 const calls: Call[] = [];
4309 const database = {
4310 id: "database-1",
4311 type: "database" as const,
4312 url: "https://api.prisma.test/v1/databases/database-1",
4313 name: "main",
4314 status: "ready" as const,
4315 createdAt,
4316 isDefault: false,
4317 defaultConnectionId: "connection-1",
4318 connections: [],
4319 project: resourceRef("projects", "project-1", "app"),
4320 region: { id: "us-east-1", name: "US East" },
4321 source: { type: "empty" as const },
4322 branchId: "branch-1",
4323 };
4324 const client = {
4325 getDatabase: (id: string) =>
4326 Effect.sync(() => {
4327 calls.push(["getDatabase", id]);
4328 return database;
4329 }),
4330 updateDatabase: () =>
4331 Effect.die(
4332 "every database belongs to a Branch; omitted branch props must not detach",
4333 ),
4334 rotateConnection: () =>
4335 Effect.die(
4336 "persisted credentials must prevent an unrelated rotation",
4337 ),
4338 } as unknown as PrismaManagementClient;
4339
4340 return Effect.gen(function* () {
4341 const provider = yield* PrismaDatabase.Provider;
4342 const result = yield* provider.reconcile(
4343 reconcileInput(
4344 "Database",
4345 {
4346 project: "project-1",
4347 name: "main",
4348 region: "us-east-1",
4349 },
4350 {
4351 databaseId: "database-1",
4352 databaseName: "main",
4353 projectId: "project-1",
4354 status: "ready" as const,
4355 region: "us-east-1",
4356 isDefault: false,
4357 branchId: "branch-1",
4358 defaultConnectionId: "connection-1",
4359 createdAt,
4360 directConnectionString: Redacted.make("postgres://persisted"),
4361 pooledConnectionString: undefined,
4362 accelerateConnectionString: undefined,
4363 host: "db.prisma.test",
4364 user: "user",
4365 password: undefined,
4366 },
4367 {
4368 project: "project-1",
4369 name: "main",
4370 region: "us-east-1",
4371 branchId: "branch-1",
4372 },
4373 ),
4374 );
4375
4376 expect(result.branchId).toBe("branch-1");
4377 expect(calls).toEqual([["getDatabase", "database-1"]]);
4378 }).pipe(
4379 Effect.provide(providerLayer(client)),
4380 Effect.provide(managementApi(client).layer),
4381 );
4382 },
4383 {
4384 tags: [
4385 "provider:prisma:app",
4386 "provider:prisma:branch",
4387 "provider:prisma:connection",
4388 "provider:prisma:customdomain",
4389 "provider:prisma:database",
4390 "provider:prisma:deployment",
4391 "provider:prisma:environmentvariable",
4392 "provider:prisma:project",
4393 "provider:prisma:sourcerepository",
4394 ],
4395 },
4396 );
4397
4398 it.effect(
4399 "forces Project and Database reconcile when adoption rotation is enabled",
4400 () => {
4401 const { client } = makeClient();
4402
4403 return Effect.gen(function* () {
4404 const projectProvider = yield* PrismaProject.Provider;
4405 const databaseProvider = yield* PrismaDatabase.Provider;
4406
4407 expect(
4408 yield* projectProvider.diff!(
4409 diffInput(
4410 {
4411 name: "app",
4412 createDatabase: false,
4413 rotateCredentialsOnAdopt: false,
4414 },
4415 {
4416 name: "app",
4417 createDatabase: false,
4418 rotateCredentialsOnAdopt: true,
4419 },
4420 ),
4421 ),
4422 ).toEqual({ action: "update" });
4423 expect(
4424 yield* databaseProvider.diff!(
4425 diffInput(
4426 {
4427 project: "project-1",
4428 name: "main",
4429 region: "us-east-1",
4430 rotateCredentialsOnAdopt: false,
4431 },
4432 {
4433 project: "project-1",
4434 name: "main",
4435 region: "us-east-1",
4436 rotateCredentialsOnAdopt: true,
4437 },
4438 ),
4439 ),
4440 ).toEqual({ action: "update" });
4441 }).pipe(
4442 Effect.provide(providerLayer(client)),
4443 Effect.provide(managementApi(client).layer),
4444 );
4445 },
4446 {
4447 tags: [
4448 "provider:prisma:app",
4449 "provider:prisma:branch",
4450 "provider:prisma:connection",
4451 "provider:prisma:customdomain",
4452 "provider:prisma:database",
4453 "provider:prisma:deployment",
4454 "provider:prisma:environmentvariable",
4455 "provider:prisma:project",
4456 "provider:prisma:sourcerepository",
4457 ],
4458 },
4459 );
4460
4461 it.effect(
4462 "rejects non-atomic explicit-name database branch creation",
4463 () => {
4464 let created = false;
4465 const client = {
4466 createDatabase: () =>
4467 Effect.sync(() => {
4468 created = true;
4469 throw new Error("must fail before create");
4470 }),
4471 } as unknown as PrismaManagementClient;
4472
4473 return Effect.gen(function* () {
4474 const provider = yield* PrismaDatabase.Provider;
4475 const error = yield* provider
4476 .reconcile(
4477 reconcileInput("Database", {
4478 project: "project-1",
4479 name: "main",
4480 region: "us-east-1",
4481 branchGitName: "main",
4482 }),
4483 )
4484 .pipe(Effect.flip);
4485
4486 expect((error as Error).message).toContain(
4487 "cannot be distinguished from a foreign database",
4488 );
4489 expect(created).toBe(false);
4490 }).pipe(
4491 Effect.provide(providerLayer(client)),
4492 Effect.provide(managementApi(client).layer),
4493 );
4494 },
4495 {
4496 tags: [
4497 "provider:prisma:app",
4498 "provider:prisma:branch",
4499 "provider:prisma:connection",
4500 "provider:prisma:customdomain",
4501 "provider:prisma:database",
4502 "provider:prisma:deployment",
4503 "provider:prisma:environmentvariable",
4504 "provider:prisma:project",
4505 "provider:prisma:sourcerepository",
4506 ],
4507 },
4508 );
4509
4510 it.effect(
4511 "recovers credentials after a generated database create conflict",
4512 () => {
4513 const calls: Call[] = [];
4514 let attemptedName: string | undefined;
4515 const recoveredDatabase = () => ({
4516 id: "database-recovered",
4517 type: "database" as const,
4518 url: "https://api.prisma.test/v1/databases/database-recovered",
4519 name: attemptedName!,
4520 status: "ready" as const,
4521 createdAt,
4522 isDefault: false,
4523 defaultConnectionId: "connection-recovered",
4524 connections: [],
4525 project: resourceRef("projects", "project-1", "app"),
4526 region: { id: "us-east-1", name: "US East" },
4527 source: { type: "empty" as const },
4528 branchId: "branch-main",
4529 });
4530 const client = {
4531 listProjectDatabases: (projectId: string, query: unknown) =>
4532 Effect.sync(() => {
4533 calls.push(["listProjectDatabases", { projectId, query }]);
4534 return attemptedName === undefined ? [] : [recoveredDatabase()];
4535 }),
4536 createDatabase: (input: { name: string }) =>
4537 Effect.gen(function* () {
4538 calls.push(["createDatabase", input]);
4539 attemptedName = input.name;
4540 return yield* Effect.fail(
4541 new PrismaApiError({
4542 method: "POST",
4543 path: "/v1/databases",
4544 status: 409,
4545 message: "already exists",
4546 }),
4547 );
4548 }),
4549 listBranches: () =>
4550 Effect.succeed([
4551 {
4552 id: "branch-main",
4553 gitName: "main",
4554 },
4555 ]),
4556 rotateConnection: (id: string) =>
4557 Effect.sync(() => {
4558 calls.push(["rotateConnection", id]);
4559 return databaseConnection("database-recovered", id);
4560 }),
4561 } as unknown as PrismaManagementClient;
4562
4563 return Effect.gen(function* () {
4564 const provider = yield* PrismaDatabase.Provider;
4565 const recovered = yield* provider.reconcile(
4566 reconcileInput("Database", {
4567 project: "project-1",
4568 region: "us-east-1",
4569 branchGitName: "main",
4570 }),
4571 );
4572
4573 expect(attemptedName).toContain("-Database-test-");
4574 expect(redactedValue(recovered.directConnectionString)).toContain(
4575 "database-recovered",
4576 );
4577 expect(calls).toContainEqual([
4578 "rotateConnection",
4579 "connection-recovered",
4580 ]);
4581 }).pipe(
4582 Effect.provide(providerLayer(client)),
4583 Effect.provide(managementApi(client).layer),
4584 Effect.provideService(Stack, {
4585 name: "prisma-database-recovery-test",
4586 stage: "test",
4587 resources: {},
4588 bindings: {},
4589 actions: {},
4590 }),
4591 Effect.provideService(Stage, "test"),
4592 Effect.provideService(InstanceId, "00000000000000000000000000000000"),
4593 );
4594 },
4595 {
4596 tags: [
4597 "provider:prisma:app",
4598 "provider:prisma:branch",
4599 "provider:prisma:connection",
4600 "provider:prisma:customdomain",
4601 "provider:prisma:database",
4602 "provider:prisma:deployment",
4603 "provider:prisma:environmentvariable",
4604 "provider:prisma:project",
4605 "provider:prisma:sourcerepository",
4606 ],
4607 },
4608 );
4609
4610 it.effect(
4611 "recovers default credentials after a generated project create conflict",
4612 () => {
4613 const calls: Call[] = [];
4614 let attemptedName: string | undefined;
4615 const database = wireDatabase({
4616 id: "database-default",
4617 name: "default",
4618 isDefault: true,
4619 defaultConnectionId: "connection-default",
4620 projectId: "project-recovered",
4621 projectName: "recovered",
4622 });
4623 // The 409 below is served as a real wire response; distilled's status
4624 // matcher is what turns it into the `Conflict` tag the provider catches.
4625 const fake = makeFakeManagementApi((request) => {
4626 if (request.pathname === "/v1/projects" && request.method === "GET") {
4627 return page(
4628 attemptedName === undefined
4629 ? []
4630 : [
4631 wireProject({
4632 id: "project-recovered",
4633 name: attemptedName,
4634 }),
4635 ],
4636 );
4637 }
4638 if (
4639 request.pathname === "/v1/projects" &&
4640 request.method === "POST"
4641 ) {
4642 attemptedName = (request.bodyJson as { name: string }).name;
4643 return conflict("already exists");
4644 }
4645 if (
4646 request.pathname === "/v1/projects/project-recovered/databases" &&
4647 request.method === "GET"
4648 ) {
4649 return page([database]);
4650 }
4651 return dispatchManagement(client, request);
4652 });
4653 const client = {
4654 rotateConnection: (id: string) =>
4655 Effect.sync(() => {
4656 calls.push(["rotateConnection", id]);
4657 return databaseConnection("database-default", id);
4658 }),
4659 } as unknown as PrismaManagementClient;
4660
4661 return Effect.gen(function* () {
4662 const provider = yield* PrismaProject.Provider;
4663 const recovered = yield* provider.reconcile(
4664 reconcileInput("Project", {
4665 createDatabase: true,
4666 region: "us-east-1",
4667 }),
4668 );
4669
4670 expect(attemptedName).toContain("-Project-test-");
4671 expect(redactedValue(recovered.directConnectionString)).toContain(
4672 "database-default",
4673 );
4674 expect(calls).toContainEqual([
4675 "rotateConnection",
4676 "connection-default",
4677 ]);
4678 // The create really was rejected with 409 over the wire and recovery
4679 // ran off the follow-up list.
4680 expect(routesOf(fake.captured)).toEqual([
4681 "GET /v1/projects",
4682 "POST /v1/projects",
4683 "GET /v1/projects",
4684 "GET /v1/projects/project-recovered/databases",
4685 "POST /v1/connections/connection-default/rotate",
4686 ]);
4687 }).pipe(
4688 Effect.provide(providerLayer(client)),
4689 Effect.provide(fake.layer),
4690 Effect.provideService(Stack, {
4691 name: "prisma-project-recovery-test",
4692 stage: "test",
4693 resources: {},
4694 bindings: {},
4695 actions: {},
4696 }),
4697 Effect.provideService(Stage, "test"),
4698 Effect.provideService(InstanceId, "00000000000000000000000000000000"),
4699 );
4700 },
4701 {
4702 tags: [
4703 "provider:prisma:app",
4704 "provider:prisma:branch",
4705 "provider:prisma:connection",
4706 "provider:prisma:customdomain",
4707 "provider:prisma:database",
4708 "provider:prisma:deployment",
4709 "provider:prisma:environmentvariable",
4710 "provider:prisma:project",
4711 "provider:prisma:sourcerepository",
4712 ],
4713 },
4714 );
4715
4716 it.effect(
4717 "rejects a createDatabase false response that contains a default database",
4718 () => {
4719 const fake = makeFakeManagementApi((request) => {
4720 if (
4721 request.pathname === "/v1/projects" &&
4722 request.method === "POST"
4723 ) {
4724 return envelope(
4725 {
4726 ...wireProject(),
4727 database: wireCreatedDatabase({
4728 id: "database-unexpected",
4729 name: "default",
4730 isDefault: true,
4731 }),
4732 },
4733 { status: 201 },
4734 );
4735 }
4736 return unhandled(request);
4737 });
4738
4739 return Effect.gen(function* () {
4740 const provider = yield* PrismaProject.Provider;
4741 const error = yield* provider
4742 .reconcile(
4743 reconcileInput("Project", {
4744 name: "app",
4745 createDatabase: false,
4746 region: "us-east-1",
4747 }),
4748 )
4749 .pipe(Effect.flip);
4750
4751 expect((error as Error).message).toContain(
4752 "created unexpected default database",
4753 );
4754 expect(routesOf(fake.captured)).toEqual(["POST /v1/projects"]);
4755 expect(fake.captured[0]?.bodyJson).toEqual({
4756 name: "app",
4757 createDatabase: false,
4758 region: "us-east-1",
4759 });
4760 }).pipe(
4761 Effect.provide(providerLayer({} as PrismaManagementClient)),
4762 Effect.provide(fake.layer),
4763 );
4764 },
4765 {
4766 tags: [
4767 "provider:prisma:app",
4768 "provider:prisma:branch",
4769 "provider:prisma:connection",
4770 "provider:prisma:customdomain",
4771 "provider:prisma:database",
4772 "provider:prisma:deployment",
4773 "provider:prisma:environmentvariable",
4774 "provider:prisma:project",
4775 "provider:prisma:sourcerepository",
4776 ],
4777 },
4778 );
4779
4780 it.effect(
4781 "ensures a default database on an existing Prisma project",
4782 () => {
4783 let createdDefault: unknown;
4784 const defaultDatabase = () =>
4785 wireDatabase({
4786 isDefault: true,
4787 defaultConnectionId: "connection-1",
4788 connections: [
4789 wireConnection({ directConnectionString: "postgres://direct" }),
4790 ],
4791 });
4792 const fake = makeFakeManagementApi((request) => {
4793 if (request.pathname === "/v1/projects" && request.method === "GET") {
4794 return page([wireProject()]);
4795 }
4796 if (
4797 request.pathname === "/v1/projects/project-1" &&
4798 request.method === "GET"
4799 ) {
4800 return envelope(wireProject());
4801 }
4802 if (
4803 request.pathname === "/v1/projects/project-1/databases" &&
4804 request.method === "GET"
4805 ) {
4806 return page([
4807 wireDatabase({ id: "database-reporting", name: "reporting" }),
4808 ...(createdDefault === undefined ? [] : [createdDefault]),
4809 ]);
4810 }
4811 if (
4812 request.pathname === "/v1/projects/project-1/databases" &&
4813 request.method === "POST"
4814 ) {
4815 createdDefault = defaultDatabase();
4816 return envelope(
4817 wireCreatedDatabase({
4818 isDefault: true,
4819 defaultConnectionId: "connection-1",
4820 connections: [
4821 wireConnection({
4822 directConnectionString: "postgres://direct",
4823 }),
4824 ],
4825 }),
4826 { status: 201 },
4827 );
4828 }
4829 return unhandled(request);
4830 });
4831
4832 return Effect.gen(function* () {
4833 const projectProvider = yield* Provider.findProvider(PrismaProject);
4834 const observed = yield* projectProvider.read!(
4835 readInput("Project", { name: "app", region: "us-east-1" }),
4836 );
4837 expect(Unowned.is(observed!)).toBe(true);
4838 const project = yield* projectProvider.reconcile(
4839 reconcileInput(
4840 "Project",
4841 { name: "app", region: "us-east-1" },
4842 observed,
4843 ),
4844 );
4845
4846 expect(project.projectId).toBe("project-1");
4847 expect(project.databaseId).toBe("database-1");
4848 expect(Redacted.value(project.directConnectionString!)).toBe(
4849 "postgres://direct",
4850 );
4851 expectJsonNotToContain(project, "postgres://direct");
4852 expect(routesOf(fake.captured)).toEqual([
4853 "GET /v1/projects",
4854 "GET /v1/projects/project-1/databases",
4855 "GET /v1/projects/project-1",
4856 "GET /v1/projects/project-1/databases",
4857 "POST /v1/projects/project-1/databases",
4858 "GET /v1/projects/project-1",
4859 "GET /v1/projects/project-1/databases",
4860 ]);
4861 expect(
4862 fake.captured.find((request) => request.method === "POST")
4863 ?.bodyJson,
4864 ).toEqual({ region: "us-east-1", isDefault: true });
4865 }).pipe(
4866 Effect.provide(
4867 ProjectProvider().pipe(
4868 Layer.provide(
4869 Layer.succeed(PrismaClient, {} as PrismaManagementClient),
4870 ),
4871 Layer.provide(liveProviderContext),
4872 ),
4873 ),
4874 Effect.provide(fake.layer),
4875 Effect.provide(FetchHttpClient.layer),
4876 Effect.provideService(Stack, {
4877 name: "prisma-project-ensure-test",
4878 stage: "test",
4879 resources: {},
4880 bindings: {},
4881 actions: {},
4882 }),
4883 Effect.provideService(Stage, "test"),
4884 );
4885 },
4886 { tags: ["provider:prisma:project"] },
4887 );
4888
4889 it.effect(
4890 "clears previously managed project settings",
4891 () => {
4892 const fake = makeFakeManagementApi((request) => {
4893 if (request.pathname === "/v1/projects/project-1") {
4894 if (request.method === "GET" || request.method === "PATCH") {
4895 return envelope(wireProject());
4896 }
4897 }
4898 if (
4899 request.pathname === "/v1/projects/project-1/databases" &&
4900 request.method === "GET"
4901 ) {
4902 return page([]);
4903 }
4904 return unhandled(request);
4905 });
4906
4907 return Effect.gen(function* () {
4908 const projectProvider = yield* PrismaProject.Provider;
4909 const project = yield* projectProvider.reconcile(
4910 reconcileInput(
4911 "Project",
4912 { name: "app", createDatabase: false },
4913 {
4914 projectId: "project-1",
4915 projectName: "app",
4916 workspaceId: "workspace-1",
4917 createdAt,
4918 defaultRegion: "us-east-1",
4919 databaseId: undefined,
4920 defaultConnectionId: undefined,
4921 directConnectionString: undefined,
4922 pooledConnectionString: undefined,
4923 accelerateConnectionString: undefined,
4924 host: undefined,
4925 user: undefined,
4926 password: undefined,
4927 },
4928 {
4929 name: "app",
4930 createDatabase: false,
4931 settings: { preview: true },
4932 },
4933 ),
4934 );
4935
4936 expect(project.projectId).toBe("project-1");
4937 expect(routesOf(fake.captured)).toEqual([
4938 "GET /v1/projects/project-1",
4939 "GET /v1/projects/project-1/databases",
4940 "PATCH /v1/projects/project-1",
4941 ]);
4942 expect(fake.captured.at(-1)?.bodyJson).toEqual({
4943 name: "app",
4944 settings: {},
4945 });
4946 }).pipe(
4947 Effect.provide(
4948 ProjectProvider().pipe(
4949 Layer.provide(
4950 Layer.succeed(PrismaClient, {} as PrismaManagementClient),
4951 ),
4952 Layer.provide(liveProviderContext),
4953 ),
4954 ),
4955 Effect.provide(fake.layer),
4956 Effect.provide(FetchHttpClient.layer),
4957 Effect.provideService(Stack, {
4958 name: "prisma-project-settings-test",
4959 stage: "test",
4960 resources: {},
4961 bindings: {},
4962 actions: {},
4963 }),
4964 Effect.provideService(Stage, "test"),
4965 );
4966 },
4967 { tags: ["provider:prisma:project"] },
4968 );
4969
4970 it.effect(
4971 "renames a project without clearing unmanaged settings",
4972 () => {
4973 const fake = makeFakeManagementApi((request) => {
4974 if (
4975 request.pathname === "/v1/projects/project-1" &&
4976 request.method === "GET"
4977 ) {
4978 return envelope(wireProject());
4979 }
4980 if (
4981 request.pathname === "/v1/projects/project-1" &&
4982 request.method === "PATCH"
4983 ) {
4984 return envelope(wireProject({ name: "renamed" }));
4985 }
4986 if (
4987 request.pathname === "/v1/projects/project-1/databases" &&
4988 request.method === "GET"
4989 ) {
4990 return page([]);
4991 }
4992 return unhandled(request);
4993 });
4994
4995 return Effect.gen(function* () {
4996 const projectProvider = yield* PrismaProject.Provider;
4997 yield* projectProvider.reconcile(
4998 reconcileInput(
4999 "Project",
5000 { name: "renamed", createDatabase: false },
5001 {
5002 projectId: "project-1",
5003 projectName: "app",
5004 workspaceId: "workspace-1",
5005 createdAt,
5006 defaultRegion: "us-east-1",
5007 databaseId: undefined,
5008 defaultConnectionId: undefined,
5009 directConnectionString: undefined,
5010 pooledConnectionString: undefined,
5011 accelerateConnectionString: undefined,
5012 host: undefined,
5013 user: undefined,
5014 password: undefined,
5015 },
5016 { name: "app", createDatabase: false },
5017 ),
5018 );
5019
5020 expect(routesOf(fake.captured)).toEqual([
5021 "GET /v1/projects/project-1",
5022 "GET /v1/projects/project-1/databases",
5023 "PATCH /v1/projects/project-1",
5024 ]);
5025 expect(fake.captured.at(-1)?.bodyJson).toEqual({ name: "renamed" });
5026 }).pipe(
5027 Effect.provide(
5028 ProjectProvider().pipe(
5029 Layer.provide(
5030 Layer.succeed(PrismaClient, {} as PrismaManagementClient),
5031 ),
5032 Layer.provide(liveProviderContext),
5033 ),
5034 ),
5035 Effect.provide(fake.layer),
5036 Effect.provide(FetchHttpClient.layer),
5037 Effect.provideService(Stack, {
5038 name: "prisma-project-rename-test",
5039 stage: "test",
5040 resources: {},
5041 bindings: {},
5042 actions: {},
5043 }),
5044 Effect.provideService(Stage, "test"),
5045 );
5046 },
5047 { tags: ["provider:prisma:project"] },
5048 );
5049
5050 it.effect(
5051 "deletes Prisma resources through their management APIs",
5052 () => {
5053 const calls: Call[] = [];
5054 const status = new Map([["version-1", "running"]]);
5055 const client = {
5056 deleteProject: (id: string) =>
5057 Effect.sync(() => {
5058 calls.push(["deleteProject", id]);
5059 }),
5060 deleteDatabase: (id: string) =>
5061 Effect.sync(() => {
5062 calls.push(["deleteDatabase", id]);
5063 }),
5064 getDatabase: (id: string) =>
5065 Effect.sync(() => {
5066 calls.push(["getDatabase", id]);
5067 return { id, isDefault: false };
5068 }),
5069 deleteConnection: (id: string) =>
5070 Effect.sync(() => {
5071 calls.push(["deleteConnection", id]);
5072 }),
5073 getConnection: (id: string) =>
5074 Effect.sync(() => {
5075 calls.push(["getConnection", id]);
5076 return {
5077 id,
5078 name: "api-000000000000",
5079 database: { id: "database-1" },
5080 };
5081 }),
5082 deleteBranch: (id: string) =>
5083 Effect.sync(() => {
5084 calls.push(["deleteBranch", id]);
5085 }),
5086 getBranch: (id: string) =>
5087 Effect.sync(() => {
5088 calls.push(["getBranch", id]);
5089 return {
5090 id,
5091 gitName: "preview",
5092 isDefault: false,
5093 role: "preview" as const,
5094 project: { id: "project-1" },
5095 };
5096 }),
5097 listApps: (query: unknown) =>
5098 Effect.sync(() => {
5099 calls.push(["listApps", query]);
5100 return [];
5101 }),
5102 listAppDeployments: (appId: string, query: unknown) =>
5103 Effect.sync(() => {
5104 calls.push(["listAppDeployments", { appId, query }]);
5105 return [
5106 {
5107 id: "version-1",
5108 type: "deployment" as const,
5109 serviceId: "service-1",
5110 url: "https://api.prisma.test/v1/deployments/version-1",
5111 foundryVersionId: "foundry-1",
5112 createdAt,
5113 },
5114 ];
5115 }),
5116 getDeployment: (id: string) =>
5117 Effect.sync(() => {
5118 calls.push(["getDeployment", id]);
5119 return {
5120 id,
5121 type: "deployment" as const,
5122 serviceId: "service-1",
5123 url: `https://api.prisma.test/v1/deployments/${id}`,
5124 foundryVersionId: "foundry-1",
5125 status: status.get(id) ?? "stopped",
5126 previewDomain: null,
5127 createdAt,
5128 };
5129 }),
5130 stopDeployment: (id: string) =>
5131 Effect.sync(() => {
5132 calls.push(["stopDeployment", id]);
5133 status.set(id, "stopped");
5134 }),
5135 deleteDeployment: (id: string) =>
5136 Effect.sync(() => {
5137 calls.push(["deleteDeployment", id]);
5138 }),
5139 deleteApp: (id: string) =>
5140 Effect.sync(() => {
5141 calls.push(["deleteApp", id]);
5142 }),
5143 getApp: (id: string) =>
5144 Effect.sync(() => {
5145 calls.push(["getApp", id]);
5146 return {
5147 id,
5148 name: "api",
5149 projectId: "project-1",
5150 region: { id: "us-east-1" },
5151 branchId: "branch-1",
5152 };
5153 }),
5154 deleteCustomDomain: (id: string) =>
5155 Effect.sync(() => {
5156 calls.push(["deleteCustomDomain", id]);
5157 }),
5158 getCustomDomain: (id: string) =>
5159 Effect.sync(() => {
5160 calls.push(["getCustomDomain", id]);
5161 return { id, appId: "service-1", hostname: "api.example.com" };
5162 }),
5163 getEnvironmentVariable: (id: string) =>
5164 Effect.sync(() => {
5165 calls.push(["getEnvironmentVariable", id]);
5166 return {
5167 id,
5168 type: "environment-variable" as const,
5169 url: `https://api.prisma.test/v1/environment-variables/${id}`,
5170 projectId: "project-1",
5171 branchId: null,
5172 class: "production" as const,
5173 key: "TOKEN",
5174 valueKid: "kid-1",
5175 isManagedBySystem: false,
5176 createdAt,
5177 updatedAt,
5178 };
5179 }),
5180 deleteEnvironmentVariable: (id: string) =>
5181 Effect.sync(() => {
5182 calls.push(["deleteEnvironmentVariable", id]);
5183 }),
5184 deleteSourceRepository: (id: string) =>
5185 Effect.sync(() => {
5186 calls.push(["deleteSourceRepository", id]);
5187 }),
5188 getSourceRepository: (id: string) =>
5189 Effect.sync(() => {
5190 calls.push(["getSourceRepository", id]);
5191 return {
5192 id,
5193 projectId: "project-1",
5194 repoId: 123,
5195 provider: "github" as const,
5196 installationId: "installation-1",
5197 };
5198 }),
5199 } as unknown as PrismaManagementClient;
5200
5201 return Effect.gen(function* () {
5202 const projectProvider = yield* PrismaProject.Provider;
5203 const databaseProvider = yield* PrismaDatabase.Provider;
5204 const connectionProvider = yield* PrismaConnection.Provider;
5205 const branchProvider = yield* PrismaBranch.Provider;
5206 const serviceProvider = yield* PrismaApp.Provider;
5207 const versionProvider = yield* PrismaDeployment.Provider;
5208 const domainProvider = yield* PrismaCustomDomain.Provider;
5209 const envProvider = yield* PrismaEnvironmentVariable.Provider;
5210 const repoProvider = yield* PrismaSourceRepository.Provider;
5211
5212 yield* versionProvider.delete(
5213 deleteInput("Deployment", {
5214 deploymentId: "version-1",
5215 appId: "service-1",
5216 }),
5217 );
5218 yield* serviceProvider.delete(
5219 deleteInput("App", {
5220 appId: "service-1",
5221 projectId: "project-1",
5222 regionId: "us-east-1",
5223 name: "api",
5224 branchId: "branch-1",
5225 }),
5226 );
5227 yield* repoProvider.delete(
5228 deleteInput("SourceRepository", {
5229 sourceRepositoryId: "repo-1",
5230 projectId: "project-1",
5231 repoId: 123,
5232 provider: "github",
5233 installationId: "installation-1",
5234 }),
5235 );
5236 yield* domainProvider.delete(
5237 deleteInput("CustomDomain", {
5238 customDomainId: "domain-1",
5239 appId: "service-1",
5240 hostname: "api.example.com",
5241 }),
5242 );
5243 yield* envProvider.delete(
5244 deleteInput("EnvironmentVariable", {
5245 environmentVariableId: "env-1",
5246 projectId: "project-1",
5247 branchId: null,
5248 class: "production",
5249 key: "TOKEN",
5250 }),
5251 );
5252 yield* branchProvider.delete(
5253 deleteInput("Branch", {
5254 branchId: "branch-1",
5255 projectId: "project-1",
5256 gitName: "preview",
5257 }),
5258 );
5259 yield* connectionProvider.delete(
5260 deleteInput("Connection", {
5261 connectionId: "connection-1",
5262 databaseId: "database-1",
5263 connectionName: "api-000000000000",
5264 }),
5265 );
5266 yield* databaseProvider.delete(
5267 deleteInput("Database", { databaseId: "database-1" }),
5268 );
5269 yield* projectProvider.delete(
5270 deleteInput("Project", { projectId: "project-1" }),
5271 );
5272
5273 expect(calls).toEqual([
5274 ["getDeployment", "version-1"],
5275 // Over the wire, an empty query arrives as an empty object.
5276 ["listAppDeployments", { appId: "service-1", query: {} }],
5277 ["getDeployment", "version-1"],
5278 ["stopDeployment", "version-1"],
5279 ["getDeployment", "version-1"],
5280 ["deleteDeployment", "version-1"],
5281 ["getApp", "service-1"],
5282 ["deleteApp", "service-1"],
5283 ["getSourceRepository", "repo-1"],
5284 ["deleteSourceRepository", "repo-1"],
5285 ["getCustomDomain", "domain-1"],
5286 ["deleteCustomDomain", "domain-1"],
5287 ["getEnvironmentVariable", "env-1"],
5288 ["deleteEnvironmentVariable", "env-1"],
5289 ["getBranch", "branch-1"],
5290 ["deleteBranch", "branch-1"],
5291 ["getConnection", "connection-1"],
5292 ["deleteConnection", "connection-1"],
5293 ["getDatabase", "database-1"],
5294 ["deleteDatabase", "database-1"],
5295 // Over the wire, query params arrive as strings.
5296 ["listApps", { limit: "100", projectId: "project-1" }],
5297 ["deleteProject", "project-1"],
5298 ]);
5299 }).pipe(
5300 Effect.provide(providerLayer(client)),
5301 Effect.provide(managementApi(client).layer),
5302 Effect.provide(FetchHttpClient.layer),
5303 Effect.provideService(Stack, {
5304 name: "prisma-provider-delete-test",
5305 stage: "test",
5306 resources: {},
5307 bindings: {},
5308 actions: {},
5309 }),
5310 Effect.provideService(Stage, "test"),
5311 );
5312 },
5313 {
5314 tags: [
5315 "provider:prisma:app",
5316 "provider:prisma:branch",
5317 "provider:prisma:connection",
5318 "provider:prisma:customdomain",
5319 "provider:prisma:database",
5320 "provider:prisma:deployment",
5321 "provider:prisma:environmentvariable",
5322 "provider:prisma:project",
5323 "provider:prisma:sourcerepository",
5324 ],
5325 },
5326 );
5327
5328 it.effect(
5329 "rejects direct delete for a default Prisma database",
5330 () => {
5331 const calls: Call[] = [];
5332 const client = {
5333 getDatabase: (id: string) =>
5334 Effect.sync(() => {
5335 calls.push(["getDatabase", id]);
5336 return { id, isDefault: true };
5337 }),
5338 deleteDatabase: (id: string) =>
5339 Effect.sync(() => {
5340 calls.push(["deleteDatabase", id]);
5341 }),
5342 } as unknown as PrismaManagementClient;
5343
5344 return Effect.gen(function* () {
5345 const databaseProvider = yield* PrismaDatabase.Provider;
5346 const result = yield* databaseProvider
5347 .delete({
5348 id: "Postgres",
5349 fqn: "Postgres",
5350 instanceId: "00000000000000000000000000000000",
5351 olds: {} as never,
5352 output: {
5353 databaseId: "database-1",
5354 projectId: "project-1",
5355 isDefault: true,
5356 },
5357 session: undefined,
5358 bindings: [],
5359 } as never)
5360 .pipe(Effect.result);
5361
5362 expect(Result.isFailure(result)).toBe(true);
5363 if (Result.isFailure(result)) {
5364 expect(String(result.failure)).toContain(
5365 "Cannot delete default Prisma database",
5366 );
5367 }
5368 expect(calls).toEqual([["getDatabase", "database-1"]]);
5369 }).pipe(
5370 Effect.provide(providerLayer(client)),
5371 Effect.provide(managementApi(client).layer),
5372 );
5373 },
5374 {
5375 tags: [
5376 "provider:prisma:app",
5377 "provider:prisma:branch",
5378 "provider:prisma:connection",
5379 "provider:prisma:customdomain",
5380 "provider:prisma:database",
5381 "provider:prisma:deployment",
5382 "provider:prisma:environmentvariable",
5383 "provider:prisma:project",
5384 "provider:prisma:sourcerepository",
5385 ],
5386 },
5387 );
5388
5389 it.effect(
5390 "checks live database default state before deleting",
5391 () => {
5392 const calls: Call[] = [];
5393 const client = {
5394 getDatabase: (id: string) =>
5395 Effect.sync(() => {
5396 calls.push(["getDatabase", id]);
5397 return { id, isDefault: true };
5398 }),
5399 deleteDatabase: (id: string) =>
5400 Effect.sync(() => {
5401 calls.push(["deleteDatabase", id]);
5402 }),
5403 } as unknown as PrismaManagementClient;
5404
5405 return Effect.gen(function* () {
5406 const databaseProvider = yield* PrismaDatabase.Provider;
5407 const result = yield* databaseProvider
5408 .delete({
5409 id: "Postgres",
5410 fqn: "Postgres",
5411 instanceId: "00000000000000000000000000000000",
5412 olds: {} as never,
5413 output: {
5414 databaseId: "database-1",
5415 projectId: "project-1",
5416 isDefault: false,
5417 },
5418 session: undefined,
5419 bindings: [],
5420 } as never)
5421 .pipe(Effect.result);
5422
5423 expect(Result.isFailure(result)).toBe(true);
5424 expect(calls).toEqual([["getDatabase", "database-1"]]);
5425 }).pipe(
5426 Effect.provide(providerLayer(client)),
5427 Effect.provide(managementApi(client).layer),
5428 );
5429 },
5430 {
5431 tags: [
5432 "provider:prisma:app",
5433 "provider:prisma:branch",
5434 "provider:prisma:connection",
5435 "provider:prisma:customdomain",
5436 "provider:prisma:database",
5437 "provider:prisma:deployment",
5438 "provider:prisma:environmentvariable",
5439 "provider:prisma:project",
5440 "provider:prisma:sourcerepository",
5441 ],
5442 },
5443 );
5444
5445 it.effect(
5446 "rejects default Prisma database delete without a session",
5447 () => {
5448 const calls: Call[] = [];
5449 const client = {
5450 getDatabase: (id: string) =>
5451 Effect.sync(() => {
5452 calls.push(["getDatabase", id]);
5453 return { id, isDefault: true };
5454 }),
5455 deleteDatabase: (id: string) =>
5456 Effect.sync(() => {
5457 calls.push(["deleteDatabase", id]);
5458 }),
5459 } as unknown as PrismaManagementClient;
5460
5461 return Effect.gen(function* () {
5462 const databaseProvider = yield* PrismaDatabase.Provider;
5463 const result = yield* databaseProvider
5464 .delete({
5465 id: "Postgres",
5466 fqn: "Postgres",
5467 instanceId: "00000000000000000000000000000000",
5468 olds: {} as never,
5469 output: {
5470 databaseId: "database-1",
5471 projectId: "project-1",
5472 isDefault: true,
5473 },
5474 session: undefined,
5475 bindings: [],
5476 } as never)
5477 .pipe(Effect.result);
5478
5479 expect(Result.isFailure(result)).toBe(true);
5480 expect(calls).toEqual([["getDatabase", "database-1"]]);
5481 }).pipe(
5482 Effect.provide(providerLayer(client)),
5483 Effect.provide(managementApi(client).layer),
5484 );
5485 },
5486 {
5487 tags: [
5488 "provider:prisma:app",
5489 "provider:prisma:branch",
5490 "provider:prisma:connection",
5491 "provider:prisma:customdomain",
5492 "provider:prisma:database",
5493 "provider:prisma:deployment",
5494 "provider:prisma:environmentvariable",
5495 "provider:prisma:project",
5496 "provider:prisma:sourcerepository",
5497 ],
5498 },
5499 );
5500
5501 it.effect(
5502 "deletes a database when stale state says it is default",
5503 () => {
5504 const calls: Call[] = [];
5505 const client = {
5506 getDatabase: (id: string) =>
5507 Effect.sync(() => {
5508 calls.push(["getDatabase", id]);
5509 return { id, isDefault: false };
5510 }),
5511 deleteDatabase: (id: string) =>
5512 Effect.sync(() => {
5513 calls.push(["deleteDatabase", id]);
5514 }),
5515 } as unknown as PrismaManagementClient;
5516
5517 return Effect.gen(function* () {
5518 const databaseProvider = yield* PrismaDatabase.Provider;
5519 yield* databaseProvider.delete({
5520 id: "Postgres",
5521 fqn: "Postgres",
5522 instanceId: "00000000000000000000000000000000",
5523 olds: {} as never,
5524 output: {
5525 databaseId: "database-1",
5526 projectId: "project-1",
5527 isDefault: true,
5528 },
5529 session: undefined,
5530 bindings: [],
5531 } as never);
5532
5533 expect(calls).toEqual([
5534 ["getDatabase", "database-1"],
5535 ["deleteDatabase", "database-1"],
5536 ]);
5537 }).pipe(
5538 Effect.provide(providerLayer(client)),
5539 Effect.provide(managementApi(client).layer),
5540 );
5541 },
5542 {
5543 tags: [
5544 "provider:prisma:app",
5545 "provider:prisma:branch",
5546 "provider:prisma:connection",
5547 "provider:prisma:customdomain",
5548 "provider:prisma:database",
5549 "provider:prisma:deployment",
5550 "provider:prisma:environmentvariable",
5551 "provider:prisma:project",
5552 "provider:prisma:sourcerepository",
5553 ],
5554 },
5555 );
5556
5557 it.effect(
5558 "rejects direct delete for a default Prisma branch",
5559 () => {
5560 const calls: Call[] = [];
5561 const client = {
5562 getBranch: (id: string) =>
5563 Effect.sync(() => {
5564 calls.push(["getBranch", id]);
5565 return {
5566 id,
5567 gitName: "preview",
5568 isDefault: true,
5569 role: "preview" as const,
5570 project: { id: "project-1" },
5571 };
5572 }),
5573 deleteBranch: (id: string) =>
5574 Effect.sync(() => {
5575 calls.push(["deleteBranch", id]);
5576 }),
5577 } as unknown as PrismaManagementClient;
5578
5579 return Effect.gen(function* () {
5580 const branchProvider = yield* PrismaBranch.Provider;
5581 const result = yield* branchProvider
5582 .delete({
5583 id: "MainBranch",
5584 fqn: "MainBranch",
5585 instanceId: "00000000000000000000000000000000",
5586 olds: {} as never,
5587 output: {
5588 branchId: "branch-1",
5589 projectId: "project-1",
5590 gitName: "preview",
5591 isDefault: true,
5592 role: "preview",
5593 },
5594 session: undefined,
5595 bindings: [],
5596 } as never)
5597 .pipe(Effect.result);
5598
5599 expect(Result.isFailure(result)).toBe(true);
5600 if (Result.isFailure(result)) {
5601 expect(String(result.failure)).toContain(
5602 "Cannot safely delete default Prisma branch",
5603 );
5604 }
5605 expect(calls).toEqual([["getBranch", "branch-1"]]);
5606 }).pipe(
5607 Effect.provide(providerLayer(client)),
5608 Effect.provide(managementApi(client).layer),
5609 );
5610 },
5611 {
5612 tags: [
5613 "provider:prisma:app",
5614 "provider:prisma:branch",
5615 "provider:prisma:connection",
5616 "provider:prisma:customdomain",
5617 "provider:prisma:database",
5618 "provider:prisma:deployment",
5619 "provider:prisma:environmentvariable",
5620 "provider:prisma:project",
5621 "provider:prisma:sourcerepository",
5622 ],
5623 },
5624 );
5625
5626 it.effect(
5627 "checks live branch default state before deleting",
5628 () => {
5629 const calls: Call[] = [];
5630 const client = {
5631 getBranch: (id: string) =>
5632 Effect.sync(() => {
5633 calls.push(["getBranch", id]);
5634 return {
5635 id,
5636 gitName: "preview",
5637 isDefault: true,
5638 role: "preview" as const,
5639 project: { id: "project-1" },
5640 };
5641 }),
5642 deleteBranch: (id: string) =>
5643 Effect.sync(() => {
5644 calls.push(["deleteBranch", id]);
5645 }),
5646 } as unknown as PrismaManagementClient;
5647
5648 return Effect.gen(function* () {
5649 const branchProvider = yield* PrismaBranch.Provider;
5650 const result = yield* branchProvider
5651 .delete({
5652 id: "MainBranch",
5653 fqn: "MainBranch",
5654 instanceId: "00000000000000000000000000000000",
5655 olds: {} as never,
5656 output: {
5657 branchId: "branch-1",
5658 projectId: "project-1",
5659 gitName: "preview",
5660 isDefault: false,
5661 role: "preview",
5662 },
5663 session: undefined,
5664 bindings: [],
5665 } as never)
5666 .pipe(Effect.result);
5667
5668 expect(Result.isFailure(result)).toBe(true);
5669 expect(calls).toEqual([["getBranch", "branch-1"]]);
5670 }).pipe(
5671 Effect.provide(providerLayer(client)),
5672 Effect.provide(managementApi(client).layer),
5673 );
5674 },
5675 {
5676 tags: [
5677 "provider:prisma:app",
5678 "provider:prisma:branch",
5679 "provider:prisma:connection",
5680 "provider:prisma:customdomain",
5681 "provider:prisma:database",
5682 "provider:prisma:deployment",
5683 "provider:prisma:environmentvariable",
5684 "provider:prisma:project",
5685 "provider:prisma:sourcerepository",
5686 ],
5687 },
5688 );
5689
5690 it.effect(
5691 "rejects default Prisma branch delete without a session",
5692 () => {
5693 const calls: Call[] = [];
5694 const client = {
5695 getBranch: (id: string) =>
5696 Effect.sync(() => {
5697 calls.push(["getBranch", id]);
5698 return {
5699 id,
5700 gitName: "preview",
5701 isDefault: true,
5702 role: "preview" as const,
5703 project: { id: "project-1" },
5704 };
5705 }),
5706 deleteBranch: (id: string) =>
5707 Effect.sync(() => {
5708 calls.push(["deleteBranch", id]);
5709 }),
5710 } as unknown as PrismaManagementClient;
5711
5712 return Effect.gen(function* () {
5713 const branchProvider = yield* PrismaBranch.Provider;
5714 const result = yield* branchProvider
5715 .delete({
5716 id: "MainBranch",
5717 fqn: "MainBranch",
5718 instanceId: "00000000000000000000000000000000",
5719 olds: {} as never,
5720 output: {
5721 branchId: "branch-1",
5722 projectId: "project-1",
5723 gitName: "preview",
5724 isDefault: true,
5725 role: "preview",
5726 },
5727 session: undefined,
5728 bindings: [],
5729 } as never)
5730 .pipe(Effect.result);
5731
5732 expect(Result.isFailure(result)).toBe(true);
5733 expect(calls).toEqual([["getBranch", "branch-1"]]);
5734 }).pipe(
5735 Effect.provide(providerLayer(client)),
5736 Effect.provide(managementApi(client).layer),
5737 );
5738 },
5739 {
5740 tags: [
5741 "provider:prisma:app",
5742 "provider:prisma:branch",
5743 "provider:prisma:connection",
5744 "provider:prisma:customdomain",
5745 "provider:prisma:database",
5746 "provider:prisma:deployment",
5747 "provider:prisma:environmentvariable",
5748 "provider:prisma:project",
5749 "provider:prisma:sourcerepository",
5750 ],
5751 },
5752 );
5753
5754 it.effect(
5755 "deletes a branch when stale state says it is default",
5756 () => {
5757 const calls: Call[] = [];
5758 const client = {
5759 getBranch: (id: string) =>
5760 Effect.sync(() => {
5761 calls.push(["getBranch", id]);
5762 return {
5763 id,
5764 gitName: "preview",
5765 isDefault: false,
5766 role: "preview" as const,
5767 project: { id: "project-1" },
5768 };
5769 }),
5770 deleteBranch: (id: string) =>
5771 Effect.sync(() => {
5772 calls.push(["deleteBranch", id]);
5773 }),
5774 } as unknown as PrismaManagementClient;
5775
5776 return Effect.gen(function* () {
5777 const branchProvider = yield* PrismaBranch.Provider;
5778 yield* branchProvider.delete({
5779 id: "MainBranch",
5780 fqn: "MainBranch",
5781 instanceId: "00000000000000000000000000000000",
5782 olds: {} as never,
5783 output: {
5784 branchId: "branch-1",
5785 projectId: "project-1",
5786 gitName: "preview",
5787 isDefault: true,
5788 role: "preview",
5789 },
5790 session: undefined,
5791 bindings: [],
5792 } as never);
5793
5794 expect(calls).toEqual([
5795 ["getBranch", "branch-1"],
5796 ["deleteBranch", "branch-1"],
5797 ]);
5798 }).pipe(
5799 Effect.provide(providerLayer(client)),
5800 Effect.provide(managementApi(client).layer),
5801 );
5802 },
5803 {
5804 tags: [
5805 "provider:prisma:app",
5806 "provider:prisma:branch",
5807 "provider:prisma:connection",
5808 "provider:prisma:customdomain",
5809 "provider:prisma:database",
5810 "provider:prisma:deployment",
5811 "provider:prisma:environmentvariable",
5812 "provider:prisma:project",
5813 "provider:prisma:sourcerepository",
5814 ],
5815 },
5816 );
5817
5818 it.effect(
5819 "refuses to adopt explicit identities after create races",
5820 () => {
5821 const calls: Call[] = [];
5822 const visible = new Set<string>();
5823 const apiConflict = (path: string) =>
5824 new PrismaApiError({
5825 method: "POST",
5826 path,
5827 status: 409,
5828 message: "already exists",
5829 });
5830 const project = {
5831 id: "project-1",
5832 type: "project" as const,
5833 url: "https://api.prisma.test/v1/projects/project-1",
5834 name: "app",
5835 createdAt,
5836 defaultRegion: "us-east-1",
5837 workspace: resourceRef("workspaces", "workspace-1", "team"),
5838 };
5839 const database = {
5840 id: "database-1",
5841 type: "database" as const,
5842 url: "https://api.prisma.test/v1/databases/database-1",
5843 name: "main",
5844 status: "ready" as const,
5845 createdAt,
5846 isDefault: false,
5847 defaultConnectionId: "connection-1",
5848 connections: [],
5849 project: resourceRef("projects", "project-1", "app"),
5850 region: { id: "us-east-1", name: "US East" },
5851 source: { type: "empty" },
5852 branchId: null,
5853 };
5854 const connection = {
5855 id: "connection-1",
5856 type: "connection" as const,
5857 url: "https://api.prisma.test/v1/connections/connection-1",
5858 name: "api-000000000000",
5859 createdAt,
5860 kind: "postgres" as const,
5861 endpoints: {
5862 direct: { host: "db.prisma.test", port: 5432 },
5863 },
5864 database: resourceRef("databases", "database-1", "main"),
5865 };
5866 const branch = {
5867 id: "branch-1",
5868 type: "branch" as const,
5869 url: "https://api.prisma.test/v1/branches/branch-1",
5870 gitName: "preview",
5871 isDefault: false,
5872 role: "preview" as const,
5873 createdAt,
5874 updatedAt,
5875 project: resourceRef("projects", "project-1", "app"),
5876 };
5877 const defaultBranch = {
5878 ...branch,
5879 id: "branch-main",
5880 url: "https://api.prisma.test/v1/branches/branch-main",
5881 gitName: "main",
5882 isDefault: true,
5883 role: "production" as const,
5884 };
5885 const service = {
5886 id: "service-1",
5887 type: "app" as const,
5888 url: "https://api.prisma.test/v1/services/service-1",
5889 name: "api",
5890 region: { id: "us-east-1", name: "US East" },
5891 projectId: "project-1",
5892 branchId: null,
5893 latestDeploymentId: null,
5894 appEndpointDomain: "service-1.prisma.build",
5895 createdAt,
5896 };
5897 const variable = {
5898 id: "env-1",
5899 type: "environment-variable" as const,
5900 url: "https://api.prisma.test/v1/environment-variables/env-1",
5901 projectId: "project-1",
5902 branchId: null,
5903 class: "production" as const,
5904 key: "TOKEN",
5905 valueKid: "kid-1",
5906 isManagedBySystem: false,
5907 createdAt,
5908 updatedAt,
5909 };
5910 const repo = {
5911 id: "repo-1",
5912 type: "source-repository" as const,
5913 url: "https://api.prisma.test/v1/source-repositories/repo-1",
5914 repoId: 123,
5915 provider: "github" as const,
5916 repoFullName: "acme/api",
5917 defaultBranch: "main",
5918 isPrivate: true,
5919 status: "active" as const,
5920 projectId: "project-1",
5921 installationId: "installation-1",
5922 createdAt,
5923 updatedAt,
5924 };
5925
5926 const fake = makeFakeManagementApi((request) => {
5927 if (request.pathname === "/v1/projects" && request.method === "GET") {
5928 return page(visible.has("project") ? [wireProject()] : []);
5929 }
5930 if (
5931 request.pathname === "/v1/projects" &&
5932 request.method === "POST"
5933 ) {
5934 visible.add("project");
5935 return conflict("already exists");
5936 }
5937 return dispatchManagement(client, request);
5938 });
5939
5940 const client = {
5941 listProjectDatabases: (projectId: string, query: unknown) =>
5942 Effect.sync(() => {
5943 calls.push(["listProjectDatabases", { projectId, query }]);
5944 return visible.has("database") ? [database] : [];
5945 }),
5946 createDatabase: (input: unknown) =>
5947 Effect.gen(function* () {
5948 calls.push(["createDatabase", input]);
5949 visible.add("database");
5950 return yield* Effect.fail(apiConflict("/v1/databases"));
5951 }),
5952 listDatabaseConnections: (databaseId: string, query: unknown) =>
5953 Effect.sync(() => {
5954 calls.push(["listDatabaseConnections", { databaseId, query }]);
5955 return visible.has("connection") ? [connection] : [];
5956 }),
5957 createConnection: (input: unknown) =>
5958 Effect.gen(function* () {
5959 calls.push(["createConnection", input]);
5960 visible.add("connection");
5961 return yield* Effect.fail(apiConflict("/v1/connections"));
5962 }),
5963 rotateConnection: (id: string) =>
5964 Effect.sync(() => {
5965 calls.push(["rotateConnection", id]);
5966 return {
5967 ...connection,
5968 endpoints: {
5969 direct: {
5970 host: "db.prisma.test",
5971 port: 5432,
5972 connectionString:
5973 "postgres://app:rotated-password@db.prisma.test/database-1",
5974 },
5975 },
5976 };
5977 }),
5978 listBranches: (projectId: string, query: unknown) =>
5979 Effect.sync(() => {
5980 calls.push(["listBranches", { projectId, query }]);
5981 const gitName = (query as { gitName?: string } | undefined)
5982 ?.gitName;
5983 if (gitName === "preview") {
5984 return visible.has("branch") ? [branch] : [];
5985 }
5986 return [
5987 defaultBranch,
5988 ...(visible.has("branch") ? [branch] : []),
5989 ];
5990 }),
5991 createBranch: (projectId: string, input: unknown) =>
5992 Effect.gen(function* () {
5993 calls.push(["createBranch", { projectId, input }]);
5994 visible.add("branch");
5995 return yield* Effect.fail(
5996 apiConflict(`/v1/projects/${projectId}/branches`),
5997 );
5998 }),
5999 listApps: (query: unknown) =>
6000 Effect.sync(() => {
6001 calls.push(["listApps", query]);
6002 return visible.has("service") ? [service] : [];
6003 }),
6004 createApp: (input: { projectId: string }) =>
6005 Effect.gen(function* () {
6006 calls.push(["createApp", input]);
6007 visible.add("service");
6008 return yield* Effect.fail(apiConflict(`/v1/services`));
6009 }),
6010 listEnvironmentVariables: (query: unknown) =>
6011 Effect.sync(() => {
6012 calls.push(["listEnvironmentVariables", query]);
6013 return visible.has("env") ? [variable] : [];
6014 }),
6015 createEnvironmentVariable: (input: unknown) =>
6016 Effect.gen(function* () {
6017 calls.push(["createEnvironmentVariable", input]);
6018 visible.add("env");
6019 return yield* Effect.fail(
6020 apiConflict("/v1/environment-variables"),
6021 );
6022 }),
6023 updateEnvironmentVariable: (id: string, input: unknown) =>
6024 Effect.sync(() => {
6025 calls.push(["updateEnvironmentVariable", { id, input }]);
6026 return variable;
6027 }),
6028 listSourceRepositories: (query: unknown) =>
6029 Effect.sync(() => {
6030 calls.push(["listSourceRepositories", query]);
6031 return visible.has("repo") ? [repo] : [];
6032 }),
6033 createSourceRepository: (input: unknown) =>
6034 Effect.gen(function* () {
6035 calls.push(["createSourceRepository", input]);
6036 visible.add("repo");
6037 return yield* Effect.fail(apiConflict("/v1/source-repositories"));
6038 }),
6039 } as unknown as PrismaManagementClient;
6040
6041 return Effect.gen(function* () {
6042 const projectProvider = yield* PrismaProject.Provider;
6043 const databaseProvider = yield* PrismaDatabase.Provider;
6044 const connectionProvider = yield* PrismaConnection.Provider;
6045 const branchProvider = yield* PrismaBranch.Provider;
6046 const serviceProvider = yield* PrismaApp.Provider;
6047 const envProvider = yield* PrismaEnvironmentVariable.Provider;
6048 const repoProvider = yield* PrismaSourceRepository.Provider;
6049
6050 const projectError = yield* projectProvider
6051 .reconcile(
6052 reconcileInput("Project", { name: "app", createDatabase: false }),
6053 )
6054 .pipe(Effect.flip);
6055 const databaseError = yield* databaseProvider
6056 .reconcile(
6057 reconcileInput("Database", {
6058 project: "project-1",
6059 name: "main",
6060 }),
6061 )
6062 .pipe(Effect.flip);
6063 const connectionOut = yield* connectionProvider.reconcile(
6064 reconcileInput("Connection", {
6065 database: "database-1",
6066 name: "api",
6067 rotate: true,
6068 }),
6069 );
6070 const branchError = yield* branchProvider
6071 .reconcile(
6072 reconcileInput("Branch", {
6073 project: "project-1",
6074 gitName: "preview",
6075 }),
6076 )
6077 .pipe(Effect.flip);
6078 const serviceError = yield* serviceProvider
6079 .reconcile(
6080 reconcileInput("App", {
6081 project: "project-1",
6082 displayName: "api",
6083 }),
6084 )
6085 .pipe(Effect.flip);
6086 const envError = yield* envProvider
6087 .reconcile(
6088 reconcileInput("EnvironmentVariable", {
6089 project: "project-1",
6090 class: "production" as const,
6091 key: "TOKEN",
6092 value: Redacted.make("secret"),
6093 }),
6094 )
6095 .pipe(Effect.flip);
6096 const repoError = yield* repoProvider
6097 .reconcile(
6098 reconcileInput("SourceRepository", {
6099 project: "project-1",
6100 providerRepositoryId: 123,
6101 }),
6102 )
6103 .pipe(Effect.flip);
6104
6105 expect((projectError as Error).message).toContain(
6106 "appeared after the adoption check",
6107 );
6108 expect((databaseError as Error).message).toContain(
6109 "appeared after the adoption check",
6110 );
6111 expect(connectionOut.connectionId).toBe("connection-1");
6112 expect(Redacted.value(connectionOut.directConnectionString!)).toBe(
6113 "postgres://app:rotated-password@db.prisma.test/database-1",
6114 );
6115 expect(Redacted.value(connectionOut.password!)).toBe(
6116 "rotated-password",
6117 );
6118 expect((branchError as Error).message).toContain(
6119 "appeared after the adoption check",
6120 );
6121 expect((serviceError as Error).message).toContain(
6122 "not owned by this App resource",
6123 );
6124 expect((envError as Error).message).toContain(
6125 "appeared after the adoption check",
6126 );
6127 expect((repoError as Error).message).toContain(
6128 "appeared after the adoption check",
6129 );
6130 expect(
6131 fake.captured
6132 .filter((request) => request.method === "POST")
6133 .map((request) => request.bodyJson),
6134 ).toEqual([
6135 { createDatabase: false, name: "app" },
6136 {
6137 projectId: "project-1",
6138 region: "us-east-1",
6139 name: "main",
6140 isDefault: false,
6141 },
6142 { databaseId: "database-1", name: "api-000000000000" },
6143 undefined,
6144 { gitName: "preview" },
6145 {
6146 displayName: "api",
6147 branchId: "branch-main",
6148 projectId: "project-1",
6149 },
6150 {
6151 projectId: "project-1",
6152 class: "production",
6153 key: "TOKEN",
6154 value: "secret",
6155 },
6156 {
6157 projectId: "project-1",
6158 provider: "github",
6159 providerRepositoryId: 123,
6160 },
6161 ]);
6162 expect(calls.filter(([name]) => name.startsWith("create"))).toEqual([
6163 [
6164 "createDatabase",
6165 {
6166 projectId: "project-1",
6167 name: "main",
6168 region: "us-east-1",
6169 isDefault: false,
6170 source: undefined,
6171 branchId: undefined,
6172 branchGitName: undefined,
6173 },
6174 ],
6175 [
6176 "createConnection",
6177 { databaseId: "database-1", name: "api-000000000000" },
6178 ],
6179 [
6180 "createBranch",
6181 {
6182 projectId: "project-1",
6183 input: { gitName: "preview", isDefault: undefined },
6184 },
6185 ],
6186 [
6187 "createApp",
6188 // JSON transport drops `undefined` members.
6189 {
6190 projectId: "project-1",
6191 displayName: "api",
6192 branchId: "branch-main",
6193 },
6194 ],
6195 [
6196 "createEnvironmentVariable",
6197 {
6198 projectId: "project-1",
6199 class: "production",
6200 key: "TOKEN",
6201 value: "secret",
6202 },
6203 ],
6204 [
6205 "createSourceRepository",
6206 {
6207 projectId: "project-1",
6208 provider: "github",
6209 providerRepositoryId: 123,
6210 installationId: undefined,
6211 },
6212 ],
6213 ]);
6214 expect(calls).toContainEqual(["rotateConnection", "connection-1"]);
6215 }).pipe(
6216 Effect.provide(providerLayer(client)),
6217 Effect.provide(fake.layer),
6218 Effect.provide(FetchHttpClient.layer),
6219 Effect.provideService(Stack, {
6220 name: "prisma-provider-conflict-test",
6221 stage: "test",
6222 resources: {},
6223 bindings: {},
6224 actions: {},
6225 }),
6226 Effect.provideService(Stage, "test"),
6227 );
6228 },
6229 {
6230 tags: [
6231 "provider:prisma:app",
6232 "provider:prisma:branch",
6233 "provider:prisma:connection",
6234 "provider:prisma:customdomain",
6235 "provider:prisma:database",
6236 "provider:prisma:deployment",
6237 "provider:prisma:environmentvariable",
6238 "provider:prisma:project",
6239 "provider:prisma:sourcerepository",
6240 ],
6241 },
6242 );
6243 },
6244);