fets
Version:
TypeScript HTTP Framework focusing on e2e type-safety, easy setup, performance & great developer experience
233 lines (232 loc) • 9.39 kB
JavaScript
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;
}