diff --git a/CHANGELOG.md b/CHANGELOG.md index 7571754..8a65c32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +2.6.0 +===== + +* (improvement) Bump zod to `4.x` +* (feature) Add proper return values for `fetchApi()`. + + 2.5.2 ===== diff --git a/package.json b/package.json index 411840a..eaeb380 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "test": "pnpm run build && cd dist && pnpm version '2.0.0-dummy' --no-git-tag-version && npm pack --dry-run && publint" }, "dependencies": { - "zod": "^3.25.57" + "zod": "^4.1.11" }, "optionalDependencies": { "next": "^15.3", diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index e6654d6..0000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,2 +0,0 @@ -onlyBuiltDependencies: - - sharp diff --git a/src/api.ts b/src/api.ts index b26638b..532d85e 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,20 +1,104 @@ -import {z} from "zod/v4-mini"; +import {z} from "zod/mini"; const errorSchema = z.object({ ok: z.literal(false), error: z.optional(z.string()), errorMessage: z.optional(z.string()), + data: z.unknown(), }); type Logger = Readonly<{ - debug: (...args: unknown[]) => unknown; - error: (...args: unknown[]) => unknown; + debug: (message: string, context?: Record) => void; + error: (message: string, context?: Record) => void; }> type ApiFetchSettings = Readonly<{ logger?: Logger; }>; +class ApiError extends Error +{ + readonly #errorCode: string; + readonly #data: unknown; + readonly #errorMessage: string|null; + + + constructor ( + errorCode: string, + data: unknown = undefined, + errorMessage: string|null|undefined = undefined, + ) + { + super(`The API request failed due to an error: ${errorCode}`); + this.#errorCode = errorCode; + this.#data = data; + this.#errorMessage = errorMessage ?? null; + } + + /** + * + */ + get errorCode () : string + { + return this.#errorCode; + } + + /** + * The data of the response + */ + get data () : unknown + { + return this.#data; + } + + /** + * A user-facing error message + */ + get errorMessage () : string|null + { + return this.#errorMessage; + } +} + +class RequestError extends Error +{ + private response: Response; + + /** + * + */ + constructor (response: Response) + { + super(); + this.response = response; + } + + /** + * + */ + get is404 () + { + return 404 === this.response.status; + } +} + +/** + * + */ +export function isApiError (value: unknown) : value is ApiError +{ + return value instanceof ApiError; +} + +/** + * + */ +export function isRequestError (value: unknown) : value is RequestError +{ + return value instanceof RequestError; +} + + /** * API helper to fetch data from an API @@ -25,20 +109,16 @@ export async function fetchApi < url: string | URL, dataSchema?: DataSchema, requestSettings: RequestInit = {}, - isDebug: boolean = false, settings: ApiFetchSettings = {}, -) : Promise> +) : Promise> { let response: Response; - const logger = settings.logger ?? console; + const logger = settings.logger; // region Send Request try { - if (isDebug) - { - logger.debug("Fetching from API", {url: url.toString()}); - } + logger?.debug("Fetching from API", {url: url.toString()}); response = await fetch( url, @@ -54,7 +134,7 @@ export async function fetchApi < } catch (error) { - logger.error( + logger?.error( "API request failed due to error", { err: error, @@ -80,7 +160,7 @@ export async function fetchApi < } catch (error) { - logger.error( + logger?.error( "API response is no JSON", { contentType: response.headers.get("content-type"), @@ -103,16 +183,12 @@ export async function fetchApi < { if (!response.ok) { - logger.error("Got success response, but API response is no success"); + logger?.error("Got success response, but API response is no success"); } // @ts-expect-error .data errors out in zod right now. So we add this and the cast in the meantime return successResponse.data.data as z.infer; } - else - { - logger.debug("No success cause", successResponse.error); - } const failureResponse = errorSchema.safeParse(responseData); @@ -120,17 +196,28 @@ export async function fetchApi < { if (response.ok) { - logger.error("Got error response, but API response is success"); + logger?.error("Got error response, but API response is success"); } - throw failureResponse.data; + throw new ApiError( + failureResponse.data.error, + failureResponse.data.data, + failureResponse.data.errorMessage, + ); } else { - logger.debug("No failure cause", failureResponse.error); + logger?.debug("No failure cause", { + error: failureResponse.error, + }); + } + + if (404 === response.status) + { + throw new RequestError(response); } - logger.error( + logger?.error( "Invalid API response", { responseData,