Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Amended 2026-10-30: from Alchemy 2.0.0-beta.84 the provider is its own package,

## Decision

Composer does not implement Alchemy resources for Prisma Cloud's Management API. It composes the official `alchemy/Prisma` provider's resources and providers, and defines its own resources only where the upstream provider has no support yet (buckets, whose routes upstream deferred) or where no Management API exists behind them (local mechanisms like migration steps).
Composer does not implement Alchemy resources for Prisma Cloud's Management API. It composes the official upstream Prisma provider's (`@alchemy.run/prisma`, formerly `alchemy/Prisma`) resources and providers, and defines its own resources only where the upstream provider has no support yet (buckets, whose routes upstream deferred) or where no Management API exists behind them (local mechanisms like migration steps).

The live wiring is composition, not implementation:

Expand Down Expand Up @@ -87,6 +87,6 @@ No plaintext reaches state: members are `Redacted` in props, and only the salted
## References

- ADR-0005 (the framework never builds or bundles user code), ADR-0034 (hosted deploy state), ADR-0039 (a service's origin is a target-resolved property), ADR-0041 (local dev runs the deploy pipeline against local providers).
- The provider: the `alchemy/Prisma` module of the `alchemy` package, 2.0.0-beta.74 or later (buckets and `Deployment.triggers` first ship there).
- The provider: the `alchemy/Prisma` module of the `alchemy` package, 2.0.0-beta.74 or later (buckets and `Deployment.triggers` first ship there). From 2.0.0-beta.84 it is the separate `@alchemy.run/prisma` package, with the same resources.
- `docs/design/05-prisma-cloud/alchemy-lowering.md` — the current resource-by-resource lowering map.
- `docs/guides/deploying.md` — operator-facing upgrade notes (one-time deployment reship, branch-database renames, leftover placeholder rows).
5 changes: 3 additions & 2 deletions docs/guides/deploying.md
Original file line number Diff line number Diff line change
Expand Up @@ -457,8 +457,8 @@ resolves a single `effect`.
The fix is to use the same `effect` as Composer. Match your own `effect`
dependency to `@prisma/composer`'s exact pin (see its `dependencies.effect`),
or, when a dependency you cannot change pins another version, force
Composer's. `<pin>` below stands for that exact version (`4.0.3` in
Composer 0.29.1). With npm, in your app's `package.json`:
Composer's. `<pin>` below stands for that exact version (`4.1.0` at the time of
writing). With npm, in your app's `package.json`:

