@prisma/composer 0.1.0 → 0.2.0

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 (55) hide show
  1. package/dist/app-config-DJdU4ubR-BwioNK8j.d.mts +411 -0
  2. package/dist/assertions.d.mts +1 -1
  3. package/dist/bin.mjs +166 -256
  4. package/dist/bin.mjs.map +1 -1
  5. package/dist/casts.d.mts +1 -1
  6. package/dist/config-ByZICgry.d.mts +1 -0
  7. package/dist/config.d.mts +3 -2
  8. package/dist/config.mjs +2 -1
  9. package/dist/config.mjs.map +1 -1
  10. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs +45 -0
  11. package/dist/container-transport-DKmKg5JQ-DKWs0ubK.mjs.map +1 -0
  12. package/dist/deploy-ByZICgry.d.mts +1 -0
  13. package/dist/deploy.d.mts +3 -2
  14. package/dist/deploy.mjs +181 -23
  15. package/dist/deploy.mjs.map +1 -1
  16. package/dist/{dist-zBU8ASQW.mjs → dist-B0axxnBf.mjs} +16 -4
  17. package/dist/dist-B0axxnBf.mjs.map +1 -0
  18. package/dist/graph-B7NcPiOr-aSUOCGTH.d.mts +18 -0
  19. package/dist/{graph-BYdCQKya-BI0njTow.mjs → graph-BmrUEdo9-6Oq1hSmS.mjs} +102 -9
  20. package/dist/graph-BmrUEdo9-6Oq1hSmS.mjs.map +1 -0
  21. package/dist/{config-ob5OhCSP-sP3GW3uu.d.mts → graph-types-BgT9UEdm-Bz-_OcJH.d.mts} +205 -114
  22. package/dist/{index-CZSc9drz.d.mts → index-B2DJ5CN4.d.mts} +5 -20
  23. package/dist/index.d.mts +4 -3
  24. package/dist/index.mjs +3 -3
  25. package/dist/{index-Dh4Zro0y.d.mts → nextjs-DLyeRR7M-B9ukbB2L.d.mts} +6 -5
  26. package/dist/nextjs-control.d.mts +6 -5
  27. package/dist/nextjs-control.mjs +7 -10
  28. package/dist/nextjs-control.mjs.map +1 -1
  29. package/dist/nextjs.d.mts +2 -2
  30. package/dist/nextjs.mjs.map +1 -1
  31. package/dist/node-control.d.mts +5 -3
  32. package/dist/node-control.mjs +96 -24
  33. package/dist/node-control.mjs.map +1 -1
  34. package/dist/node.d.mts +18 -5
  35. package/dist/node.mjs +2 -1
  36. package/dist/node.mjs.map +1 -1
  37. package/dist/report.d.mts +17 -0
  38. package/dist/report.mjs +79 -0
  39. package/dist/report.mjs.map +1 -0
  40. package/dist/service-rpc.d.mts +61 -0
  41. package/dist/service-rpc.mjs +353 -0
  42. package/dist/service-rpc.mjs.map +1 -0
  43. package/dist/testing.d.mts +2 -3
  44. package/package.json +18 -17
  45. package/dist/app-config-BUqyK6N6-CVq3uvHF.d.mts +0 -188
  46. package/dist/config-BVVgDSdq.d.mts +0 -1
  47. package/dist/deploy-BVVgDSdq.d.mts +0 -1
  48. package/dist/dist-zBU8ASQW.mjs.map +0 -1
  49. package/dist/graph-BYdCQKya-BI0njTow.mjs.map +0 -1
  50. package/dist/rpc.d.mts +0 -43
  51. package/dist/rpc.mjs +0 -132
  52. package/dist/rpc.mjs.map +0 -1
  53. package/dist/tsdown.d.mts +0 -9
  54. package/dist/tsdown.mjs +0 -35
  55. package/dist/tsdown.mjs.map +0 -1
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 * 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";
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
@@ -31,49 +21,26 @@ var AssembleError = class extends Error {
31
21
  }
32
22
  };
