From 612f6b406998346f7095634be4491da4d0496a83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 11:44:23 -0700 Subject: [PATCH 01/11] docs(next): add Middleware and Extensions sections Adds six pages to the Prisma Next docs collection under /next: Middleware (new section, 5 pages): - how-middleware-works: hooks, lifecycle, registration, built-ins - built-in-budgets: row-count and latency ceilings (BUDGET.* errors) - built-in-lints: structural query checks (LINT.* rules) - built-in-cache: opt-in per-query caching via cacheAnnotation - authoring-custom-middleware: complete slow-query warning example (implemented and tested in prisma/prisma-next examples/prisma-next-demo), hook/context reference, beforeCompile rewriting variation Extensions (new section, 1 page): - using-extensions: end-to-end pgvector setup (install, control and runtime registration, contract types, baseline migrations) plus the catalog table of available extensions All package names, import paths, option defaults, and error codes verified against the prisma/prisma-next source at v0.14.0. Uses "middleware" throughout (the retired "plugin" term does not appear). Note: the spec listed "Built-in: telemetry", but no telemetry middleware package exists in the product today; the shipped built-in from a separate package is @prisma-next/middleware-cache, documented instead. The client-extensions redirect from the ticket is deliberately not added: /orm/prisma-client/client-extensions still serves live Prisma 7 content, so redirecting it away is deferred to the broader IA work (DR-8687). Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/(index)/meta.json | 2 + .../docs/(index)/next/extensions/meta.json | 4 + .../next/extensions/using-extensions.mdx | 99 ++++++++++++ .../authoring-custom-middleware.mdx | 145 ++++++++++++++++++ .../next/middleware/built-in-budgets.mdx | 86 +++++++++++ .../next/middleware/built-in-cache.mdx | 108 +++++++++++++ .../next/middleware/built-in-lints.mdx | 88 +++++++++++ .../next/middleware/how-middleware-works.mdx | 87 +++++++++++ .../docs/(index)/next/middleware/meta.json | 10 ++ apps/docs/cspell.json | 1 + 10 files changed, 630 insertions(+) create mode 100644 apps/docs/content/docs/(index)/next/extensions/meta.json create mode 100644 apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx create mode 100644 apps/docs/content/docs/(index)/next/middleware/meta.json diff --git a/apps/docs/content/docs/(index)/meta.json b/apps/docs/content/docs/(index)/meta.json index c7375e791a..7cc5e30000 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,6 +11,8 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", + "next/middleware", + "next/extensions", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/extensions/meta.json b/apps/docs/content/docs/(index)/next/extensions/meta.json new file mode 100644 index 0000000000..e0703517b7 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/extensions/meta.json @@ -0,0 +1,4 @@ +{ + "title": "Extensions", + "pages": ["using-extensions"] +} diff --git a/apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx b/apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx new file mode 100644 index 0000000000..4a4d7b282c --- /dev/null +++ b/apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx @@ -0,0 +1,99 @@ +--- +title: Using extensions +description: Extensions add database capabilities like vector search, geospatial data, and full-text search to a Prisma Next project. +url: /next/extensions/using-extensions +metaTitle: Using extensions in Prisma Next +metaDescription: Add an extension to a Prisma Next project, from install to first query, and browse the catalog of available extensions such as pgvector, PostGIS, and ParadeDB. +badge: early-access +--- + +An extension is a package that teaches Prisma Next a database capability it does not have out of the box: new column types, query operations, and index types, along with the migrations that install the underlying database feature. Vector search, geospatial data, and full-text search all arrive this way. + +Adding one is an install plus two registrations, using pgvector as the example: + +```npm +npm install @prisma-next/extension-pgvector +``` + +Register the extension's control-plane descriptor in your project config, where it participates in contract emission and migrations: + +```ts title="prisma-next.config.ts" +import pgvector from '@prisma-next/extension-pgvector/control'; +import { defineConfig } from '@prisma-next/postgres/config'; + +export default defineConfig({ + contract: './src/prisma/contract.prisma', + extensions: [pgvector], + db: { + connection: process.env.DATABASE_URL, + }, +}); +``` + +Register its runtime descriptor on the client, where it contributes codecs and query operations: + +```ts title="src/prisma/db.ts" +import pgvector from '@prisma-next/extension-pgvector/runtime'; +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({ + contractJson, + url: process.env.DATABASE_URL, + extensions: [pgvector], +}); +``` + +The extension's types are now available in your contract. Declare a vector column with an explicit dimension: + +```prisma title="src/prisma/contract.prisma" +types { + Embedding1536 = pgvector.Vector(1536) +} + +model Post { + id String @id @default(uuid()) + title String + embedding Embedding1536? +} +``` + +Run `prisma-next db init` (or `prisma-next db update` on an existing database). Extensions ship a baseline migration in their contract space, so this step installs the database-side feature for you, `CREATE EXTENSION IF NOT EXISTS vector` in pgvector's case. Then query with the operations the extension adds: + +```ts +const plan = db.sql.public.post + .select('id', 'title') + .select('distance', (f, fns) => fns.cosineDistance(f.embedding, queryVector)) + .orderBy((f, fns) => fns.cosineDistance(f.embedding, queryVector), { direction: 'asc' }) + .limit(10) + .build(); + +const similar = await db.runtime().execute(plan); +``` + +## Capabilities + +An extension declares the database capabilities it relies on under a namespaced key, `pgvector.cosine` for the query above. You do not configure capabilities by hand: registering the extension declares them in the contract, and the runtime verifies at `connect()` time that the database it reaches actually supports what the contract uses. A managed database that lacks the underlying feature fails at connect, not mid-query. + +## Available extensions + +The catalog is first-party today and designed for community authors: extension packs are versioned npm packages with a documented layout, and the [call for extension authors](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) explains how to build one. + +| Extension | What it adds | Package | Databases | +| --- | --- | --- | --- | +| [pgvector](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector) | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | +| [PostGIS](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis) | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | +| [ParadeDB](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb) | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | +| [Supabase](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase) | A Supabase-shaped contract with `auth.*` and `storage.*` as queryable external tables | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | +| [arktype-json](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json) | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | + +Two maturity notes, since Prisma Next itself is in Early Access: the ParadeDB pack currently covers the `key_field` index option only (per-field tokenizer configuration is not there yet), and the Supabase pack is an early milestone that ships the contract surface, with the role-binding runtime still to come. Each extension's README linked above states exactly what its current version covers. + +Every table row links to the extension's guide, with schema declaration, migration behavior, and the full query surface for that pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. + +## See also + +- [How middleware works](/next/middleware/how-middleware-works) for wrapping queries rather than adding database capabilities +- [Quickstart with PostgreSQL](/next/quickstart/postgresql) to set up a project to add extensions to +- [Prisma Next overview](/orm/next) for the contract-first model extensions plug into diff --git a/apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx new file mode 100644 index 0000000000..9576a2d38c --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx @@ -0,0 +1,145 @@ +--- +title: Authoring custom middleware +description: Write your own Prisma Next middleware as an object with a name and hooks, and register it on the runtime. +url: /next/middleware/authoring-custom-middleware +metaTitle: Authoring custom middleware in Prisma Next +metaDescription: Write a custom Prisma Next middleware, with a complete slow-query warning example, the hook and context reference, and query-rewriting variations. +badge: early-access +--- + +A custom middleware is a plain object: give it a `name`, declare the runtime family it targets, and implement the hooks you need. No base class, no registration API beyond the `middleware` array. + +This complete example warns whenever a query runs longer than a threshold: + +```ts title="src/prisma/slow-query-warning.ts" +import type { SqlMiddleware } from '@prisma-next/sql-runtime'; + +export interface SlowQueryWarningOptions { + /** Latency above this many milliseconds logs a warning. Default: 250. */ + readonly thresholdMs?: number; +} + +export function slowQueryWarning(options?: SlowQueryWarningOptions): SqlMiddleware { + const thresholdMs = options?.thresholdMs ?? 250; + + return { + name: 'slow-query-warning', + familyId: 'sql', + + async afterExecute(plan, result, ctx) { + if (result.latencyMs <= thresholdMs) return; + ctx.log.warn({ + code: 'APP.SLOW_QUERY', + message: `Query took ${result.latencyMs}ms (threshold: ${thresholdMs}ms)`, + details: { + sql: plan.sql, + rowCount: result.rowCount, + latencyMs: result.latencyMs, + source: result.source, + planExecutionId: ctx.planExecutionId, + }, + }); + }, + }; +} +``` + +Register it in the `middleware` array on the runtime setup, next to the built-ins: + +```ts title="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' }; +import { slowQueryWarning } from './slow-query-warning'; + +export const db = postgres({ + contractJson, + url: process.env.DATABASE_URL, + middleware: [lints(), budgets(), slowQueryWarning({ thresholdMs: 250 })], +}); +``` + +Every query now reports when it crosses the threshold, whatever surface issued it: + +```text +warn { + code: 'APP.SLOW_QUERY', + message: 'Query took 412ms (threshold: 250ms)', + details: { sql: 'SELECT "id", "email" FROM "user" LIMIT 10', rowCount: 10, ... } +} +``` + +## How it works + +Type your middleware as `SqlMiddleware` (from `@prisma-next/sql-runtime`) and TypeScript infers every hook's parameters, so you rarely need to import the plan or context types directly. The hooks, all optional: + +- `beforeCompile(draft, ctx)` runs before the query AST is lowered to SQL. Return a new draft (`{ ...draft, ast: rewritten }`) to rewrite the query, or `undefined` to pass through. This hook is SQL-specific. +- `beforeExecute(plan, ctx, params?)` runs after lowering. `plan.sql` is the rendered statement, `plan.params` the bound parameters, and `plan.ast` the pre-lowering AST. Throw to block the query. Use `params.replaceValue(...)` to mutate parameter values before they are encoded. +- `intercept(plan, ctx)` runs last before the driver. Return `{ rows }` (an iterable or async iterable of row objects) to answer the query yourself and skip the driver, or `undefined` to pass through. +- `onRow(row, plan, ctx)` runs for every row the driver yields. +- `afterExecute(plan, result, ctx)` runs once at the end, with `result.rowCount`, `result.latencyMs`, `result.completed` (`false` when the driver errored), and `result.source` (`'driver'`, or `'middleware'` when an `intercept` answered). + +Hooks run in registration order at every site, and the context (`ctx`) is shared across one `execute()` call: + +| Context field | What it gives you | +| --- | --- | +| `ctx.log` | Structured `info` / `warn` / `error` (and optional `debug`) sinks wired to the runtime's logger | +| `ctx.now()` | Current time from the runtime's clock; prefer it over `Date.now()` so tests can control time | +| `ctx.planExecutionId` | Unique ID per `execute()` call, the same value in every hook, for correlating observations | +| `ctx.scope` | `'runtime'`, `'connection'`, or `'transaction'`; use it to skip work in scopes you should not touch | +| `ctx.contentHash(plan)` | Stable digest of the statement plus parameters, for keying caches or coalescing requests | +| `ctx.signal` | Optional `AbortSignal` for cancellation; observe it in long-running hook work | +| `ctx.contract` | The runtime's contract, when a hook needs schema information | + +## Variations + +### Rewrite queries with `beforeCompile` + +`beforeCompile` sees the typed AST before lowering, which makes cross-cutting query rewrites possible: tenant scoping, soft-delete filters, or row-level restrictions. This middleware appends a predicate to every `SELECT` on the `user` table: + +```ts +import type { SqlMiddleware } from '@prisma-next/sql-runtime'; +import { AndExpr, type BinaryExpr } from '@prisma-next/sql-relational-core/ast'; + +export function scopeUserSelects(predicate: BinaryExpr): SqlMiddleware { + return { + name: 'scope-user-selects', + familyId: 'sql', + + async beforeCompile(draft) { + if (draft.ast.kind !== 'select') return undefined; + if (draft.ast.from?.kind !== 'table-source') return undefined; + if (draft.ast.from.name !== 'user') return undefined; + const where = draft.ast.where ? AndExpr.of([draft.ast.where, predicate]) : predicate; + return { ...draft, ast: draft.ast.withWhere(where) }; + }, + }; +} +``` + +Because each middleware's returned draft feeds the next one, rewrites compose in registration order. + +### Short-circuit with `intercept` + +Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/next/middleware/built-in-cache) is the reference implementation of this pattern. + +### Support every database + +A middleware without `familyId` and `targetId` is cross-family and can register on PostgreSQL and MongoDB runtimes alike. Cross-family middleware cannot use SQL-specific surfaces like `beforeCompile` or `plan.sql`; they work against the shared hooks (`intercept`, `beforeExecute`, `onRow`, `afterExecute`) and the family-agnostic plan shape. + +## Common gotchas + +:::warning + +- The runtime validates middleware when you construct the client. A `familyId` that does not match the runtime throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` at startup, and a `targetId` without a `familyId` throws `RUNTIME.MIDDLEWARE_INCOMPATIBLE`. +- `afterExecute` also fires when the driver fails, with `result.completed` set to `false`. Errors you throw from `afterExecute` on that failure path are swallowed so they cannot mask the driver error; put must-not-fail logic elsewhere. +- Throwing from `afterExecute` on a successful query fails the call even though the database work already happened. Reserve it for cases where failing loudly is the point, as the [budgets](/next/middleware/built-in-budgets) latency check does. + +::: + +## See also + +- [How middleware works](/next/middleware/how-middleware-works) +- [Built-in: budgets](/next/middleware/built-in-budgets), [Built-in: lints](/next/middleware/built-in-lints), and [Built-in: cache](/next/middleware/built-in-cache) as production examples of the hook surface +- [Using extensions](/next/extensions/using-extensions) when you want to add database capabilities instead of wrapping queries diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx new file mode 100644 index 0000000000..fb3f4c7ef6 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx @@ -0,0 +1,86 @@ +--- +title: 'Built-in: budgets' +description: The budgets middleware fails queries that would read too many rows or run longer than a latency budget. +url: /next/middleware/built-in-budgets +metaTitle: 'Prisma Next built-in middleware: budgets' +metaDescription: Wire and configure the budgets middleware in Prisma Next to cap row counts and query latency, and understand the errors it raises. +badge: early-access +--- + +The `budgets` middleware puts a cost ceiling on every query: it rejects queries that would read more rows than you allow and reports queries that run longer than your latency budget. + +Register it on the runtime with the row and latency limits you want to enforce: + +```ts title="src/prisma/db.ts" +import postgres from '@prisma-next/postgres/runtime'; +import { budgets } from '@prisma-next/sql-runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +export const db = postgres({ + contractJson, + url: process.env.DATABASE_URL, + middleware: [ + budgets({ + maxRows: 10_000, + defaultTableRows: 10_000, + tableRows: { user: 10_000, post: 10_000 }, + maxLatencyMs: 1_000, + }), + ], +}); +``` + +`budgets` is SQL-family middleware (it declares `familyId: 'sql'`), so it registers on PostgreSQL runtimes. It ships with `@prisma-next/sql-runtime`, which comes in through `@prisma-next/postgres`; you do not install anything extra. + +## Options + +All options are optional. + +| Option | Type | Default | What it controls | +| --- | --- | --- | --- | +| `maxRows` | `number` | `10_000` | The most rows a single query may produce, checked against both the pre-execution estimate and the observed row stream | +| `defaultTableRows` | `number` | `10_000` | Assumed row count for tables not listed in `tableRows`, used by the estimator | +| `tableRows` | `Record` | `{}` | Per-table row-count assumptions for the estimator, keyed by table name | +| `maxLatencyMs` | `number` | `1_000` | Latency budget for one query, checked after execution | +| `severities.rowCount` | `'warn' \| 'error'` | `'error'` | Whether pre-execution row violations block the query or log a warning | + +## How it enforces the budget + +The middleware checks at three points in the query lifecycle: + +1. **Before execution**, it inspects the query plan's AST. A `SELECT` with no `LIMIT` (and no aggregation that collapses the result) is treated as unbounded, and a bounded query whose estimated rows exceed `maxRows` is over budget. The estimate comes from `tableRows` and `defaultTableRows`, so tune those to your data. Violations raise `BUDGET.ROWS_EXCEEDED` before the driver runs. +2. **While rows stream**, it counts what the database actually returns. If the observed count passes `maxRows`, it throws `BUDGET.ROWS_EXCEEDED` and aborts the stream, which protects you when the estimate was wrong. +3. **After execution**, it compares the measured `latencyMs` against `maxLatencyMs` and raises `BUDGET.TIME_EXCEEDED` when the query ran too long. This check runs after the work is done, so it cannot save the slow query itself; it exists to make slow queries loud instead of invisible. + +## What you see when a budget trips + +A blocked query rejects with a runtime error carrying the budget code and details: + +```text +BUDGET.ROWS_EXCEEDED: Unbounded SELECT query exceeds budget + { source: 'ast', maxRows: 10000 } +``` + +The `source` field tells you whether the violation came from the pre-execution estimate (`'ast'`) or the observed row stream (`'observed'`). Latency violations carry the measured and allowed values: + +```text +BUDGET.TIME_EXCEEDED: Query latency exceeds budget + { latencyMs: 2481, maxLatencyMs: 1000 } +``` + +To fix an unbounded-select violation, add a `LIMIT` to the query (`take(...)` on the ORM client, `.limit(...)` on the SQL query builder) or raise the budget deliberately. + +## Common gotchas + +:::warning + +The pre-execution row check estimates from `tableRows` and `defaultTableRows`, not from live table statistics. If your real tables are much larger than the configured assumptions, a query can pass the estimate and still hit the observed-row limit mid-stream. Keep `tableRows` roughly honest for the tables you query most. + +::: + +## See also + +- [How middleware works](/next/middleware/how-middleware-works) +- [Built-in: lints](/next/middleware/built-in-lints) for structural query checks that complement budgets +- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx new file mode 100644 index 0000000000..75c471dafd --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx @@ -0,0 +1,108 @@ +--- +title: 'Built-in: cache' +description: The cache middleware serves repeated reads from an in-memory store, opted in per query with a cache annotation. +url: /next/middleware/built-in-cache +metaTitle: 'Prisma Next built-in middleware: cache' +metaDescription: Wire the Prisma Next cache middleware, opt queries in with cacheAnnotation and a TTL, and understand cache keys, scopes, and hits. +badge: early-access +--- + +The cache middleware serves repeated reads from an in-memory store instead of the database. Caching is strictly opt-in per query: only queries annotated with a TTL are cached, everything else passes through untouched. + +Install the package and register the middleware first in the chain, so cache hits short-circuit before any other middleware does work: + +```npm +npm install @prisma-next/middleware-cache +``` + +```ts title="src/prisma/db.ts" +import { createCacheMiddleware } from '@prisma-next/middleware-cache'; +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({ + contractJson, + url: process.env.DATABASE_URL, + middleware: [ + createCacheMiddleware({ maxEntries: 1_000 }), + lints(), + budgets({ maxRows: 10_000 }), + ], +}); +``` + +The cache is family-agnostic: unlike `budgets` and `lints`, it works on MongoDB runtimes as well as PostgreSQL. + +## Opt a query in + +Annotate a read with `cacheAnnotation` and a `ttl` in milliseconds. On the SQL query builder, chain `.annotate(...)` onto the plan: + +```ts +import { cacheAnnotation } from '@prisma-next/middleware-cache'; + +const plan = db.sql.public.user + .select('id', 'email') + .annotate(cacheAnnotation({ ttl: 60_000 })) + .limit(10) + .build(); + +const users = await db.runtime().execute(plan); +``` + +On the ORM client, pass the annotation through the query's meta callback: + +```ts +import { cacheAnnotation } from '@prisma-next/middleware-cache'; +import { orm } from '@prisma-next/sql-orm-client'; + +const runtime = await db.connect(); +const models = orm({ runtime, context: db.context, collections: {} }).public; + +const user = await models.User.first({ id }, (meta) => + meta.annotate(cacheAnnotation({ ttl: 60_000 })), +); +``` + +The first call runs against the database and stores the raw rows. A second call with the same query and parameters inside the TTL window is served from the store, and the driver is not invoked. + +The annotation payload has three fields: + +| Field | Type | What it does | +| --- | --- | --- | +| `ttl` | `number` | Time-to-live in milliseconds. Without a `ttl`, the annotation is inert and the query is not cached | +| `skip` | `boolean` | When `true`, bypasses the cache for this call even though a `ttl` is set. Useful as a force-refresh knob | +| `key` | `string` | Overrides the computed cache key with your own string, stored as-is | + +`cacheAnnotation` is declared read-only (`applicableTo: ['read']`). Passing it to a write such as `create`, `update`, or `delete` is both a type error and a runtime error, so a mutation cannot be cached by accident. + +## How keys and hits work + +By default the cache key is a content hash of the executed query: the post-lowering statement plus its bound parameters, combined with the contract's storage hash. Two lookups with different parameter values land in different slots; the same lookup hits. Because schema migrations rotate the storage hash, entries cached before a migration cannot serve queries against the new schema. + +Cache hits are still visible to the rest of the middleware chain: `afterExecute` fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so logging and metrics keep working for cached reads. + +The cache only acts at runtime scope. Queries executed inside a transaction or on an explicitly checked-out connection always go to the database, because read-after-write consistency is the caller's expectation there. + +## Options + +| Option | Type | Default | What it controls | +| --- | --- | --- | --- | +| `maxEntries` | `number` | `1000` | Maximum entries in the default in-memory store; least-recently-used entries are evicted first. Only consulted when `store` is omitted | +| `store` | `CacheStore` | in-memory LRU | Bring your own store implementation, for example one backed by Redis | +| `clock` | `() => number` | `Date.now` | Time source for TTL expiry, useful in tests | + +## Common gotchas + +:::warning + +The default store is in-memory and per-process. Two instances of your app do not share it, and a deploy clears it. That makes it a good fit for hot, tolerant-of-staleness reads, and a poor fit as a source of truth. For shared caching, implement `store` against your infrastructure. + +::: + +## See also + +- [How middleware works](/next/middleware/how-middleware-works) +- [Authoring custom middleware](/next/middleware/authoring-custom-middleware), including how `intercept` short-circuiting works +- [Built-in: budgets](/next/middleware/built-in-budgets) diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx new file mode 100644 index 0000000000..9670312176 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx @@ -0,0 +1,88 @@ +--- +title: 'Built-in: lints' +description: The lints middleware inspects each query's structure before it runs and blocks or warns on risky shapes. +url: /next/middleware/built-in-lints +metaTitle: 'Prisma Next built-in middleware: lints' +metaDescription: Wire and configure the lints middleware in Prisma Next to block DELETE and UPDATE without WHERE and warn on unbounded or SELECT * queries. +badge: early-access +--- + +The `lints` middleware inspects the structure of every query before it executes and blocks or warns on shapes that are usually mistakes, like a `DELETE` with no `WHERE` clause. + +Register it on the runtime; the defaults are useful without configuration: + +```ts title="src/prisma/db.ts" +import postgres from '@prisma-next/postgres/runtime'; +import { lints } from '@prisma-next/sql-runtime'; +import type { Contract } from './contract.d'; +import contractJson from './contract.json' with { type: 'json' }; + +export const db = postgres({ + contractJson, + url: process.env.DATABASE_URL, + middleware: [lints()], +}); +``` + +`lints` is SQL-family middleware (it declares `familyId: 'sql'`), so it registers on PostgreSQL runtimes. It ships with `@prisma-next/sql-runtime`, which comes in through `@prisma-next/postgres`. + +## The rules + +Lints run in `beforeExecute`, against the query plan's AST. Each rule has a severity: `error` throws and blocks the query before it reaches the database, `warn` logs through the runtime log and lets the query run. + +| Rule | Code | Default severity | Fires when | +| --- | --- | --- | --- | +| DELETE without WHERE | `LINT.DELETE_WITHOUT_WHERE` | `error` | A `DELETE` has no `WHERE` clause | +| UPDATE without WHERE | `LINT.UPDATE_WITHOUT_WHERE` | `error` | An `UPDATE` has no `WHERE` clause | +| No LIMIT | `LINT.NO_LIMIT` | `warn` | A `SELECT` has no `LIMIT` clause | +| SELECT star | `LINT.SELECT_STAR` | `warn` | A query selects all columns instead of naming them | + +A warning is logged as a structured event you can route to your logger: + +```text +warn { + code: 'LINT.NO_LIMIT', + message: 'Unbounded SELECT may return large result sets', + details: { table: 'user' } +} +``` + +## Configure severities + +Raise or lower any rule per project through `severities`. For example, treat unbounded selects as hard failures in a service where every list endpoint must paginate: + +```ts +lints({ + severities: { + noLimit: 'error', + selectStar: 'warn', + }, +}); +``` + +The severity keys are `deleteWithoutWhere`, `updateWithoutWhere`, `noLimit`, and `selectStar`, plus `readOnlyMutation` for the raw-SQL guardrail described below. + +## Raw SQL + +Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. Raw SQL does not have that structure, so `lints` falls back to text heuristics for raw plans: it flags `select *`, a missing `LIMIT`, and a mutation smuggled through a read-only raw query. Control the fallback with `fallbackWhenAstMissing`: + +```ts +lints({ fallbackWhenAstMissing: 'raw' }); // default: heuristic checks on the SQL text +lints({ fallbackWhenAstMissing: 'skip' }); // skip linting for plans without an AST +``` + +Heuristics can misread SQL that a structural check would classify correctly, so treat raw-SQL lint findings as a prompt to look at the query rather than a verdict. + +## Common gotchas + +:::warning + +`LINT.DELETE_WITHOUT_WHERE` and `LINT.UPDATE_WITHOUT_WHERE` default to `error`, so a deliberate full-table mutation (for example a backfill) will be blocked. For a one-off script, either add a tautological but explicit `WHERE` clause or register `lints` with that rule set to `warn` in that script's runtime setup. Lowering the severity globally gives up the protection everywhere. + +::: + +## See also + +- [How middleware works](/next/middleware/how-middleware-works) +- [Built-in: budgets](/next/middleware/built-in-budgets) for row-count and latency ceilings +- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) diff --git a/apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx new file mode 100644 index 0000000000..095a09a340 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx @@ -0,0 +1,87 @@ +--- +title: How middleware works +description: Middleware wraps every query your app sends through Prisma Next, so you can run code before and after execution. +url: /next/middleware/how-middleware-works +metaTitle: How middleware works in Prisma Next +metaDescription: What Prisma Next middleware is, the hooks it exposes, how to register it on the runtime, and which middleware ships built in. +badge: early-access +--- + +A middleware is an object with a name and one or more hooks that the Prisma Next runtime calls at fixed points around every query, so you can observe, guard, rewrite, or answer queries without touching application code. + +You register middleware once, on the runtime setup, through the `middleware` option: + +```ts title="src/prisma/db.ts" +import { createCacheMiddleware } from '@prisma-next/middleware-cache'; +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({ + contractJson, + url: process.env.DATABASE_URL, + middleware: [ + createCacheMiddleware({ maxEntries: 1_000 }), + lints(), + budgets({ maxRows: 10_000, maxLatencyMs: 1_000 }), + ], +}); +``` + +Every query then passes through the chain: the ORM client, the SQL query builder, and raw SQL all execute through the same runtime, so one `middleware` array covers all of them. On MongoDB, pass the same option to `mongo(...)` from `@prisma-next/mongo/runtime`. + +## How it works + +Each middleware implements some of five hooks. The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. + +| Hook | When it runs | What it can do | +| --- | --- | --- | +| `beforeCompile(draft, ctx)` | Before the query AST is lowered to SQL (SQL runtimes only) | Return a modified draft to rewrite the query, for example to add a predicate | +| `beforeExecute(plan, ctx, params?)` | After lowering, before the driver runs | Inspect the plan (`plan.sql`, `plan.ast`), validate it, throw to block it, or mutate parameter values | +| `intercept(plan, ctx)` | Right before the driver | Return rows directly and skip the driver entirely (this is how the cache serves hits) | +| `onRow(row, plan, ctx)` | Once per row the driver yields | Observe or count rows; throw to abort the stream | +| `afterExecute(plan, result, ctx)` | After the query finishes | Read `result.rowCount`, `result.latencyMs`, `result.completed`, and `result.source` for timing, logging, and budgets | + +The full lifecycle for a SQL query: + +```text +beforeCompile → SQL lowering → beforeExecute → parameter encoding → intercept → driver → onRow (per row) → afterExecute +``` + +Two details of the ordering matter in practice: + +- **Registration order is execution order.** At every hook site, the first middleware in the array runs first. If a `beforeCompile` hook rewrites the query, the next middleware sees the rewritten draft. This is why the example above registers the cache first: on a cache hit, its `intercept` answers the query before `lints()` and `budgets()` do any work. +- **`intercept` short-circuits.** When a middleware returns rows from `intercept`, the driver never runs and `onRow` is skipped, but `afterExecute` still fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so observability keeps working for served-from-cache reads. + +Every hook receives a context (`ctx`) with the contract, a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/next/middleware/authoring-custom-middleware) for the full surface. + +## What ships built in + +Three middleware ship with Prisma Next today: + +| Middleware | Import | What it does | +| --- | --- | --- | +| [`budgets`](/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Fails queries that would read too many rows or run longer than a latency budget | +| [`lints`](/next/middleware/built-in-lints) | `@prisma-next/sql-runtime` | Blocks or warns on risky query shapes, such as `DELETE` without `WHERE` | +| [`cache`](/next/middleware/built-in-cache) | `@prisma-next/middleware-cache` | Serves repeated reads from an in-memory store, opted in per query | + +`budgets` and `lints` are SQL-specific and come with the SQL runtime. The cache lives in its own package because it is family-agnostic: it works on PostgreSQL and MongoDB. + +## Errors and failure behavior + +A middleware that throws from `beforeCompile`, `beforeExecute`, or `onRow` fails the query: the error propagates to the caller and the driver work stops. This is how `lints()` blocks a `DELETE` without a `WHERE` clause before it reaches the database. + +When the driver itself fails, the runtime still calls `afterExecute` on every middleware with `result.completed` set to `false`, then rethrows the original driver error. Errors thrown by `afterExecute` while handling a failed query are swallowed so they cannot mask the original error. + +## Database compatibility + +A middleware declares which runtime family it supports through an optional `familyId`. `budgets` and `lints` declare `familyId: 'sql'`, so they register on PostgreSQL but not on MongoDB. Middleware with no `familyId`, like the cache, work on any runtime. The runtime checks compatibility when you construct the client and throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` if a middleware does not match, so a wrong registration fails at startup rather than at query time. + +## See also + +- [Built-in: budgets](/next/middleware/built-in-budgets) +- [Built-in: lints](/next/middleware/built-in-lints) +- [Built-in: cache](/next/middleware/built-in-cache) +- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) +- [Using extensions](/next/extensions/using-extensions) for adding new database capabilities rather than wrapping queries diff --git a/apps/docs/content/docs/(index)/next/middleware/meta.json b/apps/docs/content/docs/(index)/next/middleware/meta.json new file mode 100644 index 0000000000..713c6a9861 --- /dev/null +++ b/apps/docs/content/docs/(index)/next/middleware/meta.json @@ -0,0 +1,10 @@ +{ + "title": "Middleware", + "pages": [ + "how-middleware-works", + "built-in-budgets", + "built-in-lints", + "built-in-cache", + "authoring-custom-middleware" + ] +} diff --git a/apps/docs/cspell.json b/apps/docs/cspell.json index af3453afcd..cce0b6b964 100644 --- a/apps/docs/cspell.json +++ b/apps/docs/cspell.json @@ -235,6 +235,7 @@ "Pacman", "pageinspect", "pegasusheavy", + "paradedb", "permitio", "pgbouncer", "pgcat", From 5b4167cc49838657a3e1c81aa1a9caf80b12d40c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 15:06:08 -0700 Subject: [PATCH 02/11] docs(next): move Middleware and Extensions under /docs/orm/next These are ORM concept docs, so they belong in the Prisma Next ORM collection at /orm/next (next to the overview), not the getting-started collection at /next. Updates frontmatter urls, cross-links, and moves the sidebar wiring from (index)/meta.json to orm/next/meta.json. No redirects needed: the /next/middleware and /next/extensions URLs were never deployed to production. Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/(index)/meta.json | 2 -- .../next/extensions/meta.json | 0 .../next/extensions/using-extensions.mdx | 4 ++-- apps/docs/content/docs/orm/next/meta.json | 6 +++++- .../authoring-custom-middleware.mdx | 12 +++++------ .../next/middleware/built-in-budgets.mdx | 8 ++++---- .../next/middleware/built-in-cache.mdx | 8 ++++---- .../next/middleware/built-in-lints.mdx | 8 ++++---- .../next/middleware/how-middleware-works.mdx | 20 +++++++++---------- .../next/middleware/meta.json | 0 10 files changed, 35 insertions(+), 33 deletions(-) rename apps/docs/content/docs/{(index) => orm}/next/extensions/meta.json (100%) rename apps/docs/content/docs/{(index) => orm}/next/extensions/using-extensions.mdx (97%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/authoring-custom-middleware.mdx (90%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/built-in-budgets.mdx (93%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/built-in-cache.mdx (94%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/built-in-lints.mdx (92%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/how-middleware-works.mdx (83%) rename apps/docs/content/docs/{(index) => orm}/next/middleware/meta.json (100%) diff --git a/apps/docs/content/docs/(index)/meta.json b/apps/docs/content/docs/(index)/meta.json index 7cc5e30000..c7375e791a 100644 --- a/apps/docs/content/docs/(index)/meta.json +++ b/apps/docs/content/docs/(index)/meta.json @@ -11,8 +11,6 @@ "---Prisma Next---", "next/quickstart", "next/add-to-existing-project", - "next/middleware", - "next/extensions", "---Prisma ORM---", "...prisma-orm", "---Prisma Postgres---", diff --git a/apps/docs/content/docs/(index)/next/extensions/meta.json b/apps/docs/content/docs/orm/next/extensions/meta.json similarity index 100% rename from apps/docs/content/docs/(index)/next/extensions/meta.json rename to apps/docs/content/docs/orm/next/extensions/meta.json diff --git a/apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx similarity index 97% rename from apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx rename to apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 4a4d7b282c..2726450629 100644 --- a/apps/docs/content/docs/(index)/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -1,7 +1,7 @@ --- title: Using extensions description: Extensions add database capabilities like vector search, geospatial data, and full-text search to a Prisma Next project. -url: /next/extensions/using-extensions +url: /orm/next/extensions/using-extensions metaTitle: Using extensions in Prisma Next metaDescription: Add an extension to a Prisma Next project, from install to first query, and browse the catalog of available extensions such as pgvector, PostGIS, and ParadeDB. badge: early-access @@ -94,6 +94,6 @@ Every table row links to the extension's guide, with schema declaration, migrati ## See also -- [How middleware works](/next/middleware/how-middleware-works) for wrapping queries rather than adding database capabilities +- [How middleware works](/orm/next/middleware/how-middleware-works) for wrapping queries rather than adding database capabilities - [Quickstart with PostgreSQL](/next/quickstart/postgresql) to set up a project to add extensions to - [Prisma Next overview](/orm/next) for the contract-first model extensions plug into diff --git a/apps/docs/content/docs/orm/next/meta.json b/apps/docs/content/docs/orm/next/meta.json index ae5f3be609..32193e9583 100644 --- a/apps/docs/content/docs/orm/next/meta.json +++ b/apps/docs/content/docs/orm/next/meta.json @@ -4,6 +4,10 @@ "root": true, "pages": [ "---Introduction---", - "index" + "index", + "---Middleware---", + "...middleware", + "---Extensions---", + "...extensions" ] } diff --git a/apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx similarity index 90% rename from apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx rename to apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index 9576a2d38c..1723d101c5 100644 --- a/apps/docs/content/docs/(index)/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -1,7 +1,7 @@ --- title: Authoring custom middleware description: Write your own Prisma Next middleware as an object with a name and hooks, and register it on the runtime. -url: /next/middleware/authoring-custom-middleware +url: /orm/next/middleware/authoring-custom-middleware metaTitle: Authoring custom middleware in Prisma Next metaDescription: Write a custom Prisma Next middleware, with a complete slow-query warning example, the hook and context reference, and query-rewriting variations. badge: early-access @@ -122,7 +122,7 @@ Because each middleware's returned draft feeds the next one, rewrites compose in ### Short-circuit with `intercept` -Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/next/middleware/built-in-cache) is the reference implementation of this pattern. +Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/orm/next/middleware/built-in-cache) is the reference implementation of this pattern. ### Support every database @@ -134,12 +134,12 @@ A middleware without `familyId` and `targetId` is cross-family and can register - The runtime validates middleware when you construct the client. A `familyId` that does not match the runtime throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` at startup, and a `targetId` without a `familyId` throws `RUNTIME.MIDDLEWARE_INCOMPATIBLE`. - `afterExecute` also fires when the driver fails, with `result.completed` set to `false`. Errors you throw from `afterExecute` on that failure path are swallowed so they cannot mask the driver error; put must-not-fail logic elsewhere. -- Throwing from `afterExecute` on a successful query fails the call even though the database work already happened. Reserve it for cases where failing loudly is the point, as the [budgets](/next/middleware/built-in-budgets) latency check does. +- Throwing from `afterExecute` on a successful query fails the call even though the database work already happened. Reserve it for cases where failing loudly is the point, as the [budgets](/orm/next/middleware/built-in-budgets) latency check does. ::: ## See also -- [How middleware works](/next/middleware/how-middleware-works) -- [Built-in: budgets](/next/middleware/built-in-budgets), [Built-in: lints](/next/middleware/built-in-lints), and [Built-in: cache](/next/middleware/built-in-cache) as production examples of the hook surface -- [Using extensions](/next/extensions/using-extensions) when you want to add database capabilities instead of wrapping queries +- [How middleware works](/orm/next/middleware/how-middleware-works) +- [Built-in: budgets](/orm/next/middleware/built-in-budgets), [Built-in: lints](/orm/next/middleware/built-in-lints), and [Built-in: cache](/orm/next/middleware/built-in-cache) as production examples of the hook surface +- [Using extensions](/orm/next/extensions/using-extensions) when you want to add database capabilities instead of wrapping queries diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx similarity index 93% rename from apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx rename to apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx index fb3f4c7ef6..63efa8c5ca 100644 --- a/apps/docs/content/docs/(index)/next/middleware/built-in-budgets.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx @@ -1,7 +1,7 @@ --- title: 'Built-in: budgets' description: The budgets middleware fails queries that would read too many rows or run longer than a latency budget. -url: /next/middleware/built-in-budgets +url: /orm/next/middleware/built-in-budgets metaTitle: 'Prisma Next built-in middleware: budgets' metaDescription: Wire and configure the budgets middleware in Prisma Next to cap row counts and query latency, and understand the errors it raises. badge: early-access @@ -81,6 +81,6 @@ The pre-execution row check estimates from `tableRows` and `defaultTableRows`, n ## See also -- [How middleware works](/next/middleware/how-middleware-works) -- [Built-in: lints](/next/middleware/built-in-lints) for structural query checks that complement budgets -- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) +- [How middleware works](/orm/next/middleware/how-middleware-works) +- [Built-in: lints](/orm/next/middleware/built-in-lints) for structural query checks that complement budgets +- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx similarity index 94% rename from apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx rename to apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx index 75c471dafd..d991f85756 100644 --- a/apps/docs/content/docs/(index)/next/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx @@ -1,7 +1,7 @@ --- title: 'Built-in: cache' description: The cache middleware serves repeated reads from an in-memory store, opted in per query with a cache annotation. -url: /next/middleware/built-in-cache +url: /orm/next/middleware/built-in-cache metaTitle: 'Prisma Next built-in middleware: cache' metaDescription: Wire the Prisma Next cache middleware, opt queries in with cacheAnnotation and a TTL, and understand cache keys, scopes, and hits. badge: early-access @@ -103,6 +103,6 @@ The default store is in-memory and per-process. Two instances of your app do not ## See also -- [How middleware works](/next/middleware/how-middleware-works) -- [Authoring custom middleware](/next/middleware/authoring-custom-middleware), including how `intercept` short-circuiting works -- [Built-in: budgets](/next/middleware/built-in-budgets) +- [How middleware works](/orm/next/middleware/how-middleware-works) +- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware), including how `intercept` short-circuiting works +- [Built-in: budgets](/orm/next/middleware/built-in-budgets) diff --git a/apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx similarity index 92% rename from apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx rename to apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx index 9670312176..50a6e49df7 100644 --- a/apps/docs/content/docs/(index)/next/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx @@ -1,7 +1,7 @@ --- title: 'Built-in: lints' description: The lints middleware inspects each query's structure before it runs and blocks or warns on risky shapes. -url: /next/middleware/built-in-lints +url: /orm/next/middleware/built-in-lints metaTitle: 'Prisma Next built-in middleware: lints' metaDescription: Wire and configure the lints middleware in Prisma Next to block DELETE and UPDATE without WHERE and warn on unbounded or SELECT * queries. badge: early-access @@ -83,6 +83,6 @@ Heuristics can misread SQL that a structural check would classify correctly, so ## See also -- [How middleware works](/next/middleware/how-middleware-works) -- [Built-in: budgets](/next/middleware/built-in-budgets) for row-count and latency ceilings -- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) +- [How middleware works](/orm/next/middleware/how-middleware-works) +- [Built-in: budgets](/orm/next/middleware/built-in-budgets) for row-count and latency ceilings +- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) diff --git a/apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx similarity index 83% rename from apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx rename to apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 095a09a340..4afca0b8f7 100644 --- a/apps/docs/content/docs/(index)/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -1,7 +1,7 @@ --- title: How middleware works description: Middleware wraps every query your app sends through Prisma Next, so you can run code before and after execution. -url: /next/middleware/how-middleware-works +url: /orm/next/middleware/how-middleware-works metaTitle: How middleware works in Prisma Next metaDescription: What Prisma Next middleware is, the hooks it exposes, how to register it on the runtime, and which middleware ships built in. badge: early-access @@ -54,7 +54,7 @@ Two details of the ordering matter in practice: - **Registration order is execution order.** At every hook site, the first middleware in the array runs first. If a `beforeCompile` hook rewrites the query, the next middleware sees the rewritten draft. This is why the example above registers the cache first: on a cache hit, its `intercept` answers the query before `lints()` and `budgets()` do any work. - **`intercept` short-circuits.** When a middleware returns rows from `intercept`, the driver never runs and `onRow` is skipped, but `afterExecute` still fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so observability keeps working for served-from-cache reads. -Every hook receives a context (`ctx`) with the contract, a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/next/middleware/authoring-custom-middleware) for the full surface. +Every hook receives a context (`ctx`) with the contract, a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) for the full surface. ## What ships built in @@ -62,9 +62,9 @@ Three middleware ship with Prisma Next today: | Middleware | Import | What it does | | --- | --- | --- | -| [`budgets`](/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Fails queries that would read too many rows or run longer than a latency budget | -| [`lints`](/next/middleware/built-in-lints) | `@prisma-next/sql-runtime` | Blocks or warns on risky query shapes, such as `DELETE` without `WHERE` | -| [`cache`](/next/middleware/built-in-cache) | `@prisma-next/middleware-cache` | Serves repeated reads from an in-memory store, opted in per query | +| [`budgets`](/orm/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Fails queries that would read too many rows or run longer than a latency budget | +| [`lints`](/orm/next/middleware/built-in-lints) | `@prisma-next/sql-runtime` | Blocks or warns on risky query shapes, such as `DELETE` without `WHERE` | +| [`cache`](/orm/next/middleware/built-in-cache) | `@prisma-next/middleware-cache` | Serves repeated reads from an in-memory store, opted in per query | `budgets` and `lints` are SQL-specific and come with the SQL runtime. The cache lives in its own package because it is family-agnostic: it works on PostgreSQL and MongoDB. @@ -80,8 +80,8 @@ A middleware declares which runtime family it supports through an optional `fami ## See also -- [Built-in: budgets](/next/middleware/built-in-budgets) -- [Built-in: lints](/next/middleware/built-in-lints) -- [Built-in: cache](/next/middleware/built-in-cache) -- [Authoring custom middleware](/next/middleware/authoring-custom-middleware) -- [Using extensions](/next/extensions/using-extensions) for adding new database capabilities rather than wrapping queries +- [Built-in: budgets](/orm/next/middleware/built-in-budgets) +- [Built-in: lints](/orm/next/middleware/built-in-lints) +- [Built-in: cache](/orm/next/middleware/built-in-cache) +- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) +- [Using extensions](/orm/next/extensions/using-extensions) for adding new database capabilities rather than wrapping queries diff --git a/apps/docs/content/docs/(index)/next/middleware/meta.json b/apps/docs/content/docs/orm/next/middleware/meta.json similarity index 100% rename from apps/docs/content/docs/(index)/next/middleware/meta.json rename to apps/docs/content/docs/orm/next/middleware/meta.json From b34f588ee88e939e042ecb20309b228812c186b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:11:36 -0700 Subject: [PATCH 03/11] docs: refine Prisma Next middleware and extensions --- .../orm/next/extensions/using-extensions.mdx | 24 +++++++++++-------- .../authoring-custom-middleware.mdx | 11 +++++---- .../orm/next/middleware/built-in-budgets.mdx | 22 +++++++++-------- .../orm/next/middleware/built-in-cache.mdx | 16 +++++++------ .../orm/next/middleware/built-in-lints.mdx | 6 ++--- .../next/middleware/how-middleware-works.mdx | 18 +++++++------- 6 files changed, 55 insertions(+), 42 deletions(-) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 2726450629..6e174254f6 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -7,15 +7,17 @@ metaDescription: Add an extension to a Prisma Next project, from install to firs badge: early-access --- -An extension is a package that teaches Prisma Next a database capability it does not have out of the box: new column types, query operations, and index types, along with the migrations that install the underlying database feature. Vector search, geospatial data, and full-text search all arrive this way. +An extension is a package that teaches Prisma Next a database capability it does not have out of the box: new column types, query operations, and index types, along with the migrations that install the underlying database feature. Vector search, geospatial data, full-text search, typed JSON, and provider-specific integrations all arrive this way. + +Use an extension when you want the database to do something powerful and still keep the Prisma Next experience: typed schema declarations, generated TypeScript, migration support, and query helpers that feel native to your app. Adding one is an install plus two registrations, using pgvector as the example: -```npm +```npm title="Terminal" npm install @prisma-next/extension-pgvector ``` -Register the extension's control-plane descriptor in your project config, where it participates in contract emission and migrations: +Register the extension's control-plane descriptor in your project config. This is the side Prisma Next uses during contract emission and migrations: ```ts title="prisma-next.config.ts" import pgvector from '@prisma-next/extension-pgvector/control'; @@ -30,7 +32,7 @@ export default defineConfig({ }); ``` -Register its runtime descriptor on the client, where it contributes codecs and query operations: +Register its runtime descriptor on the client. This is the side Prisma Next uses when your app encodes values, decodes rows, and builds extension-specific query operations: ```ts title="src/prisma/db.ts" import pgvector from '@prisma-next/extension-pgvector/runtime'; @@ -59,9 +61,9 @@ model Post { } ``` -Run `prisma-next db init` (or `prisma-next db update` on an existing database). Extensions ship a baseline migration in their contract space, so this step installs the database-side feature for you, `CREATE EXTENSION IF NOT EXISTS vector` in pgvector's case. Then query with the operations the extension adds: +Run `prisma-next db init` (or `prisma-next db update` on an existing database). Extensions can ship a baseline migration in their contract space, so this step installs the database-side feature for you, `CREATE EXTENSION IF NOT EXISTS vector` in pgvector's case. Then query with the operations the extension adds: -```ts +```ts title="src/prisma/similarity-search.ts" const plan = db.sql.public.post .select('id', 'title') .select('distance', (f, fns) => fns.cosineDistance(f.embedding, queryVector)) @@ -74,7 +76,9 @@ const similar = await db.runtime().execute(plan); ## Capabilities -An extension declares the database capabilities it relies on under a namespaced key, `pgvector.cosine` for the query above. You do not configure capabilities by hand: registering the extension declares them in the contract, and the runtime verifies at `connect()` time that the database it reaches actually supports what the contract uses. A managed database that lacks the underlying feature fails at connect, not mid-query. +An extension declares the capabilities it contributes under a namespaced key, `pgvector.cosine` for the query above. You do not configure capabilities by hand: registering the control descriptor records them in the emitted contract, and registering the runtime descriptor lets the client satisfy the contract's required extension packs. + +If the runtime descriptor for a required pack is missing, Prisma Next fails when the client/runtime is constructed instead of letting a query run with half the extension installed. Database-side support is enforced by the extension's migrations and verification path. For example, if the database cannot install pgvector, `db init`, `db update`, or verification reports the problem before you rely on application traffic. ## Available extensions @@ -85,12 +89,12 @@ The catalog is first-party today and designed for community authors: extension p | [pgvector](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector) | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | | [PostGIS](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis) | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | | [ParadeDB](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb) | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | -| [Supabase](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase) | A Supabase-shaped contract with `auth.*` and `storage.*` as queryable external tables | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | +| [Supabase](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase) | Supabase contract support plus role-bound runtime helpers for app tables and service-role access to `auth` and `storage` | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | | [arktype-json](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json) | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | -Two maturity notes, since Prisma Next itself is in Early Access: the ParadeDB pack currently covers the `key_field` index option only (per-field tokenizer configuration is not there yet), and the Supabase pack is an early milestone that ships the contract surface, with the role-binding runtime still to come. Each extension's README linked above states exactly what its current version covers. +Two maturity notes, since Prisma Next itself is in Early Access: the ParadeDB pack currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and the Supabase pack has the contract surface plus role-bound runtime helpers, while broader RLS authoring and cross-contract foreign keys are still separate follow-up projects. Each extension's README linked above states exactly what its current version covers. -Every table row links to the extension's guide, with schema declaration, migration behavior, and the full query surface for that pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. +Every table row links to the extension's source and README, with schema declaration, migration behavior, and the current query surface for that pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. ## See also diff --git a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index 1723d101c5..db8237b4e0 100644 --- a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -9,6 +9,8 @@ badge: early-access A custom middleware is a plain object: give it a `name`, declare the runtime family it targets, and implement the hooks you need. No base class, no registration API beyond the `middleware` array. +Reach for custom middleware when the behavior belongs beside data access rather than inside every resolver or service method: tenant filters, soft-delete policy, query timing, read-through fixtures, request-scoped auditing, or parameter preparation for codecs and external services. + This complete example warns whenever a query runs longer than a threshold: ```ts title="src/prisma/slow-query-warning.ts" @@ -62,7 +64,7 @@ export const db = postgres({ Every query now reports when it crosses the threshold, whatever surface issued it: -```text +```text title="Example log" warn { code: 'APP.SLOW_QUERY', message: 'Query took 412ms (threshold: 250ms)', @@ -75,7 +77,7 @@ warn { Type your middleware as `SqlMiddleware` (from `@prisma-next/sql-runtime`) and TypeScript infers every hook's parameters, so you rarely need to import the plan or context types directly. The hooks, all optional: - `beforeCompile(draft, ctx)` runs before the query AST is lowered to SQL. Return a new draft (`{ ...draft, ast: rewritten }`) to rewrite the query, or `undefined` to pass through. This hook is SQL-specific. -- `beforeExecute(plan, ctx, params?)` runs after lowering. `plan.sql` is the rendered statement, `plan.params` the bound parameters, and `plan.ast` the pre-lowering AST. Throw to block the query. Use `params.replaceValue(...)` to mutate parameter values before they are encoded. +- `beforeExecute(plan, ctx, params?)` runs after lowering and before parameter encoding. `plan.sql` is the rendered statement, `plan.params` contains user-domain parameter values, and `plan.ast` is the pre-lowering AST. Throw to block the query. Iterate `params.entries()` and call `params.replaceValue(...)` to mutate parameter values before they are encoded. - `intercept(plan, ctx)` runs last before the driver. Return `{ rows }` (an iterable or async iterable of row objects) to answer the query yourself and skip the driver, or `undefined` to pass through. - `onRow(row, plan, ctx)` runs for every row the driver yields. - `afterExecute(plan, result, ctx)` runs once at the end, with `result.rowCount`, `result.latencyMs`, `result.completed` (`false` when the driver errored), and `result.source` (`'driver'`, or `'middleware'` when an `intercept` answered). @@ -85,6 +87,7 @@ Hooks run in registration order at every site, and the context (`ctx`) is shared | Context field | What it gives you | | --- | --- | | `ctx.log` | Structured `info` / `warn` / `error` (and optional `debug`) sinks wired to the runtime's logger | +| `ctx.mode` | `'strict'` or `'permissive'`; use it when a middleware should throw in strict environments and warn elsewhere | | `ctx.now()` | Current time from the runtime's clock; prefer it over `Date.now()` so tests can control time | | `ctx.planExecutionId` | Unique ID per `execute()` call, the same value in every hook, for correlating observations | | `ctx.scope` | `'runtime'`, `'connection'`, or `'transaction'`; use it to skip work in scopes you should not touch | @@ -98,7 +101,7 @@ Hooks run in registration order at every site, and the context (`ctx`) is shared `beforeCompile` sees the typed AST before lowering, which makes cross-cutting query rewrites possible: tenant scoping, soft-delete filters, or row-level restrictions. This middleware appends a predicate to every `SELECT` on the `user` table: -```ts +```ts title="src/prisma/scope-user-selects.ts" import type { SqlMiddleware } from '@prisma-next/sql-runtime'; import { AndExpr, type BinaryExpr } from '@prisma-next/sql-relational-core/ast'; @@ -122,7 +125,7 @@ Because each middleware's returned draft feeds the next one, rewrites compose in ### Short-circuit with `intercept` -Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/orm/next/middleware/built-in-cache) is the reference implementation of this pattern. +Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. `beforeExecute` has already run, so validation and parameter mutation still happen before a short-circuit. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/orm/next/middleware/built-in-cache) is the reference implementation of this pattern. ### Support every database diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx index 63efa8c5ca..5a46883fc3 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx @@ -1,13 +1,13 @@ --- title: 'Built-in: budgets' -description: The budgets middleware fails queries that would read too many rows or run longer than a latency budget. +description: The budgets middleware caps row counts and makes over-latency queries visible. url: /orm/next/middleware/built-in-budgets metaTitle: 'Prisma Next built-in middleware: budgets' -metaDescription: Wire and configure the budgets middleware in Prisma Next to cap row counts and query latency, and understand the errors it raises. +metaDescription: Wire and configure the budgets middleware in Prisma Next to cap row counts, report query latency, and understand the errors it raises. badge: early-access --- -The `budgets` middleware puts a cost ceiling on every query: it rejects queries that would read more rows than you allow and reports queries that run longer than your latency budget. +The `budgets` middleware puts a cost ceiling on every query: it rejects or warns on queries that would read more rows than you allow, counts rows while they stream, and reports queries that run longer than your latency budget. Register it on the runtime with the row and latency limits you want to enforce: @@ -43,33 +43,35 @@ All options are optional. | `defaultTableRows` | `number` | `10_000` | Assumed row count for tables not listed in `tableRows`, used by the estimator | | `tableRows` | `Record` | `{}` | Per-table row-count assumptions for the estimator, keyed by table name | | `maxLatencyMs` | `number` | `1_000` | Latency budget for one query, checked after execution | -| `severities.rowCount` | `'warn' \| 'error'` | `'error'` | Whether pre-execution row violations block the query or log a warning | +| `severities.rowCount` | `'warn' \| 'error'` | `'error'` | Whether pre-execution row-budget violations block the query or log a warning in permissive mode. Strict mode always blocks | + +The runtime defaults to strict mode. In strict mode, over-budget latency throws `BUDGET.TIME_EXCEEDED`; in permissive mode, the same event is logged as a warning. The latency check happens after execution, so it is a visibility tool rather than a cancellation mechanism. ## How it enforces the budget The middleware checks at three points in the query lifecycle: -1. **Before execution**, it inspects the query plan's AST. A `SELECT` with no `LIMIT` (and no aggregation that collapses the result) is treated as unbounded, and a bounded query whose estimated rows exceed `maxRows` is over budget. The estimate comes from `tableRows` and `defaultTableRows`, so tune those to your data. Violations raise `BUDGET.ROWS_EXCEEDED` before the driver runs. +1. **Before execution**, it inspects the query plan's AST. A `SELECT` with no `LIMIT` (and no aggregation that collapses the result) is treated as unbounded, and a bounded query whose estimated rows exceed `maxRows` is over budget. The estimate comes from `tableRows` and `defaultTableRows`, so tune those to your data. Violations raise `BUDGET.ROWS_EXCEEDED` before the driver runs, or warn in permissive mode when `severities.rowCount` is set to `'warn'`. 2. **While rows stream**, it counts what the database actually returns. If the observed count passes `maxRows`, it throws `BUDGET.ROWS_EXCEEDED` and aborts the stream, which protects you when the estimate was wrong. -3. **After execution**, it compares the measured `latencyMs` against `maxLatencyMs` and raises `BUDGET.TIME_EXCEEDED` when the query ran too long. This check runs after the work is done, so it cannot save the slow query itself; it exists to make slow queries loud instead of invisible. +3. **After execution**, it compares the measured `latencyMs` against `maxLatencyMs` and reports `BUDGET.TIME_EXCEEDED` when the query ran too long. This check runs after the work is done, so it cannot save the slow query itself; it exists to make slow queries loud instead of invisible. ## What you see when a budget trips A blocked query rejects with a runtime error carrying the budget code and details: -```text +```text title="Row budget error" BUDGET.ROWS_EXCEEDED: Unbounded SELECT query exceeds budget { source: 'ast', maxRows: 10000 } ``` -The `source` field tells you whether the violation came from the pre-execution estimate (`'ast'`) or the observed row stream (`'observed'`). Latency violations carry the measured and allowed values: +The `source` field tells you whether the violation came from the pre-execution estimate (`'ast'`) or the observed row stream (`'observed'`). In strict mode, latency violations carry the measured and allowed values: -```text +```text title="Latency budget error" BUDGET.TIME_EXCEEDED: Query latency exceeds budget { latencyMs: 2481, maxLatencyMs: 1000 } ``` -To fix an unbounded-select violation, add a `LIMIT` to the query (`take(...)` on the ORM client, `.limit(...)` on the SQL query builder) or raise the budget deliberately. +In permissive mode, the same information is emitted through the runtime logger as a warning. To fix an unbounded-select violation, add a `LIMIT` to the query (`take(...)` on the ORM client, `.limit(...)` on the SQL query builder) or raise the budget deliberately. ## Common gotchas diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx index d991f85756..c9787c121e 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx @@ -9,9 +9,11 @@ badge: early-access The cache middleware serves repeated reads from an in-memory store instead of the database. Caching is strictly opt-in per query: only queries annotated with a TTL are cached, everything else passes through untouched. -Install the package and register the middleware first in the chain, so cache hits short-circuit before any other middleware does work: +Use it for hot, repeatable reads such as dashboards, lookup tables, feature flags, navigation data, or expensive search results where a short stale window is acceptable. -```npm +Install the package and register the middleware. Put it before other middleware that implement `intercept` when cached rows should win: + +```npm title="Terminal" npm install @prisma-next/middleware-cache ``` @@ -39,7 +41,7 @@ The cache is family-agnostic: unlike `budgets` and `lints`, it works on MongoDB Annotate a read with `cacheAnnotation` and a `ttl` in milliseconds. On the SQL query builder, chain `.annotate(...)` onto the plan: -```ts +```ts title="src/prisma/get-users-cached.ts" import { cacheAnnotation } from '@prisma-next/middleware-cache'; const plan = db.sql.public.user @@ -53,12 +55,12 @@ const users = await db.runtime().execute(plan); On the ORM client, pass the annotation through the query's meta callback: -```ts +```ts title="src/prisma/get-user-cached.ts" import { cacheAnnotation } from '@prisma-next/middleware-cache'; import { orm } from '@prisma-next/sql-orm-client'; const runtime = await db.connect(); -const models = orm({ runtime, context: db.context, collections: {} }).public; +const models = orm({ runtime, context: db.context }).public; const user = await models.User.first({ id }, (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })), @@ -81,7 +83,7 @@ The annotation payload has three fields: By default the cache key is a content hash of the executed query: the post-lowering statement plus its bound parameters, combined with the contract's storage hash. Two lookups with different parameter values land in different slots; the same lookup hits. Because schema migrations rotate the storage hash, entries cached before a migration cannot serve queries against the new schema. -Cache hits are still visible to the rest of the middleware chain: `afterExecute` fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so logging and metrics keep working for cached reads. +Cache hits are still visible to the rest of the middleware chain: `beforeExecute` hooks have already run, the driver and `onRow` are skipped, and `afterExecute` fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so logging and metrics keep working for cached reads. The cache only acts at runtime scope. Queries executed inside a transaction or on an explicitly checked-out connection always go to the database, because read-after-write consistency is the caller's expectation there. @@ -91,7 +93,7 @@ The cache only acts at runtime scope. Queries executed inside a transaction or o | --- | --- | --- | --- | | `maxEntries` | `number` | `1000` | Maximum entries in the default in-memory store; least-recently-used entries are evicted first. Only consulted when `store` is omitted | | `store` | `CacheStore` | in-memory LRU | Bring your own store implementation, for example one backed by Redis | -| `clock` | `() => number` | `Date.now` | Time source for TTL expiry, useful in tests | +| `clock` | `() => number` | `Date.now` | Time source used to stamp `storedAt` on committed entries. TTL expiry lives in the store implementation | ## Common gotchas diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx index 50a6e49df7..000c5c269c 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx @@ -39,7 +39,7 @@ Lints run in `beforeExecute`, against the query plan's AST. Each rule has a seve A warning is logged as a structured event you can route to your logger: -```text +```text title="Lint warning" warn { code: 'LINT.NO_LIMIT', message: 'Unbounded SELECT may return large result sets', @@ -51,7 +51,7 @@ warn { Raise or lower any rule per project through `severities`. For example, treat unbounded selects as hard failures in a service where every list endpoint must paginate: -```ts +```ts title="src/prisma/db.ts" lints({ severities: { noLimit: 'error', @@ -66,7 +66,7 @@ The severity keys are `deleteWithoutWhere`, `updateWithoutWhere`, `noLimit`, and Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. Raw SQL does not have that structure, so `lints` falls back to text heuristics for raw plans: it flags `select *`, a missing `LIMIT`, and a mutation smuggled through a read-only raw query. Control the fallback with `fallbackWhenAstMissing`: -```ts +```ts title="src/prisma/db.ts" lints({ fallbackWhenAstMissing: 'raw' }); // default: heuristic checks on the SQL text lints({ fallbackWhenAstMissing: 'skip' }); // skip linting for plans without an AST ``` diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 4afca0b8f7..519aeaaf4d 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -9,6 +9,8 @@ badge: early-access A middleware is an object with a name and one or more hooks that the Prisma Next runtime calls at fixed points around every query, so you can observe, guard, rewrite, or answer queries without touching application code. +Use middleware when you want one policy to cover every query surface: audit logging, query budgets, safety checks, caching, tenant scoping, test fixtures, circuit breakers, or parameter processing before values reach the driver. + You register middleware once, on the runtime setup, through the `middleware` option: ```ts title="src/prisma/db.ts" @@ -38,23 +40,23 @@ Each middleware implements some of five hooks. The runtime calls each hook on ev | Hook | When it runs | What it can do | | --- | --- | --- | | `beforeCompile(draft, ctx)` | Before the query AST is lowered to SQL (SQL runtimes only) | Return a modified draft to rewrite the query, for example to add a predicate | -| `beforeExecute(plan, ctx, params?)` | After lowering, before the driver runs | Inspect the plan (`plan.sql`, `plan.ast`), validate it, throw to block it, or mutate parameter values | -| `intercept(plan, ctx)` | Right before the driver | Return rows directly and skip the driver entirely (this is how the cache serves hits) | +| `beforeExecute(plan, ctx, params?)` | After lowering, before parameter encoding and the driver | Inspect the plan (`plan.sql`, `plan.ast`), validate it, throw to block it, or mutate parameter values | +| `intercept(plan, ctx)` | After `beforeExecute` and parameter encoding, right before the driver | Return rows directly and skip the driver entirely (this is how the cache serves hits) | | `onRow(row, plan, ctx)` | Once per row the driver yields | Observe or count rows; throw to abort the stream | | `afterExecute(plan, result, ctx)` | After the query finishes | Read `result.rowCount`, `result.latencyMs`, `result.completed`, and `result.source` for timing, logging, and budgets | The full lifecycle for a SQL query: -```text +```text title="Middleware lifecycle" beforeCompile → SQL lowering → beforeExecute → parameter encoding → intercept → driver → onRow (per row) → afterExecute ``` Two details of the ordering matter in practice: -- **Registration order is execution order.** At every hook site, the first middleware in the array runs first. If a `beforeCompile` hook rewrites the query, the next middleware sees the rewritten draft. This is why the example above registers the cache first: on a cache hit, its `intercept` answers the query before `lints()` and `budgets()` do any work. -- **`intercept` short-circuits.** When a middleware returns rows from `intercept`, the driver never runs and `onRow` is skipped, but `afterExecute` still fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so observability keeps working for served-from-cache reads. +- **Registration order is execution order at each hook site.** If a `beforeCompile` hook rewrites the query, the next middleware sees the rewritten draft. If multiple middleware implement `intercept`, the first one that returns rows wins. +- **`intercept` short-circuits the row source.** `beforeExecute` has already run by the time `intercept` fires. When a middleware returns rows from `intercept`, the driver never runs and `onRow` is skipped, but `afterExecute` still fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so observability keeps working for served-from-cache reads. -Every hook receives a context (`ctx`) with the contract, a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) for the full surface. +Every hook receives a context (`ctx`) with the contract, runtime `mode` (`'strict'` or `'permissive'`), a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) for the full surface. ## What ships built in @@ -70,13 +72,13 @@ Three middleware ship with Prisma Next today: ## Errors and failure behavior -A middleware that throws from `beforeCompile`, `beforeExecute`, or `onRow` fails the query: the error propagates to the caller and the driver work stops. This is how `lints()` blocks a `DELETE` without a `WHERE` clause before it reaches the database. +A middleware that throws from `beforeCompile`, `beforeExecute`, `intercept`, or `onRow` fails the query: the error propagates to the caller and the driver work stops. This is how `lints()` blocks a `DELETE` without a `WHERE` clause before it reaches the database. When the driver itself fails, the runtime still calls `afterExecute` on every middleware with `result.completed` set to `false`, then rethrows the original driver error. Errors thrown by `afterExecute` while handling a failed query are swallowed so they cannot mask the original error. ## Database compatibility -A middleware declares which runtime family it supports through an optional `familyId`. `budgets` and `lints` declare `familyId: 'sql'`, so they register on PostgreSQL but not on MongoDB. Middleware with no `familyId`, like the cache, work on any runtime. The runtime checks compatibility when you construct the client and throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` if a middleware does not match, so a wrong registration fails at startup rather than at query time. +A middleware declares which runtime family it supports through an optional `familyId`. `budgets` and `lints` declare `familyId: 'sql'`, so they register on PostgreSQL but not on MongoDB. Middleware with no `familyId`, like the cache, work on any runtime. The runtime checks compatibility when you construct the client and throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` or `RUNTIME.MIDDLEWARE_TARGET_MISMATCH` if a middleware does not match, so a wrong registration fails at startup rather than at query time. ## See also From 02440f2bdd62843e2b177d0564045b66622283df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:23:03 -0700 Subject: [PATCH 04/11] docs(next): address review feedback on middleware and extensions pages - Extensions table: add Status and Guide columns; mark ParadeDB and Supabase as Experimental per their READMEs; open the catalog with an explicit invitation to build an extension (call for extension authors) - Correct the Supabase row and maturity note: the runtime export is a minimal descriptor that satisfies the pack-requirements check; the role-binding runtime (asUser/asServiceRole) lands in M2 per the package README, so the page no longer claims role-bound helpers - Overview built-ins table: budgets latency wording now says the latency budget raises BUDGET.TIME_EXCEEDED after execution rather than implying it prevents slow queries - budgets: document severities.latency as accepted by the options type but not read in v0.14 (behavior is runtime-mode-driven) - lints: document unindexedPredicate as a type-level key with no active rule in v0.14, and the raw-SQL fallback severity differences (select * escalates to error for raw plans) Not acted on, deliberately: the client-extensions redirect (still serving live Prisma 7 content; deferred to the DR-8687 IA work) and Guides/Capabilities cross-links (those pages do not exist yet; lint:links would fail on dead internal links). Co-Authored-By: Claude Fable 5 --- .../orm/next/extensions/using-extensions.mdx | 20 +++++++++---------- .../orm/next/middleware/built-in-budgets.mdx | 3 ++- .../orm/next/middleware/built-in-lints.mdx | 4 ++-- .../next/middleware/how-middleware-works.mdx | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 6e174254f6..92b247f688 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -82,19 +82,19 @@ If the runtime descriptor for a required pack is missing, Prisma Next fails when ## Available extensions -The catalog is first-party today and designed for community authors: extension packs are versioned npm packages with a documented layout, and the [call for extension authors](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) explains how to build one. +Want an extension that does not exist yet? Build it: the catalog is first-party today and built for community authors, extension packs are versioned npm packages with a documented layout, and the [call for extension authors](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) explains how to write and publish one. -| Extension | What it adds | Package | Databases | -| --- | --- | --- | --- | -| [pgvector](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector) | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | -| [PostGIS](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis) | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | -| [ParadeDB](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb) | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | -| [Supabase](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase) | Supabase contract support plus role-bound runtime helpers for app tables and service-role access to `auth` and `storage` | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | -| [arktype-json](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json) | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | +| Extension | What it adds | Package | Databases | Status | Guide | +| --- | --- | --- | --- | --- | --- | +| pgvector | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | +| PostGIS | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | +| ParadeDB | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | +| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` as queryable external tables | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | +| arktype-json | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | -Two maturity notes, since Prisma Next itself is in Early Access: the ParadeDB pack currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and the Supabase pack has the contract surface plus role-bound runtime helpers, while broader RLS authoring and cross-contract foreign keys are still separate follow-up projects. Each extension's README linked above states exactly what its current version covers. +On the two experimental packs: ParadeDB currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and Supabase is an early milestone that ships the contract surface, with the role-binding runtime (`asUser()`, `asServiceRole()`), RLS authoring, and cross-contract foreign keys landing in later milestones. Each guide states exactly what its pack's current version covers. -Every table row links to the extension's source and README, with schema declaration, migration behavior, and the current query surface for that pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. +The Guide links cover schema declaration, migration behavior, and the current query surface for each pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. ## See also diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx index 5a46883fc3..962e3f4cd7 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx @@ -44,8 +44,9 @@ All options are optional. | `tableRows` | `Record` | `{}` | Per-table row-count assumptions for the estimator, keyed by table name | | `maxLatencyMs` | `number` | `1_000` | Latency budget for one query, checked after execution | | `severities.rowCount` | `'warn' \| 'error'` | `'error'` | Whether pre-execution row-budget violations block the query or log a warning in permissive mode. Strict mode always blocks | +| `severities.latency` | `'warn' \| 'error'` | — | Accepted by the options type but not read by the middleware in v0.14; latency behavior follows the runtime mode (see below) | -The runtime defaults to strict mode. In strict mode, over-budget latency throws `BUDGET.TIME_EXCEEDED`; in permissive mode, the same event is logged as a warning. The latency check happens after execution, so it is a visibility tool rather than a cancellation mechanism. +The runtime defaults to strict mode. In strict mode, over-budget latency throws `BUDGET.TIME_EXCEEDED`; in permissive mode, the same event is logged as a warning. The latency check happens after execution, so it is a visibility tool rather than a cancellation mechanism. Setting `severities.latency` does not change this in v0.14: the key exists on the options type, but the middleware does not consult it yet. ## How it enforces the budget diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx index 000c5c269c..4f5dd6b6a3 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx @@ -60,11 +60,11 @@ lints({ }); ``` -The severity keys are `deleteWithoutWhere`, `updateWithoutWhere`, `noLimit`, and `selectStar`, plus `readOnlyMutation` for the raw-SQL guardrail described below. +The severity keys are `deleteWithoutWhere`, `updateWithoutWhere`, `noLimit`, and `selectStar`, plus `readOnlyMutation` for the raw-SQL guardrail described below. The options type also accepts `unindexedPredicate`, but no rule emits that finding in v0.14. ## Raw SQL -Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. Raw SQL does not have that structure, so `lints` falls back to text heuristics for raw plans: it flags `select *`, a missing `LIMIT`, and a mutation smuggled through a read-only raw query. Control the fallback with `fallbackWhenAstMissing`: +Queries built with the ORM client or the SQL query builder carry a typed AST, and the rules above inspect it structurally. Raw SQL does not have that structure, so `lints` falls back to text heuristics for raw plans: it flags `select *`, a missing `LIMIT`, and a mutation smuggled through a raw query marked read-only (`LINT.READ_ONLY_MUTATION`). The raw heuristics carry their own defaults: `select *` escalates to `error` for raw plans (stricter than the AST rule's `warn`), a missing `LIMIT` stays a warning, and a read-only-intent mutation is an `error`. The `severities` keys above override raw findings too. Control the fallback with `fallbackWhenAstMissing`: ```ts title="src/prisma/db.ts" lints({ fallbackWhenAstMissing: 'raw' }); // default: heuristic checks on the SQL text diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 519aeaaf4d..19551f78d7 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -64,7 +64,7 @@ Three middleware ship with Prisma Next today: | Middleware | Import | What it does | | --- | --- | --- | -| [`budgets`](/orm/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Fails queries that would read too many rows or run longer than a latency budget | +| [`budgets`](/orm/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Blocks queries that would read too many rows, and raises `BUDGET.TIME_EXCEEDED` after execution when a query overruns its latency budget | | [`lints`](/orm/next/middleware/built-in-lints) | `@prisma-next/sql-runtime` | Blocks or warns on risky query shapes, such as `DELETE` without `WHERE` | | [`cache`](/orm/next/middleware/built-in-cache) | `@prisma-next/middleware-cache` | Serves repeated reads from an in-memory store, opted in per query | From f4b5465e9aaeb5d90bac4f9e64f6b223e349bc77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:27:00 -0700 Subject: [PATCH 05/11] docs(next): register only the discussed middleware in per-page setup examples The cache and authoring pages registered the full lints/budgets/cache chain in their setup snippets; each page now registers only the middleware it documents, with composition and ordering covered in prose pointing at the how-middleware-works chain example. Co-Authored-By: Claude Fable 5 --- .../next/middleware/authoring-custom-middleware.mdx | 5 ++--- .../docs/orm/next/middleware/built-in-cache.mdx | 11 ++++------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index db8237b4e0..e0743b8409 100644 --- a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -46,11 +46,10 @@ export function slowQueryWarning(options?: SlowQueryWarningOptions): SqlMiddlewa } ``` -Register it in the `middleware` array on the runtime setup, next to the built-ins: +Register it in the `middleware` array on the runtime setup, the same way as the [built-ins](/orm/next/middleware/how-middleware-works#what-ships-built-in): ```ts title="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' }; import { slowQueryWarning } from './slow-query-warning'; @@ -58,7 +57,7 @@ import { slowQueryWarning } from './slow-query-warning'; export const db = postgres({ contractJson, url: process.env.DATABASE_URL, - middleware: [lints(), budgets(), slowQueryWarning({ thresholdMs: 250 })], + middleware: [slowQueryWarning({ thresholdMs: 250 })], }); ``` diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx index c9787c121e..ee13d1f031 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx @@ -11,7 +11,7 @@ The cache middleware serves repeated reads from an in-memory store instead of th Use it for hot, repeatable reads such as dashboards, lookup tables, feature flags, navigation data, or expensive search results where a short stale window is acceptable. -Install the package and register the middleware. Put it before other middleware that implement `intercept` when cached rows should win: +Install the package and register the middleware: ```npm title="Terminal" npm install @prisma-next/middleware-cache @@ -20,21 +20,18 @@ npm install @prisma-next/middleware-cache ```ts title="src/prisma/db.ts" import { createCacheMiddleware } from '@prisma-next/middleware-cache'; 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({ contractJson, url: process.env.DATABASE_URL, - middleware: [ - createCacheMiddleware({ maxEntries: 1_000 }), - lints(), - budgets({ maxRows: 10_000 }), - ], + middleware: [createCacheMiddleware({ maxEntries: 1_000 })], }); ``` +When you combine it with other middleware such as [`lints`](/orm/next/middleware/built-in-lints) and [`budgets`](/orm/next/middleware/built-in-budgets), register the cache first: hooks run in array order, so a cache hit then short-circuits before the downstream middleware do any work. + The cache is family-agnostic: unlike `budgets` and `lints`, it works on MongoDB runtimes as well as PostgreSQL. ## Opt a query in From e5abff6d204a3aa8d4b95b4c02e260bb46155f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:37:42 -0700 Subject: [PATCH 06/11] docs: verify Prisma Next examples --- .../docs/orm/next/extensions/using-extensions.mdx | 10 +++++----- .../next/middleware/authoring-custom-middleware.mdx | 2 +- .../docs/orm/next/middleware/built-in-budgets.mdx | 2 +- .../docs/orm/next/middleware/built-in-cache.mdx | 4 ++-- .../docs/orm/next/middleware/built-in-lints.mdx | 2 +- .../docs/orm/next/middleware/how-middleware-works.mdx | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 92b247f688..91e1ce365d 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -27,7 +27,7 @@ export default defineConfig({ contract: './src/prisma/contract.prisma', extensions: [pgvector], db: { - connection: process.env.DATABASE_URL, + connection: process.env['DATABASE_URL']!, }, }); ``` @@ -42,7 +42,7 @@ import contractJson from './contract.json' with { type: 'json' }; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, extensions: [pgvector], }); ``` @@ -89,12 +89,12 @@ Want an extension that does not exist yet? Build it: the catalog is first-party | pgvector | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | | PostGIS | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | | ParadeDB | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | -| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` as queryable external tables | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | +| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` external tables, plus a role-bound runtime facade (`asUser()`, `asAnon()`, `asServiceRole()`) | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | | arktype-json | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | -On the two experimental packs: ParadeDB currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and Supabase is an early milestone that ships the contract surface, with the role-binding runtime (`asUser()`, `asServiceRole()`), RLS authoring, and cross-contract foreign keys landing in later milestones. Each guide states exactly what its pack's current version covers. +On the two experimental packs: ParadeDB currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and Supabase ships the contract surface plus role-bound runtime helpers, while broader RLS authoring and cross-contract foreign keys are still separate follow-up projects. Check the package source and examples when you need the precise current surface for an experimental pack. -The Guide links cover schema declaration, migration behavior, and the current query surface for each pack. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, and the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs. +The Guide links cover each pack's package-level setup, and the runnable examples show the current query surface in context. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs, and the [Supabase example](https://github.com/prisma/prisma-next/tree/main/examples/supabase) shows role-bound runtime usage. ## See also diff --git a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index e0743b8409..cc54198866 100644 --- a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -56,7 +56,7 @@ import { slowQueryWarning } from './slow-query-warning'; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, middleware: [slowQueryWarning({ thresholdMs: 250 })], }); ``` diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx index 962e3f4cd7..a196153ad7 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx @@ -19,7 +19,7 @@ import contractJson from './contract.json' with { type: 'json' }; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, middleware: [ budgets({ maxRows: 10_000, diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx index ee13d1f031..5f36f3f5d0 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx @@ -25,12 +25,12 @@ import contractJson from './contract.json' with { type: 'json' }; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, middleware: [createCacheMiddleware({ maxEntries: 1_000 })], }); ``` -When you combine it with other middleware such as [`lints`](/orm/next/middleware/built-in-lints) and [`budgets`](/orm/next/middleware/built-in-budgets), register the cache first: hooks run in array order, so a cache hit then short-circuits before the downstream middleware do any work. +When you combine it with other middleware that implement `intercept`, register the cache first when cached rows should win. `beforeExecute` hooks still run before a cache hit is served, and `afterExecute` still fires afterward with `result.source` set to `'middleware'`. The cache is family-agnostic: unlike `budgets` and `lints`, it works on MongoDB runtimes as well as PostgreSQL. diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx index 4f5dd6b6a3..c1bf7c4f87 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx @@ -19,7 +19,7 @@ import contractJson from './contract.json' with { type: 'json' }; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, middleware: [lints()], }); ``` diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 19551f78d7..4e4a167a9b 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -22,7 +22,7 @@ import contractJson from './contract.json' with { type: 'json' }; export const db = postgres({ contractJson, - url: process.env.DATABASE_URL, + url: process.env['DATABASE_URL']!, middleware: [ createCacheMiddleware({ maxEntries: 1_000 }), lints(), From 8ea051b3a62ca9e8447e48ae61a2633957adec0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:48:05 -0700 Subject: [PATCH 07/11] docs(next): add animated diagrams to middleware and extensions pages Two new ConceptAnimation flow scenes, using the same stepper/autoplay engine as the Compute docs: - middleware-pipeline (How middleware works): one query travelling app -> cache -> lints -> budgets -> database, with a step for the pre-driver hooks, the cache intercept short-circuit (chain edges drop out, dashed "cached rows" path returns early), and the onRow/afterExecute return lane - extension-planes (Using extensions): one package forking into the /control registration (schema types, baseline migration) and the /runtime registration (query ops, codecs), converging on PostgreSQL via db init and queries Both scenes have matching Code Hike text fallbacks in presets.ts. Verified in the dev server: light and dark themes, step transitions toggle the right nodes/edges, and every label fits its box (checked programmatically via getBBox against the box rects). Co-Authored-By: Claude Fable 5 --- .../orm/next/extensions/using-extensions.mdx | 4 + .../next/middleware/how-middleware-works.mdx | 4 + .../concept-animation/flow-presets.ts | 264 ++++++++++++++++++ .../components/concept-animation/presets.ts | 81 ++++++ 4 files changed, 353 insertions(+) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 91e1ce365d..561bc02e8a 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -11,6 +11,10 @@ An extension is a package that teaches Prisma Next a database capability it does Use an extension when you want the database to do something powerful and still keep the Prisma Next experience: typed schema declarations, generated TypeScript, migration support, and query helpers that feel native to your app. +The animation below shows how one extension package lands in a project: one install, two registrations, and a database that ends up with the feature installed. Step through it, or let it play: + + + Adding one is an install plus two registrations, using pgvector as the example: ```npm title="Terminal" diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 4e4a167a9b..c3b89ad61d 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -33,6 +33,10 @@ export const db = postgres({ Every query then passes through the chain: the ORM client, the SQL query builder, and raw SQL all execute through the same runtime, so one `middleware` array covers all of them. On MongoDB, pass the same option to `mongo(...)` from `@prisma-next/mongo/runtime`. +The animation below follows one query through that chain, including the cache hit that skips the driver. Step through it, or let it play: + + + ## How it works Each middleware implements some of five hooks. The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. diff --git a/apps/docs/src/components/concept-animation/flow-presets.ts b/apps/docs/src/components/concept-animation/flow-presets.ts index 75aa65b67c..a0c997e64a 100644 --- a/apps/docs/src/components/concept-animation/flow-presets.ts +++ b/apps/docs/src/components/concept-animation/flow-presets.ts @@ -520,6 +520,268 @@ const githubConnection: FlowScene = { ], }; +// One query's round trip: app → middleware chain → driver, and back out. +// The app and database boxes span both lanes so the return edge (and the +// cache's short-circuit) can run through the clear band under the chain. +const middlewarePipeline: FlowScene = { + label: "How a query moves through the middleware chain", + width: 712, + height: 248, + groupLabels: [{ text: "Middleware, in registration order", x: 160, y: 52 }], + nodes: [ + { + id: "app", + label: "Your app", + sub: "ORM · SQL · raw", + variant: "neutral", + x: 16, + y: 70, + w: 108, + h: 150, + }, + { + id: "cache", + label: "cache", + sub: "serves repeated reads", + variant: "source", + x: 160, + y: 70, + w: 130, + h: 64, + }, + { + id: "lints", + label: "lints", + sub: "blocks risky shapes", + variant: "scope", + x: 322, + y: 70, + w: 116, + h: 64, + }, + { + id: "budgets", + label: "budgets", + sub: "caps query cost", + variant: "production", + x: 470, + y: 70, + w: 102, + h: 64, + }, + { + id: "db", + label: "Database", + sub: "the driver", + variant: "project", + x: 606, + y: 70, + w: 90, + h: 150, + }, + ], + edges: [ + // Request lane: anchors sit at the chain's center line (dy -43 on the + // tall boxes), so the four hops read as one straight pipeline. + { id: "e-in", from: "app", fromSide: "r", to: "cache", toSide: "l", fromDy: -43 }, + { id: "e-c-l", from: "cache", fromSide: "r", to: "lints", toSide: "l" }, + { id: "e-l-b", from: "lints", fromSide: "r", to: "budgets", toSide: "l" }, + { id: "e-b-db", from: "budgets", fromSide: "r", to: "db", toSide: "l", toDy: -43 }, + // Return lane: runs through the clear band under the chain boxes. + { + id: "e-return", + from: "db", + fromSide: "l", + to: "app", + toSide: "r", + fromDy: 43, + toDy: 43, + label: "rows · onRow · afterExecute", + }, + // Cache hit: intercept answers from the chain, driver never runs. + { + id: "e-hit", + from: "cache", + fromSide: "b", + to: "app", + toSide: "r", + toDy: 43, + dashed: true, + label: "cached rows", + }, + ], + steps: [ + { + title: "1. One chain", + caption: + "Every query, from the ORM client, the SQL query builder, or raw SQL, runs through the same middleware array on its way to the driver. The chain runs in registration order: first in the array, first at every hook.", + nodes: ["app", "cache", "lints", "budgets", "db"], + edges: ["e-in", "e-c-l", "e-l-b", "e-b-db"], + emphasize: ["cache", "lints", "budgets"], + }, + { + title: "2. Before the driver", + caption: + "Before anything reaches the database, beforeCompile can rewrite the query and beforeExecute can validate it, mutate parameters, or throw to block it. This is where lints stops a DELETE without WHERE and budgets rejects an unbounded SELECT.", + nodes: ["app", "cache", "lints", "budgets", "db"], + edges: ["e-in", "e-c-l", "e-l-b", "e-b-db"], + emphasize: ["lints", "budgets"], + }, + { + title: "3. Cache answers early", + caption: + "A middleware can answer a query itself: on a hit, the cache returns rows from its intercept hook and the driver never runs. afterExecute still fires with source set to middleware, so logging and metrics see the read.", + nodes: ["app", "cache", "lints", "budgets", "db"], + edges: ["e-in", "e-hit"], + emphasize: ["cache"], + }, + { + title: "4. Rows stream back", + caption: + "On the way back, onRow fires once per row as the driver streams, and afterExecute closes the call with the observed row count and latency. That is where budgets checks its latency ceiling and a custom slow-query middleware logs its warning.", + nodes: ["app", "cache", "lints", "budgets", "db"], + edges: ["e-in", "e-c-l", "e-l-b", "e-b-db", "e-return"], + emphasize: ["app", "db"], + }, + ], +}; + +// One extension package, registered on two planes, converging on the database. +const extensionPlanes: FlowScene = { + label: "How one extension package plugs into a project", + width: 712, + height: 330, + groupLabels: [ + { text: "One package", x: 16, y: 22 }, + { text: "Two registrations", x: 270, y: 22 }, + { text: "One database", x: 560, y: 22 }, + ], + nodes: [ + { + id: "pkg", + label: "pgvector", + sub: "@prisma-next/extension-pgvector", + variant: "neutral", + x: 16, + y: 126, + w: 190, + h: 88, + }, + { + id: "control", + label: "prisma-next.config.ts", + sub: "extensions: [pgvector]", + subBelow: true, + variant: "source", + x: 270, + y: 44, + w: 224, + h: 100, + rows: [ + { key: "schema type", value: "Vector(n)", origin: "preview" }, + { key: "migration", value: "CREATE EXTENSION", origin: "preview" }, + ], + }, + { + id: "runtime", + label: "db.ts", + sub: "extensions: [pgvector]", + subBelow: true, + variant: "production", + x: 270, + y: 196, + w: 224, + h: 100, + rows: [ + { key: "query ops", value: "cosineDistance(…)", origin: "production" }, + { key: "codecs", value: "vector ↔ number[]", origin: "production" }, + ], + }, + { + id: "db", + label: "PostgreSQL", + sub: "pgvector installed", + variant: "project", + x: 560, + y: 120, + w: 136, + h: 100, + }, + ], + edges: [ + { + id: "e-control", + from: "pkg", + fromSide: "r", + to: "control", + toSide: "l", + fromDy: -14, + label: "/control", + }, + { + id: "e-runtime", + from: "pkg", + fromSide: "r", + to: "runtime", + toSide: "l", + fromDy: 14, + label: "/runtime", + }, + { + id: "e-migrate", + from: "control", + fromSide: "r", + to: "db", + toSide: "l", + toDy: -26, + label: "db init", + }, + { + id: "e-query", + from: "runtime", + fromSide: "r", + to: "db", + toSide: "l", + toDy: 26, + label: "queries", + }, + ], + steps: [ + { + title: "1. One package", + caption: + "An extension ships as one npm package with two entry points: a control export used at build time and a runtime export used when your app runs. You install it once.", + nodes: ["pkg"], + edges: [], + emphasize: ["pkg"], + }, + { + title: "2. Control plane", + caption: + "prisma-next.config.ts registers the control export. That is the side contract emission and migrations use: the pack contributes schema types like Vector(n) and ships the baseline migration that installs the database feature.", + nodes: ["pkg", "control"], + edges: ["e-control"], + emphasize: ["control"], + }, + { + title: "3. Runtime plane", + caption: + "db.ts registers the runtime export on the client. That side contributes the codecs that encode and decode vector values, and the query operations your code calls, like cosineDistance. If a required pack is missing here, the client fails at construction.", + nodes: ["pkg", "control", "runtime"], + edges: ["e-control", "e-runtime"], + emphasize: ["runtime"], + }, + { + title: "4. Meet at the database", + caption: + "db init applies the pack's baseline migration, CREATE EXTENSION IF NOT EXISTS vector, and your queries run against a database that provably has the feature the contract relies on.", + nodes: ["pkg", "control", "runtime", "db"], + edges: ["e-control", "e-runtime", "e-migrate", "e-query"], + emphasize: ["db"], + }, + ], +}; + /** * Names that render as visual flow diagrams. Any name not listed here falls * back to the Code Hike token animation in presets.ts. @@ -528,4 +790,6 @@ export const FLOW_SCENES = { "compute-model": computeModel, "env-layers": envLayers, "github-connection": githubConnection, + "middleware-pipeline": middlewarePipeline, + "extension-planes": extensionPlanes, } satisfies Partial>; diff --git a/apps/docs/src/components/concept-animation/presets.ts b/apps/docs/src/components/concept-animation/presets.ts index 2588ab620c..47c21f9dc4 100644 --- a/apps/docs/src/components/concept-animation/presets.ts +++ b/apps/docs/src/components/concept-animation/presets.ts @@ -153,6 +153,87 @@ export const CONCEPT_PRESETS = { }, ], }, + "middleware-pipeline": { + label: "How a query moves through the middleware chain", + steps: [ + { + title: "1. One chain", + code: + "your app (ORM client · SQL query builder · raw)\n" + + " │ execute(plan)\n" + + " ▼\n" + + "[[cache]] → [[lints]] → [[budgets]] → driver → database", + caption: + "Every query, whichever surface issued it, runs through the same middleware array on its way to the driver, in registration order: first in the array, first at every hook.", + }, + { + title: "2. Before the driver", + code: + "your app\n" + + " │ [[beforeCompile]] · [[beforeExecute]]\n" + + " ▼\n" + + "cache → [[lints]] → [[budgets]] → driver → database", + caption: + "Before anything reaches the database, beforeCompile can rewrite the query and beforeExecute can validate it, mutate parameters, or throw to block it. This is where lints and budgets do their pre-flight checks.", + }, + { + title: "3. Cache answers early", + code: + "your app ◄───────────── [[intercept: cached rows]]\n" + + " │ │\n" + + " ▼ │\n" + + "[[cache]] ──────────────┘ lints · budgets · driver skipped", + caption: + "A middleware can answer a query itself: on a hit, the cache returns rows from intercept and the driver never runs. afterExecute still fires with source set to middleware, so observability keeps working.", + }, + { + title: "4. Rows stream back", + code: + "your app ◄── [[onRow]] (per row) ◄── database rows\n" + + " [[afterExecute]] (rowCount · latencyMs)", + caption: + "On the way back, onRow fires once per row as the driver streams, and afterExecute closes the call with the observed row count and latency.", + }, + ], + }, + "extension-planes": { + label: "How one extension package plugs into a project", + steps: [ + { + title: "1. One package", + code: "[[@prisma-next/extension-pgvector]]\n ├─ /control (build time)\n └─ /runtime (query time)", + caption: + "An extension ships as one npm package with two entry points: a control export used at build time and a runtime export used when your app runs.", + }, + { + title: "2. Control plane", + code: + "@prisma-next/extension-pgvector\n" + + " └─ /control → [[prisma-next.config.ts]]\n" + + " schema types · [[baseline migration]]", + caption: + "prisma-next.config.ts registers the control export. Contract emission picks up schema types like Vector(n), and the pack ships the baseline migration that installs the database feature.", + }, + { + title: "3. Runtime plane", + code: + "@prisma-next/extension-pgvector\n" + + " ├─ /control → prisma-next.config.ts\n" + + " └─ /runtime → [[db.ts]]\n" + + " codecs · [[query operations]]", + caption: + "db.ts registers the runtime export on the client, which gains the pack's codecs and query operations like cosineDistance. A missing required pack fails at client construction.", + }, + { + title: "4. Meet at the database", + code: + "prisma-next.config.ts → [[db init]] → CREATE EXTENSION vector\n" + + "db.ts → [[queries]] → cosineDistance(…)", + caption: + "db init applies the pack's baseline migration, and your queries run against a database that provably has the feature the contract relies on.", + }, + ], + }, } satisfies Record; export type ConceptName = keyof typeof CONCEPT_PRESETS; From 4882373a0c33bed3394e4afa14694aa426fa0c70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 3 Jul 2026 16:49:00 -0700 Subject: [PATCH 08/11] chore: add docs dev-server launch config Lets the Claude Code preview tooling start the docs app (pnpm --filter docs exec next dev) on port 3105, leaving the default 3001 free for other local processes. Co-Authored-By: Claude Fable 5 --- .claude/launch.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .claude/launch.json diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000000..7f171c4ae5 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "docs", + "runtimeExecutable": "pnpm", + "runtimeArgs": ["--filter", "docs", "exec", "next", "dev", "--port", "3105"], + "port": 3105 + } + ] +} From c33b78656051a3ab84edf4762fd887c0cf833c64 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:05:43 +0200 Subject: [PATCH 09/11] docs(middleware): plain language, fixed animations, hand-holdy authoring guide Revision per review feedback, with the middleware behavior re-validated against @prisma-next 0.14.0 on a live database: - Fix the pipeline animation geometry: all chain boxes now share one vertical center so the request lane is a straight line, the endpoint boxes are sized to host the return lane inside them, and the empty band that read as broken layout is gone. Step 1 no longer mentions raw SQL (there is no standalone raw statement surface; raw exists as fns.raw fragments inside the SQL query builder). - Replace the dense text lifecycle line with a stepped Code Hike animation (middleware-lifecycle) that walks beforeCompile -> lowering -> beforeExecute -> encoding -> intercept -> driver -> onRow -> afterExecute, one concern per step. Mermaid source in the PR discussion. - Rewrite how-middleware-works plainly: a what-you-can-do opener, the five hooks as short subsections instead of one dense table, ordering rules pulled out, and "runtime family" explained in plain words (SQL family vs document family, what familyId buys you). - Rewrite authoring-custom-middleware as a hand-holdy numbered guide in the house guide style: build a query logger, register it, run a query, see the real output (captured from a live run), then add an option. The hook and context reference follows the working example. Includes the registration-order note (logger before budgets, per CodeRabbit) and an honest note that ctx.log has no visible sink through postgres(...) yet, so the guide logs via its own logger. - built-in-cache: the ORM opt-in now uses the facade path (db.orm.public.User.first with the meta callback, verified with a cache hit), replacing the manual orm() wiring and its already-connected footgun. Addresses CodeRabbit's collections finding as well. - built-in-lints: note that warn severities are not printed anywhere today because the client does not accept a log sink yet; error severities still block. - built-in-budgets: cross-link the take() fix to the Fundamentals pagination section; drop the em dash. - using-extensions: catalog links labeled README and described as GitHub links (CodeRabbit). - Add "Prompt your coding agent" endings per the docs-writer skill conventions. Co-Authored-By: Claude Fable 5 --- .../orm/next/extensions/using-extensions.mdx | 14 +- .../authoring-custom-middleware.mdx | 193 ++++++++++++------ .../orm/next/middleware/built-in-budgets.mdx | 4 +- .../orm/next/middleware/built-in-cache.mdx | 9 +- .../orm/next/middleware/built-in-lints.mdx | 8 +- .../next/middleware/how-middleware-works.mdx | 93 ++++++--- .../concept-animation/flow-presets.ts | 41 ++-- .../components/concept-animation/index.tsx | 2 +- .../components/concept-animation/presets.ts | 61 ++++++ 9 files changed, 287 insertions(+), 138 deletions(-) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 561bc02e8a..7519f8812e 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -88,17 +88,17 @@ If the runtime descriptor for a required pack is missing, Prisma Next fails when Want an extension that does not exist yet? Build it: the catalog is first-party today and built for community authors, extension packs are versioned npm packages with a documented layout, and the [call for extension authors](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) explains how to write and publish one. -| Extension | What it adds | Package | Databases | Status | Guide | +| Extension | What it adds | Package | Databases | Status | README | | --- | --- | --- | --- | --- | --- | -| pgvector | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | -| PostGIS | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | -| ParadeDB | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | -| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` external tables, plus a role-bound runtime facade (`asUser()`, `asAnon()`, `asServiceRole()`) | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | -| arktype-json | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | Early Access | [Guide](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | +| pgvector | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | +| PostGIS | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | +| ParadeDB | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | Experimental | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | +| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` external tables, plus a role-bound runtime facade (`asUser()`, `asAnon()`, `asServiceRole()`) | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | +| arktype-json | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | On the two experimental packs: ParadeDB currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and Supabase ships the contract surface plus role-bound runtime helpers, while broader RLS authoring and cross-contract foreign keys are still separate follow-up projects. Check the package source and examples when you need the precise current surface for an experimental pack. -The Guide links cover each pack's package-level setup, and the runnable examples show the current query surface in context. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs, and the [Supabase example](https://github.com/prisma/prisma-next/tree/main/examples/supabase) shows role-bound runtime usage. +The README links go to each package's setup notes on GitHub, and the runnable examples show the current query surface in context. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs, and the [Supabase example](https://github.com/prisma/prisma-next/tree/main/examples/supabase) shows role-bound runtime usage. ## See also diff --git a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index cc54198866..1ea90fbeb1 100644 --- a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -1,104 +1,137 @@ --- title: Authoring custom middleware -description: Write your own Prisma Next middleware as an object with a name and hooks, and register it on the runtime. +description: Build, register, and test your own Prisma Next middleware, step by step, starting with a query logger. url: /orm/next/middleware/authoring-custom-middleware metaTitle: Authoring custom middleware in Prisma Next -metaDescription: Write a custom Prisma Next middleware, with a complete slow-query warning example, the hook and context reference, and query-rewriting variations. +metaDescription: Write a custom Prisma Next middleware step by step. Build a query logger, register it, see its output, and learn the full hook surface. badge: early-access --- -A custom middleware is a plain object: give it a `name`, declare the runtime family it targets, and implement the hooks you need. No base class, no registration API beyond the `middleware` array. +## Introduction -Reach for custom middleware when the behavior belongs beside data access rather than inside every resolver or service method: tenant filters, soft-delete policy, query timing, read-through fixtures, request-scoped auditing, or parameter preparation for codecs and external services. +In this guide, you build your own middleware from scratch: a query logger that prints every query with its row count and latency. You will create the middleware file, register it, run a query, and see the output. At the end you will know the full hook surface well enough to write any middleware. -This complete example warns whenever a query runs longer than a threshold: +A custom middleware is a plain object with a `name` and the hooks you implement. There is no base class and no registration API beyond the `middleware` array you already know from [How middleware works](/orm/next/middleware/how-middleware-works). -```ts title="src/prisma/slow-query-warning.ts" -import type { SqlMiddleware } from '@prisma-next/sql-runtime'; +## Prerequisites -export interface SlowQueryWarningOptions { - /** Latency above this many milliseconds logs a warning. Default: 250. */ - readonly thresholdMs?: number; -} +- A Prisma Next project with a working `src/prisma/db.ts`. The [PostgreSQL quickstart](/next/quickstart/postgresql) sets one up in a few minutes. +- At least one model with a few rows, so the logger has something to log. -export function slowQueryWarning(options?: SlowQueryWarningOptions): SqlMiddleware { - const thresholdMs = options?.thresholdMs ?? 250; +## 1. Create the middleware +Create a new file next to your database setup. The middleware implements one hook, `afterExecute`, which runs once after every query with the outcome: + +```ts title="src/prisma/query-logger.ts" +import type { SqlMiddleware } from "@prisma-next/sql-runtime"; + +export function queryLogger(): SqlMiddleware { return { - name: 'slow-query-warning', - familyId: 'sql', + name: "query-logger", + familyId: "sql", - async afterExecute(plan, result, ctx) { - if (result.latencyMs <= thresholdMs) return; - ctx.log.warn({ - code: 'APP.SLOW_QUERY', - message: `Query took ${result.latencyMs}ms (threshold: ${thresholdMs}ms)`, - details: { - sql: plan.sql, - rowCount: result.rowCount, - latencyMs: result.latencyMs, - source: result.source, - planExecutionId: ctx.planExecutionId, - }, - }); + async afterExecute(plan, result) { + console.log( + `[query-logger] ${result.rowCount} rows in ${Math.round(result.latencyMs)}ms · ${plan.sql}`, + ); }, }; } ``` -Register it in the `middleware` array on the runtime setup, the same way as the [built-ins](/orm/next/middleware/how-middleware-works#what-ships-built-in): +Typing the object as `SqlMiddleware` gives you inferred parameter types on every hook, so you rarely import the plan or result types yourself. `familyId: "sql"` says this middleware is for SQL databases; the [family section below](#which-family-to-declare) explains when to set it. + +## 2. Register it + +Add the middleware to the `middleware` array in your client setup: ```ts title="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' }; -import { slowQueryWarning } from './slow-query-warning'; +import { queryLogger } from './query-logger'; export const db = postgres({ contractJson, url: process.env['DATABASE_URL']!, - middleware: [slowQueryWarning({ thresholdMs: 250 })], + middleware: [queryLogger()], }); ``` -Every query now reports when it crosses the threshold, whatever surface issued it: +That is the whole wiring. Every query on this client now passes through the logger, whether it comes from the ORM API or the SQL query builder. + +## 3. Run a query and check the output + +Run any query in your app, for example: + +```ts +const users = await db.orm.public.User.select("id", "email").take(5).all(); +``` + +Your terminal shows one line per query: + +```text no-copy +[query-logger] 2 rows in 18ms · SELECT "user"."id" AS "id", "user"."email" AS "email" FROM "public"."user" LIMIT 5 +``` + +If you see no output, check that the middleware is in the `middleware` array of the client your query actually uses, and that the query ran (an error before execution skips `afterExecute` only if the driver never started; failed driver calls still log, with `result.completed` set to `false`). + +## 4. Add an option + +Real middleware usually takes options. Add a threshold so the logger only reports slow queries: + +```ts title="src/prisma/query-logger.ts" +import type { SqlMiddleware } from "@prisma-next/sql-runtime"; + +export interface QueryLoggerOptions { + /** Only log queries slower than this many milliseconds. Default: 0, log everything. */ + readonly thresholdMs?: number; +} + +export function queryLogger(options?: QueryLoggerOptions): SqlMiddleware { + const thresholdMs = options?.thresholdMs ?? 0; -```text title="Example log" -warn { - code: 'APP.SLOW_QUERY', - message: 'Query took 412ms (threshold: 250ms)', - details: { sql: 'SELECT "id", "email" FROM "user" LIMIT 10', rowCount: 10, ... } + return { + name: "query-logger", + familyId: "sql", + + async afterExecute(plan, result) { + if (result.latencyMs < thresholdMs) return; + console.log( + `[query-logger] ${result.rowCount} rows in ${Math.round(result.latencyMs)}ms · ${plan.sql}`, + ); + }, + }; } ``` -## How it works +Register it with a threshold and fast queries go quiet: -Type your middleware as `SqlMiddleware` (from `@prisma-next/sql-runtime`) and TypeScript infers every hook's parameters, so you rarely need to import the plan or context types directly. The hooks, all optional: +```ts title="src/prisma/db.ts" +middleware: [queryLogger({ thresholdMs: 250 })], +``` -- `beforeCompile(draft, ctx)` runs before the query AST is lowered to SQL. Return a new draft (`{ ...draft, ast: rewritten }`) to rewrite the query, or `undefined` to pass through. This hook is SQL-specific. -- `beforeExecute(plan, ctx, params?)` runs after lowering and before parameter encoding. `plan.sql` is the rendered statement, `plan.params` contains user-domain parameter values, and `plan.ast` is the pre-lowering AST. Throw to block the query. Iterate `params.entries()` and call `params.replaceValue(...)` to mutate parameter values before they are encoded. -- `intercept(plan, ctx)` runs last before the driver. Return `{ rows }` (an iterable or async iterable of row objects) to answer the query yourself and skip the driver, or `undefined` to pass through. -- `onRow(row, plan, ctx)` runs for every row the driver yields. -- `afterExecute(plan, result, ctx)` runs once at the end, with `result.rowCount`, `result.latencyMs`, `result.completed` (`false` when the driver errored), and `result.source` (`'driver'`, or `'middleware'` when an `intercept` answered). +One placement rule worth knowing: hooks run in registration order, and a middleware that throws stops the ones after it. Register your logger before middleware that can throw, such as `budgets`, so the queries you most want to see still get logged. -Hooks run in registration order at every site, and the context (`ctx`) is shared across one `execute()` call: +You have a working, configurable middleware. The rest of this page is the surface you reach for as your middleware grows. -| Context field | What it gives you | -| --- | --- | -| `ctx.log` | Structured `info` / `warn` / `error` (and optional `debug`) sinks wired to the runtime's logger | -| `ctx.mode` | `'strict'` or `'permissive'`; use it when a middleware should throw in strict environments and warn elsewhere | -| `ctx.now()` | Current time from the runtime's clock; prefer it over `Date.now()` so tests can control time | -| `ctx.planExecutionId` | Unique ID per `execute()` call, the same value in every hook, for correlating observations | -| `ctx.scope` | `'runtime'`, `'connection'`, or `'transaction'`; use it to skip work in scopes you should not touch | -| `ctx.contentHash(plan)` | Stable digest of the statement plus parameters, for keying caches or coalescing requests | -| `ctx.signal` | Optional `AbortSignal` for cancellation; observe it in long-running hook work | -| `ctx.contract` | The runtime's contract, when a hook needs schema information | +## The hooks + +`afterExecute` is one of five hooks. Implement any subset: + +| Hook | Runs | Use it to | +| --- | --- | --- | +| `beforeCompile(draft, ctx)` | Before the query AST becomes SQL | Rewrite the query, for example add a tenant filter | +| `beforeExecute(plan, ctx, params?)` | Before the driver, with `plan.sql` rendered | Validate, throw to block, or adjust parameter values | +| `intercept(plan, ctx)` | Right before the driver | Return `{ rows }` to answer the query yourself | +| `onRow(row, plan, ctx)` | Once per streamed row | Count or sample rows; throw to abort | +| `afterExecute(plan, result, ctx)` | After the query finishes | Log timing, row count, and outcome | -## Variations +[How middleware works](/orm/next/middleware/how-middleware-works#the-five-hooks) walks the full lifecycle with an animation. Two hooks deserve a closer look here, because they change the query rather than observe it. -### Rewrite queries with `beforeCompile` +### Rewrite queries with beforeCompile -`beforeCompile` sees the typed AST before lowering, which makes cross-cutting query rewrites possible: tenant scoping, soft-delete filters, or row-level restrictions. This middleware appends a predicate to every `SELECT` on the `user` table: +`beforeCompile` sees the typed AST before it becomes SQL. Return a new draft to rewrite the query. This middleware adds a predicate to every `SELECT` on the `user` table: ```ts title="src/prisma/scope-user-selects.ts" import type { SqlMiddleware } from '@prisma-next/sql-runtime'; @@ -120,28 +153,52 @@ export function scopeUserSelects(predicate: BinaryExpr): SqlMiddleware { } ``` -Because each middleware's returned draft feeds the next one, rewrites compose in registration order. +Return `undefined` to pass the query through unchanged. Because each middleware's returned draft feeds the next, rewrites compose in registration order. -### Short-circuit with `intercept` +### Answer queries with intercept -Return rows from `intercept` to answer a query without the driver: caches, mocks and fixtures in tests, rate limiters, and circuit breakers all fit this hook. `beforeExecute` has already run, so validation and parameter mutation still happen before a short-circuit. Return raw (undecoded) row objects; the runtime runs its normal decoding pass over them, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays observable. The [cache middleware](/orm/next/middleware/built-in-cache) is the reference implementation of this pattern. +Return rows from `intercept` and the driver never runs. Caches, test fixtures, rate limiters, and circuit breakers all fit this hook. Return raw row objects; the runtime decodes them normally, and `afterExecute` reports `source: 'middleware'` so the short-circuit stays visible to your logging. The [cache middleware](/orm/next/middleware/built-in-cache) is the reference implementation. -### Support every database +## The context object -A middleware without `familyId` and `targetId` is cross-family and can register on PostgreSQL and MongoDB runtimes alike. Cross-family middleware cannot use SQL-specific surfaces like `beforeCompile` or `plan.sql`; they work against the shared hooks (`intercept`, `beforeExecute`, `onRow`, `afterExecute`) and the family-agnostic plan shape. +Every hook receives a context (`ctx`) as its last argument: + +| Field | What it gives you | +| --- | --- | +| `ctx.planExecutionId` | The same unique ID in every hook of one query, for correlating observations | +| `ctx.now()` | The runtime's clock; prefer it over `Date.now()` so tests can control time | +| `ctx.scope` | `'runtime'`, `'connection'`, or `'transaction'`; skip work in scopes you should not touch | +| `ctx.mode` | `'strict'` or `'permissive'`; throw in strict environments, warn elsewhere | +| `ctx.contentHash(plan)` | A stable digest of statement plus parameters, for cache keys | +| `ctx.contract` | The runtime's contract, when a hook needs schema information | +| `ctx.log` | Structured log sinks (`info`, `warn`, `error`) wired to the runtime's logger | + +One honest note on `ctx.log`: the `postgres(...)` client does not expose a way to attach a log sink yet, so events sent to `ctx.log` are not printed anywhere by default. For output you can see today, log directly to your own logger, as the query logger above does with `console.log`. + +## Which family to declare + +Set `familyId: 'sql'` when your middleware touches anything SQL-shaped: `plan.sql`, the query AST in `beforeCompile`, SQL parameter handling. It will register on PostgreSQL and be rejected on MongoDB at startup, with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, rather than failing at query time. + +Leave `familyId` out only when the middleware sticks to family-neutral surfaces (`intercept`, `onRow`, `afterExecute` on the shared result shape). Such a middleware runs on PostgreSQL and MongoDB alike; the cache works this way. ## Common gotchas :::warning -- The runtime validates middleware when you construct the client. A `familyId` that does not match the runtime throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` at startup, and a `targetId` without a `familyId` throws `RUNTIME.MIDDLEWARE_INCOMPATIBLE`. -- `afterExecute` also fires when the driver fails, with `result.completed` set to `false`. Errors you throw from `afterExecute` on that failure path are swallowed so they cannot mask the driver error; put must-not-fail logic elsewhere. - Throwing from `afterExecute` on a successful query fails the call even though the database work already happened. Reserve it for cases where failing loudly is the point, as the [budgets](/orm/next/middleware/built-in-budgets) latency check does. +- `afterExecute` also runs when the driver fails, with `result.completed` set to `false`. Errors you throw on that failure path are swallowed so they cannot mask the driver error. ::: -## See also +## Prompt your coding agent + +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent. Prompts that map to this guide: + +- "Write a Prisma Next middleware that logs every query slower than 250ms, and register it before budgets." +- "Add a beforeCompile middleware that scopes every SELECT on the user table to the current tenant." +- "Write an intercept middleware that returns fixture rows for the products table in tests." + +## Next steps -- [How middleware works](/orm/next/middleware/how-middleware-works) +- [How middleware works](/orm/next/middleware/how-middleware-works): the lifecycle your hooks plug into - [Built-in: budgets](/orm/next/middleware/built-in-budgets), [Built-in: lints](/orm/next/middleware/built-in-lints), and [Built-in: cache](/orm/next/middleware/built-in-cache) as production examples of the hook surface -- [Using extensions](/orm/next/extensions/using-extensions) when you want to add database capabilities instead of wrapping queries diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx index a196153ad7..5466e3b711 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-budgets.mdx @@ -44,7 +44,7 @@ All options are optional. | `tableRows` | `Record` | `{}` | Per-table row-count assumptions for the estimator, keyed by table name | | `maxLatencyMs` | `number` | `1_000` | Latency budget for one query, checked after execution | | `severities.rowCount` | `'warn' \| 'error'` | `'error'` | Whether pre-execution row-budget violations block the query or log a warning in permissive mode. Strict mode always blocks | -| `severities.latency` | `'warn' \| 'error'` | — | Accepted by the options type but not read by the middleware in v0.14; latency behavior follows the runtime mode (see below) | +| `severities.latency` | `'warn' \| 'error'` | (none) | Accepted by the options type but not read by the middleware in v0.14; latency behavior follows the runtime mode (see below) | The runtime defaults to strict mode. In strict mode, over-budget latency throws `BUDGET.TIME_EXCEEDED`; in permissive mode, the same event is logged as a warning. The latency check happens after execution, so it is a visibility tool rather than a cancellation mechanism. Setting `severities.latency` does not change this in v0.14: the key exists on the options type, but the middleware does not consult it yet. @@ -72,7 +72,7 @@ BUDGET.TIME_EXCEEDED: Query latency exceeds budget { latencyMs: 2481, maxLatencyMs: 1000 } ``` -In permissive mode, the same information is emitted through the runtime logger as a warning. To fix an unbounded-select violation, add a `LIMIT` to the query (`take(...)` on the ORM client, `.limit(...)` on the SQL query builder) or raise the budget deliberately. +In permissive mode, the same information is emitted through the runtime logger as a warning. To fix an unbounded-select violation, add a limit to the query ([`take(...)` on the ORM API](/orm/next/fundamentals/reading-data#sort-and-paginate), `.limit(...)` on the SQL query builder) or raise the budget deliberately. ## Common gotchas diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx index 5f36f3f5d0..bcdcdebf63 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-cache.mdx @@ -50,16 +50,13 @@ const plan = db.sql.public.user const users = await db.runtime().execute(plan); ``` -On the ORM client, pass the annotation through the query's meta callback: +On the ORM API, pass the annotation through the query's meta callback: ```ts title="src/prisma/get-user-cached.ts" import { cacheAnnotation } from '@prisma-next/middleware-cache'; -import { orm } from '@prisma-next/sql-orm-client'; +import { db } from './db'; -const runtime = await db.connect(); -const models = orm({ runtime, context: db.context }).public; - -const user = await models.User.first({ id }, (meta) => +const user = await db.orm.public.User.first({ id }, (meta) => meta.annotate(cacheAnnotation({ ttl: 60_000 })), ); ``` diff --git a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx index c1bf7c4f87..26e631ea31 100644 --- a/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx +++ b/apps/docs/content/docs/orm/next/middleware/built-in-lints.mdx @@ -37,7 +37,7 @@ Lints run in `beforeExecute`, against the query plan's AST. Each rule has a seve | No LIMIT | `LINT.NO_LIMIT` | `warn` | A `SELECT` has no `LIMIT` clause | | SELECT star | `LINT.SELECT_STAR` | `warn` | A query selects all columns instead of naming them | -A warning is logged as a structured event you can route to your logger: +A warning is emitted as a structured event on the runtime's log: ```text title="Lint warning" warn { @@ -47,6 +47,12 @@ warn { } ``` +:::note + +The `postgres(...)` client does not expose a way to attach a log sink yet, so `warn` findings are not printed anywhere by default; only `error` severities are observable, because they block the query. Treat `warn` rules as forward-looking until the client accepts a logger. + +::: + ## Configure severities Raise or lower any rule per project through `severities`. For example, treat unbounded selects as hard failures in a service where every list endpoint must paginate: diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index c3b89ad61d..1575c77639 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -1,17 +1,17 @@ --- title: How middleware works -description: Middleware wraps every query your app sends through Prisma Next, so you can run code before and after execution. +description: Middleware runs your code before and after every query, so one policy can cover your whole app. url: /orm/next/middleware/how-middleware-works metaTitle: How middleware works in Prisma Next metaDescription: What Prisma Next middleware is, the hooks it exposes, how to register it on the runtime, and which middleware ships built in. badge: early-access --- -A middleware is an object with a name and one or more hooks that the Prisma Next runtime calls at fixed points around every query, so you can observe, guard, rewrite, or answer queries without touching application code. +Middleware lets you run your own code around every query your app sends through Prisma Next. -Use middleware when you want one policy to cover every query surface: audit logging, query budgets, safety checks, caching, tenant scoping, test fixtures, circuit breakers, or parameter processing before values reach the driver. +For example, you can log every query with its latency, block a `DELETE` that has no `WHERE` clause before it reaches the database, or serve a repeated read from memory instead of running it again. You write the policy once, register it once, and every query follows it. No call sites to update. -You register middleware once, on the runtime setup, through the `middleware` option: +A middleware is a plain object with a name and one or more hooks. Register it once, in the `middleware` option of your client setup: ```ts title="src/prisma/db.ts" import { createCacheMiddleware } from '@prisma-next/middleware-cache'; @@ -31,36 +31,50 @@ export const db = postgres({ }); ``` -Every query then passes through the chain: the ORM client, the SQL query builder, and raw SQL all execute through the same runtime, so one `middleware` array covers all of them. On MongoDB, pass the same option to `mongo(...)` from `@prisma-next/mongo/runtime`. +Every query passes through the chain, whether it comes from the ORM API or the SQL query builder, because both execute through the same runtime. On MongoDB, pass the same option to `mongo(...)` from `@prisma-next/mongo/runtime`. -The animation below follows one query through that chain, including the cache hit that skips the driver. Step through it, or let it play: +The animation below follows one query through the chain, including the cache hit that skips the database: -## How it works +## The five hooks -Each middleware implements some of five hooks. The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. +A middleware implements only the hooks it needs. The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. Step through the lifecycle to see where each hook sits: -| Hook | When it runs | What it can do | -| --- | --- | --- | -| `beforeCompile(draft, ctx)` | Before the query AST is lowered to SQL (SQL runtimes only) | Return a modified draft to rewrite the query, for example to add a predicate | -| `beforeExecute(plan, ctx, params?)` | After lowering, before parameter encoding and the driver | Inspect the plan (`plan.sql`, `plan.ast`), validate it, throw to block it, or mutate parameter values | -| `intercept(plan, ctx)` | After `beforeExecute` and parameter encoding, right before the driver | Return rows directly and skip the driver entirely (this is how the cache serves hits) | -| `onRow(row, plan, ctx)` | Once per row the driver yields | Observe or count rows; throw to abort the stream | -| `afterExecute(plan, result, ctx)` | After the query finishes | Read `result.rowCount`, `result.latencyMs`, `result.completed`, and `result.source` for timing, logging, and budgets | + + +### beforeCompile: rewrite the query + +Runs before the query is turned into SQL, while it is still a typed AST. Return a modified draft to rewrite the query, for example to add a tenant filter to every `SELECT`. This hook exists on SQL databases only, because it works on the SQL query AST. + +### beforeExecute: validate or block -The full lifecycle for a SQL query: +Runs after the SQL is rendered and before anything reaches the database. The hook sees the full plan (`plan.sql`, `plan.ast`). Throw an error to block the query: -```text title="Middleware lifecycle" -beforeCompile → SQL lowering → beforeExecute → parameter encoding → intercept → driver → onRow (per row) → afterExecute +```ts +async beforeExecute(plan) { + if (isForbidden(plan)) throw new Error("blocked by policy"); +} ``` -Two details of the ordering matter in practice: +This is where `lints` stops a `DELETE` without `WHERE`, and where `budgets` rejects a query that would read too many rows. This hook can also adjust parameter values before they are encoded for the driver. + +### intercept: answer without the database + +Runs last before the driver. Return `{ rows }` and the driver never runs; return nothing and the query continues. This is how the cache serves repeated reads. Validation from `beforeExecute` has already happened by this point. + +### onRow: watch rows stream + +Runs once per row as the database streams results. Count rows, sample them, or throw to abort the stream. `budgets` uses this hook to stop a query that returns more rows than its estimate promised. -- **Registration order is execution order at each hook site.** If a `beforeCompile` hook rewrites the query, the next middleware sees the rewritten draft. If multiple middleware implement `intercept`, the first one that returns rows wins. -- **`intercept` short-circuits the row source.** `beforeExecute` has already run by the time `intercept` fires. When a middleware returns rows from `intercept`, the driver never runs and `onRow` is skipped, but `afterExecute` still fires on every middleware with `result.source` set to `'middleware'` instead of `'driver'`, so observability keeps working for served-from-cache reads. +### afterExecute: observe the finished query -Every hook receives a context (`ctx`) with the contract, runtime `mode` (`'strict'` or `'permissive'`), a `log` with `info`, `warn`, and `error` methods, a monotonic `now()`, the current `scope` (`'runtime'`, `'connection'`, or `'transaction'`), and a `planExecutionId` that is unique per `execute()` call, so you can correlate observations across hooks. See [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) for the full surface. +Runs once at the end, success or failure. It receives the outcome: `result.rowCount`, `result.latencyMs`, `result.completed`, and `result.source`, which is `'driver'` for a normal query and `'middleware'` when an `intercept` answered. Timing, logging, and latency budgets live here. + +Two ordering rules matter in practice: + +- Registration order is execution order at every hook. If one middleware rewrites the query in `beforeCompile`, the next middleware sees the rewritten version. +- If several middleware implement `intercept`, the first one that returns rows wins. ## What ships built in @@ -68,26 +82,37 @@ Three middleware ship with Prisma Next today: | Middleware | Import | What it does | | --- | --- | --- | -| [`budgets`](/orm/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Blocks queries that would read too many rows, and raises `BUDGET.TIME_EXCEEDED` after execution when a query overruns its latency budget | +| [`budgets`](/orm/next/middleware/built-in-budgets) | `@prisma-next/sql-runtime` | Blocks queries that would read too many rows, and reports queries that overrun a latency budget | | [`lints`](/orm/next/middleware/built-in-lints) | `@prisma-next/sql-runtime` | Blocks or warns on risky query shapes, such as `DELETE` without `WHERE` | | [`cache`](/orm/next/middleware/built-in-cache) | `@prisma-next/middleware-cache` | Serves repeated reads from an in-memory store, opted in per query | -`budgets` and `lints` are SQL-specific and come with the SQL runtime. The cache lives in its own package because it is family-agnostic: it works on PostgreSQL and MongoDB. +To write your own, follow the [authoring guide](/orm/next/middleware/authoring-custom-middleware): it builds a working query logger step by step. + +## Which databases a middleware runs on + +Prisma Next groups databases into families: the SQL family (PostgreSQL and other SQL databases) and the document family (MongoDB). A middleware that inspects SQL text or the SQL query AST only makes sense on SQL databases, so it declares which family it belongs to with `familyId: 'sql'`. + +- `budgets` and `lints` declare `familyId: 'sql'`. They register on PostgreSQL and are rejected on MongoDB. +- The cache declares no `familyId`. It works against the family-neutral parts of a query, so it runs on both PostgreSQL and MongoDB. + +The runtime checks this when you construct the client. A middleware that does not match the runtime fails at startup with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, not at query time in production. + +## When a middleware or the driver throws -## Errors and failure behavior +A middleware that throws from `beforeCompile`, `beforeExecute`, `intercept`, or `onRow` fails the query: the error goes to the caller and the database work stops. -A middleware that throws from `beforeCompile`, `beforeExecute`, `intercept`, or `onRow` fails the query: the error propagates to the caller and the driver work stops. This is how `lints()` blocks a `DELETE` without a `WHERE` clause before it reaches the database. +When the driver itself fails, `afterExecute` still runs on every middleware with `result.completed` set to `false`, so your logging sees failed queries too. Errors thrown from `afterExecute` while handling a failure are swallowed, so they cannot mask the original error. -When the driver itself fails, the runtime still calls `afterExecute` on every middleware with `result.completed` set to `false`, then rethrows the original driver error. Errors thrown by `afterExecute` while handling a failed query are swallowed so they cannot mask the original error. +## Prompt your coding agent -## Database compatibility +Projects scaffolded with `create-prisma` install Prisma Next skills for your coding agent. Prompts that map to this page: -A middleware declares which runtime family it supports through an optional `familyId`. `budgets` and `lints` declare `familyId: 'sql'`, so they register on PostgreSQL but not on MongoDB. Middleware with no `familyId`, like the cache, work on any runtime. The runtime checks compatibility when you construct the client and throws `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH` or `RUNTIME.MIDDLEWARE_TARGET_MISMATCH` if a middleware does not match, so a wrong registration fails at startup rather than at query time. +- "Register the lints and budgets middleware on our Prisma Next client with a 10k row budget." +- "Add the cache middleware and opt the dashboard queries in with a 60 second TTL." +- "Write a middleware that logs every query slower than 250ms." ## See also -- [Built-in: budgets](/orm/next/middleware/built-in-budgets) -- [Built-in: lints](/orm/next/middleware/built-in-lints) -- [Built-in: cache](/orm/next/middleware/built-in-cache) -- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware) -- [Using extensions](/orm/next/extensions/using-extensions) for adding new database capabilities rather than wrapping queries +- [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware): build and test a query logger step by step +- [Built-in: budgets](/orm/next/middleware/built-in-budgets), [Built-in: lints](/orm/next/middleware/built-in-lints), [Built-in: cache](/orm/next/middleware/built-in-cache) +- [Using extensions](/orm/next/extensions/using-extensions) for adding database capabilities rather than wrapping queries diff --git a/apps/docs/src/components/concept-animation/flow-presets.ts b/apps/docs/src/components/concept-animation/flow-presets.ts index a0c997e64a..8ff0a6ff9b 100644 --- a/apps/docs/src/components/concept-animation/flow-presets.ts +++ b/apps/docs/src/components/concept-animation/flow-presets.ts @@ -526,18 +526,19 @@ const githubConnection: FlowScene = { const middlewarePipeline: FlowScene = { label: "How a query moves through the middleware chain", width: 712, - height: 248, - groupLabels: [{ text: "Middleware, in registration order", x: 160, y: 52 }], + height: 230, + groupLabels: [{ text: "Middleware, in registration order", x: 160, y: 46 }], nodes: [ { id: "app", label: "Your app", - sub: "ORM · SQL · raw", + sub: "ORM · SQL builder", + subBelow: true, variant: "neutral", x: 16, - y: 70, + y: 64, w: 108, - h: 150, + h: 124, }, { id: "cache", @@ -545,7 +546,7 @@ const middlewarePipeline: FlowScene = { sub: "serves repeated reads", variant: "source", x: 160, - y: 70, + y: 94, w: 130, h: 64, }, @@ -555,7 +556,7 @@ const middlewarePipeline: FlowScene = { sub: "blocks risky shapes", variant: "scope", x: 322, - y: 70, + y: 94, w: 116, h: 64, }, @@ -565,7 +566,7 @@ const middlewarePipeline: FlowScene = { sub: "caps query cost", variant: "production", x: 470, - y: 70, + y: 94, w: 102, h: 64, }, @@ -573,29 +574,31 @@ const middlewarePipeline: FlowScene = { id: "db", label: "Database", sub: "the driver", + subBelow: true, variant: "project", x: 606, - y: 70, + y: 64, w: 90, - h: 150, + h: 124, }, ], edges: [ - // Request lane: anchors sit at the chain's center line (dy -43 on the - // tall boxes), so the four hops read as one straight pipeline. - { id: "e-in", from: "app", fromSide: "r", to: "cache", toSide: "l", fromDy: -43 }, + // Request lane: every box's vertical center sits on y=126, so the four + // hops are one straight line with no offsets. + { id: "e-in", from: "app", fromSide: "r", to: "cache", toSide: "l" }, { id: "e-c-l", from: "cache", fromSide: "r", to: "lints", toSide: "l" }, { id: "e-l-b", from: "lints", fromSide: "r", to: "budgets", toSide: "l" }, - { id: "e-b-db", from: "budgets", fromSide: "r", to: "db", toSide: "l", toDy: -43 }, - // Return lane: runs through the clear band under the chain boxes. + { id: "e-b-db", from: "budgets", fromSide: "r", to: "db", toSide: "l" }, + // Return lane: runs through the clear band under the middleware row, + // inside the taller endpoint boxes. { id: "e-return", from: "db", fromSide: "l", to: "app", toSide: "r", - fromDy: 43, - toDy: 43, + fromDy: 44, + toDy: 44, label: "rows · onRow · afterExecute", }, // Cache hit: intercept answers from the chain, driver never runs. @@ -605,7 +608,7 @@ const middlewarePipeline: FlowScene = { fromSide: "b", to: "app", toSide: "r", - toDy: 43, + toDy: 44, dashed: true, label: "cached rows", }, @@ -614,7 +617,7 @@ const middlewarePipeline: FlowScene = { { title: "1. One chain", caption: - "Every query, from the ORM client, the SQL query builder, or raw SQL, runs through the same middleware array on its way to the driver. The chain runs in registration order: first in the array, first at every hook.", + "Every query, whether it comes from the ORM API or the SQL query builder, runs through the same middleware array on its way to the driver. The chain runs in registration order: first in the array, first at every hook.", nodes: ["app", "cache", "lints", "budgets", "db"], edges: ["e-in", "e-c-l", "e-l-b", "e-b-db"], emphasize: ["cache", "lints", "budgets"], diff --git a/apps/docs/src/components/concept-animation/index.tsx b/apps/docs/src/components/concept-animation/index.tsx index f0d3d2c03d..8a27e67ada 100644 --- a/apps/docs/src/components/concept-animation/index.tsx +++ b/apps/docs/src/components/concept-animation/index.tsx @@ -13,7 +13,7 @@ import { CONCEPT_PRESETS, type ConceptName, parseStepTokens } from "./presets"; * way the surrounding layout never shifts as you step through. */ export function ConceptAnimation({ name }: { name: ConceptName }) { - const scene = FLOW_SCENES[name]; + const scene = (FLOW_SCENES as Partial>)[name]; if (scene) return ; const preset = CONCEPT_PRESETS[name]; diff --git a/apps/docs/src/components/concept-animation/presets.ts b/apps/docs/src/components/concept-animation/presets.ts index 47c21f9dc4..9601de7bd0 100644 --- a/apps/docs/src/components/concept-animation/presets.ts +++ b/apps/docs/src/components/concept-animation/presets.ts @@ -21,6 +21,67 @@ export interface ConceptPreset { } export const CONCEPT_PRESETS = { + "middleware-lifecycle": { + label: "The middleware lifecycle, hook by hook", + steps: [ + { + title: "1. Rewrite", + code: + "[[beforeCompile]] your hook rewrite the query AST\n" + + "SQL lowering runtime render the SQL\n" + + "beforeExecute your hook validate · block · tweak params\n" + + "param encoding runtime bind parameter values\n" + + "intercept your hook answer without the driver?\n" + + "driver database run the statement\n" + + "onRow your hook once per streamed row\n" + + "afterExecute your hook latency · row count · source", + caption: + "A query starts as a typed AST. beforeCompile sees it first and can return a rewritten draft: add a tenant filter, inject a soft-delete predicate. Then the runtime lowers the AST to SQL text.", + }, + { + title: "2. Guard", + code: + "beforeCompile your hook rewrite the query AST\n" + + "SQL lowering runtime render the SQL\n" + + "[[beforeExecute]] your hook validate · block · tweak params\n" + + "param encoding runtime bind parameter values\n" + + "intercept your hook answer without the driver?\n" + + "driver database run the statement\n" + + "onRow your hook once per streamed row\n" + + "afterExecute your hook latency · row count · source", + caption: + "beforeExecute sees the rendered statement before anything reaches the database. Throw here to block the query (this is how lints stops a DELETE without WHERE), or adjust parameter values before they are encoded.", + }, + { + title: "3. Short-circuit", + code: + "beforeCompile your hook rewrite the query AST\n" + + "SQL lowering runtime render the SQL\n" + + "beforeExecute your hook validate · block · tweak params\n" + + "param encoding runtime bind parameter values\n" + + "[[intercept]] your hook answer without the driver?\n" + + "driver database [[skipped on a cache hit]]\n" + + "onRow your hook once per streamed row\n" + + "afterExecute your hook latency · row count · source", + caption: + "intercept runs last before the driver. Return rows from it and the driver never runs: this is how the cache serves repeated reads. Return nothing and the query continues to the database.", + }, + { + title: "4. Observe", + code: + "beforeCompile your hook rewrite the query AST\n" + + "SQL lowering runtime render the SQL\n" + + "beforeExecute your hook validate · block · tweak params\n" + + "param encoding runtime bind parameter values\n" + + "intercept your hook answer without the driver?\n" + + "driver database run the statement\n" + + "[[onRow]] your hook once per streamed row\n" + + "[[afterExecute]] your hook latency · row count · source", + caption: + "On the way back, onRow fires once per row as results stream, and afterExecute closes the call with the row count, the latency, and whether the driver or a middleware answered. Log it, meter it, or enforce a budget.", + }, + ], + }, "compute-model": { label: "How Compute organizes resources and isolates branches", steps: [ From 7357f84147080df20381c50bc554195665e9587c Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:50:24 +0200 Subject: [PATCH 10/11] docs(middleware): radically simpler diagrams, e2e-proven authoring guide Per review: the diagrams carried too much text and did not land the concept. All three are redesigned from minimal Mermaid sketches (in the PR discussion) and rebuilt as three-to-five-box flow scenes with one-sentence captions: - middleware-pipeline: three boxes (Your app -> Middleware -> Database) and four steps: pass through, block, answer from cache, observe results. The middleware internals are one sub-line, not boxes. - middleware-lifecycle: the dense eight-row token table is replaced by a five-box flow of just the hooks in order (rewrite -> guard -> answer early -> per row -> observe), with the driver as an edge label between intercept and onRow. - extension-planes: four plain boxes (package -> config/db.ts -> PostgreSQL) with the migrations and queries as edge labels; the rows/chips text is gone. using-extensions now teaches by doing first: five numbered steps (install, register in config, register on client, use the type, apply and query), then the diagram as "How the pieces fit", then a rewritten plain-language Capabilities section (what the keys are for: failing at client construction and at db init, not mid-query). The catalog table drops the Status column and the long feature prose; names link to the READMEs and one line covers maturity. authoring-custom-middleware was re-run end to end on a fresh create-prisma project against a live Prisma Postgres database and the guide now mirrors that run exactly: complete file listings for both files a reader touches, the real two-line logger output (INSERT and SELECT with latency), a what-if-you-see-nothing line, the tested threshold behavior, and family selection folded in as step 5. Co-Authored-By: Claude Fable 5 --- .../orm/next/extensions/using-extensions.mdx | 51 ++- .../authoring-custom-middleware.mdx | 71 ++-- .../next/middleware/how-middleware-works.mdx | 6 +- .../concept-animation/flow-presets.ts | 315 ++++++++---------- .../components/concept-animation/index.tsx | 8 +- .../components/concept-animation/presets.ts | 61 ---- 6 files changed, 232 insertions(+), 280 deletions(-) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 7519f8812e..5428a0a822 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -11,17 +11,17 @@ An extension is a package that teaches Prisma Next a database capability it does Use an extension when you want the database to do something powerful and still keep the Prisma Next experience: typed schema declarations, generated TypeScript, migration support, and query helpers that feel native to your app. -The animation below shows how one extension package lands in a project: one install, two registrations, and a database that ends up with the feature installed. Step through it, or let it play: +Adding an extension takes one install and two registrations. The steps below use pgvector, the vector search extension, as the example. - - -Adding one is an install plus two registrations, using pgvector as the example: +## 1. Install the package ```npm title="Terminal" npm install @prisma-next/extension-pgvector ``` -Register the extension's control-plane descriptor in your project config. This is the side Prisma Next uses during contract emission and migrations: +## 2. Register it in the config + +This side is used when Prisma Next emits your contract and plans migrations: ```ts title="prisma-next.config.ts" import pgvector from '@prisma-next/extension-pgvector/control'; @@ -36,7 +36,9 @@ export default defineConfig({ }); ``` -Register its runtime descriptor on the client. This is the side Prisma Next uses when your app encodes values, decodes rows, and builds extension-specific query operations: +## 3. Register it on the client + +This side is used when your app runs queries: it adds the extension's query operations and value types: ```ts title="src/prisma/db.ts" import pgvector from '@prisma-next/extension-pgvector/runtime'; @@ -51,6 +53,8 @@ export const db = postgres({ }); ``` +## 4. Use the new type in your schema + The extension's types are now available in your contract. Declare a vector column with an explicit dimension: ```prisma title="src/prisma/contract.prisma" @@ -65,7 +69,9 @@ model Post { } ``` -Run `prisma-next db init` (or `prisma-next db update` on an existing database). Extensions can ship a baseline migration in their contract space, so this step installs the database-side feature for you, `CREATE EXTENSION IF NOT EXISTS vector` in pgvector's case. Then query with the operations the extension adds: +## 5. Apply and query + +Run `prisma-next db init` (or `prisma-next db update` on an existing database). The extension ships its own migration, so this step runs `CREATE EXTENSION IF NOT EXISTS vector` for you. Then query with the operations the extension adds: ```ts title="src/prisma/similarity-search.ts" const plan = db.sql.public.post @@ -78,27 +84,36 @@ const plan = db.sql.public.post const similar = await db.runtime().execute(plan); ``` +## How the pieces fit + +One package, two registrations, one database: + + + ## Capabilities -An extension declares the capabilities it contributes under a namespaced key, `pgvector.cosine` for the query above. You do not configure capabilities by hand: registering the control descriptor records them in the emitted contract, and registering the runtime descriptor lets the client satisfy the contract's required extension packs. +Each extension names what it adds under a key like `pgvector.cosine`. You never write these keys by hand. Registering the extension in the config records them in your contract; registering it in `db.ts` provides them at runtime. + +The point of this bookkeeping is failing early: -If the runtime descriptor for a required pack is missing, Prisma Next fails when the client/runtime is constructed instead of letting a query run with half the extension installed. Database-side support is enforced by the extension's migrations and verification path. For example, if the database cannot install pgvector, `db init`, `db update`, or verification reports the problem before you rely on application traffic. +- If your contract needs an extension that `db.ts` does not register, creating the client fails immediately. A query never runs against half an extension. +- If the database itself cannot install the extension, `db init` or `db update` reports it before your app takes traffic. ## Available extensions Want an extension that does not exist yet? Build it: the catalog is first-party today and built for community authors, extension packs are versioned npm packages with a documented layout, and the [call for extension authors](https://www.prisma.io/blog/prisma-next-call-for-extension-authors) explains how to write and publish one. -| Extension | What it adds | Package | Databases | Status | README | -| --- | --- | --- | --- | --- | --- | -| pgvector | `vector(n)` columns with dimension-safe types, cosine distance and similarity operations | `@prisma-next/extension-pgvector` | PostgreSQL with pgvector | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | -| PostGIS | `geometry` columns with SRID-aware types, GeoJSON values, distance and containment queries | `@prisma-next/extension-postgis` | PostgreSQL with PostGIS | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | -| ParadeDB | BM25 full-text search indexes, authored through the standard index surface | `@prisma-next/extension-paradedb` | PostgreSQL with ParadeDB | Experimental | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | -| Supabase | A Supabase-shaped contract with `auth.*` and `storage.*` external tables, plus a role-bound runtime facade (`asUser()`, `asAnon()`, `asServiceRole()`) | `@prisma-next/extension-supabase` | PostgreSQL on Supabase | Experimental | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | -| arktype-json | JSON columns validated against an arktype schema, typed end to end | `@prisma-next/extension-arktype-json` | PostgreSQL | Early Access | [README](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | +| Extension | Adds | Package | +| --- | --- | --- | +| [pgvector](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/pgvector#readme) | Vector columns and similarity search | `@prisma-next/extension-pgvector` | +| [PostGIS](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/postgis#readme) | Geometry columns and geo queries | `@prisma-next/extension-postgis` | +| [ParadeDB](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/paradedb#readme) | BM25 full-text search indexes | `@prisma-next/extension-paradedb` | +| [Supabase](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/supabase#readme) | Supabase auth and storage tables, role-bound clients | `@prisma-next/extension-supabase` | +| [arktype-json](https://github.com/prisma/prisma-next/tree/main/packages/3-extensions/arktype-json#readme) | JSON columns validated by an arktype schema | `@prisma-next/extension-arktype-json` | -On the two experimental packs: ParadeDB currently covers the `key_field` index option only (per-field tokenizer configuration and query functions are not there yet), and Supabase ships the contract surface plus role-bound runtime helpers, while broader RLS authoring and cross-contract foreign keys are still separate follow-up projects. Check the package source and examples when you need the precise current surface for an experimental pack. +All target PostgreSQL. ParadeDB and Supabase are experimental (ParadeDB covers the `key_field` index option only so far); the rest are Early Access. Extension names link to each package's README on GitHub. -The README links go to each package's setup notes on GitHub, and the runnable examples show the current query surface in context. The [prisma-next-demo](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo) example app shows pgvector registered alongside middleware in one working project, the [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo) and [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo) demos do the same for their packs, and the [Supabase example](https://github.com/prisma/prisma-next/tree/main/examples/supabase) shows role-bound runtime usage. +For a working project per extension, see the runnable examples: [pgvector](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-demo), [PostGIS](https://github.com/prisma/prisma-next/tree/main/examples/prisma-next-postgis-demo), [ParadeDB](https://github.com/prisma/prisma-next/tree/main/examples/paradedb-demo), and [Supabase](https://github.com/prisma/prisma-next/tree/main/examples/supabase). ## See also diff --git a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx index 1ea90fbeb1..1242da2c0b 100644 --- a/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx +++ b/apps/docs/content/docs/orm/next/middleware/authoring-custom-middleware.mdx @@ -1,22 +1,29 @@ --- title: Authoring custom middleware -description: Build, register, and test your own Prisma Next middleware, step by step, starting with a query logger. +description: Build, register, and run your own Prisma Next middleware, step by step, starting with a query logger. url: /orm/next/middleware/authoring-custom-middleware metaTitle: Authoring custom middleware in Prisma Next -metaDescription: Write a custom Prisma Next middleware step by step. Build a query logger, register it, see its output, and learn the full hook surface. +metaDescription: Write a custom Prisma Next middleware step by step. Build a query logger, register it, run it, and learn the full hook surface. badge: early-access --- ## Introduction -In this guide, you build your own middleware from scratch: a query logger that prints every query with its row count and latency. You will create the middleware file, register it, run a query, and see the output. At the end you will know the full hook surface well enough to write any middleware. +In this guide, you build your own middleware from scratch: a query logger that prints every query with its row count and latency. You create one file, register it in one place, run a query, and see the output. A custom middleware is a plain object with a `name` and the hooks you implement. There is no base class and no registration API beyond the `middleware` array you already know from [How middleware works](/orm/next/middleware/how-middleware-works). +This entire guide was run end to end on a fresh `create-prisma` project against a live Prisma Postgres database; the outputs shown are from that run. + ## Prerequisites -- A Prisma Next project with a working `src/prisma/db.ts`. The [PostgreSQL quickstart](/next/quickstart/postgresql) sets one up in a few minutes. -- At least one model with a few rows, so the logger has something to log. +- A Prisma Next project with a working `src/prisma/db.ts`. The [PostgreSQL quickstart](/next/quickstart/postgresql) sets one up in a few minutes: + +```bash +npx create-prisma@next --provider postgres +cd my-app +npm run db:init +``` ## 1. Create the middleware @@ -39,17 +46,17 @@ export function queryLogger(): SqlMiddleware { } ``` -Typing the object as `SqlMiddleware` gives you inferred parameter types on every hook, so you rarely import the plan or result types yourself. `familyId: "sql"` says this middleware is for SQL databases; the [family section below](#which-family-to-declare) explains when to set it. +Typing the object as `SqlMiddleware` gives you inferred parameter types on every hook, so you rarely import the plan or result types yourself. `familyId: "sql"` says this middleware is for SQL databases; [step 5](#5-know-which-family-to-declare) explains when to set it. ## 2. Register it -Add the middleware to the `middleware` array in your client setup: +Add the middleware to the `middleware` array in your client setup. This is the complete file after the change: ```ts title="src/prisma/db.ts" import postgres from '@prisma-next/postgres/runtime'; +import { queryLogger } from './query-logger'; import type { Contract } from './contract.d'; import contractJson from './contract.json' with { type: 'json' }; -import { queryLogger } from './query-logger'; export const db = postgres({ contractJson, @@ -60,21 +67,39 @@ export const db = postgres({ That is the whole wiring. Every query on this client now passes through the logger, whether it comes from the ORM API or the SQL query builder. -## 3. Run a query and check the output +## 3. Run a query and see the output -Run any query in your app, for example: +Put a small script in `src/index.ts` that writes and reads: + +```ts title="src/index.ts" +import { db } from "./prisma/db"; + +await db.orm.public.User.create({ + email: `mia+${Date.now()}@prisma.io`, + name: "Mia", +}); -```ts const users = await db.orm.public.User.select("id", "email").take(5).all(); +console.log(`fetched ${users.length} users`); + +await db.close(); ``` -Your terminal shows one line per query: +Run it: + +```bash +npm run dev +``` + +The logger reports both queries, with the SQL the runtime actually executed: ```text no-copy -[query-logger] 2 rows in 18ms · SELECT "user"."id" AS "id", "user"."email" AS "email" FROM "public"."user" LIMIT 5 +[query-logger] 1 rows in 18ms · INSERT INTO "public"."user" ("email", "name", "updatedAt") VALUES ($1, $2, $3) RETURNING "user"."createdAt", "user"."email", "user"."id", "user"."name", "user"."updatedAt", "user"."username" +[query-logger] 1 rows in 17ms · SELECT "user"."id" AS "id", "user"."email" AS "email" FROM "public"."user" LIMIT 5 +fetched 1 users ``` -If you see no output, check that the middleware is in the `middleware` array of the client your query actually uses, and that the query ran (an error before execution skips `afterExecute` only if the driver never started; failed driver calls still log, with `result.completed` set to `false`). +If you see the query result but no `[query-logger]` lines, the query ran on a client that does not have the middleware registered; check that your script imports `db` from the file you edited in step 2. ## 4. Add an option @@ -105,14 +130,22 @@ export function queryLogger(options?: QueryLoggerOptions): SqlMiddleware { } ``` -Register it with a threshold and fast queries go quiet: +Register it with a threshold: ```ts title="src/prisma/db.ts" middleware: [queryLogger({ thresholdMs: 250 })], ``` +Run the script again and the logger goes quiet, because both queries finish in under 250ms. Drop the threshold back to see them again. + One placement rule worth knowing: hooks run in registration order, and a middleware that throws stops the ones after it. Register your logger before middleware that can throw, such as `budgets`, so the queries you most want to see still get logged. +## 5. Know which family to declare + +Prisma Next groups databases into families: SQL (PostgreSQL) and document (MongoDB). Set `familyId: 'sql'` when your middleware touches anything SQL-shaped, like `plan.sql` in the logger above. The runtime then rejects it on MongoDB at startup with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, instead of failing at query time. + +Leave `familyId` out only when the middleware sticks to family-neutral surfaces (`intercept`, `onRow`, `afterExecute` on the shared result shape). Such a middleware runs on PostgreSQL and MongoDB alike; the [cache](/orm/next/middleware/built-in-cache) works this way. + You have a working, configurable middleware. The rest of this page is the surface you reach for as your middleware grows. ## The hooks @@ -127,7 +160,7 @@ You have a working, configurable middleware. The rest of this page is the surfac | `onRow(row, plan, ctx)` | Once per streamed row | Count or sample rows; throw to abort | | `afterExecute(plan, result, ctx)` | After the query finishes | Log timing, row count, and outcome | -[How middleware works](/orm/next/middleware/how-middleware-works#the-five-hooks) walks the full lifecycle with an animation. Two hooks deserve a closer look here, because they change the query rather than observe it. +[How middleware works](/orm/next/middleware/how-middleware-works#the-five-hooks) walks the order with an animation. Two hooks deserve a closer look here, because they change the query rather than observe it. ### Rewrite queries with beforeCompile @@ -175,12 +208,6 @@ Every hook receives a context (`ctx`) as its last argument: One honest note on `ctx.log`: the `postgres(...)` client does not expose a way to attach a log sink yet, so events sent to `ctx.log` are not printed anywhere by default. For output you can see today, log directly to your own logger, as the query logger above does with `console.log`. -## Which family to declare - -Set `familyId: 'sql'` when your middleware touches anything SQL-shaped: `plan.sql`, the query AST in `beforeCompile`, SQL parameter handling. It will register on PostgreSQL and be rejected on MongoDB at startup, with `RUNTIME.MIDDLEWARE_FAMILY_MISMATCH`, rather than failing at query time. - -Leave `familyId` out only when the middleware sticks to family-neutral surfaces (`intercept`, `onRow`, `afterExecute` on the shared result shape). Such a middleware runs on PostgreSQL and MongoDB alike; the cache works this way. - ## Common gotchas :::warning diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 1575c77639..172986c135 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -33,16 +33,18 @@ export const db = postgres({ Every query passes through the chain, whether it comes from the ORM API or the SQL query builder, because both execute through the same runtime. On MongoDB, pass the same option to `mongo(...)` from `@prisma-next/mongo/runtime`. -The animation below follows one query through the chain, including the cache hit that skips the database: +The whole model fits in one picture: your app talks to the database, and middleware sits in the middle. It can let a query pass, block it, or answer it itself: ## The five hooks -A middleware implements only the hooks it needs. The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. Step through the lifecycle to see where each hook sits: +A middleware implements only the hooks it needs. There are five, and they run in a fixed order: +The runtime calls each hook on every registered middleware, in the order they appear in the `middleware` array. What each hook is for: + ### beforeCompile: rewrite the query Runs before the query is turned into SQL, while it is still a typed AST. Return a modified draft to rewrite the query, for example to add a tenant filter to every `SELECT`. This hook exists on SQL databases only, because it works on the SQL query AST. diff --git a/apps/docs/src/components/concept-animation/flow-presets.ts b/apps/docs/src/components/concept-animation/flow-presets.ts index 8ff0a6ff9b..2543ddf845 100644 --- a/apps/docs/src/components/concept-animation/flow-presets.ts +++ b/apps/docs/src/components/concept-animation/flow-presets.ts @@ -1,4 +1,3 @@ -import type { ConceptName } from "./presets"; /** * A flow scene is a fixed box-and-arrow diagram drawn in a viewBox. Every node @@ -524,87 +523,55 @@ const githubConnection: FlowScene = { // The app and database boxes span both lanes so the return edge (and the // cache's short-circuit) can run through the clear band under the chain. const middlewarePipeline: FlowScene = { - label: "How a query moves through the middleware chain", - width: 712, - height: 230, - groupLabels: [{ text: "Middleware, in registration order", x: 160, y: 46 }], + label: "How a query moves through middleware", + width: 660, + height: 220, nodes: [ { id: "app", label: "Your app", - sub: "ORM · SQL builder", - subBelow: true, variant: "neutral", - x: 16, - y: 64, - w: 108, + x: 24, + y: 48, + w: 150, h: 124, }, { - id: "cache", - label: "cache", - sub: "serves repeated reads", + id: "mw", + label: "Middleware", + sub: "cache · lints · budgets", variant: "source", - x: 160, - y: 94, - w: 130, - h: 64, - }, - { - id: "lints", - label: "lints", - sub: "blocks risky shapes", - variant: "scope", - x: 322, - y: 94, - w: 116, - h: 64, - }, - { - id: "budgets", - label: "budgets", - sub: "caps query cost", - variant: "production", - x: 470, - y: 94, - w: 102, + x: 240, + y: 78, + w: 190, h: 64, }, { id: "db", label: "Database", - sub: "the driver", - subBelow: true, variant: "project", - x: 606, - y: 64, - w: 90, + x: 496, + y: 48, + w: 140, h: 124, }, ], edges: [ - // Request lane: every box's vertical center sits on y=126, so the four - // hops are one straight line with no offsets. - { id: "e-in", from: "app", fromSide: "r", to: "cache", toSide: "l" }, - { id: "e-c-l", from: "cache", fromSide: "r", to: "lints", toSide: "l" }, - { id: "e-l-b", from: "lints", fromSide: "r", to: "budgets", toSide: "l" }, - { id: "e-b-db", from: "budgets", fromSide: "r", to: "db", toSide: "l" }, - // Return lane: runs through the clear band under the middleware row, - // inside the taller endpoint boxes. - { - id: "e-return", + { id: "fwd1", from: "app", fromSide: "r", to: "mw", toSide: "l" }, + { id: "fwd2", from: "mw", fromSide: "r", to: "db", toSide: "l" }, + { + id: "ret", from: "db", fromSide: "l", to: "app", toSide: "r", fromDy: 44, toDy: 44, - label: "rows · onRow · afterExecute", + label: "results", }, - // Cache hit: intercept answers from the chain, driver never runs. { - id: "e-hit", - from: "cache", + id: "hit", + from: "mw", fromSide: "b", to: "app", toSide: "r", @@ -615,172 +582,171 @@ const middlewarePipeline: FlowScene = { ], steps: [ { - title: "1. One chain", + title: "1. In the middle", + caption: "Every query passes through your middleware on its way to the database.", + nodes: ["app", "mw", "db"], + edges: ["fwd1", "fwd2"], + emphasize: ["mw"], + }, + { + title: "2. Block", caption: - "Every query, whether it comes from the ORM API or the SQL query builder, runs through the same middleware array on its way to the driver. The chain runs in registration order: first in the array, first at every hook.", - nodes: ["app", "cache", "lints", "budgets", "db"], - edges: ["e-in", "e-c-l", "e-l-b", "e-b-db"], - emphasize: ["cache", "lints", "budgets"], + "Middleware can stop a query before it reaches the database. This is how lints blocks a DELETE without WHERE.", + nodes: ["app", "mw", "db"], + edges: ["fwd1"], + emphasize: ["mw"], }, { - title: "2. Before the driver", + title: "3. Answer", caption: - "Before anything reaches the database, beforeCompile can rewrite the query and beforeExecute can validate it, mutate parameters, or throw to block it. This is where lints stops a DELETE without WHERE and budgets rejects an unbounded SELECT.", - nodes: ["app", "cache", "lints", "budgets", "db"], - edges: ["e-in", "e-c-l", "e-l-b", "e-b-db"], - emphasize: ["lints", "budgets"], + "Middleware can answer a query itself. On a cache hit, the database is skipped entirely.", + nodes: ["app", "mw", "db"], + edges: ["fwd1", "hit"], + emphasize: ["mw"], }, { - title: "3. Cache answers early", + title: "4. Observe", caption: - "A middleware can answer a query itself: on a hit, the cache returns rows from its intercept hook and the driver never runs. afterExecute still fires with source set to middleware, so logging and metrics see the read.", - nodes: ["app", "cache", "lints", "budgets", "db"], - edges: ["e-in", "e-hit"], - emphasize: ["cache"], + "Results flow back through the middleware, which sees every row and the final timing.", + nodes: ["app", "mw", "db"], + edges: ["fwd1", "fwd2", "ret"], + emphasize: ["app"], }, + ], +}; + +// Five hooks in execution order; the driver runs between intercept and onRow. +const middlewareLifecycle: FlowScene = { + label: "The five hooks, in the order they run", + width: 700, + height: 150, + nodes: [ + { id: "bc", label: "beforeCompile", sub: "rewrite", variant: "source", x: 16, y: 40, w: 128, h: 64 }, + { id: "be", label: "beforeExecute", sub: "guard", variant: "scope", x: 156, y: 40, w: 128, h: 64 }, + { id: "ic", label: "intercept", sub: "answer early", variant: "production", x: 296, y: 40, w: 122, h: 64 }, + { id: "or", label: "onRow", sub: "each row", variant: "vars", x: 470, y: 40, w: 100, h: 64 }, + { id: "ae", label: "afterExecute", sub: "observe", variant: "branch", x: 582, y: 40, w: 112, h: 64 }, + ], + edges: [ + { id: "e1", from: "bc", fromSide: "r", to: "be", toSide: "l" }, + { id: "e2", from: "be", fromSide: "r", to: "ic", toSide: "l" }, + { id: "e3", from: "ic", fromSide: "r", to: "or", toSide: "l", label: "driver" }, + { id: "e4", from: "or", fromSide: "r", to: "ae", toSide: "l" }, + ], + steps: [ { - title: "4. Rows stream back", + title: "1. Rewrite", + caption: "beforeCompile can change the query while it is still a typed AST.", + nodes: ["bc", "be", "ic", "or", "ae"], + edges: ["e1", "e2", "e3", "e4"], + emphasize: ["bc"], + }, + { + title: "2. Guard", + caption: "beforeExecute sees the final SQL. Throw here to block the query.", + nodes: ["bc", "be", "ic", "or", "ae"], + edges: ["e1", "e2", "e3", "e4"], + emphasize: ["be"], + }, + { + title: "3. Answer early", + caption: "intercept can return rows itself; if it does, the driver never runs.", + nodes: ["bc", "be", "ic", "or", "ae"], + edges: ["e1", "e2", "e3", "e4"], + emphasize: ["ic"], + }, + { + title: "4. Watch and close", caption: - "On the way back, onRow fires once per row as the driver streams, and afterExecute closes the call with the observed row count and latency. That is where budgets checks its latency ceiling and a custom slow-query middleware logs its warning.", - nodes: ["app", "cache", "lints", "budgets", "db"], - edges: ["e-in", "e-c-l", "e-l-b", "e-b-db", "e-return"], - emphasize: ["app", "db"], + "The driver runs, onRow fires per streamed row, and afterExecute closes with row count and latency.", + nodes: ["bc", "be", "ic", "or", "ae"], + edges: ["e1", "e2", "e3", "e4"], + emphasize: ["or", "ae"], }, ], }; // One extension package, registered on two planes, converging on the database. const extensionPlanes: FlowScene = { - label: "How one extension package plugs into a project", - width: 712, - height: 330, - groupLabels: [ - { text: "One package", x: 16, y: 22 }, - { text: "Two registrations", x: 270, y: 22 }, - { text: "One database", x: 560, y: 22 }, - ], + label: "One extension package, two registrations, one database", + width: 680, + height: 250, nodes: [ { id: "pkg", label: "pgvector", - sub: "@prisma-next/extension-pgvector", + sub: "one npm package", variant: "neutral", x: 16, - y: 126, - w: 190, - h: 88, + y: 93, + w: 170, + h: 64, }, { - id: "control", + id: "cfg", label: "prisma-next.config.ts", - sub: "extensions: [pgvector]", - subBelow: true, + sub: "migrations", variant: "source", - x: 270, - y: 44, - w: 224, - h: 100, - rows: [ - { key: "schema type", value: "Vector(n)", origin: "preview" }, - { key: "migration", value: "CREATE EXTENSION", origin: "preview" }, - ], + x: 260, + y: 24, + w: 200, + h: 64, }, { - id: "runtime", + id: "rt", label: "db.ts", - sub: "extensions: [pgvector]", - subBelow: true, - variant: "production", - x: 270, - y: 196, - w: 224, - h: 100, - rows: [ - { key: "query ops", value: "cosineDistance(…)", origin: "production" }, - { key: "codecs", value: "vector ↔ number[]", origin: "production" }, - ], + sub: "queries", + variant: "scope", + x: 260, + y: 162, + w: 200, + h: 64, }, { - id: "db", + id: "pg", label: "PostgreSQL", - sub: "pgvector installed", variant: "project", - x: 560, - y: 120, - w: 136, - h: 100, + x: 524, + y: 93, + w: 140, + h: 64, }, ], edges: [ - { - id: "e-control", - from: "pkg", - fromSide: "r", - to: "control", - toSide: "l", - fromDy: -14, - label: "/control", - }, - { - id: "e-runtime", - from: "pkg", - fromSide: "r", - to: "runtime", - toSide: "l", - fromDy: 14, - label: "/runtime", - }, - { - id: "e-migrate", - from: "control", - fromSide: "r", - to: "db", - toSide: "l", - toDy: -26, - label: "db init", - }, - { - id: "e-query", - from: "runtime", - fromSide: "r", - to: "db", - toSide: "l", - toDy: 26, - label: "queries", - }, + { id: "p-cfg", from: "pkg", fromSide: "r", to: "cfg", toSide: "l" }, + { id: "p-rt", from: "pkg", fromSide: "r", to: "rt", toSide: "l" }, + { id: "cfg-pg", from: "cfg", fromSide: "r", to: "pg", toSide: "l", label: "db init" }, + { id: "rt-pg", from: "rt", fromSide: "r", to: "pg", toSide: "l", label: "queries" }, ], steps: [ { - title: "1. One package", - caption: - "An extension ships as one npm package with two entry points: a control export used at build time and a runtime export used when your app runs. You install it once.", + title: "1. Install", + caption: "One package brings everything the extension needs.", nodes: ["pkg"], edges: [], emphasize: ["pkg"], }, { - title: "2. Control plane", + title: "2. Config side", caption: - "prisma-next.config.ts registers the control export. That is the side contract emission and migrations use: the pack contributes schema types like Vector(n) and ships the baseline migration that installs the database feature.", - nodes: ["pkg", "control"], - edges: ["e-control"], - emphasize: ["control"], + "Registered in the config, the extension adds its schema types and its migration (CREATE EXTENSION).", + nodes: ["pkg", "cfg"], + edges: ["p-cfg"], + emphasize: ["cfg"], }, { - title: "3. Runtime plane", - caption: - "db.ts registers the runtime export on the client. That side contributes the codecs that encode and decode vector values, and the query operations your code calls, like cosineDistance. If a required pack is missing here, the client fails at construction.", - nodes: ["pkg", "control", "runtime"], - edges: ["e-control", "e-runtime"], - emphasize: ["runtime"], + title: "3. Client side", + caption: "Registered in db.ts, it adds the query operations your code calls.", + nodes: ["pkg", "cfg", "rt"], + edges: ["p-cfg", "p-rt"], + emphasize: ["rt"], }, { - title: "4. Meet at the database", - caption: - "db init applies the pack's baseline migration, CREATE EXTENSION IF NOT EXISTS vector, and your queries run against a database that provably has the feature the contract relies on.", - nodes: ["pkg", "control", "runtime", "db"], - edges: ["e-control", "e-runtime", "e-migrate", "e-query"], - emphasize: ["db"], + title: "4. The database", + caption: "db init installs the feature; your queries then use it.", + nodes: ["pkg", "cfg", "rt", "pg"], + edges: ["p-cfg", "p-rt", "cfg-pg", "rt-pg"], + emphasize: ["pg"], }, ], }; @@ -794,5 +760,8 @@ export const FLOW_SCENES = { "env-layers": envLayers, "github-connection": githubConnection, "middleware-pipeline": middlewarePipeline, + "middleware-lifecycle": middlewareLifecycle, "extension-planes": extensionPlanes, -} satisfies Partial>; +} satisfies Record; + +export type FlowName = keyof typeof FLOW_SCENES; diff --git a/apps/docs/src/components/concept-animation/index.tsx b/apps/docs/src/components/concept-animation/index.tsx index 8a27e67ada..71802383d8 100644 --- a/apps/docs/src/components/concept-animation/index.tsx +++ b/apps/docs/src/components/concept-animation/index.tsx @@ -1,4 +1,4 @@ -import { FLOW_SCENES } from "./flow-presets"; +import { FLOW_SCENES, type FlowName } from "./flow-presets"; import { FlowPlayer } from "./flow"; import { ConceptPlayer } from "./player"; import { CONCEPT_PRESETS, type ConceptName, parseStepTokens } from "./presets"; @@ -12,11 +12,11 @@ import { CONCEPT_PRESETS, type ConceptName, parseStepTokens } from "./presets"; * other name falls back to the Code Hike token animation in presets.ts. Either * way the surrounding layout never shifts as you step through. */ -export function ConceptAnimation({ name }: { name: ConceptName }) { - const scene = (FLOW_SCENES as Partial>)[name]; +export function ConceptAnimation({ name }: { name: ConceptName | FlowName }) { + const scene = (FLOW_SCENES as Partial>)[name]; if (scene) return ; - const preset = CONCEPT_PRESETS[name]; + const preset = (CONCEPT_PRESETS as Partial>)[name]; if (!preset) throw new Error(`Unknown concept animation: ${String(name)}`); const steps = preset.steps.map((step) => ({ ...parseStepTokens(step.code), diff --git a/apps/docs/src/components/concept-animation/presets.ts b/apps/docs/src/components/concept-animation/presets.ts index 9601de7bd0..47c21f9dc4 100644 --- a/apps/docs/src/components/concept-animation/presets.ts +++ b/apps/docs/src/components/concept-animation/presets.ts @@ -21,67 +21,6 @@ export interface ConceptPreset { } export const CONCEPT_PRESETS = { - "middleware-lifecycle": { - label: "The middleware lifecycle, hook by hook", - steps: [ - { - title: "1. Rewrite", - code: - "[[beforeCompile]] your hook rewrite the query AST\n" + - "SQL lowering runtime render the SQL\n" + - "beforeExecute your hook validate · block · tweak params\n" + - "param encoding runtime bind parameter values\n" + - "intercept your hook answer without the driver?\n" + - "driver database run the statement\n" + - "onRow your hook once per streamed row\n" + - "afterExecute your hook latency · row count · source", - caption: - "A query starts as a typed AST. beforeCompile sees it first and can return a rewritten draft: add a tenant filter, inject a soft-delete predicate. Then the runtime lowers the AST to SQL text.", - }, - { - title: "2. Guard", - code: - "beforeCompile your hook rewrite the query AST\n" + - "SQL lowering runtime render the SQL\n" + - "[[beforeExecute]] your hook validate · block · tweak params\n" + - "param encoding runtime bind parameter values\n" + - "intercept your hook answer without the driver?\n" + - "driver database run the statement\n" + - "onRow your hook once per streamed row\n" + - "afterExecute your hook latency · row count · source", - caption: - "beforeExecute sees the rendered statement before anything reaches the database. Throw here to block the query (this is how lints stops a DELETE without WHERE), or adjust parameter values before they are encoded.", - }, - { - title: "3. Short-circuit", - code: - "beforeCompile your hook rewrite the query AST\n" + - "SQL lowering runtime render the SQL\n" + - "beforeExecute your hook validate · block · tweak params\n" + - "param encoding runtime bind parameter values\n" + - "[[intercept]] your hook answer without the driver?\n" + - "driver database [[skipped on a cache hit]]\n" + - "onRow your hook once per streamed row\n" + - "afterExecute your hook latency · row count · source", - caption: - "intercept runs last before the driver. Return rows from it and the driver never runs: this is how the cache serves repeated reads. Return nothing and the query continues to the database.", - }, - { - title: "4. Observe", - code: - "beforeCompile your hook rewrite the query AST\n" + - "SQL lowering runtime render the SQL\n" + - "beforeExecute your hook validate · block · tweak params\n" + - "param encoding runtime bind parameter values\n" + - "intercept your hook answer without the driver?\n" + - "driver database run the statement\n" + - "[[onRow]] your hook once per streamed row\n" + - "[[afterExecute]] your hook latency · row count · source", - caption: - "On the way back, onRow fires once per row as results stream, and afterExecute closes the call with the row count, the latency, and whether the driver or a middleware answered. Log it, meter it, or enforce a budget.", - }, - ], - }, "compute-model": { label: "How Compute organizes resources and isolates branches", steps: [ From cf11d91d394fd310dccf44c62ae0d211fc161848 Mon Sep 17 00:00:00 2001 From: Ankur Datta <64993082+ankur-arch@users.noreply.github.com> Date: Tue, 7 Jul 2026 12:50:25 +0200 Subject: [PATCH 11/11] docs(middleware): cross-link the Fundamentals section now that it is merged Co-Authored-By: Claude Fable 5 --- apps/docs/content/docs/orm/next/extensions/using-extensions.mdx | 1 + .../content/docs/orm/next/middleware/how-middleware-works.mdx | 1 + 2 files changed, 2 insertions(+) diff --git a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx index 5428a0a822..906e0ee094 100644 --- a/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx +++ b/apps/docs/content/docs/orm/next/extensions/using-extensions.mdx @@ -117,6 +117,7 @@ For a working project per extension, see the runnable examples: [pgvector](https ## See also +- [Advanced queries](/orm/next/fundamentals/advanced-queries): the SQL query builder, where extension operations like `cosineDistance` appear - [How middleware works](/orm/next/middleware/how-middleware-works) for wrapping queries rather than adding database capabilities - [Quickstart with PostgreSQL](/next/quickstart/postgresql) to set up a project to add extensions to - [Prisma Next overview](/orm/next) for the contract-first model extensions plug into diff --git a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx index 172986c135..6f1e5e9831 100644 --- a/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx +++ b/apps/docs/content/docs/orm/next/middleware/how-middleware-works.mdx @@ -116,5 +116,6 @@ Projects scaffolded with `create-prisma` install Prisma Next skills for your cod ## See also - [Authoring custom middleware](/orm/next/middleware/authoring-custom-middleware): build and test a query logger step by step +- [Writing data](/orm/next/fundamentals/writing-data): the mutations that lints and budgets guard - [Built-in: budgets](/orm/next/middleware/built-in-budgets), [Built-in: lints](/orm/next/middleware/built-in-lints), [Built-in: cache](/orm/next/middleware/built-in-cache) - [Using extensions](/orm/next/extensions/using-extensions) for adding database capabilities rather than wrapping queries