@prisma/composer 0.1.0-dev.18 → 0.1.0-dev.3

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.
Files changed (43) hide show
  1. package/dist/{app-config-DJdU4ubR-BwioNK8j.d.mts → app-config-5mXxjaEl-D2Q6MU5g.d.mts} +19 -105
  2. package/dist/assertions.d.mts +1 -1
  3. package/dist/assertions.mjs.map +1 -1
  4. package/dist/bin.mjs +266 -81
  5. package/dist/bin.mjs.map +1 -1
  6. package/dist/casts-Ci5rYYaR.mjs.map +1 -1
  7. package/dist/casts.d.mts +1 -1
  8. package/dist/config-C23NciC8.d.mts +1 -0
  9. package/dist/config.d.mts +3 -3
  10. package/dist/config.mjs +1 -2
  11. package/dist/config.mjs.map +1 -1
  12. package/dist/deploy-C23NciC8.d.mts +1 -0
  13. package/dist/deploy.d.mts +2 -2
  14. package/dist/deploy.mjs +5 -12
  15. package/dist/deploy.mjs.map +1 -1
  16. package/dist/dist-B0axxnBf.mjs.map +1 -1
  17. package/dist/{graph-B7NcPiOr-aSUOCGTH.d.mts → graph-D383dfW2-Cepin3Om.d.mts} +3 -3
  18. package/dist/{graph-types-BgT9UEdm-Bz-_OcJH.d.mts → graph-types-COu3ss99-CrJcsbDJ.d.mts} +4 -4
  19. package/dist/{index-B2DJ5CN4.d.mts → index-8wU5wpMV.d.mts} +3 -3
  20. package/dist/{nextjs-DLyeRR7M-B9ukbB2L.d.mts → index-DIyo-rxT.d.mts} +5 -5
  21. package/dist/index.d.mts +3 -3
  22. package/dist/nextjs-control.d.mts +5 -5
  23. package/dist/nextjs-control.mjs.map +1 -1
  24. package/dist/nextjs.d.mts +2 -2
  25. package/dist/nextjs.mjs.map +1 -1
  26. package/dist/node-control.d.mts +4 -4
  27. package/dist/node-control.mjs.map +1 -1
  28. package/dist/node.d.mts +4 -4
  29. package/dist/node.mjs.map +1 -1
  30. package/dist/report.d.mts +3 -3
  31. package/dist/report.mjs.map +1 -1
  32. package/dist/{service-rpc.d.mts → rpc.d.mts} +11 -19
  33. package/dist/rpc.mjs +184 -0
  34. package/dist/rpc.mjs.map +1 -0
  35. package/dist/testing.d.mts +2 -2
  36. package/dist/testing.mjs.map +1 -1
  37. package/package.json +14 -14
  38. package/dist/config-ByZICgry.d.mts +0 -1
  39. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +0 -45
  40. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +0 -1
  41. package/dist/deploy-ByZICgry.d.mts +0 -1
  42. package/dist/service-rpc.mjs +0 -353
  43. package/dist/service-rpc.mjs.map +0 -1
@@ -1,75 +1,12 @@
1
- import { M as ResourceNode, V as ServiceNode, d as Graph, k as ProvisionNeed, n as Config, t as BuildAdapter, x as NodeId, y as ModuleNode } from "./graph-types-BgT9UEdm-Bz-_OcJH.mjs";
2
- import "./graph-B7NcPiOr-aSUOCGTH.mjs";
1
+ import { M as ResourceNode, V as ServiceNode, d as Graph, k as ProvisionNeed, n as Config, t as BuildAdapter, x as NodeId, y as ModuleNode } from "./graph-types-COu3ss99-CrJcsbDJ.mjs";
2
+ import "./graph-D383dfW2-Cepin3Om.mjs";
3
3
  import * as Alchemy from "alchemy";
4
4
  import { Input, StackServices } from "alchemy";
5
5
  import * as Effect from "effect/Effect";
6
6
  import * as Layer from "effect/Layer";
7
7
  import { State } from "alchemy/State/State";
