mcpField
P
prisma-next-runtime

prisma-next-runtime

github.com/prisma数据与数据库4112.1k 次安装9 周前更新
代码仓库

关于 prisma-next-runtime

介绍如何在 db.ts 中接入 Prisma Next 数据库运行时,涵盖客户端构建、中间件、环境配置、数据库门面与生命周期管理,适用于应用和短期脚本集成。

主要功能

  1. 01配置 Postgres、SQLite 和 Mongo 运行时门面
  2. 02组合遥测、Lint 与预算中间件
  3. 03区分项目配置和环境变量
  4. 04管理延迟连接与客户端生命周期
  5. 05正确使用事务和脚本清理

适用场景

  1. 01设置应用的 db.ts 入口
  2. 02为数据库操作配置中间件
  3. 03运行短期脚本并关闭连接
  4. 04为数据库门面选择运行时配置

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/prisma-next-runtime确认已加载。

详细介绍

Prisma Next — Runtime(db.ts 配置)

编辑数据契约。其余交给 Prisma 处理。

本技能介绍 运行时入口 — db.ts,以及如何通过扩展、中间件和环境配置来组合数据库客户端。

适用场景

  • 用户在初始化后首次配置 db.ts。
  • 用户想添加中间件(遥测、lint、预算、自定义中间件)。
  • 用户需要按环境进行配置(开发环境与生产环境、多区域)。
  • 用户想在 Postgres、SQLite 和 Mongo 外观之间切换。
  • 用户想用 db.transaction(...) 包装操作(Postgres 和 SQLite)。
  • 用户运行一次性脚本(tsx my-script.ts、Node CLI、CI 任务)时,查询完成后进程仍无法退出,或者需要在脚本中进行清理(db.close()、await using)。
  • 用户提到:db.ts、postgres()、mongo()、middleware、telemetry、lints、budgets、DATABASE_URL、.env、connection pool、poolOptions、开发环境与生产环境、事务、只读副本、多数据库、脚本无法退出、挂起、db.close、db.end、关闭连接、pool.end、await using。

不适用场景

  • 用户想编写查询 → prisma-next-queries。
  • 用户使用 Supabase — 以角色为先的 supabase() 工厂、asUser(jwt) / asAnon() / asServiceRole()、JWT 配置、RLS → prisma-next-supabase。
  • 用户想编辑契约 → prisma-next-contract。
  • 用户想将 Prisma Next 接入构建工具(Vite 插件、Next.js 等)→ prisma-next-build。
  • 用户想调试连接或运行时错误 → prisma-next-debug。
  • 用户想提交 Bug 或功能请求 → prisma-next-feedback。

核心概念

  • db.ts 是运行时入口。 它会从 @prisma-next/<target> 外观(@prisma-next/postgres/runtime、@prisma-next/sqlite/runtime 或 @prisma-next/mongo/runtime)导入运行时工厂、契约产物(contract.json 和 contract.d.ts 中的 Contract 类型)以及任意中间件,并导出应用其余部分可导入的 db 值。
  • 外观的运行时工厂是用户编写的 db.ts 唯一需要导入的接口。 每个工厂都是 默认 导出。Postgres:import postgres from '@prisma-next/postgres/runtime';SQLite:import sqlite from '@prisma-next/sqlite/runtime';Mongo:import mongo from '@prisma-next/mongo/runtime'。工厂签名为 <Target><Contract>(options) — 一个类型参数(即 contract.d.ts 中的 Contract 类型)和一个 options 对象。
  • 延迟连接。 工厂不会同步连接数据库。静态查询接口(db.sql、db.orm)会立即可用;驱动程序 / 连接池会在首次需要运行时的调用中实例化(或在你显式调用 await db.connect({ url }) 时实例化)。因此,在环境变量就绪前加载的模块也可以导入 db.ts。
  • 中间件按顺序组合。 middleware: [...] 数组中的第一个中间件在 最外层 运行 — 它会在操作进入时最先看到操作,并在操作返回时最后看到操作。先运行遥测中间件,预算 / lint 失败就会显示在遥测跨度内。
  • prisma-next.config.ts 与 .env 的区别。 配置文件(defineConfig({ contract, db, extensions, migrations }))用于定义项目的静态结构:契约路径、已安装的扩展、迁移目录、默认连接字符串。.env 用于存放各环境的值(DATABASE_URL、密钥)。配置文件会通过 dotenv/config 自动读取 .env。在配置文件中硬编码 DATABASE_URL 会泄露凭据,并绕过按环境覆盖的机制。
  • 构建系统 / 开发服务器集成属于另一项技能。 vite dev 自动生成内容的说明见 prisma-next-build。运行时部分(本技能)会读取 contract.json / contract.d.ts,无论这些文件如何写入磁盘,因此两项技能可以顺畅配合。

