1import {
2 type GetProjectDatabasesResponse,
3 type GetProjectsResponse,
4 getProjects,
5 getProject,
6 getProjectDatabases,
7 updateProject,
8 createProject,
9 createProjectDatabase,
10} from "@distilled.cloud/prisma/management";
11import { Retry } from "@distilled.cloud/prisma";
12import * as Effect from "effect/Effect";
13import { isResolved } from "../Diff.ts";
14import * as Redacted from "effect/Redacted";
15import * as Schedule from "effect/Schedule";
16import { Unowned } from "../AdoptPolicy.ts";
17import { createPhysicalName } from "../PhysicalName.ts";
18import * as Provider from "../Provider.ts";
19import { DEV_TIMESTAMP, devId, devProvider } from "./Internal/DevStub.ts";
20import * as ProviderLayer from "../Local/ProviderLayer.ts";
21import { Resource } from "../Resource.ts";
22import { extractConnectionSecrets } from "./Client.ts";
23import { destroyProjectApps } from "./ComputeLifecycle.ts";
24import {
25 hasCanonicalConnectionSecrets,
26 mergeConnectionSecrets,
27 recoverDatabaseConnectionSecrets,
28} from "./Internal/DatabaseSecrets.ts";
29import type { ObservedDatabase, ObservedProject } from "./Internal/Observed.ts";
30import { isInputObject, isPrismaDevId } from "./Refs.ts";
31import type { Providers } from "./Providers.ts";
32import type { PrismaSecretConnection, PrismaRegionId } from "./Types.ts";
33import { PrismaPaginationError } from "./Internal/Pagination.ts";
34
35export interface ProjectProps {
36 /**
37 * Project display name. If omitted, Alchemy generates a stable physical name.
38 */
39 name?: string;
40 /**
41 * Whether to create a default Prisma Postgres database with the project.
42 *
43 * @default true
44 */
45 createDatabase?: boolean;
46 /**
47 * Region for the default database created with the project.
48 *
49 * When `createDatabase` is false, omitting this leaves the project's default
50 * region unset.
51 *
52 * @default "us-east-1" when a default database is created
53 */
54 region?: PrismaRegionId;
55 /**
56 * Opaque project settings passed through to the Management API.
57 */
58 settings?: Record<string, unknown>;
59 /**
60 * Rotate the adopted default database connection to recover its one-time
61 * credentials. Prisma revokes the previous key on a best-effort basis and
62 * rotation may interrupt existing consumers, so adoption leaves credentials
63 * unset unless explicitly opted in.
64 *
65 * @default false
66 */
67 rotateCredentialsOnAdopt?: boolean;
68}
69
70export interface Project extends Resource<
71 "Prisma.Project",
72 ProjectProps,
73 {
74 /**
75 * Prisma project ID.
76 */
77 projectId: string;
78 /**
79 * Prisma project display name.
80 */
81 projectName: string;
82 /**
83 * Workspace ID that owns the project.
84 */
85 workspaceId: string;
86 /**
87 * ISO timestamp when the project was created.
88 */
89 createdAt: string;
90 /**
91 * Default Prisma Postgres region for the project, when available.
92 */
93 defaultRegion: string | null;
94 /**
95 * Default database ID created with or discovered for the project.
96 */
97 databaseId: string | undefined;
98 /**
99 * Default database connection ID, when a database exists.
100 */
101 defaultConnectionId: string | undefined;
102 /**
103 * Direct Postgres connection string, redacted in state.
104 */
105 directConnectionString: Redacted.Redacted<string> | undefined;
106 /**
107 * Pooled Postgres connection string, redacted in state.
108 */
109 pooledConnectionString: Redacted.Redacted<string> | undefined;
110 /**
111 * Accelerate connection string, redacted in state.
112 */
113 accelerateConnectionString: Redacted.Redacted<string> | undefined;
114 /**
115 * Direct database host, when returned by Prisma.
116 */
117 host: string | null | undefined;
118 /**
119 * Direct database username, when returned by Prisma.
120 */
121 user: string | null | undefined;
122 /**
123 * Direct database password, redacted in state.
124 */
125 password: Redacted.Redacted<string> | undefined;
126 },
127 never,
128 Providers
129> {}
130
131/**
132 * A Prisma project, optionally with a default Prisma Postgres database.
133 *
134 * A Project is the ownership boundary for its databases, branches, apps, and
135 * repository link. Destroying this resource deletes the project and its
136 * contained data. Set `createDatabase: false` when you want standalone
137 * `Prisma.Database` resources with independent lifecycles.
138 *
139 * ### Creating a Project
140 * **Example:** Project with a default database
141 * ```typescript
142 * const project = yield* Prisma.Project("app", {
143 * name: "app",
144 * region: "us-east-1",
145 * });
146 * ```
147 *
148 * **Example:** Project only
149 * ```typescript
150 * const project = yield* Prisma.Project("control-plane", {
151 * createDatabase: false,
152 * });
153 * ```
154 *
155 * @resource
156 * @product Project
157 */
158export const Project = Resource<Project>("Prisma.Project");
159
160const createName = (id: string, name: string | undefined) =>
161 name === undefined ? createPhysicalName({ id }) : Effect.succeed(name);
162
163// Distilled emits the Management API's cursor-paginated list operations as
164// plain ops, so callers walk `pagination` themselves — the same shape the Neon
165// provider uses (see `src/Neon/Project.ts` findProjectByName).
166const listProjects = () =>
167 Effect.gen(function* () {
168 const projects: GetProjectsResponse["data"][number][] = [];
169 let cursor: string | undefined;
170 while (true) {
171 const page = yield* getProjects(cursor === undefined ? {} : { cursor });
172 projects.push(...page.data);
173 const nextCursor = page.pagination.nextCursor;
174 if (!page.pagination.hasMore) break;
175 if (nextCursor === null) {
176 return yield* Effect.fail(
177 new PrismaPaginationError({
178 message:
179 "Invalid Prisma Management API pagination response from getProjects: hasMore was true without a non-empty nextCursor",
180 }),
181 );
182 }
183 cursor = nextCursor;
184 }
185 return projects;
186 });
187
188const findProjectByName = (name: string) =>
189 listProjects().pipe(
190 Effect.flatMap((projects) => {
191 const matches = projects.filter((p) => p.name === name);
192 return matches.length > 1
193 ? Effect.fail(
194 new Error(
195 `Multiple Prisma projects are named '${name}'; use a unique project name before managing it with Alchemy.`,
196 ),
197 )
198 : Effect.succeed(matches[0]);
199 }),
200 );
201
202class GeneratedProjectNotVisible extends Error {}
203
204const generatedProjectRecoverySchedule = Schedule.max([
205 Schedule.exponential("250 millis"),
206 Schedule.recurs(6),
207]);
208
209const recoverGeneratedProjectAfterConflict = (name: string) =>
210 findProjectByName(name).pipe(
211 Effect.flatMap((project) =>
212 project
213 ? Effect.succeed(project)
214 : Effect.fail(
215 new GeneratedProjectNotVisible(
216 `Generated Prisma project '${name}' already exists but is not visible yet.`,
217 ),
218 ),
219 ),
220 Effect.retry({
221 while: (error) => error instanceof GeneratedProjectNotVisible,
222 schedule: generatedProjectRecoverySchedule,
223 }),
224 );
225
226const listProjectDatabases = (projectId: string) =>
227 Effect.gen(function* () {
228 const databases: GetProjectDatabasesResponse["data"][number][] = [];
229 let cursor: string | undefined;
230 while (true) {
231 const page = yield* getProjectDatabases(
232 cursor === undefined
233 ? { projectId, limit: 100 }
234 : { projectId, limit: 100, cursor },
235 );
236 databases.push(...page.data);
237 const nextCursor = page.pagination.nextCursor;
238 if (!page.pagination.hasMore) break;
239 if (nextCursor === null) {
240 return yield* Effect.fail(
241 new PrismaPaginationError({
242 message:
243 "Invalid Prisma Management API pagination response from getProjectDatabases: hasMore was true without a non-empty nextCursor",
244 }),
245 );
246 }
247 cursor = nextCursor;
248 }
249 return databases;
250 });
251
252const defaultDatabase = (projectId: string) =>
253 listProjectDatabases(projectId).pipe(
254 Effect.flatMap((databases) => {
255 const matches = databases.filter((db) => db.isDefault);
256 return matches.length > 1
257 ? Effect.fail(
258 new Error(
259 `Prisma project '${projectId}' has multiple default databases; refusing to select one arbitrarily.`,
260 ),
261 )
262 : Effect.succeed(matches[0]);
263 }),
264 );
265
266class DefaultDatabaseConsistencyError extends Error {}
267
268const defaultDatabaseConsistencySchedule = Schedule.max([
269 Schedule.exponential("250 millis"),
270 Schedule.recurs(6),
271]);
272
273const requireDefaultDatabaseInRegion = (
274 database: ObservedDatabase | undefined,
275 projectName: string,
276 desiredRegion: string,
277) =>
278 database?.region?.id === desiredRegion
279 ? Effect.succeed(database)
280 : Effect.fail(
281 new DefaultDatabaseConsistencyError(
282 database
283 ? `Prisma project '${projectName}' still has default database '${database.id}' in region '${database.region?.id ?? "unknown"}', but region '${desiredRegion}' was requested. Retry after any in-progress default database promotion completes.`
284 : `Prisma project '${projectName}' does not expose the requested default database in region '${desiredRegion}'. Retry after any in-progress default database creation completes.`,
285 ),
286 );
287
288type ProjectDatabaseAttrs = ObservedDatabase;
289
290const observeDesiredDefaultDatabase = (
291 projectName: string,
292 projectId: string,
293 expectedDatabaseId: string,
294 desiredRegion: string,
295) =>
296 Effect.gen(function* () {
297 const project = (yield* getProject({ id: projectId }).pipe(
298 Effect.catchTag("NotFound", () =>
299 Effect.fail(
300 new DefaultDatabaseConsistencyError(
301 `Prisma project '${projectName}' (${projectId}) is not visible yet while verifying its new default database.`,
302 ),
303 ),
304 ),
305 )).data;
306 const database = yield* requireDefaultDatabaseInRegion(
307 yield* defaultDatabase(projectId).pipe(
308 Effect.catchTag("NotFound", () =>
309 Effect.fail(
310 new DefaultDatabaseConsistencyError(
311 `Prisma project '${projectName}' default database list is not visible yet.`,
312 ),
313 ),
314 ),
315 ),
316 projectName,
317 desiredRegion,
318 );
319 if (database.id !== expectedDatabaseId) {
320 return yield* Effect.fail(
321 new DefaultDatabaseConsistencyError(
322 `Prisma project '${projectName}' exposes default database '${database.id}', but newly created database '${expectedDatabaseId}' was expected. Retry after the in-progress default database promotion completes.`,
323 ),
324 );
325 }
326 if (project.defaultRegion !== desiredRegion) {
327 return yield* Effect.fail(
328 new DefaultDatabaseConsistencyError(
329 `Prisma project '${projectName}' still reports default region '${project.defaultRegion ?? "unknown"}', but region '${desiredRegion}' was requested. Retry after the in-progress default database promotion completes.`,
330 ),
331 );
332 }
333 return { project, database };
334 }).pipe(
335 Effect.retry({
336 while: (error) => error instanceof DefaultDatabaseConsistencyError,
337 schedule: defaultDatabaseConsistencySchedule,
338 }),
339 );
340
341const attrsFrom = (
342 project: ObservedProject,
343 database: ObservedDatabase | undefined,
344 secrets: PrismaSecretConnection,
345): Project["Attributes"] => ({
346 projectId: project.id,
347 projectName: project.name,
348 workspaceId: project.workspace.id,
349 createdAt: project.createdAt,
350 // The default database observation is authoritative. Project.defaultRegion
351 // may briefly lag after Prisma atomically promotes a replacement database.
352 defaultRegion: database?.region?.id ?? project.defaultRegion,
353 databaseId: database?.id,
354 defaultConnectionId: database?.defaultConnectionId ?? undefined,
355 directConnectionString: secrets.directConnectionString,
356 pooledConnectionString: secrets.pooledConnectionString,
357 accelerateConnectionString: secrets.accelerateConnectionString,
358 host: secrets.host,
359 user: secrets.user,
360 password: secrets.password,
361});
362
363const ProviderLive = () =>
364 Provider.effect(
365 Project,
366 Effect.gen(function* () {
367 return {
368 stables: ["projectId"],
369 list: Effect.fn(function* () {
370 const projects = yield* listProjects();
371 return yield* Effect.forEach(
372 projects,
373 Effect.fn(function* (project) {
374 const database = yield* defaultDatabase(project.id).pipe(
375 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
376 );
377 return attrsFrom(project, database, {});
378 }),
379 { concurrency: 8 },
380 );
381 }),
382 diff: Effect.fn(function* ({ id, olds = {}, news = {}, output }) {
383 if (isPrismaDevId(output?.projectId)) {
384 return { action: "update" } as const;
385 }
386 if (!isInputObject(news)) return undefined;
387 if (
388 isResolved(news.rotateCredentialsOnAdopt) &&
389 news.rotateCredentialsOnAdopt === true &&
390 olds.rotateCredentialsOnAdopt !== true
391 ) {
392 return { action: "update" } as const;
393 }
394 const desiredCreateDatabase = isResolved(news.createDatabase)
395 ? (news.createDatabase ?? true)
396 : undefined;
397 const desiredRegion = isResolved(news.region)
398 ? (news.region ?? "us-east-1")
399 : undefined;
400 const hadDatabase =
401 output?.databaseId !== undefined ||
402 (!output && (olds.createDatabase ?? true));
403
404 // The API cannot remove a project's last/default database. Moving
405 // from a database-bearing project to `createDatabase: false`
406 // therefore requires replacing the project itself.
407 if (desiredCreateDatabase === false && hadDatabase) {
408 // A replacement may coexist when its generated name changes with
409 // the instance id, or when this update also changes the explicit
410 // name. A stable explicit name collides with the old generation
411 // and must be removed before creating the new project.
412 const deleteFirst = isResolved(news.name)
413 ? news.name !== undefined &&
414 (!output || news.name === output.projectName)
415 : true;
416 return { action: "replace", deleteFirst } as const;
417 }
418
419 // Adding a missing default database and replacing only that database
420 // for a region change are both supported in-place. Preserve the
421 // project (and its apps, repository link, environment, and IDs).
422 if (
423 desiredCreateDatabase === true &&
424 output?.databaseId !== undefined &&
425 desiredRegion !== undefined &&
426 output.defaultRegion !== desiredRegion
427 ) {
428 return yield* Effect.fail(
429 new Error(
430 `Cannot safely change Prisma project '${output.projectName}' default database region from '${output.defaultRegion ?? "unknown"}' to '${desiredRegion}' in place. The Management API would create an empty default database and leave the data-bearing database to be deleted. Create a replacement project and perform an explicit data migration/cutover instead.`,
431 ),
432 );
433 }
434 if (
435 desiredCreateDatabase === true &&
436 (output?.databaseId === undefined ||
437 (olds.createDatabase ?? true) === false)
438 ) {
439 return { action: "update" } as const;
440 }
441 const updateProps = {
442 name: news.name,
443 settings: news.settings,
444 };
445 if (!isResolved(updateProps)) return undefined;
446 const resolvedUpdateProps = updateProps as Pick<
447 ProjectProps,
448 "name" | "settings"
449 >;
450 const nextName = yield* createName(id, resolvedUpdateProps.name);
451 const oldName =
452 output?.projectName ?? (yield* createName(id, olds.name));
453 if (
454 nextName !== oldName ||
455 // Settings are write-only in the Management API project
456 // response. Re-apply an explicit desired value every deploy so
457 // out-of-band changes converge, and write `{}` once when a
458 // previously managed settings prop is removed.
459 resolvedUpdateProps.settings !== undefined ||
460 (olds.settings !== undefined &&
461 resolvedUpdateProps.settings === undefined)
462 ) {
463 return { action: "update" } as const;
464 }
465 return undefined;
466 }),
467 read: Effect.fn(function* ({ id, output, olds = {} }) {
468 const projectId = isPrismaDevId(output?.projectId)
469 ? undefined
470 : output?.projectId;
471 const project = projectId
472 ? yield* getProject({ id: projectId }).pipe(
473 Effect.map((response) => response.data),
474 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
475 )
476 : yield* findProjectByName(yield* createName(id, olds.name));
477 if (!project) return undefined;
478 const database = yield* defaultDatabase(project.id).pipe(
479 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
480 );
481 const cachedSecrets =
482 output?.databaseId === database?.id ? output : undefined;
483 const attrs = attrsFrom(project, database, {
484 directConnectionString: cachedSecrets?.directConnectionString,
485 pooledConnectionString: cachedSecrets?.pooledConnectionString,
486 accelerateConnectionString:
487 cachedSecrets?.accelerateConnectionString,
488 host: cachedSecrets?.host,
489 user: cachedSecrets?.user,
490 password: cachedSecrets?.password,
491 });
492 // An omitted name is derived from this exact PlanScope instance ID.
493 // Finding it proves this is create-recovery, not a foreign natural-
494 // key match. User-supplied names still require explicit adoption.
495 return projectId === undefined && olds.name !== undefined
496 ? Unowned(attrs)
497 : attrs;
498 }),
499 reconcile: Effect.fn(function* ({ id, news = {}, olds, output }) {
500 const name = yield* createName(id, news.name);
501 const outputProjectId = isPrismaDevId(output?.projectId)
502 ? undefined
503 : output?.projectId;
504 let project: ObservedProject | undefined = outputProjectId
505 ? yield* getProject({ id: outputProjectId }).pipe(
506 Effect.map((response) => response.data),
507 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
508 )
509 : news.name === undefined
510 ? yield* findProjectByName(name)
511 : undefined;
512
513 let createdDatabase: ProjectDatabaseAttrs | undefined;
514 let secrets: PrismaSecretConnection = {};
515 let createdProject = false;
516 let recoverCreateSecrets = false;
517 if (!project) {
518 const result = yield* createProject({
519 name,
520 createDatabase: news.createDatabase ?? true,
521 region: news.region,
522 }).pipe(
523 // A replayed create would make a second project; the retry
524 // policy cannot see the request, so opt out explicitly.
525 Retry.none,
526 Effect.map((response) => {
527 const project: ObservedProject = response.data;
528 return {
529 project,
530 database: response.data.database ?? undefined,
531 secrets: extractConnectionSecrets(
532 response.data.database?.connections[0],
533 ),
534 created: true,
535 recoverSecrets: true,
536 };
537 }),
538 Effect.catchTag("Conflict", () =>
539 news.name === undefined
540 ? recoverGeneratedProjectAfterConflict(name).pipe(
541 Effect.map((project) => ({
542 project,
543 database: undefined,
544 secrets: {},
545 // The generated physical name is owned by this
546 // resource instance. Treat a conflict followed by an
547 // exact read as lost-response recovery so write-only
548 // default credentials are restored.
549 created: false,
550 recoverSecrets: true,
551 })),
552 )
553 : Effect.fail(
554 new Error(
555 `Prisma project '${name}' appeared after the adoption check. Refusing to take it over; rerun with adoption enabled if it is the intended project.`,
556 ),
557 ),
558 ),
559 );
560 project = result.project;
561 createdDatabase = result.database;
562 secrets = result.secrets;
563 createdProject = result.created;
564 recoverCreateSecrets = result.recoverSecrets;
565 }
566 if (!project) {
567 return yield* Effect.fail(
568 new Error(
569 `Prisma project '${name}' could not be observed after create recovery.`,
570 ),
571 );
572 }
573 const ownedGeneratedIdentity =
574 news.name === undefined && project.name === name;
575
576 if (news.createDatabase === false && createdDatabase) {
577 return yield* Effect.fail(
578 new Error(
579 `Prisma created unexpected default database '${createdDatabase.name}' (${createdDatabase.id}) for project '${project.name}' even though createDatabase was false. Refusing to persist a state the Management API cannot remove in place.`,
580 ),
581 );
582 }
583
584 if (news.createDatabase === false && !createdProject) {
585 const existingDefault = yield* defaultDatabase(project.id).pipe(
586 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
587 );
588 if (existingDefault) {
589 return yield* Effect.fail(
590 new Error(
591 `Prisma project '${project.name}' already owns default database '${existingDefault.name}' (${existingDefault.id}), which cannot be removed in place. Refusing to adopt or reconcile it as createDatabase: false; replace the Prisma.Project instead.`,
592 ),
593 );
594 }
595 }
596
597 // Project reads do not expose settings. Whenever settings are
598 // explicitly managed, write them during reconcile (including the
599 // forced reconcile after adoption) instead of trusting `olds` as an
600 // observation of cloud state. When the prop is removed after being
601 // managed, an empty object clears the previously managed settings.
602 const settingsChanged =
603 news.settings !== undefined || olds?.settings !== undefined;
604 if (project.name !== name || settingsChanged) {
605 project = (yield* updateProject({
606 id: project.id,
607 name,
608 ...(settingsChanged ? { settings: news.settings ?? {} } : {}),
609 })).data;
610 }
611 const projectId = project.id;
612
613 let database = createdDatabase;
614 let defaultDatabaseChanged = false;
615 if (news.createDatabase !== false && !database) {
616 database = yield* defaultDatabase(projectId).pipe(
617 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
618 );
619 }
620
621 if (news.createDatabase !== false) {
622 const desiredRegion = news.region ?? "us-east-1";
623 if (database && database.region?.id !== desiredRegion) {
624 return yield* Effect.fail(
625 new Error(
626 `Cannot safely change Prisma project '${name}' default database region from '${database.region?.id ?? "unknown"}' to '${desiredRegion}' in place. The Management API would create an empty default database and risk deleting the existing data-bearing database. Create a replacement project and perform an explicit data migration/cutover instead.`,
627 ),
628 );
629 }
630 if (!database) {
631 defaultDatabaseChanged = true;
632 const created: ObservedDatabase = yield* createProjectDatabase({
633 projectId,
634 region: desiredRegion,
635 isDefault: true,
636 }).pipe(
637 Retry.none,
638 Effect.map((response) => response.data),
639 Effect.catchTag("Conflict", () =>
640 defaultDatabase(projectId).pipe(
641 Effect.flatMap((database) =>
642 requireDefaultDatabaseInRegion(
643 database,
644 name,
645 desiredRegion,
646 ),
647 ),
648 ),
649 ),
650 );
651 database = yield* requireDefaultDatabaseInRegion(
652 created,
653 name,
654 desiredRegion,
655 );
656 secrets = extractConnectionSecrets(created.connections[0]);
657 }
658 }
659
660 // Project.defaultRegion is derived from the current default database;
661 // re-read both resources after an in-place database replacement and
662 // verify the Management API actually exposes the desired region.
663 if (defaultDatabaseChanged) {
664 const changedDatabaseId = database?.id;
665 if (changedDatabaseId === undefined) {
666 return yield* Effect.fail(
667 new DefaultDatabaseConsistencyError(
668 `Prisma project '${name}' did not return an identifier for its new default database.`,
669 ),
670 );
671 }
672 const observed = yield* observeDesiredDefaultDatabase(
673 name,
674 project.id,
675 changedDatabaseId,
676 news.region ?? "us-east-1",
677 );
678 project = observed.project;
679 database = observed.database;
680 }
681
682 const persistedSecrets =
683 output &&
684 database !== undefined &&
685 output.databaseId === database.id
686 ? {
687 directConnectionString: output.directConnectionString,
688 pooledConnectionString: output.pooledConnectionString,
689 accelerateConnectionString: output.accelerateConnectionString,
690 host: output.host,
691 user: output.user,
692 password: output.password,
693 }
694 : {};
695 const knownSecrets = mergeConnectionSecrets(
696 secrets,
697 persistedSecrets,
698 );
699 let finalSecrets = knownSecrets;
700 if (
701 database &&
702 (recoverCreateSecrets ||
703 (ownedGeneratedIdentity &&
704 !hasCanonicalConnectionSecrets(knownSecrets)) ||
705 defaultDatabaseChanged ||
706 olds !== undefined ||
707 news.rotateCredentialsOnAdopt === true)
708 ) {
709 const recovered = yield* recoverDatabaseConnectionSecrets(
710 database,
711 knownSecrets,
712 );
713 database = recovered.database;
714 finalSecrets = recovered.secrets;
715 }
716
717 return attrsFrom(project, database, finalSecrets);
718 }),
719 delete: Effect.fn(function* ({ output }) {
720 if (isPrismaDevId(output.projectId)) return;
721 yield* destroyProjectApps(output.projectId);
722 }),
723 };
724 }),
725 );
726
727const ProviderLocal = () =>
728 devProvider(Project, ["projectId"], ({ id, news }) => ({
729 projectId: devId("project", id),
730 projectName: news.name ?? id,
731 workspaceId: devId("workspace", "local"),
732 createdAt: DEV_TIMESTAMP,
733 defaultRegion:
734 news.createDatabase === false
735 ? (news.region ?? null)
736 : (news.region ?? "us-east-1"),
737 databaseId:
738 news.createDatabase === false ? undefined : devId("database", id),
739 defaultConnectionId:
740 news.createDatabase === false ? undefined : devId("connection", id),
741 directConnectionString: undefined,
742 pooledConnectionString: undefined,
743 accelerateConnectionString: undefined,
744 host: undefined,
745 user: undefined,
746 password: undefined,
747 }));
748
749export const ProjectProvider = () =>
750 ProviderLayer.dual(Project, {
751 local: () => ProviderLocal(),
752 live: () => ProviderLive(),
753 });