UNPKG

@creamapi/cream

Version:

Concise REST API Maker - An extension library for express to create REST APIs faster

437 lines (436 loc) 20.2 kB
"use strict"; /* * Copyright 2024 Raul Radu * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.TRANSACTION_MANAGER_METADATA_KEY = exports.MIDDLEWARE_METADATA_KEY = exports.HEADERS_METADATA_KEY = exports.PARAMS_METADATA_KEY = exports.BODY_METADATA_KEY = void 0; exports.ExpressCall = ExpressCall; exports.Get = Get; exports.Post = Post; exports.Put = Put; exports.Delete = Delete; exports.ExpressController = ExpressController; exports.BodyField = BodyField; exports.Body = Body; exports.UrlParameter = UrlParameter; exports.Header = Header; require("reflect-metadata"); const ExpressErrorHandler_1 = require("../ExpressErrorHandler/ExpressErrorHandler"); const HttpMethod_1 = require("../HttpUtils/HttpMethod"); const Route_1 = require("../HttpUtils/Route"); const ParameterProp_1 = require("./ParameterProp"); const Serializer_1 = require("../Serializer/Serializer"); const HttpReturnCode_1 = require("../HttpUtils/HttpReturnCode"); const TransactionManager_1 = require("../ExchangeUtils/TransactionManager"); const ArgsBuilder_1 = require("./ArgsBuilder"); const Header_1 = require("../HttpUtils/Headers/Header"); const Cookie_1 = require("../HttpUtils/Cookies/Cookie"); const DynamicCookie_1 = require("../HttpUtils/Cookies/DynamicCookie"); exports.BODY_METADATA_KEY = Symbol('express:bodyAssoc'); exports.PARAMS_METADATA_KEY = Symbol('express:paramAssoc'); exports.HEADERS_METADATA_KEY = Symbol('express:headersAssoc'); exports.MIDDLEWARE_METADATA_KEY = Symbol('express:middlewareAssoc'); exports.TRANSACTION_MANAGER_METADATA_KEY = Symbol('express:transactionManager'); /** * This method is used to declare a method of a class (that must extend {@link ExpressModule}) * to be an API endpoint. This endpoint is bound to the router representing the class basepoint. * The basepoint (or zone) is defined by using {@link ExpressController} decorator.\ * The decorated method is not altered and that means it can be used as a normal method. * * @remarks * It is suggested to use {@link Get}, {@link Post}, {@link Put} and {@link Delete}\ * Methods are bound to the router in a topdown approach, this means that from Express's point of * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. * @example * ```ts * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; * * \@ExpressController("/my-base-route") * export class MyController extends ExpressModule { * \@ExpressCall("/hello-world", HttpMethod.GET) * myMethod(): string{ * return "hello, world"; * } * * // It also works with asynchronous methods * \@ExpressCall("/hello-world-async", HttpMethod.GET) * async myMethodAsync(): Promise<string> { * return "hello, async world!"; * } * } * ``` * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition * @param httpMethod The HTTP Method that must be used for the path. See {@link HttpMethod} for available methods * @returns returns the descriptor of the method. */ function ExpressCall(relativePath, httpMethod) { return function (target, propertyName, descriptor) { let method = descriptor.value; let methodTransactionManager = new TransactionManager_1.TransactionManager(method, target); Reflect.defineMetadata(exports.TRANSACTION_MANAGER_METADATA_KEY, methodTransactionManager, target, propertyName); let newMethod = (thisArg) => async function (req, res, next) { let argsBuilder = new ArgsBuilder_1.ArgsBuilder(req); let bodyAssocs = Reflect.getOwnMetadata(exports.BODY_METADATA_KEY, target, propertyName); let paramAssoc = Reflect.getOwnMetadata(exports.PARAMS_METADATA_KEY, target, propertyName); let headerMappings = Reflect.getOwnMetadata(exports.HEADERS_METADATA_KEY, target, propertyName); let middlewareAssoc = Reflect.getOwnMetadata(exports.MIDDLEWARE_METADATA_KEY, target, propertyName); argsBuilder .addBodyAssociations(bodyAssocs) .addParametersAssociations(paramAssoc) .addHeaderAssociations(headerMappings) .addMiddlewareAssociations(middlewareAssoc); try { let bootstrapSerializer = new Serializer_1.BootstrapSerializer(); // I want to reset the transaction manager here to guarantee that // it is always in the standard state methodTransactionManager.reset(thisArg); let result = await method.apply(thisArg, argsBuilder.finalize()); let data = await bootstrapSerializer.start(result); try { let cookiesManager = methodTransactionManager.getResponseCookiesManager(); let dynamicCookies = Reflect.getMetadata(DynamicCookie_1.HTTP_DYNAMIC_COOKIES, result) || []; cookiesManager.concat(dynamicCookies.map((cookieMap) => Cookie_1.Cookie.fromCookieOpts(cookieMap.cookieName, result[cookieMap.propertyName], cookieMap.opts))); } catch (e) { /* do nothing */ } try { methodTransactionManager.ReturnCode(Reflect.getMetadata(HttpReturnCode_1.HTTP_CODE_METADATA_KEY, result)); } catch (e) { /* do nothing */ } try { methodTransactionManager.setHeaders(Reflect.getMetadata(Header_1.HTTP_HEADERS_METADATA_KEY, result)); } catch (e) { /* Do nothing */ } methodTransactionManager .finalizeTransaction(res) .send(data); } catch (e) { if (e instanceof ExpressErrorHandler_1.RestError) { res.status(e.statusCode); } else { res.status(500); } next(e); } }; let methodRouters = Reflect.getOwnMetadata(Route_1.ROUTES_METADATA_KEY, target) || []; methodRouters.push(new Route_1.Route(relativePath, newMethod, propertyName, httpMethod)); Reflect.defineMetadata(Route_1.ROUTES_METADATA_KEY, methodRouters, target); return descriptor; }; } /** * This method is used to declare a method of a class (that must extend {@link ExpressModule}) * to be an API endpoint as a GET http method. * * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter * To understand its behaviour please see {@link ExpressCall}. * * @remarks Methods are bound to the router in a topdown approach, this means that from Express's point of * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. * @example * ```ts * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; * * \@ExpressController("/my-base-route") * export class MyController extends ExpressModule { * \@Get("/hello-world") * myMethod(): string{ * return "hello, world"; * } * * // It also works with asynchronous methods * \@Get("/hello-world-async") * async myMethodAsync(): Promise<string> { * return "hello, async world!"; * } * } * ``` * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition * @returns returns the descriptor of the method. */ function Get(relativePath) { return ExpressCall(relativePath, HttpMethod_1.HttpMethod.GET); } /** * This method is used to declare a method of a class (that must extend {@link ExpressModule}) * to be an API endpoint as a POST http method. * * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. * * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter * To understand its behaviour please see {@link ExpressCall}. * * @remarks * Methods are bound to the router in a topdown approach, this means that from Express's point of * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. * * @example * ```ts * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; * * \@ExpressController("/my-base-route") * export class MyController extends ExpressModule { * \@Post("/hello-world") * myMethod(@Body body: any): string{ * return "hello, world"; * } * * // It also works with asynchronous methods * \@Post("/hello-world-async") * async myMethodAsync(@BodyField("myField") myField: string): Promise<string> { * return myField; * } * } * ``` * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition * @returns returns the descriptor of the method. */ function Post(relativePath) { return ExpressCall(relativePath, HttpMethod_1.HttpMethod.POST); } /** * This method is used to declare a method of a class (that must extend {@link ExpressModule}) * to be an API endpoint as a PUT http method. * * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. * * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter * To understand its behaviour please see {@link ExpressCall}. * * @remarks Methods are bound to the router in a topdown approach, this means that from Express's point of * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. * @example * ```ts * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; * * \@ExpressController("/my-base-route") * export class MyController extends ExpressModule { * \@Put("/hello-world") * myMethod(@Body body: any): string{ * return "hello, world"; * } * * // It also works with asynchronous methods * \@Put("/hello-world-async") * async myMethodAsync(@BodyField("myField") myField: string): Promise<string> { * return myField; * } * } * ``` * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition * @returns returns the descriptor of the method. */ function Put(relativePath) { return ExpressCall(relativePath, HttpMethod_1.HttpMethod.PUT); } /** * This method is used to declare a method of a class (that must extend {@link ExpressModule}) * to be an API endpoint as a DELETE http method. * * To retrieve the body passed as an argument just use {@link Body} or {@link BodyField}. * * This method just works by calling {@link ExpressCall} with {@link HttpMethod.GET} as the second parameter * To understand its behaviour please see {@link ExpressCall}. * * @remarks * Methods are bound to the router in a topdown approach, this means that from Express's point of * view the top method is called first if two paths collide. Two paths collide when both the path and the http ù * method is the same so a path x bound to a Get request will not collide to the same path bound to a Post request. * * @example * ```ts * import { ExpressController, ExpressModule, ExpressCall, HttpMethod } from "@creamapi/cream"; * * \@ExpressController("/my-base-route") * export class MyController extends ExpressModule { * \@Delete("/hello-world") * myMethod(@Body body: any): string{ * return "hello, world"; * } * * // It also works with asynchronous methods * \@Delete("/hello-world-async") * async myMethodAsync(@BodyField("myField") myField: string): Promise<string> { * return myField; * } * } * ``` * @param relativePath Is the path relative to the basepoint. The path must follow the Express path definition * @returns returns the descriptor of the method. */ function Delete(relativePath) { return ExpressCall(relativePath, HttpMethod_1.HttpMethod.DELETE); } /** * This decorator is used to make a class to be a controller that handles HTTP requests. * The class decorated as a controller must inherit from {@link ExpressModule}. * In practice this will bound any {@link ExpressCall}-decorated method to an express router. * The router is also bound to the baseRoute parameter.\ * The call tree will look something like this: * ``` * / <- this is the basepoint (parameter baseRoute) a controller is bound to * |- router1 <- This is one controller * | |- GET * | | |- /path1 * | | | |- method1-1 <- this is a endpoint * | | | |- method1-2 <- multiple methods can be bound to the same route (aka they collide) * | | |- /path2 * | | | |- method1-3 * | | |- / <- this will look like it is bound to the base path * | | |- method1-4 * | |- POST * | |- PUT * | |- DELETE * |- router2 <- multiple controller can be bound to the same basepoint * | |- GET * | | |- /path1 * | | |- method2-1 <- this method is bound to the same path as method1-1 * | |- POST * | |- PUT * | |- DELETE * |- /new-endpoint <- this is another basepoint. Any method bound to this method will be bound to the base path /new-endpoint * | |- router1 * | |- GET * | |- POST * | |- PUT * | |- DELETE * ``` * @remarks * For whom want to work on low lever prototyping this decorator will alter the prototype of the * decorated class by adding functionalities without altering its behavior, including the constructor. * @param baseRoute the URL to which the controller is bound to * @returns a new class that extends the base decorated class that implements a few functions * that will bound routes to the router. Albeit it is a brand new class its usage is completely * transparent for the users. */ function ExpressController(baseRoute) { return function (target) { let routes = Reflect.getOwnMetadata(Route_1.ROUTES_METADATA_KEY, target.prototype) || []; return class extends target { constructor(...args) { super(...args); if (target.name != '') { this.className = target.name; } for (let route of routes) { this.initRoute(route); } this.baseUrl = baseRoute; } initRoute(route) { let expressRoute = this.router.route(route.route); let expressRouteParams = route.getMiddlewareMethods(); this.methodsMiddlewareList = this.methodsMiddlewareList.concat(route.middlewares); expressRouteParams.push(route.method(this)); switch (route.httpMethod) { case HttpMethod_1.HttpMethod.GET: expressRoute.get.apply(expressRoute, expressRouteParams); return; case HttpMethod_1.HttpMethod.POST: expressRoute.post.apply(expressRoute, expressRouteParams); return; case HttpMethod_1.HttpMethod.DELETE: expressRoute.delete.apply(expressRoute, expressRouteParams); return; case HttpMethod_1.HttpMethod.PUT: expressRoute.put.apply(expressRoute, expressRouteParams); return; } } }; }; } /** * This parameter decorator will decorate a method parameter by associating it with a field in the body. * This permits the autofill of the parameter with the corresponding field (named fieldName) in the body. * @remarks * If the field is undefined the field will be filled as undefined.\ * If no body is provided to the request then all parameters decorated with BodyField will be undefined * @param fieldName the field name in the body * @returns the decorator function */ function BodyField(fieldName) { return function (target, propertyKey, parameterIndex) { let existingRequiredParameters = Reflect.getOwnMetadata(exports.BODY_METADATA_KEY, target, propertyKey) || []; existingRequiredParameters.push(new ParameterProp_1.ParameterProp(parameterIndex, fieldName)); Reflect.defineMetadata(exports.BODY_METADATA_KEY, existingRequiredParameters, target, propertyKey); }; } /** * This parameter decorator will decorate a method parameter by associating it with the whole body. * If no body is provided to the request then all attributes decorated with Body will be undefined * @returns the decorator function */ function Body() { return BodyField(':body'); } /** * This parameter decorator will decorate a method parameter by associating it with a field in the URL. * This field must be defined in the url like normally done in express. * @example * ```ts * // we are in a controller class * * \@Get("/concat-space/:myParam1/:myParam2") * concatWithSpace( * \@UrlParameter("myParam1") param1: string, * \@UrlParameter("myParam2") param2: string * ): string { * return param1 + " " + param2; * } * //... * ``` * @remarks If the field is undefined in the URL request the field will be filled as undefined.\ * In general parameters are non-null because missing one parameter when making the request * will result to a different HTTP call and by extension a different controller method. * @param fieldName the field name in the body * @returns the decorator function */ function UrlParameter(fieldName) { return function (target, propertyKey, parameterIndex) { let existingRequiredParameters = Reflect.getOwnMetadata(exports.PARAMS_METADATA_KEY, target, propertyKey) || []; existingRequiredParameters.push(new ParameterProp_1.ParameterProp(parameterIndex, fieldName)); Reflect.defineMetadata(exports.PARAMS_METADATA_KEY, existingRequiredParameters, target, propertyKey); }; } /** * This parameter decorator will decorate a method parameter by associating it with a Request Header (eg. content-type). * @remarks If the header is undefined the field will be filled as undefined. * @param headerName the http header name * @returns the decorator function */ function Header(headerName) { return function (target, propertyKey, parameterIndex) { let existingHeaderMappings = Reflect.getOwnMetadata(exports.HEADERS_METADATA_KEY, target, propertyKey) || []; existingHeaderMappings.push(new ParameterProp_1.ParameterProp(parameterIndex, headerName)); Reflect.defineMetadata(exports.HEADERS_METADATA_KEY, existingHeaderMappings, target, propertyKey); }; }