Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,87 @@
# error-codes/error-codes
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The error-codes module defines the stable registry of programmatic
failure codes raised by `@suaveplan/api-plugin-user`. Every code lives
behind a `UserError` class extending the canonical `SuaveplanError`
base, so callers can `instanceof UserError` to narrow `error.code` to
the union of seven `USER.*` codes — never a free-form string. The
registry is the contract: adding, removing, or renaming a code is a
breaking change for consumers asserting on the value.
## Features
- Seven stable codes covering every plugin failure path: lookup misses,
invalid factory options, disabled self-delete, profile-update
failures, missing services, schema-apply failures, and route-prefix
collisions.
- `defineCodes("USER", [...])` registers the codes globally so the
unified error registry can resolve them by string.
- `UserError` carries `userCode` alongside the canonical `code` field
so downstream code that does not import the base class can still
branch on the discriminant.
- Each thrown instance carries `context.statusCode` so the host's
error-handler middleware maps the failure to the right HTTP status
without per-route boilerplate.
## Usage: Basic
```ts
import { USER_CODES, UserError } from "@suaveplan/api-plugin-user/error-codes/error-codes";
if (!row) {
throw new UserError(USER_CODES.NOT_FOUND, "user not found", {
context: { statusCode: 404, userId: id },
});
}
```
## Usage: Advanced
Compose the registry with the `@suaveplan/error` `cause` chain so the
original database error survives translation to HTTP without losing
its stack:
```ts
try {
await db.execute(sql);
} catch (cause) {
throw new UserError(USER_CODES.PROFILE_UPDATE_FAILED, "patch failed", {
cause,
context: { statusCode: 500, userId, fields: Object.keys(patch) },
});
}
```
## API Reference
### `USER_CODES`
`Readonly<Record<"NOT_FOUND" | "INVALID_INPUT" | "SELF_DELETE_DISABLED" | "PROFILE_UPDATE_FAILED" | "SERVICE_NOT_AVAILABLE" | "SCHEMA_APPLY_FAILED" | "ROUTE_PREFIX_COLLISION", string>>`.
Each value is the fully-qualified `"USER.<NAME>"` string used as the
discriminant.
### `UserError`
`class UserError extends SuaveplanError<UserErrorCode>` — public
constructor `(code, message, options?)` so callers can construct the
error directly. `userCode` mirrors `code` for ergonomic narrowing.
### `UserErrorCode`
Union of all seven literal code values.
## Implementation Notes
- `defineCodes` performs duplicate-detection at module-load time; a
duplicate name throws synchronously so a stale build fails loudly.
- The seven codes are intentionally cohesive — admin-side listings,
avatar uploads, and GDPR export are scoped to follow-up plugins with
their own registries.
- `statusCode` is always carried on `context`, not as a top-level
field, so the error registry remains transport-agnostic.

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { USER_CODES, UserError } from "./error-codes.js";
describe("USER_CODES registry", () => {
it("declares 7 stable codes", { timeout: 5000 }, () => {
const values = Object.values(USER_CODES);
expect(values.length).toBe(7);
expect(new Set(values).size).toBe(7);
for (const v of values) {
expect(v.startsWith("USER.")).toBe(true);
}
});
it("carries each documented name", { timeout: 5000 }, () => {
const names: Array<keyof typeof USER_CODES> = [
"NOT_FOUND",
"INVALID_INPUT",
"SELF_DELETE_DISABLED",
"PROFILE_UPDATE_FAILED",
"SERVICE_NOT_AVAILABLE",
"SCHEMA_APPLY_FAILED",
"ROUTE_PREFIX_COLLISION",
];
for (const n of names) {
expect(typeof USER_CODES[n]).toBe("string");
}
});
});
describe("UserError class", () => {
it("narrows code to userCode and stores message", { timeout: 5000 }, () => {
const err = new UserError(USER_CODES.NOT_FOUND, "boom", {
context: { statusCode: 404 },
});
expect(err).toBeInstanceOf(UserError);
expect(err.code).toBe(USER_CODES.NOT_FOUND);
expect(err.userCode).toBe(USER_CODES.NOT_FOUND);
expect(err.message).toBe("boom");
});
it("preserves cause chain", { timeout: 5000 }, () => {
const inner = new Error("inner");
const err = new UserError(USER_CODES.PROFILE_UPDATE_FAILED, "outer", {
cause: inner,
});
expect(err.cause).toBe(inner);
});
});

View File

@@ -0,0 +1,59 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/api-plugin-user`.
*
* Every failure path in the user plugin raises a {@link UserError}
* carrying one of these 7 `USER.*` codes. Consumers branch on the
* string value (e.g. `error.code === USER_CODES.NOT_FOUND`).
*
* - `NOT_FOUND` — a lookup (`getById`, `updateProfile`) targeted a user
* record that does not exist. The error-handler middleware maps this
* to HTTP 404 via the `statusCode: 404` field on the error context.
* - `INVALID_INPUT` — `createUserPlugin(options)` received options that
* failed `UserPluginOptionsSchema` validation. `cause` holds the
* `ZodError`.
* - `SELF_DELETE_DISABLED` — `DELETE /users/me` was hit while the plugin
* was configured with `allowSelfDelete: false`. Mapped to HTTP 403.
* - `PROFILE_UPDATE_FAILED` — the `PATCH /users/me` write (or the
* downstream `updateProfile` service call) failed at the database
* layer. `cause` carries the underlying error.
* - `SERVICE_NOT_AVAILABLE` — a required `ServiceRegistry` slot was
* absent. `context.missing` lists the missing slot names.
* - `SCHEMA_APPLY_FAILED` — the bundled SurrealQL schema apply step
* threw at `onInit` (`migrationRunner.run` or the inline `execute`
* loop rejected).
* - `ROUTE_PREFIX_COLLISION` — the configured `routePrefix` collides
* with routes already registered by another plugin in the host's
* `OpenAPIRegistry`.
*/
export const USER_CODES = defineCodes("USER", [
"NOT_FOUND",
"INVALID_INPUT",
"SELF_DELETE_DISABLED",
"PROFILE_UPDATE_FAILED",
"SERVICE_NOT_AVAILABLE",
"SCHEMA_APPLY_FAILED",
"ROUTE_PREFIX_COLLISION",
] as const);
export type UserErrorCode = (typeof USER_CODES)[keyof typeof USER_CODES];
/**
* Concrete error class for the api-plugin-user package. Extends
* {@link SuaveplanError} so `instanceof UserError` narrows `code` to
* {@link UserErrorCode}.
*/
export class UserError extends SuaveplanError<UserErrorCode> {
public readonly userCode: UserErrorCode;
public constructor(
code: UserErrorCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
this.userCode = code;
}
}

View File

@@ -0,0 +1,5 @@
export type { UserErrorCode } from "./error-codes.js";
export {
USER_CODES,
UserError,
} from "./error-codes.js";