@dotpress/adapter-fastify 2.0.0-alpha.4 → 2.0.0-alpha.5

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.
@@ -0,0 +1,16 @@
1
+ // src/options.ts
2
+ var assertKnownOptions = (label, options, known, prefix = "") => {
3
+ if (typeof options !== "object" || options === null) {
4
+ throw new TypeError(`${label}: options must be an object.`);
5
+ }
6
+ for (const key of Object.keys(options)) {
7
+ if (!known.includes(key)) {
8
+ throw new TypeError(`${label}: unknown option "${prefix}${key}".`);
9
+ }
10
+ }
11
+ };
12
+
13
+ export {
14
+ assertKnownOptions
15
+ };
16
+ //# sourceMappingURL=chunk-DWK6R675.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/options.ts"],"sourcesContent":["/**\n * Throws a `TypeError` for a key of `options` that is not in `known`: a\n * misspelled option would otherwise be ignored without a word.\n */\nexport const assertKnownOptions = (\n label: string,\n options: object,\n known: readonly string[],\n prefix = ''\n): void => {\n if (typeof options !== 'object' || options === null) {\n throw new TypeError(`${label}: options must be an object.`)\n }\n for (const key of Object.keys(options)) {\n if (!known.includes(key)) {\n throw new TypeError(`${label}: unknown option \"${prefix}${key}\".`)\n }\n }\n}\n"],"mappings":";AAIO,IAAM,qBAAqB,CAChC,OACA,SACA,OACA,SAAS,OACA;AACT,MAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACnD,UAAM,IAAI,UAAU,GAAG,KAAK,8BAA8B;AAAA,EAC5D;AACA,aAAW,OAAO,OAAO,KAAK,OAAO,GAAG;AACtC,QAAI,CAAC,MAAM,SAAS,GAAG,GAAG;AACxB,YAAM,IAAI,UAAU,GAAG,KAAK,qBAAqB,MAAM,GAAG,GAAG,IAAI;AAAA,IACnE;AAAA,EACF;AACF;","names":[]}
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { Server } from 'node:http';
2
- import { HttpAdapter } from '@dotpress/core';
2
+ import { HttpAdapter, ListenAddress } from '@dotpress/core';
3
3
  import { FastifyInstance } from 'fastify';
4
4
 
