Initial Commit
This commit is contained in:
@@ -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.
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export type { SearchErrorCode } from "./error-codes.js";
|
||||
export {
|
||||
SEARCH_CODES,
|
||||
SearchError,
|
||||
} from "./error-codes.js";
|
||||
Reference in New Issue
Block a user