```json
"overrides": {
Expand Down Expand Up @@ -514,6 +514,7 @@ What deployed apps actually run into, and what to do about it:
ingress) buffers a response until it completes, so an open SSE tail
delivers nothing and times out at 60s. Don't build on streamed HTTP
responses.
- **A new named hostname adds a DNS wait to the deploy.** Before it attaches the hostname, the deploy asks the hostname's authoritative nameservers for its CNAME record, every 3 seconds for up to a minute, and waits 10 more seconds once they all answer. The machine that runs the deploy needs DNS access; if the lookups fail, the deploy goes ahead and the platform reports any missing record.
- **Next.js: pages that call `service.load()` need
`export const dynamic = 'force-dynamic'`.** The runtime environment doesn't
exist at build time, and Next won't re-read it for prerendered routes.
Expand Down
4 changes: 2 additions & 2 deletions examples/bucket/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-cli": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"effect": "4.0.5"
"alchemy": "catalog:",
"effect": "catalog:"
},
"devDependencies": {
"@types/bun": "^1.3.13",
Expand Down
6 changes: 3 additions & 3 deletions examples/cron/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"destroy": "( set -a; . \"${PRISMA_DEPLOY_ENV:-../../.env}\"; set +a; bun ../../scripts/composer-destroy.ts --environment production ${CRON_STACK_NAME:+--name \"$CRON_STACK_NAME\"} )"
},
"dependencies": {
"@effect/platform-bun": "4.0.5",
"@effect/platform-bun": "catalog:",
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"alchemy": "catalog:",
"arktype": "^2.2.3",
"effect": "4.0.5"
"effect": "catalog:"
},
"devDependencies": {
"@prisma/composer": "workspace:0.38.0",
Expand Down
2 changes: 1 addition & 1 deletion examples/orm-demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"@prisma/orm-postgres": "8.0.0-rc.23",
"effect": "4.0.5",
"effect": "catalog:",
"pg": "8.22.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions examples/storage/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
"@aws-sdk/client-s3": "^3.1085.0",
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"effect": "4.0.5"
"alchemy": "catalog:",
"effect": "catalog:"
},
"devDependencies": {
"@prisma/composer": "workspace:0.38.0",
Expand Down
6 changes: 3 additions & 3 deletions examples/store/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@
"destroy": "( set -a; . ../../.env; set +a; bun ../../scripts/composer-destroy.ts --environment production ${STORE_STACK_NAME:+--name \"$STORE_STACK_NAME\"} )"
},
"dependencies": {
"@effect/platform-bun": "4.0.5",
"@effect/platform-bun": "catalog:",
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-cli": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"@store/catalog": "workspace:0.38.0",
"@store/orders": "workspace:0.38.0",
"@store/promotions": "workspace:0.38.0",
"@store/storefront": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"alchemy": "catalog:",
"arktype": "^2.2.3",
"effect": "4.0.5"
"effect": "catalog:"
},
"devDependencies": {
"@types/bun": "^1.3.13",
Expand Down
6 changes: 3 additions & 3 deletions examples/storefront-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@
"test": "bun test module.test.ts"
},
"dependencies": {
"@effect/platform-bun": "4.0.5",
"@effect/platform-bun": "catalog:",
"@prisma/composer": "workspace:0.38.0",
"@prisma/composer-prisma-cloud": "workspace:0.38.0",
"@storefront-auth/auth": "workspace:0.38.0",
"@storefront-auth/storefront": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"alchemy": "catalog:",
"arktype": "^2.2.3",
"effect": "4.0.5"
"effect": "catalog:"
},
"devDependencies": {
"dotenv": "^17.4.2",
Expand Down
4 changes: 2 additions & 2 deletions packages/0-framework/1-core/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
},
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"alchemy": "2.0.0-beta.84",
"effect": "4.0.5",
"alchemy": "catalog:",
"effect": "catalog:",
"@internal/foundation": "workspace:0.38.0"
},
"devDependencies": {
Expand Down
3 changes: 2 additions & 1 deletion packages/0-framework/3-tooling/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,12 @@
"dotenv-flow": "4.1.0"
},
"devDependencies": {
"@alchemy.run/prisma": "catalog:",
"@internal/tsdown-config": "workspace:0.38.0",
"alchemy": "2.0.0-beta.84",
"@types/bun": "^1.3.13",
"@types/cross-spawn": "^6.0.6",
"@types/node": "^26.0.1",
"alchemy": "catalog:",
"tsdown": "^0.22.7",
"typescript": "^6.0.3"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,13 @@ describe('the failure cause through a real child process', () => {
}, 15_000);
});

/** The `+`-joined string literals around each non-comment source line that mentions `--adopt`, as the message they build. */
function adoptMessagesIn(source: string): string[] {
/** The `+`-joined string literals around each non-comment source line that mentions `marker`, as the message they build. */
function adoptMessagesIn(source: string, marker = '--adopt'): string[] {
const lines = source.split('\n');
const joinedWithNext = (index: number) => /\+\s*$/.test(lines[index] ?? '');
const messages: string[] = [];
lines.forEach((line, index) => {
if (!line.includes('--adopt') || /^\s*(\*|\/\/)/.test(line)) return;
if (!line.includes(marker) || /^\s*(\*|\/\/)/.test(line)) return;
let start = index;
while (start > 0 && joinedWithNext(start - 1)) start--;
let end = index;
Expand All @@ -203,7 +203,8 @@ function adoptMessagesIn(source: string): string[] {
.slice(start, end + 1)
.join('\n')
.match(/"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*`/g) ?? [];
messages.push(literals.map((literal) => literal.slice(1, -1)).join(''));
const message = literals.map((literal) => literal.slice(1, -1)).join('');
if (message.includes(marker)) messages.push(message);
});
return messages;
}
Expand All @@ -214,6 +215,10 @@ describe('withComposerAdvice', () => {
"OwnedBySomeoneElse: Cannot adopt resource 'auth.database-conn' (Prisma.Connection): it exists in the cloud but is not owned by this stack/stage/logical-id. Re-run with `--adopt` (or wrap the effect in `adopt(true)`) to take it over.",
"OwnedBySomeoneElse: Cannot resume creating resource 'storefront-svc' (Prisma.App): a resource with its physical identity exists in the cloud but is not owned by this stack/stage/logical-id. Re-run with `--adopt` (or wrap the effect in `adopt(true)`) to take it over.",
'Resource exists in the cloud but is not owned by this stack — leaving it in place (re-deploy with --adopt to take ownership, then destroy).',
"Prisma custom domain 'shop.com' already exists on App 'app_1' but is not owned by this resource. Import it with explicit adoption instead of silently taking it over.",
"Prisma app 'shop.web' already exists on the requested branch but is not owned by this App resource. Import it with explicit adoption or choose a different display name.",
"A Prisma database named 'shop' appeared after the adoption check. Refusing to take it over; rerun with adoption enabled if it is the intended database.",
"Cannot replace resource 'storefront-svc' (Prisma.App): it must be deleted before its replacement is pre-created, but the replacement's identity depends on outputs that are not resolved yet, so its ownership cannot be checked first. Re-run with `--adopt` (or wrap the effect in `adopt(true)`) to replace it anyway.",
];
for (const line of upstream) {
const shown = withComposerAdvice(line);
Expand All @@ -238,4 +243,21 @@ describe('withComposerAdvice', () => {
expect(withComposerAdvice(message)).not.toContain('--adopt');
}
});

test('rewrites every adoption message in the installed Prisma provider', () => {
const providerSrc = path.join(
path.dirname(fileURLToPath(import.meta.resolve('@alchemy.run/prisma/package.json'))),
'src',
);
const messages = fs
.readdirSync(providerSrc, { recursive: true, encoding: 'utf8' })
.filter((file) => file.endsWith('.ts'))
.flatMap((file) =>
adoptMessagesIn(fs.readFileSync(path.join(providerSrc, file), 'utf8'), 'adoption'),
);
expect(messages.length).toBeGreaterThan(0);
for (const message of messages) {
expect(withComposerAdvice(message)).not.toMatch(/explicit adoption|adoption enabled/);
}
});
Comment thread
wmadden-electric marked this conversation as resolved.
Resolved
});
14 changes: 13 additions & 1 deletion packages/0-framework/3-tooling/cli/src/deployment-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,25 @@ const OUTPUT_TAIL_LIMIT = 64 * 1024;
/** Fits the platform's 5000-character build `errorMessage` with room for the exit-status prefix. */
const ENGINE_FAILURE_CAUSE_LIMIT = 1000;

