1import * as Data from "effect/Data";
2import * as Context from "effect/Context";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as Layer from "effect/Layer";
6import * as Redacted from "effect/Redacted";
7import * as Schedule from "effect/Schedule";
8import * as Stream from "effect/Stream";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientError from "effect/http/HttpClientError";
11import * as HttpClientRequest from "effect/http/HttpClientRequest";
12import { type ObservedConnection, secretValue } from "./Internal/Observed.ts";
13import { PrismaEnvironment } from "./PrismaEnvironment.ts";
14import type {
15 App,
16 AccelerateRegion,
17 AppCreateInput,
18 AppDeploymentTarget,
19 AppUpdateInput,
20 BackupListResponse,
21 Branch,
22 BranchCreateInput,
23 BranchUpdateInput,
24 Bucket,
25 BucketCreateInput,
26 BucketKey,
27 BucketKeyCreateInput,
28 BucketKeyWithSecret,
29 BuildLogsQuery,
30 BuildLogsRequest,
31 ConnectionCreateInput,
32 CurrentPrincipal,
33 CustomDomain,
34 CustomDomainCreateInput,
35 CustomDomainCreateResult,
36 DataResponse,
37 Database,
38 DatabaseConnection,
39 DatabaseConnectionCreateInput,
40 DatabaseConnectionWithOptionalSecrets,
41 DatabaseConnectionWithSecrets,
42 DatabaseCreateInput,
43 DatabaseUpdateInput,
44 DatabaseUsage,
45 Deployment,
46 DeploymentCreateInput,
47 DeploymentCreateResult,
48 DeploymentListItem,
49 DeploymentLogsQuery,
50 DeploymentLogsRequest,
51 EnvironmentVariable,
52 EnvironmentVariableCreateInput,
53 EnvironmentVariableUpdateInput,
54 Integration,
55 ListResponse,
56 PrismaBranchIdFilter,
57 PrismaSecretConnection,
58 Project,
59 ProjectCreateInput,
60 ProjectDatabaseCreateInput,
61 ProjectTransferInput,
62 ProjectUpdateInput,
63 PostgresRegion,
64 PromoteAppResult,
65 RollbackAppResult,
66 Region,
67 RestoreDatabaseInput,
68 RestoredDatabase,
69 ScmInstallIntent,
70 ScmInstallIntentCreateInput,
71 ScmInstallation,
72 ScmRepository,
73 SourceRepository,
74 SourceRepositoryCreateInput,
75 StartDeploymentResult,
76 Workspace,
77} from "./Types.ts";
78
79type Method = "GET" | "POST" | "PATCH" | "DELETE";
80
81export class PrismaApiError extends Data.TaggedError("PrismaApiError")<{
82 method: Method;
83 path: string;
84 status: number;
85 message: string;
86 /** Raw response bodies may contain credentials and are always redacted. */
87 body?: Redacted.Redacted<string>;
88}> {}
89
90export class PrismaApiDecodeError extends Data.TaggedError(
91 "PrismaApiDecodeError",
92)<{
93 method: Method;
94 path: string;
95 /** Byte length is retained for diagnostics without retaining response data. */
96 bodyLength: number;
97 message: string;
98}> {}
99
100interface RequestOptions {
101 query?: object;
102 body?: unknown;
103 headers?: Record<string, string>;
104 timeout?: Duration.Input;
105 onSuccessfulBodyLength?: (
106 bodyLength: number,
107 ) => Effect.Effect<void, PrismaApiDecodeError>;
108}
109
110export interface PrismaManagementClient {
111 listWorkspaces(
112 query?: PaginationQuery,
113 ): Effect.Effect<Workspace[], PrismaApiError | PrismaApiDecodeError>;
114 getWorkspace(
115 id: string,
116 ): Effect.Effect<Workspace, PrismaApiError | PrismaApiDecodeError>;
117 getCurrentPrincipal(): Effect.Effect<
118 CurrentPrincipal,
119 PrismaApiError | PrismaApiDecodeError
120 >;
121 listRegions(
122 query?: RegionListQuery,
123 ): Effect.Effect<Region[], PrismaApiError | PrismaApiDecodeError>;
124 listPostgresRegions(): Effect.Effect<
125 PostgresRegion[],
126 PrismaApiError | PrismaApiDecodeError
127 >;
128 listAccelerateRegions(): Effect.Effect<
129 AccelerateRegion[],
130 PrismaApiError | PrismaApiDecodeError
131 >;
132 listProjects(
133 query?: PaginationQuery,
134 ): Effect.Effect<Project[], PrismaApiError | PrismaApiDecodeError>;
135 getProject(
136 id: string,
137 ): Effect.Effect<Project, PrismaApiError | PrismaApiDecodeError>;
138 createProject(
139 input: ProjectCreateInput,
140 ): Effect.Effect<ProjectCreateResult, PrismaApiError | PrismaApiDecodeError>;
141 updateProject(
142 id: string,
143 input: ProjectUpdateInput,
144 ): Effect.Effect<Project, PrismaApiError | PrismaApiDecodeError>;
145 deleteProject(
146 id: string,
147 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
148 transferProject(
149 id: string,
150 input: ProjectTransferInput,
151 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
152 listDatabases(
153 query?: DatabaseListQuery,
154 ): Effect.Effect<Database[], PrismaApiError | PrismaApiDecodeError>;
155 listProjectDatabases(
156 projectId: string,
157 query?: PaginationQuery,
158 ): Effect.Effect<Database[], PrismaApiError | PrismaApiDecodeError>;
159 getDatabase(
160 id: string,
161 ): Effect.Effect<Database, PrismaApiError | PrismaApiDecodeError>;
162 createDatabase(
163 input: DatabaseCreateInput,
164 ): Effect.Effect<DatabaseCreateResult, PrismaApiError | PrismaApiDecodeError>;
165 createProjectDatabase(
166 projectId: string,
167 input: ProjectDatabaseCreateInput,
168 ): Effect.Effect<
169 ProjectDatabaseCreateResult,
170 PrismaApiError | PrismaApiDecodeError
171 >;
172 updateDatabase(
173 id: string,
174 input: DatabaseUpdateInput,
175 ): Effect.Effect<Database, PrismaApiError | PrismaApiDecodeError>;
176 deleteDatabase(
177 id: string,
178 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
179 listBackups(
180 databaseId: string,
181 query?: BackupListQuery,
182 ): Effect.Effect<BackupListResponse, PrismaApiError | PrismaApiDecodeError>;
183 restoreDatabase(
184 targetDatabaseId: string,
185 input: RestoreDatabaseInput,
186 ): Effect.Effect<RestoredDatabase, PrismaApiError | PrismaApiDecodeError>;
187 getDatabaseUsage(
188 databaseId: string,
189 query?: DatabaseUsageQuery,
190 ): Effect.Effect<DatabaseUsage, PrismaApiError | PrismaApiDecodeError>;
191 listConnections(
192 query?: ConnectionListQuery,
193 ): Effect.Effect<DatabaseConnection[], PrismaApiError | PrismaApiDecodeError>;
194 listDatabaseConnections(
195 databaseId: string,
196 query?: PaginationQuery,
197 ): Effect.Effect<DatabaseConnection[], PrismaApiError | PrismaApiDecodeError>;
198 getConnection(
199 id: string,
200 ): Effect.Effect<DatabaseConnection, PrismaApiError | PrismaApiDecodeError>;
201 createConnection(
202 input: ConnectionCreateInput,
203 ): Effect.Effect<
204 DatabaseConnectionWithSecrets,
205 PrismaApiError | PrismaApiDecodeError
206 >;
207 createDatabaseConnection(
208 databaseId: string,
209 input: DatabaseConnectionCreateInput,
210 ): Effect.Effect<
211 DatabaseConnectionWithSecrets,
212 PrismaApiError | PrismaApiDecodeError
213 >;
214 deleteConnection(
215 id: string,
216 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
217 rotateConnection(
218 id: string,
219 ): Effect.Effect<
220 DatabaseConnectionWithSecrets,
221 PrismaApiError | PrismaApiDecodeError
222 >;
223 listBranches(
224 projectId: string,
225 query?: BranchListQuery,
226 ): Effect.Effect<Branch[], PrismaApiError | PrismaApiDecodeError>;
227 getBranch(
228 id: string,
229 ): Effect.Effect<Branch, PrismaApiError | PrismaApiDecodeError>;
230 createBranch(
231 projectId: string,
232 input: BranchCreateInput,
233 ): Effect.Effect<Branch, PrismaApiError | PrismaApiDecodeError>;
234 updateBranch(
235 id: string,
236 input: BranchUpdateInput,
237 ): Effect.Effect<Branch, PrismaApiError | PrismaApiDecodeError>;
238 deleteBranch(
239 id: string,
240 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
241 listBuckets(
242 query?: BucketListQuery,
243 ): Effect.Effect<Bucket[], PrismaApiError | PrismaApiDecodeError>;
244 getBucket(
245 id: string,
246 ): Effect.Effect<Bucket, PrismaApiError | PrismaApiDecodeError>;
247 createBucket(
248 input: BucketCreateInput,
249 ): Effect.Effect<Bucket, PrismaApiError | PrismaApiDecodeError>;
250 deleteBucket(
251 id: string,
252 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
253 listBucketKeys(
254 bucketId: string,
255 query?: PaginationQuery,
256 ): Effect.Effect<BucketKey[], PrismaApiError | PrismaApiDecodeError>;
257 createBucketKey(
258 bucketId: string,
259 input: BucketKeyCreateInput,
260 ): Effect.Effect<BucketKeyWithSecret, PrismaApiError | PrismaApiDecodeError>;
261 deleteBucketKey(
262 bucketId: string,
263 keyId: string,
264 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
265 getCustomDomain(
266 id: string,
267 ): Effect.Effect<CustomDomain, PrismaApiError | PrismaApiDecodeError>;
268 deleteCustomDomain(
269 id: string,
270 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
271 retryCustomDomain(
272 id: string,
273 ): Effect.Effect<CustomDomain, PrismaApiError | PrismaApiDecodeError>;
274 listApps(
275 query?: AppListQuery,
276 ): Effect.Effect<App[], PrismaApiError | PrismaApiDecodeError>;
277 getApp(id: string): Effect.Effect<App, PrismaApiError | PrismaApiDecodeError>;
278 createApp(
279 input: AppCreateInput,
280 ): Effect.Effect<App, PrismaApiError | PrismaApiDecodeError>;
281 updateApp(
282 id: string,
283 input: AppUpdateInput,
284 ): Effect.Effect<App, PrismaApiError | PrismaApiDecodeError>;
285 deleteApp(
286 id: string,
287 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
288 promoteApp(
289 id: string,
290 target: AppDeploymentTarget,
291 ): Effect.Effect<PromoteAppResult, PrismaApiError | PrismaApiDecodeError>;
292 rollbackApp(
293 id: string,
294 target: AppDeploymentTarget,
295 ): Effect.Effect<RollbackAppResult, PrismaApiError | PrismaApiDecodeError>;
296 listAppDomains(
297 appId: string,
298 ): Effect.Effect<CustomDomain[], PrismaApiError | PrismaApiDecodeError>;
299 createAppDomain(
300 appId: string,
301 input: CustomDomainCreateInput,
302 ): Effect.Effect<
303 CustomDomainCreateResult,
304 PrismaApiError | PrismaApiDecodeError
305 >;
306 listAppDeployments(
307 appId: string,
308 query?: PaginationQuery,
309 ): Effect.Effect<DeploymentListItem[], PrismaApiError | PrismaApiDecodeError>;
310 createAppDeployment(
311 appId: string,
312 input?: DeploymentCreateInput,
313 ): Effect.Effect<
314 DeploymentCreateResult,
315 PrismaApiError | PrismaApiDecodeError
316 >;
317 getDeployment(
318 id: string,
319 ): Effect.Effect<Deployment, PrismaApiError | PrismaApiDecodeError>;
320 deleteDeployment(
321 id: string,
322 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
323 startDeployment(
324 id: string,
325 ): Effect.Effect<
326 StartDeploymentResult,
327 PrismaApiError | PrismaApiDecodeError
328 >;
329 stopDeployment(
330 id: string,
331 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
332 getDeploymentLogsRequest(
333 id: string,
334 query?: DeploymentLogsQuery,
335 ): Effect.Effect<DeploymentLogsRequest, PrismaApiError>;
336 getBuildLogsRequest(
337 buildId: string,
338 query?: BuildLogsQuery,
339 ): Effect.Effect<BuildLogsRequest, PrismaApiError>;
340 listEnvironmentVariables(
341 query?: EnvironmentVariableListQuery,
342 ): Effect.Effect<
343 EnvironmentVariable[],
344 PrismaApiError | PrismaApiDecodeError
345 >;
346 getEnvironmentVariable(
347 id: string,
348 ): Effect.Effect<EnvironmentVariable, PrismaApiError | PrismaApiDecodeError>;
349 createEnvironmentVariable(
350 input: EnvironmentVariableCreateInput,
351 ): Effect.Effect<EnvironmentVariable, PrismaApiError | PrismaApiDecodeError>;
352 updateEnvironmentVariable(
353 id: string,
354 input: EnvironmentVariableUpdateInput,
355 ): Effect.Effect<EnvironmentVariable, PrismaApiError | PrismaApiDecodeError>;
356 deleteEnvironmentVariable(
357 id: string,
358 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
359 listIntegrations(
360 query: IntegrationListQuery,
361 ): Effect.Effect<Integration[], PrismaApiError | PrismaApiDecodeError>;
362 listWorkspaceIntegrations(
363 workspaceId: string,
364 query?: PaginationQuery,
365 ): Effect.Effect<Integration[], PrismaApiError | PrismaApiDecodeError>;
366 getIntegration(
367 id: string,
368 ): Effect.Effect<Integration, PrismaApiError | PrismaApiDecodeError>;
369 deleteIntegration(
370 id: string,
371 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
372 revokeWorkspaceIntegration(
373 workspaceId: string,
374 clientId: string,
375 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
376 listScmInstallations(query: {
377 workspaceId: string;
378 cursor?: string | null;
379 limit?: number;
380 }): Effect.Effect<ScmInstallation[], PrismaApiError | PrismaApiDecodeError>;
381 createScmInstallIntent(
382 input: ScmInstallIntentCreateInput,
383 ): Effect.Effect<ScmInstallIntent, PrismaApiError | PrismaApiDecodeError>;
384 listScmInstallationRepositories(
385 installationId: string,
386 query?: PaginationQuery,
387 ): Effect.Effect<ScmRepository[], PrismaApiError | PrismaApiDecodeError>;
388 listSourceRepositories(
389 query: SourceRepositoryListQuery,
390 ): Effect.Effect<SourceRepository[], PrismaApiError | PrismaApiDecodeError>;
391 getSourceRepository(
392 id: string,
393 ): Effect.Effect<SourceRepository, PrismaApiError | PrismaApiDecodeError>;
394 createSourceRepository(
395 input: SourceRepositoryCreateInput,
396 ): Effect.Effect<SourceRepository, PrismaApiError | PrismaApiDecodeError>;
397 deleteSourceRepository(
398 id: string,
399 ): Effect.Effect<void, PrismaApiError | PrismaApiDecodeError>;
400}
401
402export class PrismaClient extends Context.Service<
403 PrismaClient,
404 PrismaManagementClient
405>()("Prisma::PrismaClient") {}
406
407export interface PaginationQuery {
408 cursor?: string | null;
409 limit?: number;
410}
411
412export interface BackupListQuery {
413 limit?: number;
414}
415
416export interface DatabaseUsageQuery {
417 startDate?: string;
418 endDate?: string;
419}
420
421export interface RegionListQuery {
422 product?: "postgres" | "accelerate";
423}
424
425export interface DatabaseListQuery extends PaginationQuery {
426 projectId?: string;
427 branchId?: PrismaBranchIdFilter;
428 branchGitName?: string;
429}
430
431export interface ConnectionListQuery extends PaginationQuery {
432 databaseId?: string;
433}
434
435export interface BranchListQuery extends PaginationQuery {
436 gitName?: string;
437 gitNameContains?: string;
438}
439
440export interface BucketListQuery extends PaginationQuery {
441 projectId?: string;
442 branchId?: PrismaBranchIdFilter;
443 branchGitName?: string;
444}
445
446export interface AppListQuery extends PaginationQuery {
447 projectId?: string;
448 branchId?: PrismaBranchIdFilter;
449 branchGitName?: string;
450}
451
452export type {
453 BuildLogsQuery,
454 BuildLogsRequest,
455 DeploymentLogsQuery,
456 DeploymentLogsRequest,
457};
458
459export interface EnvironmentVariableListQuery extends PaginationQuery {
460 projectId?: string;
461 class?: "production" | "preview";
462 key?: string;
463 branchId?: string;
464}
465
466export interface IntegrationListQuery extends PaginationQuery {
467 workspaceId: string;
468}
469
470export interface SourceRepositoryListQuery extends PaginationQuery {
471 projectId: string;
472}
473
474/** Response from the flat `POST /v1/databases` operation. */
475export interface DatabaseCreateResult extends Omit<Database, "connections"> {
476 connections: DatabaseConnectionWithOptionalSecrets[];
477}
478
479/** Response from `POST /v1/projects/{projectId}/databases`. */
480export interface ProjectDatabaseCreateResult extends Omit<
481 DatabaseCreateResult,
482 "region" | "status"
483> {
484 status: "provisioning" | "ready";
485 region: { id: string; name: string };
486}
487
488export interface ProjectCreateDatabaseResult extends Omit<
489 ProjectDatabaseCreateResult,
490 "project"
491> {}
492
493export interface ProjectCreateResult extends Project {
494 database: ProjectCreateDatabaseResult | null;
495}
496
497export const isNotFound = (error: unknown): boolean =>
498 error instanceof PrismaApiError && error.status === 404;
499
500export const isConflict = (error: unknown): boolean =>
501 error instanceof PrismaApiError && error.status === 409;
502
503const redactedString = (
504 value: unknown,
505): Redacted.Redacted<string> | undefined =>
506 typeof value === "string" ? Redacted.make(value) : undefined;
507
508const decodeUrlComponent = (value: string) => {
509 try {
510 return decodeURIComponent(value);
511 } catch {
512 return value;
513 }
514};
515
516export const extractConnectionSecrets = (
517 connection: ObservedConnection | undefined | null,
518): PrismaSecretConnection => {
519 if (!connection) return {};
520 const direct = secretValue(connection.endpoints?.direct?.connectionString);
521 const pooled = secretValue(connection.endpoints?.pooled?.connectionString);
522 const accelerate = secretValue(
523 connection.endpoints?.accelerate?.connectionString,
524 );
525 const directUrl = (() => {
526 if (!direct) return undefined;
527 try {
528 return new URL(direct);
529 } catch {
530 return undefined;
531 }
532 })();
533 return {
534 directConnectionString: redactedString(direct),
535 pooledConnectionString: redactedString(pooled),
536 accelerateConnectionString: redactedString(accelerate),
537 host: directUrl?.hostname ?? connection.endpoints?.direct?.host ?? null,
538 user: directUrl?.username ? decodeUrlComponent(directUrl.username) : null,
539 password: directUrl?.password
540 ? redactedString(decodeUrlComponent(directUrl.password))
541 : undefined,
542 };
543};
544
545export const requestBody = <T>(response: DataResponse<T>): T => response.data;
546
547const isRetryableStatus = (status: number) =>
548 status === 0 || status === 408 || status === 429 || status >= 500;
549
550const REQUEST_AND_BODY_TIMEOUT = "10 seconds" as const;
551const MAX_SUCCESS_BODY_BYTES = 4 * 1024 * 1024;
552const MAX_ERROR_BODY_BYTES = 64 * 1024;
553const MAX_PAGINATION_PAGES = 1_000;
554const MAX_PAGINATION_ITEMS = 100_000;
555const MAX_PAGINATION_BODY_BYTES = 64 * 1024 * 1024;
556const PAGINATION_TIMEOUT = "2 minutes" as const;
557const ROLLBACK_REQUEST_TIMEOUT = "30 seconds" as const;
558const PROVISIONING_REQUEST_TIMEOUT = "2 minutes" as const;
559const INVALID_PATH_SEGMENT = "__alchemy_invalid_prisma_path_segment__";
560
561const pathSegment = (value: string): string => {
562 if (
563 value.length === 0 ||
564 value === "." ||
565 value === ".." ||
566 value === INVALID_PATH_SEGMENT ||
567 /[\\/?#%]/.test(value) ||
568 value.includes("\0")
569 ) {
570 return INVALID_PATH_SEGMENT;
571 }
572 try {
573 return encodeURIComponent(value);
574 } catch {
575 return INVALID_PATH_SEGMENT;
576 }
577};
578
579const isRetryablePost = (path: string) =>
580 path.endsWith("/start") ||
581 path.endsWith("/stop") ||
582 path.endsWith("/promote") ||
583 path.endsWith("/rollback");
584
585const isRetryableRequest = (method: Method, path: string) =>
586 method === "GET" ||
587 method === "DELETE" ||
588 method === "PATCH" ||
589 (method === "POST" && isRetryablePost(path));
590
591const retryTransient = <A, E, R>(
592 method: Method,
593 path: string,
594 effect: Effect.Effect<A, E, R>,
595) =>
596 effect.pipe(
597 Effect.retry({
598 while: (e) =>
599 e instanceof PrismaApiError &&
600 isRetryableRequest(method, path) &&
601 isRetryableStatus(e.status),
602 schedule: Schedule.max([
603 Schedule.exponential("100 millis"),
604 Schedule.recurs(4),
605 ]),
606 }),
607 );
608
609function makePrismaClient(): Effect.Effect<
610 PrismaManagementClient,
611 never,
612 PrismaEnvironment | HttpClient.HttpClient
613> {
614 return Effect.gen(function* () {
615 const env = yield* PrismaEnvironment;
616 const http = yield* HttpClient.HttpClient;
617
618 function isValidApiPath(path: string): boolean {
619 if (
620 (path !== "/v1" && !path.startsWith("/v1/")) ||
621 path.startsWith("//") ||
622 path.includes("\\") ||
623 path.includes("?") ||
624 path.includes("#") ||
625 path.includes(INVALID_PATH_SEGMENT)
626 ) {
627 return false;
628 }
629 try {
630 const base = new URL(env.baseUrl);
631 const resolved = new URL(path, base);
632 return (
633 resolved.origin === base.origin &&
634 (resolved.pathname === "/v1" || resolved.pathname.startsWith("/v1/"))
635 );
636 } catch {
637 return false;
638 }
639 }
640
641 const buildUrl = (
642 path: string,
643 query?: object,
644 ): Effect.Effect<string, PrismaApiError> => {
645 if (!isValidApiPath(path)) {
646 return Effect.fail(
647 new PrismaApiError({
648 method: "GET",
649 path,
650 status: 0,
651 message: "Refused an invalid Prisma Management API path parameter",
652 }),
653 );
654 }
655 return Effect.sync(() => {
656 const url = new URL(path, env.baseUrl);
657 for (const [key, value] of Object.entries(query ?? {})) {
658 if (value === undefined || value === null) continue;
659 if (Array.isArray(value)) {
660 for (const item of value) {
661 url.searchParams.append(key, String(item));
662 }
663 } else {
664 url.searchParams.set(key, String(value));
665 }
666 }
667 return url.toString();
668 });
669 };
670
671 const buildWebSocketUrl = (path: string, query?: object) =>
672 buildUrl(path, query).pipe(
673 Effect.map((value) => {
674 const url = new URL(value);
675 if (url.protocol === "https:") url.protocol = "wss:";
676 if (url.protocol === "http:") url.protocol = "ws:";
677 return url.toString();
678 }),
679 );
680
681 const logsQuery = (
682 query: DeploymentLogsQuery | undefined,
683 ): Record<string, unknown> | undefined => {
684 if (!query) return undefined;
685 const { fromStart, ...rest } = query;
686 return {
687 ...rest,
688 from_start: fromStart === undefined ? undefined : String(fromStart),
689 };
690 };
691
692 const makeRequest = (
693 method: Method,
694 url: string,
695 options: RequestOptions | undefined,
696 ) => {
697 const init =
698 method === "GET"
699 ? HttpClientRequest.get(url)
700 : method === "POST"
701 ? HttpClientRequest.post(url)
702 : method === "PATCH"
703 ? HttpClientRequest.patch(url)
704 : HttpClientRequest.delete(url);
705 const request = init.pipe(
706 HttpClientRequest.bearerToken(Redacted.value(env.serviceToken)),
707 HttpClientRequest.setHeaders({
708 Accept: "application/json",
709 "User-Agent": "alchemy-prisma/1.0",
710 ...options?.headers,
711 }),
712 );
713 return options?.body === undefined
714 ? request
715 : request.pipe(HttpClientRequest.bodyJsonUnsafe(options.body));
716 };
717
718 const requestWithStatus = <T>(
719 method: Method,
720 path: string,
721 options?: RequestOptions,
722 ): Effect.Effect<
723 { status: number; body: T },
724 PrismaApiError | PrismaApiDecodeError
725 > => {
726 if (!isValidApiPath(path)) {
727 return Effect.fail(
728 new PrismaApiError({
729 method,
730 path,
731 status: 0,
732 message:
733 "Refused an invalid Prisma Management API path or path parameter outside the configured /v1 origin",
734 }),
735 );
736 }
737 return retryTransient(
738 method,
739 path,
740 Effect.gen(function* () {
741 const url = yield* buildUrl(path, options?.query);
742 const req = makeRequest(method, url, options);
743 const response = Effect.gen(function* () {
744 const res = yield* http.execute(req).pipe(
745 Effect.mapError(
746 (e) =>
747 new PrismaApiError({
748 method,
749 path,
750 status: 0,
751 message: e instanceof Error ? e.message : String(e),
752 }),
753 ),
754 );
755
756 const maxBodyBytes =
757 res.status >= 200 && res.status < 300
758 ? MAX_SUCCESS_BODY_BYTES
759 : MAX_ERROR_BODY_BYTES;
760 const bodyTooLarge = (bodyLength: number) =>
761 res.status >= 200 && res.status < 300
762 ? new PrismaApiDecodeError({
763 method,
764 path,
765 bodyLength,
766 message: `Prisma Management API response exceeded the ${maxBodyBytes} byte safety limit`,
767 })
768 : new PrismaApiError({
769 method,
770 path,
771 status: res.status,
772 message: `Prisma Management API error response exceeded the ${maxBodyBytes} byte safety limit`,
773 });
774 const declaredLength = Number(res.headers["content-length"]);
775 if (
776 Number.isSafeInteger(declaredLength) &&
777 declaredLength > maxBodyBytes
778 ) {
779 return yield* bodyTooLarge(declaredLength);
780 }
781
782 const collected = yield* Stream.runFoldEffect(
783 res.stream,
784 () => ({ chunks: [] as Uint8Array[], bytes: 0 }),
785 (state, chunk) => {
786 const bytes = state.bytes + chunk.byteLength;
787 if (bytes > maxBodyBytes) {
788 return Effect.fail(bodyTooLarge(bytes));
789 }
790 state.chunks.push(chunk);
791 state.bytes = bytes;
792 return Effect.succeed(state);
793 },
794 ).pipe(
795 Effect.catch((error) => {
796 if (
797 error instanceof PrismaApiError ||
798 error instanceof PrismaApiDecodeError
799 ) {
800 return Effect.fail(error);
801 }
802 if (
803 HttpClientError.isHttpClientError(error) &&
804 error.reason._tag === "EmptyBodyError"
805 ) {
806 return Effect.succeed({
807 chunks: [] as Uint8Array[],
808 bytes: 0,
809 });
810 }
811 return Effect.fail(
812 new PrismaApiError({
813 method,
814 path,
815 status: 0,
816 message: `Failed to read Prisma Management API response: ${
817 error instanceof Error ? error.message : String(error)
818 }`,
819 }),
820 );
821 }),
822 );
823 const bytes = new Uint8Array(collected.bytes);
824 let offset = 0;
825 for (const chunk of collected.chunks) {
826 bytes.set(chunk, offset);
827 offset += chunk.byteLength;
828 }
829 const text = new TextDecoder().decode(bytes);
830 return { status: res.status, text, bodyLength: collected.bytes };
831 });
832 const { status, text, bodyLength } = yield* response;
833 if (status < 200 || status >= 300) {
834 return yield* new PrismaApiError({
835 method,
836 path,
837 status,
838 message: formatErrorMessage(status, text),
839 body: text.length === 0 ? undefined : Redacted.make(text),
840 });
841 }
842 if (options?.onSuccessfulBodyLength !== undefined) {
843 yield* options.onSuccessfulBodyLength(bodyLength);
844 }
845 if (status === 204 || text.length === 0) {
846 return { status, body: undefined as T };
847 }
848 const body = yield* Effect.try({
849 try: () => JSON.parse(text) as T,
850 catch: () =>
851 new PrismaApiDecodeError({
852 method,
853 path,
854 bodyLength: new TextEncoder().encode(text).byteLength,
855 message: "Prisma Management API returned invalid JSON",
856 }),
857 });
858 return { status, body };
859 }),
860 ).pipe(
861 // The deadline covers the complete operation, including transient
862 // retries and streaming the response body. A per-attempt timeout would
863 // multiply the advertised deadline and can leave a hung peer tying up
864 // a deployment for every retry slot.
865 Effect.timeoutOrElse({
866 duration: options?.timeout ?? REQUEST_AND_BODY_TIMEOUT,
867 orElse: () =>
868 Effect.fail(
869 new PrismaApiError({
870 method,
871 path,
872 status: 0,
873 message:
874 options?.timeout === undefined
875 ? `Prisma Management API request timed out after ${REQUEST_AND_BODY_TIMEOUT}`
876 : "Prisma Management API request timed out at the operation-specific deadline",
877 }),
878 ),
879 }),
880 );
881 };
882
883 const request = <T>(
884 method: Method,
885 path: string,
886 options?: RequestOptions,
887 ): Effect.Effect<T, PrismaApiError | PrismaApiDecodeError> =>
888 requestWithStatus<T>(method, path, options).pipe(
889 Effect.map(({ body }) => body),
890 );
891
892 const dataWithStatus = <T>(
893 method: Method,
894 path: string,
895 options?: RequestOptions,
896 ) =>
897 requestWithStatus<unknown>(method, path, options).pipe(
898 Effect.flatMap(({ status, body: response }) =>
899 response !== null &&
900 typeof response === "object" &&
901 Object.hasOwn(response, "data")
902 ? Effect.succeed({
903 status,
904 data: requestBody(response as DataResponse<T>),
905 })
906 : Effect.fail(
907 new PrismaApiDecodeError({
908 method,
909 path,
910 bodyLength: 0,
911 message:
912 "Prisma Management API response did not contain a data envelope",
913 }),
914 ),
915 ),
916 );
917
918 const data = <T>(method: Method, path: string, options?: RequestOptions) =>
919 dataWithStatus<T>(method, path, options).pipe(
920 Effect.map(({ data }) => data),
921 );
922
923 const list = <T>(
924 path: string,
925 query?: object,
926 onSuccessfulBodyLength?: RequestOptions["onSuccessfulBodyLength"],
927 ) =>
928 request<ListResponse<T>>("GET", path, {
929 query,
930 onSuccessfulBodyLength,
931 });
932
933 const paginate = <T>(path: string, query?: object) =>
934 Effect.suspend(() => {
935 let observedPages = 0;
936 let observedItems = 0;
937 let observedBodyBytes = 0;
938 let observedCursor = false;
939 return Effect.gen(function* () {
940 const items: T[] = [];
941 const queryRecord = query as { cursor?: unknown } | undefined;
942 let cursor =
943 typeof queryRecord?.cursor === "string"
944 ? queryRecord.cursor
945 : undefined;
946 const seenCursors = new Set<string>();
947 if (cursor !== undefined) seenCursors.add(cursor);
948 observedCursor = cursor !== undefined;
949
950 const protocolError = (message: string) =>
951 new PrismaApiDecodeError({
952 method: "GET",
953 path,
954 bodyLength: 0,
955 message: `Invalid Prisma Management API pagination response: ${message}`,
956 });
957 const accountForBody = (bodyLength: number) => {
958 const aggregateBodyLength = observedBodyBytes + bodyLength;
959 if (aggregateBodyLength > MAX_PAGINATION_BODY_BYTES) {
960 return Effect.fail(
961 new PrismaApiDecodeError({
962 method: "GET",
963 path,
964 bodyLength: aggregateBodyLength,
965 message: `Invalid Prisma Management API pagination response: exceeded the ${MAX_PAGINATION_BODY_BYTES} aggregate response byte safety limit`,
966 }),
967 );
968 }
969 observedBodyBytes = aggregateBodyLength;
970 return Effect.void;
971 };
972
973 while (true) {
974 if (observedPages >= MAX_PAGINATION_PAGES) {
975 return yield* protocolError(
976 `exceeded the ${MAX_PAGINATION_PAGES} page safety limit`,
977 );
978 }
979 const page = yield* list<T>(
980 path,
981 { ...query, cursor },
982 accountForBody,
983 );
984 observedPages += 1;
985
986 if (
987 page === null ||
988 typeof page !== "object" ||
989 !Array.isArray(page.data) ||
990 page.pagination === null ||
991 typeof page.pagination !== "object" ||
992 typeof page.pagination.hasMore !== "boolean"
993 ) {
994 return yield* protocolError(
995 "expected data[] and pagination.hasMore",
996 );
997 }
998 if (page.data.length > MAX_PAGINATION_ITEMS - items.length) {
999 return yield* protocolError(
1000 `exceeded the ${MAX_PAGINATION_ITEMS} item safety limit`,
1001 );
1002 }
1003 items.push(...page.data);
1004 observedItems = items.length;
1005
1006 if (!page.pagination.hasMore) return items;
1007 const nextCursor = page.pagination.nextCursor;
1008 if (typeof nextCursor !== "string" || nextCursor.length === 0) {
1009 return yield* protocolError(
1010 "hasMore was true without a non-empty nextCursor",
1011 );
1012 }
1013 if (seenCursors.has(nextCursor)) {
1014 return yield* protocolError("nextCursor repeated");
1015 }
1016 seenCursors.add(nextCursor);
1017 cursor = nextCursor;
1018 observedCursor = true;
1019 }
1020 }).pipe(
1021 Effect.timeoutOrElse({
1022 duration: PAGINATION_TIMEOUT,
1023 orElse: () =>
1024 Effect.fail(
1025 new PrismaApiError({
1026 method: "GET",
1027 path,
1028 status: 0,
1029 message: `Prisma Management API pagination timed out after ${PAGINATION_TIMEOUT} (${observedPages} pages, ${observedItems} items, ${observedBodyBytes} response bytes, cursor ${observedCursor ? "present" : "absent"})`,
1030 }),
1031 ),
1032 }),
1033 );
1034 });
1035
1036 const service = {
1037 listWorkspaces: (query) => paginate<Workspace>("/v1/workspaces", query),
1038 getWorkspace: (id) =>
1039 data<Workspace>("GET", `/v1/workspaces/${pathSegment(id)}`),
1040 getCurrentPrincipal: () => data<CurrentPrincipal>("GET", "/v1/me"),
1041 listRegions: (query) => data<Region[]>("GET", "/v1/regions", { query }),
1042 listPostgresRegions: () =>
1043 data<PostgresRegion[]>("GET", "/v1/regions/postgres"),
1044 listAccelerateRegions: () =>
1045 data<AccelerateRegion[]>("GET", "/v1/regions/accelerate"),
1046
1047 listProjects: (query) => paginate<Project>("/v1/projects", query),
1048 getProject: (id) =>
1049 data<Project>("GET", `/v1/projects/${pathSegment(id)}`),
1050 createProject: (input) =>
1051 data<ProjectCreateResult>("POST", "/v1/projects", {
1052 body: input,
1053 timeout: PROVISIONING_REQUEST_TIMEOUT,
1054 }),
1055 updateProject: (id, input) =>
1056 data<Project>("PATCH", `/v1/projects/${pathSegment(id)}`, {
1057 body: input,
1058 }),
1059 deleteProject: (id) =>
1060 request<void>("DELETE", `/v1/projects/${pathSegment(id)}`, {
1061 timeout: PROVISIONING_REQUEST_TIMEOUT,
1062 }),
1063 transferProject: (id, input) =>
1064 request<void>("POST", `/v1/projects/${pathSegment(id)}/transfer`, {
1065 body: input,
1066 }),
1067
1068 listDatabases: (query) => paginate<Database>("/v1/databases", query),
1069 listProjectDatabases: (projectId, query) =>
1070 paginate<Database>(
1071 `/v1/projects/${pathSegment(projectId)}/databases`,
1072 query,
1073 ),
1074 getDatabase: (id) =>
1075 data<Database>("GET", `/v1/databases/${pathSegment(id)}`),
1076 createDatabase: (input) =>
1077 data<DatabaseCreateResult>("POST", "/v1/databases", {
1078 body: input,
1079 timeout: PROVISIONING_REQUEST_TIMEOUT,
1080 }),
1081 createProjectDatabase: (projectId, input) =>
1082 data<ProjectDatabaseCreateResult>(
1083 "POST",
1084 `/v1/projects/${pathSegment(projectId)}/databases`,
1085 { body: input, timeout: PROVISIONING_REQUEST_TIMEOUT },
1086 ),
1087 updateDatabase: (id, input) =>
1088 data<Database>("PATCH", `/v1/databases/${pathSegment(id)}`, {
1089 body: input,
1090 }),
1091 deleteDatabase: (id) =>
1092 request<void>("DELETE", `/v1/databases/${pathSegment(id)}`, {
1093 timeout: PROVISIONING_REQUEST_TIMEOUT,
1094 }),
1095 listBackups: (databaseId, query) =>
1096 request<BackupListResponse>(
1097 "GET",
1098 `/v1/databases/${pathSegment(databaseId)}/backups`,
1099 { query },
1100 ),
1101 restoreDatabase: (targetDatabaseId, input) =>
1102 data<RestoredDatabase>(
1103 "POST",
1104 `/v1/databases/${pathSegment(targetDatabaseId)}/restore`,
1105 { body: input, timeout: PROVISIONING_REQUEST_TIMEOUT },
1106 ),
1107 getDatabaseUsage: (databaseId, query) =>
1108 request<DatabaseUsage>(
1109 "GET",
1110 `/v1/databases/${pathSegment(databaseId)}/usage`,
1111 { query },
1112 ),
1113
1114 listConnections: (query) =>
1115 paginate<DatabaseConnection>("/v1/connections", query),
1116 listDatabaseConnections: (databaseId, query) =>
1117 paginate<DatabaseConnection>(
1118 `/v1/databases/${pathSegment(databaseId)}/connections`,
1119 query,
1120 ),
1121 getConnection: (id) =>
1122 data<DatabaseConnection>("GET", `/v1/connections/${pathSegment(id)}`),
1123 createConnection: (input) =>
1124 data<DatabaseConnectionWithSecrets>("POST", "/v1/connections", {
1125 body: input,
1126 }),
1127 createDatabaseConnection: (databaseId, input) =>
1128 data<DatabaseConnectionWithSecrets>(
1129 "POST",
1130 `/v1/databases/${pathSegment(databaseId)}/connections`,
1131 { body: input },
1132 ),
1133 deleteConnection: (id) =>
1134 request<void>("DELETE", `/v1/connections/${pathSegment(id)}`),
1135 rotateConnection: (id) =>
1136 data<DatabaseConnectionWithSecrets>(
1137 "POST",
1138 `/v1/connections/${pathSegment(id)}/rotate`,
1139 ),
1140
1141 listBranches: (projectId, query) =>
1142 paginate<Branch>(
1143 `/v1/projects/${pathSegment(projectId)}/branches`,
1144 query,
1145 ),
1146 getBranch: (id) => data<Branch>("GET", `/v1/branches/${pathSegment(id)}`),
1147 createBranch: (projectId, input) =>
1148 data<Branch>(
1149 "POST",
1150 `/v1/projects/${pathSegment(projectId)}/branches`,
1151 {
1152 body: input,
1153 },
1154 ),
1155 updateBranch: (id, input) =>
1156 data<Branch>("PATCH", `/v1/branches/${pathSegment(id)}`, {
1157 body: input,
1158 }),
1159 deleteBranch: (id) =>
1160 request<void>("DELETE", `/v1/branches/${pathSegment(id)}`),
1161
1162 listBuckets: (query) => paginate<Bucket>("/v1/buckets", query),
1163 getBucket: (id) => data<Bucket>("GET", `/v1/buckets/${pathSegment(id)}`),
1164 createBucket: (input) =>
1165 data<Bucket>("POST", "/v1/buckets", { body: input }),
1166 deleteBucket: (id) =>
1167 request<void>("DELETE", `/v1/buckets/${pathSegment(id)}`),
1168 listBucketKeys: (bucketId, query) =>
1169 paginate<BucketKey>(`/v1/buckets/${pathSegment(bucketId)}/keys`, query),
1170 createBucketKey: (bucketId, input) =>
1171 data<BucketKeyWithSecret>(
1172 "POST",
1173 `/v1/buckets/${pathSegment(bucketId)}/keys`,
1174 { body: input },
1175 ),
1176 deleteBucketKey: (bucketId, keyId) =>
1177 request<void>(
1178 "DELETE",
1179 `/v1/buckets/${pathSegment(bucketId)}/keys/${pathSegment(keyId)}`,
1180 ),
1181
1182 getCustomDomain: (id) =>
1183 data<CustomDomain>("GET", `/v1/domains/${pathSegment(id)}`),
1184 deleteCustomDomain: (id) =>
1185 request<void>("DELETE", `/v1/domains/${pathSegment(id)}`),
1186 retryCustomDomain: (id) =>
1187 data<CustomDomain>("POST", `/v1/domains/${pathSegment(id)}/retry`),
1188
1189 listApps: (query) => paginate<App>("/v1/apps", query),
1190 getApp: (id) => data<App>("GET", `/v1/apps/${pathSegment(id)}`),
1191 createApp: (input) =>
1192 data<App>("POST", "/v1/apps", {
1193 body: input,
1194 timeout: PROVISIONING_REQUEST_TIMEOUT,
1195 }),
1196 updateApp: (id, input) =>
1197 data<App>("PATCH", `/v1/apps/${pathSegment(id)}`, { body: input }),
1198 deleteApp: (id) =>
1199 request<void>("DELETE", `/v1/apps/${pathSegment(id)}`, {
1200 timeout: PROVISIONING_REQUEST_TIMEOUT,
1201 }),
1202 promoteApp: (id, target) =>
1203 data<PromoteAppResult>("POST", `/v1/apps/${pathSegment(id)}/promote`, {
1204 body: target,
1205 }),
1206 rollbackApp: (id, target) =>
1207 data<RollbackAppResult>(
1208 "POST",
1209 `/v1/apps/${pathSegment(id)}/rollback`,
1210 {
1211 body: target,
1212 timeout: ROLLBACK_REQUEST_TIMEOUT,
1213 },
1214 ),
1215 listAppDomains: (appId) =>
1216 paginate<CustomDomain>(`/v1/apps/${pathSegment(appId)}/domains`),
1217 createAppDomain: (appId, input) => {
1218 const path = `/v1/apps/${pathSegment(appId)}/domains`;
1219 return dataWithStatus<CustomDomain>("POST", path, {
1220 body: input,
1221 }).pipe(
1222 Effect.flatMap(({ status, data: domain }) =>
1223 status === 200 || status === 201
1224 ? Effect.succeed({ status, domain })
1225 : Effect.fail(
1226 new PrismaApiDecodeError({
1227 method: "POST",
1228 path,
1229 bodyLength: 0,
1230 message: `Prisma Management API returned unexpected HTTP ${status} for custom-domain creation`,
1231 }),
1232 ),
1233 ),
1234 );
1235 },
1236 listAppDeployments: (appId, query) =>
1237 paginate<DeploymentListItem>(
1238 `/v1/apps/${pathSegment(appId)}/deployments`,
1239 query,
1240 ),
1241 createAppDeployment: (appId, input) =>
1242 data<DeploymentCreateResult>(
1243 "POST",
1244 `/v1/apps/${pathSegment(appId)}/deployments`,
1245 input === undefined ? undefined : { body: input },
1246 ),
1247 getDeployment: (id) =>
1248 data<Deployment>("GET", `/v1/deployments/${pathSegment(id)}`),
1249 deleteDeployment: (id) =>
1250 request<void>("DELETE", `/v1/deployments/${pathSegment(id)}`),
1251 startDeployment: (id) =>
1252 data<StartDeploymentResult>(
1253 "POST",
1254 `/v1/deployments/${pathSegment(id)}/start`,
1255 ),
1256 stopDeployment: (id) =>
1257 request<void>("POST", `/v1/deployments/${pathSegment(id)}/stop`),
1258 getDeploymentLogsRequest: (id, query) =>
1259 buildWebSocketUrl(
1260 `/v1/deployments/${pathSegment(id)}/logs`,
1261 logsQuery(query),
1262 ).pipe(
1263 Effect.map((url): DeploymentLogsRequest => ({
1264 url,
1265 headers: {
1266 Authorization: Redacted.make(
1267 `Bearer ${Redacted.value(env.serviceToken)}`,
1268 ),
1269 },
1270 })),
1271 ),
1272 getBuildLogsRequest: (buildId, query) =>
1273 buildUrl(`/v1/builds/${pathSegment(buildId)}/logs`, query).pipe(
1274 Effect.map((url): BuildLogsRequest => ({
1275 url,
1276 headers: {
1277 Authorization: Redacted.make(
1278 `Bearer ${Redacted.value(env.serviceToken)}`,
1279 ),
1280 Accept: "application/x-ndjson",
1281 },
1282 })),
1283 ),
1284
1285 listEnvironmentVariables: (query) =>
1286 paginate<EnvironmentVariable>("/v1/environment-variables", query),
1287 getEnvironmentVariable: (id) =>
1288 data<EnvironmentVariable>(
1289 "GET",
1290 `/v1/environment-variables/${pathSegment(id)}`,
1291 ),
1292 createEnvironmentVariable: (input) =>
1293 data<EnvironmentVariable>("POST", "/v1/environment-variables", {
1294 body: input,
1295 }),
1296 updateEnvironmentVariable: (id, input) =>
1297 data<EnvironmentVariable>(
1298 "PATCH",
1299 `/v1/environment-variables/${pathSegment(id)}`,
1300 { body: input },
1301 ),
1302 deleteEnvironmentVariable: (id) =>
1303 request<void>("DELETE", `/v1/environment-variables/${pathSegment(id)}`),
1304
1305 listIntegrations: (query) =>
1306 paginate<Integration>("/v1/integrations", query),
1307 listWorkspaceIntegrations: (workspaceId, query) =>
1308 paginate<Integration>(
1309 `/v1/workspaces/${pathSegment(workspaceId)}/integrations`,
1310 query,
1311 ),
1312 getIntegration: (id) =>
1313 data<Integration>("GET", `/v1/integrations/${pathSegment(id)}`),
1314 deleteIntegration: (id) =>
1315 request<void>("DELETE", `/v1/integrations/${pathSegment(id)}`),
1316 revokeWorkspaceIntegration: (workspaceId, clientId) =>
1317 request<void>(
1318 "DELETE",
1319 `/v1/workspaces/${pathSegment(workspaceId)}/integrations/${pathSegment(clientId)}`,
1320 ),
1321
1322 listScmInstallations: (query) =>
1323 paginate<ScmInstallation>("/v1/scm-installations", query),
1324 createScmInstallIntent: (input) =>
1325 data<ScmInstallIntent>(
1326 "POST",
1327 "/v1/scm-installations/install-intents",
1328 {
1329 body: input,
1330 },
1331 ),
1332 listScmInstallationRepositories: (installationId, query) =>
1333 paginate<ScmRepository>(
1334 `/v1/scm-installations/${pathSegment(installationId)}/repositories`,
1335 query,
1336 ),
1337
1338 listSourceRepositories: (query) =>
1339 paginate<SourceRepository>("/v1/source-repositories", query),
1340 getSourceRepository: (id) =>
1341 data<SourceRepository>(
1342 "GET",
1343 `/v1/source-repositories/${pathSegment(id)}`,
1344 ),
1345 createSourceRepository: (input) =>
1346 data<SourceRepository>("POST", "/v1/source-repositories", {
1347 body: input,
1348 }),
1349 deleteSourceRepository: (id) =>
1350 request<void>("DELETE", `/v1/source-repositories/${pathSegment(id)}`),
1351 } satisfies PrismaManagementClient;
1352 return service;
1353 });
1354}
1355
1356const SAFE_ERROR_CODE = /^[a-zA-Z0-9][a-zA-Z0-9:._-]{0,127}$/;
1357
1358const parseErrorCode = (body: string): string | undefined => {
1359 if (body.length === 0) return undefined;
1360 try {
1361 const json = JSON.parse(body) as {
1362 code?: unknown;
1363 error?: unknown;
1364 };
1365 const nested =
1366 json.error !== null && typeof json.error === "object"
1367 ? (json.error as { code?: unknown }).code
1368 : undefined;
1369 const code = json.code ?? nested;
1370 return typeof code === "string" && SAFE_ERROR_CODE.test(code)
1371 ? code
1372 : undefined;
1373 } catch {
1374 return undefined;
1375 }
1376};
1377
1378const formatErrorMessage = (status: number, body: string): string => {
1379 const code = parseErrorCode(body);
1380 return code === undefined
1381 ? `HTTP ${status}`
1382 : `Prisma Management API request failed (${code})`;
1383};
1384
1385export const PrismaClientLive = Layer.effect(PrismaClient, makePrismaClient());