工作流 — 基本 db.ts

概念:db.ts 是生成的契约产物(面向目标)与执行查询的运行时之间的衔接处。有三个关键导入——运行时工厂、Contract 类型(用于为静态查询接口提供类型),以及 JSON 产物(以便运行时在构造时验证结构)。

对于 --target postgres,init 会生成类似这样的内容:

// src/prisma/db.ts
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL'],
});

(目前 init 会在 prisma/db.ts 生成该文件——参见 prisma-next-quickstart 中的 TML-2532。标准路径为 src/prisma/db.ts;src/ 其余部分会根据目录层级从 ./prisma/db 或 ../prisma/db 导入。)

需要了解三点:

  • <Contract> 类型参数至关重要。 如果没有它,静态接口会退化为通用形状,并且模型名称的自动补全也会消失。务必从生成的 ./contract.d.ts 导入 Contract。
  • 必须使用 with { type: 'json' }。 这是 Node 的 ESM JSON 导入属性规范。没有它,导入会报错。
  • 构造时的 url 是可选的。 如果加载 db.ts 时没有设置 DATABASE_URL,工厂仍会返回客户端;之后可以调用 await db.connect({ url })。工厂采用延迟抛错的方式——仅在实际需要运行时时才会抛错。

Mongo façade 的构造形式相同——import mongo from '@prisma-next/mongo/runtime'——生命周期方法也相同,即 db.connect(...) / db.close()。Mongo façade 不提供 db.transaction(...)。 变通方法请参见 Prisma Next 目前尚未实现的功能。ORM 接口有一处不同:键。 在 Mongo 中,db.orm 使用集合的存储名称作为键(取自 @@map(...),如果未设置 @@map,则使用小写的模型名称),而不是 PSL 模型名称——因此,model User { … @@map("users") } 对应 db.orm.users,而不是 db.orm.User。Mongo 完全没有 SQL 构建器接口(db.sql.<table>)(db.sql 的值为 undefined)。完整规则以及 SQL 目标示例的改写方法,请参见 prisma-next-queries § MongoDB ORM 寻址。

工作流 — 作为脚本运行(清理)

概念:连接、查询,然后期望进程退出的短脚本在 Postgres 上会一直挂起,因为 façade 所拥有的 pg.Pool 会让 Node 的事件循环保持运行。数据往返操作会成功,但脚本永远不会退出。请在脚本返回前调用 await db.close()(或者在 脚本模块的顶层使用 await using,以便模块退出时执行清理——关于原因,请参见下方的块作用域警告)。

普通写法 — 从 db.ts 导出 db,在脚本中导入,并在末尾关闭:

// src/scripts/hello.ts
import { db } from '../prisma/db';

const created = await db.orm.User.create({ email: '[email protected]', name: 'Alice' });
const read = await db.orm.User.first();
console.log({ created, read });

await db.close();

TS 5.2+ 惯用写法 — 在脚本模块的顶层构造客户端,并让 [Symbol.asyncDispose] 在模块退出时调用 close():

// src/scripts/hello.ts — top-level await in a script module
import postgres from '@prisma-next/postgres/runtime';
import type { Contract } from '../prisma/contract.d';
import contractJson from '../prisma/contract.json' with { type: 'json' };

await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });

const user = await db.orm.User.first();
console.log(user);
// db.close() runs automatically when the script module exits.
await using 的作用域限定于代码块 — 请勿将其放在请求处理程序中