8
- //#region ../../0-framework/1-core/core/dist/app-config-DJdU4ubR.d.mts
9
- //#region src/container-transport.d.ts
10
- /**
11
- * Carries resolved containers from the CLI process into the alchemy process
12
- * (ADR-0037). A deploy runs as two processes: the CLI resolves each
13
- * extension's containers, then spawns `alchemy`, which re-imports the config
14
- * from scratch and needs those containers back — and env vars are the only
15
- * channel between the two. So the CLI writes each instance's `serialize()`
16
- * output into one env var per extension, and in the alchemy process
17
- * `deserializeContainers` reads each var back through the same extension's
18
- * descriptor. The framework owns the vars; it never reads their contents.
19
- */
20
- /** The key an extension resolves a container from: which app, which stage. */
21
- interface LocateContainerInput {
22
- /** The application name (root node's name, or `--name`). */
23
- readonly appName: string;
24
- /** The named stage, or `undefined` for the default (production) stage. */
25
- readonly stage: string | undefined;
26
- }
27
- /**
28
- * One resolved container. The framework sees only this interface; the
29
- * extension that produced the instance narrows it back to its own concrete
30
- * type wherever the framework hands it back (ADR-0037).
31
- */
32
- interface ContainerInstance {
33
- readonly input: LocateContainerInput;
34
- /** Serialize to a non-empty string for the process transport above. The format is the extension's own; only its `deserialize` reads it. */
35
- serialize(): string;
36
- }
37
- /**
38
- * The platform containers an app deploys into, as one lifecycle. `I` is
39
- * the extension's own instance type — the same descriptor produces and
40
- * consumes it, so the extension gets full typing internally while the
41
- * framework stores the erased form. METHOD SYNTAX REQUIRED on all four
42
- * members: the erased assignment into ExtensionDescriptor compiles only
43
- * through method bivariance; property-arrow members are checked
44
- * contravariantly and the assignment fails (same rule as
45
- * ServiceLowering<P, S> — ADR-0033).
46
- */
47
- interface ContainerDescriptor<I extends ContainerInstance = ContainerInstance> {
48
- /** Resolve the container for (appName, stage), creating anything absent. Called by `deploy`. */
49
- ensure(input: LocateContainerInput): Promise<I>;
50
- /** Find the container for (appName, stage); `undefined` when nothing exists. Called by `destroy` — never creates. */
51
- locate(input: LocateContainerInput): Promise<I | undefined>;
52
- /** Remove the container after a successful destroy, after every extension's `teardown` has run. Failure policy is the extension's. */
53
- remove(instance: I): Promise<void>;
54
- /** Reconstruct an instance from its own `serialize()` output — the far end of the framework's parent→child transport. */
55
- deserialize(serialized: string): I;
56
- }
57
- /** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
58
- declare function containerEnvVarName(extensionId: string): string;
59
- /** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
60
- declare function containerEnv(instances: ReadonlyMap<string, ContainerInstance>): Record<string, string>;
61
- /** The slice of `PrismaAppConfig.extensions` this module needs — kept narrow so this shared-plane module never imports the control-plane `ExtensionDescriptor`/`PrismaAppConfig` types (ADR-0028's plane split). */
62
- interface ContainerTransportExtension {
63
- readonly id: string;
64
- readonly container?: ContainerDescriptor;
65
- }
66
- /**
67
- * The alchemy-process side: for each extension with a container descriptor
68
- * whose var is present in `env`, call its deserialize. Absent var → no entry.
69
- */
70
- declare function deserializeContainers(extensions: readonly ContainerTransportExtension[], env: Readonly<Record<string, string | undefined>>): ReadonlyMap<string, ContainerInstance>;
71
- //#endregion
72
- //#region src/control/deploy.d.ts
8
+ //#region ../../0-framework/1-core/core/dist/app-config-5mXxjaEl.d.mts
9
+ //#region src/exports/deploy.d.ts
73
10
  /** The Layer shape every Alchemy state store must satisfy — what `LowerOptions.state` and `PrismaAppConfig.state` both traffic in. */
74
11
  type AlchemyStateLayer = Layer.Layer<State, never, StackServices>;
75
12
  /**
@@ -155,13 +92,6 @@ interface LowerContext {
155
92
  * it with its own type guard.
156
93
  */
157
94
  readonly application: unknown;
158
- /**
159
- * The owning extension's resolved container, deserialized from the
160
- * framework transport; core never reads it — the extension narrows it
161
- * with its own type guard. `undefined` when the extension declares no
162
- * container descriptor.
163
- */
164
- readonly container: ContainerInstance | undefined;
165
95
  /** Already-lowered deps (topo order). */
166
96
  readonly lowered: ReadonlyMap<NodeId, Outputs>;
167
97
  /** Every provisioned param value minted this lowering, keyed by edge id (ADR-0031). */
@@ -289,12 +219,10 @@ declare function joinDeployment(graph: Graph, entries: readonly {
289
219
  }[]): readonly DeployedNode[];
290
220
  /**
291
221
  * The state-layer precedence a deploy resolves to: an explicit opts.state
292
- * always wins; failing that, the config's own (required) state descriptor,
293
- * created with its owning extension's resolved container (`undefined` when
294
- * that extension declared none). A pure function so the precedence is
295
- * testable without booting Alchemy.
222
+ * always wins; failing that, the config's own (required) state. A pure
223
+ * function so the precedence is testable without booting Alchemy.
296
224
  */
