mcpField
P
prisma-composer

prisma-composer

github.com/prisma開発665 回インストール今日更新
リポジトリ

prisma-composer について

prisma-composerは、Prisma Cloud向けアプリを型付きサービスとリソースで構成するTypeScriptフレームワークとCLIです。モジュール単位でアプリを組み立て、依存関係の配線やRPC契約をコンパイル時に検証します。

主な機能

  1. 01型付きモジュールによるアプリ構成
  2. 02サービス接続のコンパイル時検査
  3. 03型付きRPCクライアントの生成
  4. 04型付き入力と依存関係のバインド
  5. 05ビルド成果物の組み立てとデプロイ

活用シーン

  1. 01再利用可能なサービスでのアプリ構築
  2. 02型付きRPCによるサービス連携
  3. 03Prisma Cloud ComputeとPostgresのプロビジョニング

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/prisma/composer --skill prisma-composer

インストール後、Claude Code で/prisma-composerを入力して読み込まれたか確認します。

詳細

Prisma Composer を使ったアプリの作成

Prisma App は、TypeScript で構成される Module のツリーです。末端には サービス(compute())と リソース(rawPostgres())があり、ルートの モジュールが型付きポートを通じてそれらを接続します。コードが受け取るものは、 サービスノードというただ1つの場所からすべて提供されます。

  • service.load() — 依存関係(型付き RPC クライアント、データベースバインディング)
  • service.input() — サービスの入力全体をまとめた、スキーマ検証済みの型付き オブジェクト。入力に含まれる認証情報は、値を秘匿する SecretString ボックスです
  • service.port() — バインドするために予約されたポート(デフォルトは 3000)。型付きで、 process.env は使いません

このフレームワークは、コードをバンドルしたり変換したりしません。お好みの バンドラー(bun build、next build)でアプリをビルドします。prisma-composer deploy は、 ビルド済みの出力をまとめ、Prisma Cloud(Compute + Prisma Postgres)にプロビジョニングします。

ここでの開発を速く、間違いにくくする要素は2つあります。どちらも活用してください。

  • コードを書く前に構成します。 機能を自分で実装する前に、既存の Module(下記)を 使ってください。接続に必要なのは数行です。
  • コンパイラーが接続をチェックします。 誤ったプロデューサーに接続された依存関係、 RPC ハンドラーの不足、形状が正しくない設定値などは、すべてデプロイ時ではなく tsc でエラーになります。型チェック、ビルド、デプロイの順に行ってください。 アプリが正しいかどうかを確かめるためにクラウドを使う必要はありません。

package.json に登場するパッケージは2つだけです。

PackageProvides
@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 データベースを所有し、storefront Next.js アプリに利用される RPC コントラクトを提供する auth サービスを見てみましょう。

コントラクトは、それを所有するサービスとともに配置します。メッセージの型には Standard Schema バリデーターを使用します。arkt​​ype が標準の選択肢です。

// 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() でコントラクトを提供します。ハンドラーマップのキーは expose ポートの名前に対応し、コンパイル時に網羅性が検証されます。

// 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 を返します。これを宣言する必要はありません。コントラクト、サービス、モジュール、アプリのコードのいずれにもキーはありません。

特に、次の2つのルールを守ってください。これに加えて独自のサービス間認証を実装しないこと、また、デプロイ済みの /rpc/<method> に curl して動作確認するようユーザーに案内しないことです。接続されていない呼び出し元は必ず 401 になり、デプロイが壊れているように見えますが、実際はそうではありません。コンシューマー経由、またはローカルでデバッグしてください。

呼び出しには冪等性キーが付与され、安全に再試行されます。 生成されたクライアントによるすべての呼び出しには Idempotency-Key が付きます。ターゲットのコールドスタート中に切断された呼び出しはバックオフを伴って再試行され、serve() はキーごとに1回だけ呼び出しを実行します。最初の呼び出しが完了した後に再試行が届いた場合は、ハンドラーを再実行せず、その結果を再送します。そのため、すべてのメソッドは安全に再試行でき、コントラクトで何かを宣言する必要はありません(「冪等かどうか」のフラグを追加しないでください。フレームワークにはそのような機能はありません)。その結果、次の2点が当てはまります。ハンドラーには省略可能な第3引数 (input, deps, ctx) を追加でき、1つのインスタンスのメモリーを超えて厳密に1回だけ実行する必要がある場合は、ctx.idempotencyKey(string | undefined。キーなしの呼び出し元の場合は値がありません)を読み取れます(ほとんどの場合、必要ありません)。また、ヘッダーのないリクエストは拒否されるのではなく、重複排除なしで1回だけ処理されるため、手作業で実装したプローブは動作しますが、再試行の安全性は得られません。