这是本节最重要的规则。await using db = postgres(...) 会在所属代码块退出时释放资源。在脚本模块中,该代码块是模块主体,资源会在进程退出时释放——这没有问题。在请求处理程序中,所属代码块是处理程序函数,因此资源会在每次请求之后释放——每次调用都会新建一个 pg.Pool,引发 TCP 连接风暴,形成不断拆建连接的热循环。

// DO NOT do this — closes the pool after every request.
app.get('/users', async (req, res) => {
  await using db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL! });
  const users = await db.orm.User.all();
  res.json(users);
});

正确的服务端模式是在 db.ts 中定义模块级单例,由处理程序导入,并且在进程生命周期内绝不关闭:

// src/prisma/db.ts — constructed once, lives for the process
export const db = postgres<Contract>({ contractJson, url: process.env.DATABASE_URL });

// src/routes/users.ts
import { db } from '../prisma/db';

app.get('/users', async (req, res) => {
  const users = await db.orm.User.all();
  res.json(users);
});

服务端(HTTP 处理程序、请求循环中的工作线程)在稳定运行期间完全不会调用 db.close()。连接池会在进程生命周期内保持打开。db.close() 和 await using 适用于生命周期较短的脚本——tsx my-script.ts、Node CLI 命令、CI 任务、一次性数据填充运行——不适用于在请求循环中运行的代码。

语义:

  • close() 是幂等的。 调用两次不会产生任何影响。
  • close() 是终止性的。 关闭后的 db 不会重新连接——如果需要另一个连接,请构造一个新客户端。关闭后,db.runtime()、db.connect(...)、db.transaction(...) 和 db.prepare(...) 都会以 Error('<target> client is closed') 拒绝(例如 'Postgres client is closed'、'SQLite client is closed'、'Mongo client is closed')。
  • close() 不会中止正在执行的查询。 调用 close() 前,请先 await 尚未完成的工作。调用 close() 后,db.runtime().execute(plan) 返回的异步迭代器,以及仍在使用的 PreparedStatement 句柄,都会在下次调用时失败。
  • 所有权。 close() 只会释放由该门面构造的资源(来自 { url } 的 pg.Pool、来自 { url } / { uri, dbName } 的 MongoClient、来自 { path } 的 SQLite 句柄)。如果你提供了自己的 pg.Pool / pg.Client(Postgres pg: 选项)、mongodb.MongoClient(Mongo mongoClient: 选项)或预构建的 binding,db.close() 不会触及这些资源——它们的生命周期由你管理。

不存在 db.end()。 通用的 node-postgres 方法名是 pg.Pool 上的 pool.end();Prisma Next 运行时客户端并不是 pg.Pool。正确的调用方式是 await db.close()。

工作流 — 遥测中间件

其理念是:遥测中间件会监视每项操作,并为其生成结构化事件(开始、成功、错误)。将这些事件与可观测性技术栈的收集器配合使用。

import postgres from '@prisma-next/postgres/runtime';
import { createTelemetryMiddleware } from '@prisma-next/middleware-telemetry';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL'],
  middleware: [
    createTelemetryMiddleware({
      onEvent: (event) => {
        // forward to your collector, log, etc.
      },
    }),
  ],
});

createTelemetryMiddleware 作为独立的用户可安装软件包(@prisma-next/middleware-telemetry)发布,而不是作为 postgres 门面的 /middleware 子路径提供。请直接安装。运行 pnpm ls @prisma-next/middleware-telemetry,确认它已写入锁文件。

工作流 — Lints 和预算中间件

其理念是:lints 会捕获通过类型检查但仍然存在的编写错误(例如,在大型表上执行不带 WHERE 的 DELETE、不带 LIMIT 的 SELECT);预算则会在运行时强制执行行数和延迟上限。两者都会通过结构化错误封装呈现问题,以便 Agent 根据代码分支处理。

这些功能在底层 SQL 运行时软件包(@prisma-next/sql-runtime)中提供,目前尚未从 postgres 门面重新导出——请参阅 Prisma Next 目前尚未实现的功能。examples/prisma-next-demo/src/prisma/db.ts 下的示例应用展示了规范的导入方式。