297
- declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig, containers: ReadonlyMap<string, ContainerInstance>): AlchemyStateLayer;
225
+ declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig): AlchemyStateLayer;
298
226
  /**
299
227
  * All configured extensions' providers merged, config array order — an
300
228
  * extension without `providers` is skipped; no used-extensions-only
@@ -313,7 +241,7 @@ declare function lowering(root: ModuleNode, config: PrismaAppConfig, opts: Lower
313
241
  */
314
242
  declare function lower(root: ModuleNode, config: PrismaAppConfig, opts: LowerOptions): Effect.Effect<Alchemy.CompiledStack<undefined, any>, import("effect/Config").ConfigError, never>;
315
243
  //#endregion
316
- //#region src/control/app-config.d.ts
244
+ //#region src/exports/app-config.d.ts
317
245
  /**
318
246
  * One extension's control-plane registry: everything the deploy pipeline may
319
247
  * look up for a node whose `extension` field names this package. `nodes` is
@@ -347,38 +275,24 @@ interface ExtensionDescriptor {
347
275
  * fail the command handles that itself. Async: it talks to the platform.
348
276
  */
349
277
  readonly teardown?: (input: TeardownInput) => Promise<void>;
350
- /**
351
- * The extension's container lifecycle, when its platform has containers
352
- * (ADR-0038). The CLI resolves containers after assembly and before any
353
- * stack file or Alchemy run (deploy ensures, destroy locates); the
354
- * resolved instance reaches the alchemy process through the env transport
355
- * in container-transport.ts.
356
- */
357
- readonly container?: ContainerDescriptor;
358
- }
359
- /**
360
- * The deploy's one state store. It names its owning extension so core knows
361
- * whose resolved container to pass into `create` (ADR-0038).
362
- */
363
- interface StateDescriptor {
364
- /** The owning extension's id — matched against `ExtensionDescriptor.id`. */
365
- readonly extension: string;
366
- /** Build the state layer. `container` is the owning extension's resolved instance; `undefined` when it declared no container descriptor. */
367
- create(container: ContainerInstance | undefined): AlchemyStateLayer;
368
278
  }
369
279
  /** The resolved deploy context handed to an extension's `preflight` hook. */
