| 1 | // Runtime error taxonomy for the Prisma ORM v8 Effect client. Worker-safe: |
| 2 | // imports nothing beyond effect, and detects Prisma's error shapes |
| 3 | // structurally (via their stable `kind` / `code` / `sqlState` discriminants) |
| 4 | // so this module never has to value-import the optional peer. |
| 5 | // |
| 6 | // Granularity policy: a dedicated tag wherever the discriminant is STABLE |
| 7 | // and handling genuinely branches — the SQL-standard integrity-violation |
| 8 | // SQLSTATEs (23xxx) and Prisma's error categories (`ORM.*`, |
| 9 | // `RUNTIME.*`, ...). Individual codes stay a typed `code` field rather than |
| 10 | // one class per code: the code set churns per RC, while categories and |
| 11 | // SQLSTATEs do not. |
| 12 | import * as Data from "effect/Data"; |
| 13 | |
| 14 | /** |
| 15 | * Prisma structured error codes observed in 8.0.0-rc.8 — an OPEN union |
| 16 | * (`string` stays assignable) so new RC codes never break, while known ones |
| 17 | * autocomplete. |
| 18 | */ |
| 19 | export type ErrorCode = |
| 20 | | "ORM.TABLE_UNKNOWN" |
| 21 | | "ORM.COLUMN_UNKNOWN" |
| 22 | | "ORM.RELATION_UNKNOWN" |
| 23 | | "ORM.INCLUDE_INVALID" |
| 24 | | "ORM.AGGREGATE_UNSUPPORTED" |
| 25 | | "ORM.MUTATION_ROW_MISSING" |
| 26 | | "ORM.CAPABILITY_MISSING" |
| 27 | | "RUNTIME.ENCODE_FAILED" |
| 28 | | "RUNTIME.DECODE_FAILED" |
| 29 | | "RUNTIME.ABORTED" |
| 30 | | "RUNTIME.CODEC_MISSING" |
| 31 | | "RUNTIME.PREPARE_UNUSED_PARAM" |
| 32 | | "RUNTIME.PREPARE_BIND_ON_ADHOC" |
| 33 | | "RUNTIME.TRANSACTION_CLOSED" |
| 34 | | "RUNTIME.TRANSACTION_COMMIT_FAILED" |
| 35 | | "RUNTIME.TRANSACTION_ROLLBACK_FAILED" |
| 36 | | "RUNTIME.BINDING_INVALID" |
| 37 | | "RUNTIME.BINDING_MISSING" |
| 38 | | "BUDGET.ROWS_EXCEEDED" |
| 39 | | "DRIVER.NOT_CONNECTED" |
| 40 | | "DRIVER.ALREADY_CONNECTED" |
| 41 | | "DRIVER.PREPARE_FAILED" |
| 42 | | "CONTRACT.MARKER_MISSING" |
| 43 | | "CONTRACT.MARKER_MISMATCH" |
| 44 | | "CONTRACT.VERIFY_FAILED" |
| 45 | | (string & {}); |
| 46 | |
| 47 | interface QueryErrorFields { |
| 48 | message: string; |
| 49 | sqlState?: string | undefined; |
| 50 | constraint?: string | undefined; |
| 51 | table?: string | undefined; |
| 52 | column?: string | undefined; |
| 53 | detail?: string | undefined; |
| 54 | cause: unknown; |
| 55 | } |
| 56 | |
| 57 | // ── integrity violations (SQL-standard SQLSTATE class 23) ───────────── |
| 58 | // The driver normalizes target codes onto SQLSTATE, so these tags are |
| 59 | // stable across databases and RCs. |
| 60 | |
| 61 | /** |
| 62 | * Unique or primary-key conflict (SQLSTATE `23505`) — the "email already |
| 63 | * registered" case: |
| 64 | * |
| 65 | * ```typescript |
| 66 | * Effect.catchTag("Prisma.UniqueViolationError", () => |
| 67 | * Effect.succeed("already registered"), |
| 68 | * ) |
| 69 | * ``` |
| 70 | */ |
| 71 | export class UniqueViolationError extends Data.TaggedError( |
| 72 | "Prisma.UniqueViolationError", |
| 73 | )<QueryErrorFields> {} |
| 74 | |
| 75 | /** Foreign-key violation (SQLSTATE `23503`) — a referenced row is missing. */ |
| 76 | export class ForeignKeyViolationError extends Data.TaggedError( |
| 77 | "Prisma.ForeignKeyViolationError", |
| 78 | )<QueryErrorFields> {} |
| 79 | |
| 80 | /** NOT NULL violation (SQLSTATE `23502`). */ |
| 81 | export class NotNullViolationError extends Data.TaggedError( |
| 82 | "Prisma.NotNullViolationError", |
| 83 | )<QueryErrorFields> {} |
| 84 | |
| 85 | /** CHECK constraint violation (SQLSTATE `23514`). */ |
| 86 | export class CheckViolationError extends Data.TaggedError( |
| 87 | "Prisma.CheckViolationError", |
| 88 | )<QueryErrorFields> {} |
| 89 | |
| 90 | /** Every integrity-constraint tag (SQLSTATE class 23). */ |
| 91 | export type ConstraintViolationError = |
| 92 | | UniqueViolationError |
| 93 | | ForeignKeyViolationError |
| 94 | | NotNullViolationError |
| 95 | | CheckViolationError; |
| 96 | |
| 97 | // ── other SQL / connection failures ─────────────────────────────────── |
| 98 | |
| 99 | /** |
| 100 | * Any other SQL statement failure (syntax, permissions, serialization, |
| 101 | * ...). `sqlState` carries the driver-normalized SQLSTATE for finer |
| 102 | * refinement. |
| 103 | */ |
| 104 | export class QueryError extends Data.TaggedError( |
| 105 | "Prisma.QueryError", |
| 106 | )<QueryErrorFields> {} |
| 107 | |
| 108 | /** |
| 109 | * A connection-level failure (refused, reset, timed out). `transient` |
| 110 | * carries the driver's own judgment of retryability: |
| 111 | * |
| 112 | * ```typescript |
| 113 | * Effect.retry(effect, { |
| 114 | * while: (e) => e._tag === "Prisma.ConnectionError" && e.transient === true, |
| 115 | * times: 3, |
| 116 | * }) |
| 117 | * ``` |
| 118 | */ |
| 119 | export class ConnectionError extends Data.TaggedError( |
| 120 | "Prisma.ConnectionError", |
| 121 | )<{ |
| 122 | message: string; |
| 123 | transient?: boolean | undefined; |
| 124 | cause: unknown; |
| 125 | }> {} |
| 126 | |
| 127 | // ── Prisma pipeline categories ──────────────────────────────────────── |
| 128 | |
| 129 | /** |
| 130 | * ORM-lane misuse or validation failure (`ORM.*` codes: unknown |
| 131 | * table/column/relation, invalid include, unsupported aggregate, ...). |
| 132 | * Almost always a programming bug — don't retry. |
| 133 | */ |
| 134 | export class OrmError extends Data.TaggedError("Prisma.OrmError")<{ |
| 135 | code: ErrorCode; |
| 136 | message: string; |
| 137 | cause: unknown; |
| 138 | }> {} |
| 139 | |
| 140 | /** |
| 141 | * Execution-pipeline failure (`RUNTIME.*`, `DRIVER.*`, `BUDGET.*`, |
| 142 | * `CONTRACT.*`, `PLAN.*`, `LINT.*`, `MIGRATION.*` codes: encode/decode, |
| 143 | * codec registry, transaction lifecycle, row budgets, marker |
| 144 | * verification, ...). `code` pins the exact failure. |
| 145 | */ |
| 146 | export class RuntimeError extends Data.TaggedError("Prisma.RuntimeError")<{ |
| 147 | code: ErrorCode; |
| 148 | message: string; |
| 149 | cause: unknown; |
| 150 | }> {} |
| 151 | |
| 152 | /** A failure Prisma did not classify at all (no kind, no code). */ |
| 153 | export class UnknownError extends Data.TaggedError("Prisma.UnknownError")<{ |
| 154 | code?: ErrorCode | undefined; |
| 155 | message: string; |
| 156 | cause: unknown; |
| 157 | }> {} |
| 158 | |
| 159 | /** |
| 160 | * Deliberate transaction rollback. Yield `tx.rollback()` inside |
| 161 | * `db.transaction` to abort: the transaction rolls back and this error |
| 162 | * surfaces in the caller's error channel, catchable with |
| 163 | * `Effect.catchTag("Prisma.RollbackError", ...)`. |
| 164 | */ |
| 165 | export class RollbackError extends Data.TaggedError( |
| 166 | "Prisma.RollbackError", |
| 167 | )<{}> {} |
| 168 | |
| 169 | /** Everything a Prisma query can fail with. */ |
| 170 | export type ClientError = |
| 171 | | ConstraintViolationError |
| 172 | | QueryError |
| 173 | | ConnectionError |
| 174 | | OrmError |
| 175 | | RuntimeError |
| 176 | | UnknownError; |
| 177 | |
| 178 | const field = (value: unknown, key: string): string | undefined => { |
| 179 | if (typeof value !== "object" || value === null) return undefined; |
| 180 | const v = (value as Record<string, unknown>)[key]; |
| 181 | return typeof v === "string" ? v : undefined; |
| 182 | }; |
| 183 | |
| 184 | const message = (cause: unknown): string => |
| 185 | cause instanceof Error ? cause.message : String(cause); |
| 186 | |
| 187 | /** |
| 188 | * The single funnel converting Prisma's thrown errors into the typed |
| 189 | * taxonomy. Detection uses stable discriminants: `kind: 'sql_query' | |
| 190 | * 'sql_connection'` on driver errors (with SQLSTATE refining the class-23 |
| 191 | * integrity violations onto their own tags), and the `CATEGORY.NAME` |
| 192 | * structured `code` on ORM/pipeline errors. |
| 193 | */ |
| 194 | export const wrapPrismaError = (cause: unknown): ClientError => { |
| 195 | const kind = field(cause, "kind"); |
| 196 | if (kind === "sql_query") { |
| 197 | const fields: QueryErrorFields = { |
| 198 | message: message(cause), |
| 199 | sqlState: field(cause, "sqlState"), |
| 200 | constraint: field(cause, "constraint"), |
| 201 | table: field(cause, "table"), |
| 202 | column: field(cause, "column"), |
| 203 | detail: field(cause, "detail"), |
| 204 | cause, |
| 205 | }; |
| 206 | switch (fields.sqlState) { |
| 207 | case "23505": |
| 208 | return new UniqueViolationError(fields); |
| 209 | case "23503": |
| 210 | return new ForeignKeyViolationError(fields); |
| 211 | case "23502": |
| 212 | return new NotNullViolationError(fields); |
| 213 | case "23514": |
| 214 | return new CheckViolationError(fields); |
| 215 | default: |
| 216 | return new QueryError(fields); |
| 217 | } |
| 218 | } |
| 219 | if (kind === "sql_connection") { |
| 220 | const transient = (cause as { transient?: unknown }).transient; |
| 221 | return new ConnectionError({ |
| 222 | message: message(cause), |
| 223 | transient: typeof transient === "boolean" ? transient : undefined, |
| 224 | cause, |
| 225 | }); |
| 226 | } |
| 227 | const code = field(cause, "code"); |
| 228 | if (code !== undefined) { |
| 229 | return code.startsWith("ORM.") |
| 230 | ? new OrmError({ code, message: message(cause), cause }) |
| 231 | : new RuntimeError({ code, message: message(cause), cause }); |
| 232 | } |
| 233 | return new UnknownError({ |
| 234 | code: undefined, |
| 235 | message: message(cause), |
| 236 | cause, |
| 237 | }); |
| 238 | }; |