UNPKG

fets

Version:

TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience

233 lines (232 loc) • 9.39 kB
import * as DefaultFetchAPI from '@whatwg-node/fetch'; import { createServerAdapter } from '@whatwg-node/server'; import { useOpenAPI } from './plugins/openapi.js'; import { isLazySerializedResponse } from './Response.js'; import { addHandlersToMethod } from './utils.js'; import { useZod } from './zod/zod.js'; const HTTP_METHODS = [ 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH', ]; const EMPTY_OBJECT = {}; const EMPTY_MATCH = { pathname: { groups: {} } }; export function createRouterBase({ fetchAPI: givenFetchAPI, base: basePath = '/', plugins = [], swaggerUI, } = {}, openAPIDocument) { const fetchAPI = { ...DefaultFetchAPI, ...givenFetchAPI, }; const __onRouterInitHooks = []; const onRouteHooks = []; const onSerializeResponseHooks = []; for (const plugin of plugins) { if (plugin.onRouterInit) { __onRouterInitHooks.push(plugin.onRouterInit); } if (plugin.onRoute) { onRouteHooks.push(plugin.onRoute); } if (plugin.onSerializeResponse) { onSerializeResponseHooks.push(plugin.onSerializeResponse); } } const handlersByPatternByMethod = new Map(); const internalPatternsByMethod = new Map(); // Use this in `handle` for iteration to get better performance const patternHandlerObjByMethod = new Map(); return { openAPIDocument, async handle(request, context) { let url = new Proxy(EMPTY_OBJECT, { get(_target, prop, _receiver) { url = new fetchAPI.URL(request.url, 'http://localhost'); return Reflect.get(url, prop, url); }, }); const methodPatternMaps = patternHandlerObjByMethod.get(request.method); if (methodPatternMaps) { const queryProxy = new Proxy({}, { get(_, prop) { if (prop !== 'then' && !url.searchParams.has(prop)) { return undefined; } const allQueries = url.searchParams.getAll(prop.toString()); if (allQueries.length === 0) { return ''; } return allQueries.length === 1 ? allQueries[0] : allQueries; }, has(_, prop) { return url.searchParams.has(prop.toString()); }, }); for (const { pattern, handlers } of methodPatternMaps) { // Do not parse URL if not needed let match = null; if (pattern.isPattern) { match = pattern.exec(url); } else if (request.url.endsWith(pattern.pathname) || url.pathname === pattern.pathname) { match = EMPTY_MATCH; } if (match != null) { const routerRequest = new Proxy(request, { get(target, prop) { if (prop === 'parsedUrl') { return url; } if (prop === 'params') { return new Proxy(match.pathname.groups, { get(_, prop) { const value = match.pathname.groups[prop.toString()]; if (value != null) { return decodeURIComponent(value); } return value; }, }); } if (prop === 'query') { return queryProxy; } const targetProp = target[prop]; if (typeof targetProp === 'function') { return targetProp.bind(target); } return targetProp; }, has(target, prop) { return (prop in target || prop === 'parsedUrl' || prop === 'params' || prop === 'query'); }, }); for (const handler of handlers) { const handlerResult = await handler(routerRequest, context); if (handlerResult) { if (isLazySerializedResponse(handlerResult)) { for (const onSerializeResponseHook of onSerializeResponseHooks) { onSerializeResponseHook({ request: routerRequest, path: pattern.pathname, lazyResponse: handlerResult, serverContext: context, }); } return (handlerResult.actualResponse || fetchAPI.Response.json(handlerResult.jsonObj, handlerResult.init)); } return handlerResult; } } } } } if (swaggerUI?.endpoint) { return new fetchAPI.Response(null, { status: 302, headers: { location: swaggerUI.endpoint, }, }); } return new fetchAPI.Response(null, { status: 404 }); }, route(opts) { const { operationId, description, method, path, schemas, tags, internal, handler } = opts; const handlers = Array.isArray(handler) ? handler : [handler]; if (!method) { for (const method of HTTP_METHODS) { addHandlersToMethod({ operationId, description, method, path, schemas, handlers, tags, internal, // Router specific onRouteHooks, openAPIDocument, basePath, fetchAPI, handlersByPatternByMethod, internalPatternsByMethod, patternHandlerObjByMethod, }); } } else { addHandlersToMethod({ operationId, description, method, path, schemas, handlers, tags, internal, // Router specific onRouteHooks, openAPIDocument, basePath, fetchAPI, handlersByPatternByMethod, internalPatternsByMethod, patternHandlerObjByMethod, }); } return this; }, __client: {}, __onRouterInitHooks, }; } export function createRouter(options = {}) { const { openAPI: { endpoint: oasEndpoint = '/openapi.json', ...openAPIDocument } = {}, swaggerUI: { endpoint: swaggerUIEndpoint = '/docs', ...swaggerUIOpts } = {}, plugins: userPlugins = [], base = '/', } = options; openAPIDocument.openapi = openAPIDocument.openapi || '3.0.1'; const oasInfo = (openAPIDocument.info ||= {}); oasInfo.title ||= 'feTS API'; oasInfo.description ||= 'An API written with feTS'; oasInfo.version ||= '1.0.0'; if (base !== '/') { openAPIDocument.servers = openAPIDocument.servers || [ { url: base, }, ]; } const plugins = [ ...(oasEndpoint || swaggerUIEndpoint ? [ useOpenAPI({ oasEndpoint, swaggerUIEndpoint, swaggerUIOpts, }), ] : []), useZod(), ...userPlugins, ]; const finalOpts = { ...options, swaggerUI: { endpoint: swaggerUIEndpoint, ...swaggerUIOpts, }, base, plugins, }; const routerBaseObject = createRouterBase(finalOpts, openAPIDocument); const router = createServerAdapter(routerBaseObject, finalOpts); for (const onRouterInitHook of routerBaseObject.__onRouterInitHooks) { onRouterInitHook(router); } return router; }