diff --git a/apps/docs/content/docs/cli/error-reference.mdx b/apps/docs/content/docs/cli/error-reference.mdx index bc2a7d05c79..aed4ce7475e 100644 --- a/apps/docs/content/docs/cli/error-reference.mdx +++ b/apps/docs/content/docs/cli/error-reference.mdx @@ -1,38 +1,38 @@ --- title: Error reference -description: Every structured error code the unified Prisma CLI can emit, by namespace, with the condition that raises it. +description: The structured error codes of the Prisma CLI platform commands, by namespace, with the condition that raises each one. url: /cli/error-reference metaTitle: Error reference | Prisma CLI -metaDescription: Every structured error code the unified Prisma CLI can emit, by namespace, with the condition that raises it. +metaDescription: The structured error codes of the Prisma CLI platform commands, by namespace, with the condition that raises each one. --- {/* Generated by scripts/generate-error-reference.mjs from https://github.com/prisma/prisma-cli/blob/main/docs/reference/error-reference.md - Do not edit by hand — changes are overwritten by the sync workflow. */} + Do not edit by hand. The sync workflow overwrites changes. */} -Every user-facing error the unified Prisma CLI emits is a structured envelope identified by a dotted `NAMESPACE.SUBCODE` code (see [Error Conventions](https://github.com/prisma/prisma-cli/blob/main/docs/product/error-conventions.md) and [ADR 0003](https://github.com/prisma/prisma-cli/blob/main/docs/architecture/adrs/0003-structured-output-and-errors.md)). This page lists every published code. Each code anchors as `#` — the fragment an emitted error's `docsUrl` resolves to. This page is generated from the canonical registry in the `prisma/prisma-cli` repository, whose CI requires every code in production source to be documented before it ships. +Every user-facing error the unified Prisma CLI emits is a structured envelope identified by a dotted `NAMESPACE.SUBCODE` code (see [Error Conventions](https://github.com/prisma/prisma-cli/blob/main/docs/product/error-conventions.md) and [ADR 0003](https://github.com/prisma/prisma-cli/blob/main/docs/architecture/adrs/0003-structured-output-and-errors.md)). This page lists the codes from every `prisma` command except the Prisma ORM ones. The codes from `prisma contract`, `prisma db`, `prisma migration`, and `prisma orm`, and the codes your app can raise at runtime, are on the [Prisma ORM error reference](/orm/reference/error-reference). When an error prints a `docsUrl`, that link opens the code's entry on this page. This page is generated from the `prisma/prisma-cli` repository. + +One exception: the `CLI.*` codes on this page come from the command-line layer that every `prisma` command runs through, for its arguments, for loading `prisma.config.ts`, for prompts, and for consent. They are on this page even when you ran a Prisma ORM command. `CLI.CONFIG_SECTION_INVALID` from `prisma contract emit` is one of them. The Prisma ORM commands also raise `CLI.*` codes of their own, and those are on the [Prisma ORM error reference](/orm/reference/error-reference). If a `CLI.*` code is not on this page, it is on the other. Recognize an error programmatically by running with `--json` and matching on `error.code` in the emitted envelope — never on message text. Envelopes carry `message`, and optionally `why`, `fix`, `where`, `meta`, `cause`, `nextActions`, and `docsUrl`. Most codes on this page are expected failures. Some are warn-severity diagnostics that ride a successful run: the command completes and exits `0`, and the diagnostic carries the code (the entry says so where it applies). The process exit-code contract is in [Error Conventions](https://github.com/prisma/prisma-cli/blob/main/docs/product/error-conventions.md). -Every code on this page is assigned where the error is raised, and this page lists every code the CLI can emit. No boundary invents a code from a server response or rewrites one on the way out, so an error's code never depends on which command you reached the failure through. When a REST API request fails, the CLI raises the domain's registered `*.API_ERROR` code and carries the API's own code and HTTP status in `meta.apiCode` and `meta.status`, where they are data you can read rather than a code you have to guess at. - -Namespaces: - -| Namespace | Covers | -| --- | --- | -| `AUTH` | Workspace authentication and sessions (`prisma auth`, credential resolution) | -| `BRANCH` | Branch listing (`prisma branch`) | -| `BUCKET` | Bucket and bucket-key management (`prisma bucket`) | -| `CLI` | Engine-level invocation: arguments, config loading, prompts, consent, credentials, child processes | -| `FEEDBACK` | Sending product feedback (`prisma feedback`) | -| `GIT` | Git repository connections (`prisma git`) | -| `INIT` | Project initialization diagnostics (`prisma init`) | -| `POSTGRES` | Database management (`prisma postgres`) | -| `PROJECT` | Project and environment management (`prisma project`) | -| `SERVICE` | Deployed service management (`prisma service`) | -| `SKILLS` | Agent-skill delivery (`prisma skills`) | +Every code on this page is assigned where the error is raised. No boundary invents a code from a server response or rewrites one on the way out, so an error's code never depends on which command you reached the failure through. When a REST API request fails, the CLI raises the domain's registered `*.API_ERROR` code and carries the API's own code and HTTP status in `meta.apiCode` and `meta.status`, where they are data you can read rather than a code you have to guess at. + +Namespaces on this page: + +- [`AUTH`](#auth): Workspace authentication and sessions (`prisma auth`, credential resolution) +- [`BRANCH`](#branch): Branch listing (`prisma branch`) +- [`BUCKET`](#bucket): Bucket and bucket-key management (`prisma bucket`) +- [`CLI`](#cli): Engine-level invocation: arguments, config loading, prompts, consent, credentials, child processes +- [`FEEDBACK`](#feedback): Sending product feedback (`prisma feedback`) +- [`GIT`](#git): Git repository connections (`prisma git`) +- [`INIT`](#init): Project initialization diagnostics (`prisma init`) +- [`POSTGRES`](#postgres): Database management (`prisma postgres`) +- [`PROJECT`](#project): Project and environment management (`prisma project`) +- [`SERVICE`](#service): Deployed service management (`prisma service`) +- [`SKILLS`](#skills): Agent-skill delivery (`prisma skills`) ## AUTH diff --git a/apps/docs/content/docs/orm/reference/error-reference.mdx b/apps/docs/content/docs/orm/reference/error-reference.mdx index 62fcc305549..a1882e7dd5c 100644 --- a/apps/docs/content/docs/orm/reference/error-reference.mdx +++ b/apps/docs/content/docs/orm/reference/error-reference.mdx @@ -1,16 +1,18 @@ --- title: Error reference -description: Every structured error code Prisma ORM can emit, by namespace, with the condition that raises it. +description: The structured error codes of the Prisma ORM commands, runtime, and extensions, by namespace, with the condition that raises each one. url: /orm/reference/error-reference metaTitle: Prisma ORM error reference -metaDescription: Every structured error code Prisma ORM can emit, by namespace, with the condition that raises it. +metaDescription: The structured error codes of the Prisma ORM commands, runtime, and extensions, by namespace, with the condition that raises each one. --- {/* Generated by scripts/generate-error-reference.mjs from https://github.com/prisma/orm/blob/main/docs/reference/error-reference.md - Do not edit by hand — changes are overwritten by the sync workflow. */} + Do not edit by hand. The sync workflow overwrites changes. */} -Every user-facing Prisma ORM error is a structured envelope identified by a dotted `NAMESPACE.SUBCODE` code (see [ADR 239](https://github.com/prisma/orm/blob/main/docs/architecture%20docs/adrs/ADR%20239%20-%20Errors%20are%20structural%20envelopes%20with%20dotted%20namespace%20codes.md) and [Error Handling](https://github.com/prisma/orm/blob/main/docs/Error%20Handling.md)). This page lists every published code. Each code anchors as `#` — the exact fragment every emitted error carries in its `docsUrl`. This page is generated from the canonical reference in the `prisma/orm` repository, whose CI requires every code in production source to be documented before it ships. +Every user-facing Prisma ORM error is a structured envelope identified by a dotted `NAMESPACE.SUBCODE` code (see [ADR 239](https://github.com/prisma/orm/blob/main/docs/architecture%20docs/adrs/ADR%20239%20-%20Errors%20are%20structural%20envelopes%20with%20dotted%20namespace%20codes.md) and [Error Handling](https://github.com/prisma/orm/blob/main/docs/Error%20Handling.md)). This page lists the codes from the Prisma ORM commands, which start with `prisma contract`, `prisma db`, `prisma migration`, and `prisma orm`, and the codes your app can raise at runtime, including from extensions. Every other `prisma` command has its codes on the [CLI error reference](/cli/error-reference). When an error prints a `docsUrl`, that link opens the code's entry on this page. This page is generated from the `prisma/orm` repository. + +One exception: some `CLI.*` codes come from the command-line layer that every `prisma` command runs through, for its arguments, for loading `prisma.config.ts`, for prompts, and for consent. Those are on the [CLI error reference](/cli/error-reference) even when you ran a Prisma ORM command. `CLI.CONFIG_SECTION_INVALID` from `prisma contract emit` is one of them. The `CLI.*` codes on this page are the ones the Prisma ORM commands raise themselves. If a `CLI.*` code is not on this page, it is on the other. Recognize an error programmatically with `isStructuredError` from your facade package's `utils/structured-error` subpath (for example `@prisma/orm-postgres/utils/structured-error`) and match on `error.code`, never `instanceof`. Envelopes carry `message`, and optionally `why`, `fix`, `where`, `cause`, `docsUrl`, and the structured context each entry below lists as its **Payload**. The payload arrives on `error.meta` when the envelope was built by `structuredError` and on `error.details` when it was built by `runtimeError`; a few codes are raised both ways, so read whichever property the envelope carries. @@ -22,24 +24,23 @@ A command may also **complete with findings**: it ran to its end and has a resul Codes that predate the dotted scheme were renamed at 0.16; the full old→new crosswalk (`PN-DOMAIN-NNNN` → `NAMESPACE.SUBCODE`) is in [ADR 239](https://github.com/prisma/orm/blob/main/docs/architecture%20docs/adrs/ADR%20239%20-%20Errors%20are%20structural%20envelopes%20with%20dotted%20namespace%20codes.md). -Namespaces: - -| Namespace | Covers | -| --- | --- | -| `CONFIG` | Loading and validating `prisma.config.ts` | -| `CLI` | Command-line argument and invocation errors | -| `CONTRACT` | Contract authoring, emission, validation, and the contract↔database relationship (markers, schema verification) | -| `PSL` | The PSL source text itself (parse, format, interpret), including a Prisma ORM 7 schema read as the contract source | -| `ORM` | ORM client and query-builder DSL API misuse | -| `RUNTIME` | Query execution and runtime wiring | -| `DRIVER` | Database driver connection and protocol failures | -| `MIGRATION` | Migration authoring, planning, checking, and execution | -| `PLAN` | Query plan constraints | -| `BUDGET` | Query budget violations | -| `LINT` | Query lint findings | -| `PARADEDB` | ParadeDB extension: search-function arguments | -| `POSTGIS` | PostGIS extension: geometry construction and validation | -| `SUPABASE` | Supabase extension: config wiring and JWT handling | +Namespaces on this page: + +- [`CONFIG`](#config): Loading and validating `prisma.config.ts` +- [`CLI`](#cli): Command-line argument and invocation errors +- [`CONTRACT`](#contract): Contract authoring, emission, validation, and the contract↔database relationship (markers, schema verification) +- [`PSL`](#psl): The PSL source text itself (parse, format, interpret), including a Prisma ORM 7 schema read as the contract source +- [`ORM`](#orm): ORM client and query-builder DSL API misuse +- [`RUNTIME`](#runtime): Query execution and runtime wiring +- [`DRIVER`](#driver): Database driver connection and protocol failures +- [`MIGRATION`](#migration): Migration authoring, planning, checking, and execution +- [`PLAN`](#plan): Query plan constraints +- [`BUDGET`](#budget): Query budget violations +- [`LINT`](#lint): Query lint findings +- [`PARADEDB`](#paradedb): ParadeDB extension: search-function arguments +- [`POSTGIS`](#postgis): PostGIS extension: geometry construction and validation +- [`SUPABASE`](#supabase): Supabase extension: config wiring and JWT handling +- [`TESTKIT`](#testkit) ## CONFIG @@ -269,7 +270,7 @@ The control plane resolves a codec referenced by the contract (a `CodecRef.codec ### CONTRACT.CAST_REFUSED [#CONTRACT.CAST_REFUSED] -A value handed to a data type's cast, or to an authoring entry that reads written text, is not one that type takes: it is not in the shape the source type stores, its magnitude is outside the range the receiving type holds, or the text is not a boolean. Raised by a target's or extension's casts and authoring entries. A contract source reading a column default reports it to the author as the PSL diagnostic `PSL_INVALID_DEFAULT_LITERAL`. Payload: `why`, `fix`. +A value handed to a data type's cast, or to an authoring entry that reads written text, is not one that type takes: it is not in the shape the source type stores, its magnitude is outside the range the receiving type holds, the text is not a boolean, or the text is not a date or time the receiving type holds. A date or time text is refused when it has an offset its type does not hold or lacks one its type needs, has more fraction digits than its type holds (six, or three for `sqlite/datetime`, which holds milliseconds), names a date or time that does not exist, or is outside the range of years its type holds; the message shows text the type takes. The date and time codecs raise it from `encodeJson` for a value their type does not hold, such as a `Temporal` value with digits below one microsecond. Raised by a target's or extension's casts and authoring entries. A contract source reading a column default reports it to the author as the PSL diagnostic `PSL_INVALID_DEFAULT_LITERAL`. Payload: `why`, `fix`. ### CONTRACT.CHECK_NAME_RESERVED [#CONTRACT.CHECK_NAME_RESERVED] @@ -391,7 +392,7 @@ Two authoring entries claim the same written form — the same literal tag, or t ### CONTRACT.DEFAULT_INVALID [#CONTRACT.DEFAULT_INVALID] -A field's default declaration is invalid: `defaultSql` is used on an enum field, a field declares both `default` and `executionDefaults`, or a field is nullable while carrying `executionDefaults`. Raised while authoring/building a SQL contract. Payload: `modelName`, `fieldName`, `reason`. The SQL TypeScript builder also raises it when the column's codec refuses the value passed to `.default(value)`, because the value is not the codec's input type (for example a string on `field.dateTime()`, whose codec takes a `Temporal.Instant`); TypeScript reports the same mistake as a type error when the field comes from the `defineContract` factory; the message carries the codec's own message, the original error is the `cause`, and the meta is `modelName`, `fieldName`, `codecId`, `reason: 'codec-refused-default'` and, for a list column, the 1-based `elementPosition`. It raises it too when the build has a codec lookup and no pack in the contract declares the column's codec, so the default cannot be checked (`reason: 'codec-not-found'`; list the pack that owns the codec in `extensions`), and when a list field has a default that is not an array (`reason: 'list-default-not-array'`). The Mongo TypeScript builder raises it for execution defaults it cannot key to one collection field: on a variant model's field (`reason: 'executionDefaults-on-variant'`; declare the field on the base model, whose defaults apply to every variant), or with different phases on two models stored in the same collection (`reason: 'executionDefaults-conflict'`; identical ones are merged). The PSL interpreter reports the same cases as `PSL_PRESET_ON_VARIANT_FIELD` and `PSL_PRESET_CONFLICT`. Also raised by the Postgres adapter's DDL renderer when a hand-authored `col(...)` pairs an `autoincrement()` default with a type that isn't `SERIAL`/`BIGSERIAL`/`SMALLSERIAL` (or their `SERIAL4`/`SERIAL8`/`SERIAL2` aliases). Meta in that case: `nativeType`. Also raised by the TypeScript `sql` template tag when the body cannot be canonicalized, with the same message as the PSL diagnostics `PSL_TAGGED_LITERAL_NUL` and `PSL_TAGGED_LITERAL_TOO_LARGE` (meta: `reason`, `offset`) or is exactly `now()` or `autoincrement()` (`` Write .default(now()) instead of sql`now()`; now() is a Prisma default function, not raw SQL. ``; meta: `reason: 'reserved-function'`, `expression`), or fails the SQL body check (`Default SQL must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.`; meta: `reason: 'unsafe-sql'`, `expression`), and by both the Postgres and SQLite migration planners when a function default in the contract fails that same check at DDL time (meta: `expression`). +A field's default declaration is invalid: `defaultSql` is used on an enum field, a field declares both `default` and `executionDefaults`, or a field is nullable while carrying `executionDefaults`. Raised while authoring/building a SQL contract. Payload: `modelName`, `fieldName`, `reason`. The SQL TypeScript builder also raises it when the column's codec refuses the value passed to `.default(value)`, because the value is not the codec's input type (for example a string on `field.dateTime()`, whose codec takes a `Temporal.Instant`); TypeScript reports the same mistake as a type error when the field comes from the `defineContract` factory; the message carries the codec's own message, the original error is the `cause`, and the meta is `modelName`, `fieldName`, `codecId`, `reason: 'codec-refused-default'` and, for a list column, the 1-based `elementPosition`. It raises it too when the build has a codec lookup and no pack in the contract declares the column's codec, so the default cannot be checked (`reason: 'codec-not-found'`; list the pack that owns the codec in `extensions`), and when a list field has a default that is not an array (`reason: 'list-default-not-array'`). The Mongo TypeScript builder raises it for execution defaults it cannot key to one collection field: on a variant model's field (`reason: 'executionDefaults-on-variant'`; declare the field on the base model, whose defaults apply to every variant), or with different phases on two models stored in the same collection (`reason: 'executionDefaults-conflict'`; identical ones are merged). The PSL interpreter reports the same cases as `PSL_PRESET_ON_VARIANT_FIELD` and `PSL_PRESET_CONFLICT`. Also raised by the Postgres adapter's DDL renderer when a hand-authored `col(...)` pairs an `autoincrement()` default with a type that isn't `SERIAL`/`BIGSERIAL`/`SMALLSERIAL` (or their `SERIAL4`/`SERIAL8`/`SERIAL2` aliases). Meta in that case: `nativeType`. Also raised by the TypeScript `sql` template tag when the body cannot be canonicalized, with the same message as the PSL diagnostics `PSL_TAGGED_LITERAL_NUL` and `PSL_TAGGED_LITERAL_TOO_LARGE` (meta: `reason`, `offset`) or is exactly `now()` or `autoincrement()` (`` Write .default(now()) instead of sql`now()`; now() is a Prisma default function, not raw SQL. ``; meta: `reason: 'reserved-function'`, `expression`), or fails the SQL body check (`Default SQL must not contain semicolons, SQL comment tokens, dollar-quoting, or subqueries.`; meta: `reason: 'unsafe-sql'`, `expression`), and by both the Postgres and SQLite migration planners when a function default in the contract fails that same check at DDL time (meta: `expression`). Both migration planners also raise it for a literal default in the contract that its column's data type refuses, such as a date without an offset on a `timestamptz` column, which a contract emitted by an earlier version can hold; the message carries the type's refusal and says to re-emit the contract (meta: `reason: 'default-not-canonical'`, `column`). `db update`, `db init` and `migration plan` report the planner's error under this code. Schema verification reports the same refusal as the explanation of the default's mismatch, or of its absence when the database has no default. ### CONTRACT.DEFAULT_SQL_INTERPOLATION [#CONTRACT.DEFAULT_SQL_INTERPOLATION] @@ -1163,16 +1164,18 @@ A raw-SQL tagged template interpolated a JS value whose type cannot be auto-infe ### RUNTIME.TEMPORAL_UNAVAILABLE [#RUNTIME.TEMPORAL_UNAVAILABLE] -A value that only a global `Temporal` implementation can produce or read was needed in a runtime that has none. Two paths raise it, and they carry different metadata: +A value that only a `Temporal` implementation can produce or read was needed in a process that has neither a global `Temporal` nor the fallback `Temporal` of the Postgres target. Two paths raise it, and they carry different metadata: -- A Temporal-backed codec (`pg/date-temporal@1`, `pg/timestamp-temporal@1`, `pg/timestamptz-temporal@1`, `pg/time-temporal@1`) encoding or decoding a value. Payload: `codecId`, `operation` (`'encode'` or `'decode'`). -- The `instantNow` mutation-default generator producing a value, for `temporal.createdAt()`, `temporal.updatedAt()`, or for a `temporal.timestamptz(…)` / `timestamp(…)` preset given an `onCreate`/`onUpdate` of `'now'`. No codec is involved. Payload: `generatorId`. +- A Temporal-backed codec (`pg/date-temporal@1`, `pg/timestamp-temporal@1`, `pg/timestamptz-temporal@1`, `pg/time-temporal@1`) decoding a value. Payload: `codecId`, `operation` (`'decode'`). Encoding a `Temporal` value needs no implementation and does not raise this error. +- The `instantNow` or `plainDateTimeNow` mutation-default generator producing a value, for `temporal.createdAt()`, `temporal.updatedAt()`, or for a `temporal.timestamptz(…)` / `timestamp(…)` preset given an `onCreate`/`onUpdate` of `'now'`. No codec is involved. Payload: `generatorId`. The check is lazy: registering the target, validating a contract, building a runtime, resolving a descriptor and constructing a codec instance all succeed without `Temporal`. Only producing or interpreting a value fails. -That covers more than an explicit write. It is raised on **reads**, because the check is the first thing a Temporal codec does on decode: selecting the column is enough. And it is raised on an **insert into a table carrying `temporal.updatedAt()`**, because that column's clock produces a `Temporal.Instant` even when your code never mentions a temporal value; that path reports `generatorId` rather than `codecId`, since no codec has been reached yet. +It is raised on **reads**, because the check is the first thing a Temporal codec does on decode: selecting the column is enough. And it is raised on an **insert into a table carrying `temporal.updatedAt()`**, because that column's clock produces a `Temporal.Instant` even when your code never mentions a temporal value; that path reports `generatorId` rather than `codecId`, since no codec has been reached yet. + +This error never occurs in the control plane. The CLI commands, `node migration.ts`, the Vite plugin, the language server and the programmatic control client all load `prisma.config.ts` or the control descriptors, and so load the target's control entry. The target's control entry (`@internal/target-postgres/control`) sets the fallback `Temporal`, from `temporal-polyfill`, when it is loaded. It does not set `globalThis.Temporal`. `temporal-polyfill` is a required peer dependency of `@prisma/orm-postgres` and `@prisma/orm-target-postgres`. npm, pnpm and bun install it automatically; a project that uses Yarn adds it to its own dependencies. When it is not installed, loading the control entry fails because Node.js cannot find the package `temporal-polyfill`, not with this error. The fallback is held once per process, and every Postgres codec in the process uses it. An application process that loads no control-plane code and has no global `Temporal` gets `RUNTIME.TEMPORAL_UNAVAILABLE`. An application that loads control-plane code in its own process, such as server code under `vite dev` with the Prisma Vite plugin or a script that calls the control client, decodes dates without its own `Temporal`, and then fails in production. An application that uses the Temporal codecs must load its own `Temporal`. -Install a global implementation before any query runs (`import 'temporal-polyfill/full/global'`), or author the column with its `*String` type (`DateString`, `TimestampString(p)`, `TimestamptzString(p)`, `TimeString(p)`) to read and write PostgreSQL's own text, which needs no Temporal at all. +In the application, install a global implementation before any query runs (`import 'temporal-polyfill/full/global'`), or author the column with its `*String` type (`DateString`, `TimestampString(p)`, `TimestamptzString(p)`, `TimeString(p)`) to read and write PostgreSQL's own text, which needs no Temporal at all. ### RUNTIME.TRANSACTION_CLOSED [#RUNTIME.TRANSACTION_CLOSED] diff --git a/apps/docs/scripts/generate-error-reference.mjs b/apps/docs/scripts/generate-error-reference.mjs index 67f86c1b6bb..cd571637a05 100644 --- a/apps/docs/scripts/generate-error-reference.mjs +++ b/apps/docs/scripts/generate-error-reference.mjs @@ -89,21 +89,34 @@ function applyCliNamingStandard(body) { return replaceInProse(applyVersionNamingStandard(body), /\bManagement API\b/g, "REST API"); } +const ORM_PAGE = "[Prisma ORM error reference](/orm/reference/error-reference)"; +const CLI_PAGE = "[CLI error reference](/cli/error-reference)"; + export const TARGETS = { orm: { sourceRepo: "prisma/orm", output: join(HERE, "../content/docs/orm/reference/error-reference.mdx"), applyNamingStandard: applyOrmNamingStandard, hostedIntro: - "Each code anchors as `#` — the exact fragment every emitted error carries in its " + - "`docsUrl`. This page is generated from the canonical reference in the `prisma/orm` " + - "repository, whose CI requires every code in production source to be documented before it ships.", + "When an error prints a `docsUrl`, that link opens the code's entry on this page. " + + "This page is generated from the `prisma/orm` repository.", + scope: + "This page lists the codes from the Prisma ORM commands, which start with `prisma contract`, " + + "`prisma db`, `prisma migration`, and `prisma orm`, and the codes your app can raise at runtime, " + + `including from extensions. Every other \`prisma\` command has its codes on the ${CLI_PAGE}.`, + sharedCliNamespace: + "One exception: some `CLI.*` codes come from the command-line layer that every `prisma` command " + + "runs through, for its arguments, for loading `prisma.config.ts`, for prompts, and for consent. " + + `Those are on the ${CLI_PAGE} even when you ran a Prisma ORM command. ` + + "`CLI.CONFIG_SECTION_INVALID` from `prisma contract emit` is one of them. " + + "The `CLI.*` codes on this page are the ones the Prisma ORM commands raise themselves. " + + "If a `CLI.*` code is not on this page, it is on the other.", frontmatter: `--- title: Error reference -description: Every structured error code Prisma ORM can emit, by namespace, with the condition that raises it. +description: The structured error codes of the Prisma ORM commands, runtime, and extensions, by namespace, with the condition that raises each one. url: /orm/reference/error-reference metaTitle: Prisma ORM error reference -metaDescription: Every structured error code Prisma ORM can emit, by namespace, with the condition that raises it. +metaDescription: The structured error codes of the Prisma ORM commands, runtime, and extensions, by namespace, with the condition that raises each one. --- `, }, @@ -112,15 +125,25 @@ metaDescription: Every structured error code Prisma ORM can emit, by namespace, output: join(HERE, "../content/docs/cli/error-reference.mdx"), applyNamingStandard: applyCliNamingStandard, hostedIntro: - "Each code anchors as `#` — the fragment an emitted error's `docsUrl` resolves to. " + - "This page is generated from the canonical registry in the `prisma/prisma-cli` repository, " + - "whose CI requires every code in production source to be documented before it ships.", + "When an error prints a `docsUrl`, that link opens the code's entry on this page. " + + "This page is generated from the `prisma/prisma-cli` repository.", + scope: + "This page lists the codes from every `prisma` command except the Prisma ORM ones. The codes " + + "from `prisma contract`, `prisma db`, `prisma migration`, and `prisma orm`, and the codes your " + + `app can raise at runtime, are on the ${ORM_PAGE}.`, + sharedCliNamespace: + "One exception: the `CLI.*` codes on this page come from the command-line layer that every " + + "`prisma` command runs through, for its arguments, for loading `prisma.config.ts`, for prompts, " + + "and for consent. They are on this page even when you ran a Prisma ORM command. " + + "`CLI.CONFIG_SECTION_INVALID` from `prisma contract emit` is one of them. " + + `The Prisma ORM commands also raise \`CLI.*\` codes of their own, and those are on the ${ORM_PAGE}. ` + + "If a `CLI.*` code is not on this page, it is on the other.", frontmatter: `--- title: Error reference -description: Every structured error code the unified Prisma CLI can emit, by namespace, with the condition that raises it. +description: The structured error codes of the Prisma CLI platform commands, by namespace, with the condition that raises each one. url: /cli/error-reference metaTitle: Error reference | Prisma CLI -metaDescription: Every structured error code the unified Prisma CLI can emit, by namespace, with the condition that raises it. +metaDescription: The structured error codes of the Prisma CLI platform commands, by namespace, with the condition that raises each one. --- `, }, @@ -159,6 +182,72 @@ function assertMdxSafe(markdown) { } } +const SOURCE_SCOPE_SENTENCE = "This page lists every published code."; + +// Each page covers a subset of the CLI's commands, so the source's claims to +// list every code are replaced with the target's scope and a link to the +// other page. +function addScope(body, target, warnings) { + body = body.replace(", and this page lists every code the CLI can emit.", "."); + if (body.includes(SOURCE_SCOPE_SENTENCE)) { + return body.replace(SOURCE_SCOPE_SENTENCE, target.scope); + } + warnings.push( + `The source no longer contains ${JSON.stringify(SOURCE_SCOPE_SENTENCE)}. ` + + "The scope sentence was inserted after the first paragraph instead.", + ); + const firstParagraphEnd = body.search(/\n\s*\n/); + if (firstParagraphEnd === -1) return `${body.trimEnd()}\n\n${target.scope}\n`; + return `${body.slice(0, firstParagraphEnd)}\n\n${target.scope}${body.slice(firstParagraphEnd)}`; +} + +/** Fumadocs' heading slug (github-slugger) for the heading shapes the sources use. */ +function headingSlug(text) { + return text + .toLowerCase() + .replace(/[^\p{L}\p{N}\s_-]/gu, "") + .replace(/\s/g, "-"); +} + +const SOURCE_NAMESPACE_TABLE = /^Namespaces:\n\n((?:\|.*\|\n)+)\n?/m; + +// The source's own namespace table can fall behind its `##` sections, so the +// list is rebuilt from the sections. The table still supplies each +// namespace's description. +function replaceNamespaceList(body, target) { + const descriptions = new Map(); + const table = body.match(SOURCE_NAMESPACE_TABLE); + if (table) { + body = body.replace(SOURCE_NAMESPACE_TABLE, ""); + for (const row of table[1].split("\n")) { + const cells = row.split("|").map((cell) => cell.trim()); + const namespace = cells[1]?.match(/^`([^`]+)`$/)?.[1]; + if (namespace && cells[2]) descriptions.set(namespace, cells[2]); + } + } + + const namespaces = [...body.matchAll(/^## (.+)$/gm)].map((match) => match[1].trim()); + if (namespaces.length === 0) return body; + const items = namespaces.map((namespace) => { + const description = descriptions.get(namespace); + const link = `[\`${namespace}\`](#${headingSlug(namespace)})`; + return description ? `- ${link}: ${description}` : `- ${link}`; + }); + const list = ["Namespaces on this page:", "", ...items, ""]; + + const firstSection = body.search(/^## /m); + body = `${body.slice(0, firstSection)}${list.join("\n")}\n${body.slice(firstSection)}`; + if (!namespaces.includes("CLI")) return body; + + // The note answers the reader who arrived with a CLI.* code, so it sits + // directly under the paragraph that holds the scope sentence. + const scopeAt = body.indexOf(target.scope); + const from = scopeAt === -1 ? 0 : scopeAt; + const paragraphEnd = body.slice(from).search(/\n\s*\n/); + const at = paragraphEnd === -1 ? body.length : from + paragraphEnd; + return `${body.slice(0, at)}\n\n${target.sharedCliNamespace}${body.slice(at)}`; +} + export function transform(target, markdown) { assertMdxSafe(markdown); @@ -173,6 +262,10 @@ export function transform(target, markdown) { target.hostedIntro, ); + const warnings = []; + body = addScope(body, target, warnings); + body = replaceNamespaceList(body, target); + // Repo-relative links point at files that only exist in the source repo. const blobBase = `https://github.com/${target.sourceRepo}/blob/main/docs/reference/`; body = body.replace(/\]\((\.\.?\/[^)]+)\)/g, (_, linkTarget) => { @@ -207,11 +300,11 @@ export function transform(target, markdown) { const header = `${target.frontmatter} {/* Generated by scripts/generate-error-reference.mjs from https://github.com/${target.sourceRepo}/blob/main/docs/reference/error-reference.md - Do not edit by hand — changes are overwritten by the sync workflow. */} + Do not edit by hand. The sync workflow overwrites changes. */} `; - return { mdx: header + body, codeCount: codes.length }; + return { mdx: header + body, codeCount: codes.length, warnings }; } if (process.argv[1] === fileURLToPath(import.meta.url)) { @@ -222,7 +315,8 @@ if (process.argv[1] === fileURLToPath(import.meta.url)) { `Unknown --target ${JSON.stringify(targetName)}. Known: ${Object.keys(TARGETS).join(", ")}`, ); } - const { mdx, codeCount } = transform(target, await loadSource(target)); + const { mdx, codeCount, warnings } = transform(target, await loadSource(target)); + for (const warning of warnings) console.warn(`Warning: ${warning}`); writeFileSync(target.output, mdx); console.log(`Wrote ${target.output} with ${codeCount} error codes.`); } diff --git a/scripts/generate-error-reference.test.ts b/scripts/generate-error-reference.test.ts index d90a13c45f6..08c574f8766 100644 --- a/scripts/generate-error-reference.test.ts +++ b/scripts/generate-error-reference.test.ts @@ -87,3 +87,77 @@ test("a duplicate undotted code is rejected", () => { /Duplicate error codes in source: PSL_PRESET_CONFLICT/, ); }); + +const intro = "Every error is a structured envelope. This page lists every published code."; + +function sourceWith({ intro: firstParagraph = intro, namespaces = ["AUTH", "CLI"] }: { intro?: string; namespaces?: string[] } = {}) { + const sections = namespaces.map( + (namespace) => `## ${namespace}\n\n### ${namespace}.FAILED\n\nIt failed.\n`, + ); + return [ + "# Error reference", + "", + firstParagraph, + "", + "Match on `error.code`.", + "", + "Namespaces:", + "", + "| Namespace | Covers |", + "| --- | --- |", + "| `AUTH` | Workspace authentication |", + "", + ...sections, + ].join("\n"); +} + +test("the source's claim to list every code becomes the target's scope with a link to the other page", () => { + const { mdx, warnings } = transform(TARGETS.orm, sourceWith()); + + assert.ok(mdx.includes(`Every error is a structured envelope. ${TARGETS.orm.scope}`)); + assert.ok(mdx.includes("[CLI error reference](/cli/error-reference)")); + assert.ok(!mdx.includes("This page lists every published code.")); + assert.deepEqual(warnings, []); +}); + +test("the namespace list comes from the source's sections and keeps the table's descriptions", () => { + const { mdx } = transform(TARGETS.cli, sourceWith({ namespaces: ["AUTH", "PROJECT"] })); + + assert.ok( + mdx.includes( + "Namespaces on this page:\n\n- [`AUTH`](#auth): Workspace authentication\n- [`PROJECT`](#project)\n", + ), + ); + assert.ok(!mdx.includes("| Namespace | Covers |")); + assert.ok(mdx.indexOf("Namespaces on this page:") < mdx.indexOf("## AUTH")); +}); + +test("the shared CLI namespace is explained only when the source has a CLI section", () => { + const withCli = transform(TARGETS.cli, sourceWith({ namespaces: ["AUTH", "CLI"] })).mdx; + const withoutCli = transform(TARGETS.cli, sourceWith({ namespaces: ["AUTH"] })).mdx; + + assert.ok(withCli.includes(TARGETS.cli.sharedCliNamespace)); + assert.ok(withCli.indexOf(TARGETS.cli.sharedCliNamespace) < withCli.indexOf("Namespaces on this page:")); + assert.ok(!withoutCli.includes("One exception:")); +}); + +test("a reworded source intro still gets the scope sentence after its first paragraph, with a warning", () => { + const { mdx, warnings } = transform( + TARGETS.orm, + sourceWith({ intro: "Every error is a structured envelope." }), + ); + + assert.ok( + mdx.includes( + `Every error is a structured envelope.\n\n${TARGETS.orm.scope}\n\n${TARGETS.orm.sharedCliNamespace}\n\nMatch on`, + ), + ); + assert.equal(warnings.length, 1); + assert.match(warnings[0], /This page lists every published code/); +}); + +test("the frontmatter descriptions do not claim to list every code", () => { + for (const target of Object.values(TARGETS)) { + assert.doesNotMatch(target.frontmatter, /\bevery\b/i); + } +});