@prisma/composer-prisma-cloud 0.1.0-dev.15 → 0.1.0-dev.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control.d.mts +5 -28
- package/dist/control.mjs +318 -46
- package/dist/control.mjs.map +1 -1
- package/package.json +13 -13
package/dist/control.d.mts
CHANGED
|
@@ -2,44 +2,19 @@ import { t as ProviderParamEntry } from "./serializer-Cx5slrV4-xJfH6EWS.mjs";
|
|
|
2
2
|
import { createManagementApiClient } from "@prisma/management-api-sdk";
|
|
3
3
|
import "effect/Context";
|
|
4
4
|
import "effect/Effect";
|
|
5
|
-
import
|
|
5
|
+
import "effect/Layer";
|
|
6
6
|
import "effect/Redacted";
|
|
7
7
|
import "effect/Config";
|
|
8
8
|
import "alchemy/Provider";
|
|
9
|
-
import { Resource
|
|
9
|
+
import { Resource } from "alchemy";
|
|
10
10
|
import "effect/Schedule";
|
|
11
|
-
import postgres from "postgres";
|
|
12
|
-
import { State, StateStoreError } from "alchemy/State";
|
|
13
11
|
import * as Output from "alchemy/Output";
|
|
14
|
-
import { ExtensionDescriptor } from "@prisma/composer/config";
|
|
12
|
+
import { ExtensionDescriptor, StateDescriptor } from "@prisma/composer/config";
|
|
15
13
|
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/compute.d.mts
|
|
16
14
|
/** Every region Prisma Compute serves — the runtime source of truth; `ComputeRegion` is derived from it so the two can never drift. */
|
|
17
15
|
declare const COMPUTE_REGIONS: readonly ["us-east-1", "us-west-1", "eu-west-3", "eu-central-1", "ap-northeast-1", "ap-southeast-1"];
|
|
18
16
|
type ComputeRegion = (typeof COMPUTE_REGIONS)[number];
|
|
19
17
|
//#endregion
|
|
20
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/state.d.mts
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region src/state/layer.d.ts
|
|
23
|
-
/**
|
|
24
|
-
* The hosted Alchemy state store. On layer init (scoped, once per stack
|
|
25
|
-
* run): resolve the stage's Branch, find-or-create its `prisma-composer-state`
|
|
26
|
-
* database, create a fresh connection, migrate the schema, and acquire the
|
|
27
|
-
* (stack, stage) advisory lock — see `bootstrap.ts` and `lock.ts`. The
|
|
28
|
-
* Management API plumbing (`ManagementClient`, `PrismaCredentials`) is
|
|
29
|
-
* provided internally, so the returned layer's only requirements are the
|
|
30
|
-
* ones alchemy itself already provides to every state store
|
|
31
|
-
* (`StackServices`).
|
|
32
|
-
*
|
|
33
|
-
* Any bootstrap/lock/migration failure is wrapped into an operator-facing
|
|
34
|
-
* `HostedStateBootstrapError` (naming the Project/Branch and the step that
|
|
35
|
-
* failed, never the raw driver/API error — see `errors.ts`) before dying the
|
|
36
|
-
* layer (loud, immediate, unrecoverable) rather than surfacing as a typed
|
|
37
|
-
* error — matching core's `LowerOptions.state: Layer.Layer<State, never,
|
|
38
|
-
* StackServices>` contract and alchemy's own convention (e.g. a missing
|
|
39
|
-
* state store is `Effect.die` in `Stack.make`).
|
|
40
|
-
*/
|
|
41
|
-
declare const prismaState: () => Layer.Layer<State, never, StackServices>;
|
|
42
|
-
//#endregion
|
|
43
18
|
//#region ../../1-prisma-cloud/1-extensions/target/dist/control.d.mts
|
|
44
19
|
//#region src/descriptors/shared.d.ts
|
|
45
20
|
/**
|
|
@@ -69,6 +44,8 @@ interface ProviderParam extends ProviderParamEntry {
|
|
|
69
44
|
}
|
|
70
45
|
//#endregion
|
|
71
46
|
//#region src/control/extension.d.ts
|
|
47
|
+
/** The user-facing state descriptor: `state: prismaState()` in `prisma-composer.config.ts` (ADR-0017). */
|
|
48
|
+
declare const prismaState: () => StateDescriptor;
|
|
72
49
|
interface PrismaCloudOptions {
|
|
73
50
|
/** Defaults to the PRISMA_WORKSPACE_ID environment variable. */
|
|
74
51
|
workspaceId?: string;
|
package/dist/control.mjs
CHANGED
|
@@ -622,7 +622,107 @@ const ProjectProvider = () => Provider.effect(Project, Effect.gen(function* () {
|
|
|
622
622
|
})
|
|
623
623
|
};
|
|
624
624
|
}));
|
|
625
|
-
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
|
|
627
|
+
/** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
|
|
628
|
+
var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
|
|
629
|
+
const listAllProjects = (client) => Effect.gen(function* () {
|
|
630
|
+
const projects = [];
|
|
631
|
+
let cursor;
|
|
632
|
+
for (;;) {
|
|
633
|
+
const query = cursor === void 0 ? {} : { cursor };
|
|
634
|
+
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
635
|
+
projects.push(...page.data);
|
|
636
|
+
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
637
|
+
cursor = page.pagination.nextCursor;
|
|
638
|
+
}
|
|
639
|
+
return projects;
|
|
640
|
+
});
|
|
641
|
+
/**
|
|
642
|
+
* Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
|
|
643
|
+
* bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
|
|
644
|
+
* one (the same normalization `state/bootstrap.ts` applies to the same
|
|
645
|
+
* `/v1/projects` listing).
|
|
646
|
+
*/
|
|
647
|
+
const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
|
|
648
|
+
/**
|
|
649
|
+
* Finds the app's Project by name in the workspace — PDP allows duplicate
|
|
650
|
+
* project names, so more than one can match; the oldest wins. Creates one
|
|
651
|
+
* if none match, unless `ensure` is `false` (find-only — `destroy`), in
|
|
652
|
+
* which case an absent Project fails with `ContainerNotFoundError`. No
|
|
653
|
+
* ownership marker and no `--project` override (both deferred — see
|
|
654
|
+
* ADR-0019).
|
|
655
|
+
*/
|
|
656
|
+
const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
|
|
657
|
+
const oldest = (yield* listAllProjects(client)).filter((p) => bareWorkspaceId(p.workspace.id) === bareWorkspaceId(workspaceId) && p.name === appName).sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
658
|
+
if (oldest !== void 0) return oldest.id;
|
|
659
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
|
|
660
|
+
return (yield* call(() => client.POST("/v1/projects", { body: {
|
|
661
|
+
name: appName,
|
|
662
|
+
workspaceId
|
|
663
|
+
} }))).data.id;
|
|
664
|
+
});
|
|
665
|
+
const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
666
|
+
path: { projectId },
|
|
667
|
+
query: { gitName }
|
|
668
|
+
} })).pipe(Effect.map((page) => page.data[0]?.id));
|
|
669
|
+
/**
|
|
670
|
+
* Finds the stage's Branch by its exact `gitName`, creating it if absent
|
|
671
|
+
* unless `ensure` is `false` (find-only — `destroy`), in which case an
|
|
672
|
+
* absent Branch fails with `ContainerNotFoundError`. The Management API has
|
|
673
|
+
* no server-side "create-or-return" idempotency (`POST
|
|
674
|
+
* /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
|
|
675
|
+
* field to make that a no-op), so idempotency is client-side: observe
|
|
676
|
+
* first, and on a racing 409 from create, re-observe rather than fail.
|
|
677
|
+
*/
|
|
678
|
+
const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
|
|
679
|
+
const existing = yield* findBranchId(client, projectId, gitName);
|
|
680
|
+
if (existing !== void 0) return existing;
|
|
681
|
+
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
|
|
682
|
+
appName,
|
|
683
|
+
stage: gitName
|
|
684
|
+
}));
|
|
685
|
+
return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
|
|
686
|
+
params: { path: { projectId } },
|
|
687
|
+
body: { gitName }
|
|
688
|
+
})).pipe(Effect.map((r) => r.data.id), Effect.catch((err) => err.status === 409 ? findBranchId(client, projectId, gitName).pipe(Effect.flatMap((id) => id === void 0 ? Effect.fail(err) : Effect.succeed(id))) : Effect.fail(err)));
|
|
689
|
+
});
|
|
690
|
+
/**
|
|
691
|
+
* Resolves the two containers a stage's deploy runs into (ADR-0019): the
|
|
692
|
+
* app's **Project**, found-or-created by name, and — for a named stage
|
|
693
|
+
* only — its **Branch**, found-or-created by `gitName`. The default stage
|
|
694
|
+
* (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
|
|
695
|
+
* false` (`destroy`), nothing is created — an absent Project or Branch
|
|
696
|
+
* fails with `ContainerNotFoundError` instead.
|
|
697
|
+
*/
|
|
698
|
+
const resolveContainer = (opts) => Effect.gen(function* () {
|
|
699
|
+
const client = yield* ManagementClient;
|
|
700
|
+
const ensure = opts.ensure ?? true;
|
|
701
|
+
const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
|
|
702
|
+
if (opts.stage === void 0) return { projectId };
|
|
703
|
+
return {
|
|
704
|
+
projectId,
|
|
705
|
+
branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
/**
|
|
709
|
+
* Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
|
|
710
|
+
* the Branch still has live members or is the production/default Branch —
|
|
711
|
+
* that surfaces as a `PrismaApiError`.
|
|
712
|
+
*/
|
|
713
|
+
const deleteBranch = (branchId) => Effect.gen(function* () {
|
|
714
|
+
const client = yield* ManagementClient;
|
|
715
|
+
yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
|
|
716
|
+
});
|
|
717
|
+
/**
|
|
718
|
+
* Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
|
|
719
|
+
* 400 if the Project still has live dependencies (e.g. another stage's
|
|
720
|
+
* Branch/resources) — that surfaces as a `PrismaApiError`.
|
|
721
|
+
*/
|
|
722
|
+
const deleteProject = (projectId) => Effect.gen(function* () {
|
|
723
|
+
const client = yield* ManagementClient;
|
|
724
|
+
yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
|
|
725
|
+
});
|
|
626
726
|
/** The collection of Prisma resource providers. */
|
|
627
727
|
var Providers = class extends Provider.ProviderCollection()("Prisma") {};
|
|
628
728
|
/**
|
|
@@ -1280,11 +1380,8 @@ const guardStateService = (service, checkLive, now = Date.now) => {
|
|
|
1280
1380
|
* StackServices>` contract and alchemy's own convention (e.g. a missing
|
|
1281
1381
|
* state store is `Effect.die` in `Stack.make`).
|
|
1282
1382
|
*/
|
|
1283
|
-
const
|
|
1284
|
-
const projectId =
|
|
1285
|
-
if (projectId === void 0 || projectId.length === 0) throw new Error("prismaState(): environment variable PRISMA_PROJECT_ID is required (the CLI sets it — deploy via `prisma-composer deploy`).");
|
|
1286
|
-
const branchIdEnv = process.env["PRISMA_BRANCH_ID"];
|
|
1287
|
-
const branchId = branchIdEnv === void 0 || branchIdEnv.length === 0 ? void 0 : branchIdEnv;
|
|
1383
|
+
const prismaStateLayer = (ids) => {
|
|
1384
|
+
const { projectId, branchId } = ids;
|
|
1288
1385
|
return Layer.effect(State, Effect.gen(function* () {
|
|
1289
1386
|
const stack = yield* Stack;
|
|
1290
1387
|
const container = branchId === void 0 ? projectId : `${projectId}/${branchId}`;
|
|
@@ -1307,18 +1404,181 @@ const prismaState = () => {
|
|
|
1307
1404
|
};
|
|
1308
1405
|
//#endregion
|
|
1309
1406
|
//#region ../../1-prisma-cloud/1-extensions/target/dist/control.mjs
|
|
1407
|
+
const PRISMA_CLOUD_EXTENSION_ID = "@prisma/composer-prisma-cloud";
|
|
1408
|
+
var PrismaCloudContainer = class {
|
|
1409
|
+
input;
|
|
1410
|
+
projectId;
|
|
1411
|
+
branchId;
|
|
1412
|
+
constructor(input, projectId, branchId) {
|
|
1413
|
+
this.input = input;
|
|
1414
|
+
this.projectId = projectId;
|
|
1415
|
+
this.branchId = branchId;
|
|
1416
|
+
}
|
|
1417
|
+
serialize() {
|
|
1418
|
+
return JSON.stringify({
|
|
1419
|
+
input: this.input,
|
|
1420
|
+
projectId: this.projectId,
|
|
1421
|
+
...this.branchId !== void 0 ? { branchId: this.branchId } : {}
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
};
|
|
1425
|
+
/** `instanceof` — parent-side instances and child-side deserialized instances are both constructed by this module. */
|
|
1426
|
+
function isPrismaCloudContainer(value) {
|
|
1427
|
+
return value instanceof PrismaCloudContainer;
|
|
1428
|
+
}
|
|
1429
|
+
/** Narrow-or-throw for hook inputs. */
|
|
1430
|
+
function prismaCloudContainerOf(value) {
|
|
1431
|
+
if (!isPrismaCloudContainer(value)) throw new Error("the Prisma Cloud container was not resolved — the extension's container descriptor did not run.");
|
|
1432
|
+
return value;
|
|
1433
|
+
}
|
|
1434
|
+
function isRecord(value) {
|
|
1435
|
+
return typeof value === "object" && value !== null;
|
|
1436
|
+
}
|
|
1437
|
+
function invalidPayloadError(reason) {
|
|
1438
|
+
return /* @__PURE__ */ new Error(`${PRISMA_CLOUD_EXTENSION_ID}: invalid container transport payload — ${reason}.`);
|
|
1439
|
+
}
|
|
1440
|
+
/** Reconstructs a `PrismaCloudContainer` from `serialize()`'s JSON output — real narrowing, no casts. */
|
|
1441
|
+
function deserialize(serialized) {
|
|
1442
|
+
let parsed;
|
|
1443
|
+
try {
|
|
1444
|
+
parsed = JSON.parse(serialized);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
throw invalidPayloadError(`not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
1447
|
+
}
|
|
1448
|
+
if (!isRecord(parsed)) throw invalidPayloadError("not an object");
|
|
1449
|
+
const input = parsed["input"];
|
|
1450
|
+
if (!isRecord(input)) throw invalidPayloadError("\"input\" is not an object");
|
|
1451
|
+
const appName = input["appName"];
|
|
1452
|
+
if (typeof appName !== "string") throw invalidPayloadError("\"input.appName\" is not a string");
|
|
1453
|
+
const stage = input["stage"];
|
|
1454
|
+
if (stage !== void 0 && typeof stage !== "string") throw invalidPayloadError("\"input.stage\" is not a string or absent");
|
|
1455
|
+
const projectId = parsed["projectId"];
|
|
1456
|
+
if (typeof projectId !== "string") throw invalidPayloadError("\"projectId\" is not a string");
|
|
1457
|
+
const branchId = parsed["branchId"];
|
|
1458
|
+
if (branchId !== void 0 && typeof branchId !== "string") throw invalidPayloadError("\"branchId\" is not a string or absent");
|
|
1459
|
+
return new PrismaCloudContainer({
|
|
1460
|
+
appName,
|
|
1461
|
+
stage
|
|
1462
|
+
}, projectId, branchId);
|
|
1463
|
+
}
|
|
1464
|
+
const workspaceRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_WORKSPACE_ID is required.");
|
|
1465
|
+
const tokenRequiredError$2 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
1466
|
+
function requireWorkspaceId() {
|
|
1467
|
+
const workspaceId = process.env["PRISMA_WORKSPACE_ID"];
|
|
1468
|
+
if (workspaceId === void 0 || workspaceId.length === 0) throw workspaceRequiredError();
|
|
1469
|
+
return workspaceId;
|
|
1470
|
+
}
|
|
1471
|
+
function requireTokenUnlessInjected(deps) {
|
|
1472
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw tokenRequiredError$2();
|
|
1473
|
+
}
|
|
1474
|
+
async function ensureContainer(input, deps) {
|
|
1475
|
+
const workspaceId = requireWorkspaceId();
|
|
1476
|
+
requireTokenUnlessInjected(deps);
|
|
1477
|
+
const program = resolveContainer({
|
|
1478
|
+
workspaceId,
|
|
1479
|
+
appName: input.appName,
|
|
1480
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
1481
|
+
ensure: true
|
|
1482
|
+
}).pipe(Effect.map((c) => ({
|
|
1483
|
+
ok: true,
|
|
1484
|
+
container: c
|
|
1485
|
+
})), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1486
|
+
ok: false,
|
|
1487
|
+
message: `Prisma Management API error resolving containers: ${e.message}.`
|
|
1488
|
+
})));
|
|
1489
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1490
|
+
const outcome = await Effect.runPromise(provided);
|
|
1491
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
1492
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
1493
|
+
}
|
|
1494
|
+
async function locateContainer(input, deps) {
|
|
1495
|
+
const workspaceId = requireWorkspaceId();
|
|
1496
|
+
requireTokenUnlessInjected(deps);
|
|
1497
|
+
const program = resolveContainer({
|
|
1498
|
+
workspaceId,
|
|
1499
|
+
appName: input.appName,
|
|
1500
|
+
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
1501
|
+
ensure: false
|
|
1502
|
+
}).pipe(Effect.map((c) => ({
|
|
1503
|
+
ok: true,
|
|
1504
|
+
container: c
|
|
1505
|
+
})), Effect.catchTag("ContainerNotFoundError", () => Effect.succeed({ ok: false })), Effect.catchTag("PrismaApiError", (e) => Effect.fail(/* @__PURE__ */ new Error(`Prisma Management API error resolving containers: ${e.message}.`))));
|
|
1506
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1507
|
+
const outcome = await Effect.runPromise(provided);
|
|
1508
|
+
if (!outcome.ok) return void 0;
|
|
1509
|
+
return new PrismaCloudContainer(input, outcome.container.projectId, outcome.container.branchId);
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Soft-deletes a named stage's Branch after a successful `alchemy destroy`
|
|
1513
|
+
* has removed its members — the Management API refuses to delete a Branch
|
|
1514
|
+
* that still has live members.
|
|
1515
|
+
*/
|
|
1516
|
+
async function removeStageBranch(branchId, deps) {
|
|
1517
|
+
requireTokenUnlessInjected(deps);
|
|
1518
|
+
const program = deleteBranch(branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1519
|
+
ok: false,
|
|
1520
|
+
message: `Failed to delete the stage Branch: ${e.message}.`
|
|
1521
|
+
})));
|
|
1522
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1523
|
+
const outcome = await Effect.runPromise(provided);
|
|
1524
|
+
if (!outcome.ok) throw new Error(outcome.message);
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* Best-effort cleanup after a successful `--production` destroy: removes
|
|
1528
|
+
* the app's Project so hand-run stacks don't accumulate as empty Projects
|
|
1529
|
+
* (they eventually hit the workspace's plan limit). Unlike `removeStageBranch`,
|
|
1530
|
+
* this never throws: the destroy itself already succeeded, and the API's own
|
|
1531
|
+
* 400 ("still has dependencies") is the only check that matters — failing
|
|
1532
|
+
* the command over a cleanup step would be worse than leaving a Project shell.
|
|
1533
|
+
*/
|
|
1534
|
+
async function removeAppProject(projectId, deps) {
|
|
1535
|
+
if (deps?.client === void 0 && (process.env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
|
|
1536
|
+
console.warn(`Skipped removing the Project (${projectId}): PRISMA_SERVICE_TOKEN is not set.`);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
const program = deleteProject(projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
1540
|
+
ok: false,
|
|
1541
|
+
error: e
|
|
1542
|
+
})));
|
|
1543
|
+
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
1544
|
+
const outcome = await Effect.runPromise(provided);
|
|
1545
|
+
if (outcome.ok) {
|
|
1546
|
+
console.log(`Removed the Project (${projectId}) — nothing was left in it.`);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
if (outcome.error.status === 400) {
|
|
1550
|
+
console.log(`Kept the Project (${projectId}) — it still has another stage's resources.`);
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
console.warn(`Could not remove the Project (${projectId}) after destroy: ${outcome.error.message}.`);
|
|
1554
|
+
}
|
|
1555
|
+
function containerDescriptor(deps) {
|
|
1556
|
+
return {
|
|
1557
|
+
ensure: (input) => ensureContainer(input, deps),
|
|
1558
|
+
locate: (input) => locateContainer(input, deps),
|
|
1559
|
+
remove: (instance) => instance.input.stage !== void 0 ? removeStageBranch(instance.branchId ?? missingBranchId(instance), deps) : removeAppProject(instance.projectId, deps),
|
|
1560
|
+
deserialize
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
/** Defensive: a named-stage container always resolves a Branch together with its stage — `ensure`/`locate`/`deserialize` never produce one without the other. */
|
|
1564
|
+
function missingBranchId(instance) {
|
|
1565
|
+
throw new Error(`${PRISMA_CLOUD_EXTENSION_ID}: a named-stage ("${instance.input.stage}") container instance is missing its branchId — this is a bug in ensure/locate/deserialize.`);
|
|
1566
|
+
}
|
|
1310
1567
|
const PRISMA_NAME_MIN = 3;
|
|
1311
1568
|
const PRISMA_NAME_MAX = 65;
|
|
1312
1569
|
function validateName(value, source) {
|
|
1313
1570
|
if (value.length < PRISMA_NAME_MIN || value.length > PRISMA_NAME_MAX) throw new Error(`prisma-cloud: ${source} "${value}" (${value.length} characters) is not a valid Prisma resource name — Prisma requires ${PRISMA_NAME_MIN}–${PRISMA_NAME_MAX} characters. Rename the provision id (or the deploy --name) to fit.`);
|
|
1314
1571
|
}
|
|
1315
1572
|
function isCloudApplication(value) {
|
|
1316
|
-
return typeof value === "object" && value !== null && "projectId" in value && typeof value.projectId === "string";
|
|
1573
|
+
return typeof value === "object" && value !== null && "projectId" in value && typeof value.projectId === "string" && "branchId" in value && (value.branchId === void 0 || typeof value.branchId === "string");
|
|
1317
1574
|
}
|
|
1318
1575
|
/** Narrows `ctx.application`, which core hands over as `unknown`, to this extension's own product; throws naming the hook when it hasn't run. */
|
|
1319
|
-
function
|
|
1576
|
+
function cloudApplicationOf(application) {
|
|
1320
1577
|
if (!isCloudApplication(application)) throw new Error("prisma-cloud: ctx.application is not this extension's application product — the prismaCloud() application hook must run before any node lowers.");
|
|
1321
|
-
return application
|
|
1578
|
+
return application;
|
|
1579
|
+
}
|
|
1580
|
+
function projectIdOf(application) {
|
|
1581
|
+
return cloudApplicationOf(application).projectId;
|
|
1322
1582
|
}
|
|
1323
1583
|
/**
|
|
1324
1584
|
* One Bucket per module-provisioned bucket resource — `id` is the module
|
|
@@ -1327,13 +1587,14 @@ function projectIdOf(application) {
|
|
|
1327
1587
|
* carrier, and its attributes (endpoint, bucketName, accessKeyId,
|
|
1328
1588
|
* secretAccessKey) become the four S3Config outputs consumers resolve by name.
|
|
1329
1589
|
*/
|
|
1330
|
-
function bucketDescriptor(
|
|
1590
|
+
function bucketDescriptor(_o) {
|
|
1331
1591
|
const lowering = ({ id, application }) => Effect.gen(function* () {
|
|
1332
1592
|
validateName(id, "resource name (from provision id)");
|
|
1593
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1333
1594
|
const bkt = yield* Bucket(`${id}-bucket`, {
|
|
1334
1595
|
projectId: projectIdOf(application),
|
|
1335
1596
|
name: id,
|
|
1336
|
-
...
|
|
1597
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1337
1598
|
});
|
|
1338
1599
|
const key = yield* BucketKey(`${id}-key`, {
|
|
1339
1600
|
bucketId: bkt.id,
|
|
@@ -1369,20 +1630,22 @@ function computeDescriptor(o) {
|
|
|
1369
1630
|
provision: ({ id, application }) => Effect.gen(function* () {
|
|
1370
1631
|
validateName(id, "service name (from provision id)");
|
|
1371
1632
|
const projectId = projectIdOf(application);
|
|
1633
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1372
1634
|
return {
|
|
1373
1635
|
serviceId: (yield* ComputeService(`${id}-svc`, {
|
|
1374
1636
|
projectId,
|
|
1375
1637
|
name: id,
|
|
1376
1638
|
region: o.region ?? "us-east-1",
|
|
1377
|
-
...
|
|
1639
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1378
1640
|
})).id,
|
|
1379
1641
|
projectId
|
|
1380
1642
|
};
|
|
1381
1643
|
}),
|
|
1382
1644
|
serialize: (ctx, provisioned, config) => Effect.gen(function* () {
|
|
1383
1645
|
const { address, node, graph } = ctx;
|
|
1384
|
-
const
|
|
1385
|
-
const
|
|
1646
|
+
const branchId = cloudApplicationOf(ctx.application).branchId;
|
|
1647
|
+
const cls = branchId ? "preview" : "production";
|
|
1648
|
+
const branch = branchId !== void 0 ? { branchId } : {};
|
|
1386
1649
|
const projectId = provisioned.projectId;
|
|
1387
1650
|
const svc = node;
|
|
1388
1651
|
const records = [];
|
|
@@ -1522,11 +1785,12 @@ const PgWarmProvider = () => Provider.effect(PgWarm, Effect.succeed(pgWarmProvid
|
|
|
1522
1785
|
function postgresDescriptor(o) {
|
|
1523
1786
|
const lowering = ({ id, application }) => Effect.gen(function* () {
|
|
1524
1787
|
validateName(id, "resource name (from provision id)");
|
|
1788
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1525
1789
|
const db = yield* Database(`${id}-db`, {
|
|
1526
1790
|
projectId: projectIdOf(application),
|
|
1527
1791
|
name: id,
|
|
1528
1792
|
region: o.region ?? "us-east-1",
|
|
1529
|
-
...
|
|
1793
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1530
1794
|
});
|
|
1531
1795
|
const conn = yield* Connection(`${id}-conn`, {
|
|
1532
1796
|
databaseId: db.id,
|
|
@@ -1790,11 +2054,12 @@ const PnMigrationProvider = () => Provider.effect(PnMigration, Effect.succeed(pn
|
|
|
1790
2054
|
function prismaNextDescriptor(o) {
|
|
1791
2055
|
const lowering = ({ id, node, application }) => Effect.gen(function* () {
|
|
1792
2056
|
validateName(id, "resource name (from provision id)");
|
|
2057
|
+
const branchId = cloudApplicationOf(application).branchId;
|
|
1793
2058
|
const db = yield* Database(`${id}-db`, {
|
|
1794
2059
|
projectId: projectIdOf(application),
|
|
1795
2060
|
name: id,
|
|
1796
2061
|
region: o.region ?? "us-east-1",
|
|
1797
|
-
...
|
|
2062
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1798
2063
|
});
|
|
1799
2064
|
const conn = yield* Connection(`${id}-conn`, {
|
|
1800
2065
|
databaseId: db.id,
|
|
@@ -1931,7 +2196,7 @@ function s3StoreDescriptor(o) {
|
|
|
1931
2196
|
*
|
|
1932
2197
|
* Control-plane only (imported by control.ts → prisma-composer.config.ts); runs
|
|
1933
2198
|
* in the CLI parent, so it builds its own Management API client from env — the
|
|
1934
|
-
* same credential path `
|
|
2199
|
+
* same credential path `container.ts`'s `ensure`/`locate` use.
|
|
1935
2200
|
*/
|
|
1936
2201
|
/** production for the default stage; preview for a named stage — matching how the pack writes config rows. */
|
|
1937
2202
|
const classFor = (branchId) => branchId === void 0 ? "production" : "preview";
|
|
@@ -1974,21 +2239,21 @@ async function existsOnPlatform(client, projectId, branchId, key) {
|
|
|
1974
2239
|
* EnvironmentVariable.ts). A 409 means a concurrent deploy already provisioned
|
|
1975
2240
|
* it — tolerated. The value is never logged.
|
|
1976
2241
|
*/
|
|
1977
|
-
async function fillMissing(client,
|
|
2242
|
+
async function fillMissing(client, projectId, branchId, key, value) {
|
|
1978
2243
|
const res = await client.POST("/v1/environment-variables", { body: {
|
|
1979
|
-
projectId
|
|
1980
|
-
class: classFor(
|
|
2244
|
+
projectId,
|
|
2245
|
+
class: classFor(branchId),
|
|
1981
2246
|
key,
|
|
1982
2247
|
value,
|
|
1983
|
-
...
|
|
2248
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
1984
2249
|
} });
|
|
1985
2250
|
if (res.error !== void 0 && res.response.status !== 409) throw fillFailedError(key, res.error);
|
|
1986
2251
|
}
|
|
1987
2252
|
const tokenRequiredError$1 = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for deploy preflight.");
|
|
1988
2253
|
const listFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: Prisma Management API error listing "${key}": ${JSON.stringify(error)}.`);
|
|
1989
2254
|
const fillFailedError = (key, error) => /* @__PURE__ */ new Error(`deploy preflight: failed to provision "${key}" from the deploy shell: ${JSON.stringify(error)}.`);
|
|
1990
|
-
function missingError(missing,
|
|
1991
|
-
const scope =
|
|
2255
|
+
function missingError(missing, branchId, stage) {
|
|
2256
|
+
const scope = branchId === void 0 ? "the production class (project-level template)" : `the preview class of stage "${stage ?? branchId}" (branch override or template)`;
|
|
1992
2257
|
const lines = missing.map((m) => ` - ${m.name} (required by service "${m.serviceAddress}")`);
|
|
1993
2258
|
return /* @__PURE__ */ new Error(`Deploy preflight failed — ${missing.length} env var(s) (secret or env-sourced param) are not provisioned on Prisma Cloud for ${scope}, and are absent from the deploy shell:\n${lines.join("\n")}\n\nSet each in the deploy shell environment (the CLI will provision it on deploy), or create it on the platform (Prisma Console or the Management API) in ${scope}.`);
|
|
1994
2259
|
}
|
|
@@ -2007,6 +2272,7 @@ async function managementClient$1() {
|
|
|
2007
2272
|
* from env.
|
|
2008
2273
|
*/
|
|
2009
2274
|
async function runPreflight(input, deps) {
|
|
2275
|
+
const { projectId, branchId } = prismaCloudContainerOf(input.container);
|
|
2010
2276
|
const names = /* @__PURE__ */ new Map();
|
|
2011
2277
|
for (const binding of provisionManifest(input.graph)) {
|
|
2012
2278
|
const name = secretName(binding);
|
|
@@ -2027,15 +2293,15 @@ async function runPreflight(input, deps) {
|
|
|
2027
2293
|
const client = deps?.client ?? await managementClient$1();
|
|
2028
2294
|
const missing = [];
|
|
2029
2295
|
for (const meta of names.values()) {
|
|
2030
|
-
if (await existsOnPlatform(client,
|
|
2296
|
+
if (await existsOnPlatform(client, projectId, branchId, meta.name)) continue;
|
|
2031
2297
|
const shellValue = process.env[meta.name];
|
|
2032
2298
|
if (shellValue !== void 0 && shellValue.length > 0) {
|
|
2033
|
-
await fillMissing(client,
|
|
2299
|
+
await fillMissing(client, projectId, branchId, meta.name, shellValue);
|
|
2034
2300
|
continue;
|
|
2035
2301
|
}
|
|
2036
2302
|
missing.push(meta);
|
|
2037
2303
|
}
|
|
2038
|
-
if (missing.length > 0) throw missingError(missing, input);
|
|
2304
|
+
if (missing.length > 0) throw missingError(missing, branchId, input.stage);
|
|
2039
2305
|
}
|
|
2040
2306
|
const tokenRequiredError = () => /* @__PURE__ */ new Error("environment variable PRISMA_SERVICE_TOKEN is required for destroy teardown.");
|
|
2041
2307
|
async function managementClient() {
|
|
@@ -2060,12 +2326,13 @@ async function managementClient() {
|
|
|
2060
2326
|
* builds a client from env and verifies against the real database.
|
|
2061
2327
|
*/
|
|
2062
2328
|
async function runTeardown(input, deps) {
|
|
2063
|
-
const
|
|
2329
|
+
const { projectId, branchId } = prismaCloudContainerOf(input.container);
|
|
2330
|
+
const isNamedStage = branchId !== void 0;
|
|
2064
2331
|
try {
|
|
2065
2332
|
const client = deps?.client ?? await managementClient();
|
|
2066
2333
|
await Effect.runPromise(deleteStateDatabaseWith({
|
|
2067
|
-
projectId
|
|
2068
|
-
...
|
|
2334
|
+
projectId,
|
|
2335
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
2069
2336
|
}, deps?.verify ?? verifyOwnership).pipe(Effect.provideService(ManagementClient, client)));
|
|
2070
2337
|
} catch (error) {
|
|
2071
2338
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -2143,6 +2410,17 @@ const streamsApiKeyValue = (refs) => {
|
|
|
2143
2410
|
return distinct[0] ?? "";
|
|
2144
2411
|
});
|
|
2145
2412
|
};
|
|
2413
|
+
/** The user-facing state descriptor: `state: prismaState()` in `prisma-composer.config.ts` (ADR-0017). */
|
|
2414
|
+
const prismaState = () => ({
|
|
2415
|
+
extension: PRISMA_CLOUD_EXTENSION_ID,
|
|
2416
|
+
create: (container) => {
|
|
2417
|
+
const { projectId, branchId } = prismaCloudContainerOf(container);
|
|
2418
|
+
return prismaStateLayer(branchId !== void 0 ? {
|
|
2419
|
+
projectId,
|
|
2420
|
+
branchId
|
|
2421
|
+
} : { projectId });
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2146
2424
|
const KNOWN_REGION_SET = new Set(COMPUTE_REGIONS);
|
|
2147
2425
|
function isComputeRegion(value) {
|
|
2148
2426
|
return KNOWN_REGION_SET.has(value);
|
|
@@ -2198,34 +2476,25 @@ function buildProviderParams(entries, values) {
|
|
|
2198
2476
|
const PROVIDER_PARAMS = buildProviderParams(RESERVED_PROVIDER_PARAMS, PROVIDER_PARAM_VALUES);
|
|
2199
2477
|
/**
|
|
2200
2478
|
* Resolves the factory's env-or-option inputs, failing fast with the exact
|
|
2201
|
-
* variable name.
|
|
2202
|
-
* also runs in the CLI parent, before they're set; the required check lives in `application.provision`.
|
|
2479
|
+
* variable name.
|
|
2203
2480
|
*/
|
|
2204
2481
|
function resolveOptions(opts) {
|
|
2205
2482
|
const workspaceId = opts.workspaceId ?? process.env["PRISMA_WORKSPACE_ID"];
|
|
2206
2483
|
if (workspaceId === void 0 || workspaceId.length === 0) throw new Error("prismaCloud(): environment variable PRISMA_WORKSPACE_ID is required.");
|
|
2207
|
-
const projectId = process.env["PRISMA_PROJECT_ID"] || void 0;
|
|
2208
|
-
const branchId = process.env["PRISMA_BRANCH_ID"] || void 0;
|
|
2209
2484
|
if (opts.region !== void 0) return {
|
|
2210
2485
|
workspaceId,
|
|
2211
2486
|
region: opts.region,
|
|
2212
|
-
projectId,
|
|
2213
|
-
branchId,
|
|
2214
2487
|
providerParams: PROVIDER_PARAMS
|
|
2215
2488
|
};
|
|
2216
2489
|
const region = process.env["PRISMA_REGION"];
|
|
2217
2490
|
if (region === void 0 || region.length === 0) return {
|
|
2218
2491
|
workspaceId,
|
|
2219
|
-
projectId,
|
|
2220
|
-
branchId,
|
|
2221
2492
|
providerParams: PROVIDER_PARAMS
|
|
2222
2493
|
};
|
|
2223
2494
|
if (!isComputeRegion(region)) throw new Error(`prismaCloud(): environment variable PRISMA_REGION="${region}" is not a known region (expected one of: ${COMPUTE_REGIONS.join(", ")}).`);
|
|
2224
2495
|
return {
|
|
2225
2496
|
workspaceId,
|
|
2226
2497
|
region,
|
|
2227
|
-
projectId,
|
|
2228
|
-
branchId,
|
|
2229
2498
|
providerParams: PROVIDER_PARAMS
|
|
2230
2499
|
};
|
|
2231
2500
|
}
|
|
@@ -2233,21 +2502,24 @@ function resolveOptions(opts) {
|
|
|
2233
2502
|
const prismaCloud = (opts = {}) => {
|
|
2234
2503
|
const o = resolveOptions(opts);
|
|
2235
2504
|
return {
|
|
2236
|
-
id:
|
|
2505
|
+
id: PRISMA_CLOUD_EXTENSION_ID,
|
|
2506
|
+
container: containerDescriptor(),
|
|
2237
2507
|
providers: () => asProvidersLayer(Layer.mergeAll(providers(), PgWarmProvider(), PnMigrationProvider(), S3CredentialsProvider(), ServiceKeyProvider())),
|
|
2238
2508
|
preflight: (input) => runPreflight(input),
|
|
2239
2509
|
teardown: (input) => runTeardown(input),
|
|
2240
|
-
application: { provision: () => Effect.gen(function* () {
|
|
2241
|
-
const projectId =
|
|
2242
|
-
if (projectId === void 0 || projectId.length === 0) throw new Error("prismaCloud(): environment variable PRISMA_PROJECT_ID is required (the CLI sets it — deploy via `prisma-composer deploy`).");
|
|
2510
|
+
application: { provision: (ctx) => Effect.gen(function* () {
|
|
2511
|
+
const { projectId, branchId } = prismaCloudContainerOf(ctx.container);
|
|
2243
2512
|
for (const key of ["DATABASE_URL", "DATABASE_URL_POOLED"]) yield* EnvironmentVariable(`${key}-poison`, {
|
|
2244
2513
|
projectId,
|
|
2245
2514
|
key,
|
|
2246
2515
|
value: "-",
|
|
2247
|
-
class:
|
|
2248
|
-
...
|
|
2516
|
+
class: branchId ? "preview" : "production",
|
|
2517
|
+
...branchId !== void 0 ? { branchId } : {}
|
|
2249
2518
|
});
|
|
2250
|
-
return {
|
|
2519
|
+
return {
|
|
2520
|
+
projectId,
|
|
2521
|
+
branchId
|
|
2522
|
+
};
|
|
2251
2523
|
}) },
|
|
2252
2524
|
provisions: PROVISIONERS,
|
|
2253
2525
|
nodes: {
|