UNPKG

@trpc/server

Version:

The tRPC server library

178 lines (175 loc) • 6.14 kB
import { Readable } from 'node:stream'; import { pipeline } from 'node:stream/promises'; import { splitSetCookieString } from '../../vendor/cookie-es/set-cookie/split.mjs'; function determinePayloadFormat(event) { // https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html // According to AWS support, version is is extracted from the version property in the event. // If there is no version property, then the version is implied as 1.0 const unknownEvent = event; if (typeof unknownEvent.version === 'undefined') { return '1.0'; } else { return unknownEvent.version; } } function getHeadersAndCookiesFromResponse(response) { const headers = Object.fromEntries(response.headers.entries()); const cookies = splitSetCookieString(response.headers.getSetCookie()).map((cookie)=>cookie.trim()); delete headers['set-cookie']; return { headers, cookies }; } const v1Processor = { // same as getPath above getTRPCPath: (event)=>{ if (!event.pathParameters) { // Then this event was not triggered by a resource denoted with {proxy+} return event.path.split('/').pop() ?? ''; } const matches = event.resource.matchAll(/\{(.*?)\}/g); for (const match of matches){ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const group = match[1]; if (group.includes('+') && event.pathParameters) { return event.pathParameters[group.replace('+', '')] ?? ''; } } return event.path.slice(1); }, url (event) { const hostname = event.requestContext.domainName ?? event.headers['host'] ?? event.multiValueHeaders?.['host']?.[0] ?? 'localhost'; const searchParams = new URLSearchParams(); for (const [key, value] of Object.entries(event.queryStringParameters ?? {})){ if (value !== undefined) { searchParams.append(key, value); } } const qs = searchParams.toString(); return { hostname, pathname: event.path, search: qs && `?${qs}` }; }, getHeaders: (event)=>{ const headers = new Headers(); for (const [key, value] of Object.entries(event.headers ?? {})){ if (value !== undefined) { headers.append(key, value); } } for (const [k, values] of Object.entries(event.multiValueHeaders ?? {})){ if (values) { values.forEach((v)=>headers.append(k, v)); } } return headers; }, getMethod: (event)=>event.httpMethod, toResult: async (response)=>{ const { headers, cookies } = getHeadersAndCookiesFromResponse(response); const result = { ...cookies.length && { multiValueHeaders: { 'set-cookie': cookies } }, statusCode: response.status, body: await response.text(), headers }; return result; } }; const v2Processor = { getTRPCPath: (event)=>{ const matches = event.routeKey.matchAll(/\{(.*?)\}/g); for (const match of matches){ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const group = match[1]; if (group.includes('+') && event.pathParameters) { return event.pathParameters[group.replace('+', '')] ?? ''; } } return event.rawPath.slice(1); }, url (event) { return { hostname: event.requestContext.domainName, pathname: event.rawPath, search: event.rawQueryString && `?${event.rawQueryString}` }; }, getHeaders: (event)=>{ const headers = new Headers(); for (const [key, value] of Object.entries(event.headers ?? {})){ if (value !== undefined) { headers.append(key, value); } } if (event.cookies) { headers.append('cookie', event.cookies.join('; ')); } return headers; }, getMethod: (event)=>event.requestContext.http.method, toResult: async (response)=>{ const { headers, cookies } = getHeadersAndCookiesFromResponse(response); const result = { cookies, statusCode: response.status, body: await response.text(), headers }; return result; }, toStream: async (response, stream)=>{ const { headers, cookies } = getHeadersAndCookiesFromResponse(response); const metadata = { statusCode: response.status, headers, cookies }; const responseStream = awslambda.HttpResponseStream.from(stream, metadata); if (response.body) { await pipeline(Readable.fromWeb(response.body), responseStream); } else { responseStream.end(); } } }; function getPlanner(event) { const version = determinePayloadFormat(event); let processor; switch(version){ case '1.0': processor = v1Processor; break; case '2.0': processor = v2Processor; break; default: throw new Error(`Unsupported version: ${version}`); } const urlParts = processor.url(event); const url = `https://${urlParts.hostname}${urlParts.pathname}${urlParts.search}`; const init = { headers: processor.getHeaders(event), method: processor.getMethod(event), // @ts-expect-error this is fine duplex: 'half' }; if (event.body) { init.body = event.isBase64Encoded ? Buffer.from(event.body, 'base64') : event.body; } const request = new Request(url, init); return { path: processor.getTRPCPath(event), request, toResult: processor.toResult, toStream: processor.toStream }; } export { getPlanner };