370
280
  interface PreflightInput {
371
281
  /** The loaded application graph — the manifest of prerequisites is read from it (`provisionManifest`). */
372
282
  readonly graph: Graph;
373
- /** The calling extension's own resolved container; `undefined` when it declares no container descriptor. Narrow with the extension's guard. */
374
- readonly container: ContainerInstance | undefined;
283
+ /** The resolved Prisma Cloud Project id. */
284
+ readonly projectId: string;
285
+ /** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
286
+ readonly branchId: string | undefined;
375
287
  /** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
376
288
  readonly stage: string | undefined;
377
289
  }
378
290
  /** The resolved destroy context handed to an extension's `teardown` hook. */
379
291
  interface TeardownInput {
380
- /** The calling extension's own resolved container; `undefined` when it declares no container descriptor. Narrow with the extension's guard. */
381
- readonly container: ContainerInstance | undefined;
292
+ /** The resolved Prisma Cloud Project id. */
293
+ readonly projectId: string;
294
+ /** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
295
+ readonly branchId: string | undefined;
382
296
  /** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
383
297
  readonly stage: string | undefined;
384
298
  }
@@ -402,10 +316,10 @@ type NodeDescriptor = ({
402
316
  */
403
317
  interface PrismaAppConfig {
404
318
  readonly extensions: ExtensionDescriptor[];
405
- readonly state: StateDescriptor;
319
+ readonly state: () => AlchemyStateLayer;
406
320
  }
407
321
  /** Typed identity — exists so `prisma-composer.config.ts` gets checked against PrismaAppConfig where it is written. */
408
322
  declare function defineConfig(config: PrismaAppConfig): PrismaAppConfig;
409
323
  //#endregion
410
- export { containerEnvVarName as A, ProvisionEdge as C, TeardownInput as D, StateDescriptor as E, lowering as F, mergedProviders as I, resolveStateLayer as L, deserializeContainers as M, joinDeployment as N, buildConfig as O, lower as P, PrismaAppConfig as S, ServiceLowering as T, Lowering as _, Bundle as a, PackageInput as b, DeployedEntity as c, ExtensionDescriptor as d, LocateContainerInput as f, LoweredResult as g, LowerOptions as h, AssembleInput as i, defineConfig as j, containerEnv as k, DeployedNode as l, LowerError as m, ApplicationDescriptor as n, ContainerDescriptor as o, LowerContext as p, Artifact as r, ContainerInstance as s, AlchemyStateLayer as t, DeploymentResult as u, NodeDescriptor as v, ProvisionerDescriptor as w, PreflightInput as x, Outputs as y };
411
- //# sourceMappingURL=app-config-DJdU4ubR-BwioNK8j.d.mts.map
324
+ export { resolveStateLayer as A, TeardownInput as C, lower as D, joinDeployment as E, lowering as O, ServiceLowering as S, defineConfig as T, PackageInput as _, Bundle as a, ProvisionEdge as b, DeploymentResult as c, LowerError as d, LowerOptions as f, Outputs as g, NodeDescriptor as h, AssembleInput as i, mergedProviders as k, ExtensionDescriptor as l, Lowering as m, ApplicationDescriptor as n, DeployedEntity as o, LoweredResult as p, Artifact as r, DeployedNode as s, AlchemyStateLayer as t, LowerContext as u, PreflightInput as v, buildConfig as w, ProvisionerDescriptor as x, PrismaAppConfig as y };
325
+ //# sourceMappingURL=app-config-5mXxjaEl-D2Q6MU5g.d.mts.map
@@ -1,5 +1,5 @@
1
1
  //#region ../../0-framework/0-foundation/foundation/dist/assertions.d.mts
2
- //#region src/assertions.d.ts
2
+ //#region src/exports/assertions.d.ts
3
3
  /**
4
4
  * Asserts that a value is defined (not null or undefined).
5
5
  * Use for invariants where the value should always exist at runtime.
@@ -1 +1 @@
1
- {"version":3,"file":"assertions.mjs","names":[],"sources":["../../../0-framework/0-foundation/foundation/dist/assertions.mjs"],"sourcesContent":["//#region src/assertions.ts\n/**\n* Asserts that a value is defined (not null or undefined).\n* Use for invariants where the value should always exist at runtime.\n*\n* @throws Error if value is null or undefined\n*\n* @example\n* ```typescript\n* const port = config.ports[name];\n* assertDefined(port, `Port \"${name}\" not found`);\n* // port is now narrowed to non-nullable\n* ```\n*/\nfunction assertDefined(value, message) {\n\tif (value === null || value === void 0) throw new Error(message);\n}\n/**\n* Asserts that a condition is true.\n* Use for invariants that should always hold at runtime.\n*\n* @throws Error if condition is false\n*\n* @example\n* ```typescript\n* invariant(edges.length > 0, 'A wired module must have at least one connection');\n* ```\n*/\nfunction invariant(condition, message) {\n\tif (!condition) throw new Error(message);\n}\n//#endregion\nexport { assertDefined, invariant };\n\n//# sourceMappingURL=assertions.mjs.map"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,cAAc,OAAO,SAAS;CACtC,IAAI,UAAU,QAAQ,UAAU,KAAK,GAAG,MAAM,IAAI,MAAM,OAAO;AAChE;;;;;;;;;;;;AAYA,SAAS,UAAU,WAAW,SAAS;CACtC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,OAAO;AACxC"}
1
+ {"version":3,"file":"assertions.mjs","names":[],"sources":["../../../0-framework/0-foundation/foundation/dist/assertions.mjs"],"sourcesContent":["//#region src/exports/assertions.ts\n/**\n* Asserts that a value is defined (not null or undefined).\n* Use for invariants where the value should always exist at runtime.\n*\n* @throws Error if value is null or undefined\n*\n* @example\n* ```typescript\n* const port = config.ports[name];\n* assertDefined(port, `Port \"${name}\" not found`);\n* // port is now narrowed to non-nullable\n* ```\n*/\nfunction assertDefined(value, message) {\n\tif (value === null || value === void 0) throw new Error(message);\n}\n/**\n* Asserts that a condition is true.\n* Use for invariants that should always hold at runtime.\n*\n* @throws Error if condition is false\n*\n* @example\n* ```typescript\n* invariant(edges.length > 0, 'A wired module must have at least one connection');\n* ```\n*/\nfunction invariant(condition, message) {\n\tif (!condition) throw new Error(message);\n}\n//#endregion\nexport { assertDefined, invariant };\n\n//# sourceMappingURL=assertions.mjs.map"],"mappings":";;;;;;;;;;;;;;AAcA,SAAS,cAAc,OAAO,SAAS;CACtC,IAAI,UAAU,QAAQ,UAAU,KAAK,GAAG,MAAM,IAAI,MAAM,OAAO;AAChE;;;;;;;;;;;;AAYA,SAAS,UAAU,WAAW,SAAS;CACtC,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,OAAO;AACxC"}
package/dist/bin.mjs CHANGED
@@ -2,9 +2,19 @@
2
2
  import { Cli, Command, Option, UsageError } from "clipanion";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
+ import { spawnSync } from "node:child_process";
6
+ import * as Layer from "effect/Layer";
7
+ import { createManagementApiClient } from "@prisma/management-api-sdk";
8
+ import * as Context from "effect/Context";
9
+ import * as Effect from "effect/Effect";
10
+ import * as Redacted from "effect/Redacted";
11
+ import * as Config from "effect/Config";
12
+ import * as Data from "effect/Data";
13
+ import * as Provider from "alchemy/Provider";
14
+ import { Resource } from "alchemy";
15
+ import * as Schedule from "effect/Schedule";
5
16
  import * as c12 from "c12";
6
17
  import { pathToFileURL } from "node:url";
7
- import { spawnSync } from "node:child_process";
8
18
  //#region ../../0-framework/3-tooling/assemble/dist/index.mjs
9
19
  /**
10
20
  * A user-facing assembly failure with a message that already names the fix
@@ -575,34 +585,155 @@ function Load(root, opts) {
575
585
  throw new LoadError("Load expects a service or module root (received another node kind).");
576
586
  }
577
587
  //#endregion
578
- //#region ../../0-framework/1-core/core/dist/container-transport-DKmKg5JQ.mjs
579
- /** '@prisma/composer-prisma-cloud' → 'PRISMA_COMPOSER_CONTAINER_PRISMA_COMPOSER_PRISMA_CLOUD' */
580
- function containerEnvVarName(extensionId) {
581
- return `PRISMA_COMPOSER_CONTAINER_${extensionId.toUpperCase().replace(/[^A-Z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`;
582
- }
583
- function collisionError(a, b, varName) {
584
- return /* @__PURE__ */ new Error(`Extension ids "${a}" and "${b}" both mangle to the container transport variable "${varName}" — rename one of the extensions.`);
585
- }
586
- function emptySerializeError(extensionId) {
587
- return /* @__PURE__ */ new Error(`Extension "${extensionId}"'s container instance serialized to an empty string — ContainerInstance.serialize() must return a non-empty string.`);
588
- }
589
- /** The env entries the CLI sets on the alchemy process: `{ [containerEnvVarName(id)]: instance.serialize() }` for every resolved instance. */
590
- function containerEnv(instances) {
591
- const env = {};
592
- const ownerByVarName = /* @__PURE__ */ new Map();
593
- for (const [extensionId, instance] of instances) {
594
- const varName = containerEnvVarName(extensionId);
595
- const owner = ownerByVarName.get(varName);
596
- if (owner !== void 0) throw collisionError(owner, extensionId, varName);
597
- ownerByVarName.set(varName, extensionId);
598
- const serialized = instance.serialize();
599
- if (serialized.length === 0) throw emptySerializeError(extensionId);
600
- env[varName] = serialized;
588
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
589
+ /**
590
+ * The Prisma service token used to authenticate Management API calls. Kept
591
+ * as a Redacted value so it never lands in logs or error output.
592
+ */
593
+ var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
594
+ /** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
595
+ const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
596
+ return { token: yield* Config.redacted("PRISMA_SERVICE_TOKEN") };
597
+ }));
598
+ /**
599
+ * The typed Prisma Management API client, built once from the resolved
600
+ * credentials. Providers yield this in their outer Effect and call it inside
601
+ * `reconcile` / `delete`.
602
+ */
603
+ var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
604
+ const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
605
+ const { token } = yield* PrismaCredentials;
606
+ return createManagementApiClient({ token: Redacted.value(token) });
607
+ }));
608
+ /** A non-2xx response from the Management API (or a transport failure). */
609
+ var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
610
+ const attempt = (f) => Effect.tryPromise({
611
+ try: f,
612
+ catch: (cause) => new PrismaApiError({
613
+ status: 0,
614
+ message: String(cause)
615
+ })
616
+ });
617
+ const fail = (r) => Effect.fail(new PrismaApiError({
618
+ status: r.response.status,
619
+ message: JSON.stringify(r.error)
620
+ }));
621
+ /** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
622
+ const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
623
+ /** Fire-and-forget a call, tolerating a 404 (already deleted). */
624
+ const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
625
+ Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
626
+ Resource("Prisma.ComputeService");
627
+ Resource("Prisma.Deployment");
628
+ Resource("Prisma.EnvironmentVariable");
629
+ Resource("PrismaCloud.ServiceKey");
630
+ Resource("Prisma.Connection");
631
+ Resource("Prisma.Database");
632
+ Resource("Prisma.Project");
633
+ //#endregion
634
+ //#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
635
+ /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
636
+ var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
637
+ const listAllProjects = (client) => Effect.gen(function* () {
638
+ const projects = [];
639
+ let cursor;
640
+ for (;;) {
641
+ const query = cursor === void 0 ? {} : { cursor };
642
+ const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
643
+ projects.push(...page.data);
644
+ if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
645
+ cursor = page.pagination.nextCursor;
601
646
  }
602
- return env;
603
- }
647
+ return projects;
648
+ });
649
+ /**
650
+ * Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
651
+ * bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
652
+ * one (the same normalization `state/bootstrap.ts` applies to the same
653
+ * `/v1/projects` listing).
654
+ */
655
+ const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
656
+ /**
657
+ * Finds the app's Project by name in the workspace — PDP allows duplicate
658
+ * project names, so more than one can match; the oldest wins. Creates one
659
+ * if none match, unless `ensure` is `false` (find-only — `destroy`), in
660
+ * which case an absent Project fails with `ContainerNotFoundError`. No
661
+ * ownership marker and no `--project` override (both deferred — see
662
+ * ADR-0019).
663
+ */
664
+ const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
665
+ 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];
666
+ if (oldest !== void 0) return oldest.id;
667
+ if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
668
+ return (yield* call(() => client.POST("/v1/projects", { body: {
669
+ name: appName,
670
+ workspaceId
671
+ } }))).data.id;
672
+ });
673
+ const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
674
+ path: { projectId },
675
+ query: { gitName }
676
+ } })).pipe(Effect.map((page) => page.data[0]?.id));
677
+ /**
678
+ * Finds the stage's Branch by its exact `gitName`, creating it if absent
679
+ * unless `ensure` is `false` (find-only — `destroy`), in which case an
680
+ * absent Branch fails with `ContainerNotFoundError`. The Management API has
681
+ * no server-side "create-or-return" idempotency (`POST
682
+ * /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
683
+ * field to make that a no-op), so idempotency is client-side: observe
684
+ * first, and on a racing 409 from create, re-observe rather than fail.
685
+ */
686
+ const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
687
+ const existing = yield* findBranchId(client, projectId, gitName);
688
+ if (existing !== void 0) return existing;
689
+ if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
690
+ appName,
691
+ stage: gitName
692
+ }));
693
+ return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
694
+ params: { path: { projectId } },
695
+ body: { gitName }
696
+ })).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)));
697
+ });
698
+ /**
699
+ * Resolves the two containers a stage's deploy runs into (ADR-0019): the
700
+ * app's **Project**, found-or-created by name, and — for a named stage
701
+ * only — its **Branch**, found-or-created by `gitName`. The default stage
702
+ * (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
703
+ * false` (`destroy`), nothing is created — an absent Project or Branch
704
+ * fails with `ContainerNotFoundError` instead.
705
+ */
706
+ const resolveContainer = (opts) => Effect.gen(function* () {
707
+ const client = yield* ManagementClient;
708
+ const ensure = opts.ensure ?? true;
709
+ const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
710
+ if (opts.stage === void 0) return { projectId };
711
+ return {
712
+ projectId,
713
+ branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
714
+ };
715
+ });
716
+ /**
717
+ * Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
718
+ * the Branch still has live members or is the production/default Branch —
719
+ * that surfaces as a `PrismaApiError`.
720
+ */
721
+ const deleteBranch = (branchId) => Effect.gen(function* () {
722
+ const client = yield* ManagementClient;
723
+ yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
724
+ });
725
+ /**
726
+ * Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
727
+ * 400 if the Project still has live dependencies (e.g. another stage's
728
+ * Branch/resources) — that surfaces as a `PrismaApiError`.
729
+ */
730
+ const deleteProject = (projectId) => Effect.gen(function* () {
731
+ const client = yield* ManagementClient;
732
+ yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
733
+ });
734
+ Provider.ProviderCollection()("Prisma");
604
735
  //#endregion