import postgres from '@prisma-next/postgres/runtime';
import { budgets, lints } from '@prisma-next/sql-runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL'],
  middleware: [
    lints({
      severities: {
        selectStar: 'warn',
        noLimit: 'error',
        deleteWithoutWhere: 'error',
        updateWithoutWhere: 'error',
        readOnlyMutation: 'error',
      },
    }),
    budgets({
      maxRows: 10_000,
      defaultTableRows: 10_000,
      tableRows: { user: 10_000, post: 50_000 },
      maxLatencyMs: 1_000,
      severities: { rowCount: 'error', latency: 'warn' },
    }),
  ],
});

如需了解完整的选项,请阅读源码:packages/2-sql/5-runtime/src/middleware/lints.ts 和 .../budgets.ts。severities 中的键(lints 的 selectStar、noLimit、deleteWithoutWhere、updateWithoutWhere、readOnlyMutation;budgets 的 rowCount、latency)是唯一可信来源;不要推测 ripgrep 搜不到的键。

工作流 — 组合多个中间件

middleware: [
  createTelemetryMiddleware({ onEvent }),  // outermost — sees all sub-failures as inner errors
  lints({ severities: { noLimit: 'error' } }),
  budgets({ maxLatencyMs: 5_000 }),         // innermost — runs closest to the driver
],

顺序很重要:最外层会包装内层。遥测放在最前面,预算 / lint 失败就会作为 spans 被记录(Agent 可以在同一条 trace 中,将 lint 代码与操作关联起来)。

工作流 — 配置连接

概念:运行时采用三种绑定形式之一——url、pg(预先构造的 pg.Pool 或 pg.Client),或 binding(显式的类型标签)。三者互斥。pg 形式适用于已自行管理连接池的项目(例如 Lambda 层);url 是默认形式。连接池调优参数为 poolOptions.connectionTimeoutMillis / poolOptions.idleTimeoutMillis,而不是 driverOptions。

// Default — URL string, factory constructs the pool.
postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL'],
  poolOptions: {
    connectionTimeoutMillis: 20_000,
    idleTimeoutMillis: 30_000,
  },
});

// BYO pool — pass a pg.Pool you already created.
import { Pool } from 'pg';
const pool = new Pool({ connectionString: process.env['DATABASE_URL'] });
postgres<Contract>({ contractJson, pg: pool });

url 和 pg 键在类型层面互斥;同时传入两者会报错。

DATABASE_URL 存放在 .env 中。CLI 会在 emit / verify / migration 命令中读取它;运行时会在加载 db.ts 时通过 process.env 读取它。

工作流 — 按环境配置(开发环境与生产环境)

概念:每个环境使用一个 DATABASE_URL;其余 db.ts 结构保持一致。对于中间件差异(例如仅在开发环境中启用严格 lint),可在 db.ts 中根据 process.env['NODE_ENV'] 分支处理。

const isProd = process.env['NODE_ENV'] === 'production';

export const db = postgres<Contract>({
  contractJson,
  url: process.env['DATABASE_URL'],
  middleware: isProd
    ? [createTelemetryMiddleware({ onEvent })]
    : [
        createTelemetryMiddleware({ onEvent }),
        lints({ severities: { noLimit: 'error', deleteWithoutWhere: 'error' } }),
      ],
});

本地使用 .env;生产环境使用部署平台的机密。切勿提交 .env。

工作流 — 事务

此概念适用于 Postgres 和 SQLite。db.transaction(fn) 会开启一个事务,并向回调提供 tx 上下文,其中包含与 db 相同的 sql / orm 接口;回调成功返回时提交,抛出任何错误时回滚。在回调内部,请使用 tx.sql 和 tx.orm,而不是 db.sql / db.orm,以便写入操作在事务中执行。Mongo façade 不提供 db.transaction(...)。

await db.transaction(async (tx) => {
  const user = await tx.orm.User.create({ email: '[email protected]' });
  await tx.orm.Post.create({ userId: user.id, title: 'hello' });
  // If either call throws, both inserts roll back.
});

