import { NextApiRequest, NextApiResponse } from 'next' import { AnyRoute, Resolver, ResolverByName, ResolverNames, Route, } from './routes' import { inferRoutes } from './helpers' import { Config, DEFAULT_CONFIG } from './config' export type RouterConfig = Partial< Config & { /** * Method to return an object based on the request when no matching route was found. * By default, the method returns an object like this: * * `{ status: 404, message: 'No endpoint found...' }` * * @param req the incoming request */ notFoundReturn: (req: NextApiRequest) => any /** * Function to create the context object for each route. * This function is called upon *every* request. * * @param req the incoming request */ createContext: (req: NextApiRequest) => Context /** * The [catch-all](https://nextjs.org/docs/api-routes/dynamic-api-routes#catch-all-api-routes) api files name **without** dots. * This name needs to correspond to the file in the api folder where the api handler is exported from. */ apiFileName: string } > export class Router, Context = void> { private readonly _configuration: Required> private constructor( private _routes: Routes[], configuration: RouterConfig = {} ) { this._configuration = { parser: DEFAULT_CONFIG.parser, stringify: DEFAULT_CONFIG.stringify, notFoundReturn: (req: NextApiRequest) => ({ status: 404, message: 'No endpoint found for "' + req.query.route[0] + '"', }), createContext: () => ({} as Context), apiFileName: 'route', ...configuration, } if ( !this._configuration.apiFileName || this._configuration.apiFileName.includes('.') ) { throw new Error( 'Router configuration error: apiFileName must be a non-empty string without dots' ) } } static createRouter(config?: RouterConfig) { return new Router([], config) } addRoute( path: TName, resolver: Resolver ): Router | Routes, Context> { return new Router([[path, resolver], ...this._routes], this._configuration) } getResolver>( path: TName ): ResolverByName | undefined { return this._routes.find(([routePath]) => routePath === path)?.[1] } getApiHandler() { return async (req: NextApiRequest, res: NextApiResponse): Promise => { const { createContext, parser, stringify, notFoundReturn, apiFileName } = this._configuration const body = req.body if (typeof body !== 'string') { return res .status(400) .setHeader('Content-Type', 'application/json') .send( stringify({ status: 400, message: 'no body', }) ) } const { input } = parser(body) const route = req.query[apiFileName][0] const resolver = this.getResolver(route) if (resolver) { const context = createContext(req) const result = await resolver(input, context) return res .status(200) .setHeader('Content-Type', 'application/json') .send(stringify({ result })) } res .status(404) .setHeader('Content-Type', 'application/json') .send(stringify(notFoundReturn(req))) } } merge>( path: TPrefix, router: TRouter ): Router | Routes, Context> { const mergedRoutes = router._routes.map(([name, resolver]) => [ `${path}.${name}`, resolver, ]) as AnyRoute[] return new Router( [...this._routes, ...mergedRoutes], this._configuration ) as unknown as Router | Routes, Context> } } export type AnyRouter = Router export type MergedRoutes< TPrefix extends string, TRouter extends AnyRouter > = PrefixRoute> export type PrefixRoute< TPrefix extends string, TRoute extends AnyRoute > = TRoute extends Route< infer TName, infer TInput, infer TOutput, infer TContext > ? Route<`${TPrefix}.${TName}`, TInput, TOutput, TContext> : never export type RouteNames = inferRoutes[0] export const createRouter = Router.createRouter