@grace-js/grace
Version:
An opinionated API framework
366 lines • 14.3 kB
JavaScript
;
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