回调会返回你从中返回的任何值——事务包装器会将其原样传递。tx 对象提供 execute(plan),用于在事务中执行 SQL 构建器计划。

工作流 — 在 Postgres、SQLite 和 Mongo 之间切换

概念:façade 的选择固化在 db.ts(@prisma-next/postgres、@prisma-next/sqlite 或 @prisma-next/mongo)和 prisma-next.config.ts(你从中导入哪个 defineConfig)中。若要切换项目的目标,请在同一目录中重新运行 prisma-next init 并选择其他目标——初始化流程会检测到现有脚手架,并提示是否重新初始化(--force 可跳过提示)。PN 会为新的 façade 重新生成 prisma-next.config.ts 和 db.ts。必须根据新目标的惯用方式重新编写契约源文件(Mongo 表达嵌套文档;Postgres / SQLite 表达关系)。

切换后(Mongo):

// src/prisma/db.ts (Mongo)
import mongo from '@prisma-next/mongo/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = mongo<Contract>({ contractJson, url: process.env['DATABASE_URL'] });

SQLite:

// src/prisma/db.ts (SQLite)
import sqlite from '@prisma-next/sqlite/runtime';
import type { Contract } from './contract.d';
import contractJson from './contract.json' with { type: 'json' };

export const db = sqlite<Contract>({ contractJson, path: 'app.db' });

构造时 path 为可选参数(之后可调用 db.connect({ path }));省略它时,façade 仍会返回一个客户端。SQLite façade 提供与 Postgres 相同的 db.sql、db.orm、db.transaction(...)、db.close() 和 [Symbol.asyncDispose] 接口。Mongo façade 提供 db.orm、db.close() 和 [Symbol.asyncDispose],但没有 db.sql 和 db.transaction(...)。

db.sql / db.orm 接口的名称保持不变;各接口提供的运算符则根据目标类型而定(Mongo 不支持 JOIN)。

工作流 — 构建系统 / 开发服务器集成

如果你希望开发服务器运行期间自动重新生成契约制品(而不是每次契约源文件变更时都手动运行 prisma-next contract emit),可使用 prisma-next-build 中的构建工具插件:

  • Vite:安装 @prisma-next/vite-plugin-contract-emit,并在 vite.config.ts 中注册 prismaVitePlugin('prisma-next.config.ts')。
  • Next.js、Webpack、esbuild、Rollup、Turbopack:目前还没有第一方插件——可通过运行 prisma-next contract emit 的 prebuild 脚本作为变通方案。操作指南请参阅 prisma-next-build。

运行时部分(本技能)无论如何都相同:db.ts 会从磁盘读取 contract.json + contract.d.ts。构建系统插件的作用是在开发期间确保这些文件保持最新。

常见陷阱

  1. 在 prisma-next.config.ts 中硬编码 DATABASE_URL。 这会泄露凭据,并绕过各环境的覆盖设置。请使用 .env。
  2. 在 postgres<Contract>(...) 中省略 <Contract> 类型参数。 如果省略,静态接口会退化为通用形态,你也会失去模型的自动补全功能。没有第二个类型参数——旧的双参数签名(postgres<Contract, TypeMaps>)已不存在。
  3. 导入合同时忘记添加 with { type: 'json' }。 这是 Node ESM JSON 导入属性规范的要求。
  4. 中间件顺序很重要。 外层中间件会包装内层中间件。如果希望遥测捕获内层中间件的错误,请将遥测放在最前面。
  5. 从不存在的门面子路径导入中间件。 @prisma-next/postgres/middleware 并不存在。遥测来自 @prisma-next/middleware-telemetry;目前,lint / 预算来自 @prisma-next/sql-runtime(参见 Prisma Next 尚未实现的功能)。
  6. 臆造 lint / 预算选项名称。 Lint 使用 severities(包含上述五个键),而不是 requireWhere / maxRowsWithoutLimit。预算使用 maxLatencyMs(而不是 maxDurationMs),以及 maxRows / defaultTableRows / tableRows。拿不准时,请阅读源码。
  7. 切换目标后未重新生成。 合同产物具有目标专属结构;更改目标后请重新生成。
  8. Postgres 查询结束后脚本仍不退出。 pg.Pool 会让 Node 事件循环保持运行。解决方法:在脚本返回前执行 await db.close(),或者在脚本模块顶部使用 await using db = postgres<Contract>(...)。不要在请求处理程序中使用 await using db = postgres(...)——它的作用域仅限于代码块,每次请求后都会关闭连接池。正确的服务端模式是在 db.ts 中使用模块级单例,使其在整个进程生命周期内保持运行。