5
- interface FastifyAdapterOptions<TApp extends FastifyInstance> {
5
+ interface FastifyAdapterOptions<TApp extends FastifyInstance = FastifyInstance> {
6
6
  /** Fastify instance to mount the routes on. Defaults to `Fastify()`. */
7
7
  app?: TApp;
8
8
  /**
@@ -27,7 +27,7 @@ interface FastifyAdapter<TApp extends FastifyInstance = FastifyInstance> extends
27
27
  * Starts the HTTP server (`fastify.listen()`). Without `host`, listens on
28
28
  * every interface, like Node.js.
29
29
  */
30
- listen(port: number, host?: string): Promise<void>;
30
+ listen(port: number, host?: string): Promise<ListenAddress>;
31
31
  /**
32
32
  * Closes the Fastify instance (`onClose` hooks of the plugins): waits for
33
33
  * pending requests, and closes the responses being streamed (their
package/dist/index.js CHANGED
@@ -1,11 +1,21 @@
1
+ import {
2
+ assertKnownOptions
3
+ } from "./chunk-DWK6R675.js";
4
+
1
5
  // src/index.ts
6
+ import {
7
+ HttpError
8
+ } from "@dotpress/core";
2
9
  import {
3
10
  DEFAULT_BODY_LIMIT,
4
- HttpError,
5
11
  parseQueryString,
6
12
  readRequestBody
7
- } from "@dotpress/core";
13
+ } from "@dotpress/core/adapter";
8
14
  import Fastify from "fastify";
15
+ var addressOf = (server) => {
16
+ const address = server.address();
17
+ return { port: address.port, host: address.address };
18
+ };
9
19
  var trackConnections = (server) => {
10
20
  const sockets = /* @__PURE__ */ new Set();
11
21
  let closing = false;
@@ -107,7 +117,6 @@ var fromNativeError = (error) => {
107
117
  statusCode,
108
118
  ERROR_CODES[statusCode] ?? "CLIENT_ERROR",
109
119
  error.message || "Bad Request",
110
- void 0,
111
120
  { cause: error }
112
121
  );
113
122
  };
@@ -121,10 +130,16 @@ var waitForDrain = (res) => new Promise((resolve) => {
121
130
  res.on("close", done);
122
131
  });
123
132
  function fastifyAdapter(options = {}) {
133
+ assertKnownOptions("fastifyAdapter()", options, [
134
+ "app",
135
+ "beforeRoutes",
136
+ "afterRoutes",
137
+ "bodyLimit"
138
+ ]);
124
139
  const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
125
140
  if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {
126
141
  throw new RangeError(
127
- "Invalid bodyLimit: expected a non-negative integer (bytes)."
142
+ "fastifyAdapter(): bodyLimit must be a non-negative integer (bytes)."
128
143
  );
129
144
  }
130
145
  const application = options.app ?? Fastify();
@@ -276,6 +291,9 @@ function fastifyAdapter(options = {}) {
276
291
  await app.ready();
277
292
  },
278
293
  listen: async (port, host) => {
294
+ if (listening) {
295
+ throw new Error("The Fastify adapter is already listening.");
296
+ }
279
297
  const app = application;
280
298
  try {
281
299
  await app.listen({ port, host: host ?? "::" });
@@ -286,6 +304,7 @@ function fastifyAdapter(options = {}) {
286
304
  }
287
305
  listening = true;
288
306
  closeConnections = trackConnections(app.server);
307
+ return addressOf(app.server);
289
308
  },
290
309
  close: async () => {
291
310
  for (const stop of [...streams]) stop();
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { Socket } from 'node:net'\nimport type { Readable } from 'node:stream'\nimport {\n DEFAULT_BODY_LIMIT,\n HttpError,\n parseQueryString,\n readRequestBody,\n type CompiledRoute,\n type DotpressRuntime,\n type HttpAdapter,\n type NormalizedRequest,\n type NormalizedResponse,\n type RequestBody,\n} from '@dotpress/core'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n} from 'fastify'\n\nexport interface FastifyAdapterOptions<TApp extends FastifyInstance> {\n /** Fastify instance to mount the routes on. Defaults to `Fastify()`. */\n app?: TApp\n /**\n * Registers hooks and plugins applied to the Dotpress routes\n * (`@fastify/helmet`…), and native routes.\n */\n beforeRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Registers hooks, plugins and native routes in an encapsulated context:\n * they do not apply to the Dotpress routes (except plugins wrapped with\n * `fastify-plugin`, which apply to the parent context).\n */\n afterRoutes?: (app: FastifyInstance) => void | Promise<void>\n /**\n * Maximum request body size, in bytes. Defaults to 1 MiB. Routes can\n * override it with their own `bodyLimit`.\n */\n bodyLimit?: number\n}\n\nexport interface FastifyAdapter<\n TApp extends FastifyInstance = FastifyInstance,\n> extends HttpAdapter<TApp> {\n /**\n * Starts the HTTP server (`fastify.listen()`). Without `host`, listens on\n * every interface, like Node.js.\n */\n listen(port: number, host?: string): Promise<void>\n /**\n * Closes the Fastify instance (`onClose` hooks of the plugins): waits for\n * pending requests, and closes the responses being streamed (their\n * `req.signal` is aborted).\n */\n close(): Promise<void>\n /** HTTP server started by `listen()`, if any. */\n server(): Server | undefined\n}\n\n/**\n * Lets `close()` complete as soon as the pending requests are served, and\n * returns the function to call on close. Before Node.js 26, `server.close()`\n * waits for the connections that have sent nothing yet (a browser\n * preconnect…) until `headersTimeout`; and, whatever the version, the\n * connections of the pending requests stay open once served, until\n * `keepAliveTimeout`. Fastify keeps listening during its `preClose` hooks:\n * the connections accepted from then on are destroyed right away.\n */\nconst trackConnections = (server: Server) => {\n const sockets = new Set<Socket>()\n let closing = false\n server.on('connection', (socket: Socket) => {\n if (closing) {\n socket.destroy()\n return\n }\n sockets.add(socket)\n socket.once('close', () => sockets.delete(socket))\n })\n server.on(\n 'request',\n (_request: IncomingMessage, response: ServerResponse) => {\n response.once('finish', () => {\n if (closing) server.closeIdleConnections()\n })\n }\n )\n return () => {\n closing = true\n for (const socket of sockets) {\n if (socket.bytesRead === 0) socket.destroy()\n }\n }\n}\n\n/** Rejection reason when the client goes away while the body is read. */\nconst CLIENT_GONE = Symbol('client gone')\n\nconst encoder = new TextEncoder()\n\n/**\n * Web stream of a request body. Cancelling it discards the rest of the body\n * instead of destroying the socket, so that the error response can still\n * be sent.\n */\nconst toWebStream = (payload: Readable): ReadableStream<Uint8Array> => {\n let cleanup = () => {}\n return new ReadableStream<Uint8Array>({\n start(controller) {\n const onData = (chunk: Buffer | string) => {\n // Streams of `preParsing` hooks may produce strings.\n controller.enqueue(\n typeof chunk === 'string'\n ? encoder.encode(chunk)\n : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)\n )\n }\n const onEnd = () => {\n cleanup()\n controller.close()\n }\n // Also emitted when the client goes away (`ECONNRESET`).\n const onError = () => {\n cleanup()\n controller.error(CLIENT_GONE)\n }\n cleanup = () => {\n payload.off('data', onData)\n payload.off('end', onEnd)\n payload.off('error', onError)\n }\n payload.on('data', onData)\n payload.on('end', onEnd)\n payload.on('error', onError)\n },\n cancel() {\n // Still flowing without listener: the rest is discarded.\n cleanup()\n },\n })\n}\n\n/**\n * Reads the body with the rules of the core (`readRequestBody()`), from the\n * payload given by Fastify (after its `preParsing` hooks). Fastify does not\n * read the body of `GET` and `HEAD` requests.\n */\nconst readBody = (\n request: FastifyRequest,\n limit: number\n): Promise<RequestBody> => {\n const payload = request.body as Readable | undefined\n const headers = new Headers()\n for (const name of ['content-type', 'content-encoding', 'content-length']) {\n const value = request.headers[name]\n if (typeof value === 'string') headers.set(name, value)\n }\n return readRequestBody(\n new Request('http://localhost/', {\n method: 'POST',\n headers,\n body: payload ? toWebStream(payload) : null,\n duplex: 'half',\n } as RequestInit),\n limit\n )\n}\n\n/** Code of client errors. */\nconst ERROR_CODES: Record<number, string> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 408: 'REQUEST_TIMEOUT',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'TOO_MANY_REQUESTS',\n}\n\n/**\n * Converts the client errors (`statusCode` 4xx) raised by Fastify and its\n * plugins into Dotpress errors, with the same status and message. Anything\n * else is an unexpected error.\n */\nconst fromNativeError = (error: unknown): unknown => {\n if (!(error instanceof Error)) return error\n\n const { statusCode } = error as { statusCode?: unknown }\n if (typeof statusCode !== 'number' || !Number.isInteger(statusCode)) {\n return error\n }\n if (statusCode < 400 || statusCode > 499) return error\n\n return new HttpError(\n statusCode,\n ERROR_CODES[statusCode] ?? 'CLIENT_ERROR',\n error.message || 'Bad Request',\n undefined,\n { cause: error }\n )\n}\n\nconst waitForDrain = (res: ServerResponse) =>\n new Promise<void>((resolve) => {\n const done = () => {\n res.off('drain', done)\n res.off('close', done)\n resolve()\n }\n res.on('drain', done)\n res.on('close', done)\n })\n\n/**\n * Creates the Fastify adapter (Fastify 5).\n *\n * @example\n * const app = await createApp({\n * adapter: fastifyAdapter({\n * beforeRoutes: async (app) => {\n * await app.register(helmet)\n * },\n * }),\n * })\n * await app.listen(3000)\n */\nexport function fastifyAdapter(\n options?: FastifyAdapterOptions<FastifyInstance>\n): FastifyAdapter\nexport function fastifyAdapter<TApp extends FastifyInstance>(\n options: FastifyAdapterOptions<TApp> & { app: TApp }\n): FastifyAdapter<TApp>\nexport function fastifyAdapter<TApp extends FastifyInstance>(\n options: FastifyAdapterOptions<TApp> = {}\n): FastifyAdapter<TApp> {\n const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT\n\n if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {\n throw new RangeError(\n 'Invalid bodyLimit: expected a non-negative integer (bytes).'\n )\n }\n\n const application = options.app ?? (Fastify() as unknown as TApp)\n let mounted = false\n let listening = false\n let closeConnections = () => {}\n /** Closes the responses being streamed, so that `close()` can complete. */\n const streams = new Set<() => void>()\n\n const normalize = (\n request: FastifyRequest,\n signal?: AbortSignal\n ): NormalizedRequest => {\n const url = request.url\n const index = url.indexOf('?')\n return {\n method: request.method,\n path: index === -1 ? url : url.slice(0, index),\n url: `${request.protocol}://${request.host}${url}`,\n params: {},\n query: parseQueryString(index === -1 ? '' : url.slice(index)),\n headers: request.headers,\n body: undefined,\n signal,\n ip: request.ip,\n raw: request,\n }\n }\n\n const send = (\n reply: FastifyReply,\n response: NormalizedResponse,\n controller?: AbortController\n ): FastifyReply | Promise<void> => {\n const { body } = response\n if (body instanceof ReadableStream) {\n return sendStream(reply, response, body, controller)\n }\n\n reply.code(response.status)\n for (const [name, value] of Object.entries(response.headers)) {\n reply.header(name, value)\n }\n if (body === null) return reply.send()\n if (typeof body === 'string') return reply.send(body)\n return reply.send(\n Buffer.from(body.buffer, body.byteOffset, body.byteLength)\n )\n }\n\n /**\n * Streams the body with the response of Node.js: Fastify only sends the\n * headers with the first chunk, too late for Server-Sent Events and for\n * streams that fail right away. `onSend` hooks do not apply.\n */\n const sendStream = async (\n reply: FastifyReply,\n response: NormalizedResponse,\n body: ReadableStream<Uint8Array>,\n controller: AbortController | undefined\n ): Promise<void> => {\n reply.hijack()\n const res = reply.raw\n\n res.statusCode = response.status\n // Headers set by hooks (`@fastify/helmet`…), then by Dotpress.\n for (const [name, value] of Object.entries(reply.getHeaders())) {\n if (value !== undefined) res.setHeader(name, value)\n }\n for (const [name, value] of Object.entries(response.headers)) {\n res.setHeader(name, value)\n }\n\n if (reply.request.method === 'HEAD') {\n await body.cancel().catch(() => {})\n res.end()\n return\n }\n\n const reader = body.getReader()\n const cancel = () => {\n reader.cancel().catch(() => {})\n }\n const stop = () => {\n controller?.abort()\n cancel()\n }\n\n res.on('close', cancel)\n streams.add(stop)\n res.flushHeaders()\n\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await waitForDrain(res)\n }\n\n // The reader is cancelled when the client goes away.\n res.end()\n } catch {\n // Ending the response would hide the failure: abort the connection.\n res.destroy()\n } finally {\n res.off('close', cancel)\n streams.delete(stop)\n }\n }\n\n const handleRoute = async (\n route: CompiledRoute,\n request: FastifyRequest,\n reply: FastifyReply,\n runtime: DotpressRuntime\n ) => {\n const controller = new AbortController()\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) controller.abort()\n })\n\n const normalized = normalize(request, controller.signal)\n\n try {\n const { body, files, rawBody } = await readBody(\n request,\n route.definition.bodyLimit ?? bodyLimit\n )\n normalized.body = body\n normalized.files = files\n normalized.rawBody = rawBody\n } catch (error) {\n if (error === CLIENT_GONE) return reply.hijack()\n return send(reply, await runtime.handleError(error, normalized))\n }\n\n normalized.params = { ...(request.params as Record<string, string>) }\n return send(reply, await route.execute(normalized), controller)\n }\n\n return {\n name: 'fastify',\n\n mount: async (\n routes: readonly CompiledRoute[],\n runtime: DotpressRuntime\n ) => {\n if (mounted) throw new Error('The Fastify adapter is already mounted.')\n mounted = true\n\n const app = application as unknown as FastifyInstance\n\n // Set first: encapsulated plugins inherit them when they are created.\n app.setNotFoundHandler(async (request, reply) =>\n send(reply, await runtime.handleNotFound(normalize(request)))\n )\n app.setErrorHandler(async (error, request, reply) =>\n send(\n reply,\n await runtime.handleError(fromNativeError(error), normalize(request))\n )\n )\n\n await options.beforeRoutes?.(application)\n\n await app.register(async (child) => {\n // Dotpress reads the bodies: the payload is passed as is.\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', (_request, payload, done) => {\n done(null, payload)\n })\n\n // Explicit HEAD routes first: GET routes add one otherwise.\n const ordered = [...routes].sort(\n (a, b) => Number(b.method === 'head') - Number(a.method === 'head')\n )\n for (const route of ordered) {\n const method = route.method.toUpperCase() as 'GET'\n const register = (url: string) => {\n child.route({\n method,\n url,\n exposeHeadRoute: true,\n handler: (request, reply) =>\n handleRoute(route, request, reply, runtime),\n })\n }\n\n register(route.path)\n // Trailing slashes are ignored, whatever the router options\n // (`findRoute()` follows `ignoreTrailingSlash`).\n if (\n route.path !== '/' &&\n child.findRoute({ method, url: `${route.path}/` }) === null\n ) {\n register(`${route.path}/`)\n }\n }\n })\n\n // Root hooks apply to every route, whatever the order: only an\n // encapsulated context keeps them away from the Dotpress routes.\n const { afterRoutes } = options\n if (afterRoutes) {\n await app.register(async (scope) => {\n await afterRoutes(scope)\n })\n }\n await app.ready()\n },\n\n listen: async (port: number, host?: string) => {\n const app = application as unknown as FastifyInstance\n try {\n await app.listen({ port, host: host ?? '::' })\n } catch (error) {\n // No IPv6: every IPv4 interface, like Node.js.\n const code = (error as { code?: unknown }).code\n if (host !== undefined || code !== 'EAFNOSUPPORT') throw error\n await app.listen({ port, host: '0.0.0.0' })\n }\n listening = true\n closeConnections = trackConnections(app.server as Server)\n },\n\n close: async () => {\n // Streams never end by themselves: close them.\n for (const stop of [...streams]) stop()\n listening = false\n closeConnections()\n await (application as unknown as FastifyInstance).close()\n },\n\n native: () => application,\n server: () =>\n listening\n ? ((application as unknown as FastifyInstance).server as Server)\n : undefined,\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { IncomingMessage, Server, ServerResponse } from 'node:http'\nimport type { AddressInfo, Socket } from 'node:net'\nimport type { Readable } from 'node:stream'\nimport {\n HttpError,\n type HttpAdapter,\n type NormalizedRequest,\n type NormalizedResponse,\n type ListenAddress,\n} from '@dotpress/core'\nimport {\n DEFAULT_BODY_LIMIT,\n parseQueryString,\n readRequestBody,\n type CompiledRoute,\n type DotpressRuntime,\n type RequestBody,\n} from '@dotpress/core/adapter'\nimport Fastify, {\n type FastifyInstance,\n type FastifyReply,\n type FastifyRequest,\n} from 'fastify'\nimport { assertKnownOptions } from './options.js'\n\nexport interface FastifyAdapterOptions<\n TApp extends FastifyInstance = FastifyInstance,\n> {\n /** Fastify instance to mount the routes on. Defaults to `Fastify()`. */\n app?: TApp\n /**\n * Registers hooks and plugins applied to the Dotpress routes\n * (`@fastify/helmet`…), and native routes.\n */\n beforeRoutes?: (app: TApp) => void | Promise<void>\n /**\n * Registers hooks, plugins and native routes in an encapsulated context:\n * they do not apply to the Dotpress routes (except plugins wrapped with\n * `fastify-plugin`, which apply to the parent context).\n */\n afterRoutes?: (app: FastifyInstance) => void | Promise<void>\n /**\n * Maximum request body size, in bytes. Defaults to 1 MiB. Routes can\n * override it with their own `bodyLimit`.\n */\n bodyLimit?: number\n}\n\nexport interface FastifyAdapter<\n TApp extends FastifyInstance = FastifyInstance,\n> extends HttpAdapter<TApp> {\n /**\n * Starts the HTTP server (`fastify.listen()`). Without `host`, listens on\n * every interface, like Node.js.\n */\n listen(port: number, host?: string): Promise<ListenAddress>\n /**\n * Closes the Fastify instance (`onClose` hooks of the plugins): waits for\n * pending requests, and closes the responses being streamed (their\n * `req.signal` is aborted).\n */\n close(): Promise<void>\n /** HTTP server started by `listen()`, if any. */\n server(): Server | undefined\n}\n\n/** Address of a listening Node.js server. */\nconst addressOf = (server: Server): ListenAddress => {\n const address = server.address() as AddressInfo\n return { port: address.port, host: address.address }\n}\n\n/**\n * Lets `close()` complete as soon as the pending requests are served, and\n * returns the function to call on close. Before Node.js 26, `server.close()`\n * waits for the connections that have sent nothing yet (a browser\n * preconnect…) until `headersTimeout`; and, whatever the version, the\n * connections of the pending requests stay open once served, until\n * `keepAliveTimeout`. Fastify keeps listening during its `preClose` hooks:\n * the connections accepted from then on are destroyed right away.\n */\nconst trackConnections = (server: Server) => {\n const sockets = new Set<Socket>()\n let closing = false\n server.on('connection', (socket: Socket) => {\n if (closing) {\n socket.destroy()\n return\n }\n sockets.add(socket)\n socket.once('close', () => sockets.delete(socket))\n })\n server.on(\n 'request',\n (_request: IncomingMessage, response: ServerResponse) => {\n response.once('finish', () => {\n if (closing) server.closeIdleConnections()\n })\n }\n )\n return () => {\n closing = true\n for (const socket of sockets) {\n if (socket.bytesRead === 0) socket.destroy()\n }\n }\n}\n\n/** Rejection reason when the client goes away while the body is read. */\nconst CLIENT_GONE = Symbol('client gone')\n\nconst encoder = new TextEncoder()\n\n/**\n * Web stream of a request body. Cancelling it discards the rest of the body\n * instead of destroying the socket, so that the error response can still\n * be sent.\n */\nconst toWebStream = (payload: Readable): ReadableStream<Uint8Array> => {\n let cleanup = () => {}\n return new ReadableStream<Uint8Array>({\n start(controller) {\n const onData = (chunk: Buffer | string) => {\n // Streams of `preParsing` hooks may produce strings.\n controller.enqueue(\n typeof chunk === 'string'\n ? encoder.encode(chunk)\n : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength)\n )\n }\n const onEnd = () => {\n cleanup()\n controller.close()\n }\n // Also emitted when the client goes away (`ECONNRESET`).\n const onError = () => {\n cleanup()\n controller.error(CLIENT_GONE)\n }\n cleanup = () => {\n payload.off('data', onData)\n payload.off('end', onEnd)\n payload.off('error', onError)\n }\n payload.on('data', onData)\n payload.on('end', onEnd)\n payload.on('error', onError)\n },\n cancel() {\n // Still flowing without listener: the rest is discarded.\n cleanup()\n },\n })\n}\n\n/**\n * Reads the body with the rules of the core (`readRequestBody()`), from the\n * payload given by Fastify (after its `preParsing` hooks). Fastify does not\n * read the body of `GET` and `HEAD` requests.\n */\nconst readBody = (\n request: FastifyRequest,\n limit: number\n): Promise<RequestBody> => {\n const payload = request.body as Readable | undefined\n const headers = new Headers()\n for (const name of ['content-type', 'content-encoding', 'content-length']) {\n const value = request.headers[name]\n if (typeof value === 'string') headers.set(name, value)\n }\n return readRequestBody(\n new Request('http://localhost/', {\n method: 'POST',\n headers,\n body: payload ? toWebStream(payload) : null,\n duplex: 'half',\n } as RequestInit),\n limit\n )\n}\n\n/** Code of client errors. */\nconst ERROR_CODES: Record<number, string> = {\n 400: 'BAD_REQUEST',\n 401: 'UNAUTHORIZED',\n 403: 'FORBIDDEN',\n 404: 'NOT_FOUND',\n 405: 'METHOD_NOT_ALLOWED',\n 408: 'REQUEST_TIMEOUT',\n 409: 'CONFLICT',\n 413: 'PAYLOAD_TOO_LARGE',\n 415: 'UNSUPPORTED_MEDIA_TYPE',\n 422: 'UNPROCESSABLE_ENTITY',\n 429: 'TOO_MANY_REQUESTS',\n}\n\n/**\n * Converts the client errors (`statusCode` 4xx) raised by Fastify and its\n * plugins into Dotpress errors, with the same status and message. Anything\n * else is an unexpected error.\n */\nconst fromNativeError = (error: unknown): unknown => {\n if (!(error instanceof Error)) return error\n\n const { statusCode } = error as { statusCode?: unknown }\n if (typeof statusCode !== 'number' || !Number.isInteger(statusCode)) {\n return error\n }\n if (statusCode < 400 || statusCode > 499) return error\n\n return new HttpError(\n statusCode,\n ERROR_CODES[statusCode] ?? 'CLIENT_ERROR',\n error.message || 'Bad Request',\n { cause: error }\n )\n}\n\nconst waitForDrain = (res: ServerResponse) =>\n new Promise<void>((resolve) => {\n const done = () => {\n res.off('drain', done)\n res.off('close', done)\n resolve()\n }\n res.on('drain', done)\n res.on('close', done)\n })\n\n/**\n * Creates the Fastify adapter (Fastify 5).\n *\n * @example\n * const app = await createApp({\n * adapter: fastifyAdapter({\n * beforeRoutes: async (app) => {\n * await app.register(helmet)\n * },\n * }),\n * })\n * await app.listen(3000)\n */\nexport function fastifyAdapter(\n options?: FastifyAdapterOptions<FastifyInstance>\n): FastifyAdapter\nexport function fastifyAdapter<TApp extends FastifyInstance>(\n options: FastifyAdapterOptions<TApp> & { app: TApp }\n): FastifyAdapter<TApp>\nexport function fastifyAdapter<TApp extends FastifyInstance>(\n options: FastifyAdapterOptions<TApp> = {}\n): FastifyAdapter<TApp> {\n assertKnownOptions('fastifyAdapter()', options, [\n 'app',\n 'beforeRoutes',\n 'afterRoutes',\n 'bodyLimit',\n ] satisfies (keyof FastifyAdapterOptions<TApp>)[])\n const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT\n\n if (!Number.isInteger(bodyLimit) || bodyLimit < 0) {\n throw new RangeError(\n 'fastifyAdapter(): bodyLimit must be a non-negative integer (bytes).'\n )\n }\n\n const application = options.app ?? (Fastify() as unknown as TApp)\n let mounted = false\n let listening = false\n let closeConnections = () => {}\n /** Closes the responses being streamed, so that `close()` can complete. */\n const streams = new Set<() => void>()\n\n const normalize = (\n request: FastifyRequest,\n signal?: AbortSignal\n ): NormalizedRequest => {\n const url = request.url\n const index = url.indexOf('?')\n return {\n method: request.method,\n path: index === -1 ? url : url.slice(0, index),\n url: `${request.protocol}://${request.host}${url}`,\n params: {},\n query: parseQueryString(index === -1 ? '' : url.slice(index)),\n headers: request.headers,\n body: undefined,\n signal,\n ip: request.ip,\n raw: request,\n }\n }\n\n const send = (\n reply: FastifyReply,\n response: NormalizedResponse,\n controller?: AbortController\n ): FastifyReply | Promise<void> => {\n const { body } = response\n if (body instanceof ReadableStream) {\n return sendStream(reply, response, body, controller)\n }\n\n reply.code(response.status)\n for (const [name, value] of Object.entries(response.headers)) {\n reply.header(name, value)\n }\n if (body === null) return reply.send()\n if (typeof body === 'string') return reply.send(body)\n return reply.send(\n Buffer.from(body.buffer, body.byteOffset, body.byteLength)\n )\n }\n\n /**\n * Streams the body with the response of Node.js: Fastify only sends the\n * headers with the first chunk, too late for Server-Sent Events and for\n * streams that fail right away. `onSend` hooks do not apply.\n */\n const sendStream = async (\n reply: FastifyReply,\n response: NormalizedResponse,\n body: ReadableStream<Uint8Array>,\n controller: AbortController | undefined\n ): Promise<void> => {\n reply.hijack()\n const res = reply.raw\n\n res.statusCode = response.status\n // Headers set by hooks (`@fastify/helmet`…), then by Dotpress.\n for (const [name, value] of Object.entries(reply.getHeaders())) {\n if (value !== undefined) res.setHeader(name, value)\n }\n for (const [name, value] of Object.entries(response.headers)) {\n res.setHeader(name, value)\n }\n\n if (reply.request.method === 'HEAD') {\n await body.cancel().catch(() => {})\n res.end()\n return\n }\n\n const reader = body.getReader()\n const cancel = () => {\n reader.cancel().catch(() => {})\n }\n const stop = () => {\n controller?.abort()\n cancel()\n }\n\n res.on('close', cancel)\n streams.add(stop)\n res.flushHeaders()\n\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await waitForDrain(res)\n }\n\n // The reader is cancelled when the client goes away.\n res.end()\n } catch {\n // Ending the response would hide the failure: abort the connection.\n res.destroy()\n } finally {\n res.off('close', cancel)\n streams.delete(stop)\n }\n }\n\n const handleRoute = async (\n route: CompiledRoute,\n request: FastifyRequest,\n reply: FastifyReply,\n runtime: DotpressRuntime\n ) => {\n const controller = new AbortController()\n reply.raw.on('close', () => {\n if (!reply.raw.writableFinished) controller.abort()\n })\n\n const normalized = normalize(request, controller.signal)\n\n try {\n const { body, files, rawBody } = await readBody(\n request,\n route.definition.bodyLimit ?? bodyLimit\n )\n normalized.body = body\n normalized.files = files\n normalized.rawBody = rawBody\n } catch (error) {\n if (error === CLIENT_GONE) return reply.hijack()\n return send(reply, await runtime.handleError(error, normalized))\n }\n\n normalized.params = { ...(request.params as Record<string, string>) }\n return send(reply, await route.execute(normalized), controller)\n }\n\n return {\n name: 'fastify',\n\n mount: async (\n routes: readonly CompiledRoute[],\n runtime: DotpressRuntime\n ) => {\n if (mounted) throw new Error('The Fastify adapter is already mounted.')\n mounted = true\n\n const app = application as unknown as FastifyInstance\n\n // Set first: encapsulated plugins inherit them when they are created.\n app.setNotFoundHandler(async (request, reply) =>\n send(reply, await runtime.handleNotFound(normalize(request)))\n )\n app.setErrorHandler(async (error, request, reply) =>\n send(\n reply,\n await runtime.handleError(fromNativeError(error), normalize(request))\n )\n )\n\n await options.beforeRoutes?.(application)\n\n await app.register(async (child) => {\n // Dotpress reads the bodies: the payload is passed as is.\n child.removeAllContentTypeParsers()\n child.addContentTypeParser('*', (_request, payload, done) => {\n done(null, payload)\n })\n\n // Explicit HEAD routes first: GET routes add one otherwise.\n const ordered = [...routes].sort(\n (a, b) => Number(b.method === 'head') - Number(a.method === 'head')\n )\n for (const route of ordered) {\n const method = route.method.toUpperCase() as 'GET'\n const register = (url: string) => {\n child.route({\n method,\n url,\n exposeHeadRoute: true,\n handler: (request, reply) =>\n handleRoute(route, request, reply, runtime),\n })\n }\n\n register(route.path)\n // Trailing slashes are ignored, whatever the router options\n // (`findRoute()` follows `ignoreTrailingSlash`).\n if (\n route.path !== '/' &&\n child.findRoute({ method, url: `${route.path}/` }) === null\n ) {\n register(`${route.path}/`)\n }\n }\n })\n\n // Root hooks apply to every route, whatever the order: only an\n // encapsulated context keeps them away from the Dotpress routes.\n const { afterRoutes } = options\n if (afterRoutes) {\n await app.register(async (scope) => {\n await afterRoutes(scope)\n })\n }\n await app.ready()\n },\n\n listen: async (port: number, host?: string) => {\n if (listening) {\n throw new Error('The Fastify adapter is already listening.')\n }\n const app = application as unknown as FastifyInstance\n try {\n await app.listen({ port, host: host ?? '::' })\n } catch (error) {\n // No IPv6: every IPv4 interface, like Node.js.\n const code = (error as { code?: unknown }).code\n if (host !== undefined || code !== 'EAFNOSUPPORT') throw error\n await app.listen({ port, host: '0.0.0.0' })\n }\n listening = true\n closeConnections = trackConnections(app.server as Server)\n return addressOf(app.server as Server)\n },\n\n close: async () => {\n // Streams never end by themselves: close them.\n for (const stop of [...streams]) stop()\n listening = false\n closeConnections()\n await (application as unknown as FastifyInstance).close()\n },\n\n native: () => application,\n server: () =>\n listening\n ? ((application as unknown as FastifyInstance).server as Server)\n : undefined,\n }\n}\n"],"mappings":"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","names":[]}
package/dist/lambda.d.ts CHANGED
@@ -3,14 +3,20 @@ import { FastifyInstance } from 'fastify';
3
3
 
4
4
  /**
5
5
  * Event of API Gateway REST APIs (payload v1), HTTP APIs and Function URLs
6
- * (payload v2), or Application Load Balancers.
6
+ * (payload v2), or Application Load Balancers. Read by the handler only:
7
+ * the types of `@types/aws-lambda` fit.
7
8
  */
8
9
  type LambdaEvent = object;
10
+ /** Context of the invocation, given by the Lambda runtime. */
11
+ type LambdaContext = object;
9
12
  /** Result of the handler, in the format of the event. */
10
13
  interface LambdaResult {
11
14
  statusCode: number;
15
+ /** Application Load Balancers only. */
16
+ statusDescription?: string;
12
17
  headers?: Record<string, string>;
13
18
  multiValueHeaders?: Record<string, string[]>;
19
+ /** Payload v2 only. */
14
20
  cookies?: string[];
15
21
  body: string;
16
22
  isBase64Encoded?: boolean;
@@ -24,8 +30,11 @@ interface LambdaHandlerOptions {
24
30
  */
25
31
  isContentTypeBinary?: (contentType: string) => boolean;
26
32
  }
27
- /** AWS Lambda handler. */
28
- type LambdaHandler = (event: LambdaEvent, context?: object) => Promise<LambdaResult>;
33
+ /**
34
+ * AWS Lambda handler: API Gateway REST (payload v1) and HTTP APIs (payload
35
+ * v2), Function URLs, Application Load Balancer.
36
+ */
37
+ type LambdaHandler = (event: LambdaEvent, context?: LambdaContext) => Promise<LambdaResult>;
29
38
  /** App of the factory, created with `fastifyAdapter()`. */
30
39
  type LambdaApp = Pick<App<FastifyInstance>, 'native'>;
31
40
  /**
@@ -48,4 +57,4 @@ type LambdaApp = Pick<App<FastifyInstance>, 'native'>;
48
57
  declare const createLambdaHandler: (factory: () => LambdaApp | Promise<LambdaApp>, options?: LambdaHandlerOptions) => LambdaHandler;
49
58
 
50
59
  export { createLambdaHandler };
51
- export type { LambdaApp, LambdaEvent, LambdaHandler, LambdaHandlerOptions, LambdaResult };
60
+ export type { LambdaApp, LambdaContext, LambdaEvent, LambdaHandler, LambdaHandlerOptions, LambdaResult };
package/dist/lambda.js CHANGED
@@ -1,3 +1,7 @@
1
+ import {
2
+ assertKnownOptions
3
+ } from "./chunk-DWK6R675.js";
4
+
1
5
  // src/lambda.ts
2
6
  import {
3
7
  createConsoleLogger,
@@ -48,6 +52,10 @@ var failureApp = () => {
48
52
  return app;
49
53
  };
50
54
  var createLambdaHandler = (factory, options = {}) => {
55
+ assertKnownOptions("createLambdaHandler()", options, [
56
+ "logger",
57
+ "isContentTypeBinary"
58
+ ]);
51
59
  const logger = options.logger ?? createConsoleLogger();
52
60
  const isBinary = options.isContentTypeBinary ?? ((type) => !isTextType(type));
53
61
  const toHandler = (app) => {
@@ -81,7 +89,7 @@ var createLambdaHandler = (factory, options = {}) => {
81
89
  const native = app?.native?.();
82
90
  if (typeof native?.inject !== "function") {
83
91
  throw new TypeError(
84
- "createLambdaHandler() expects an app created with fastifyAdapter() of @dotpress/adapter-fastify."
92
+ "createLambdaHandler(): expected an app created with fastifyAdapter() of @dotpress/adapter-fastify."
85
93
  );
86
94
  }
87
95
  return toHandler(native);
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/lambda.ts"],"sourcesContent":["import {\n createConsoleLogger,\n internalError,\n toErrorBody,\n type App,\n type Logger,\n} from '@dotpress/core'\nimport awsLambdaFastify from '@fastify/aws-lambda'\nimport Fastify, { type FastifyInstance } from 'fastify'\n\n/**\n * Event of API Gateway REST APIs (payload v1), HTTP APIs and Function URLs\n * (payload v2), or Application Load Balancers.\n */\nexport type LambdaEvent = object\n\n/** Result of the handler, in the format of the event. */\nexport interface LambdaResult {\n statusCode: number\n headers?: Record<string, string>\n multiValueHeaders?: Record<string, string[]>\n cookies?: string[]\n body: string\n isBase64Encoded?: boolean\n}\n\nexport interface LambdaHandlerOptions {\n /** Logs the failures to create the app. Defaults to a console logger. */\n logger?: Logger\n /**\n * Whether a response content type is binary (sent in base64). Defaults to\n * everything except text, JSON and XML.\n */\n isContentTypeBinary?: (contentType: string) => boolean\n}\n\n/** AWS Lambda handler. */\nexport type LambdaHandler = (\n event: LambdaEvent,\n context?: object\n) => Promise<LambdaResult>\n\n/** App of the factory, created with `fastifyAdapter()`. */\nexport type LambdaApp = Pick<App<FastifyInstance>, 'native'>\n\nconst TEXT_TYPES =\n /^(text\\/|application\\/(json|xml|javascript|x-www-form-urlencoded)$|[^;]*\\+(json|xml)$)/\n\nconst isTextType = (contentType: string) =>\n TEXT_TYPES.test(contentType.split(';', 1)[0]!.trim().toLowerCase())\n\ninterface EventShape {\n version?: string\n path?: string\n rawPath?: string\n rawQueryString?: string\n headers?: Record<string, string | undefined> | null\n multiValueHeaders?: Record<string, string[] | undefined> | null\n queryStringParameters?: Record<string, string | undefined> | null\n multiValueQueryStringParameters?: Record<string, string[] | undefined> | null\n requestContext?: { elb?: unknown; identity?: object }\n}\n\n/** Query string of a load balancer event (not decoded by the load balancer). */\nconst albQuery = (event: EventShape) => {\n const parameters =\n event.multiValueQueryStringParameters ?? event.queryStringParameters ?? {}\n const pairs: string[] = []\n for (const [key, values] of Object.entries(parameters)) {\n for (const value of [values ?? []].flat()) pairs.push(`${key}=${value}`)\n }\n return pairs.join('&')\n}\n\n/** Path with the query string. */\nconst withQuery = (path = '/', query = '') =>\n query ? `${path}?${query}` : path\n\n/**\n * Prepares the event for `@fastify/aws-lambda`.\n *\n * - The query string is passed as sent, with the path, when the event has\n * it: the parameters decoded by API Gateway or by the library lose the\n * plus signs (spaces) and the commas of the values (payload v2).\n * - Behind a load balancer, the client address is the last entry of\n * `X-Forwarded-For`, appended by the load balancer.\n */\nconst prepare = (event: EventShape): EventShape => {\n const context = event.requestContext\n const raw = {\n ...event,\n queryStringParameters: null,\n multiValueQueryStringParameters: null,\n }\n\n if (event.version === '2.0') {\n return { ...raw, rawPath: withQuery(event.rawPath, event.rawQueryString) }\n }\n // API Gateway REST APIs only give the decoded parameters.\n if (context?.elb === undefined) return event\n\n const forwarded =\n event.multiValueHeaders?.['x-forwarded-for']?.at(-1) ??\n event.headers?.['x-forwarded-for']\n return {\n ...raw,\n path: withQuery(event.path, albQuery(event)),\n // Used as the remote address of the request (when defined).\n requestContext: {\n ...context,\n identity: { sourceIp: forwarded?.split(',').at(-1)?.trim() },\n },\n }\n}\n\n/** Answers `500 INTERNAL_ERROR` when the app cannot be created. */\nconst failureApp = () => {\n const app = Fastify()\n app.all('*', async (_request, reply) =>\n reply\n .code(500)\n .header('content-type', 'application/json; charset=utf-8')\n .send(JSON.stringify(toErrorBody(internalError())))\n )\n return app\n}\n\n/**\n * Creates the AWS Lambda handler of an app (`@fastify/aws-lambda`).\n *\n * The app is created by the factory at the first invocation (cold start),\n * then reused by the next invocations of the execution environment.\n * Invocations received meanwhile wait for the same app. When the factory\n * fails, the error is logged, the invocation gets a `500` error, and the\n * next invocation calls the factory again.\n *\n * Responses are sent once complete: streamed responses (`stream()`) are\n * buffered, and Server-Sent Events are not supported.\n *\n * @example\n * export const handler = createLambdaHandler(() =>\n * createApp({ adapter: fastifyAdapter(), plugins: [authPlugin(auth)] })\n * )\n */\nexport const createLambdaHandler = (\n factory: () => LambdaApp | Promise<LambdaApp>,\n options: LambdaHandlerOptions = {}\n): LambdaHandler => {\n const logger = options.logger ?? createConsoleLogger()\n const isBinary = options.isContentTypeBinary ?? ((type) => !isTextType(type))\n\n const toHandler = (app: FastifyInstance): LambdaHandler => {\n const create = (albMultiValueHeaders: boolean) =>\n awsLambdaFastify(app, {\n // The instance is ready: it cannot be decorated anymore.\n decorateRequest: false,\n albMultiValueHeaders,\n enforceBase64: ({ headers }) => {\n const type = headers['content-type']\n const encoding = headers['content-encoding']\n return (\n (typeof type === 'string' && isBinary(type)) ||\n // Compressed (`@fastify/compress`).\n (typeof encoding === 'string' && encoding !== 'identity')\n )\n },\n }) as unknown as LambdaHandler\n const single = create(false)\n const multi = create(true)\n\n return async (event, context) => {\n const prepared = prepare(event as EventShape)\n if (\n prepared.requestContext?.elb === undefined ||\n !prepared.multiValueHeaders\n ) {\n return single(prepared, context)\n }\n // Load balancers with multi-value headers only read these ones.\n const result = await multi(prepared, context)\n delete result.headers\n return result\n }\n }\n\n let ready: Promise<LambdaHandler> | undefined\n let failure: LambdaHandler | undefined\n\n const init = async (): Promise<LambdaHandler> => {\n const app = await factory()\n const native: unknown = app?.native?.()\n if (typeof (native as Partial<FastifyInstance>)?.inject !== 'function') {\n throw new TypeError(\n 'createLambdaHandler() expects an app created with fastifyAdapter() of @dotpress/adapter-fastify.'\n )\n }\n return toHandler(native as FastifyInstance)\n }\n\n return async (event, context) => {\n const current = (ready ??= init())\n let handler: LambdaHandler\n try {\n handler = await current\n } catch (error) {\n // Retried at the next invocation.\n if (ready === current) ready = undefined\n logger.error(\n error instanceof Error ? error : { error },\n 'Failed to create the app'\n )\n failure ??= toHandler(failureApp())\n return failure(event, context)\n }\n return handler(event, context)\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/lambda.ts"],"sourcesContent":["import {\n createConsoleLogger,\n internalError,\n toErrorBody,\n type App,\n type Logger,\n} from '@dotpress/core'\nimport awsLambdaFastify from '@fastify/aws-lambda'\nimport Fastify, { type FastifyInstance } from 'fastify'\nimport { assertKnownOptions } from './options.js'\n\n// The Lambda types are the same in @dotpress/adapter-hono/lambda and\n// @dotpress/adapter-fastify/lambda: keep them in sync.\n/**\n * Event of API Gateway REST APIs (payload v1), HTTP APIs and Function URLs\n * (payload v2), or Application Load Balancers. Read by the handler only:\n * the types of `@types/aws-lambda` fit.\n */\nexport type LambdaEvent = object\n\n/** Context of the invocation, given by the Lambda runtime. */\nexport type LambdaContext = object\n\n/** Result of the handler, in the format of the event. */\nexport interface LambdaResult {\n statusCode: number\n /** Application Load Balancers only. */\n statusDescription?: string\n headers?: Record<string, string>\n multiValueHeaders?: Record<string, string[]>\n /** Payload v2 only. */\n cookies?: string[]\n body: string\n isBase64Encoded?: boolean\n}\n\nexport interface LambdaHandlerOptions {\n /** Logs the failures to create the app. Defaults to a console logger. */\n logger?: Logger\n /**\n * Whether a response content type is binary (sent in base64). Defaults to\n * everything except text, JSON and XML.\n */\n isContentTypeBinary?: (contentType: string) => boolean\n}\n\n/**\n * AWS Lambda handler: API Gateway REST (payload v1) and HTTP APIs (payload\n * v2), Function URLs, Application Load Balancer.\n */\nexport type LambdaHandler = (\n event: LambdaEvent,\n context?: LambdaContext\n) => Promise<LambdaResult>\n\n/** App of the factory, created with `fastifyAdapter()`. */\nexport type LambdaApp = Pick<App<FastifyInstance>, 'native'>\n\nconst TEXT_TYPES =\n /^(text\\/|application\\/(json|xml|javascript|x-www-form-urlencoded)$|[^;]*\\+(json|xml)$)/\n\nconst isTextType = (contentType: string) =>\n TEXT_TYPES.test(contentType.split(';', 1)[0]!.trim().toLowerCase())\n\ninterface EventShape {\n version?: string\n path?: string\n rawPath?: string\n rawQueryString?: string\n headers?: Record<string, string | undefined> | null\n multiValueHeaders?: Record<string, string[] | undefined> | null\n queryStringParameters?: Record<string, string | undefined> | null\n multiValueQueryStringParameters?: Record<string, string[] | undefined> | null\n requestContext?: { elb?: unknown; identity?: object }\n}\n\n/** Query string of a load balancer event (not decoded by the load balancer). */\nconst albQuery = (event: EventShape) => {\n const parameters =\n event.multiValueQueryStringParameters ?? event.queryStringParameters ?? {}\n const pairs: string[] = []\n for (const [key, values] of Object.entries(parameters)) {\n for (const value of [values ?? []].flat()) pairs.push(`${key}=${value}`)\n }\n return pairs.join('&')\n}\n\n/** Path with the query string. */\nconst withQuery = (path = '/', query = '') =>\n query ? `${path}?${query}` : path\n\n/**\n * Prepares the event for `@fastify/aws-lambda`.\n *\n * - The query string is passed as sent, with the path, when the event has\n * it: the parameters decoded by API Gateway or by the library lose the\n * plus signs (spaces) and the commas of the values (payload v2).\n * - Behind a load balancer, the client address is the last entry of\n * `X-Forwarded-For`, appended by the load balancer.\n */\nconst prepare = (event: EventShape): EventShape => {\n const context = event.requestContext\n const raw = {\n ...event,\n queryStringParameters: null,\n multiValueQueryStringParameters: null,\n }\n\n if (event.version === '2.0') {\n return { ...raw, rawPath: withQuery(event.rawPath, event.rawQueryString) }\n }\n // API Gateway REST APIs only give the decoded parameters.\n if (context?.elb === undefined) return event\n\n const forwarded =\n event.multiValueHeaders?.['x-forwarded-for']?.at(-1) ??\n event.headers?.['x-forwarded-for']\n return {\n ...raw,\n path: withQuery(event.path, albQuery(event)),\n // Used as the remote address of the request (when defined).\n requestContext: {\n ...context,\n identity: { sourceIp: forwarded?.split(',').at(-1)?.trim() },\n },\n }\n}\n\n/** Answers `500 INTERNAL_ERROR` when the app cannot be created. */\nconst failureApp = () => {\n const app = Fastify()\n app.all('*', async (_request, reply) =>\n reply\n .code(500)\n .header('content-type', 'application/json; charset=utf-8')\n .send(JSON.stringify(toErrorBody(internalError())))\n )\n return app\n}\n\n/**\n * Creates the AWS Lambda handler of an app (`@fastify/aws-lambda`).\n *\n * The app is created by the factory at the first invocation (cold start),\n * then reused by the next invocations of the execution environment.\n * Invocations received meanwhile wait for the same app. When the factory\n * fails, the error is logged, the invocation gets a `500` error, and the\n * next invocation calls the factory again.\n *\n * Responses are sent once complete: streamed responses (`stream()`) are\n * buffered, and Server-Sent Events are not supported.\n *\n * @example\n * export const handler = createLambdaHandler(() =>\n * createApp({ adapter: fastifyAdapter(), plugins: [authPlugin(auth)] })\n * )\n */\nexport const createLambdaHandler = (\n factory: () => LambdaApp | Promise<LambdaApp>,\n options: LambdaHandlerOptions = {}\n): LambdaHandler => {\n assertKnownOptions('createLambdaHandler()', options, [\n 'logger',\n 'isContentTypeBinary',\n ] satisfies (keyof LambdaHandlerOptions)[])\n const logger = options.logger ?? createConsoleLogger()\n const isBinary = options.isContentTypeBinary ?? ((type) => !isTextType(type))\n\n const toHandler = (app: FastifyInstance): LambdaHandler => {\n const create = (albMultiValueHeaders: boolean) =>\n awsLambdaFastify(app, {\n // The instance is ready: it cannot be decorated anymore.\n decorateRequest: false,\n albMultiValueHeaders,\n enforceBase64: ({ headers }) => {\n const type = headers['content-type']\n const encoding = headers['content-encoding']\n return (\n (typeof type === 'string' && isBinary(type)) ||\n // Compressed (`@fastify/compress`).\n (typeof encoding === 'string' && encoding !== 'identity')\n )\n },\n }) as unknown as LambdaHandler\n const single = create(false)\n const multi = create(true)\n\n return async (event, context) => {\n const prepared = prepare(event as EventShape)\n if (\n prepared.requestContext?.elb === undefined ||\n !prepared.multiValueHeaders\n ) {\n return single(prepared, context)\n }\n // Load balancers with multi-value headers only read these ones.\n const result = await multi(prepared, context)\n delete result.headers\n return result\n }\n }\n\n let ready: Promise<LambdaHandler> | undefined\n let failure: LambdaHandler | undefined\n\n const init = async (): Promise<LambdaHandler> => {\n const app = await factory()\n const native: unknown = app?.native?.()\n if (typeof (native as Partial<FastifyInstance>)?.inject !== 'function') {\n throw new TypeError(\n 'createLambdaHandler(): expected an app created with fastifyAdapter() of @dotpress/adapter-fastify.'\n )\n }\n return toHandler(native as FastifyInstance)\n }\n\n return async (event, context) => {\n const current = (ready ??= init())\n let handler: LambdaHandler\n try {\n handler = await current\n } catch (error) {\n // Retried at the next invocation.\n if (ready === current) ready = undefined\n logger.error(\n error instanceof Error ? error : { error },\n 'Failed to create the app'\n )\n failure ??= toHandler(failureApp())\n return failure(event, context)\n }\n return handler(event, context)\n }\n}\n"],"mappings":";;;;;AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AACP,OAAO,sBAAsB;AAC7B,OAAO,aAAuC;AAkD9C,IAAM,aACJ;AAEF,IAAM,aAAa,CAAC,gBAClB,WAAW,KAAK,YAAY,MAAM,KAAK,CAAC,EAAE,CAAC,EAAG,KAAK,EAAE,YAAY,CAAC;AAepE,IAAM,WAAW,CAAC,UAAsB;AACtC,QAAM,aACJ,MAAM,mCAAmC,MAAM,yBAAyB,CAAC;AAC3E,QAAM,QAAkB,CAAC;AACzB,aAAW,CAAC,KAAK,MAAM,KAAK,OAAO,QAAQ,UAAU,GAAG;AACtD,eAAW,SAAS,CAAC,UAAU,CAAC,CAAC,EAAE,KAAK,EAAG,OAAM,KAAK,GAAG,GAAG,IAAI,KAAK,EAAE;AAAA,EACzE;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAGA,IAAM,YAAY,CAAC,OAAO,KAAK,QAAQ,OACrC,QAAQ,GAAG,IAAI,IAAI,KAAK,KAAK;AAW/B,IAAM,UAAU,CAAC,UAAkC;AACjD,QAAM,UAAU,MAAM;AACtB,QAAM,MAAM;AAAA,IACV,GAAG;AAAA,IACH,uBAAuB;AAAA,IACvB,iCAAiC;AAAA,EACnC;AAEA,MAAI,MAAM,YAAY,OAAO;AAC3B,WAAO,EAAE,GAAG,KAAK,SAAS,UAAU,MAAM,SAAS,MAAM,cAAc,EAAE;AAAA,EAC3E;AAEA,MAAI,SAAS,QAAQ,OAAW,QAAO;AAEvC,QAAM,YACJ,MAAM,oBAAoB,iBAAiB,GAAG,GAAG,EAAE,KACnD,MAAM,UAAU,iBAAiB;AACnC,SAAO;AAAA,IACL,GAAG;AAAA,IACH,MAAM,UAAU,MAAM,MAAM,SAAS,KAAK,CAAC;AAAA;AAAA,IAE3C,gBAAgB;AAAA,MACd,GAAG;AAAA,MACH,UAAU,EAAE,UAAU,WAAW,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,KAAK,EAAE;AAAA,IAC7D;AAAA,EACF;AACF;AAGA,IAAM,aAAa,MAAM;AACvB,QAAM,MAAM,QAAQ;AACpB,MAAI;AAAA,IAAI;AAAA,IAAK,OAAO,UAAU,UAC5B,MACG,KAAK,GAAG,EACR,OAAO,gBAAgB,iCAAiC,EACxD,KAAK,KAAK,UAAU,YAAY,cAAc,CAAC,CAAC,CAAC;AAAA,EACtD;AACA,SAAO;AACT;AAmBO,IAAM,sBAAsB,CACjC,SACA,UAAgC,CAAC,MACf;AAClB,qBAAmB,yBAAyB,SAAS;AAAA,IACnD;AAAA,IACA;AAAA,EACF,CAA0C;AAC1C,QAAM,SAAS,QAAQ,UAAU,oBAAoB;AACrD,QAAM,WAAW,QAAQ,wBAAwB,CAAC,SAAS,CAAC,WAAW,IAAI;AAE3E,QAAM,YAAY,CAAC,QAAwC;AACzD,UAAM,SAAS,CAAC,yBACd,iBAAiB,KAAK;AAAA;AAAA,MAEpB,iBAAiB;AAAA,MACjB;AAAA,MACA,eAAe,CAAC,EAAE,QAAQ,MAAM;AAC9B,cAAM,OAAO,QAAQ,cAAc;AACnC,cAAM,WAAW,QAAQ,kBAAkB;AAC3C,eACG,OAAO,SAAS,YAAY,SAAS,IAAI;AAAA,QAEzC,OAAO,aAAa,YAAY,aAAa;AAAA,MAElD;AAAA,IACF,CAAC;AACH,UAAM,SAAS,OAAO,KAAK;AAC3B,UAAM,QAAQ,OAAO,IAAI;AAEzB,WAAO,OAAO,OAAO,YAAY;AAC/B,YAAM,WAAW,QAAQ,KAAmB;AAC5C,UACE,SAAS,gBAAgB,QAAQ,UACjC,CAAC,SAAS,mBACV;AACA,eAAO,OAAO,UAAU,OAAO;AAAA,MACjC;AAEA,YAAM,SAAS,MAAM,MAAM,UAAU,OAAO;AAC5C,aAAO,OAAO;AACd,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AAEJ,QAAM,OAAO,YAAoC;AAC/C,UAAM,MAAM,MAAM,QAAQ;AAC1B,UAAM,SAAkB,KAAK,SAAS;AACtC,QAAI,OAAQ,QAAqC,WAAW,YAAY;AACtE,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO,UAAU,MAAyB;AAAA,EAC5C;AAEA,SAAO,OAAO,OAAO,YAAY;AAC/B,UAAM,UAAW,UAAU,KAAK;AAChC,QAAI;AACJ,QAAI;AACF,gBAAU,MAAM;AAAA,IAClB,SAAS,OAAO;AAEd,UAAI,UAAU,QAAS,SAAQ;AAC/B,aAAO;AAAA,QACL,iBAAiB,QAAQ,QAAQ,EAAE,MAAM;AAAA,QACzC;AAAA,MACF;AACA,kBAAY,UAAU,WAAW,CAAC;AAClC,aAAO,QAAQ,OAAO,OAAO;AAAA,IAC/B;AACA,WAAO,QAAQ,OAAO,OAAO;AAAA,EAC/B;AACF;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotpress/adapter-fastify",
3
- "version": "2.0.0-alpha.4",
3
+ "version": "2.0.0-alpha.5",
4
4
  "description": "Fastify 5 adapter for Dotpress, on Node.js and AWS Lambda",
5
5
  "author": "Renaud Guyon",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "node": ">=22.12"
35
35
  },
36
36
  "peerDependencies": {
37
- "@dotpress/core": "^2.0.0-alpha.4",
37
+ "@dotpress/core": "^2.0.0-alpha.5",
38
38
  "@fastify/aws-lambda": "^6.1.0",
39
39
  "fastify": "^5.0.0"
40
40
  },
@@ -44,8 +44,8 @@
44
44
  }
45
45
  },
46
46
  "devDependencies": {
47
- "@dotpress/core": "2.0.0-alpha.4",
48
- "@dotpress/testing": "2.0.0-alpha.4",
47
+ "@dotpress/core": "2.0.0-alpha.5",
48
+ "@dotpress/testing": "2.0.0-alpha.5",
49
49
  "@fastify/aws-lambda": "^6.4.2",
50
50
  "fastify": "^5.12.8",
51
51
  "zod": "^4.6.7"