{"version":3,"sources":["../src/HttpRestService.ts"],"names":[],"mappings":"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","sourcesContent":["import { Buffer } from 'buffer'\nimport chalk from 'chalk'\nimport { createServer } from 'http'\nimport { AddressInfo } from 'net'\nimport { URL } from 'url'\nimport { createRouter, isHandler } from './createRouter'\nimport { logger, enableLogger } from './logger'\nimport { type Methods } from './methods'\nimport { RestRouter, Request, Response, HttpHeader } from './types'\nimport { getRouteParams, handleServiceError } from './utils'\n\nexport { createRouter }\nexport { enableLogger }\n\ntype HttpRestServiceOptions = {\n    name: string\n    port: number\n    path: string\n    silent: boolean\n}\n\nconst collect = <T>(stream: NodeJS.ReadableStream): Promise<T[]> => {\n    const chunks: T[] = []\n    return new Promise((resolve, reject) => {\n        stream.on('data', (chunk: T) => chunks.push(chunk))\n        stream.on('end', () => resolve(chunks))\n        stream.on('error', reject)\n    })\n}\n\n/**\n * Really simple multipart/form-data parser, that doesn't support uploading files.\n * @param body\n * @param boundary\n * @returns\n */\nconst parseMultipartFormData = (body: string, boundary: string): Record<string, string> => {\n    const parts = body.split(boundary).slice(1, -1)\n    const parsedData: Record<string, string> = {}\n\n    for (const part of parts) {\n        const [, header, value] = part.split(/(?:\\r\\n)+/)\n        const nameMatch = header.match(/name=\"([^\"]+)\"/)\n\n        if (nameMatch) {\n            const [, name] = nameMatch\n            parsedData[name] = value\n        }\n    }\n\n    return parsedData\n}\n\nconst getBody = async (contentType: string, stream: NodeJS.ReadableStream) => {\n    const chunks = await collect<Buffer>(stream)\n    const body = Buffer.concat(chunks).toString()\n\n    if (!body.length) {\n        return null\n    }\n    if (contentType.includes('application/json')) {\n        return JSON.parse(body)\n    } else if (contentType.includes('multipart/form-data')) {\n        const [, boundary] = contentType.split('boundary=')\n        return parseMultipartFormData(body, boundary)\n    } else if (contentType.includes('application/x-www-form-urlencoded')) {\n        const parsed = new URLSearchParams(body)\n        return Object.fromEntries(parsed.entries())\n    }\n    return body\n}\n\nexport class HttpRestService {\n    public readonly server: ReturnType<typeof createServer>\n    private options: Partial<HttpRestServiceOptions>\n    public readonly url: Promise<URL>\n\n    constructor(private router: RestRouter, options: Partial<HttpRestServiceOptions> = {}) {\n        this.options = {\n            name: 'http-rest-service',\n            ...options,\n        }\n\n        this.server = createServer()\n\n        this.url = new Promise((resolve, reject) => {\n            this.server.on('listening', () => {\n                const address = this.server.address() as AddressInfo\n                const url = new URL(this.options.path ?? '/', `http://localhost:${address.port}`)\n                logger(`${this.options.name}: Listening on ${url.toString()}`)\n                resolve(url)\n            })\n            this.server.on('error', e => {\n                logger('ERROR', e)\n                if (!this.url) {\n                    // never started successfully\n                    reject(e)\n                }\n            })\n        })\n\n        this.server.listen(this.options.port)\n        this.server.on('listening', () => {\n            this.isOpen = true\n            const port: number = (this.server.address() as AddressInfo).port\n            !this.options.silent &&\n                console.log(chalk.green(`${this.options.name}: listening on port ${port}`))\n        })\n        this.server.on('request', async (req, res) => {\n            logger('->', req.method, req.url)\n            if (!req.url) {\n                logger('request does not have a url.')\n                res.statusCode = 404\n                res.write('Not found. Request does not have a url.')\n                res.end()\n                return\n            }\n            const requestUrl = new URL(req.url, await this.url)\n            const matchingRoutes = this.router.routes.filter(route => {\n                if (!route.method) {\n                    // middleware\n                    return route.regexp.test(requestUrl.pathname)\n                } else {\n                    return route.method === req.method && route.regexp.test(requestUrl.pathname)\n                }\n            })\n            logger(\n                'matching routes:',\n                matchingRoutes.map(route => route.path),\n            )\n\n            const response: Response = {\n                setHeader(key, value) {\n                    res.setHeader(key, value)\n                    return response\n                },\n                status: (code: number) => {\n                    res.statusCode = code\n                    return response\n                },\n                send: (data: bigint | string | object | Array<unknown> | number) => {\n                    switch (typeof data) {\n                        case 'string':\n                        case 'number':\n                        case 'bigint':\n                            res.write(data.toString())\n                            break\n                        case 'object':\n                            res.write(JSON.stringify(data))\n                            break\n                    }\n                    res.end()\n                    return response\n                },\n                json: data => {\n                    res.setHeader('Content-Type', 'application/json; charset=utf-8')\n                    res.write(JSON.stringify(data))\n                    res.end()\n                    return response\n                },\n                end: () => {\n                    res.end()\n                },\n            }\n\n            const fullUrl = new URL(req.url ?? '/', `http://localhost`)\n            const query = Object.fromEntries(fullUrl.searchParams.entries())\n            const body = await getBody(req.headers['content-type'] ?? '', req)\n\n            const request: Omit<Request<string>, 'params'> = {\n                host: (await this.url).host,\n                query: query,\n                headers: req.headers as HttpHeader,\n                method: req.method as Methods,\n                url: req.url,\n                body,\n            }\n\n            const promises: Promise<unknown>[] = []\n            const errorHandlers = [...router.errorHandlers]\n\n            const executeRoutes = async () => {\n                const route = matchingRoutes.shift()\n                if (!route || !isHandler(route.handler)) return\n                const params = getRouteParams(route, requestUrl)\n                await route.handler(Object.assign(request, { params }), response, async () => {\n                    // next is called, execute next route in the list\n                    await executeRoutes()\n                })\n            }\n            promises.push(executeRoutes())\n\n            while (promises.length) {\n                try {\n                    await promises.shift()\n                } catch (e) {\n                    handleServiceError(\n                        e,\n                        errorHandlers,\n                        Object.assign(request, { params: {} }),\n                        response,\n                    )\n                }\n            }\n        })\n\n        this.server.on('upgrade', () => {\n            // TODO: same as above. But verify with sockjs implementation.\n        })\n    }\n\n    private isOpen = false\n    dispose() {\n        if (!this.isOpen) return Promise.resolve()\n        return new Promise<void>((resolve, reject) => {\n            this.server.close(err => {\n                if (err) {\n                    return reject(err)\n                }\n                this.isOpen = false\n                resolve()\n            })\n        })\n    }\n}\n"]}