ローカル/テスト時リソースはプロビジョニングされないため、serve() はすべての呼び出しをそのまま通します。inputs にキーを指定しないでください
バインディングごと1つのプロバイダーを利用する2つのコンシューマーは異なるキーを持つため、片方からキーが漏れても、もう片方になりすますことはできません
スコープサービス単位です。有効なキーであれば、そのサービスが公開するすべてのメソッドにアクセスできます。アクセスを分けるには、2つのサービスに分割してください
ローテーションバインディングを削除する(またはスタックを破棄する)と、再デプロイされます。通常の再デプロイでは何も変わらず、ローテーションされません
ストレージデプロイが所有し、書き換える 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(サービスの入力バインディング。入力スキーマを宣言している場合に限り必須。§ Service input を参照)、および secrets(モジュール境界に転送されたシークレットの要件をバインド)を受け取ります。

ビルドはご自身で行います

フレームワークは、ビルド済みのものだけを組み立てます。ビルドするのはユーザー、組み立てるのはフレームワークです。通常のサーバープロセスの場合、entry は単一の自己完結型 ESM ファイルを指す必要があります。ランタイムの組み込みモジュール(bun、bun:*、node:*)はすべてインライン化せずに残します。これらはデプロイ VM が提供します。デプロイではそのファイルだけをコピーし、node_modules は配布しないため、インライン化されずに残ったものは起動時に失敗します。このようなファイルを生成できるバンドラーであれば、どれでも使用できます。bun の場合:

bun build src/server.ts --target=bun --outfile dist/server.mjs

1 つのパッケージに 2 つのサービスがある場合は、エントリーごとに 1 つずつ、個別にビルドする必要があります。1 つのマルチエントリービルドにすると、共有コードがチャンクに分割され、どちらの出力にも含まれなくなります。

ビルドによって単一ファイルではなくディレクトリが出力される場合(Bun の HTML インポートで生成される、サーバーとそのサーバーが配信するクライアントバンドル、CSS、画像など)、dir でディレクトリを指定し、entry でその中にある起動ファイルを指定します。

build: node({ module: import.meta.url, dir: '../dist/server', entry: 'server.js' })

dir はサービスモジュールを基準に解決され、entry は dir 内で解決され、さらにその中のサブディレクトリも指定できます。デプロイではディレクトリツリーをそのままコピーして、指定されたファイルを起動するため、サーバーは作業ディレクトリではなく import.meta.url を基準に兄弟ファイルを解決する必要があります。自動判定は行われません。また、次の 2 つのルールに注意してください。ディレクトリツリーにシンボリックリンクを含めることはできません(パッケージャーが拒否します。組み立てに失敗し、リンク名が表示されます)。また、entry は dir 内のファイルである必要があります(../ はエスケープではなくエラーになります)。単一ファイル形式では dir を省略します。

Next.js の場合、output: 'standalone' を指定した next build がビルド全体です。nextjs({ module, appDir }) で、アプリのルートをデプロイに伝えます。

デプロイ前に必ずビルドしてください。prisma-composer deploy はビルドを行いません。

デプロイ設定

通常、prisma-composer.config.ts は module.ts と同じ場所に置きますが、先祖ディレクトリのどこに置いてもかまいません。CLI はまずエントリーのディレクトリを検索し、次に各親ディレクトリを順に検索して、最も近い設定ファイルを使用します。このファイルが読み込まれるのは prisma-composer deploy/destroy の実行時のみで、アプリのコードからインポートされることはありません。JavaScript のみのプロジェクトでは、TypeScript のビルド対象から除外するために prisma-composer.config.mjs または .js という名前を付けられます(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 サービスが含まれる場合は、@prisma/composer/nextjs/control の nextjsBuild() を extensions に追加します。

データベース

Postgres の依存関係には 2 種類あります。

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 リソースへ 切り替えられます。

再利用可能なモジュール

Module は再利用の単位です。内部(データベースやサービス)は自身で所有し、 型付きポートのみを公開します。境界は第 2 引数で宣言し、ビルダーで内部を接続し、 公開するポートを返します。

// 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 };
  },
);