33
23
  /**
34
- * The wrapper-inlining decision (S2's open question — see the project's
35
- * design-notes.md and this slice's final report for the write-up).
36
- *
37
- * The CLI has no config file, so it can't take per-app `wrapperNoExternal`
38
- * patterns — it needs one general rule for what an assembler inlines into
39
- * the wrapper build besides `@prisma/*` (already the assemblers' own
40
- * default).
41
- *
42
- * The rule: inline everything except the runtime's own modules — `bun`,
43
- * `bun:*` (e.g. bun:sqlite), and `node:*` builtins — which resolve inside
44
- * the deploy VM at runtime and must never be bundled. Verified (both
45
- * `@prisma/composer/node/control` and `@prisma/composer/nextjs/control` set
46
- * `external: ['bun']` ahead of `noExternal` in their tsdown call, and tsdown/
47
- * rolldown's explicit `external` wins over a `noExternal` match — proven by
48
- * building an app's wrapper with this exact pattern: the
49
- * `bun` import stays external while everything else, including the app's own
50
- * workspace deps, gets inlined) — see this slice's final report for the
51
- * storefront-auth wrapper proof (arktype + `@storefront-auth/auth/contract`
52
- * inlined with no other file changes there).
53
- */
54
- const INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS = [/^(?!bun$)(?!bun:)(?!node:).+/];
55
- /**
56
- * The registry route for one service's build: descriptor by
57
- * `build.extension`, control by `build.type`, kind must be "build". The CLI's
58
- * coverage validation reports the same misses earlier with the config fix;
59
- * these errors are the backstop for programmatic callers.
24
+ * The registry route for one service's build: extension by
25
+ * `build.extension`, node descriptor by `build.type`, kind must be "build".
26
+ * The CLI's coverage validation reports the same misses earlier with the
27
+ * config fix; these errors are the backstop for programmatic callers.
60
28
  */
