@danmat/query-server 0.1.1 → 0.1.2

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["formatAcceptQuery","negotiateQuery"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @danmat/query-server\n *\n * Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008),\n * built on Web-standard `Request`/`Response` — so they work in Hono, Deno,\n * Bun, Cloudflare Workers, and Node (via a web adapter).\n *\n * Validate incoming QUERY requests, enforce the RFC's `Content-Type`\n * requirement, negotiate the accepted query formats, and advertise them with\n * the `Accept-Query` response header.\n *\n * @see https://www.rfc-editor.org/rfc/rfc10008\n */\n\nimport {\n formatAcceptQuery,\n negotiateQuery,\n type MediaRangeInput,\n} from \"@danmat/accept-query\";\n\n/** The QUERY method name. */\nexport const QUERY_METHOD = \"QUERY\";\n\nexport interface QueryRequestOptions {\n /**\n * Also accept `POST` requests carrying `X-HTTP-Method-Override: QUERY` as\n * QUERY requests — the fallback that clients like `@danmat/query-fetch` use\n * for servers that don't route QUERY natively. Default `true`.\n */\n allowMethodOverride?: boolean;\n}\n\nexport interface QueryValidationOptions extends QueryRequestOptions {\n /**\n * Media types this server accepts as query bodies. When provided, requests\n * with an unsupported `Content-Type` are rejected with `415` and an\n * `Accept-Query` header advertising these types.\n */\n accept?: MediaRangeInput[];\n}\n\n/**\n * An error describing why a request is not a valid QUERY, carrying the HTTP\n * status and headers that should be sent in response.\n */\nexport class QueryRequestError extends Error {\n override name = \"QueryRequestError\";\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(message: string, status: number, headers: Record<string, string> = {}) {\n super(message);\n this.status = status;\n this.headers = headers;\n Object.setPrototypeOf(this, QueryRequestError.prototype);\n }\n\n /** Render this error as a JSON `Response` with the appropriate status/headers. */\n toResponse(): Response {\n return new Response(JSON.stringify({ error: this.message }), {\n status: this.status,\n headers: { \"content-type\": \"application/json\", ...this.headers },\n });\n }\n}\n\n/** Whether a request should be handled as a QUERY (honoring method override). */\nexport function isQueryRequest(\n request: Request,\n options: QueryRequestOptions = {},\n): boolean {\n const { allowMethodOverride = true } = options;\n const method = request.method.toUpperCase();\n if (method === QUERY_METHOD) return true;\n if (allowMethodOverride && method === \"POST\") {\n return (\n (request.headers.get(\"x-http-method-override\") ?? \"\").toUpperCase() ===\n QUERY_METHOD\n );\n }\n return false;\n}\n\n/** Build an `Accept-Query` header value from the media types a server accepts. */\nexport function acceptQueryHeader(mediaTypes: MediaRangeInput[]): string {\n return formatAcceptQuery(mediaTypes);\n}\n\nfunction isAccepted(contentType: string, accept: MediaRangeInput[]): boolean {\n return negotiateQuery(formatAcceptQuery(accept), [contentType]) !== null;\n}\n\n/**\n * Validate that `request` is a well-formed QUERY per RFC 10008. Throws a\n * {@link QueryRequestError} (with the right HTTP status) when it isn't:\n *\n * - not a QUERY request → `405 Method Not Allowed` (`Allow: QUERY`)\n * - missing `Content-Type` → `400 Bad Request`\n * - unsupported `Content-Type` (when `accept` is given) → `415` + `Accept-Query`\n */\nexport function assertQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): void {\n const { accept, allowMethodOverride = true } = options;\n\n if (!isQueryRequest(request, { allowMethodOverride })) {\n throw new QueryRequestError(\n `Expected a ${QUERY_METHOD} request but received ${request.method}.`,\n 405,\n { allow: QUERY_METHOD },\n );\n }\n\n const contentType = request.headers.get(\"content-type\");\n if (!contentType) {\n throw new QueryRequestError(\n \"A QUERY request must include a Content-Type (RFC 10008).\",\n 400,\n );\n }\n\n if (accept && accept.length > 0 && !isAccepted(contentType, accept)) {\n throw new QueryRequestError(\n `Unsupported query media type \"${contentType}\".`,\n 415,\n { \"accept-query\": acceptQueryHeader(accept) },\n );\n }\n}\n\n/**\n * Non-throwing companion to {@link assertQueryRequest}. Returns an error\n * `Response` to send back, or `null` if the request is valid.\n *\n * @example\n * ```ts\n * const bad = checkQueryRequest(request, { accept: [\"application/json\"] });\n * if (bad) return bad;\n * const query = await readQueryJson(request);\n * ```\n */\nexport function checkQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): Response | null {\n try {\n assertQueryRequest(request, options);\n return null;\n } catch (error) {\n if (error instanceof QueryRequestError) return error.toResponse();\n throw error;\n }\n}\n\n/**\n * Read a QUERY request body as JSON, guarding the content type. Throws a\n * {@link QueryRequestError} (`415` for a non-JSON type, `400` for invalid JSON).\n */\nexport async function readQueryJson<T = unknown>(\n request: Request,\n): Promise<T> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n if (!/\\bjson\\b/i.test(contentType)) {\n throw new QueryRequestError(\n `Expected a JSON query body but Content-Type was \"${contentType || \"absent\"}\".`,\n 415,\n );\n }\n try {\n return (await request.json()) as T;\n } catch {\n throw new QueryRequestError(\"Query body is not valid JSON.\", 400);\n }\n}\n\n/** Return a copy of `response` with an `Accept-Query` header advertising `mediaTypes`. */\nexport function withAcceptQuery(\n response: Response,\n mediaTypes: MediaRangeInput[],\n): Response {\n const headers = new Headers(response.headers);\n headers.set(\"accept-query\", acceptQueryHeader(mediaTypes));\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["formatAcceptQuery","negotiateQuery"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @danmat/query-server\n *\n * Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008),\n * built on Web-standard `Request`/`Response` — so they work in Hono, Deno,\n * Bun, Cloudflare Workers, and Node (via a web adapter).\n *\n * Validate incoming QUERY requests, enforce the RFC's `Content-Type`\n * requirement, negotiate the accepted query formats, and advertise them with\n * the `Accept-Query` response header.\n *\n * @see https://www.rfc-editor.org/rfc/rfc10008\n */\n\nimport {\n formatAcceptQuery,\n type MediaRangeInput,\n negotiateQuery,\n} from \"@danmat/accept-query\";\n\n/** The QUERY method name. */\nexport const QUERY_METHOD = \"QUERY\";\n\nexport interface QueryRequestOptions {\n /**\n * Also accept `POST` requests carrying `X-HTTP-Method-Override: QUERY` as\n * QUERY requests — the fallback that clients like `@danmat/query-fetch` use\n * for servers that don't route QUERY natively. Default `true`.\n */\n allowMethodOverride?: boolean;\n}\n\nexport interface QueryValidationOptions extends QueryRequestOptions {\n /**\n * Media types this server accepts as query bodies. When provided, requests\n * with an unsupported `Content-Type` are rejected with `415` and an\n * `Accept-Query` header advertising these types.\n */\n accept?: MediaRangeInput[];\n}\n\n/**\n * An error describing why a request is not a valid QUERY, carrying the HTTP\n * status and headers that should be sent in response.\n */\nexport class QueryRequestError extends Error {\n override name = \"QueryRequestError\";\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(\n message: string,\n status: number,\n headers: Record<string, string> = {},\n ) {\n super(message);\n this.status = status;\n this.headers = headers;\n Object.setPrototypeOf(this, QueryRequestError.prototype);\n }\n\n /** Render this error as a JSON `Response` with the appropriate status/headers. */\n toResponse(): Response {\n return new Response(JSON.stringify({ error: this.message }), {\n status: this.status,\n headers: { \"content-type\": \"application/json\", ...this.headers },\n });\n }\n}\n\n/** Whether a request should be handled as a QUERY (honoring method override). */\nexport function isQueryRequest(\n request: Request,\n options: QueryRequestOptions = {},\n): boolean {\n const { allowMethodOverride = true } = options;\n const method = request.method.toUpperCase();\n if (method === QUERY_METHOD) return true;\n if (allowMethodOverride && method === \"POST\") {\n return (\n (request.headers.get(\"x-http-method-override\") ?? \"\").toUpperCase() ===\n QUERY_METHOD\n );\n }\n return false;\n}\n\n/** Build an `Accept-Query` header value from the media types a server accepts. */\nexport function acceptQueryHeader(mediaTypes: MediaRangeInput[]): string {\n return formatAcceptQuery(mediaTypes);\n}\n\nfunction isAccepted(contentType: string, accept: MediaRangeInput[]): boolean {\n return negotiateQuery(formatAcceptQuery(accept), [contentType]) !== null;\n}\n\n/**\n * Validate that `request` is a well-formed QUERY per RFC 10008. Throws a\n * {@link QueryRequestError} (with the right HTTP status) when it isn't:\n *\n * - not a QUERY request → `405 Method Not Allowed` (`Allow: QUERY`)\n * - missing `Content-Type` → `400 Bad Request`\n * - unsupported `Content-Type` (when `accept` is given) → `415` + `Accept-Query`\n */\nexport function assertQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): void {\n const { accept, allowMethodOverride = true } = options;\n\n if (!isQueryRequest(request, { allowMethodOverride })) {\n throw new QueryRequestError(\n `Expected a ${QUERY_METHOD} request but received ${request.method}.`,\n 405,\n { allow: QUERY_METHOD },\n );\n }\n\n const contentType = request.headers.get(\"content-type\");\n if (!contentType) {\n throw new QueryRequestError(\n \"A QUERY request must include a Content-Type (RFC 10008).\",\n 400,\n );\n }\n\n if (accept && accept.length > 0 && !isAccepted(contentType, accept)) {\n throw new QueryRequestError(\n `Unsupported query media type \"${contentType}\".`,\n 415,\n { \"accept-query\": acceptQueryHeader(accept) },\n );\n }\n}\n\n/**\n * Non-throwing companion to {@link assertQueryRequest}. Returns an error\n * `Response` to send back, or `null` if the request is valid.\n *\n * @example\n * ```ts\n * const bad = checkQueryRequest(request, { accept: [\"application/json\"] });\n * if (bad) return bad;\n * const query = await readQueryJson(request);\n * ```\n */\nexport function checkQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): Response | null {\n try {\n assertQueryRequest(request, options);\n return null;\n } catch (error) {\n if (error instanceof QueryRequestError) return error.toResponse();\n throw error;\n }\n}\n\n/**\n * Read a QUERY request body as JSON, guarding the content type. Throws a\n * {@link QueryRequestError} (`415` for a non-JSON type, `400` for invalid JSON).\n */\nexport async function readQueryJson<T = unknown>(request: Request): Promise<T> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n if (!/\\bjson\\b/i.test(contentType)) {\n throw new QueryRequestError(\n `Expected a JSON query body but Content-Type was \"${contentType || \"absent\"}\".`,\n 415,\n );\n }\n try {\n return (await request.json()) as T;\n } catch {\n throw new QueryRequestError(\"Query body is not valid JSON.\", 400);\n }\n}\n\n/** Return a copy of `response` with an `Accept-Query` header advertising `mediaTypes`. */\nexport function withAcceptQuery(\n response: Response,\n mediaTypes: MediaRangeInput[],\n): Response {\n const headers = new Headers(response.headers);\n headers.set(\"accept-query\", acceptQueryHeader(mediaTypes));\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n"]}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @danmat/query-server\n *\n * Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008),\n * built on Web-standard `Request`/`Response` — so they work in Hono, Deno,\n * Bun, Cloudflare Workers, and Node (via a web adapter).\n *\n * Validate incoming QUERY requests, enforce the RFC's `Content-Type`\n * requirement, negotiate the accepted query formats, and advertise them with\n * the `Accept-Query` response header.\n *\n * @see https://www.rfc-editor.org/rfc/rfc10008\n */\n\nimport {\n formatAcceptQuery,\n negotiateQuery,\n type MediaRangeInput,\n} from \"@danmat/accept-query\";\n\n/** The QUERY method name. */\nexport const QUERY_METHOD = \"QUERY\";\n\nexport interface QueryRequestOptions {\n /**\n * Also accept `POST` requests carrying `X-HTTP-Method-Override: QUERY` as\n * QUERY requests — the fallback that clients like `@danmat/query-fetch` use\n * for servers that don't route QUERY natively. Default `true`.\n */\n allowMethodOverride?: boolean;\n}\n\nexport interface QueryValidationOptions extends QueryRequestOptions {\n /**\n * Media types this server accepts as query bodies. When provided, requests\n * with an unsupported `Content-Type` are rejected with `415` and an\n * `Accept-Query` header advertising these types.\n */\n accept?: MediaRangeInput[];\n}\n\n/**\n * An error describing why a request is not a valid QUERY, carrying the HTTP\n * status and headers that should be sent in response.\n */\nexport class QueryRequestError extends Error {\n override name = \"QueryRequestError\";\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(message: string, status: number, headers: Record<string, string> = {}) {\n super(message);\n this.status = status;\n this.headers = headers;\n Object.setPrototypeOf(this, QueryRequestError.prototype);\n }\n\n /** Render this error as a JSON `Response` with the appropriate status/headers. */\n toResponse(): Response {\n return new Response(JSON.stringify({ error: this.message }), {\n status: this.status,\n headers: { \"content-type\": \"application/json\", ...this.headers },\n });\n }\n}\n\n/** Whether a request should be handled as a QUERY (honoring method override). */\nexport function isQueryRequest(\n request: Request,\n options: QueryRequestOptions = {},\n): boolean {\n const { allowMethodOverride = true } = options;\n const method = request.method.toUpperCase();\n if (method === QUERY_METHOD) return true;\n if (allowMethodOverride && method === \"POST\") {\n return (\n (request.headers.get(\"x-http-method-override\") ?? \"\").toUpperCase() ===\n QUERY_METHOD\n );\n }\n return false;\n}\n\n/** Build an `Accept-Query` header value from the media types a server accepts. */\nexport function acceptQueryHeader(mediaTypes: MediaRangeInput[]): string {\n return formatAcceptQuery(mediaTypes);\n}\n\nfunction isAccepted(contentType: string, accept: MediaRangeInput[]): boolean {\n return negotiateQuery(formatAcceptQuery(accept), [contentType]) !== null;\n}\n\n/**\n * Validate that `request` is a well-formed QUERY per RFC 10008. Throws a\n * {@link QueryRequestError} (with the right HTTP status) when it isn't:\n *\n * - not a QUERY request → `405 Method Not Allowed` (`Allow: QUERY`)\n * - missing `Content-Type` → `400 Bad Request`\n * - unsupported `Content-Type` (when `accept` is given) → `415` + `Accept-Query`\n */\nexport function assertQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): void {\n const { accept, allowMethodOverride = true } = options;\n\n if (!isQueryRequest(request, { allowMethodOverride })) {\n throw new QueryRequestError(\n `Expected a ${QUERY_METHOD} request but received ${request.method}.`,\n 405,\n { allow: QUERY_METHOD },\n );\n }\n\n const contentType = request.headers.get(\"content-type\");\n if (!contentType) {\n throw new QueryRequestError(\n \"A QUERY request must include a Content-Type (RFC 10008).\",\n 400,\n );\n }\n\n if (accept && accept.length > 0 && !isAccepted(contentType, accept)) {\n throw new QueryRequestError(\n `Unsupported query media type \"${contentType}\".`,\n 415,\n { \"accept-query\": acceptQueryHeader(accept) },\n );\n }\n}\n\n/**\n * Non-throwing companion to {@link assertQueryRequest}. Returns an error\n * `Response` to send back, or `null` if the request is valid.\n *\n * @example\n * ```ts\n * const bad = checkQueryRequest(request, { accept: [\"application/json\"] });\n * if (bad) return bad;\n * const query = await readQueryJson(request);\n * ```\n */\nexport function checkQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): Response | null {\n try {\n assertQueryRequest(request, options);\n return null;\n } catch (error) {\n if (error instanceof QueryRequestError) return error.toResponse();\n throw error;\n }\n}\n\n/**\n * Read a QUERY request body as JSON, guarding the content type. Throws a\n * {@link QueryRequestError} (`415` for a non-JSON type, `400` for invalid JSON).\n */\nexport async function readQueryJson<T = unknown>(\n request: Request,\n): Promise<T> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n if (!/\\bjson\\b/i.test(contentType)) {\n throw new QueryRequestError(\n `Expected a JSON query body but Content-Type was \"${contentType || \"absent\"}\".`,\n 415,\n );\n }\n try {\n return (await request.json()) as T;\n } catch {\n throw new QueryRequestError(\"Query body is not valid JSON.\", 400);\n }\n}\n\n/** Return a copy of `response` with an `Accept-Query` header advertising `mediaTypes`. */\nexport function withAcceptQuery(\n response: Response,\n mediaTypes: MediaRangeInput[],\n): Response {\n const headers = new Headers(response.headers);\n headers.set(\"accept-query\", acceptQueryHeader(mediaTypes));\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * @danmat/query-server\n *\n * Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008),\n * built on Web-standard `Request`/`Response` — so they work in Hono, Deno,\n * Bun, Cloudflare Workers, and Node (via a web adapter).\n *\n * Validate incoming QUERY requests, enforce the RFC's `Content-Type`\n * requirement, negotiate the accepted query formats, and advertise them with\n * the `Accept-Query` response header.\n *\n * @see https://www.rfc-editor.org/rfc/rfc10008\n */\n\nimport {\n formatAcceptQuery,\n type MediaRangeInput,\n negotiateQuery,\n} from \"@danmat/accept-query\";\n\n/** The QUERY method name. */\nexport const QUERY_METHOD = \"QUERY\";\n\nexport interface QueryRequestOptions {\n /**\n * Also accept `POST` requests carrying `X-HTTP-Method-Override: QUERY` as\n * QUERY requests — the fallback that clients like `@danmat/query-fetch` use\n * for servers that don't route QUERY natively. Default `true`.\n */\n allowMethodOverride?: boolean;\n}\n\nexport interface QueryValidationOptions extends QueryRequestOptions {\n /**\n * Media types this server accepts as query bodies. When provided, requests\n * with an unsupported `Content-Type` are rejected with `415` and an\n * `Accept-Query` header advertising these types.\n */\n accept?: MediaRangeInput[];\n}\n\n/**\n * An error describing why a request is not a valid QUERY, carrying the HTTP\n * status and headers that should be sent in response.\n */\nexport class QueryRequestError extends Error {\n override name = \"QueryRequestError\";\n readonly status: number;\n readonly headers: Record<string, string>;\n\n constructor(\n message: string,\n status: number,\n headers: Record<string, string> = {},\n ) {\n super(message);\n this.status = status;\n this.headers = headers;\n Object.setPrototypeOf(this, QueryRequestError.prototype);\n }\n\n /** Render this error as a JSON `Response` with the appropriate status/headers. */\n toResponse(): Response {\n return new Response(JSON.stringify({ error: this.message }), {\n status: this.status,\n headers: { \"content-type\": \"application/json\", ...this.headers },\n });\n }\n}\n\n/** Whether a request should be handled as a QUERY (honoring method override). */\nexport function isQueryRequest(\n request: Request,\n options: QueryRequestOptions = {},\n): boolean {\n const { allowMethodOverride = true } = options;\n const method = request.method.toUpperCase();\n if (method === QUERY_METHOD) return true;\n if (allowMethodOverride && method === \"POST\") {\n return (\n (request.headers.get(\"x-http-method-override\") ?? \"\").toUpperCase() ===\n QUERY_METHOD\n );\n }\n return false;\n}\n\n/** Build an `Accept-Query` header value from the media types a server accepts. */\nexport function acceptQueryHeader(mediaTypes: MediaRangeInput[]): string {\n return formatAcceptQuery(mediaTypes);\n}\n\nfunction isAccepted(contentType: string, accept: MediaRangeInput[]): boolean {\n return negotiateQuery(formatAcceptQuery(accept), [contentType]) !== null;\n}\n\n/**\n * Validate that `request` is a well-formed QUERY per RFC 10008. Throws a\n * {@link QueryRequestError} (with the right HTTP status) when it isn't:\n *\n * - not a QUERY request → `405 Method Not Allowed` (`Allow: QUERY`)\n * - missing `Content-Type` → `400 Bad Request`\n * - unsupported `Content-Type` (when `accept` is given) → `415` + `Accept-Query`\n */\nexport function assertQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): void {\n const { accept, allowMethodOverride = true } = options;\n\n if (!isQueryRequest(request, { allowMethodOverride })) {\n throw new QueryRequestError(\n `Expected a ${QUERY_METHOD} request but received ${request.method}.`,\n 405,\n { allow: QUERY_METHOD },\n );\n }\n\n const contentType = request.headers.get(\"content-type\");\n if (!contentType) {\n throw new QueryRequestError(\n \"A QUERY request must include a Content-Type (RFC 10008).\",\n 400,\n );\n }\n\n if (accept && accept.length > 0 && !isAccepted(contentType, accept)) {\n throw new QueryRequestError(\n `Unsupported query media type \"${contentType}\".`,\n 415,\n { \"accept-query\": acceptQueryHeader(accept) },\n );\n }\n}\n\n/**\n * Non-throwing companion to {@link assertQueryRequest}. Returns an error\n * `Response` to send back, or `null` if the request is valid.\n *\n * @example\n * ```ts\n * const bad = checkQueryRequest(request, { accept: [\"application/json\"] });\n * if (bad) return bad;\n * const query = await readQueryJson(request);\n * ```\n */\nexport function checkQueryRequest(\n request: Request,\n options: QueryValidationOptions = {},\n): Response | null {\n try {\n assertQueryRequest(request, options);\n return null;\n } catch (error) {\n if (error instanceof QueryRequestError) return error.toResponse();\n throw error;\n }\n}\n\n/**\n * Read a QUERY request body as JSON, guarding the content type. Throws a\n * {@link QueryRequestError} (`415` for a non-JSON type, `400` for invalid JSON).\n */\nexport async function readQueryJson<T = unknown>(request: Request): Promise<T> {\n const contentType = request.headers.get(\"content-type\") ?? \"\";\n if (!/\\bjson\\b/i.test(contentType)) {\n throw new QueryRequestError(\n `Expected a JSON query body but Content-Type was \"${contentType || \"absent\"}\".`,\n 415,\n );\n }\n try {\n return (await request.json()) as T;\n } catch {\n throw new QueryRequestError(\"Query body is not valid JSON.\", 400);\n }\n}\n\n/** Return a copy of `response` with an `Accept-Query` header advertising `mediaTypes`. */\nexport function withAcceptQuery(\n response: Response,\n mediaTypes: MediaRangeInput[],\n): Response {\n const headers = new Headers(response.headers);\n headers.set(\"accept-query\", acceptQueryHeader(mediaTypes));\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers,\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmat/query-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Framework-agnostic server helpers for the HTTP QUERY method (RFC 10008): validate requests, negotiate content types, and advertise Accept-Query. Built on Web-standard Request/Response.",
5
5
  "keywords": [
6
6
  "http",
@@ -39,7 +39,10 @@
39
39
  "test:watch": "vitest",
40
40
  "coverage": "vitest run --coverage",
41
41
  "typecheck": "tsc --noEmit",
42
- "prepublishOnly": "npm run build"
42
+ "prepublishOnly": "npm run build",
43
+ "lint": "biome check .",
44
+ "format": "biome check --write .",
45
+ "prepare": "husky"
43
46
  },
44
47
  "author": "Dan Matthew <dannymatthew@gmail.com>",
45
48
  "license": "MIT",
@@ -58,9 +61,15 @@
58
61
  "@danmat/accept-query": "^0.1.0"
59
62
  },
60
63
  "devDependencies": {
64
+ "@biomejs/biome": "^2.5.2",
61
65
  "@vitest/coverage-v8": "^2.1.8",
66
+ "husky": "^9.1.7",
67
+ "lint-staged": "^15.5.2",
62
68
  "tsup": "^8.3.5",
63
69
  "typescript": "^5.7.2",
64
70
  "vitest": "^2.1.8"
71
+ },
72
+ "lint-staged": {
73
+ "*.{ts,js,json,jsonc,md}": "biome check --write --no-errors-on-unmatched"
65
74
  }
66
75
  }