关于 prisma-composer
prisma-composer 是面向 Prisma Cloud 应用的 TypeScript 框架与命令行工具,可组合带类型的服务和资源。它以模块组织应用,并在编译阶段检查依赖连接与 RPC 契约。
主要功能
- 01组合带类型的模块
- 02编译时检查服务连接
- 03生成带类型的 RPC 客户端
- 04绑定类型化输入与依赖
- 05组装并部署构建产物
适用场景
- 01用可复用服务构建应用
- 02通过类型化 RPC 连接服务
- 03配置 Prisma Cloud Compute 与 Postgres
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ prisma/ composer --skill prisma-composer安装后在 Claude Code 中输入/prisma-composer确认已加载。
详细介绍
使用 Prisma Composer 编写应用
Prisma App 是由 Module 组成的 TypeScript 树结构。叶节点是
服务(compute())和资源(rawPostgres());根模块通过类型化端口将它们连接在一起。你的代码只从一个地方获取所有内容——服务节点:
service.load()— 依赖项(类型化 RPC 客户端、数据库绑定)service.input()— 服务的完整输入,是一个经过架构验证的类型化对象;其中的凭据会装在可脱敏的SecretString容器中service.port()— 要绑定的预留端口(默认值为 3000),带有类型;绝不使用process.env
框架绝不会打包或转换你的代码。你可以使用自己喜欢的任何打包工具构建应用(bun build、next build);prisma-composer deploy 会组装构建产物,并将其部署到 Prisma Cloud(Compute + Prisma Postgres)。
两点让这里的构建更快、更不容易出错——请充分利用:
- 先组合,再编写。 在自行实现某项能力之前,先考虑使用现有的 Module(见下文);接入它只需几行代码。
- 编译器会检查连接。 依赖项连接到了错误的提供方、缺少 RPC 处理程序、配置值形状错误——这些问题都会在
tsc阶段失败,而不是等到部署时才发现。先进行类型检查,再构建,最后部署;不要依赖云端来判断应用是否正确。
package.json 中只会出现两个包,而且仅有这两个:
| 包 | 提供内容 |
|---|---|
@prisma/composer | 核心编写功能:module、secret、isSecretString、/arktype(secretString() 架构叶节点)、/rpc、/node、/nextjs、/config、/testing、prisma-composer CLI |
@prisma/composer-prisma-cloud | Prisma Cloud 目标:compute、postgres、envSecret、envParam、/control、/testing,以及共享的 /cron、/storage、/streams、/orm 模块 |
tsconfig 和导入说明符
在入口依赖图中(即从 module.ts 可达的所有内容),相对导入可以使用 ./service.js 或不带扩展名的 ./service。CLI 会在 Node 下将 .js 和不带扩展名的说明符映射到匹配的 .ts 源文件;Bun 原生支持此功能。
最精简的 tsconfig:
{
"compilerOptions": {
"target": "ES2022",
"module": "Preserve",
"moduleResolution": "bundler",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"types": ["bun"]
},
"include": ["module.ts", "src"]
}
服务的组成
一个服务由 4 个小文件组成。示例:一个拥有 Postgres 数据库并提供 RPC 契约的 auth 服务,由 storefront Next.js 应用调用。
契约与拥有它的服务放在一起。消息类型由任意 Standard Schema 验证器定义;项目选择 arktype 作为默认方案:
// auth/src/contract.ts
import { contract, rpc } from '@prisma/composer/service-rpc';
import { type } from 'arktype';
export const authContract = contract({
verify: rpc({ input: type({ token: 'string' }), output: type({ ok: 'boolean' }) }),
});
服务声明是纯数据——名称、依赖项、构建配置和公开端口。不包含行为,也不包含平台键:
// auth/src/service.ts
import node from '@prisma/composer/node';
import { compute, postgres } from '@prisma/composer-prisma-cloud';
import { authContract } from './contract.ts';
export default compute({
name: 'auth',
deps: { db: rawPostgres() },
build: node({ module: import.meta.url, entry: '../dist/server.mjs' }),
expose: { rpc: authContract },
});
服务器入口是构建产物,也是平台启动的内容。它通过 load() 读取依赖项,并通过 serve() 提供契约服务——处理程序映射以公开端口的名称为键,并在编译时强制要求覆盖所有端口:
// auth/src/server.ts
import { serve } from '@prisma/composer/service-rpc';
import { SQL } from 'bun';
import service from './service.ts';
const { db } = service.load(); // { url } — you build your own client
const port = service.port(); // the reserved port, resolved (default 3000)
const sql = new SQL({ url: db.url, max: 1, idleTimeout: 10 });
const handler = serve(service, {
rpc: {
verify: async ({ token }) => ({ ok: token.length > 0 }),
},
});
export default handler;
// Bind all interfaces — Compute routes external HTTP to the VM; a
// loopback-only listener is unreachable.
Bun.serve({ port, hostname: '0.0.0.0', fetch: handler });
使用方将依赖项声明为 rpc(contract),并从 load() 获取类型化客户端:
// storefront/src/service.ts
import nextjs from '@prisma/composer/nextjs';
import { rpc } from '@prisma/composer/service-rpc';
import { compute } from '@prisma/composer-prisma-cloud';
import { authContract } from '@my-app/auth/contract';
export default compute({
name: 'storefront',
deps: { auth: rpc(authContract) },
build: nextjs({ module: import.meta.url, appDir: '..' }),
});
// storefront/app/page.tsx
import service from '../src/service.ts';
// load() reads the runtime environment, which doesn't exist at build time —
// render per request instead of prerendering.
export const dynamic = 'force-dynamic';
export default async function Home() {
const { auth } = service.load();
const { ok } = await auth.verify({ token: 'demo-token' });
return <p>Signed in: {String(ok)}</p>;
}
服务间调用会自动进行身份验证。 部署时,框架会为每个使用方→提供方绑定生成不同且不可猜测的服务密钥:使用方的客户端会在每次调用时发送该密钥,而 serve() 会在处理程序运行之前,向其他任何调用方返回 401。无需声明此密钥——契约、服务、模块或应用代码中都不需要包含密钥。
这对你有两条具体要求:不要在此基础上自行构建服务间身份验证,也不要让用户通过 curl 已部署的 /rpc/<method> 来检查它是否正常工作——未接线的调用方总会收到 401,看起来像是部署出错,但事实并非如此。请通过使用方或在本地进行调试。
调用会携带幂等键,并自动安全重试。 生成的客户端发出的每次调用都会携带 Idempotency-Key;如果目标在冷启动期间丢弃了调用,客户端会通过退避策略重试,而 serve() 会为每个键运行一次调用——如果重试在首次调用完成后到达,就会重放首次调用的响应,而不是再次运行处理程序。因此,每个方法都可以安全重试,契约中也无需声明相关信息(不要添加“是否幂等”标志——框架没有这个标志)。这对你有两个影响:如果处理程序需要在单个实例的内存之外实现严格的一次性执行(大多数情况不需要),可以接收可选的第三个参数 (input, deps, ctx),并读取 ctx.idempotencyKey(类型为 string | undefined——无键调用方不会提供此值);不带请求头的请求仍会处理一次,但不会去重,而不是被拒绝,因此手动编写的探测请求可以正常工作,只是没有重试安全保障。
| 本地 / 测试环境 | 不会预配任何资源,因此 serve() 会直接传递所有调用——切勿在 inputs 中提供键 |
| 每个绑定 | 同一提供方的两个使用方持有不同的键,因此其中一个泄露密钥也无法冒充另一个 |
| 作用范围 | 服务级别——任何有效密钥都能访问该服务公开的所有方法;如需分别控制访问权限,请拆分为两个服务 |
| 轮换 | 移除绑定(或销毁堆栈)并重新部署——普通重新部署不会执行轮换 |
| 存储 | 部署方管理并重写的 COMPOSER_* 变量;切勿手动编辑 |
这是一个能力令牌(“我是此应用接线到你的服务”),不是秘密;其值保存在部署状态中——这与 secret() 有意不同,框架永远不会持有后者的值。prisma/composer 仓库中的 docs/design/90-decisions/ADR-0030… 说明了相关原因。
根模块
根模块负责预配各个组件,并将公开端口接入依赖项槽位。它就是应用——prisma-composer deploy 会加载其默认导出:
// module.ts
import { module } from '@prisma/composer';
import authModule from '@my-app/auth';
import storefrontService from '@my-app/storefront';
export default module('my-app', ({ provision }) => {
const auth = provision(authModule);
provision(storefrontService, { deps: { auth: auth.rpc } });
});
provision(node, opts?) 接受 id(默认为节点自身的名称)、deps(将每个已声明的依赖项接入已预配的引用或公开端口)、input(服务的输入绑定——当且仅当服务声明了输入架构时必需,参见 § 服务输入),以及 secrets(绑定模块边界转发的机密需求)。
构建由你负责
框架只组装你构建的内容——用户负责构建,框架负责组装。对于普通的服务进程,entry 必须指向单个自包含的 ESM 文件:除运行时内置模块(bun、bun:*、node:*)外,所有内容都必须内联,这些内置模块由部署 VM 提供。部署时只会复制这一个文件,绝不会打包 node_modules,因此任何未内联的内容都会导致启动失败。任何能生成此类文件的打包工具都可以使用。使用 bun 时:
bun build src/server.ts --target=bun --outfile dist/server.mjs
一个包中的两个服务意味着需要分别构建两次,每个入口各构建一次,而不是进行一次多入口构建,否则共享代码会被拆分到两个输出文件都不包含的代码块中。
如果构建输出的是一个目录,而不是单个文件——例如 Bun 的 HTML 导入会生成服务端以及它要提供的客户端包、CSS 和图片——请用 dir 指定目录,并用 entry 指定其中启动的文件:
build: node({ module: import.meta.url, dir: '../dist/server', entry: 'server.js' })
dir 相对于服务模块解析;entry 在 dir 内解析,也可以是嵌套路径。部署会原样复制整个目录树并启动指定文件,因此服务端必须基于 import.meta.url 而非工作目录解析同级文件。不会自动推断任何内容,并且有两条规则需要遵守:目录树中不得包含符号链接(打包器会拒绝它们——组装会失败,并指出链接名称);entry 必须是 dir 内的文件(../ 会报错,而非用于跳出目录)。单文件形式请省略 dir。
对于 Next.js,next build 配合 output: 'standalone' 就是完整构建;nextjs({ module, appDir }) 用于告知部署系统应用根目录的位置。
部署前务必先构建——prisma-composer deploy 不会替你构建。
部署配置
prisma-composer.config.ts 通常与 module.ts 放在一起,但也可以位于任意祖先目录中:CLI 会先搜索入口所在目录,再逐级搜索其父目录,并使用距离最近的配置文件。此配置文件只由 prisma-composer deploy/destroy 读取,不会被应用代码导入。纯 JavaScript 项目可以将其命名为 prisma-composer.config.mjs 或 .js,以避免将其纳入 TypeScript 构建(即使构建启用了 allowJs,仍需通过 exclude 显式排除);.mts 是 TypeScript ES 模块的写法。在同一目录中,优先级依次为 .ts、.mts、.mjs、.js:
// prisma-composer.config.ts
import { defineConfig } from '@prisma/composer/config';
import { nodeBuild } from '@prisma/composer/node/control';
import { prismaCloud, prismaState } from '@prisma/composer-prisma-cloud/control';
export default defineConfig({
extensions: [prismaCloud(), nodeBuild()],
state: () => prismaState(), // deploy state, in its own database on the stage's branch
});
如果应用包含 Next.js 服务,请将 nextjsBuild() 从 @prisma/composer/nextjs/control 添加到 extensions 中。
数据库
两种 Postgres 依赖:
rawPostgres() — 绑定为 { url },应用拥有自己的客户端。
在服务入口中构造,参见上文的身份验证示例。
postgres(...) — 使用 Prisma ORM 类型的数据库:load()
会返回框架根据你的数据契约构造的类型化客户端,因此
db.orm.public.Product.all() 之类的查询会在编译时进行类型检查。数据契约由
prisma contract emit 从 contract.prisma 生成,并只包装一次,由两端引用:
// src/data.ts — the ONE value both ends reference
import { dataContract } from '@prisma/composer-prisma-cloud/orm';
import type { Contract } from '../contract.d.ts';
import contractJson from '../contract.json' with { type: 'json' };
export const catalogData = dataContract<Contract>(contractJson);
依赖端为 deps: { db: postgres(catalogData) }。资源端(在拥有该数据库的模块内)还会指定
prisma.config.ts 路径,部署的迁移步骤会加载此路径以查找
migrations/ — 已提交的迁移会在部署时、服务启动前重放:
const db = provision(
postgres({ name: 'database', contract: catalogData, config: './prisma.config.ts' }),
);
(postgres 同时用于两端:仅契约是依赖端;选项对象是资源端。)
部署只重放迁移:它会应用提交在 migrations/ 下的迁移,绝不会自行创建架构。每项架构变更(包括新数据库的首个架构)都遵循相同的流程:
- 编辑
contract.prisma。 prisma contract emit— 重新生成contract.json+contract.d.ts。prisma migration plan --name <slug>— 将迁移写入migrations/(在空迁移图上,这会生成基线迁移, 从空状态迁移到你的架构)。- 将
migrations/与变更一同提交,然后部署。全新的数据库会 从空状态重放整个迁移路径。
如果没有已编写的迁移路径能够到达目标契约,部署(以及针对过期本地数据库运行的
prisma-composer dev)会拒绝执行,并报告 MIGRATION_PATH_NOT_FOUND,同时指出可选操作:按上述方式编写缺失的迁移,或仅在针对本地数据库迭代时,直接通过 prisma db update 更新。部署前绝不要跳过第 3 步。
完整模式请参见 prisma/composer 仓库中的 examples/store/modules/catalog。
对象存储
bucket 是原始的 S3 兼容对象存储桶,与 postgres 一同导入:
import { bucket, compute } from '@prisma/composer-prisma-cloud';
// service.ts — dependency end: receives { url, bucket, accessKeyId, secretAccessKey }
export default compute({ name: 'uploads', deps: { store: bucket() } });
// module.ts — resource end: provisions the bucket and mints a keypair
const store = provision(bucket({ name: 'uploads' }));
provision(uploadsService, { deps: { store } });
可将绑定用于任何 S3 兼容客户端:其结构与标准 S3 配置一致,也兼容来自 /storage 的 s3() 依赖,因此任何接入了 s3() 的服务,都可以改接 bucket 资源,而无需更改服务声明。
可复用模块
模块是复用的基本单元:它拥有自己的内部组件(数据库、服务),并且只暴露类型化端口。在第二个参数中声明边界;在构建器中连接内部组件;返回暴露的端口:
// auth/src/module.ts — a Module that owns its own Postgres
import { module, secret } from '@prisma/composer';
import { postgres } from '@prisma/composer-prisma-cloud';
import { authContract } from './contract.ts';
import authService from './service.ts';
export default module(
'auth',
{ secrets: { signingKey: secret() }, expose: { rpc: authContract } },
({ secrets, provision }) => {
const db = provision(rawPostgres({ name: 'database' }));
const service = provision(authService, {
id: 'service',
deps: { db },
input: { signingKey: secrets.signingKey }, // forwarded ref as a binding leaf
});
return { rpc: service.rpc };
},
);
需要注意的命名规则:平台会拒绝长度不足 3 个字符的资源 ID(数据库应命名为 'database',而不是 'db');若服务名称与其所属模块相同,则会显示为 auth.auth,除非为其指定显式的 id。
模块也可以声明边界 deps — 父级以连接服务依赖的相同方式连接这些输入。使用方看不到模块的内部组件。
可组合的构建模块
模块是构建模块:配置一个模块、连接它暴露的端口,就完成了——无需重新实现模块已有的功能。第一方模块集包含在 @prisma/composer-prisma-cloud 中。它规模不大,还在持续扩充:
| 导入 | 配置内容 | 暴露内容 |
|---|---|---|
从 /cron 导入的 cron | 一个始终运行的调度器,按计划向运行器服务触发任务 | 无 |
从 /storage 导入的 storage | 一个由 S3 支持的 Blob 存储(自有 Postgres + 生成的凭据) | store |
从 /streams 导入的 streams | 基于 store 的持久化追加式事件流 | streams |
查找更多模块。 Composer 扩展是一个自带模块、资源或部署目标的包,发布在 npm 上,名称为 prisma-composer-*。这是约定的命名方式,因此你可以据此查找。这个生态系统还很新:目前,以上模块和你编写的应用模块就是全部可用模块,所以在确认 npm 上确实存在相应包之前,不要贸然使用 prisma-composer-* 包。
Cron 端到端示例——计划是唯一事实来源;serveSchedule 会在编译时穷尽其所有任务 ID:
// service.ts
import { defineSchedule, triggerContract } from '@prisma/composer-prisma-cloud/cron';
export const schedule = defineSchedule({ tick: '60s' });
// the runner service exposes { trigger: triggerContract }
// server.ts
import { serveSchedule } from '@prisma/composer-prisma-cloud/cron';
const handler = serveSchedule(service, schedule, {
tick: (deps) => deps.worker.tick({}),
});
// module.ts — the cron module's boundary deps mirror the runner's own
provision(cron({ schedule, runner: runnerService }), { deps: { worker: worker.rpc } });
服务输入
选择通道基本上就是决定的全部:
| 值是…… | 声明 | 提供 | 读取 |
|---|---|---|---|
| 由另一个节点生成 | deps: { db: rawPostgres() } | 在 provision() 中连接 | load() |
| 其他任何值——配置或凭据 | input 架构中的一个字段 | 在 provision() 中绑定:字面值、envParam() 或 envSecret() | input() |
服务将所有传入配置——普通值和凭据——一并声明为一个 Standard Schema(本项目首选 arktype)。凭据是一个类型为用于脱敏的 SecretString 盒子的字段;条件合法性(“未启用账单时不能提供 Stripe 密钥”)可通过普通的架构联合类型表达:
// service.ts — the shapes that are legal
import { secretString } from '@prisma/composer/arktype';
import { type } from 'arktype';
compute({
name: 'scheduler',
input: type({
jobs: type({ jobId: 'string', every: 'string' }).array(),
'region?': 'string',
apiKey: secretString(),
}),
// ...
});
// module.ts — where each value comes from; the binding mirrors the schema's shape
import { envParam, envSecret } from '@prisma/composer-prisma-cloud';
provision(scheduler, {
input: {
jobs: [{ jobId: 'tick', every: '60s' }], // a literal
region: envParam('REGION'), // a per-stage platform variable
apiKey: envSecret('SCHEDULER_API_KEY'), // a credential — name only, never the value
},
});
// server.ts — one call, one validated typed object
const input = service.input();
input.apiKey.expose(); // the only way to a secret's value; the box redacts everywhere else
需要特别注意的规则:
- 通过验证强制执行机密性:如果架构期望
SecretString,却绑定了字面值,部署就会失败;如果将envSecret绑定到普通字符串字段,结果也一样。不要把凭据放在普通字段中。 envParam值会以原始字符串形式传入——请将它们绑定到字符串字段。阶段的平台变量才是存储位置;部署用的 shell 只负责初始化它(预检会将 shell 中存在但平台中缺失的变量名复制过去;如果两边都没有,就会提前报错并指出变量名)。更改平台变量的值需要重新部署。- 是否允许缺失由架构决定:绑定到环境变量的字段,其变量未设置(或为空)时会解析为省略键——只有架构允许时(可选字段、联合类型分支)才合法。部署报告会打印序列化后的输入文档(不含机密:机密会以
{"$secret":"VAR"}引用的形式传递),以及所有解析为缺失的键。 - 保留字段
port(默认值为 3000)不属于架构——通过service.port()读取(与service.origin()并列),不要使用process.env。框架还会为 Next.js standalone 导出PORT,由其自行绑定。 - 模块可以转发对机密的需求,而无需知道平台上的变量名(参见上方的 auth 模块);转发的引用是一个绑定叶节点。
prisma/composer 仓库中的 examples/env-param 和 examples/storefront-auth 是可运行的版本。
测试
测试时,只需决定 load() 向代码提供什么,切勿修改被测试的代码:
| 你想要…… | 使用 | 来自 |
|---|---|---|
| 隔离测试页面/操作/处理程序 | mockService | @prisma/composer/testing |
| 使用伪依赖运行真实的启动和请求路径 | bootstrapService | @prisma/composer-prisma-cloud/testing |
单元测试 — mockService。 返回服务的副本,其 load() 会提供你的替身(并根据声明的依赖项进行类型检查),其 input() 会在一个扁平对象中提供你在保留键 input 下传入的对象(仅当服务声明了输入架构时才必须提供;原样传递,不做验证)。如何连接模块替换由你的运行器负责(Vitest 中使用 vi.mock,bun test 中使用 mock.module):
// page.test.tsx
import { mockService } from '@prisma/composer/testing';
import realService from '../src/service.ts';
vi.mock('../src/service.ts', () => ({
default: mockService(realService, {
auth: { verify: async () => ({ ok: true }) }, // wrong shape = compile error
}),
}));
import Page from './page.tsx';
expect(renderToString(await Page())).toContain('Signed in: true');
集成测试 — bootstrapService。 在进程内启动服务真正构建出的入口,并使用你选择的配置,就像部署时启动一样;通过真实 HTTP 与其交互。使用 bun test 运行:
import { bootstrapService } from '@prisma/composer-prisma-cloud/testing';
import fakeAuth from '@my-app/auth/fake'; // in-memory handler, no db
import storefront from '../src/service.ts';
const fake = Bun.serve({ port: 0, fetch: fakeAuth });
const app = await bootstrapService(storefront, {
service: { port: 4310 },
inputs: { auth: { url: fake.url.href } },
});
const res = await app.fetch(new Request(app.url));
service.port必须是具体端口 — 入口会自行监听;不会返回由操作系统分配的端口。- 无需
close()— 每个集成测试文件都在独立进程中运行(bun test 会这样做)。 - Next.js 服务需要第三个参数,即一个启动 thunk,因为构建后的入口位于 Next 的独立输出中 — 使用
@prisma/composer/nextjs/control中的standaloneServerPath解析它。bootstrapService会在启动前将解析出的端口导出为process.env.PORT,Next 的独立服务器会绑定到此端口。 - 具有输入架构的服务需要在配置中提供
input— 绑定方式与provision()完全相同,并经过真实的序列化/读取路径,因此已启动入口中的input()会看到部署时生成的内容。
传入的伪对象。 依赖项的类型就是它的契约,因此任何符合该形状的值都是有效的替身:普通对象(最快)、通过内存处理程序运行的真实客户端,或真实的本地服务器(bootstrapService 驱动的方式)。在依赖项自己的包中通过 /fake 入口点提供其伪对象,并放在 src/ 之外,这样伪对象和真实服务始终共用同一份契约。
本地运行
prisma-composer dev module.ts 会在这台机器上运行整个应用 — 每个服务及其 Postgres 和存储桶都会按部署时的方式连接 — 无需云凭据(无需 PRISMA_*)。它会针对本地模拟器运行与部署相同的流水线,因此要像部署一样先构建:
turbo run build && prisma-composer dev module.ts
它会打印每个服务的本地 URL(“前门”),监视构建输出并在服务构建发生变化时重启该服务,随后持续运行直至按下 Ctrl-C。Ctrl-C 会停止应用的进程,但会保留本地数据库、存储桶及其中的数据,因此下次运行 dev 时可以热启动;--fresh 会先清除此应用的本地实例及数据。
dev 不会打印服务日志 — 否则服务运行多起来后,日志会淹没前门信息。日志通过单独的命令查看:
| 你想要…… | 运行 |
|---|---|
| 在本地运行应用 | prisma-composer dev module.ts |
| 从干净状态启动(清除本地数据) | prisma-composer dev module.ts --fresh |
| 跟踪所有服务的日志 | prisma-composer log module.ts |
| 跟踪单个服务的日志 | prisma-composer log module.ts <address> |
| 先显示更多历史记录 | prisma-composer log module.ts --tail <n> |
prisma-composer log 会跟踪正在运行的应用的合并日志,每行都以其服务为前缀([catalog.service] …);传入点分地址可将范围缩小到单个服务。它只会读取日志 — 不会构建、配置、启动或停止任何内容。--tail <n> 用于设置实时输出前显示的近期历史记录量(默认值为 20;0 表示仅显示实时内容)。未设置的密钥不会阻止本地运行:它会被替换为占位符并附带警告,只有在实际调用的外部服务中,执行到使用该密钥的代码路径时才会失败。目前尚不支持 Windows。
部署
恰好需要两个环境变量:PRISMA_SERVICE_TOKEN 和
PRISMA_WORKSPACE_ID。目标环境 — stage — 通过命令行选择,
而不是在代码中选择:
| 你要…… | 运行 |
|---|---|
| 部署到生产环境 | prisma-composer deploy module.ts |
| 部署隔离环境 | prisma-composer deploy module.ts --stage <name> |
| 为单次运行覆盖应用名称 | prisma-composer deploy module.ts --name demo-42 |
| 拆除隔离环境 | prisma-composer destroy module.ts --stage <name> |
| 拆除生产环境的资源 | prisma-composer destroy module.ts --production |
Prisma App 是一个 Project;stage 是它的一个 Branch — 拥有独立的计算资源、空数据库和配置。部署具有幂等性:重新部署 stage 会更新其中的资源。stage 名称必须是有效的 git ref 名称;无效名称会导致硬错误。
拆除操作始终要求明确指定目标 — 单独运行 prisma-composer destroy
会报错,同时指定 --stage 和 --production 也会报错。拆除 stage
会在移除其资源后删除对应的 Branch;生产环境的 Branch 本身永远不会被删除,只会移除其中的资源。拆除生产环境也会在 Project 为空时删除 Project 本身,因此手动运行的堆栈不会遗留空 Project — 但如果 Project 仍持有另一个 stage 的资源,则会保留。拆除操作绝不会创建任何内容:拆除从未部署过的 stage 会失败,而不会创建该环境。
turbo run build && prisma-composer deploy module.ts --stage pr-42
部署会打印什么
部署结束时会打印应用自身的拓扑 — 编写时使用的名称、每个名称对应的平台资源,以及公共 URL。树形结构对应模块结构(auth.api 是 auth 模块中的 api 服务):
storefront-auth
├─ auth
│ └─ api compute-service cps_abc123
│ https://xyz.ewr.prisma.build
├─ db postgres-database db_def456
└─ web compute-service cps_ghi789
https://uvw.ewr.prisma.build
请从这里读取 ID,而不要让用户去 Console 里寻找。只有地址确实公开时才会显示 URL — 计算服务会打印 URL,数据库不会(它有连接字符串,而非公共端点);产品是机密材料的节点(如 s3-credentials 密钥对)完全不会显示资源行。没有发布任何可报告内容的节点仍会显示,并标记为 (no entities reported)。
旧版部署结束时会输出部署引擎生成的原始 { outputs: {} } 数据块 —
它始终为空,与应用无关。该内容现已移除;它既未配置任何内容,也未被任何内容使用。
部署时会检查连接契约
连接会按名称声明所需的值,另一端的生产者必须提供这些值。生产者漏掉任何值都会导致部署失败,并指出连接边、参数以及生产者实际提供的内容:
Connection input "auth.db" declares param "url", but its producer "db" did not
supply it — the producer's outputs carry [host].
在有问题的一端修复:将名称添加到生产者在降级过程中返回的输出中;或者,如果缺失确实合法,则在连接上将参数标记为 optional(此时消费者会读取 undefined)。
这是部署时的拒绝,而非部署损坏 — 即使应用代码没有变化,也可能出现这种情况。过去这个缺口会被静默放过:值以 undefined 的形式到达消费者,进入其环境变量,并在启动时导致该服务崩溃,错误归咎于读取方而非提供方。不要仅为了绕过问题而将参数设为可选,除非缺失确实有效;否则就会重新引入静默的 undefined。
只有在你编写了连接或其中一端的扩展时,才可能遇到此问题 — 所有随附的模块块都会提供其声明的内容。
通过代码驱动部署
@prisma/composer/control 在进程内提供 CLI 的操作:类型化的
deploy、destroy、dev 和 log,返回结构化结果——无需 argv、
无需 CLI 渲染、无需退出码(派生的部署引擎自身的继承输出仍可能到达宿主终端)。CLI 本身只是这些操作的渲染器。
import { deploy } from '@prisma/composer/control';
const result = await deploy({ entry: 'module.ts', stage: 'pr-42' });
// result: { ok: true, value: { summary? } } | { ok: false, failure }
- 失败会以
{ ok: false, failure }的形式返回,其中failure是结构化错误:根据带点的failure.code分支处理(例如ASSEMBLE.BUILD_FAILED、DEPLOY.ENGINE_FAILED——ADR-0044 的封闭注册表),并包含 CLI 渲染的、用于说明修复方式的message/why/fix。引擎失败的meta.diagnostics(退出码、复现命令;使用导出的executionDiagnostics(failure)读取)描述当前执行机制——对于需要持久稳定的信息,应根据code/message/cause分支处理。效果版本冲突为DEPS.EFFECT_VERSION_CONFLICT,且在运行某项操作之前,导入模块不会执行任何操作。操作出现非结构化拒绝属于 composer 中的错误,并非预期的失败。 destroy接受target: { kind: 'production' } | { kind: 'stage', stage }——必须明确指定,绝不会使用默认值。deploy的summary(已部署的拓扑)尽力提供;部署成功时为undefined属于正常情况。- 部署引擎的实时输出仍会流向宿主进程的 stdio——这是当前机制;这些操作不会捕获输出。
dev会解析为{ ok: true, value: session }或失败;会话为{ endpoints, stop(), closed },通过onEvent提供进度,信号处理由宿主负责。log会解析为{ ok: true, value: { appName, services, lines } }或失败,其中lines是一个由调用方提供的AbortSignal终止的AsyncIterable(也可由提前停止的消费者终止);运行中的服务数量为零是有效结果,并非错误。
生产环境注意事项
- 缩容至零会关闭空闲的数据库连接。 持久化客户端会陷入 502 重启循环,除非你将连接池保持较小并使其易于重新连接(Bun 使用
new SQL({ url, max: 1, idleTimeout: 10 })),并记录uncaughtException/unhandledRejection,而不是直接退出。 - 绑定
0.0.0.0,而不是回环地址——Compute 会将外部 HTTP 请求路由到 VM。 - 调用
load()的 Next.js 页面需要export const dynamic = 'force-dynamic'——运行时环境在构建时并不存在,而且 Next 会忽略预渲染路由的运行时环境变量。 - 已部署的
/rpc/<method>会对未连接的对端返回401。 每个 RPC 绑定都会携带自动配置的服务密钥,因此手动编写的curl永远不会获得授权;没有已连接消费者的提供方会拒绝所有请求。这并非部署故障——通过消费者访问,或在本地运行(本地不会强制执行这些限制)。 - 冷启动会重置服务间连接。 调用缩容至零的服务可能会收到
ECONNRESET;请重试。 - 每条
prisma-composer命令都会在启动时因effect版本冲突而停止(Dependency conflict: alchemy resolves effect@...)。另一个依赖引入了更新的effect,包管理器将其提升到了 Composer 固定版本之上。按错误提示操作:在应用的package.jsonoverrides中固定整个effect依赖系列(yarn:resolutions;pnpm:pnpm.overrides)——包括effect、@effect/sql-d1、@effect/sql-pg、@effect/vitest和@effect/platform-bun/-node/-node-shared,全部使用 Composer 的精确固定版本——然后重新安装。(这是针对上游 alchemy 错误的变通方法:其自身 effect 系列的版本范围会超出其代码支持的范围。仓库中的示例包含该配置块。) - 入口层会缓冲流式响应。 持续打开的 SSE 流不会传输任何内容,并会在 60s 后超时——不要基于流式 HTTP 响应构建功能。
Composer 目前尚不支持的功能
明确指出缺失之处,不要臆造 API:
- 不支持交互式身份验证。 部署只能通过静态的
PRISMA_SERVICE_TOKEN进行身份验证;没有login流程。 - 不支持内存中的契约绑定。 目前无法在不使用 HTTP 的情况下,将依赖项连接到同位置的处理程序;请使用
bootstrapService和环回假对象。 - 契约类型仅支持基于 HTTP 的 RPC。 不支持 gRPC、WebSocket 或流式契约。
对于其他缺失功能,请先查看 prisma/composer 仓库中的示例和设计文档(examples/、docs/design/10-domains/、docs/design/90-decisions/),然后在那里提交 issue,不要猜测。