/** Alchemy's advice to rerun its own CLI with `--adopt`, which a Composer user has no way to do. */
/** Alchemy's advice to rerun its own CLI with `--adopt` or to import a resource with adoption, which a Composer user has no way to do. */
const ALCHEMY_ADOPT_ADVICE: readonly (readonly [RegExp, string])[] = [
[
/Re-run with `--adopt` \(or wrap the effect in `adopt\(true\)`\) to take it over\./g,
'Something other than this app holds it. Remove that resource, then deploy again.',
],
[
/Re-run with `--adopt` \(or wrap the effect in `adopt\(true\)`\) to replace it anyway\./g,
'Remove that resource, then deploy again.',
],
[/\(re-deploy with --adopt to take ownership, then destroy\)/g, '(remove it by hand)'],
[
/Import it with explicit adoption (?:instead of silently taking it over|or choose a different [\w ]+ name)\./g,
'Something other than this app holds it. Remove that resource, then deploy again.',
],
[
/; rerun with adoption enabled if it is the intended [\w ]+\./g,
'. Remove that resource, then deploy again.',
],
];

/** Replaces Alchemy CLI advice in engine output with steps that work from Composer. */
Expand Down
6 changes: 3 additions & 3 deletions packages/1-prisma-cloud/0-lowering/local-target/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@alchemy.run/prisma": "catalog:",
"@internal/bundle-paths": "workspace:0.38.0",
"@internal/core": "workspace:0.38.0",
"@internal/dev-emulators": "workspace:0.38.0",
"@internal/lowering": "workspace:0.38.0",
"@internal/s3-protocol": "workspace:0.38.0",
"@prisma/dev": "^0.25.2",
"@alchemy.run/prisma": "2.0.0-beta.84",
"alchemy": "2.0.0-beta.84",
"effect": "4.0.5",
"alchemy": "catalog:",
"effect": "catalog:",
"tar": "^7.5.21"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
import { afterEach, describe, expect, mock, spyOn, test } from 'bun:test';
import * as Prisma from '@alchemy.run/prisma';
import type { ContainerInstance, LocalTargetProvidersInput } from '@internal/core/config';
import { runScratch, scratchStack } from '@internal/lowering/scratch-stack';
import * as State from 'alchemy/State';
import { inMemoryState } from 'alchemy/State/InMemoryState';
import * as Core from 'alchemy/Test/Core';
import * as Effect from 'effect/Effect';
import { localTargetProviders } from '../providers.ts';