605
- //#region ../../0-framework/3-tooling/cli/dist/cli-Bc6_i0ls.mjs
736
+ //#region ../../0-framework/3-tooling/cli/dist/cli-BeT-XTbX.mjs
606
737
  /**
607
738
  * A user-facing failure with a message that already names the fix (deploy-cli.md
608
739
  * § Error surface). `bin.ts` catches this — and any other Error, including
@@ -614,6 +745,89 @@ var CliError = class extends Error {
614
745
  this.name = "CliError";
615
746
  }
616
747
  };
748
+ /**
749
+ * Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
750
+ * via `@internal/lowering`'s `resolveContainer`, before the generated stack file
751
+ * runs — `deploy` creates-if-absent, `destroy` finds only.
752
+ */
753
+ /** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
754
+ function validateStageName(stage) {
755
+ const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
756
+ if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
757
+ if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
758
+ }
759
+ async function ensureContainers(input, deps) {
760
+ const env = input.env ?? process.env;
761
+ const workspaceId = env["PRISMA_WORKSPACE_ID"];
762
+ if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
763
+ if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
764
+ if (input.stage !== void 0) validateStageName(input.stage);
765
+ const program = resolveContainer({
766
+ workspaceId,
767
+ appName: input.appName,
768
+ ...input.stage !== void 0 ? { stage: input.stage } : {},
769
+ ensure: input.command === "deploy"
770
+ }).pipe(Effect.map((c) => ({
771
+ ok: true,
772
+ container: c
773
+ })), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
774
+ ok: false,
775
+ message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
776
+ })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
777
+ ok: false,
778
+ message: `Prisma Management API error resolving containers: ${e.message}.`
779
+ })));
780
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
781
+ const outcome = await Effect.runPromise(provided);
782
+ if (!outcome.ok) throw new CliError(outcome.message);
783
+ return outcome.container;
784
+ }
785
+ /**
786
+ * Soft-deletes a named stage's Branch after a successful `alchemy destroy`
787
+ * has removed its members (spec §10) — the Management API refuses to delete
788
+ * a Branch that still has live members.
789
+ */
790
+ async function deleteStageBranch(input, deps) {
791
+ const env = input.env ?? process.env;
792
+ if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
793
+ const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
794
+ ok: false,
795
+ message: `Failed to delete the stage Branch: ${e.message}.`
796
+ })));
797
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
798
+ const outcome = await Effect.runPromise(provided);
799
+ if (!outcome.ok) throw new CliError(outcome.message);
800
+ }
801
+ /**
802
+ * Best-effort cleanup after a successful `--production` destroy: removes
803
+ * the app's Project so hand-run stacks don't accumulate as empty Projects
804
+ * (they eventually hit the workspace's plan limit). Unlike `deleteStageBranch`,
805
+ * this never throws: the destroy itself already succeeded, and the API's own
806
+ * 400 ("still has dependencies") is the only check that matters — failing
807
+ * the command over a cleanup step would be worse than leaving a Project shell.
808
+ */
809
+ async function deleteAppProject(input, deps) {
810
+ const env = input.env ?? process.env;
811
+ if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
812
+ console.warn(`Skipped removing the Project (${input.projectId}): PRISMA_SERVICE_TOKEN is not set.`);
813
+ return;
814
+ }
815
+ const program = deleteProject(input.projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
816
+ ok: false,
817
+ error: e
818
+ })));
819
+ const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
820
+ const outcome = await Effect.runPromise(provided);
821
+ if (outcome.ok) {
822
+ console.log(`Removed the Project (${input.projectId}) — nothing was left in it.`);
823
+ return;
824
+ }
825
+ if (outcome.error.status === 400) {
826
+ console.log(`Kept the Project (${input.projectId}) — it still has another stage's resources.`);
827
+ return;
828
+ }
829
+ console.warn(`Could not remove the Project (${input.projectId}) after destroy: ${outcome.error.message}.`);
830
+ }
617
831
  /** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
618
832
  const GENERATED_DIR = ".prisma-composer";
619
833
  const GENERATED_FILE = "alchemy.run.ts";
@@ -716,8 +930,7 @@ function validateConfigShape(loaded, configPath) {
716
930
  if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
717
931
  seen.add(id);
718
932
  }
719
- const state = loaded["state"];
720
- if (!isRecord(state) || typeof state["extension"] !== "string" || typeof state["create"] !== "function") throw fieldError("state", "must be a state descriptor (e.g. prismaState())");
933
+ if (typeof loaded["state"] !== "function") throw fieldError("state", "must be a function returning the deploy state layer (e.g. () => prismaState())");
721
934
  return blindCast(loaded);
722
935
  }
723
936
  /**
@@ -776,7 +989,7 @@ function resolveAlchemyBin(startDir) {
776
989
  dir = parent;
777
990
  }
778
991
  }
779
- /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
992
+ /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus the resolved Project/Branch ids. */
780
993
  function runAlchemy(input) {
781
994
  const bin = resolveAlchemyBin(input.cwd);
782
995
  const args = [
@@ -790,7 +1003,8 @@ function runAlchemy(input) {
790
1003
  stdio: "inherit",
791
1004
  env: {
792
1005
  ...input.env ?? process.env,
793
- ...input.containerEnv
1006
+ PRISMA_PROJECT_ID: input.projectId,
1007
+ ...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
794
1008
  }
795
1009
  });
796
1010
  if (result.error !== void 0) throw result.error;
@@ -816,12 +1030,6 @@ function validateRegistryCoverage(graph, config) {
816
1030
  lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
817
1031
  }
818
1032
  }
