@creamapi/cream
Version:
Concise REST API Maker - An extension library for express to create REST APIs faster
437 lines (436 loc) • 20.2 kB
JavaScript
;
/*
* 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);
};
}