UNPKG

@grace-js/grace

Version:

An opinionated API framework

366 lines 14.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createGrace = exports.Grace = void 0; const error_js_1 = require("./errors/error.js"); const response_js_1 = require("./routes/response.js"); const glob_1 = require("glob"); const url_js_1 = require("./utils/url.js"); const zod_1 = require("zod"); const fast_querystring_1 = __importDefault(require("fast-querystring")); const trie_js_1 = require("./routers/trie.js"); const adapter_js_1 = require("./runtime/node/adapter.js"); const openapi_js_1 = require("./utils/openapi.js"); const fs = __importStar(require("node:fs")); class Grace { constructor(router, adapter, verbose = false) { this.routes = []; this.before = []; this.after = []; this.error = []; this.router = router; this.adapter = adapter; this.verbose = verbose; } registerPlugin(plugin) { plugin(this); return this; } registerBefore(before) { this.before.push(before); return this; } registerAfter(after) { this.after.push(after); return this; } registerError(error) { this.error.push(error); return this; } registerRoute(route) { this.router.addRoute(route); this.routes.push(route); return this; } registerRoutes(path) { this.registerRoutesAsync(path); // workaround return this; } exportOpenAPI(path) { const openapi = (0, openapi_js_1.graceToOpenAPISpec)(this); fs.writeFileSync(path, JSON.stringify(openapi, null, 2)); return this; } async fetch(request) { const response = await this.handleInternally(request); if (!response) { throw new Error('No response was returned'); } if ((0, response_js_1.convertStatusCode)(response.code) === 204) { return new Response(undefined, { status: (0, response_js_1.convertStatusCode)(response.code), headers: response.headers }); } if (typeof response.body === 'object') { if (response.body instanceof Blob) { return new Response(response.body, { status: (0, response_js_1.convertStatusCode)(response.code), headers: response.headers }); } return new Response(JSON.stringify(response.body), { status: (0, response_js_1.convertStatusCode)(response.code), headers: { 'Content-Type': 'application/json', ...response.headers } }); } return new Response(response.body, { status: (0, response_js_1.convertStatusCode)(response.code), headers: { 'Content-Type': 'text/plain', ...response.headers } }); } listen(port) { this.adapter.listen(this, port); } close() { this.adapter.close(); } async handleInternally(request) { try { let headers = {}; let beforeResponse = null; for (const handler of this.before) { const result = await handler(request); if (result?.headers) { headers = { ...headers, ...result.headers }; } if (typeof result === 'object' && 'code' in result) { beforeResponse = result; } } if (beforeResponse) { beforeResponse.headers = { ...beforeResponse.headers, ...headers }; for (const handler of this.after) { await handler(request, beforeResponse); } return beforeResponse; } const pathname = (0, url_js_1.getPath)(request.url); const matched = this.router.match(pathname, request.method); if (!matched) { throw new error_js_1.APIError(404, { message: 'Not Found' }); } const route = matched.route; const rawParameters = matched.params; let parameters = rawParameters; if (route.schema?.params) { try { parameters = await route.schema.params.parseAsync(rawParameters); } catch (e) { if (e instanceof zod_1.ZodError) { throw new error_js_1.APIError(400, { message: 'Bad Request: ' + e.message }, e); } else { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } } let body; const hasBody = route.schema?.body != null; if (request.method !== 'GET') { if (request.headers.get('content-type')?.includes('multipart/form-data')) { const formData = await request.clone().formData(); const rawBody = {}; formData.forEach((value, key) => { rawBody[key] = value; }); if (hasBody) { try { body = await route.schema.body.parseAsync(rawBody); } catch (e) { if (e instanceof zod_1.ZodError) { throw new error_js_1.APIError(400, { message: 'Bad Request: ' + e.message }, e); } else { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } } else { body = rawBody; } } else { let rawBody = null; try { rawBody = await request.clone().json(); } catch (e) { if (request.headers.get('content-type')?.includes('application/json')) { throw new error_js_1.APIError(400, { message: 'Bad Request: Request body is not JSON:\n' + await request.clone().text() }, e); } } console.log('Raw Body', rawBody); if (hasBody) { try { body = await route.schema.body.parseAsync(rawBody); console.log('Body', body); } catch (e) { if (e instanceof zod_1.ZodError) { throw new error_js_1.APIError(400, { message: 'Bad Request: ' + e.message }, e); } else { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } } else { body = rawBody; } } } const rawQuery = fast_querystring_1.default.parse((0, url_js_1.getQuery)(request.url)); let query = rawQuery; if (route.schema?.query) { try { query = await route.schema.query.parseAsync(rawQuery); } catch (e) { if (e instanceof zod_1.ZodError) { throw new error_js_1.APIError(400, { message: 'Bad Request: ' + e.message }, e); } else { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } } const rawHeaders = {}; request.headers.forEach((value, key) => { rawHeaders[key] = value; }); let ctxHeaders = rawHeaders; if (route.schema?.headers) { try { ctxHeaders = await route.schema.headers.parseAsync(rawHeaders); } catch (e) { if (e instanceof zod_1.ZodError) { throw new error_js_1.APIError(400, { message: 'Bad Request: ' + e.message }, e); } else { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } } const context = { request, body, query, params: parameters, headers: ctxHeaders, extras: {}, app: this }; try { let response; for (const before of route.before ?? []) { if (!response) { response = await before(context); } } if (!response) { response = await route.handler(context); } if (response.headers) { headers = { ...headers, ...response.headers }; } for (const after of route.after ?? []) { await after(context, response); } for (const handler of this.after) { await handler(request, response); } response.headers = { ...response.headers, ...headers }; return response; } catch (e) { throw new error_js_1.APIError(500, { message: 'Internal Server Error' }, e); } } catch (e) { if (e instanceof error_js_1.APIError) { return await this.handleError(request, e); } return await this.handleError(request, new error_js_1.APIError(500, { message: 'Internal Server Error' }, e)); } } async handleError(request, error) { if (this.error.length < 1) { console.error('There was an error while handling a request, but no error handlers were registered!'); console.error(error.error ?? error); } for (const handler of this.error) { const response = await handler(request, error); if (response) { return response; } } return { code: (error.code ?? 500), body: { message: error.message ?? 'Internal Server Error' } }; } async registerRoutesAsync(path) { const pathWithoutGlob = path.replace(/\*\.?\w*\*?/g, '').replace('//', '/'); for (const pathname of (0, glob_1.globSync)(path)) { if (!pathname.endsWith('.js') && !pathname.endsWith('.ts')) { continue; } const route = await Promise.resolve(`${pathname}`).then(s => __importStar(require(s))); if (route.default && route.default.handler) { if (!route.default.path || !route.default.method) { const split = pathname.replace(pathWithoutGlob, '').split('.')[0].split('/'); const method = split.pop()?.toUpperCase(); const remaining = split.reverse(); let name = '/'; while (split.length > 0) { const part = remaining.pop(); if (!part) { continue; } if (name === '/') { name += part; continue; } name += '/' + part; } if (!method || !name) { throw new Error('Invalid route path' + pathname.replace(pathWithoutGlob, '')); } route.default.path = name; route.default.method = method; } this.registerRoute(route.default); } } } } exports.Grace = Grace; function createGrace({ router = new trie_js_1.TrieRouter(), adapter = new adapter_js_1.NodeAdapter(), verbose = false } = { router: new trie_js_1.TrieRouter(), adapter: new adapter_js_1.NodeAdapter(), verbose: false }) { return new Grace(router, adapter, verbose); } exports.createGrace = createGrace; //# sourceMappingURL=grace.js.map