819
- /** A stage name must be a valid git ref (deploy-cli.md) — checked via `git check-ref-format`, never silently normalized. Runs before anything platform-specific. */
820
- function validateStageName(stage) {
821
- const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
822
- if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
823
- if (result.status !== 0) throw new CliError(`Invalid --stage "${stage}": must be a valid git ref name (git check-ref-format rejected "refs/heads/${stage}").`);
824
- }
825
1033
  /**
826
1034
  * Argument parsing (clipanion — prisma-next's CLI idiom, see
827
1035
  * prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
@@ -917,7 +1125,6 @@ async function run(argv, deps = {}) {
917
1125
  throw error;
918
1126
  }
919
1127
  const stage = effectiveStage(args);
920
- if (stage !== void 0) validateStageName(stage);
921
1128
  const cwd = process.cwd();
922
1129
  if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
923
1130
  const resolvedEntryPath = path.resolve(cwd, args.entry);
@@ -937,32 +1144,18 @@ async function run(argv, deps = {}) {
937
1144
  if (args.command === "destroy" && error instanceof Error) throw new CliError(`${error.message}\n\ndestroy evaluates the same stack program as deploy, which packages the built artifacts — so the app must be built first. Run the build, then retry the destroy.`);
938
1145
  throw error;
939
1146
  }
940
- const containers = /* @__PURE__ */ new Map();
941
- for (const extension of config.extensions) {
942
- if (extension.container === void 0) continue;
943
- try {
944
- if (args.command === "deploy") containers.set(extension.id, await extension.container.ensure({
945
- appName: name,
946
- stage
947
- }));
948
- else {
949
- const instance = await extension.container.locate({
950
- appName: name,
951
- stage
952
- });
953
- if (instance === void 0) throw new CliError(`Nothing deployed for ${name}${stage !== void 0 ? `/${stage}` : ""} — deploy it first.`);
954
- containers.set(extension.id, instance);
955
- }
956
- } catch (error) {
957
- throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
958
- }
959
- }
1147
+ const { projectId, branchId } = await (deps.ensureContainers ?? ensureContainers)({
1148
+ command: args.command,
1149
+ appName: name,
1150
+ stage
1151
+ });
960
1152
  if (args.command === "deploy") for (const extension of config.extensions) {
961
1153
  if (extension.preflight === void 0) continue;
962
1154
  try {
963
1155
  await extension.preflight({
964
1156
  graph,
965
- container: containers.get(extension.id),
1157
+ projectId,
1158
+ branchId,
966
1159
  stage
967
1160
  });
968
1161
  } catch (error) {
@@ -982,36 +1175,28 @@ async function run(argv, deps = {}) {
982
1175
  stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
983
1176
  cwd,
984
1177
  stage,
985
- containerEnv: containerEnv(containers)
1178
+ projectId,
1179
+ ...branchId !== void 0 ? { branchId } : {}
986
1180
  });
987
1181
  if (status !== 0) {
988
1182
  console.error(`\nGenerated stack file: ${stackPath}`);
989
1183
  console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
990
1184
  return status;
991
1185
  }
992
- if (args.command === "destroy") {
993
- for (const extension of config.extensions) {
994
- if (extension.teardown === void 0) continue;
995
- try {
996
- await extension.teardown({
997
- container: containers.get(extension.id),
998
- stage
999
- });
1000
- } catch (error) {
1001
- throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1002
- }
1003
- }
1004
- for (const extension of config.extensions) {
1005
- if (extension.container === void 0) continue;
1006
- const instance = containers.get(extension.id);
1007
- if (instance === void 0) continue;
1008
- try {
1009
- await extension.container.remove(instance);
1010
- } catch (error) {
1011
- throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1012
- }
1186
+ if (args.command === "destroy") for (const extension of config.extensions) {
1187
+ if (extension.teardown === void 0) continue;
1188
+ try {
1189
+ await extension.teardown({
1190
+ projectId,
1191
+ branchId,
1192
+ stage
1193
+ });
1194
+ } catch (error) {
1195
+ throw error instanceof CliError ? error : new CliError(error instanceof Error ? error.message : String(error));
1013
1196
  }
1014
1197
  }
1198
+ if (args.command === "destroy" && branchId !== void 0) await (deps.deleteBranch ?? ((input) => deleteStageBranch(input)))({ branchId });
1199
+ else if (args.command === "destroy") await (deps.deleteProject ?? ((input) => deleteAppProject(input)))({ projectId });
1015
1200
  return status;
1016
1201
  } catch (error) {
1017
1202
  console.error(`\nGenerated stack file: ${stackPath}`);