@prisma/composer 0.1.0-dev.16 → 0.1.0-dev.18
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/{app-config-FyPJc4X--D8hIqPlm.d.mts → app-config-DJdU4ubR-BwioNK8j.d.mts} +101 -15
- package/dist/bin.mjs +81 -268
- package/dist/bin.mjs.map +1 -1
- package/dist/config-ByZICgry.d.mts +1 -0
- package/dist/config.d.mts +3 -3
- package/dist/config.mjs +2 -1
- package/dist/config.mjs.map +1 -1
- package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +45 -0
- package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +1 -0
- package/dist/deploy-ByZICgry.d.mts +1 -0
- package/dist/deploy.d.mts +2 -2
- package/dist/deploy.mjs +12 -5
- package/dist/deploy.mjs.map +1 -1
- package/dist/nextjs-control.d.mts +3 -3
- package/dist/node-control.d.mts +3 -3
- package/dist/report.d.mts +2 -2
- package/package.json +10 -10
- package/dist/config-DWUbTR4B.d.mts +0 -1
- package/dist/deploy-DWUbTR4B.d.mts +0 -1
|
@@ -5,7 +5,70 @@ 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-
|
|
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
|
|
9
72
|
//#region src/control/deploy.d.ts
|
|
10
73
|
/** The Layer shape every Alchemy state store must satisfy — what `LowerOptions.state` and `PrismaAppConfig.state` both traffic in. */
|
|
11
74
|
type AlchemyStateLayer = Layer.Layer<State, never, StackServices>;
|
|
@@ -92,6 +155,13 @@ interface LowerContext {
|
|
|
92
155
|
* it with its own type guard.
|
|
93
156
|
*/
|
|
94
157
|
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;
|
|
95
165
|
/** Already-lowered deps (topo order). */
|
|
96
166
|
readonly lowered: ReadonlyMap<NodeId, Outputs>;
|
|
97
167
|
/** Every provisioned param value minted this lowering, keyed by edge id (ADR-0031). */
|
|
@@ -219,10 +289,12 @@ declare function joinDeployment(graph: Graph, entries: readonly {
|
|
|
219
289
|
}[]): readonly DeployedNode[];
|
|
220
290
|
/**
|
|
221
291
|
* The state-layer precedence a deploy resolves to: an explicit opts.state
|
|
222
|
-
* always wins; failing that, the config's own (required) state
|
|
223
|
-
*
|
|
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.
|
|
224
296
|
*/
|
|
225
|
-
declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig): AlchemyStateLayer;
|
|
297
|
+
declare function resolveStateLayer(opts: LowerOptions, config: PrismaAppConfig, containers: ReadonlyMap<string, ContainerInstance>): AlchemyStateLayer;
|
|
226
298
|
/**
|
|
227
299
|
* All configured extensions' providers merged, config array order — an
|
|
228
300
|
* extension without `providers` is skipped; no used-extensions-only
|
|
@@ -275,24 +347,38 @@ interface ExtensionDescriptor {
|
|
|
275
347
|
* fail the command handles that itself. Async: it talks to the platform.
|
|
276
348
|
*/
|
|
277
349
|
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;
|
|
278
368
|
}
|
|
279
369
|
/** The resolved deploy context handed to an extension's `preflight` hook. */
|
|
280
370
|
interface PreflightInput {
|
|
281
371
|
/** The loaded application graph — the manifest of prerequisites is read from it (`provisionManifest`). */
|
|
282
372
|
readonly graph: Graph;
|
|
283
|
-
/** The resolved
|
|
284
|
-
readonly
|
|
285
|
-
/** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
|
|
286
|
-
readonly branchId: string | undefined;
|
|
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;
|
|
287
375
|
/** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
|
|
288
376
|
readonly stage: string | undefined;
|
|
289
377
|
}
|
|
290
378
|
/** The resolved destroy context handed to an extension's `teardown` hook. */
|
|
291
379
|
interface TeardownInput {
|
|
292
|
-
/** The resolved
|
|
293
|
-
readonly
|
|
294
|
-
/** The resolved Branch id for a named stage; `undefined` for the default (production) stage. */
|
|
295
|
-
readonly branchId: string | undefined;
|
|
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;
|
|
296
382
|
/** The stage name (`--stage`), or `undefined` for the default stage — for diagnostics/scope. */
|
|
297
383
|
readonly stage: string | undefined;
|
|
298
384
|
}
|
|
@@ -316,10 +402,10 @@ type NodeDescriptor = ({
|
|
|
316
402
|
*/
|
|
317
403
|
interface PrismaAppConfig {
|
|
318
404
|
readonly extensions: ExtensionDescriptor[];
|
|
319
|
-
readonly state:
|
|
405
|
+
readonly state: StateDescriptor;
|
|
320
406
|
}
|
|
321
407
|
/** Typed identity — exists so `prisma-composer.config.ts` gets checked against PrismaAppConfig where it is written. */
|
|
322
408
|
declare function defineConfig(config: PrismaAppConfig): PrismaAppConfig;
|
|
323
409
|
//#endregion
|
|
324
|
-
export {
|
|
325
|
-
//# sourceMappingURL=app-config-
|
|
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
|
package/dist/bin.mjs
CHANGED
|
@@ -2,19 +2,9 @@
|
|
|
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 { createManagementApiClient } from "@prisma/management-api-sdk";
|
|
7
|
-
import * as Context from "effect/Context";
|
|
8
|
-
import * as Effect from "effect/Effect";
|
|
9
|
-
import * as Layer from "effect/Layer";
|
|
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";
|
|
16
5
|
import * as c12 from "c12";
|
|
17
6
|
import { pathToFileURL } from "node:url";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
18
8
|
//#region ../../0-framework/3-tooling/assemble/dist/index.mjs
|
|
19
9
|
/**
|
|
20
10
|
* A user-facing assembly failure with a message that already names the fix
|
|
@@ -585,157 +575,34 @@ function Load(root, opts) {
|
|
|
585
575
|
throw new LoadError("Load expects a service or module root (received another node kind).");
|
|
586
576
|
}
|
|
587
577
|
//#endregion
|
|
588
|
-
//#region ../../
|
|
589
|
-
/**
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
}));
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
*/
|
|
603
|
-
|
|
604
|
-
const
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
-
Resource("Prisma.Bucket");
|
|
626
|
-
Resource("Prisma.BucketKey");
|
|
627
|
-
Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
|
|
628
|
-
Resource("Prisma.ComputeService");
|
|
629
|
-
Resource("Prisma.Deployment");
|
|
630
|
-
Resource("Prisma.EnvironmentVariable");
|
|
631
|
-
Resource("PrismaCloud.ServiceKey");
|
|
632
|
-
Resource("Prisma.Connection");
|
|
633
|
-
Resource("Prisma.Database");
|
|
634
|
-
Resource("Prisma.Project");
|
|
635
|
-
//#endregion
|
|
636
|
-
//#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
|
|
637
|
-
/** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
|
|
638
|
-
var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
|
|
639
|
-
const listAllProjects = (client) => Effect.gen(function* () {
|
|
640
|
-
const projects = [];
|
|
641
|
-
let cursor;
|
|
642
|
-
for (;;) {
|
|
643
|
-
const query = cursor === void 0 ? {} : { cursor };
|
|
644
|
-
const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
|
|
645
|
-
projects.push(...page.data);
|
|
646
|
-
if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
|
|
647
|
-
cursor = page.pagination.nextCursor;
|
|
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;
|
|
648
601
|
}
|
|
649
|
-
return
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
|
|
653
|
-
* bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
|
|
654
|
-
* one (the same normalization `state/bootstrap.ts` applies to the same
|
|
655
|
-
* `/v1/projects` listing).
|
|
656
|
-
*/
|
|
657
|
-
const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
|
|
658
|
-
/**
|
|
659
|
-
* Finds the app's Project by name in the workspace — PDP allows duplicate
|
|
660
|
-
* project names, so more than one can match; the oldest wins. Creates one
|
|
661
|
-
* if none match, unless `ensure` is `false` (find-only — `destroy`), in
|
|
662
|
-
* which case an absent Project fails with `ContainerNotFoundError`. No
|
|
663
|
-
* ownership marker and no `--project` override (both deferred — see
|
|
664
|
-
* ADR-0019).
|
|
665
|
-
*/
|
|
666
|
-
const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
|
|
667
|
-
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];
|
|
668
|
-
if (oldest !== void 0) return oldest.id;
|
|
669
|
-
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
|
|
670
|
-
return (yield* call(() => client.POST("/v1/projects", { body: {
|
|
671
|
-
name: appName,
|
|
672
|
-
workspaceId
|
|
673
|
-
} }))).data.id;
|
|
674
|
-
});
|
|
675
|
-
const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
|
|
676
|
-
path: { projectId },
|
|
677
|
-
query: { gitName }
|
|
678
|
-
} })).pipe(Effect.map((page) => page.data[0]?.id));
|
|
679
|
-
/**
|
|
680
|
-
* Finds the stage's Branch by its exact `gitName`, creating it if absent
|
|
681
|
-
* unless `ensure` is `false` (find-only — `destroy`), in which case an
|
|
682
|
-
* absent Branch fails with `ContainerNotFoundError`. The Management API has
|
|
683
|
-
* no server-side "create-or-return" idempotency (`POST
|
|
684
|
-
* /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
|
|
685
|
-
* field to make that a no-op), so idempotency is client-side: observe
|
|
686
|
-
* first, and on a racing 409 from create, re-observe rather than fail.
|
|
687
|
-
*/
|
|
688
|
-
const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
|
|
689
|
-
const existing = yield* findBranchId(client, projectId, gitName);
|
|
690
|
-
if (existing !== void 0) return existing;
|
|
691
|
-
if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
|
|
692
|
-
appName,
|
|
693
|
-
stage: gitName
|
|
694
|
-
}));
|
|
695
|
-
return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
|
|
696
|
-
params: { path: { projectId } },
|
|
697
|
-
body: { gitName }
|
|
698
|
-
})).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)));
|
|
699
|
-
});
|
|
700
|
-
/**
|
|
701
|
-
* Resolves the two containers a stage's deploy runs into (ADR-0019): the
|
|
702
|
-
* app's **Project**, found-or-created by name, and — for a named stage
|
|
703
|
-
* only — its **Branch**, found-or-created by `gitName`. The default stage
|
|
704
|
-
* (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
|
|
705
|
-
* false` (`destroy`), nothing is created — an absent Project or Branch
|
|
706
|
-
* fails with `ContainerNotFoundError` instead.
|
|
707
|
-
*/
|
|
708
|
-
const resolveContainer = (opts) => Effect.gen(function* () {
|
|
709
|
-
const client = yield* ManagementClient;
|
|
710
|
-
const ensure = opts.ensure ?? true;
|
|
711
|
-
const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
|
|
712
|
-
if (opts.stage === void 0) return { projectId };
|
|
713
|
-
return {
|
|
714
|
-
projectId,
|
|
715
|
-
branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
|
|
716
|
-
};
|
|
717
|
-
});
|
|
718
|
-
/**
|
|
719
|
-
* Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
|
|
720
|
-
* the Branch still has live members or is the production/default Branch —
|
|
721
|
-
* that surfaces as a `PrismaApiError`.
|
|
722
|
-
*/
|
|
723
|
-
const deleteBranch = (branchId) => Effect.gen(function* () {
|
|
724
|
-
const client = yield* ManagementClient;
|
|
725
|
-
yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
|
|
726
|
-
});
|
|
727
|
-
/**
|
|
728
|
-
* Deletes a Project. Tolerates a 404 (already gone). The API refuses with a
|
|
729
|
-
* 400 if the Project still has live dependencies (e.g. another stage's
|
|
730
|
-
* Branch/resources) — that surfaces as a `PrismaApiError`.
|
|
731
|
-
*/
|
|
732
|
-
const deleteProject = (projectId) => Effect.gen(function* () {
|
|
733
|
-
const client = yield* ManagementClient;
|
|
734
|
-
yield* callVoid(() => client.DELETE("/v1/projects/{id}", { params: { path: { id: projectId } } }));
|
|
735
|
-
});
|
|
736
|
-
Provider.ProviderCollection()("Prisma");
|
|
602
|
+
return env;
|
|
603
|
+
}
|
|
737
604
|
//#endregion
|
|
738
|
-
//#region ../../0-framework/3-tooling/cli/dist/cli-
|
|
605
|
+
//#region ../../0-framework/3-tooling/cli/dist/cli-Bc6_i0ls.mjs
|
|
739
606
|
/**
|
|
740
607
|
* A user-facing failure with a message that already names the fix (deploy-cli.md
|
|
741
608
|
* § Error surface). `bin.ts` catches this — and any other Error, including
|
|
@@ -747,89 +614,6 @@ var CliError = class extends Error {
|
|
|
747
614
|
this.name = "CliError";
|
|
748
615
|
}
|
|
749
616
|
};
|
|
750
|
-
/**
|
|
751
|
-
* Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
|
|
752
|
-
* via `@internal/lowering`'s `resolveContainer`, before the generated stack file
|
|
753
|
-
* runs — `deploy` creates-if-absent, `destroy` finds only.
|
|
754
|
-
*/
|
|
755
|
-
/** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
|
|
756
|
-
function validateStageName(stage) {
|
|
757
|
-
const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
|
|
758
|
-
if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
|
|
759
|
-
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}").`);
|
|
760
|
-
}
|
|
761
|
-
async function ensureContainers(input, deps) {
|
|
762
|
-
const env = input.env ?? process.env;
|
|
763
|
-
const workspaceId = env["PRISMA_WORKSPACE_ID"];
|
|
764
|
-
if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
|
|
765
|
-
if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
766
|
-
if (input.stage !== void 0) validateStageName(input.stage);
|
|
767
|
-
const program = resolveContainer({
|
|
768
|
-
workspaceId,
|
|
769
|
-
appName: input.appName,
|
|
770
|
-
...input.stage !== void 0 ? { stage: input.stage } : {},
|
|
771
|
-
ensure: input.command === "deploy"
|
|
772
|
-
}).pipe(Effect.map((c) => ({
|
|
773
|
-
ok: true,
|
|
774
|
-
container: c
|
|
775
|
-
})), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
|
|
776
|
-
ok: false,
|
|
777
|
-
message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
|
|
778
|
-
})), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
779
|
-
ok: false,
|
|
780
|
-
message: `Prisma Management API error resolving containers: ${e.message}.`
|
|
781
|
-
})));
|
|
782
|
-
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
783
|
-
const outcome = await Effect.runPromise(provided);
|
|
784
|
-
if (!outcome.ok) throw new CliError(outcome.message);
|
|
785
|
-
return outcome.container;
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Soft-deletes a named stage's Branch after a successful `alchemy destroy`
|
|
789
|
-
* has removed its members (spec §10) — the Management API refuses to delete
|
|
790
|
-
* a Branch that still has live members.
|
|
791
|
-
*/
|
|
792
|
-
async function deleteStageBranch(input, deps) {
|
|
793
|
-
const env = input.env ?? process.env;
|
|
794
|
-
if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
|
|
795
|
-
const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
796
|
-
ok: false,
|
|
797
|
-
message: `Failed to delete the stage Branch: ${e.message}.`
|
|
798
|
-
})));
|
|
799
|
-
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
800
|
-
const outcome = await Effect.runPromise(provided);
|
|
801
|
-
if (!outcome.ok) throw new CliError(outcome.message);
|
|
802
|
-
}
|
|
803
|
-
/**
|
|
804
|
-
* Best-effort cleanup after a successful `--production` destroy: removes
|
|
805
|
-
* the app's Project so hand-run stacks don't accumulate as empty Projects
|
|
806
|
-
* (they eventually hit the workspace's plan limit). Unlike `deleteStageBranch`,
|
|
807
|
-
* this never throws: the destroy itself already succeeded, and the API's own
|
|
808
|
-
* 400 ("still has dependencies") is the only check that matters — failing
|
|
809
|
-
* the command over a cleanup step would be worse than leaving a Project shell.
|
|
810
|
-
*/
|
|
811
|
-
async function deleteAppProject(input, deps) {
|
|
812
|
-
const env = input.env ?? process.env;
|
|
813
|
-
if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) {
|
|
814
|
-
console.warn(`Skipped removing the Project (${input.projectId}): PRISMA_SERVICE_TOKEN is not set.`);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
const program = deleteProject(input.projectId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
|
|
818
|
-
ok: false,
|
|
819
|
-
error: e
|
|
820
|
-
})));
|
|
821
|
-
const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
|
|
822
|
-
const outcome = await Effect.runPromise(provided);
|
|
823
|
-
if (outcome.ok) {
|
|
824
|
-
console.log(`Removed the Project (${input.projectId}) — nothing was left in it.`);
|
|
825
|
-
return;
|
|
826
|
-
}
|
|
827
|
-
if (outcome.error.status === 400) {
|
|
828
|
-
console.log(`Kept the Project (${input.projectId}) — it still has another stage's resources.`);
|
|
829
|
-
return;
|
|
830
|
-
}
|
|
831
|
-
console.warn(`Could not remove the Project (${input.projectId}) after destroy: ${outcome.error.message}.`);
|
|
832
|
-
}
|
|
833
617
|
/** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
|
|
834
618
|
const GENERATED_DIR = ".prisma-composer";
|
|
835
619
|
const GENERATED_FILE = "alchemy.run.ts";
|
|
@@ -932,7 +716,8 @@ function validateConfigShape(loaded, configPath) {
|
|
|
932
716
|
if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
|
|
933
717
|
seen.add(id);
|
|
934
718
|
}
|
|
935
|
-
|
|
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())");
|
|
936
721
|
return blindCast(loaded);
|
|
937
722
|
}
|
|
938
723
|
/**
|
|
@@ -991,7 +776,7 @@ function resolveAlchemyBin(startDir) {
|
|
|
991
776
|
dir = parent;
|
|
992
777
|
}
|
|
993
778
|
}
|
|
994
|
-
/** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus
|
|
779
|
+
/** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
|
|
995
780
|
function runAlchemy(input) {
|
|
996
781
|
const bin = resolveAlchemyBin(input.cwd);
|
|
997
782
|
const args = [
|
|
@@ -1005,8 +790,7 @@ function runAlchemy(input) {
|
|
|
1005
790
|
stdio: "inherit",
|
|
1006
791
|
env: {
|
|
1007
792
|
...input.env ?? process.env,
|
|
1008
|
-
|
|
1009
|
-
...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
|
|
793
|
+
...input.containerEnv
|
|
1010
794
|
}
|
|
1011
795
|
});
|
|
1012
796
|
if (result.error !== void 0) throw result.error;
|
|
@@ -1032,6 +816,12 @@ function validateRegistryCoverage(graph, config) {
|
|
|
1032
816
|
lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
|
|
1033
817
|
}
|
|
1034
818
|
}
|
|
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
|
+
}
|
|
1035
825
|
/**
|
|
1036
826
|
* Argument parsing (clipanion — prisma-next's CLI idiom, see
|
|
1037
827
|
* prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
|
|
@@ -1127,6 +917,7 @@ async function run(argv, deps = {}) {
|
|
|
1127
917
|
throw error;
|
|
1128
918
|
}
|
|
1129
919
|
const stage = effectiveStage(args);
|
|
920
|
+
if (stage !== void 0) validateStageName(stage);
|
|
1130
921
|
const cwd = process.cwd();
|
|
1131
922
|
if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
|
|
1132
923
|
const resolvedEntryPath = path.resolve(cwd, args.entry);
|
|
@@ -1146,18 +937,32 @@ async function run(argv, deps = {}) {
|
|
|
1146
937
|
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.`);
|
|
1147
938
|
throw error;
|
|
1148
939
|
}
|
|
1149
|
-
const
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
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
|
+
}
|
|
1154
960
|
if (args.command === "deploy") for (const extension of config.extensions) {
|
|
1155
961
|
if (extension.preflight === void 0) continue;
|
|
1156
962
|
try {
|
|
1157
963
|
await extension.preflight({
|
|
1158
964
|
graph,
|
|
1159
|
-
|
|
1160
|
-
branchId,
|
|
965
|
+
container: containers.get(extension.id),
|
|
1161
966
|
stage
|
|
1162
967
|
});
|
|
1163
968
|
} catch (error) {
|
|
@@ -1177,28 +982,36 @@ async function run(argv, deps = {}) {
|
|
|
1177
982
|
stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
|
|
1178
983
|
cwd,
|
|
1179
984
|
stage,
|
|
1180
|
-
|
|
1181
|
-
...branchId !== void 0 ? { branchId } : {}
|
|
985
|
+
containerEnv: containerEnv(containers)
|
|
1182
986
|
});
|
|
1183
987
|
if (status !== 0) {
|
|
1184
988
|
console.error(`\nGenerated stack file: ${stackPath}`);
|
|
1185
989
|
console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
|
|
1186
990
|
return status;
|
|
1187
991
|
}
|
|
1188
|
-
if (args.command === "destroy")
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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
|
+
}
|
|
1198
1013
|
}
|
|
1199
1014
|
}
|
|
1200
|
-
if (args.command === "destroy" && branchId !== void 0) await (deps.deleteBranch ?? ((input) => deleteStageBranch(input)))({ branchId });
|
|
1201
|
-
else if (args.command === "destroy") await (deps.deleteProject ?? ((input) => deleteAppProject(input)))({ projectId });
|
|
1202
1015
|
return status;
|
|
1203
1016
|
} catch (error) {
|
|
1204
1017
|
console.error(`\nGenerated stack file: ${stackPath}`);
|