61
- function buildControlAssemble(config, node, address, cwd) {
29
+ function buildDescriptorAssemble(config, node, address, cwd) {
62
30
  const { extension, type } = node.build;
63
- const descriptor = config.extensions.find((candidate) => candidate.id === extension);
64
- if (descriptor === void 0) throw new AssembleError(`No extension "${extension}" is configured (needed by service "${node.name}"'s build) — add it to prisma-composer.config.ts's \`extensions\`.`);
65
- const control = descriptor.nodes[type];
66
- if (control === void 0) throw new AssembleError(`Extension "${extension}" has no control for build type "${type}" (known: ${Object.keys(descriptor.nodes).join(", ")}).`);
67
- if (control.kind !== "build") throw new AssembleError(`Extension "${extension}"'s control for type "${type}" is a "${control.kind}" control — a service build descriptor needs a "build" control.`);
68
- return control.assemble({
31
+ const extensionDescriptor = config.extensions.find((candidate) => candidate.id === extension);
32
+ if (extensionDescriptor === void 0) throw new AssembleError(`No extension "${extension}" is configured (needed by service "${node.name}"'s build) — add it to prisma-composer.config.ts's \`extensions\`.`);
33
+ const nodeDescriptor = extensionDescriptor.nodes[type];
34
+ if (nodeDescriptor === void 0) throw new AssembleError(`Extension "${extension}" has no descriptor for build type "${type}" (known: ${Object.keys(extensionDescriptor.nodes).join(", ")}).`);
35
+ if (nodeDescriptor.kind !== "build") throw new AssembleError(`Extension "${extension}"'s descriptor for type "${type}" is a "${nodeDescriptor.kind}" descriptorassembling a service build needs a "build" descriptor.`);
36
+ return nodeDescriptor.assemble({
69
37
  build: node.build,
70
- wrapperNoExternal: INLINE_EVERYTHING_EXCEPT_RUNTIME_BUILTINS,
71
38
  address,
72
39
  cwd
73
40
  });
74
41
  }
75
42
  async function assembleServices(graph, config, cwd, run) {
76
- const runAssembler = run ?? ((node, address, nodeCwd) => buildControlAssemble(config, node, address, nodeCwd));
43
+ const runAssembler = run ?? ((node, address, nodeCwd) => buildDescriptorAssemble(config, node, address, nodeCwd));
77
44
  const serviceNodes = graph.nodes.filter((n) => n.node.kind === "service");
78
45
  if (serviceNodes.length === 0) throw new AssembleError("The loaded graph has no service to assemble.");
79
46
  const bundles = {};
@@ -121,7 +88,7 @@ function blindCast(input) {
121
88
  return input;
122
89
  }
123
90
  //#endregion
124
- //#region ../../0-framework/1-core/core/dist/graph-BYdCQKya.mjs
91
+ //#region ../../0-framework/1-core/core/dist/graph-BmrUEdo9.mjs
125
92
  /** Thrown by Load when the graph is malformed. */
126
93
  var LoadError = class extends Error {
127
94
  constructor(message) {
@@ -139,6 +106,11 @@ const SECRET_SOURCE = blindCast(Symbol.for("prisma:secret-source"));
139
106
  function isSecretSource(value) {
140
107
  return typeof value === "object" && value !== null && blindCast(value)[SECRET_SOURCE] === true;
141
108
  }
109
+ const PARAM_SOURCE = blindCast(Symbol.for("prisma:param-source"));
110
+ /** True if `value` is a param source (an `envParam` result or a forwarded ctx.params ref). */
111
+ function isParamSource(value) {
112
+ return typeof value === "object" && value !== null && blindCast(value)[PARAM_SOURCE] === true;
113
+ }
142
114
  /**
143
115
  * True if `value` was constructed by this module's factories. Checks the
144
116
  * brand only, never a prototype — a graph may mix nodes from a different
@@ -238,7 +210,8 @@ function loadService(root, rootId) {
238
210
  root: rootGraphNode,
239
211
  nodes: [...topoSort(nodes, edges), rootGraphNode],
240
212
  edges,
241
- secrets: []
213
+ secrets: [],
214
+ params: []
242
215
  };
243
216
  }
244
217
  /**
@@ -285,6 +258,8 @@ function producerIdOf(ref) {
285
258
  const MODULE_INPUT_KEY = Symbol("prisma:module-input-key");
286
259
  /** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.secrets` forwarding channel. */
287
260
  const MODULE_SECRET_KEY = Symbol("prisma:module-secret-key");
261
+ /** Same per-key identity trick as MODULE_INPUT_KEY, for the parallel `ctx.params` forwarding channel. */
262
+ const MODULE_PARAM_KEY = Symbol("prisma:module-param-key");
288
263
  /** Whether `ref` carries a callable `satisfies` that accepts `required` truthily. */
289
264
  function satisfiesRequired(ref, required) {
290
265
  return typeof ref === "object" && ref !== null && "satisfies" in ref && typeof ref.satisfies === "function" && ref.satisfies(required);
@@ -344,6 +319,29 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
344
319
  for (const slot of Object.keys(secretWiring)) if (!Object.hasOwn(child.secretSlots, slot)) throw new LoadError(`The secrets for "${id}" name "${slot}", which is not a secret slot of that ${kind} (module "${enclosingModuleName}").`);
345
320
  }
346
321
  /**
322
+ * Checks the params wired into a provisioned SERVICE: unlike a secret slot, a
323
+ * declared param is never required to be bound here — it may fall back to its
324
+ * own `default` (checked later, at `buildConfig`) — so this only rejects a
325
+ * wired key that names something other than a real declared param.
326
+ */
327
+ function validateServiceParamBinding(child, id, paramWiring, enclosingModuleName) {
328
+ for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.params, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param of that service (module "${enclosingModuleName}").`);
329
+ }
330
+ /**
331
+ * Checks the params wired into a provisioned MODULE: every declared
332
+ * param-forwarding slot must be bound to a real `ParamSource` — a slot has no
333
+ * schema of its own, so (unlike a service param) it cannot fall back to a
334
+ * literal or a default; the param analog of `validateSecretBinding`.
335
+ */
336
+ function validateParamNeedBinding(child, id, paramWiring, enclosingModuleName) {
337
+ for (const slot of Object.keys(child.paramSlots)) {
338
+ const bound = paramWiring[slot];
339
+ if (bound === void 0) throw new LoadError(`Param slot "${slot}" of provisioned module "${id}" is not bound (module "${enclosingModuleName}") — bind it with a param source (e.g. envParam('NAME')) or forward ctx.params.`);
340
+ if (!isParamSource(bound)) throw new LoadError(`Param slot "${slot}" of "${id}" (module "${enclosingModuleName}") was wired with a non-source value — a module param-forwarding slot carries no schema to validate a literal against; use envParam('NAME') or a forwarded ctx.params ref.`);
341
+ }
342
+ for (const slot of Object.keys(paramWiring)) if (!Object.hasOwn(child.paramSlots, slot)) throw new LoadError(`The params for "${id}" name "${slot}", which is not a param slot of that module (module "${enclosingModuleName}").`);
343
+ }
344
+ /**
347
345
  * Recursively flattens one module's body into the shared graph state and
348
346
  * returns its resolved ModuleOutputs (one ref-port per expose key) for the
349
347
  * caller (the enclosing provision() call, or Load itself for the root) to
@@ -357,10 +355,11 @@ function validateSecretBinding(child, id, secretWiring, enclosingModuleName) {
357
355
  * forward in a producer provisioned by an ancestor scope, and it is the
358
356
  * shared `byId` (keyed by full address) that lets that resolve.
359
357
  */
360
- function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pending, secretBindings, byId) {
358
+ function flatten(moduleNode, address, wiring, secretWiring, paramWiring, nodes, edges, pending, secretBindings, paramBindings, byId) {
361
359
  const localIds = /* @__PURE__ */ new Set();
362
360
  const used = /* @__PURE__ */ new Set();
363
361
  const usedSecrets = /* @__PURE__ */ new Set();
362
+ const usedParams = /* @__PURE__ */ new Set();
364
363
  const ctxInputs = {};
365
364
  for (const key of Object.keys(moduleNode.deps)) {
366
365
  const wired = wiring[key];
@@ -383,10 +382,22 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
383
382
  const markSecretsUsed = (values) => {
384
383
  for (const value of Object.values(values)) for (const key of Object.keys(ctxSecrets)) if (value === ctxSecrets[key]) usedSecrets.add(key);
385
384
  };
385
+ const ctxParams = {};
386
+ for (const key of Object.keys(moduleNode.paramSlots)) {
387
+ const bound = paramWiring[key];
388
+ ctxParams[key] = typeof bound === "object" && bound !== null ? {
389
+ ...bound,
390
+ [MODULE_PARAM_KEY]: key
391
+ } : bound;
392
+ }
393
+ const markParamsUsed = (values) => {
394
+ for (const value of Object.values(values)) for (const key of Object.keys(ctxParams)) if (value === ctxParams[key]) usedParams.add(key);
395
+ };
386
396
  const provision = (child, opts) => {
387
397
  const id = opts?.id ?? child.name;
388
398
  const provisionWiring = opts?.deps;
389
399
  const provisionSecrets = opts?.secrets;
400
+ const provisionParams = opts?.params;
390
401
  if (typeof id !== "string" || id.length === 0) throw new LoadError(`provision() requires a non-empty id (module "${moduleNode.name}").`);
391
402
  if (id.includes("_") || id.includes(".")) throw new LoadError(`provision() id "${id}" (module "${moduleNode.name}") may not contain "_" or "." — "_" is the config-key separator and "." the node-id path separator; either inside an id collides with the joined form of other names.`);
392
403
  if (localIds.has(id)) throw new LoadError(`Duplicate provision id "${id}" in module "${moduleNode.name}".`);
@@ -397,6 +408,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
397
408
  if (child.kind === "resource") {
398
409
  if (provisionWiring !== void 0) throw new LoadError(`provision("${id}") received deps for a resource — a resource has no dependency slots to satisfy.`);
399
410
  if (provisionSecrets !== void 0) throw new LoadError(`provision("${id}") received secrets for a resource — a resource has no secret slots to satisfy.`);
411
+ if (provisionParams !== void 0) throw new LoadError(`provision("${id}") received params for a resource — a resource has no params to satisfy.`);
400
412
  if (child.type.length === 0) throw new LoadError(`provision("${id}") received a resource with an empty node type.`);
401
413
  byId.set(fullAddress, child);
402
414
  nodes.push({
@@ -410,6 +422,8 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
410
422
  const localSecrets = { ...provisionSecrets ?? {} };
411
423
  markSecretsUsed(localSecrets);
412
424
  validateSecretBinding(child, id, localSecrets, moduleNode.name);
425
+ const localParams = { ...provisionParams ?? {} };
426
+ markParamsUsed(localParams);
413
427
  if (child.kind === "service") {
414
428
  for (const slot of Object.keys(child.secretSlots)) {
415
429
  const bound = localSecrets[slot];
@@ -419,6 +433,12 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
419
433
  source: bound
420
434
  });
421
435
  }
436
+ validateServiceParamBinding(child, id, localParams, moduleNode.name);
437
+ for (const [slot, binding] of Object.entries(localParams)) paramBindings.push({
438
+ serviceAddress: fullAddress,
439
+ slot,
440
+ binding
441
+ });
422
442
  const inputs = serviceInputs(child, fullAddress);
423
443
  nodes.push(...inputs.nodes, {
424
444
  id: fullAddress,
@@ -435,6 +455,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
435
455
  byId.set(fullAddress, child);
436
456
  return refFor(fullAddress, child);
437
457
  }
458
+ validateParamNeedBinding(child, id, localParams, moduleNode.name);
438
459
  edges.push(...wiringEdges(localWiring, fullAddress));
439
460
  pending.push({
440
461
  deps: child.deps,
@@ -443,7 +464,7 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
443
464
  targetKind: "module",
444
465
  enclosingModuleName: moduleNode.name
445
466
  });
446
- const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, nodes, edges, pending, secretBindings, byId);
467
+ const childOutputs = flatten(child, fullAddress, localWiring, localSecrets, localParams, nodes, edges, pending, secretBindings, paramBindings, byId);
447
468
  nodes.push({
448
469
  id: fullAddress,
449
470
  node: child
@@ -457,12 +478,14 @@ function flatten(moduleNode, address, wiring, secretWiring, nodes, edges, pendin
457
478
  const ctx = blindCast({
458
479
  inputs: ctxInputs,
459
480
  secrets: ctxSecrets,
481
+ params: ctxParams,
460
482
  provision: blindCast(provision)
461
483
  });
462
484
  const outputs = blindCast(moduleNode.body(ctx) ?? {});
463
485
  markUsed(outputs);
464
486
  for (const key of Object.keys(moduleNode.deps)) if (!used.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares input "${key}" but never forwards it into a provision nor returns it as an output.`);
465
487
  for (const key of Object.keys(moduleNode.secretSlots)) if (!usedSecrets.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares secret "${key}" but never forwards it into a provision.`);
488
+ for (const key of Object.keys(moduleNode.paramSlots)) if (!usedParams.has(key)) throw new LoadError(`Module "${moduleNode.name}" declares param "${key}" but never forwards it into a provision.`);
466
489
  for (const [key, contract] of Object.entries(moduleNode.expose)) {
467
490
  const port = outputs[key];
468
491
  if (port === void 0) throw new LoadError(`Module "${moduleNode.name}" declares expose "${key}" but its body did not return a port for it.`);
@@ -482,12 +505,18 @@ function loadModule(root, opts) {
482
505
  const names = rootSecretKeys.map((k) => `"${k}"`).join(", ");
483
506
  throw new LoadError(`Module "${root.name}" declares secret${rootSecretKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds secrets with envSecret('NAME'), it does not declare secret slots of its own.`);
484
507
  }
508
+ const rootParamKeys = Object.keys(root.paramSlots);
509
+ if (rootParamKeys.length > 0) {
510
+ const names = rootParamKeys.map((k) => `"${k}"`).join(", ");
511
+ throw new LoadError(`Module "${root.name}" declares param${rootParamKeys.length > 1 ? "s" : ""} ${names} but is being deployed as the root — a root has no enclosing scope to bind them; the root binds params directly on each provision() call, it does not declare param-forwarding slots of its own.`);
512
+ }
485
513
  const nodes = [];
486
514
  const edges = [];
487
515
  const pending = [];
488
516
  const secretBindings = [];
517
+ const paramBindings = [];
489
518
  const byId = /* @__PURE__ */ new Map();
490
- flatten(root, void 0, {}, {}, nodes, edges, pending, secretBindings, byId);
519
+ flatten(root, void 0, {}, {}, {}, nodes, edges, pending, secretBindings, paramBindings, byId);
491
520
  for (const entry of pending) validateWiring(entry, byId);
492
521
  assertDependencyDag(edges);
493
522
  const rootGraphNode = {
@@ -498,7 +527,8 @@ function loadModule(root, opts) {
498
527
  root: rootGraphNode,
499
528
  nodes: [...topoSort(nodes, edges), rootGraphNode],
500
529
  edges,
501
- secrets: secretBindings
530
+ secrets: secretBindings,
531
+ params: paramBindings
502
532
  };
503
533
  }
504
534
  /**
@@ -545,145 +575,34 @@ function Load(root, opts) {
545
575
  throw new LoadError("Load expects a service or module root (received another node kind).");
546
576
  }
547
577
  //#endregion
548
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/http-CxGdfSAP.mjs
549
- /**
550
- * The Prisma service token used to authenticate Management API calls. Kept
551
- * as a Redacted value so it never lands in logs or error output.
552
- */
553
- var PrismaCredentials = class extends Context.Service()("PrismaCredentials") {};
554
- /** Resolve the token from the `PRISMA_SERVICE_TOKEN` environment variable. */
555
- const fromEnv = () => Layer.effect(PrismaCredentials, Effect.gen(function* () {
556
- return { token: yield* Config.redacted("PRISMA_SERVICE_TOKEN") };
557
- }));
558
- /**
559
- * The typed Prisma Management API client, built once from the resolved
560
- * credentials. Providers yield this in their outer Effect and call it inside
561
- * `reconcile` / `delete`.
562
- */
563
- var ManagementClient = class extends Context.Service()("PrismaManagementClient") {};
564
- const layer = () => Layer.effect(ManagementClient, Effect.gen(function* () {
565
- const { token } = yield* PrismaCredentials;
566
- return createManagementApiClient({ token: Redacted.value(token) });
567
- }));
568
- /** A non-2xx response from the Management API (or a transport failure). */
569
- var PrismaApiError = class extends Data.TaggedError("PrismaApiError") {};
570
- const attempt = (f) => Effect.tryPromise({
571
- try: f,
572
- catch: (cause) => new PrismaApiError({
573
- status: 0,
574
- message: String(cause)
575
- })
576
- });
577
- const fail = (r) => Effect.fail(new PrismaApiError({
578
- status: r.response.status,
579
- message: JSON.stringify(r.error)
580
- }));
581
- /** Unwrap `data`, failing on any API error. Preserves the SDK's response type. */
582
- const call = (f) => attempt(f).pipe(Effect.flatMap((r) => r.error !== void 0 || r.data === void 0 ? fail(r) : Effect.succeed(r.data)));
583
- /** Fire-and-forget a call, tolerating a 404 (already deleted). */
584
- const callVoid = (f) => attempt(f).pipe(Effect.flatMap((r) => r.response.status === 404 || r.error === void 0 ? Effect.void : fail(r)));
585
- Schedule.both(Schedule.exponential("2 seconds", 2), Schedule.during("5 minutes"));
586
- Resource("Prisma.ComputeService");
587
- Resource("Prisma.Deployment");
588
- Resource("Prisma.EnvironmentVariable");
589
- Resource("Prisma.Connection");
590
- Resource("Prisma.Database");
591
- Resource("Prisma.Project");
592
- //#endregion
593
- //#region ../../1-prisma-cloud/0-lowering/lowering/dist/index.mjs
594
- /** Raised with `ensure: false` when the app's Project (or a named stage's Branch) doesn't exist. */
595
- var ContainerNotFoundError = class extends Data.TaggedError("ContainerNotFoundError") {};
596
- const listAllProjects = (client) => Effect.gen(function* () {
597
- const projects = [];
598
- let cursor;
599
- for (;;) {
600
- const query = cursor === void 0 ? {} : { cursor };
601
- const page = yield* call(() => client.GET("/v1/projects", { params: { query } }));
602
- projects.push(...page.data);
603
- if (!page.pagination.hasMore || page.pagination.nextCursor === null) break;
604
- 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;
605
601
  }
606
- return projects;
607
- });
608
- /**
609
- * Workspace ids circulate in two shapes: `wksp_`-prefixed and bare. Compare
610
- * bare-to-bare so a `wksp_`-prefixed API id still matches a bare configured
611
- * one (the same normalization `state/bootstrap.ts` applies to the same
612
- * `/v1/projects` listing).
613
- */
614
- const bareWorkspaceId = (id) => id.startsWith("wksp_") ? id.slice(5) : id;
615
- /**
616
- * Finds the app's Project by name in the workspace — PDP allows duplicate
617
- * project names, so more than one can match; the oldest wins. Creates one
618
- * if none match, unless `ensure` is `false` (find-only — `destroy`), in
619
- * which case an absent Project fails with `ContainerNotFoundError`. No
620
- * ownership marker and no `--project` override (both deferred — see
621
- * ADR-0019).
622
- */
623
- const resolveProject = (client, workspaceId, appName, ensure) => Effect.gen(function* () {
624
- 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];
625
- if (oldest !== void 0) return oldest.id;
626
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({ appName }));
627
- return (yield* call(() => client.POST("/v1/projects", { body: {
628
- name: appName,
629
- workspaceId
630
- } }))).data.id;
631
- });
632
- const findBranchId = (client, projectId, gitName) => call(() => client.GET("/v1/projects/{projectId}/branches", { params: {
633
- path: { projectId },
634
- query: { gitName }
635
- } })).pipe(Effect.map((page) => page.data[0]?.id));
636
- /**
637
- * Finds the stage's Branch by its exact `gitName`, creating it if absent
638
- * unless `ensure` is `false` (find-only — `destroy`), in which case an
639
- * absent Branch fails with `ContainerNotFoundError`. The Management API has
640
- * no server-side "create-or-return" idempotency (`POST
641
- * /v1/projects/:id/branches` 409s on a duplicate `gitName`, with no request
642
- * field to make that a no-op), so idempotency is client-side: observe
643
- * first, and on a racing 409 from create, re-observe rather than fail.
644
- */
645
- const resolveBranch = (client, projectId, gitName, appName, ensure) => Effect.gen(function* () {
646
- const existing = yield* findBranchId(client, projectId, gitName);
647
- if (existing !== void 0) return existing;
648
- if (!ensure) return yield* Effect.fail(new ContainerNotFoundError({
649
- appName,
650
- stage: gitName
651
- }));
652
- return yield* call(() => client.POST("/v1/projects/{projectId}/branches", {
653
- params: { path: { projectId } },
654
- body: { gitName }
655
- })).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)));
656
- });
657
- /**
658
- * Resolves the two containers a stage's deploy runs into (ADR-0019): the
659
- * app's **Project**, found-or-created by name, and — for a named stage
660
- * only — its **Branch**, found-or-created by `gitName`. The default stage
661
- * (no `stage`) creates no Branch; `branchId` is omitted. With `ensure:
662
- * false` (`destroy`), nothing is created — an absent Project or Branch
663
- * fails with `ContainerNotFoundError` instead.
664
- */
665
- const resolveContainer = (opts) => Effect.gen(function* () {
666
- const client = yield* ManagementClient;
667
- const ensure = opts.ensure ?? true;
668
- const projectId = yield* resolveProject(client, opts.workspaceId, opts.appName, ensure);
669
- if (opts.stage === void 0) return { projectId };
670
- return {
671
- projectId,
672
- branchId: yield* resolveBranch(client, projectId, opts.stage, opts.appName, ensure)
673
- };
674
- });
675
- /**
676
- * Soft-deletes a Branch. Tolerates a 404 (already gone). The API refuses if
677
- * the Branch still has live members or is the production/default Branch —
678
- * that surfaces as a `PrismaApiError`.
679
- */
680
- const deleteBranch = (branchId) => Effect.gen(function* () {
681
- const client = yield* ManagementClient;
682
- yield* callVoid(() => client.DELETE("/v1/branches/{branchId}", { params: { path: { branchId } } }));
683
- });
684
- Provider.ProviderCollection()("Prisma");
602
+ return env;
603
+ }
685
604
  //#endregion
686
- //#region ../../0-framework/3-tooling/cli/dist/cli-BGF4efRK.mjs
605
+ //#region ../../0-framework/3-tooling/cli/dist/cli-Bc6_i0ls.mjs
687
606
  /**
688
607
  * A user-facing failure with a message that already names the fix (deploy-cli.md
689
608
  * § Error surface). `bin.ts` catches this — and any other Error, including
@@ -695,59 +614,6 @@ var CliError = class extends Error {
695
614
  this.name = "CliError";
696
615
  }
697
616
  };
698
- /**
699
- * Pipeline pre-stack step: resolves the app's Project + (named stage) Branch
700
- * via `@internal/lowering`'s `resolveContainer`, before the generated stack file
701
- * runs — `deploy` creates-if-absent, `destroy` finds only.
702
- */
703
- /** Validates `stage` as a git ref name via `git check-ref-format` — no silent normalization. */
704
- function validateStageName(stage) {
705
- const result = spawnSync("git", ["check-ref-format", `refs/heads/${stage}`], { stdio: "ignore" });
706
- if (result.error) throw new CliError(`git is required to validate --stage "${stage}" (git check-ref-format): ${result.error.message}.`);
707
- 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}").`);
708
- }
709
- async function ensureContainers(input, deps) {
710
- const env = input.env ?? process.env;
711
- const workspaceId = env["PRISMA_WORKSPACE_ID"];
712
- if (workspaceId === void 0 || workspaceId.length === 0) throw new CliError("environment variable PRISMA_WORKSPACE_ID is required.");
713
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
714
- if (input.stage !== void 0) validateStageName(input.stage);
715
- const program = resolveContainer({
716
- workspaceId,
717
- appName: input.appName,
718
- ...input.stage !== void 0 ? { stage: input.stage } : {},
719
- ensure: input.command === "deploy"
720
- }).pipe(Effect.map((c) => ({
721
- ok: true,
722
- container: c
723
- })), Effect.catchTag("ContainerNotFoundError", (e) => Effect.succeed({
724
- ok: false,
725
- message: `Nothing deployed for ${e.appName}${e.stage ? `/${e.stage}` : ""} — deploy it first.`
726
- })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
727
- ok: false,
728
- message: `Prisma Management API error resolving containers: ${e.message}.`
729
- })));
730
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
731
- const outcome = await Effect.runPromise(provided);
732
- if (!outcome.ok) throw new CliError(outcome.message);
733
- return outcome.container;
734
- }
735
- /**
736
- * Soft-deletes a named stage's Branch after a successful `alchemy destroy`
737
- * has removed its members (spec §10) — the Management API refuses to delete
738
- * a Branch that still has live members.
739
- */
740
- async function deleteStageBranch(input, deps) {
741
- const env = input.env ?? process.env;
742
- if (deps?.client === void 0 && (env["PRISMA_SERVICE_TOKEN"] ?? "").length === 0) throw new CliError("environment variable PRISMA_SERVICE_TOKEN is required.");
743
- const program = deleteBranch(input.branchId).pipe(Effect.map(() => ({ ok: true })), Effect.catchTag("PrismaApiError", (e) => Effect.succeed({
744
- ok: false,
745
- message: `Failed to delete the stage Branch: ${e.message}.`
746
- })));
747
- const provided = deps?.client !== void 0 ? program.pipe(Effect.provideService(ManagementClient, deps.client)) : program.pipe(Effect.provide(layer().pipe(Layer.provide(fromEnv()))));
748
- const outcome = await Effect.runPromise(provided);
749
- if (!outcome.ok) throw new CliError(outcome.message);
750
- }
751
617
  /** Pipeline step 6: writes a regenerated-every-run, independently runnable stack module at `.prisma-composer/alchemy.run.ts`. */
752
618
  const GENERATED_DIR = ".prisma-composer";
753
619
  const GENERATED_FILE = "alchemy.run.ts";
@@ -768,6 +634,7 @@ function renderOptions(input) {
768
634
  lines.push(" bundles: {");
769
635
  for (const [id, bundle] of Object.entries(input.assembled.bundles)) lines.push(` ${quote(id)}: ${renderBundle(bundle)},`);
770
636
  lines.push(" },");
637
+ lines.push(" report: deploymentReport,");
771
638
  return lines.join("\n");
772
639
  }
773
640
  /** Renders the stack module's source (tests assert on it without touching disk) — uses `//` headers, not a block comment, since a cwd path with a star-slash could close one early. */
@@ -782,6 +649,7 @@ function renderStackFile(input) {
782
649
  //
783
650
  // bisects a CLI bug from an Alchemy bug (deploy-cli.md § Implementation decisions).
784
651
  import { lower } from '@prisma/composer/deploy';
652
+ import { deploymentReport } from '@prisma/composer/report';
785
653
  import config from ${quote(configImport)};
786
654
  import app from ${quote(appImport)};
787
655
 
@@ -848,7 +716,8 @@ function validateConfigShape(loaded, configPath) {
848
716
  if (seen.has(id)) throw new CliError(`${CONFIG_FILENAME}: extension "${id}" is listed more than once in \`extensions\`.`);
849
717
  seen.add(id);
850
718
  }
851
- if (typeof loaded["state"] !== "function") throw fieldError("state", "must be a function returning the deploy state layer (e.g. () => prismaState())");
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())");
852
721
  return blindCast(loaded);
853
722
  }
854
723
  /**
@@ -907,7 +776,7 @@ function resolveAlchemyBin(startDir) {
907
776
  dir = parent;
908
777
  }
909
778
  }
910
- /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus the resolved Project/Branch ids. */
779
+ /** Runs `alchemy deploy|destroy <stack file> --yes [--stage <stage>]`, inheriting stdio + env, plus every extension's resolved container. */
911
780
  function runAlchemy(input) {
912
781
  const bin = resolveAlchemyBin(input.cwd);
913
782
  const args = [
@@ -921,8 +790,7 @@ function runAlchemy(input) {
921
790
  stdio: "inherit",
922
791
  env: {
923
792
  ...input.env ?? process.env,
924
- PRISMA_PROJECT_ID: input.projectId,
925
- ...input.branchId !== void 0 ? { PRISMA_BRANCH_ID: input.branchId } : {}
793
+ ...input.containerEnv
926
794
  }
927
795
  });
928
796
  if (result.error !== void 0) throw result.error;
@@ -948,6 +816,12 @@ function validateRegistryCoverage(graph, config) {
948
816
  lookup(extensions, node.build.extension, node.build.type, "build", `service node "${id}"'s build descriptor`);
949
817
  }
950
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
+ }
951
825
  /**
952
826
  * Argument parsing (clipanion — prisma-next's CLI idiom, see
953
827
  * prisma-next/packages/1-framework/3-tooling/cli/src/migration-cli.ts) +
@@ -1043,6 +917,7 @@ async function run(argv, deps = {}) {
1043
917
  throw error;
1044
918
  }
1045
919
  const stage = effectiveStage(args);
920
+ if (stage !== void 0) validateStageName(stage);
1046
921
  const cwd = process.cwd();
1047
922
  if (args.command === "destroy") warnIfNoLocalDeployState(cwd);
1048
923
  const resolvedEntryPath = path.resolve(cwd, args.entry);
@@ -1062,18 +937,32 @@ async function run(argv, deps = {}) {
1062
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.`);
1063
938
  throw error;
1064
939
  }
1065
- const { projectId, branchId } = await (deps.ensureContainers ?? ensureContainers)({
1066
- command: args.command,
1067
- appName: name,
1068
- stage
1069
- });
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
+ }
1070
960
  if (args.command === "deploy") for (const extension of config.extensions) {
1071
961
  if (extension.preflight === void 0) continue;
1072
962
  try {
1073
963
  await extension.preflight({
1074
964
  graph,
1075
- projectId,
1076
- branchId,
965
+ container: containers.get(extension.id),
1077
966
  stage
1078
967
  });
1079
968
  } catch (error) {
@@ -1093,15 +982,36 @@ async function run(argv, deps = {}) {
1093
982
  stackFileRelativePath: GENERATED_STACK_RELATIVE_PATH,
1094
983
  cwd,
1095
984
  stage,
1096
- projectId,
1097
- ...branchId !== void 0 ? { branchId } : {}
985
+ containerEnv: containerEnv(containers)
1098
986
  });
1099
987
  if (status !== 0) {
1100
988
  console.error(`\nGenerated stack file: ${stackPath}`);
1101
989
  console.error(`Run \`alchemy ${args.command} ${GENERATED_STACK_RELATIVE_PATH} --yes\` from ${cwd} to reproduce this directly.`);
1102
990
  return status;
1103
991
  }
1104
- if (args.command === "destroy" && branchId !== void 0) await (deps.deleteBranch ?? ((input) => deleteStageBranch(input)))({ branchId });
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
+ }
1013
+ }
1014
+ }
1105
1015
  return status;
1106
1016
  } catch (error) {
1107
1017
  console.error(`\nGenerated stack file: ${stackPath}`);