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,96 @@
# 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-search`. Every failure
path inside the search engine, the analyzer registry, the route
handlers, and the plugin lifecycle throws a `SearchError` extending
`SuaveplanError`, so callers can `instanceof SearchError` to narrow
`error.code` to the 10 `SEARCH.*` codes without touching free-form
strings. The registry is the public contract: adding, removing, or
renaming a code is a breaking change for consumers that switch on the
value.
## Features
- Ten stable codes covering every plugin failure: index lookup misses,
index-name collisions, hard cap on indexes / documents-per-index,
document removal misses, unknown analyzer names, factory-option
validation, query-time validation failures, missing host services,
and route-prefix collisions.
- `defineCodes("SEARCH", [...])` registers every code globally so the
unified error registry can resolve any `SEARCH.*` string back to a
human-friendly description.
- `SearchError` carries `searchCode` 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 {
SEARCH_CODES,
SearchError,
} from "@suaveplan/api-plugin-search";
if (!indexes.has(name)) {
throw new SearchError(
SEARCH_CODES.INDEX_NOT_FOUND,
`Index "${name}" not found`,
{ context: { statusCode: 404, indexName: name } }
);
}
```
## Usage: Advanced
Compose the registry with the `@suaveplan/error` `cause` chain so the
original Zod failure survives translation to HTTP without losing its
stack:
```ts
const parsed = SearchPluginOptionsSchema.safeParse(input);
if (!parsed.success) {
throw new SearchError(
SEARCH_CODES.INVALID_OPTIONS,
"createSearchPlugin received invalid options",
{ cause: parsed.error, context: { statusCode: 500 } }
);
}
```
## API Reference
### `SEARCH_CODES`
`Readonly<Record<"INDEX_NOT_FOUND" | "INDEX_ALREADY_EXISTS" | "INDEX_LIMIT_REACHED" | "DOCUMENT_LIMIT_REACHED" | "DOCUMENT_NOT_FOUND" | "UNKNOWN_ANALYZER" | "INVALID_OPTIONS" | "INVALID_QUERY" | "SERVICE_NOT_AVAILABLE" | "ROUTE_PREFIX_COLLISION", string>>`.
Each value is the fully-qualified `"SEARCH.<NAME>"` string.
### `SearchError`
`class SearchError extends SuaveplanError<SearchErrorCode>` — public
constructor `(code, message, options?)` so callers can construct the
error directly. `searchCode` mirrors `code` for ergonomic narrowing.
### `SearchErrorCode`
Union of all 10 literal `"SEARCH.*"` code values.
## Implementation Notes
The codes are declared with a `readonly` tuple so the type system can
prove the array length and value union at compile time. Every code is
wired into at least one production code path in
`src/service/service.ts`, `src/analyzer/analyzer.ts`,
`src/plugin/plugin.ts`, or `src/routes/build-routes.ts`. The
`spec-conformance` test pins the union shape to prevent silent drift,
and the error-codes test pins the count + name set so removing a code
fails CI.

View File

@@ -0,0 +1,48 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SEARCH_CODES, SearchError } from "./error-codes.js";
describe("SEARCH_CODES registry", () => {
it("declares 10 SEARCH.* codes", { timeout: 5000 }, () => {
const values = Object.values(SEARCH_CODES);
expect(values.length).toBe(10);
expect(new Set(values).size).toBe(10);
for (const v of values) {
expect(v.startsWith("SEARCH.")).toBe(true);
}
});
it("carries each documented name", { timeout: 5000 }, () => {
const names: Array<keyof typeof SEARCH_CODES> = [
"INDEX_NOT_FOUND",
"INDEX_ALREADY_EXISTS",
"INDEX_LIMIT_REACHED",
"DOCUMENT_LIMIT_REACHED",
"DOCUMENT_NOT_FOUND",
"UNKNOWN_ANALYZER",
"INVALID_OPTIONS",
"INVALID_QUERY",
"SERVICE_NOT_AVAILABLE",
"ROUTE_PREFIX_COLLISION",
];
for (const n of names) {
expect(typeof SEARCH_CODES[n]).toBe("string");
}
});
});
describe("SearchError class", () => {
it("SearchError narrows code to SearchErrorCode and preserves cause", {
timeout: 5000,
}, () => {
const inner = new Error("inner");
const err = new SearchError(SEARCH_CODES.INDEX_NOT_FOUND, "boom", {
cause: inner,
context: { statusCode: 404 },
});
expect(err).toBeInstanceOf(SearchError);
expect(err.code).toBe(SEARCH_CODES.INDEX_NOT_FOUND);
expect(err.searchCode).toBe(SEARCH_CODES.INDEX_NOT_FOUND);
expect(err.message).toBe("boom");
expect(err.cause).toBe(inner);
});
});

View File

@@ -0,0 +1,69 @@
import type { SuaveplanErrorOptions } from "@suaveplan/error";
import { defineCodes, SuaveplanError } from "@suaveplan/error";
/**
* Stable error-code registry for `@suaveplan/api-plugin-search`.
*
* Every failure path in the search plugin raises a {@link SearchError}
* carrying one of these 10 `SEARCH.*` codes. Consumers branch on the
* string value (e.g. `error.code === SEARCH_CODES.INDEX_NOT_FOUND`).
*
* - `INDEX_NOT_FOUND` — a lookup (`getSearchIndex`, `indexDocument`,
* `removeDocument`, `search`, `suggest`) referenced an index name that
* has not been created. Maps to HTTP 404.
* - `INDEX_ALREADY_EXISTS` — `createSearchIndex` was called with a name
* that already exists in the index map. Maps to HTTP 400.
* - `INDEX_LIMIT_REACHED` — `createSearchIndex` was called when the
* number of indexes equals `maxIndexes`. Maps to HTTP 400.
* - `DOCUMENT_LIMIT_REACHED` — `indexDocument` would push the per-index
* document count above `maxDocumentsPerIndex` for a new doc id. Maps
* to HTTP 400.
* - `DOCUMENT_NOT_FOUND` — `removeDocument` was called with a doc id
* absent from the target index. Maps to HTTP 404.
* - `UNKNOWN_ANALYZER` — `getAnalyzer(name)` received a name that is
* not registered in the built-in analyzer table. Maps to HTTP 400.
* - `INVALID_OPTIONS` — `createSearchPlugin(options)` received options
* that failed `SearchPluginOptionsSchema` validation. `cause` holds
* the `ZodError`. Maps to HTTP 500 (factory-time misconfiguration).
* - `INVALID_QUERY` — a route received a request body whose Zod
* validation failed at the service layer (e.g. empty index name).
* Maps to HTTP 400.
* - `SERVICE_NOT_AVAILABLE` — `onReady` could not resolve the host
* `app` service from `ServiceRegistry`. `context.missing` lists the
* missing slot. Maps to HTTP 500.
* - `ROUTE_PREFIX_COLLISION` — the configured `routePrefix` collides
* with routes already registered by another plugin. Maps to HTTP 500.
*/
export const SEARCH_CODES = defineCodes("SEARCH", [
"INDEX_NOT_FOUND",
"INDEX_ALREADY_EXISTS",
"INDEX_LIMIT_REACHED",
"DOCUMENT_LIMIT_REACHED",
"DOCUMENT_NOT_FOUND",
"UNKNOWN_ANALYZER",
"INVALID_OPTIONS",
"INVALID_QUERY",
"SERVICE_NOT_AVAILABLE",
"ROUTE_PREFIX_COLLISION",
] as const);
/** Union of all 10 `SEARCH.*` string-literal codes. */
export type SearchErrorCode = (typeof SEARCH_CODES)[keyof typeof SEARCH_CODES];
/**
* Concrete error class for the api-plugin-search package. Extends
* {@link SuaveplanError} so `instanceof SearchError` narrows `code` to
* {@link SearchErrorCode}.
*/
export class SearchError extends SuaveplanError<SearchErrorCode> {
public readonly searchCode: SearchErrorCode;
public constructor(
code: SearchErrorCode,
message: string,
options?: SuaveplanErrorOptions
) {
super(code, message, options);
this.searchCode = code;
}
}

View File

@@ -0,0 +1,5 @@
export type { SearchErrorCode } from "./error-codes.js";
export {
SEARCH_CODES,
SearchError,
} from "./error-codes.js";