Prisma Next 尚未实现的功能

  • @prisma-next/postgres/middleware 子路径。 postgres 门面会重新导出运行时工厂(./runtime)、配置(./config)、合同构建器(./contract-builder)、控制(./control)、系列(./family)、目标(./target)和无服务器(./serverless)——但不包括中间件。目前的变通方法:从 @prisma-next/sql-runtime 导入 lints 和 budgets,并从 @prisma-next/middleware-telemetry 导入 createTelemetryMiddleware。请通过 prisma-next-feedback 提交你遇到的其他缺口。
  • 多数据库路由 / 只读副本。 Prisma Next 未提供内置的主库 / 副本路由器,也没有支持分片的客户端。变通方法:为每个数据存储配置独立的 db.ts 实例,并在应用代码中调用正确的实例。如果需要一流的多数据库路由支持,请通过 prisma-next-feedback 技能提交功能请求。
  • 将连接池作为一等配置字段。 poolOptions.connectionTimeoutMillis 和 poolOptions.idleTimeoutMillis 已接入,但 pg.Pool 的其他调优选项(最大连接数、allowExitOnIdle、SSL 选项等)没有以名称形式公开。变通方法:自行构建 pg.Pool 并通过 pg: 传入。如果希望门面公开更多连接池字段,请通过 prisma-next-feedback 技能提交功能请求。
  • 内置查询日志中间件。 Prisma Next 未提供“记录每条查询”的中间件。变通方法:编写一个包装每项操作并记录日志的小型自定义中间件;或者使用 createTelemetryMiddleware,在 onEvent 回调中记录日志。如果需要内置查询日志,请通过 prisma-next-feedback 技能提交功能请求。

参考文件

此技能有意仅包含正文;选项级别的详细信息应以 prisma-next init --help、packages/3-extensions/postgres/src/config/define-config.ts 中的 defineConfig 工厂、packages/3-extensions/postgres/src/runtime/postgres.ts 中的 postgres() 工厂,以及 packages/2-sql/5-runtime/src/middleware/{lints,budgets}.ts 中的中间件源码为准。拿不准时,请阅读源码。

检查清单

  • db.ts 从 @prisma-next/<target>/runtime(postgres、sqlite 或 mongo)导入运行时工厂,并从 ./contract.d 导入 Contract 类型。
  • 在契约 JSON 导入语句中使用 with { type: 'json' }。
  • <Contract> 是 postgres<Contract>(...) 唯一的类型参数(没有第二个参数)。
  • DATABASE_URL 位于 .env 中,而不是 prisma-next.config.ts 中。
  • 有意安排中间件顺序(通常将遥测放在最外层)。
  • lints / budgets 使用已验证的选项键(severities、maxLatencyMs、maxRows、tableRows)。
  • 按 NODE_ENV 或类似变量控制各环境间的差异(如有)。
  • 未在任何已提交的文件中硬编码凭据。
  • 未臆造 @prisma-next/postgres/middleware 子路径、@prisma-next/postgres-extension-audit 包,或 postgres<...> 的第二个类型参数。
  • 未声称 Mongo 门面上存在 db.transaction(...)——只有 Postgres 和 SQLite 提供此功能。
  • 未臆造只读副本 / 多数据库 / 额外的连接池配置——已指向 What Prisma Next doesn't do yet,并引导至 prisma-next-feedback。
  • 对于构建系统 / 开发服务器相关提示(Vite 插件、Next.js 插件等),已引导至 prisma-next-build。

评论

支持 Markdown