@btravstack/prisma

中文文档
0.58.0发布于 2026/11/08MITTypeScriptESM + CJS零依赖构建来源已验证近 30 天下载 8418

The Prisma starter for @btravstack/core: DATABASE_URL through Config, the Postgres driver adapter, and a client whose pool is the application scope's

原始使用文档
文档目录8 节
  1. A worked example
  2. What it owns
  3. Result s, on the @btravstack/prisma/result subpath
  4. Row-level security, on the @btravstack/prisma/rls subpath
  5. Tenant discovery for scheduled work
  6. What it does not
  7. Options
  8. License

@btravstack/prisma

The Prisma starter for @btravstack/core: DATABASE_URL bound through Config, per-query observation as middleware, and a client whose pool is the application scope's. Prisma 8.

📖 Documentation · API Reference

pnpm add @btravstack/prisma @btravstack/core @btravstack/config @btravstack/di unthrown \
  @prisma/[email protected] [email protected]

Install the required peers, so the application holds a single copy of each; temporal-polyfill is the Postgres package's own peer from rc.14. The @prisma/orm-postgres peer is ^8.0.0-rc.11, and both ends are tested: an application already on rc.11 keeps it.

Upgrading with released migrations: upgrade @btravstack/prisma and its @btravstack/* peers, and leave Prisma where it is. No contract, migration or database marker changes. Upgrading Prisma itself from rc.11 rewrites every released migration and re-signs every database, which is Prisma's path rather than this package's; see Supported Prisma trains. The prisma CLI is yours, as a dev dependency: the client is built from your emitted contract, so there is none for this package to ship. Node >=22.12.

Production packaging: the pinned Postgres facade requires @prisma/orm-toolchain, including its CLI-engine peer and build-tool closure. A /runtime import and pnpm deploy --prod do not remove those transitive dependencies. Keep contract emission in the build stage and migrations in a deployment Job, with strict peers enabled; a toolchain-free runtime install remains blocked upstream. That section documents the manifest split and the tracked dependency.

This is Prisma 8, which is a different package family rather than a version bump — @prisma/orm-postgres replaces @prisma/client and @prisma/adapter-pg both, and there is no @prisma/instrumentation. For Prisma 7 use @btravstack/[email protected].

A worked example

import { prismaDatabase } from "@btravstack/prisma";
import { contractFromJson } from "@prisma/orm-postgres/static";

const contract = contractFromJson<Contract>(contractJson);
const database = prismaDatabase("OrderDatabase")({
  client: ({ url, middleware }) => postgres({ contract, url, middleware }),
});

That is the whole surface. database is a module carrying the port a composition root reads; the provider binding DATABASE_URL, the resourceful client provider and the health check are inside it:

export const PersistenceModule = Module("Persistence")({
  imports: [database],
  exports: [database.port],
  needs: [Env],
});

The client type is yours, and that is deliberate. A Prisma 8 client is typed by your emitted Contract and built from your contract.json, so there is none for this package to ship. From Prisma rc.19, turn that JSON into a typed value once with contractFromJson<Contract>(contractJson) and pass contract to postgres({ contract, ... }). The client arrow is where your contract and this starter meet, and the port is typed by exactly what it returns. Spread the middleware it hands you into the client: that is where the per-query observation below comes from.

What it owns

  • DATABASE_URL through Config. A missing or blank value is a modeled ConfigInvalid naming the variable, not a throw — so a misconfigured deployment exits 78 with the reason on stderr instead of crashing on the first query.
  • The pool's lifetime. The provider is resourceful, so release closes it on every exit path, including a boot that fails after it ran.
  • A database that answers before any work runs. Opening the scope runs the health check's probe, so a fresh pool is verified before its first burst of transactions, and a database that does not answer fails the boot with DatabaseUnreachable — the database's name and the driver's cause, never the URL.
  • A role row security applies to. Where any table has row-level security enabled, a role that is a superuser, has BYPASSRLS, or owns a row-secured table that is not FORCEd fails the boot with RowSecurityBypassed, naming the role and the tables it would bypass — every policy would admit every row for it.
  • A health check, named after the starter — SELECT 1 through the raw lane, folded into the kernel's GET /healthz with nothing wired. /readyz does not read it: failing readiness on a dependency every replica shares removes them all at once.
  • Every query handed to Observers, as one statement middleware, so composing observability() writes the failures as lines and otel() opens the spans and mints the instruments — with no flag here and no port list to satisfy when you compose neither. A middleware sees the ORM lane, the SQL builder and the raw lane alike, and carries the runtime's own latencyMs.

Results, on the @btravstack/prisma/result subpath

Prisma 8 throws a structured error carrying PostgreSQL's own SQLSTATE. tryQuery turns that into a Result with three modeled arms — UniqueConstraintViolation (23505, with the constraint the database named), ForeignKeyViolation (23503) and NotAuthorized (42501, including a write a row-security policy refused). Anything else is a defect, unless you list it in raises — a SQLSTATE your own trigger raises on purpose then arrives as a RaisedException carrying that code.

const saved = tryQuery(() => db.orm.orders.Order.create({ orderId })).mapErrCases(
  (matcher, defect) =>
    matcher
      .with(P.tag("UniqueConstraintViolation"), () => duplicate(orderId))
      .with(P.tag("ForeignKeyViolation"), P.tag("NotAuthorized"), (cause) => defect(cause)),
);

It takes a thunk rather than a promise: an AsyncResult is eager, so a promise built at the call site has already started before the Result exists.

Row-level security, on the @btravstack/prisma/rls subpath

The policy is declared in your contract — @@rls on the model and a policy_all block beside it — and the planner emits ENABLE ROW LEVEL SECURITY and CREATE POLICY like any other migration operation. The pin is a transaction:

const orders = await tenantPinned(db, tenant, (tx) => tx.orm.orders.Order.all());

For a consistent snapshot, pass { isolationLevel: "repeatable read", readOnly: true } as the fourth argument. The starter establishes these characteristics before the tenant pin. Use its boot-probed client and the callback's tx for every related read; omitted characteristics preserve the connection defaults. See consistent snapshots.

set_config(…, true) is transaction-local, which is why this opens one rather than handing back a pinned client: a session-scoped pin works only while the pool happens to return the same connection, and fails silently the first time it does not. The policy must read the same setting — app.tenant_id is only the default — because a tenantPinned(db, tenant, work, { setting }) and a policy naming a different one deny every row and every write, which looks exactly like row security working.

Two halves stay the deployment's, and both fail quietly when forgotten: Prisma 8 authors policies but no GRANTs, and it emits ENABLE ROW LEVEL SECURITY with no way to say FORCE — so the table's owner, which is the role that ran the migrations, bypasses every policy. Connect as a role that is neither the owner nor a superuser — the boot refuses either with RowSecurityBypassed. The reference page carries the DDL and the smaller lines that fail the same way.

Tenant discovery for scheduled work

tenantListing on /rls declares a tenant-ID-only query (100 IDs per page, resuming after the last ID) and a checked Prisma migration installing its function, owner policy and execution grant. It works under forced row-level security while ordinary application reads remain tenant-scoped. Shared data does not require a privileged application connection. See tenant listing.

What it does not

Running migrations. A deployment runs prisma db migrate against the same database before the process starts — and, where row security is on, as its owner rather than the role DATABASE_URL carries. An application that migrates itself at boot races every other replica.

Engine-level tracing. There is no engine: Prisma 8 is a TypeScript runtime and ships no instrumentation package, so the statement middleware is the whole seam and it already carries the runtime's own latency.

Transactions. Commit boundaries belong to the adapter, spelled at the call — db.transaction(fn) is the primitive. The rls subpath opens one transaction to pin a setting on and the work inside it is yours. There is no unit-scoped transaction and there will not be one; see the kernel maps nothing.

A repository base class. Ports are your application's vocabulary, not this package's.

Options

OptionWhereWhat it is
nameprismaDatabase(name)the port's id and the health check's name — required
clientprismaDatabase(name)({ client })builds your client from the URL and middleware this package bound — required
DATABASE_URLenvironment, read by prismaDatabase(name)the connection string — required, validated at graph build; blank is an error, exit 78
settingtenantPinned(db, tenant, work, { setting })the PostgreSQL run-time setting the policy reads — default app.tenant_id
isolationLevel, readOnlytenantPinned(db, tenant, work, options)transaction characteristics established before the pin — connection defaults when omitted
raisestryQuery(run, { raises }), tenantPinned(…, { raises })SQLSTATEs modeled as RaisedException rather than defects — default none
schema, table, tenantColumntenantListing(options)the physical table and tenant column for a narrow listing
name, owner, roletenantListing(options)the listing function, its trusted owner and application caller

There is no instrumented flag: observation is a set port every call is handed to, so a graph composing no observability pays one inert call and no port list. The full table — defaults, semantics and the reasoning — lives on the reference page, which is this list's one detailed home.

License

MIT

版本与发布记录

@btravstack/prisma 共发布 62 个版本,首次发布于 2026/08/26;过去 12 个月发布了 62 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.58.0latest2026/11/08今天
0.57.02026/11/071 天前
0.56.12026/11/062 天前
0.56.02026/11/062 天前
0.55.12026/11/035 天前
0.55.02026/11/035 天前

关于 @btravstack/prisma 的常见问题

@btravstack/prisma 是什么?

The Prisma starter for @btravstack/core: DATABASE_URL through Config, the Postgres driver adapter, and a client whose pool is the application scope's

如何安装 @btravstack/prisma?

在项目目录运行 npm install @btravstack/prisma,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:unthrown(^5.0.0)、@btravstack/di(^0.58.0)、@btravstack/core(^0.58.0)、@btravstack/config(^0.58.0)、@prisma/orm-postgres(^8.0.0-rc.11)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@btravstack/prisma 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@btravstack/prisma 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@btravstack/prisma 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@btravstack/prisma 可以商用吗?

@btravstack/prisma 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@btravstack/prisma 的下载量有多少?

根据 npm 官方下载统计,@btravstack/prisma 在 2026/10/09 – 2026/11/07 这 30 天内被下载 8,418 次,日均约 281 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@btravstack/prisma 还在维护吗?

最新版本 0.58.0 发布于 2026/11/08(今天);至今共发布 62 个版本,其中过去 12 个月发布了 62 个;首次发布于 2026/08/26。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。