mcpField
P
prisma-composer

prisma-composer

github.com/prisma开发665 次安装今天更新
代码仓库

关于 prisma-composer

prisma-composer 是面向 Prisma Cloud 应用的 TypeScript 框架与命令行工具,可组合带类型的服务和资源。它以模块组织应用,并在编译阶段检查依赖连接与 RPC 契约。

主要功能

  1. 01组合带类型的模块
  2. 02编译时检查服务连接
  3. 03生成带类型的 RPC 客户端
  4. 04绑定类型化输入与依赖
  5. 05组装并部署构建产物

适用场景

  1. 01用可复用服务构建应用
  2. 02通过类型化 RPC 连接服务
  3. 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-cloudPrisma 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/ 下的迁移,绝不会自行创建架构。每项架构变更(包括新数据库的首个架构)都遵循相同的流程:

  1. 编辑 contract.prisma。
  2. prisma contract emit — 重新生成 contract.json + contract.d.ts。
  3. prisma migration plan --name <slug> — 将迁移写入 migrations/(在空迁移图上,这会生成基线迁移, 从空状态迁移到你的架构)。
  4. 将 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.json overrides 中固定整个 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,不要猜测。

评论

支持 Markdown