Expand Down Expand Up @@ -32,11 +31,11 @@ describe('a custom domain under the local providers', () => {

test('calls nothing, records the hostname as active with no DNS records, and converges on a second deploy', async () => {
const fetch = spyOn(globalThis, 'fetch');
const options = { providers: localTargetProviders(input), state: inMemoryState(), dev: false };
const stack = Core.scratchStack(options, 'local-custom-domain');
const options = { providers: localTargetProviders(input), dev: false };
const stack = scratchStack(options, 'local-custom-domain');

const first = await Core.run(stack.deploy(program), options);
const second = await Core.run(stack.deploy(program), options);
const first = await runScratch(stack.deploy(program), options);
const second = await runScratch(stack.deploy(program), options);

expect(first).toMatchObject({
customDomainId: 'local-shop.site.domain.shop.com',
Expand All @@ -51,11 +50,11 @@ describe('a custom domain under the local providers', () => {

test('a hostname change creates the new custom domain and removes the old one, calling nothing', async () => {
const fetch = spyOn(globalThis, 'fetch');
const options = { providers: localTargetProviders(input), state: inMemoryState(), dev: false };
const stack = Core.scratchStack(options, 'local-custom-domain-change');
const options = { providers: localTargetProviders(input), dev: false };
const stack = scratchStack(options, 'local-custom-domain-change');

await Core.run(stack.deploy(programFor('shop.com')), options);
const changed = await Core.run(stack.deploy(programFor('new.shop.com')), options);
await runScratch(stack.deploy(programFor('shop.com')), options);
const changed = await runScratch(stack.deploy(programFor('new.shop.com')), options);

expect(changed).toMatchObject({
customDomainId: 'local-shop.site.domain.new.shop.com',
Expand Down
11 changes: 6 additions & 5 deletions packages/1-prisma-cloud/0-lowering/lowering/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"./builds": "./dist/builds.mjs",
"./compute": "./dist/compute.mjs",
"./deployed": "./dist/deployed.mjs",
"./scratch-stack": "./dist/scratch-stack.mjs",
"./state": "./dist/state.mjs",
"./package.json": "./package.json"
},
Expand All @@ -17,15 +18,15 @@
"clean": "rm -rf dist"
},
"dependencies": {
"@distilled.cloud/prisma": "1.0.0-rc.17",
"@effect/platform-node": "4.0.5",
"@alchemy.run/prisma": "catalog:",
"@distilled.cloud/prisma": "catalog:",
"@effect/platform-node": "catalog:",
"@internal/bundle-paths": "workspace:0.38.0",
"@internal/core": "workspace:0.38.0",
"@internal/foundation": "workspace:0.38.0",
"@prisma/management-api-sdk": "^1.80.0",
"@alchemy.run/prisma": "2.0.0-beta.84",
"alchemy": "2.0.0-beta.84",
"effect": "4.0.5"
"alchemy": "catalog:",
"effect": "catalog:"
},
"devDependencies": {
"@internal/tsdown-config": "workspace:0.38.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import { expect, test } from 'bun:test';
import * as Prisma from '@alchemy.run/prisma';
import * as Output from 'alchemy/Output';
import * as Provider from 'alchemy/Provider';
import { inMemoryState } from 'alchemy/State/InMemoryState';
import * as Core from 'alchemy/Test/Core';
import * as Deferred from 'effect/Deferred';
import * as Effect from 'effect/Effect';
import * as Exit from 'effect/Exit';
import * as Fiber from 'effect/Fiber';
import * as Layer from 'effect/Layer';
import * as Redacted from 'effect/Redacted';
import { data, fakeManagementApi, page } from '../../__tests__/fake-management-api.ts';
import { runScratch, scratchStack } from '../../scratch-stack.ts';
import { appAfterEnvironment } from '../deployment-edge.ts';

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down Expand Up @@ -190,8 +189,8 @@ test.each(scenarios)(
),
Layer.provideMerge(api),
);
const options = { providers, state: inMemoryState(), dev: false };
const stack = Core.scratchStack(options, 'deployment-input-order');
const options = { providers, dev: false };
const stack = scratchStack(options, 'deployment-input-order');

const program = (inputValue: string, release: number, includeNewVariable: boolean) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -228,7 +227,7 @@ test.each(scenarios)(
return { deployment };
});

await Core.run(
await runScratch(
Effect.gen(function* () {
yield* stack.deploy(program('{"required":"old"}', 1, false));
if (repairDrift) values.set('COMPOSER_INPUT', 'out-of-band-drift');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { runScratch, type ScratchStackOptions, scratchStack } from '../scratch-stack.ts';
Loading
Loading