@jsonapi-serde/integration-koa 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2025-present, Ben Scholzen 'DASPRiD'
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,22 @@
1
+ # JSON:API Serde - Koa Integration
2
+
3
+ [![Test](https://github.com/DASPRiD/jsonapi-serde-js/actions/workflows/test.yml/badge.svg)](https://github.com/DASPRiD/jsonapi-serde-js/actions/workflows/test.yml)
4
+ [![codecov](https://codecov.io/gh/DASPRiD/jsonapi-serde-js/graph/badge.svg?token=UfRUzGqiN3)](https://codecov.io/gh/DASPRiD/jsonapi-serde-js)
5
+
6
+ ---
7
+
8
+ Koa integration for [@jsonapi-serde/server](https://github.com/dasprid/jsonapi-serde-js).
9
+
10
+ ## Documentation
11
+
12
+ To check out docs, visit [jsonapi-serde.js.org](https://jsonapi-serde.js.org).
13
+
14
+ ## Changelog
15
+
16
+ Detailed changes for each release are documented in the [CHANGELOG](https://github.com/dasprid/jsonapi-serde-js/blob/main/packages/integration/koa/CHANGELOG.md)
17
+
18
+ ## License
19
+
20
+ [BSD-3-Clause](https://github.com/dasprid/jsonapi-serde-js/blob/main/LICENSE)
21
+
22
+ Copyright (c) 2025-present, Ben Scholzen 'DASPRiD'
@@ -0,0 +1,3 @@
1
+ export * from "./middleware.js";
2
+ export * from "./response.js";
3
+ export * from "./tree-router.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export * from "./middleware.js";
2
+ export * from "./response.js";
3
+ export * from "./tree-router.js";
@@ -0,0 +1,33 @@
1
+ import { type JsonApiMediaType } from "@jsonapi-serde/server/http";
2
+ import type { Middleware } from "koa";
3
+ /**
4
+ * The Koa state shape extended with JSON:API parsing metadata
5
+ */
6
+ export type JsonApiContextState = {
7
+ jsonApi: {
8
+ /**
9
+ * List of acceptable JSON:API media types parsed from the `Accept` header
10
+ */
11
+ acceptableTypes: JsonApiMediaType[];
12
+ };
13
+ };
14
+ /**
15
+ * Middleware that parses and validates the `Accept` header for JSON:API media types
16
+ *
17
+ * On success, adds `acceptableTypes` to `ctx.state.jsonApi`.
18
+ * On failure, responds with HTTP 400 and a JSON:API error document.
19
+ */
20
+ export declare const jsonApiRequestMiddleware: () => Middleware<Partial<JsonApiContextState>>;
21
+ type ErrorMiddlewareOptions = {
22
+ /**
23
+ * Optional error logger callback
24
+ */
25
+ logError?: (error: unknown, exposed: boolean) => void;
26
+ };
27
+ /**
28
+ * Middleware to catch errors thrown during request handling and respond with JSON:API-compliant error documents
29
+ *
30
+ * Supports optional error logging via the `logError` callback.
31
+ */
32
+ export declare const jsonApiErrorMiddleware: (options?: ErrorMiddlewareOptions) => Middleware;
33
+ export {};
@@ -0,0 +1,107 @@
1
+ import { JsonApiError } from "@jsonapi-serde/server/common";
2
+ import { MediaTypeParserError, getAcceptableMediaTypes, } from "@jsonapi-serde/server/http";
3
+ import { isHttpError } from "http-errors";
4
+ /**
5
+ * Middleware that parses and validates the `Accept` header for JSON:API media types
6
+ *
7
+ * On success, adds `acceptableTypes` to `ctx.state.jsonApi`.
8
+ * On failure, responds with HTTP 400 and a JSON:API error document.
9
+ */
10
+ export const jsonApiRequestMiddleware = () => {
11
+ return async (context, next) => {
12
+ try {
13
+ context.state.jsonApi = {
14
+ acceptableTypes: getAcceptableMediaTypes(context.get("Accept")),
15
+ };
16
+ }
17
+ catch (error) {
18
+ /* node:coverage disable */
19
+ if (!(error instanceof MediaTypeParserError)) {
20
+ throw error;
21
+ }
22
+ /* node:coverage enable */
23
+ const document = new JsonApiError({
24
+ status: "400",
25
+ code: "bad_request",
26
+ title: "Bad Request",
27
+ detail: error.message,
28
+ source: {
29
+ header: "accept",
30
+ },
31
+ }).toDocument();
32
+ context.set("Content-Type", document.getContentType());
33
+ context.status = document.getStatus();
34
+ context.body = document.getBody();
35
+ return;
36
+ }
37
+ return next();
38
+ };
39
+ };
40
+ /**
41
+ * Maps various error types to a JSON:API error object
42
+ *
43
+ * Determines if the error details can be safely exposed to clients.
44
+ */
45
+ const getJsonApiError = (error) => {
46
+ if (isHttpError(error) && error.expose) {
47
+ return [
48
+ new JsonApiError({
49
+ status: error.status.toString(),
50
+ code: error.name
51
+ .replace(/Error$/, "")
52
+ .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
53
+ .replace(/^_+|_+$/g, ""),
54
+ title: error.message,
55
+ }),
56
+ true,
57
+ ];
58
+ }
59
+ if (error instanceof Error &&
60
+ "status" in error &&
61
+ typeof error.status === "number" &&
62
+ error.status >= 400 &&
63
+ error.status < 500) {
64
+ return [
65
+ new JsonApiError({
66
+ status: error.status.toString(),
67
+ code: error.name
68
+ .replace(/Error$/, "")
69
+ .replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
70
+ .replace(/^_+|_+$/g, ""),
71
+ title: error.message,
72
+ }),
73
+ true,
74
+ ];
75
+ }
76
+ if (error instanceof JsonApiError) {
77
+ return [error, error.status < 500];
78
+ }
79
+ return [
80
+ new JsonApiError({
81
+ status: "500",
82
+ code: "internal_server_error",
83
+ title: "Internal Server Error",
84
+ }),
85
+ false,
86
+ ];
87
+ };
88
+ /**
89
+ * Middleware to catch errors thrown during request handling and respond with JSON:API-compliant error documents
90
+ *
91
+ * Supports optional error logging via the `logError` callback.
92
+ */
93
+ export const jsonApiErrorMiddleware = (options) => {
94
+ return async (context, next) => {
95
+ try {
96
+ await next();
97
+ }
98
+ catch (error) {
99
+ const [jsonApiError, exposed] = getJsonApiError(error);
100
+ options?.logError?.(error, exposed);
101
+ const document = jsonApiError.toDocument();
102
+ context.set("Content-Type", document.getContentType());
103
+ context.status = document.getStatus();
104
+ context.body = document.getBody();
105
+ }
106
+ };
107
+ };
@@ -0,0 +1,26 @@
1
+ import type { BodyContext } from "@jsonapi-serde/server/request";
2
+ import type { Context as KoaContext } from "koa";
3
+ type BodyParserKoaContext = KoaContext & {
4
+ request: {
5
+ body?: unknown;
6
+ };
7
+ };
8
+ /**
9
+ * Extracts a `BodyContext` from a Koa context for JSON:API request parsing
10
+ *
11
+ * This function reads the request body and content-type header from the Koa context and validates that the body is
12
+ * either a string or an object.
13
+ *
14
+ * @throws {Error} If the body is neither a string nor a non-null object.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * import { bodyContext } from "@jsonapi-serde/koa";
19
+ *
20
+ * app.use(async (ctx, next) => {
21
+ * const context = bodyContext(ctx);
22
+ * });
23
+ * ```
24
+ */
25
+ export declare const bodyContext: (koaContext: BodyParserKoaContext) => BodyContext;
26
+ export {};
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Extracts a `BodyContext` from a Koa context for JSON:API request parsing
3
+ *
4
+ * This function reads the request body and content-type header from the Koa context and validates that the body is
5
+ * either a string or an object.
6
+ *
7
+ * @throws {Error} If the body is neither a string nor a non-null object.
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { bodyContext } from "@jsonapi-serde/koa";
12
+ *
13
+ * app.use(async (ctx, next) => {
14
+ * const context = bodyContext(ctx);
15
+ * });
16
+ * ```
17
+ */
18
+ export const bodyContext = (koaContext) => {
19
+ const body = koaContext.request.body;
20
+ if ((typeof body !== "string" && typeof body !== "object") || body === null) {
21
+ throw new Error("Body must either be a string or an object");
22
+ }
23
+ return {
24
+ body: body,
25
+ contentType: koaContext.get("Content-Type"),
26
+ };
27
+ };
@@ -0,0 +1,26 @@
1
+ import type { JsonApiDocument } from "@jsonapi-serde/server/common";
2
+ import type { ParameterizedContext } from "koa";
3
+ import type { JsonApiContextState } from "./middleware.js";
4
+ /**
5
+ * Sends a JSON:API response using the given Koa context and JSON:API document
6
+ *
7
+ * This function sets the HTTP status, response body, and content-type headers according to the provided JSON:API
8
+ * document. It also verifies that the client's Accept header matches the media types supported by the document.
9
+ *
10
+ * The function requires that the `jsonApiRequestMiddleware` has been registered on the Koa app to populate
11
+ * `context.state.jsonApi` with the acceptable media types.
12
+ *
13
+ * @throws {JsonApiError} when the document's media type is not acceptable by the client.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { sendJsonApiResponse } from "./response";
18
+ * import { jsonApiRequestMiddleware } from "./middleware";
19
+ *
20
+ * app.use(async (context) => {
21
+ * const document = {}; // Serialized document
22
+ * sendJsonApiResponse(context, document);
23
+ * });
24
+ * ```
25
+ */
26
+ export declare const sendJsonApiResponse: (context: ParameterizedContext<Partial<JsonApiContextState>>, document: JsonApiDocument) => void;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Sends a JSON:API response using the given Koa context and JSON:API document
3
+ *
4
+ * This function sets the HTTP status, response body, and content-type headers according to the provided JSON:API
5
+ * document. It also verifies that the client's Accept header matches the media types supported by the document.
6
+ *
7
+ * The function requires that the `jsonApiRequestMiddleware` has been registered on the Koa app to populate
8
+ * `context.state.jsonApi` with the acceptable media types.
9
+ *
10
+ * @throws {JsonApiError} when the document's media type is not acceptable by the client.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import { sendJsonApiResponse } from "./response";
15
+ * import { jsonApiRequestMiddleware } from "./middleware";
16
+ *
17
+ * app.use(async (context) => {
18
+ * const document = {}; // Serialized document
19
+ * sendJsonApiResponse(context, document);
20
+ * });
21
+ * ```
22
+ */
23
+ export const sendJsonApiResponse = (context, document) => {
24
+ if (!context.state.jsonApi) {
25
+ throw new Error("You must register `jsonApiRequestMiddleware` in Koa");
26
+ }
27
+ document.verifyAcceptMediaType(context.state.jsonApi.acceptableTypes);
28
+ context.status = document.getStatus();
29
+ context.body = document.getBody();
30
+ context.set("content-type", document.getContentType());
31
+ };
@@ -0,0 +1,22 @@
1
+ import type { Context } from "koa";
2
+ type BaseContext = Pick<Context, "response" | "remove" | "status" | "body">;
3
+ /**
4
+ * Handles HTTP method not allowed or resource not found errors for koa-tree-router
5
+ *
6
+ * This function inspects the `Allow` header of the response:
7
+ * - If it is empty, it means no allowed methods exist, so it removes the header and throws a 404 Not Found JSON:API
8
+ * error.
9
+ * - Otherwise, it throws a 405 Method Not Allowed JSON:API error including the allowed methods in the error detail.
10
+ *
11
+ * Intended for use as a fallback handler when a request uses an HTTP method* that is not supported on the resource.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import Router from "koa-tree-router";
16
+ * import { treeRouterMethodNotAllowedHandler } from "@jsonapi-serde/koa";
17
+ *
18
+ * const router = Router({ onMethodNotAllowed: treeRouterMethodNotAllowedHandler });
19
+ * ```
20
+ */
21
+ export declare const treeRouterMethodNotAllowedHandler: <TContext extends BaseContext>(context: TContext) => never;
22
+ export {};
@@ -0,0 +1,35 @@
1
+ import { JsonApiError } from "@jsonapi-serde/server/common";
2
+ /**
3
+ * Handles HTTP method not allowed or resource not found errors for koa-tree-router
4
+ *
5
+ * This function inspects the `Allow` header of the response:
6
+ * - If it is empty, it means no allowed methods exist, so it removes the header and throws a 404 Not Found JSON:API
7
+ * error.
8
+ * - Otherwise, it throws a 405 Method Not Allowed JSON:API error including the allowed methods in the error detail.
9
+ *
10
+ * Intended for use as a fallback handler when a request uses an HTTP method* that is not supported on the resource.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * import Router from "koa-tree-router";
15
+ * import { treeRouterMethodNotAllowedHandler } from "@jsonapi-serde/koa";
16
+ *
17
+ * const router = Router({ onMethodNotAllowed: treeRouterMethodNotAllowedHandler });
18
+ * ```
19
+ */
20
+ export const treeRouterMethodNotAllowedHandler = (context) => {
21
+ if (context.response.headers.allow === "") {
22
+ context.remove("allow");
23
+ throw new JsonApiError({
24
+ status: "404",
25
+ code: "not_found",
26
+ title: "Resource not found",
27
+ });
28
+ }
29
+ throw new JsonApiError({
30
+ status: "405",
31
+ code: "method_not_allowed",
32
+ title: "Method not allowed",
33
+ detail: `Allowed methods: ${context.response.headers.allow}`,
34
+ });
35
+ };
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@jsonapi-serde/integration-koa",
3
+ "version": "0.0.0",
4
+ "description": "Koa integration for @jsonapi-serde/server",
5
+ "type": "module",
6
+ "author": "Ben Scholzen 'DASPRiD'",
7
+ "license": "BSD-3-Clause",
8
+ "keywords": [
9
+ "jsonapi",
10
+ "typescript",
11
+ "zod",
12
+ "serializer",
13
+ "deserializer",
14
+ "parser",
15
+ "koa"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/dasprid/jsonapi-serde-js.git"
20
+ },
21
+ "files": [
22
+ "dist/**/*"
23
+ ],
24
+ "exports": {
25
+ ".": {
26
+ "types": "./src/index.ts",
27
+ "@jsonapi-serde": "./src/index.ts",
28
+ "default": "./dist/index.js"
29
+ }
30
+ },
31
+ "devDependencies": {
32
+ "@types/http-errors": "^2.0.4",
33
+ "@types/koa": "^2.15.0",
34
+ "http-errors": "^2.0.0",
35
+ "koa": "^3.0.0",
36
+ "koa-tree-router": "^0.13.1",
37
+ "@jsonapi-serde/server": "^0.0.0"
38
+ },
39
+ "peerDependencies": {
40
+ "koa": "^2.0.0 || ^3.0.0",
41
+ "http-errors": "^1.0.0 || ^2.0.0",
42
+ "@jsonapi-serde/server": "^0.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "tsc -p tsconfig.build.json",
46
+ "test": "tsx -C jsonapi-serde --test --test-reporter=spec --experimental-test-module-mocks --no-warnings=ExperimentalWarning test/**/*.ts",
47
+ "typecheck": "tsc --noEmit",
48
+ "ci:test": "c8 --reporter=lcov pnpm test"
49
+ }
50
+ }