注意が必要な命名規則:プロビジョン ID が 3 文字未満の場合、プラットフォームに 拒否されます(データベース名は 'db' ではなく 'database' にします)。 また、サービス名がそれを囲むモジュール名と同じ場合、明示的な id を指定しないと auth.auth として表示されます。

モジュールでは境界の deps も宣言できます。これは、親がサービスの依存関係と 同じように接続する入力です。利用側からモジュールの内部が見えることはありません。

組み合わせて使える基本ブロック

モジュールは基本ブロックです。プロビジョニングし、公開ポートを接続すれば完了です。モジュールがすでに担っている機能を再実装する必要はありません。ファーストパーティーのモジュールは @prisma/composer-prisma-cloud に含まれています。数はまだ少ないですが、増えています。

インポートプロビジョニングするもの公開するもの
/cron からの cronランナーサービスで指定したスケジュールを実行する、常時稼働のスケジューラーなし
/storage からの storageS3 を基盤とする BLOB ストア(独自の Postgres と発行済み認証情報を使用)store
/streams からの streamsstore 上で永続化される、追記専用のイベントストリーム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()

サービスは、通常の値と認証情報をまとめた受信設定全体を、1 つの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 の値は加工されていない文字列として届きます。文字列フィールドにバインドしてください。値の保存先はステージのプラットフォーム変数であり、デプロイするシェルから値を設定するのは初期設定時のみです(事前チェックでは、プラットフォームにまだない名前をシェルからコピーし、両方にない場合は変数名を示して早期に失敗します)。プラットフォームの値を変更するには、再デプロイが必要です。
  • 値がない場合の扱いはスキーマが決めます:環境変数にバインドされたフィールドの変数が未設定(または空)の場合、結果は キーなし になります。これはスキーマで許可されている場合(オプションのフィールド、またはユニオンの選択肢)に限り有効です。デプロイレポートには、シリアライズされた入力ドキュメント(シークレットは含まず、シークレットは {"$secret":"VAR"} ポインターとして渡されます)と、値なしになったすべてのキーが表示されます。
  • 予約済みの port(デフォルトは 3000)はスキーマの対象外です。process.env ではなく、service.origin() と並ぶ service.port() から読み取ってください。フレームワークは 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 キーに渡したオブジェクトを、1 つのフラットなオブジェクトとして返します(サービスが入力スキーマを宣言している場合に限り必須です。そのまま渡され、検証は行われません)。モジュールの置き換えはテストランナーの役割です(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 は具体的な値である必要があります — エントリが自分でリッスンするため、OS が割り当てたポートは返されません。
  • close() はありません — 各結合テストファイルを独立したプロセスで実行してください(bun test がそうします)。
  • Next.js サービスには 3 つ目の引数として起動関数が必要です。ビルド済みエントリは Next のスタンドアロン出力内にあるため、@prisma/composer/nextjs/control の standaloneServerPath を使って解決してください。bootstrapService は起動前に解決済みポートを process.env.PORT としてエクスポートします。Next のスタンドアロンサーバーはこのポートにバインドします。
  • 入力スキーマを持つサービスでは、設定に input が必要です — provision() とまったく同じバインディングを使い、実際のシリアライズ/読み取り経路で処理されるため、起動したエントリの input() はデプロイ時に生成されるものを受け取ります。

渡す偽物について。 依存関係の型がその契約なので、その形状に合う値ならどれでも有効なダブルです。単純なオブジェクト(最速)、インメモリハンドラーを使う実際のクライアント、または実際のローカルサーバー(bootstrapService が操作するもの)を使えます。偽物は依存関係自身のパッケージから /fake エントリポイントとして src/ の外に提供してください。これにより、偽物と実際のサービスが常に同じ契約を共有できます。

ローカルでの実行

prisma-composer dev module.ts を実行すると、クラウド認証情報(PRISMA_*)なしで、このマシン上にアプリ全体を起動できます。デプロイ時と同じように、すべてのサービス、Postgres、バケットが接続されます。ローカルエミュレーターに対してデプロイと同じパイプラインを実行するため、デプロイ時とまったく同じように、先にビルドしてください。

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
1 つのサービスのログを追う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の2つだけです。対象環境であるステージは、 コード内ではなく、コマンドラインで指定します。

実行したい操作実行するコマンド
本番環境にデプロイするprisma-composer deploy module.ts
分離された環境にデプロイするprisma-composer deploy module.ts --stage <name>
1回の実行でアプリ名を上書きするprisma-composer deploy module.ts --name demo-42
分離された環境を削除するprisma-composer destroy module.ts --stage <name>
本番環境のリソースを削除するprisma-composer destroy module.ts --production

Prisma Appは1つのProjectであり、ステージはそのBranchです。それぞれ独自の コンピューティングリソース、空のデータベース、設定を持ちます。デプロイは 冪等です。同じステージに再デプロイすると、そのステージ内のリソースが更新されます。ステージ名は 有効なgit ref名である必要があります。無効な名前を指定すると、エラーになります。

削除する場合は、必ず対象を明示する必要があります。引数なしの prisma-composer destroyはエラーになり、--stageと--productionの併用もエラーです。ステージを 削除すると、そのリソースを削除した後にBranchが削除されます。本番環境のBranch自体が 削除されることはなく、その中のリソースだけが削除されます。本番環境を削除すると、 空になったProjectも削除されるため、手動で実行したスタックが空のProjectを 残すことはありません。ただし、別のステージのリソースが残っているProjectは保持されます。削除処理で何かが作成されることはありません。デプロイしたことのないステージを 削除しようとすると、作成されるのではなく失敗します。

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は、実際に公開されているアドレスに限って表示されます。コンピューティング サービスには表示されますが、データベースには表示されません(データベースには公開 エンドポイントではなく接続文字列があります)。製品がシークレット情報であるノード( s3-credentialsキーペアなど)は、リソース行自体を出力しません。報告可能なものを 何も公開しなかったノードも表示され、(no entities reported)と記されます。

以前のデプロイでは、最後にデプロイエンジンから生の{ outputs: {} }というデータが出力されていました。 常に空で、アプリとは無関係のものでした。これは削除されました。これを設定したものも、 利用したものもありません。

接続の契約はデプロイ時に検証されます

接続では必要な値を名前で宣言し、接続先のプロデューサーはそれらを提供する必要があります。 プロデューサーが値を1つでも提供しない場合、デプロイは失敗し、どの接続で、 どのパラメーターが不足しているか、またプロデューサーが何を提供したかが示されます。

Connection input "auth.db" declares param "url", but its producer "db" did not
supply it — the producer's outputs carry [host].

問題がある側で修正してください。プロデューサーのloweringが返すoutputsにその名前を 追加するか、値がなくても本当に問題ない場合は、接続のパラメーターに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 で 分岐してください。effect のバージョン競合は 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 です(または、コンシューマーが途中で処理を停止した場合に終了します)。 実行中のサービスが 0 件でも有効な結果であり、エラーではありません。

本番環境での注意点

  • スケールゼロにすると、アイドル状態のデータベース接続が閉じられます。 永続的なクライアントは、接続プールを小さくして再接続しやすくしないと、 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 のバグへの回避策です。alchemy 自身の effect ファミリーの バージョン範囲は、そのコードがサポートする範囲を超えてしまいます。リポジトリの サンプルにはこの設定が含まれています。)
  • イングレスはストリーミングレスポンスをバッファリングします。 開いたままの SSE 接続は何も配信せず、60 秒でタイムアウトします。ストリーミング HTTP レスポンスを 前提に設計しないでください。

Composer がまだ対応していないこと

API を作り出すのではなく、未対応の機能を明記してください。

  • 対話型認証はありません。 デプロイ時の認証には静的な PRISMA_SERVICE_TOKEN のみを使用します。login フローはありません。
  • メモリ内のコントラクトバインディングはありません。 依存関係を、HTTP を介さずに同じ場所にあるハンドラーへ接続することは、まだできません。ループバックのフェイクを使って bootstrapService を使用してください。
  • コントラクトの種類は HTTP 経由の RPC のみです。 gRPC、WebSocket、ストリーミングコントラクトには対応していません。

それ以外に不足しているものについては、prisma/composer リポジトリのサンプルと設計ドキュメント(examples/、docs/design/10-domains/、 docs/design/90-decisions/)を確認し、推測せずにリポジトリでイシューを作成してください。

コメント