From 6a233f036dbffbd0e5cc97c04ebfbca4c3324553 Mon Sep 17 00:00:00 2001 From: manusant Date: Tue, 5 Dec 2023 18:51:09 +0000 Subject: [PATCH 01/16] Several features for the node-boot persistence layer --- app-config.yaml | 55 +++ package.json | 7 +- packages/context/src/ApplicationContext.ts | 9 +- packages/context/src/ioc/index.ts | 1 + packages/context/src/ioc/types.ts | 33 ++ packages/context/src/metadata/index.ts | 1 - .../context/src/options/ApplicationOptions.ts | 4 +- .../context/src/options/ComponentOptions.ts | 22 +- packages/core/src/decorators/Component.ts | 4 +- .../src/decorators/NodeBootApplication.ts | 4 - packages/core/src/decorators/Service.ts | 4 +- .../core/src/properties/CorsProperties.ts | 77 ++++ .../src/properties/MultipartProperties.ts | 44 +++ packages/core/src/properties/index.ts | 1 + packages/core/src/server/BaseServer.ts | 49 ++- packages/di/src/decorators/Inject.ts | 29 +- packages/di/src/ioc/Constructable.ts | 10 - packages/di/src/ioc/index.ts | 1 - packages/di/src/ioc/types.ts | 16 +- packages/extension/src/Optional.ts | 10 +- pnpm-lock.yaml | 27 ++ samples/sample-express/app-config.yaml | 65 +++- samples/sample-express/package.json | 1 + samples/sample-express/src/app.ts | 17 +- .../src/config/AppConfigProperties.ts | 14 + .../src/config/BackendConfigProperties.ts | 10 - .../src/config/ClassTransformConfiguration.ts | 2 - .../src/config/MultipleConfigurations.ts | 10 +- .../src/controllers/users.controller.ts | 15 +- .../src/middlewares/error.middleware.ts | 4 +- .../src/persistence/CustomNamingStrategy.ts | 14 + .../src/persistence/entities/User.ts | 3 + .../sample-express/src/persistence/index.ts | 3 + .../listeners/GlobalEntityEventListener.ts | 157 ++++++++ .../listeners/UserEntityEventListener.ts | 36 ++ .../src/persistence/listeners/index.ts | 2 + .../migrations/1701774002463-migration.ts | 34 ++ .../migrations/1701786331338-migration.ts | 15 + .../src/persistence/migrations/index.ts | 2 + .../repositories/UserRepository.ts | 14 +- .../src/services/greeting.service.ts | 14 + .../src/services/users.service.ts | 21 +- samples/sample-fastify/app-config.yaml | 64 +++- samples/sample-fastify/src/app.ts | 8 +- samples/sample-koa/app-config.yaml | 64 +++- samples/sample-koa/src/app.ts | 8 +- servers/fastify-server/package.json | 1 + servers/fastify-server/src/FastifyServer.ts | 2 +- .../src/driver/FastifyDriver.ts | 64 +++- starters/persistence/README.md | 4 +- starters/persistence/package.json | 2 + .../persistence/src/PersistenceContext.ts | 22 ++ .../src/adapter/DefaultRepositoriesAdapter.ts | 11 +- .../src/adapter/PersistenceLogger.ts | 59 +++ .../src/config/DataSourceConfiguration.ts | 62 +++- .../src/config/PersistenceConfiguration.ts | 62 +++- .../src/config/QueryCacheConfiguration.ts | 87 +++++ .../src/decorator/DataRepository.ts | 6 +- .../src/decorator/EnableRepositories.ts | 4 + .../src/decorator/EntityEventSubscriber.ts | 11 + .../persistence/src/decorator/Migration.ts | 10 + .../src/decorator/PersistenceCache.ts | 13 + .../decorator/PersistenceNamingStrategy.ts | 10 + starters/persistence/src/decorator/index.ts | 4 + .../persistence/src/{hooks => hook}/hooks.ts | 0 .../persistence/src/{hooks => hook}/index.ts | 0 starters/persistence/src/index.ts | 3 +- .../src/metadata/RepositoryMetadata.ts | 6 +- .../AuroraMysqlConnectionProperties.ts | 29 ++ .../AuroraPostgresConnectionProperties.ts | 39 ++ .../BetterSqlite3ConnectionProperties.ts | 72 ++++ .../property/CockroachConnectionProperties.ts | 62 ++++ .../property/CommonDataSourceProperties.ts | 115 ++++++ .../src/property/MongoConnectionProperties.ts | 344 ++++++++++++++++++ .../src/property/MysqlConnectionProperties.ts | 143 ++++++++ .../property/OracleConnectionProperties.ts | 38 ++ .../src/property/PersistenceProperties.ts | 29 ++ .../property/PostgresConnectionProperties.ts | 96 +++++ .../src/property/QueryCacheProperties.ts | 37 ++ .../src/property/SapConnectionProperties.ts | 64 ++++ .../property/SpannerConnectionProperties.ts | 147 ++++++++ .../property/SqlServerConnectionProperties.ts | 314 ++++++++++++++++ .../property/SqliteConnectionProperties.ts | 62 ++++ starters/persistence/src/types.ts | 7 + 84 files changed, 2873 insertions(+), 173 deletions(-) create mode 100644 app-config.yaml create mode 100644 packages/context/src/ioc/types.ts create mode 100644 packages/core/src/properties/CorsProperties.ts create mode 100644 packages/core/src/properties/MultipartProperties.ts create mode 100644 packages/core/src/properties/index.ts delete mode 100644 packages/di/src/ioc/Constructable.ts create mode 100644 samples/sample-express/src/config/AppConfigProperties.ts delete mode 100644 samples/sample-express/src/config/BackendConfigProperties.ts create mode 100644 samples/sample-express/src/persistence/CustomNamingStrategy.ts create mode 100644 samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts create mode 100644 samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts create mode 100644 samples/sample-express/src/persistence/listeners/index.ts create mode 100644 samples/sample-express/src/persistence/migrations/1701774002463-migration.ts create mode 100644 samples/sample-express/src/persistence/migrations/1701786331338-migration.ts create mode 100644 samples/sample-express/src/persistence/migrations/index.ts create mode 100644 samples/sample-express/src/services/greeting.service.ts create mode 100644 starters/persistence/src/PersistenceContext.ts create mode 100644 starters/persistence/src/adapter/PersistenceLogger.ts create mode 100644 starters/persistence/src/config/QueryCacheConfiguration.ts create mode 100644 starters/persistence/src/decorator/EntityEventSubscriber.ts create mode 100644 starters/persistence/src/decorator/Migration.ts create mode 100644 starters/persistence/src/decorator/PersistenceCache.ts create mode 100644 starters/persistence/src/decorator/PersistenceNamingStrategy.ts rename starters/persistence/src/{hooks => hook}/hooks.ts (100%) rename starters/persistence/src/{hooks => hook}/index.ts (100%) rename {packages/context => starters/persistence}/src/metadata/RepositoryMetadata.ts (82%) create mode 100644 starters/persistence/src/property/AuroraMysqlConnectionProperties.ts create mode 100644 starters/persistence/src/property/AuroraPostgresConnectionProperties.ts create mode 100644 starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts create mode 100644 starters/persistence/src/property/CockroachConnectionProperties.ts create mode 100644 starters/persistence/src/property/CommonDataSourceProperties.ts create mode 100644 starters/persistence/src/property/MongoConnectionProperties.ts create mode 100644 starters/persistence/src/property/MysqlConnectionProperties.ts create mode 100644 starters/persistence/src/property/OracleConnectionProperties.ts create mode 100644 starters/persistence/src/property/PersistenceProperties.ts create mode 100644 starters/persistence/src/property/PostgresConnectionProperties.ts create mode 100644 starters/persistence/src/property/QueryCacheProperties.ts create mode 100644 starters/persistence/src/property/SapConnectionProperties.ts create mode 100644 starters/persistence/src/property/SpannerConnectionProperties.ts create mode 100644 starters/persistence/src/property/SqlServerConnectionProperties.ts create mode 100644 starters/persistence/src/property/SqliteConnectionProperties.ts create mode 100644 starters/persistence/src/types.ts diff --git a/app-config.yaml b/app-config.yaml new file mode 100644 index 00000000..3675adfe --- /dev/null +++ b/app-config.yaml @@ -0,0 +1,55 @@ +node-boot: + # App configurations + application: + name: "" + platform: "" + environment: "" + defaultErrorHandler: false + port: 3000 + api: + routePrefix: "" + nullResultCode: 200 + undefinedResultCode: 200 + paramOptions: + required: false + validations: + enable: true + enableDebugMessages: false + skipUndefinedProperties: false + skipNullProperties: false + skipMissingProperties: false + whitelist: false + forbidNonWhitelisted: false + forbidUnknownValues: false + stopAtFirstError: false + + # Server configurations + server: + cors: + origin: "*" + methods: + - GET + - POST + - DELETE + - PUT + credentials: true + maxAge: 55000 + cacheControl: 4096 + preflightContinue: true + optionsSuccessStatus: 204 + preflight: true + strictPreflight: false + exposedHeaders: [] + allowedHeaders: [] + multipart: + throwFileSizeLimit: true + limits: + fieldNameSize: 128 + fieldSize: 128 + fields: 10 + fileSize: 4096 + files: 5 + headerPairs: 10 + + # Storage configurations + persistence: diff --git a/package.json b/package.json index 9b84b1e7..56fe2ab0 100644 --- a/package.json +++ b/package.json @@ -59,5 +59,10 @@ "rimraf": "^4.3.1", "turbo": "^1.10.12", "typescript": "^5.1.6" - } + }, + "files": [ + "dist", + "config.d.ts" + ], + "configSchema": "config.d.ts" } diff --git a/packages/context/src/ApplicationContext.ts b/packages/context/src/ApplicationContext.ts index b0f017cf..7851aec3 100644 --- a/packages/context/src/ApplicationContext.ts +++ b/packages/context/src/ApplicationContext.ts @@ -7,9 +7,11 @@ import type { ConfigurationAdapter, ConfigurationPropertiesAdapter, } from "./adapters"; -import {ActuatorAdapter, OpenApiBridgeAdapter} from "./adapters"; -import {RepositoriesAdapter} from "./adapters"; -import {RepositoryMetadata} from "./metadata"; +import { + ActuatorAdapter, + OpenApiBridgeAdapter, + RepositoriesAdapter, +} from "./adapters"; export class ApplicationContext { private static context: ApplicationContext; @@ -25,7 +27,6 @@ export class ApplicationContext { controllerClasses: Function[] = []; interceptorClasses: Function[] = []; globalMiddlewares: Function[] = []; - repositories: RepositoryMetadata[] = []; /** * Indicates if class-transformer should be used to perform serialization / deserialization. diff --git a/packages/context/src/ioc/index.ts b/packages/context/src/ioc/index.ts index aaae8619..5b4cdeb7 100644 --- a/packages/context/src/ioc/index.ts +++ b/packages/context/src/ioc/index.ts @@ -1 +1,2 @@ export * from "./IocContainer"; +export * from "./types"; diff --git a/packages/context/src/ioc/types.ts b/packages/context/src/ioc/types.ts new file mode 100644 index 00000000..7f4ee185 --- /dev/null +++ b/packages/context/src/ioc/types.ts @@ -0,0 +1,33 @@ +/** + * Used to create unique typed component identifier. + * Useful when component has only interface, but don't have a class. + */ +export declare class Token { + name?: string; + + /** + * @param name Token name, optional and only used for debugging purposes. + */ + constructor(name?: string); +} + +/** + * Unique service identifier. + * Can be some class type, or string id, or instance of Token. + */ +export declare type ServiceIdentifier = + | Constructable + | CallableFunction + | Token + | string; + +/** + * Generic type for class definitions. + * Example usage: + * ``` + * function createSomeInstance(myClassDefinition: Constructable) { + * return new myClassDefinition() + * } + * ``` + */ +export declare type Constructable = new (...args: any[]) => T; diff --git a/packages/context/src/metadata/index.ts b/packages/context/src/metadata/index.ts index 9665d393..2094dcc9 100644 --- a/packages/context/src/metadata/index.ts +++ b/packages/context/src/metadata/index.ts @@ -1,2 +1 @@ export * from "./metadata.keys"; -export * from "./RepositoryMetadata"; diff --git a/packages/context/src/options/ApplicationOptions.ts b/packages/context/src/options/ApplicationOptions.ts index 3d6e83ab..a316ecd5 100644 --- a/packages/context/src/options/ApplicationOptions.ts +++ b/packages/context/src/options/ApplicationOptions.ts @@ -3,8 +3,8 @@ */ export interface ApplicationOptions { environment?: string; - platformName?: string; - appName?: string; + platform?: string; + name?: string; port?: number; /** diff --git a/packages/context/src/options/ComponentOptions.ts b/packages/context/src/options/ComponentOptions.ts index 5d220aa5..845c7e11 100644 --- a/packages/context/src/options/ComponentOptions.ts +++ b/packages/context/src/options/ComponentOptions.ts @@ -1,4 +1,14 @@ -export type ComponentOptions = { +import {Constructable, ServiceIdentifier} from "../ioc"; + +export type ComponentOptions = { + /** Unique identifier of the referenced service. */ + id?: ServiceIdentifier; + /** + * Class definition of the service what is used to initialize given service. + * This property maybe null if the value of the service is set manually. + * If id is not set then it serves as service id. + */ + type?: Constructable; /** * Indicates if this component must be global and same instance must be used across all containers. */ @@ -17,4 +27,14 @@ export type ComponentOptions = { * By default the registered classes are only instantiated when they are requested from the container. */ eager?: boolean; + /** + * Factory function used to initialize this service. + * Can be regular function ("createCar" for example), + * or other service which produces this instance ([CarFactory, "createCar"] for example). + */ + factory?: [Constructable, string] | CallableFunction | undefined; + /** + * Instance of the target class. + */ + value?: unknown | Symbol; }; diff --git a/packages/core/src/decorators/Component.ts b/packages/core/src/decorators/Component.ts index abd4d058..68fd64fb 100644 --- a/packages/core/src/decorators/Component.ts +++ b/packages/core/src/decorators/Component.ts @@ -1,5 +1,5 @@ -import {ComponentOptions} from "@node-boot/context"; -import {decorateDi, DiOptions, Token} from "@node-boot/di"; +import {ComponentOptions, Token} from "@node-boot/context"; +import {decorateDi, DiOptions} from "@node-boot/di"; /** * Marks class as a Component that can be injected using the DI Container. diff --git a/packages/core/src/decorators/NodeBootApplication.ts b/packages/core/src/decorators/NodeBootApplication.ts index 444357b8..dc76c92c 100644 --- a/packages/core/src/decorators/NodeBootApplication.ts +++ b/packages/core/src/decorators/NodeBootApplication.ts @@ -19,10 +19,6 @@ export function NodeBootApplication(options?: ApplicationOptions): Function { context.applicationOptions = { ...options, - environment: options?.environment ?? "development", - port: options?.port ?? 3000, - platformName: options?.platformName ?? "node-boot", - appName: options?.appName ?? "node-boot-app", }; // Bind Configurations adapters to search from @Beans under the Application class diff --git a/packages/core/src/decorators/Service.ts b/packages/core/src/decorators/Service.ts index 3cf74c05..5b1abf1e 100644 --- a/packages/core/src/decorators/Service.ts +++ b/packages/core/src/decorators/Service.ts @@ -1,5 +1,5 @@ -import {decorateDi, DiOptions, Token} from "@node-boot/di"; -import {ComponentOptions} from "@node-boot/context"; +import {decorateDi, DiOptions} from "@node-boot/di"; +import {ComponentOptions, Token} from "@node-boot/context"; /** * Marks class as a Component that can be injected using the DI Container. diff --git a/packages/core/src/properties/CorsProperties.ts b/packages/core/src/properties/CorsProperties.ts new file mode 100644 index 00000000..1da1e02a --- /dev/null +++ b/packages/core/src/properties/CorsProperties.ts @@ -0,0 +1,77 @@ +/** + * Express CORS: https://expressjs.com/en/resources/middleware/cors.html + * Koa CORS: https://github.com/koajs/cors + * Fastify CORS: https://github.com/fastify/fastify-cors + * + * */ +export type CorsProperties = { + /** + * Configures the Fastify Lifecycle Hook. + */ + hook?: + | "onRequest" + | "preParsing" + | "preValidation" + | "preHandler" + | "preSerialization" + | "onSend"; + /** + * Configures the Access-Control-Allow-Origin CORS header. + */ + origin?: boolean | string | "*" | RegExp | Array; + /** + * Configures the Access-Control-Allow-Methods CORS header. + * Expects a comma-delimited string (ex: 'GET,PUT,POST') or an array (ex: ['GET', 'PUT', 'POST']). + */ + methods?: string | string[]; + /** + * Configures the Access-Control-Allow-Credentials CORS header. + * Set to true to pass the header, otherwise it is omitted. + */ + credentials?: boolean; + /** + * Configures the Access-Control-Max-Age CORS header. + * Set to an integer to pass the header, otherwise it is omitted. + */ + maxAge?: number; + /** + * Configures the Cache-Control header for CORS preflight responses. + * Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, + * or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define + * the header value), otherwise the header is omitted. + */ + cacheControl?: number | string; + /** + * Pass the CORS preflight response to the route handler (default: false). + */ + preflightContinue?: boolean; + /** + * Provides a status code to use for successful OPTIONS requests, + * since some legacy browsers (IE11, various SmartTVs) choke on 204. + */ + optionsSuccessStatus?: number; + /** + * Pass the CORS preflight response to the route handler (default: false). + */ + preflight?: boolean; + /** + * Enforces strict requirement of the CORS preflight request headers (Access-Control-Request-Method and Origin). + * Preflight requests without the required headers will result in 400 errors when set to `true` (default: `true`). + */ + strictPreflight?: boolean; + /** + * Configures the Access-Control-Expose-Headers CORS header. + * Expects a comma-delimited string (ex: 'Content-Range,X-Content-Range') + * or an array (ex: ['Content-Range', 'X-Content-Range']). + * If not specified, no custom headers are exposed. + */ + exposedHeaders?: string | string[]; + /** + * Configures the Access-Control-Allow-Headers CORS header. + * Expects a comma-delimited string (ex: 'Content-Type,Authorization') + * or an array (ex: ['Content-Type', 'Authorization']). If not + * specified, defaults to reflecting the headers specified in the + * request's Access-Control-Request-Headers header. + */ + allowedHeaders?: string | string[]; +}; diff --git a/packages/core/src/properties/MultipartProperties.ts b/packages/core/src/properties/MultipartProperties.ts new file mode 100644 index 00000000..74d3f3d8 --- /dev/null +++ b/packages/core/src/properties/MultipartProperties.ts @@ -0,0 +1,44 @@ +/** + * Fastify https://github.com/fastify/fastify-multipart + * Express uses express multer: https://github.com/expressjs/multer#limits + * Koa uses koa multer: https://github.com/expressjs/multer#limits + * */ +export interface MultipartProperties { + /** + * Allow throwing error when file size limit reached. + * Fastify Only + */ + throwFileSizeLimit?: boolean; + + limits?: { + /** + * Max field name size in bytes + */ + fieldNameSize?: number; + + /** + * Max field value size in bytes + */ + fieldSize?: number; + + /** + * Max number of non-file fields + */ + fields?: number; + + /** + * For multipart forms, the max file size + */ + fileSize?: number; + + /** + * Max number of file fields + */ + files?: number; + + /** + * Max number of header key=>value pairs + */ + headerPairs?: number; + }; +} diff --git a/packages/core/src/properties/index.ts b/packages/core/src/properties/index.ts new file mode 100644 index 00000000..68689d5d --- /dev/null +++ b/packages/core/src/properties/index.ts @@ -0,0 +1 @@ +export {CorsProperties} from "./CorsProperties"; diff --git a/packages/core/src/server/BaseServer.ts b/packages/core/src/server/BaseServer.ts index f2b941a3..d60564f2 100644 --- a/packages/core/src/server/BaseServer.ts +++ b/packages/core/src/server/BaseServer.ts @@ -1,8 +1,9 @@ -import {ApplicationContext, Config} from "@node-boot/context"; +import {ApiOptions, ApplicationContext, Config} from "@node-boot/context"; import {Logger} from "winston"; import {createLogger} from "../logger"; import {ConfigService, loadNodeBootConfig} from "@node-boot/config"; import {useContainer} from "routing-controllers"; +import {ApplicationOptions} from "@node-boot/context/src"; export abstract class BaseServer { protected logger: Logger; @@ -12,8 +13,9 @@ export abstract class BaseServer { protected async init() { const context = ApplicationContext.get(); - await this.initLogger(context); await this.loadConfig(context); + this.setupAppConfigs(context); + await this.initLogger(context); } abstract listen(); @@ -61,17 +63,12 @@ export abstract class BaseServer { ); } - /* if (context.repositoriesAdapter && context.diOptions) { - this.logger.info(`Binding persistence repositories`); - context.repositoriesAdapter.bind(context.diOptions.iocContainer); - }*/ - if (context.actuatorAdapter) { this.logger.info(`Binding Actuator endpoints`); context.actuatorAdapter.bind( { serverType: this.serverType, - appName: context.applicationOptions.appName!, + appName: context.applicationOptions.name!, }, this.getFramework(), this.getRouter(), @@ -93,8 +90,38 @@ export abstract class BaseServer { } } + private setupAppConfigs(context: ApplicationContext) { + const appConfigs = + this.config.getOptional("node-boot.app"); + const apiConfigs = this.config.getOptional("node-boot.api"); + + context.applicationOptions = { + environment: + context.applicationOptions?.environment ?? + appConfigs?.environment ?? + "development", + port: context.applicationOptions?.port ?? appConfigs?.port ?? 3000, + platform: + context.applicationOptions?.platform ?? + appConfigs?.platform ?? + "node-boot", + name: + context.applicationOptions?.name ?? + appConfigs?.name ?? + "node-boot-app", + defaultErrorHandler: + context.applicationOptions?.defaultErrorHandler ?? + appConfigs?.defaultErrorHandler ?? + false, + customErrorHandler: + context.applicationOptions?.customErrorHandler ?? + appConfigs?.customErrorHandler ?? + false, + apiOptions: context.applicationOptions.apiOptions ?? apiConfigs, + }; + } + private async loadConfig(context: ApplicationContext) { - this.logger.info(`Loading Node-Boot configurations`); this.config = ( await loadNodeBootConfig({ argv: process.argv, @@ -106,8 +133,8 @@ export abstract class BaseServer { private async initLogger(context: ApplicationContext) { this.logger = createLogger( - context.applicationOptions.appName!, - context.applicationOptions.platformName!, + context.applicationOptions.name!, + context.applicationOptions.platform!, ); this.logger.info(`Initializing Node-Boot logger`); context.diOptions?.iocContainer.set(Logger, this.logger); diff --git a/packages/di/src/decorators/Inject.ts b/packages/di/src/decorators/Inject.ts index 7b50afa4..dcedf8b9 100644 --- a/packages/di/src/decorators/Inject.ts +++ b/packages/di/src/decorators/Inject.ts @@ -1,10 +1,7 @@ -import { - Abstract, - decorateInjection, - InjectionOptions, - Newable, - Token, -} from "../ioc"; +import {Abstract, decorateInjection, InjectionOptions, Newable} from "../ioc"; +import {Token} from "typedi"; + +export const REQUIRES_FIELD_INJECTION_KEY = "custom:requiresFieldInjection"; /** * Injects a service/component into a class property or constructor parameter. @@ -17,6 +14,24 @@ export function Inject(symbolValue?: symbol): Function; export function Inject(token: Token): Function; export function Inject(options?: InjectionOptions): Function { return (target: Object, propertyName: string | Symbol, index?: number) => { + // Registering metadata for custom filed injection (used for example in the Persistence Event Subscribers) + if (propertyName && typeof propertyName === "string") { + const propertyType = Reflect.getMetadata( + "design:type", + target, + propertyName, + ); + const injectProperties: string[] = + Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, target) || []; + injectProperties.push(propertyName); + Reflect.defineMetadata( + REQUIRES_FIELD_INJECTION_KEY, + injectProperties, + target, + ); + } + + // Normal injection decorateInjection(target, propertyName, index, options); }; } diff --git a/packages/di/src/ioc/Constructable.ts b/packages/di/src/ioc/Constructable.ts deleted file mode 100644 index acd1b012..00000000 --- a/packages/di/src/ioc/Constructable.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Generic type for class definitions. - * Example usage: - * ``` - * function createSomeInstance(myClassDefinition: Constructable) { - * return new myClassDefinition() - * } - * ``` - */ -export declare type Constructable = new (...args: any[]) => T; diff --git a/packages/di/src/ioc/index.ts b/packages/di/src/ioc/index.ts index 2fee4059..d885884b 100644 --- a/packages/di/src/ioc/index.ts +++ b/packages/di/src/ioc/index.ts @@ -1,4 +1,3 @@ -export * from "./Constructable"; export * from "./makeDiDecoration"; export * from "./makeInjectionDecoration"; export * from "./types"; diff --git a/packages/di/src/ioc/types.ts b/packages/di/src/ioc/types.ts index 203f5425..15c0b2f3 100644 --- a/packages/di/src/ioc/types.ts +++ b/packages/di/src/ioc/types.ts @@ -1,18 +1,4 @@ -import {Constructable} from "./Constructable"; -import {ComponentOptions} from "@node-boot/context"; - -/** - * Used to create unique typed component identifier. - * Useful when component has only interface, but don't have a class. - */ -export declare class Token { - name?: string; - - /** - * @param name Token name, optional and only used for debugging purposes. - */ - constructor(name?: string); -} +import {ComponentOptions, Constructable, Token} from "@node-boot/context"; export type Newable = (type?: never) => Constructable; diff --git a/packages/extension/src/Optional.ts b/packages/extension/src/Optional.ts index 9827c068..34f10524 100644 --- a/packages/extension/src/Optional.ts +++ b/packages/extension/src/Optional.ts @@ -124,16 +124,22 @@ export class Optional { } /** - * The map method is used to transform the value inside the Optional object using a provided mapper function. + * The map method is used to transform the value inside the Optional object using a provided mapper function, + * or return a default value if the Optional is empty. * * @param mapper - A function that takes the current value of type T and returns a new value of type U. + * @param defaultValue - default value when the optional is empty * @return Returns a new Optional object with the mapped value. + * returns a new Optional object with the mapped value or the default Optional value. * Throws an error if the original Optional object is empty. * */ - map(mapper: (value: T) => U): Optional { + map(mapper: (value: T) => U, defaultValue?: U): Optional { if (this.isPresent()) { return Optional.of(mapper(this.value!)); } + if (defaultValue) { + return Optional.of(defaultValue); + } throw new Error("Map operation cannot be called on an empty Optional"); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c51e37d..ee7cb5f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -530,6 +530,9 @@ importers: '@fastify/cookie': specifier: ^9.0.4 version: 9.0.4 + '@fastify/cors': + specifier: ^8.4.1 + version: 8.4.1 '@fastify/multipart': specifier: ^7.7.3 version: 7.7.3 @@ -638,6 +641,12 @@ importers: '@node-boot/core': specifier: 1.0.0 version: link:../../packages/core + '@node-boot/di': + specifier: 1.0.0 + version: link:../../packages/di + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension reflect-metadata: specifier: '>=0.1.13' version: 0.1.13 @@ -1160,6 +1169,13 @@ packages: fastify-plugin: 4.5.1 dev: false + /@fastify/cors@8.4.1: + resolution: {integrity: sha512-iYQJtrY3pFiDS5mo5zRaudzg2OcUdJ96PD6xfkKOOEilly5nnrFZx/W6Sce2T79xxlEn2qpU3t5+qS2phS369w==} + dependencies: + fastify-plugin: 4.5.1 + mnemonist: 0.39.5 + dev: false + /@fastify/deepmerge@1.3.0: resolution: {integrity: sha512-J8TOSBq3SoZbDhM9+R/u77hP93gz/rajSA+K2kGyijPpORPWUXHUpTaleoj+92As0S9uPRP7Oi8IqMf0u+ro6A==} dev: false @@ -1597,6 +1613,7 @@ packages: /@koa/router@12.0.0: resolution: {integrity: sha512-cnnxeKHXlt7XARJptflGURdJaO+ITpNkOHmQu7NHmCoRinPbyvFzce/EG/E8Zy81yQ1W9MoSdtklc3nyaDReUw==} engines: {node: '>= 12'} + deprecated: '**IMPORTANT 10x+ PERFORMANCE UPGRADE**: Please upgrade to v12.0.1+ as we have fixed an issue with debuglog causing 10x slower router benchmark performance, see https://github.com/koajs/router/pull/173' requiresBuild: true dependencies: http-errors: 2.0.0 @@ -5117,6 +5134,12 @@ packages: hasBin: true dev: false + /mnemonist@0.39.5: + resolution: {integrity: sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==} + dependencies: + obliterator: 2.0.4 + dev: false + /ms@2.0.0: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} requiresBuild: true @@ -5265,6 +5288,10 @@ packages: /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} + /obliterator@2.0.4: + resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==} + dev: false + /on-exit-leak-free@2.1.0: resolution: {integrity: sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==} dev: false diff --git a/samples/sample-express/app-config.yaml b/samples/sample-express/app-config.yaml index 9d608d70..138de7f7 100644 --- a/samples/sample-express/app-config.yaml +++ b/samples/sample-express/app-config.yaml @@ -1,3 +1,62 @@ -backend: - baseUrl: http://localhost:7000 - allowInsecureCookie: true +node-boot: + # App configurations + app: + name: "facts-service" + platform: "tech-insights" + environment: "development" + defaultErrorHandler: false + port: 3000 + + api: + routePrefix: "/api" + nullResultCode: 200 + undefinedResultCode: 200 + paramOptions: + required: false + validations: + enable: true + enableDebugMessages: false + skipUndefinedProperties: false + skipNullProperties: false + skipMissingProperties: false + whitelist: false + forbidNonWhitelisted: false + forbidUnknownValues: false + stopAtFirstError: false + + # Server configurations + server: + cors: + origin: "*" + methods: + - GET + - POST + - DELETE + - PUT + credentials: true + maxAge: 55000 + cacheControl: 4096 + preflightContinue: true + optionsSuccessStatus: 204 + preflight: true + strictPreflight: false + exposedHeaders: [] + allowedHeaders: [] + multipart: + throwFileSizeLimit: true + limits: + fieldNameSize: 128 + fieldSize: 128 + fields: 10 + fileSize: 4096 + files: 5 + headerPairs: 10 + + # Storage configurations + persistence: + type: "better-sqlite3" + synchronize: false # False, meaning that the application rely on migrations + cache: true + runMigrations: true + better-sqlite3: + database: "express-sample.db" diff --git a/samples/sample-express/package.json b/samples/sample-express/package.json index 31035a43..3ea122cc 100644 --- a/samples/sample-express/package.json +++ b/samples/sample-express/package.json @@ -18,6 +18,7 @@ "main": "dist/index.js", "types": "dist/index.d.ts", "scripts": { + "create:migration": "typeorm migration:create ./src/persistence/migrations/migration", "start": "pnpm run build && node dist/server.js", "start:prod": "pnpm run build && NODE_ENV=production node dist/server.js", "dev": "NODE_ENV=development nodemon", diff --git a/samples/sample-express/src/app.ts b/samples/sample-express/src/app.ts index e3432292..c9d1ff87 100644 --- a/samples/sample-express/src/app.ts +++ b/samples/sample-express/src/app.ts @@ -9,7 +9,7 @@ import { NodeBootApplication, } from "@node-boot/core"; import {EnableOpenApi} from "@node-boot/openapi"; -import {BackendConfigProperties} from "./config/BackendConfigProperties"; +import {AppConfigProperties} from "./config/AppConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; @@ -23,7 +23,7 @@ import {EnableRepositories} from "@node-boot/starter-persistence"; @EnableDI(Container) @EnableOpenApi() -@Configurations([BackendConfigProperties, MultipleConfigurations]) +@Configurations([AppConfigProperties, MultipleConfigurations]) @Controllers([UserController]) @GlobalMiddlewares([LoggingMiddleware, ErrorMiddleware]) //@EnableComponentScan() @@ -40,16 +40,7 @@ import {EnableRepositories} from "@node-boot/starter-persistence"; @EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationResolver) @EnableActuator() @EnableRepositories() -@NodeBootApplication({ - environment: "development", - appName: "facts-service", - platformName: "tech-insights", - defaultErrorHandler: false, - port: 3000, - apiOptions: { - routePrefix: "/api", - }, -}) +@NodeBootApplication() export class FactsServiceApp { static start() { NodeBoot.run(ExpressServer) @@ -59,6 +50,8 @@ export class FactsServiceApp { }) .catch(error => { console.error("Error starting Node-Boot application.", error); + // Terminate the process with a non-zero exit code (1). + process.exit(1); }); } } diff --git a/samples/sample-express/src/config/AppConfigProperties.ts b/samples/sample-express/src/config/AppConfigProperties.ts new file mode 100644 index 00000000..3198602a --- /dev/null +++ b/samples/sample-express/src/config/AppConfigProperties.ts @@ -0,0 +1,14 @@ +import {ConfigurationProperties} from "@node-boot/config"; + +@ConfigurationProperties({ + configPath: "node-boot.app", + configName: "app-config", +}) +export class AppConfigProperties { + name: string; + platform: string; + environment: string; + defaultErrorHandler: boolean; + customErrorHandler?: boolean; + port: number; +} diff --git a/samples/sample-express/src/config/BackendConfigProperties.ts b/samples/sample-express/src/config/BackendConfigProperties.ts deleted file mode 100644 index acd6b4b0..00000000 --- a/samples/sample-express/src/config/BackendConfigProperties.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {ConfigurationProperties} from "@node-boot/config"; - -@ConfigurationProperties({ - configPath: "backend", - configName: "backend-config", -}) -export class BackendConfigProperties { - baseUrl: string; - allowInsecureCookie: boolean; -} diff --git a/samples/sample-express/src/config/ClassTransformConfiguration.ts b/samples/sample-express/src/config/ClassTransformConfiguration.ts index 691a7663..a551ce02 100644 --- a/samples/sample-express/src/config/ClassTransformConfiguration.ts +++ b/samples/sample-express/src/config/ClassTransformConfiguration.ts @@ -1,11 +1,9 @@ import { ClassToPlainTransform, - Configuration, EnableClassTransformer, PlainToClassTransform, } from "@node-boot/core"; -@Configuration() @EnableClassTransformer({enabled: false}) @ClassToPlainTransform({ strategy: "exposeAll", diff --git a/samples/sample-express/src/config/MultipleConfigurations.ts b/samples/sample-express/src/config/MultipleConfigurations.ts index 2e32fc33..051095d3 100644 --- a/samples/sample-express/src/config/MultipleConfigurations.ts +++ b/samples/sample-express/src/config/MultipleConfigurations.ts @@ -1,7 +1,11 @@ -import {Configuration, Configurations} from "@node-boot/core"; +import {Configurations} from "@node-boot/core"; import {SecurityConfiguration} from "./SecurityConfiguration"; import {ClassTransformConfiguration} from "./ClassTransformConfiguration"; +import {CustomNamingStrategy} from "../persistence"; -@Configuration() -@Configurations([SecurityConfiguration, ClassTransformConfiguration]) +@Configurations([ + SecurityConfiguration, + ClassTransformConfiguration, + CustomNamingStrategy, +]) export class MultipleConfigurations {} diff --git a/samples/sample-express/src/controllers/users.controller.ts b/samples/sample-express/src/controllers/users.controller.ts index 887b2fc7..a5b7e304 100644 --- a/samples/sample-express/src/controllers/users.controller.ts +++ b/samples/sample-express/src/controllers/users.controller.ts @@ -11,7 +11,7 @@ import { import {UserService} from "../services/users.service"; import {ValidationMiddleware} from "../middlewares/validation.middleware"; import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; -import {BackendConfigProperties} from "../config/BackendConfigProperties"; +import {AppConfigProperties} from "../config/AppConfigProperties"; import {Logger} from "winston"; import {Controller} from "@node-boot/core"; import {Inject} from "@node-boot/di"; @@ -24,8 +24,8 @@ export class UserController { constructor( private readonly user: UserService, private readonly logger: Logger, - @Inject("backend-config") - private readonly backendConfigProperties: BackendConfigProperties, + @Inject("app-config") + private readonly appConfigProperties: AppConfigProperties, ) {} @Get("/") @@ -33,13 +33,20 @@ export class UserController { async getUsers() { this.logger.info( `Injected backend configuration properties: ${JSON.stringify( - this.backendConfigProperties, + this.appConfigProperties, )}`, ); const findAllUsersData: User[] = await this.user.findAllUser(); return {data: findAllUsersData, message: "findAll"}; } + @Get("/query/") + @OpenAPI({summary: "Return a list of users using a custom query"}) + async getWithCustomQuery() { + const data: User[] = await this.user.findWithCustomQuery(); + return {data: data, message: "findWithCustomQuery"}; + } + @Get("/:id") @OpenAPI({summary: "Return find a user"}) async getUserById(@Param("id") userId: number) { diff --git a/samples/sample-express/src/middlewares/error.middleware.ts b/samples/sample-express/src/middlewares/error.middleware.ts index cb85f53f..4ce5b51f 100644 --- a/samples/sample-express/src/middlewares/error.middleware.ts +++ b/samples/sample-express/src/middlewares/error.middleware.ts @@ -16,7 +16,9 @@ export class ErrorMiddleware implements ExpressErrorMiddlewareInterface { this.logger.error( `[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`, ); - res.status(status).json({message}); + // FIXME Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client + // FIXME Fix this after refactoring routing-controllers library + // res.status(status).json({message}); } catch (error) { next(error); } diff --git a/samples/sample-express/src/persistence/CustomNamingStrategy.ts b/samples/sample-express/src/persistence/CustomNamingStrategy.ts new file mode 100644 index 00000000..078ae4a3 --- /dev/null +++ b/samples/sample-express/src/persistence/CustomNamingStrategy.ts @@ -0,0 +1,14 @@ +import {DefaultNamingStrategy} from "typeorm"; +import {PersistenceNamingStrategy} from "@node-boot/starter-persistence"; + +@PersistenceNamingStrategy() +export class CustomNamingStrategy extends DefaultNamingStrategy { + name = "sample-naming-strategy"; + + override tableName( + targetName: string, + userSpecifiedName: string | undefined, + ): string { + return `nb-${super.tableName(targetName, userSpecifiedName)}`; + } +} diff --git a/samples/sample-express/src/persistence/entities/User.ts b/samples/sample-express/src/persistence/entities/User.ts index 8b8e8948..82271d6f 100644 --- a/samples/sample-express/src/persistence/entities/User.ts +++ b/samples/sample-express/src/persistence/entities/User.ts @@ -10,4 +10,7 @@ export class User { @Column() password: string; + + @Column() + name?: string; // New field } diff --git a/samples/sample-express/src/persistence/index.ts b/samples/sample-express/src/persistence/index.ts index d5365844..8435a3ed 100644 --- a/samples/sample-express/src/persistence/index.ts +++ b/samples/sample-express/src/persistence/index.ts @@ -1,2 +1,5 @@ export * from "./entities"; export * from "./repositories"; +export * from "./migrations"; +export * from "./listeners"; +export {CustomNamingStrategy} from "./CustomNamingStrategy"; diff --git a/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts new file mode 100644 index 00000000..b5ca1da7 --- /dev/null +++ b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts @@ -0,0 +1,157 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import { + EntitySubscriberInterface, + InsertEvent, + RecoverEvent, + RemoveEvent, + SoftRemoveEvent, + TransactionCommitEvent, + TransactionRollbackEvent, + TransactionStartEvent, + UpdateEvent, +} from "typeorm"; +import {Logger} from "winston"; +import {Inject} from "@node-boot/di"; + +@EntityEventSubscriber() +export class GlobalEntityEventListener implements EntitySubscriberInterface { + @Inject() + private logger: Logger; + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + this.logger.info(`AFTER ENTITY LOADED: `, entity); + } + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE ENTITY INSERTED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + this.logger.info(`AFTER ENTITY INSERTED: `, event.entity); + } + + /** + * Called before entity update. + */ + beforeUpdate(event: UpdateEvent) { + this.logger.info(`BEFORE ENTITY UPDATED: `, event.entity); + } + + /** + * Called after entity update. + */ + afterUpdate(event: UpdateEvent) { + this.logger.info(`AFTER ENTITY UPDATED: `, event.entity); + } + + /** + * Called before entity removal. + */ + beforeRemove(event: RemoveEvent) { + this.logger.info( + `BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, + event.entity, + ); + } + + /** + * Called after entity removal. + */ + afterRemove(event: RemoveEvent) { + this.logger.info( + `AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, + event.entity, + ); + } + + /** + * Called before entity removal. + */ + beforeSoftRemove(event: SoftRemoveEvent) { + this.logger.info( + `BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, + event.entity, + ); + } + + /** + * Called after entity removal. + */ + afterSoftRemove(event: SoftRemoveEvent) { + this.logger.info( + `AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, + event.entity, + ); + } + + /** + * Called before entity recovery. + */ + beforeRecover(event: RecoverEvent) { + this.logger.info( + `BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, + event.entity, + ); + } + + /** + * Called after entity recovery. + */ + afterRecover(event: RecoverEvent) { + this.logger.info( + `AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, + event.entity, + ); + } + + /** + * Called before transaction start. + */ + beforeTransactionStart(event: TransactionStartEvent) { + this.logger.info(`BEFORE TRANSACTION STARTED`); + } + + /** + * Called after transaction start. + */ + afterTransactionStart(event: TransactionStartEvent) { + this.logger.info(`AFTER TRANSACTION STARTED`); + } + + /** + * Called before transaction commit. + */ + beforeTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`BEFORE TRANSACTION COMMITTED`); + } + + /** + * Called after transaction commit. + */ + afterTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`AFTER TRANSACTION COMMITTED`); + } + + /** + * Called before transaction rollback. + */ + beforeTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`BEFORE TRANSACTION ROLLBACK`); + } + + /** + * Called after transaction rollback. + */ + afterTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`AFTER TRANSACTION ROLLBACK`); + } +} diff --git a/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts new file mode 100644 index 00000000..e7e780aa --- /dev/null +++ b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts @@ -0,0 +1,36 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import {EntitySubscriberInterface, InsertEvent} from "typeorm"; +import {User} from "../entities"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; +import {GreetingService} from "../../services/greeting.service"; + +@EntityEventSubscriber() +export class UserEntityEventListener + implements EntitySubscriberInterface +{ + @Inject() + private logger: Logger; + + @Inject() + private greetingService: GreetingService; + + /** + * Indicates that this subscriber only listen to User events. + */ + listenTo() { + return User; + } + + /** + * Called before user insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE USER INSERTED: `, event.entity); + } + + afterInsert(event: InsertEvent): Promise | void { + this.logger.info(`AFTER USER INSERTED: `, event.entity); + this.greetingService.sayHello(event.entity); + } +} diff --git a/samples/sample-express/src/persistence/listeners/index.ts b/samples/sample-express/src/persistence/listeners/index.ts new file mode 100644 index 00000000..9a3d3a70 --- /dev/null +++ b/samples/sample-express/src/persistence/listeners/index.ts @@ -0,0 +1,2 @@ +export {GlobalEntityEventListener} from "./GlobalEntityEventListener"; +export {UserEntityEventListener} from "./UserEntityEventListener"; diff --git a/samples/sample-express/src/persistence/migrations/1701774002463-migration.ts b/samples/sample-express/src/persistence/migrations/1701774002463-migration.ts new file mode 100644 index 00000000..76c4591e --- /dev/null +++ b/samples/sample-express/src/persistence/migrations/1701774002463-migration.ts @@ -0,0 +1,34 @@ +import {MigrationInterface, QueryRunner, Table} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701774002463 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "nb-user", + columns: [ + { + name: "id", + type: "INTEGER", + isPrimary: true, + isGenerated: true, + generationStrategy: "increment", + }, + { + name: "email", + type: "varchar", + }, + { + name: "password", + type: "varchar", + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("nb-user"); + } +} diff --git a/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts b/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts new file mode 100644 index 00000000..93df1e8b --- /dev/null +++ b/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts @@ -0,0 +1,15 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701786331338 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "nb-user" ADD COLUMN "name" varchar(255)`, + ); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "nb-user" DROP COLUMN "name"`); + } +} diff --git a/samples/sample-express/src/persistence/migrations/index.ts b/samples/sample-express/src/persistence/migrations/index.ts new file mode 100644 index 00000000..f3ee9f0b --- /dev/null +++ b/samples/sample-express/src/persistence/migrations/index.ts @@ -0,0 +1,2 @@ +export {Migration1701774002463} from "./1701774002463-migration"; +export {Migration1701786331338} from "./1701786331338-migration"; diff --git a/samples/sample-express/src/persistence/repositories/UserRepository.ts b/samples/sample-express/src/persistence/repositories/UserRepository.ts index a36cefdd..fdabc794 100644 --- a/samples/sample-express/src/persistence/repositories/UserRepository.ts +++ b/samples/sample-express/src/persistence/repositories/UserRepository.ts @@ -3,4 +3,16 @@ import {DataRepository} from "@node-boot/starter-persistence"; import {User} from "../entities"; @DataRepository(User) -export class UserRepository extends Repository {} +export class UserRepository extends Repository { + /** + * Example custom query using the built-in query builder. + * + * For detailed info check: https://orkhan.gitbook.io/typeorm/docs/select-query-builder + * */ + findByQueryIn() { + // SELECT ... FROM users user WHERE user.id IN (1, 2) + return this.createQueryBuilder("user") + .where("user.id IN (:...ids)", {ids: [1, 2]}) + .getMany(); + } +} diff --git a/samples/sample-express/src/services/greeting.service.ts b/samples/sample-express/src/services/greeting.service.ts new file mode 100644 index 00000000..3886f755 --- /dev/null +++ b/samples/sample-express/src/services/greeting.service.ts @@ -0,0 +1,14 @@ +import {Logger} from "winston"; +import {Service} from "@node-boot/core"; +import {User} from "../persistence"; + +@Service() +export class GreetingService { + constructor(private readonly logger: Logger) {} + + public sayHello(user: User): void { + this.logger.info( + `I'm really happy that you exists ${user.id}/${user.email}`, + ); + } +} diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index 35c9f6a5..bbf6d0a8 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -25,11 +25,16 @@ export class UserService { public async findAllUser(): Promise { this.logger.info("Getting all users"); - const baseUrl = this.configService.getString("backend.baseUrl"); + const appName = this.configService.getString("node-boot.app.name"); this.logger.info( - `Reading backend.baseUrl from app-config.yam: ${baseUrl}`, + `Reading node-boot.app.name from app-config.yam: ${appName}`, ); - return await this.userRepository.find(); + return this.userRepository.find(); + } + + public async findWithCustomQuery(): Promise { + this.logger.info("Getting all users with a custom query"); + return this.userRepository.findByQueryIn(); } public async findUserById(userId: number): Promise { @@ -51,7 +56,7 @@ export class UserService { this.logger.info("Transaction was successfully committed"); }); - return await Optional.of(existingUser) + return Optional.of(existingUser) .ifPresentThrow( () => new HttpException( @@ -59,7 +64,7 @@ export class UserService { `This email ${userData.email} already exists`, ), ) - .elseAsync(async () => await this.userRepository.save(userData)); + .elseAsync(() => this.userRepository.save(userData)); } @Transactional() @@ -71,7 +76,7 @@ export class UserService { id: userId, }); - return await Optional.of(user) + return Optional.of(user) .orElseThrow(() => new HttpException(409, "User doesn't exist")) .map(user => { return { @@ -97,9 +102,7 @@ export class UserService { await Optional.of(user) .orElseThrow(() => new HttpException(409, "User doesn't exist")) - .runAsync( - async user => await this.userRepository.delete({id: userId}), - ); + .runAsync(user => this.userRepository.delete({id: userId})); throw new Error( "Error after deleting that should rollback transaction", diff --git a/samples/sample-fastify/app-config.yaml b/samples/sample-fastify/app-config.yaml index 9d608d70..7523493e 100644 --- a/samples/sample-fastify/app-config.yaml +++ b/samples/sample-fastify/app-config.yaml @@ -1,3 +1,61 @@ -backend: - baseUrl: http://localhost:7000 - allowInsecureCookie: true +node-boot: + # App configurations + app: + name: "facts-service" + platform: "tech-insights" + environment: "development" + defaultErrorHandler: false + port: 10000 + + api: + routePrefix: "/api" + nullResultCode: 200 + undefinedResultCode: 200 + paramOptions: + required: false + validations: + enable: true + enableDebugMessages: false + skipUndefinedProperties: false + skipNullProperties: false + skipMissingProperties: false + whitelist: false + forbidNonWhitelisted: false + forbidUnknownValues: false + stopAtFirstError: false + + # Server configurations + server: + cors: + origin: "*" + methods: + - GET + - POST + - DELETE + - PUT + credentials: true + maxAge: 55000 + cacheControl: 4096 + preflightContinue: true + optionsSuccessStatus: 204 + preflight: true + strictPreflight: false + exposedHeaders: [] + allowedHeaders: [] + multipart: + throwFileSizeLimit: true + limits: + fieldNameSize: 128 + fieldSize: 128 + fields: 10 + fileSize: 4096 + files: 5 + headerPairs: 10 + + # Storage configurations + persistence: + type: "better-sqlite3" + synchronize: true # FIXME create tables automatically through migrations instead of synchronize, + cache: true + better-sqlite3: + database: "fastify-sample.db" diff --git a/samples/sample-fastify/src/app.ts b/samples/sample-fastify/src/app.ts index d62d561f..63c6f220 100644 --- a/samples/sample-fastify/src/app.ts +++ b/samples/sample-fastify/src/app.ts @@ -38,13 +38,7 @@ import {EnableActuator} from "@node-boot/starter-actuator"; * */ @EnableActuator() @EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationResolver) -@NodeBootApplication({ - environment: "development", - appName: "facts-service", - platformName: "tech-insights", - defaultErrorHandler: false, - port: 3000, -}) +@NodeBootApplication() export class FactsServiceApp { static start() { NodeBoot.run(FastifyServer) diff --git a/samples/sample-koa/app-config.yaml b/samples/sample-koa/app-config.yaml index 9d608d70..d818ebe0 100644 --- a/samples/sample-koa/app-config.yaml +++ b/samples/sample-koa/app-config.yaml @@ -1,3 +1,61 @@ -backend: - baseUrl: http://localhost:7000 - allowInsecureCookie: true +node-boot: + # App configurations + app: + name: "facts-service" + platform: "tech-insights" + environment: "development" + defaultErrorHandler: false + port: 10000 + + api: + routePrefix: "/api" + nullResultCode: 200 + undefinedResultCode: 200 + paramOptions: + required: false + validations: + enable: true + enableDebugMessages: false + skipUndefinedProperties: false + skipNullProperties: false + skipMissingProperties: false + whitelist: false + forbidNonWhitelisted: false + forbidUnknownValues: false + stopAtFirstError: false + + # Server configurations + server: + cors: + origin: "*" + methods: + - GET + - POST + - DELETE + - PUT + credentials: true + maxAge: 55000 + cacheControl: 4096 + preflightContinue: true + optionsSuccessStatus: 204 + preflight: true + strictPreflight: false + exposedHeaders: [] + allowedHeaders: [] + multipart: + throwFileSizeLimit: true + limits: + fieldNameSize: 128 + fieldSize: 128 + fields: 10 + fileSize: 4096 + files: 5 + headerPairs: 10 + + # Storage configurations + persistence: + type: "better-sqlite3" + synchronize: true # FIXME create tables automatically through migrations instead of synchronize, + cache: true + better-sqlite3: + database: "koa-sample.db" diff --git a/samples/sample-koa/src/app.ts b/samples/sample-koa/src/app.ts index ed7200b0..e61ef994 100644 --- a/samples/sample-koa/src/app.ts +++ b/samples/sample-koa/src/app.ts @@ -37,13 +37,7 @@ import {EnableActuator} from "@node-boot/starter-actuator"; * */ @EnableActuator() @EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationResolver) -@NodeBootApplication({ - environment: "development", - appName: "facts-service", - platformName: "tech-insights", - defaultErrorHandler: false, - port: 3000, -}) +@NodeBootApplication() export class FactsServiceApp { static start() { NodeBoot.run(KoaServer) diff --git a/servers/fastify-server/package.json b/servers/fastify-server/package.json index 1fb76e4d..fb73998e 100644 --- a/servers/fastify-server/package.json +++ b/servers/fastify-server/package.json @@ -35,6 +35,7 @@ "@fastify/session": "^10.4.0", "@fastify/multipart": "^7.7.3", "@fastify/view": "^8.0.0", + "@fastify/cors": "^8.4.1", "template-url": "^1.0.0", "handlebars": "^4.7.8" }, diff --git a/servers/fastify-server/src/FastifyServer.ts b/servers/fastify-server/src/FastifyServer.ts index 1dba8f69..e4e7d10a 100644 --- a/servers/fastify-server/src/FastifyServer.ts +++ b/servers/fastify-server/src/FastifyServer.ts @@ -42,7 +42,7 @@ export class FastifyServer extends BaseServer< handlebars: require("handlebars"), }, }, - fileOptions: {}, + multipartOptions: {}, }, this.framework, ); diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index 9637bb7e..46a2ebc7 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -34,6 +34,7 @@ import {FastifySessionOptions} from "@fastify/session"; import {FastifyMultipartOptions} from "@fastify/multipart"; import {FastifyViewOptions} from "@fastify/view"; import templateUrl from "template-url"; +import {FastifyCorsOptions} from "@fastify/cors"; const actionToHttpMethodMap = { delete: "DELETE", @@ -46,8 +47,9 @@ const actionToHttpMethodMap = { export type ServerOptions = { cookieOptions?: FastifyCookieOptions; + corsOptions?: FastifyCorsOptions; sessionOptions?: FastifySessionOptions; - fileOptions: FastifyMultipartOptions; + multipartOptions?: FastifyMultipartOptions; templateOptions?: FastifyViewOptions; }; @@ -66,23 +68,42 @@ export class FastifyDriver extends BaseDriver { } initialize() { - const fastifyCookie = this.loadCookie(); - this.useApp().register(fastifyCookie, this.serverOptions.cookieOptions); + if (this.serverOptions.cookieOptions) { + const fastifyCookie = this.loadCookie(); + this.useApp().register( + fastifyCookie, + this.serverOptions.cookieOptions, + ); + } - const fastifySession = this.loadSession(); - this.useApp().register( - fastifySession, - this.serverOptions.sessionOptions, - ); + if (this.serverOptions.corsOptions) { + const fastifyCors = this.loadCors(); + this.useApp().register(fastifyCors, this.serverOptions.corsOptions); + } - const fastifyMultipart = this.loadMultipart(); - this.useApp().register( - fastifyMultipart, - this.serverOptions.fileOptions, - ); + if (this.serverOptions.sessionOptions) { + const fastifySession = this.loadSession(); + this.useApp().register( + fastifySession, + this.serverOptions.sessionOptions, + ); + } - const fastifyView = this.loadView(); - this.useApp().register(fastifyView, this.serverOptions.templateOptions); + if (this.serverOptions.multipartOptions) { + const fastifyMultipart = this.loadMultipart(); + this.useApp().register( + fastifyMultipart, + this.serverOptions.multipartOptions, + ); + } + + if (this.serverOptions.templateOptions) { + const fastifyView = this.loadView(); + this.useApp().register( + fastifyView, + this.serverOptions.templateOptions, + ); + } } /** @@ -668,6 +689,19 @@ export class FastifyDriver extends BaseDriver { } } + /** + * Dynamically loads @fastify/cors module. + */ + protected loadCors() { + try { + return require("@fastify/cors"); + } catch (e) { + throw new Error( + "@fastify/cors package was not found installed. Try to install it: npm install @fastify/cors --save", + ); + } + } + /** * Dynamically loads @fastify/view module. */ diff --git a/starters/persistence/README.md b/starters/persistence/README.md index 70e257a9..edc363cb 100644 --- a/starters/persistence/README.md +++ b/starters/persistence/README.md @@ -1,6 +1,6 @@ -# Typescript example #2 +# Node-Boot Persistence -The second typescript example for the Monorepo example +Based on TypeORM: https://orkhan.gitbook.io/typeorm/docs ## License diff --git a/starters/persistence/package.json b/starters/persistence/package.json index 9c1b9c51..5d0b3576 100644 --- a/starters/persistence/package.json +++ b/starters/persistence/package.json @@ -32,6 +32,8 @@ "@node-boot/config": "1.0.0", "@node-boot/context": "1.0.0", "@node-boot/core": "1.0.0", + "@node-boot/extension": "1.0.0", + "@node-boot/di": "1.0.0", "typeorm-transactional": "^0.5.0", "winston": "^3.10.0" }, diff --git a/starters/persistence/src/PersistenceContext.ts b/starters/persistence/src/PersistenceContext.ts new file mode 100644 index 00000000..671d93f5 --- /dev/null +++ b/starters/persistence/src/PersistenceContext.ts @@ -0,0 +1,22 @@ +import {RepositoryMetadata} from "./metadata/RepositoryMetadata"; +import {NamingStrategyInterface} from "typeorm/naming-strategy/NamingStrategyInterface"; +import {QueryResultCache} from "typeorm/cache/QueryResultCache"; +import {EntitySubscriberInterface, MigrationInterface} from "typeorm"; + +export class PersistenceContext { + private static context: PersistenceContext; + + repositories: RepositoryMetadata[] = []; + migrations: (new (...args: any[]) => MigrationInterface)[] = []; + eventSubscribers: (new (...args: any[]) => EntitySubscriberInterface)[] = + []; + namingStrategy?: new (...args: any[]) => NamingStrategyInterface; + queryCache?: new (...args: any[]) => QueryResultCache; + + static get(): PersistenceContext { + if (!PersistenceContext.context) { + PersistenceContext.context = new PersistenceContext(); + } + return PersistenceContext.context; + } +} diff --git a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts index 91402407..af32e9f6 100644 --- a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts +++ b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts @@ -1,21 +1,16 @@ -import { - ApplicationContext, - IocContainer, - RepositoriesAdapter, -} from "@node-boot/context"; +import {IocContainer, RepositoriesAdapter} from "@node-boot/context"; import {EntityManager} from "typeorm"; import {Logger} from "winston"; +import {PersistenceContext} from "../PersistenceContext"; export class DefaultRepositoriesAdapter implements RepositoriesAdapter { bind(iocContainer: IocContainer): void { const entityManager = iocContainer.get(EntityManager); const logger = iocContainer.get(Logger); - for (const repository of ApplicationContext.get().repositories) { + for (const repository of PersistenceContext.get().repositories) { const {target, entity, type} = repository; - const entityMetadata = entityManager.connection.getMetadata(entity); - const entityRepositoryInstance = new (target as any)( entity, entityManager, diff --git a/starters/persistence/src/adapter/PersistenceLogger.ts b/starters/persistence/src/adapter/PersistenceLogger.ts new file mode 100644 index 00000000..ea1cf5ae --- /dev/null +++ b/starters/persistence/src/adapter/PersistenceLogger.ts @@ -0,0 +1,59 @@ +import {AbstractLogger, LogLevel, LogMessage} from "typeorm"; +import {QueryRunner} from "typeorm/query-runner/QueryRunner"; +import {Logger} from "winston"; + +export class PersistenceLogger extends AbstractLogger { + constructor(private readonly logger: Logger) { + super(); + } + + /** + * Write log to specific output. + */ + protected writeLog( + level: LogLevel, + logMessage: LogMessage | LogMessage[], + queryRunner?: QueryRunner, + ) { + const messages = this.prepareLogMessages(logMessage, { + highlightSql: false, + }); + + for (const message of messages) { + switch (message.type ?? level) { + case "log": + case "schema-build": + case "migration": + this.logger.debug(message.message); + break; + + case "info": + case "query": + if (message.prefix) { + this.logger.info(message.prefix, message.message); + } else { + this.logger.info(message.message); + } + break; + + case "warn": + case "query-slow": + if (message.prefix) { + this.logger.warn(message.prefix, message.message); + } else { + this.logger.warn(message.message); + } + break; + + case "error": + case "query-error": + if (message.prefix) { + this.logger.error(message.prefix, message.message); + } else { + this.logger.error(message.message); + } + break; + } + } + } +} diff --git a/starters/persistence/src/config/DataSourceConfiguration.ts b/starters/persistence/src/config/DataSourceConfiguration.ts index d420ff5a..e0ab2b2d 100644 --- a/starters/persistence/src/config/DataSourceConfiguration.ts +++ b/starters/persistence/src/config/DataSourceConfiguration.ts @@ -1,15 +1,69 @@ import {Bean, Configuration} from "@node-boot/core"; import {BeansContext} from "@node-boot/context"; import {DataSourceOptions} from "typeorm/data-source/DataSourceOptions"; +import {PersistenceContext} from "../PersistenceContext"; +import {PersistenceProperties} from "../property/PersistenceProperties"; +import {PersistenceLogger} from "../adapter/PersistenceLogger"; +import {PERSISTENCE_CONFIG_PATH} from "../types"; +import {QUERY_CACHE_CONFIG} from "./QueryCacheConfiguration"; @Configuration() export class DataSourceConfiguration { @Bean("datasource-config") - public dataSourceConfig({config}: BeansContext): DataSourceOptions { + public dataSourceConfig({ + config, + iocContainer, + logger, + }: BeansContext): DataSourceOptions { + const persistenceProperties = config.get( + PERSISTENCE_CONFIG_PATH, + ); + if (!persistenceProperties) { + throw new Error(`'${PERSISTENCE_CONFIG_PATH}' configuration node is required when persistence is enabled. + Please add the persistence configuration depending on the data source you are using or remove + the @EnableRepositories from your application.`); + } + + const persistenceLogger = new PersistenceLogger(logger); + const persistenceContext = PersistenceContext.get(); + + const namingStrategy = persistenceContext.namingStrategy + ? new persistenceContext.namingStrategy() + : undefined; + + let cacheConfig; + if (iocContainer.has(QUERY_CACHE_CONFIG)) { + cacheConfig = iocContainer.get(QUERY_CACHE_CONFIG); + } else { + logger.warn( + "No query cache configuration found while building datasource configuration", + ); + } + + const databaseConfigs = + persistenceProperties[persistenceProperties.type]; + if (!databaseConfigs) { + throw new Error( + `Invalid persistence configuration. No database specific configuration found for ${persistenceProperties.type} database under ${PERSISTENCE_CONFIG_PATH}' configuration node.`, + ); + } + // Set the type from configurations to the driver/connection type + databaseConfigs.type = persistenceProperties.type; + + logger.info( + `${persistenceContext.eventSubscribers.length} subscribers found and ready to be registered`, + ); + logger.info( + `${persistenceContext.migrations.length} migrations found and ready to be registered`, + ); + return { - type: "better-sqlite3", - database: "express-sample.db", - synchronize: true, // FIXME create tables automatically through migrations instead of synchronize + ...(databaseConfigs as DataSourceOptions), + namingStrategy, + subscribers: persistenceContext.eventSubscribers, + migrations: persistenceContext.migrations, + logger: persistenceLogger, + cache: cacheConfig, }; } } diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index e3ec527b..624af305 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -2,25 +2,39 @@ import {Bean, Configuration} from "@node-boot/core"; import {DataSource, EntityManager} from "typeorm"; import {ApplicationContext, BeansContext} from "@node-boot/context"; import {DataSourceOptions} from "typeorm/data-source/DataSourceOptions"; +import {PersistenceContext} from "../PersistenceContext"; +import {PERSISTENCE_CONFIG_PATH} from "../types"; +import {REQUIRES_FIELD_INJECTION_KEY} from "@node-boot/di"; /** - * The PersistenceConfiguration class is a configuration class that is responsible for configuring and providing the - * necessary persistence components, such as the data source and entity manager. + * The PersistenceConfiguration class is responsible for configuring the persistence layer of the application. + * It defines two beans: dataSource and entityManager, which are used to manage the database connection and perform database operations. + * + * Main functionalities: + * * Configuring the DataSource bean for the persistence layer. + * * Initializing the DataSource and running migrations if enabled. + * * Binding data repositories to the DI container. + * + * @author manusant (ney.br.santos@gmail.com) * */ @Configuration() export class PersistenceConfiguration { @Bean() - public dataSource({iocContainer, logger}: BeansContext): DataSource { + public dataSource({ + iocContainer, + logger, + config, + }: BeansContext): DataSource { logger.info("Configuring persistence DataSource"); - const config = iocContainer.get( + const datasourceConfig = iocContainer.get( "datasource-config", ) as DataSourceOptions; - const entities = ApplicationContext.get().repositories.map( + const entities = PersistenceContext.get().repositories.map( repository => repository.entity, ); const dataSource = new DataSource({ - ...config, + ...datasourceConfig, entities, }); @@ -28,6 +42,42 @@ export class PersistenceConfiguration { .initialize() .then(() => { logger.info("Persistence DataSource successfully initialized"); + + // Run migrations if enabled + const runMigrations = config.getOptionalBoolean( + `${PERSISTENCE_CONFIG_PATH}.runMigrations`, + ); + if (runMigrations) { + logger.info("Running migrations"); + dataSource + .runMigrations() + .then(migrations => { + logger.info( + `${migrations.length} migration was successfully executed`, + ); + }) + .catch(reason => { + logger.info(`Migrations failed due to:`, reason); + }); + } + + for (const subscriber of dataSource.subscribers) { + for (const fieldToInject of Reflect.getMetadata( + REQUIRES_FIELD_INJECTION_KEY, + subscriber, + ) || []) { + // Extract type metadata for field injection. This is useful for custom injection in some modules + const propertyType = Reflect.getMetadata( + "design:type", + subscriber, + fieldToInject, + ); + subscriber[fieldToInject] = + iocContainer.get(propertyType); + } + } + + // Bind Data Repositories if DI container is configured const context = ApplicationContext.get(); if (context.diOptions) { logger.info(`Binding persistence repositories`); diff --git a/starters/persistence/src/config/QueryCacheConfiguration.ts b/starters/persistence/src/config/QueryCacheConfiguration.ts new file mode 100644 index 00000000..af3b71b8 --- /dev/null +++ b/starters/persistence/src/config/QueryCacheConfiguration.ts @@ -0,0 +1,87 @@ +import {Bean, Configuration} from "@node-boot/core"; +import {BeansContext} from "@node-boot/context"; +import {DataSource} from "typeorm"; +import {QueryResultCache} from "typeorm/cache/QueryResultCache"; +import {QueryCacheProperties} from "../property/QueryCacheProperties"; +import {PersistenceContext} from "../PersistenceContext"; +import {PERSISTENCE_CONFIG_PATH} from "../types"; + +export const QUERY_CACHE_CONFIG = "query-cache-config"; + +export type QueryCacheConfig = + | boolean + | (QueryCacheProperties & { + /** + * Factory function for custom cache providers that implement QueryResultCache. + */ + provider?: (connection: DataSource) => QueryResultCache; + }); + +@Configuration() +export class QueryCacheConfiguration { + @Bean() + public queryCacheConfig({iocContainer, logger, config}: BeansContext) { + logger.info("Preparing cache configurations"); + + const persistenceProperties = config.getOptionalConfig( + PERSISTENCE_CONFIG_PATH, + ); + + if (persistenceProperties) { + // Cache config can be a boolean or a complex config object + const cacheConfig = + persistenceProperties.getOptional( + "cache", + ); + const cacheEnabled = + persistenceProperties.getOptionalBoolean("cache"); + + if (cacheConfig || cacheEnabled !== undefined) { + let cacheProvider: any; + // Setup cache provider if a custom provider is configured through @PersistenceCache decorator + const QueryCache = PersistenceContext.get().queryCache; + if (QueryCache) { + cacheProvider = (connection: DataSource) => + new QueryCache(connection); + } + + if (cacheConfig) { + logger.info( + `Configuring query cache with options from configuration${ + cacheProvider ? " and custom cache provider" : "" + }`, + ); + iocContainer.set(QUERY_CACHE_CONFIG, { + ...cacheConfig, + provider: cacheProvider, + }); + } else if (cacheProvider) { + logger.info( + `Configuring query cache with custom cache provider`, + ); + iocContainer.set(QUERY_CACHE_CONFIG, { + provider: cacheProvider, + }); + } else if (cacheEnabled) { + // If cache is only enabled, falling back to database cache or to a custom provider if specified + logger.info( + `${ + cacheProvider + ? "Configuring custom query cache provider" + : "Enabling database query cache with default configurations" + }`, + ); + iocContainer.set( + QUERY_CACHE_CONFIG, + cacheProvider ? {provider: cacheProvider} : true, + ); + } else { + // Cache is explicitly disabled + logger.warn( + "Persistence query cache is not enabled. Enable it to boost your application performance.", + ); + } + } + } + } +} diff --git a/starters/persistence/src/decorator/DataRepository.ts b/starters/persistence/src/decorator/DataRepository.ts index b875b0ff..430bdc82 100644 --- a/starters/persistence/src/decorator/DataRepository.ts +++ b/starters/persistence/src/decorator/DataRepository.ts @@ -1,5 +1,5 @@ -import "reflect-metadata"; -import {ApplicationContext, RepositoryType} from "@node-boot/context"; +import {PersistenceContext} from "../PersistenceContext"; +import {RepositoryType} from "../types"; function getRepositoryType(prototype: any): RepositoryType | undefined { while (prototype) { @@ -28,7 +28,7 @@ export const DataRepository = (entity: Function): ClassDecorator => { Reflect.defineMetadata("custom:repotype", repoType, target.prototype); - ApplicationContext.get().repositories.push({ + PersistenceContext.get().repositories.push({ target, entity, type: repoType, diff --git a/starters/persistence/src/decorator/EnableRepositories.ts b/starters/persistence/src/decorator/EnableRepositories.ts index 0e4c40ef..ee16d789 100644 --- a/starters/persistence/src/decorator/EnableRepositories.ts +++ b/starters/persistence/src/decorator/EnableRepositories.ts @@ -2,6 +2,7 @@ import {ApplicationContext} from "@node-boot/context"; import {DefaultRepositoriesAdapter} from "../adapter"; import {DataSourceConfiguration, PersistenceConfiguration} from "../config"; import {TransactionConfiguration} from "../config/TransactionConfiguration"; +import {QueryCacheConfiguration} from "../config/QueryCacheConfiguration"; export const EnableRepositories = (): ClassDecorator => { return (target: Function) => { @@ -9,6 +10,9 @@ export const EnableRepositories = (): ClassDecorator => { ApplicationContext.get().repositoriesAdapter = new DefaultRepositoriesAdapter(); + // Resolve query cache configurations + new QueryCacheConfiguration(); + // Resolve data source configurations from configuration properties new DataSourceConfiguration(); diff --git a/starters/persistence/src/decorator/EntityEventSubscriber.ts b/starters/persistence/src/decorator/EntityEventSubscriber.ts new file mode 100644 index 00000000..01fa8115 --- /dev/null +++ b/starters/persistence/src/decorator/EntityEventSubscriber.ts @@ -0,0 +1,11 @@ +import {EntitySubscriberInterface, EventSubscriber} from "typeorm"; +import {PersistenceContext} from "../PersistenceContext"; + +export function EntityEventSubscriber< + T extends new (...args: any[]) => EntitySubscriberInterface, +>() { + return (target: T) => { + EventSubscriber()(target); + PersistenceContext.get().eventSubscribers.push(target); + }; +} diff --git a/starters/persistence/src/decorator/Migration.ts b/starters/persistence/src/decorator/Migration.ts new file mode 100644 index 00000000..238ea1f1 --- /dev/null +++ b/starters/persistence/src/decorator/Migration.ts @@ -0,0 +1,10 @@ +import {PersistenceContext} from "../PersistenceContext"; +import {MigrationInterface} from "typeorm"; + +export function Migration< + T extends new (...args: any[]) => MigrationInterface, +>() { + return (target: T) => { + PersistenceContext.get().migrations.push(target); + }; +} diff --git a/starters/persistence/src/decorator/PersistenceCache.ts b/starters/persistence/src/decorator/PersistenceCache.ts new file mode 100644 index 00000000..9b13dc81 --- /dev/null +++ b/starters/persistence/src/decorator/PersistenceCache.ts @@ -0,0 +1,13 @@ +import {PersistenceContext} from "../PersistenceContext"; +import {QueryResultCache} from "typeorm/cache/QueryResultCache"; +import {decorateDi} from "@node-boot/di"; + +export function PersistenceCache< + T extends new (...args: any[]) => QueryResultCache, +>() { + return (target: T) => { + // Inject dependencies if DI container is configured + decorateDi(target); + PersistenceContext.get().queryCache = target; + }; +} diff --git a/starters/persistence/src/decorator/PersistenceNamingStrategy.ts b/starters/persistence/src/decorator/PersistenceNamingStrategy.ts new file mode 100644 index 00000000..a66b50aa --- /dev/null +++ b/starters/persistence/src/decorator/PersistenceNamingStrategy.ts @@ -0,0 +1,10 @@ +import {NamingStrategyInterface} from "typeorm/naming-strategy/NamingStrategyInterface"; +import {PersistenceContext} from "../PersistenceContext"; + +export function PersistenceNamingStrategy< + T extends new (...args: any[]) => NamingStrategyInterface, +>() { + return (target: T) => { + PersistenceContext.get().namingStrategy = target; + }; +} diff --git a/starters/persistence/src/decorator/index.ts b/starters/persistence/src/decorator/index.ts index d1586943..225f463f 100644 --- a/starters/persistence/src/decorator/index.ts +++ b/starters/persistence/src/decorator/index.ts @@ -1,3 +1,7 @@ export {DataRepository} from "./DataRepository"; export {EnableRepositories} from "./EnableRepositories"; export {Transactional} from "./Transactional"; +export {PersistenceNamingStrategy} from "./PersistenceNamingStrategy"; +export {PersistenceCache} from "./PersistenceCache"; +export {Migration} from "./Migration"; +export {EntityEventSubscriber} from "./EntityEventSubscriber"; diff --git a/starters/persistence/src/hooks/hooks.ts b/starters/persistence/src/hook/hooks.ts similarity index 100% rename from starters/persistence/src/hooks/hooks.ts rename to starters/persistence/src/hook/hooks.ts diff --git a/starters/persistence/src/hooks/index.ts b/starters/persistence/src/hook/index.ts similarity index 100% rename from starters/persistence/src/hooks/index.ts rename to starters/persistence/src/hook/index.ts diff --git a/starters/persistence/src/index.ts b/starters/persistence/src/index.ts index b69b5813..f7adedf0 100644 --- a/starters/persistence/src/index.ts +++ b/starters/persistence/src/index.ts @@ -1,2 +1,3 @@ export * from "./decorator"; -export * from "./hooks"; +export * from "./hook"; +export * from "./types"; diff --git a/packages/context/src/metadata/RepositoryMetadata.ts b/starters/persistence/src/metadata/RepositoryMetadata.ts similarity index 82% rename from packages/context/src/metadata/RepositoryMetadata.ts rename to starters/persistence/src/metadata/RepositoryMetadata.ts index 9c04dacd..4b40b434 100644 --- a/packages/context/src/metadata/RepositoryMetadata.ts +++ b/starters/persistence/src/metadata/RepositoryMetadata.ts @@ -1,8 +1,4 @@ -export enum RepositoryType { - SQL = "sql", - MONGO = "mongo", - TREE = "tree", -} +import {RepositoryType} from "../types"; /** * Arguments for EntityRepositoryMetadata class, helps to construct an EntityRepositoryMetadata object. diff --git a/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts new file mode 100644 index 00000000..1f459555 --- /dev/null +++ b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts @@ -0,0 +1,29 @@ +import {AuroraMysqlConnectionCredentialsOptions} from "typeorm/driver/aurora-mysql/AuroraMysqlConnectionCredentialsOptions"; + +/** + * MySQL specific connection options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface AuroraMysqlConnectionProperties + extends AuroraMysqlConnectionCredentialsOptions { + readonly region: string; + + readonly secretArn: string; + + readonly resourceArn: string; + + readonly database: string; + + readonly serviceConfigOptions?: {[key: string]: any}; // pass optional AWS.ConfigurationOptions here + + readonly formatOptions?: {[key: string]: any; castParameters: boolean}; + + /** + * Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. + * (Default: true) + */ + readonly legacySpatialSupport?: boolean; + + readonly poolSize?: never; +} diff --git a/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts b/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts new file mode 100644 index 00000000..e0c6018c --- /dev/null +++ b/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts @@ -0,0 +1,39 @@ +/** + * Postgres-specific connection options. + */ +export interface AuroraPostgresConnectionProperties { + readonly region: string; + + readonly secretArn: string; + + readonly resourceArn: string; + + readonly database: string; + + /** + * The driver object + * This defaults to require("typeorm-aurora-data-api-driver") + */ + readonly driver?: any; + + /** + * The Postgres extension to use to generate UUID columns. Defaults to uuid-ossp. + * If pgcrypto is selected, TypeORM will use the gen_random_uuid() function from this extension. + * If uuid-ossp is selected, TypeORM will use the uuid_generate_v4() function from this extension. + */ + readonly uuidExtension?: "pgcrypto" | "uuid-ossp"; + + readonly transformParameters?: boolean; + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; + + readonly serviceConfigOptions?: {[key: string]: any}; + + readonly formatOptions?: {[key: string]: any; castParameters: boolean}; + + readonly poolSize?: never; +} diff --git a/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts b/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts new file mode 100644 index 00000000..cacdca07 --- /dev/null +++ b/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts @@ -0,0 +1,72 @@ +/** + * Sqlite-specific connection options. + */ +export interface BetterSqlite3ConnectionProperties { + /** + * Storage type or path to the storage. + */ + readonly database: string; + + /** + * The driver object + * This defaults to require("better-sqlite3") + */ + readonly driver?: any; + + /** + * Encryption key for for SQLCipher. + */ + readonly key?: string; + + /** + * Cache size of sqlite statement to speed up queries. + * Default: 100. + */ + readonly statementCacheSize?: number; + + /** + * Function to run before a database is used in typeorm. + * You can set pragmas, register plugins or register + * functions or aggregates in this function. + */ + readonly prepareDatabase?: (db: any) => void | Promise; + + /** + * Open the database connection in readonly mode. + * Default: false. + */ + readonly readonly?: boolean; + + /** + * If the database does not exist, an Error will be thrown instead of creating a new file. + * This option does not affect in-memory or readonly database connections. + * Default: false. + */ + readonly fileMustExist?: boolean; + + /** + * The number of milliseconds to wait when executing queries + * on a locked database, before throwing a SQLITE_BUSY error. + * Default: 5000. + */ + readonly timeout?: number; + + /** + * Provide a function that gets called with every SQL string executed by the database connection. + */ + readonly verbose?: Function; + + /** + * Relative or absolute path to the native addon (better_sqlite3.node). + */ + readonly nativeBinding?: string; + + readonly poolSize?: never; + + /** + * Enables WAL mode. By default its disabled. + * + * @see https://www.sqlite.org/wal.html + */ + readonly enableWAL?: boolean; +} diff --git a/starters/persistence/src/property/CockroachConnectionProperties.ts b/starters/persistence/src/property/CockroachConnectionProperties.ts new file mode 100644 index 00000000..65f932b3 --- /dev/null +++ b/starters/persistence/src/property/CockroachConnectionProperties.ts @@ -0,0 +1,62 @@ +import {CockroachConnectionCredentialsOptions} from "typeorm/driver/cockroachdb/CockroachConnectionCredentialsOptions"; + +/** + * Cockroachdb-specific connection options. + */ +export interface CockroachConnectionProperties + extends CockroachConnectionCredentialsOptions { + /** + * Enable time travel queries on cockroachdb. + * https://www.cockroachlabs.com/docs/stable/as-of-system-time.html + */ + readonly timeTravelQueries: boolean; + + /** + * Schema name. + */ + readonly schema?: string; + + /** + * The driver object + * This defaults to `require("pg")`. + */ + readonly driver?: any; + + /** + * The driver object + * This defaults to `require("pg-native")`. + */ + readonly nativeDriver?: any; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: CockroachConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: CockroachConnectionCredentialsOptions[]; + }; + + /** + * sets the application_name var to help db administrators identify + * the service using this connection. Defaults to 'undefined' + */ + readonly applicationName?: string; + + /** + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; + + /** + * Max number of transaction retries in case of 40001 error. + */ + readonly maxTransactionRetries?: number; +} diff --git a/starters/persistence/src/property/CommonDataSourceProperties.ts b/starters/persistence/src/property/CommonDataSourceProperties.ts new file mode 100644 index 00000000..22b6b8f1 --- /dev/null +++ b/starters/persistence/src/property/CommonDataSourceProperties.ts @@ -0,0 +1,115 @@ +import {DatabaseType, LoggerOptions} from "typeorm"; +import {QueryCacheProperties} from "./QueryCacheProperties"; + +/** + * CommonDataSourceProperties is set of DataSource properties shared by all database types. + */ +export interface CommonDataSourceProperties { + /** + * Database type. This value is required. + */ + readonly type: DatabaseType; + + /** + * Migrations table name, in case of different name from "migrations". + * Accepts single string name. + */ + readonly migrationsTableName?: string; + + /** + * Transaction mode for migrations to run in + */ + readonly migrationsTransactionMode?: "all" | "none" | "each"; + + /** + * Typeorm metadata table name, in case of different name from "typeorm_metadata". + * Accepts single string name. + */ + readonly metadataTableName?: string; + + /** + * Logging options. + */ + readonly logging?: LoggerOptions; + + /** + * Maximum number of milliseconds query should be executed before logger log a warning. + */ + readonly maxQueryExecutionTime?: number; + + /** + * Maximum number of clients the pool should contain. + */ + readonly poolSize?: number; + + /** + * Indicates if database schema should be auto created on every application launch. + * Be careful with this option and don't use this in production - otherwise you can lose production data. + * This option is useful during debug and development. + * Alternative to it, you can use CLI and run schema:sync command. + * + * Note that for MongoDB database it does not create schema, because MongoDB is schemaless. + * Instead, it syncs just by creating indices. + */ + readonly synchronize?: boolean; + + /** + * Indicates if migrations should be auto run on every application launch. + * Alternative to it, you can use CLI and run migrations:run command. + */ + readonly migrationsRun?: boolean; + + /** + * Drops the schema each time connection is being established. + * Be careful with this option and don't use this in production - otherwise you'll lose all production data. + * This option is useful during debug and development. + */ + readonly dropSchema?: boolean; + + /** + * Prefix to use on all tables (collections) of this connection in the database. + */ + readonly entityPrefix?: string; + + /** + * When creating new Entity instances, skip all constructors when true. + */ + readonly entitySkipConstructor?: boolean; + + /** + * Extra connection options to be passed to the underlying driver. + * + * todo: deprecate this and move all database-specific types into hts own connection options object. + */ + readonly extra?: any; + + /** + * Specifies how relations must be loaded - using "joins" or separate queries. + * If you are loading too much data with nested joins it's better to load relations + * using separate queries. + * + * Default strategy is "join", but this default can be changed here. + * Also, strategy can be set per-query in FindOptions and QueryBuilder. + */ + readonly relationLoadStrategy?: "join" | "query"; + + /** + * Optionally applied "typename" to the model. + * If set, then each hydrated model will have this property with the target model / entity name inside. + * + * (works like a discriminator property). + */ + readonly typename?: string; + + /** + * Holds reference to the baseDirectory where configuration file are expected. + * + * @internal + */ + baseDirectory?: string; + + /** + * Allows to setup cache options. + */ + readonly cache?: boolean | QueryCacheProperties; +} diff --git a/starters/persistence/src/property/MongoConnectionProperties.ts b/starters/persistence/src/property/MongoConnectionProperties.ts new file mode 100644 index 00000000..8e420210 --- /dev/null +++ b/starters/persistence/src/property/MongoConnectionProperties.ts @@ -0,0 +1,344 @@ +import {ReadPreference} from "typeorm"; + +/** + * MongoDB specific connection options. + * Synced with http://mongodb.github.io/node-mongodb-native/3.1/api/MongoClient.html + */ +export interface MongoConnectionProperties { + /** + * Connection url where perform connection to. + */ + readonly url?: string; + + /** + * Database host. + */ + readonly host?: string; + + /** + * Database host replica set. + */ + readonly hostReplicaSet?: string; + + /** + * Database host port. + */ + readonly port?: number; + + /** + * Database username. + */ + readonly username?: string; + + /** + * Database password. + */ + readonly password?: string; + + /** + * Database name to connect to. + */ + readonly database?: string; + + /** + * Specifies whether to force dispatch all operations to the specified host. Default: false + */ + readonly directConnection?: boolean; + + /** + * The driver object + * This defaults to require("mongodb") + */ + readonly driver?: any; + + /** + * Use ssl connection (needs to have a mongod server with ssl support). Default: false + */ + readonly ssl?: boolean; + + /** + * Validate mongod server certificate against ca (needs to have a mongod server with ssl support, 2.4 or higher). + * Default: true + */ + readonly sslValidate?: boolean; + + /** + * Array of valid certificates either as Buffers or Strings + * (needs to have a mongod server with ssl support, 2.4 or higher). + */ + readonly sslCA?: string | Buffer; + + /** + * String or buffer containing the certificate we wish to present + * (needs to have a mongod server with ssl support, 2.4 or higher) + */ + readonly sslCert?: string | Buffer; + + /** + * String or buffer containing the certificate private key we wish to present + * (needs to have a mongod server with ssl support, 2.4 or higher) + */ + readonly sslKey?: string; + + /** + * String or buffer containing the certificate password + * (needs to have a mongod server with ssl support, 2.4 or higher) + */ + readonly sslPass?: string | Buffer; + + /** + * SSL Certificate revocation list binary buffer + * (needs to have a mongod server with ssl support, 2.4 or higher) + */ + readonly sslCRL?: string | Buffer; + + /** + * Reconnect on error. Default: true + */ + readonly autoReconnect?: boolean; + + /** + * TCP Socket NoDelay option. Default: true + */ + readonly noDelay?: boolean; + + /** + * The number of milliseconds to wait before initiating keepAlive on the TCP socket. Default: 30000 + */ + readonly keepAlive?: number; + + /** + * TCP Connection timeout setting. Default: 30000 + */ + readonly connectTimeoutMS?: number; + + /** + * Version of IP stack. Can be 4, 6. + * If undefined, will attempt to connect with IPv6, and will fall back to IPv4 on failure + */ + readonly family?: number; + + /** + * TCP Socket timeout setting. Default: 360000 + */ + readonly socketTimeoutMS?: number; + + /** + * Server attempt to reconnect #times. Default 30 + */ + readonly reconnectTries?: number; + + /** + * Server will wait #milliseconds between retries. Default 1000 + */ + readonly reconnectInterval?: number; + + /** + * Control if high availability monitoring runs for Replicaset or Mongos proxies. Default true + */ + readonly ha?: boolean; + + /** + * The High availability period for replicaset inquiry. Default: 10000 + */ + readonly haInterval?: number; + + /** + * The name of the replicaset to connect to + */ + readonly replicaSet?: string; + + /** + * Sets the range of servers to pick when using NEAREST (lowest ping ms + the latency fence, ex: range of 1 to (1 + 15) ms). + * Default: 15 + */ + readonly acceptableLatencyMS?: number; + + /** + * Sets the range of servers to pick when using NEAREST (lowest ping ms + the latency fence, ex: range of 1 to (1 + 15) ms). + * Default: 15 + */ + readonly secondaryAcceptableLatencyMS?: number; + + /** + * Sets if the driver should connect even if no primary is available. Default: false + */ + readonly connectWithNoPrimary?: boolean; + + /** + * If the database authentication is dependent on another databaseName. + */ + readonly authSource?: string; + + /** + * The write concern. + */ + readonly w?: string | number; + + /** + * The write concern timeout value. + */ + readonly wtimeout?: number; + + /** + * Specify a journal write concern. Default: false + */ + readonly j?: boolean; + + /** + * Force server to assign _id values instead of driver. Default: false + */ + readonly forceServerObjectId?: boolean; + + /** + * Serialize functions on any object. Default: false + */ + readonly serializeFunctions?: boolean; + + /** + * Specify if the BSON serializer should ignore undefined fields. Default: false + */ + readonly ignoreUndefined?: boolean; + + /** + * Return document results as raw BSON buffers. Default: false + */ + readonly raw?: boolean; + + /** + * Promotes Long values to number if they fit inside the 53 bits resolution. Default: true + */ + readonly promoteLongs?: boolean; + + /** + * Promotes Binary BSON values to native Node Buffers. Default: false + */ + readonly promoteBuffers?: boolean; + + /** + * Promotes BSON values to native types where possible, set to false to only receive wrapper types. Default: true + */ + readonly promoteValues?: boolean; + + /** + * Enable the wrapping of the callback in the current domain, disabled by default to avoid perf hit. Default: false + */ + readonly domainsEnabled?: boolean; + + /** + * Sets a cap on how many operations the driver will buffer up before giving up on getting a working connection, + * default is -1 which is unlimited. + */ + readonly bufferMaxEntries?: number; + + /** + * The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, + * ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). + */ + readonly readPreference?: ReadPreference | string; + + /** + * A primary key factory object for generation of custom _id keys. + */ + readonly pkFactory?: any; + + /** + * A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible. + */ + readonly promiseLibrary?: any; + + /** + * Specify a read concern for the collection. (only MongoDB 3.2 or higher supported). + */ + readonly readConcern?: any; + + /** + * Specify a maxStalenessSeconds value for secondary reads, minimum is 90 seconds + */ + readonly maxStalenessSeconds?: number; + + /** + * Specify the log level used by the driver logger (error/warn/info/debug). + */ + readonly loggerLevel?: "error" | "warn" | "info" | "debug"; + + // Do not overwrite BaseDataSourceOptions.logger + // readonly logger?: any; + + /** + * Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function + * Default: true + */ + readonly checkServerIdentity?: boolean | Function; + + /** + * Validate MongoClient passed in options for correctness. Default: false + */ + readonly validateOptions?: boolean | any; + + /** + * The name of the application that created this MongoClient instance. MongoDB 3.4 and newer will print this value in the server log upon establishing each connection. It is also recorded in the slow query log and profile collections + */ + readonly appname?: string; + + /** + * Sets the authentication mechanism that MongoDB will use to authenticate the connection + */ + readonly authMechanism?: string; + + /** + * Type of compression to use: snappy or zlib + */ + readonly compression?: any; + + /** + * Specify a file sync write concern. Default: false + */ + readonly fsync?: boolean; + + /** + * Read preference tags + */ + readonly readPreferenceTags?: any[]; + + /** + * The number of retries for a tailable cursor. Default: 5 + */ + readonly numberOfRetries?: number; + + /** + * Enable auto reconnecting for single server instances. Default: true + */ + readonly auto_reconnect?: boolean; + + /** + * Enable command monitoring for this client. Default: false + */ + readonly monitorCommands?: boolean; + + /** + * If present, the connection pool will be initialized with minSize connections, and will never dip below minSize connections + */ + readonly minSize?: number; + + /** + * Determines whether or not to use the new url parser. Default: false + */ + readonly useNewUrlParser?: boolean; + + /** + * Determines whether or not to use the new Server Discovery and Monitoring engine. Default: false + * https://github.com/mongodb/node-mongodb-native/releases/tag/v3.2.1 + */ + readonly useUnifiedTopology?: boolean; + + /** + * Automatic Client-Side Field Level Encryption configuration. + */ + readonly autoEncryption?: any; + + /** + * Enables or disables the ability to retry writes upon encountering transient network errors. + */ + readonly retryWrites?: boolean; +} diff --git a/starters/persistence/src/property/MysqlConnectionProperties.ts b/starters/persistence/src/property/MysqlConnectionProperties.ts new file mode 100644 index 00000000..208a2131 --- /dev/null +++ b/starters/persistence/src/property/MysqlConnectionProperties.ts @@ -0,0 +1,143 @@ +import {MysqlConnectionCredentialsOptions} from "typeorm/driver/mysql/MysqlConnectionCredentialsOptions"; + +/** + * MySQL specific connection options. + * + * @see https://github.com/mysqljs/mysql#connection-options + */ +export interface MysqlConnectionProperties + extends MysqlConnectionCredentialsOptions { + /** + * The driver object + * This defaults to require("mysql"). + * Falls back to require("mysql2") + */ + readonly driver?: any; + + /** + * The charset for the connection. This is called "collation" in the SQL-level of MySQL (like utf8_general_ci). + * If a SQL-level charset is specified (like utf8mb4) then the default collation for that charset is used. + * Default: 'UTF8_GENERAL_CI' + */ + readonly charset?: string; + + /** + * The timezone configured on the MySQL server. + * This is used to type cast server date/time values to JavaScript Date object and vice versa. + * This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM. (Default: 'local') + */ + readonly timezone?: string; + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + */ + readonly connectTimeout?: number; + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + * This difference between connectTimeout and acquireTimeout is subtle and is described in the mysqljs/mysql docs + * https://github.com/mysqljs/mysql/tree/master#pool-options + */ + readonly acquireTimeout?: number; + + /** + * Allow connecting to MySQL instances that ask for the old (insecure) authentication method. (Default: false) + */ + readonly insecureAuth?: boolean; + + /** + * When dealing with big numbers (BIGINT and DECIMAL columns) in the database, you should enable this option (Default: false) + */ + readonly supportBigNumbers?: boolean; + + /** + * Enabling both supportBigNumbers and bigNumberStrings forces big numbers (BIGINT and DECIMAL columns) to be always + * returned as JavaScript String objects (Default: false). Enabling supportBigNumbers but leaving bigNumberStrings + * disabled will return big numbers as String objects only when they cannot be accurately represented with + * [JavaScript Number objects](http://ecma262-5.com/ELS5_HTML.htm#Section_8.5) (which happens when they exceed the [-2^53, +2^53] range), + * otherwise they will be returned as Number objects. This option is ignored if supportBigNumbers is disabled. + */ + readonly bigNumberStrings?: boolean; + + /** + * Force date types (TIMESTAMP, DATETIME, DATE) to be returned as strings rather then inflated into JavaScript Date objects. + * Can be true/false or an array of type names to keep as strings. + */ + readonly dateStrings?: boolean | string[]; + + /** + * Prints protocol details to stdout. Can be true/false or an array of packet type names that should be printed. + * (Default: false) + */ + readonly debug?: boolean | string[]; + + /** + * Generates stack traces on Error to include call site of library entrance ("long stack traces"). + * Slight performance penalty for most calls. (Default: true) + */ + readonly trace?: boolean; + + /** + * Allow multiple mysql statements per query. Be careful with this, it could increase the scope of SQL injection attacks. + * (Default: false) + */ + readonly multipleStatements?: boolean; + + /** + * Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. + * (Default: true) + */ + readonly legacySpatialSupport?: boolean; + + /** + * List of connection flags to use other than the default ones. It is also possible to blacklist default ones. + * For more information, check https://github.com/mysqljs/mysql#connection-flags. + */ + readonly flags?: string[]; + + /** + * TypeORM will automatically use package found in your node_modules, prioritizing mysql over mysql2, + * but you can specify it manually + */ + readonly connectorPackage?: "mysql" | "mysql2"; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: MysqlConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: MysqlConnectionCredentialsOptions[]; + + /** + * If true, PoolCluster will attempt to reconnect when connection fails. (Default: true) + */ + readonly canRetry?: boolean; + + /** + * If connection fails, node's errorCount increases. + * When errorCount is greater than removeNodeErrorCount, remove a node in the PoolCluster. (Default: 5) + */ + readonly removeNodeErrorCount?: number; + + /** + * If connection fails, specifies the number of milliseconds before another connection attempt will be made. + * If set to 0, then node will be removed instead and never re-used. (Default: 0) + */ + readonly restoreNodeTimeout?: number; + + /** + * Determines how slaves are selected: + * RR: Select one alternately (Round-Robin). + * RANDOM: Select the node by random function. + * ORDER: Select the first node available unconditionally. + */ + readonly selector?: "RR" | "RANDOM" | "ORDER"; + }; +} diff --git a/starters/persistence/src/property/OracleConnectionProperties.ts b/starters/persistence/src/property/OracleConnectionProperties.ts new file mode 100644 index 00000000..3097b798 --- /dev/null +++ b/starters/persistence/src/property/OracleConnectionProperties.ts @@ -0,0 +1,38 @@ +import {OracleConnectionCredentialsOptions} from "typeorm/driver/oracle/OracleConnectionCredentialsOptions"; + +/** + * Oracle-specific connection options. + */ +export interface OracleConnectionProperties + extends OracleConnectionCredentialsOptions { + /** + * Schema name. By default is "public". + */ + readonly schema?: string; + + /** + * The driver object + * This defaults to require("oracledb") + */ + readonly driver?: any; + + /** + * A boolean determining whether to pass time values in UTC or local time. (default: false). + */ + readonly useUTC?: boolean; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: OracleConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: OracleConnectionCredentialsOptions[]; + }; +} diff --git a/starters/persistence/src/property/PersistenceProperties.ts b/starters/persistence/src/property/PersistenceProperties.ts new file mode 100644 index 00000000..320c5670 --- /dev/null +++ b/starters/persistence/src/property/PersistenceProperties.ts @@ -0,0 +1,29 @@ +import {CommonDataSourceProperties} from "./CommonDataSourceProperties"; +import {MysqlConnectionProperties} from "./MysqlConnectionProperties"; +import {PostgresConnectionProperties} from "./PostgresConnectionProperties"; +import {AuroraPostgresConnectionProperties} from "./AuroraPostgresConnectionProperties"; +import {AuroraMysqlConnectionProperties} from "./AuroraMysqlConnectionProperties"; +import {BetterSqlite3ConnectionProperties} from "./BetterSqlite3ConnectionProperties"; +import {CockroachConnectionProperties} from "./CockroachConnectionProperties"; +import {MongoConnectionProperties} from "./MongoConnectionProperties"; +import {OracleConnectionProperties} from "./OracleConnectionProperties"; +import {SapConnectionProperties} from "./SapConnectionProperties"; +import {SpannerConnectionProperties} from "./SpannerConnectionProperties"; +import {SqliteConnectionProperties} from "./SqliteConnectionProperties"; +import {SqlServerConnectionProperties} from "./SqlServerConnectionProperties"; + +export type PersistenceProperties = CommonDataSourceProperties & { + "aurora-mysql": AuroraMysqlConnectionProperties; + "aurora-postgres": AuroraPostgresConnectionProperties; + "better-sqlite3": BetterSqlite3ConnectionProperties; + cockroachdb: CockroachConnectionProperties; + mongodb: MongoConnectionProperties; + mysql: MysqlConnectionProperties; + mariadb: MysqlConnectionProperties; + oracle: OracleConnectionProperties; + postgres: PostgresConnectionProperties; + sap: SapConnectionProperties; + spanner: SpannerConnectionProperties; + sqlite: SqliteConnectionProperties; + mssql: SqlServerConnectionProperties; +}; diff --git a/starters/persistence/src/property/PostgresConnectionProperties.ts b/starters/persistence/src/property/PostgresConnectionProperties.ts new file mode 100644 index 00000000..0eaabd00 --- /dev/null +++ b/starters/persistence/src/property/PostgresConnectionProperties.ts @@ -0,0 +1,96 @@ +import {PostgresConnectionCredentialsOptions} from "typeorm/driver/postgres/PostgresConnectionCredentialsOptions"; + +/** + * Postgres-specific connection options. + */ +export interface PostgresConnectionProperties + extends PostgresConnectionCredentialsOptions { + /** + * Schema name. + */ + readonly schema?: string; + + /** + * The driver object + * This defaults to `require("pg")`. + */ + readonly driver?: any; + + /** + * The driver object + * This defaults to `require("pg-native")`. + */ + readonly nativeDriver?: any; + + /** + * A boolean determining whether to pass time values in UTC or local time. (default: false). + */ + readonly useUTC?: boolean; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: PostgresConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: PostgresConnectionCredentialsOptions[]; + }; + + /** + * The milliseconds before a timeout occurs during the initial connection to the postgres + * server. If undefined, or set to 0, there is no timeout. Defaults to undefined. + */ + readonly connectTimeoutMS?: number; + + /** + * The Postgres extension to use to generate UUID columns. Defaults to uuid-ossp. + * If pgcrypto is selected, TypeORM will use the gen_random_uuid() function from this extension. + * If uuid-ossp is selected, TypeORM will use the uuid_generate_v4() function from this extension. + */ + readonly uuidExtension?: "pgcrypto" | "uuid-ossp"; + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; + + /** + * Include notification messages from Postgres server in client logs + */ + readonly logNotifications?: boolean; + + /** + * Automatically install postgres extensions + */ + readonly installExtensions?: boolean; + + /** + * sets the application_name var to help db administrators identify + * the service using this connection. Defaults to 'undefined' + */ + readonly applicationName?: string; + + /** + * Return 64-bit integers (int8) as JavaScript integers. + * + * Because JavaScript doesn't have support for 64-bit integers node-postgres cannot confidently + * parse int8 data type results as numbers because if you have a huge number it will overflow + * and the result you'd get back from node-postgres would not be the result in the database. + * That would be a very bad thing so node-postgres just returns int8 results as strings and leaves the parsing up to you. + * + * Enabling parseInt8 will cause node-postgres to parse int8 results as numbers. + * Note: the maximum safe integer in js is: Number.MAX_SAFE_INTEGER (`+2^53`) + * + * @see [JavaScript Number objects](http://ecma262-5.com/ELS5_HTML.htm#Section_8.5) + * @see [node-postgres int8 explanation](https://github.com/brianc/node-pg-types#:~:text=on%20projects%3A%20return-,64%2Dbit%20integers,-(int8)%20as) + * @see [node-postgres defaults.parseInt8 implementation](https://github.com/brianc/node-postgres/blob/pg%408.8.0/packages/pg/lib/defaults.js#L80) + */ + readonly parseInt8?: boolean; +} diff --git a/starters/persistence/src/property/QueryCacheProperties.ts b/starters/persistence/src/property/QueryCacheProperties.ts new file mode 100644 index 00000000..e45e3076 --- /dev/null +++ b/starters/persistence/src/property/QueryCacheProperties.ts @@ -0,0 +1,37 @@ +export type QueryCacheProperties = { + /** + * Type of caching. + * + * - "database" means cached values will be stored in the separate table in database. This is default value. + * - "redis" means cached values will be stored inside redis. You must provide redis connection options. + */ + type?: "database" | "redis" | "ioredis" | "ioredis/cluster"; // todo: add mongodb and other cache providers as well in the future + + /** + * Configurable table name for "database" type cache. + * Default value is "query-result-cache" + */ + tableName?: string; + + /** + * Used to provide redis connection options. + */ + options?: any; + + /** + * If set to true then queries (using find methods and QueryBuilder's methods) will always be cached. + */ + alwaysEnabled?: boolean; + + /** + * Time in milliseconds in which cache will expire. + * This can be setup per-query. + * Default value is 1000 which is equivalent to 1 second. + */ + duration?: number; + + /** + * Used to specify if cache errors should be ignored, and pass through the call to the Database. + */ + ignoreErrors?: boolean; +}; diff --git a/starters/persistence/src/property/SapConnectionProperties.ts b/starters/persistence/src/property/SapConnectionProperties.ts new file mode 100644 index 00000000..cc92453a --- /dev/null +++ b/starters/persistence/src/property/SapConnectionProperties.ts @@ -0,0 +1,64 @@ +import {SapConnectionCredentialsOptions} from "typeorm/driver/sap/SapConnectionCredentialsOptions"; + +/** + * SAP Hana specific connection options. + */ +export interface SapConnectionProperties + extends SapConnectionCredentialsOptions { + /** + * Database schema. + */ + readonly schema?: string; + + /** + * The driver objects + * This defaults to require("hdb-pool") + */ + readonly driver?: any; + + /** + * The driver objects + * This defaults to require("@sap/hana-client") + */ + readonly hanaClientDriver?: any; + + /** + * Pool options. + */ + readonly pool?: { + /** + * Max number of connections. + */ + readonly max?: number; + + /** + * Minimum number of connections. + */ + readonly min?: number; + + /** + * Maximum number of waiting requests allowed. (default=0, no limit). + */ + readonly maxWaitingRequests?: number; + /** + * Max milliseconds a request will wait for a resource before timing out. (default=5000) + */ + readonly requestTimeout?: number; + /** + * How often to run resource timeout checks. (default=0, disabled) + */ + readonly checkInterval?: number; + /** + * Idle timeout + */ + readonly idleTimeout?: number; + + /** + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly poolErrorHandler?: (err: any) => any; + }; + + readonly poolSize?: never; +} diff --git a/starters/persistence/src/property/SpannerConnectionProperties.ts b/starters/persistence/src/property/SpannerConnectionProperties.ts new file mode 100644 index 00000000..a311fa60 --- /dev/null +++ b/starters/persistence/src/property/SpannerConnectionProperties.ts @@ -0,0 +1,147 @@ +import {SpannerConnectionCredentialsOptions} from "typeorm/driver/spanner/SpannerConnectionCredentialsOptions"; + +/** + * Spanner specific connection options. + */ +export interface SpannerConnectionProperties + extends SpannerConnectionCredentialsOptions { + /** + * Database type. + */ + readonly type: "spanner"; + + /** + * The driver object + * This defaults to require("@google-cloud/spanner"). + */ + readonly driver?: any; + + // todo + readonly database?: string; + + // todo + readonly schema?: string; + + /** + * The charset for the connection. This is called "collation" in the SQL-level of MySQL (like utf8_general_ci). + * If a SQL-level charset is specified (like utf8mb4) then the default collation for that charset is used. + * Default: 'UTF8_GENERAL_CI' + */ + readonly charset?: string; + + /** + * The timezone configured on the MySQL server. + * This is used to type cast server date/time values to JavaScript Date object and vice versa. + * This can be 'local', 'Z', or an offset in the form +HH:MM or -HH:MM. (Default: 'local') + */ + readonly timezone?: string; + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + */ + readonly connectTimeout?: number; + + /** + * The milliseconds before a timeout occurs during the initial connection to the MySQL server. (Default: 10000) + * This difference between connectTimeout and acquireTimeout is subtle and is described in the mysqljs/mysql docs + * https://github.com/mysqljs/mysql/tree/master#pool-options + */ + readonly acquireTimeout?: number; + + /** + * Allow connecting to MySQL instances that ask for the old (insecure) authentication method. (Default: false) + */ + readonly insecureAuth?: boolean; + + /** + * When dealing with big numbers (BIGINT and DECIMAL columns) in the database, you should enable this option (Default: false) + */ + readonly supportBigNumbers?: boolean; + + /** + * Enabling both supportBigNumbers and bigNumberStrings forces big numbers (BIGINT and DECIMAL columns) to be always + * returned as JavaScript String objects (Default: false). Enabling supportBigNumbers but leaving bigNumberStrings + * disabled will return big numbers as String objects only when they cannot be accurately represented with + * [JavaScript Number objects](http://ecma262-5.com/ELS5_HTML.htm#Section_8.5) (which happens when they exceed the [-2^53, +2^53] range), + * otherwise they will be returned as Number objects. This option is ignored if supportBigNumbers is disabled. + */ + readonly bigNumberStrings?: boolean; + + /** + * Force date types (TIMESTAMP, DATETIME, DATE) to be returned as strings rather then inflated into JavaScript Date objects. + * Can be true/false or an array of type names to keep as strings. + */ + readonly dateStrings?: boolean | string[]; + + /** + * Prints protocol details to stdout. Can be true/false or an array of packet type names that should be printed. + * (Default: false) + */ + readonly debug?: boolean | string[]; + + /** + * Generates stack traces on Error to include call site of library entrance ("long stack traces"). + * Slight performance penalty for most calls. (Default: true) + */ + readonly trace?: boolean; + + /** + * Allow multiple mysql statements per query. Be careful with this, it could increase the scope of SQL injection attacks. + * (Default: false) + */ + readonly multipleStatements?: boolean; + + /** + * Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. + * (Default: true) + */ + readonly legacySpatialSupport?: boolean; + + /** + * List of connection flags to use other than the default ones. It is also possible to blacklist default ones. + * For more information, check https://github.com/mysqljs/mysql#connection-flags. + */ + readonly flags?: string[]; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: SpannerConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: SpannerConnectionCredentialsOptions[]; + + /** + * If true, PoolCluster will attempt to reconnect when connection fails. (Default: true) + */ + readonly canRetry?: boolean; + + /** + * If connection fails, node's errorCount increases. + * When errorCount is greater than removeNodeErrorCount, remove a node in the PoolCluster. (Default: 5) + */ + readonly removeNodeErrorCount?: number; + + /** + * If connection fails, specifies the number of milliseconds before another connection attempt will be made. + * If set to 0, then node will be removed instead and never re-used. (Default: 0) + */ + readonly restoreNodeTimeout?: number; + + /** + * Determines how slaves are selected: + * RR: Select one alternately (Round-Robin). + * RANDOM: Select the node by random function. + * ORDER: Select the first node available unconditionally. + */ + readonly selector?: "RR" | "RANDOM" | "ORDER"; + }; + + readonly poolSize?: never; +} diff --git a/starters/persistence/src/property/SqlServerConnectionProperties.ts b/starters/persistence/src/property/SqlServerConnectionProperties.ts new file mode 100644 index 00000000..7626dffd --- /dev/null +++ b/starters/persistence/src/property/SqlServerConnectionProperties.ts @@ -0,0 +1,314 @@ +import {SqlServerConnectionCredentialsOptions} from "typeorm/driver/sqlserver/SqlServerConnectionCredentialsOptions"; + +/** + * Microsoft Sql Server specific connection options. + */ +export interface SqlServerConnectionProperties + extends SqlServerConnectionCredentialsOptions { + /** + * Database type. + */ + readonly type: "mssql"; + + /** + * Connection timeout in ms (default: 15000). + */ + readonly connectionTimeout?: number; + + /** + * Request timeout in ms (default: 15000). NOTE: msnodesqlv8 driver doesn't support timeouts < 1 second. + */ + readonly requestTimeout?: number; + + /** + * Stream recordsets/rows instead of returning them all at once as an argument of callback (default: false). + * You can also enable streaming for each request independently (request.stream = true). + * Always set to true if you plan to work with large amount of rows. + */ + readonly stream?: boolean; + + /** + * Database schema. + */ + readonly schema?: string; + + /** + * The driver object + * This defaults to `require("mssql")` + */ + readonly driver?: any; + + /** + * An optional object/dictionary with the any of the properties + */ + readonly pool?: { + /** + * Maximum number of resources to create at any given time. (default=1) + */ + readonly max?: number; + + /** + * Minimum number of resources to keep in pool at any given time. If this is set >= max, the pool will silently + * set the min to equal max. (default=0) + */ + readonly min?: number; + + /** + * Maximum number of queued requests allowed, additional acquire calls will be callback with an err in a future + * cycle of the event loop. + */ + readonly maxWaitingClients?: number; + + /** + * Should the pool validate resources before giving them to clients. Requires that either factory.validate or + * factory.validateAsync to be specified + */ + readonly testOnBorrow?: boolean; + + /** + * Max milliseconds an acquire call will wait for a resource before timing out. (default no limit), if supplied should non-zero positive integer. + */ + readonly acquireTimeoutMillis?: number; + + /** + * If true the oldest resources will be first to be allocated. If false the most recently released resources will + * be the first to be allocated. This in effect turns the pool's behaviour from a queue into a stack. boolean, + * (default true) + */ + readonly fifo?: boolean; + + /** + * Int between 1 and x - if set, borrowers can specify their relative priority in the queue if no resources + * are available. see example. (default 1) + */ + readonly priorityRange?: number; + + /** + * How often to run eviction checks. Default: 0 (does not run). + */ + readonly evictionRunIntervalMillis?: number; + + /** + * Number of resources to check each eviction run. Default: 3. + */ + readonly numTestsPerRun?: number; + + /** + * Amount of time an object may sit idle in the pool before it is eligible for eviction by the idle object + * evictor (if any), with the extra condition that at least "min idle" object instances remain in the pool. + * Default -1 (nothing can get evicted) + */ + readonly softIdleTimeoutMillis?: number; + + /** + * The minimum amount of time that an object may sit idle in the pool before it is eligible for eviction due + * to idle time. Supercedes softIdleTimeoutMillis Default: 30000 + */ + readonly idleTimeoutMillis?: number; + + /* + * Function handling errors thrown by drivers pool. + * Defaults to logging error with `warn` level. + */ + readonly errorHandler?: (err: any) => any; + }; + + /** + * Extra options + */ + readonly options?: { + /** + * The named instance to connect to + */ + readonly instanceName?: string; + + /** + * By default, if the database requestion by options.database cannot be accessed, the connection will fail with + * an error. However, if options.fallbackToDefaultDb is set to true, then the user's default database will + * be used instead (Default: false). + */ + readonly fallbackToDefaultDb?: boolean; + + /** + * If true, SET ANSI_NULL_DFLT_ON ON will be set in the initial sql. This means new columns will be nullable by + * default. See the T-SQL documentation for more details. (Default: true). + */ + readonly enableAnsiNullDefault?: boolean; + + /** + * The number of milliseconds before the attempt to connect is considered failed (default: 15000). + */ + readonly connectTimeout?: number; + + /** + * The number of milliseconds before the cancel (abort) of a request is considered failed (default: 5000). + */ + readonly cancelTimeout?: number; + + /** + * The size of TDS packets (subject to negotiation with the server). Should be a power of 2. (default: 4096). + */ + readonly packetSize?: number; + + /** + * A boolean determining whether to pass time values in UTC or local time. (default: false). + */ + readonly useUTC?: boolean; + + /** + * A boolean determining whether to rollback a transaction automatically if any error is encountered during + * the given transaction's execution. This sets the value for SET XACT_ABORT during the initial SQL phase + * of a connection (documentation). + */ + readonly abortTransactionOnError?: boolean; + + /** + * A string indicating which network interface (ip address) to use when connecting to SQL Server. + */ + readonly localAddress?: string; + + /** + * A boolean determining whether to return rows as arrays or key-value collections. (default: false). + */ + readonly useColumnNames?: boolean; + + /** + * A boolean, controlling whether the column names returned will have the first letter converted to lower case + * (true) or not. This value is ignored if you provide a columnNameReplacer. (default: false). + */ + readonly camelCaseColumns?: boolean; + + /** + * A boolean, controlling whatever to disable RETURNING / OUTPUT statements. + */ + readonly disableOutputReturning?: boolean; + + /** + * A boolean, controlling whether MssqlParameter types char, varchar, and text are converted to their unicode equivalents, nchar, nvarchar, and ntext. + * (default: false, meaning that char/varchar/text parameters will be converted to nchar/nvarchar/ntext) + */ + readonly disableAsciiToUnicodeParamConversion?: boolean; + + /** + * Debug options + */ + readonly debug?: { + /** + * A boolean, controlling whether debug events will be emitted with text describing packet details + * (default: false). + */ + readonly packet?: boolean; + + /** + * A boolean, controlling whether debug events will be emitted with text describing packet data details + * (default: false). + */ + readonly data?: boolean; + + /** + * A boolean, controlling whether debug events will be emitted with text describing packet payload details + * (default: false). + */ + readonly payload?: boolean; + + /** + * A boolean, controlling whether debug events will be emitted with text describing token stream tokens + * (default: false). + */ + readonly token?: boolean; + }; + + /** + * The default isolation level that transactions will be run with. The isolation levels are available + * from require('tedious').ISOLATION_LEVEL. (default: READ_COMMITTED). + */ + readonly isolation?: + | "READ_UNCOMMITTED" + | "READ_COMMITTED" + | "REPEATABLE_READ" + | "SERIALIZABLE" + | "SNAPSHOT"; + + /** + * The default isolation level for new connections. All out-of-transaction queries are executed with this + * setting. The isolation levels are available from require('tedious').ISOLATION_LEVEL . + */ + readonly connectionIsolationLevel?: + | "READ_UNCOMMITTED" + | "READ_COMMITTED" + | "REPEATABLE_READ" + | "SERIALIZABLE" + | "SNAPSHOT"; + + /** + * A boolean, determining whether the connection will request read only access from a SQL Server + * Availability Group. For more information, see here. (default: false). + */ + readonly readOnlyIntent?: boolean; + + /** + * A boolean determining whether or not the connection will be encrypted. Set to true if you're on + * Windows Azure. (default: true). + */ + readonly encrypt?: boolean; + + /** + * When encryption is used, an object may be supplied that will be used for the first argument when calling + * tls.createSecurePair (default: {}). + */ + readonly cryptoCredentialsDetails?: any; + + /** + * A boolean, that when true will expose received rows in Requests' done* events. See done, doneInProc and + * doneProc. (default: false) + * Caution: If many row are received, enabling this option could result in excessive memory usage. + */ + readonly rowCollectionOnDone?: boolean; + + /** + * A boolean, that when true will expose received rows in Requests' completion callback. See new Request. (default: false) + * Caution: If many row are received, enabling this option could result in excessive memory usage. + */ + readonly rowCollectionOnRequestCompletion?: boolean; + + /** + * The version of TDS to use. If server doesn't support specified version, negotiated version is used instead. + * The versions are available from require('tedious').TDS_VERSION. (default: 7_4). + */ + readonly tdsVersion?: string; + + /** + * A boolean, that when true will abort a query when an overflow or divide-by-zero error occurs during query execution. + */ + readonly enableArithAbort?: boolean; + + /** + * Application name used for identifying a specific application in profiling, logging or tracing tools of SQL Server. + * (default: node-mssql) + */ + readonly appName?: string; + + /** + * A boolean, controlling whether encryption occurs if there is no verifiable server certificate. + * (default: false) + */ + readonly trustServerCertificate?: boolean; + }; + + /** + * Replication setup. + */ + readonly replication?: { + /** + * Master server used by orm to perform writes. + */ + readonly master: SqlServerConnectionCredentialsOptions; + + /** + * List of read-from severs (slaves). + */ + readonly slaves: SqlServerConnectionCredentialsOptions[]; + }; + + readonly poolSize?: never; +} diff --git a/starters/persistence/src/property/SqliteConnectionProperties.ts b/starters/persistence/src/property/SqliteConnectionProperties.ts new file mode 100644 index 00000000..e61d7fff --- /dev/null +++ b/starters/persistence/src/property/SqliteConnectionProperties.ts @@ -0,0 +1,62 @@ +/** + * Sqlite-specific connection options. + */ +export interface SqliteConnectionProperties { + /** + * Database type. + */ + readonly type: "sqlite"; + + /** + * Storage type or path to the storage. + */ + readonly database: string; + + /** + * The driver object + * This defaults to require("sqlite3") + */ + readonly driver?: any; + + /** + * Encryption key for for SQLCipher. + */ + readonly key?: string; + + /** + * In your SQLite application when you perform parallel writes its common to face SQLITE_BUSY error. + * This error indicates that SQLite failed to write to the database file since someone else already writes into it. + * Since SQLite cannot handle parallel saves this error cannot be avoided. + * + * To simplify life's of those who have this error this particular option sets a timeout within which ORM will try + * to perform requested write operation again and again until it receives SQLITE_BUSY error. + * + * Enabling WAL can improve your app performance and face less SQLITE_BUSY issues. + * Time in milliseconds. + */ + readonly busyErrorRetry?: number; + + /** + * Enables WAL mode. By default its disabled. + * + * @see https://www.sqlite.org/wal.html + */ + readonly enableWAL?: boolean; + + /** + * Specifies the open file flags. By default its undefined. + * @see https://www.sqlite.org/c3ref/c_open_autoproxy.html + * @see https://github.com/TryGhost/node-sqlite3/blob/master/test/open_close.test.js + */ + readonly flags?: number; + + readonly poolSize?: never; + + /** + * Query or change the setting of the busy timeout. + * Time in milliseconds. + * + * @see https://www.sqlite.org/pragma.html#pragma_busy_timeout + */ + readonly busyTimeout?: number; +} diff --git a/starters/persistence/src/types.ts b/starters/persistence/src/types.ts new file mode 100644 index 00000000..32967c74 --- /dev/null +++ b/starters/persistence/src/types.ts @@ -0,0 +1,7 @@ +export const PERSISTENCE_CONFIG_PATH = "node-boot.persistence"; + +export enum RepositoryType { + SQL = "sql", + MONGO = "mongo", + TREE = "tree", +} From 2a79e95c25b226aa0e8f9cf6e4213011cf020906 Mon Sep 17 00:00:00 2001 From: manusant Date: Tue, 5 Dec 2023 19:06:33 +0000 Subject: [PATCH 02/16] comment --- starters/persistence/src/config/PersistenceConfiguration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index 624af305..1d880cfa 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -61,6 +61,7 @@ export class PersistenceConfiguration { }); } + // Inject dependencies into Subscriber instances for (const subscriber of dataSource.subscribers) { for (const fieldToInject of Reflect.getMetadata( REQUIRES_FIELD_INJECTION_KEY, From fbfc32700e04eaf5aa8a7fb9e822be24eac37349 Mon Sep 17 00:00:00 2001 From: manusant Date: Tue, 5 Dec 2023 19:15:22 +0000 Subject: [PATCH 03/16] add some formatting to avoid excessive line brakes --- .prettierrc.yaml | 1 + .../authorization/src/decorator/Authorized.ts | 5 +- .../src/decorator/EnableAuthorization.ts | 4 +- .../src/resolver/AuthorizationResolver.ts | 5 +- .../src/decorator/ConfigurationProperties.ts | 26 +-- packages/config/src/service/ConfigService.ts | 5 +- .../src/service/ObservableConfigProxy.test.ts | 12 +- packages/config/src/service/config.ts | 16 +- packages/context/src/ApplicationContext.ts | 6 +- .../src/adapters/BeansConfigurationAdapter.ts | 29 +-- packages/core/src/decorators/Bean.ts | 7 +- packages/core/src/decorators/Configuration.ts | 4 +- .../core/src/decorators/Configurations.ts | 4 +- packages/core/src/decorators/Controller.ts | 17 +- .../src/decorators/EnableAutoConfiguration.ts | 38 +--- .../src/decorators/EnableClassTransformer.ts | 14 +- .../src/decorators/EnableComponentScan.ts | 16 +- .../src/decorators/NodeBootApplication.ts | 29 +-- .../AuthorizationCheckerNotDefinedError.ts | 5 +- packages/core/src/logger/winston.logger.ts | 16 +- .../FastifyErrorHandlerInterface.ts | 6 +- .../middlewares/FastifyMiddlewareInterface.ts | 6 +- packages/core/src/server/BaseServer.ts | 37 +--- packages/core/src/server/NodeBoot.ts | 6 +- packages/di/src/decorators/Inject.ts | 12 +- packages/di/src/ioc/makeDiDecoration.ts | 14 +- .../di/src/ioc/makeInjectionDecoration.ts | 8 +- packages/di/src/ioc/types.ts | 7 +- packages/extension/src/Optional.ts | 20 +- .../openapi/src/adapter/ExpressOpenApi.ts | 10 +- .../openapi/src/adapter/FastifyOpenApi.ts | 6 +- .../openapi/src/decorator/EnableOpenApi.ts | 14 +- .../src/auth/DefaultAuthorizationResolver.ts | 3 +- .../src/config/MultipleConfigurations.ts | 6 +- .../src/controllers/users.controller.ts | 16 +- samples/sample-express/src/dtos/users.dto.ts | 8 +- .../src/middlewares/validation.middleware.ts | 4 +- .../sample-express/src/models/users.model.ts | 12 +- .../src/persistence/CustomNamingStrategy.ts | 5 +- .../listeners/GlobalEntityEventListener.ts | 30 +-- .../listeners/UserEntityEventListener.ts | 4 +- .../migrations/1701786331338-migration.ts | 4 +- .../src/services/greeting.service.ts | 4 +- .../src/services/users.service.ts | 24 +-- .../src/auth/DefaultAuthorizationResolver.ts | 3 +- .../src/controllers/users.controller.ts | 15 +- samples/sample-fastify/src/dtos/users.dto.ts | 8 +- .../src/middlewares/LoggingMiddleware.ts | 7 +- .../src/middlewares/customErrorHandler.ts | 13 +- .../sample-fastify/src/models/users.model.ts | 12 +- .../src/services/users.service.ts | 20 +- .../src/auth/DefaultAuthorizationResolver.ts | 3 +- .../src/controllers/users.controller.ts | 16 +- samples/sample-koa/src/dtos/users.dto.ts | 8 +- .../src/middlewares/validation.middleware.ts | 4 +- samples/sample-koa/src/models/users.model.ts | 12 +- .../sample-koa/src/services/users.service.ts | 20 +- servers/express-server/src/ExpressServer.ts | 17 +- servers/fastify-server/src/FastifyServer.ts | 13 +- .../src/driver/FastifyDriver.ts | 191 +++++------------- servers/fastify-server/src/utils.ts | 4 +- servers/koa-server/src/KoaServer.ts | 12 +- .../src/adapter/DefaultActuatorAdapter.ts | 9 +- .../src/adapter/ExpressActuatorAdapter.ts | 21 +- .../src/adapter/FastifyActuatorAdapter.ts | 15 +- .../src/adapter/KoaActuatorAdapter.ts | 3 +- .../actuator/src/service/MetadataService.ts | 9 +- .../persistence/src/PersistenceContext.ts | 3 +- .../src/adapter/DefaultRepositoriesAdapter.ts | 11 +- .../src/config/DataSourceConfiguration.ts | 13 +- .../src/config/PersistenceConfiguration.ts | 30 +-- .../src/config/QueryCacheConfiguration.ts | 19 +- .../src/decorator/EnableRepositories.ts | 3 +- .../persistence/src/decorator/Migration.ts | 4 +- .../src/decorator/PersistenceCache.ts | 4 +- .../src/decorator/Transactional.ts | 9 +- starters/persistence/src/hook/hooks.ts | 8 +- .../AuroraMysqlConnectionProperties.ts | 3 +- .../property/CockroachConnectionProperties.ts | 3 +- .../src/property/MysqlConnectionProperties.ts | 3 +- .../property/OracleConnectionProperties.ts | 3 +- .../property/PostgresConnectionProperties.ts | 3 +- .../src/property/SapConnectionProperties.ts | 3 +- .../property/SpannerConnectionProperties.ts | 3 +- .../property/SqlServerConnectionProperties.ts | 3 +- 85 files changed, 250 insertions(+), 838 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index d3a260ef..413519dc 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,3 +4,4 @@ singleQuote: false trailingComma: "all" arrowParens: "avoid" bracketSpacing: false +printWidth: 100 diff --git a/packages/authorization/src/decorator/Authorized.ts b/packages/authorization/src/decorator/Authorized.ts index 4f8ffdfb..cc5a13da 100644 --- a/packages/authorization/src/decorator/Authorized.ts +++ b/packages/authorization/src/decorator/Authorized.ts @@ -31,10 +31,7 @@ export function Authorized(role: Function): Function; * @param roleOrRoles Arguments for routing-controllers @Authorized decorator */ export function Authorized(roleOrRoles?: string | string[] | Function) { - return ( - clsOrObject: Function | Object, - method?: string, - ) => { + return (clsOrObject: Function | Object, method?: string) => { // DI is optional and the decorator will only be applied if the DI container dependency is available. InnerAuthorized(roleOrRoles)(clsOrObject, method); }; diff --git a/packages/authorization/src/decorator/EnableAuthorization.ts b/packages/authorization/src/decorator/EnableAuthorization.ts index 5897912b..3d0f9228 100644 --- a/packages/authorization/src/decorator/EnableAuthorization.ts +++ b/packages/authorization/src/decorator/EnableAuthorization.ts @@ -25,9 +25,7 @@ export function EnableAuthorization( if (CurrentUserResolverClass) { const userResolver = new CurrentUserResolverClass(); - ApplicationContext.get().currentUserChecker = async ( - action: Action, - ) => { + ApplicationContext.get().currentUserChecker = async (action: Action) => { return userResolver.getCurrentUser(action); }; } diff --git a/packages/authorization/src/resolver/AuthorizationResolver.ts b/packages/authorization/src/resolver/AuthorizationResolver.ts index f364d6de..c1c1eb26 100644 --- a/packages/authorization/src/resolver/AuthorizationResolver.ts +++ b/packages/authorization/src/resolver/AuthorizationResolver.ts @@ -5,8 +5,5 @@ import {RequestContext} from "@node-boot/context"; * Must return true or promise with boolean true resolved for authorization to succeed. */ export interface AuthorizationResolver { - authorize( - context: RequestContext, - roles: any[], - ): Promise | boolean; + authorize(context: RequestContext, roles: any[]): Promise | boolean; } diff --git a/packages/config/src/decorator/ConfigurationProperties.ts b/packages/config/src/decorator/ConfigurationProperties.ts index b7f21a73..83f33700 100644 --- a/packages/config/src/decorator/ConfigurationProperties.ts +++ b/packages/config/src/decorator/ConfigurationProperties.ts @@ -1,14 +1,8 @@ -import { - ApplicationContext, - ConfigurationPropertiesAdapter, - IocContainer, -} from "@node-boot/context"; +import {ApplicationContext, ConfigurationPropertiesAdapter, IocContainer} from "@node-boot/context"; import {ConfigurationPropertiesMetadata} from "../metadata"; import {ConfigService} from "../service"; -export function ConfigurationProperties( - args: ConfigurationPropertiesMetadata, -): Function { +export function ConfigurationProperties(args: ConfigurationPropertiesMetadata): Function { return function (target: any) { Reflect.defineMetadata("config:isConfigProperties", true, target); Reflect.defineMetadata("config:path", args.configPath, target); @@ -17,22 +11,16 @@ export function ConfigurationProperties( new (class implements ConfigurationPropertiesAdapter { bind(iocContainer: IocContainer) { const config: ConfigService = iocContainer.get("config"); - const configProperties = config.get( - args.configPath, - ); + const configProperties = config.get(args.configPath); if (configProperties) { const instance = new target(); for (const propertyName in configProperties) { if ( - Object.prototype.hasOwnProperty.call( - configProperties, - propertyName, - ) + Object.prototype.hasOwnProperty.call(configProperties, propertyName) ) { - instance[propertyName] = - configProperties[propertyName]; + instance[propertyName] = configProperties[propertyName]; } } if (!iocContainer.has(args.configName)) { @@ -43,9 +31,7 @@ export function ConfigurationProperties( ); } } else { - throw new Error( - `Configuration for prefix '${args.configPath}' not found.`, - ); + throw new Error(`Configuration for prefix '${args.configPath}' not found.`); } } })(), diff --git a/packages/config/src/service/ConfigService.ts b/packages/config/src/service/ConfigService.ts index 097b1dd9..679289d1 100644 --- a/packages/config/src/service/ConfigService.ts +++ b/packages/config/src/service/ConfigService.ts @@ -7,10 +7,7 @@ export class ConfigService implements Config { private readonly subscribers: (() => void)[] = []; - constructor( - private readonly parent?: ConfigService, - private parentKey?: string, - ) { + constructor(private readonly parent?: ConfigService, private parentKey?: string) { if (parent && !parentKey) { throw new Error("parentKey is required if parent is set"); } diff --git a/packages/config/src/service/ObservableConfigProxy.test.ts b/packages/config/src/service/ObservableConfigProxy.test.ts index f481454c..e0bb5f43 100644 --- a/packages/config/src/service/ObservableConfigProxy.test.ts +++ b/packages/config/src/service/ObservableConfigProxy.test.ts @@ -72,15 +72,9 @@ describe("ObservableConfigProxy", () => { expect(config3.getNumber("x")).toBe(6); config1.setConfig(new ConfigReader({})); - expect(() => config1.getNumber("x")).toThrow( - "Missing required config value at 'x'", - ); - expect(() => config2.getNumber("x")).toThrow( - "Missing required config value at 'a'", - ); - expect(() => config3.getNumber("x")).toThrow( - "Missing required config value at 'a'", - ); + expect(() => config1.getNumber("x")).toThrow("Missing required config value at 'x'"); + expect(() => config2.getNumber("x")).toThrow("Missing required config value at 'a'"); + expect(() => config3.getNumber("x")).toThrow("Missing required config value at 'a'"); config1.setConfig(new ConfigReader({x: "s", a: {x: "s", b: {x: "s"}}})); expect(() => config1.getNumber("x")).toThrow( diff --git a/packages/config/src/service/config.ts b/packages/config/src/service/config.ts index 37056fe0..57f4d400 100644 --- a/packages/config/src/service/config.ts +++ b/packages/config/src/service/config.ts @@ -1,11 +1,7 @@ import {resolve as resolvePath} from "path"; import parseArgs from "minimist"; import {findPaths} from "@backstage/cli-common"; -import { - ConfigTarget, - loadConfig, - LoadConfigOptionsRemote, -} from "@backstage/config-loader"; +import {ConfigTarget, loadConfig, LoadConfigOptionsRemote} from "@backstage/config-loader"; import type {AppConfig} from "@backstage/config"; import {ConfigReader} from "@backstage/config"; import {ConfigService} from "./ConfigService"; @@ -51,11 +47,7 @@ export async function loadNodeBootConfig(options: { remote: options.remote, watch: { onChange(newConfigs) { - console.info( - `Reloaded config from ${newConfigs - .map(c => c.context) - .join(", ")}`, - ); + console.info(`Reloaded config from ${newConfigs.map(c => c.context).join(", ")}`); const configsToMerge = [...newConfigs]; if (options.additionalConfigs) { configsToMerge.push(...options.additionalConfigs); @@ -71,9 +63,7 @@ export async function loadNodeBootConfig(options: { }, }); - console.info( - `Loaded config from ${appConfigs.map(c => c.context).join(", ")}`, - ); + console.info(`Loaded config from ${appConfigs.map(c => c.context).join(", ")}`); const finalAppConfigs = [...appConfigs]; if (options.additionalConfigs) { diff --git a/packages/context/src/ApplicationContext.ts b/packages/context/src/ApplicationContext.ts index 7851aec3..d4d96074 100644 --- a/packages/context/src/ApplicationContext.ts +++ b/packages/context/src/ApplicationContext.ts @@ -7,11 +7,7 @@ import type { ConfigurationAdapter, ConfigurationPropertiesAdapter, } from "./adapters"; -import { - ActuatorAdapter, - OpenApiBridgeAdapter, - RepositoriesAdapter, -} from "./adapters"; +import {ActuatorAdapter, OpenApiBridgeAdapter, RepositoriesAdapter} from "./adapters"; export class ApplicationContext { private static context: ApplicationContext; diff --git a/packages/core/src/adapters/BeansConfigurationAdapter.ts b/packages/core/src/adapters/BeansConfigurationAdapter.ts index df85897c..1f420c43 100644 --- a/packages/core/src/adapters/BeansConfigurationAdapter.ts +++ b/packages/core/src/adapters/BeansConfigurationAdapter.ts @@ -18,40 +18,23 @@ export class BeansConfigurationAdapter implements ConfigurationAdapter { ); } - async bind( - beansContext: BeansContext, - ): Promise { + async bind(beansContext: BeansContext): Promise { const {iocContainer} = beansContext; const prototype = this.target.prototype; const propertyNames = Object.getOwnPropertyNames(prototype); for (const propertyName of propertyNames) { - const descriptor = Object.getOwnPropertyDescriptor( - prototype, - propertyName, - ); - const isBean = Reflect.getMetadata( - BEAN_METADATA_KEY, - prototype, - propertyName, - ); - const beanName = Reflect.getMetadata( - BEAN_NAME_METADATA_KEY, - prototype, - propertyName, - ); + const descriptor = Object.getOwnPropertyDescriptor(prototype, propertyName); + const isBean = Reflect.getMetadata(BEAN_METADATA_KEY, prototype, propertyName); + const beanName = Reflect.getMetadata(BEAN_NAME_METADATA_KEY, prototype, propertyName); if (descriptor && descriptor.value && isBean) { let beanInstance: any; // Deal with Beans async factory functions if (descriptor.value.constructor.name === "AsyncFunction") { - beanInstance = await descriptor.value.bind(this.target)( - beansContext, - ); + beanInstance = await descriptor.value.bind(this.target)(beansContext); } else { - beanInstance = descriptor.value.bind(this.target)( - beansContext, - ); + beanInstance = descriptor.value.bind(this.target)(beansContext); } if (beanName) { diff --git a/packages/core/src/decorators/Bean.ts b/packages/core/src/decorators/Bean.ts index f5b28b21..0dd7f760 100644 --- a/packages/core/src/decorators/Bean.ts +++ b/packages/core/src/decorators/Bean.ts @@ -4,12 +4,7 @@ export function Bean(beanName?: string): Function { return function (target: any, propertyKey: string) { Reflect.defineMetadata(BEAN_METADATA_KEY, true, target, propertyKey); if (beanName) { - Reflect.defineMetadata( - BEAN_NAME_METADATA_KEY, - beanName, - target, - propertyKey, - ); + Reflect.defineMetadata(BEAN_NAME_METADATA_KEY, beanName, target, propertyKey); } }; } diff --git a/packages/core/src/decorators/Configuration.ts b/packages/core/src/decorators/Configuration.ts index 1854642e..b3212ec9 100644 --- a/packages/core/src/decorators/Configuration.ts +++ b/packages/core/src/decorators/Configuration.ts @@ -7,8 +7,6 @@ export function Configuration(): Function { return function (target: Function) { Reflect.defineMetadata(IS_CONFIGURATION_KEY, true, target); - ApplicationContext.get().configurationAdapters.push( - new BeansConfigurationAdapter(target), - ); + ApplicationContext.get().configurationAdapters.push(new BeansConfigurationAdapter(target)); }; } diff --git a/packages/core/src/decorators/Configurations.ts b/packages/core/src/decorators/Configurations.ts index 695e41fe..99d02747 100644 --- a/packages/core/src/decorators/Configurations.ts +++ b/packages/core/src/decorators/Configurations.ts @@ -1,6 +1,4 @@ -export function Configurations( - configurationClasses: (new (...args: any[]) => any)[], -): Function { +export function Configurations(configurationClasses: (new (...args: any[]) => any)[]): Function { return function (target: any) { configurationClasses.map(ClassConstructor => new ClassConstructor()); }; diff --git a/packages/core/src/decorators/Controller.ts b/packages/core/src/decorators/Controller.ts index 0492a0ec..94691ae1 100644 --- a/packages/core/src/decorators/Controller.ts +++ b/packages/core/src/decorators/Controller.ts @@ -1,10 +1,7 @@ import {Controller as InnerController} from "routing-controllers"; import {decorateDi} from "@node-boot/di"; import {ControllerOptions} from "routing-controllers/types/decorator-options/ControllerOptions"; -import { - CONTROLLER_PATH_METADATA_KEY, - CONTROLLER_VERSION_METADATA_KEY, -} from "@node-boot/context"; +import {CONTROLLER_PATH_METADATA_KEY, CONTROLLER_VERSION_METADATA_KEY} from "@node-boot/context"; /** * Defines a class as a controller. @@ -15,19 +12,11 @@ import { * @param version controller version to be used as part of the controller route * @param options Extra options that apply to all controller actions */ -export function Controller( - baseRoute?: string, - version?: string, - options?: ControllerOptions, -) { +export function Controller(baseRoute?: string, version?: string, options?: ControllerOptions) { return (target: TFunction) => { if (version !== undefined) { baseRoute = baseRoute ? `/${version}${baseRoute}` : `/${version}`; - Reflect.defineMetadata( - CONTROLLER_VERSION_METADATA_KEY, - version, - target, - ); + Reflect.defineMetadata(CONTROLLER_VERSION_METADATA_KEY, version, target); } Reflect.defineMetadata(CONTROLLER_PATH_METADATA_KEY, baseRoute, target); diff --git a/packages/core/src/decorators/EnableAutoConfiguration.ts b/packages/core/src/decorators/EnableAutoConfiguration.ts index a5faa835..78a20a71 100644 --- a/packages/core/src/decorators/EnableAutoConfiguration.ts +++ b/packages/core/src/decorators/EnableAutoConfiguration.ts @@ -12,30 +12,19 @@ function getProjectRootDirectory(): string { async function instantiateClasses(rootDir, classes) { for (const classData of classes) { - const { - class: className, - path: classPath, - arguments: classArguments, - } = classData; + const {class: className, path: classPath, arguments: classArguments} = classData; // Use dynamic import to handle the class asynchronously. const module = require(path.join(rootDir, classPath)); const Class = module[className]; - const argumentsMetadata = Reflect.getMetadata( - "design:paramtypes", - Class, - ); + const argumentsMetadata = Reflect.getMetadata("design:paramtypes", Class); // Check if the class has any constructor arguments (dependencies). if (argumentsMetadata && argumentsMetadata.length > 0) { const dependencies = argumentsMetadata.map((argType, index) => { - const argValue = classArguments - ? classArguments[index] - : undefined; - return typeof argValue !== "undefined" - ? argValue - : new argType(); + const argValue = classArguments ? classArguments[index] : undefined; + return typeof argValue !== "undefined" ? argValue : new argType(); }); const instance = new Class(...dependencies); @@ -70,24 +59,15 @@ export function EnableAutoConfiguration(): Function { await instantiateClasses(rootDir, config.Configurations); } - if ( - config.ConfigurationProperties && - Array.isArray(config.ConfigurationProperties) - ) { - await instantiateClasses( - rootDir, - config.ConfigurationProperties, - ); + if (config.ConfigurationProperties && Array.isArray(config.ConfigurationProperties)) { + await instantiateClasses(rootDir, config.ConfigurationProperties); } if (config.Controllers && Array.isArray(config.Controllers)) { await instantiateClasses(rootDir, config.Controllers); } - if ( - config.JsonControllers && - Array.isArray(config.JsonControllers) - ) { + if (config.JsonControllers && Array.isArray(config.JsonControllers)) { await instantiateClasses(rootDir, config.JsonControllers); } @@ -95,9 +75,7 @@ export function EnableAutoConfiguration(): Function { await instantiateClasses(rootDir, config.Services); } } else { - console.error( - "nodeBoot-info.json not found in the root directory.", - ); + console.error("nodeBoot-info.json not found in the root directory."); } }; } diff --git a/packages/core/src/decorators/EnableClassTransformer.ts b/packages/core/src/decorators/EnableClassTransformer.ts index d70359ab..a03f5d4c 100644 --- a/packages/core/src/decorators/EnableClassTransformer.ts +++ b/packages/core/src/decorators/EnableClassTransformer.ts @@ -1,17 +1,13 @@ import {ApplicationContext, TransformerOptions} from "@node-boot/context"; import {ClassTransformOptions} from "class-transformer"; -export function ClassToPlainTransform( - options: ClassTransformOptions, -): Function { +export function ClassToPlainTransform(options: ClassTransformOptions): Function { return function (target: Function) { ApplicationContext.get().classToPlainTransformOptions = options; }; } -export function PlainToClassTransform( - options: ClassTransformOptions, -): Function { +export function PlainToClassTransform(options: ClassTransformOptions): Function { return function (target: Function) { ApplicationContext.get().plainToClassTransformOptions = options; }; @@ -21,10 +17,8 @@ export function EnableClassTransformer(options?: TransformerOptions): Function { return function (target: Function) { ApplicationContext.get().classTransformer = options?.enabled ?? true; - ApplicationContext.get().classToPlainTransformOptions = - options?.classToPlain; + ApplicationContext.get().classToPlainTransformOptions = options?.classToPlain; - ApplicationContext.get().plainToClassTransformOptions = - options?.plainToClass; + ApplicationContext.get().plainToClassTransformOptions = options?.plainToClass; }; } diff --git a/packages/core/src/decorators/EnableComponentScan.ts b/packages/core/src/decorators/EnableComponentScan.ts index b246fbca..bd822013 100644 --- a/packages/core/src/decorators/EnableComponentScan.ts +++ b/packages/core/src/decorators/EnableComponentScan.ts @@ -9,8 +9,7 @@ export function EnableComponentScan(options?: ComponentScanOptions): Function { interceptorPaths: ["/interceptors"], }; - const srcDir = - __dirname.substring(0, __dirname.indexOf("/src")) + "/dist"; + const srcDir = __dirname.substring(0, __dirname.indexOf("/src")) + "/dist"; if (options.controllerPaths) { ApplicationContext.get().controllerClasses = getClassesFromPaths( @@ -56,9 +55,7 @@ export function importClassesFromDirectories( } else if (exported instanceof Array) { exported.forEach((i: any) => loadFileClasses(i, allLoaded)); } else if (exported instanceof Object || typeof exported === "object") { - Object.keys(exported).forEach(key => - loadFileClasses(exported[key], allLoaded), - ); + Object.keys(exported).forEach(key => loadFileClasses(exported[key], allLoaded)); } return allLoaded; @@ -66,18 +63,13 @@ export function importClassesFromDirectories( const allFiles = directories.reduce((allDirs, dir) => { // Replace \ with / for glob - return allDirs.concat( - require("glob").sync(path.normalize(dir).replace(/\\/g, "/")), - ); + return allDirs.concat(require("glob").sync(path.normalize(dir).replace(/\\/g, "/"))); }, [] as string[]); const dirs = allFiles .filter(file => { const dtsExtension = file.substring(file.length - 5, file.length); - return ( - formats.indexOf(path.extname(file)) !== -1 && - dtsExtension !== ".d.ts" - ); + return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== ".d.ts"; }) .map(file => { return require(file); diff --git a/packages/core/src/decorators/NodeBootApplication.ts b/packages/core/src/decorators/NodeBootApplication.ts index dc76c92c..acd06223 100644 --- a/packages/core/src/decorators/NodeBootApplication.ts +++ b/packages/core/src/decorators/NodeBootApplication.ts @@ -1,8 +1,4 @@ -import { - ApplicationAdapter, - ApplicationContext, - ApplicationOptions, -} from "@node-boot/context"; +import {ApplicationAdapter, ApplicationContext, ApplicationOptions} from "@node-boot/context"; import {RoutingControllersOptions} from "routing-controllers"; import {BeansConfigurationAdapter} from "../adapters"; @@ -22,9 +18,7 @@ export function NodeBootApplication(options?: ApplicationOptions): Function { }; // Bind Configurations adapters to search from @Beans under the Application class - context.configurationAdapters.push( - new BeansConfigurationAdapter(target), - ); + context.configurationAdapters.push(new BeansConfigurationAdapter(target)); // Bind Application Adapter context.applicationAdapter = new (class implements ApplicationAdapter { @@ -44,23 +38,16 @@ export function NodeBootApplication(options?: ApplicationOptions): Function { origin: ORIGIN, credentials: CREDENTIALS },*/ - routePrefix: - context.applicationOptions.apiOptions?.routePrefix, + routePrefix: context.applicationOptions.apiOptions?.routePrefix, defaults: { - nullResultCode: - context.applicationOptions.apiOptions - ?.nullResultCode, - paramOptions: - context.applicationOptions.apiOptions?.paramOptions, + nullResultCode: context.applicationOptions.apiOptions?.nullResultCode, + paramOptions: context.applicationOptions.apiOptions?.paramOptions, undefinedResultCode: - context.applicationOptions.apiOptions - ?.undefinedResultCode, + context.applicationOptions.apiOptions?.undefinedResultCode, }, classTransformer: context.classTransformer, - classToPlainTransformOptions: - context.classToPlainTransformOptions, - plainToClassTransformOptions: - context.plainToClassTransformOptions, + classToPlainTransformOptions: context.classToPlainTransformOptions, + plainToClassTransformOptions: context.plainToClassTransformOptions, controllers: context.controllerClasses, middlewares: context.globalMiddlewares, defaultErrorHandler: options?.defaultErrorHandler, diff --git a/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts b/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts index cd10a174..5751210f 100644 --- a/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts +++ b/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts @@ -10,9 +10,6 @@ export class AuthorizationCheckerNotDefinedError extends InternalServerError { super( `Cannot use @Authorized decorator. Please define authorizationChecker function in routing-controllers action before using it.`, ); - Object.setPrototypeOf( - this, - AuthorizationCheckerNotDefinedError.prototype, - ); + Object.setPrototypeOf(this, AuthorizationCheckerNotDefinedError.prototype); } } diff --git a/packages/core/src/logger/winston.logger.ts b/packages/core/src/logger/winston.logger.ts index 62946c6f..42feb115 100644 --- a/packages/core/src/logger/winston.logger.ts +++ b/packages/core/src/logger/winston.logger.ts @@ -34,14 +34,10 @@ export function createRootLogger( { level: env["LOG_LEVEL"] || "info", format: - env["NODE_ENV"] === "production" - ? winston.format.json() - : colorFormat(), + env["NODE_ENV"] === "production" ? winston.format.json() : colorFormat(), transports: [ new winston.transports.Console({ - silent: - env["JEST_WORKER_ID"] !== undefined && - !env["LOG_LEVEL"], + silent: env["JEST_WORKER_ID"] !== undefined && !env["LOG_LEVEL"], }), ], }, @@ -68,17 +64,13 @@ function colorFormat(): Format { }, }), format.printf((info: TransformableInfo) => { - const {timestamp, level, message, plugin, service, ...fields} = - info; + const {timestamp, level, message, plugin, service, ...fields} = info; const prefix = plugin || service; const timestampColor = colorizer.colorize("timestamp", timestamp); const prefixColor = colorizer.colorize("prefix", prefix); const extraFields = Object.entries(fields) - .map( - ([key, value]) => - `${colorizer.colorize("field", `${key}`)}=${value}`, - ) + .map(([key, value]) => `${colorizer.colorize("field", `${key}`)}=${value}`) .join(" "); return `${timestampColor} ${prefixColor} ${level} ${message} ${extraFields}`; diff --git a/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts b/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts index 7bc0f626..f6b2d078 100644 --- a/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts +++ b/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts @@ -4,10 +4,6 @@ * * Given that it is not possible to process all uncaught errors sensibly, the best way to deal with them is to crash. */ -export interface FastifyErrorHandlerInterface< - TRequest = any, - TReply = any, - TError = any, -> { +export interface FastifyErrorHandlerInterface { error(request: TRequest, reply: TReply, error: TError): any; } diff --git a/packages/core/src/middlewares/FastifyMiddlewareInterface.ts b/packages/core/src/middlewares/FastifyMiddlewareInterface.ts index 0fc36e80..9b0689dc 100644 --- a/packages/core/src/middlewares/FastifyMiddlewareInterface.ts +++ b/packages/core/src/middlewares/FastifyMiddlewareInterface.ts @@ -2,11 +2,7 @@ * Used to register middlewares. * This signature is used for Fastify hooks. */ -export interface FastifyMiddlewareInterface< - TRequest = any, - TReply = any, - TDone = any, -> { +export interface FastifyMiddlewareInterface { /** * Called before controller action is being executed. * This signature is used for Fastify hooks. diff --git a/packages/core/src/server/BaseServer.ts b/packages/core/src/server/BaseServer.ts index d60564f2..5e9f4bb4 100644 --- a/packages/core/src/server/BaseServer.ts +++ b/packages/core/src/server/BaseServer.ts @@ -30,9 +30,7 @@ export abstract class BaseServer { // Initialize configuration and logging await this.init(); - this.logger.info( - `Running Node-Boot application with '${this.serverType.toUpperCase()}'`, - ); + this.logger.info(`Running Node-Boot application with '${this.serverType.toUpperCase()}'`); const context = ApplicationContext.get(); if (context.diOptions) { this.logger.info(`Binding Node-Boot @Configuration classes`); @@ -45,22 +43,15 @@ export abstract class BaseServer { }); } - this.logger.info( - `Binding Node-Boot @ConfigurationProperties classes`, - ); + this.logger.info(`Binding Node-Boot @ConfigurationProperties classes`); for (const configurationPropertiesAdapter of context.configurationPropertiesAdapters) { - configurationPropertiesAdapter.bind( - context.diOptions.iocContainer, - ); + configurationPropertiesAdapter.bind(context.diOptions.iocContainer); } // it's important to set container before any operation you do with routing-controllers, // including importing controllers this.logger.info(`Setting DI container`); - useContainer( - context.diOptions.iocContainer, - context.diOptions.options, - ); + useContainer(context.diOptions.iocContainer, context.diOptions.options); } if (context.actuatorAdapter) { @@ -80,8 +71,7 @@ export abstract class BaseServer { const openApiAdapter = context.openApi.bind(this.serverType); openApiAdapter.bind( { - basePath: - context.applicationOptions.apiOptions?.routePrefix, + basePath: context.applicationOptions.apiOptions?.routePrefix, controllers: context.controllerClasses, }, framework, @@ -91,24 +81,15 @@ export abstract class BaseServer { } private setupAppConfigs(context: ApplicationContext) { - const appConfigs = - this.config.getOptional("node-boot.app"); + const appConfigs = this.config.getOptional("node-boot.app"); const apiConfigs = this.config.getOptional("node-boot.api"); context.applicationOptions = { environment: - context.applicationOptions?.environment ?? - appConfigs?.environment ?? - "development", + context.applicationOptions?.environment ?? appConfigs?.environment ?? "development", port: context.applicationOptions?.port ?? appConfigs?.port ?? 3000, - platform: - context.applicationOptions?.platform ?? - appConfigs?.platform ?? - "node-boot", - name: - context.applicationOptions?.name ?? - appConfigs?.name ?? - "node-boot-app", + platform: context.applicationOptions?.platform ?? appConfigs?.platform ?? "node-boot", + name: context.applicationOptions?.name ?? appConfigs?.name ?? "node-boot-app", defaultErrorHandler: context.applicationOptions?.defaultErrorHandler ?? appConfigs?.defaultErrorHandler ?? diff --git a/packages/core/src/server/NodeBoot.ts b/packages/core/src/server/NodeBoot.ts index 6e45a70e..9aab7901 100644 --- a/packages/core/src/server/NodeBoot.ts +++ b/packages/core/src/server/NodeBoot.ts @@ -1,9 +1,9 @@ import {BaseServer} from "./BaseServer"; export class NodeBoot { - static async run< - TApplicationServer extends new (...args: any[]) => BaseServer, - >(applicationServer: TApplicationServer): Promise> { + static async run BaseServer>( + applicationServer: TApplicationServer, + ): Promise> { const application = new applicationServer(); return application.run(); } diff --git a/packages/di/src/decorators/Inject.ts b/packages/di/src/decorators/Inject.ts index dcedf8b9..8b717589 100644 --- a/packages/di/src/decorators/Inject.ts +++ b/packages/di/src/decorators/Inject.ts @@ -16,19 +16,11 @@ export function Inject(options?: InjectionOptions): Function { return (target: Object, propertyName: string | Symbol, index?: number) => { // Registering metadata for custom filed injection (used for example in the Persistence Event Subscribers) if (propertyName && typeof propertyName === "string") { - const propertyType = Reflect.getMetadata( - "design:type", - target, - propertyName, - ); + const propertyType = Reflect.getMetadata("design:type", target, propertyName); const injectProperties: string[] = Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, target) || []; injectProperties.push(propertyName); - Reflect.defineMetadata( - REQUIRES_FIELD_INJECTION_KEY, - injectProperties, - target, - ); + Reflect.defineMetadata(REQUIRES_FIELD_INJECTION_KEY, injectProperties, target); } // Normal injection diff --git a/packages/di/src/ioc/makeDiDecoration.ts b/packages/di/src/ioc/makeDiDecoration.ts index d46e08b5..9f98e280 100644 --- a/packages/di/src/ioc/makeDiDecoration.ts +++ b/packages/di/src/ioc/makeDiDecoration.ts @@ -3,20 +3,14 @@ import {DiOptions} from "./types"; /** * Apply dependency injection decorator if dependency injection framework is available * */ -export function decorateDi( - target: TFunction, - options?: DiOptions, -): boolean { +export function decorateDi(target: TFunction, options?: DiOptions): boolean { return decorateTypeDi(target, options) || decorateInversify(target); } /** * Apply @Service decorator if TypeDI framework is available * */ -function decorateTypeDi( - target: TFunction, - options?: DiOptions, -): boolean { +function decorateTypeDi(target: TFunction, options?: DiOptions): boolean { let decorated: boolean; try { const {Service} = require("typedi"); @@ -24,9 +18,7 @@ function decorateTypeDi( decorated = true; } catch (error) { // TypeDi is not available - console.warn( - "@Service decorator is only applied if 'TypeDi' dependency is available!", - ); + console.warn("@Service decorator is only applied if 'TypeDi' dependency is available!"); decorated = false; } return decorated; diff --git a/packages/di/src/ioc/makeInjectionDecoration.ts b/packages/di/src/ioc/makeInjectionDecoration.ts index aa14c4fd..2fc65570 100644 --- a/packages/di/src/ioc/makeInjectionDecoration.ts +++ b/packages/di/src/ioc/makeInjectionDecoration.ts @@ -35,9 +35,7 @@ function decorateTypeDi( } // TypeDi is not available - console.warn( - "@Service decorator is only applied if 'TypeDi' dependency is available!", - ); + console.warn("@Service decorator is only applied if 'TypeDi' dependency is available!"); decorated = false; } return decorated; @@ -59,9 +57,7 @@ function decorateInversify( decorated = true; } catch (error) { // Inversify is not available - console.warn( - "@inject decorator is only applied if 'Inversify' dependency is available!", - ); + console.warn("@inject decorator is only applied if 'Inversify' dependency is available!"); decorated = false; } return decorated; diff --git a/packages/di/src/ioc/types.ts b/packages/di/src/ioc/types.ts index 15c0b2f3..c9d6c4e7 100644 --- a/packages/di/src/ioc/types.ts +++ b/packages/di/src/ioc/types.ts @@ -8,9 +8,4 @@ export interface Abstract { export type DiOptions = ComponentOptions | string | Token; -export type InjectionOptions = - | string - | symbol - | Token - | Abstract - | Newable; +export type InjectionOptions = string | symbol | Token | Abstract | Newable; diff --git a/packages/extension/src/Optional.ts b/packages/extension/src/Optional.ts index 34f10524..8285266d 100644 --- a/packages/extension/src/Optional.ts +++ b/packages/extension/src/Optional.ts @@ -157,9 +157,7 @@ export class Optional { if (this.isPresent()) { return mapper(this.value!); } - throw new Error( - "FlatMap operation cannot be called on an empty Optional", - ); + throw new Error("FlatMap operation cannot be called on an empty Optional"); } /** @@ -182,15 +180,13 @@ export class Optional { return Optional.of(this.value.filter(predicate) as T); } else if (this.value instanceof Map || this.value instanceof Set) { // Filter map or set - const filteredEntries = Array.from(this.value.entries()).filter( - ([key, value]) => predicate(value), + const filteredEntries = Array.from(this.value.entries()).filter(([key, value]) => + predicate(value), ); if (this.value instanceof Map) { return Optional.of(new Map(filteredEntries) as T); } else { - return Optional.of( - new Set(filteredEntries.map(([key, value]) => value)) as T, - ); + return Optional.of(new Set(filteredEntries.map(([key, value]) => value)) as T); } } else { // Filter single value @@ -207,9 +203,7 @@ export class Optional { * Returns an empty Optional object if the original Optional object is not present. * */ convert(converter: (value: T) => U): Optional { - return this.isPresent() - ? Optional.of(converter(this.value!)) - : Optional.empty(); + return this.isPresent() ? Optional.of(converter(this.value!)) : Optional.empty(); } /** @@ -325,9 +319,7 @@ export class Optional { const result = callback(this.value as T); return Optional.of(result); } - throw new Error( - "Running operations in empty/undefined objects is not possible", - ); + throw new Error("Running operations in empty/undefined objects is not possible"); } /** diff --git a/packages/openapi/src/adapter/ExpressOpenApi.ts b/packages/openapi/src/adapter/ExpressOpenApi.ts index c355a640..9f1800ab 100644 --- a/packages/openapi/src/adapter/ExpressOpenApi.ts +++ b/packages/openapi/src/adapter/ExpressOpenApi.ts @@ -7,14 +7,8 @@ export class ExpressOpenApi implements OpenApiAdapter { if (swaggerUi?.serve) { const {spec, options} = OpenApiSpecAdapter.adapt(openApiOptions); - router.get(options.swaggerOptions.url, (req, res) => - res.json(spec), - ); - server.use( - "/api-docs", - swaggerUi.serve, - swaggerUi.setup(spec, options), - ); + router.get(options.swaggerOptions.url, (req, res) => res.json(spec)); + server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(spec, options)); } else { throw new Error( "Unable to initialize Swagger UI. 'swagger-ui-express' dependency is missing. " + diff --git a/packages/openapi/src/adapter/FastifyOpenApi.ts b/packages/openapi/src/adapter/FastifyOpenApi.ts index 4f231823..9a1f2001 100644 --- a/packages/openapi/src/adapter/FastifyOpenApi.ts +++ b/packages/openapi/src/adapter/FastifyOpenApi.ts @@ -3,11 +3,7 @@ import {FastifyInstance} from "fastify"; import {OpenApiSpecAdapter} from "./OpenApiSpecAdapter"; export class FastifyOpenApi implements OpenApiAdapter { - bind( - openApiOptions: OpenApiOptions, - server: FastifyInstance, - router: FastifyInstance, - ): void { + bind(openApiOptions: OpenApiOptions, server: FastifyInstance, router: FastifyInstance): void { const {spec, options} = OpenApiSpecAdapter.adapt(openApiOptions); router.get(options.swaggerOptions.url, async (request, reply) => { diff --git a/packages/openapi/src/decorator/EnableOpenApi.ts b/packages/openapi/src/decorator/EnableOpenApi.ts index 7626d9e6..ee88a2f6 100644 --- a/packages/openapi/src/decorator/EnableOpenApi.ts +++ b/packages/openapi/src/decorator/EnableOpenApi.ts @@ -1,8 +1,4 @@ -import { - ApplicationContext, - OpenApiAdapter, - OpenApiBridgeAdapter, -} from "@node-boot/context"; +import {ApplicationContext, OpenApiAdapter, OpenApiBridgeAdapter} from "@node-boot/context"; import * as oa from "openapi3-ts"; import {ExpressOpenApi} from "../adapter"; import {KoaOpenApi} from "../adapter/KoaOpenApi"; @@ -13,13 +9,9 @@ import {FastifyOpenApi} from "../adapter/FastifyOpenApi"; * * @param openApi The OpenAPI definitions and base config */ -export function EnableOpenApi( - openApi: Partial = {}, -): Function { +export function EnableOpenApi(openApi: Partial = {}): Function { return function (object: Function) { - ApplicationContext.get().openApi = new (class - implements OpenApiBridgeAdapter - { + ApplicationContext.get().openApi = new (class implements OpenApiBridgeAdapter { bind(serverType: string): OpenApiAdapter { switch (serverType) { case "express": diff --git a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts index 6fb0e0fb..40f49bc0 100644 --- a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts @@ -18,8 +18,7 @@ export class DefaultAuthorizationResolver implements AuthorizationResolver { roles: ["USER", "ADMIN"], }; if (user && !roles.length) return true; - if (user && roles.find(role => user.roles.indexOf(role) !== -1)) - return true; + if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true; return false; } } diff --git a/samples/sample-express/src/config/MultipleConfigurations.ts b/samples/sample-express/src/config/MultipleConfigurations.ts index 051095d3..f28ed4f5 100644 --- a/samples/sample-express/src/config/MultipleConfigurations.ts +++ b/samples/sample-express/src/config/MultipleConfigurations.ts @@ -3,9 +3,5 @@ import {SecurityConfiguration} from "./SecurityConfiguration"; import {ClassTransformConfiguration} from "./ClassTransformConfiguration"; import {CustomNamingStrategy} from "../persistence"; -@Configurations([ - SecurityConfiguration, - ClassTransformConfiguration, - CustomNamingStrategy, -]) +@Configurations([SecurityConfiguration, ClassTransformConfiguration, CustomNamingStrategy]) export class MultipleConfigurations {} diff --git a/samples/sample-express/src/controllers/users.controller.ts b/samples/sample-express/src/controllers/users.controller.ts index a5b7e304..edb75745 100644 --- a/samples/sample-express/src/controllers/users.controller.ts +++ b/samples/sample-express/src/controllers/users.controller.ts @@ -1,13 +1,4 @@ -import { - Body, - Delete, - Get, - HttpCode, - Param, - Post, - Put, - UseBefore, -} from "routing-controllers"; +import {Body, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "routing-controllers"; import {UserService} from "../services/users.service"; import {ValidationMiddleware} from "../middlewares/validation.middleware"; import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; @@ -68,10 +59,7 @@ export class UserController { @UseBefore(ValidationMiddleware(UpdateUserDto)) @OpenAPI({summary: "Update a user"}) async updateUser(@Param("id") userId: number, @Body() userData: User) { - const updateUserData: User = await this.user.updateUser( - userId, - userData, - ); + const updateUserData: User = await this.user.updateUser(userId, userData); return {data: updateUserData, message: "updated"}; } diff --git a/samples/sample-express/src/dtos/users.dto.ts b/samples/sample-express/src/dtos/users.dto.ts index 54f9a141..72edaaeb 100644 --- a/samples/sample-express/src/dtos/users.dto.ts +++ b/samples/sample-express/src/dtos/users.dto.ts @@ -1,10 +1,4 @@ -import { - IsEmail, - IsString, - IsNotEmpty, - MinLength, - MaxLength, -} from "class-validator"; +import {IsEmail, IsString, IsNotEmpty, MinLength, MaxLength} from "class-validator"; export class CreateUserDto { @IsEmail() diff --git a/samples/sample-express/src/middlewares/validation.middleware.ts b/samples/sample-express/src/middlewares/validation.middleware.ts index 0a405a6e..99d38336 100644 --- a/samples/sample-express/src/middlewares/validation.middleware.ts +++ b/samples/sample-express/src/middlewares/validation.middleware.ts @@ -30,9 +30,7 @@ export const ValidationMiddleware = ( }) .catch((errors: ValidationError[]) => { const message = errors - .map((error: ValidationError) => - Object.values(error.constraints ?? {}), - ) + .map((error: ValidationError) => Object.values(error.constraints ?? {})) .join(", "); next(new HttpException(400, message)); }); diff --git a/samples/sample-express/src/models/users.model.ts b/samples/sample-express/src/models/users.model.ts index 0e4d3214..ea265301 100644 --- a/samples/sample-express/src/models/users.model.ts +++ b/samples/sample-express/src/models/users.model.ts @@ -4,25 +4,21 @@ export const UserModel: User[] = [ { id: 1, email: "example1@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 2, email: "example2@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 3, email: "example3@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 4, email: "example4@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, ]; diff --git a/samples/sample-express/src/persistence/CustomNamingStrategy.ts b/samples/sample-express/src/persistence/CustomNamingStrategy.ts index 078ae4a3..0ae4fe88 100644 --- a/samples/sample-express/src/persistence/CustomNamingStrategy.ts +++ b/samples/sample-express/src/persistence/CustomNamingStrategy.ts @@ -5,10 +5,7 @@ import {PersistenceNamingStrategy} from "@node-boot/starter-persistence"; export class CustomNamingStrategy extends DefaultNamingStrategy { name = "sample-naming-strategy"; - override tableName( - targetName: string, - userSpecifiedName: string | undefined, - ): string { + override tableName(targetName: string, userSpecifiedName: string | undefined): string { return `nb-${super.tableName(targetName, userSpecifiedName)}`; } } diff --git a/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts index b5ca1da7..71a478c8 100644 --- a/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts +++ b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts @@ -57,60 +57,42 @@ export class GlobalEntityEventListener implements EntitySubscriberInterface { * Called before entity removal. */ beforeRemove(event: RemoveEvent) { - this.logger.info( - `BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, - event.entity, - ); + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); } /** * Called after entity removal. */ afterRemove(event: RemoveEvent) { - this.logger.info( - `AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, - event.entity, - ); + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); } /** * Called before entity removal. */ beforeSoftRemove(event: SoftRemoveEvent) { - this.logger.info( - `BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, - event.entity, - ); + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); } /** * Called after entity removal. */ afterSoftRemove(event: SoftRemoveEvent) { - this.logger.info( - `AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, - event.entity, - ); + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); } /** * Called before entity recovery. */ beforeRecover(event: RecoverEvent) { - this.logger.info( - `BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, - event.entity, - ); + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); } /** * Called after entity recovery. */ afterRecover(event: RecoverEvent) { - this.logger.info( - `AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, - event.entity, - ); + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); } /** diff --git a/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts index e7e780aa..de6e861d 100644 --- a/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts +++ b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts @@ -6,9 +6,7 @@ import {Logger} from "winston"; import {GreetingService} from "../../services/greeting.service"; @EntityEventSubscriber() -export class UserEntityEventListener - implements EntitySubscriberInterface -{ +export class UserEntityEventListener implements EntitySubscriberInterface { @Inject() private logger: Logger; diff --git a/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts b/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts index 93df1e8b..5cd11022 100644 --- a/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts +++ b/samples/sample-express/src/persistence/migrations/1701786331338-migration.ts @@ -4,9 +4,7 @@ import {Migration} from "@node-boot/starter-persistence"; @Migration() export class Migration1701786331338 implements MigrationInterface { async up(queryRunner: QueryRunner): Promise { - await queryRunner.query( - `ALTER TABLE "nb-user" ADD COLUMN "name" varchar(255)`, - ); + await queryRunner.query(`ALTER TABLE "nb-user" ADD COLUMN "name" varchar(255)`); } async down(queryRunner: QueryRunner): Promise { diff --git a/samples/sample-express/src/services/greeting.service.ts b/samples/sample-express/src/services/greeting.service.ts index 3886f755..4ad72ada 100644 --- a/samples/sample-express/src/services/greeting.service.ts +++ b/samples/sample-express/src/services/greeting.service.ts @@ -7,8 +7,6 @@ export class GreetingService { constructor(private readonly logger: Logger) {} public sayHello(user: User): void { - this.logger.info( - `I'm really happy that you exists ${user.id}/${user.email}`, - ); + this.logger.info(`I'm really happy that you exists ${user.id}/${user.email}`); } } diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index bbf6d0a8..640f13a6 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -26,9 +26,7 @@ export class UserService { public async findAllUser(): Promise { this.logger.info("Getting all users"); const appName = this.configService.getString("node-boot.app.name"); - this.logger.info( - `Reading node-boot.app.name from app-config.yam: ${appName}`, - ); + this.logger.info(`Reading node-boot.app.name from app-config.yam: ${appName}`); return this.userRepository.find(); } @@ -58,20 +56,13 @@ export class UserService { return Optional.of(existingUser) .ifPresentThrow( - () => - new HttpException( - 409, - `This email ${userData.email} already exists`, - ), + () => new HttpException(409, `This email ${userData.email} already exists`), ) .elseAsync(() => this.userRepository.save(userData)); } @Transactional() - public async updateUser( - userId: number, - userData: UpdateUserDto, - ): Promise { + public async updateUser(userId: number, userData: UpdateUserDto): Promise { const user = await this.userRepository.findOneBy({ id: userId, }); @@ -94,18 +85,13 @@ export class UserService { }); runOnTransactionRollback(error => { - this.logger.warn( - "Transactions was rolled back due to error:", - error, - ); + this.logger.warn("Transactions was rolled back due to error:", error); }); await Optional.of(user) .orElseThrow(() => new HttpException(409, "User doesn't exist")) .runAsync(user => this.userRepository.delete({id: userId})); - throw new Error( - "Error after deleting that should rollback transaction", - ); + throw new Error("Error after deleting that should rollback transaction"); } } diff --git a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts index 6fb0e0fb..40f49bc0 100644 --- a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts @@ -18,8 +18,7 @@ export class DefaultAuthorizationResolver implements AuthorizationResolver { roles: ["USER", "ADMIN"], }; if (user && !roles.length) return true; - if (user && roles.find(role => user.roles.indexOf(role) !== -1)) - return true; + if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true; return false; } } diff --git a/samples/sample-fastify/src/controllers/users.controller.ts b/samples/sample-fastify/src/controllers/users.controller.ts index c08b1777..17fd07c7 100644 --- a/samples/sample-fastify/src/controllers/users.controller.ts +++ b/samples/sample-fastify/src/controllers/users.controller.ts @@ -1,12 +1,4 @@ -import { - Body, - Delete, - Get, - HttpCode, - Param, - Post, - Put, -} from "routing-controllers"; +import {Body, Delete, Get, HttpCode, Param, Post, Put} from "routing-controllers"; import {UserService} from "../services/users.service"; import {User} from "../interfaces/users.interface"; import {BackendConfigProperties} from "../config/BackendConfigProperties"; @@ -58,10 +50,7 @@ export class UserController { @Put("/users/:id") @OpenAPI({summary: "Update a user"}) async updateUser(@Param("id") userId: number, @Body() userData: User) { - const updateUserData: User[] = await this.user.updateUser( - userId, - userData, - ); + const updateUserData: User[] = await this.user.updateUser(userId, userData); return {data: updateUserData, message: "updated"}; } diff --git a/samples/sample-fastify/src/dtos/users.dto.ts b/samples/sample-fastify/src/dtos/users.dto.ts index 54f9a141..72edaaeb 100644 --- a/samples/sample-fastify/src/dtos/users.dto.ts +++ b/samples/sample-fastify/src/dtos/users.dto.ts @@ -1,10 +1,4 @@ -import { - IsEmail, - IsString, - IsNotEmpty, - MinLength, - MaxLength, -} from "class-validator"; +import {IsEmail, IsString, IsNotEmpty, MinLength, MaxLength} from "class-validator"; export class CreateUserDto { @IsEmail() diff --git a/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts b/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts index 6432c31a..1264e657 100644 --- a/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts @@ -6,12 +6,7 @@ import {HookHandlerDoneFunction} from "fastify/types/hooks"; @Middleware({type: "before"}) export class LoggingMiddleware - implements - FastifyMiddlewareInterface< - FastifyRequest, - FastifyReply, - HookHandlerDoneFunction - > + implements FastifyMiddlewareInterface { @Inject() private logger: Logger; diff --git a/samples/sample-fastify/src/middlewares/customErrorHandler.ts b/samples/sample-fastify/src/middlewares/customErrorHandler.ts index 666d7c6d..543abbf2 100644 --- a/samples/sample-fastify/src/middlewares/customErrorHandler.ts +++ b/samples/sample-fastify/src/middlewares/customErrorHandler.ts @@ -8,21 +8,12 @@ import {errorCodes, FastifyError} from "fastify"; @ErrorHandler() export class CustomErrorHandler - implements - FastifyErrorHandlerInterface< - FastifyRequest, - FastifyReply, - FastifyError - > + implements FastifyErrorHandlerInterface { @Inject() private logger: Logger; - error( - request: FastifyRequest, - reply: FastifyReply, - error: FastifyError, - ): void { + error(request: FastifyRequest, reply: FastifyReply, error: FastifyError): void { if (error instanceof errorCodes.FST_ERR_BAD_STATUS_CODE) { // Log error this.logger.error(error); diff --git a/samples/sample-fastify/src/models/users.model.ts b/samples/sample-fastify/src/models/users.model.ts index afc16e92..d31342f3 100644 --- a/samples/sample-fastify/src/models/users.model.ts +++ b/samples/sample-fastify/src/models/users.model.ts @@ -5,25 +5,21 @@ export const UserModel: User[] = [ { id: 1, email: "example1@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 2, email: "example2@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 3, email: "example3@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 4, email: "example4@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, ]; diff --git a/samples/sample-fastify/src/services/users.service.ts b/samples/sample-fastify/src/services/users.service.ts index dc53b088..f1aa9f08 100644 --- a/samples/sample-fastify/src/services/users.service.ts +++ b/samples/sample-fastify/src/services/users.service.ts @@ -9,19 +9,14 @@ import {NotFoundError} from "routing-controllers"; @Service() export class UserService { - constructor( - private readonly logger: Logger, - private readonly configService: ConfigService, - ) {} + constructor(private readonly logger: Logger, private readonly configService: ConfigService) {} public async findAllUser(): Promise { this.logger.info("Getting all users"); const users: User[] = UserModel; const baseUrl = this.configService.getString("backend.baseUrl"); - this.logger.info( - `Reading backend.baseUrl from app-config.yam: ${baseUrl}`, - ); + this.logger.info(`Reading backend.baseUrl from app-config.yam: ${baseUrl}`); return users; } @@ -33,19 +28,12 @@ export class UserService { public async createUser(userData: CreateUserDto): Promise { const findUser = UserModel.find(user => user.email === userData.email); - if (findUser) - throw new HttpException( - 409, - `This email ${userData.email} already exists`, - ); + if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); return {id: UserModel.length + 1, ...userData}; } - public async updateUser( - userId: number, - userData: CreateUserDto, - ): Promise { + public async updateUser(userId: number, userData: CreateUserDto): Promise { const findUser = UserModel.find(user => user.id === userId); if (!findUser) throw new HttpException(409, "User doesn't exist"); diff --git a/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts index 6fb0e0fb..40f49bc0 100644 --- a/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts @@ -18,8 +18,7 @@ export class DefaultAuthorizationResolver implements AuthorizationResolver { roles: ["USER", "ADMIN"], }; if (user && !roles.length) return true; - if (user && roles.find(role => user.roles.indexOf(role) !== -1)) - return true; + if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true; return false; } } diff --git a/samples/sample-koa/src/controllers/users.controller.ts b/samples/sample-koa/src/controllers/users.controller.ts index 2cb92f86..fd042324 100644 --- a/samples/sample-koa/src/controllers/users.controller.ts +++ b/samples/sample-koa/src/controllers/users.controller.ts @@ -1,13 +1,4 @@ -import { - Body, - Delete, - Get, - HttpCode, - Param, - Post, - Put, - UseBefore, -} from "routing-controllers"; +import {Body, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "routing-controllers"; import {UserService} from "../services/users.service"; import {User} from "../interfaces/users.interface"; import {ValidationMiddleware} from "../middlewares/validation.middleware"; @@ -61,10 +52,7 @@ export class UserController { @UseBefore(ValidationMiddleware(UpdateUserDto)) @OpenAPI({summary: "Update a user"}) async updateUser(@Param("id") userId: number, @Body() userData: User) { - const updateUserData: User[] = await this.user.updateUser( - userId, - userData, - ); + const updateUserData: User[] = await this.user.updateUser(userId, userData); return {data: updateUserData, message: "updated"}; } diff --git a/samples/sample-koa/src/dtos/users.dto.ts b/samples/sample-koa/src/dtos/users.dto.ts index 54f9a141..72edaaeb 100644 --- a/samples/sample-koa/src/dtos/users.dto.ts +++ b/samples/sample-koa/src/dtos/users.dto.ts @@ -1,10 +1,4 @@ -import { - IsEmail, - IsString, - IsNotEmpty, - MinLength, - MaxLength, -} from "class-validator"; +import {IsEmail, IsString, IsNotEmpty, MinLength, MaxLength} from "class-validator"; export class CreateUserDto { @IsEmail() diff --git a/samples/sample-koa/src/middlewares/validation.middleware.ts b/samples/sample-koa/src/middlewares/validation.middleware.ts index b4a13bad..824ca6ab 100644 --- a/samples/sample-koa/src/middlewares/validation.middleware.ts +++ b/samples/sample-koa/src/middlewares/validation.middleware.ts @@ -29,9 +29,7 @@ export const ValidationMiddleware = ( }) .catch((errors: ValidationError[]) => { const message = errors - .map((error: ValidationError) => - Object.values(error.constraints ?? {}), - ) + .map((error: ValidationError) => Object.values(error.constraints ?? {})) .join(", "); next(new HttpException(400, message)); }); diff --git a/samples/sample-koa/src/models/users.model.ts b/samples/sample-koa/src/models/users.model.ts index afc16e92..d31342f3 100644 --- a/samples/sample-koa/src/models/users.model.ts +++ b/samples/sample-koa/src/models/users.model.ts @@ -5,25 +5,21 @@ export const UserModel: User[] = [ { id: 1, email: "example1@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 2, email: "example2@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 3, email: "example3@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, { id: 4, email: "example4@email.com", - password: - "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", + password: "$2b$10$TBEfaCe1oo.2jfkBDWcj/usBj4oECsW2wOoDXpCa2IH9xqCpEK/hC", }, ]; diff --git a/samples/sample-koa/src/services/users.service.ts b/samples/sample-koa/src/services/users.service.ts index dc53b088..f1aa9f08 100644 --- a/samples/sample-koa/src/services/users.service.ts +++ b/samples/sample-koa/src/services/users.service.ts @@ -9,19 +9,14 @@ import {NotFoundError} from "routing-controllers"; @Service() export class UserService { - constructor( - private readonly logger: Logger, - private readonly configService: ConfigService, - ) {} + constructor(private readonly logger: Logger, private readonly configService: ConfigService) {} public async findAllUser(): Promise { this.logger.info("Getting all users"); const users: User[] = UserModel; const baseUrl = this.configService.getString("backend.baseUrl"); - this.logger.info( - `Reading backend.baseUrl from app-config.yam: ${baseUrl}`, - ); + this.logger.info(`Reading backend.baseUrl from app-config.yam: ${baseUrl}`); return users; } @@ -33,19 +28,12 @@ export class UserService { public async createUser(userData: CreateUserDto): Promise { const findUser = UserModel.find(user => user.email === userData.email); - if (findUser) - throw new HttpException( - 409, - `This email ${userData.email} already exists`, - ); + if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); return {id: UserModel.length + 1, ...userData}; } - public async updateUser( - userId: number, - userData: CreateUserDto, - ): Promise { + public async updateUser(userId: number, userData: CreateUserDto): Promise { const findUser = UserModel.find(user => user.id === userId); if (!findUser) throw new HttpException(409, "User doesn't exist"); diff --git a/servers/express-server/src/ExpressServer.ts b/servers/express-server/src/ExpressServer.ts index 51ef6a16..3a7a1926 100644 --- a/servers/express-server/src/ExpressServer.ts +++ b/servers/express-server/src/ExpressServer.ts @@ -3,10 +3,7 @@ import express from "express"; import {useExpressServer} from "routing-controllers"; import {BaseServer} from "@node-boot/core"; -export class ExpressServer extends BaseServer< - express.Application, - express.Application -> { +export class ExpressServer extends BaseServer { public framework: express.Application; constructor() { @@ -23,9 +20,7 @@ export class ExpressServer extends BaseServer< // Bind application container through adapter if (context.applicationAdapter) { - const configs = context.applicationAdapter.bind( - context.diOptions?.iocContainer, - ); + const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); useExpressServer(this.framework, configs); } else { throw new Error( @@ -41,12 +36,8 @@ export class ExpressServer extends BaseServer< this.framework.listen(context.applicationOptions.port, () => { this.logger.info(`=================================`); - this.logger.info( - `======= ENV: ${context.applicationOptions.environment} =======`, - ); - this.logger.info( - `🚀 App listening on the port ${context.applicationOptions.port}`, - ); + this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); + this.logger.info(`🚀 App listening on the port ${context.applicationOptions.port}`); this.logger.info(`=================================`); }); } diff --git a/servers/fastify-server/src/FastifyServer.ts b/servers/fastify-server/src/FastifyServer.ts index e4e7d10a..6687b122 100644 --- a/servers/fastify-server/src/FastifyServer.ts +++ b/servers/fastify-server/src/FastifyServer.ts @@ -4,10 +4,7 @@ import {BaseServer} from "@node-boot/core"; import Fastify, {FastifyInstance} from "fastify"; import {FastifyDriver} from "./driver/FastifyDriver"; -export class FastifyServer extends BaseServer< - FastifyInstance, - FastifyInstance -> { +export class FastifyServer extends BaseServer { private readonly framework: FastifyInstance; constructor() { @@ -23,9 +20,7 @@ export class FastifyServer extends BaseServer< // Bind application container through adapter if (context.applicationAdapter) { - const configs = context.applicationAdapter.bind( - context.diOptions?.iocContainer, - ); + const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); const driver = new FastifyDriver( { @@ -67,9 +62,7 @@ export class FastifyServer extends BaseServer< process.exit(1); } this.logger.info(`=================================`); - this.logger.info( - `======= ENV: ${context.applicationOptions.environment} =======`, - ); + this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); this.logger.info(`🚀 App listening on ${address}`); this.logger.info(`=================================`); }, diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index 46a2ebc7..0bc3e9a9 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -10,17 +10,9 @@ import { RoutingControllersOptions, UseMetadata, } from "routing-controllers"; -import { - FastifyError, - FastifyInstance, - FastifyReply, - FastifyRequest, -} from "fastify"; +import {FastifyError, FastifyInstance, FastifyReply, FastifyRequest} from "fastify"; import {HTTPMethods} from "fastify/types/utils"; -import { - DoneFuncWithErrOrRes, - HookHandlerDoneFunction, -} from "fastify/types/hooks"; +import {DoneFuncWithErrOrRes, HookHandlerDoneFunction} from "fastify/types/hooks"; import { AccessDeniedError, AuthorizationCheckerNotDefinedError, @@ -54,10 +46,7 @@ export type ServerOptions = { }; export class FastifyDriver extends BaseDriver { - constructor( - private readonly serverOptions: ServerOptions, - private fastify?: FastifyInstance, - ) { + constructor(private readonly serverOptions: ServerOptions, private fastify?: FastifyInstance) { super(); this.loadFastify(); this.app = this.fastify; @@ -70,10 +59,7 @@ export class FastifyDriver extends BaseDriver { initialize() { if (this.serverOptions.cookieOptions) { const fastifyCookie = this.loadCookie(); - this.useApp().register( - fastifyCookie, - this.serverOptions.cookieOptions, - ); + this.useApp().register(fastifyCookie, this.serverOptions.cookieOptions); } if (this.serverOptions.corsOptions) { @@ -83,36 +69,24 @@ export class FastifyDriver extends BaseDriver { if (this.serverOptions.sessionOptions) { const fastifySession = this.loadSession(); - this.useApp().register( - fastifySession, - this.serverOptions.sessionOptions, - ); + this.useApp().register(fastifySession, this.serverOptions.sessionOptions); } if (this.serverOptions.multipartOptions) { const fastifyMultipart = this.loadMultipart(); - this.useApp().register( - fastifyMultipart, - this.serverOptions.multipartOptions, - ); + this.useApp().register(fastifyMultipart, this.serverOptions.multipartOptions); } if (this.serverOptions.templateOptions) { const fastifyView = this.loadView(); - this.useApp().register( - fastifyView, - this.serverOptions.templateOptions, - ); + this.useApp().register(fastifyView, this.serverOptions.templateOptions); } } /** * Registers middleware that run before controller actions. */ - registerMiddleware( - middleware: MiddlewareMetadata, - options: RoutingControllersOptions, - ): void { + registerMiddleware(middleware: MiddlewareMetadata, options: RoutingControllersOptions): void { // Register a custom error Handler if ((middleware.instance as FastifyErrorHandlerInterface).error) { const errorHandler = ( @@ -120,11 +94,7 @@ export class FastifyDriver extends BaseDriver { request: FastifyRequest, reply: FastifyReply, ) => { - (middleware.instance as FastifyErrorHandlerInterface).error( - request, - reply, - error, - ); + (middleware.instance as FastifyErrorHandlerInterface).error(request, reply, error); }; // Name the function for better debugging @@ -140,14 +110,7 @@ export class FastifyDriver extends BaseDriver { reply: FastifyReply, done: HookHandlerDoneFunction, ) => { - this.callGlobalMiddleware( - request, - options, - middleware, - reply, - done, - undefined, - ); + this.callGlobalMiddleware(request, options, middleware, reply, done, undefined); }; // Name the function for better debugging @@ -160,14 +123,7 @@ export class FastifyDriver extends BaseDriver { payload: any, done: DoneFuncWithErrOrRes, ) => { - this.callGlobalMiddleware( - request, - options, - middleware, - reply, - done, - payload, - ); + this.callGlobalMiddleware(request, options, middleware, reply, done, payload); }; // Name the function for better debugging @@ -187,9 +143,12 @@ export class FastifyDriver extends BaseDriver { ) { if (request.url.startsWith(options.routePrefix || "/")) { try { - const useResult = ( - middleware.instance as FastifyMiddlewareInterface - ).use(request, reply, done, payload); + const useResult = (middleware.instance as FastifyMiddlewareInterface).use( + request, + reply, + done, + payload, + ); if (this.isPromiseLike(useResult)) { useResult .then(useResult => done()) @@ -225,10 +184,7 @@ export class FastifyDriver extends BaseDriver { }); } - registerAction( - actionMetadata: ActionMetadata, - executeAction: (options: Action) => any, - ) { + registerAction(actionMetadata: ActionMetadata, executeAction: (options: Action) => any) { const defaultMiddlewares: any[] = []; if (actionMetadata.isAuthorizedUsed) { @@ -238,12 +194,7 @@ export class FastifyDriver extends BaseDriver { reply: FastifyReply, done: HookHandlerDoneFunction, ) => { - await this.checkAuthorization( - request, - reply, - done, - actionMetadata, - ); + await this.checkAuthorization(request, reply, done, actionMetadata); }, ); } @@ -254,16 +205,9 @@ export class FastifyDriver extends BaseDriver { // ... }*/ - const uses = [ - ...actionMetadata.controllerMetadata.uses, - ...actionMetadata.uses, - ]; - const beforeMiddlewares = this.prepareUseMiddlewares( - uses.filter(use => !use.afterAction), - ); - const afterMiddlewares = this.prepareUseMiddlewares( - uses.filter(use => use.afterAction), - ); + const uses = [...actionMetadata.controllerMetadata.uses, ...actionMetadata.uses]; + const beforeMiddlewares = this.prepareUseMiddlewares(uses.filter(use => !use.afterAction)); + const afterMiddlewares = this.prepareUseMiddlewares(uses.filter(use => use.afterAction)); const errorMiddlewares = this.prepareUseErrorMiddlewares(uses); const routeHandler = async (request, reply) => { @@ -277,28 +221,16 @@ export class FastifyDriver extends BaseDriver { } }; - const afterMiddlewaresAdapter = async ( - request, - reply, - payload, - done, - ) => { - afterMiddlewares.forEach(middleware => - middleware(request, reply, payload, done), - ); + const afterMiddlewaresAdapter = async (request, reply, payload, done) => { + afterMiddlewares.forEach(middleware => middleware(request, reply, payload, done)); }; const errorMiddlewaresAdapter = async (request, reply, error, done) => { - errorMiddlewares.forEach(middleware => - middleware(request, reply, error, done), - ); + errorMiddlewares.forEach(middleware => middleware(request, reply, error, done)); }; // Register route and hooks - const route = ActionMetadata.appendBaseRoute( - this.routePrefix, - actionMetadata.fullRoute, - ); + const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); this.useApp().route({ method: this.actionToHttpMethod(actionMetadata), url: route.toString(), @@ -318,8 +250,7 @@ export class FastifyDriver extends BaseDriver { } async checkAuthorization(request, reply, done, actionMetadata) { - if (!this.authorizationChecker) - throw new AuthorizationCheckerNotDefinedError(); + if (!this.authorizationChecker) throw new AuthorizationCheckerNotDefinedError(); const action: Action = {request, response: reply, next: done}; try { @@ -356,10 +287,9 @@ export class FastifyDriver extends BaseDriver { payload?: any, ) => { try { - const useResult = - getFromContainer( - use.middleware, - ).use(request, reply, done, payload); + const useResult = getFromContainer( + use.middleware, + ).use(request, reply, done, payload); if (this.isPromiseLike(useResult)) { useResult.catch((error: any) => { @@ -466,9 +396,7 @@ export class FastifyDriver extends BaseDriver { // For example, for cookies: // https://github.com/fastify/fastify-cookie case "cookie": - return this.useApp().parseCookie(request.headers.cookie || "")[ - param.name - ]; + return this.useApp().parseCookie(request.headers.cookie || "")[param.name]; case "cookies": return this.useApp().parseCookie(request.headers.cookie || ""); @@ -487,18 +415,11 @@ export class FastifyDriver extends BaseDriver { } } - handleError( - error: any, - action: ActionMetadata | undefined, - options: Action, - ) { + handleError(error: any, action: ActionMetadata | undefined, options: Action) { // Handle error using Fastify's reply if (action) { Object.keys(action.headers).forEach(name => { - (options.response as FastifyReply).header( - name, - action.headers[name], - ); + (options.response as FastifyReply).header(name, action.headers[name]); }); } @@ -565,32 +486,20 @@ export class FastifyDriver extends BaseDriver { } } - private applyTemplateRender( - result: any, - options: Action, - action: ActionMetadata, - ) { + private applyTemplateRender(result: any, options: Action, action: ActionMetadata) { // if template is set then render it const renderOptions = result && result instanceof Object ? result : {}; - options.response.view( - action.renderedTemplate, - renderOptions, - (err, html) => { - if (err) { - throw err; - } else if (html) { - options.response.send(html); - } - }, - ); + options.response.view(action.renderedTemplate, renderOptions, (err, html) => { + if (err) { + throw err; + } else if (html) { + options.response.send(html); + } + }); } - private applyRedirect( - result: any, - options: Action, - action: ActionMetadata, - ) { + private applyRedirect(result: any, options: Action, action: ActionMetadata) { // if redirect is set then do it if (typeof result === "string") { options.response.redirect(result); @@ -603,11 +512,7 @@ export class FastifyDriver extends BaseDriver { } } - private applyResponseStatus( - result: any, - action: ActionMetadata, - options: Action, - ) { + private applyResponseStatus(result: any, action: ActionMetadata, options: Action) { if (result === undefined && action.undefinedResultCode) { if (action.undefinedResultCode instanceof Function) { throw new (action.undefinedResultCode as any)(options); @@ -644,9 +549,7 @@ export class FastifyDriver extends BaseDriver { } } } else { - throw new Error( - "Cannot load fastify. Try to install all required dependencies.", - ); + throw new Error("Cannot load fastify. Try to install all required dependencies."); } } @@ -719,10 +622,6 @@ export class FastifyDriver extends BaseDriver { * Checks if given value is a Promise-like object. */ isPromiseLike(arg: any): arg is Promise { - return ( - arg != null && - typeof arg === "object" && - typeof arg.then === "function" - ); + return arg != null && typeof arg === "object" && typeof arg.then === "function"; } } diff --git a/servers/fastify-server/src/utils.ts b/servers/fastify-server/src/utils.ts index 1689fe49..2f9e3b9f 100644 --- a/servers/fastify-server/src/utils.ts +++ b/servers/fastify-server/src/utils.ts @@ -1,5 +1,3 @@ export function isPromiseLike(arg: any): arg is Promise { - return ( - arg != null && typeof arg === "object" && typeof arg.then === "function" - ); + return arg != null && typeof arg === "object" && typeof arg.then === "function"; } diff --git a/servers/koa-server/src/KoaServer.ts b/servers/koa-server/src/KoaServer.ts index 17c6edf3..af429424 100644 --- a/servers/koa-server/src/KoaServer.ts +++ b/servers/koa-server/src/KoaServer.ts @@ -21,9 +21,7 @@ export class KoaServer extends BaseServer { // Bind application container through adapter if (context.applicationAdapter) { - const configs = context.applicationAdapter.bind( - context.diOptions?.iocContainer, - ); + const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); const driver = new KoaDriver(this.framework, this.router); createServer(driver, configs); @@ -41,12 +39,8 @@ export class KoaServer extends BaseServer { this.framework.listen(context.applicationOptions.port, () => { this.logger.info(`=================================`); - this.logger.info( - `======= ENV: ${context.applicationOptions.environment} =======`, - ); - this.logger.info( - `🚀 App listening on the port ${context.applicationOptions.port}`, - ); + this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); + this.logger.info(`🚀 App listening on the port ${context.applicationOptions.port}`); this.logger.info(`=================================`); }); } diff --git a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts index e78c533a..f9208f50 100644 --- a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts +++ b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts @@ -1,8 +1,4 @@ -import { - ActuatorAdapter, - ActuatorOptions, - ApplicationContext, -} from "@node-boot/context"; +import {ActuatorAdapter, ActuatorOptions, ApplicationContext} from "@node-boot/context"; import Prometheus from "prom-client"; import {ExpressActuatorAdapter} from "./ExpressActuatorAdapter"; import {InfoService} from "../service/InfoService"; @@ -55,8 +51,7 @@ export class DefaultActuatorAdapter implements ActuatorAdapter { const context = this.setupMetrics(options); const metadataService = new MetadataService(); - const configService = - ApplicationContext.get().diOptions?.iocContainer.get(ConfigService); + const configService = ApplicationContext.get().diOptions?.iocContainer.get(ConfigService); let frameworkAdapter: ActuatorAdapter; switch (options.serverType) { diff --git a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts index eab5c62f..66ed3000 100644 --- a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts +++ b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts @@ -13,18 +13,13 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { private readonly configService?: ConfigService, ) {} - bind( - options: ActuatorOptions, - server: express.Application, - router: express.Application, - ): void { + bind(options: ActuatorOptions, server: express.Application, router: express.Application): void { router.use((req, res, next) => { // Start a timer for every request made res.locals.startEpoch = Date.now(); res.once("finish", () => { - const responseTimeInMilliseconds = - Date.now() - res.locals.startEpoch; + const responseTimeInMilliseconds = Date.now() - res.locals.startEpoch; this.context.http_request_duration_milliseconds .labels(req.method, req.path, res.statusCode) @@ -55,22 +50,16 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { }); router.get("/actuator/memory", (req, res) => { - this.infoService - .getMemory() - .then(data => res.status(200).json(data)); + this.infoService.getMemory().then(data => res.status(200).json(data)); }); router.get("/actuator/metrics", (req, res) => { - this.context.register - .getMetricsAsJSON() - .then(data => res.status(200).json(data)); + this.context.register.getMetricsAsJSON().then(data => res.status(200).json(data)); }); router.get("/actuator/prometheus", (req, res) => { res.setHeader("Content-Type", this.context.register.contentType); - this.context.register - .metrics() - .then(data => res.status(200).send(data)); + this.context.register.metrics().then(data => res.status(200).send(data)); }); router.get("/actuator/controllers", (req, res) => { diff --git a/starters/actuator/src/adapter/FastifyActuatorAdapter.ts b/starters/actuator/src/adapter/FastifyActuatorAdapter.ts index 9ea6dc86..bad5d903 100644 --- a/starters/actuator/src/adapter/FastifyActuatorAdapter.ts +++ b/starters/actuator/src/adapter/FastifyActuatorAdapter.ts @@ -13,11 +13,7 @@ export class FastifyActuatorAdapter implements ActuatorAdapter { private readonly configService?: ConfigService, ) {} - bind( - options: ActuatorOptions, - server: FastifyInstance, - router: FastifyInstance, - ): void { + bind(options: ActuatorOptions, server: FastifyInstance, router: FastifyInstance): void { router.addHook("onRequest", (request, reply, done) => { // Start a timer for every request made request.log.info({event: "onRequest"}, "Request received"); @@ -27,16 +23,11 @@ export class FastifyActuatorAdapter implements ActuatorAdapter { router.addHook("onSend", (request, reply, payload, done) => { // Retrieve data from the request context - const responseTimeInMilliseconds = - Date.now() - request["locals"].startEpoch; + const responseTimeInMilliseconds = Date.now() - request["locals"].startEpoch; // Observe response time this.context.http_request_duration_milliseconds - .labels( - request.method, - request.url, - reply.statusCode.toString(), - ) + .labels(request.method, request.url, reply.statusCode.toString()) .observe(responseTimeInMilliseconds); done(null, payload); diff --git a/starters/actuator/src/adapter/KoaActuatorAdapter.ts b/starters/actuator/src/adapter/KoaActuatorAdapter.ts index 1008f437..45d4ffb5 100644 --- a/starters/actuator/src/adapter/KoaActuatorAdapter.ts +++ b/starters/actuator/src/adapter/KoaActuatorAdapter.ts @@ -22,8 +22,7 @@ export class KoaActuatorAdapter implements ActuatorAdapter { await next(); - const responseTimeInMilliseconds = - Date.now() - ctx.state["startEpoch"]; + const responseTimeInMilliseconds = Date.now() - ctx.state["startEpoch"]; this.context.http_request_duration_milliseconds .labels(ctx.method, ctx.path, ctx.status.toString()) diff --git a/starters/actuator/src/service/MetadataService.ts b/starters/actuator/src/service/MetadataService.ts index 393c17ae..9ffc2be9 100644 --- a/starters/actuator/src/service/MetadataService.ts +++ b/starters/actuator/src/service/MetadataService.ts @@ -1,13 +1,8 @@ import {getMetadataArgsStorage, MetadataArgsStorage} from "routing-controllers"; -import { - CONTROLLER_PATH_METADATA_KEY, - CONTROLLER_VERSION_METADATA_KEY, -} from "@node-boot/context"; +import {CONTROLLER_PATH_METADATA_KEY, CONTROLLER_VERSION_METADATA_KEY} from "@node-boot/context"; export class MetadataService { - constructor( - private readonly metadataStorage: MetadataArgsStorage = getMetadataArgsStorage(), - ) {} + constructor(private readonly metadataStorage: MetadataArgsStorage = getMetadataArgsStorage()) {} getControllers() { return this.metadataStorage.controllers.map(controller => { diff --git a/starters/persistence/src/PersistenceContext.ts b/starters/persistence/src/PersistenceContext.ts index 671d93f5..aaa47816 100644 --- a/starters/persistence/src/PersistenceContext.ts +++ b/starters/persistence/src/PersistenceContext.ts @@ -8,8 +8,7 @@ export class PersistenceContext { repositories: RepositoryMetadata[] = []; migrations: (new (...args: any[]) => MigrationInterface)[] = []; - eventSubscribers: (new (...args: any[]) => EntitySubscriberInterface)[] = - []; + eventSubscribers: (new (...args: any[]) => EntitySubscriberInterface)[] = []; namingStrategy?: new (...args: any[]) => NamingStrategyInterface; queryCache?: new (...args: any[]) => QueryResultCache; diff --git a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts index af32e9f6..f42bf6fe 100644 --- a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts +++ b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts @@ -18,15 +18,12 @@ export class DefaultRepositoriesAdapter implements RepositoriesAdapter { ); logger.info( - `Registering an '${type.toString()}' repository '${ - target.name - }' for entity '${entity.name}'`, + `Registering an '${type.toString()}' repository '${target.name}' for entity '${ + entity.name + }'`, ); // Set repository to entity manager cache - (entityManager as any).repositories.set( - target, - entityRepositoryInstance, - ); + (entityManager as any).repositories.set(target, entityRepositoryInstance); // set it to the DI container iocContainer.set(target, entityRepositoryInstance); } diff --git a/starters/persistence/src/config/DataSourceConfiguration.ts b/starters/persistence/src/config/DataSourceConfiguration.ts index e0ab2b2d..90f020f4 100644 --- a/starters/persistence/src/config/DataSourceConfiguration.ts +++ b/starters/persistence/src/config/DataSourceConfiguration.ts @@ -10,14 +10,8 @@ import {QUERY_CACHE_CONFIG} from "./QueryCacheConfiguration"; @Configuration() export class DataSourceConfiguration { @Bean("datasource-config") - public dataSourceConfig({ - config, - iocContainer, - logger, - }: BeansContext): DataSourceOptions { - const persistenceProperties = config.get( - PERSISTENCE_CONFIG_PATH, - ); + public dataSourceConfig({config, iocContainer, logger}: BeansContext): DataSourceOptions { + const persistenceProperties = config.get(PERSISTENCE_CONFIG_PATH); if (!persistenceProperties) { throw new Error(`'${PERSISTENCE_CONFIG_PATH}' configuration node is required when persistence is enabled. Please add the persistence configuration depending on the data source you are using or remove @@ -40,8 +34,7 @@ export class DataSourceConfiguration { ); } - const databaseConfigs = - persistenceProperties[persistenceProperties.type]; + const databaseConfigs = persistenceProperties[persistenceProperties.type]; if (!databaseConfigs) { throw new Error( `Invalid persistence configuration. No database specific configuration found for ${persistenceProperties.type} database under ${PERSISTENCE_CONFIG_PATH}' configuration node.`, diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index 1d880cfa..e3a4fd14 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -20,19 +20,11 @@ import {REQUIRES_FIELD_INJECTION_KEY} from "@node-boot/di"; @Configuration() export class PersistenceConfiguration { @Bean() - public dataSource({ - iocContainer, - logger, - config, - }: BeansContext): DataSource { + public dataSource({iocContainer, logger, config}: BeansContext): DataSource { logger.info("Configuring persistence DataSource"); - const datasourceConfig = iocContainer.get( - "datasource-config", - ) as DataSourceOptions; + const datasourceConfig = iocContainer.get("datasource-config") as DataSourceOptions; - const entities = PersistenceContext.get().repositories.map( - repository => repository.entity, - ); + const entities = PersistenceContext.get().repositories.map(repository => repository.entity); const dataSource = new DataSource({ ...datasourceConfig, entities, @@ -52,9 +44,7 @@ export class PersistenceConfiguration { dataSource .runMigrations() .then(migrations => { - logger.info( - `${migrations.length} migration was successfully executed`, - ); + logger.info(`${migrations.length} migration was successfully executed`); }) .catch(reason => { logger.info(`Migrations failed due to:`, reason); @@ -73,8 +63,7 @@ export class PersistenceConfiguration { subscriber, fieldToInject, ); - subscriber[fieldToInject] = - iocContainer.get(propertyType); + subscriber[fieldToInject] = iocContainer.get(propertyType); } } @@ -82,9 +71,7 @@ export class PersistenceConfiguration { const context = ApplicationContext.get(); if (context.diOptions) { logger.info(`Binding persistence repositories`); - context.repositoriesAdapter?.bind( - context.diOptions.iocContainer, - ); + context.repositoriesAdapter?.bind(context.diOptions.iocContainer); } else { throw new Error( "diOptions with an IOC Container is required for Data Repositories", @@ -92,10 +79,7 @@ export class PersistenceConfiguration { } }) .catch(err => { - logger.error( - "Error during Persistence DataSource initialization", - err, - ); + logger.error("Error during Persistence DataSource initialization", err); process.exit(1); }); logger.info("DataSource bean provided successfully"); diff --git a/starters/persistence/src/config/QueryCacheConfiguration.ts b/starters/persistence/src/config/QueryCacheConfiguration.ts index af3b71b8..ca4152c5 100644 --- a/starters/persistence/src/config/QueryCacheConfiguration.ts +++ b/starters/persistence/src/config/QueryCacheConfiguration.ts @@ -23,26 +23,19 @@ export class QueryCacheConfiguration { public queryCacheConfig({iocContainer, logger, config}: BeansContext) { logger.info("Preparing cache configurations"); - const persistenceProperties = config.getOptionalConfig( - PERSISTENCE_CONFIG_PATH, - ); + const persistenceProperties = config.getOptionalConfig(PERSISTENCE_CONFIG_PATH); if (persistenceProperties) { // Cache config can be a boolean or a complex config object - const cacheConfig = - persistenceProperties.getOptional( - "cache", - ); - const cacheEnabled = - persistenceProperties.getOptionalBoolean("cache"); + const cacheConfig = persistenceProperties.getOptional("cache"); + const cacheEnabled = persistenceProperties.getOptionalBoolean("cache"); if (cacheConfig || cacheEnabled !== undefined) { let cacheProvider: any; // Setup cache provider if a custom provider is configured through @PersistenceCache decorator const QueryCache = PersistenceContext.get().queryCache; if (QueryCache) { - cacheProvider = (connection: DataSource) => - new QueryCache(connection); + cacheProvider = (connection: DataSource) => new QueryCache(connection); } if (cacheConfig) { @@ -56,9 +49,7 @@ export class QueryCacheConfiguration { provider: cacheProvider, }); } else if (cacheProvider) { - logger.info( - `Configuring query cache with custom cache provider`, - ); + logger.info(`Configuring query cache with custom cache provider`); iocContainer.set(QUERY_CACHE_CONFIG, { provider: cacheProvider, }); diff --git a/starters/persistence/src/decorator/EnableRepositories.ts b/starters/persistence/src/decorator/EnableRepositories.ts index ee16d789..fabc96a2 100644 --- a/starters/persistence/src/decorator/EnableRepositories.ts +++ b/starters/persistence/src/decorator/EnableRepositories.ts @@ -7,8 +7,7 @@ import {QueryCacheConfiguration} from "../config/QueryCacheConfiguration"; export const EnableRepositories = (): ClassDecorator => { return (target: Function) => { // Register repositories adapter - ApplicationContext.get().repositoriesAdapter = - new DefaultRepositoriesAdapter(); + ApplicationContext.get().repositoriesAdapter = new DefaultRepositoriesAdapter(); // Resolve query cache configurations new QueryCacheConfiguration(); diff --git a/starters/persistence/src/decorator/Migration.ts b/starters/persistence/src/decorator/Migration.ts index 238ea1f1..51e8d665 100644 --- a/starters/persistence/src/decorator/Migration.ts +++ b/starters/persistence/src/decorator/Migration.ts @@ -1,9 +1,7 @@ import {PersistenceContext} from "../PersistenceContext"; import {MigrationInterface} from "typeorm"; -export function Migration< - T extends new (...args: any[]) => MigrationInterface, ->() { +export function Migration MigrationInterface>() { return (target: T) => { PersistenceContext.get().migrations.push(target); }; diff --git a/starters/persistence/src/decorator/PersistenceCache.ts b/starters/persistence/src/decorator/PersistenceCache.ts index 9b13dc81..ec96c746 100644 --- a/starters/persistence/src/decorator/PersistenceCache.ts +++ b/starters/persistence/src/decorator/PersistenceCache.ts @@ -2,9 +2,7 @@ import {PersistenceContext} from "../PersistenceContext"; import {QueryResultCache} from "typeorm/cache/QueryResultCache"; import {decorateDi} from "@node-boot/di"; -export function PersistenceCache< - T extends new (...args: any[]) => QueryResultCache, ->() { +export function PersistenceCache QueryResultCache>() { return (target: T) => { // Inject dependencies if DI container is configured decorateDi(target); diff --git a/starters/persistence/src/decorator/Transactional.ts b/starters/persistence/src/decorator/Transactional.ts index 63e079fa..bf1c062c 100644 --- a/starters/persistence/src/decorator/Transactional.ts +++ b/starters/persistence/src/decorator/Transactional.ts @@ -1,7 +1,4 @@ -import { - Transactional as InnerTransactional, - WrapInTransactionOptions, -} from "typeorm-transactional"; +import {Transactional as InnerTransactional, WrapInTransactionOptions} from "typeorm-transactional"; /** * The Transactional function is a decorator that can be applied to methods in TypeScript classes. It wraps the decorated @@ -25,9 +22,7 @@ import { * isolationLevel: The isolation level of the transaction. * name: The name or symbol of the method being decorated. * */ -export const Transactional = ( - options?: WrapInTransactionOptions, -): MethodDecorator => { +export const Transactional = (options?: WrapInTransactionOptions): MethodDecorator => { return ( target: Object, propertyKey: string | symbol, diff --git a/starters/persistence/src/hook/hooks.ts b/starters/persistence/src/hook/hooks.ts index b8a96d7c..e2873829 100644 --- a/starters/persistence/src/hook/hooks.ts +++ b/starters/persistence/src/hook/hooks.ts @@ -14,15 +14,11 @@ export const runOnTransactionRollback = (cb: (e: Error) => void) => { return innerRunOnTransactionRollback(cb); }; -export const runOnTransactionComplete = ( - cb: (e: Error | undefined) => void, -) => { +export const runOnTransactionComplete = (cb: (e: Error | undefined) => void) => { return innerRunOnTransactionComplete(cb); }; -export const runInTransaction = < - Func extends (this: unknown) => ReturnType, ->( +export const runInTransaction = ReturnType>( fn: Func, options?: WrapInTransactionOptions, ) => { diff --git a/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts index 1f459555..e9ec4537 100644 --- a/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts +++ b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts @@ -5,8 +5,7 @@ import {AuroraMysqlConnectionCredentialsOptions} from "typeorm/driver/aurora-mys * * @see https://github.com/mysqljs/mysql#connection-options */ -export interface AuroraMysqlConnectionProperties - extends AuroraMysqlConnectionCredentialsOptions { +export interface AuroraMysqlConnectionProperties extends AuroraMysqlConnectionCredentialsOptions { readonly region: string; readonly secretArn: string; diff --git a/starters/persistence/src/property/CockroachConnectionProperties.ts b/starters/persistence/src/property/CockroachConnectionProperties.ts index 65f932b3..b34f3ec3 100644 --- a/starters/persistence/src/property/CockroachConnectionProperties.ts +++ b/starters/persistence/src/property/CockroachConnectionProperties.ts @@ -3,8 +3,7 @@ import {CockroachConnectionCredentialsOptions} from "typeorm/driver/cockroachdb/ /** * Cockroachdb-specific connection options. */ -export interface CockroachConnectionProperties - extends CockroachConnectionCredentialsOptions { +export interface CockroachConnectionProperties extends CockroachConnectionCredentialsOptions { /** * Enable time travel queries on cockroachdb. * https://www.cockroachlabs.com/docs/stable/as-of-system-time.html diff --git a/starters/persistence/src/property/MysqlConnectionProperties.ts b/starters/persistence/src/property/MysqlConnectionProperties.ts index 208a2131..169ce402 100644 --- a/starters/persistence/src/property/MysqlConnectionProperties.ts +++ b/starters/persistence/src/property/MysqlConnectionProperties.ts @@ -5,8 +5,7 @@ import {MysqlConnectionCredentialsOptions} from "typeorm/driver/mysql/MysqlConne * * @see https://github.com/mysqljs/mysql#connection-options */ -export interface MysqlConnectionProperties - extends MysqlConnectionCredentialsOptions { +export interface MysqlConnectionProperties extends MysqlConnectionCredentialsOptions { /** * The driver object * This defaults to require("mysql"). diff --git a/starters/persistence/src/property/OracleConnectionProperties.ts b/starters/persistence/src/property/OracleConnectionProperties.ts index 3097b798..f6f0690c 100644 --- a/starters/persistence/src/property/OracleConnectionProperties.ts +++ b/starters/persistence/src/property/OracleConnectionProperties.ts @@ -3,8 +3,7 @@ import {OracleConnectionCredentialsOptions} from "typeorm/driver/oracle/OracleCo /** * Oracle-specific connection options. */ -export interface OracleConnectionProperties - extends OracleConnectionCredentialsOptions { +export interface OracleConnectionProperties extends OracleConnectionCredentialsOptions { /** * Schema name. By default is "public". */ diff --git a/starters/persistence/src/property/PostgresConnectionProperties.ts b/starters/persistence/src/property/PostgresConnectionProperties.ts index 0eaabd00..7b0273dd 100644 --- a/starters/persistence/src/property/PostgresConnectionProperties.ts +++ b/starters/persistence/src/property/PostgresConnectionProperties.ts @@ -3,8 +3,7 @@ import {PostgresConnectionCredentialsOptions} from "typeorm/driver/postgres/Post /** * Postgres-specific connection options. */ -export interface PostgresConnectionProperties - extends PostgresConnectionCredentialsOptions { +export interface PostgresConnectionProperties extends PostgresConnectionCredentialsOptions { /** * Schema name. */ diff --git a/starters/persistence/src/property/SapConnectionProperties.ts b/starters/persistence/src/property/SapConnectionProperties.ts index cc92453a..ce2d172c 100644 --- a/starters/persistence/src/property/SapConnectionProperties.ts +++ b/starters/persistence/src/property/SapConnectionProperties.ts @@ -3,8 +3,7 @@ import {SapConnectionCredentialsOptions} from "typeorm/driver/sap/SapConnectionC /** * SAP Hana specific connection options. */ -export interface SapConnectionProperties - extends SapConnectionCredentialsOptions { +export interface SapConnectionProperties extends SapConnectionCredentialsOptions { /** * Database schema. */ diff --git a/starters/persistence/src/property/SpannerConnectionProperties.ts b/starters/persistence/src/property/SpannerConnectionProperties.ts index a311fa60..d88f5911 100644 --- a/starters/persistence/src/property/SpannerConnectionProperties.ts +++ b/starters/persistence/src/property/SpannerConnectionProperties.ts @@ -3,8 +3,7 @@ import {SpannerConnectionCredentialsOptions} from "typeorm/driver/spanner/Spanne /** * Spanner specific connection options. */ -export interface SpannerConnectionProperties - extends SpannerConnectionCredentialsOptions { +export interface SpannerConnectionProperties extends SpannerConnectionCredentialsOptions { /** * Database type. */ diff --git a/starters/persistence/src/property/SqlServerConnectionProperties.ts b/starters/persistence/src/property/SqlServerConnectionProperties.ts index 7626dffd..720f8de0 100644 --- a/starters/persistence/src/property/SqlServerConnectionProperties.ts +++ b/starters/persistence/src/property/SqlServerConnectionProperties.ts @@ -3,8 +3,7 @@ import {SqlServerConnectionCredentialsOptions} from "typeorm/driver/sqlserver/Sq /** * Microsoft Sql Server specific connection options. */ -export interface SqlServerConnectionProperties - extends SqlServerConnectionCredentialsOptions { +export interface SqlServerConnectionProperties extends SqlServerConnectionCredentialsOptions { /** * Database type. */ From ef935acfc826461fd94894f8409c313817c8f881 Mon Sep 17 00:00:00 2001 From: manusant Date: Tue, 5 Dec 2023 19:19:46 +0000 Subject: [PATCH 04/16] add some formatting to avoid excessive line brakes --- starters/persistence/src/config/PersistenceConfiguration.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index e3a4fd14..311f6fd3 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -25,6 +25,7 @@ export class PersistenceConfiguration { const datasourceConfig = iocContainer.get("datasource-config") as DataSourceOptions; const entities = PersistenceContext.get().repositories.map(repository => repository.entity); + const dataSource = new DataSource({ ...datasourceConfig, entities, From 962372ca818ada014dd58a0754bf375aaf8bb200 Mon Sep 17 00:00:00 2001 From: manusant Date: Wed, 6 Dec 2023 11:15:58 +0000 Subject: [PATCH 05/16] add some doc --- .../src/persistence/listeners/UserEntityEventListener.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts index de6e861d..434e3488 100644 --- a/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts +++ b/samples/sample-express/src/persistence/listeners/UserEntityEventListener.ts @@ -5,6 +5,12 @@ import {Inject} from "@node-boot/di"; import {Logger} from "winston"; import {GreetingService} from "../../services/greeting.service"; +/** + * The UserEntityEventListener class is an event subscriber that listens to events related to the User entity. + * It is responsible for logging information before and after a user is inserted, and also for invoking the sayHello + * method of the GreetingService class. + * + * */ @EntityEventSubscriber() export class UserEntityEventListener implements EntitySubscriberInterface { @Inject() From 9550ef7f08e3add2b356c8d97b8fda6c9c5b066c Mon Sep 17 00:00:00 2001 From: manusant Date: Thu, 7 Dec 2023 00:30:29 +0000 Subject: [PATCH 06/16] Add @DatasourceConfiguration decorator, several improvements. Datasource consistency validation. Fine tunning migrations vs database sync --- samples/sample-express/app-config.yaml | 2 +- samples/sample-express/src/app.ts | 2 - .../DatasourceOverridesConfiguration.ts | 9 ++ .../src/persistence/entities/User.ts | 2 +- .../sample-express/src/persistence/index.ts | 1 + samples/sample-fastify/app-config.yaml | 3 +- samples/sample-koa/app-config.yaml | 3 +- .../persistence/src/PersistenceContext.ts | 4 + .../src/config/DataSourceConfiguration.ts | 51 +++++-- .../src/config/PersistenceConfiguration.ts | 142 +++++++++++++----- .../src/decorator/DatasourceConfiguration.ts | 8 + starters/persistence/src/decorator/index.ts | 1 + starters/persistence/src/metadata/index.ts | 1 + .../AuroraMysqlConnectionProperties.ts | 5 - .../AuroraPostgresConnectionProperties.ts | 16 -- .../BetterSqlite3ConnectionProperties.ts | 12 -- .../property/CockroachConnectionProperties.ts | 18 --- .../src/property/MongoConnectionProperties.ts | 51 +------ .../src/property/MysqlConnectionProperties.ts | 7 - .../src/property/NodeBootDataSourceOptions.ts | 34 +++++ .../property/OracleConnectionProperties.ts | 6 - .../property/PostgresConnectionProperties.ts | 18 --- .../src/property/SapConnectionProperties.ts | 18 --- .../property/SpannerConnectionProperties.ts | 6 - .../property/SqlServerConnectionProperties.ts | 17 --- .../property/SqliteConnectionProperties.ts | 11 -- 26 files changed, 211 insertions(+), 237 deletions(-) create mode 100644 samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts create mode 100644 starters/persistence/src/decorator/DatasourceConfiguration.ts create mode 100644 starters/persistence/src/metadata/index.ts create mode 100644 starters/persistence/src/property/NodeBootDataSourceOptions.ts diff --git a/samples/sample-express/app-config.yaml b/samples/sample-express/app-config.yaml index 138de7f7..2847ceaa 100644 --- a/samples/sample-express/app-config.yaml +++ b/samples/sample-express/app-config.yaml @@ -57,6 +57,6 @@ node-boot: type: "better-sqlite3" synchronize: false # False, meaning that the application rely on migrations cache: true - runMigrations: true + migrationsRun: true better-sqlite3: database: "express-sample.db" diff --git a/samples/sample-express/src/app.ts b/samples/sample-express/src/app.ts index c9d1ff87..97656990 100644 --- a/samples/sample-express/src/app.ts +++ b/samples/sample-express/src/app.ts @@ -50,8 +50,6 @@ export class FactsServiceApp { }) .catch(error => { console.error("Error starting Node-Boot application.", error); - // Terminate the process with a non-zero exit code (1). - process.exit(1); }); } } diff --git a/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts new file mode 100644 index 00000000..4ebbd714 --- /dev/null +++ b/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts @@ -0,0 +1,9 @@ +import {DatasourceConfiguration} from "@node-boot/starter-persistence"; + +@DatasourceConfiguration({ + type: "better-sqlite3", + database: "express-sample.db", + synchronize: true, + migrationsRun: true, +}) +export class DatasourceOverridesConfiguration {} diff --git a/samples/sample-express/src/persistence/entities/User.ts b/samples/sample-express/src/persistence/entities/User.ts index 82271d6f..be938b7f 100644 --- a/samples/sample-express/src/persistence/entities/User.ts +++ b/samples/sample-express/src/persistence/entities/User.ts @@ -11,6 +11,6 @@ export class User { @Column() password: string; - @Column() + @Column({nullable: true}) name?: string; // New field } diff --git a/samples/sample-express/src/persistence/index.ts b/samples/sample-express/src/persistence/index.ts index 8435a3ed..17a8bf58 100644 --- a/samples/sample-express/src/persistence/index.ts +++ b/samples/sample-express/src/persistence/index.ts @@ -3,3 +3,4 @@ export * from "./repositories"; export * from "./migrations"; export * from "./listeners"; export {CustomNamingStrategy} from "./CustomNamingStrategy"; +export {DatasourceOverridesConfiguration} from "./DatasourceOverridesConfiguration"; diff --git a/samples/sample-fastify/app-config.yaml b/samples/sample-fastify/app-config.yaml index 7523493e..5ad325be 100644 --- a/samples/sample-fastify/app-config.yaml +++ b/samples/sample-fastify/app-config.yaml @@ -55,7 +55,8 @@ node-boot: # Storage configurations persistence: type: "better-sqlite3" - synchronize: true # FIXME create tables automatically through migrations instead of synchronize, + synchronize: false # False, meaning that the application rely on migrations cache: true + migrationsRun: true better-sqlite3: database: "fastify-sample.db" diff --git a/samples/sample-koa/app-config.yaml b/samples/sample-koa/app-config.yaml index d818ebe0..32c39765 100644 --- a/samples/sample-koa/app-config.yaml +++ b/samples/sample-koa/app-config.yaml @@ -55,7 +55,8 @@ node-boot: # Storage configurations persistence: type: "better-sqlite3" - synchronize: true # FIXME create tables automatically through migrations instead of synchronize, + synchronize: false # False, meaning that the application rely on migrations cache: true + migrationsRun: true better-sqlite3: database: "koa-sample.db" diff --git a/starters/persistence/src/PersistenceContext.ts b/starters/persistence/src/PersistenceContext.ts index aaa47816..3f397719 100644 --- a/starters/persistence/src/PersistenceContext.ts +++ b/starters/persistence/src/PersistenceContext.ts @@ -2,6 +2,7 @@ import {RepositoryMetadata} from "./metadata/RepositoryMetadata"; import {NamingStrategyInterface} from "typeorm/naming-strategy/NamingStrategyInterface"; import {QueryResultCache} from "typeorm/cache/QueryResultCache"; import {EntitySubscriberInterface, MigrationInterface} from "typeorm"; +import {NodeBootDataSourceOptions} from "./property/NodeBootDataSourceOptions"; export class PersistenceContext { private static context: PersistenceContext; @@ -11,6 +12,9 @@ export class PersistenceContext { eventSubscribers: (new (...args: any[]) => EntitySubscriberInterface)[] = []; namingStrategy?: new (...args: any[]) => NamingStrategyInterface; queryCache?: new (...args: any[]) => QueryResultCache; + databaseConnectionOverrides?: NodeBootDataSourceOptions; + synchronizeDatabase?: boolean; + migrationsRun?: boolean; static get(): PersistenceContext { if (!PersistenceContext.context) { diff --git a/starters/persistence/src/config/DataSourceConfiguration.ts b/starters/persistence/src/config/DataSourceConfiguration.ts index 90f020f4..651472b3 100644 --- a/starters/persistence/src/config/DataSourceConfiguration.ts +++ b/starters/persistence/src/config/DataSourceConfiguration.ts @@ -19,11 +19,10 @@ export class DataSourceConfiguration { } const persistenceLogger = new PersistenceLogger(logger); - const persistenceContext = PersistenceContext.get(); + const {databaseConnectionOverrides, eventSubscribers, migrations, namingStrategy} = + PersistenceContext.get(); - const namingStrategy = persistenceContext.namingStrategy - ? new persistenceContext.namingStrategy() - : undefined; + const strategy = namingStrategy ? new namingStrategy() : undefined; let cacheConfig; if (iocContainer.has(QUERY_CACHE_CONFIG)) { @@ -34,7 +33,7 @@ export class DataSourceConfiguration { ); } - const databaseConfigs = persistenceProperties[persistenceProperties.type]; + let databaseConfigs = persistenceProperties[persistenceProperties.type]; if (!databaseConfigs) { throw new Error( `Invalid persistence configuration. No database specific configuration found for ${persistenceProperties.type} database under ${PERSISTENCE_CONFIG_PATH}' configuration node.`, @@ -43,20 +42,42 @@ export class DataSourceConfiguration { // Set the type from configurations to the driver/connection type databaseConfigs.type = persistenceProperties.type; - logger.info( - `${persistenceContext.eventSubscribers.length} subscribers found and ready to be registered`, - ); - logger.info( - `${persistenceContext.migrations.length} migrations found and ready to be registered`, - ); + logger.info(`${eventSubscribers.length} subscribers found and ready to be registered`); + logger.info(`${migrations.length} migrations found and ready to be registered`); + + if (databaseConnectionOverrides) { + if (databaseConnectionOverrides.type !== persistenceProperties.type) { + throw new Error(`Database type mismatch between configuration properties (${persistenceProperties.type}) + and @DatasourceConfiguration(...) (${databaseConnectionOverrides.type})`); + } + + databaseConfigs = { + ...databaseConfigs, + ...(databaseConnectionOverrides as any), + }; + } + + if (databaseConfigs.synchronize && databaseConfigs.migrationsRun) { + throw new Error( + `Only one of "synchronize" or "migrationsRun" config property can be enabled. Please set one of them to false`, + ); + } + + // Save the synchronization and migration state + PersistenceContext.get().synchronizeDatabase = databaseConfigs.synchronize; + PersistenceContext.get().migrationsRun = databaseConfigs.migrationsRun; return { - ...(databaseConfigs as DataSourceOptions), - namingStrategy, - subscribers: persistenceContext.eventSubscribers, - migrations: persistenceContext.migrations, + ...databaseConfigs, + namingStrategy: strategy, + subscribers: eventSubscribers, + migrations: migrations, logger: persistenceLogger, cache: cacheConfig, + // IMPORTANT: Disable synchronization and migrations run during datasource initialization. If enabled it will be synced afterward + // If enabled here it will cause injection issues + synchronize: false, + migrationsRun: false, }; } } diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index 311f6fd3..cf082003 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -1,10 +1,10 @@ import {Bean, Configuration} from "@node-boot/core"; import {DataSource, EntityManager} from "typeorm"; -import {ApplicationContext, BeansContext} from "@node-boot/context"; +import {ApplicationContext, BeansContext, IocContainer} from "@node-boot/context"; import {DataSourceOptions} from "typeorm/data-source/DataSourceOptions"; import {PersistenceContext} from "../PersistenceContext"; -import {PERSISTENCE_CONFIG_PATH} from "../types"; import {REQUIRES_FIELD_INJECTION_KEY} from "@node-boot/di"; +import {Logger} from "winston"; /** * The PersistenceConfiguration class is responsible for configuring the persistence layer of the application. @@ -36,51 +36,34 @@ export class PersistenceConfiguration { .then(() => { logger.info("Persistence DataSource successfully initialized"); + const {synchronizeDatabase, migrationsRun} = PersistenceContext.get(); + // Inject dependencies into Subscriber instances + PersistenceConfiguration.setupInjection(logger, dataSource, iocContainer); + + const initializationPromises: Promise[] = []; // Run migrations if enabled - const runMigrations = config.getOptionalBoolean( - `${PERSISTENCE_CONFIG_PATH}.runMigrations`, - ); - if (runMigrations) { - logger.info("Running migrations"); - dataSource - .runMigrations() - .then(migrations => { - logger.info(`${migrations.length} migration was successfully executed`); - }) - .catch(reason => { - logger.info(`Migrations failed due to:`, reason); - }); + if (migrationsRun) { + initializationPromises.push( + PersistenceConfiguration.runMigration(logger, dataSource), + ); } - // Inject dependencies into Subscriber instances - for (const subscriber of dataSource.subscribers) { - for (const fieldToInject of Reflect.getMetadata( - REQUIRES_FIELD_INJECTION_KEY, - subscriber, - ) || []) { - // Extract type metadata for field injection. This is useful for custom injection in some modules - const propertyType = Reflect.getMetadata( - "design:type", - subscriber, - fieldToInject, - ); - subscriber[fieldToInject] = iocContainer.get(propertyType); - } + if (synchronizeDatabase) { + initializationPromises.push( + PersistenceConfiguration.runDatabaseSync(logger, dataSource), + ); } // Bind Data Repositories if DI container is configured - const context = ApplicationContext.get(); - if (context.diOptions) { - logger.info(`Binding persistence repositories`); - context.repositoriesAdapter?.bind(context.diOptions.iocContainer); - } else { - throw new Error( - "diOptions with an IOC Container is required for Data Repositories", - ); - } + PersistenceConfiguration.bindDataRepositories(logger); + + // Validate database consistency + Promise.all(initializationPromises).then(_ => + PersistenceConfiguration.ensureDatabase(logger, dataSource), + ); }) .catch(err => { - logger.error("Error during Persistence DataSource initialization", err); + logger.error("Error during Persistence DataSource initialization:", err); process.exit(1); }); logger.info("DataSource bean provided successfully"); @@ -94,4 +77,85 @@ export class PersistenceConfiguration { logger.info("EntityManager bean provided successfully"); return dataSource.manager; } + + static setupInjection( + logger: Logger, + dataSource: DataSource, + iocContainer: IocContainer, + ) { + const subscribers = dataSource.subscribers; + logger.info( + `Setting up dependency injection for ${subscribers.length} persistence event subscribers`, + ); + for (const subscriber of subscribers) { + for (const fieldToInject of Reflect.getMetadata( + REQUIRES_FIELD_INJECTION_KEY, + subscriber, + ) || []) { + // Extract type metadata for field injection. This is useful for custom injection in some modules + const propertyType = Reflect.getMetadata("design:type", subscriber, fieldToInject); + subscriber[fieldToInject] = iocContainer.get(propertyType); + } + } + logger.info(`${subscribers.length} persistence event subscribers successfully injected`); + } + + static async runMigration(logger: Logger, dataSource: DataSource) { + logger.info("Running migrations"); + try { + const migrations = await dataSource.runMigrations(); + logger.info(`${migrations.length} migration was successfully executed`); + } catch (error) { + logger.info(`Migrations failed due to:`, error); + } + } + + static bindDataRepositories(logger: Logger) { + const context = ApplicationContext.get(); + if (context.diOptions) { + logger.info(`Binding persistence repositories`); + context.repositoriesAdapter?.bind(context.diOptions.iocContainer); + } else { + throw new Error("diOptions with an IOC Container is required for Data Repositories"); + } + } + + static async runDatabaseSync(logger: Logger, dataSource: DataSource) { + logger.info(`Starting database synchronization`); + try { + await dataSource.synchronize(); + logger.info(`Database synchronization was successful`); + } catch (error) { + logger.error(`Error running database synchronization:`, error); + } + } + + static async ensureDatabase(logger: Logger, dataSource: DataSource) { + const queryRunner = dataSource.createQueryRunner(); + + try { + const tables = await queryRunner.getTables(); + const entities = dataSource.entityMetadatas; + logger.info( + `Database validation: Running consistency validation for ${tables.length}-tables/${entities.length}-entities.`, + ); + + const existingEntities = entities.filter( + entity => tables.find(table => table.name.includes(entity.tableName)) !== undefined, + ); + + if (existingEntities.length !== entities.length) { + logger.error( + `Inconsistent persistence layer: There are ${entities.length} entities registered but ${existingEntities.length} are in the database.`, + ); + logger.warn(`Please enable database sync through "node-boot.persistence.synchronize: true" + or activate migrations through "node-boot.persistence.migrationsRun: true" to properly setup the database. This is important in order to avoid runtime errors in the application`); + process.exit(1); + } + logger.info(`Basic database consistency validation passed`); + } catch (error) { + logger.error(`Error validating database:`, error); + process.exit(1); + } + } } diff --git a/starters/persistence/src/decorator/DatasourceConfiguration.ts b/starters/persistence/src/decorator/DatasourceConfiguration.ts new file mode 100644 index 00000000..800de320 --- /dev/null +++ b/starters/persistence/src/decorator/DatasourceConfiguration.ts @@ -0,0 +1,8 @@ +import {PersistenceContext} from "../PersistenceContext"; +import {NodeBootDataSourceOptions} from "../property/NodeBootDataSourceOptions"; + +export function DatasourceConfiguration(options: NodeBootDataSourceOptions): ClassDecorator { + return (target: Function) => { + PersistenceContext.get().databaseConnectionOverrides = options; + }; +} diff --git a/starters/persistence/src/decorator/index.ts b/starters/persistence/src/decorator/index.ts index 225f463f..d68eb528 100644 --- a/starters/persistence/src/decorator/index.ts +++ b/starters/persistence/src/decorator/index.ts @@ -5,3 +5,4 @@ export {PersistenceNamingStrategy} from "./PersistenceNamingStrategy"; export {PersistenceCache} from "./PersistenceCache"; export {Migration} from "./Migration"; export {EntityEventSubscriber} from "./EntityEventSubscriber"; +export {DatasourceConfiguration} from "./DatasourceConfiguration"; diff --git a/starters/persistence/src/metadata/index.ts b/starters/persistence/src/metadata/index.ts new file mode 100644 index 00000000..728bce7d --- /dev/null +++ b/starters/persistence/src/metadata/index.ts @@ -0,0 +1 @@ +export {RepositoryMetadata} from "./RepositoryMetadata"; diff --git a/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts index e9ec4537..fd5c32b3 100644 --- a/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts +++ b/starters/persistence/src/property/AuroraMysqlConnectionProperties.ts @@ -13,11 +13,6 @@ export interface AuroraMysqlConnectionProperties extends AuroraMysqlConnectionCr readonly resourceArn: string; readonly database: string; - - readonly serviceConfigOptions?: {[key: string]: any}; // pass optional AWS.ConfigurationOptions here - - readonly formatOptions?: {[key: string]: any; castParameters: boolean}; - /** * Use spatial functions like GeomFromText and AsText which are removed in MySQL 8. * (Default: true) diff --git a/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts b/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts index e0c6018c..ac8659f7 100644 --- a/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts +++ b/starters/persistence/src/property/AuroraPostgresConnectionProperties.ts @@ -10,12 +10,6 @@ export interface AuroraPostgresConnectionProperties { readonly database: string; - /** - * The driver object - * This defaults to require("typeorm-aurora-data-api-driver") - */ - readonly driver?: any; - /** * The Postgres extension to use to generate UUID columns. Defaults to uuid-ossp. * If pgcrypto is selected, TypeORM will use the gen_random_uuid() function from this extension. @@ -25,15 +19,5 @@ export interface AuroraPostgresConnectionProperties { readonly transformParameters?: boolean; - /* - * Function handling errors thrown by drivers pool. - * Defaults to logging error with `warn` level. - */ - readonly poolErrorHandler?: (err: any) => any; - - readonly serviceConfigOptions?: {[key: string]: any}; - - readonly formatOptions?: {[key: string]: any; castParameters: boolean}; - readonly poolSize?: never; } diff --git a/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts b/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts index cacdca07..e788063c 100644 --- a/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts +++ b/starters/persistence/src/property/BetterSqlite3ConnectionProperties.ts @@ -24,13 +24,6 @@ export interface BetterSqlite3ConnectionProperties { */ readonly statementCacheSize?: number; - /** - * Function to run before a database is used in typeorm. - * You can set pragmas, register plugins or register - * functions or aggregates in this function. - */ - readonly prepareDatabase?: (db: any) => void | Promise; - /** * Open the database connection in readonly mode. * Default: false. @@ -51,11 +44,6 @@ export interface BetterSqlite3ConnectionProperties { */ readonly timeout?: number; - /** - * Provide a function that gets called with every SQL string executed by the database connection. - */ - readonly verbose?: Function; - /** * Relative or absolute path to the native addon (better_sqlite3.node). */ diff --git a/starters/persistence/src/property/CockroachConnectionProperties.ts b/starters/persistence/src/property/CockroachConnectionProperties.ts index b34f3ec3..5d257544 100644 --- a/starters/persistence/src/property/CockroachConnectionProperties.ts +++ b/starters/persistence/src/property/CockroachConnectionProperties.ts @@ -15,18 +15,6 @@ export interface CockroachConnectionProperties extends CockroachConnectionCreden */ readonly schema?: string; - /** - * The driver object - * This defaults to `require("pg")`. - */ - readonly driver?: any; - - /** - * The driver object - * This defaults to `require("pg-native")`. - */ - readonly nativeDriver?: any; - /** * Replication setup. */ @@ -48,12 +36,6 @@ export interface CockroachConnectionProperties extends CockroachConnectionCreden */ readonly applicationName?: string; - /** - * Function handling errors thrown by drivers pool. - * Defaults to logging error with `warn` level. - */ - readonly poolErrorHandler?: (err: any) => any; - /** * Max number of transaction retries in case of 40001 error. */ diff --git a/starters/persistence/src/property/MongoConnectionProperties.ts b/starters/persistence/src/property/MongoConnectionProperties.ts index 8e420210..630bb966 100644 --- a/starters/persistence/src/property/MongoConnectionProperties.ts +++ b/starters/persistence/src/property/MongoConnectionProperties.ts @@ -1,5 +1,3 @@ -import {ReadPreference} from "typeorm"; - /** * MongoDB specific connection options. * Synced with http://mongodb.github.io/node-mongodb-native/3.1/api/MongoClient.html @@ -235,22 +233,12 @@ export interface MongoConnectionProperties { * The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, * ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). */ - readonly readPreference?: ReadPreference | string; - - /** - * A primary key factory object for generation of custom _id keys. - */ - readonly pkFactory?: any; - - /** - * A Promise library class the application wishes to use such as Bluebird, must be ES6 compatible. - */ - readonly promiseLibrary?: any; - - /** - * Specify a read concern for the collection. (only MongoDB 3.2 or higher supported). - */ - readonly readConcern?: any; + readonly readPreference?: + | "primary" + | "primaryPreferred" + | "secondary" + | "secondaryPreferred" + | "nearest"; /** * Specify a maxStalenessSeconds value for secondary reads, minimum is 90 seconds @@ -258,18 +246,10 @@ export interface MongoConnectionProperties { readonly maxStalenessSeconds?: number; /** - * Specify the log level used by the driver logger (error/warn/info/debug). - */ - readonly loggerLevel?: "error" | "warn" | "info" | "debug"; - - // Do not overwrite BaseDataSourceOptions.logger - // readonly logger?: any; - - /** - * Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. You can pass in a boolean or your own checkServerIdentity override function + * Ensure we check server identify during SSL, set to false to disable checking. Only works for Node 0.12.x or higher. * Default: true */ - readonly checkServerIdentity?: boolean | Function; + readonly checkServerIdentity?: boolean; /** * Validate MongoClient passed in options for correctness. Default: false @@ -286,21 +266,11 @@ export interface MongoConnectionProperties { */ readonly authMechanism?: string; - /** - * Type of compression to use: snappy or zlib - */ - readonly compression?: any; - /** * Specify a file sync write concern. Default: false */ readonly fsync?: boolean; - /** - * Read preference tags - */ - readonly readPreferenceTags?: any[]; - /** * The number of retries for a tailable cursor. Default: 5 */ @@ -332,11 +302,6 @@ export interface MongoConnectionProperties { */ readonly useUnifiedTopology?: boolean; - /** - * Automatic Client-Side Field Level Encryption configuration. - */ - readonly autoEncryption?: any; - /** * Enables or disables the ability to retry writes upon encountering transient network errors. */ diff --git a/starters/persistence/src/property/MysqlConnectionProperties.ts b/starters/persistence/src/property/MysqlConnectionProperties.ts index 169ce402..f828cbb6 100644 --- a/starters/persistence/src/property/MysqlConnectionProperties.ts +++ b/starters/persistence/src/property/MysqlConnectionProperties.ts @@ -6,13 +6,6 @@ import {MysqlConnectionCredentialsOptions} from "typeorm/driver/mysql/MysqlConne * @see https://github.com/mysqljs/mysql#connection-options */ export interface MysqlConnectionProperties extends MysqlConnectionCredentialsOptions { - /** - * The driver object - * This defaults to require("mysql"). - * Falls back to require("mysql2") - */ - readonly driver?: any; - /** * The charset for the connection. This is called "collation" in the SQL-level of MySQL (like utf8_general_ci). * If a SQL-level charset is specified (like utf8mb4) then the default collation for that charset is used. diff --git a/starters/persistence/src/property/NodeBootDataSourceOptions.ts b/starters/persistence/src/property/NodeBootDataSourceOptions.ts new file mode 100644 index 00000000..651617ed --- /dev/null +++ b/starters/persistence/src/property/NodeBootDataSourceOptions.ts @@ -0,0 +1,34 @@ +import {MysqlConnectionOptions} from "typeorm/driver/mysql/MysqlConnectionOptions"; +import {PostgresConnectionOptions} from "typeorm/driver/postgres/PostgresConnectionOptions"; +import {CockroachConnectionOptions} from "typeorm/driver/cockroachdb/CockroachConnectionOptions"; +import {SqliteConnectionOptions} from "typeorm/driver/sqlite/SqliteConnectionOptions"; +import {SqlServerConnectionOptions} from "typeorm/driver/sqlserver/SqlServerConnectionOptions"; +import {SapConnectionOptions} from "typeorm/driver/sap/SapConnectionOptions"; +import {OracleConnectionOptions} from "typeorm/driver/oracle/OracleConnectionOptions"; +import {MongoConnectionOptions} from "typeorm/driver/mongodb/MongoConnectionOptions"; +import {AuroraMysqlConnectionOptions} from "typeorm/driver/aurora-mysql/AuroraMysqlConnectionOptions"; +import {AuroraPostgresConnectionOptions} from "typeorm/driver/aurora-postgres/AuroraPostgresConnectionOptions"; +import {BetterSqlite3ConnectionOptions} from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions"; +import {SpannerConnectionOptions} from "typeorm/driver/spanner/SpannerConnectionOptions"; + +type NotOverridable = + | "subscribers" + | "namingStrategy" + | "cache" + | "logger" + | "entities" + | "migrations"; + +export type NodeBootDataSourceOptions = + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit + | Omit; diff --git a/starters/persistence/src/property/OracleConnectionProperties.ts b/starters/persistence/src/property/OracleConnectionProperties.ts index f6f0690c..507b1b17 100644 --- a/starters/persistence/src/property/OracleConnectionProperties.ts +++ b/starters/persistence/src/property/OracleConnectionProperties.ts @@ -9,12 +9,6 @@ export interface OracleConnectionProperties extends OracleConnectionCredentialsO */ readonly schema?: string; - /** - * The driver object - * This defaults to require("oracledb") - */ - readonly driver?: any; - /** * A boolean determining whether to pass time values in UTC or local time. (default: false). */ diff --git a/starters/persistence/src/property/PostgresConnectionProperties.ts b/starters/persistence/src/property/PostgresConnectionProperties.ts index 7b0273dd..f26da90e 100644 --- a/starters/persistence/src/property/PostgresConnectionProperties.ts +++ b/starters/persistence/src/property/PostgresConnectionProperties.ts @@ -9,18 +9,6 @@ export interface PostgresConnectionProperties extends PostgresConnectionCredenti */ readonly schema?: string; - /** - * The driver object - * This defaults to `require("pg")`. - */ - readonly driver?: any; - - /** - * The driver object - * This defaults to `require("pg-native")`. - */ - readonly nativeDriver?: any; - /** * A boolean determining whether to pass time values in UTC or local time. (default: false). */ @@ -54,12 +42,6 @@ export interface PostgresConnectionProperties extends PostgresConnectionCredenti */ readonly uuidExtension?: "pgcrypto" | "uuid-ossp"; - /* - * Function handling errors thrown by drivers pool. - * Defaults to logging error with `warn` level. - */ - readonly poolErrorHandler?: (err: any) => any; - /** * Include notification messages from Postgres server in client logs */ diff --git a/starters/persistence/src/property/SapConnectionProperties.ts b/starters/persistence/src/property/SapConnectionProperties.ts index ce2d172c..a913fb64 100644 --- a/starters/persistence/src/property/SapConnectionProperties.ts +++ b/starters/persistence/src/property/SapConnectionProperties.ts @@ -9,18 +9,6 @@ export interface SapConnectionProperties extends SapConnectionCredentialsOptions */ readonly schema?: string; - /** - * The driver objects - * This defaults to require("hdb-pool") - */ - readonly driver?: any; - - /** - * The driver objects - * This defaults to require("@sap/hana-client") - */ - readonly hanaClientDriver?: any; - /** * Pool options. */ @@ -51,12 +39,6 @@ export interface SapConnectionProperties extends SapConnectionCredentialsOptions * Idle timeout */ readonly idleTimeout?: number; - - /** - * Function handling errors thrown by drivers pool. - * Defaults to logging error with `warn` level. - */ - readonly poolErrorHandler?: (err: any) => any; }; readonly poolSize?: never; diff --git a/starters/persistence/src/property/SpannerConnectionProperties.ts b/starters/persistence/src/property/SpannerConnectionProperties.ts index d88f5911..ca457917 100644 --- a/starters/persistence/src/property/SpannerConnectionProperties.ts +++ b/starters/persistence/src/property/SpannerConnectionProperties.ts @@ -9,12 +9,6 @@ export interface SpannerConnectionProperties extends SpannerConnectionCredential */ readonly type: "spanner"; - /** - * The driver object - * This defaults to require("@google-cloud/spanner"). - */ - readonly driver?: any; - // todo readonly database?: string; diff --git a/starters/persistence/src/property/SqlServerConnectionProperties.ts b/starters/persistence/src/property/SqlServerConnectionProperties.ts index 720f8de0..8fb5d269 100644 --- a/starters/persistence/src/property/SqlServerConnectionProperties.ts +++ b/starters/persistence/src/property/SqlServerConnectionProperties.ts @@ -4,11 +4,6 @@ import {SqlServerConnectionCredentialsOptions} from "typeorm/driver/sqlserver/Sq * Microsoft Sql Server specific connection options. */ export interface SqlServerConnectionProperties extends SqlServerConnectionCredentialsOptions { - /** - * Database type. - */ - readonly type: "mssql"; - /** * Connection timeout in ms (default: 15000). */ @@ -31,12 +26,6 @@ export interface SqlServerConnectionProperties extends SqlServerConnectionCreden */ readonly schema?: string; - /** - * The driver object - * This defaults to `require("mssql")` - */ - readonly driver?: any; - /** * An optional object/dictionary with the any of the properties */ @@ -104,12 +93,6 @@ export interface SqlServerConnectionProperties extends SqlServerConnectionCreden * to idle time. Supercedes softIdleTimeoutMillis Default: 30000 */ readonly idleTimeoutMillis?: number; - - /* - * Function handling errors thrown by drivers pool. - * Defaults to logging error with `warn` level. - */ - readonly errorHandler?: (err: any) => any; }; /** diff --git a/starters/persistence/src/property/SqliteConnectionProperties.ts b/starters/persistence/src/property/SqliteConnectionProperties.ts index e61d7fff..e3838a79 100644 --- a/starters/persistence/src/property/SqliteConnectionProperties.ts +++ b/starters/persistence/src/property/SqliteConnectionProperties.ts @@ -2,22 +2,11 @@ * Sqlite-specific connection options. */ export interface SqliteConnectionProperties { - /** - * Database type. - */ - readonly type: "sqlite"; - /** * Storage type or path to the storage. */ readonly database: string; - /** - * The driver object - * This defaults to require("sqlite3") - */ - readonly driver?: any; - /** * Encryption key for for SQLCipher. */ From 04810423c2e325c24fbfdf958ab491987935b6d8 Mon Sep 17 00:00:00 2001 From: manusant Date: Thu, 7 Dec 2023 00:43:51 +0000 Subject: [PATCH 07/16] Shine on the PersistenceConfiguration --- .../src/config/PersistenceConfiguration.ts | 64 ++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index cf082003..de03374c 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -12,13 +12,26 @@ import {Logger} from "winston"; * * Main functionalities: * * Configuring the DataSource bean for the persistence layer. - * * Initializing the DataSource and running migrations if enabled. - * * Binding data repositories to the DI container. + * * Initializing the DataSource. + * * Run migrations if enabled + * * Run database Sync if enabled + * * Binding data repositories to the DI container + * * Validate persistence layer consistency * * @author manusant (ney.br.santos@gmail.com) * */ @Configuration() export class PersistenceConfiguration { + /** + * The dataSource method in the PersistenceConfiguration class is responsible for configuring and providing the + * DataSource object for the persistence layer of the application. + * + * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. + * @param logger (Logger): An instance of the logger class used for logging messages. + * @param config (Config): An instance of the configuration class used for retrieving configuration values. + * + * @return dataSource (DataSource): The configured and initialized DataSource object for the persistence layer. + * */ @Bean() public dataSource({iocContainer, logger, config}: BeansContext): DataSource { logger.info("Configuring persistence DataSource"); @@ -70,6 +83,15 @@ export class PersistenceConfiguration { return dataSource; } + /** + * The entityManager method in the PersistenceConfiguration class is responsible for providing an instance of the + * EntityManager class, which is used for managing database operations. + * + * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. + * @param logger (Logger): An instance of the logger class used for logging messages. + * + * @return entityManager (EntityManager): The provided instance of the EntityManager class + * */ @Bean() public entityManager({iocContainer, logger}: BeansContext): EntityManager { logger.info("Providing EntityManager"); @@ -78,6 +100,15 @@ export class PersistenceConfiguration { return dataSource.manager; } + /** + * The setupInjection method in the PersistenceConfiguration class is responsible for setting up dependency injection + * for the persistence event subscribers. It retrieves the subscribers from the dataSource object and iterates over + * each subscriber to inject the required dependencies using the IoC container. + * + * @param logger (Logger): An instance of the logger class used for logging messages. + * @param dataSource (DataSource): An instance of the DataSource class representing the database connection. + * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. + * */ static setupInjection( logger: Logger, dataSource: DataSource, @@ -100,6 +131,13 @@ export class PersistenceConfiguration { logger.info(`${subscribers.length} persistence event subscribers successfully injected`); } + /** + * The runMigration method in the PersistenceConfiguration class is responsible for running database migrations + * using the dataSource object. It logs the success or failure of the migration operation. + * + * @param logger (Logger): An instance of the logger class used for logging messages. + * @param dataSource (DataSource): An instance of the DataSource class representing the database connection. + * */ static async runMigration(logger: Logger, dataSource: DataSource) { logger.info("Running migrations"); try { @@ -110,6 +148,13 @@ export class PersistenceConfiguration { } } + /** + * The bindDataRepositories method in the PersistenceConfiguration class is responsible for binding the data + * repositories to the IoC container. It checks if the diOptions property is defined in the ApplicationContext and + * then calls the bind method on the repositoriesAdapter using the IoC container. + * + * @param logger (Logger): An instance of the logger class used for logging messages + * */ static bindDataRepositories(logger: Logger) { const context = ApplicationContext.get(); if (context.diOptions) { @@ -120,6 +165,14 @@ export class PersistenceConfiguration { } } + /** + * The runDatabaseSync method in the PersistenceConfiguration class is responsible for starting the synchronization + * of the database. It calls the synchronize method on the DataSource object to perform the synchronization and logs + * the success or failure of the operation. + * + * @param logger (Logger): An instance of the logger class used for logging messages. + * @param dataSource (DataSource): An instance of the DataSource class representing the database connection. + * */ static async runDatabaseSync(logger: Logger, dataSource: DataSource) { logger.info(`Starting database synchronization`); try { @@ -130,6 +183,13 @@ export class PersistenceConfiguration { } } + /** + * The ensureDatabase method in the PersistenceConfiguration class is responsible for validating the consistency of + * the database by comparing the registered entities with the existing tables in the database. + * + * @param logger (Logger): An instance of the logger class used for logging messages. + * @param dataSource (DataSource): An instance of the DataSource class representing the database connection. + * */ static async ensureDatabase(logger: Logger, dataSource: DataSource) { const queryRunner = dataSource.createQueryRunner(); From e2242d27bf62d3f21d2560a11a423e531f3f9097 Mon Sep 17 00:00:00 2001 From: manusant Date: Thu, 7 Dec 2023 11:16:09 +0000 Subject: [PATCH 08/16] small jsdoc improvement --- .../src/config/PersistenceConfiguration.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index de03374c..a9344e73 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -23,7 +23,7 @@ import {Logger} from "winston"; @Configuration() export class PersistenceConfiguration { /** - * The dataSource method in the PersistenceConfiguration class is responsible for configuring and providing the + * The dataSource method is responsible for configuring and providing the * DataSource object for the persistence layer of the application. * * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. @@ -84,7 +84,7 @@ export class PersistenceConfiguration { } /** - * The entityManager method in the PersistenceConfiguration class is responsible for providing an instance of the + * The entityManager method is responsible for providing an instance of the * EntityManager class, which is used for managing database operations. * * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. @@ -101,7 +101,7 @@ export class PersistenceConfiguration { } /** - * The setupInjection method in the PersistenceConfiguration class is responsible for setting up dependency injection + * The setupInjection method is responsible for setting up dependency injection * for the persistence event subscribers. It retrieves the subscribers from the dataSource object and iterates over * each subscriber to inject the required dependencies using the IoC container. * @@ -132,7 +132,7 @@ export class PersistenceConfiguration { } /** - * The runMigration method in the PersistenceConfiguration class is responsible for running database migrations + * The runMigration method is responsible for running database migrations * using the dataSource object. It logs the success or failure of the migration operation. * * @param logger (Logger): An instance of the logger class used for logging messages. @@ -149,7 +149,7 @@ export class PersistenceConfiguration { } /** - * The bindDataRepositories method in the PersistenceConfiguration class is responsible for binding the data + * The bindDataRepositories method is responsible for binding the data * repositories to the IoC container. It checks if the diOptions property is defined in the ApplicationContext and * then calls the bind method on the repositoriesAdapter using the IoC container. * @@ -166,7 +166,7 @@ export class PersistenceConfiguration { } /** - * The runDatabaseSync method in the PersistenceConfiguration class is responsible for starting the synchronization + * The runDatabaseSync method is responsible for starting the synchronization * of the database. It calls the synchronize method on the DataSource object to perform the synchronization and logs * the success or failure of the operation. * @@ -184,7 +184,7 @@ export class PersistenceConfiguration { } /** - * The ensureDatabase method in the PersistenceConfiguration class is responsible for validating the consistency of + * The ensureDatabase method is responsible for validating the consistency of * the database by comparing the registered entities with the existing tables in the database. * * @param logger (Logger): An instance of the logger class used for logging messages. From b8621726bb7115851d700e15fe676574fdfa6b7d Mon Sep 17 00:00:00 2001 From: manusant Date: Wed, 20 Dec 2023 15:56:50 +0000 Subject: [PATCH 09/16] Moving away from routing-controllers and standardization --- .prettierrc.yaml | 2 +- packages/authorization/package.json | 6 +- .../authorization/src/decorator/Authorized.ts | 16 +- .../src/decorator/CurrentUser.ts | 17 +- .../src/decorator/EnableAuthorization.ts | 29 +- packages/authorization/src/index.ts | 1 - .../src/resolver/AuthorizationResolver.ts | 9 - .../src/resolver/CurrentUserResolver.ts | 8 - packages/authorization/src/resolver/index.ts | 2 - .../src/decorator/ConfigurationProperties.ts | 4 +- .../src/service/ObservableConfigProxy.test.ts | 12 +- packages/config/src/service/config.ts | 4 +- packages/context/package.json | 7 +- packages/context/src/ApplicationContext.ts | 9 +- .../src/adapters/ApplicationAdapter.ts | 4 +- packages/context/src/checkers.ts | 20 + packages/context/src/handlers.ts | 27 ++ packages/context/src/index.ts | 2 + packages/context/src/ioc/IocContainer.ts | 21 - packages/context/src/ioc/container.ts | 51 ++ packages/context/src/ioc/index.ts | 2 +- packages/context/src/ioc/types.ts | 45 +- .../context/src/metadata/ActionMetadata.ts | 255 ++++++++++ .../src/metadata/ControllerMetadata.ts | 83 ++++ .../src/metadata/InterceptorMetadata.ts | 39 ++ .../src/metadata/MiddlewareMetadata.ts | 52 ++ .../context/src/metadata/ParamMetadata.ts | 132 +++++ .../src/metadata/ResponseHandleMetadata.ts | 40 ++ packages/context/src/metadata/UseMetadata.ts | 41 ++ .../src/metadata/args/ActionMetadataArgs.ts | 44 ++ .../metadata/args/ControllerMetadataArgs.ts | 26 + .../metadata/args/ErrorHandlerMetadataArgs.ts | 14 + .../metadata/args/InterceptorMetadataArgs.ts | 19 + .../metadata/args/MiddlewareMetadataArgs.ts | 24 + .../src/metadata/args/ParamMetadataArgs.ts | 74 +++ .../args/ResponseHandleMetadataArgs.ts | 31 ++ .../args/UseInterceptorMetadataArgs.ts | 31 ++ .../src/metadata/args/UseMetadataArgs.ts | 25 + packages/context/src/metadata/args/index.ts | 9 + packages/context/src/metadata/index.ts | 9 + .../src/metadata/options/BodyOptions.ts | 38 ++ .../src/metadata/options/ControllerOptions.ts | 14 + .../src/metadata/options/HandlerOptions.ts | 14 + .../src/metadata/options/ParamOptions.ts | 40 ++ .../src/metadata/options/UploadOptions.ts | 15 + .../context/src/metadata/options/index.ts | 5 + packages/context/src/metadata/types.ts | 0 .../src/options/DependencyInjectionOptions.ts | 3 +- .../src/options/NodeBootEngineOptions.ts | 110 +++++ packages/context/src/options/index.ts | 1 + packages/context/src/types.ts | 118 ++++- packages/core/package.json | 7 +- .../src/adapters/BeansConfigurationAdapter.ts | 18 +- .../src/configuration/LoggerConfiguration.ts | 11 - packages/core/src/decorators/All.ts | 30 ++ packages/core/src/decorators/Body.ts | 23 + packages/core/src/decorators/BodyParam.ts | 23 + packages/core/src/decorators/ContentType.ts | 16 + packages/core/src/decorators/Controller.ts | 13 +- packages/core/src/decorators/CookieParam.ts | 23 + packages/core/src/decorators/CookieParams.ts | 18 + packages/core/src/decorators/Ctx.ts | 18 + packages/core/src/decorators/Delete.ts | 30 ++ .../src/decorators/EnableAutoConfiguration.ts | 44 -- .../src/decorators/EnableComponentScan.ts | 24 +- packages/core/src/decorators/ErrorHandler.ts | 12 +- packages/core/src/decorators/Get.ts | 30 ++ packages/core/src/decorators/Head.ts | 30 ++ packages/core/src/decorators/Header.ts | 17 + packages/core/src/decorators/HeaderParam.ts | 23 + packages/core/src/decorators/HeaderParams.ts | 18 + packages/core/src/decorators/HttpCode.ts | 18 + packages/core/src/decorators/Interceptor.ts | 15 +- .../core/src/decorators/JsonController.ts | 23 + packages/core/src/decorators/Location.ts | 16 + packages/core/src/decorators/Method.ts | 30 ++ packages/core/src/decorators/Middleware.ts | 14 +- .../src/decorators/NodeBootApplication.ts | 13 +- packages/core/src/decorators/OnNull.ts | 28 ++ packages/core/src/decorators/OnUndefined.ts | 28 ++ packages/core/src/decorators/Param.ts | 20 + packages/core/src/decorators/Params.ts | 22 + packages/core/src/decorators/Patch.ts | 30 ++ packages/core/src/decorators/Post.ts | 30 ++ packages/core/src/decorators/Put.ts | 30 ++ packages/core/src/decorators/QueryParam.ts | 24 + packages/core/src/decorators/QueryParams.ts | 23 + packages/core/src/decorators/Redirect.ts | 16 + packages/core/src/decorators/Render.ts | 16 + packages/core/src/decorators/Req.ts | 18 + packages/core/src/decorators/Res.ts | 18 + .../ResponseClassTransformOptions.ts | 16 + packages/core/src/decorators/Session.ts | 21 + packages/core/src/decorators/SessionParam.ts | 22 + packages/core/src/decorators/State.ts | 20 + packages/core/src/decorators/UploadedFile.ts | 21 + packages/core/src/decorators/UploadedFiles.ts | 21 + packages/core/src/decorators/UseAfter.ts | 36 ++ packages/core/src/decorators/UseBefore.ts | 36 ++ .../core/src/decorators/UseInterceptor.ts | 30 ++ packages/core/src/decorators/index.ts | 51 +- packages/core/src/error/index.ts | 3 - packages/core/src/index.ts | 2 - packages/core/src/logger/winston.logger.ts | 14 +- .../FastifyErrorHandlerInterface.ts | 9 - .../FastifyErrorMiddlewareInterface.ts | 16 - .../middlewares/FastifyMiddlewareInterface.ts | 11 - packages/core/src/middlewares/index.ts | 3 - .../core/src/properties/CorsProperties.ts | 8 +- packages/core/src/server/BaseServer.ts | 21 +- .../{core => di}/src/decorators/EnableDI.ts | 8 +- packages/di/src/decorators/Inject.ts | 3 +- packages/di/src/decorators/index.ts | 1 + packages/di/src/ioc/makeDiDecoration.ts | 4 +- .../di/src/ioc/makeInjectionDecoration.ts | 26 +- packages/engine/.eslintignore | 3 + packages/engine/.lintstagedrc.js | 5 + packages/engine/.prettierignore | 4 + packages/engine/LICENSE | 21 + packages/engine/README.md | 27 ++ packages/engine/jest.config.js | 6 + packages/engine/nodemon.json | 7 + packages/engine/package.json | 42 ++ packages/engine/src/core/NodeBootDriver.ts | 197 ++++++++ packages/engine/src/core/NodeBootEngine.ts | 159 ++++++ packages/engine/src/core/NodeBootToolkit.ts | 104 ++++ packages/engine/src/core/index.ts | 3 + packages/engine/src/index.ts | 2 + .../src/metadata/MetadataArgsStorage.ts | 158 ++++++ .../engine/src/metadata/MetadataBuilder.ts | 187 +++++++ .../src/service/ActionParameterHandler.ts | 219 +++++++++ .../engine/src/service/ComponentImporter.ts | 34 ++ packages/engine/src/util/index.ts | 2 + .../engine/src/util/isPromiseLike.ts | 3 + packages/engine/src/util/runInSequence.ts | 20 + packages/engine/tsconfig.build.json | 8 + packages/engine/tsconfig.json | 4 + packages/error/.eslintignore | 3 + packages/error/.lintstagedrc.js | 5 + packages/error/.prettierignore | 4 + packages/error/LICENSE | 21 + packages/error/README.md | 27 ++ packages/error/jest.config.js | 6 + packages/error/nodemon.json | 7 + packages/error/package.json | 31 ++ .../src/error/AccessDeniedError.ts | 7 +- .../AuthorizationCheckerNotDefinedError.ts | 6 +- .../src/error/AuthorizationRequiredError.ts | 7 +- .../CurrentUserCheckerNotDefinedError.ts | 13 + .../src/error/ParamNormalizationError.ts | 14 + .../error/src/error/ParamRequiredError.ts | 58 +++ .../src/error/ParameterParseJsonError.ts | 13 + packages/error/src/error/index.ts | 7 + .../error/src/http-error/BadRequestError.ts | 15 + .../error/src/http-error/ForbiddenError.ts | 15 + packages/error/src/http-error/HttpError.ts | 18 + .../src/http-error/InternalServerError.ts | 15 + .../src/http-error/MethodNotAllowedError.ts | 15 + .../src/http-error/NotAcceptableError.ts | 15 + .../error/src/http-error/NotFoundError.ts | 15 + .../error/src/http-error/UnauthorizedError.ts | 15 + packages/error/src/http-error/index.ts | 8 + packages/error/src/index.ts | 2 + packages/error/tsconfig.build.json | 8 + packages/error/tsconfig.json | 4 + packages/extension/src/ClassFiles.ts | 40 ++ packages/extension/src/Optional.ts | 20 +- packages/extension/src/Param.ts | 54 +++ packages/extension/src/index.ts | 2 + packages/extension/src/types.ts | 15 + .../openapi/src/decorator/EnableOpenApi.ts | 3 +- packages/openapi/src/decorator/OpenAPI.ts | 4 +- pnpm-lock.yaml | 116 +++-- samples/sample-express/package.json | 3 +- samples/sample-express/src/app.ts | 12 +- .../src/auth/DefaultAuthorizationResolver.ts | 17 +- .../src/auth/LoggedInUserResolver.ts | 17 +- .../src/config/ClassTransformConfiguration.ts | 6 +- .../src/controllers/users.controller.ts | 9 +- .../src/exceptions/httpException.ts | 10 - .../src/middlewares/ErrorMiddleware.ts | 27 ++ .../src/middlewares/LoggingMiddleware.ts | 10 +- .../src/middlewares/error.middleware.ts | 26 - .../src/middlewares/validation.middleware.ts | 15 +- .../src/services/users.service.ts | 23 +- samples/sample-fastify/package.json | 6 +- samples/sample-fastify/src/app.ts | 14 +- .../src/auth/DefaultAuthorizationResolver.ts | 21 +- .../src/auth/LoggedInUserResolver.ts | 18 +- .../src/config/AppConfigProperties.ts | 14 + .../src/config/ClassTransformConfiguration.ts | 7 +- .../src/controllers/users.controller.ts | 48 +- .../src/exceptions/httpException.ts | 2 +- .../src/middlewares/CustomErrorHandler.ts | 24 + .../src/middlewares/LoggingMiddleware.ts | 14 +- .../src/middlewares/customErrorHandler.ts | 27 -- .../src/persistence/CustomNamingStrategy.ts | 11 + .../DatasourceOverridesConfiguration.ts | 9 + .../src/persistence/entities/User.ts | 16 + .../src/persistence/entities/index.ts | 1 + .../sample-fastify/src/persistence/index.ts | 6 + .../listeners/GlobalEntityEventListener.ts | 139 ++++++ .../listeners/UserEntityEventListener.ts | 40 ++ .../src/persistence/listeners/index.ts | 2 + .../migrations/1701774002463-migration.ts | 34 ++ .../migrations/1701786331338-migration.ts | 13 + .../src/persistence/migrations/index.ts | 2 + .../repositories/UserRepository.ts | 18 + .../src/persistence/repositories/index.ts | 1 + .../src/services/greeting.service.ts | 12 + .../src/services/users.service.ts | 88 ++-- samples/sample-koa/package.json | 5 +- samples/sample-koa/src/app.ts | 14 +- ...lver.ts => DefaultAuthorizationChecker.ts} | 17 +- .../src/auth/LoggedInUserResolver.ts | 20 +- .../src/config/AppConfigProperties.ts | 14 + .../src/config/ClassTransformConfiguration.ts | 7 +- .../src/controllers/users.controller.ts | 42 +- .../src/exceptions/httpException.ts | 2 +- .../src/interfaces/users.interface.ts | 2 +- .../src/middlewares/CustomErrorHandler.ts | 27 ++ .../src/middlewares/LoggingMiddleware.ts | 11 +- .../src/middlewares/validation.middleware.ts | 11 +- .../src/persistence/CustomNamingStrategy.ts | 11 + .../DatasourceOverridesConfiguration.ts | 9 + .../src/persistence/entities/User.ts | 16 + .../src/persistence/entities/index.ts | 1 + samples/sample-koa/src/persistence/index.ts | 6 + .../listeners/GlobalEntityEventListener.ts | 139 ++++++ .../listeners/UserEntityEventListener.ts | 40 ++ .../src/persistence/listeners/index.ts | 2 + .../migrations/1701774002463-migration.ts | 34 ++ .../migrations/1701786331338-migration.ts | 13 + .../src/persistence/migrations/index.ts | 2 + .../repositories/UserRepository.ts | 18 + .../src/persistence/repositories/index.ts | 1 + .../src/services/greeting.service.ts | 12 + .../sample-koa/src/services/users.service.ts | 88 ++-- servers/express-server/package.json | 10 +- .../src/driver/ExpressDriver.ts | 456 ++++++++++++++++++ servers/express-server/src/driver/index.ts | 1 + servers/express-server/src/index.ts | 3 +- .../src/{ => server}/ExpressServer.ts | 11 +- servers/express-server/src/server/index.ts | 1 + servers/fastify-server/package.json | 3 +- .../src/driver/FastifyDriver.ts | 366 +++++--------- servers/fastify-server/src/driver/index.ts | 1 + servers/fastify-server/src/index.ts | 2 +- .../src/loader/DependenciesLoader.ts | 56 +++ servers/fastify-server/src/loader/index.ts | 1 + .../src/{ => server}/FastifyServer.ts | 33 +- servers/fastify-server/src/server/index.ts | 1 + servers/koa-server/package.json | 7 +- servers/koa-server/src/driver/KoaDriver.ts | 391 +++++++++++++++ servers/koa-server/src/driver/index.ts | 1 + servers/koa-server/src/index.ts | 2 +- .../koa-server/src/{ => server}/KoaServer.ts | 9 +- servers/koa-server/src/server/index.ts | 1 + .../src/adapter/DefaultActuatorAdapter.ts | 29 +- .../src/adapter/ExpressActuatorAdapter.ts | 4 +- .../src/adapter/KoaActuatorAdapter.ts | 4 +- starters/actuator/src/service/InfoService.ts | 4 +- .../actuator/src/service/MetadataService.ts | 14 +- .../src/adapter/DefaultRepositoriesAdapter.ts | 12 +- .../src/adapter/PersistenceLogger.ts | 6 +- .../src/config/DataSourceConfiguration.ts | 11 +- .../src/config/PersistenceConfiguration.ts | 35 +- .../src/config/QueryCacheConfiguration.ts | 21 +- .../src/config/TransactionConfiguration.ts | 6 +- .../src/decorator/EntityEventSubscriber.ts | 4 +- .../decorator/PersistenceNamingStrategy.ts | 4 +- .../src/decorator/Transactional.ts | 6 +- starters/persistence/src/hook/hooks.ts | 5 +- .../src/property/MongoConnectionProperties.ts | 7 +- .../src/property/NodeBootDataSourceOptions.ts | 8 +- .../property/SqlServerConnectionProperties.ts | 14 +- 276 files changed, 6422 insertions(+), 1164 deletions(-) delete mode 100644 packages/authorization/src/resolver/AuthorizationResolver.ts delete mode 100644 packages/authorization/src/resolver/CurrentUserResolver.ts delete mode 100644 packages/authorization/src/resolver/index.ts create mode 100644 packages/context/src/checkers.ts create mode 100644 packages/context/src/handlers.ts delete mode 100644 packages/context/src/ioc/IocContainer.ts create mode 100644 packages/context/src/ioc/container.ts create mode 100644 packages/context/src/metadata/ActionMetadata.ts create mode 100644 packages/context/src/metadata/ControllerMetadata.ts create mode 100644 packages/context/src/metadata/InterceptorMetadata.ts create mode 100644 packages/context/src/metadata/MiddlewareMetadata.ts create mode 100644 packages/context/src/metadata/ParamMetadata.ts create mode 100644 packages/context/src/metadata/ResponseHandleMetadata.ts create mode 100644 packages/context/src/metadata/UseMetadata.ts create mode 100644 packages/context/src/metadata/args/ActionMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/ControllerMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/ErrorHandlerMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/InterceptorMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/MiddlewareMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/ParamMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/ResponseHandleMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/UseInterceptorMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/UseMetadataArgs.ts create mode 100644 packages/context/src/metadata/args/index.ts create mode 100644 packages/context/src/metadata/options/BodyOptions.ts create mode 100644 packages/context/src/metadata/options/ControllerOptions.ts create mode 100644 packages/context/src/metadata/options/HandlerOptions.ts create mode 100644 packages/context/src/metadata/options/ParamOptions.ts create mode 100644 packages/context/src/metadata/options/UploadOptions.ts create mode 100644 packages/context/src/metadata/options/index.ts delete mode 100644 packages/context/src/metadata/types.ts create mode 100644 packages/context/src/options/NodeBootEngineOptions.ts delete mode 100644 packages/core/src/configuration/LoggerConfiguration.ts create mode 100644 packages/core/src/decorators/All.ts create mode 100644 packages/core/src/decorators/Body.ts create mode 100644 packages/core/src/decorators/BodyParam.ts create mode 100644 packages/core/src/decorators/ContentType.ts create mode 100644 packages/core/src/decorators/CookieParam.ts create mode 100644 packages/core/src/decorators/CookieParams.ts create mode 100644 packages/core/src/decorators/Ctx.ts create mode 100644 packages/core/src/decorators/Delete.ts create mode 100644 packages/core/src/decorators/Get.ts create mode 100644 packages/core/src/decorators/Head.ts create mode 100644 packages/core/src/decorators/Header.ts create mode 100644 packages/core/src/decorators/HeaderParam.ts create mode 100644 packages/core/src/decorators/HeaderParams.ts create mode 100644 packages/core/src/decorators/HttpCode.ts create mode 100644 packages/core/src/decorators/JsonController.ts create mode 100644 packages/core/src/decorators/Location.ts create mode 100644 packages/core/src/decorators/Method.ts create mode 100644 packages/core/src/decorators/OnNull.ts create mode 100644 packages/core/src/decorators/OnUndefined.ts create mode 100644 packages/core/src/decorators/Param.ts create mode 100644 packages/core/src/decorators/Params.ts create mode 100644 packages/core/src/decorators/Patch.ts create mode 100644 packages/core/src/decorators/Post.ts create mode 100644 packages/core/src/decorators/Put.ts create mode 100644 packages/core/src/decorators/QueryParam.ts create mode 100644 packages/core/src/decorators/QueryParams.ts create mode 100644 packages/core/src/decorators/Redirect.ts create mode 100644 packages/core/src/decorators/Render.ts create mode 100644 packages/core/src/decorators/Req.ts create mode 100644 packages/core/src/decorators/Res.ts create mode 100644 packages/core/src/decorators/ResponseClassTransformOptions.ts create mode 100644 packages/core/src/decorators/Session.ts create mode 100644 packages/core/src/decorators/SessionParam.ts create mode 100644 packages/core/src/decorators/State.ts create mode 100644 packages/core/src/decorators/UploadedFile.ts create mode 100644 packages/core/src/decorators/UploadedFiles.ts create mode 100644 packages/core/src/decorators/UseAfter.ts create mode 100644 packages/core/src/decorators/UseBefore.ts create mode 100644 packages/core/src/decorators/UseInterceptor.ts delete mode 100644 packages/core/src/error/index.ts delete mode 100644 packages/core/src/middlewares/FastifyErrorHandlerInterface.ts delete mode 100644 packages/core/src/middlewares/FastifyErrorMiddlewareInterface.ts delete mode 100644 packages/core/src/middlewares/FastifyMiddlewareInterface.ts delete mode 100644 packages/core/src/middlewares/index.ts rename packages/{core => di}/src/decorators/EnableDI.ts (55%) create mode 100644 packages/engine/.eslintignore create mode 100644 packages/engine/.lintstagedrc.js create mode 100644 packages/engine/.prettierignore create mode 100644 packages/engine/LICENSE create mode 100644 packages/engine/README.md create mode 100644 packages/engine/jest.config.js create mode 100644 packages/engine/nodemon.json create mode 100644 packages/engine/package.json create mode 100644 packages/engine/src/core/NodeBootDriver.ts create mode 100644 packages/engine/src/core/NodeBootEngine.ts create mode 100644 packages/engine/src/core/NodeBootToolkit.ts create mode 100644 packages/engine/src/core/index.ts create mode 100644 packages/engine/src/index.ts create mode 100644 packages/engine/src/metadata/MetadataArgsStorage.ts create mode 100644 packages/engine/src/metadata/MetadataBuilder.ts create mode 100644 packages/engine/src/service/ActionParameterHandler.ts create mode 100644 packages/engine/src/service/ComponentImporter.ts create mode 100644 packages/engine/src/util/index.ts rename servers/fastify-server/src/utils.ts => packages/engine/src/util/isPromiseLike.ts (71%) create mode 100644 packages/engine/src/util/runInSequence.ts create mode 100644 packages/engine/tsconfig.build.json create mode 100644 packages/engine/tsconfig.json create mode 100644 packages/error/.eslintignore create mode 100644 packages/error/.lintstagedrc.js create mode 100644 packages/error/.prettierignore create mode 100644 packages/error/LICENSE create mode 100644 packages/error/README.md create mode 100644 packages/error/jest.config.js create mode 100644 packages/error/nodemon.json create mode 100644 packages/error/package.json rename packages/{core => error}/src/error/AccessDeniedError.ts (53%) rename packages/{core => error}/src/error/AuthorizationCheckerNotDefinedError.ts (61%) rename packages/{core => error}/src/error/AuthorizationRequiredError.ts (56%) create mode 100644 packages/error/src/error/CurrentUserCheckerNotDefinedError.ts create mode 100644 packages/error/src/error/ParamNormalizationError.ts create mode 100644 packages/error/src/error/ParamRequiredError.ts create mode 100644 packages/error/src/error/ParameterParseJsonError.ts create mode 100644 packages/error/src/error/index.ts create mode 100644 packages/error/src/http-error/BadRequestError.ts create mode 100644 packages/error/src/http-error/ForbiddenError.ts create mode 100644 packages/error/src/http-error/HttpError.ts create mode 100644 packages/error/src/http-error/InternalServerError.ts create mode 100644 packages/error/src/http-error/MethodNotAllowedError.ts create mode 100644 packages/error/src/http-error/NotAcceptableError.ts create mode 100644 packages/error/src/http-error/NotFoundError.ts create mode 100644 packages/error/src/http-error/UnauthorizedError.ts create mode 100644 packages/error/src/http-error/index.ts create mode 100644 packages/error/src/index.ts create mode 100644 packages/error/tsconfig.build.json create mode 100644 packages/error/tsconfig.json create mode 100644 packages/extension/src/ClassFiles.ts create mode 100644 packages/extension/src/Param.ts create mode 100644 packages/extension/src/types.ts delete mode 100644 samples/sample-express/src/exceptions/httpException.ts create mode 100644 samples/sample-express/src/middlewares/ErrorMiddleware.ts delete mode 100644 samples/sample-express/src/middlewares/error.middleware.ts create mode 100644 samples/sample-fastify/src/config/AppConfigProperties.ts create mode 100644 samples/sample-fastify/src/middlewares/CustomErrorHandler.ts delete mode 100644 samples/sample-fastify/src/middlewares/customErrorHandler.ts create mode 100644 samples/sample-fastify/src/persistence/CustomNamingStrategy.ts create mode 100644 samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts create mode 100644 samples/sample-fastify/src/persistence/entities/User.ts create mode 100644 samples/sample-fastify/src/persistence/entities/index.ts create mode 100644 samples/sample-fastify/src/persistence/index.ts create mode 100644 samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts create mode 100644 samples/sample-fastify/src/persistence/listeners/UserEntityEventListener.ts create mode 100644 samples/sample-fastify/src/persistence/listeners/index.ts create mode 100644 samples/sample-fastify/src/persistence/migrations/1701774002463-migration.ts create mode 100644 samples/sample-fastify/src/persistence/migrations/1701786331338-migration.ts create mode 100644 samples/sample-fastify/src/persistence/migrations/index.ts create mode 100644 samples/sample-fastify/src/persistence/repositories/UserRepository.ts create mode 100644 samples/sample-fastify/src/persistence/repositories/index.ts create mode 100644 samples/sample-fastify/src/services/greeting.service.ts rename samples/sample-koa/src/auth/{DefaultAuthorizationResolver.ts => DefaultAuthorizationChecker.ts} (57%) create mode 100644 samples/sample-koa/src/config/AppConfigProperties.ts create mode 100644 samples/sample-koa/src/middlewares/CustomErrorHandler.ts create mode 100644 samples/sample-koa/src/persistence/CustomNamingStrategy.ts create mode 100644 samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts create mode 100644 samples/sample-koa/src/persistence/entities/User.ts create mode 100644 samples/sample-koa/src/persistence/entities/index.ts create mode 100644 samples/sample-koa/src/persistence/index.ts create mode 100644 samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts create mode 100644 samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts create mode 100644 samples/sample-koa/src/persistence/listeners/index.ts create mode 100644 samples/sample-koa/src/persistence/migrations/1701774002463-migration.ts create mode 100644 samples/sample-koa/src/persistence/migrations/1701786331338-migration.ts create mode 100644 samples/sample-koa/src/persistence/migrations/index.ts create mode 100644 samples/sample-koa/src/persistence/repositories/UserRepository.ts create mode 100644 samples/sample-koa/src/persistence/repositories/index.ts create mode 100644 samples/sample-koa/src/services/greeting.service.ts create mode 100644 servers/express-server/src/driver/ExpressDriver.ts create mode 100644 servers/express-server/src/driver/index.ts rename servers/express-server/src/{ => server}/ExpressServer.ts (81%) create mode 100644 servers/express-server/src/server/index.ts create mode 100644 servers/fastify-server/src/driver/index.ts create mode 100644 servers/fastify-server/src/loader/DependenciesLoader.ts create mode 100644 servers/fastify-server/src/loader/index.ts rename servers/fastify-server/src/{ => server}/FastifyServer.ts (68%) create mode 100644 servers/fastify-server/src/server/index.ts create mode 100644 servers/koa-server/src/driver/KoaDriver.ts create mode 100644 servers/koa-server/src/driver/index.ts rename servers/koa-server/src/{ => server}/KoaServer.ts (84%) create mode 100644 servers/koa-server/src/server/index.ts diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 413519dc..8e69d835 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,4 +4,4 @@ singleQuote: false trailingComma: "all" arrowParens: "avoid" bracketSpacing: false -printWidth: 100 +printWidth: 150 diff --git a/packages/authorization/package.json b/packages/authorization/package.json index e639b041..1121cf4c 100644 --- a/packages/authorization/package.json +++ b/packages/authorization/package.json @@ -29,9 +29,7 @@ "typecheck": "tsc" }, "dependencies": { - "@node-boot/context": "1.0.0" - }, - "peerDependencies": { - "routing-controllers": ">=0.10.4" + "@node-boot/context": "1.0.0", + "@node-boot/engine": "1.0.0" } } diff --git a/packages/authorization/src/decorator/Authorized.ts b/packages/authorization/src/decorator/Authorized.ts index cc5a13da..82e678b9 100644 --- a/packages/authorization/src/decorator/Authorized.ts +++ b/packages/authorization/src/decorator/Authorized.ts @@ -1,4 +1,4 @@ -import {Authorized as InnerAuthorized} from "routing-controllers"; +import {NodeBootToolkit} from "@node-boot/engine"; /** * Marks controller action to have a special access. @@ -27,12 +27,14 @@ export function Authorized(role: Function): Function; /** * Marks controller action to have a special access. * Authorization logic must be defined in routing-controllers settings. - * - * @param roleOrRoles Arguments for routing-controllers @Authorized decorator */ -export function Authorized(roleOrRoles?: string | string[] | Function) { - return (clsOrObject: Function | Object, method?: string) => { - // DI is optional and the decorator will only be applied if the DI container dependency is available. - InnerAuthorized(roleOrRoles)(clsOrObject, method); +export function Authorized(roleOrRoles?: string | string[] | Function): Function { + return function (clsOrObject: Function | Object, method?: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "authorized", + target: method ? clsOrObject.constructor : (clsOrObject as Function), + method: method, + value: roleOrRoles, + }); }; } diff --git a/packages/authorization/src/decorator/CurrentUser.ts b/packages/authorization/src/decorator/CurrentUser.ts index 7672e8a1..7f990eb1 100644 --- a/packages/authorization/src/decorator/CurrentUser.ts +++ b/packages/authorization/src/decorator/CurrentUser.ts @@ -1,13 +1,18 @@ -import {CurrentUser as InnerCurrentUser} from "routing-controllers"; +import {NodeBootToolkit} from "@node-boot/engine"; /** * Injects currently authorized user. * Authorization logic must be defined in routing-controllers settings. - * - * @param args Arguments for routing-controllers @CurrentUser decorator */ -export function CurrentUser(...args: Parameters) { - return (target: Object, methodName: string, index: number) => { - InnerCurrentUser(...args)(target, methodName, index); +export function CurrentUser(options?: {required?: boolean}) { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "current-user", + object: object, + method: methodName, + index: index, + parse: false, + required: options?.required ?? false, + }); }; } diff --git a/packages/authorization/src/decorator/EnableAuthorization.ts b/packages/authorization/src/decorator/EnableAuthorization.ts index 3d0f9228..a0d6d868 100644 --- a/packages/authorization/src/decorator/EnableAuthorization.ts +++ b/packages/authorization/src/decorator/EnableAuthorization.ts @@ -1,33 +1,22 @@ -import {ApplicationContext, RequestContext} from "@node-boot/context"; -import {AuthorizationResolver, CurrentUserResolver} from "../resolver"; -import {Action} from "routing-controllers/types/Action"; +import {ApplicationContext, AuthorizationChecker, CurrentUserChecker} from "@node-boot/context"; /** * Enable Authorization features by providing an Authorization and current user resolvers. * - * @param CurrentUserResolverClass class implementing CurrentUserResolver interface - * @param AuthorizationResolverClass class implementing AuthorizationResolver interface + * @param CurrentUserCheckerClass class implementing CurrentUserResolver interface + * @param AuthorizationCheckerClass class implementing AuthorizationResolver interface */ export function EnableAuthorization( - CurrentUserResolverClass?: new () => CurrentUserResolver, - AuthorizationResolverClass?: new () => AuthorizationResolver, + CurrentUserCheckerClass?: new () => CurrentUserChecker, + AuthorizationCheckerClass?: new () => AuthorizationChecker, ): Function { return function (object: Function) { - if (AuthorizationResolverClass) { - const authResolver = new AuthorizationResolverClass(); - ApplicationContext.get().authorizationChecker = async ( - context: RequestContext, - roles: any[], - ) => { - return authResolver.authorize(context, roles); - }; + if (AuthorizationCheckerClass) { + ApplicationContext.get().authorizationChecker = new AuthorizationCheckerClass(); } - if (CurrentUserResolverClass) { - const userResolver = new CurrentUserResolverClass(); - ApplicationContext.get().currentUserChecker = async (action: Action) => { - return userResolver.getCurrentUser(action); - }; + if (CurrentUserCheckerClass) { + ApplicationContext.get().currentUserChecker = new CurrentUserCheckerClass(); } }; } diff --git a/packages/authorization/src/index.ts b/packages/authorization/src/index.ts index b3a3f20e..ad891925 100644 --- a/packages/authorization/src/index.ts +++ b/packages/authorization/src/index.ts @@ -1,2 +1 @@ export * from "./decorator"; -export * from "./resolver"; diff --git a/packages/authorization/src/resolver/AuthorizationResolver.ts b/packages/authorization/src/resolver/AuthorizationResolver.ts deleted file mode 100644 index c1c1eb26..00000000 --- a/packages/authorization/src/resolver/AuthorizationResolver.ts +++ /dev/null @@ -1,9 +0,0 @@ -import {RequestContext} from "@node-boot/context"; - -/** - * Special function used to resolver user authorization roles per request. - * Must return true or promise with boolean true resolved for authorization to succeed. - */ -export interface AuthorizationResolver { - authorize(context: RequestContext, roles: any[]): Promise | boolean; -} diff --git a/packages/authorization/src/resolver/CurrentUserResolver.ts b/packages/authorization/src/resolver/CurrentUserResolver.ts deleted file mode 100644 index d34158b1..00000000 --- a/packages/authorization/src/resolver/CurrentUserResolver.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {RequestContext} from "@node-boot/context"; - -/** - * Special function used to get currently authorized user. - */ -export interface CurrentUserResolver { - getCurrentUser(context: RequestContext): Promise | any; -} diff --git a/packages/authorization/src/resolver/index.ts b/packages/authorization/src/resolver/index.ts deleted file mode 100644 index 7c285932..00000000 --- a/packages/authorization/src/resolver/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export {AuthorizationResolver} from "./AuthorizationResolver"; -export {CurrentUserResolver} from "./CurrentUserResolver"; diff --git a/packages/config/src/decorator/ConfigurationProperties.ts b/packages/config/src/decorator/ConfigurationProperties.ts index 83f33700..a6fa0c72 100644 --- a/packages/config/src/decorator/ConfigurationProperties.ts +++ b/packages/config/src/decorator/ConfigurationProperties.ts @@ -17,9 +17,7 @@ export function ConfigurationProperties(args: ConfigurationPropertiesMetadata): const instance = new target(); for (const propertyName in configProperties) { - if ( - Object.prototype.hasOwnProperty.call(configProperties, propertyName) - ) { + if (Object.prototype.hasOwnProperty.call(configProperties, propertyName)) { instance[propertyName] = configProperties[propertyName]; } } diff --git a/packages/config/src/service/ObservableConfigProxy.test.ts b/packages/config/src/service/ObservableConfigProxy.test.ts index e0bb5f43..9b0d30d4 100644 --- a/packages/config/src/service/ObservableConfigProxy.test.ts +++ b/packages/config/src/service/ObservableConfigProxy.test.ts @@ -77,15 +77,9 @@ describe("ObservableConfigProxy", () => { expect(() => config3.getNumber("x")).toThrow("Missing required config value at 'a'"); config1.setConfig(new ConfigReader({x: "s", a: {x: "s", b: {x: "s"}}})); - expect(() => config1.getNumber("x")).toThrow( - "Unable to convert config value for key 'x' in 'mock-config' to a number", - ); - expect(() => config2.getNumber("x")).toThrow( - "Unable to convert config value for key 'a.x' in 'mock-config' to a number", - ); - expect(() => config3.getNumber("x")).toThrow( - "Unable to convert config value for key 'a.b.x' in 'mock-config' to a number", - ); + expect(() => config1.getNumber("x")).toThrow("Unable to convert config value for key 'x' in 'mock-config' to a number"); + expect(() => config2.getNumber("x")).toThrow("Unable to convert config value for key 'a.x' in 'mock-config' to a number"); + expect(() => config3.getNumber("x")).toThrow("Unable to convert config value for key 'a.b.x' in 'mock-config' to a number"); }); it("should make sub configs available as expected", () => { diff --git a/packages/config/src/service/config.ts b/packages/config/src/service/config.ts index 57f4d400..46b2b6dc 100644 --- a/packages/config/src/service/config.ts +++ b/packages/config/src/service/config.ts @@ -30,9 +30,7 @@ export async function loadNodeBootConfig(options: { }): Promise<{config: ConfigService}> { const args = parseArgs(options.argv); - const configTargets: ConfigTarget[] = [args["config"] ?? []] - .flat() - .map(arg => (isValidUrl(arg) ? {url: arg} : {path: resolvePath(arg)})); + const configTargets: ConfigTarget[] = [args["config"] ?? []].flat().map(arg => (isValidUrl(arg) ? {url: arg} : {path: resolvePath(arg)})); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); diff --git a/packages/context/package.json b/packages/context/package.json index 98794629..95d74aee 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -29,12 +29,11 @@ "typecheck": "tsc" }, "dependencies": { - "reflect-metadata": "^0.1.13", - "class-transformer": "^0.5.1" + "reflect-metadata": "^0.1.13" }, - "devDependencies": {}, "peerDependencies": { - "routing-controllers": ">=0.10.4", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0", "winston": ">=3.10.0" } } diff --git a/packages/context/src/ApplicationContext.ts b/packages/context/src/ApplicationContext.ts index d4d96074..56483afe 100644 --- a/packages/context/src/ApplicationContext.ts +++ b/packages/context/src/ApplicationContext.ts @@ -1,13 +1,8 @@ -import type {CurrentUserChecker} from "routing-controllers/types/CurrentUserChecker"; -import type {AuthorizationChecker} from "routing-controllers/types/AuthorizationChecker"; import type {ClassTransformOptions} from "class-transformer"; import type {ApplicationOptions, DependencyInjectionOptions} from "./options"; -import type { - ApplicationAdapter, - ConfigurationAdapter, - ConfigurationPropertiesAdapter, -} from "./adapters"; +import type {ApplicationAdapter, ConfigurationAdapter, ConfigurationPropertiesAdapter} from "./adapters"; import {ActuatorAdapter, OpenApiBridgeAdapter, RepositoriesAdapter} from "./adapters"; +import {AuthorizationChecker, CurrentUserChecker} from "./checkers"; export class ApplicationContext { private static context: ApplicationContext; diff --git a/packages/context/src/adapters/ApplicationAdapter.ts b/packages/context/src/adapters/ApplicationAdapter.ts index 04f733c3..47300aa1 100644 --- a/packages/context/src/adapters/ApplicationAdapter.ts +++ b/packages/context/src/adapters/ApplicationAdapter.ts @@ -1,7 +1,7 @@ -import type {RoutingControllersOptions} from "routing-controllers/types/RoutingControllersOptions"; import type {NodeBootAdapter} from "./NodeBootAdapter"; import {IocContainer} from "../ioc"; +import {NodeBootEngineOptions} from "../options"; export interface ApplicationAdapter extends NodeBootAdapter { - bind(iocContainer?: IocContainer): RoutingControllersOptions; + bind(iocContainer?: IocContainer): NodeBootEngineOptions; } diff --git a/packages/context/src/checkers.ts b/packages/context/src/checkers.ts new file mode 100644 index 00000000..34470337 --- /dev/null +++ b/packages/context/src/checkers.ts @@ -0,0 +1,20 @@ +import {Action} from "./types"; + +/** + * Special function used to check user authorization roles per request. + * Must return true or promise with boolean true resolved for authorization to succeed. + */ +export interface AuthorizationChecker { + check(action: Action, roles: TRole[]): Promise | boolean; +} + +/** + * Special function used to get currently authorized user. + */ +export interface CurrentUserChecker { + check(action: Action): Promise | any; +} + +export interface RoleChecker { + check(action: Action): boolean | Promise; +} diff --git a/packages/context/src/handlers.ts b/packages/context/src/handlers.ts new file mode 100644 index 00000000..39d4feac --- /dev/null +++ b/packages/context/src/handlers.ts @@ -0,0 +1,27 @@ +import {Action} from "./types"; +import {ActionMetadata} from "./metadata"; + +/** + * Classes that intercepts response result must implement this interface. + */ +export interface InterceptorInterface { + /** + * Called before success response is being sent to the request. + * Returned result will be sent to the user. + */ + intercept(action: Action, result: any): any | Promise; +} + +export interface MiddlewareInterface { + use(action: Action, payload?: unknown): any; +} + +/** + * n Node.js, uncaught errors are likely to cause memory leaks, file descriptor leaks, and other major production issues. + * Domains were a failed attempt to fix this. + * + * Given that it is not possible to process all uncaught errors sensibly, the best way to deal with them is to crash. + */ +export interface ErrorHandlerInterface { + onError(error: TError, action: Action, metadata?: ActionMetadata): any; +} diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 27308e52..3add772e 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -5,5 +5,7 @@ export * from "./ioc"; export * from "./options"; export * from "./ApplicationContext"; export * from "./types"; +export * from "./handlers"; +export * from "./checkers"; export * from "./metadata"; export * from "./config"; diff --git a/packages/context/src/ioc/IocContainer.ts b/packages/context/src/ioc/IocContainer.ts deleted file mode 100644 index 0d98064d..00000000 --- a/packages/context/src/ioc/IocContainer.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type {Action, ClassConstructor} from "routing-controllers"; - -/** - * Allows routing controllers to resolve objects using your IoC container - */ -export interface IocContainer { - get(someClass: ClassConstructor, action?: Action): T; - - get(id: string, action?: Action): T; - - /** - * Sets a value for the given type or service name in the container. - */ - set(type: Function, value: any): TContainer; - - set(name: string, value: any): TContainer; - - has(type: ClassConstructor): boolean; - - has(id: string): boolean; -} diff --git a/packages/context/src/ioc/container.ts b/packages/context/src/ioc/container.ts new file mode 100644 index 00000000..c338b6c2 --- /dev/null +++ b/packages/context/src/ioc/container.ts @@ -0,0 +1,51 @@ +import {UseContainerOptions} from "class-validator"; +import {ClassConstructor, IocContainer} from "./types"; +import {Action} from "../types"; + +let userContainer: {get(someClass: ClassConstructor | Function, action?: Action): T}; +let userContainerOptions: UseContainerOptions | undefined; + +/** + * Container to be used by this library for inversion control. If container was not implicitly set then by default + * container simply creates a new instance of the given class. + */ +const defaultContainer: {get(someClass: ClassConstructor | Function): T} = new (class { + private instances: {type: Function; object: any}[] = []; + + get(someClass: ClassConstructor): T { + let instance = this.instances.find(instance => instance.type === someClass); + if (!instance) { + instance = {type: someClass, object: new someClass()}; + this.instances.push(instance); + } + + return instance.object; + } +})(); + +/** + * Sets container to be used by this library. + */ +export function useContainer(iocAdapter: IocContainer, options?: UseContainerOptions) { + userContainer = iocAdapter; + userContainerOptions = options; +} + +/** + * Gets the IOC container used by this library. + * @param someClass A class constructor to resolve + * @param action The request/response context that `someClass` is being resolved for + */ +export function getFromContainer(someClass: ClassConstructor | Function, action?: Action): T { + if (userContainer) { + try { + const instance = userContainer.get(someClass, action); + if (instance) return instance; + + if (!userContainerOptions || !userContainerOptions.fallback) return instance; + } catch (error) { + if (!userContainerOptions || !userContainerOptions.fallbackOnErrors) throw error; + } + } + return defaultContainer.get(someClass); +} diff --git a/packages/context/src/ioc/index.ts b/packages/context/src/ioc/index.ts index 5b4cdeb7..12be31f7 100644 --- a/packages/context/src/ioc/index.ts +++ b/packages/context/src/ioc/index.ts @@ -1,2 +1,2 @@ -export * from "./IocContainer"; +export * from "./container"; export * from "./types"; diff --git a/packages/context/src/ioc/types.ts b/packages/context/src/ioc/types.ts index 7f4ee185..e60b5a75 100644 --- a/packages/context/src/ioc/types.ts +++ b/packages/context/src/ioc/types.ts @@ -1,3 +1,5 @@ +import {Action} from "../types"; + /** * Used to create unique typed component identifier. * Useful when component has only interface, but don't have a class. @@ -15,11 +17,7 @@ export declare class Token { * Unique service identifier. * Can be some class type, or string id, or instance of Token. */ -export declare type ServiceIdentifier = - | Constructable - | CallableFunction - | Token - | string; +export declare type ServiceIdentifier = Constructable | CallableFunction | Token | string; /** * Generic type for class definitions. @@ -31,3 +29,40 @@ export declare type ServiceIdentifier = * ``` */ export declare type Constructable = new (...args: any[]) => T; + +export type ClassConstructor = {new (...args: any[]): T}; + +/** + * Container options. + */ +export interface UseContainerOptions { + /** + * If set to true, then default container will be used in the case if given container haven't returned anything. + */ + fallback?: boolean; + + /** + * If set to true, then default container will be used in the case if given container thrown an exception. + */ + fallbackOnErrors?: boolean; +} + +/** + * Allows routing controllers to resolve objects using your IoC container + */ +export interface IocContainer { + get(someClass: ClassConstructor, action?: Action): T; + + get(id: string, action?: Action): T; + + /** + * Sets a value for the given type or service name in the container. + */ + set(type: Function, value: any): TContainer; + + set(name: string, value: any): TContainer; + + has(type: ClassConstructor): boolean; + + has(id: string): boolean; +} diff --git a/packages/context/src/metadata/ActionMetadata.ts b/packages/context/src/metadata/ActionMetadata.ts new file mode 100644 index 00000000..659dddbb --- /dev/null +++ b/packages/context/src/metadata/ActionMetadata.ts @@ -0,0 +1,255 @@ +import {ActionMetadataArgs} from "./args"; +import {ClassTransformOptions} from "class-transformer"; +import {ControllerMetadata} from "./ControllerMetadata"; +import {InterceptorMetadata} from "./InterceptorMetadata"; +import {ParamMetadata} from "./ParamMetadata"; +import {ResponseHandlerMetadata} from "./ResponseHandleMetadata"; +import {UseMetadata} from "./UseMetadata"; +import {HandlerOptions} from "./options"; +import {Action, ActionType} from "../types"; +import {NodeBootEngineOptions} from "../options"; + +/** + * Action metadata. + */ +export class ActionMetadata { + /** + * Action's controller. + */ + controllerMetadata: ControllerMetadata; + + /** + * Action's parameters. + */ + params: ParamMetadata[]; + + /** + * Action's use metadatas. + */ + uses: UseMetadata[]; + + /** + * Action's use interceptors. + */ + interceptors: InterceptorMetadata[]; + + /** + * Class on which's method this action is attached. + */ + target: Function; + + /** + * Object's method that will be executed on this action. + */ + method: string; + + /** + * Action-specific options. + */ + options?: HandlerOptions; + + /** + * Action type represents http method used for the registered route. Can be one of the value defined in ActionTypes + * class. + */ + type: ActionType; + + /** + * Route to be registered for the action. + */ + route?: string | RegExp; + + /** + * Full route to this action (includes controller base route). + */ + fullRoute: string | RegExp; + + /** + * Indicates if this action uses Body. + */ + isBodyUsed: boolean; + + /** + * Indicates if this action uses Uploaded File. + */ + isFileUsed: boolean; + + /** + * Indicates if this action uses Uploaded Files. + */ + isFilesUsed: boolean; + + /** + * Indicates if controller of this action is json-typed. + */ + isJsonTyped: boolean; + + /** + * Indicates if this action uses Authorized decorator. + */ + isAuthorizedUsed: boolean; + + /** + * Class-transformer options for the action response content. + */ + responseClassTransformOptions: ClassTransformOptions; + + /** + * Http code to be used on undefined action returned content. + */ + undefinedResultCode: number | Function; + + /** + * Http code to be used on null action returned content. + */ + nullResultCode: number | Function; + + /** + * Http code to be set on successful response. + */ + successHttpCode: number; + + /** + * Specifies redirection url for this action. + */ + redirect: string; + + /** + * Rendered template to be used for this controller action. + */ + renderedTemplate: string; + + /** + * Response headers to be set. + */ + headers: {[name: string]: any}; + + /** + * Extra options used by @Body decorator. + */ + bodyExtraOptions: any; + + /** + * Roles set by @Authorized decorator. + */ + authorizedRoles: any[]; + + /** + * Params to be appended to the method call. + */ + appendParams?: (action: Action) => any[]; + + /** + * Special function that will be called instead of orignal method of the target. + */ + methodOverride?: (actionMetadata: ActionMetadata, action: Action, params: any[]) => Promise | any; + + constructor(controllerMetadata: ControllerMetadata, args: ActionMetadataArgs, private globalOptions: NodeBootEngineOptions) { + this.controllerMetadata = controllerMetadata; + this.route = args.route; + this.target = args.target; + this.method = args.method; + this.options = args.options; + this.type = args.type; + this.appendParams = args.appendParams; + this.methodOverride = args.methodOverride; + } + + /** + * Appends base route to a given regexp route. + */ + static appendBaseRoute(baseRoute: string, route: RegExp | string) { + const prefix = `${baseRoute.length > 0 && baseRoute.indexOf("/") < 0 ? "/" : ""}${baseRoute}`; + if (typeof route === "string") return `${prefix}${route}`; + + if (!baseRoute || baseRoute === "") return route; + + const fullPath = `^${prefix}${route.toString().substr(1)}?$`; + + return new RegExp(fullPath, route.flags); + } + + /** + * Builds everything action metadata needs. + * Action metadata can be used only after its build. + */ + build(responseHandlers: ResponseHandlerMetadata[]) { + const classTransformerResponseHandler = responseHandlers.find(handler => handler.type === "response-class-transform-options"); + const undefinedResultHandler = responseHandlers.find(handler => handler.type === "on-undefined"); + const nullResultHandler = responseHandlers.find(handler => handler.type === "on-null"); + const successCodeHandler = responseHandlers.find(handler => handler.type === "success-code"); + const redirectHandler = responseHandlers.find(handler => handler.type === "redirect"); + const renderedTemplateHandler = responseHandlers.find(handler => handler.type === "rendered-template"); + const authorizedHandler = responseHandlers.find(handler => handler.type === "authorized"); + const contentTypeHandler = responseHandlers.find(handler => handler.type === "content-type"); + const bodyParam = this.params.find(param => param.type === "body"); + + if (classTransformerResponseHandler) this.responseClassTransformOptions = classTransformerResponseHandler.value; + + this.undefinedResultCode = undefinedResultHandler + ? undefinedResultHandler.value + : this.globalOptions.defaults && this.globalOptions.defaults.undefinedResultCode; + + this.nullResultCode = nullResultHandler ? nullResultHandler.value : this.globalOptions.defaults && this.globalOptions.defaults.nullResultCode; + + if (successCodeHandler) this.successHttpCode = successCodeHandler.value; + if (redirectHandler) this.redirect = redirectHandler.value; + if (renderedTemplateHandler) this.renderedTemplate = renderedTemplateHandler.value; + + this.bodyExtraOptions = bodyParam ? bodyParam.extraOptions : undefined; + this.isBodyUsed = !!this.params.find(param => param.type === "body" || param.type === "body-param"); + this.isFilesUsed = !!this.params.find(param => param.type === "files"); + this.isFileUsed = !!this.params.find(param => param.type === "file"); + this.isJsonTyped = contentTypeHandler !== undefined ? /json/.test(contentTypeHandler.value) : this.controllerMetadata.type === "json"; + this.fullRoute = this.buildFullRoute(); + this.headers = this.buildHeaders(responseHandlers); + + this.isAuthorizedUsed = this.controllerMetadata.isAuthorizedUsed || !!authorizedHandler; + this.authorizedRoles = (this.controllerMetadata.authorizedRoles || []).concat((authorizedHandler && authorizedHandler.value) || []); + } + + /** + * Calls action method. + * Action method is an action defined in a user controller. + */ + callMethod(params: any[], action: Action) { + const controllerInstance = this.controllerMetadata.getInstance(action); + // eslint-disable-next-line prefer-spread + return controllerInstance[this.method].apply(controllerInstance, params); + } + + /** + * Builds full action route. + */ + private buildFullRoute(): string | RegExp { + if (this.route instanceof RegExp) { + if (this.controllerMetadata.route) { + return ActionMetadata.appendBaseRoute(this.controllerMetadata.route, this.route); + } + return this.route; + } + + let path = ""; + if (this.controllerMetadata.route) path += this.controllerMetadata.route; + if (this.route && typeof this.route === "string") path += this.route; + return path; + } + + /** + * Builds action response headers. + */ + private buildHeaders(responseHandlers: ResponseHandlerMetadata[]) { + const contentTypeHandler = responseHandlers.find(handler => handler.type === "content-type"); + const locationHandler = responseHandlers.find(handler => handler.type === "location"); + + const headers: {[name: string]: string} = {}; + if (locationHandler) headers["Location"] = locationHandler.value; + + if (contentTypeHandler) headers["Content-type"] = contentTypeHandler.value; + + const headerHandlers = responseHandlers.filter(handler => handler.type === "header"); + if (headerHandlers) headerHandlers.map(handler => (headers[handler.value] = handler.secondaryValue)); + + return headers; + } +} diff --git a/packages/context/src/metadata/ControllerMetadata.ts b/packages/context/src/metadata/ControllerMetadata.ts new file mode 100644 index 00000000..b9783537 --- /dev/null +++ b/packages/context/src/metadata/ControllerMetadata.ts @@ -0,0 +1,83 @@ +import {ActionMetadata} from "./ActionMetadata"; +import {ControllerMetadataArgs} from "./args"; +import {UseMetadata} from "./UseMetadata"; +import {ResponseHandlerMetadata} from "./ResponseHandleMetadata"; +import {InterceptorMetadata} from "./InterceptorMetadata"; +import {getFromContainer} from "../ioc"; +import {ControllerOptions} from "./options"; +import {Action} from "../types"; + +/** + * Controller metadata. + */ +export class ControllerMetadata { + /** + * Controller actions. + */ + actions: ActionMetadata[]; + + /** + * Indicates object which is used by this controller. + */ + target: Function; + + /** + * Base route for all actions registered in this controller. + */ + route?: string; + + /** + * Controller type. Can be default or json-typed. Json-typed controllers operate with json requests and responses. + */ + type: "default" | "json"; + + /** + * Options that apply to all controller actions. + */ + options?: ControllerOptions; + + /** + * Middleware "use"-s applied to a whole controller. + */ + uses: UseMetadata[]; + + /** + * Middleware "use"-s applied to a whole controller. + */ + interceptors: InterceptorMetadata[]; + + /** + * Indicates if this action uses Authorized decorator. + */ + isAuthorizedUsed: boolean; + + /** + * Roles set by @Authorized decorator. + */ + authorizedRoles: any[]; + + constructor(args: ControllerMetadataArgs) { + this.target = args.target; + this.route = args.route; + this.type = args.type; + this.options = args.options; + } + + /** + * Gets instance of the controller. + * @param action Details around the request session + */ + getInstance(action: Action): any { + return getFromContainer(this.target, action); + } + + /** + * Builds everything controller metadata needs. + * Controller metadata should be used only after its build. + */ + build(responseHandlers: ResponseHandlerMetadata[]) { + const authorizedHandler = responseHandlers.find(handler => handler.type === "authorized" && !handler.method); + this.isAuthorizedUsed = !!authorizedHandler; + this.authorizedRoles = [].concat((authorizedHandler && authorizedHandler.value) || []); + } +} diff --git a/packages/context/src/metadata/InterceptorMetadata.ts b/packages/context/src/metadata/InterceptorMetadata.ts new file mode 100644 index 00000000..03270bd1 --- /dev/null +++ b/packages/context/src/metadata/InterceptorMetadata.ts @@ -0,0 +1,39 @@ +import {UseInterceptorMetadataArgs} from "./args"; + +/** + * "Use interceptor" metadata. + */ +export class InterceptorMetadata { + /** + * Object class of the interceptor class. + */ + target: Function; + + /** + * Method used by this "use". + */ + method?: string; + + /** + * Interceptor class or function to be executed by this "use". + */ + interceptor: Function; + + /** + * Indicates if this interceptor is global or not. + */ + global?: boolean; + + /** + * Interceptor priority. Used for global interceptors. + */ + priority: number; + + constructor(args: UseInterceptorMetadataArgs) { + this.target = args.target; + this.method = args.method; + this.interceptor = args.interceptor; + this.priority = args.priority ?? 0; + this.global = args.global; + } +} diff --git a/packages/context/src/metadata/MiddlewareMetadata.ts b/packages/context/src/metadata/MiddlewareMetadata.ts new file mode 100644 index 00000000..18a3c17f --- /dev/null +++ b/packages/context/src/metadata/MiddlewareMetadata.ts @@ -0,0 +1,52 @@ +import {MiddlewareMetadataArgs} from "./args"; +import {getFromContainer} from "../ioc"; +import {ErrorHandlerInterface, InterceptorInterface, MiddlewareInterface} from "../handlers"; + +/** + * Middleware metadata. + */ +export class MiddlewareMetadata { + /** + * Indicates if this middleware is global, thous applied to all routes. + */ + global: boolean; + + /** + * Object class of the middleware class. + */ + target: Function; + + /** + * Execution priority of the middleware. + */ + priority: number; + + /** + * Indicates if middleware must be executed after routing action is executed. + */ + type: "before" | "after"; + + constructor(args: MiddlewareMetadataArgs) { + this.global = args.global; + this.target = args.target; + this.priority = args.priority; + this.type = args.type; + } + + /** + * Gets middleware instance from the container. + */ + + /*get instance(): ExpressMiddlewareInterface | KoaMiddlewareInterface | ExpressErrorMiddlewareInterface { + return getFromContainer( + this.target, + ); + }*/ + + /** + * Gets middleware instance from the container. + */ + get instance(): ErrorHandlerInterface | InterceptorInterface | MiddlewareInterface { + return getFromContainer(this.target); + } +} diff --git a/packages/context/src/metadata/ParamMetadata.ts b/packages/context/src/metadata/ParamMetadata.ts new file mode 100644 index 00000000..0133a29c --- /dev/null +++ b/packages/context/src/metadata/ParamMetadata.ts @@ -0,0 +1,132 @@ +import {ValidatorOptions} from "class-validator"; +import {ActionMetadata} from "./ActionMetadata"; +import {ParamMetadataArgs} from "./args"; +import {ClassTransformOptions} from "class-transformer"; +import {Action, ParamType} from "../types"; + +/** + * Action Parameter metadata. + */ +export class ParamMetadata { + /** + * Parameter's action. + */ + actionMetadata: ActionMetadata; + + /** + * Object on which's method's parameter this parameter is attached. + */ + object: any; + + /** + * Method on which's parameter is attached. + */ + method?: string; + + /** + * Index (# number) of the parameter in the method signature. + */ + index: number; + + /** + * Parameter type. + */ + type: ParamType; + + /** + * Parameter name. + */ + name: string; + + /** + * Parameter target type. + */ + targetType?: any; + + /** + * Parameter target type's name in lowercase. + */ + targetName = ""; + + /** + * Indicates if target type is an object. + */ + isTargetObject = false; + + /** + * Parameter target. + */ + target: any; + + /** + * Specifies if parameter should be parsed as json or not. + */ + parse: boolean; + + /** + * Indicates if this parameter is required or not + */ + required?: boolean; + + /** + * Transforms the value. + */ + transform?: (action: Action, value?: any) => Promise | any; + + /** + * If true, string values are cast to arrays + */ + isArray?: boolean; + + /** + * Additional parameter options. + * For example it can be uploader middleware options or body-parser middleware options. + */ + extraOptions: any; + + /** + * Class transform options used to perform plainToClass operation. + */ + classTransform?: ClassTransformOptions; + + /** + * If true, class-validator will be used to validate param object. + * If validation options are given then it means validation will be applied (is true). + */ + validate?: boolean | ValidatorOptions; + + constructor(actionMetadata: ActionMetadata, args: ParamMetadataArgs) { + this.actionMetadata = actionMetadata; + + this.target = args.object.constructor; + this.method = args.method; + this.extraOptions = args.extraOptions; + this.index = args.index; + this.type = args.type; + this.name = args.name!; + this.parse = args.parse; + this.required = args.required; + this.transform = args.transform; + this.classTransform = args.classTransform; + this.validate = args.validate; + this.isArray = args.isArray; + + if (args.explicitType) { + this.targetType = args.explicitType; + } else { + const ParamTypes = (Reflect as any).getMetadata("design:paramtypes", args.object, args.method); + if (typeof ParamTypes !== "undefined") { + this.targetType = ParamTypes[args.index]; + } + } + + if (this.targetType) { + if (this.targetType instanceof Function && this.targetType.name) { + this.targetName = this.targetType.name.toLowerCase(); + } else if (typeof this.targetType === "string") { + this.targetName = this.targetType.toLowerCase(); + } + this.isTargetObject = this.targetType instanceof Function || this.targetType.toLowerCase() === "object"; + } + } +} diff --git a/packages/context/src/metadata/ResponseHandleMetadata.ts b/packages/context/src/metadata/ResponseHandleMetadata.ts new file mode 100644 index 00000000..8cd74c71 --- /dev/null +++ b/packages/context/src/metadata/ResponseHandleMetadata.ts @@ -0,0 +1,40 @@ +import {ResponseHandlerMetadataArgs} from "./args"; +import {ResponseHandlerType} from "../types"; + +/** + * Response handler metadata. + */ +export class ResponseHandlerMetadata { + /** + * Class on which's method decorator is set. + */ + target: Function; + + /** + * Method on which decorator is set. + */ + method?: string; + + /** + * Property type. See ResponsePropertyMetadataType for possible values. + */ + type: ResponseHandlerType; + + /** + * Property value. Can be status code, content-type, header name, template name, etc. + */ + value: any; + + /** + * Secondary property value. Can be header value for example. + */ + secondaryValue: any; + + constructor(args: ResponseHandlerMetadataArgs) { + this.target = args.target; + this.method = args.method; + this.type = args.type; + this.value = args.value; + this.secondaryValue = args.secondaryValue; + } +} diff --git a/packages/context/src/metadata/UseMetadata.ts b/packages/context/src/metadata/UseMetadata.ts new file mode 100644 index 00000000..e26fcefc --- /dev/null +++ b/packages/context/src/metadata/UseMetadata.ts @@ -0,0 +1,41 @@ +import {UseMetadataArgs} from "./args"; + +/** + * "Use middleware" metadata. + */ +export class UseMetadata { + /** + * Object class of the middleware class. + */ + target: Function; + + /** + * Method used by this "use". + */ + method?: string; + + /** + * Middleware to be executed by this "use". + */ + middleware: Function; + + /** + * Indicates if middleware must be executed after routing action is executed. + */ + afterAction: boolean; + + constructor(args: UseMetadataArgs) { + this.target = args.target; + this.method = args.method; + this.middleware = args.middleware; + this.afterAction = args.afterAction; + } + + isCustomMiddleware() { + return this.middleware.prototype?.use; + } + + isErrorMiddleware() { + return this.middleware.prototype?.onError; + } +} diff --git a/packages/context/src/metadata/args/ActionMetadataArgs.ts b/packages/context/src/metadata/args/ActionMetadataArgs.ts new file mode 100644 index 00000000..f2a2aafd --- /dev/null +++ b/packages/context/src/metadata/args/ActionMetadataArgs.ts @@ -0,0 +1,44 @@ +import {ActionMetadata} from "../ActionMetadata"; +import {HandlerOptions} from "../options"; +import {Action, ActionType} from "../../types"; + +/** + * Action metadata used to storage information about registered action. + */ +export interface ActionMetadataArgs { + /** + * Route to be registered for the action. + */ + route?: string | RegExp; + + /** + * Class on which's method this action is attached. + */ + target: Function; + + /** + * Object's method that will be executed on this action. + */ + method: string; + + /** + * Action-specific options. + */ + options?: HandlerOptions; + + /** + * Action type represents http method used for the registered route. Can be one of the value defined in ActionTypes + * class. + */ + type: ActionType; + + /** + * Params to be appended to the method call. + */ + appendParams?: (action: Action) => any[]; + + /** + * Special function that will be called instead of orignal method of the target. + */ + methodOverride?: (actionMetadata: ActionMetadata, action: Action, params: any[]) => Promise | any; +} diff --git a/packages/context/src/metadata/args/ControllerMetadataArgs.ts b/packages/context/src/metadata/args/ControllerMetadataArgs.ts new file mode 100644 index 00000000..fdf71359 --- /dev/null +++ b/packages/context/src/metadata/args/ControllerMetadataArgs.ts @@ -0,0 +1,26 @@ +import {ControllerOptions} from "../options"; + +/** + * Controller metadata used to storage information about registered controller. + */ +export interface ControllerMetadataArgs { + /** + * Indicates object which is used by this controller. + */ + target: Function; + + /** + * Base route for all actions registered in this controller. + */ + route?: string; + + /** + * Controller type. Can be default or json-typed. Json-typed controllers operate with json requests and responses. + */ + type: "default" | "json"; + + /** + * Options that apply to all controller actions. + */ + options?: ControllerOptions; +} diff --git a/packages/context/src/metadata/args/ErrorHandlerMetadataArgs.ts b/packages/context/src/metadata/args/ErrorHandlerMetadataArgs.ts new file mode 100644 index 00000000..bed0200e --- /dev/null +++ b/packages/context/src/metadata/args/ErrorHandlerMetadataArgs.ts @@ -0,0 +1,14 @@ +/** + * Metadata used to store registered error handlers. + */ +export interface ErrorHandlerMetadataArgs { + /** + * Object class of the error handler class. + */ + target: Function; + + /** + * Execution priority of the error handler. + */ + priority: number; +} diff --git a/packages/context/src/metadata/args/InterceptorMetadataArgs.ts b/packages/context/src/metadata/args/InterceptorMetadataArgs.ts new file mode 100644 index 00000000..eec6ee40 --- /dev/null +++ b/packages/context/src/metadata/args/InterceptorMetadataArgs.ts @@ -0,0 +1,19 @@ +/** + * Metadata used to store registered interceptor. + */ +export interface InterceptorMetadataArgs { + /** + * Object class of the interceptor class. + */ + target: Function; + + /** + * Indicates if this interceptor is global, thous applied to all routes. + */ + global: boolean; + + /** + * Execution priority of the interceptor. + */ + priority: number; +} diff --git a/packages/context/src/metadata/args/MiddlewareMetadataArgs.ts b/packages/context/src/metadata/args/MiddlewareMetadataArgs.ts new file mode 100644 index 00000000..decd5088 --- /dev/null +++ b/packages/context/src/metadata/args/MiddlewareMetadataArgs.ts @@ -0,0 +1,24 @@ +/** + * Metadata used to store registered middlewares. + */ +export interface MiddlewareMetadataArgs { + /** + * Object class of the middleware class. + */ + target: Function; + + /** + * Indicates if this middleware is global, thous applied to all routes. + */ + global: boolean; + + /** + * Execution priority of the middleware. + */ + priority: number; + + /** + * Indicates if middleware must be executed after routing action is executed. + */ + type: "before" | "after"; +} diff --git a/packages/context/src/metadata/args/ParamMetadataArgs.ts b/packages/context/src/metadata/args/ParamMetadataArgs.ts new file mode 100644 index 00000000..0be29e7d --- /dev/null +++ b/packages/context/src/metadata/args/ParamMetadataArgs.ts @@ -0,0 +1,74 @@ +import {ValidatorOptions} from "class-validator"; +import {ClassTransformOptions} from "class-transformer"; +import {ParamType} from "../../types"; + +/** + * Controller metadata used to storage information about registered parameters. + */ +export interface ParamMetadataArgs { + /** + * Parameter object. + */ + object: any; + + /** + * Method on which's parameter is attached. + */ + method?: string; + + /** + * Index (# number) of the parameter in the method signature. + */ + index: number; + + /** + * Parameter type. + */ + type: ParamType; + + /** + * Parameter name. + */ + name?: string; + + /** + * Specifies if parameter should be parsed as json or not. + */ + parse: boolean; + + /** + * Indicates if this parameter is required or not + */ + required?: boolean; + + /** + * Transforms the value. + */ + transform?: (value?: any, request?: any, response?: any) => Promise | any; + + /** + * Extra parameter options. + */ + extraOptions?: any; + + /** + * Class transform options used to perform plainToClass operation. + */ + classTransform?: ClassTransformOptions; + + /** + * If true, class-validator will be used to validate param object. + * If validation options are given then it means validation will be applied (is true). + */ + validate?: boolean | ValidatorOptions; + + /** + * Explicitly set type which should be used for Body to perform transformation. + */ + explicitType?: any; + + /** + * Explicitly tell that the QueryParam is an array to force routing-controller to cast it + */ + isArray?: boolean; +} diff --git a/packages/context/src/metadata/args/ResponseHandleMetadataArgs.ts b/packages/context/src/metadata/args/ResponseHandleMetadataArgs.ts new file mode 100644 index 00000000..e4773dd5 --- /dev/null +++ b/packages/context/src/metadata/args/ResponseHandleMetadataArgs.ts @@ -0,0 +1,31 @@ +import {ResponseHandlerType} from "../../types"; + +/** + * Storages information about registered response handlers. + */ +export interface ResponseHandlerMetadataArgs { + /** + * Class on which's method decorator is set. + */ + target: Function; + + /** + * Method on which decorator is set. + */ + method?: string; + + /** + * Property type. See ResponsePropertyMetadataType for possible values. + */ + type: ResponseHandlerType; + + /** + * Property value. Can be status code, content-type, header name, template name, etc. + */ + value?: any; + + /** + * Secondary property value. Can be header value for example. + */ + secondaryValue?: any; +} diff --git a/packages/context/src/metadata/args/UseInterceptorMetadataArgs.ts b/packages/context/src/metadata/args/UseInterceptorMetadataArgs.ts new file mode 100644 index 00000000..88a0c220 --- /dev/null +++ b/packages/context/src/metadata/args/UseInterceptorMetadataArgs.ts @@ -0,0 +1,31 @@ +/** + * Metadata used to store registered intercept for a specific controller or controller action. + */ +export interface UseInterceptorMetadataArgs { + /** + * Controller class where this intercept was used. + */ + target: Function; + + /** + * Controller method to which this intercept is applied. + * If method is not given it means intercept is used on the controller. + * Then intercept is applied to all controller's actions. + */ + method?: string; + + /** + * Interceptor class or a function to be executed. + */ + interceptor: Function; + + /** + * Indicates if this interceptor is global, thous applied to all routes. + */ + global?: boolean; + + /** + * Execution priority of the interceptor. + */ + priority?: number; +} diff --git a/packages/context/src/metadata/args/UseMetadataArgs.ts b/packages/context/src/metadata/args/UseMetadataArgs.ts new file mode 100644 index 00000000..1f0d24fa --- /dev/null +++ b/packages/context/src/metadata/args/UseMetadataArgs.ts @@ -0,0 +1,25 @@ +/** + * Metadata used to store registered middlewares. + */ +export interface UseMetadataArgs { + /** + * Object class of this "use". + */ + target: Function; + + /** + * Method to which this "use" is applied. + * If method is not given it means "use" is used on the controller. Then "use" applied to all controller's actions. + */ + method?: string; + + /** + * Middleware to be executed for this "use". + */ + middleware: Function; + + /** + * Indicates if middleware must be executed after routing action is executed. + */ + afterAction: boolean; +} diff --git a/packages/context/src/metadata/args/index.ts b/packages/context/src/metadata/args/index.ts new file mode 100644 index 00000000..2422e2c9 --- /dev/null +++ b/packages/context/src/metadata/args/index.ts @@ -0,0 +1,9 @@ +export {ActionMetadataArgs} from "./ActionMetadataArgs"; +export {ControllerMetadataArgs} from "./ControllerMetadataArgs"; +export {ErrorHandlerMetadataArgs} from "./ErrorHandlerMetadataArgs"; +export {InterceptorMetadataArgs} from "./InterceptorMetadataArgs"; +export {MiddlewareMetadataArgs} from "./MiddlewareMetadataArgs"; +export {ParamMetadataArgs} from "./ParamMetadataArgs"; +export {ResponseHandlerMetadataArgs} from "./ResponseHandleMetadataArgs"; +export {UseInterceptorMetadataArgs} from "./UseInterceptorMetadataArgs"; +export {UseMetadataArgs} from "./UseMetadataArgs"; diff --git a/packages/context/src/metadata/index.ts b/packages/context/src/metadata/index.ts index 2094dcc9..48c7a678 100644 --- a/packages/context/src/metadata/index.ts +++ b/packages/context/src/metadata/index.ts @@ -1 +1,10 @@ export * from "./metadata.keys"; +export {ActionMetadata} from "./ActionMetadata"; +export {ControllerMetadata} from "./ControllerMetadata"; +export {InterceptorMetadata} from "./InterceptorMetadata"; +export {MiddlewareMetadata} from "./MiddlewareMetadata"; +export {ParamMetadata} from "./ParamMetadata"; +export {ResponseHandlerMetadata} from "./ResponseHandleMetadata"; +export {UseMetadata} from "./UseMetadata"; +export * from "./args"; +export * from "./options"; diff --git a/packages/context/src/metadata/options/BodyOptions.ts b/packages/context/src/metadata/options/BodyOptions.ts new file mode 100644 index 00000000..6d36575d --- /dev/null +++ b/packages/context/src/metadata/options/BodyOptions.ts @@ -0,0 +1,38 @@ +import {ValidatorOptions} from "class-validator"; +import {ClassTransformOptions} from "class-transformer"; + +/** + * Body decorator parameters. + */ +export interface BodyOptions { + /** + * If set to true then request body will become required. + * If user performs a request and body is not in a request then routing-controllers will throw an error. + */ + required?: boolean; + + /** + * Class-transformer options used to perform plainToClass operation. + * + * @see https://github.com/pleerock/class-transformer + */ + transform?: ClassTransformOptions; + + /** + * If true, class-validator will be used to validate param object. + * If validation options are given then class-validator will perform validation with given options. + * + * @see https://github.com/pleerock/class-validator + */ + validate?: boolean | ValidatorOptions; + + /** + * Extra options to be passed to body-parser middleware. + */ + options?: any; + + /** + * Explicitly set type which should be used for Body to perform transformation. + */ + type?: any; +} diff --git a/packages/context/src/metadata/options/ControllerOptions.ts b/packages/context/src/metadata/options/ControllerOptions.ts new file mode 100644 index 00000000..4fe86b6c --- /dev/null +++ b/packages/context/src/metadata/options/ControllerOptions.ts @@ -0,0 +1,14 @@ +/** + * Extra options that apply to each controller action. + */ +export interface ControllerOptions { + /** + * If set to false, class-transformer won't be used to perform request serialization. + */ + transformRequest?: boolean; + + /** + * If set to false, class-transformer won't be used to perform response serialization. + */ + transformResponse?: boolean; +} diff --git a/packages/context/src/metadata/options/HandlerOptions.ts b/packages/context/src/metadata/options/HandlerOptions.ts new file mode 100644 index 00000000..869ef0a4 --- /dev/null +++ b/packages/context/src/metadata/options/HandlerOptions.ts @@ -0,0 +1,14 @@ +/** + * Extra handler-specific options. + */ +export interface HandlerOptions { + /** + * If set to false, class-transformer won't be used to perform request serialization. + */ + transformRequest?: boolean; + + /** + * If set to false, class-transformer won't be used to perform response serialization. + */ + transformResponse?: boolean; +} diff --git a/packages/context/src/metadata/options/ParamOptions.ts b/packages/context/src/metadata/options/ParamOptions.ts new file mode 100644 index 00000000..ca22e78f --- /dev/null +++ b/packages/context/src/metadata/options/ParamOptions.ts @@ -0,0 +1,40 @@ +import {ValidatorOptions} from "class-validator"; +import {ClassTransformOptions} from "class-transformer"; + +/** + * Extra options set to the parameter decorators. + */ +export interface ParamOptions { + /** + * If set to true then parameter will be required. + * If user performs a request and required parameter is not in a request then routing-controllers will throw an error. + */ + required?: boolean; + + /** + * If set to true then parameter will be parsed to json. + * Parsing is automatically done if parameter type is a class type. + */ + parse?: boolean; + + /** + * Class transform options used to perform plainToClass operation. + */ + transform?: ClassTransformOptions; + + /** + * If true, class-validator will be used to validate param object. + * If validation options are given then class-validator will perform validation with given options. + */ + validate?: boolean | ValidatorOptions; + + /** + * Explicitly set type which should be used for param to perform transformation. + */ + type?: any; + + /** + * Force value to be cast as an array. + */ + isArray?: boolean; +} diff --git a/packages/context/src/metadata/options/UploadOptions.ts b/packages/context/src/metadata/options/UploadOptions.ts new file mode 100644 index 00000000..6edbca98 --- /dev/null +++ b/packages/context/src/metadata/options/UploadOptions.ts @@ -0,0 +1,15 @@ +/** + * Upload decorator parameters. + */ +export interface UploadOptions { + /** + * If set to true then uploaded file become required. + * If user performs a request and file is not in a request then routing-controllers will throw an error. + */ + required?: boolean; + + /** + * Special upload options passed to an upload middleware. + */ + options?: any; +} diff --git a/packages/context/src/metadata/options/index.ts b/packages/context/src/metadata/options/index.ts new file mode 100644 index 00000000..cc947c60 --- /dev/null +++ b/packages/context/src/metadata/options/index.ts @@ -0,0 +1,5 @@ +export {BodyOptions} from "./BodyOptions"; +export {ControllerOptions} from "./ControllerOptions"; +export {HandlerOptions} from "./HandlerOptions"; +export {ParamOptions} from "./ParamOptions"; +export {UploadOptions} from "./UploadOptions"; diff --git a/packages/context/src/metadata/types.ts b/packages/context/src/metadata/types.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/packages/context/src/options/DependencyInjectionOptions.ts b/packages/context/src/options/DependencyInjectionOptions.ts index 622e6223..e6556b03 100644 --- a/packages/context/src/options/DependencyInjectionOptions.ts +++ b/packages/context/src/options/DependencyInjectionOptions.ts @@ -1,5 +1,4 @@ -import {UseContainerOptions} from "routing-controllers/types/container"; -import {IocContainer} from "../ioc"; +import {IocContainer, UseContainerOptions} from "../ioc"; export type DependencyInjectionOptions = { iocContainer: IocContainer; diff --git a/packages/context/src/options/NodeBootEngineOptions.ts b/packages/context/src/options/NodeBootEngineOptions.ts new file mode 100644 index 00000000..cce4c8d1 --- /dev/null +++ b/packages/context/src/options/NodeBootEngineOptions.ts @@ -0,0 +1,110 @@ +import {ClassTransformOptions} from "class-transformer"; +import {ValidatorOptions} from "class-validator"; +import {AuthorizationChecker, CurrentUserChecker} from "../checkers"; + +/** + * NodeBoot initialization options. + */ +export interface NodeBootEngineOptions { + /** + * Indicates if cors are enabled. + * This requires installation of additional module (cors for express and @koa/cors for koa). + */ + cors?: boolean | Object; + + /** + * Global route prefix, for example '/api'. + */ + routePrefix?: string; + + /** + * List of controllers to register in the framework or directories from where to import all your controllers. + */ + controllers?: Function[] | string[]; + + /** + * List of middlewares to register in the framework or directories from where to import all your middlewares. + */ + middlewares?: Function[] | string[]; + + /** + * List of interceptors to register in the framework or directories from where to import all your interceptors. + */ + interceptors?: Function[] | string[]; + + /** + * Indicates if class-transformer should be used to perform serialization / deserialization. + */ + classTransformer?: boolean; + + /** + * Global class transformer options passed to class-transformer during classToPlain operation. + * This operation is being executed when server returns response to user. + */ + classToPlainTransformOptions?: ClassTransformOptions; + + /** + * Global class transformer options passed to class-transformer during plainToClass operation. + * This operation is being executed when parsing user parameters. + */ + plainToClassTransformOptions?: ClassTransformOptions; + + /** + * Indicates if class-validator should be used to auto validate objects injected into params. + * You can also directly pass validator options to enable validator with a given options. + */ + validation?: boolean | ValidatorOptions; + + /** + * Indicates if development mode is enabled. + * By default its enabled if your NODE_ENV is not equal to "production". + */ + development?: boolean; + + /** + * Indicates if default routing-controller's error handler is enabled or not. + * Enabled by default. + */ + defaultErrorHandler?: boolean; + + /** + * Map of error overrides. + */ + errorOverridingMap?: {[key: string]: any}; + + /** + * Special function used to check user authorization roles per request. + * Must return true or promise with boolean true resolved for authorization to succeed. + */ + authorizationChecker?: AuthorizationChecker; + + /** + * Special function used to get currently authorized user. + */ + currentUserChecker?: CurrentUserChecker; + + /** + * Default settings + */ + defaults?: { + /** + * If set, all null responses will return specified status code by default + */ + nullResultCode?: number; + + /** + * If set, all undefined responses will return specified status code by default + */ + undefinedResultCode?: number; + + /** + * Default param options + */ + paramOptions?: { + /** + * If true, all non-set parameters will be required by default + */ + required?: boolean; + }; + }; +} diff --git a/packages/context/src/options/index.ts b/packages/context/src/options/index.ts index 016610c8..2f790937 100644 --- a/packages/context/src/options/index.ts +++ b/packages/context/src/options/index.ts @@ -3,3 +3,4 @@ export * from "./ComponentOptions"; export * from "./ApplicationOptions"; export * from "./TransformerOptions"; export * from "./DependencyInjectionOptions"; +export * from "./NodeBootEngineOptions"; diff --git a/packages/context/src/types.ts b/packages/context/src/types.ts index 9ee6d297..d53cf8da 100644 --- a/packages/context/src/types.ts +++ b/packages/context/src/types.ts @@ -1,8 +1,33 @@ import {IocContainer} from "./ioc"; -import {Action} from "routing-controllers"; import {Logger} from "winston"; import {Config} from "./config"; +/** + * Controller action properties. + */ +export interface Action { + /** + * Action Request object. + */ + request: TRequest; + + /** + * Action Response object. + */ + response: TResponse; + + /** + * Content in which action is executed. + * Koa-specific property. + */ + context?: any; + + /** + * "Next" function used to call next middleware. + */ + next?: TNext; +} + export type BeansContext = { iocContainer: IocContainer; application: TApplication; @@ -11,6 +36,93 @@ export type BeansContext = { }; /** - * Controller action properties. + * Used to register custom parameter handler in the controller action parameters. + */ +export interface CustomParameterDecorator { + /** + * Indicates if this parameter is required or not. + * If parameter is required and value provided by it is not set then routing-controllers will throw an error. + */ + required?: boolean; + + /** + * Factory function that returns value to be written to this parameter. + * In function it provides you Action object which contains current request, response, context objects. + * It also provides you original value of this parameter. + * It can return promise, and if it returns promise then promise will be resolved before calling controller action. + */ + value: (action: Action, value?: any) => Promise | any; +} + +/** + * Controller action type. + */ +export type ActionType = + | "all" + | "checkout" + | "connect" + | "copy" + | "delete" + | "get" + | "head" + | "lock" + | "merge" + | "mkactivity" + | "mkcol" + | "move" + | "m-search" + | "notify" + | "options" + | "patch" + | "post" + | "propfind" + | "proppatch" + | "purge" + | "put" + | "report" + | "search" + | "subscribe" + | "trace" + | "unlock" + | "unsubscribe"; + +/** + * Controller action's parameter type. + */ +export type ParamType = + | "body" + | "body-param" + | "query" + | "queries" + | "header" + | "headers" + | "file" + | "files" + | "param" + | "params" + | "session" + | "session-param" + | "state" + | "cookie" + | "cookies" + | "request" + | "response" + | "context" + | "current-user" + | "custom-converter"; + +/** + * Response handler type. */ -export type RequestContext = Action; +export type ResponseHandlerType = + | "success-code" + | "error-code" + | "content-type" + | "header" + | "rendered-template" + | "redirect" + | "location" + | "on-null" + | "on-undefined" + | "response-class-transform-options" + | "authorized"; diff --git a/packages/core/package.json b/packages/core/package.json index 54fde64c..077e2b25 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -32,14 +32,15 @@ "@node-boot/context": "1.0.0", "@node-boot/config": "1.0.0", "@node-boot/di": "1.0.0", - "class-transformer": "^0.5.1", + "@node-boot/engine": "1.0.0", "reflect-metadata": "^0.1.13", "lodash": "^4.17.21", "logform": "^2.5.1", "glob": "^10.3.3" }, "peerDependencies": { - "routing-controllers": ">=0.10.4", - "winston": ">=3.10.0" + "winston": ">=3.10.0", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0" } } diff --git a/packages/core/src/adapters/BeansConfigurationAdapter.ts b/packages/core/src/adapters/BeansConfigurationAdapter.ts index 1f420c43..98ad6e7f 100644 --- a/packages/core/src/adapters/BeansConfigurationAdapter.ts +++ b/packages/core/src/adapters/BeansConfigurationAdapter.ts @@ -1,9 +1,4 @@ -import { - BEAN_METADATA_KEY, - BEAN_NAME_METADATA_KEY, - BeansContext, - ConfigurationAdapter, -} from "@node-boot/context"; +import {BEAN_METADATA_KEY, BEAN_NAME_METADATA_KEY, BeansContext, ConfigurationAdapter} from "@node-boot/context"; export class BeansConfigurationAdapter implements ConfigurationAdapter { constructor(private readonly target: Function) {} @@ -43,11 +38,7 @@ export class BeansConfigurationAdapter implements ConfigurationAdapter { if (this.isPrimitive(beanInstance)) { iocContainer.set(propertyName, beanInstance); } else if (beanInstance) { - let beanType = Reflect.getMetadata( - "design:returntype", - prototype, - propertyName, - ); + let beanType = Reflect.getMetadata("design:returntype", prototype, propertyName); if (beanType === Promise) { throw new Error( @@ -58,10 +49,7 @@ export class BeansConfigurationAdapter implements ConfigurationAdapter { if (!beanType) { // When no return type is provided by the bean function - beanType = - typeof beanInstance === "function" - ? beanInstance - : beanInstance.constructor; + beanType = typeof beanInstance === "function" ? beanInstance : beanInstance.constructor; } iocContainer.set(beanType, beanInstance); } diff --git a/packages/core/src/configuration/LoggerConfiguration.ts b/packages/core/src/configuration/LoggerConfiguration.ts deleted file mode 100644 index 51216926..00000000 --- a/packages/core/src/configuration/LoggerConfiguration.ts +++ /dev/null @@ -1,11 +0,0 @@ -import {Logger} from "winston"; -import {Bean, Configuration} from "../decorators"; -import {createLogger} from "../logger"; - -@Configuration() -export class LoggerConfiguration { - @Bean() - public logger(): Logger { - return createLogger("node-boot-core", "node-boot"); - } -} diff --git a/packages/core/src/decorators/All.ts b/packages/core/src/decorators/All.ts new file mode 100644 index 00000000..e5279601 --- /dev/null +++ b/packages/core/src/decorators/All.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ControllerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when a request comes on a given route. + * Must be applied on a controller action. + */ +export function All(route?: RegExp): Function; + +/** + * Registers an action to be executed when a request comes on a given route. + * Must be applied on a controller action. + */ +export function All(route?: string): Function; + +/** + * Registers an action to be executed when a request comes on a given route. + * Must be applied on a controller action. + */ +export function All(route?: string | RegExp, options?: ControllerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "all", + target: object.constructor, + method: methodName, + route: route, + options, + }); + }; +} diff --git a/packages/core/src/decorators/Body.ts b/packages/core/src/decorators/Body.ts new file mode 100644 index 00000000..d710c6f6 --- /dev/null +++ b/packages/core/src/decorators/Body.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {BodyOptions} from "@node-boot/context"; + +/** + * Allows to inject a request body value to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Body(options?: BodyOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "body", + object: object, + method: methodName, + index: index, + parse: false, + required: options?.required ?? false, + classTransform: options?.transform ?? undefined, + validate: options?.validate ?? undefined, + explicitType: options?.type ?? undefined, + extraOptions: options?.options ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/BodyParam.ts b/packages/core/src/decorators/BodyParam.ts new file mode 100644 index 00000000..4f94c010 --- /dev/null +++ b/packages/core/src/decorators/BodyParam.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Takes partial data of the request body. + * Must be applied on a controller action parameter. + */ +export function BodyParam(name: string, options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "body-param", + object: object, + method: methodName, + index: index, + name: name, + parse: options?.parse ?? false, + required: options?.required ?? false, + explicitType: options?.type ?? undefined, + classTransform: options?.transform ?? undefined, + validate: options?.validate ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/ContentType.ts b/packages/core/src/decorators/ContentType.ts new file mode 100644 index 00000000..39b84448 --- /dev/null +++ b/packages/core/src/decorators/ContentType.ts @@ -0,0 +1,16 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Sets response Content-Type. + * Must be applied on a controller action. + */ +export function ContentType(contentType: string): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "content-type", + target: object.constructor, + method: methodName, + value: contentType, + }); + }; +} diff --git a/packages/core/src/decorators/Controller.ts b/packages/core/src/decorators/Controller.ts index 94691ae1..e50fc40b 100644 --- a/packages/core/src/decorators/Controller.ts +++ b/packages/core/src/decorators/Controller.ts @@ -1,7 +1,6 @@ -import {Controller as InnerController} from "routing-controllers"; import {decorateDi} from "@node-boot/di"; -import {ControllerOptions} from "routing-controllers/types/decorator-options/ControllerOptions"; -import {CONTROLLER_PATH_METADATA_KEY, CONTROLLER_VERSION_METADATA_KEY} from "@node-boot/context"; +import {CONTROLLER_PATH_METADATA_KEY, CONTROLLER_VERSION_METADATA_KEY, ControllerOptions} from "@node-boot/context"; +import {NodeBootToolkit} from "@node-boot/engine"; /** * Defines a class as a controller. @@ -23,6 +22,12 @@ export function Controller(baseRoute?: string, version?: string, options?: Contr // DI is optional and the decorator will only be applied if the DI container dependency is available. decorateDi(target); - InnerController(baseRoute, options)(target); + // Register controller metadata into the engine + NodeBootToolkit.getMetadataArgsStorage().controllers.push({ + type: "default", + route: baseRoute, + target, + options, + }); }; } diff --git a/packages/core/src/decorators/CookieParam.ts b/packages/core/src/decorators/CookieParam.ts new file mode 100644 index 00000000..0cd7377b --- /dev/null +++ b/packages/core/src/decorators/CookieParam.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects a request's cookie value to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function CookieParam(name: string, options?: ParamOptions) { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "cookie", + object: object, + method: methodName, + index: index, + name: name, + parse: options?.parse ?? false, + required: options?.required ?? false, + explicitType: options?.type ?? undefined, + classTransform: options?.transform ?? undefined, + validate: options?.validate ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/CookieParams.ts b/packages/core/src/decorators/CookieParams.ts new file mode 100644 index 00000000..9e70ee9d --- /dev/null +++ b/packages/core/src/decorators/CookieParams.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects all request's cookies to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function CookieParams() { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "cookies", + object: object, + method: methodName, + index: index, + parse: false, + required: false, + }); + }; +} diff --git a/packages/core/src/decorators/Ctx.ts b/packages/core/src/decorators/Ctx.ts new file mode 100644 index 00000000..8334c58b --- /dev/null +++ b/packages/core/src/decorators/Ctx.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects a Koa's Context object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Ctx(): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "context", + object: object, + method: methodName, + index: index, + parse: false, + required: false, + }); + }; +} diff --git a/packages/core/src/decorators/Delete.ts b/packages/core/src/decorators/Delete.ts new file mode 100644 index 00000000..52504525 --- /dev/null +++ b/packages/core/src/decorators/Delete.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers a controller method to be executed when DELETE request comes on a given route. + * Must be applied on a controller action. + */ +export function Delete(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers a controller method to be executed when DELETE request comes on a given route. + * Must be applied on a controller action. + */ +export function Delete(route?: string, options?: HandlerOptions): Function; + +/** + * Registers a controller method to be executed when DELETE request comes on a given route. + * Must be applied on a controller action. + */ +export function Delete(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "delete", + target: object.constructor, + method: methodName, + route: route, + options, + }); + }; +} diff --git a/packages/core/src/decorators/EnableAutoConfiguration.ts b/packages/core/src/decorators/EnableAutoConfiguration.ts index 78a20a71..8e994fc7 100644 --- a/packages/core/src/decorators/EnableAutoConfiguration.ts +++ b/packages/core/src/decorators/EnableAutoConfiguration.ts @@ -35,47 +35,3 @@ async function instantiateClasses(rootDir, classes) { } } } - -// FIXME NOT WORKING YET -export function EnableAutoConfiguration(): Function { - return async function (target: Function) { - const rootDir = getProjectRootDirectory(); - - const configFile = path.join(rootDir, "nodeBoot-info.json"); - - if (fs.existsSync(configFile)) { - const configContent = fs.readFileSync(configFile, "utf-8"); - const config = JSON.parse(configContent); - - /*if (config.NodeBootExpressApplication) { - requireFiles(rootDir, [config.NodeBootExpressApplication]); - }*/ - - /* if (config.NodeBootKoaApplication) { - requireFiles(rootDir, [config.NodeBootKoaApplication]); - }*/ - - if (config.Configurations && Array.isArray(config.Configurations)) { - await instantiateClasses(rootDir, config.Configurations); - } - - if (config.ConfigurationProperties && Array.isArray(config.ConfigurationProperties)) { - await instantiateClasses(rootDir, config.ConfigurationProperties); - } - - if (config.Controllers && Array.isArray(config.Controllers)) { - await instantiateClasses(rootDir, config.Controllers); - } - - if (config.JsonControllers && Array.isArray(config.JsonControllers)) { - await instantiateClasses(rootDir, config.JsonControllers); - } - - if (config.Services && Array.isArray(config.Services)) { - await instantiateClasses(rootDir, config.Services); - } - } else { - console.error("nodeBoot-info.json not found in the root directory."); - } - }; -} diff --git a/packages/core/src/decorators/EnableComponentScan.ts b/packages/core/src/decorators/EnableComponentScan.ts index bd822013..11502f05 100644 --- a/packages/core/src/decorators/EnableComponentScan.ts +++ b/packages/core/src/decorators/EnableComponentScan.ts @@ -12,32 +12,21 @@ export function EnableComponentScan(options?: ComponentScanOptions): Function { const srcDir = __dirname.substring(0, __dirname.indexOf("/src")) + "/dist"; if (options.controllerPaths) { - ApplicationContext.get().controllerClasses = getClassesFromPaths( - options.controllerPaths, - srcDir, - ); + ApplicationContext.get().controllerClasses = getClassesFromPaths(options.controllerPaths, srcDir); } if (options.interceptorPaths) { - ApplicationContext.get().interceptorClasses = getClassesFromPaths( - options.interceptorPaths, - srcDir, - ); + ApplicationContext.get().interceptorClasses = getClassesFromPaths(options.interceptorPaths, srcDir); } if (options.middlewarePaths) { - ApplicationContext.get().globalMiddlewares = getClassesFromPaths( - options.middlewarePaths, - srcDir, - ); + ApplicationContext.get().globalMiddlewares = getClassesFromPaths(options.middlewarePaths, srcDir); } }; } function getClassesFromPaths(componentPaths: string[], srcDir: string) { - const paths = componentPaths.map(componentPath => - path.join(srcDir, `${componentPath}/**/*.js`), - ); + const paths = componentPaths.map(componentPath => path.join(srcDir, `${componentPath}/**/*.js`)); return importClassesFromDirectories(paths); } @@ -45,10 +34,7 @@ function getClassesFromPaths(componentPaths: string[], srcDir: string) { /** * Loads all exported classes from the given directory. */ -export function importClassesFromDirectories( - directories: string[], - formats = [".js", ".ts", ".tsx"], -): Function[] { +export function importClassesFromDirectories(directories: string[], formats = [".js", ".ts", ".tsx"]): Function[] { const loadFileClasses = function (exported: any, allLoaded: Function[]) { if (exported instanceof Function) { allLoaded.push(exported); diff --git a/packages/core/src/decorators/ErrorHandler.ts b/packages/core/src/decorators/ErrorHandler.ts index 563c4e52..8a9731cb 100644 --- a/packages/core/src/decorators/ErrorHandler.ts +++ b/packages/core/src/decorators/ErrorHandler.ts @@ -1,18 +1,12 @@ -import {ExpressErrorMiddlewareInterface} from "routing-controllers"; import {Middleware} from "./Middleware"; -import {ApplicationContext} from "@node-boot/context"; -import {FastifyErrorHandlerInterface} from "../middlewares"; +import {ApplicationContext, ErrorHandlerInterface} from "@node-boot/context"; /** * Marks given class as an ErrorHandler Middleware. * Allows to create global Error handler. */ -export function ErrorHandler< - T extends new (...args: any[]) => - | ExpressErrorMiddlewareInterface - | FastifyErrorHandlerInterface, ->() { - return (target: T) => { +export function ErrorHandler ErrorHandlerInterface>() { + return (target: THandler) => { ApplicationContext.get().applicationOptions.customErrorHandler = true; Middleware({type: "after"})(target); }; diff --git a/packages/core/src/decorators/Get.ts b/packages/core/src/decorators/Get.ts new file mode 100644 index 00000000..e62804f6 --- /dev/null +++ b/packages/core/src/decorators/Get.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when GET request comes on a given route. + * Must be applied on a controller action. + */ +export function Get(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when GET request comes on a given route. + * Must be applied on a controller action. + */ +export function Get(route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when GET request comes on a given route. + * Must be applied on a controller action. + */ +export function Get(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "get", + target: object.constructor, + method: methodName, + options, + route, + }); + }; +} diff --git a/packages/core/src/decorators/Head.ts b/packages/core/src/decorators/Head.ts new file mode 100644 index 00000000..d3ad965a --- /dev/null +++ b/packages/core/src/decorators/Head.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when HEAD request comes on a given route. + * Must be applied on a controller action. + */ +export function Head(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when HEAD request comes on a given route. + * Must be applied on a controller action. + */ +export function Head(route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when HEAD request comes on a given route. + * Must be applied on a controller action. + */ +export function Head(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "head", + target: object.constructor, + method: methodName, + options, + route, + }); + }; +} diff --git a/packages/core/src/decorators/Header.ts b/packages/core/src/decorators/Header.ts new file mode 100644 index 00000000..7117211e --- /dev/null +++ b/packages/core/src/decorators/Header.ts @@ -0,0 +1,17 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Sets response header. + * Must be applied on a controller action. + */ +export function Header(name: string, value: string): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "header", + target: object.constructor, + method: methodName, + value: name, + secondaryValue: value, + }); + }; +} diff --git a/packages/core/src/decorators/HeaderParam.ts b/packages/core/src/decorators/HeaderParam.ts new file mode 100644 index 00000000..58f9685d --- /dev/null +++ b/packages/core/src/decorators/HeaderParam.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects a request's http header value to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function HeaderParam(name: string, options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "header", + object: object, + method: methodName, + index: index, + name: name, + parse: options?.parse ?? false, + required: options?.required ?? false, + classTransform: options?.transform ?? undefined, + explicitType: options?.type ?? undefined, + validate: options?.validate ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/HeaderParams.ts b/packages/core/src/decorators/HeaderParams.ts new file mode 100644 index 00000000..cd9154e1 --- /dev/null +++ b/packages/core/src/decorators/HeaderParams.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects all request's http headers to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function HeaderParams(): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "headers", + object: object, + method: methodName, + index: index, + parse: false, + required: false, + }); + }; +} diff --git a/packages/core/src/decorators/HttpCode.ts b/packages/core/src/decorators/HttpCode.ts new file mode 100644 index 00000000..a0ff865c --- /dev/null +++ b/packages/core/src/decorators/HttpCode.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Sets response HTTP status code. + * Http code will be set only when controller action is successful. + * In the case if controller action rejects or throws an exception http code won't be applied. + * Must be applied on a controller action. + */ +export function HttpCode(code: number): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "success-code", + target: object.constructor, + method: methodName, + value: code, + }); + }; +} diff --git a/packages/core/src/decorators/Interceptor.ts b/packages/core/src/decorators/Interceptor.ts index 6f487b2f..4941d175 100644 --- a/packages/core/src/decorators/Interceptor.ts +++ b/packages/core/src/decorators/Interceptor.ts @@ -1,16 +1,21 @@ -import {Interceptor as InnerInterceptor} from "routing-controllers"; import {decorateDi} from "@node-boot/di"; +import {NodeBootToolkit} from "@node-boot/engine"; /** - * Registers a global interceptor.. + * Registers a global interceptor. * - * @param args Arguments for routing-controllers @Interceptor decorator: + * @param options Arguments for @Interceptor decorator: *
- priority Middleware priority in the chain */ -export function Interceptor(...args: Parameters) { +export function Interceptor(options?: {priority?: number}) { return (target: TFunction) => { // DI is optional and the decorator will only be applied if the DI container dependency is available. decorateDi(target); - InnerInterceptor(...args)(target); + // Registering the interceptor + NodeBootToolkit.getMetadataArgsStorage().interceptors.push({ + target: target, + global: true, + priority: options?.priority ?? 0, + }); }; } diff --git a/packages/core/src/decorators/JsonController.ts b/packages/core/src/decorators/JsonController.ts new file mode 100644 index 00000000..55e21dc7 --- /dev/null +++ b/packages/core/src/decorators/JsonController.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ControllerOptions} from "@node-boot/context"; + +/** + * Defines a class as a JSON controller. If JSON controller is used, then all controller actions will return + * a serialized json data, and its response content-type always will be application/json. + * + * @param baseRoute Extra path you can apply as a base route to all controller actions + * @param options Extra options that apply to all controller actions + * + * @deprecated + * FIXME move the json behaviour to the @Controller and delete this decorator + */ +export function JsonController(baseRoute?: string, options?: ControllerOptions) { + return function (object: Function) { + NodeBootToolkit.getMetadataArgsStorage().controllers.push({ + type: "json", + target: object, + route: baseRoute, + options, + }); + }; +} diff --git a/packages/core/src/decorators/Location.ts b/packages/core/src/decorators/Location.ts new file mode 100644 index 00000000..5fdb49a3 --- /dev/null +++ b/packages/core/src/decorators/Location.ts @@ -0,0 +1,16 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Sets Location header with given value to the response. + * Must be applied on a controller action. + */ +export function Location(url: string): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "location", + target: object.constructor, + method: methodName, + value: url, + }); + }; +} diff --git a/packages/core/src/decorators/Method.ts b/packages/core/src/decorators/Method.ts new file mode 100644 index 00000000..73df057b --- /dev/null +++ b/packages/core/src/decorators/Method.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ActionType, HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when request with specified method comes on a given route. + * Must be applied on a controller action. + */ +export function Method(method: ActionType, route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when request with specified method comes on a given route. + * Must be applied on a controller action. + */ +export function Method(method: ActionType, route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when request with specified method comes on a given route. + * Must be applied on a controller action. + */ +export function Method(method: ActionType, route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: method, + target: object.constructor, + method: methodName, + options, + route, + }); + }; +} diff --git a/packages/core/src/decorators/Middleware.ts b/packages/core/src/decorators/Middleware.ts index a348ad5f..6bfec4c5 100644 --- a/packages/core/src/decorators/Middleware.ts +++ b/packages/core/src/decorators/Middleware.ts @@ -1,18 +1,24 @@ -import {Middleware as InnerMiddleware} from "routing-controllers"; import {decorateDi} from "@node-boot/di"; +import {NodeBootToolkit} from "@node-boot/engine"; /** * Marks given class as a middleware. * Allows to create global middlewares and control order of middleware execution. * - * @param args Arguments for routing-controllers @Middleware decorator: + * @param options Arguments for routing-controllers @Middleware decorator: *
- type Type of decorator. before for inbound and after for outbound middleware. *
- priority Middleware priority in the chain */ -export function Middleware(...args: Parameters) { +export function Middleware(options: {type: "after" | "before"; priority?: number}) { return (target: TFunction) => { // DI is optional and the decorator will only be applied if the DI container dependency is available. decorateDi(target); - InnerMiddleware(...args)(target); + + NodeBootToolkit.getMetadataArgsStorage().middlewares.push({ + target: target, + global: true, + type: options?.type ?? "before", + priority: options?.priority ?? 0, + }); }; } diff --git a/packages/core/src/decorators/NodeBootApplication.ts b/packages/core/src/decorators/NodeBootApplication.ts index acd06223..f1e39565 100644 --- a/packages/core/src/decorators/NodeBootApplication.ts +++ b/packages/core/src/decorators/NodeBootApplication.ts @@ -1,5 +1,4 @@ -import {ApplicationAdapter, ApplicationContext, ApplicationOptions} from "@node-boot/context"; -import {RoutingControllersOptions} from "routing-controllers"; +import {ApplicationAdapter, ApplicationContext, ApplicationOptions, NodeBootEngineOptions} from "@node-boot/context"; import {BeansConfigurationAdapter} from "../adapters"; /** @@ -22,13 +21,10 @@ export function NodeBootApplication(options?: ApplicationOptions): Function { // Bind Application Adapter context.applicationAdapter = new (class implements ApplicationAdapter { - bind(): RoutingControllersOptions { + bind(): NodeBootEngineOptions { const context = ApplicationContext.get(); - if ( - context.applicationOptions.customErrorHandler && - context.applicationOptions.defaultErrorHandler - ) { + if (context.applicationOptions.customErrorHandler && context.applicationOptions.defaultErrorHandler) { throw new Error( `Invalid configurations: 'defaultErrorHandler' cannot be enabled if an @ErrorHandler is provided. Please disable defaultErrorHandler or delete the custom @ErrorHandler.`, ); @@ -42,8 +38,7 @@ export function NodeBootApplication(options?: ApplicationOptions): Function { defaults: { nullResultCode: context.applicationOptions.apiOptions?.nullResultCode, paramOptions: context.applicationOptions.apiOptions?.paramOptions, - undefinedResultCode: - context.applicationOptions.apiOptions?.undefinedResultCode, + undefinedResultCode: context.applicationOptions.apiOptions?.undefinedResultCode, }, classTransformer: context.classTransformer, classToPlainTransformOptions: context.classToPlainTransformOptions, diff --git a/packages/core/src/decorators/OnNull.ts b/packages/core/src/decorators/OnNull.ts new file mode 100644 index 00000000..78d608cd --- /dev/null +++ b/packages/core/src/decorators/OnNull.ts @@ -0,0 +1,28 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to null. + * Must be applied on a controller action. + */ +export function OnNull(code: number): Function; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to null. + * Must be applied on a controller action. + */ +export function OnNull(error: Function): Function; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to null. + * Must be applied on a controller action. + */ +export function OnNull(codeOrError: number | Function): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "on-null", + target: object.constructor, + method: methodName, + value: codeOrError, + }); + }; +} diff --git a/packages/core/src/decorators/OnUndefined.ts b/packages/core/src/decorators/OnUndefined.ts new file mode 100644 index 00000000..f1636044 --- /dev/null +++ b/packages/core/src/decorators/OnUndefined.ts @@ -0,0 +1,28 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to undefined. + * Must be applied on a controller action. + */ +export function OnUndefined(code: number): Function; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to undefined. + * Must be applied on a controller action. + */ +export function OnUndefined(error: Function): Function; + +/** + * Used to set specific HTTP status code when result returned by a controller action is equal to undefined. + * Must be applied on a controller action. + */ +export function OnUndefined(codeOrError: number | Function): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "on-undefined", + target: object.constructor, + method: methodName, + value: codeOrError, + }); + }; +} diff --git a/packages/core/src/decorators/Param.ts b/packages/core/src/decorators/Param.ts new file mode 100644 index 00000000..0173f7a9 --- /dev/null +++ b/packages/core/src/decorators/Param.ts @@ -0,0 +1,20 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects a request's route parameter value to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Param(name: string): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "param", + object: object, + method: methodName, + index: index, + name: name, + parse: false, // it does not make sense for Param to be parsed + required: true, // params are always required, because if they are missing router will not match the route + classTransform: undefined, + }); + }; +} diff --git a/packages/core/src/decorators/Params.ts b/packages/core/src/decorators/Params.ts new file mode 100644 index 00000000..6db4aff5 --- /dev/null +++ b/packages/core/src/decorators/Params.ts @@ -0,0 +1,22 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects all request's route parameters to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Params(options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "params", + object: object, + method: methodName, + index: index, + parse: options?.parse ?? false, + required: options?.required ?? false, + classTransform: options?.transform ?? undefined, + explicitType: options?.type ?? undefined, + validate: options?.validate ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/Patch.ts b/packages/core/src/decorators/Patch.ts new file mode 100644 index 00000000..3e5e671a --- /dev/null +++ b/packages/core/src/decorators/Patch.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when PATCH request comes on a given route. + * Must be applied on a controller action. + */ +export function Patch(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when PATCH request comes on a given route. + * Must be applied on a controller action. + */ +export function Patch(route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when PATCH request comes on a given route. + * Must be applied on a controller action. + */ +export function Patch(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "patch", + target: object.constructor, + method: methodName, + route: route, + options, + }); + }; +} diff --git a/packages/core/src/decorators/Post.ts b/packages/core/src/decorators/Post.ts new file mode 100644 index 00000000..c8e837ac --- /dev/null +++ b/packages/core/src/decorators/Post.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when POST request comes on a given route. + * Must be applied on a controller action. + */ +export function Post(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when POST request comes on a given route. + * Must be applied on a controller action. + */ +export function Post(route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when POST request comes on a given route. + * Must be applied on a controller action. + */ +export function Post(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "post", + target: object.constructor, + method: methodName, + options, + route, + }); + }; +} diff --git a/packages/core/src/decorators/Put.ts b/packages/core/src/decorators/Put.ts new file mode 100644 index 00000000..d463a53a --- /dev/null +++ b/packages/core/src/decorators/Put.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {HandlerOptions} from "@node-boot/context"; + +/** + * Registers an action to be executed when PUT request comes on a given route. + * Must be applied on a controller action. + */ +export function Put(route?: RegExp, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when PUT request comes on a given route. + * Must be applied on a controller action. + */ +export function Put(route?: string, options?: HandlerOptions): Function; + +/** + * Registers an action to be executed when PUT request comes on a given route. + * Must be applied on a controller action. + */ +export function Put(route?: string | RegExp, options?: HandlerOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().actions.push({ + type: "put", + target: object.constructor, + method: methodName, + route: route, + options, + }); + }; +} diff --git a/packages/core/src/decorators/QueryParam.ts b/packages/core/src/decorators/QueryParam.ts new file mode 100644 index 00000000..4f8e9a4a --- /dev/null +++ b/packages/core/src/decorators/QueryParam.ts @@ -0,0 +1,24 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects a request's query parameter value to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function QueryParam(name: string, options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "query", + object: object, + method: methodName, + index: index, + name: name, + parse: options?.parse ?? false, + required: options?.required ?? false, + classTransform: options?.transform ?? undefined, + explicitType: options?.type ?? undefined, + validate: options?.validate ?? undefined, + isArray: options?.isArray ?? false, + }); + }; +} diff --git a/packages/core/src/decorators/QueryParams.ts b/packages/core/src/decorators/QueryParams.ts new file mode 100644 index 00000000..7b2cd8b2 --- /dev/null +++ b/packages/core/src/decorators/QueryParams.ts @@ -0,0 +1,23 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects all request's query parameters to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function QueryParams(options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "queries", + object: object, + method: methodName, + index: index, + name: "", + parse: options?.parse ?? false, + required: options?.required ?? false, + classTransform: options?.transform ?? undefined, + explicitType: options?.type ?? undefined, + validate: options?.validate ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/Redirect.ts b/packages/core/src/decorators/Redirect.ts new file mode 100644 index 00000000..bf81b4f2 --- /dev/null +++ b/packages/core/src/decorators/Redirect.ts @@ -0,0 +1,16 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Sets Redirect header with given value to the response. + * Must be applied on a controller action. + */ +export function Redirect(url: string): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "redirect", + target: object.constructor, + method: methodName, + value: url, + }); + }; +} diff --git a/packages/core/src/decorators/Render.ts b/packages/core/src/decorators/Render.ts new file mode 100644 index 00000000..373f0cfa --- /dev/null +++ b/packages/core/src/decorators/Render.ts @@ -0,0 +1,16 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Specifies a template to be rendered by a controller action. + * Must be applied on a controller action. + */ +export function Render(template: string): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "rendered-template", + target: object.constructor, + method: methodName, + value: template, + }); + }; +} diff --git a/packages/core/src/decorators/Req.ts b/packages/core/src/decorators/Req.ts new file mode 100644 index 00000000..bfc7ec91 --- /dev/null +++ b/packages/core/src/decorators/Req.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects a Request object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Req(): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "request", + object: object, + method: methodName, + index: index, + parse: false, + required: false, + }); + }; +} diff --git a/packages/core/src/decorators/Res.ts b/packages/core/src/decorators/Res.ts new file mode 100644 index 00000000..68195e00 --- /dev/null +++ b/packages/core/src/decorators/Res.ts @@ -0,0 +1,18 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects a Response object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Res(): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "response", + object: object, + method: methodName, + index: index, + parse: false, + required: false, + }); + }; +} diff --git a/packages/core/src/decorators/ResponseClassTransformOptions.ts b/packages/core/src/decorators/ResponseClassTransformOptions.ts new file mode 100644 index 00000000..4584c1e4 --- /dev/null +++ b/packages/core/src/decorators/ResponseClassTransformOptions.ts @@ -0,0 +1,16 @@ +import {ClassTransformOptions} from "class-transformer"; +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Options to be set to class-transformer for the result of the response. + */ +export function ResponseClassTransformOptions(options: ClassTransformOptions): Function { + return function (object: Object, methodName: string) { + NodeBootToolkit.getMetadataArgsStorage().responseHandlers.push({ + type: "response-class-transform-options", + value: options, + target: object.constructor, + method: methodName, + }); + }; +} diff --git a/packages/core/src/decorators/Session.ts b/packages/core/src/decorators/Session.ts new file mode 100644 index 00000000..2d5006af --- /dev/null +++ b/packages/core/src/decorators/Session.ts @@ -0,0 +1,21 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects a Session object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function Session(options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "session", + object: object, + method: methodName, + index: index, + parse: false, // it makes no sense for Session object to be parsed as json + required: options?.required ?? true, + classTransform: options?.transform, + validate: options?.validate ?? false, + }); + }; +} diff --git a/packages/core/src/decorators/SessionParam.ts b/packages/core/src/decorators/SessionParam.ts new file mode 100644 index 00000000..be695cae --- /dev/null +++ b/packages/core/src/decorators/SessionParam.ts @@ -0,0 +1,22 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {ParamOptions} from "@node-boot/context"; + +/** + * Injects a Session object property to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function SessionParam(propertyName: string, options?: ParamOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "session-param", + object: object, + method: methodName, + index: index, + name: propertyName, + parse: false, // it makes no sense for Session object to be parsed as json + required: options?.required ?? false, + classTransform: options?.transform, + validate: options?.validate ?? false, + }); + }; +} diff --git a/packages/core/src/decorators/State.ts b/packages/core/src/decorators/State.ts new file mode 100644 index 00000000..14fa7175 --- /dev/null +++ b/packages/core/src/decorators/State.ts @@ -0,0 +1,20 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Injects a State object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function State(objectName?: string): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "state", + object: object, + method: methodName, + index: index, + name: objectName, + parse: false, // it does not make sense for Session to be parsed + required: true, // when we demand session object, it must exist (working session middleware) + classTransform: undefined, + }); + }; +} diff --git a/packages/core/src/decorators/UploadedFile.ts b/packages/core/src/decorators/UploadedFile.ts new file mode 100644 index 00000000..05bee1b7 --- /dev/null +++ b/packages/core/src/decorators/UploadedFile.ts @@ -0,0 +1,21 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {UploadOptions} from "@node-boot/context"; + +/** + * Injects an uploaded file object to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function UploadedFile(name: string, options?: UploadOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "file", + object: object, + method: methodName, + index: index, + name: name, + parse: false, + required: options?.required ?? false, + extraOptions: options?.options ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/UploadedFiles.ts b/packages/core/src/decorators/UploadedFiles.ts new file mode 100644 index 00000000..f447a9b6 --- /dev/null +++ b/packages/core/src/decorators/UploadedFiles.ts @@ -0,0 +1,21 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {UploadOptions} from "@node-boot/context"; + +/** + * Injects all uploaded files to the controller action parameter. + * Must be applied on a controller action parameter. + */ +export function UploadedFiles(name: string, options?: UploadOptions): Function { + return function (object: Object, methodName: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "files", + object: object, + method: methodName, + index: index, + name: name, + parse: false, + required: options?.required ?? false, + extraOptions: options?.options ?? undefined, + }); + }; +} diff --git a/packages/core/src/decorators/UseAfter.ts b/packages/core/src/decorators/UseAfter.ts new file mode 100644 index 00000000..14630de2 --- /dev/null +++ b/packages/core/src/decorators/UseAfter.ts @@ -0,0 +1,36 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Specifies a given middleware to be used for controller or controller action AFTER the action executes. + * Must be set to controller action or controller class. + */ +export function UseAfter(...middlewares: Array): Function; + +/** + * Specifies a given middleware to be used for controller or controller action AFTER the action executes. + * Must be set to controller action or controller class. + */ +export function UseAfter(...middlewares: Array<(context: any, next: () => Promise) => Promise>): Function; + +/** + * Specifies a given middleware to be used for controller or controller action AFTER the action executes. + * Must be set to controller action or controller class. + */ +export function UseAfter(...middlewares: Array<(request: any, response: any, next: Function) => any>): Function; + +/** + * Specifies a given middleware to be used for controller or controller action AFTER the action executes. + * Must be set to controller action or controller class. + */ +export function UseAfter(...middlewares: Array any)>): Function { + return function (objectOrFunction: Object | Function, methodName?: string) { + middlewares.forEach(middleware => { + NodeBootToolkit.getMetadataArgsStorage().uses.push({ + target: methodName ? objectOrFunction.constructor : (objectOrFunction as Function), + method: methodName, + middleware: middleware, + afterAction: true, + }); + }); + }; +} diff --git a/packages/core/src/decorators/UseBefore.ts b/packages/core/src/decorators/UseBefore.ts new file mode 100644 index 00000000..a95c4141 --- /dev/null +++ b/packages/core/src/decorators/UseBefore.ts @@ -0,0 +1,36 @@ +import {NodeBootToolkit} from "@node-boot/engine"; + +/** + * Specifies a given middleware to be used for controller or controller action BEFORE the action executes. + * Must be set to controller action or controller class. + */ +export function UseBefore(...middlewares: Array): Function; + +/** + * Specifies a given middleware to be used for controller or controller action BEFORE the action executes. + * Must be set to controller action or controller class. + */ +export function UseBefore(...middlewares: Array<(context: any, next: () => Promise) => Promise>): Function; + +/** + * Specifies a given middleware to be used for controller or controller action BEFORE the action executes. + * Must be set to controller action or controller class. + */ +export function UseBefore(...middlewares: Array<(request: any, response: any, next: Function) => any>): Function; + +/** + * Specifies a given middleware to be used for controller or controller action BEFORE the action executes. + * Must be set to controller action or controller class. + */ +export function UseBefore(...middlewares: Array any)>): Function { + return function (objectOrFunction: Object | Function, methodName?: string) { + middlewares.forEach(middleware => { + NodeBootToolkit.getMetadataArgsStorage().uses.push({ + target: methodName ? objectOrFunction.constructor : (objectOrFunction as Function), + method: methodName, + middleware: middleware, + afterAction: false, + }); + }); + }; +} diff --git a/packages/core/src/decorators/UseInterceptor.ts b/packages/core/src/decorators/UseInterceptor.ts new file mode 100644 index 00000000..6f73a183 --- /dev/null +++ b/packages/core/src/decorators/UseInterceptor.ts @@ -0,0 +1,30 @@ +import {NodeBootToolkit} from "@node-boot/engine"; +import {Action} from "@node-boot/context"; + +/** + * Specifies a given interceptor middleware or interceptor function to be used for controller or controller action. + * Must be set to controller action or controller class. + */ +export function UseInterceptor(...interceptors: Array): Function; + +/** + * Specifies a given interceptor middleware or interceptor function to be used for controller or controller action. + * Must be set to controller action or controller class. + */ +export function UseInterceptor(...interceptors: Array<(action: Action, result: any) => any>): Function; + +/** + * Specifies a given interceptor middleware or interceptor function to be used for controller or controller action. + * Must be set to controller action or controller class. + */ +export function UseInterceptor(...interceptors: Array any)>): Function { + return function (objectOrFunction: Object | Function, methodName?: string) { + interceptors.forEach(interceptor => { + NodeBootToolkit.getMetadataArgsStorage().useInterceptors.push({ + interceptor: interceptor, + target: methodName ? objectOrFunction.constructor : (objectOrFunction as Function), + method: methodName, + }); + }); + }; +} diff --git a/packages/core/src/decorators/index.ts b/packages/core/src/decorators/index.ts index 45b03078..863e77ed 100644 --- a/packages/core/src/decorators/index.ts +++ b/packages/core/src/decorators/index.ts @@ -1,17 +1,54 @@ +export * from "./All"; +export * from "./Bean"; +export * from "./Body"; +export * from "./BodyParam"; +export * from "./Component"; export * from "./Configuration"; export * from "./Configurations"; +export * from "./ContentType"; +export * from "./Controller"; export * from "./Controllers"; +export * from "./CookieParam"; +export * from "./CookieParams"; +export * from "./Ctx"; +export * from "./Delete"; export * from "./EnableAutoConfiguration"; export * from "./EnableClassTransformer"; export * from "./EnableComponentScan"; +export * from "./ErrorHandler"; +export * from "./Get"; export * from "./GlobalMiddlewares"; +export * from "./Head"; +export * from "./Header"; +export * from "./HeaderParam"; +export * from "./HeaderParams"; +export * from "./HttpCode"; +export * from "./Interceptor"; export * from "./Interceptors"; +export * from "./Location"; +export * from "./Method"; +export * from "./Middleware"; export * from "./NodeBootApplication"; -export * from "./EnableDI"; -export * from "./Bean"; -export * from "./Controller"; -export * from "./Component"; +export * from "./OnNull"; +export * from "./OnUndefined"; +export * from "./Param"; +export * from "./Params"; +export * from "./Patch"; +export * from "./Post"; +export * from "./Put"; +export * from "./QueryParam"; +export * from "./QueryParams"; +export * from "./Redirect"; +export * from "./Render"; +export * from "./Req"; +export * from "./Res"; +export * from "./ResponseClassTransformOptions"; export * from "./Service"; -export * from "./Middleware"; -export * from "./Middleware"; -export * from "./ErrorHandler"; +export * from "./Session"; +export * from "./SessionParam"; +export * from "./State"; +export * from "./UploadedFile"; +export * from "./UploadedFiles"; +export * from "./UseAfter"; +export * from "./UseBefore"; +export * from "./UseInterceptor"; diff --git a/packages/core/src/error/index.ts b/packages/core/src/error/index.ts deleted file mode 100644 index d325c6ca..00000000 --- a/packages/core/src/error/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./AccessDeniedError"; -export * from "./AuthorizationCheckerNotDefinedError"; -export * from "./AuthorizationRequiredError"; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 78766845..dcc120b3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -3,5 +3,3 @@ import "reflect-metadata"; export * from "./decorators"; export * from "./logger"; export * from "./server"; -export * from "./error"; -export * from "./middlewares"; diff --git a/packages/core/src/logger/winston.logger.ts b/packages/core/src/logger/winston.logger.ts index 42feb115..7750f44d 100644 --- a/packages/core/src/logger/winston.logger.ts +++ b/packages/core/src/logger/winston.logger.ts @@ -24,17 +24,13 @@ export function getVoidLogger(): winston.Logger { * * @public */ -export function createRootLogger( - options: winston.LoggerOptions = {}, - env = process.env, -): winston.Logger { +export function createRootLogger(options: winston.LoggerOptions = {}, env = process.env): winston.Logger { return winston .createLogger( merge( { level: env["LOG_LEVEL"] || "info", - format: - env["NODE_ENV"] === "production" ? winston.format.json() : colorFormat(), + format: env["NODE_ENV"] === "production" ? winston.format.json() : colorFormat(), transports: [ new winston.transports.Console({ silent: env["JEST_WORKER_ID"] !== undefined && !env["LOG_LEVEL"], @@ -80,11 +76,7 @@ function colorFormat(): Format { export const createLogger = (service: string, platform: string) => { const logger = createRootLogger(); - logger.format = winston.format.combine( - winston.format.timestamp(), - winston.format.splat(), - logger.format, - ); + logger.format = winston.format.combine(winston.format.timestamp(), winston.format.splat(), logger.format); logger.defaultMeta = { service, diff --git a/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts b/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts deleted file mode 100644 index f6b2d078..00000000 --- a/packages/core/src/middlewares/FastifyErrorHandlerInterface.ts +++ /dev/null @@ -1,9 +0,0 @@ -/** - * n Node.js, uncaught errors are likely to cause memory leaks, file descriptor leaks, and other major production issues. - * Domains were a failed attempt to fix this. - * - * Given that it is not possible to process all uncaught errors sensibly, the best way to deal with them is to crash. - */ -export interface FastifyErrorHandlerInterface { - error(request: TRequest, reply: TReply, error: TError): any; -} diff --git a/packages/core/src/middlewares/FastifyErrorMiddlewareInterface.ts b/packages/core/src/middlewares/FastifyErrorMiddlewareInterface.ts deleted file mode 100644 index 694a13c7..00000000 --- a/packages/core/src/middlewares/FastifyErrorMiddlewareInterface.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Fastify error middlewares can implement this interface. - * - * This hook is useful if you need to do some custom error logging or add some specific header in case of error. - * It is not intended for changing the error, and calling reply.send will throw an exception. - * This hook will be executed only after the customErrorHandler has been executed, and only if the customErrorHandler sends an error back to the user (Note that the default customErrorHandler always sends the error back to the user). - * Notice: unlike the other hooks, pass an error to the done function is not supported. - */ -export interface FastifyErrorMiddlewareInterface { - useError( - request: TRequest, - reply: TReply, - error: TError, - done: () => void, - ): any; -} diff --git a/packages/core/src/middlewares/FastifyMiddlewareInterface.ts b/packages/core/src/middlewares/FastifyMiddlewareInterface.ts deleted file mode 100644 index 9b0689dc..00000000 --- a/packages/core/src/middlewares/FastifyMiddlewareInterface.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Used to register middlewares. - * This signature is used for Fastify hooks. - */ -export interface FastifyMiddlewareInterface { - /** - * Called before controller action is being executed. - * This signature is used for Fastify hooks. - */ - use(request: TRequest, reply: TReply, done: TDone, payload?: any): any; -} diff --git a/packages/core/src/middlewares/index.ts b/packages/core/src/middlewares/index.ts deleted file mode 100644 index e539577a..00000000 --- a/packages/core/src/middlewares/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./FastifyErrorHandlerInterface"; -export * from "./FastifyMiddlewareInterface"; -export * from "./FastifyErrorMiddlewareInterface"; diff --git a/packages/core/src/properties/CorsProperties.ts b/packages/core/src/properties/CorsProperties.ts index 1da1e02a..c951b5df 100644 --- a/packages/core/src/properties/CorsProperties.ts +++ b/packages/core/src/properties/CorsProperties.ts @@ -8,13 +8,7 @@ export type CorsProperties = { /** * Configures the Fastify Lifecycle Hook. */ - hook?: - | "onRequest" - | "preParsing" - | "preValidation" - | "preHandler" - | "preSerialization" - | "onSend"; + hook?: "onRequest" | "preParsing" | "preValidation" | "preHandler" | "preSerialization" | "onSend"; /** * Configures the Access-Control-Allow-Origin CORS header. */ diff --git a/packages/core/src/server/BaseServer.ts b/packages/core/src/server/BaseServer.ts index 5e9f4bb4..ba4aaebe 100644 --- a/packages/core/src/server/BaseServer.ts +++ b/packages/core/src/server/BaseServer.ts @@ -1,8 +1,7 @@ -import {ApiOptions, ApplicationContext, Config} from "@node-boot/context"; +import {ApiOptions, ApplicationContext, Config, useContainer} from "@node-boot/context"; import {Logger} from "winston"; import {createLogger} from "../logger"; import {ConfigService, loadNodeBootConfig} from "@node-boot/config"; -import {useContainer} from "routing-controllers"; import {ApplicationOptions} from "@node-boot/context/src"; export abstract class BaseServer { @@ -85,19 +84,12 @@ export abstract class BaseServer { const apiConfigs = this.config.getOptional("node-boot.api"); context.applicationOptions = { - environment: - context.applicationOptions?.environment ?? appConfigs?.environment ?? "development", + environment: context.applicationOptions?.environment ?? appConfigs?.environment ?? "development", port: context.applicationOptions?.port ?? appConfigs?.port ?? 3000, platform: context.applicationOptions?.platform ?? appConfigs?.platform ?? "node-boot", name: context.applicationOptions?.name ?? appConfigs?.name ?? "node-boot-app", - defaultErrorHandler: - context.applicationOptions?.defaultErrorHandler ?? - appConfigs?.defaultErrorHandler ?? - false, - customErrorHandler: - context.applicationOptions?.customErrorHandler ?? - appConfigs?.customErrorHandler ?? - false, + defaultErrorHandler: context.applicationOptions?.defaultErrorHandler ?? appConfigs?.defaultErrorHandler ?? false, + customErrorHandler: context.applicationOptions?.customErrorHandler ?? appConfigs?.customErrorHandler ?? false, apiOptions: context.applicationOptions.apiOptions ?? apiConfigs, }; } @@ -113,10 +105,7 @@ export abstract class BaseServer { } private async initLogger(context: ApplicationContext) { - this.logger = createLogger( - context.applicationOptions.name!, - context.applicationOptions.platform!, - ); + this.logger = createLogger(context.applicationOptions.name!, context.applicationOptions.platform!); this.logger.info(`Initializing Node-Boot logger`); context.diOptions?.iocContainer.set(Logger, this.logger); context.diOptions?.iocContainer.set("logger", this.logger); diff --git a/packages/core/src/decorators/EnableDI.ts b/packages/di/src/decorators/EnableDI.ts similarity index 55% rename from packages/core/src/decorators/EnableDI.ts rename to packages/di/src/decorators/EnableDI.ts index ded5ed86..d288d783 100644 --- a/packages/core/src/decorators/EnableDI.ts +++ b/packages/di/src/decorators/EnableDI.ts @@ -1,5 +1,4 @@ -import {ApplicationContext, IocContainer} from "@node-boot/context"; -import {UseContainerOptions} from "routing-controllers/types/container"; +import {ApplicationContext, IocContainer, UseContainerOptions} from "@node-boot/context"; /** * Defines the IOC container to use for Dependency-injection @@ -7,10 +6,7 @@ import {UseContainerOptions} from "routing-controllers/types/container"; * @param iocContainer The IOC container to be used * @param options Extra options for the IOC container */ -export function EnableDI( - iocContainer: IocContainer, - options?: UseContainerOptions, -): Function { +export function EnableDI(iocContainer: IocContainer, options?: UseContainerOptions): Function { return function (object: Function) { ApplicationContext.get().diOptions = { iocContainer, diff --git a/packages/di/src/decorators/Inject.ts b/packages/di/src/decorators/Inject.ts index 8b717589..9af7dc02 100644 --- a/packages/di/src/decorators/Inject.ts +++ b/packages/di/src/decorators/Inject.ts @@ -17,8 +17,7 @@ export function Inject(options?: InjectionOptions): Function { // Registering metadata for custom filed injection (used for example in the Persistence Event Subscribers) if (propertyName && typeof propertyName === "string") { const propertyType = Reflect.getMetadata("design:type", target, propertyName); - const injectProperties: string[] = - Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, target) || []; + const injectProperties: string[] = Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, target) || []; injectProperties.push(propertyName); Reflect.defineMetadata(REQUIRES_FIELD_INJECTION_KEY, injectProperties, target); } diff --git a/packages/di/src/decorators/index.ts b/packages/di/src/decorators/index.ts index e6f1fc1c..83f2ba01 100644 --- a/packages/di/src/decorators/index.ts +++ b/packages/di/src/decorators/index.ts @@ -1 +1,2 @@ export * from "./Inject"; +export * from "./EnableDI"; diff --git a/packages/di/src/ioc/makeDiDecoration.ts b/packages/di/src/ioc/makeDiDecoration.ts index 9f98e280..b4772855 100644 --- a/packages/di/src/ioc/makeDiDecoration.ts +++ b/packages/di/src/ioc/makeDiDecoration.ts @@ -35,9 +35,7 @@ function decorateInversify(target: TFunction): boolean { decorated = true; } catch (error) { // Inversify is not available - console.warn( - "@injectable decorator is only applied if 'Inversify' dependency is available!", - ); + console.warn("@injectable decorator is only applied if 'Inversify' dependency is available!"); decorated = false; } return decorated; diff --git a/packages/di/src/ioc/makeInjectionDecoration.ts b/packages/di/src/ioc/makeInjectionDecoration.ts index 2fc65570..497920e8 100644 --- a/packages/di/src/ioc/makeInjectionDecoration.ts +++ b/packages/di/src/ioc/makeInjectionDecoration.ts @@ -3,27 +3,14 @@ import {InjectionOptions} from "./types"; /** * Apply proper @Inject decorator if dependency injection framework is available * */ -export function decorateInjection( - target: Object, - propertyName: string | Symbol, - index?: number, - options?: InjectionOptions, -): boolean { - return ( - decorateTypeDi(target, propertyName, index, options) || - decorateInversify(target, propertyName, index) - ); +export function decorateInjection(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { + return decorateTypeDi(target, propertyName, index, options) || decorateInversify(target, propertyName, index); } /** * Apply @Inject decorator if TypeDI framework is available * */ -function decorateTypeDi( - target: Object, - propertyName: string | Symbol, - index?: number, - options?: InjectionOptions, -): boolean { +function decorateTypeDi(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { let decorated: boolean; try { const {Inject} = require("typedi"); @@ -44,12 +31,7 @@ function decorateTypeDi( /** * Apply @inject decorator if Inversify framework is available * */ -function decorateInversify( - target: Object, - propertyName: string | Symbol, - index?: number, - options?: InjectionOptions, -): boolean { +function decorateInversify(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { let decorated: boolean; try { const {inject} = require("inversify"); diff --git a/packages/engine/.eslintignore b/packages/engine/.eslintignore new file mode 100644 index 00000000..5d3719c6 --- /dev/null +++ b/packages/engine/.eslintignore @@ -0,0 +1,3 @@ +# Generated files +node_modules +dist diff --git a/packages/engine/.lintstagedrc.js b/packages/engine/.lintstagedrc.js new file mode 100644 index 00000000..85ad482a --- /dev/null +++ b/packages/engine/.lintstagedrc.js @@ -0,0 +1,5 @@ +const baseConfig = require("../../.lintstagedrc.js"); + +module.exports = { + ...baseConfig, +}; diff --git a/packages/engine/.prettierignore b/packages/engine/.prettierignore new file mode 100644 index 00000000..319ebdd1 --- /dev/null +++ b/packages/engine/.prettierignore @@ -0,0 +1,4 @@ +# Generated files +pnpm-lock.yaml +node_modules +dist \ No newline at end of file diff --git a/packages/engine/LICENSE b/packages/engine/LICENSE new file mode 100644 index 00000000..b5d03778 --- /dev/null +++ b/packages/engine/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 NodeBoot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/engine/README.md b/packages/engine/README.md new file mode 100644 index 00000000..70e257a9 --- /dev/null +++ b/packages/engine/README.md @@ -0,0 +1,27 @@ +# Typescript example #2 + +The second typescript example for the Monorepo example + +## License + +MIT License + +Copyright (c) 2023 NodeBoot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/engine/jest.config.js b/packages/engine/jest.config.js new file mode 100644 index 00000000..ef8af537 --- /dev/null +++ b/packages/engine/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('jest').Config} */ +module.exports = { + transform: { + "^.+\\.(t|j)sx?$": "@swc/jest", + }, +}; diff --git a/packages/engine/nodemon.json b/packages/engine/nodemon.json new file mode 100644 index 00000000..d30a2c4c --- /dev/null +++ b/packages/engine/nodemon.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/nodemon.json", + "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], + "ignoreRoot": [], + "ext": "ts,js", + "exec": "pnpm typecheck && pnpm build" +} diff --git a/packages/engine/package.json b/packages/engine/package.json new file mode 100644 index 00000000..74fb8b76 --- /dev/null +++ b/packages/engine/package.json @@ -0,0 +1,42 @@ +{ + "name": "@node-boot/engine", + "version": "1.0.0", + "description": "Node-Boot engine that enables hte node-boot magic with application server frameworks through drivers", + "author": "Manuel Santos ", + "license": "MIT", + "keywords": [ + "driver", + "engine", + "core" + ], + "repository": { + "type": "git", + "url": "https://github.com/nodejs-boot/node-boot.git" + }, + "publishConfig": { + "access": "public" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "pnpm lint --fix", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "jest", + "typecheck": "tsc" + }, + "dependencies": { + "@node-boot/context": "1.0.0", + "@node-boot/error": "1.0.0", + "@node-boot/extension": "1.0.0" + }, + "peerDependencies": { + "class-transformer": "^0.5.1", + "class-validator": "^0.14.0" + }, + "optionalDependencies": {} +} diff --git a/packages/engine/src/core/NodeBootDriver.ts b/packages/engine/src/core/NodeBootDriver.ts new file mode 100644 index 00000000..add0ea8e --- /dev/null +++ b/packages/engine/src/core/NodeBootDriver.ts @@ -0,0 +1,197 @@ +import {ValidatorOptions} from "class-validator"; +import {ClassTransformOptions, instanceToPlain} from "class-transformer"; +import {HttpError} from "@node-boot/error"; +import { + Action, + ActionMetadata, + AuthorizationChecker, + CurrentUserChecker, + MiddlewareMetadata, + NodeBootEngineOptions, + ParamMetadata, +} from "@node-boot/context"; + +/** + * Base driver functionality for all other drivers. + * Abstract layer to organize controllers integration with different http server implementations. + */ +export abstract class NodeBootDriver { + /** + * Reference to the underlying framework app object. + */ + app: TServer; + + /** + * Indicates if class-transformer should be used or not. + */ + useClassTransformer: boolean; + + /** + * Indicates if class-validator should be used or not. + */ + enableValidation: boolean; + + /** + * Global class transformer options passed to class-transformer during classToPlain operation. + * This operation is being executed when server returns response to user. + */ + classToPlainTransformOptions?: ClassTransformOptions; + + /** + * Global class-validator options passed during validate operation. + */ + validationOptions: ValidatorOptions; + + /** + * Global class transformer options passed to class-transformer during plainToClass operation. + * This operation is being executed when parsing user parameters. + */ + plainToClassTransformOptions?: ClassTransformOptions; + + /** + * Indicates if default routing-controllers error handler should be used or not. + */ + isDefaultErrorHandlingEnabled: boolean; + + /** + * Indicates if routing-controllers should operate in development mode. + */ + developmentMode: boolean; + + /** + * Global application prefix. + */ + routePrefix = ""; + + /** + * Indicates if cors are enabled. + * This requires installation of additional module (cors for express and @koa/cors for koa). + */ + cors?: boolean | Object; + + /** + * Map of error overrides. + */ + errorOverridingMap: {[key: string]: any}; + + /** + * Special function used to check user authorization roles per request. + * Must return true or promise with boolean true resolved for authorization to succeed. + */ + authorizationChecker?: AuthorizationChecker; + + /** + * Special function used to get currently authorized user. + */ + currentUserChecker?: CurrentUserChecker; + + protected transformResult(result: any, actionMetadata: ActionMetadata, action: TAction): any { + // check if we need to transform result + const shouldTransform = + this.useClassTransformer && // transform only if class-transformer is enabled + actionMetadata.options?.transformResponse !== false && // don't transform if actionMetadata response transform is disabled + result instanceof Object && // don't transform primitive types (string/number/boolean) + !( + (result instanceof Uint8Array || result.pipe instanceof Function) // don't transform binary data // don't transform streams + ); + + // transform result if needed + if (shouldTransform) { + const options = actionMetadata.responseClassTransformOptions || this.classToPlainTransformOptions; + result = instanceToPlain(result, options); + } + + return result; + } + + protected processJsonError(error: any) { + if (!this.isDefaultErrorHandlingEnabled) return error; + + if (typeof error.toJSON === "function") return error.toJSON(); + + let processedError: any = {}; + if (error instanceof Error) { + const name = error.name && error.name !== "Error" ? error.name : error.constructor.name; + processedError.name = name; + + if (error.message) processedError.message = error.message; + if (error.stack && this.developmentMode) processedError.stack = error.stack; + + Object.keys(error) + .filter(key => key !== "stack" && key !== "name" && key !== "message" && (!(error instanceof HttpError) || key !== "httpCode")) + .forEach(key => (processedError[key] = (error as any)[key])); + + if (this.errorOverridingMap) + Object.keys(this.errorOverridingMap) + .filter(key => name === key) + .forEach(key => (processedError = this.merge(processedError, this.errorOverridingMap[key]))); + + return Object.keys(processedError).length > 0 ? processedError : undefined; + } + + return error; + } + + protected processTextError(error: any) { + if (!this.isDefaultErrorHandlingEnabled) return error; + + if (error instanceof Error) { + if (this.developmentMode && error.stack) { + return error.stack; + } else if (error.message) { + return error.message; + } + } + return error; + } + + protected merge(obj1: any, obj2: any): any { + const result: any = {}; + for (const i in obj1) { + if (i in obj2 && typeof obj1[i] === "object" && i !== null) { + result[i] = this.merge(obj1[i], obj2[i]); + } else { + result[i] = obj1[i]; + } + } + for (const i in obj2) { + result[i] = obj2[i]; + } + return result; + } + + /** + * Initializes the things driver needs before routes and middleware registration. + */ + abstract initialize(): void; + + /** + * Registers given middleware. + */ + abstract registerMiddleware(middleware: MiddlewareMetadata, options: NodeBootEngineOptions): void; + + /** + * Registers actionMetadata in the driver. + */ + abstract registerAction(actionMetadata: ActionMetadata, executeCallback: (action: TAction) => any): void; + + /** + * Registers all routes in the framework. + */ + abstract registerRoutes(): void; + + /** + * Gets param from the request. + */ + abstract getParamFromRequest(action: TAction, param: ParamMetadata): any; + + /** + * Defines an algorithm of how to handle error during executing controller actionMetadata. + */ + abstract handleError(error: any, actionMetadata: ActionMetadata, action: TAction): any; + + /** + * Defines an algorithm of how to handle success result of executing controller actionMetadata. + */ + abstract handleSuccess(result: any, actionMetadata: ActionMetadata, action: TAction): void; +} diff --git a/packages/engine/src/core/NodeBootEngine.ts b/packages/engine/src/core/NodeBootEngine.ts new file mode 100644 index 00000000..8d585c63 --- /dev/null +++ b/packages/engine/src/core/NodeBootEngine.ts @@ -0,0 +1,159 @@ +import {MetadataBuilder} from "../metadata/MetadataBuilder"; +import {Action, ActionMetadata, getFromContainer, InterceptorInterface, InterceptorMetadata, NodeBootEngineOptions} from "@node-boot/context"; +import {isPromiseLike, runInSequence} from "../util"; +import {NodeBootDriver} from "./NodeBootDriver"; +import {ActionParameterHandler} from "../service/ActionParameterHandler"; + +/** + * Registers controllers and middlewares in the given server framework. + */ +export class NodeBootEngine> { + /** + * Used to check and handle controller action parameters. + */ + private parameterHandler: ActionParameterHandler; + + /** + * Used to build metadata objects for controllers and middlewares. + */ + private metadataBuilder: MetadataBuilder; + + /** + * Global interceptors run on each controller action. + */ + private interceptors: InterceptorMetadata[] = []; + + constructor(private driver: TDriver, private options: NodeBootEngineOptions) { + this.parameterHandler = new ActionParameterHandler(driver); + this.metadataBuilder = new MetadataBuilder(options); + } + + /** + * Initializes the things driver needs before routes and middleware registration. + */ + initialize(): this { + this.driver.initialize(); + return this; + } + + /** + * Registers all given interceptors. + */ + registerInterceptors(classes?: Function[]): this { + const interceptors = this.metadataBuilder + .buildInterceptorMetadata(classes) + .sort((middleware1, middleware2) => middleware1.priority - middleware2.priority) + .reverse(); + this.interceptors.push(...interceptors); + return this; + } + + /** + * Registers all given controllers and actions from those controllers. + */ + registerControllers(classes?: Function[]): this { + const controllers = this.metadataBuilder.buildControllerMetadata(classes); + controllers.forEach(controller => { + controller.actions.forEach(actionMetadata => { + const interceptorFns = this.prepareInterceptors([ + ...this.interceptors, + ...actionMetadata.controllerMetadata.interceptors, + ...actionMetadata.interceptors, + ]); + this.driver.registerAction(actionMetadata, (action: Action) => { + return this.executeAction(actionMetadata, action, interceptorFns); + }); + }); + }); + this.driver.registerRoutes(); + return this; + } + + /** + * Registers post-execution middlewares in the driver. + */ + registerMiddlewares(type: "before" | "after", classes?: Function[]): this { + this.metadataBuilder + .buildMiddlewareMetadata(classes) + .filter(middleware => middleware.global && middleware.type === type) + .sort((middleware1, middleware2) => middleware2.priority - middleware1.priority) + .forEach(middleware => this.driver.registerMiddleware(middleware, this.options)); + + return this; + } + + /** + * Executes given controller action. + */ + protected executeAction(actionMetadata: ActionMetadata, action: Action, interceptorFns: Function[]) { + // compute all parameters + const paramsPromises = actionMetadata.params + .sort((param1, param2) => param1.index - param2.index) + .map(param => this.parameterHandler.handle(action, param)); + + // after all parameters are computed + return Promise.all(paramsPromises) + .then(params => { + // execute action and handle result + const allParams = actionMetadata.appendParams ? actionMetadata.appendParams(action).concat(params) : params; + const result = actionMetadata.methodOverride + ? actionMetadata.methodOverride(actionMetadata, action, allParams) + : actionMetadata.callMethod(allParams, action); + return this.handleCallMethodResult(result, actionMetadata, action, interceptorFns); + }) + .catch(error => { + // otherwise simply handle error without action execution + return this.driver.handleError(error, actionMetadata, action); + }); + } + + /** + * Handles result of the action method execution. + */ + protected handleCallMethodResult(result: any, action: ActionMetadata, options: Action, interceptorFns: Function[]): any { + if (isPromiseLike(result)) { + return result + .then((data: any) => { + return this.handleCallMethodResult(data, action, options, interceptorFns); + }) + .catch((error: any) => { + return this.driver.handleError(error, action, options); + }); + } else { + if (interceptorFns) { + const awaitPromise = runInSequence(interceptorFns, interceptorFn => { + const interceptedResult = interceptorFn(options, result); + if (isPromiseLike(interceptedResult)) { + return interceptedResult.then((resultFromPromise: any) => { + result = resultFromPromise; + }); + } else { + result = interceptedResult; + return Promise.resolve(); + } + }); + + return awaitPromise + .then(() => this.driver.handleSuccess(result, action, options)) + .catch(error => this.driver.handleError(error, action, options)); + } else { + return this.driver.handleSuccess(result, action, options); + } + } + } + + /** + * Creates interceptors from the given "use interceptors". + */ + protected prepareInterceptors(uses: InterceptorMetadata[]): Function[] { + return uses.map(use => { + if (use.interceptor.prototype && use.interceptor.prototype.intercept) { + // if this is function instance of InterceptorInterface + return function (action: Action, result: any) { + return getFromContainer(use.interceptor, action).intercept(action, result); + }; + } + return use.interceptor; + }); + } +} diff --git a/packages/engine/src/core/NodeBootToolkit.ts b/packages/engine/src/core/NodeBootToolkit.ts new file mode 100644 index 00000000..7756ed62 --- /dev/null +++ b/packages/engine/src/core/NodeBootToolkit.ts @@ -0,0 +1,104 @@ +import {MetadataArgsStorage} from "../metadata/MetadataArgsStorage"; +import {NodeBootEngine} from "./NodeBootEngine"; +import {ValidationOptions} from "class-validator"; +import {NodeBootDriver} from "./NodeBootDriver"; +import {ComponentImporter} from "../service/ComponentImporter"; +import {CustomParameterDecorator, NodeBootEngineOptions} from "@node-boot/context"; + +export class NodeBootToolkit { + /** + * Gets metadata args storage. + * Metadata args storage follows the best practices and stores metadata in a global variable. + */ + static getMetadataArgsStorage(): MetadataArgsStorage { + return MetadataArgsStorage.get(); + } + + /** + * Registers all loaded actions in your application using selected driver. + */ + static createServer>(driver: TDriver, options?: NodeBootEngineOptions): any { + NodeBootToolkit.createEngine(driver, options); + return driver.app; + } + + /** + * Registers all loaded actions in your express application. + */ + static createEngine>(driver: TDriver, options: NodeBootEngineOptions = {}): void { + // import all controllers, middlewares and error handlers + const controllerClasses = ComponentImporter.importControllers(options); + const middlewareClasses = ComponentImporter.importMiddlewares(options); + const interceptorClasses = ComponentImporter.importInterceptors(options); + + this.configureDriver(driver, options); + + // next create a controller executor + new NodeBootEngine(driver, options) + .initialize() + .registerInterceptors(interceptorClasses) + .registerMiddlewares("before", middlewareClasses) + .registerControllers(controllerClasses) + .registerMiddlewares("after", middlewareClasses); // todo: register only for loaded controllers? + } + + private static configureDriver>(driver: TDriver, options: NodeBootEngineOptions) { + if (options && options.development !== undefined) { + driver.developmentMode = options.development; + } else { + driver.developmentMode = process.env["NODE_ENV"] !== "production"; + } + + if (options.defaultErrorHandler !== undefined) { + driver.isDefaultErrorHandlingEnabled = options.defaultErrorHandler; + } else { + driver.isDefaultErrorHandlingEnabled = true; + } + + if (options.classTransformer !== undefined) { + driver.useClassTransformer = options.classTransformer; + } else { + driver.useClassTransformer = true; + } + + if (options.validation !== undefined) { + driver.enableValidation = !!options.validation; + if (typeof options.validation !== "boolean") { + driver.validationOptions = options.validation as ValidationOptions; + } + } else { + driver.enableValidation = true; + } + + driver.classToPlainTransformOptions = options.classToPlainTransformOptions; + driver.plainToClassTransformOptions = options.plainToClassTransformOptions; + + if (options.errorOverridingMap !== undefined) driver.errorOverridingMap = options.errorOverridingMap; + + if (options.routePrefix !== undefined) driver.routePrefix = options.routePrefix; + + if (options.currentUserChecker !== undefined) driver.currentUserChecker = options.currentUserChecker; + + if (options.authorizationChecker !== undefined) driver.authorizationChecker = options.authorizationChecker; + + driver.cors = options.cors; + } + + /** + * Registers custom parameter decorator used in the controller actions. + */ + static createParamDecorator(options: CustomParameterDecorator) { + return function (object: Object, method: string, index: number) { + NodeBootToolkit.getMetadataArgsStorage().params.push({ + type: "custom-converter", + name: "custom-param-decorator", + object: object, + method: method, + index: index, + parse: false, + required: options.required, + transform: options.value, + }); + }; + } +} diff --git a/packages/engine/src/core/index.ts b/packages/engine/src/core/index.ts new file mode 100644 index 00000000..8dc885e8 --- /dev/null +++ b/packages/engine/src/core/index.ts @@ -0,0 +1,3 @@ +export {NodeBootDriver} from "./NodeBootDriver"; +export {NodeBootEngine} from "./NodeBootEngine"; +export {NodeBootToolkit} from "./NodeBootToolkit"; diff --git a/packages/engine/src/index.ts b/packages/engine/src/index.ts new file mode 100644 index 00000000..b597944d --- /dev/null +++ b/packages/engine/src/index.ts @@ -0,0 +1,2 @@ +export * from "./core"; +export * from "./util"; diff --git a/packages/engine/src/metadata/MetadataArgsStorage.ts b/packages/engine/src/metadata/MetadataArgsStorage.ts new file mode 100644 index 00000000..7731cc5e --- /dev/null +++ b/packages/engine/src/metadata/MetadataArgsStorage.ts @@ -0,0 +1,158 @@ +import { + ActionMetadataArgs, + ControllerMetadataArgs, + InterceptorMetadataArgs, + MiddlewareMetadataArgs, + ParamMetadataArgs, + ResponseHandlerMetadataArgs, + UseInterceptorMetadataArgs, + UseMetadataArgs, +} from "@node-boot/context"; + +/** + * Storage all metadata read from decorators. + */ +export class MetadataArgsStorage { + /** + * Registered controller metadata args. + */ + controllers: ControllerMetadataArgs[] = []; + + /** + * Registered middleware metadata args. + */ + middlewares: MiddlewareMetadataArgs[] = []; + + /** + * Registered interceptor metadata args. + */ + interceptors: InterceptorMetadataArgs[] = []; + + /** + * Registered "use middleware" metadata args. + */ + uses: UseMetadataArgs[] = []; + + /** + * Registered "use interceptor" metadata args. + */ + useInterceptors: UseInterceptorMetadataArgs[] = []; + + /** + * Registered action metadata args. + */ + actions: ActionMetadataArgs[] = []; + + /** + * Registered param metadata args. + */ + params: ParamMetadataArgs[] = []; + + /** + * Registered response handler metadata args. + */ + responseHandlers: ResponseHandlerMetadataArgs[] = []; + + /** + * Filters registered middlewares by a given classes. + */ + filterMiddlewareMetadatasForClasses(classes: Function[]): MiddlewareMetadataArgs[] { + return classes + .map(cls => this.middlewares.find(middleware => middleware.target === cls)) + .filter(middlewareArgs => middlewareArgs !== undefined) as MiddlewareMetadataArgs[]; // this might be not needed if all classes where decorated with `@Middleware` + } + + /** + * Filters registered interceptors by a given classes. + */ + filterInterceptorMetadatasForClasses(classes: Function[]): InterceptorMetadataArgs[] { + return this.interceptors.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + + /** + * Filters registered controllers by a given classes. + */ + filterControllerMetadatasForClasses(classes: Function[]): ControllerMetadataArgs[] { + return this.controllers.filter(ctrl => { + return classes.filter(cls => ctrl.target === cls).length > 0; + }); + } + + /** + * Filters registered actions by a given classes. + */ + filterActionsWithTarget(target: Function): ActionMetadataArgs[] { + return this.actions.filter(action => action.target === target); + } + + /** + * Filters registered "use middlewares" by a given target class and method name. + */ + filterUsesWithTargetAndMethod(target: Function, methodName?: string): UseMetadataArgs[] { + return this.uses.filter(use => { + return use.target === target && use.method === methodName; + }); + } + + /** + * Filters registered "use interceptors" by a given target class and method name. + */ + filterInterceptorUsesWithTargetAndMethod(target: Function, methodName?: string): UseInterceptorMetadataArgs[] { + return this.useInterceptors.filter(use => { + return use.target === target && use.method === methodName; + }); + } + + /** + * Filters parameters by a given classes. + */ + filterParamsWithTargetAndMethod(target: Function, methodName: string): ParamMetadataArgs[] { + return this.params.filter(param => { + return param.object.constructor === target && param.method === methodName; + }); + } + + /** + * Filters response handlers by a given class. + */ + filterResponseHandlersWithTarget(target: Function): ResponseHandlerMetadataArgs[] { + return this.responseHandlers.filter(property => { + return property.target === target; + }); + } + + /** + * Filters response handlers by a given classes. + */ + filterResponseHandlersWithTargetAndMethod(target: Function, methodName: string): ResponseHandlerMetadataArgs[] { + return this.responseHandlers.filter(property => { + return property.target === target && property.method === methodName; + }); + } + + /** + * Removes all saved metadata. + */ + reset() { + this.controllers = []; + this.middlewares = []; + this.interceptors = []; + this.uses = []; + this.useInterceptors = []; + this.actions = []; + this.params = []; + this.responseHandlers = []; + } + + /** + * Gets metadata args storage. + * Metadata args storage follows the best practices and stores metadata in a global variable. + */ + static get(): MetadataArgsStorage { + if (!(global as any).engineMetadataArgsStorage) (global as any).engineMetadataArgsStorage = new MetadataArgsStorage(); + + return (global as any).engineMetadataArgsStorage; + } +} diff --git a/packages/engine/src/metadata/MetadataBuilder.ts b/packages/engine/src/metadata/MetadataBuilder.ts new file mode 100644 index 00000000..5b1f8c45 --- /dev/null +++ b/packages/engine/src/metadata/MetadataBuilder.ts @@ -0,0 +1,187 @@ +import {MetadataArgsStorage} from "./MetadataArgsStorage"; +import { + ActionMetadata, + ControllerMetadata, + InterceptorMetadata, + MiddlewareMetadata, + NodeBootEngineOptions, + ParamMetadata, + ParamMetadataArgs, + ResponseHandlerMetadata, + UseMetadata, +} from "@node-boot/context"; + +/** + * Builds metadata from the given metadata arguments. + */ +export class MetadataBuilder { + constructor(private options: NodeBootEngineOptions) {} + + /** + * Builds controller metadata from a registered controller metadata args. + */ + buildControllerMetadata(classes?: Function[]): ControllerMetadata[] { + return this.createControllers(classes); + } + + /** + * Builds middleware metadata from a registered middleware metadata args. + */ + buildMiddlewareMetadata(classes?: Function[]): MiddlewareMetadata[] { + return this.createMiddlewares(classes); + } + + /** + * Builds interceptor metadata from a registered interceptor metadata args. + */ + buildInterceptorMetadata(classes?: Function[]): InterceptorMetadata[] { + return this.createInterceptors(classes); + } + + /** + * Creates middleware metadatas. + */ + protected createMiddlewares(classes?: Function[]): MiddlewareMetadata[] { + const metadataArgsStorage = MetadataArgsStorage.get(); + const middlewares = !classes ? metadataArgsStorage.middlewares : metadataArgsStorage.filterMiddlewareMetadatasForClasses(classes); + return middlewares.map(middlewareArgs => new MiddlewareMetadata(middlewareArgs)); + } + + /** + * Creates interceptor metadatas. + */ + protected createInterceptors(classes?: Function[]): InterceptorMetadata[] { + const metadataArgsStorage = MetadataArgsStorage.get(); + const interceptors = !classes ? metadataArgsStorage.interceptors : metadataArgsStorage.filterInterceptorMetadatasForClasses(classes); + return interceptors.map( + interceptorArgs => + new InterceptorMetadata({ + ...interceptorArgs, + interceptor: interceptorArgs.target, + }), + ); + } + + /** + * Creates controller metadatas. + */ + protected createControllers(classes?: Function[]): ControllerMetadata[] { + const metadataArgsStorage = MetadataArgsStorage.get(); + const controllers = !classes ? metadataArgsStorage.controllers : metadataArgsStorage.filterControllerMetadatasForClasses(classes); + return controllers.map(controllerArgs => { + const controller = new ControllerMetadata(controllerArgs); + controller.build(this.createControllerResponseHandlers(controller)); + controller.actions = this.createActions(controller); + controller.uses = this.createControllerUses(controller); + controller.interceptors = this.createControllerInterceptorUses(controller); + return controller; + }); + } + + /** + * Creates action metadatas. + */ + protected createActions(controller: ControllerMetadata): ActionMetadata[] { + const actionsWithTarget: ActionMetadata[] = []; + for (let target = controller.target; target; target = Object.getPrototypeOf(target)) { + const actions = MetadataArgsStorage.get().filterActionsWithTarget(target); + const methods = actionsWithTarget.map(a => a.method); + actions + .filter(({method}) => !methods.includes(method)) + .forEach(actionArgs => { + const action = new ActionMetadata( + controller, + { + ...actionArgs, + target: controller.target, + }, + this.options, + ); + action.options = {...controller.options, ...actionArgs.options}; + action.params = this.createParams(action); + action.uses = this.createActionUses(action); + action.interceptors = this.createActionInterceptorUses(action); + action.build(this.createActionResponseHandlers(action)); + + actionsWithTarget.push(action); + }); + } + + return actionsWithTarget; + } + + /** + * Creates param metadatas. + */ + protected createParams(action: ActionMetadata): ParamMetadata[] { + return MetadataArgsStorage.get() + .filterParamsWithTargetAndMethod(action.target, action.method) + .map(paramArgs => new ParamMetadata(action, this.decorateDefaultParamOptions(paramArgs))); + } + + /** + * Creates response handler metadatas for action. + */ + protected createActionResponseHandlers(action: ActionMetadata): ResponseHandlerMetadata[] { + return MetadataArgsStorage.get() + .filterResponseHandlersWithTargetAndMethod(action.target, action.method) + .map(handlerArgs => new ResponseHandlerMetadata(handlerArgs)); + } + + /** + * Creates response handler metadatas for controller. + */ + protected createControllerResponseHandlers(controller: ControllerMetadata): ResponseHandlerMetadata[] { + return MetadataArgsStorage.get() + .filterResponseHandlersWithTarget(controller.target) + .map(handlerArgs => new ResponseHandlerMetadata(handlerArgs)); + } + + /** + * Creates use metadatas for actions. + */ + protected createActionUses(action: ActionMetadata): UseMetadata[] { + return MetadataArgsStorage.get() + .filterUsesWithTargetAndMethod(action.target, action.method) + .map(useArgs => new UseMetadata(useArgs)); + } + + /** + * Creates use interceptors for actions. + */ + protected createActionInterceptorUses(action: ActionMetadata): InterceptorMetadata[] { + return MetadataArgsStorage.get() + .filterInterceptorUsesWithTargetAndMethod(action.target, action.method) + .map(useArgs => new InterceptorMetadata(useArgs)); + } + + /** + * Creates use metadatas for controllers. + */ + protected createControllerUses(controller: ControllerMetadata): UseMetadata[] { + return MetadataArgsStorage.get() + .filterUsesWithTargetAndMethod(controller.target) + .map(useArgs => new UseMetadata(useArgs)); + } + + /** + * Creates use interceptors for controllers. + */ + protected createControllerInterceptorUses(controller: ControllerMetadata): InterceptorMetadata[] { + return MetadataArgsStorage.get() + .filterInterceptorUsesWithTargetAndMethod(controller.target) + .map(useArgs => new InterceptorMetadata(useArgs)); + } + + /** + * Decorate paramArgs with default settings + */ + private decorateDefaultParamOptions(paramArgs: ParamMetadataArgs) { + const options = this.options?.defaults?.paramOptions; + if (!options) return paramArgs; + + if (paramArgs.required === undefined) paramArgs.required = options.required || false; + + return paramArgs; + } +} diff --git a/packages/engine/src/service/ActionParameterHandler.ts b/packages/engine/src/service/ActionParameterHandler.ts new file mode 100644 index 00000000..8aae6cce --- /dev/null +++ b/packages/engine/src/service/ActionParameterHandler.ts @@ -0,0 +1,219 @@ +import {plainToInstance} from "class-transformer"; +import {validateOrReject as validate, ValidationError} from "class-validator"; +import {isPromiseLike} from "../util"; +import { + AuthorizationRequiredError, + BadRequestError, + CurrentUserCheckerNotDefinedError, + InvalidParamError, + ParameterParseJsonError, + ParamRequiredError, +} from "@node-boot/error"; +import {Optional, Param} from "@node-boot/extension"; +import {NodeBootDriver} from "../core"; +import {Action, ParamMetadata} from "@node-boot/context"; + +/** + * Handles action parameter. + */ +export class ActionParameterHandler> { + constructor(private driver: TDriver) {} + + /** + * Handles action parameter. + */ + handle(action: Action, param: ParamMetadata): Promise | any { + if (param.type === "request") return action.request; + if (param.type === "response") return action.response; + if (param.type === "context") return action.context; + + // get parameter value from request and normalize it + const value = this.normalizeParamValue(this.driver.getParamFromRequest(action, param), param); + if (isPromiseLike(value)) { + return value.then(value => this.handleValue(value, action, param)); + } + return this.handleValue(value, action, param); + } + + /** + * Handles non-promise value. + */ + protected handleValue(value: any, action: Action, param: ParamMetadata): Promise | any { + // if transform function is given for this param then apply it + if (param.transform) value = param.transform(action, value); + + // if its current-user decorator then get its value + if (param.type === "current-user") { + value = Optional.of(this.driver.currentUserChecker) + .orElseThrow(() => new CurrentUserCheckerNotDefinedError()) + .map(userChecker => userChecker.check(action)) + .get(); + } + + // check cases when parameter is required but its empty and throw errors in this case + if (param.required) { + const isValueEmpty = value === null || value === undefined || value === ""; + const isValueEmptyObject = typeof value === "object" && value !== null && Object.keys(value).length === 0; + + if (param.type === "body" && !param.name && (isValueEmpty || isValueEmptyObject)) { + // body has a special check and error message + return Promise.reject( + new ParamRequiredError( + { + method: action.request.method, + url: action.request.url, + }, + param, + ), + ); + } else if (param.type === "current-user") { + // current user has a special check as well + if (isPromiseLike(value)) { + return value.then(currentUser => { + if (!currentUser) { + return Promise.reject(new AuthorizationRequiredError(action.request.method, action.request.url)); + } + return currentUser; + }); + } else { + if (!value) return Promise.reject(new AuthorizationRequiredError(action.request.method, action.request.url)); + } + } else if (param.name && isValueEmpty) { + // regular check for all other parameters // todo: figure out something with param.name usage and multiple things params (query params, upload files etc.) + return Promise.reject( + new ParamRequiredError( + { + method: action.request.method, + url: action.request.url, + }, + param, + ), + ); + } + } + + return value; + } + + /** + * Normalizes parameter value. + */ + protected async normalizeParamValue(value: any, param: ParamMetadata): Promise { + if (value === null || value === undefined) return value; + + const isNormalizationNeeded = typeof value === "object" && ["queries", "headers", "params", "cookies"].includes(param.type); + const isTargetPrimitive = ["number", "string", "boolean"].includes(param.targetName); + const isTransformationNeeded = (param.parse || param.isTargetObject) && param.type !== "param"; + + // if param value is an object and param type match, normalize its string properties + if (isNormalizationNeeded) { + await Promise.all( + Object.keys(value).map(async key => { + const keyValue = value[key]; + if (typeof keyValue === "string") { + const ParamType: Function | undefined = (Reflect as any).getMetadata("design:type", param.targetType.prototype, key); + if (ParamType) { + const typeString = ParamType.name.toLowerCase(); + value[key] = await this.normalizeParamValue(keyValue, { + ...param, + name: key, + targetType: ParamType, + targetName: typeString, + }); + } + } + }), + ); + } + // if value is a string, normalize it to demanded type + else if (typeof value === "string") { + switch (param.targetName) { + case "number": + case "string": + case "boolean": + case "date": + return Param.ofString(value, param) + .orElseThrow(() => new InvalidParamError(value, param.name, param.targetName)) + .map(normalizedValue => (param.isArray ? [normalizedValue] : normalizedValue)) + .get(); + case "array": + return [value]; + } + } else if (Array.isArray(value)) { + return value.map(v => + Param.ofString(v, param) + .orElseThrow(() => new InvalidParamError(v, param.name, param.targetName)) + .map(normalizedValue => (param.isArray ? [normalizedValue] : normalizedValue)) + .get(), + ); + } + + // if target type is not primitive, transform and validate it + if (!isTargetPrimitive && isTransformationNeeded) { + value = this.parseValue(value, param); + value = this.transformValue(value, param); + value = await this.validateValue(value, param); + } + + return value; + } + + /** + * Parses string value into a JSON object. + */ + protected parseValue(value: any, paramMetadata: ParamMetadata): any { + if (typeof value === "string") { + if (["queries", "query"].includes(paramMetadata.type) && paramMetadata.targetName === "array") { + return [value]; + } else { + try { + return JSON.parse(value); + } catch (error) { + throw new ParameterParseJsonError(paramMetadata.name, value); + } + } + } + return value; + } + + /** + * Perform class-transformation if enabled. + */ + protected transformValue(value: any, paramMetadata: ParamMetadata): any { + if ( + this.driver.useClassTransformer && + paramMetadata.actionMetadata.options?.transformRequest !== false && + paramMetadata.targetType && + paramMetadata.targetType !== Object && + !(value instanceof paramMetadata.targetType) + ) { + const options = paramMetadata.classTransform || this.driver.plainToClassTransformOptions; + value = plainToInstance(paramMetadata.targetType, value, options); + } + return value; + } + + /** + * Perform class-validation if enabled. + */ + protected validateValue(value: any, paramMetadata: ParamMetadata): Promise | any { + // Validate only if validations is enabled globally via configurations + if (this.driver.enableValidation) { + const shouldValidate = paramMetadata.targetType && paramMetadata.targetType !== Object && value instanceof paramMetadata.targetType; + + // When enabled globally, still skip validation if disabled by the route + if (paramMetadata.validate !== false && shouldValidate) { + const options = Object.assign({forbidUnknownValues: false}, this.driver.validationOptions, paramMetadata.validate); + return validate(value, options) + .then(() => value) + .catch((validationErrors: ValidationError[]) => { + const error: any = new BadRequestError(`Invalid ${paramMetadata.type}, check 'errors' property for more info.`); + error.errors = validationErrors; + error.paramName = paramMetadata.name; + throw error; + }); + } + } + return value; + } +} diff --git a/packages/engine/src/service/ComponentImporter.ts b/packages/engine/src/service/ComponentImporter.ts new file mode 100644 index 00000000..d016b15c --- /dev/null +++ b/packages/engine/src/service/ComponentImporter.ts @@ -0,0 +1,34 @@ +import {ClassFiles} from "@node-boot/extension/src"; +import {NodeBootEngineOptions} from "@node-boot/context"; + +export class ComponentImporter { + static importInterceptors(options: NodeBootEngineOptions) { + let interceptorClasses: Function[] = []; + if (options?.interceptors?.length) { + interceptorClasses = (options.interceptors as any[]).filter(controller => controller instanceof Function); + const interceptorDirs = (options.interceptors as any[]).filter(controller => typeof controller === "string"); + interceptorClasses.push(...ClassFiles.loadFromDirectories(interceptorDirs)); + } + return interceptorClasses; + } + + static importMiddlewares(options: NodeBootEngineOptions) { + let middlewareClasses: Function[] = []; + if (options?.middlewares?.length) { + middlewareClasses = (options.middlewares as any[]).filter(controller => controller instanceof Function); + const middlewareDirs = (options.middlewares as any[]).filter(controller => typeof controller === "string"); + middlewareClasses.push(...ClassFiles.loadFromDirectories(middlewareDirs)); + } + return middlewareClasses; + } + + static importControllers(options: NodeBootEngineOptions) { + let controllerClasses: Function[] = []; + if (options?.controllers?.length) { + controllerClasses = (options.controllers as any[]).filter(controller => controller instanceof Function); + const controllerDirs = (options.controllers as any[]).filter(controller => typeof controller === "string"); + controllerClasses.push(...ClassFiles.loadFromDirectories(controllerDirs)); + } + return controllerClasses; + } +} diff --git a/packages/engine/src/util/index.ts b/packages/engine/src/util/index.ts new file mode 100644 index 00000000..d29e9b87 --- /dev/null +++ b/packages/engine/src/util/index.ts @@ -0,0 +1,2 @@ +export * from "./isPromiseLike"; +export * from "./runInSequence"; diff --git a/servers/fastify-server/src/utils.ts b/packages/engine/src/util/isPromiseLike.ts similarity index 71% rename from servers/fastify-server/src/utils.ts rename to packages/engine/src/util/isPromiseLike.ts index 2f9e3b9f..1ce2e4c7 100644 --- a/servers/fastify-server/src/utils.ts +++ b/packages/engine/src/util/isPromiseLike.ts @@ -1,3 +1,6 @@ +/** + * Checks if given value is a Promise-like object. + */ export function isPromiseLike(arg: any): arg is Promise { return arg != null && typeof arg === "object" && typeof arg.then === "function"; } diff --git a/packages/engine/src/util/runInSequence.ts b/packages/engine/src/util/runInSequence.ts new file mode 100644 index 00000000..e70d53bb --- /dev/null +++ b/packages/engine/src/util/runInSequence.ts @@ -0,0 +1,20 @@ +/** + * Runs given callback that returns promise for each item in the given collection in order. + * Operations executed after each other, right after previous promise being resolved. + */ +export function runInSequence(collection: T[], callback: (item: T) => Promise): Promise { + const results: U[] = []; + return collection + .reduce((promise, item) => { + return promise + .then(() => { + return callback(item); + }) + .then(result => { + results.push(result); + }); + }, Promise.resolve()) + .then(() => { + return results; + }); +} diff --git a/packages/engine/tsconfig.build.json b/packages/engine/tsconfig.build.json new file mode 100644 index 00000000..7e503902 --- /dev/null +++ b/packages/engine/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/engine/tsconfig.json b/packages/engine/tsconfig.json new file mode 100644 index 00000000..20f4beb5 --- /dev/null +++ b/packages/engine/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*"] +} diff --git a/packages/error/.eslintignore b/packages/error/.eslintignore new file mode 100644 index 00000000..5d3719c6 --- /dev/null +++ b/packages/error/.eslintignore @@ -0,0 +1,3 @@ +# Generated files +node_modules +dist diff --git a/packages/error/.lintstagedrc.js b/packages/error/.lintstagedrc.js new file mode 100644 index 00000000..85ad482a --- /dev/null +++ b/packages/error/.lintstagedrc.js @@ -0,0 +1,5 @@ +const baseConfig = require("../../.lintstagedrc.js"); + +module.exports = { + ...baseConfig, +}; diff --git a/packages/error/.prettierignore b/packages/error/.prettierignore new file mode 100644 index 00000000..319ebdd1 --- /dev/null +++ b/packages/error/.prettierignore @@ -0,0 +1,4 @@ +# Generated files +pnpm-lock.yaml +node_modules +dist \ No newline at end of file diff --git a/packages/error/LICENSE b/packages/error/LICENSE new file mode 100644 index 00000000..b5d03778 --- /dev/null +++ b/packages/error/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 NodeBoot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/error/README.md b/packages/error/README.md new file mode 100644 index 00000000..70e257a9 --- /dev/null +++ b/packages/error/README.md @@ -0,0 +1,27 @@ +# Typescript example #2 + +The second typescript example for the Monorepo example + +## License + +MIT License + +Copyright (c) 2023 NodeBoot + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/error/jest.config.js b/packages/error/jest.config.js new file mode 100644 index 00000000..ef8af537 --- /dev/null +++ b/packages/error/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('jest').Config} */ +module.exports = { + transform: { + "^.+\\.(t|j)sx?$": "@swc/jest", + }, +}; diff --git a/packages/error/nodemon.json b/packages/error/nodemon.json new file mode 100644 index 00000000..d30a2c4c --- /dev/null +++ b/packages/error/nodemon.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/nodemon.json", + "watch": ["./src/**", "./node_modules/@mme/**/dist/**"], + "ignoreRoot": [], + "ext": "ts,js", + "exec": "pnpm typecheck && pnpm build" +} diff --git a/packages/error/package.json b/packages/error/package.json new file mode 100644 index 00000000..196b9b69 --- /dev/null +++ b/packages/error/package.json @@ -0,0 +1,31 @@ +{ + "name": "@node-boot/error", + "version": "1.0.0", + "description": "Node-Boot base errors/exceptions", + "author": "Manuel Santos ", + "license": "MIT", + "keywords": [ + "error", + "exception" + ], + "repository": { + "type": "git", + "url": "https://github.com/nodejs-boot/node-boot.git" + }, + "publishConfig": { + "access": "public" + }, + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.build.json", + "clean:build": "rimraf ./dist", + "dev": "nodemon", + "lint": "eslint . --ext .js,.ts", + "lint:fix": "pnpm lint --fix", + "format": "prettier --check .", + "format:fix": "prettier --write .", + "test": "jest", + "typecheck": "tsc" + } +} diff --git a/packages/core/src/error/AccessDeniedError.ts b/packages/error/src/error/AccessDeniedError.ts similarity index 53% rename from packages/core/src/error/AccessDeniedError.ts rename to packages/error/src/error/AccessDeniedError.ts index 55651bf6..b0c814df 100644 --- a/packages/core/src/error/AccessDeniedError.ts +++ b/packages/error/src/error/AccessDeniedError.ts @@ -1,4 +1,4 @@ -import {Action, ForbiddenError} from "routing-controllers"; +import {ForbiddenError} from "../http-error/ForbiddenError"; /** * Thrown when route is guarded by @Authorized decorator. @@ -6,11 +6,10 @@ import {Action, ForbiddenError} from "routing-controllers"; export class AccessDeniedError extends ForbiddenError { override name = "AccessDeniedError"; - constructor(action: Action) { + constructor(method: string, url: string) { super(); Object.setPrototypeOf(this, AccessDeniedError.prototype); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const uri = `${action.request.method} ${action.request.url}`; // todo: check it it works in koa + const uri = `${method} ${url}`; this.message = `Access is denied for request on ${uri}`; } } diff --git a/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts b/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts similarity index 61% rename from packages/core/src/error/AuthorizationCheckerNotDefinedError.ts rename to packages/error/src/error/AuthorizationCheckerNotDefinedError.ts index 5751210f..b7b6b0fa 100644 --- a/packages/core/src/error/AuthorizationCheckerNotDefinedError.ts +++ b/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts @@ -1,4 +1,4 @@ -import {InternalServerError} from "routing-controllers"; +import {InternalServerError} from "../http-error/InternalServerError"; /** * Thrown when authorizationChecker function is not defined in routing-controllers options. @@ -7,9 +7,7 @@ export class AuthorizationCheckerNotDefinedError extends InternalServerError { override name = "AuthorizationCheckerNotDefinedError"; constructor() { - super( - `Cannot use @Authorized decorator. Please define authorizationChecker function in routing-controllers action before using it.`, - ); + super(`Cannot use @Authorized decorator. Please define authorizationChecker function in routing-controllers action before using it.`); Object.setPrototypeOf(this, AuthorizationCheckerNotDefinedError.prototype); } } diff --git a/packages/core/src/error/AuthorizationRequiredError.ts b/packages/error/src/error/AuthorizationRequiredError.ts similarity index 56% rename from packages/core/src/error/AuthorizationRequiredError.ts rename to packages/error/src/error/AuthorizationRequiredError.ts index c64a6c33..58caf5ca 100644 --- a/packages/core/src/error/AuthorizationRequiredError.ts +++ b/packages/error/src/error/AuthorizationRequiredError.ts @@ -1,4 +1,4 @@ -import {Action, UnauthorizedError} from "routing-controllers"; +import {UnauthorizedError} from "../http-error/UnauthorizedError"; /** * Thrown when authorization is required thought @CurrentUser decorator. @@ -6,11 +6,10 @@ import {Action, UnauthorizedError} from "routing-controllers"; export class AuthorizationRequiredError extends UnauthorizedError { override name = "AuthorizationRequiredError"; - constructor(action: Action) { + constructor(method: string, url: string) { super(); Object.setPrototypeOf(this, AuthorizationRequiredError.prototype); - // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - const uri = `${action.request.method} ${action.request.url}`; // todo: check it it works in koa + const uri = `${method} ${url}`; this.message = `Authorization is required for request on ${uri}`; } } diff --git a/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts b/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts new file mode 100644 index 00000000..6a82849f --- /dev/null +++ b/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts @@ -0,0 +1,13 @@ +import {InternalServerError} from "../http-error/InternalServerError"; + +/** + * Thrown when currentUserChecker function is not defined in routing-controllers options. + */ +export class CurrentUserCheckerNotDefinedError extends InternalServerError { + override name = "CurrentUserCheckerNotDefinedError"; + + constructor() { + super(`Cannot use @CurrentUser decorator. Please define currentUserChecker function in routing-controllers action before using it.`); + Object.setPrototypeOf(this, CurrentUserCheckerNotDefinedError.prototype); + } +} diff --git a/packages/error/src/error/ParamNormalizationError.ts b/packages/error/src/error/ParamNormalizationError.ts new file mode 100644 index 00000000..3d39d701 --- /dev/null +++ b/packages/error/src/error/ParamNormalizationError.ts @@ -0,0 +1,14 @@ +import {BadRequestError} from "../http-error/BadRequestError"; + +/** + * Caused when user query parameter is invalid (cannot be parsed into selected type). + */ +export class InvalidParamError extends BadRequestError { + override name = "ParamNormalizationError"; + + constructor(value: any, parameterName: string, parameterType: string) { + super(`Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into ${parameterType}.`); + + Object.setPrototypeOf(this, InvalidParamError.prototype); + } +} diff --git a/packages/error/src/error/ParamRequiredError.ts b/packages/error/src/error/ParamRequiredError.ts new file mode 100644 index 00000000..7a5a2ec8 --- /dev/null +++ b/packages/error/src/error/ParamRequiredError.ts @@ -0,0 +1,58 @@ +import {BadRequestError} from "../http-error/BadRequestError"; + +/** + * Thrown when parameter is required, but was missing in a user request. + */ +export class ParamRequiredError extends BadRequestError { + override name = "ParamRequiredError"; + + constructor(action: {method: string; url: string}, param: {type: string; name: string}) { + super(); + Object.setPrototypeOf(this, ParamRequiredError.prototype); + + let paramName: string; + switch (param.type) { + case "param": + paramName = `Parameter "${param.name}" is`; + break; + + case "body": + paramName = "Request body is"; + break; + + case "body-param": + paramName = `Body parameter "${param.name}" is`; + break; + + case "query": + paramName = `Query parameter "${param.name}" is`; + break; + + case "header": + paramName = `Header "${param.name}" is`; + break; + + case "file": + paramName = `Uploaded file "${param.name}" is`; + break; + + case "files": + paramName = `Uploaded files "${param.name}" are`; + break; + + case "session": + paramName = "Session is"; + break; + + case "cookie": + paramName = "Cookie is"; + break; + + default: + paramName = "Parameter is"; + } + + const uri = `${action.method} ${action.url}`; + this.message = `${paramName} required for request on ${uri}`; + } +} diff --git a/packages/error/src/error/ParameterParseJsonError.ts b/packages/error/src/error/ParameterParseJsonError.ts new file mode 100644 index 00000000..0d499daa --- /dev/null +++ b/packages/error/src/error/ParameterParseJsonError.ts @@ -0,0 +1,13 @@ +import {BadRequestError} from "../http-error/BadRequestError"; + +/** + * Caused when user parameter is invalid json string and cannot be parsed. + */ +export class ParameterParseJsonError extends BadRequestError { + override name = "ParameterParseJsonError"; + + constructor(parameterName: string, value: any) { + super(`Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into JSON.`); + Object.setPrototypeOf(this, ParameterParseJsonError.prototype); + } +} diff --git a/packages/error/src/error/index.ts b/packages/error/src/error/index.ts new file mode 100644 index 00000000..64bff44f --- /dev/null +++ b/packages/error/src/error/index.ts @@ -0,0 +1,7 @@ +export {AccessDeniedError} from "./AccessDeniedError"; +export {AuthorizationCheckerNotDefinedError} from "./AuthorizationCheckerNotDefinedError"; +export {AuthorizationRequiredError} from "./AuthorizationRequiredError"; +export {CurrentUserCheckerNotDefinedError} from "./CurrentUserCheckerNotDefinedError"; +export {ParamRequiredError} from "./ParamRequiredError"; +export {ParameterParseJsonError} from "./ParameterParseJsonError"; +export {InvalidParamError} from "./ParamNormalizationError"; diff --git a/packages/error/src/http-error/BadRequestError.ts b/packages/error/src/http-error/BadRequestError.ts new file mode 100644 index 00000000..918a12ce --- /dev/null +++ b/packages/error/src/http-error/BadRequestError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 400 HTTP error. + */ +export class BadRequestError extends HttpError { + override name = "BadRequestError"; + + constructor(message?: string) { + super(400); + Object.setPrototypeOf(this, BadRequestError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/ForbiddenError.ts b/packages/error/src/http-error/ForbiddenError.ts new file mode 100644 index 00000000..bd65bd36 --- /dev/null +++ b/packages/error/src/http-error/ForbiddenError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 403 HTTP error. + */ +export class ForbiddenError extends HttpError { + override name = "ForbiddenError"; + + constructor(message?: string) { + super(403); + Object.setPrototypeOf(this, ForbiddenError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/HttpError.ts b/packages/error/src/http-error/HttpError.ts new file mode 100644 index 00000000..3f220ac1 --- /dev/null +++ b/packages/error/src/http-error/HttpError.ts @@ -0,0 +1,18 @@ +/** + * Used to throw HTTP errors. + * Just do throw new HttpError(code, message) in your controller action and + * default error handler will catch it and give in your response given code and message . + */ +export class HttpError extends Error { + httpCode: number; + + constructor(httpCode: number, message?: string) { + super(); + Object.setPrototypeOf(this, HttpError.prototype); + + if (httpCode) this.httpCode = httpCode; + if (message) this.message = message; + + this.stack = new Error().stack; + } +} diff --git a/packages/error/src/http-error/InternalServerError.ts b/packages/error/src/http-error/InternalServerError.ts new file mode 100644 index 00000000..88b807bc --- /dev/null +++ b/packages/error/src/http-error/InternalServerError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 500 HTTP error. + */ +export class InternalServerError extends HttpError { + override name = "InternalServerError"; + + constructor(message: string) { + super(500); + Object.setPrototypeOf(this, InternalServerError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/MethodNotAllowedError.ts b/packages/error/src/http-error/MethodNotAllowedError.ts new file mode 100644 index 00000000..cf857c45 --- /dev/null +++ b/packages/error/src/http-error/MethodNotAllowedError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for todo HTTP error. + */ +export class MethodNotAllowedError extends HttpError { + override name = "MethodNotAllowedError"; + + constructor(message?: string) { + super(405); + Object.setPrototypeOf(this, MethodNotAllowedError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/NotAcceptableError.ts b/packages/error/src/http-error/NotAcceptableError.ts new file mode 100644 index 00000000..8c01f00c --- /dev/null +++ b/packages/error/src/http-error/NotAcceptableError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 406 HTTP error. + */ +export class NotAcceptableError extends HttpError { + override name = "NotAcceptableError"; + + constructor(message?: string) { + super(406); + Object.setPrototypeOf(this, NotAcceptableError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/NotFoundError.ts b/packages/error/src/http-error/NotFoundError.ts new file mode 100644 index 00000000..b2f3a1a4 --- /dev/null +++ b/packages/error/src/http-error/NotFoundError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 404 HTTP error. + */ +export class NotFoundError extends HttpError { + override name = "NotFoundError"; + + constructor(message?: string) { + super(404); + Object.setPrototypeOf(this, NotFoundError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/UnauthorizedError.ts b/packages/error/src/http-error/UnauthorizedError.ts new file mode 100644 index 00000000..618c4b85 --- /dev/null +++ b/packages/error/src/http-error/UnauthorizedError.ts @@ -0,0 +1,15 @@ +import {HttpError} from "./HttpError"; + +/** + * Exception for 401 HTTP error. + */ +export class UnauthorizedError extends HttpError { + override name = "UnauthorizedError"; + + constructor(message?: string) { + super(401); + Object.setPrototypeOf(this, UnauthorizedError.prototype); + + if (message) this.message = message; + } +} diff --git a/packages/error/src/http-error/index.ts b/packages/error/src/http-error/index.ts new file mode 100644 index 00000000..70c22763 --- /dev/null +++ b/packages/error/src/http-error/index.ts @@ -0,0 +1,8 @@ +export {BadRequestError} from "./BadRequestError"; +export {ForbiddenError} from "./ForbiddenError"; +export {HttpError} from "./HttpError"; +export {InternalServerError} from "./InternalServerError"; +export {MethodNotAllowedError} from "./MethodNotAllowedError"; +export {NotAcceptableError} from "./NotAcceptableError"; +export {NotFoundError} from "./NotFoundError"; +export {UnauthorizedError} from "./UnauthorizedError"; diff --git a/packages/error/src/index.ts b/packages/error/src/index.ts new file mode 100644 index 00000000..35865021 --- /dev/null +++ b/packages/error/src/index.ts @@ -0,0 +1,2 @@ +export * from "./error"; +export * from "./http-error"; diff --git a/packages/error/tsconfig.build.json b/packages/error/tsconfig.build.json new file mode 100644 index 00000000..7e503902 --- /dev/null +++ b/packages/error/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["**/*.spec.ts"] +} diff --git a/packages/error/tsconfig.json b/packages/error/tsconfig.json new file mode 100644 index 00000000..20f4beb5 --- /dev/null +++ b/packages/error/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src/**/*"] +} diff --git a/packages/extension/src/ClassFiles.ts b/packages/extension/src/ClassFiles.ts new file mode 100644 index 00000000..b7cad6b5 --- /dev/null +++ b/packages/extension/src/ClassFiles.ts @@ -0,0 +1,40 @@ +import * as path from "path"; + +/** + * + * @author Manuel Santos + * */ +export class ClassFiles { + static load(dirs: any, allLoaded: Function[]) { + if (dirs instanceof Function) { + allLoaded.push(dirs); + } else if (dirs instanceof Array) { + dirs.forEach((i: any) => ClassFiles.load(i, allLoaded)); + } else if (dirs instanceof Object || typeof dirs === "object") { + Object.keys(dirs).forEach(key => ClassFiles.load(dirs[key], allLoaded)); + } + return allLoaded; + } + + /** + * Loads all exported classes from the given directory. + */ + static loadFromDirectories(directories: string[], formats = [".js", ".ts", ".tsx"]): Function[] { + const allFiles = directories.reduce((allDirs, dir) => { + // Replace \ with / for glob + // eslint-disable-next-line @typescript-eslint/no-var-requires + return allDirs.concat(require("glob").sync(path.normalize(dir).replace(/\\/g, "/"))); + }, [] as string[]); + + const dirs = allFiles + .filter(file => { + const dtsExtension = file.substring(file.length - 5, file.length); + return formats.indexOf(path.extname(file)) !== -1 && dtsExtension !== ".d.ts"; + }) + .map(file => { + return require(file); + }); + + return ClassFiles.load(dirs, []); + } +} diff --git a/packages/extension/src/Optional.ts b/packages/extension/src/Optional.ts index 8285266d..ec9afa87 100644 --- a/packages/extension/src/Optional.ts +++ b/packages/extension/src/Optional.ts @@ -70,6 +70,22 @@ export class Optional { return this; } + ifThrow(predicate: (value: T) => boolean, errorProvider: () => Error): Optional { + this.ifEmptyThrow(() => new Error("'ifThrow' can only be called for non empty optionals")); + if (predicate(this.value!)) { + throw errorProvider(); + } + return this; + } + + if(predicate: (value: T) => boolean, mapper: (value: T) => U): Optional { + this.ifEmptyThrow(() => new Error("'if' can only be called for non empty optionals")); + if (predicate(this.value!)) { + return Optional.of(mapper(this.value!)); + } + return this; + } + /** * The get method is used to retrieve the value inside the Optional object. If the Optional * object is empty, it throws an error indicating that the value is not present. @@ -180,9 +196,7 @@ export class Optional { return Optional.of(this.value.filter(predicate) as T); } else if (this.value instanceof Map || this.value instanceof Set) { // Filter map or set - const filteredEntries = Array.from(this.value.entries()).filter(([key, value]) => - predicate(value), - ); + const filteredEntries = Array.from(this.value.entries()).filter(([key, value]) => predicate(value)); if (this.value instanceof Map) { return Optional.of(new Map(filteredEntries) as T); } else { diff --git a/packages/extension/src/Param.ts b/packages/extension/src/Param.ts new file mode 100644 index 00000000..01c75ead --- /dev/null +++ b/packages/extension/src/Param.ts @@ -0,0 +1,54 @@ +import {Optional} from "./Optional"; +import {OfParamMetadata} from "./types"; + +/** + * + * @author Manuel Santos + * */ +export class Param { + static ofString(value: string | undefined | null, paramMetadata: OfParamMetadata): Optional { + if (value === null || value === undefined) { + return Optional.empty(); + } + switch (paramMetadata.targetName) { + case "number": + return Param.ofNumber(value); + case "boolean": + return Param.ofBoolean(value); + case "date": + return Param.ofDate(value); + case "string": + default: + return Optional.of(value); + } + } + + static ofNumber(value: string): Optional { + if (value === "") { + return Optional.empty(); + } + const valueNumber = +value; + if (Number.isNaN(valueNumber)) { + return Optional.empty(); + } + return Optional.of(valueNumber); + } + + static ofBoolean(value: string): Optional { + if (value === "true" || value === "1" || value === "") { + return Optional.of(true); + } else if (value === "false" || value === "0") { + return Optional.of(false); + } else { + return Optional.empty(); + } + } + + static ofDate(value: string): Optional { + const parsedDate = new Date(value); + if (Number.isNaN(parsedDate.getTime())) { + return Optional.empty(); + } + return Optional.of(parsedDate); + } +} diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index 5e0aef9c..5e2671e4 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -1 +1,3 @@ export {Optional} from "./Optional"; +export {Param} from "./Param"; +export {ClassFiles} from "./ClassFiles"; diff --git a/packages/extension/src/types.ts b/packages/extension/src/types.ts new file mode 100644 index 00000000..8c22995a --- /dev/null +++ b/packages/extension/src/types.ts @@ -0,0 +1,15 @@ +export type OfParamMetadata = { + /** + * Parameter type. + */ + type: string; + /** + * Parameter name. + */ + name: string; + + /** + * Parameter target type's name in lowercase. + */ + targetName: string; +}; diff --git a/packages/openapi/src/decorator/EnableOpenApi.ts b/packages/openapi/src/decorator/EnableOpenApi.ts index ee88a2f6..5cf67779 100644 --- a/packages/openapi/src/decorator/EnableOpenApi.ts +++ b/packages/openapi/src/decorator/EnableOpenApi.ts @@ -22,8 +22,7 @@ export function EnableOpenApi(openApi: Partial = {}): Function return new FastifyOpenApi(); default: throw new Error( - "OpenAPI feature is only allowed for express and koa servers. " + - "Please remove @EnableOpenApi from your application", + "OpenAPI feature is only allowed for express and koa servers. " + "Please remove @EnableOpenApi from your application", ); } } diff --git a/packages/openapi/src/decorator/OpenAPI.ts b/packages/openapi/src/decorator/OpenAPI.ts index 7f1af700..c4d3744d 100644 --- a/packages/openapi/src/decorator/OpenAPI.ts +++ b/packages/openapi/src/decorator/OpenAPI.ts @@ -11,9 +11,7 @@ import {OpenAPI as InnerOpenAPI} from "routing-controllers-openapi"; * returning an updated Operation. */ export function OpenAPI(...args: Parameters) { - return ( - ...innerArgs: [Function] | [object, string, PropertyDescriptor] - ) => { + return (...innerArgs: [Function] | [object, string, PropertyDescriptor]) => { InnerOpenAPI(...args)(...innerArgs); }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee7cb5f0..6710b0f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,9 +68,9 @@ importers: '@node-boot/context': specifier: 1.0.0 version: link:../context - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) + '@node-boot/engine': + specifier: 1.0.0 + version: link:../engine packages/config: dependencies: @@ -102,12 +102,12 @@ importers: class-transformer: specifier: ^0.5.1 version: 0.5.1 + class-validator: + specifier: ^0.14.0 + version: 0.14.0 reflect-metadata: specifier: ^0.1.13 version: 0.1.13 - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) winston: specifier: '>=3.10.0' version: 3.10.0 @@ -123,9 +123,15 @@ importers: '@node-boot/di': specifier: 1.0.0 version: link:../di + '@node-boot/engine': + specifier: 1.0.0 + version: link:../engine class-transformer: specifier: ^0.5.1 version: 0.5.1 + class-validator: + specifier: ^0.14.0 + version: 0.14.0 glob: specifier: ^10.3.3 version: 10.3.3 @@ -138,9 +144,6 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) winston: specifier: '>=3.10.0' version: 3.10.0 @@ -158,6 +161,26 @@ importers: specifier: ^0.10.0 version: 0.10.0 + packages/engine: + dependencies: + '@node-boot/context': + specifier: 1.0.0 + version: link:../context + '@node-boot/error': + specifier: 1.0.0 + version: link:../error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../extension + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.0 + version: 0.14.0 + + packages/error: {} + packages/extension: {} packages/openapi: @@ -217,6 +240,9 @@ importers: '@node-boot/di': specifier: 1.0.0 version: link:../../packages/di + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error '@node-boot/express-server': specifier: 1.0.0 version: link:../../servers/express-server @@ -265,12 +291,6 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 - routing-controllers: - specifier: ^0.10.4 - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) - routing-controllers-openapi: - specifier: ^4.0.0 - version: 4.0.0(routing-controllers@0.10.4) typedi: specifier: ^0.10.0 version: 0.10.0 @@ -323,6 +343,12 @@ importers: '@node-boot/di': specifier: 1.0.0 version: link:../../packages/di + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension '@node-boot/fastify-server': specifier: 1.0.0 version: link:../../servers/fastify-server @@ -332,6 +358,9 @@ importers: '@node-boot/starter-actuator': specifier: 1.0.0 version: link:../../starters/actuator + '@node-boot/starter-persistence': + specifier: 1.0.0 + version: link:../../starters/persistence body-parser: specifier: ^1.20.2 version: 1.20.2 @@ -365,15 +394,12 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 - routing-controllers: - specifier: ^0.10.4 - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) - routing-controllers-openapi: - specifier: ^4.0.0 - version: 4.0.0(routing-controllers@0.10.4) typedi: specifier: ^0.10.0 version: 0.10.0 + typeorm: + specifier: ^0.3.17 + version: 0.3.17(better-sqlite3@9.1.1) winston: specifier: ^3.10.0 version: 3.10.0 @@ -423,6 +449,12 @@ importers: '@node-boot/di': specifier: 1.0.0 version: link:../../packages/di + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension '@node-boot/koa-server': specifier: 1.0.0 version: link:../../servers/koa-server @@ -432,6 +464,9 @@ importers: '@node-boot/starter-actuator': specifier: 1.0.0 version: link:../../starters/actuator + '@node-boot/starter-persistence': + specifier: 1.0.0 + version: link:../../starters/persistence class-transformer: specifier: ^0.5.1 version: 0.5.1 @@ -459,12 +494,12 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 - routing-controllers: - specifier: ^0.10.4 - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) typedi: specifier: ^0.10.0 version: 0.10.0 + typeorm: + specifier: ^0.3.17 + version: 0.3.17(better-sqlite3@9.1.1) winston: specifier: ^3.10.0 version: 3.10.0 @@ -502,6 +537,12 @@ importers: '@node-boot/core': specifier: 1.0.0 version: link:../../packages/core + '@node-boot/engine': + specifier: 1.0.0 + version: link:../../packages/engine + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error body-parser: specifier: ^1.20.2 version: 1.20.2 @@ -511,9 +552,6 @@ importers: multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.1 - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) devDependencies: '@types/body-parser': specifier: ^1.19.2 @@ -548,15 +586,18 @@ importers: '@node-boot/core': specifier: 1.0.0 version: link:../../packages/core + '@node-boot/engine': + specifier: 1.0.0 + version: link:../../packages/engine + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error fastify: specifier: '>=4.21.0' version: 4.21.0 handlebars: specifier: ^4.7.8 version: 4.7.8 - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) template-url: specifier: ^1.0.0 version: 1.0.0 @@ -575,6 +616,12 @@ importers: '@node-boot/core': specifier: 1.0.0 version: link:../../packages/core + '@node-boot/engine': + specifier: 1.0.0 + version: link:../../packages/engine + '@node-boot/error': + specifier: 1.0.0 + version: link:../../packages/error koa: specifier: '>=2.14.2' version: 2.14.2 @@ -588,6 +635,9 @@ importers: '@types/koa': specifier: ^2.13.8 version: 2.13.8 + '@types/koa-router': + specifier: ^7.4.8 + version: 7.4.8 starters/actuator: dependencies: @@ -1966,6 +2016,12 @@ packages: helmet: 4.6.0 dev: true + /@types/koa-router@7.4.8: + resolution: {integrity: sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A==} + dependencies: + '@types/koa': 2.13.11 + dev: true + /@types/koa@2.13.11: resolution: {integrity: sha512-0HZSGNdmLlLRvSxv0ngLSp09Hw98c+2XL3ZRYmkE6y8grqTweKEyyaj7LgxkyPUv0gQ5pNS/a7kHXo2Iwha1rA==} dependencies: diff --git a/samples/sample-express/package.json b/samples/sample-express/package.json index 3ea122cc..02216703 100644 --- a/samples/sample-express/package.json +++ b/samples/sample-express/package.json @@ -39,11 +39,10 @@ "@node-boot/express-server": "1.0.0", "@node-boot/authorization": "1.0.0", "@node-boot/di": "1.0.0", + "@node-boot/error": "1.0.0", "@node-boot/starter-actuator": "1.0.0", "@node-boot/starter-persistence": "1.0.0", "@node-boot/extension": "1.0.0", - "routing-controllers": "^0.10.4", - "routing-controllers-openapi": "^4.0.0", "reflect-metadata": "^0.1.13", "typeorm": "^0.3.17", "express": "^4.18.2", diff --git a/samples/sample-express/src/app.ts b/samples/sample-express/src/app.ts index 97656990..f239f554 100644 --- a/samples/sample-express/src/app.ts +++ b/samples/sample-express/src/app.ts @@ -1,25 +1,19 @@ import "reflect-metadata"; import {Container} from "typedi"; -import { - Configurations, - Controllers, - EnableDI, - GlobalMiddlewares, - NodeBoot, - NodeBootApplication, -} from "@node-boot/core"; +import {Configurations, Controllers, GlobalMiddlewares, NodeBoot, NodeBootApplication} from "@node-boot/core"; import {EnableOpenApi} from "@node-boot/openapi"; import {AppConfigProperties} from "./config/AppConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; -import {ErrorMiddleware} from "./middlewares/error.middleware"; import {EnableAuthorization} from "@node-boot/authorization"; import {LoggedInUserResolver} from "./auth/LoggedInUserResolver"; import {DefaultAuthorizationResolver} from "./auth/DefaultAuthorizationResolver"; import {ExpressServer} from "@node-boot/express-server"; import {EnableActuator} from "@node-boot/starter-actuator"; import {EnableRepositories} from "@node-boot/starter-persistence"; +import {EnableDI} from "@node-boot/di"; +import {ErrorMiddleware} from "./middlewares/ErrorMiddleware"; @EnableDI(Container) @EnableOpenApi() diff --git a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts index 40f49bc0..fdb595a0 100644 --- a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts @@ -1,17 +1,24 @@ -import {AuthorizationResolver} from "@node-boot/authorization"; -import {RequestContext} from "@node-boot/context"; +import {Action, AuthorizationChecker} from "@node-boot/context"; import {Component} from "@node-boot/core"; +import {Request, Response} from "express"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class DefaultAuthorizationResolver implements AuthorizationResolver { - async authorize(context: RequestContext, roles: any[]): Promise { +export class DefaultAuthorizationResolver implements AuthorizationChecker { + @Inject() + private logger: Logger; + + async check(action: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check // checker must return either boolean (true or false) // either promise that resolves a boolean value // demo code: - const token = context.request.headers["authorization"]; + this.logger.info(`Checking authorization`); + + const token = action.request.headers["authorization"]; //const user = await getEntityManager().findOneByToken(User, token); const user = { diff --git a/samples/sample-express/src/auth/LoggedInUserResolver.ts b/samples/sample-express/src/auth/LoggedInUserResolver.ts index 75dfcc14..f046a7fd 100644 --- a/samples/sample-express/src/auth/LoggedInUserResolver.ts +++ b/samples/sample-express/src/auth/LoggedInUserResolver.ts @@ -1,13 +1,20 @@ -import {RequestContext} from "@node-boot/context"; import {Component} from "@node-boot/core"; -import {CurrentUserResolver} from "@node-boot/authorization"; +import {Action, CurrentUserChecker} from "@node-boot/context"; +import {Request, Response} from "express"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class LoggedInUserResolver implements CurrentUserResolver { - async getCurrentUser(context: RequestContext): Promise { +export class LoggedInUserResolver implements CurrentUserChecker { + @Inject() + private logger: Logger; + + async check(action: Action): Promise { + this.logger.info(`Checking current logged in user`); + // Your logic to fetch the current user from the request, database, or any other source // For example, you might want to retrieve user info from a session, token, or database - context.request; + action.request; return { id: 1, username: "exampleUser", diff --git a/samples/sample-express/src/config/ClassTransformConfiguration.ts b/samples/sample-express/src/config/ClassTransformConfiguration.ts index a551ce02..fa0209c1 100644 --- a/samples/sample-express/src/config/ClassTransformConfiguration.ts +++ b/samples/sample-express/src/config/ClassTransformConfiguration.ts @@ -1,8 +1,4 @@ -import { - ClassToPlainTransform, - EnableClassTransformer, - PlainToClassTransform, -} from "@node-boot/core"; +import {ClassToPlainTransform, EnableClassTransformer, PlainToClassTransform} from "@node-boot/core"; @EnableClassTransformer({enabled: false}) @ClassToPlainTransform({ diff --git a/samples/sample-express/src/controllers/users.controller.ts b/samples/sample-express/src/controllers/users.controller.ts index edb75745..6dbe5ff8 100644 --- a/samples/sample-express/src/controllers/users.controller.ts +++ b/samples/sample-express/src/controllers/users.controller.ts @@ -1,10 +1,9 @@ -import {Body, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "routing-controllers"; +import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "@node-boot/core"; import {UserService} from "../services/users.service"; import {ValidationMiddleware} from "../middlewares/validation.middleware"; import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; import {AppConfigProperties} from "../config/AppConfigProperties"; import {Logger} from "winston"; -import {Controller} from "@node-boot/core"; import {Inject} from "@node-boot/di"; import {OpenAPI} from "@node-boot/openapi"; import {Authorized} from "@node-boot/authorization"; @@ -22,11 +21,7 @@ export class UserController { @Get("/") @OpenAPI({summary: "Return a list of users"}) async getUsers() { - this.logger.info( - `Injected backend configuration properties: ${JSON.stringify( - this.appConfigProperties, - )}`, - ); + this.logger.info(`Injected backend configuration properties: ${JSON.stringify(this.appConfigProperties)}`); const findAllUsersData: User[] = await this.user.findAllUser(); return {data: findAllUsersData, message: "findAll"}; } diff --git a/samples/sample-express/src/exceptions/httpException.ts b/samples/sample-express/src/exceptions/httpException.ts deleted file mode 100644 index d2322b92..00000000 --- a/samples/sample-express/src/exceptions/httpException.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {HttpError} from "routing-controllers"; - -export class HttpException extends HttpError { - public status: number; - - constructor(status: number, message: string) { - super(status, message); - this.status = status; - } -} diff --git a/samples/sample-express/src/middlewares/ErrorMiddleware.ts b/samples/sample-express/src/middlewares/ErrorMiddleware.ts new file mode 100644 index 00000000..c46490dd --- /dev/null +++ b/samples/sample-express/src/middlewares/ErrorMiddleware.ts @@ -0,0 +1,27 @@ +import {Logger} from "winston"; +import {ErrorHandler} from "@node-boot/core"; +import {Inject} from "@node-boot/di"; +import {Action, ErrorHandlerInterface} from "@node-boot/context"; +import {Request, Response} from "express"; +import {HttpError} from "@node-boot/error"; + +@ErrorHandler() +export class ErrorMiddleware implements ErrorHandlerInterface { + @Inject() + private logger: Logger; + + onError(error: HttpError, action: Action): any { + const {request, response, next} = action; + try { + const status: number = error.httpCode || 500; + const message: string = error.message || "Something went wrong"; + + this.logger.error(`[${request.method}] ${request.path} >> StatusCode:: ${status}, Message:: ${message}`); + // FIXME Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client + // FIXME Fix this after refactoring routing-controllers library + // res.status(status).json({message}); + } catch (error) { + next?.(error); + } + } +} diff --git a/samples/sample-express/src/middlewares/LoggingMiddleware.ts b/samples/sample-express/src/middlewares/LoggingMiddleware.ts index 792fc133..372b7ead 100644 --- a/samples/sample-express/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-express/src/middlewares/LoggingMiddleware.ts @@ -1,15 +1,17 @@ -import {ExpressMiddlewareInterface} from "routing-controllers"; import {Logger} from "winston"; import {Middleware} from "@node-boot/core"; import {Inject} from "@node-boot/di"; +import {Action, MiddlewareInterface} from "@node-boot/context"; +import {Request, Response} from "express"; @Middleware({type: "before"}) -export class LoggingMiddleware implements ExpressMiddlewareInterface { +export class LoggingMiddleware implements MiddlewareInterface { @Inject() private logger: Logger; - use(request: any, response: any, next: (err?: any) => any): void { + use(action: Action): any { + const {request, response, next} = action; this.logger.info(`Logging Middleware: Incoming request`); - next(); + next?.(); } } diff --git a/samples/sample-express/src/middlewares/error.middleware.ts b/samples/sample-express/src/middlewares/error.middleware.ts deleted file mode 100644 index 4ce5b51f..00000000 --- a/samples/sample-express/src/middlewares/error.middleware.ts +++ /dev/null @@ -1,26 +0,0 @@ -import {ExpressErrorMiddlewareInterface} from "routing-controllers"; -import {Logger} from "winston"; -import {ErrorHandler} from "@node-boot/core"; -import {Inject} from "@node-boot/di"; - -@ErrorHandler() -export class ErrorMiddleware implements ExpressErrorMiddlewareInterface { - @Inject() - private logger: Logger; - - error(error: any, req: any, res: any, next: (err?: any) => any): void { - try { - const status: number = error.status || 500; - const message: string = error.message || "Something went wrong"; - - this.logger.error( - `[${req.method}] ${req.path} >> StatusCode:: ${status}, Message:: ${message}`, - ); - // FIXME Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client - // FIXME Fix this after refactoring routing-controllers library - // res.status(status).json({message}); - } catch (error) { - next(error); - } - } -} diff --git a/samples/sample-express/src/middlewares/validation.middleware.ts b/samples/sample-express/src/middlewares/validation.middleware.ts index 99d38336..68a62e27 100644 --- a/samples/sample-express/src/middlewares/validation.middleware.ts +++ b/samples/sample-express/src/middlewares/validation.middleware.ts @@ -1,7 +1,7 @@ import {plainToInstance} from "class-transformer"; import {validateOrReject, ValidationError} from "class-validator"; import {NextFunction, Request, Response} from "express"; -import {HttpException} from "../exceptions/httpException"; +import {HttpError} from "@node-boot/error"; /** * @name ValidationMiddleware @@ -11,12 +11,7 @@ import {HttpException} from "../exceptions/httpException"; * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present */ -export const ValidationMiddleware = ( - type: any, - skipMissingProperties = false, - whitelist = false, - forbidNonWhitelisted = false, -) => { +export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = false, forbidNonWhitelisted = false) => { return (req: Request, res: Response, next: NextFunction) => { const dto = plainToInstance(type, req.body); validateOrReject(dto, { @@ -29,10 +24,8 @@ export const ValidationMiddleware = ( next(); }) .catch((errors: ValidationError[]) => { - const message = errors - .map((error: ValidationError) => Object.values(error.constraints ?? {})) - .join(", "); - next(new HttpException(400, message)); + const message = errors.map((error: ValidationError) => Object.values(error.constraints ?? {})).join(", "); + next(new HttpError(400, message)); }); }; }; diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index 640f13a6..ab0b3ec0 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -1,25 +1,16 @@ -import {HttpException} from "../exceptions/httpException"; import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; import {Logger} from "winston"; import {ConfigService} from "@node-boot/config"; import {Service} from "@node-boot/core"; -import {NotFoundError} from "routing-controllers"; import {User, UserRepository} from "../persistence"; import {UserModel} from "../models/users.model"; import {Optional} from "@node-boot/extension"; -import { - runOnTransactionCommit, - runOnTransactionRollback, - Transactional, -} from "@node-boot/starter-persistence"; +import {runOnTransactionCommit, runOnTransactionRollback, Transactional} from "@node-boot/starter-persistence"; +import {HttpError, NotFoundError} from "@node-boot/error"; @Service() export class UserService { - constructor( - private readonly logger: Logger, - private readonly configService: ConfigService, - private readonly userRepository: UserRepository, - ) { + constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { UserModel.forEach(user => this.userRepository.save(user)); } @@ -55,9 +46,7 @@ export class UserService { }); return Optional.of(existingUser) - .ifPresentThrow( - () => new HttpException(409, `This email ${userData.email} already exists`), - ) + .ifPresentThrow(() => new HttpError(409, `This email ${userData.email} already exists`)) .elseAsync(() => this.userRepository.save(userData)); } @@ -68,7 +57,7 @@ export class UserService { }); return Optional.of(user) - .orElseThrow(() => new HttpException(409, "User doesn't exist")) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) .map(user => { return { ...user, @@ -89,7 +78,7 @@ export class UserService { }); await Optional.of(user) - .orElseThrow(() => new HttpException(409, "User doesn't exist")) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) .runAsync(user => this.userRepository.delete({id: userId})); throw new Error("Error after deleting that should rollback transaction"); diff --git a/samples/sample-fastify/package.json b/samples/sample-fastify/package.json index 5c01dc65..dd915361 100644 --- a/samples/sample-fastify/package.json +++ b/samples/sample-fastify/package.json @@ -38,11 +38,13 @@ "@node-boot/fastify-server": "1.0.0", "@node-boot/authorization": "1.0.0", "@node-boot/di": "1.0.0", + "@node-boot/error": "1.0.0", + "@node-boot/extension": "1.0.0", "@node-boot/starter-actuator": "1.0.0", - "routing-controllers": "^0.10.4", - "routing-controllers-openapi": "^4.0.0", + "@node-boot/starter-persistence": "1.0.0", "reflect-metadata": "^0.1.13", "fastify": "^4.21.0", + "typeorm": "^0.3.17", "middie": "^7.1.0", "fastify-multer": "^2.0.3", "body-parser": "^1.20.2", diff --git a/samples/sample-fastify/src/app.ts b/samples/sample-fastify/src/app.ts index 63c6f220..66c19ce4 100644 --- a/samples/sample-fastify/src/app.ts +++ b/samples/sample-fastify/src/app.ts @@ -1,24 +1,19 @@ import "reflect-metadata"; import {Container} from "typedi"; -import { - Configurations, - Controllers, - EnableDI, - GlobalMiddlewares, - NodeBoot, - NodeBootApplication, -} from "@node-boot/core"; +import {Configurations, Controllers, GlobalMiddlewares, NodeBoot, NodeBootApplication} from "@node-boot/core"; import {BackendConfigProperties} from "./config/BackendConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; -import {CustomErrorHandler} from "./middlewares/customErrorHandler"; import {EnableAuthorization} from "@node-boot/authorization"; import {LoggedInUserResolver} from "./auth/LoggedInUserResolver"; import {DefaultAuthorizationResolver} from "./auth/DefaultAuthorizationResolver"; import {FastifyServer} from "@node-boot/fastify-server"; import {EnableOpenApi} from "@node-boot/openapi"; import {EnableActuator} from "@node-boot/starter-actuator"; +import {EnableRepositories} from "@node-boot/starter-persistence"; +import {EnableDI} from "@node-boot/di"; +import {CustomErrorHandler} from "./middlewares/CustomErrorHandler"; @EnableDI(Container) @EnableOpenApi() @@ -38,6 +33,7 @@ import {EnableActuator} from "@node-boot/starter-actuator"; * */ @EnableActuator() @EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationResolver) +@EnableRepositories() @NodeBootApplication() export class FactsServiceApp { static start() { diff --git a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts index 40f49bc0..2b22a17e 100644 --- a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts @@ -1,24 +1,31 @@ -import {AuthorizationResolver} from "@node-boot/authorization"; -import {RequestContext} from "@node-boot/context"; +import {Action, AuthorizationChecker} from "@node-boot/context"; import {Component} from "@node-boot/core"; +import {FastifyRequest} from "fastify/types/request"; +import {FastifyReply} from "fastify/types/reply"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class DefaultAuthorizationResolver implements AuthorizationResolver { - async authorize(context: RequestContext, roles: any[]): Promise { +export class DefaultAuthorizationResolver implements AuthorizationChecker { + @Inject() + private logger: Logger; + + async check(action: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check // checker must return either boolean (true or false) // either promise that resolves a boolean value // demo code: - const token = context.request.headers["authorization"]; + this.logger.info(`Checking authorization`); + + const token = action.request.headers["authorization"]; //const user = await getEntityManager().findOneByToken(User, token); const user = { roles: ["USER", "ADMIN"], }; if (user && !roles.length) return true; - if (user && roles.find(role => user.roles.indexOf(role) !== -1)) return true; - return false; + return user && roles.find(role => user.roles.indexOf(role) !== -1) !== undefined; } } diff --git a/samples/sample-fastify/src/auth/LoggedInUserResolver.ts b/samples/sample-fastify/src/auth/LoggedInUserResolver.ts index 75dfcc14..adb59b41 100644 --- a/samples/sample-fastify/src/auth/LoggedInUserResolver.ts +++ b/samples/sample-fastify/src/auth/LoggedInUserResolver.ts @@ -1,13 +1,21 @@ -import {RequestContext} from "@node-boot/context"; +import {Action, CurrentUserChecker} from "@node-boot/context"; import {Component} from "@node-boot/core"; -import {CurrentUserResolver} from "@node-boot/authorization"; +import {FastifyRequest} from "fastify/types/request"; +import {FastifyReply} from "fastify/types/reply"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class LoggedInUserResolver implements CurrentUserResolver { - async getCurrentUser(context: RequestContext): Promise { +export class LoggedInUserResolver implements CurrentUserChecker { + @Inject() + private logger: Logger; + + async check(action: Action): Promise { + this.logger.info(`Checking current logged in user`); + // Your logic to fetch the current user from the request, database, or any other source // For example, you might want to retrieve user info from a session, token, or database - context.request; + action.request; return { id: 1, username: "exampleUser", diff --git a/samples/sample-fastify/src/config/AppConfigProperties.ts b/samples/sample-fastify/src/config/AppConfigProperties.ts new file mode 100644 index 00000000..3198602a --- /dev/null +++ b/samples/sample-fastify/src/config/AppConfigProperties.ts @@ -0,0 +1,14 @@ +import {ConfigurationProperties} from "@node-boot/config"; + +@ConfigurationProperties({ + configPath: "node-boot.app", + configName: "app-config", +}) +export class AppConfigProperties { + name: string; + platform: string; + environment: string; + defaultErrorHandler: boolean; + customErrorHandler?: boolean; + port: number; +} diff --git a/samples/sample-fastify/src/config/ClassTransformConfiguration.ts b/samples/sample-fastify/src/config/ClassTransformConfiguration.ts index 691a7663..b6d0cba4 100644 --- a/samples/sample-fastify/src/config/ClassTransformConfiguration.ts +++ b/samples/sample-fastify/src/config/ClassTransformConfiguration.ts @@ -1,9 +1,4 @@ -import { - ClassToPlainTransform, - Configuration, - EnableClassTransformer, - PlainToClassTransform, -} from "@node-boot/core"; +import {ClassToPlainTransform, Configuration, EnableClassTransformer, PlainToClassTransform} from "@node-boot/core"; @Configuration() @EnableClassTransformer({enabled: false}) diff --git a/samples/sample-fastify/src/controllers/users.controller.ts b/samples/sample-fastify/src/controllers/users.controller.ts index 17fd07c7..e237f41a 100644 --- a/samples/sample-fastify/src/controllers/users.controller.ts +++ b/samples/sample-fastify/src/controllers/users.controller.ts @@ -1,44 +1,44 @@ -import {Body, Delete, Get, HttpCode, Param, Post, Put} from "routing-controllers"; +import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put} from "@node-boot/core"; import {UserService} from "../services/users.service"; -import {User} from "../interfaces/users.interface"; -import {BackendConfigProperties} from "../config/BackendConfigProperties"; +import {AppConfigProperties} from "../config/AppConfigProperties"; import {Logger} from "winston"; -import {Controller} from "@node-boot/core"; import {Inject} from "@node-boot/di"; import {OpenAPI} from "@node-boot/openapi"; -import {Authorized, CurrentUser} from "@node-boot/authorization"; +import {Authorized} from "@node-boot/authorization"; +import {User} from "../persistence"; -@Controller() +@Controller("/users", "v1") export class UserController { constructor( private readonly user: UserService, private readonly logger: Logger, - @Inject("backend-config") - private readonly backendConfigProperties: BackendConfigProperties, + @Inject("app-config") + private readonly appConfigProperties: AppConfigProperties, ) {} - @Get("/users") - @Authorized() + @Get("/") @OpenAPI({summary: "Return a list of users"}) - async getUsers(@CurrentUser() loggedIn: any) { - this.logger.info( - `Injected backend configuration properties: ${JSON.stringify( - this.backendConfigProperties, - )}`, - ); - this.logger.info(`Logged in user is ${loggedIn.username}`); + async getUsers() { + this.logger.info(`Injected backend configuration properties: ${JSON.stringify(this.appConfigProperties)}`); const findAllUsersData: User[] = await this.user.findAllUser(); return {data: findAllUsersData, message: "findAll"}; } - @Get("/users/:id") + @Get("/query/") + @OpenAPI({summary: "Return a list of users using a custom query"}) + async getWithCustomQuery() { + const data: User[] = await this.user.findWithCustomQuery(); + return {data: data, message: "findWithCustomQuery"}; + } + + @Get("/:id") @OpenAPI({summary: "Return find a user"}) async getUserById(@Param("id") userId: number) { const findOneUserData: User = await this.user.findUserById(userId); return {data: findOneUserData, message: "findOne"}; } - @Post("/users") + @Post("/") @HttpCode(201) @OpenAPI({summary: "Create a new user"}) @Authorized() @@ -47,17 +47,17 @@ export class UserController { return {data: createUserData, message: "created"}; } - @Put("/users/:id") + @Put("/:id") @OpenAPI({summary: "Update a user"}) async updateUser(@Param("id") userId: number, @Body() userData: User) { - const updateUserData: User[] = await this.user.updateUser(userId, userData); + const updateUserData: User = await this.user.updateUser(userId, userData); return {data: updateUserData, message: "updated"}; } - @Delete("/users/:id") + @Delete("/:id") @OpenAPI({summary: "Delete a user"}) async deleteUser(@Param("id") userId: number) { - const deleteUserData: User[] = await this.user.deleteUser(userId); - return {data: deleteUserData, message: "deleted"}; + await this.user.deleteUser(userId); + return {message: `User ${userId} successfully deleted`}; } } diff --git a/samples/sample-fastify/src/exceptions/httpException.ts b/samples/sample-fastify/src/exceptions/httpException.ts index d2322b92..b08d7642 100644 --- a/samples/sample-fastify/src/exceptions/httpException.ts +++ b/samples/sample-fastify/src/exceptions/httpException.ts @@ -1,4 +1,4 @@ -import {HttpError} from "routing-controllers"; +import {HttpError} from "@node-boot/error"; export class HttpException extends HttpError { public status: number; diff --git a/samples/sample-fastify/src/middlewares/CustomErrorHandler.ts b/samples/sample-fastify/src/middlewares/CustomErrorHandler.ts new file mode 100644 index 00000000..ee6a2ae1 --- /dev/null +++ b/samples/sample-fastify/src/middlewares/CustomErrorHandler.ts @@ -0,0 +1,24 @@ +import {Logger} from "winston"; +import {ErrorHandler} from "@node-boot/core"; +import {Inject} from "@node-boot/di"; +import {errorCodes, FastifyError, FastifyReply, FastifyRequest} from "fastify"; +import {Action, ErrorHandlerInterface} from "@node-boot/context"; + +@ErrorHandler() +export class CustomErrorHandler implements ErrorHandlerInterface { + @Inject() + private logger: Logger; + + onError(error: FastifyError, action: Action): void { + const {response} = action; + if (error instanceof errorCodes.FST_ERR_BAD_STATUS_CODE) { + // Log error + this.logger.error(error); + // Send error response + response.status(500).send({ok: false}); + } else { + // fastify will use parent error handler to handle this + response.send(error); + } + } +} diff --git a/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts b/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts index 1264e657..bbbe8b6b 100644 --- a/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-fastify/src/middlewares/LoggingMiddleware.ts @@ -1,22 +1,16 @@ import {Logger} from "winston"; -import {FastifyMiddlewareInterface, Middleware} from "@node-boot/core"; +import {Middleware} from "@node-boot/core"; import {Inject} from "@node-boot/di"; import {FastifyReply, FastifyRequest} from "fastify"; import {HookHandlerDoneFunction} from "fastify/types/hooks"; +import {Action, MiddlewareInterface} from "@node-boot/context"; @Middleware({type: "before"}) -export class LoggingMiddleware - implements FastifyMiddlewareInterface -{ +export class LoggingMiddleware implements MiddlewareInterface { @Inject() private logger: Logger; - use( - request: FastifyRequest, - reply: FastifyReply, - done: HookHandlerDoneFunction, - payload?: any, - ): void { + use(action: Action, payload?: any): void { this.logger.info(`Logging Middleware: Incoming request`); //done(); } diff --git a/samples/sample-fastify/src/middlewares/customErrorHandler.ts b/samples/sample-fastify/src/middlewares/customErrorHandler.ts deleted file mode 100644 index 543abbf2..00000000 --- a/samples/sample-fastify/src/middlewares/customErrorHandler.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {Logger} from "winston"; -import {ErrorHandler} from "@node-boot/core"; -import {Inject} from "@node-boot/di"; -import {FastifyRequest} from "fastify/types/request"; -import {FastifyReply} from "fastify/types/reply"; -import {FastifyErrorHandlerInterface} from "@node-boot/core/src"; -import {errorCodes, FastifyError} from "fastify"; - -@ErrorHandler() -export class CustomErrorHandler - implements FastifyErrorHandlerInterface -{ - @Inject() - private logger: Logger; - - error(request: FastifyRequest, reply: FastifyReply, error: FastifyError): void { - if (error instanceof errorCodes.FST_ERR_BAD_STATUS_CODE) { - // Log error - this.logger.error(error); - // Send error response - reply.status(500).send({ok: false}); - } else { - // fastify will use parent error handler to handle this - reply.send(error); - } - } -} diff --git a/samples/sample-fastify/src/persistence/CustomNamingStrategy.ts b/samples/sample-fastify/src/persistence/CustomNamingStrategy.ts new file mode 100644 index 00000000..0ae4fe88 --- /dev/null +++ b/samples/sample-fastify/src/persistence/CustomNamingStrategy.ts @@ -0,0 +1,11 @@ +import {DefaultNamingStrategy} from "typeorm"; +import {PersistenceNamingStrategy} from "@node-boot/starter-persistence"; + +@PersistenceNamingStrategy() +export class CustomNamingStrategy extends DefaultNamingStrategy { + name = "sample-naming-strategy"; + + override tableName(targetName: string, userSpecifiedName: string | undefined): string { + return `nb-${super.tableName(targetName, userSpecifiedName)}`; + } +} diff --git a/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts new file mode 100644 index 00000000..4ebbd714 --- /dev/null +++ b/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts @@ -0,0 +1,9 @@ +import {DatasourceConfiguration} from "@node-boot/starter-persistence"; + +@DatasourceConfiguration({ + type: "better-sqlite3", + database: "express-sample.db", + synchronize: true, + migrationsRun: true, +}) +export class DatasourceOverridesConfiguration {} diff --git a/samples/sample-fastify/src/persistence/entities/User.ts b/samples/sample-fastify/src/persistence/entities/User.ts new file mode 100644 index 00000000..be938b7f --- /dev/null +++ b/samples/sample-fastify/src/persistence/entities/User.ts @@ -0,0 +1,16 @@ +import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"; + +@Entity() +export class User { + @PrimaryGeneratedColumn() + id: number; + + @Column() + email: string; + + @Column() + password: string; + + @Column({nullable: true}) + name?: string; // New field +} diff --git a/samples/sample-fastify/src/persistence/entities/index.ts b/samples/sample-fastify/src/persistence/entities/index.ts new file mode 100644 index 00000000..bddc181c --- /dev/null +++ b/samples/sample-fastify/src/persistence/entities/index.ts @@ -0,0 +1 @@ +export {User} from "./User"; diff --git a/samples/sample-fastify/src/persistence/index.ts b/samples/sample-fastify/src/persistence/index.ts new file mode 100644 index 00000000..17a8bf58 --- /dev/null +++ b/samples/sample-fastify/src/persistence/index.ts @@ -0,0 +1,6 @@ +export * from "./entities"; +export * from "./repositories"; +export * from "./migrations"; +export * from "./listeners"; +export {CustomNamingStrategy} from "./CustomNamingStrategy"; +export {DatasourceOverridesConfiguration} from "./DatasourceOverridesConfiguration"; diff --git a/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts new file mode 100644 index 00000000..71a478c8 --- /dev/null +++ b/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts @@ -0,0 +1,139 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import { + EntitySubscriberInterface, + InsertEvent, + RecoverEvent, + RemoveEvent, + SoftRemoveEvent, + TransactionCommitEvent, + TransactionRollbackEvent, + TransactionStartEvent, + UpdateEvent, +} from "typeorm"; +import {Logger} from "winston"; +import {Inject} from "@node-boot/di"; + +@EntityEventSubscriber() +export class GlobalEntityEventListener implements EntitySubscriberInterface { + @Inject() + private logger: Logger; + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + this.logger.info(`AFTER ENTITY LOADED: `, entity); + } + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE ENTITY INSERTED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + this.logger.info(`AFTER ENTITY INSERTED: `, event.entity); + } + + /** + * Called before entity update. + */ + beforeUpdate(event: UpdateEvent) { + this.logger.info(`BEFORE ENTITY UPDATED: `, event.entity); + } + + /** + * Called after entity update. + */ + afterUpdate(event: UpdateEvent) { + this.logger.info(`AFTER ENTITY UPDATED: `, event.entity); + } + + /** + * Called before entity removal. + */ + beforeRemove(event: RemoveEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity removal. + */ + afterRemove(event: RemoveEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called before entity removal. + */ + beforeSoftRemove(event: SoftRemoveEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + } + + /** + * Called after entity removal. + */ + afterSoftRemove(event: SoftRemoveEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + } + + /** + * Called before entity recovery. + */ + beforeRecover(event: RecoverEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + } + + /** + * Called after entity recovery. + */ + afterRecover(event: RecoverEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + } + + /** + * Called before transaction start. + */ + beforeTransactionStart(event: TransactionStartEvent) { + this.logger.info(`BEFORE TRANSACTION STARTED`); + } + + /** + * Called after transaction start. + */ + afterTransactionStart(event: TransactionStartEvent) { + this.logger.info(`AFTER TRANSACTION STARTED`); + } + + /** + * Called before transaction commit. + */ + beforeTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`BEFORE TRANSACTION COMMITTED`); + } + + /** + * Called after transaction commit. + */ + afterTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`AFTER TRANSACTION COMMITTED`); + } + + /** + * Called before transaction rollback. + */ + beforeTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`BEFORE TRANSACTION ROLLBACK`); + } + + /** + * Called after transaction rollback. + */ + afterTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`AFTER TRANSACTION ROLLBACK`); + } +} diff --git a/samples/sample-fastify/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-fastify/src/persistence/listeners/UserEntityEventListener.ts new file mode 100644 index 00000000..434e3488 --- /dev/null +++ b/samples/sample-fastify/src/persistence/listeners/UserEntityEventListener.ts @@ -0,0 +1,40 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import {EntitySubscriberInterface, InsertEvent} from "typeorm"; +import {User} from "../entities"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; +import {GreetingService} from "../../services/greeting.service"; + +/** + * The UserEntityEventListener class is an event subscriber that listens to events related to the User entity. + * It is responsible for logging information before and after a user is inserted, and also for invoking the sayHello + * method of the GreetingService class. + * + * */ +@EntityEventSubscriber() +export class UserEntityEventListener implements EntitySubscriberInterface { + @Inject() + private logger: Logger; + + @Inject() + private greetingService: GreetingService; + + /** + * Indicates that this subscriber only listen to User events. + */ + listenTo() { + return User; + } + + /** + * Called before user insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE USER INSERTED: `, event.entity); + } + + afterInsert(event: InsertEvent): Promise | void { + this.logger.info(`AFTER USER INSERTED: `, event.entity); + this.greetingService.sayHello(event.entity); + } +} diff --git a/samples/sample-fastify/src/persistence/listeners/index.ts b/samples/sample-fastify/src/persistence/listeners/index.ts new file mode 100644 index 00000000..9a3d3a70 --- /dev/null +++ b/samples/sample-fastify/src/persistence/listeners/index.ts @@ -0,0 +1,2 @@ +export {GlobalEntityEventListener} from "./GlobalEntityEventListener"; +export {UserEntityEventListener} from "./UserEntityEventListener"; diff --git a/samples/sample-fastify/src/persistence/migrations/1701774002463-migration.ts b/samples/sample-fastify/src/persistence/migrations/1701774002463-migration.ts new file mode 100644 index 00000000..76c4591e --- /dev/null +++ b/samples/sample-fastify/src/persistence/migrations/1701774002463-migration.ts @@ -0,0 +1,34 @@ +import {MigrationInterface, QueryRunner, Table} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701774002463 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "nb-user", + columns: [ + { + name: "id", + type: "INTEGER", + isPrimary: true, + isGenerated: true, + generationStrategy: "increment", + }, + { + name: "email", + type: "varchar", + }, + { + name: "password", + type: "varchar", + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("nb-user"); + } +} diff --git a/samples/sample-fastify/src/persistence/migrations/1701786331338-migration.ts b/samples/sample-fastify/src/persistence/migrations/1701786331338-migration.ts new file mode 100644 index 00000000..5cd11022 --- /dev/null +++ b/samples/sample-fastify/src/persistence/migrations/1701786331338-migration.ts @@ -0,0 +1,13 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701786331338 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "nb-user" ADD COLUMN "name" varchar(255)`); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "nb-user" DROP COLUMN "name"`); + } +} diff --git a/samples/sample-fastify/src/persistence/migrations/index.ts b/samples/sample-fastify/src/persistence/migrations/index.ts new file mode 100644 index 00000000..f3ee9f0b --- /dev/null +++ b/samples/sample-fastify/src/persistence/migrations/index.ts @@ -0,0 +1,2 @@ +export {Migration1701774002463} from "./1701774002463-migration"; +export {Migration1701786331338} from "./1701786331338-migration"; diff --git a/samples/sample-fastify/src/persistence/repositories/UserRepository.ts b/samples/sample-fastify/src/persistence/repositories/UserRepository.ts new file mode 100644 index 00000000..fdabc794 --- /dev/null +++ b/samples/sample-fastify/src/persistence/repositories/UserRepository.ts @@ -0,0 +1,18 @@ +import {Repository} from "typeorm"; +import {DataRepository} from "@node-boot/starter-persistence"; +import {User} from "../entities"; + +@DataRepository(User) +export class UserRepository extends Repository { + /** + * Example custom query using the built-in query builder. + * + * For detailed info check: https://orkhan.gitbook.io/typeorm/docs/select-query-builder + * */ + findByQueryIn() { + // SELECT ... FROM users user WHERE user.id IN (1, 2) + return this.createQueryBuilder("user") + .where("user.id IN (:...ids)", {ids: [1, 2]}) + .getMany(); + } +} diff --git a/samples/sample-fastify/src/persistence/repositories/index.ts b/samples/sample-fastify/src/persistence/repositories/index.ts new file mode 100644 index 00000000..3ddfbbbf --- /dev/null +++ b/samples/sample-fastify/src/persistence/repositories/index.ts @@ -0,0 +1 @@ +export {UserRepository} from "./UserRepository"; diff --git a/samples/sample-fastify/src/services/greeting.service.ts b/samples/sample-fastify/src/services/greeting.service.ts new file mode 100644 index 00000000..4ad72ada --- /dev/null +++ b/samples/sample-fastify/src/services/greeting.service.ts @@ -0,0 +1,12 @@ +import {Logger} from "winston"; +import {Service} from "@node-boot/core"; +import {User} from "../persistence"; + +@Service() +export class GreetingService { + constructor(private readonly logger: Logger) {} + + public sayHello(user: User): void { + this.logger.info(`I'm really happy that you exists ${user.id}/${user.email}`); + } +} diff --git a/samples/sample-fastify/src/services/users.service.ts b/samples/sample-fastify/src/services/users.service.ts index f1aa9f08..ab0b3ec0 100644 --- a/samples/sample-fastify/src/services/users.service.ts +++ b/samples/sample-fastify/src/services/users.service.ts @@ -1,52 +1,86 @@ -import {User} from "../interfaces/users.interface"; -import {UserModel} from "../models/users.model"; -import {HttpException} from "../exceptions/httpException"; -import {CreateUserDto} from "../dtos/users.dto"; +import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; import {Logger} from "winston"; import {ConfigService} from "@node-boot/config"; import {Service} from "@node-boot/core"; -import {NotFoundError} from "routing-controllers"; +import {User, UserRepository} from "../persistence"; +import {UserModel} from "../models/users.model"; +import {Optional} from "@node-boot/extension"; +import {runOnTransactionCommit, runOnTransactionRollback, Transactional} from "@node-boot/starter-persistence"; +import {HttpError, NotFoundError} from "@node-boot/error"; @Service() export class UserService { - constructor(private readonly logger: Logger, private readonly configService: ConfigService) {} + constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { + UserModel.forEach(user => this.userRepository.save(user)); + } public async findAllUser(): Promise { this.logger.info("Getting all users"); - const users: User[] = UserModel; + const appName = this.configService.getString("node-boot.app.name"); + this.logger.info(`Reading node-boot.app.name from app-config.yam: ${appName}`); + return this.userRepository.find(); + } - const baseUrl = this.configService.getString("backend.baseUrl"); - this.logger.info(`Reading backend.baseUrl from app-config.yam: ${baseUrl}`); - return users; + public async findWithCustomQuery(): Promise { + this.logger.info("Getting all users with a custom query"); + return this.userRepository.findByQueryIn(); } public async findUserById(userId: number): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new NotFoundError("User doesn't exist"); - return findUser; + const user = await this.userRepository.findOneBy({ + id: userId, + }); + return Optional.of(user) + .orElseThrow(() => new NotFoundError("User doesn't exist")) + .get(); } + @Transactional() public async createUser(userData: CreateUserDto): Promise { - const findUser = UserModel.find(user => user.email === userData.email); - if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); + const existingUser = await this.userRepository.findOneBy({ + email: userData.email, + }); - return {id: UserModel.length + 1, ...userData}; - } + runOnTransactionCommit(() => { + this.logger.info("Transaction was successfully committed"); + }); - public async updateUser(userId: number, userData: CreateUserDto): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new HttpException(409, "User doesn't exist"); + return Optional.of(existingUser) + .ifPresentThrow(() => new HttpError(409, `This email ${userData.email} already exists`)) + .elseAsync(() => this.userRepository.save(userData)); + } - return UserModel.map((user: User) => { - if (user.id === findUser.id) user = {id: userId, ...userData}; - return user; + @Transactional() + public async updateUser(userId: number, userData: UpdateUserDto): Promise { + const user = await this.userRepository.findOneBy({ + id: userId, }); + + return Optional.of(user) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) + .map(user => { + return { + ...user, + userData, + }; + }) + .runAsync(async user => await this.userRepository.save(user)); } - public async deleteUser(userId: number): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new HttpException(409, "User doesn't exist"); + @Transactional() + public async deleteUser(userId: number): Promise { + const user = await this.userRepository.findOneBy({ + id: userId, + }); + + runOnTransactionRollback(error => { + this.logger.warn("Transactions was rolled back due to error:", error); + }); + + await Optional.of(user) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) + .runAsync(user => this.userRepository.delete({id: userId})); - return UserModel.filter(user => user.id !== findUser.id); + throw new Error("Error after deleting that should rollback transaction"); } } diff --git a/samples/sample-koa/package.json b/samples/sample-koa/package.json index f497aece..bd3df68c 100644 --- a/samples/sample-koa/package.json +++ b/samples/sample-koa/package.json @@ -38,9 +38,12 @@ "@node-boot/koa-server": "1.0.0", "@node-boot/authorization": "1.0.0", "@node-boot/di": "1.0.0", + "@node-boot/error": "1.0.0", "@node-boot/starter-actuator": "1.0.0", - "routing-controllers": "^0.10.4", + "@node-boot/starter-persistence": "1.0.0", + "@node-boot/extension": "1.0.0", "reflect-metadata": "^0.1.13", + "typeorm": "^0.3.17", "koa": ">=2.14.2", "@koa/router": "^12.0.0", "koa-bodyparser": "^4.4.1", diff --git a/samples/sample-koa/src/app.ts b/samples/sample-koa/src/app.ts index e61ef994..3d194281 100644 --- a/samples/sample-koa/src/app.ts +++ b/samples/sample-koa/src/app.ts @@ -1,23 +1,17 @@ import "reflect-metadata"; import {Container} from "typedi"; -import { - Configurations, - Controllers, - EnableDI, - GlobalMiddlewares, - NodeBoot, - NodeBootApplication, -} from "@node-boot/core"; +import {Configurations, Controllers, GlobalMiddlewares, NodeBoot, NodeBootApplication} from "@node-boot/core"; import {BackendConfigProperties} from "./config/BackendConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; import {EnableAuthorization} from "@node-boot/authorization"; import {LoggedInUserResolver} from "./auth/LoggedInUserResolver"; -import {DefaultAuthorizationResolver} from "./auth/DefaultAuthorizationResolver"; import {KoaServer} from "@node-boot/koa-server"; import {EnableOpenApi} from "@node-boot/openapi"; import {EnableActuator} from "@node-boot/starter-actuator"; +import {DefaultAuthorizationChecker} from "./auth/DefaultAuthorizationChecker"; +import {EnableDI} from "@node-boot/di"; @EnableDI(Container) @EnableOpenApi() @@ -36,7 +30,7 @@ import {EnableActuator} from "@node-boot/starter-actuator"; }) * */ @EnableActuator() -@EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationResolver) +@EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationChecker) @NodeBootApplication() export class FactsServiceApp { static start() { diff --git a/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts similarity index 57% rename from samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts rename to samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts index 40f49bc0..ac1dab01 100644 --- a/samples/sample-koa/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts @@ -1,17 +1,24 @@ -import {AuthorizationResolver} from "@node-boot/authorization"; -import {RequestContext} from "@node-boot/context"; +import {Action, AuthorizationChecker} from "@node-boot/context"; import {Component} from "@node-boot/core"; +import {Request, Response} from "koa"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class DefaultAuthorizationResolver implements AuthorizationResolver { - async authorize(context: RequestContext, roles: any[]): Promise { +export class DefaultAuthorizationChecker implements AuthorizationChecker { + @Inject() + private logger: Logger; + + async check(action: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check // checker must return either boolean (true or false) // either promise that resolves a boolean value // demo code: - const token = context.request.headers["authorization"]; + this.logger.info(`Checking authorization`); + + const token = action.request.headers["authorization"]; //const user = await getEntityManager().findOneByToken(User, token); const user = { diff --git a/samples/sample-koa/src/auth/LoggedInUserResolver.ts b/samples/sample-koa/src/auth/LoggedInUserResolver.ts index 75dfcc14..328d654c 100644 --- a/samples/sample-koa/src/auth/LoggedInUserResolver.ts +++ b/samples/sample-koa/src/auth/LoggedInUserResolver.ts @@ -1,16 +1,24 @@ -import {RequestContext} from "@node-boot/context"; +import {Action, CurrentUserChecker} from "@node-boot/context"; import {Component} from "@node-boot/core"; -import {CurrentUserResolver} from "@node-boot/authorization"; +import {Request, Response} from "koa"; +import {User} from "../interfaces/users.interface"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; @Component() -export class LoggedInUserResolver implements CurrentUserResolver { - async getCurrentUser(context: RequestContext): Promise { +export class LoggedInUserResolver implements CurrentUserChecker { + @Inject() + private logger: Logger; + + async check(action: Action): Promise { + this.logger.info(`Checking current logged in user`); + // Your logic to fetch the current user from the request, database, or any other source // For example, you might want to retrieve user info from a session, token, or database - context.request; + action.request; return { id: 1, - username: "exampleUser", + email: "user@example.com", // ... other user properties }; } diff --git a/samples/sample-koa/src/config/AppConfigProperties.ts b/samples/sample-koa/src/config/AppConfigProperties.ts new file mode 100644 index 00000000..3198602a --- /dev/null +++ b/samples/sample-koa/src/config/AppConfigProperties.ts @@ -0,0 +1,14 @@ +import {ConfigurationProperties} from "@node-boot/config"; + +@ConfigurationProperties({ + configPath: "node-boot.app", + configName: "app-config", +}) +export class AppConfigProperties { + name: string; + platform: string; + environment: string; + defaultErrorHandler: boolean; + customErrorHandler?: boolean; + port: number; +} diff --git a/samples/sample-koa/src/config/ClassTransformConfiguration.ts b/samples/sample-koa/src/config/ClassTransformConfiguration.ts index 691a7663..b6d0cba4 100644 --- a/samples/sample-koa/src/config/ClassTransformConfiguration.ts +++ b/samples/sample-koa/src/config/ClassTransformConfiguration.ts @@ -1,9 +1,4 @@ -import { - ClassToPlainTransform, - Configuration, - EnableClassTransformer, - PlainToClassTransform, -} from "@node-boot/core"; +import {ClassToPlainTransform, Configuration, EnableClassTransformer, PlainToClassTransform} from "@node-boot/core"; @Configuration() @EnableClassTransformer({enabled: false}) diff --git a/samples/sample-koa/src/controllers/users.controller.ts b/samples/sample-koa/src/controllers/users.controller.ts index fd042324..6dbe5ff8 100644 --- a/samples/sample-koa/src/controllers/users.controller.ts +++ b/samples/sample-koa/src/controllers/users.controller.ts @@ -1,44 +1,46 @@ -import {Body, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "routing-controllers"; +import {Body, Controller, Delete, Get, HttpCode, Param, Post, Put, UseBefore} from "@node-boot/core"; import {UserService} from "../services/users.service"; -import {User} from "../interfaces/users.interface"; import {ValidationMiddleware} from "../middlewares/validation.middleware"; import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; -import {BackendConfigProperties} from "../config/BackendConfigProperties"; +import {AppConfigProperties} from "../config/AppConfigProperties"; import {Logger} from "winston"; -import {Controller} from "@node-boot/core"; import {Inject} from "@node-boot/di"; import {OpenAPI} from "@node-boot/openapi"; import {Authorized} from "@node-boot/authorization"; +import {User} from "../persistence"; -@Controller() +@Controller("/users", "v1") export class UserController { constructor( private readonly user: UserService, private readonly logger: Logger, - @Inject("backend-config") - private readonly backendConfigProperties: BackendConfigProperties, + @Inject("app-config") + private readonly appConfigProperties: AppConfigProperties, ) {} - @Get("/users") + @Get("/") @OpenAPI({summary: "Return a list of users"}) async getUsers() { - this.logger.info( - `Injected backend configuration properties: ${JSON.stringify( - this.backendConfigProperties, - )}`, - ); + this.logger.info(`Injected backend configuration properties: ${JSON.stringify(this.appConfigProperties)}`); const findAllUsersData: User[] = await this.user.findAllUser(); return {data: findAllUsersData, message: "findAll"}; } - @Get("/users/:id") + @Get("/query/") + @OpenAPI({summary: "Return a list of users using a custom query"}) + async getWithCustomQuery() { + const data: User[] = await this.user.findWithCustomQuery(); + return {data: data, message: "findWithCustomQuery"}; + } + + @Get("/:id") @OpenAPI({summary: "Return find a user"}) async getUserById(@Param("id") userId: number) { const findOneUserData: User = await this.user.findUserById(userId); return {data: findOneUserData, message: "findOne"}; } - @Post("/users") + @Post("/") @HttpCode(201) @UseBefore(ValidationMiddleware(CreateUserDto)) @OpenAPI({summary: "Create a new user"}) @@ -48,18 +50,18 @@ export class UserController { return {data: createUserData, message: "created"}; } - @Put("/users/:id") + @Put("/:id") @UseBefore(ValidationMiddleware(UpdateUserDto)) @OpenAPI({summary: "Update a user"}) async updateUser(@Param("id") userId: number, @Body() userData: User) { - const updateUserData: User[] = await this.user.updateUser(userId, userData); + const updateUserData: User = await this.user.updateUser(userId, userData); return {data: updateUserData, message: "updated"}; } - @Delete("/users/:id") + @Delete("/:id") @OpenAPI({summary: "Delete a user"}) async deleteUser(@Param("id") userId: number) { - const deleteUserData: User[] = await this.user.deleteUser(userId); - return {data: deleteUserData, message: "deleted"}; + await this.user.deleteUser(userId); + return {message: `User ${userId} successfully deleted`}; } } diff --git a/samples/sample-koa/src/exceptions/httpException.ts b/samples/sample-koa/src/exceptions/httpException.ts index d2322b92..b08d7642 100644 --- a/samples/sample-koa/src/exceptions/httpException.ts +++ b/samples/sample-koa/src/exceptions/httpException.ts @@ -1,4 +1,4 @@ -import {HttpError} from "routing-controllers"; +import {HttpError} from "@node-boot/error"; export class HttpException extends HttpError { public status: number; diff --git a/samples/sample-koa/src/interfaces/users.interface.ts b/samples/sample-koa/src/interfaces/users.interface.ts index 4eeb5801..21c512d7 100644 --- a/samples/sample-koa/src/interfaces/users.interface.ts +++ b/samples/sample-koa/src/interfaces/users.interface.ts @@ -1,5 +1,5 @@ export interface User { id?: number; email: string; - password: string; + password?: string; } diff --git a/samples/sample-koa/src/middlewares/CustomErrorHandler.ts b/samples/sample-koa/src/middlewares/CustomErrorHandler.ts new file mode 100644 index 00000000..df82dddc --- /dev/null +++ b/samples/sample-koa/src/middlewares/CustomErrorHandler.ts @@ -0,0 +1,27 @@ +import {Logger} from "winston"; +import {ErrorHandler} from "@node-boot/core"; +import {Inject} from "@node-boot/di"; +import {Action, ErrorHandlerInterface} from "@node-boot/context"; +import {Request, Response} from "koa"; +import {HttpError} from "@node-boot/error"; + +@ErrorHandler() +export class CustomErrorHandler implements ErrorHandlerInterface { + @Inject() + private logger: Logger; + + onError(error: HttpError, action: Action): void { + const {request, response, next} = action; + try { + const status: number = error.httpCode || 500; + const message: string = error.message || "Something went wrong"; + + this.logger.error(`[${request.method}] ${request.path} >> StatusCode:: ${status}, Message:: ${message}`); + // FIXME Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client + // FIXME Fix this after refactoring routing-controllers library + // res.status(status).json({message}); + } catch (error) { + next?.(error); + } + } +} diff --git a/samples/sample-koa/src/middlewares/LoggingMiddleware.ts b/samples/sample-koa/src/middlewares/LoggingMiddleware.ts index b390e4e6..709d1309 100644 --- a/samples/sample-koa/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-koa/src/middlewares/LoggingMiddleware.ts @@ -1,16 +1,19 @@ -import {KoaMiddlewareInterface} from "routing-controllers"; import {Logger} from "winston"; import {Middleware} from "@node-boot/core"; import {Inject} from "@node-boot/di"; +import {Action, MiddlewareInterface} from "@node-boot/context"; +import Application, {Request, Response} from "koa"; @Middleware({type: "before"}) -export class LoggingMiddleware implements KoaMiddlewareInterface { +export class LoggingMiddleware implements MiddlewareInterface { @Inject() private logger: Logger; - use(context: any, next: (err?: any) => Promise): Promise { + async use(action: Action, payload?: unknown): Promise { this.logger.info(`Logging Middleware: Incoming request`); - return next() + action.context; + return action + .next?.() .then(() => { console.log("do something after execution"); }) diff --git a/samples/sample-koa/src/middlewares/validation.middleware.ts b/samples/sample-koa/src/middlewares/validation.middleware.ts index 824ca6ab..a3c1b3c5 100644 --- a/samples/sample-koa/src/middlewares/validation.middleware.ts +++ b/samples/sample-koa/src/middlewares/validation.middleware.ts @@ -10,12 +10,7 @@ import {HttpException} from "../exceptions/httpException"; * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present */ -export const ValidationMiddleware = ( - type: any, - skipMissingProperties = false, - whitelist = false, - forbidNonWhitelisted = false, -) => { +export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = false, forbidNonWhitelisted = false) => { return (req: any, res: any, next: any) => { const dto = plainToInstance(type, req.body); validateOrReject(dto, { @@ -28,9 +23,7 @@ export const ValidationMiddleware = ( next(); }) .catch((errors: ValidationError[]) => { - const message = errors - .map((error: ValidationError) => Object.values(error.constraints ?? {})) - .join(", "); + const message = errors.map((error: ValidationError) => Object.values(error.constraints ?? {})).join(", "); next(new HttpException(400, message)); }); }; diff --git a/samples/sample-koa/src/persistence/CustomNamingStrategy.ts b/samples/sample-koa/src/persistence/CustomNamingStrategy.ts new file mode 100644 index 00000000..0ae4fe88 --- /dev/null +++ b/samples/sample-koa/src/persistence/CustomNamingStrategy.ts @@ -0,0 +1,11 @@ +import {DefaultNamingStrategy} from "typeorm"; +import {PersistenceNamingStrategy} from "@node-boot/starter-persistence"; + +@PersistenceNamingStrategy() +export class CustomNamingStrategy extends DefaultNamingStrategy { + name = "sample-naming-strategy"; + + override tableName(targetName: string, userSpecifiedName: string | undefined): string { + return `nb-${super.tableName(targetName, userSpecifiedName)}`; + } +} diff --git a/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts new file mode 100644 index 00000000..4ebbd714 --- /dev/null +++ b/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts @@ -0,0 +1,9 @@ +import {DatasourceConfiguration} from "@node-boot/starter-persistence"; + +@DatasourceConfiguration({ + type: "better-sqlite3", + database: "express-sample.db", + synchronize: true, + migrationsRun: true, +}) +export class DatasourceOverridesConfiguration {} diff --git a/samples/sample-koa/src/persistence/entities/User.ts b/samples/sample-koa/src/persistence/entities/User.ts new file mode 100644 index 00000000..be938b7f --- /dev/null +++ b/samples/sample-koa/src/persistence/entities/User.ts @@ -0,0 +1,16 @@ +import {Column, Entity, PrimaryGeneratedColumn} from "typeorm"; + +@Entity() +export class User { + @PrimaryGeneratedColumn() + id: number; + + @Column() + email: string; + + @Column() + password: string; + + @Column({nullable: true}) + name?: string; // New field +} diff --git a/samples/sample-koa/src/persistence/entities/index.ts b/samples/sample-koa/src/persistence/entities/index.ts new file mode 100644 index 00000000..bddc181c --- /dev/null +++ b/samples/sample-koa/src/persistence/entities/index.ts @@ -0,0 +1 @@ +export {User} from "./User"; diff --git a/samples/sample-koa/src/persistence/index.ts b/samples/sample-koa/src/persistence/index.ts new file mode 100644 index 00000000..17a8bf58 --- /dev/null +++ b/samples/sample-koa/src/persistence/index.ts @@ -0,0 +1,6 @@ +export * from "./entities"; +export * from "./repositories"; +export * from "./migrations"; +export * from "./listeners"; +export {CustomNamingStrategy} from "./CustomNamingStrategy"; +export {DatasourceOverridesConfiguration} from "./DatasourceOverridesConfiguration"; diff --git a/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts new file mode 100644 index 00000000..71a478c8 --- /dev/null +++ b/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts @@ -0,0 +1,139 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import { + EntitySubscriberInterface, + InsertEvent, + RecoverEvent, + RemoveEvent, + SoftRemoveEvent, + TransactionCommitEvent, + TransactionRollbackEvent, + TransactionStartEvent, + UpdateEvent, +} from "typeorm"; +import {Logger} from "winston"; +import {Inject} from "@node-boot/di"; + +@EntityEventSubscriber() +export class GlobalEntityEventListener implements EntitySubscriberInterface { + @Inject() + private logger: Logger; + + /** + * Called after entity is loaded. + */ + afterLoad(entity: any) { + this.logger.info(`AFTER ENTITY LOADED: `, entity); + } + + /** + * Called before entity insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE ENTITY INSERTED: `, event.entity); + } + + /** + * Called after entity insertion. + */ + afterInsert(event: InsertEvent) { + this.logger.info(`AFTER ENTITY INSERTED: `, event.entity); + } + + /** + * Called before entity update. + */ + beforeUpdate(event: UpdateEvent) { + this.logger.info(`BEFORE ENTITY UPDATED: `, event.entity); + } + + /** + * Called after entity update. + */ + afterUpdate(event: UpdateEvent) { + this.logger.info(`AFTER ENTITY UPDATED: `, event.entity); + } + + /** + * Called before entity removal. + */ + beforeRemove(event: RemoveEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called after entity removal. + */ + afterRemove(event: RemoveEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + } + + /** + * Called before entity removal. + */ + beforeSoftRemove(event: SoftRemoveEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + } + + /** + * Called after entity removal. + */ + afterSoftRemove(event: SoftRemoveEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + } + + /** + * Called before entity recovery. + */ + beforeRecover(event: RecoverEvent) { + this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + } + + /** + * Called after entity recovery. + */ + afterRecover(event: RecoverEvent) { + this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + } + + /** + * Called before transaction start. + */ + beforeTransactionStart(event: TransactionStartEvent) { + this.logger.info(`BEFORE TRANSACTION STARTED`); + } + + /** + * Called after transaction start. + */ + afterTransactionStart(event: TransactionStartEvent) { + this.logger.info(`AFTER TRANSACTION STARTED`); + } + + /** + * Called before transaction commit. + */ + beforeTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`BEFORE TRANSACTION COMMITTED`); + } + + /** + * Called after transaction commit. + */ + afterTransactionCommit(event: TransactionCommitEvent) { + this.logger.info(`AFTER TRANSACTION COMMITTED`); + } + + /** + * Called before transaction rollback. + */ + beforeTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`BEFORE TRANSACTION ROLLBACK`); + } + + /** + * Called after transaction rollback. + */ + afterTransactionRollback(event: TransactionRollbackEvent) { + this.logger.info(`AFTER TRANSACTION ROLLBACK`); + } +} diff --git a/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts new file mode 100644 index 00000000..434e3488 --- /dev/null +++ b/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts @@ -0,0 +1,40 @@ +import {EntityEventSubscriber} from "@node-boot/starter-persistence"; +import {EntitySubscriberInterface, InsertEvent} from "typeorm"; +import {User} from "../entities"; +import {Inject} from "@node-boot/di"; +import {Logger} from "winston"; +import {GreetingService} from "../../services/greeting.service"; + +/** + * The UserEntityEventListener class is an event subscriber that listens to events related to the User entity. + * It is responsible for logging information before and after a user is inserted, and also for invoking the sayHello + * method of the GreetingService class. + * + * */ +@EntityEventSubscriber() +export class UserEntityEventListener implements EntitySubscriberInterface { + @Inject() + private logger: Logger; + + @Inject() + private greetingService: GreetingService; + + /** + * Indicates that this subscriber only listen to User events. + */ + listenTo() { + return User; + } + + /** + * Called before user insertion. + */ + beforeInsert(event: InsertEvent) { + this.logger.info(`BEFORE USER INSERTED: `, event.entity); + } + + afterInsert(event: InsertEvent): Promise | void { + this.logger.info(`AFTER USER INSERTED: `, event.entity); + this.greetingService.sayHello(event.entity); + } +} diff --git a/samples/sample-koa/src/persistence/listeners/index.ts b/samples/sample-koa/src/persistence/listeners/index.ts new file mode 100644 index 00000000..9a3d3a70 --- /dev/null +++ b/samples/sample-koa/src/persistence/listeners/index.ts @@ -0,0 +1,2 @@ +export {GlobalEntityEventListener} from "./GlobalEntityEventListener"; +export {UserEntityEventListener} from "./UserEntityEventListener"; diff --git a/samples/sample-koa/src/persistence/migrations/1701774002463-migration.ts b/samples/sample-koa/src/persistence/migrations/1701774002463-migration.ts new file mode 100644 index 00000000..76c4591e --- /dev/null +++ b/samples/sample-koa/src/persistence/migrations/1701774002463-migration.ts @@ -0,0 +1,34 @@ +import {MigrationInterface, QueryRunner, Table} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701774002463 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + name: "nb-user", + columns: [ + { + name: "id", + type: "INTEGER", + isPrimary: true, + isGenerated: true, + generationStrategy: "increment", + }, + { + name: "email", + type: "varchar", + }, + { + name: "password", + type: "varchar", + }, + ], + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropTable("nb-user"); + } +} diff --git a/samples/sample-koa/src/persistence/migrations/1701786331338-migration.ts b/samples/sample-koa/src/persistence/migrations/1701786331338-migration.ts new file mode 100644 index 00000000..5cd11022 --- /dev/null +++ b/samples/sample-koa/src/persistence/migrations/1701786331338-migration.ts @@ -0,0 +1,13 @@ +import {MigrationInterface, QueryRunner} from "typeorm"; +import {Migration} from "@node-boot/starter-persistence"; + +@Migration() +export class Migration1701786331338 implements MigrationInterface { + async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "nb-user" ADD COLUMN "name" varchar(255)`); + } + + async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "nb-user" DROP COLUMN "name"`); + } +} diff --git a/samples/sample-koa/src/persistence/migrations/index.ts b/samples/sample-koa/src/persistence/migrations/index.ts new file mode 100644 index 00000000..f3ee9f0b --- /dev/null +++ b/samples/sample-koa/src/persistence/migrations/index.ts @@ -0,0 +1,2 @@ +export {Migration1701774002463} from "./1701774002463-migration"; +export {Migration1701786331338} from "./1701786331338-migration"; diff --git a/samples/sample-koa/src/persistence/repositories/UserRepository.ts b/samples/sample-koa/src/persistence/repositories/UserRepository.ts new file mode 100644 index 00000000..fdabc794 --- /dev/null +++ b/samples/sample-koa/src/persistence/repositories/UserRepository.ts @@ -0,0 +1,18 @@ +import {Repository} from "typeorm"; +import {DataRepository} from "@node-boot/starter-persistence"; +import {User} from "../entities"; + +@DataRepository(User) +export class UserRepository extends Repository { + /** + * Example custom query using the built-in query builder. + * + * For detailed info check: https://orkhan.gitbook.io/typeorm/docs/select-query-builder + * */ + findByQueryIn() { + // SELECT ... FROM users user WHERE user.id IN (1, 2) + return this.createQueryBuilder("user") + .where("user.id IN (:...ids)", {ids: [1, 2]}) + .getMany(); + } +} diff --git a/samples/sample-koa/src/persistence/repositories/index.ts b/samples/sample-koa/src/persistence/repositories/index.ts new file mode 100644 index 00000000..3ddfbbbf --- /dev/null +++ b/samples/sample-koa/src/persistence/repositories/index.ts @@ -0,0 +1 @@ +export {UserRepository} from "./UserRepository"; diff --git a/samples/sample-koa/src/services/greeting.service.ts b/samples/sample-koa/src/services/greeting.service.ts new file mode 100644 index 00000000..4ad72ada --- /dev/null +++ b/samples/sample-koa/src/services/greeting.service.ts @@ -0,0 +1,12 @@ +import {Logger} from "winston"; +import {Service} from "@node-boot/core"; +import {User} from "../persistence"; + +@Service() +export class GreetingService { + constructor(private readonly logger: Logger) {} + + public sayHello(user: User): void { + this.logger.info(`I'm really happy that you exists ${user.id}/${user.email}`); + } +} diff --git a/samples/sample-koa/src/services/users.service.ts b/samples/sample-koa/src/services/users.service.ts index f1aa9f08..ab0b3ec0 100644 --- a/samples/sample-koa/src/services/users.service.ts +++ b/samples/sample-koa/src/services/users.service.ts @@ -1,52 +1,86 @@ -import {User} from "../interfaces/users.interface"; -import {UserModel} from "../models/users.model"; -import {HttpException} from "../exceptions/httpException"; -import {CreateUserDto} from "../dtos/users.dto"; +import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; import {Logger} from "winston"; import {ConfigService} from "@node-boot/config"; import {Service} from "@node-boot/core"; -import {NotFoundError} from "routing-controllers"; +import {User, UserRepository} from "../persistence"; +import {UserModel} from "../models/users.model"; +import {Optional} from "@node-boot/extension"; +import {runOnTransactionCommit, runOnTransactionRollback, Transactional} from "@node-boot/starter-persistence"; +import {HttpError, NotFoundError} from "@node-boot/error"; @Service() export class UserService { - constructor(private readonly logger: Logger, private readonly configService: ConfigService) {} + constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { + UserModel.forEach(user => this.userRepository.save(user)); + } public async findAllUser(): Promise { this.logger.info("Getting all users"); - const users: User[] = UserModel; + const appName = this.configService.getString("node-boot.app.name"); + this.logger.info(`Reading node-boot.app.name from app-config.yam: ${appName}`); + return this.userRepository.find(); + } - const baseUrl = this.configService.getString("backend.baseUrl"); - this.logger.info(`Reading backend.baseUrl from app-config.yam: ${baseUrl}`); - return users; + public async findWithCustomQuery(): Promise { + this.logger.info("Getting all users with a custom query"); + return this.userRepository.findByQueryIn(); } public async findUserById(userId: number): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new NotFoundError("User doesn't exist"); - return findUser; + const user = await this.userRepository.findOneBy({ + id: userId, + }); + return Optional.of(user) + .orElseThrow(() => new NotFoundError("User doesn't exist")) + .get(); } + @Transactional() public async createUser(userData: CreateUserDto): Promise { - const findUser = UserModel.find(user => user.email === userData.email); - if (findUser) throw new HttpException(409, `This email ${userData.email} already exists`); + const existingUser = await this.userRepository.findOneBy({ + email: userData.email, + }); - return {id: UserModel.length + 1, ...userData}; - } + runOnTransactionCommit(() => { + this.logger.info("Transaction was successfully committed"); + }); - public async updateUser(userId: number, userData: CreateUserDto): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new HttpException(409, "User doesn't exist"); + return Optional.of(existingUser) + .ifPresentThrow(() => new HttpError(409, `This email ${userData.email} already exists`)) + .elseAsync(() => this.userRepository.save(userData)); + } - return UserModel.map((user: User) => { - if (user.id === findUser.id) user = {id: userId, ...userData}; - return user; + @Transactional() + public async updateUser(userId: number, userData: UpdateUserDto): Promise { + const user = await this.userRepository.findOneBy({ + id: userId, }); + + return Optional.of(user) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) + .map(user => { + return { + ...user, + userData, + }; + }) + .runAsync(async user => await this.userRepository.save(user)); } - public async deleteUser(userId: number): Promise { - const findUser = UserModel.find(user => user.id === userId); - if (!findUser) throw new HttpException(409, "User doesn't exist"); + @Transactional() + public async deleteUser(userId: number): Promise { + const user = await this.userRepository.findOneBy({ + id: userId, + }); + + runOnTransactionRollback(error => { + this.logger.warn("Transactions was rolled back due to error:", error); + }); + + await Optional.of(user) + .orElseThrow(() => new HttpError(409, "User doesn't exist")) + .runAsync(user => this.userRepository.delete({id: userId})); - return UserModel.filter(user => user.id !== findUser.id); + throw new Error("Error after deleting that should rollback transaction"); } } diff --git a/servers/express-server/package.json b/servers/express-server/package.json index fdf11b3e..10b72767 100644 --- a/servers/express-server/package.json +++ b/servers/express-server/package.json @@ -1,12 +1,13 @@ { "name": "@node-boot/express-server", "version": "1.0.0", - "description": "The first typescript example for the Monorepo example", + "description": "Express server for Node-Boot", "author": "Manuel Santos ", "license": "MIT", "keywords": [ - "monorepo", - "typescript" + "express", + "server", + "engine" ], "repository": { "type": "git", @@ -31,11 +32,12 @@ "dependencies": { "@node-boot/context": "1.0.0", "@node-boot/core": "1.0.0", + "@node-boot/error": "1.0.0", + "@node-boot/engine": "1.0.0", "body-parser": "^1.20.2", "multer": "^1.4.5-lts.1" }, "peerDependencies": { - "routing-controllers": ">=0.10.4", "express": ">=4.18.2" }, "devDependencies": { diff --git a/servers/express-server/src/driver/ExpressDriver.ts b/servers/express-server/src/driver/ExpressDriver.ts new file mode 100644 index 00000000..18623476 --- /dev/null +++ b/servers/express-server/src/driver/ExpressDriver.ts @@ -0,0 +1,456 @@ +import {isPromiseLike, NodeBootDriver} from "@node-boot/engine"; +import { + Action, + ActionMetadata, + ErrorHandlerInterface, + getFromContainer, + MiddlewareMetadata, + NodeBootEngineOptions, + ParamMetadata, + UseMetadata, +} from "@node-boot/context"; +import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, NotFoundError} from "@node-boot/error"; +import {Application, Request, Response} from "express"; +import {MiddlewareInterface} from "@node-boot/context/src"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const cookie = require("cookie"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const templateUrl = require("template-url"); + +/** + * Integration with express framework. + */ +export class ExpressDriver extends NodeBootDriver { + constructor(express?: Application) { + super(); + this.app = express ?? this.loadExpress(); + } + + /** + * Initializes the things driver needs before routes and middlewares registration. + */ + initialize() { + if (this.cors) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const cors = require("cors"); + if (this.cors === true) { + this.app.use(cors()); + } else { + this.app.use(cors(this.cors)); + } + } + } + + /** + * Registers middleware that run before controller actions. + */ + registerMiddleware(middleware: MiddlewareMetadata, options: NodeBootEngineOptions): void { + let middlewareWrapper; + + // FIXME Improve this code using the the DI container + //middleware.getInstance(); + + // if its an error handler then register it with proper signature in express + if ((middleware.instance as ErrorHandlerInterface).onError) { + middlewareWrapper = (error: any, request: any, response: any, next: (err?: any) => any) => { + (middleware.instance as ErrorHandlerInterface).onError(error, {request, response, next}); + }; + } + + // if its a regular middleware then register it as express middleware + else if ((middleware.instance as MiddlewareInterface).use) { + middlewareWrapper = (request: any, response: any, next: (err: any) => any) => { + try { + const useResult = (middleware.instance as MiddlewareInterface).use({request, response, next}); + if (isPromiseLike(useResult)) { + useResult.catch((error: any) => { + this.handleError(error, undefined, {request, response, next}); + return error; + }); + } + } catch (error) { + this.handleError(error, undefined, {request, response, next}); + } + }; + } + + if (middlewareWrapper) { + // Name the function for better debugging + Object.defineProperty(middlewareWrapper, "name", { + value: middleware.instance.constructor.name, + writable: true, + }); + + this.app.use(options.routePrefix || "/", middlewareWrapper); + } + } + + /** + * Registers action in the driver. + */ + registerAction(actionMetadata: ActionMetadata, executeCallback: (options: Action) => any): void { + // middlewares required for this action + const defaultMiddlewares: any[] = []; + + if (actionMetadata.isBodyUsed) { + if (actionMetadata.isJsonTyped) { + defaultMiddlewares.push(this.loadBodyParser().json(actionMetadata.bodyExtraOptions)); + } else { + defaultMiddlewares.push(this.loadBodyParser().text(actionMetadata.bodyExtraOptions)); + } + } + + if (actionMetadata.isAuthorizedUsed) { + defaultMiddlewares.push((request: Request, response: Response, next: Function) => { + if (!this.authorizationChecker) throw new AuthorizationCheckerNotDefinedError(); + + const action: Action = {request, response, next}; + try { + const checkResult = this.authorizationChecker.check(action, actionMetadata.authorizedRoles); + + const handleError = (result: any) => { + if (!result) { + const error = + actionMetadata.authorizedRoles.length === 0 + ? new AuthorizationRequiredError(action.request.method, action.request.url) + : new AccessDeniedError(action.request.method, action.request.url); + this.handleError(error, actionMetadata, action); + } else { + next(); + } + }; + + if (isPromiseLike(checkResult)) { + checkResult.then(result => handleError(result)).catch(error => this.handleError(error, actionMetadata, action)); + } else { + handleError(checkResult); + } + } catch (error) { + this.handleError(error, actionMetadata, action); + } + }); + } + + if (actionMetadata.isFileUsed || actionMetadata.isFilesUsed) { + const multer = this.loadMulter(); + actionMetadata.params + .filter(param => param.type === "file") + .forEach(param => { + defaultMiddlewares.push(multer(param.extraOptions).single(param.name)); + }); + actionMetadata.params + .filter(param => param.type === "files") + .forEach(param => { + defaultMiddlewares.push(multer(param.extraOptions).array(param.name)); + }); + } + + // user used middlewares + const uses = [...actionMetadata.controllerMetadata.uses, ...actionMetadata.uses]; + const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction)); + const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction)); + + // prepare route and route handler function + const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); + const routeHandler = function routeHandler(request: any, response: any, next: Function) { + return executeCallback({request, response, next}); + }; + + // This ensures that a request is only processed once to prevent unhandled rejections saying + // "Can't set headers after they are sent" + // Some examples of reasons a request may cause multiple route calls: + // * Express calls the "get" route automatically when we call the "head" route: + // Reference: https://expressjs.com/en/4x/api.html#router.METHOD + // This causes a double execution on our side. + // * Multiple routes match the request (e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)). + // The following middleware only starts an action processing if the request has not been processed before. + const routeGuard = function routeGuard(request: any, response: any, next: Function) { + if (!request.routingControllersStarted) { + request.routingControllersStarted = true; + return next(); + } + }; + + // finally register action in express + this.app[actionMetadata.type.toLowerCase()]( + ...[route, routeGuard, ...beforeMiddlewares, ...defaultMiddlewares, routeHandler, ...afterMiddlewares], + ); + } + + /** + * Registers all routes in the framework. + */ + // eslint-disable-next-line @typescript-eslint/no-empty-function + registerRoutes() {} + + /** + * Gets param from the request. + */ + getParamFromRequest(action: Action, param: ParamMetadata): any { + const request: any = action.request; + switch (param.type) { + case "body": + return request.body; + + case "body-param": + return request.body[param.name]; + + case "param": + return request.params[param.name]; + + case "params": + return request.params; + + case "session-param": + return request.session[param.name]; + + case "session": + return request.session; + + case "state": + throw new Error("@State decorators are not supported by express driver."); + + case "query": + return request.query[param.name]; + + case "queries": + return request.query; + + case "header": + return request.headers[param.name.toLowerCase()]; + + case "headers": + return request.headers; + + case "file": + return request.file; + + case "files": + return request.files; + + case "cookie": + if (!request.headers.cookie) return; + return cookie.parse(request.headers.cookie)[param.name]; + + case "cookies": + if (!request.headers.cookie) return {}; + return cookie.parse(request.headers.cookie); + } + } + + /** + * Handles result of successfully executed controller actionMetadata. + */ + handleSuccess(result: any, actionMetadata: ActionMetadata, action: Action): void { + // if the actionMetadata returned the response object itself, short-circuits + if (result && result === action.response) { + action.next?.(); + return; + } + + // transform result if needed + result = this.transformResult(result, actionMetadata, action); + + // set http status code + if (result === undefined && actionMetadata.undefinedResultCode) { + if (actionMetadata.undefinedResultCode instanceof Function) { + throw new (actionMetadata.undefinedResultCode as any)(action); + } + action.response.status(actionMetadata.undefinedResultCode); + } else if (result === null) { + if (actionMetadata.nullResultCode) { + if (actionMetadata.nullResultCode instanceof Function) { + throw new (actionMetadata.nullResultCode as any)(action); + } + action.response.status(actionMetadata.nullResultCode); + } else { + action.response.status(204); + } + } else if (actionMetadata.successHttpCode) { + action.response.status(actionMetadata.successHttpCode); + } + + // apply http headers + Object.keys(actionMetadata.headers).forEach(name => { + action.response.header(name, actionMetadata.headers[name]); + }); + + if (actionMetadata.redirect) { + // if redirect is set then do it + if (typeof result === "string") { + action.response.redirect(result); + } else if (result instanceof Object) { + action.response.redirect(templateUrl(actionMetadata.redirect, result)); + } else { + action.response.redirect(actionMetadata.redirect); + } + + action.next?.(); + } else if (actionMetadata.renderedTemplate) { + // if template is set then render it + const renderOptions = result && result instanceof Object ? result : {}; + + action.response.render(actionMetadata.renderedTemplate, renderOptions, (err: any, html: string) => { + if (err && actionMetadata.isJsonTyped) { + return action.next?.(err); + } else if (err && !actionMetadata.isJsonTyped) { + return action.next?.(err); + } else if (html) { + action.response.send(html); + } + action.next?.(); + }); + } else if (result === undefined) { + // throw NotFoundError on undefined response + + if (actionMetadata.undefinedResultCode) { + if (actionMetadata.isJsonTyped) { + action.response.json(); + } else { + action.response.send(); + } + action.next?.(); + } else { + throw new NotFoundError(); + } + } else if (result === null) { + // send null response + if (actionMetadata.isJsonTyped) { + action.response.json(null); + } else { + action.response.send(null); + } + action.next?.(); + } else if (result instanceof Buffer) { + // check if it's binary data (Buffer) + action.response.end(result, "binary"); + } else if (result instanceof Uint8Array) { + // check if it's binary data (typed array) + action.response.end(Buffer.from(result as any), "binary"); + } else if (result.pipe instanceof Function) { + result.pipe(action.response); + } else { + // send regular result + if (actionMetadata.isJsonTyped) { + action.response.json(result); + } else { + action.response.send(result); + } + action.next?.(); + } + } + + /** + * Handles result of failed executed controller action. + */ + handleError(error: any, action: ActionMetadata | undefined, options: Action): any { + if (this.isDefaultErrorHandlingEnabled) { + const response: any = options.response; + + // set http code + // note that we can't use error instanceof HttpError properly anymore because of new typescript emit process + if (error.httpCode) { + response.status(error.httpCode); + } else { + response.status(500); + } + + // apply http headers + if (action) { + Object.keys(action.headers).forEach(name => { + response.header(name, action.headers[name]); + }); + } + + // send error content + if (action && action.isJsonTyped) { + response.json(this.processJsonError(error)); + } else { + response.send(this.processTextError(error)); // todo: no need to do it because express by default does it + } + } + options.next?.(error); + } + + /** + * Creates middlewares from the given "use"-s. + */ + protected prepareMiddlewares(uses: UseMetadata[]) { + const middlewareFunctions: Function[] = []; + uses.forEach((use: UseMetadata) => { + if (use.middleware.prototype && use.middleware.prototype.use) { + // if this is function instance of MiddlewareInterface + middlewareFunctions.push((request: any, response: any, next: (err: any) => any) => { + try { + const useResult = getFromContainer(use.middleware).use({ + request, + response, + next, + }); + if (isPromiseLike(useResult)) { + useResult.catch((error: any) => { + this.handleError(error, undefined, {request, response, next}); + return error; + }); + } + + return useResult; + } catch (error) { + this.handleError(error, undefined, {request, response, next}); + } + }); + } else if (use.middleware.prototype && use.middleware.prototype.error) { + // if this is function instance of ErrorMiddlewareInterface + middlewareFunctions.push(function (error: any, request: any, response: any, next: (err: any) => any) { + return getFromContainer(use.middleware).onError(error, { + request, + response, + next, + }); + }); + } else { + middlewareFunctions.push(use.middleware); + } + }); + return middlewareFunctions; + } + + /** + * Dynamically loads express module. + */ + protected loadExpress(): Application { + if (require) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require("express")(); + } catch (e) { + throw new Error("express package was not found installed. Try to install it: npm install express --save"); + } + } else { + throw new Error("Cannot load express. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads body-parser module. + */ + protected loadBodyParser() { + try { + return require("body-parser"); + } catch (e) { + throw new Error("body-parser package was not found installed. Try to install it: npm install body-parser --save"); + } + } + + /** + * Dynamically loads multer module. + */ + protected loadMulter() { + try { + return require("multer"); + } catch (e) { + throw new Error("multer package was not found installed. Try to install it: npm install multer --save"); + } + } +} diff --git a/servers/express-server/src/driver/index.ts b/servers/express-server/src/driver/index.ts new file mode 100644 index 00000000..9b0c3246 --- /dev/null +++ b/servers/express-server/src/driver/index.ts @@ -0,0 +1 @@ +export {ExpressDriver} from "./ExpressDriver"; diff --git a/servers/express-server/src/index.ts b/servers/express-server/src/index.ts index 69aabe01..a4e3c0e2 100644 --- a/servers/express-server/src/index.ts +++ b/servers/express-server/src/index.ts @@ -1 +1,2 @@ -export {ExpressServer} from "./ExpressServer"; +export * from "./driver"; +export * from "./server"; diff --git a/servers/express-server/src/ExpressServer.ts b/servers/express-server/src/server/ExpressServer.ts similarity index 81% rename from servers/express-server/src/ExpressServer.ts rename to servers/express-server/src/server/ExpressServer.ts index 3a7a1926..c7ec4d38 100644 --- a/servers/express-server/src/ExpressServer.ts +++ b/servers/express-server/src/server/ExpressServer.ts @@ -1,7 +1,8 @@ import {ApplicationContext} from "@node-boot/context"; import express from "express"; -import {useExpressServer} from "routing-controllers"; import {BaseServer} from "@node-boot/core"; +import {ExpressDriver} from "../driver"; +import {NodeBootToolkit} from "@node-boot/engine"; export class ExpressServer extends BaseServer { public framework: express.Application; @@ -21,11 +22,11 @@ export class ExpressServer extends BaseServer=0.10.4", "fastify": ">=4.21.0" } } diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index 0bc3e9a9..1b400aa5 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -1,32 +1,26 @@ +import {isPromiseLike, NodeBootDriver} from "@node-boot/engine"; import { Action, ActionMetadata, - BaseDriver, + ErrorHandlerInterface, getFromContainer, - HttpError, + MiddlewareInterface, MiddlewareMetadata, - NotFoundError, + NodeBootEngineOptions, ParamMetadata, - RoutingControllersOptions, UseMetadata, -} from "routing-controllers"; +} from "@node-boot/context"; import {FastifyError, FastifyInstance, FastifyReply, FastifyRequest} from "fastify"; import {HTTPMethods} from "fastify/types/utils"; import {DoneFuncWithErrOrRes, HookHandlerDoneFunction} from "fastify/types/hooks"; -import { - AccessDeniedError, - AuthorizationCheckerNotDefinedError, - AuthorizationRequiredError, - FastifyErrorHandlerInterface, - FastifyErrorMiddlewareInterface, - FastifyMiddlewareInterface, -} from "@node-boot/core"; import {FastifyCookieOptions} from "@fastify/cookie"; import {FastifySessionOptions} from "@fastify/session"; import {FastifyMultipartOptions} from "@fastify/multipart"; import {FastifyViewOptions} from "@fastify/view"; import templateUrl from "template-url"; import {FastifyCorsOptions} from "@fastify/cors"; +import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, HttpError, NotFoundError} from "@node-boot/error"; +import {DependenciesLoader} from "../loader"; const actionToHttpMethodMap = { delete: "DELETE", @@ -45,97 +39,79 @@ export type ServerOptions = { templateOptions?: FastifyViewOptions; }; -export class FastifyDriver extends BaseDriver { - constructor(private readonly serverOptions: ServerOptions, private fastify?: FastifyInstance) { +export class FastifyDriver extends NodeBootDriver> { + constructor(private readonly serverOptions: ServerOptions, fastify?: FastifyInstance) { super(); - this.loadFastify(); - this.app = this.fastify; - } - - useApp() { - return this.app as FastifyInstance; + this.app = fastify ?? this.loadFastify(); } initialize() { if (this.serverOptions.cookieOptions) { - const fastifyCookie = this.loadCookie(); - this.useApp().register(fastifyCookie, this.serverOptions.cookieOptions); + const fastifyCookie = DependenciesLoader.loadCookie(); + this.app.register(fastifyCookie, this.serverOptions.cookieOptions); } if (this.serverOptions.corsOptions) { - const fastifyCors = this.loadCors(); - this.useApp().register(fastifyCors, this.serverOptions.corsOptions); + const fastifyCors = DependenciesLoader.loadCors(); + this.app.register(fastifyCors, this.serverOptions.corsOptions); } if (this.serverOptions.sessionOptions) { - const fastifySession = this.loadSession(); - this.useApp().register(fastifySession, this.serverOptions.sessionOptions); + const fastifySession = DependenciesLoader.loadSession(); + this.app.register(fastifySession, this.serverOptions.sessionOptions); } if (this.serverOptions.multipartOptions) { - const fastifyMultipart = this.loadMultipart(); - this.useApp().register(fastifyMultipart, this.serverOptions.multipartOptions); + const fastifyMultipart = DependenciesLoader.loadMultipart(); + this.app.register(fastifyMultipart, this.serverOptions.multipartOptions); } if (this.serverOptions.templateOptions) { - const fastifyView = this.loadView(); - this.useApp().register(fastifyView, this.serverOptions.templateOptions); + const fastifyView = DependenciesLoader.loadView(); + this.app.register(fastifyView, this.serverOptions.templateOptions); } } /** * Registers middleware that run before controller actions. */ - registerMiddleware(middleware: MiddlewareMetadata, options: RoutingControllersOptions): void { + registerMiddleware(middleware: MiddlewareMetadata, options: NodeBootEngineOptions): void { // Register a custom error Handler - if ((middleware.instance as FastifyErrorHandlerInterface).error) { - const errorHandler = ( - error: FastifyError, - request: FastifyRequest, - reply: FastifyReply, - ) => { - (middleware.instance as FastifyErrorHandlerInterface).error(request, reply, error); + if ((middleware.instance as ErrorHandlerInterface).onError) { + const errorHandler = (error: FastifyError, request: FastifyRequest, reply: FastifyReply) => { + (middleware.instance as ErrorHandlerInterface).onError(error, {request, response: reply}); }; // Name the function for better debugging this.nameGlobalMiddlewareFunction(errorHandler, middleware); - this.useApp().setErrorHandler(errorHandler); + this.app.setErrorHandler(errorHandler); } // if its a regular middleware then register it as fastify preHandler hook - else if ((middleware.instance as FastifyMiddlewareInterface).use) { + else if ((middleware.instance as MiddlewareInterface).use) { let fastifyHook; if (middleware.type === "before") { - fastifyHook = ( - request: FastifyRequest, - reply: FastifyReply, - done: HookHandlerDoneFunction, - ) => { + fastifyHook = (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => { this.callGlobalMiddleware(request, options, middleware, reply, done, undefined); }; // Name the function for better debugging this.nameGlobalMiddlewareFunction(fastifyHook, middleware); - this.useApp().addHook("preHandler", fastifyHook); + this.app.addHook("preHandler", fastifyHook); } else { - fastifyHook = ( - request: FastifyRequest, - reply: FastifyReply, - payload: any, - done: DoneFuncWithErrOrRes, - ) => { + fastifyHook = (request: FastifyRequest, reply: FastifyReply, payload: any, done: DoneFuncWithErrOrRes) => { this.callGlobalMiddleware(request, options, middleware, reply, done, payload); }; // Name the function for better debugging this.nameGlobalMiddlewareFunction(fastifyHook, middleware); - this.useApp().addHook("onSend", fastifyHook); + this.app.addHook("onSend", fastifyHook); } } } private callGlobalMiddleware( request: FastifyRequest, - options: RoutingControllersOptions, + options: NodeBootEngineOptions, middleware: MiddlewareMetadata, reply: FastifyReply, done: DoneFuncWithErrOrRes, @@ -143,13 +119,15 @@ export class FastifyDriver extends BaseDriver { ) { if (request.url.startsWith(options.routePrefix || "/")) { try { - const useResult = (middleware.instance as FastifyMiddlewareInterface).use( - request, - reply, - done, + const useResult = (middleware.instance as MiddlewareInterface).use( + { + request, + response: reply, + next: done, + }, payload, ); - if (this.isPromiseLike(useResult)) { + if (isPromiseLike(useResult)) { useResult .then(useResult => done()) .catch((error: any) => { @@ -163,7 +141,7 @@ export class FastifyDriver extends BaseDriver { } else { done(); } - } catch (error) { + } catch (error: any) { this.handleError(error, undefined, { request, response: reply, @@ -184,19 +162,13 @@ export class FastifyDriver extends BaseDriver { }); } - registerAction(actionMetadata: ActionMetadata, executeAction: (options: Action) => any) { + registerAction(actionMetadata: ActionMetadata, executeAction: (action: Action) => any) { const defaultMiddlewares: any[] = []; if (actionMetadata.isAuthorizedUsed) { - defaultMiddlewares.push( - async ( - request: FastifyRequest, - reply: FastifyReply, - done: HookHandlerDoneFunction, - ) => { - await this.checkAuthorization(request, reply, done, actionMetadata); - }, - ); + defaultMiddlewares.push(async (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => { + await this.checkAuthorization(request, reply, done, actionMetadata); + }); } // TODO Make sure nothing is required if @fastify/multipart is registered @@ -210,28 +182,28 @@ export class FastifyDriver extends BaseDriver { const afterMiddlewares = this.prepareUseMiddlewares(uses.filter(use => use.afterAction)); const errorMiddlewares = this.prepareUseErrorMiddlewares(uses); - const routeHandler = async (request, reply) => { + const routeHandler = async (request: FastifyRequest, reply: FastifyReply) => { // This ensures that a request is only processed once. Multiple routes may match a request // e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)), only the first matching route should // be called. // The following middleware only starts an action processing if the request has not been processed before. - if (!request.routingControllersStarted) { - request.routingControllersStarted = true; + if (!request["locals"].routeStarted) { + request["locals"].routeStarted = true; await executeAction({request, response: reply}); } }; - const afterMiddlewaresAdapter = async (request, reply, payload, done) => { + const afterMiddlewaresAdapter = async (request: FastifyRequest, reply: FastifyReply, payload: any, done: DoneFuncWithErrOrRes) => { afterMiddlewares.forEach(middleware => middleware(request, reply, payload, done)); }; - const errorMiddlewaresAdapter = async (request, reply, error, done) => { + const errorMiddlewaresAdapter = async (request: FastifyRequest, reply: FastifyReply, error: Error, done: () => void) => { errorMiddlewares.forEach(middleware => middleware(request, reply, error, done)); }; // Register route and hooks const route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); - this.useApp().route({ + this.app.route({ method: this.actionToHttpMethod(actionMetadata), url: route.toString(), preHandler: [...beforeMiddlewares, ...defaultMiddlewares], @@ -249,25 +221,22 @@ export class FastifyDriver extends BaseDriver { return httpMethod; } - async checkAuthorization(request, reply, done, actionMetadata) { + async checkAuthorization(request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction, actionMetadata: ActionMetadata) { if (!this.authorizationChecker) throw new AuthorizationCheckerNotDefinedError(); const action: Action = {request, response: reply, next: done}; try { - const checkResult = await this.authorizationChecker( - action, - actionMetadata.authorizedRoles, - ); + const checkResult = await this.authorizationChecker.check(action, actionMetadata.authorizedRoles); if (!checkResult) { const error = actionMetadata.authorizedRoles.length === 0 - ? new AuthorizationRequiredError(action) - : new AccessDeniedError(action); - await this.handleError(error, actionMetadata, action); + ? new AuthorizationRequiredError(action.request.method, action.request.url) + : new AccessDeniedError(action.request.method, action.request.url); + this.handleError(error, actionMetadata, action); } - } catch (error) { - await this.handleError(error, actionMetadata, action); + } catch (error: any) { + this.handleError(error, actionMetadata, action); } } @@ -277,21 +246,17 @@ export class FastifyDriver extends BaseDriver { protected prepareUseMiddlewares(uses: UseMetadata[]) { const middlewareFunctions: Function[] = []; uses.forEach((use: UseMetadata) => { - if (this.isCustomMiddleware(use)) { + if (use.isCustomMiddleware()) { // if this is function instance of MiddlewareInterface middlewareFunctions.push( - ( - request: FastifyRequest, - reply: FastifyReply, - done: HookHandlerDoneFunction, - payload?: any, - ) => { + (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction | DoneFuncWithErrOrRes, payload?: any) => { try { - const useResult = getFromContainer( - use.middleware, - ).use(request, reply, done, payload); + const useResult = getFromContainer(use.middleware).use( + {request, response: reply, next: done}, + payload, + ); - if (this.isPromiseLike(useResult)) { + if (isPromiseLike(useResult)) { useResult.catch((error: any) => { this.handleError(error, undefined, { request, @@ -302,7 +267,7 @@ export class FastifyDriver extends BaseDriver { }); } return useResult; - } catch (error) { + } catch (error: any) { this.handleError(error, undefined, { request, response: reply, @@ -311,7 +276,7 @@ export class FastifyDriver extends BaseDriver { } }, ); - } else if (!this.isErrorMiddleware(use)) { + } else if (!use.isErrorMiddleware()) { // NOT a custom middleware // FIXME - Check if we can support use middleware using @fastify/middie // middlewareFunctions.push(use.middleware); @@ -325,40 +290,21 @@ export class FastifyDriver extends BaseDriver { */ prepareUseErrorMiddlewares(uses: UseMetadata[]) { const middlewareFunctions: Function[] = []; - uses.filter((use: UseMetadata) => this.isErrorMiddleware(use)).forEach( - (use: UseMetadata) => { - // if this is function instance of ErrorMiddlewareInterface - middlewareFunctions.push( - ( - request: FastifyRequest, - reply: FastifyReply, - error: any, - done: () => void, - ) => { - return getFromContainer( - use.middleware, - ).useError(request, reply, error, done); - }, - ); - }, - ); + uses.filter((use: UseMetadata) => use.isErrorMiddleware()).forEach((use: UseMetadata) => { + // if this is function instance of ErrorMiddlewareInterface + middlewareFunctions.push((request: FastifyRequest, reply: FastifyReply, error: FastifyError, done: () => void) => { + return getFromContainer(use.middleware).onError(error, {request, response: reply, next: done}); + }); + }); return middlewareFunctions; } - private isCustomMiddleware(use: UseMetadata) { - return use.middleware.prototype && use.middleware.prototype.use; - } - - private isErrorMiddleware(use: UseMetadata) { - return use.middleware.prototype && use.middleware.prototype.useError; - } - registerRoutes() { // Register all routes in Fastify // You will need to implement route registration for Fastify } - getParamFromRequest(action: Action, param: ParamMetadata): any { + getParamFromRequest(action: Action, param: ParamMetadata): any { const request = action.request; switch (param.type) { // TODO - https://www.npmjs.com/package/@fastify/session @@ -372,16 +318,16 @@ export class FastifyDriver extends BaseDriver { return request.body; case "body-param": - return request.body[param.name]; + return (request.body as any)[param.name]; case "param": - return request.params[param.name]; + return (request.params as any)[param.name]; case "params": return request.params; case "query": - return request.query[param.name]; + return (request.query as any)[param.name]; case "queries": return request.query; @@ -396,10 +342,10 @@ export class FastifyDriver extends BaseDriver { // For example, for cookies: // https://github.com/fastify/fastify-cookie case "cookie": - return this.useApp().parseCookie(request.headers.cookie || "")[param.name]; + return this.app.parseCookie(request.headers.cookie || "")[param.name]; case "cookies": - return this.useApp().parseCookie(request.headers.cookie || ""); + return this.app.parseCookie(request.headers.cookie || ""); // For files, you may need to use Fastify's file handling mechanisms // https://snyk.io/blog/node-js-file-uploads-with-fastify/ @@ -415,88 +361,82 @@ export class FastifyDriver extends BaseDriver { } } - handleError(error: any, action: ActionMetadata | undefined, options: Action) { + handleError(error: Error, actionMetadata: ActionMetadata | undefined, action: Action) { // Handle error using Fastify's reply - if (action) { - Object.keys(action.headers).forEach(name => { - (options.response as FastifyReply).header(name, action.headers[name]); + if (actionMetadata) { + Object.keys(actionMetadata.headers).forEach(name => { + (action.response as FastifyReply).header(name, actionMetadata.headers[name]); }); } // set http status if (error instanceof HttpError && error.httpCode) { - options.response.code(error.httpCode); + action.response.code(error.httpCode); } else { - options.response.code(500); + action.response.code(500); } - options.response.send(error); + action.response.send(error); } - handleSuccess(result: any, action: ActionMetadata, options: Action): void { + handleSuccess(result: any, actionMetadata: ActionMetadata, action: Action) { // Handle success using Fastify's reply - // if the action returned the response object itself, short-circuits - if (result && result === options.response) { + // if the actionMetadata returned the response object itself, short-circuits + if (result && result === action.response) { return; } // set http status code - this.applyResponseStatus(result, action, options); + this.applyResponseStatus(result, actionMetadata, action); // apply http headers - Object.keys(action.headers).forEach(name => { - options.response.header(name, action.headers[name]); + Object.keys(actionMetadata.headers).forEach(name => { + action.response.header(name, actionMetadata.headers[name]); }); - if (action.redirect) { + if (actionMetadata.redirect) { // Apply redirect - this.applyRedirect(result, options, action); - } else if (action.renderedTemplate) { + this.applyRedirect(result, action, actionMetadata); + } else if (actionMetadata.renderedTemplate) { // Apply render template - this.applyTemplateRender(result, options, action); + this.applyTemplateRender(result, action, actionMetadata); } else if (result === undefined) { - this.applyUndefined(action, options); + this.applyUndefined(actionMetadata, action); } else if (result === null) { // send null response - options.response.send(null); + action.response.send(null); } else if (result instanceof Buffer) { // check if it's binary data (Buffer) - options.response.send(result); + action.response.send(result); } else if (result instanceof Uint8Array) { // check if it's binary data (typed array) - options.response.send(Buffer.from(result)); + action.response.send(Buffer.from(result)); } else if (result.pipe instanceof Function) { - result.pipe(options.response.raw); + result.pipe(action.response.raw); } else { // send regular result - options.response.send(result); + action.response.send(result); } } - private applyUndefined(action: ActionMetadata, options: Action) { + private applyUndefined(actionMetadata: ActionMetadata, action: Action) { // Apply undefined result // throw NotFoundError on undefined response - if (action.undefinedResultCode) { - if (action.isJsonTyped) { - options.response.send({}); // Sending an empty object in Fastify as an equivalent of response.json() + if (actionMetadata.undefinedResultCode) { + if (actionMetadata.isJsonTyped) { + action.response.send({}); // Sending an empty object in Fastify as an equivalent of response.json() } else { - options.response.send(); + action.response.send(); } } else { throw new NotFoundError(); } } - private applyTemplateRender(result: any, options: Action, action: ActionMetadata) { + private applyTemplateRender(result: any, action: Action, actionMetadata: ActionMetadata) { // if template is set then render it + // Check doc https://www.npmjs.com/package/@fastify/view const renderOptions = result && result instanceof Object ? result : {}; - - options.response.view(action.renderedTemplate, renderOptions, (err, html) => { - if (err) { - throw err; - } else if (html) { - options.response.send(html); - } - }); + action.response.view(actionMetadata.renderedTemplate, renderOptions); } private applyRedirect(result: any, options: Action, action: ActionMetadata) { @@ -535,93 +475,17 @@ export class FastifyDriver extends BaseDriver { /** * Dynamically loads fastify module. */ - protected loadFastify() { + private loadFastify(): FastifyInstance { if (require) { - if (!this.fastify) { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const fastify = require("fastify")(); - this.fastify = fastify(); - } catch (e) { - throw new Error( - "fastify package was not found installed. Try to install it: npm install fastify --save", - ); - } + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const fastify = require("fastify")(); + return fastify(); + } catch (e) { + throw new Error("fastify package was not found installed. Try to install it: npm install fastify --save"); } } else { throw new Error("Cannot load fastify. Try to install all required dependencies."); } } - - /** - * Dynamically loads @fastify/session module. - */ - protected loadSession() { - try { - return require("@fastify/session"); - } catch (e) { - throw new Error( - "@fastify/session package was not found installed. Try to install it: npm install @fastify/session --save", - ); - } - } - - /** - * Dynamically loads @fastify/cookie module. - */ - protected loadCookie() { - try { - return require("@fastify/cookie"); - } catch (e) { - throw new Error( - "@fastify/cookie package was not found installed. Try to install it: npm install @fastify/cookie --save", - ); - } - } - - /** - * Dynamically loads @fastify/multipart module. - */ - protected loadMultipart() { - try { - return require("@fastify/multipart"); - } catch (e) { - throw new Error( - "@fastify/multipart package was not found installed. Try to install it: npm install @fastify/multipart --save", - ); - } - } - - /** - * Dynamically loads @fastify/cors module. - */ - protected loadCors() { - try { - return require("@fastify/cors"); - } catch (e) { - throw new Error( - "@fastify/cors package was not found installed. Try to install it: npm install @fastify/cors --save", - ); - } - } - - /** - * Dynamically loads @fastify/view module. - */ - protected loadView() { - try { - return require("@fastify/view"); - } catch (e) { - throw new Error( - "@fastify/view package was not found installed. Try to install it: npm install @fastify/view --save", - ); - } - } - - /** - * Checks if given value is a Promise-like object. - */ - isPromiseLike(arg: any): arg is Promise { - return arg != null && typeof arg === "object" && typeof arg.then === "function"; - } } diff --git a/servers/fastify-server/src/driver/index.ts b/servers/fastify-server/src/driver/index.ts new file mode 100644 index 00000000..7ac581bc --- /dev/null +++ b/servers/fastify-server/src/driver/index.ts @@ -0,0 +1 @@ +export {FastifyDriver} from "./FastifyDriver"; diff --git a/servers/fastify-server/src/index.ts b/servers/fastify-server/src/index.ts index 1434c24d..f5b7cccc 100644 --- a/servers/fastify-server/src/index.ts +++ b/servers/fastify-server/src/index.ts @@ -1 +1 @@ -export {FastifyServer} from "./FastifyServer"; +export {FastifyServer} from "./server"; diff --git a/servers/fastify-server/src/loader/DependenciesLoader.ts b/servers/fastify-server/src/loader/DependenciesLoader.ts new file mode 100644 index 00000000..4b4309c1 --- /dev/null +++ b/servers/fastify-server/src/loader/DependenciesLoader.ts @@ -0,0 +1,56 @@ +export class DependenciesLoader { + /** + * Dynamically loads @fastify/session module. + */ + static loadSession() { + try { + return require("@fastify/session"); + } catch (e) { + throw new Error("@fastify/session package was not found installed. Try to install it: npm install @fastify/session --save"); + } + } + + /** + * Dynamically loads @fastify/cookie module. + */ + static loadCookie() { + try { + return require("@fastify/cookie"); + } catch (e) { + throw new Error("@fastify/cookie package was not found installed. Try to install it: npm install @fastify/cookie --save"); + } + } + + /** + * Dynamically loads @fastify/multipart module. + */ + static loadMultipart() { + try { + return require("@fastify/multipart"); + } catch (e) { + throw new Error("@fastify/multipart package was not found installed. Try to install it: npm install @fastify/multipart --save"); + } + } + + /** + * Dynamically loads @fastify/cors module. + */ + static loadCors() { + try { + return require("@fastify/cors"); + } catch (e) { + throw new Error("@fastify/cors package was not found installed. Try to install it: npm install @fastify/cors --save"); + } + } + + /** + * Dynamically loads @fastify/view module. + */ + static loadView() { + try { + return require("@fastify/view"); + } catch (e) { + throw new Error("@fastify/view package was not found installed. Try to install it: npm install @fastify/view --save"); + } + } +} diff --git a/servers/fastify-server/src/loader/index.ts b/servers/fastify-server/src/loader/index.ts new file mode 100644 index 00000000..89b91d13 --- /dev/null +++ b/servers/fastify-server/src/loader/index.ts @@ -0,0 +1 @@ +export {DependenciesLoader} from "./DependenciesLoader"; diff --git a/servers/fastify-server/src/FastifyServer.ts b/servers/fastify-server/src/server/FastifyServer.ts similarity index 68% rename from servers/fastify-server/src/FastifyServer.ts rename to servers/fastify-server/src/server/FastifyServer.ts index 6687b122..3960bc09 100644 --- a/servers/fastify-server/src/FastifyServer.ts +++ b/servers/fastify-server/src/server/FastifyServer.ts @@ -1,8 +1,8 @@ import {ApplicationContext} from "@node-boot/context"; -import {createServer} from "routing-controllers"; import {BaseServer} from "@node-boot/core"; import Fastify, {FastifyInstance} from "fastify"; -import {FastifyDriver} from "./driver/FastifyDriver"; +import {FastifyDriver} from "../driver"; +import {NodeBootToolkit} from "@node-boot/engine"; export class FastifyServer extends BaseServer { private readonly framework: FastifyInstance; @@ -41,11 +41,9 @@ export class FastifyServer extends BaseServer }, this.framework, ); - createServer(driver, configs); + NodeBootToolkit.createServer(driver, configs); } else { - throw new Error( - "Error stating Application. Please enable NodeBoot application using @NodeBootApplication", - ); + throw new Error("Error stating Application. Please enable NodeBoot application using @NodeBootApplication"); } return this; @@ -54,19 +52,16 @@ export class FastifyServer extends BaseServer public listen() { const context = ApplicationContext.get(); - this.framework.listen( - {port: context.applicationOptions.port}, - (err: Error | null, address: string) => { - if (err) { - this.logger.error(err); - process.exit(1); - } - this.logger.info(`=================================`); - this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); - this.logger.info(`🚀 App listening on ${address}`); - this.logger.info(`=================================`); - }, - ); + this.framework.listen({port: context.applicationOptions.port}, (err: Error | null, address: string) => { + if (err) { + this.logger.error(err); + process.exit(1); + } + this.logger.info(`=================================`); + this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); + this.logger.info(`🚀 App listening on ${address}`); + this.logger.info(`=================================`); + }); } getFramework(): FastifyInstance { diff --git a/servers/fastify-server/src/server/index.ts b/servers/fastify-server/src/server/index.ts new file mode 100644 index 00000000..1434c24d --- /dev/null +++ b/servers/fastify-server/src/server/index.ts @@ -0,0 +1 @@ +export {FastifyServer} from "./FastifyServer"; diff --git a/servers/koa-server/package.json b/servers/koa-server/package.json index a3d8f70b..574fcf9b 100644 --- a/servers/koa-server/package.json +++ b/servers/koa-server/package.json @@ -30,7 +30,9 @@ }, "dependencies": { "@node-boot/context": "1.0.0", - "@node-boot/core": "1.0.0" + "@node-boot/core": "1.0.0", + "@node-boot/engine": "1.0.0", + "@node-boot/error": "1.0.0" }, "peerDependencies": { "routing-controllers": ">=0.10.4", @@ -40,6 +42,7 @@ "@koa/multer": "^3.0.2" }, "devDependencies": { - "@types/koa": "^2.13.8" + "@types/koa": "^2.13.8", + "@types/koa-router": "^7.4.8" } } diff --git a/servers/koa-server/src/driver/KoaDriver.ts b/servers/koa-server/src/driver/KoaDriver.ts new file mode 100644 index 00000000..254693fa --- /dev/null +++ b/servers/koa-server/src/driver/KoaDriver.ts @@ -0,0 +1,391 @@ +import {isPromiseLike, NodeBootDriver} from "@node-boot/engine"; +import {Action, ActionMetadata, getFromContainer, MiddlewareMetadata, ParamMetadata, RoleChecker, UseMetadata} from "@node-boot/context"; +import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, HttpError, NotFoundError} from "@node-boot/error"; +import Koa from "koa"; +import Router from "@koa/router"; +import {MiddlewareInterface} from "@node-boot/context/src"; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const cookie = require("cookie"); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const templateUrl = require("template-url"); + +/** + * Integration with koa framework. + */ +export class KoaDriver extends NodeBootDriver { + constructor(private readonly koa?: Koa, private readonly router?: Router) { + super(); + this.app = koa ?? this.loadKoa(); + this.router = router ?? this.loadRouter(); + } + + /** + * Initializes the things driver needs before routes and middleware registration. + */ + initialize() { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const bodyParser = require("koa-bodyparser"); + this.app.use(bodyParser()); + if (this.cors) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const cors = require("@koa/cors"); + if (this.cors === true) { + this.app.use(cors()); + } else { + this.app.use(cors(this.cors)); + } + } + } + + /** + * Registers middleware that run before controller actions. + */ + registerMiddleware(middleware: MiddlewareMetadata): void { + if ((middleware.instance as MiddlewareInterface).use) { + this.app.use(function (context: any, next: any) { + return (middleware.instance as MiddlewareInterface).use({ + request: context.request, + response: context.response, + context, + next, + }); + }); + } + } + + /** + * Registers action in the driver. + */ + registerAction(actionMetadata: ActionMetadata, executeCallback: (options: Action) => any): void { + // middlewares required for this action + const defaultMiddlewares: any[] = []; + + if (actionMetadata.isAuthorizedUsed) { + defaultMiddlewares.push((context: any, next: Function) => { + if (!this.authorizationChecker) throw new AuthorizationCheckerNotDefinedError(); + + const action: Action = {request: context.request, response: context.response, context, next}; + try { + const checkResult = + actionMetadata.authorizedRoles instanceof Function + ? getFromContainer(actionMetadata.authorizedRoles, action).check(action) + : this.authorizationChecker.check(action, actionMetadata.authorizedRoles); + + const handleError = (result: any) => { + if (!result) { + const error = + actionMetadata.authorizedRoles.length === 0 + ? new AuthorizationRequiredError(action.request.name, action.request.url) + : new AccessDeniedError(action.request.name, action.request.url); + return this.handleError(error, actionMetadata, action); + } else { + return next(); + } + }; + + if (isPromiseLike(checkResult)) { + return checkResult.then(result => handleError(result)).catch(error => this.handleError(error, actionMetadata, action)); + } else { + return handleError(checkResult); + } + } catch (error) { + return this.handleError(error, actionMetadata, action); + } + }); + } + + if (actionMetadata.isFileUsed || actionMetadata.isFilesUsed) { + const multer = this.loadMulter(); + actionMetadata.params + .filter(param => param.type === "file") + .forEach(param => { + defaultMiddlewares.push(multer(param.extraOptions).single(param.name)); + }); + actionMetadata.params + .filter(param => param.type === "files") + .forEach(param => { + defaultMiddlewares.push(multer(param.extraOptions).array(param.name)); + }); + } + + // user used middlewares + const uses = actionMetadata.controllerMetadata.uses.concat(actionMetadata.uses); + const beforeMiddlewares = this.prepareMiddlewares(uses.filter(use => !use.afterAction)); + const afterMiddlewares = this.prepareMiddlewares(uses.filter(use => use.afterAction)); + + // prepare route and route handler function + let route = ActionMetadata.appendBaseRoute(this.routePrefix, actionMetadata.fullRoute); + + // @koa/router is strict about trailing slashes, allow accessing routes without them + if (typeof route === "string" && route.length > 1 && route.endsWith("/")) { + route = route.substring(0, route.length - 1); + } + + const routeHandler = (context: any, next: () => Promise) => { + const options: Action = {request: context.request, response: context.response, context, next}; + return executeCallback(options); + }; + + // This ensures that a request is only processed once. Multiple routes may match a request + // e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)), only the first matching route should + // be called. + // The following middleware only starts an action processing if the request has not been processed before. + const routeGuard = (context: any, next: () => Promise) => { + if (!context.request.routingControllersStarted) { + context.request.routingControllersStarted = true; + return next(); + } + return; + }; + + // finally register action in koa + this.router[actionMetadata.type.toLowerCase()]( + ...[route, routeGuard, ...beforeMiddlewares, ...defaultMiddlewares, routeHandler, ...afterMiddlewares], + ); + } + + /** + * Registers all routes in the framework. + */ + registerRoutes() { + this.app.use(this.router.routes()); + this.app.use(this.router.allowedMethods()); + // FIXME Bind Error handler here + //this.app.onerror = err => { + // console.log(err); + //} + } + + /** + * Gets param from the request. + */ + getParamFromRequest(actionOptions: Action, param: ParamMetadata): any { + const context = actionOptions.context; + const request: any = actionOptions.request; + switch (param.type) { + case "body": + return request.body; + + case "body-param": + return request.body[param.name]; + + case "param": + return context.params[param.name]; + + case "params": + return context.params; + + case "session": + return context.session; + + case "session-param": + return context.session[param.name]; + + case "state": + if (param.name) return context.state[param.name]; + return context.state; + + case "query": + return context.query[param.name]; + + case "queries": + return context.query; + + case "file": + return actionOptions.context.request.file; + + case "files": + return actionOptions.context.request.files; + + case "header": + return context.headers[param.name.toLowerCase()]; + + case "headers": + return request.headers; + + case "cookie": + if (!context.headers.cookie) return; + return cookie.parse(context.headers.cookie)[param.name]; + + case "cookies": + if (!request.headers.cookie) return {}; + return cookie.parse(request.headers.cookie); + } + } + + /** + * Handles result of successfully executed controller action. + */ + handleSuccess(result: any, action: ActionMetadata, options: Action): void { + // if the action returned the context or the response object itself, short-circuits + if (result && (result === options.response || result === options.context)) { + return options.next?.(); + } + + // transform result if needed + result = this.transformResult(result, action, options); + + if (action.redirect) { + // if redirect is set then do it + if (typeof result === "string") { + options.response.redirect(result); + } else if (result instanceof Object) { + options.response.redirect(templateUrl(action.redirect, result)); + } else { + options.response.redirect(action.redirect); + } + } else if (action.renderedTemplate) { + // if template is set then render it + // FIXME: not working in koa + throw new Error("'renderedTemplate' is not supported for Koa yet"); + /* const renderOptions = result && result instanceof Object ? result : {}; + + this.app.use(async function(ctx: any, next: any) { + await ctx.render(action.renderedTemplate, renderOptions); + });*/ + } else if (result === undefined) { + // throw NotFoundError on undefined response + if (action.undefinedResultCode instanceof Function) { + throw new (action.undefinedResultCode as any)(options); + } else if (!action.undefinedResultCode) { + throw new NotFoundError(); + } + } else if (result === null) { + // send null response + if (action.nullResultCode instanceof Function) throw new (action.nullResultCode as any)(options); + + options.response.body = null; + } else if (result instanceof Uint8Array) { + // check if it's binary data (typed array) + options.response.body = Buffer.from(result as any); + } else { + // send regular result + options.response.body = result; + } + + // set http status code + if (result === undefined && action.undefinedResultCode) { + options.response.status = action.undefinedResultCode; + } else if (result === null && action.nullResultCode) { + options.response.status = action.nullResultCode; + } else if (action.successHttpCode) { + options.response.status = action.successHttpCode; + } else if (options.response.body === null) { + options.response.status = 204; + } + + // apply http headers + Object.keys(action.headers).forEach(name => { + options.response.set(name, action.headers[name]); + }); + + return options.next?.(); + } + + /** + * Handles result of failed executed controller action. + */ + handleError(error: any, action: ActionMetadata | undefined, options: Action) { + return new Promise((resolve, reject) => { + if (this.isDefaultErrorHandlingEnabled) { + // apply http headers + if (action) { + Object.keys(action.headers).forEach(name => { + options.response.set(name, action.headers[name]); + }); + } + + // send error content + if (action && action.isJsonTyped) { + options.response.body = this.processJsonError(error); + } else { + options.response.body = this.processTextError(error); + } + + // set http status + if (error instanceof HttpError && error.httpCode) { + options.response.status = error.httpCode; + } else { + options.response.status = 500; + } + + return resolve(); + } + return reject(error); + }); + } + + /** + * Creates middlewares from the given "use"-s. + */ + protected prepareMiddlewares(uses: UseMetadata[]) { + const middlewareFunctions: Function[] = []; + uses.forEach(use => { + if (use.middleware.prototype && use.middleware.prototype.use) { + // if this is function instance of MiddlewareInterface + middlewareFunctions.push(async (context: any, next: (err?: any) => Promise) => { + try { + return await getFromContainer(use.middleware).use({ + request: context.request, + response: context.response, + context, + next, + }); + } catch (error) { + return await this.handleError(error, undefined, { + request: context.request, + response: context.response, + context, + next, + }); + } + }); + } else { + middlewareFunctions.push(use.middleware); + } + }); + return middlewareFunctions; + } + + /** + * Dynamically loads koa module. + */ + protected loadKoa(): Koa { + if (require) { + try { + return new (require("koa"))(); + } catch (e) { + throw new Error("koa package was not found installed. Try to install it: npm install koa@next --save"); + } + } else { + throw new Error("Cannot load koa. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads @koa/router module. + */ + private loadRouter(): Router { + if (require) { + try { + return new (require("@koa/router"))(); + } catch (e) { + throw new Error("@koa/router package was not found installed. Try to install it: npm install @koa/router --save"); + } + } else { + throw new Error("Cannot load koa. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads @koa/multer module. + */ + private loadMulter() { + try { + return require("@koa/multer"); + } catch (e) { + throw new Error("@koa/multer package was not found installed. Try to install it: npm install @koa/multer --save"); + } + } +} diff --git a/servers/koa-server/src/driver/index.ts b/servers/koa-server/src/driver/index.ts new file mode 100644 index 00000000..f15c4b8f --- /dev/null +++ b/servers/koa-server/src/driver/index.ts @@ -0,0 +1 @@ +export {KoaDriver} from "./KoaDriver"; diff --git a/servers/koa-server/src/index.ts b/servers/koa-server/src/index.ts index c5e02c6d..485b6da7 100644 --- a/servers/koa-server/src/index.ts +++ b/servers/koa-server/src/index.ts @@ -1 +1 @@ -export {KoaServer} from "./KoaServer"; +export {KoaServer} from "./server"; diff --git a/servers/koa-server/src/KoaServer.ts b/servers/koa-server/src/server/KoaServer.ts similarity index 84% rename from servers/koa-server/src/KoaServer.ts rename to servers/koa-server/src/server/KoaServer.ts index af429424..eccab259 100644 --- a/servers/koa-server/src/KoaServer.ts +++ b/servers/koa-server/src/server/KoaServer.ts @@ -1,8 +1,9 @@ import {ApplicationContext} from "@node-boot/context"; import Koa from "koa"; import Router from "@koa/router"; -import {createServer, KoaDriver} from "routing-controllers"; import {BaseServer} from "@node-boot/core"; +import {NodeBootToolkit} from "@node-boot/engine"; +import {KoaDriver} from "../driver"; export class KoaServer extends BaseServer { private readonly framework: Koa; @@ -24,11 +25,9 @@ export class KoaServer extends BaseServer { const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); const driver = new KoaDriver(this.framework, this.router); - createServer(driver, configs); + NodeBootToolkit.createServer(driver, configs); } else { - throw new Error( - "Error stating Application. Please enable NodeBoot application using @NodeBootApplication", - ); + throw new Error("Error stating Application. Please enable NodeBoot application using @NodeBootApplication"); } return this; diff --git a/servers/koa-server/src/server/index.ts b/servers/koa-server/src/server/index.ts new file mode 100644 index 00000000..c5e02c6d --- /dev/null +++ b/servers/koa-server/src/server/index.ts @@ -0,0 +1 @@ +export {KoaServer} from "./KoaServer"; diff --git a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts index f9208f50..868a2208 100644 --- a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts +++ b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts @@ -11,10 +11,7 @@ import {KoaActuatorAdapter} from "./KoaActuatorAdapter"; export class DefaultActuatorAdapter implements ActuatorAdapter { private metricsContext: MetricsContext; - constructor( - private readonly register = new Prometheus.Registry(), - private readonly infoService: InfoService = new InfoService(), - ) {} + constructor(private readonly register = new Prometheus.Registry(), private readonly infoService: InfoService = new InfoService()) {} private setupMetrics(options: ActuatorOptions) { this.register.setDefaultLabels({ @@ -56,33 +53,17 @@ export class DefaultActuatorAdapter implements ActuatorAdapter { let frameworkAdapter: ActuatorAdapter; switch (options.serverType) { case "express": - frameworkAdapter = new ExpressActuatorAdapter( - context, - this.infoService, - metadataService, - configService, - ); + frameworkAdapter = new ExpressActuatorAdapter(context, this.infoService, metadataService, configService); break; case "koa": - frameworkAdapter = new KoaActuatorAdapter( - context, - this.infoService, - metadataService, - configService, - ); + frameworkAdapter = new KoaActuatorAdapter(context, this.infoService, metadataService, configService); break; case "fastify": - frameworkAdapter = new FastifyActuatorAdapter( - context, - this.infoService, - metadataService, - configService, - ); + frameworkAdapter = new FastifyActuatorAdapter(context, this.infoService, metadataService, configService); break; default: throw new Error( - "Actuator feature is only allowed for express, koa and fastify servers. " + - "Please remove @EnableActuator from your application", + "Actuator feature is only allowed for express, koa and fastify servers. " + "Please remove @EnableActuator from your application", ); } frameworkAdapter.bind(options, server, router); diff --git a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts index 66ed3000..c7649c5d 100644 --- a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts +++ b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts @@ -21,9 +21,7 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { res.once("finish", () => { const responseTimeInMilliseconds = Date.now() - res.locals.startEpoch; - this.context.http_request_duration_milliseconds - .labels(req.method, req.path, res.statusCode) - .observe(responseTimeInMilliseconds); + this.context.http_request_duration_milliseconds.labels(req.method, req.path, res.statusCode).observe(responseTimeInMilliseconds); }); // Increment the HTTP request counter diff --git a/starters/actuator/src/adapter/KoaActuatorAdapter.ts b/starters/actuator/src/adapter/KoaActuatorAdapter.ts index 45d4ffb5..fe7719ca 100644 --- a/starters/actuator/src/adapter/KoaActuatorAdapter.ts +++ b/starters/actuator/src/adapter/KoaActuatorAdapter.ts @@ -24,9 +24,7 @@ export class KoaActuatorAdapter implements ActuatorAdapter { const responseTimeInMilliseconds = Date.now() - ctx.state["startEpoch"]; - this.context.http_request_duration_milliseconds - .labels(ctx.method, ctx.path, ctx.status.toString()) - .observe(responseTimeInMilliseconds); + this.context.http_request_duration_milliseconds.labels(ctx.method, ctx.path, ctx.status.toString()).observe(responseTimeInMilliseconds); // Increment the HTTP request counter this.context.http_request_counter diff --git a/starters/actuator/src/service/InfoService.ts b/starters/actuator/src/service/InfoService.ts index a85a8801..5ab3f921 100644 --- a/starters/actuator/src/service/InfoService.ts +++ b/starters/actuator/src/service/InfoService.ts @@ -69,9 +69,7 @@ export class InfoService { let git; if (properties !== undefined) { - const time = dateFormat - ? dayjs(properties.get("git.commit.time")).format(dateFormat) - : properties.get("git.commit.time"); + const time = dateFormat ? dayjs(properties.get("git.commit.time")).format(dateFormat) : properties.get("git.commit.time"); if (infoGitMode === "simple") { git = { diff --git a/starters/actuator/src/service/MetadataService.ts b/starters/actuator/src/service/MetadataService.ts index 9ffc2be9..8d6b6b9e 100644 --- a/starters/actuator/src/service/MetadataService.ts +++ b/starters/actuator/src/service/MetadataService.ts @@ -6,17 +6,9 @@ export class MetadataService { getControllers() { return this.metadataStorage.controllers.map(controller => { - const controllerPath = Reflect.getMetadata( - CONTROLLER_PATH_METADATA_KEY, - controller.target, - ); - const controllerVersion = Reflect.getMetadata( - CONTROLLER_VERSION_METADATA_KEY, - controller.target, - ); - const actions = this.metadataStorage.actions.filter( - action => action.target.name === controller.target.name, - ); + const controllerPath = Reflect.getMetadata(CONTROLLER_PATH_METADATA_KEY, controller.target); + const controllerVersion = Reflect.getMetadata(CONTROLLER_VERSION_METADATA_KEY, controller.target); + const actions = this.metadataStorage.actions.filter(action => action.target.name === controller.target.name); return { actions, controller: controller.target.name, diff --git a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts index f42bf6fe..3d51b69a 100644 --- a/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts +++ b/starters/persistence/src/adapter/DefaultRepositoriesAdapter.ts @@ -11,17 +11,9 @@ export class DefaultRepositoriesAdapter implements RepositoriesAdapter { for (const repository of PersistenceContext.get().repositories) { const {target, entity, type} = repository; - const entityRepositoryInstance = new (target as any)( - entity, - entityManager, - entityManager.queryRunner, - ); + const entityRepositoryInstance = new (target as any)(entity, entityManager, entityManager.queryRunner); - logger.info( - `Registering an '${type.toString()}' repository '${target.name}' for entity '${ - entity.name - }'`, - ); + logger.info(`Registering an '${type.toString()}' repository '${target.name}' for entity '${entity.name}'`); // Set repository to entity manager cache (entityManager as any).repositories.set(target, entityRepositoryInstance); // set it to the DI container diff --git a/starters/persistence/src/adapter/PersistenceLogger.ts b/starters/persistence/src/adapter/PersistenceLogger.ts index ea1cf5ae..f819da26 100644 --- a/starters/persistence/src/adapter/PersistenceLogger.ts +++ b/starters/persistence/src/adapter/PersistenceLogger.ts @@ -10,11 +10,7 @@ export class PersistenceLogger extends AbstractLogger { /** * Write log to specific output. */ - protected writeLog( - level: LogLevel, - logMessage: LogMessage | LogMessage[], - queryRunner?: QueryRunner, - ) { + protected writeLog(level: LogLevel, logMessage: LogMessage | LogMessage[], queryRunner?: QueryRunner) { const messages = this.prepareLogMessages(logMessage, { highlightSql: false, }); diff --git a/starters/persistence/src/config/DataSourceConfiguration.ts b/starters/persistence/src/config/DataSourceConfiguration.ts index 651472b3..ac001b5f 100644 --- a/starters/persistence/src/config/DataSourceConfiguration.ts +++ b/starters/persistence/src/config/DataSourceConfiguration.ts @@ -19,8 +19,7 @@ export class DataSourceConfiguration { } const persistenceLogger = new PersistenceLogger(logger); - const {databaseConnectionOverrides, eventSubscribers, migrations, namingStrategy} = - PersistenceContext.get(); + const {databaseConnectionOverrides, eventSubscribers, migrations, namingStrategy} = PersistenceContext.get(); const strategy = namingStrategy ? new namingStrategy() : undefined; @@ -28,9 +27,7 @@ export class DataSourceConfiguration { if (iocContainer.has(QUERY_CACHE_CONFIG)) { cacheConfig = iocContainer.get(QUERY_CACHE_CONFIG); } else { - logger.warn( - "No query cache configuration found while building datasource configuration", - ); + logger.warn("No query cache configuration found while building datasource configuration"); } let databaseConfigs = persistenceProperties[persistenceProperties.type]; @@ -58,9 +55,7 @@ export class DataSourceConfiguration { } if (databaseConfigs.synchronize && databaseConfigs.migrationsRun) { - throw new Error( - `Only one of "synchronize" or "migrationsRun" config property can be enabled. Please set one of them to false`, - ); + throw new Error(`Only one of "synchronize" or "migrationsRun" config property can be enabled. Please set one of them to false`); } // Save the synchronization and migration state diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index a9344e73..833885cb 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -56,24 +56,18 @@ export class PersistenceConfiguration { const initializationPromises: Promise[] = []; // Run migrations if enabled if (migrationsRun) { - initializationPromises.push( - PersistenceConfiguration.runMigration(logger, dataSource), - ); + initializationPromises.push(PersistenceConfiguration.runMigration(logger, dataSource)); } if (synchronizeDatabase) { - initializationPromises.push( - PersistenceConfiguration.runDatabaseSync(logger, dataSource), - ); + initializationPromises.push(PersistenceConfiguration.runDatabaseSync(logger, dataSource)); } // Bind Data Repositories if DI container is configured PersistenceConfiguration.bindDataRepositories(logger); // Validate database consistency - Promise.all(initializationPromises).then(_ => - PersistenceConfiguration.ensureDatabase(logger, dataSource), - ); + Promise.all(initializationPromises).then(_ => PersistenceConfiguration.ensureDatabase(logger, dataSource)); }) .catch(err => { logger.error("Error during Persistence DataSource initialization:", err); @@ -109,20 +103,11 @@ export class PersistenceConfiguration { * @param dataSource (DataSource): An instance of the DataSource class representing the database connection. * @param iocContainer (IocContainer): An instance of the IoC container used for dependency injection. * */ - static setupInjection( - logger: Logger, - dataSource: DataSource, - iocContainer: IocContainer, - ) { + static setupInjection(logger: Logger, dataSource: DataSource, iocContainer: IocContainer) { const subscribers = dataSource.subscribers; - logger.info( - `Setting up dependency injection for ${subscribers.length} persistence event subscribers`, - ); + logger.info(`Setting up dependency injection for ${subscribers.length} persistence event subscribers`); for (const subscriber of subscribers) { - for (const fieldToInject of Reflect.getMetadata( - REQUIRES_FIELD_INJECTION_KEY, - subscriber, - ) || []) { + for (const fieldToInject of Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, subscriber) || []) { // Extract type metadata for field injection. This is useful for custom injection in some modules const propertyType = Reflect.getMetadata("design:type", subscriber, fieldToInject); subscriber[fieldToInject] = iocContainer.get(propertyType); @@ -196,13 +181,9 @@ export class PersistenceConfiguration { try { const tables = await queryRunner.getTables(); const entities = dataSource.entityMetadatas; - logger.info( - `Database validation: Running consistency validation for ${tables.length}-tables/${entities.length}-entities.`, - ); + logger.info(`Database validation: Running consistency validation for ${tables.length}-tables/${entities.length}-entities.`); - const existingEntities = entities.filter( - entity => tables.find(table => table.name.includes(entity.tableName)) !== undefined, - ); + const existingEntities = entities.filter(entity => tables.find(table => table.name.includes(entity.tableName)) !== undefined); if (existingEntities.length !== entities.length) { logger.error( diff --git a/starters/persistence/src/config/QueryCacheConfiguration.ts b/starters/persistence/src/config/QueryCacheConfiguration.ts index ca4152c5..a350dd00 100644 --- a/starters/persistence/src/config/QueryCacheConfiguration.ts +++ b/starters/persistence/src/config/QueryCacheConfiguration.ts @@ -39,11 +39,7 @@ export class QueryCacheConfiguration { } if (cacheConfig) { - logger.info( - `Configuring query cache with options from configuration${ - cacheProvider ? " and custom cache provider" : "" - }`, - ); + logger.info(`Configuring query cache with options from configuration${cacheProvider ? " and custom cache provider" : ""}`); iocContainer.set(QUERY_CACHE_CONFIG, { ...cacheConfig, provider: cacheProvider, @@ -56,21 +52,12 @@ export class QueryCacheConfiguration { } else if (cacheEnabled) { // If cache is only enabled, falling back to database cache or to a custom provider if specified logger.info( - `${ - cacheProvider - ? "Configuring custom query cache provider" - : "Enabling database query cache with default configurations" - }`, - ); - iocContainer.set( - QUERY_CACHE_CONFIG, - cacheProvider ? {provider: cacheProvider} : true, + `${cacheProvider ? "Configuring custom query cache provider" : "Enabling database query cache with default configurations"}`, ); + iocContainer.set(QUERY_CACHE_CONFIG, cacheProvider ? {provider: cacheProvider} : true); } else { // Cache is explicitly disabled - logger.warn( - "Persistence query cache is not enabled. Enable it to boost your application performance.", - ); + logger.warn("Persistence query cache is not enabled. Enable it to boost your application performance."); } } } diff --git a/starters/persistence/src/config/TransactionConfiguration.ts b/starters/persistence/src/config/TransactionConfiguration.ts index 53647f60..fa03515b 100644 --- a/starters/persistence/src/config/TransactionConfiguration.ts +++ b/starters/persistence/src/config/TransactionConfiguration.ts @@ -1,11 +1,7 @@ import {Bean, Configuration} from "@node-boot/core"; import {BeansContext} from "@node-boot/context"; import {DataSource} from "typeorm"; -import { - addTransactionalDataSource, - initializeTransactionalContext, - StorageDriver, -} from "typeorm-transactional"; +import {addTransactionalDataSource, initializeTransactionalContext, StorageDriver} from "typeorm-transactional"; @Configuration() export class TransactionConfiguration { diff --git a/starters/persistence/src/decorator/EntityEventSubscriber.ts b/starters/persistence/src/decorator/EntityEventSubscriber.ts index 01fa8115..8fac6247 100644 --- a/starters/persistence/src/decorator/EntityEventSubscriber.ts +++ b/starters/persistence/src/decorator/EntityEventSubscriber.ts @@ -1,9 +1,7 @@ import {EntitySubscriberInterface, EventSubscriber} from "typeorm"; import {PersistenceContext} from "../PersistenceContext"; -export function EntityEventSubscriber< - T extends new (...args: any[]) => EntitySubscriberInterface, ->() { +export function EntityEventSubscriber EntitySubscriberInterface>() { return (target: T) => { EventSubscriber()(target); PersistenceContext.get().eventSubscribers.push(target); diff --git a/starters/persistence/src/decorator/PersistenceNamingStrategy.ts b/starters/persistence/src/decorator/PersistenceNamingStrategy.ts index a66b50aa..35581f61 100644 --- a/starters/persistence/src/decorator/PersistenceNamingStrategy.ts +++ b/starters/persistence/src/decorator/PersistenceNamingStrategy.ts @@ -1,9 +1,7 @@ import {NamingStrategyInterface} from "typeorm/naming-strategy/NamingStrategyInterface"; import {PersistenceContext} from "../PersistenceContext"; -export function PersistenceNamingStrategy< - T extends new (...args: any[]) => NamingStrategyInterface, ->() { +export function PersistenceNamingStrategy NamingStrategyInterface>() { return (target: T) => { PersistenceContext.get().namingStrategy = target; }; diff --git a/starters/persistence/src/decorator/Transactional.ts b/starters/persistence/src/decorator/Transactional.ts index bf1c062c..1711bc11 100644 --- a/starters/persistence/src/decorator/Transactional.ts +++ b/starters/persistence/src/decorator/Transactional.ts @@ -23,11 +23,7 @@ import {Transactional as InnerTransactional, WrapInTransactionOptions} from "typ * name: The name or symbol of the method being decorated. * */ export const Transactional = (options?: WrapInTransactionOptions): MethodDecorator => { - return ( - target: Object, - propertyKey: string | symbol, - descriptor: TypedPropertyDescriptor, - ) => { + return (target: Object, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor) => { InnerTransactional(options)(target, propertyKey, descriptor); }; }; diff --git a/starters/persistence/src/hook/hooks.ts b/starters/persistence/src/hook/hooks.ts index e2873829..7f75e95a 100644 --- a/starters/persistence/src/hook/hooks.ts +++ b/starters/persistence/src/hook/hooks.ts @@ -18,9 +18,6 @@ export const runOnTransactionComplete = (cb: (e: Error | undefined) => void) => return innerRunOnTransactionComplete(cb); }; -export const runInTransaction = ReturnType>( - fn: Func, - options?: WrapInTransactionOptions, -) => { +export const runInTransaction = ReturnType>(fn: Func, options?: WrapInTransactionOptions) => { return wrapInTransaction(fn, options)(); }; diff --git a/starters/persistence/src/property/MongoConnectionProperties.ts b/starters/persistence/src/property/MongoConnectionProperties.ts index 630bb966..c48a9c5f 100644 --- a/starters/persistence/src/property/MongoConnectionProperties.ts +++ b/starters/persistence/src/property/MongoConnectionProperties.ts @@ -233,12 +233,7 @@ export interface MongoConnectionProperties { * The preferred read preference (ReadPreference.PRIMARY, ReadPreference.PRIMARY_PREFERRED, ReadPreference.SECONDARY, * ReadPreference.SECONDARY_PREFERRED, ReadPreference.NEAREST). */ - readonly readPreference?: - | "primary" - | "primaryPreferred" - | "secondary" - | "secondaryPreferred" - | "nearest"; + readonly readPreference?: "primary" | "primaryPreferred" | "secondary" | "secondaryPreferred" | "nearest"; /** * Specify a maxStalenessSeconds value for secondary reads, minimum is 90 seconds diff --git a/starters/persistence/src/property/NodeBootDataSourceOptions.ts b/starters/persistence/src/property/NodeBootDataSourceOptions.ts index 651617ed..04c192da 100644 --- a/starters/persistence/src/property/NodeBootDataSourceOptions.ts +++ b/starters/persistence/src/property/NodeBootDataSourceOptions.ts @@ -11,13 +11,7 @@ import {AuroraPostgresConnectionOptions} from "typeorm/driver/aurora-postgres/Au import {BetterSqlite3ConnectionOptions} from "typeorm/driver/better-sqlite3/BetterSqlite3ConnectionOptions"; import {SpannerConnectionOptions} from "typeorm/driver/spanner/SpannerConnectionOptions"; -type NotOverridable = - | "subscribers" - | "namingStrategy" - | "cache" - | "logger" - | "entities" - | "migrations"; +type NotOverridable = "subscribers" | "namingStrategy" | "cache" | "logger" | "entities" | "migrations"; export type NodeBootDataSourceOptions = | Omit diff --git a/starters/persistence/src/property/SqlServerConnectionProperties.ts b/starters/persistence/src/property/SqlServerConnectionProperties.ts index 8fb5d269..7a129a36 100644 --- a/starters/persistence/src/property/SqlServerConnectionProperties.ts +++ b/starters/persistence/src/property/SqlServerConnectionProperties.ts @@ -204,23 +204,13 @@ export interface SqlServerConnectionProperties extends SqlServerConnectionCreden * The default isolation level that transactions will be run with. The isolation levels are available * from require('tedious').ISOLATION_LEVEL. (default: READ_COMMITTED). */ - readonly isolation?: - | "READ_UNCOMMITTED" - | "READ_COMMITTED" - | "REPEATABLE_READ" - | "SERIALIZABLE" - | "SNAPSHOT"; + readonly isolation?: "READ_UNCOMMITTED" | "READ_COMMITTED" | "REPEATABLE_READ" | "SERIALIZABLE" | "SNAPSHOT"; /** * The default isolation level for new connections. All out-of-transaction queries are executed with this * setting. The isolation levels are available from require('tedious').ISOLATION_LEVEL . */ - readonly connectionIsolationLevel?: - | "READ_UNCOMMITTED" - | "READ_COMMITTED" - | "REPEATABLE_READ" - | "SERIALIZABLE" - | "SNAPSHOT"; + readonly connectionIsolationLevel?: "READ_UNCOMMITTED" | "READ_COMMITTED" | "REPEATABLE_READ" | "SERIALIZABLE" | "SNAPSHOT"; /** * A boolean, determining whether the connection will request read only access from a SQL Server From c20e86e36d283fb8ca172988fb3b5da512d6e73e Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 02:36:24 +0000 Subject: [PATCH 10/16] several improvements --- packages/context/src/config/index.ts | 1 - packages/context/src/index.ts | 2 +- .../src/options/NodeBootEngineOptions.ts | 6 - .../src/{config => services}/Config.ts | 40 ++-- .../context/src/services/LoggerService.ts | 17 ++ packages/context/src/services/index.ts | 3 + packages/context/src/services/types.ts | 23 +++ packages/context/src/types.ts | 2 +- packages/core/src/server/BaseServer.ts | 5 +- packages/engine/src/core/NodeBootDriver.ts | 6 - packages/engine/src/core/NodeBootEngine.ts | 2 +- packages/engine/src/core/NodeBootToolkit.ts | 14 +- .../src/service/ActionParameterHandler.ts | 4 +- .../engine/src/service/ComponentImporter.ts | 2 +- packages/extension/src/Optional.ts | 4 +- packages/extension/src/ServerConfig.ts | 96 ++++++++++ packages/extension/src/index.ts | 1 + pnpm-lock.yaml | 179 ++++++++++++++++-- .../src/services/users.service.ts | 3 +- servers/express-server/package.json | 12 +- .../src/driver/ExpressDriver.ts | 106 +++++------ .../src/loader/DependenciesLoader.ts | 41 ++++ servers/express-server/src/loader/index.ts | 1 + .../src/server/ExpressServer.ts | 13 +- servers/fastify-server/package.json | 1 + .../src/driver/FastifyDriver.ts | 95 ++++++---- .../src/server/FastifyServer.ts | 47 +++-- servers/koa-server/package.json | 19 +- servers/koa-server/src/driver/KoaDriver.ts | 121 ++++++------ .../src/loader/DependenciesLoader.ts | 73 +++++++ servers/koa-server/src/loader/index.ts | 1 + servers/koa-server/src/server/KoaServer.ts | 14 +- 32 files changed, 685 insertions(+), 269 deletions(-) delete mode 100644 packages/context/src/config/index.ts rename packages/context/src/{config => services}/Config.ts (86%) create mode 100644 packages/context/src/services/LoggerService.ts create mode 100644 packages/context/src/services/index.ts create mode 100644 packages/context/src/services/types.ts create mode 100644 packages/extension/src/ServerConfig.ts create mode 100644 servers/express-server/src/loader/DependenciesLoader.ts create mode 100644 servers/express-server/src/loader/index.ts create mode 100644 servers/koa-server/src/loader/DependenciesLoader.ts create mode 100644 servers/koa-server/src/loader/index.ts diff --git a/packages/context/src/config/index.ts b/packages/context/src/config/index.ts deleted file mode 100644 index f7ca0fcf..00000000 --- a/packages/context/src/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./Config"; diff --git a/packages/context/src/index.ts b/packages/context/src/index.ts index 3add772e..7e2e0df4 100644 --- a/packages/context/src/index.ts +++ b/packages/context/src/index.ts @@ -8,4 +8,4 @@ export * from "./types"; export * from "./handlers"; export * from "./checkers"; export * from "./metadata"; -export * from "./config"; +export * from "./services"; diff --git a/packages/context/src/options/NodeBootEngineOptions.ts b/packages/context/src/options/NodeBootEngineOptions.ts index cce4c8d1..7b26778a 100644 --- a/packages/context/src/options/NodeBootEngineOptions.ts +++ b/packages/context/src/options/NodeBootEngineOptions.ts @@ -6,12 +6,6 @@ import {AuthorizationChecker, CurrentUserChecker} from "../checkers"; * NodeBoot initialization options. */ export interface NodeBootEngineOptions { - /** - * Indicates if cors are enabled. - * This requires installation of additional module (cors for express and @koa/cors for koa). - */ - cors?: boolean | Object; - /** * Global route prefix, for example '/api'. */ diff --git a/packages/context/src/config/Config.ts b/packages/context/src/services/Config.ts similarity index 86% rename from packages/context/src/config/Config.ts rename to packages/context/src/services/Config.ts index 2b32c1ce..8cb2af40 100644 --- a/packages/context/src/config/Config.ts +++ b/packages/context/src/services/Config.ts @@ -1,26 +1,4 @@ -/** - * A type representing all allowed JSON primitive values. - * - */ -export type JsonPrimitive = number | string | boolean | null; -/** - * A type representing all allowed JSON object values. - * - */ -export type JsonObject = { - [key in string]?: JsonValue; -}; -/** - * A type representing all allowed JSON array values. - * - */ -export type JsonArray = Array; - -/** - * A type representing all allowed JSON values. - * - */ -export type JsonValue = JsonObject | JsonArray | JsonPrimitive; +import {JsonValue} from "./types"; /** * The interface used to represent static configuration at runtime. @@ -37,18 +15,22 @@ export type Config = { subscribe?(onChange: () => void): { unsubscribe: () => void; }; + /** * Checks whether the given key is present. */ has(key: string): boolean; + /** * Lists all available configuration keys. */ keys(): string[]; + /** * Same as `getOptional`, but will throw an error if there's no value for the given key. */ get(key?: string): T; + /** * Read out all configuration data for the given key. * @@ -58,52 +40,64 @@ export type Config = { * shapes of the configuration. */ getOptional(key?: string): T | undefined; + /** * Same as `getOptionalConfig`, but will throw an error if there's no value for the given key. */ getConfig(key: string): Config; + /** * Creates a sub-view of the configuration object. * The configuration value at the position of the provided key must be an object. */ getOptionalConfig(key: string): Config | undefined; + /** * Same as `getOptionalConfigArray`, but will throw an error if there's no value for the given key. */ getConfigArray(key: string): Config[]; + /** * Creates a sub-view of an array of configuration objects. * The configuration value at the position of the provided key must be an array of objects. */ getOptionalConfigArray(key: string): Config[] | undefined; + /** * Same as `getOptionalNumber`, but will throw an error if there's no value for the given key. */ getNumber(key: string): number; + /** * Reads a configuration value at the given key, expecting it to be a number. */ getOptionalNumber(key: string): number | undefined; + /** * Same as `getOptionalBoolean`, but will throw an error if there's no value for the given key. */ getBoolean(key: string): boolean; + /** * Reads a configuration value at the given key, expecting it to be a boolean. */ getOptionalBoolean(key: string): boolean | undefined; + /** * Same as `getOptionalString`, but will throw an error if there's no value for the given key. */ getString(key: string): string; + /** * Reads a configuration value at the given key, expecting it to be a string. */ getOptionalString(key: string): string | undefined; + /** * Same as `getOptionalStringArray`, but will throw an error if there's no value for the given key. */ getStringArray(key: string): string[]; + /** * Reads a configuration value at the given key, expecting it to be an array of strings. */ diff --git a/packages/context/src/services/LoggerService.ts b/packages/context/src/services/LoggerService.ts new file mode 100644 index 00000000..8abc6fba --- /dev/null +++ b/packages/context/src/services/LoggerService.ts @@ -0,0 +1,17 @@ +import {JsonObject} from "./types"; + +/** + * A service that provides a logging facility. + * + */ +export interface LoggerService { + error(message: string, meta?: Error | JsonObject): void; + + warn(message: string, meta?: Error | JsonObject): void; + + info(message: string, meta?: Error | JsonObject): void; + + debug(message: string, meta?: Error | JsonObject): void; + + child(meta: JsonObject): LoggerService; +} diff --git a/packages/context/src/services/index.ts b/packages/context/src/services/index.ts new file mode 100644 index 00000000..9275f85b --- /dev/null +++ b/packages/context/src/services/index.ts @@ -0,0 +1,3 @@ +export {Config} from "./Config"; +export {LoggerService} from "./LoggerService"; +export * from "./types"; diff --git a/packages/context/src/services/types.ts b/packages/context/src/services/types.ts new file mode 100644 index 00000000..0c25b296 --- /dev/null +++ b/packages/context/src/services/types.ts @@ -0,0 +1,23 @@ +/** + * A type representing all allowed JSON primitive values. + * + */ +export type JsonPrimitive = number | string | boolean | null; +/** + * A type representing all allowed JSON object values. + * + */ +export type JsonObject = { + [key in string]?: JsonValue; +}; +/** + * A type representing all allowed JSON array values. + * + */ +export type JsonArray = Array; + +/** + * A type representing all allowed JSON values. + * + */ +export type JsonValue = JsonObject | JsonArray | JsonPrimitive; diff --git a/packages/context/src/types.ts b/packages/context/src/types.ts index d53cf8da..9fcbe7a6 100644 --- a/packages/context/src/types.ts +++ b/packages/context/src/types.ts @@ -1,6 +1,6 @@ import {IocContainer} from "./ioc"; import {Logger} from "winston"; -import {Config} from "./config"; +import {Config} from "./services"; /** * Controller action properties. diff --git a/packages/core/src/server/BaseServer.ts b/packages/core/src/server/BaseServer.ts index ba4aaebe..539d8266 100644 --- a/packages/core/src/server/BaseServer.ts +++ b/packages/core/src/server/BaseServer.ts @@ -1,12 +1,11 @@ -import {ApiOptions, ApplicationContext, Config, useContainer} from "@node-boot/context"; +import {ApiOptions, ApplicationContext, ApplicationOptions, useContainer} from "@node-boot/context"; import {Logger} from "winston"; import {createLogger} from "../logger"; import {ConfigService, loadNodeBootConfig} from "@node-boot/config"; -import {ApplicationOptions} from "@node-boot/context/src"; export abstract class BaseServer { protected logger: Logger; - protected config: Config; + protected config: ConfigService; protected constructor(private readonly serverType: string) {} diff --git a/packages/engine/src/core/NodeBootDriver.ts b/packages/engine/src/core/NodeBootDriver.ts index add0ea8e..30f9fdf1 100644 --- a/packages/engine/src/core/NodeBootDriver.ts +++ b/packages/engine/src/core/NodeBootDriver.ts @@ -63,12 +63,6 @@ export abstract class NodeBootDriver { */ routePrefix = ""; - /** - * Indicates if cors are enabled. - * This requires installation of additional module (cors for express and @koa/cors for koa). - */ - cors?: boolean | Object; - /** * Map of error overrides. */ diff --git a/packages/engine/src/core/NodeBootEngine.ts b/packages/engine/src/core/NodeBootEngine.ts index 8d585c63..f5f4e428 100644 --- a/packages/engine/src/core/NodeBootEngine.ts +++ b/packages/engine/src/core/NodeBootEngine.ts @@ -110,7 +110,7 @@ export class NodeBootEngine> { /** * Handles result of the action method execution. */ - protected handleCallMethodResult(result: any, action: ActionMetadata, options: Action, interceptorFns: Function[]): any { + protected handleCallMethodResult(result: any, action: ActionMetadata, options: Action, interceptorFns: Function[]) { if (isPromiseLike(result)) { return result .then((data: any) => { diff --git a/packages/engine/src/core/NodeBootToolkit.ts b/packages/engine/src/core/NodeBootToolkit.ts index 7756ed62..793bdfec 100644 --- a/packages/engine/src/core/NodeBootToolkit.ts +++ b/packages/engine/src/core/NodeBootToolkit.ts @@ -4,6 +4,7 @@ import {ValidationOptions} from "class-validator"; import {NodeBootDriver} from "./NodeBootDriver"; import {ComponentImporter} from "../service/ComponentImporter"; import {CustomParameterDecorator, NodeBootEngineOptions} from "@node-boot/context"; +import {Optional} from "@node-boot/extension"; export class NodeBootToolkit { /** @@ -73,15 +74,10 @@ export class NodeBootToolkit { driver.classToPlainTransformOptions = options.classToPlainTransformOptions; driver.plainToClassTransformOptions = options.plainToClassTransformOptions; - if (options.errorOverridingMap !== undefined) driver.errorOverridingMap = options.errorOverridingMap; - - if (options.routePrefix !== undefined) driver.routePrefix = options.routePrefix; - - if (options.currentUserChecker !== undefined) driver.currentUserChecker = options.currentUserChecker; - - if (options.authorizationChecker !== undefined) driver.authorizationChecker = options.authorizationChecker; - - driver.cors = options.cors; + Optional.of(options.errorOverridingMap).ifPresent(it => (driver.errorOverridingMap = it)); + Optional.of(options.routePrefix).ifPresent(it => (driver.routePrefix = it)); + Optional.of(options.currentUserChecker).ifPresent(it => (driver.currentUserChecker = it)); + Optional.of(options.authorizationChecker).ifPresent(it => (driver.authorizationChecker = it)); } /** diff --git a/packages/engine/src/service/ActionParameterHandler.ts b/packages/engine/src/service/ActionParameterHandler.ts index 8aae6cce..356907ee 100644 --- a/packages/engine/src/service/ActionParameterHandler.ts +++ b/packages/engine/src/service/ActionParameterHandler.ts @@ -17,7 +17,7 @@ import {Action, ParamMetadata} from "@node-boot/context"; * Handles action parameter. */ export class ActionParameterHandler> { - constructor(private driver: TDriver) {} + constructor(private readonly driver: TDriver) {} /** * Handles action parameter. @@ -98,7 +98,7 @@ export class ActionParameterHandler { + protected async normalizeParamValue(value: any, param: ParamMetadata) { if (value === null || value === undefined) return value; const isNormalizationNeeded = typeof value === "object" && ["queries", "headers", "params", "cookies"].includes(param.type); diff --git a/packages/engine/src/service/ComponentImporter.ts b/packages/engine/src/service/ComponentImporter.ts index d016b15c..b8f672f7 100644 --- a/packages/engine/src/service/ComponentImporter.ts +++ b/packages/engine/src/service/ComponentImporter.ts @@ -1,4 +1,4 @@ -import {ClassFiles} from "@node-boot/extension/src"; +import {ClassFiles} from "@node-boot/extension"; import {NodeBootEngineOptions} from "@node-boot/context"; export class ComponentImporter { diff --git a/packages/extension/src/Optional.ts b/packages/extension/src/Optional.ts index ec9afa87..522fac6d 100644 --- a/packages/extension/src/Optional.ts +++ b/packages/extension/src/Optional.ts @@ -250,11 +250,13 @@ export class Optional { * The ifEmpty method executes a specified action if the value inside the Optional object is empty. * * @param action A callback function that performs an action when the value inside the Optional object is empty. + * @return Returns the Optional object itself. * */ - ifEmpty(action: () => void): void { + ifEmpty(action: () => void): Optional { if (this.isEmpty()) { action(); } + return this; } /** diff --git a/packages/extension/src/ServerConfig.ts b/packages/extension/src/ServerConfig.ts new file mode 100644 index 00000000..07c18204 --- /dev/null +++ b/packages/extension/src/ServerConfig.ts @@ -0,0 +1,96 @@ +import {Optional} from "./Optional"; + +export type MaybeOptions = { + enabled?: boolean; + options?: OptionsType; +}; + +export type ServerConfigOptions< + CookieOptions = unknown, + CorsOptions = unknown, + SessionOptions = unknown, + MultipartOptions = unknown, + TemplateOptions = unknown, +> = { + cookie?: MaybeOptions; + cors?: MaybeOptions; + session?: MaybeOptions; + multipart?: MaybeOptions; + template?: MaybeOptions; +}; + +export class ServerConfig { + private readonly value: T | undefined | null; + + private constructor(value: T | undefined | null) { + this.value = value; + } + + static of(value: T | undefined | null): ServerConfig { + return new ServerConfig(value); + } + + asOptional(): Optional { + return Optional.of(this.value); + } + + configured(optionsName: string): Optional { + if (this.value?.[optionsName]) { + if (this.value[optionsName]?.enabled !== undefined) { + // If is configured set up the configuration + if (this.value[optionsName].enabled) { + return Optional.of(this.value[optionsName].options); + } + } else { + // By default, set up with default configs + return Optional.of(true); + } + } + return Optional.empty(); + } + + ifCors(consumer: (value?: any) => void, not?: () => void): ServerConfig { + this.configured("cors") + .ifEmpty(() => not?.()) + .ifPresent(options => this.consumerWrapper(options, consumer)); + return this; + } + + ifCookies(consumer: (value?: any) => void, not?: () => void): ServerConfig { + this.configured("cookie") + .ifEmpty(() => not?.()) + .ifPresent(options => this.consumerWrapper(options, consumer)); + return this; + } + + ifSession(consumer: (value?: any) => void, not?: () => void): ServerConfig { + this.configured("session") + .ifEmpty(() => not?.()) + .ifPresent(options => this.consumerWrapper(options, consumer)); + return this; + } + + ifMultipart(consumer: (value?: any) => void, not?: () => void): ServerConfig { + this.configured("multipart") + .ifEmpty(() => not?.()) + .ifPresent(options => this.consumerWrapper(options, consumer)); + return this; + } + + ifTemplate(consumer: (value?: any) => void, not?: () => void): ServerConfig { + this.configured("template") + .ifEmpty(() => not?.()) + .ifPresent(options => this.consumerWrapper(options, consumer)); + return this; + } + + private consumerWrapper(options: T | true, consumer: (value?: T) => void) { + if (options === true) { + // By default, use default configs + consumer(); + } else { + //If feature options is provided, set up the configuration + consumer(options); + } + } +} diff --git a/packages/extension/src/index.ts b/packages/extension/src/index.ts index 5e2671e4..31b0477b 100644 --- a/packages/extension/src/index.ts +++ b/packages/extension/src/index.ts @@ -1,3 +1,4 @@ export {Optional} from "./Optional"; export {Param} from "./Param"; export {ClassFiles} from "./ClassFiles"; +export * from "./ServerConfig"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6710b0f3..bdfab3a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -543,22 +543,46 @@ importers: '@node-boot/error': specifier: 1.0.0 version: link:../../packages/error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension body-parser: specifier: ^1.20.2 version: 1.20.2 + cookie: + specifier: ^0.6.0 + version: 0.6.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 express: specifier: '>=4.18.2' version: 4.18.2 + express-session: + specifier: ^1.17.3 + version: 1.17.3 multer: specifier: ^1.4.5-lts.1 version: 1.4.5-lts.1 + template-url: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: '@types/body-parser': specifier: ^1.19.2 version: 1.19.2 + '@types/cookie': + specifier: ^0.6.0 + version: 0.6.0 + '@types/cors': + specifier: ^2.8.17 + version: 2.8.17 '@types/express': specifier: ^4.17.17 version: 4.17.17 + '@types/express-session': + specifier: ^1.17.10 + version: 1.17.10 '@types/multer': specifier: ^1.4.7 version: 1.4.7 @@ -592,6 +616,9 @@ importers: '@node-boot/error': specifier: 1.0.0 version: link:../../packages/error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension fastify: specifier: '>=4.21.0' version: 4.21.0 @@ -604,6 +631,9 @@ importers: servers/koa-server: dependencies: + '@koa/cors': + specifier: ^5.0.0 + version: 5.0.0 '@koa/multer': specifier: ^3.0.2 version: 3.0.2(multer@1.4.5-lts.1) @@ -622,22 +652,40 @@ importers: '@node-boot/error': specifier: 1.0.0 version: link:../../packages/error + '@node-boot/extension': + specifier: 1.0.0 + version: link:../../packages/extension koa: - specifier: '>=2.14.2' + specifier: ^2.14.2 version: 2.14.2 koa-bodyparser: specifier: ^4.4.1 version: 4.4.1 - routing-controllers: - specifier: '>=0.10.4' - version: 0.10.4(class-transformer@0.5.1)(class-validator@0.14.0) + koa-cookies: + specifier: ^4.0.2 + version: 4.0.2 + koa-session: + specifier: ^6.4.0 + version: 6.4.0 + template-url: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: '@types/koa': - specifier: ^2.13.8 - version: 2.13.8 - '@types/koa-router': - specifier: ^7.4.8 - version: 7.4.8 + specifier: ^2.13.12 + version: 2.13.12 + '@types/koa-session': + specifier: ^6.4.5 + version: 6.4.5 + '@types/koa__cors': + specifier: ^5.0.0 + version: 5.0.0 + '@types/koa__multer': + specifier: ^2.0.7 + version: 2.0.7 + '@types/koa__router': + specifier: ^12.0.4 + version: 12.0.4 starters/actuator: dependencies: @@ -1647,6 +1695,13 @@ packages: vary: 1.1.2 dev: false + /@koa/cors@5.0.0: + resolution: {integrity: sha512-x/iUDjcS90W69PryLDIMgFyV21YLTnG9zOpPXS7Bkt2b8AsY3zZsIpOLBkYr9fBcF3HbkKaER5hOBZLfpLgYNw==} + engines: {node: '>= 14.0.0'} + dependencies: + vary: 1.1.2 + dev: false + /@koa/multer@3.0.2(multer@1.4.5-lts.1): resolution: {integrity: sha512-Q6WfPpE06mJWyZD1fzxM6zWywaoo+zocAn2YA9QYz4RsecoASr1h/kSzG0c5seDpFVKCMZM9raEfuM7XfqbRLw==} engines: {node: '>= 8'} @@ -1911,6 +1966,10 @@ packages: /@types/content-disposition@0.5.5: resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==} + /@types/cookie@0.6.0: + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + dev: true + /@types/cookiejar@2.1.2: resolution: {integrity: sha512-t73xJJrvdTjXrn4jLS9VSGRbz0nUY3cl2DMGDU48lKl+HR9dbbjW2A9r3g40VA++mQpy6uuHg33gy7du2BKpog==} dev: true @@ -1929,6 +1988,12 @@ packages: '@types/node': 20.4.2 dev: true + /@types/cors@2.8.17: + resolution: {integrity: sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==} + dependencies: + '@types/node': 18.15.3 + dev: true + /@types/express-serve-static-core@4.17.35: resolution: {integrity: sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==} dependencies: @@ -1937,6 +2002,12 @@ packages: '@types/range-parser': 1.2.4 '@types/send': 0.17.1 + /@types/express-session@1.17.10: + resolution: {integrity: sha512-U32bC/s0ejXijw5MAzyaV4tuZopCh/K7fPoUDyNbsRXHvPSeymygYD1RFL99YOLhF5PNOkzswvOTRaVHdL1zMw==} + dependencies: + '@types/express': 4.17.17 + dev: true + /@types/express@4.17.17: resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==} dependencies: @@ -2001,7 +2072,7 @@ packages: /@types/koa-compose@3.2.5: resolution: {integrity: sha512-B8nG/OoE1ORZqCkBVsup/AKcvjdgoHnfi4pZMn5UwAPCbhk/96xyv284eBYW8JlQbQ7zDmnpFr68I/40mFoIBQ==} dependencies: - '@types/koa': 2.13.11 + '@types/koa': 2.13.12 /@types/koa-cors@0.0.2: resolution: {integrity: sha512-uNaDY26HUVO+2C6arK8ZFODs9mBjYprD8mlvkVe2bYdX9wzEeKtycVXPafXpUkePhMh4sffIMkhRDyedokG/QA==} @@ -2016,10 +2087,11 @@ packages: helmet: 4.6.0 dev: true - /@types/koa-router@7.4.8: - resolution: {integrity: sha512-SkWlv4F9f+l3WqYNQHnWjYnyTxYthqt8W9az2RTdQW7Ay8bc00iRZcrb8MC75iEfPqnGcg2csEl8tTG1NQPD4A==} + /@types/koa-session@6.4.5: + resolution: {integrity: sha512-Vc6+fslnPuMH2v9y80WYeo39UMo8mweuNNthKCwYU2ZE6l5vnRrzRU3BRvexKwsoI5sxsRl5CxDsBlLI8kY/XA==} dependencies: - '@types/koa': 2.13.11 + '@types/cookies': 0.7.7 + '@types/koa': 2.13.12 dev: true /@types/koa@2.13.11: @@ -2033,6 +2105,19 @@ packages: '@types/keygrip': 1.0.2 '@types/koa-compose': 3.2.5 '@types/node': 20.4.2 + dev: true + + /@types/koa@2.13.12: + resolution: {integrity: sha512-vAo1KuDSYWFDB4Cs80CHvfmzSQWeUb909aQib0C0aFx4sw0K9UZFz2m5jaEP+b3X1+yr904iQiruS0hXi31jbw==} + dependencies: + '@types/accepts': 1.3.5 + '@types/content-disposition': 0.5.5 + '@types/cookies': 0.7.7 + '@types/http-assert': 1.5.3 + '@types/http-errors': 2.0.1 + '@types/keygrip': 1.0.2 + '@types/koa-compose': 3.2.5 + '@types/node': 18.15.3 /@types/koa@2.13.8: resolution: {integrity: sha512-Ugmxmgk/yPRW3ptBTh9VjOLwsKWJuGbymo1uGX0qdaqqL18uJiiG1ZoV0rxCOYSaDGhvEp5Ece02Amx0iwaxQQ==} @@ -2046,10 +2131,22 @@ packages: '@types/koa-compose': 3.2.5 '@types/node': 20.4.2 + /@types/koa__cors@5.0.0: + resolution: {integrity: sha512-LCk/n25Obq5qlernGOK/2LUwa/2YJb2lxHUkkvYFDOpLXlVI6tKcdfCHRBQnOY4LwH6el5WOLs6PD/a8Uzau6g==} + dependencies: + '@types/koa': 2.13.12 + dev: true + + /@types/koa__multer@2.0.7: + resolution: {integrity: sha512-O7hiAEpdgW1nly93jQ8TVL2nPC7Bg1HHRf1/LGNQb7ygGBjNgZWpliCm7tswNW3MjcgYbTtz0+Sca5wHne+RyA==} + dependencies: + '@types/koa': 2.13.12 + dev: true + /@types/koa__router@12.0.4: resolution: {integrity: sha512-Y7YBbSmfXZpa/m5UGGzb7XadJIRBRnwNY9cdAojZGp65Cpe5MAP3mOZE7e3bImt8dfKS4UFcR16SLH8L/z7PBw==} dependencies: - '@types/koa': 2.13.11 + '@types/koa': 2.13.12 dev: true /@types/lodash@4.14.197: @@ -3007,13 +3104,17 @@ packages: engines: {node: '>= 0.6'} requiresBuild: true dev: false - optional: true /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} dev: false + /cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + dev: false + /cookies@0.8.0: resolution: {integrity: sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==} engines: {node: '>= 0.8'} @@ -3041,6 +3142,12 @@ packages: vary: 1.1.2 dev: false + /crc@3.8.0: + resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==} + dependencies: + buffer: 5.7.1 + dev: false + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: false @@ -3497,7 +3604,6 @@ packages: transitivePeerDependencies: - supports-color dev: false - optional: true /express@4.18.2: resolution: {integrity: sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==} @@ -4139,6 +4245,10 @@ packages: dependencies: binary-extensions: 2.2.0 + /is-class-hotfix@0.0.6: + resolution: {integrity: sha512-0n+pzCC6ICtVr/WXnN2f03TK/3BfXY7me4cjCAqT8TYXEl0+JBRoqBo94JJHXcyDSLUeWbNX8Fvy5g5RJdAstQ==} + dev: false + /is-core-module@2.11.0: resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} dependencies: @@ -4194,6 +4304,14 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-type-of@1.4.0: + resolution: {integrity: sha512-EddYllaovi5ysMLMEN7yzHEKh8A850cZ7pykrY1aNRQGn/CDjRDE9qEWbIdt7xGEVJmjBXzU/fNnC4ABTm8tEQ==} + dependencies: + core-util-is: 1.0.3 + is-class-hotfix: 0.0.6 + isstream: 0.1.2 + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} requiresBuild: true @@ -4202,6 +4320,10 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: false + /istanbul-lib-coverage@3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} @@ -4817,6 +4939,11 @@ packages: koa-compose: 4.1.0 dev: false + /koa-cookies@4.0.2: + resolution: {integrity: sha512-5V4LBrIgBUyVRUL1z7cG2nkQQMIvdJtxZMTC40ztoXKNgKgpttBVQHXB/dgQfODPggmlVZ5/9bii4qCcl0dgjQ==} + engines: {node: '>=18.0.0'} + dev: false + /koa-helmet@7.0.2: resolution: {integrity: sha512-AvzS6VuEfFgbAm0mTUnkk/BpMarMcs5A56g+f0sfrJ6m63wII48d2GDrnUQGp0Nj+RR950vNtgqXm9UJSe7GOg==} engines: {node: '>= 14.0.0'} @@ -4824,6 +4951,18 @@ packages: helmet: 6.2.0 dev: false + /koa-session@6.4.0: + resolution: {integrity: sha512-h/dxmSOvNEXpHQPRs4TV03TZVFyZIjmYQiTAW5JBFTYBOZ0VdpZ8QEE6Dud75g8z9JNGXi3m++VqRmqToB+c2A==} + engines: {node: '>=8.0.0'} + dependencies: + crc: 3.8.0 + debug: 4.3.4 + is-type-of: 1.4.0 + uuid: 8.3.2 + transitivePeerDependencies: + - supports-color + dev: false + /koa2-swagger-ui@5.8.0: resolution: {integrity: sha512-uVHh2hajX5Q/6+BkRrK0Rrf84Duc6dB73WIU1mb3cLfellMR1klAIYEX1p1bOsNjB97uqCwyn/cz3Sfi1/IxcQ==} requiresBuild: true @@ -5365,7 +5504,6 @@ packages: engines: {node: '>= 0.8'} requiresBuild: true dev: false - optional: true /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -5763,7 +5901,6 @@ packages: engines: {node: '>= 0.8'} requiresBuild: true dev: false - optional: true /range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} @@ -6858,7 +6995,6 @@ packages: dependencies: random-bytes: 1.0.0 dev: false - optional: true /undefsafe@2.0.5: resolution: {integrity: sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==} @@ -6901,6 +7037,11 @@ packages: requiresBuild: true dev: false + /uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + dev: false + /uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index ab0b3ec0..84cd5259 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -1,12 +1,12 @@ import {CreateUserDto, UpdateUserDto} from "../dtos/users.dto"; import {Logger} from "winston"; -import {ConfigService} from "@node-boot/config"; import {Service} from "@node-boot/core"; import {User, UserRepository} from "../persistence"; import {UserModel} from "../models/users.model"; import {Optional} from "@node-boot/extension"; import {runOnTransactionCommit, runOnTransactionRollback, Transactional} from "@node-boot/starter-persistence"; import {HttpError, NotFoundError} from "@node-boot/error"; +import {ConfigService} from "@node-boot/config"; @Service() export class UserService { @@ -18,6 +18,7 @@ export class UserService { this.logger.info("Getting all users"); const appName = this.configService.getString("node-boot.app.name"); this.logger.info(`Reading node-boot.app.name from app-config.yam: ${appName}`); + return this.userRepository.find(); } diff --git a/servers/express-server/package.json b/servers/express-server/package.json index 10b72767..e2cc345b 100644 --- a/servers/express-server/package.json +++ b/servers/express-server/package.json @@ -34,8 +34,13 @@ "@node-boot/core": "1.0.0", "@node-boot/error": "1.0.0", "@node-boot/engine": "1.0.0", + "@node-boot/extension": "1.0.0", "body-parser": "^1.20.2", - "multer": "^1.4.5-lts.1" + "multer": "^1.4.5-lts.1", + "cors": "^2.8.5", + "template-url": "^1.0.0", + "cookie": "^0.6.0", + "express-session": "^1.17.3" }, "peerDependencies": { "express": ">=4.18.2" @@ -43,6 +48,9 @@ "devDependencies": { "@types/express": "^4.17.17", "@types/body-parser": "^1.19.2", - "@types/multer": "^1.4.7" + "@types/multer": "^1.4.7", + "@types/cookie": "^0.6.0", + "@types/cors": "^2.8.17", + "@types/express-session": "^1.17.10" } } diff --git a/servers/express-server/src/driver/ExpressDriver.ts b/servers/express-server/src/driver/ExpressDriver.ts index 18623476..11a248c2 100644 --- a/servers/express-server/src/driver/ExpressDriver.ts +++ b/servers/express-server/src/driver/ExpressDriver.ts @@ -11,35 +11,57 @@ import { } from "@node-boot/context"; import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, NotFoundError} from "@node-boot/error"; import {Application, Request, Response} from "express"; -import {MiddlewareInterface} from "@node-boot/context/src"; +import {LoggerService, MiddlewareInterface} from "@node-boot/context/src"; +import cookie, {CookieParseOptions, CookieSerializeOptions} from "cookie"; +import cors, {CorsOptions} from "cors"; +import session, {SessionOptions} from "express-session"; +import {ServerConfig, ServerConfigOptions} from "@node-boot/extension"; +import {Options as MulterOptions} from "multer"; +import {DependenciesLoader} from "../loader"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const cookie = require("cookie"); // eslint-disable-next-line @typescript-eslint/no-var-requires const templateUrl = require("template-url"); +export type ExpressServerConfigs = ServerConfigOptions; + +type ExpressServerOptions = { + logger: LoggerService; + configs?: ExpressServerConfigs; + express?: Application; +}; + +type CookieOptions = { + secret?: string | string[]; + options?: CookieParseOptions; +}; + /** * Integration with express framework. */ export class ExpressDriver extends NodeBootDriver { - constructor(express?: Application) { + private readonly logger: LoggerService; + private readonly configs?: ExpressServerConfigs; + + constructor(serverOptions: ExpressServerOptions) { super(); - this.app = express ?? this.loadExpress(); + this.app = serverOptions.express ?? DependenciesLoader.loadExpress(); + this.logger = serverOptions.logger; + this.configs = serverOptions.configs; } /** * Initializes the things driver needs before routes and middlewares registration. */ initialize() { - if (this.cors) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const cors = require("cors"); - if (this.cors === true) { - this.app.use(cors()); - } else { - this.app.use(cors(this.cors)); - } - } + ServerConfig.of(this.configs) + .ifCors( + options => this.app.use(cors(options)), + () => this.logger.warn(`CORS is not configured`), + ) + .ifSession( + options => this.app.use(session(options)), + () => this.logger.warn(`Session is not configured`), + ); } /** @@ -48,9 +70,6 @@ export class ExpressDriver extends NodeBootDriver { registerMiddleware(middleware: MiddlewareMetadata, options: NodeBootEngineOptions): void { let middlewareWrapper; - // FIXME Improve this code using the the DI container - //middleware.getInstance(); - // if its an error handler then register it with proper signature in express if ((middleware.instance as ErrorHandlerInterface).onError) { middlewareWrapper = (error: any, request: any, response: any, next: (err?: any) => any) => { @@ -95,9 +114,9 @@ export class ExpressDriver extends NodeBootDriver { if (actionMetadata.isBodyUsed) { if (actionMetadata.isJsonTyped) { - defaultMiddlewares.push(this.loadBodyParser().json(actionMetadata.bodyExtraOptions)); + defaultMiddlewares.push(DependenciesLoader.loadBodyParser().json(actionMetadata.bodyExtraOptions)); } else { - defaultMiddlewares.push(this.loadBodyParser().text(actionMetadata.bodyExtraOptions)); + defaultMiddlewares.push(DependenciesLoader.loadBodyParser().text(actionMetadata.bodyExtraOptions)); } } @@ -133,7 +152,7 @@ export class ExpressDriver extends NodeBootDriver { } if (actionMetadata.isFileUsed || actionMetadata.isFilesUsed) { - const multer = this.loadMulter(); + const multer = DependenciesLoader.loadMulter(); actionMetadata.params .filter(param => param.type === "file") .forEach(param => { @@ -230,12 +249,11 @@ export class ExpressDriver extends NodeBootDriver { return request.files; case "cookie": - if (!request.headers.cookie) return; - return cookie.parse(request.headers.cookie)[param.name]; - + if (request.headers.cookie) return; + return cookie.parse(request.headers.cookie, this.configs?.cookie?.options)[param.name]; case "cookies": - if (!request.headers.cookie) return {}; - return cookie.parse(request.headers.cookie); + if (!request.headers.cookie) return; + return cookie.parse(request.headers.cookie, this.configs?.cookie?.options); } } @@ -415,42 +433,4 @@ export class ExpressDriver extends NodeBootDriver { }); return middlewareFunctions; } - - /** - * Dynamically loads express module. - */ - protected loadExpress(): Application { - if (require) { - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - return require("express")(); - } catch (e) { - throw new Error("express package was not found installed. Try to install it: npm install express --save"); - } - } else { - throw new Error("Cannot load express. Try to install all required dependencies."); - } - } - - /** - * Dynamically loads body-parser module. - */ - protected loadBodyParser() { - try { - return require("body-parser"); - } catch (e) { - throw new Error("body-parser package was not found installed. Try to install it: npm install body-parser --save"); - } - } - - /** - * Dynamically loads multer module. - */ - protected loadMulter() { - try { - return require("multer"); - } catch (e) { - throw new Error("multer package was not found installed. Try to install it: npm install multer --save"); - } - } } diff --git a/servers/express-server/src/loader/DependenciesLoader.ts b/servers/express-server/src/loader/DependenciesLoader.ts new file mode 100644 index 00000000..cf001b74 --- /dev/null +++ b/servers/express-server/src/loader/DependenciesLoader.ts @@ -0,0 +1,41 @@ +import {Application} from "express"; + +export class DependenciesLoader { + /** + * Dynamically loads express module. + */ + static loadExpress(): Application { + if (require) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require("express")(); + } catch (e) { + throw new Error("express package was not found installed. Try to install it: npm install express --save"); + } + } else { + throw new Error("Cannot load express. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads body-parser module. + */ + static loadBodyParser() { + try { + return require("body-parser"); + } catch (e) { + throw new Error("body-parser package was not found installed. Try to install it: npm install body-parser --save"); + } + } + + /** + * Dynamically loads multer module. + */ + static loadMulter() { + try { + return require("multer"); + } catch (e) { + throw new Error("multer package was not found installed. Try to install it: npm install multer --save"); + } + } +} diff --git a/servers/express-server/src/loader/index.ts b/servers/express-server/src/loader/index.ts new file mode 100644 index 00000000..89b91d13 --- /dev/null +++ b/servers/express-server/src/loader/index.ts @@ -0,0 +1 @@ +export {DependenciesLoader} from "./DependenciesLoader"; diff --git a/servers/express-server/src/server/ExpressServer.ts b/servers/express-server/src/server/ExpressServer.ts index c7ec4d38..159a7649 100644 --- a/servers/express-server/src/server/ExpressServer.ts +++ b/servers/express-server/src/server/ExpressServer.ts @@ -3,6 +3,7 @@ import express from "express"; import {BaseServer} from "@node-boot/core"; import {ExpressDriver} from "../driver"; import {NodeBootToolkit} from "@node-boot/engine"; +import {ExpressServerConfigs} from "../driver/ExpressDriver"; export class ExpressServer extends BaseServer { public framework: express.Application; @@ -21,10 +22,16 @@ export class ExpressServer extends BaseServer; + +type FastifyServerOptions = { + logger: LoggerService; + configs?: FastifyServerConfigs; + fastify?: FastifyInstance; }; export class FastifyDriver extends NodeBootDriver> { - constructor(private readonly serverOptions: ServerOptions, fastify?: FastifyInstance) { + private readonly logger: LoggerService; + private readonly configs?: FastifyServerConfigs; + + constructor(options: FastifyServerOptions) { super(); - this.app = fastify ?? this.loadFastify(); + this.logger = options.logger; + this.configs = options.configs; + this.app = options.fastify ?? this.loadFastify(); } initialize() { - if (this.serverOptions.cookieOptions) { - const fastifyCookie = DependenciesLoader.loadCookie(); - this.app.register(fastifyCookie, this.serverOptions.cookieOptions); - } - - if (this.serverOptions.corsOptions) { - const fastifyCors = DependenciesLoader.loadCors(); - this.app.register(fastifyCors, this.serverOptions.corsOptions); - } - - if (this.serverOptions.sessionOptions) { - const fastifySession = DependenciesLoader.loadSession(); - this.app.register(fastifySession, this.serverOptions.sessionOptions); - } - - if (this.serverOptions.multipartOptions) { - const fastifyMultipart = DependenciesLoader.loadMultipart(); - this.app.register(fastifyMultipart, this.serverOptions.multipartOptions); - } - - if (this.serverOptions.templateOptions) { - const fastifyView = DependenciesLoader.loadView(); - this.app.register(fastifyView, this.serverOptions.templateOptions); - } + ServerConfig.of(this.configs) + .ifCookies( + options => { + const fastifyCookie = DependenciesLoader.loadCookie(); + this.app.register(fastifyCookie, options); + }, + () => this.logger.warn(`Cookies is not configured`), + ) + .ifCors( + options => { + const fastifyCors = DependenciesLoader.loadCors(); + this.app.register(fastifyCors, options); + }, + () => this.logger.warn(`CORS is not configured`), + ) + .ifSession( + options => { + const fastifySession = DependenciesLoader.loadSession(); + this.app.register(fastifySession, options); + }, + () => this.logger.warn(`Session is not configured`), + ) + .ifTemplate( + options => { + const fastifyView = DependenciesLoader.loadView(); + this.app.register(fastifyView, options); + }, + () => this.logger.warn(`Session is not configured`), + ) + .ifMultipart( + options => { + const fastifyMultipart = DependenciesLoader.loadMultipart(); + this.app.register(fastifyMultipart, options); + }, + () => this.logger.warn(`Multipart is not configured`), + ); } /** @@ -293,7 +318,11 @@ export class FastifyDriver extends NodeBootDriver use.isErrorMiddleware()).forEach((use: UseMetadata) => { // if this is function instance of ErrorMiddlewareInterface middlewareFunctions.push((request: FastifyRequest, reply: FastifyReply, error: FastifyError, done: () => void) => { - return getFromContainer(use.middleware).onError(error, {request, response: reply, next: done}); + return getFromContainer(use.middleware).onError(error, { + request, + response: reply, + next: done, + }); }); }); return middlewareFunctions; diff --git a/servers/fastify-server/src/server/FastifyServer.ts b/servers/fastify-server/src/server/FastifyServer.ts index 3960bc09..347b400f 100644 --- a/servers/fastify-server/src/server/FastifyServer.ts +++ b/servers/fastify-server/src/server/FastifyServer.ts @@ -3,6 +3,7 @@ import {BaseServer} from "@node-boot/core"; import Fastify, {FastifyInstance} from "fastify"; import {FastifyDriver} from "../driver"; import {NodeBootToolkit} from "@node-boot/engine"; +import {FastifyServerConfigs} from "../driver/FastifyDriver"; export class FastifyServer extends BaseServer { private readonly framework: FastifyInstance; @@ -20,28 +21,42 @@ export class FastifyServer extends BaseServer // Bind application container through adapter if (context.applicationAdapter) { - const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); - - const driver = new FastifyDriver( - { - cookieOptions: { + const engineOptions = context.applicationAdapter.bind(context.diOptions?.iocContainer); + const serverConfigs: FastifyServerConfigs = { + cookie: { + enabled: true, + options: { secret: "my-secret", // for cookies signature hook: "onRequest", // set to false to disable cookie autoparsing or set autoparsing on any of the following hooks: 'onRequest', 'preParsing', 'preHandler', 'preValidation'. default: 'onRequest' parseOptions: {}, // options for parsing cookies }, - sessionOptions: { + }, + session: { + enabled: false, + options: { secret: "a secret with minimum length of 32 characters", }, - templateOptions: { + }, + template: { + enabled: false, + options: { engine: { handlebars: require("handlebars"), }, }, - multipartOptions: {}, }, - this.framework, - ); - NodeBootToolkit.createServer(driver, configs); + multipart: { + enabled: false, + }, + }; + + const driver = new FastifyDriver({ + configs: serverConfigs, + fastify: this.framework, + logger: this.logger, + }); + + NodeBootToolkit.createServer(driver, engineOptions); } else { throw new Error("Error stating Application. Please enable NodeBoot application using @NodeBootApplication"); } @@ -55,12 +70,12 @@ export class FastifyServer extends BaseServer this.framework.listen({port: context.applicationOptions.port}, (err: Error | null, address: string) => { if (err) { this.logger.error(err); - process.exit(1); + } else { + this.logger.info(`=================================`); + this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); + this.logger.info(`🚀 App listening on ${address}`); + this.logger.info(`=================================`); } - this.logger.info(`=================================`); - this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); - this.logger.info(`🚀 App listening on ${address}`); - this.logger.info(`=================================`); }); } diff --git a/servers/koa-server/package.json b/servers/koa-server/package.json index 574fcf9b..61f516ab 100644 --- a/servers/koa-server/package.json +++ b/servers/koa-server/package.json @@ -32,17 +32,24 @@ "@node-boot/context": "1.0.0", "@node-boot/core": "1.0.0", "@node-boot/engine": "1.0.0", - "@node-boot/error": "1.0.0" + "@node-boot/error": "1.0.0", + "@node-boot/extension": "1.0.0", + "template-url": "^1.0.0" }, "peerDependencies": { - "routing-controllers": ">=0.10.4", - "koa": ">=2.14.2", + "koa": "^2.14.2", "@koa/router": "^12.0.0", "koa-bodyparser": "^4.4.1", - "@koa/multer": "^3.0.2" + "@koa/multer": "^3.0.2", + "@koa/cors": "^5.0.0", + "koa-session": "^6.4.0", + "koa-cookies": "^4.0.2" }, "devDependencies": { - "@types/koa": "^2.13.8", - "@types/koa-router": "^7.4.8" + "@types/koa": "^2.13.12", + "@types/koa-session": "^6.4.5", + "@types/koa__multer": "^2.0.7", + "@types/koa__cors": "^5.0.0", + "@types/koa__router": "^12.0.4" } } diff --git a/servers/koa-server/src/driver/KoaDriver.ts b/servers/koa-server/src/driver/KoaDriver.ts index 254693fa..3d90c8cc 100644 --- a/servers/koa-server/src/driver/KoaDriver.ts +++ b/servers/koa-server/src/driver/KoaDriver.ts @@ -1,23 +1,41 @@ import {isPromiseLike, NodeBootDriver} from "@node-boot/engine"; -import {Action, ActionMetadata, getFromContainer, MiddlewareMetadata, ParamMetadata, RoleChecker, UseMetadata} from "@node-boot/context"; +import {Action, ActionMetadata, getFromContainer, MiddlewareMetadata, ParamMetadata, UseMetadata} from "@node-boot/context"; import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, HttpError, NotFoundError} from "@node-boot/error"; import Koa from "koa"; import Router from "@koa/router"; -import {MiddlewareInterface} from "@node-boot/context/src"; +import {LoggerService, MiddlewareInterface} from "@node-boot/context/src"; +import {ServerConfig, ServerConfigOptions} from "@node-boot/extension"; +import {DependenciesLoader} from "../loader"; +import session, {opts as SessionOptions} from "koa-session"; +import {parseCookie} from "koa-cookies"; +import cors, {Options as CorsOptions} from "@koa/cors"; -// eslint-disable-next-line @typescript-eslint/no-var-requires -const cookie = require("cookie"); // eslint-disable-next-line @typescript-eslint/no-var-requires const templateUrl = require("template-url"); +export type KoaServerConfigs = ServerConfigOptions; + +type KoaServerOptions = { + logger: LoggerService; + configs?: KoaServerConfigs; + koa?: Koa; + router?: Router; +}; + /** * Integration with koa framework. */ export class KoaDriver extends NodeBootDriver { - constructor(private readonly koa?: Koa, private readonly router?: Router) { + private readonly logger: LoggerService; + private readonly router: Router; + private readonly configs?: KoaServerConfigs; + + constructor(serverOptions: KoaServerOptions) { super(); - this.app = koa ?? this.loadKoa(); - this.router = router ?? this.loadRouter(); + this.logger = serverOptions.logger; + this.configs = serverOptions.configs; + this.app = serverOptions.koa ?? DependenciesLoader.loadKoa(); + this.router = serverOptions.router ?? DependenciesLoader.loadRouter(); } /** @@ -27,15 +45,26 @@ export class KoaDriver extends NodeBootDriver { // eslint-disable-next-line @typescript-eslint/no-var-requires const bodyParser = require("koa-bodyparser"); this.app.use(bodyParser()); - if (this.cors) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const cors = require("@koa/cors"); - if (this.cors === true) { - this.app.use(cors()); - } else { - this.app.use(cors(this.cors)); - } - } + + ServerConfig.of(this.configs) + .ifCors( + options => this.app.use(cors(options)), + () => this.logger.warn(`CORS is not configured`), + ) + .ifCookies( + options => this.app.use(parseCookie()), // always add all cookies to ctx.cookies + () => this.logger.warn(`Cookies is not configured`), + ) + .ifSession( + options => { + if (options) { + this.app.use(session(options, this.app)); + } else { + this.app.use(session(this.app)); + } + }, + () => this.logger.warn(`Session is not configured`), + ); } /** @@ -67,10 +96,7 @@ export class KoaDriver extends NodeBootDriver { const action: Action = {request: context.request, response: context.response, context, next}; try { - const checkResult = - actionMetadata.authorizedRoles instanceof Function - ? getFromContainer(actionMetadata.authorizedRoles, action).check(action) - : this.authorizationChecker.check(action, actionMetadata.authorizedRoles); + const checkResult = this.authorizationChecker.check(action, actionMetadata.authorizedRoles); const handleError = (result: any) => { if (!result) { @@ -96,7 +122,7 @@ export class KoaDriver extends NodeBootDriver { } if (actionMetadata.isFileUsed || actionMetadata.isFilesUsed) { - const multer = this.loadMulter(); + const multer = DependenciesLoader.loadMulter(); actionMetadata.params .filter(param => param.type === "file") .forEach(param => { @@ -193,10 +219,10 @@ export class KoaDriver extends NodeBootDriver { return context.query; case "file": - return actionOptions.context.request.file; + return context.request.file; case "files": - return actionOptions.context.request.files; + return context.request.files; case "header": return context.headers[param.name.toLowerCase()]; @@ -205,12 +231,12 @@ export class KoaDriver extends NodeBootDriver { return request.headers; case "cookie": - if (!context.headers.cookie) return; - return cookie.parse(context.headers.cookie)[param.name]; + if (!context.cookies) return; + return context.cookies[param.name]; case "cookies": - if (!request.headers.cookie) return {}; - return cookie.parse(request.headers.cookie); + if (!context.cookies) return {}; + return context.cookies; } } @@ -347,45 +373,4 @@ export class KoaDriver extends NodeBootDriver { }); return middlewareFunctions; } - - /** - * Dynamically loads koa module. - */ - protected loadKoa(): Koa { - if (require) { - try { - return new (require("koa"))(); - } catch (e) { - throw new Error("koa package was not found installed. Try to install it: npm install koa@next --save"); - } - } else { - throw new Error("Cannot load koa. Try to install all required dependencies."); - } - } - - /** - * Dynamically loads @koa/router module. - */ - private loadRouter(): Router { - if (require) { - try { - return new (require("@koa/router"))(); - } catch (e) { - throw new Error("@koa/router package was not found installed. Try to install it: npm install @koa/router --save"); - } - } else { - throw new Error("Cannot load koa. Try to install all required dependencies."); - } - } - - /** - * Dynamically loads @koa/multer module. - */ - private loadMulter() { - try { - return require("@koa/multer"); - } catch (e) { - throw new Error("@koa/multer package was not found installed. Try to install it: npm install @koa/multer --save"); - } - } } diff --git a/servers/koa-server/src/loader/DependenciesLoader.ts b/servers/koa-server/src/loader/DependenciesLoader.ts new file mode 100644 index 00000000..0b32849b --- /dev/null +++ b/servers/koa-server/src/loader/DependenciesLoader.ts @@ -0,0 +1,73 @@ +import Koa from "koa"; +import Router from "@koa/router"; + +export class DependenciesLoader { + /** + * Dynamically loads koa module. + */ + static loadKoa(): Koa { + if (require) { + try { + return new (require("koa"))(); + } catch (e) { + throw new Error("koa package was not found installed. Try to install it: npm install koa@next --save"); + } + } else { + throw new Error("Cannot load koa. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads @koa/router module. + */ + static loadRouter(): Router { + if (require) { + try { + return new (require("@koa/router"))(); + } catch (e) { + throw new Error("@koa/router package was not found installed. Try to install it: npm install @koa/router --save"); + } + } else { + throw new Error("Cannot load koa. Try to install all required dependencies."); + } + } + + /** + * Dynamically loads multer and @koa/multer module. + */ + static loadMulter() { + try { + require("multer"); + } catch (e) { + throw new Error("multer package was not found installed. Try to install it: npm install multer --save"); + } + + try { + return require("@koa/multer"); + } catch (e) { + throw new Error("@koa/multer package was not found installed. Try to install it: npm install @koa/multer --save"); + } + } + + /** + * Dynamically loads @koa/cors module. + */ + static loadCors() { + try { + return require("@koa/cors"); + } catch (e) { + throw new Error("@koa/cors package was not found installed. Try to install it: npm install @koa/cors --save"); + } + } + + /** + * Dynamically loads koa-session module. + */ + static loadSession() { + try { + return require("koa-session"); + } catch (e) { + throw new Error("koa-session package was not found installed. Try to install it: npm install koa-session --save"); + } + } +} diff --git a/servers/koa-server/src/loader/index.ts b/servers/koa-server/src/loader/index.ts new file mode 100644 index 00000000..89b91d13 --- /dev/null +++ b/servers/koa-server/src/loader/index.ts @@ -0,0 +1 @@ +export {DependenciesLoader} from "./DependenciesLoader"; diff --git a/servers/koa-server/src/server/KoaServer.ts b/servers/koa-server/src/server/KoaServer.ts index eccab259..79f2adcb 100644 --- a/servers/koa-server/src/server/KoaServer.ts +++ b/servers/koa-server/src/server/KoaServer.ts @@ -4,6 +4,7 @@ import Router from "@koa/router"; import {BaseServer} from "@node-boot/core"; import {NodeBootToolkit} from "@node-boot/engine"; import {KoaDriver} from "../driver"; +import {KoaServerConfigs} from "../driver/KoaDriver"; export class KoaServer extends BaseServer { private readonly framework: Koa; @@ -22,10 +23,17 @@ export class KoaServer extends BaseServer { // Bind application container through adapter if (context.applicationAdapter) { - const configs = context.applicationAdapter.bind(context.diOptions?.iocContainer); + const engineOptions = context.applicationAdapter.bind(context.diOptions?.iocContainer); - const driver = new KoaDriver(this.framework, this.router); - NodeBootToolkit.createServer(driver, configs); + const serverConfigs: KoaServerConfigs = {}; + + const driver = new KoaDriver({ + configs: serverConfigs, + koa: this.framework, + logger: this.logger, + router: this.router, + }); + NodeBootToolkit.createServer(driver, engineOptions); } else { throw new Error("Error stating Application. Please enable NodeBoot application using @NodeBootApplication"); } From 4aa3107481fdf49f6828ec2103c8a9dd92dba01e Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 02:39:16 +0000 Subject: [PATCH 11/16] several improvements --- .prettierrc.yaml | 2 +- samples/sample-express/src/services/users.service.ts | 6 +++++- samples/sample-fastify/src/services/users.service.ts | 6 +++++- samples/sample-koa/src/services/users.service.ts | 6 +++++- 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 8e69d835..0c6d956c 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -4,4 +4,4 @@ singleQuote: false trailingComma: "all" arrowParens: "avoid" bracketSpacing: false -printWidth: 150 +printWidth: 120 diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index 84cd5259..a41f690e 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -10,7 +10,11 @@ import {ConfigService} from "@node-boot/config"; @Service() export class UserService { - constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { + constructor( + private readonly logger: Logger, + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + ) { UserModel.forEach(user => this.userRepository.save(user)); } diff --git a/samples/sample-fastify/src/services/users.service.ts b/samples/sample-fastify/src/services/users.service.ts index ab0b3ec0..bc546b63 100644 --- a/samples/sample-fastify/src/services/users.service.ts +++ b/samples/sample-fastify/src/services/users.service.ts @@ -10,7 +10,11 @@ import {HttpError, NotFoundError} from "@node-boot/error"; @Service() export class UserService { - constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { + constructor( + private readonly logger: Logger, + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + ) { UserModel.forEach(user => this.userRepository.save(user)); } diff --git a/samples/sample-koa/src/services/users.service.ts b/samples/sample-koa/src/services/users.service.ts index ab0b3ec0..bc546b63 100644 --- a/samples/sample-koa/src/services/users.service.ts +++ b/samples/sample-koa/src/services/users.service.ts @@ -10,7 +10,11 @@ import {HttpError, NotFoundError} from "@node-boot/error"; @Service() export class UserService { - constructor(private readonly logger: Logger, private readonly configService: ConfigService, private readonly userRepository: UserRepository) { + constructor( + private readonly logger: Logger, + private readonly configService: ConfigService, + private readonly userRepository: UserRepository, + ) { UserModel.forEach(user => this.userRepository.save(user)); } From 0517b67f1cb551d4df6b6daf35b7a14dbf6bcac6 Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 02:41:22 +0000 Subject: [PATCH 12/16] format --- .../src/service/ObservableConfigProxy.test.ts | 12 ++- packages/config/src/service/config.ts | 4 +- packages/context/src/handlers.ts | 7 +- .../context/src/metadata/ActionMetadata.ts | 23 +++-- packages/core/src/decorators/UseAfter.ts | 4 +- packages/core/src/decorators/UseBefore.ts | 4 +- packages/core/src/server/BaseServer.ts | 6 +- .../di/src/ioc/makeInjectionDecoration.ts | 21 ++++- packages/engine/src/core/NodeBootDriver.ts | 8 +- packages/engine/src/core/NodeBootEngine.ts | 13 ++- packages/engine/src/core/NodeBootToolkit.ts | 15 +++- .../src/metadata/MetadataArgsStorage.ts | 3 +- .../engine/src/metadata/MetadataBuilder.ts | 12 ++- .../src/service/ActionParameterHandler.ts | 33 +++++-- .../engine/src/service/ComponentImporter.ts | 4 +- .../AuthorizationCheckerNotDefinedError.ts | 4 +- .../CurrentUserCheckerNotDefinedError.ts | 4 +- .../src/error/ParamNormalizationError.ts | 6 +- .../src/error/ParameterParseJsonError.ts | 4 +- packages/extension/src/Param.ts | 5 +- .../openapi/src/decorator/EnableOpenApi.ts | 3 +- .../src/middlewares/validation.middleware.ts | 11 ++- .../src/middlewares/validation.middleware.ts | 11 ++- .../src/driver/ExpressDriver.ts | 18 +++- .../src/loader/DependenciesLoader.ts | 8 +- .../src/driver/FastifyDriver.ts | 88 ++++++++++++++----- .../src/loader/DependenciesLoader.ts | 20 +++-- servers/koa-server/src/driver/KoaDriver.ts | 21 ++++- .../src/loader/DependenciesLoader.ts | 16 +++- .../src/adapter/DefaultActuatorAdapter.ts | 22 ++++- .../src/adapter/ExpressActuatorAdapter.ts | 4 +- .../src/adapter/KoaActuatorAdapter.ts | 4 +- starters/actuator/src/service/InfoService.ts | 4 +- .../actuator/src/service/MetadataService.ts | 4 +- .../src/config/DataSourceConfiguration.ts | 4 +- .../src/config/PersistenceConfiguration.ts | 12 ++- .../src/config/QueryCacheConfiguration.ts | 16 +++- starters/persistence/src/hook/hooks.ts | 5 +- .../property/SqlServerConnectionProperties.ts | 7 +- 39 files changed, 371 insertions(+), 99 deletions(-) diff --git a/packages/config/src/service/ObservableConfigProxy.test.ts b/packages/config/src/service/ObservableConfigProxy.test.ts index 9b0d30d4..e0bb5f43 100644 --- a/packages/config/src/service/ObservableConfigProxy.test.ts +++ b/packages/config/src/service/ObservableConfigProxy.test.ts @@ -77,9 +77,15 @@ describe("ObservableConfigProxy", () => { expect(() => config3.getNumber("x")).toThrow("Missing required config value at 'a'"); config1.setConfig(new ConfigReader({x: "s", a: {x: "s", b: {x: "s"}}})); - expect(() => config1.getNumber("x")).toThrow("Unable to convert config value for key 'x' in 'mock-config' to a number"); - expect(() => config2.getNumber("x")).toThrow("Unable to convert config value for key 'a.x' in 'mock-config' to a number"); - expect(() => config3.getNumber("x")).toThrow("Unable to convert config value for key 'a.b.x' in 'mock-config' to a number"); + expect(() => config1.getNumber("x")).toThrow( + "Unable to convert config value for key 'x' in 'mock-config' to a number", + ); + expect(() => config2.getNumber("x")).toThrow( + "Unable to convert config value for key 'a.x' in 'mock-config' to a number", + ); + expect(() => config3.getNumber("x")).toThrow( + "Unable to convert config value for key 'a.b.x' in 'mock-config' to a number", + ); }); it("should make sub configs available as expected", () => { diff --git a/packages/config/src/service/config.ts b/packages/config/src/service/config.ts index 46b2b6dc..57f4d400 100644 --- a/packages/config/src/service/config.ts +++ b/packages/config/src/service/config.ts @@ -30,7 +30,9 @@ export async function loadNodeBootConfig(options: { }): Promise<{config: ConfigService}> { const args = parseArgs(options.argv); - const configTargets: ConfigTarget[] = [args["config"] ?? []].flat().map(arg => (isValidUrl(arg) ? {url: arg} : {path: resolvePath(arg)})); + const configTargets: ConfigTarget[] = [args["config"] ?? []] + .flat() + .map(arg => (isValidUrl(arg) ? {url: arg} : {path: resolvePath(arg)})); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); diff --git a/packages/context/src/handlers.ts b/packages/context/src/handlers.ts index 39d4feac..c868a12c 100644 --- a/packages/context/src/handlers.ts +++ b/packages/context/src/handlers.ts @@ -22,6 +22,11 @@ export interface MiddlewareInterface { +export interface ErrorHandlerInterface< + TError extends Error = Error, + TRequest = any, + TResponse = any, + TNext = Function, +> { onError(error: TError, action: Action, metadata?: ActionMetadata): any; } diff --git a/packages/context/src/metadata/ActionMetadata.ts b/packages/context/src/metadata/ActionMetadata.ts index 659dddbb..7712ce1e 100644 --- a/packages/context/src/metadata/ActionMetadata.ts +++ b/packages/context/src/metadata/ActionMetadata.ts @@ -144,7 +144,11 @@ export class ActionMetadata { */ methodOverride?: (actionMetadata: ActionMetadata, action: Action, params: any[]) => Promise | any; - constructor(controllerMetadata: ControllerMetadata, args: ActionMetadataArgs, private globalOptions: NodeBootEngineOptions) { + constructor( + controllerMetadata: ControllerMetadata, + args: ActionMetadataArgs, + private globalOptions: NodeBootEngineOptions, + ) { this.controllerMetadata = controllerMetadata; this.route = args.route; this.target = args.target; @@ -174,7 +178,9 @@ export class ActionMetadata { * Action metadata can be used only after its build. */ build(responseHandlers: ResponseHandlerMetadata[]) { - const classTransformerResponseHandler = responseHandlers.find(handler => handler.type === "response-class-transform-options"); + const classTransformerResponseHandler = responseHandlers.find( + handler => handler.type === "response-class-transform-options", + ); const undefinedResultHandler = responseHandlers.find(handler => handler.type === "on-undefined"); const nullResultHandler = responseHandlers.find(handler => handler.type === "on-null"); const successCodeHandler = responseHandlers.find(handler => handler.type === "success-code"); @@ -190,7 +196,9 @@ export class ActionMetadata { ? undefinedResultHandler.value : this.globalOptions.defaults && this.globalOptions.defaults.undefinedResultCode; - this.nullResultCode = nullResultHandler ? nullResultHandler.value : this.globalOptions.defaults && this.globalOptions.defaults.nullResultCode; + this.nullResultCode = nullResultHandler + ? nullResultHandler.value + : this.globalOptions.defaults && this.globalOptions.defaults.nullResultCode; if (successCodeHandler) this.successHttpCode = successCodeHandler.value; if (redirectHandler) this.redirect = redirectHandler.value; @@ -200,12 +208,17 @@ export class ActionMetadata { this.isBodyUsed = !!this.params.find(param => param.type === "body" || param.type === "body-param"); this.isFilesUsed = !!this.params.find(param => param.type === "files"); this.isFileUsed = !!this.params.find(param => param.type === "file"); - this.isJsonTyped = contentTypeHandler !== undefined ? /json/.test(contentTypeHandler.value) : this.controllerMetadata.type === "json"; + this.isJsonTyped = + contentTypeHandler !== undefined + ? /json/.test(contentTypeHandler.value) + : this.controllerMetadata.type === "json"; this.fullRoute = this.buildFullRoute(); this.headers = this.buildHeaders(responseHandlers); this.isAuthorizedUsed = this.controllerMetadata.isAuthorizedUsed || !!authorizedHandler; - this.authorizedRoles = (this.controllerMetadata.authorizedRoles || []).concat((authorizedHandler && authorizedHandler.value) || []); + this.authorizedRoles = (this.controllerMetadata.authorizedRoles || []).concat( + (authorizedHandler && authorizedHandler.value) || [], + ); } /** diff --git a/packages/core/src/decorators/UseAfter.ts b/packages/core/src/decorators/UseAfter.ts index 14630de2..1351e27e 100644 --- a/packages/core/src/decorators/UseAfter.ts +++ b/packages/core/src/decorators/UseAfter.ts @@ -22,7 +22,9 @@ export function UseAfter(...middlewares: Array<(request: any, response: any, nex * Specifies a given middleware to be used for controller or controller action AFTER the action executes. * Must be set to controller action or controller class. */ -export function UseAfter(...middlewares: Array any)>): Function { +export function UseAfter( + ...middlewares: Array any)> +): Function { return function (objectOrFunction: Object | Function, methodName?: string) { middlewares.forEach(middleware => { NodeBootToolkit.getMetadataArgsStorage().uses.push({ diff --git a/packages/core/src/decorators/UseBefore.ts b/packages/core/src/decorators/UseBefore.ts index a95c4141..38e24299 100644 --- a/packages/core/src/decorators/UseBefore.ts +++ b/packages/core/src/decorators/UseBefore.ts @@ -22,7 +22,9 @@ export function UseBefore(...middlewares: Array<(request: any, response: any, ne * Specifies a given middleware to be used for controller or controller action BEFORE the action executes. * Must be set to controller action or controller class. */ -export function UseBefore(...middlewares: Array any)>): Function { +export function UseBefore( + ...middlewares: Array any)> +): Function { return function (objectOrFunction: Object | Function, methodName?: string) { middlewares.forEach(middleware => { NodeBootToolkit.getMetadataArgsStorage().uses.push({ diff --git a/packages/core/src/server/BaseServer.ts b/packages/core/src/server/BaseServer.ts index 539d8266..cc7908fe 100644 --- a/packages/core/src/server/BaseServer.ts +++ b/packages/core/src/server/BaseServer.ts @@ -87,8 +87,10 @@ export abstract class BaseServer { port: context.applicationOptions?.port ?? appConfigs?.port ?? 3000, platform: context.applicationOptions?.platform ?? appConfigs?.platform ?? "node-boot", name: context.applicationOptions?.name ?? appConfigs?.name ?? "node-boot-app", - defaultErrorHandler: context.applicationOptions?.defaultErrorHandler ?? appConfigs?.defaultErrorHandler ?? false, - customErrorHandler: context.applicationOptions?.customErrorHandler ?? appConfigs?.customErrorHandler ?? false, + defaultErrorHandler: + context.applicationOptions?.defaultErrorHandler ?? appConfigs?.defaultErrorHandler ?? false, + customErrorHandler: + context.applicationOptions?.customErrorHandler ?? appConfigs?.customErrorHandler ?? false, apiOptions: context.applicationOptions.apiOptions ?? apiConfigs, }; } diff --git a/packages/di/src/ioc/makeInjectionDecoration.ts b/packages/di/src/ioc/makeInjectionDecoration.ts index 497920e8..c3d1b76d 100644 --- a/packages/di/src/ioc/makeInjectionDecoration.ts +++ b/packages/di/src/ioc/makeInjectionDecoration.ts @@ -3,14 +3,24 @@ import {InjectionOptions} from "./types"; /** * Apply proper @Inject decorator if dependency injection framework is available * */ -export function decorateInjection(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { +export function decorateInjection( + target: Object, + propertyName: string | Symbol, + index?: number, + options?: InjectionOptions, +): boolean { return decorateTypeDi(target, propertyName, index, options) || decorateInversify(target, propertyName, index); } /** * Apply @Inject decorator if TypeDI framework is available * */ -function decorateTypeDi(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { +function decorateTypeDi( + target: Object, + propertyName: string | Symbol, + index?: number, + options?: InjectionOptions, +): boolean { let decorated: boolean; try { const {Inject} = require("typedi"); @@ -31,7 +41,12 @@ function decorateTypeDi(target: Object, propertyName: string | Symbol, index?: n /** * Apply @inject decorator if Inversify framework is available * */ -function decorateInversify(target: Object, propertyName: string | Symbol, index?: number, options?: InjectionOptions): boolean { +function decorateInversify( + target: Object, + propertyName: string | Symbol, + index?: number, + options?: InjectionOptions, +): boolean { let decorated: boolean; try { const {inject} = require("inversify"); diff --git a/packages/engine/src/core/NodeBootDriver.ts b/packages/engine/src/core/NodeBootDriver.ts index 30f9fdf1..865eea4d 100644 --- a/packages/engine/src/core/NodeBootDriver.ts +++ b/packages/engine/src/core/NodeBootDriver.ts @@ -112,7 +112,13 @@ export abstract class NodeBootDriver { if (error.stack && this.developmentMode) processedError.stack = error.stack; Object.keys(error) - .filter(key => key !== "stack" && key !== "name" && key !== "message" && (!(error instanceof HttpError) || key !== "httpCode")) + .filter( + key => + key !== "stack" && + key !== "name" && + key !== "message" && + (!(error instanceof HttpError) || key !== "httpCode"), + ) .forEach(key => (processedError[key] = (error as any)[key])); if (this.errorOverridingMap) diff --git a/packages/engine/src/core/NodeBootEngine.ts b/packages/engine/src/core/NodeBootEngine.ts index f5f4e428..8f837535 100644 --- a/packages/engine/src/core/NodeBootEngine.ts +++ b/packages/engine/src/core/NodeBootEngine.ts @@ -1,5 +1,12 @@ import {MetadataBuilder} from "../metadata/MetadataBuilder"; -import {Action, ActionMetadata, getFromContainer, InterceptorInterface, InterceptorMetadata, NodeBootEngineOptions} from "@node-boot/context"; +import { + Action, + ActionMetadata, + getFromContainer, + InterceptorInterface, + InterceptorMetadata, + NodeBootEngineOptions, +} from "@node-boot/context"; import {isPromiseLike, runInSequence} from "../util"; import {NodeBootDriver} from "./NodeBootDriver"; import {ActionParameterHandler} from "../service/ActionParameterHandler"; @@ -95,7 +102,9 @@ export class NodeBootEngine> { return Promise.all(paramsPromises) .then(params => { // execute action and handle result - const allParams = actionMetadata.appendParams ? actionMetadata.appendParams(action).concat(params) : params; + const allParams = actionMetadata.appendParams + ? actionMetadata.appendParams(action).concat(params) + : params; const result = actionMetadata.methodOverride ? actionMetadata.methodOverride(actionMetadata, action, allParams) : actionMetadata.callMethod(allParams, action); diff --git a/packages/engine/src/core/NodeBootToolkit.ts b/packages/engine/src/core/NodeBootToolkit.ts index 793bdfec..8af5dcf3 100644 --- a/packages/engine/src/core/NodeBootToolkit.ts +++ b/packages/engine/src/core/NodeBootToolkit.ts @@ -18,7 +18,10 @@ export class NodeBootToolkit { /** * Registers all loaded actions in your application using selected driver. */ - static createServer>(driver: TDriver, options?: NodeBootEngineOptions): any { + static createServer>( + driver: TDriver, + options?: NodeBootEngineOptions, + ): any { NodeBootToolkit.createEngine(driver, options); return driver.app; } @@ -26,7 +29,10 @@ export class NodeBootToolkit { /** * Registers all loaded actions in your express application. */ - static createEngine>(driver: TDriver, options: NodeBootEngineOptions = {}): void { + static createEngine>( + driver: TDriver, + options: NodeBootEngineOptions = {}, + ): void { // import all controllers, middlewares and error handlers const controllerClasses = ComponentImporter.importControllers(options); const middlewareClasses = ComponentImporter.importMiddlewares(options); @@ -43,7 +49,10 @@ export class NodeBootToolkit { .registerMiddlewares("after", middlewareClasses); // todo: register only for loaded controllers? } - private static configureDriver>(driver: TDriver, options: NodeBootEngineOptions) { + private static configureDriver>( + driver: TDriver, + options: NodeBootEngineOptions, + ) { if (options && options.development !== undefined) { driver.developmentMode = options.development; } else { diff --git a/packages/engine/src/metadata/MetadataArgsStorage.ts b/packages/engine/src/metadata/MetadataArgsStorage.ts index 7731cc5e..f4cffb57 100644 --- a/packages/engine/src/metadata/MetadataArgsStorage.ts +++ b/packages/engine/src/metadata/MetadataArgsStorage.ts @@ -151,7 +151,8 @@ export class MetadataArgsStorage { * Metadata args storage follows the best practices and stores metadata in a global variable. */ static get(): MetadataArgsStorage { - if (!(global as any).engineMetadataArgsStorage) (global as any).engineMetadataArgsStorage = new MetadataArgsStorage(); + if (!(global as any).engineMetadataArgsStorage) + (global as any).engineMetadataArgsStorage = new MetadataArgsStorage(); return (global as any).engineMetadataArgsStorage; } diff --git a/packages/engine/src/metadata/MetadataBuilder.ts b/packages/engine/src/metadata/MetadataBuilder.ts index 5b1f8c45..481bb197 100644 --- a/packages/engine/src/metadata/MetadataBuilder.ts +++ b/packages/engine/src/metadata/MetadataBuilder.ts @@ -43,7 +43,9 @@ export class MetadataBuilder { */ protected createMiddlewares(classes?: Function[]): MiddlewareMetadata[] { const metadataArgsStorage = MetadataArgsStorage.get(); - const middlewares = !classes ? metadataArgsStorage.middlewares : metadataArgsStorage.filterMiddlewareMetadatasForClasses(classes); + const middlewares = !classes + ? metadataArgsStorage.middlewares + : metadataArgsStorage.filterMiddlewareMetadatasForClasses(classes); return middlewares.map(middlewareArgs => new MiddlewareMetadata(middlewareArgs)); } @@ -52,7 +54,9 @@ export class MetadataBuilder { */ protected createInterceptors(classes?: Function[]): InterceptorMetadata[] { const metadataArgsStorage = MetadataArgsStorage.get(); - const interceptors = !classes ? metadataArgsStorage.interceptors : metadataArgsStorage.filterInterceptorMetadatasForClasses(classes); + const interceptors = !classes + ? metadataArgsStorage.interceptors + : metadataArgsStorage.filterInterceptorMetadatasForClasses(classes); return interceptors.map( interceptorArgs => new InterceptorMetadata({ @@ -67,7 +71,9 @@ export class MetadataBuilder { */ protected createControllers(classes?: Function[]): ControllerMetadata[] { const metadataArgsStorage = MetadataArgsStorage.get(); - const controllers = !classes ? metadataArgsStorage.controllers : metadataArgsStorage.filterControllerMetadatasForClasses(classes); + const controllers = !classes + ? metadataArgsStorage.controllers + : metadataArgsStorage.filterControllerMetadatasForClasses(classes); return controllers.map(controllerArgs => { const controller = new ControllerMetadata(controllerArgs); controller.build(this.createControllerResponseHandlers(controller)); diff --git a/packages/engine/src/service/ActionParameterHandler.ts b/packages/engine/src/service/ActionParameterHandler.ts index 356907ee..2c5f800e 100644 --- a/packages/engine/src/service/ActionParameterHandler.ts +++ b/packages/engine/src/service/ActionParameterHandler.ts @@ -71,12 +71,17 @@ export class ActionParameterHandler { if (!currentUser) { - return Promise.reject(new AuthorizationRequiredError(action.request.method, action.request.url)); + return Promise.reject( + new AuthorizationRequiredError(action.request.method, action.request.url), + ); } return currentUser; }); } else { - if (!value) return Promise.reject(new AuthorizationRequiredError(action.request.method, action.request.url)); + if (!value) + return Promise.reject( + new AuthorizationRequiredError(action.request.method, action.request.url), + ); } } else if (param.name && isValueEmpty) { // regular check for all other parameters // todo: figure out something with param.name usage and multiple things params (query params, upload files etc.) @@ -101,7 +106,8 @@ export class ActionParameterHandler { const keyValue = value[key]; if (typeof keyValue === "string") { - const ParamType: Function | undefined = (Reflect as any).getMetadata("design:type", param.targetType.prototype, key); + const ParamType: Function | undefined = (Reflect as any).getMetadata( + "design:type", + param.targetType.prototype, + key, + ); if (ParamType) { const typeString = ParamType.name.toLowerCase(); value[key] = await this.normalizeParamValue(keyValue, { @@ -199,15 +209,24 @@ export class ActionParameterHandler | any { // Validate only if validations is enabled globally via configurations if (this.driver.enableValidation) { - const shouldValidate = paramMetadata.targetType && paramMetadata.targetType !== Object && value instanceof paramMetadata.targetType; + const shouldValidate = + paramMetadata.targetType && + paramMetadata.targetType !== Object && + value instanceof paramMetadata.targetType; // When enabled globally, still skip validation if disabled by the route if (paramMetadata.validate !== false && shouldValidate) { - const options = Object.assign({forbidUnknownValues: false}, this.driver.validationOptions, paramMetadata.validate); + const options = Object.assign( + {forbidUnknownValues: false}, + this.driver.validationOptions, + paramMetadata.validate, + ); return validate(value, options) .then(() => value) .catch((validationErrors: ValidationError[]) => { - const error: any = new BadRequestError(`Invalid ${paramMetadata.type}, check 'errors' property for more info.`); + const error: any = new BadRequestError( + `Invalid ${paramMetadata.type}, check 'errors' property for more info.`, + ); error.errors = validationErrors; error.paramName = paramMetadata.name; throw error; diff --git a/packages/engine/src/service/ComponentImporter.ts b/packages/engine/src/service/ComponentImporter.ts index b8f672f7..f37f8ac1 100644 --- a/packages/engine/src/service/ComponentImporter.ts +++ b/packages/engine/src/service/ComponentImporter.ts @@ -6,7 +6,9 @@ export class ComponentImporter { let interceptorClasses: Function[] = []; if (options?.interceptors?.length) { interceptorClasses = (options.interceptors as any[]).filter(controller => controller instanceof Function); - const interceptorDirs = (options.interceptors as any[]).filter(controller => typeof controller === "string"); + const interceptorDirs = (options.interceptors as any[]).filter( + controller => typeof controller === "string", + ); interceptorClasses.push(...ClassFiles.loadFromDirectories(interceptorDirs)); } return interceptorClasses; diff --git a/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts b/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts index b7b6b0fa..b7f8097f 100644 --- a/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts +++ b/packages/error/src/error/AuthorizationCheckerNotDefinedError.ts @@ -7,7 +7,9 @@ export class AuthorizationCheckerNotDefinedError extends InternalServerError { override name = "AuthorizationCheckerNotDefinedError"; constructor() { - super(`Cannot use @Authorized decorator. Please define authorizationChecker function in routing-controllers action before using it.`); + super( + `Cannot use @Authorized decorator. Please define authorizationChecker function in routing-controllers action before using it.`, + ); Object.setPrototypeOf(this, AuthorizationCheckerNotDefinedError.prototype); } } diff --git a/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts b/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts index 6a82849f..caa67caa 100644 --- a/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts +++ b/packages/error/src/error/CurrentUserCheckerNotDefinedError.ts @@ -7,7 +7,9 @@ export class CurrentUserCheckerNotDefinedError extends InternalServerError { override name = "CurrentUserCheckerNotDefinedError"; constructor() { - super(`Cannot use @CurrentUser decorator. Please define currentUserChecker function in routing-controllers action before using it.`); + super( + `Cannot use @CurrentUser decorator. Please define currentUserChecker function in routing-controllers action before using it.`, + ); Object.setPrototypeOf(this, CurrentUserCheckerNotDefinedError.prototype); } } diff --git a/packages/error/src/error/ParamNormalizationError.ts b/packages/error/src/error/ParamNormalizationError.ts index 3d39d701..86cee2c4 100644 --- a/packages/error/src/error/ParamNormalizationError.ts +++ b/packages/error/src/error/ParamNormalizationError.ts @@ -7,7 +7,11 @@ export class InvalidParamError extends BadRequestError { override name = "ParamNormalizationError"; constructor(value: any, parameterName: string, parameterType: string) { - super(`Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into ${parameterType}.`); + super( + `Given parameter ${parameterName} is invalid. Value (${JSON.stringify( + value, + )}) cannot be parsed into ${parameterType}.`, + ); Object.setPrototypeOf(this, InvalidParamError.prototype); } diff --git a/packages/error/src/error/ParameterParseJsonError.ts b/packages/error/src/error/ParameterParseJsonError.ts index 0d499daa..32402f99 100644 --- a/packages/error/src/error/ParameterParseJsonError.ts +++ b/packages/error/src/error/ParameterParseJsonError.ts @@ -7,7 +7,9 @@ export class ParameterParseJsonError extends BadRequestError { override name = "ParameterParseJsonError"; constructor(parameterName: string, value: any) { - super(`Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into JSON.`); + super( + `Given parameter ${parameterName} is invalid. Value (${JSON.stringify(value)}) cannot be parsed into JSON.`, + ); Object.setPrototypeOf(this, ParameterParseJsonError.prototype); } } diff --git a/packages/extension/src/Param.ts b/packages/extension/src/Param.ts index 01c75ead..68c91de6 100644 --- a/packages/extension/src/Param.ts +++ b/packages/extension/src/Param.ts @@ -6,7 +6,10 @@ import {OfParamMetadata} from "./types"; * @author Manuel Santos * */ export class Param { - static ofString(value: string | undefined | null, paramMetadata: OfParamMetadata): Optional { + static ofString( + value: string | undefined | null, + paramMetadata: OfParamMetadata, + ): Optional { if (value === null || value === undefined) { return Optional.empty(); } diff --git a/packages/openapi/src/decorator/EnableOpenApi.ts b/packages/openapi/src/decorator/EnableOpenApi.ts index 5cf67779..ee88a2f6 100644 --- a/packages/openapi/src/decorator/EnableOpenApi.ts +++ b/packages/openapi/src/decorator/EnableOpenApi.ts @@ -22,7 +22,8 @@ export function EnableOpenApi(openApi: Partial = {}): Function return new FastifyOpenApi(); default: throw new Error( - "OpenAPI feature is only allowed for express and koa servers. " + "Please remove @EnableOpenApi from your application", + "OpenAPI feature is only allowed for express and koa servers. " + + "Please remove @EnableOpenApi from your application", ); } } diff --git a/samples/sample-express/src/middlewares/validation.middleware.ts b/samples/sample-express/src/middlewares/validation.middleware.ts index 68a62e27..f7957759 100644 --- a/samples/sample-express/src/middlewares/validation.middleware.ts +++ b/samples/sample-express/src/middlewares/validation.middleware.ts @@ -11,7 +11,12 @@ import {HttpError} from "@node-boot/error"; * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present */ -export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = false, forbidNonWhitelisted = false) => { +export const ValidationMiddleware = ( + type: any, + skipMissingProperties = false, + whitelist = false, + forbidNonWhitelisted = false, +) => { return (req: Request, res: Response, next: NextFunction) => { const dto = plainToInstance(type, req.body); validateOrReject(dto, { @@ -24,7 +29,9 @@ export const ValidationMiddleware = (type: any, skipMissingProperties = false, w next(); }) .catch((errors: ValidationError[]) => { - const message = errors.map((error: ValidationError) => Object.values(error.constraints ?? {})).join(", "); + const message = errors + .map((error: ValidationError) => Object.values(error.constraints ?? {})) + .join(", "); next(new HttpError(400, message)); }); }; diff --git a/samples/sample-koa/src/middlewares/validation.middleware.ts b/samples/sample-koa/src/middlewares/validation.middleware.ts index a3c1b3c5..824ca6ab 100644 --- a/samples/sample-koa/src/middlewares/validation.middleware.ts +++ b/samples/sample-koa/src/middlewares/validation.middleware.ts @@ -10,7 +10,12 @@ import {HttpException} from "../exceptions/httpException"; * @param whitelist Even if your object is an instance of a validation class it can contain additional properties that are not defined * @param forbidNonWhitelisted If you would rather to have an error thrown when any non-whitelisted properties are present */ -export const ValidationMiddleware = (type: any, skipMissingProperties = false, whitelist = false, forbidNonWhitelisted = false) => { +export const ValidationMiddleware = ( + type: any, + skipMissingProperties = false, + whitelist = false, + forbidNonWhitelisted = false, +) => { return (req: any, res: any, next: any) => { const dto = plainToInstance(type, req.body); validateOrReject(dto, { @@ -23,7 +28,9 @@ export const ValidationMiddleware = (type: any, skipMissingProperties = false, w next(); }) .catch((errors: ValidationError[]) => { - const message = errors.map((error: ValidationError) => Object.values(error.constraints ?? {})).join(", "); + const message = errors + .map((error: ValidationError) => Object.values(error.constraints ?? {})) + .join(", "); next(new HttpException(400, message)); }); }; diff --git a/servers/express-server/src/driver/ExpressDriver.ts b/servers/express-server/src/driver/ExpressDriver.ts index 11a248c2..e42d19d6 100644 --- a/servers/express-server/src/driver/ExpressDriver.ts +++ b/servers/express-server/src/driver/ExpressDriver.ts @@ -9,7 +9,12 @@ import { ParamMetadata, UseMetadata, } from "@node-boot/context"; -import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, NotFoundError} from "@node-boot/error"; +import { + AccessDeniedError, + AuthorizationCheckerNotDefinedError, + AuthorizationRequiredError, + NotFoundError, +} from "@node-boot/error"; import {Application, Request, Response} from "express"; import {LoggerService, MiddlewareInterface} from "@node-boot/context/src"; import cookie, {CookieParseOptions, CookieSerializeOptions} from "cookie"; @@ -22,7 +27,12 @@ import {DependenciesLoader} from "../loader"; // eslint-disable-next-line @typescript-eslint/no-var-requires const templateUrl = require("template-url"); -export type ExpressServerConfigs = ServerConfigOptions; +export type ExpressServerConfigs = ServerConfigOptions< + CookieParseOptions & CookieSerializeOptions, + CorsOptions, + SessionOptions, + MulterOptions +>; type ExpressServerOptions = { logger: LoggerService; @@ -141,7 +151,9 @@ export class ExpressDriver extends NodeBootDriver { }; if (isPromiseLike(checkResult)) { - checkResult.then(result => handleError(result)).catch(error => this.handleError(error, actionMetadata, action)); + checkResult + .then(result => handleError(result)) + .catch(error => this.handleError(error, actionMetadata, action)); } else { handleError(checkResult); } diff --git a/servers/express-server/src/loader/DependenciesLoader.ts b/servers/express-server/src/loader/DependenciesLoader.ts index cf001b74..4d31d75d 100644 --- a/servers/express-server/src/loader/DependenciesLoader.ts +++ b/servers/express-server/src/loader/DependenciesLoader.ts @@ -10,7 +10,9 @@ export class DependenciesLoader { // eslint-disable-next-line @typescript-eslint/no-var-requires return require("express")(); } catch (e) { - throw new Error("express package was not found installed. Try to install it: npm install express --save"); + throw new Error( + "express package was not found installed. Try to install it: npm install express --save", + ); } } else { throw new Error("Cannot load express. Try to install all required dependencies."); @@ -24,7 +26,9 @@ export class DependenciesLoader { try { return require("body-parser"); } catch (e) { - throw new Error("body-parser package was not found installed. Try to install it: npm install body-parser --save"); + throw new Error( + "body-parser package was not found installed. Try to install it: npm install body-parser --save", + ); } } diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index b93fc8bb..4c04d46c 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -20,7 +20,13 @@ import {FastifyMultipartOptions} from "@fastify/multipart"; import {FastifyViewOptions} from "@fastify/view"; import templateUrl from "template-url"; import {FastifyCorsOptions} from "@fastify/cors"; -import {AccessDeniedError, AuthorizationCheckerNotDefinedError, AuthorizationRequiredError, HttpError, NotFoundError} from "@node-boot/error"; +import { + AccessDeniedError, + AuthorizationCheckerNotDefinedError, + AuthorizationRequiredError, + HttpError, + NotFoundError, +} from "@node-boot/error"; import {DependenciesLoader} from "../loader"; import {ServerConfig, ServerConfigOptions} from "@node-boot/extension"; @@ -123,7 +129,12 @@ export class FastifyDriver extends NodeBootDriver { + fastifyHook = ( + request: FastifyRequest, + reply: FastifyReply, + payload: any, + done: DoneFuncWithErrOrRes, + ) => { this.callGlobalMiddleware(request, options, middleware, reply, done, payload); }; @@ -187,13 +198,18 @@ export class FastifyDriver extends NodeBootDriver) => any) { + registerAction( + actionMetadata: ActionMetadata, + executeAction: (action: Action) => any, + ) { const defaultMiddlewares: any[] = []; if (actionMetadata.isAuthorizedUsed) { - defaultMiddlewares.push(async (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => { - await this.checkAuthorization(request, reply, done, actionMetadata); - }); + defaultMiddlewares.push( + async (request: FastifyRequest, reply: FastifyReply, done: HookHandlerDoneFunction) => { + await this.checkAuthorization(request, reply, done, actionMetadata); + }, + ); } // TODO Make sure nothing is required if @fastify/multipart is registered @@ -218,11 +234,21 @@ export class FastifyDriver extends NodeBootDriver { + const afterMiddlewaresAdapter = async ( + request: FastifyRequest, + reply: FastifyReply, + payload: any, + done: DoneFuncWithErrOrRes, + ) => { afterMiddlewares.forEach(middleware => middleware(request, reply, payload, done)); }; - const errorMiddlewaresAdapter = async (request: FastifyRequest, reply: FastifyReply, error: Error, done: () => void) => { + const errorMiddlewaresAdapter = async ( + request: FastifyRequest, + reply: FastifyReply, + error: Error, + done: () => void, + ) => { errorMiddlewares.forEach(middleware => middleware(request, reply, error, done)); }; @@ -246,7 +272,12 @@ export class FastifyDriver extends NodeBootDriver { + ( + request: FastifyRequest, + reply: FastifyReply, + done: HookHandlerDoneFunction | DoneFuncWithErrOrRes, + payload?: any, + ) => { try { const useResult = getFromContainer(use.middleware).use( {request, response: reply, next: done}, @@ -317,13 +353,15 @@ export class FastifyDriver extends NodeBootDriver use.isErrorMiddleware()).forEach((use: UseMetadata) => { // if this is function instance of ErrorMiddlewareInterface - middlewareFunctions.push((request: FastifyRequest, reply: FastifyReply, error: FastifyError, done: () => void) => { - return getFromContainer(use.middleware).onError(error, { - request, - response: reply, - next: done, - }); - }); + middlewareFunctions.push( + (request: FastifyRequest, reply: FastifyReply, error: FastifyError, done: () => void) => { + return getFromContainer(use.middleware).onError(error, { + request, + response: reply, + next: done, + }); + }, + ); }); return middlewareFunctions; } @@ -390,7 +428,11 @@ export class FastifyDriver extends NodeBootDriver) { + handleError( + error: Error, + actionMetadata: ActionMetadata | undefined, + action: Action, + ) { // Handle error using Fastify's reply if (actionMetadata) { Object.keys(actionMetadata.headers).forEach(name => { @@ -461,7 +503,11 @@ export class FastifyDriver extends NodeBootDriver, actionMetadata: ActionMetadata) { + private applyTemplateRender( + result: any, + action: Action, + actionMetadata: ActionMetadata, + ) { // if template is set then render it // Check doc https://www.npmjs.com/package/@fastify/view const renderOptions = result && result instanceof Object ? result : {}; @@ -511,7 +557,9 @@ export class FastifyDriver extends NodeBootDriver { }; if (isPromiseLike(checkResult)) { - return checkResult.then(result => handleError(result)).catch(error => this.handleError(error, actionMetadata, action)); + return checkResult + .then(result => handleError(result)) + .catch(error => this.handleError(error, actionMetadata, action)); } else { return handleError(checkResult); } diff --git a/servers/koa-server/src/loader/DependenciesLoader.ts b/servers/koa-server/src/loader/DependenciesLoader.ts index 0b32849b..6abd6c2d 100644 --- a/servers/koa-server/src/loader/DependenciesLoader.ts +++ b/servers/koa-server/src/loader/DependenciesLoader.ts @@ -25,7 +25,9 @@ export class DependenciesLoader { try { return new (require("@koa/router"))(); } catch (e) { - throw new Error("@koa/router package was not found installed. Try to install it: npm install @koa/router --save"); + throw new Error( + "@koa/router package was not found installed. Try to install it: npm install @koa/router --save", + ); } } else { throw new Error("Cannot load koa. Try to install all required dependencies."); @@ -45,7 +47,9 @@ export class DependenciesLoader { try { return require("@koa/multer"); } catch (e) { - throw new Error("@koa/multer package was not found installed. Try to install it: npm install @koa/multer --save"); + throw new Error( + "@koa/multer package was not found installed. Try to install it: npm install @koa/multer --save", + ); } } @@ -56,7 +60,9 @@ export class DependenciesLoader { try { return require("@koa/cors"); } catch (e) { - throw new Error("@koa/cors package was not found installed. Try to install it: npm install @koa/cors --save"); + throw new Error( + "@koa/cors package was not found installed. Try to install it: npm install @koa/cors --save", + ); } } @@ -67,7 +73,9 @@ export class DependenciesLoader { try { return require("koa-session"); } catch (e) { - throw new Error("koa-session package was not found installed. Try to install it: npm install koa-session --save"); + throw new Error( + "koa-session package was not found installed. Try to install it: npm install koa-session --save", + ); } } } diff --git a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts index 868a2208..315042c0 100644 --- a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts +++ b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts @@ -11,7 +11,10 @@ import {KoaActuatorAdapter} from "./KoaActuatorAdapter"; export class DefaultActuatorAdapter implements ActuatorAdapter { private metricsContext: MetricsContext; - constructor(private readonly register = new Prometheus.Registry(), private readonly infoService: InfoService = new InfoService()) {} + constructor( + private readonly register = new Prometheus.Registry(), + private readonly infoService: InfoService = new InfoService(), + ) {} private setupMetrics(options: ActuatorOptions) { this.register.setDefaultLabels({ @@ -53,17 +56,28 @@ export class DefaultActuatorAdapter implements ActuatorAdapter { let frameworkAdapter: ActuatorAdapter; switch (options.serverType) { case "express": - frameworkAdapter = new ExpressActuatorAdapter(context, this.infoService, metadataService, configService); + frameworkAdapter = new ExpressActuatorAdapter( + context, + this.infoService, + metadataService, + configService, + ); break; case "koa": frameworkAdapter = new KoaActuatorAdapter(context, this.infoService, metadataService, configService); break; case "fastify": - frameworkAdapter = new FastifyActuatorAdapter(context, this.infoService, metadataService, configService); + frameworkAdapter = new FastifyActuatorAdapter( + context, + this.infoService, + metadataService, + configService, + ); break; default: throw new Error( - "Actuator feature is only allowed for express, koa and fastify servers. " + "Please remove @EnableActuator from your application", + "Actuator feature is only allowed for express, koa and fastify servers. " + + "Please remove @EnableActuator from your application", ); } frameworkAdapter.bind(options, server, router); diff --git a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts index c7649c5d..66ed3000 100644 --- a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts +++ b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts @@ -21,7 +21,9 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { res.once("finish", () => { const responseTimeInMilliseconds = Date.now() - res.locals.startEpoch; - this.context.http_request_duration_milliseconds.labels(req.method, req.path, res.statusCode).observe(responseTimeInMilliseconds); + this.context.http_request_duration_milliseconds + .labels(req.method, req.path, res.statusCode) + .observe(responseTimeInMilliseconds); }); // Increment the HTTP request counter diff --git a/starters/actuator/src/adapter/KoaActuatorAdapter.ts b/starters/actuator/src/adapter/KoaActuatorAdapter.ts index fe7719ca..45d4ffb5 100644 --- a/starters/actuator/src/adapter/KoaActuatorAdapter.ts +++ b/starters/actuator/src/adapter/KoaActuatorAdapter.ts @@ -24,7 +24,9 @@ export class KoaActuatorAdapter implements ActuatorAdapter { const responseTimeInMilliseconds = Date.now() - ctx.state["startEpoch"]; - this.context.http_request_duration_milliseconds.labels(ctx.method, ctx.path, ctx.status.toString()).observe(responseTimeInMilliseconds); + this.context.http_request_duration_milliseconds + .labels(ctx.method, ctx.path, ctx.status.toString()) + .observe(responseTimeInMilliseconds); // Increment the HTTP request counter this.context.http_request_counter diff --git a/starters/actuator/src/service/InfoService.ts b/starters/actuator/src/service/InfoService.ts index 5ab3f921..a85a8801 100644 --- a/starters/actuator/src/service/InfoService.ts +++ b/starters/actuator/src/service/InfoService.ts @@ -69,7 +69,9 @@ export class InfoService { let git; if (properties !== undefined) { - const time = dateFormat ? dayjs(properties.get("git.commit.time")).format(dateFormat) : properties.get("git.commit.time"); + const time = dateFormat + ? dayjs(properties.get("git.commit.time")).format(dateFormat) + : properties.get("git.commit.time"); if (infoGitMode === "simple") { git = { diff --git a/starters/actuator/src/service/MetadataService.ts b/starters/actuator/src/service/MetadataService.ts index 8d6b6b9e..9edf51a0 100644 --- a/starters/actuator/src/service/MetadataService.ts +++ b/starters/actuator/src/service/MetadataService.ts @@ -8,7 +8,9 @@ export class MetadataService { return this.metadataStorage.controllers.map(controller => { const controllerPath = Reflect.getMetadata(CONTROLLER_PATH_METADATA_KEY, controller.target); const controllerVersion = Reflect.getMetadata(CONTROLLER_VERSION_METADATA_KEY, controller.target); - const actions = this.metadataStorage.actions.filter(action => action.target.name === controller.target.name); + const actions = this.metadataStorage.actions.filter( + action => action.target.name === controller.target.name, + ); return { actions, controller: controller.target.name, diff --git a/starters/persistence/src/config/DataSourceConfiguration.ts b/starters/persistence/src/config/DataSourceConfiguration.ts index ac001b5f..13a62e83 100644 --- a/starters/persistence/src/config/DataSourceConfiguration.ts +++ b/starters/persistence/src/config/DataSourceConfiguration.ts @@ -55,7 +55,9 @@ export class DataSourceConfiguration { } if (databaseConfigs.synchronize && databaseConfigs.migrationsRun) { - throw new Error(`Only one of "synchronize" or "migrationsRun" config property can be enabled. Please set one of them to false`); + throw new Error( + `Only one of "synchronize" or "migrationsRun" config property can be enabled. Please set one of them to false`, + ); } // Save the synchronization and migration state diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index 833885cb..89e00f73 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -67,7 +67,9 @@ export class PersistenceConfiguration { PersistenceConfiguration.bindDataRepositories(logger); // Validate database consistency - Promise.all(initializationPromises).then(_ => PersistenceConfiguration.ensureDatabase(logger, dataSource)); + Promise.all(initializationPromises).then(_ => + PersistenceConfiguration.ensureDatabase(logger, dataSource), + ); }) .catch(err => { logger.error("Error during Persistence DataSource initialization:", err); @@ -181,9 +183,13 @@ export class PersistenceConfiguration { try { const tables = await queryRunner.getTables(); const entities = dataSource.entityMetadatas; - logger.info(`Database validation: Running consistency validation for ${tables.length}-tables/${entities.length}-entities.`); + logger.info( + `Database validation: Running consistency validation for ${tables.length}-tables/${entities.length}-entities.`, + ); - const existingEntities = entities.filter(entity => tables.find(table => table.name.includes(entity.tableName)) !== undefined); + const existingEntities = entities.filter( + entity => tables.find(table => table.name.includes(entity.tableName)) !== undefined, + ); if (existingEntities.length !== entities.length) { logger.error( diff --git a/starters/persistence/src/config/QueryCacheConfiguration.ts b/starters/persistence/src/config/QueryCacheConfiguration.ts index a350dd00..324954c3 100644 --- a/starters/persistence/src/config/QueryCacheConfiguration.ts +++ b/starters/persistence/src/config/QueryCacheConfiguration.ts @@ -39,7 +39,11 @@ export class QueryCacheConfiguration { } if (cacheConfig) { - logger.info(`Configuring query cache with options from configuration${cacheProvider ? " and custom cache provider" : ""}`); + logger.info( + `Configuring query cache with options from configuration${ + cacheProvider ? " and custom cache provider" : "" + }`, + ); iocContainer.set(QUERY_CACHE_CONFIG, { ...cacheConfig, provider: cacheProvider, @@ -52,12 +56,18 @@ export class QueryCacheConfiguration { } else if (cacheEnabled) { // If cache is only enabled, falling back to database cache or to a custom provider if specified logger.info( - `${cacheProvider ? "Configuring custom query cache provider" : "Enabling database query cache with default configurations"}`, + `${ + cacheProvider + ? "Configuring custom query cache provider" + : "Enabling database query cache with default configurations" + }`, ); iocContainer.set(QUERY_CACHE_CONFIG, cacheProvider ? {provider: cacheProvider} : true); } else { // Cache is explicitly disabled - logger.warn("Persistence query cache is not enabled. Enable it to boost your application performance."); + logger.warn( + "Persistence query cache is not enabled. Enable it to boost your application performance.", + ); } } } diff --git a/starters/persistence/src/hook/hooks.ts b/starters/persistence/src/hook/hooks.ts index 7f75e95a..e2873829 100644 --- a/starters/persistence/src/hook/hooks.ts +++ b/starters/persistence/src/hook/hooks.ts @@ -18,6 +18,9 @@ export const runOnTransactionComplete = (cb: (e: Error | undefined) => void) => return innerRunOnTransactionComplete(cb); }; -export const runInTransaction = ReturnType>(fn: Func, options?: WrapInTransactionOptions) => { +export const runInTransaction = ReturnType>( + fn: Func, + options?: WrapInTransactionOptions, +) => { return wrapInTransaction(fn, options)(); }; diff --git a/starters/persistence/src/property/SqlServerConnectionProperties.ts b/starters/persistence/src/property/SqlServerConnectionProperties.ts index 7a129a36..6d98cf1f 100644 --- a/starters/persistence/src/property/SqlServerConnectionProperties.ts +++ b/starters/persistence/src/property/SqlServerConnectionProperties.ts @@ -210,7 +210,12 @@ export interface SqlServerConnectionProperties extends SqlServerConnectionCreden * The default isolation level for new connections. All out-of-transaction queries are executed with this * setting. The isolation levels are available from require('tedious').ISOLATION_LEVEL . */ - readonly connectionIsolationLevel?: "READ_UNCOMMITTED" | "READ_COMMITTED" | "REPEATABLE_READ" | "SERIALIZABLE" | "SNAPSHOT"; + readonly connectionIsolationLevel?: + | "READ_UNCOMMITTED" + | "READ_COMMITTED" + | "REPEATABLE_READ" + | "SERIALIZABLE" + | "SNAPSHOT"; /** * A boolean, determining whether the connection will request read only access from a SQL Server From c4ed80ca294ed45e8c1b19a3adfe63275cf6cddd Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 03:29:38 +0000 Subject: [PATCH 13/16] fix app runs --- .../DatasourceOverridesConfiguration.ts | 2 +- samples/sample-fastify/app-config.yaml | 2 +- samples/sample-fastify/src/app.ts | 4 +-- .../src/config/BackendConfigProperties.ts | 10 ------- .../DatasourceOverridesConfiguration.ts | 4 +-- samples/sample-koa/app-config.yaml | 2 +- samples/sample-koa/src/app.ts | 6 ++-- .../src/config/BackendConfigProperties.ts | 10 ------- .../DatasourceOverridesConfiguration.ts | 4 +-- .../src/driver/FastifyDriver.ts | 9 +----- .../src/server/FastifyServer.ts | 29 ++----------------- 11 files changed, 16 insertions(+), 66 deletions(-) delete mode 100644 samples/sample-fastify/src/config/BackendConfigProperties.ts delete mode 100644 samples/sample-koa/src/config/BackendConfigProperties.ts diff --git a/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts index 4ebbd714..5a7b9a01 100644 --- a/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts +++ b/samples/sample-express/src/persistence/DatasourceOverridesConfiguration.ts @@ -3,7 +3,7 @@ import {DatasourceConfiguration} from "@node-boot/starter-persistence"; @DatasourceConfiguration({ type: "better-sqlite3", database: "express-sample.db", - synchronize: true, + synchronize: false, migrationsRun: true, }) export class DatasourceOverridesConfiguration {} diff --git a/samples/sample-fastify/app-config.yaml b/samples/sample-fastify/app-config.yaml index 5ad325be..740fb84e 100644 --- a/samples/sample-fastify/app-config.yaml +++ b/samples/sample-fastify/app-config.yaml @@ -5,7 +5,7 @@ node-boot: platform: "tech-insights" environment: "development" defaultErrorHandler: false - port: 10000 + port: 3000 api: routePrefix: "/api" diff --git a/samples/sample-fastify/src/app.ts b/samples/sample-fastify/src/app.ts index 66c19ce4..ac3d50bd 100644 --- a/samples/sample-fastify/src/app.ts +++ b/samples/sample-fastify/src/app.ts @@ -1,7 +1,6 @@ import "reflect-metadata"; import {Container} from "typedi"; import {Configurations, Controllers, GlobalMiddlewares, NodeBoot, NodeBootApplication} from "@node-boot/core"; -import {BackendConfigProperties} from "./config/BackendConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; @@ -14,10 +13,11 @@ import {EnableActuator} from "@node-boot/starter-actuator"; import {EnableRepositories} from "@node-boot/starter-persistence"; import {EnableDI} from "@node-boot/di"; import {CustomErrorHandler} from "./middlewares/CustomErrorHandler"; +import {AppConfigProperties} from "./config/AppConfigProperties"; @EnableDI(Container) @EnableOpenApi() -@Configurations([BackendConfigProperties, MultipleConfigurations]) +@Configurations([AppConfigProperties, MultipleConfigurations]) @Controllers([UserController]) @GlobalMiddlewares([LoggingMiddleware, CustomErrorHandler]) //@EnableComponentScan() diff --git a/samples/sample-fastify/src/config/BackendConfigProperties.ts b/samples/sample-fastify/src/config/BackendConfigProperties.ts deleted file mode 100644 index acd6b4b0..00000000 --- a/samples/sample-fastify/src/config/BackendConfigProperties.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {ConfigurationProperties} from "@node-boot/config"; - -@ConfigurationProperties({ - configPath: "backend", - configName: "backend-config", -}) -export class BackendConfigProperties { - baseUrl: string; - allowInsecureCookie: boolean; -} diff --git a/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts index 4ebbd714..10f1a2c6 100644 --- a/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts +++ b/samples/sample-fastify/src/persistence/DatasourceOverridesConfiguration.ts @@ -2,8 +2,8 @@ import {DatasourceConfiguration} from "@node-boot/starter-persistence"; @DatasourceConfiguration({ type: "better-sqlite3", - database: "express-sample.db", - synchronize: true, + database: "fastify-sample.db", + synchronize: false, migrationsRun: true, }) export class DatasourceOverridesConfiguration {} diff --git a/samples/sample-koa/app-config.yaml b/samples/sample-koa/app-config.yaml index 32c39765..39823f5f 100644 --- a/samples/sample-koa/app-config.yaml +++ b/samples/sample-koa/app-config.yaml @@ -5,7 +5,7 @@ node-boot: platform: "tech-insights" environment: "development" defaultErrorHandler: false - port: 10000 + port: 3000 api: routePrefix: "/api" diff --git a/samples/sample-koa/src/app.ts b/samples/sample-koa/src/app.ts index 3d194281..421dea0c 100644 --- a/samples/sample-koa/src/app.ts +++ b/samples/sample-koa/src/app.ts @@ -1,7 +1,6 @@ import "reflect-metadata"; import {Container} from "typedi"; import {Configurations, Controllers, GlobalMiddlewares, NodeBoot, NodeBootApplication} from "@node-boot/core"; -import {BackendConfigProperties} from "./config/BackendConfigProperties"; import {UserController} from "./controllers/users.controller"; import {LoggingMiddleware} from "./middlewares/LoggingMiddleware"; import {MultipleConfigurations} from "./config/MultipleConfigurations"; @@ -12,10 +11,12 @@ import {EnableOpenApi} from "@node-boot/openapi"; import {EnableActuator} from "@node-boot/starter-actuator"; import {DefaultAuthorizationChecker} from "./auth/DefaultAuthorizationChecker"; import {EnableDI} from "@node-boot/di"; +import {AppConfigProperties} from "./config/AppConfigProperties"; +import {EnableRepositories} from "@node-boot/starter-persistence"; @EnableDI(Container) @EnableOpenApi() -@Configurations([BackendConfigProperties, MultipleConfigurations]) +@Configurations([AppConfigProperties, MultipleConfigurations]) @Controllers([UserController]) @GlobalMiddlewares([LoggingMiddleware]) //@EnableComponentScan() @@ -31,6 +32,7 @@ import {EnableDI} from "@node-boot/di"; * */ @EnableActuator() @EnableAuthorization(LoggedInUserResolver, DefaultAuthorizationChecker) +@EnableRepositories() @NodeBootApplication() export class FactsServiceApp { static start() { diff --git a/samples/sample-koa/src/config/BackendConfigProperties.ts b/samples/sample-koa/src/config/BackendConfigProperties.ts deleted file mode 100644 index acd6b4b0..00000000 --- a/samples/sample-koa/src/config/BackendConfigProperties.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {ConfigurationProperties} from "@node-boot/config"; - -@ConfigurationProperties({ - configPath: "backend", - configName: "backend-config", -}) -export class BackendConfigProperties { - baseUrl: string; - allowInsecureCookie: boolean; -} diff --git a/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts b/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts index 4ebbd714..1f794d64 100644 --- a/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts +++ b/samples/sample-koa/src/persistence/DatasourceOverridesConfiguration.ts @@ -2,8 +2,8 @@ import {DatasourceConfiguration} from "@node-boot/starter-persistence"; @DatasourceConfiguration({ type: "better-sqlite3", - database: "express-sample.db", - synchronize: true, + database: "koa-sample.db", + synchronize: false, migrationsRun: true, }) export class DatasourceOverridesConfiguration {} diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index 4c04d46c..98412570 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -224,14 +224,7 @@ export class FastifyDriver extends NodeBootDriver { - // This ensures that a request is only processed once. Multiple routes may match a request - // e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)), only the first matching route should - // be called. - // The following middleware only starts an action processing if the request has not been processed before. - if (!request["locals"].routeStarted) { - request["locals"].routeStarted = true; - await executeAction({request, response: reply}); - } + await executeAction({request, response: reply}); }; const afterMiddlewaresAdapter = async ( diff --git a/servers/fastify-server/src/server/FastifyServer.ts b/servers/fastify-server/src/server/FastifyServer.ts index 347b400f..62bc89cb 100644 --- a/servers/fastify-server/src/server/FastifyServer.ts +++ b/servers/fastify-server/src/server/FastifyServer.ts @@ -22,33 +22,7 @@ export class FastifyServer extends BaseServer // Bind application container through adapter if (context.applicationAdapter) { const engineOptions = context.applicationAdapter.bind(context.diOptions?.iocContainer); - const serverConfigs: FastifyServerConfigs = { - cookie: { - enabled: true, - options: { - secret: "my-secret", // for cookies signature - hook: "onRequest", // set to false to disable cookie autoparsing or set autoparsing on any of the following hooks: 'onRequest', 'preParsing', 'preHandler', 'preValidation'. default: 'onRequest' - parseOptions: {}, // options for parsing cookies - }, - }, - session: { - enabled: false, - options: { - secret: "a secret with minimum length of 32 characters", - }, - }, - template: { - enabled: false, - options: { - engine: { - handlebars: require("handlebars"), - }, - }, - }, - multipart: { - enabled: false, - }, - }; + const serverConfigs: FastifyServerConfigs = {}; const driver = new FastifyDriver({ configs: serverConfigs, @@ -70,6 +44,7 @@ export class FastifyServer extends BaseServer this.framework.listen({port: context.applicationOptions.port}, (err: Error | null, address: string) => { if (err) { this.logger.error(err); + process.exit(1); } else { this.logger.info(`=================================`); this.logger.info(`======= ENV: ${context.applicationOptions.environment} =======`); From 4b79fe82c63ca10fdb95482f0a4f06137bc42742 Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 03:55:39 +0000 Subject: [PATCH 14/16] dependencies cleanup --- pnpm-lock.yaml | 150 +++++++++------------------- samples/sample-express/package.json | 3 + samples/sample-fastify/package.json | 18 ++-- samples/sample-koa/package.json | 5 +- servers/express-server/package.json | 14 +-- servers/fastify-server/package.json | 15 ++- 6 files changed, 77 insertions(+), 128 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bdfab3a0..533a33af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -270,12 +270,18 @@ importers: class-validator: specifier: ^0.14.0 version: 0.14.0 + cookie: + specifier: ^0.6.0 + version: 0.6.0 cors: specifier: ^2.8.5 version: 2.8.5 express: specifier: ^4.18.2 version: 4.18.2 + express-session: + specifier: ^1.17.3 + version: 1.17.3 helmet: specifier: ^7.0.0 version: 7.0.0 @@ -291,6 +297,9 @@ importers: reflect-metadata: specifier: ^0.1.13 version: 0.1.13 + swagger-ui-express: + specifier: ^5.0.0 + version: 5.0.0(express@4.18.2) typedi: specifier: ^0.10.0 version: 0.10.0 @@ -328,6 +337,27 @@ importers: samples/sample-fastify: dependencies: + '@fastify/cookie': + specifier: ^9.0.4 + version: 9.0.4 + '@fastify/cors': + specifier: ^8.4.1 + version: 8.4.1 + '@fastify/multipart': + specifier: ^7.7.3 + version: 7.7.3 + '@fastify/session': + specifier: ^10.4.0 + version: 10.4.0 + '@fastify/swagger': + specifier: ^8.10.0 + version: 8.10.0 + '@fastify/swagger-ui': + specifier: ^1.9.3 + version: 1.9.3 + '@fastify/view': + specifier: ^8.0.0 + version: 8.0.0 '@node-boot/authorization': specifier: 1.0.0 version: link:../../packages/authorization @@ -361,36 +391,18 @@ importers: '@node-boot/starter-persistence': specifier: 1.0.0 version: link:../../starters/persistence - body-parser: - specifier: ^1.20.2 - version: 1.20.2 + better-sqlite3: + specifier: ^9.1.1 + version: 9.1.1 class-transformer: specifier: ^0.5.1 version: 0.5.1 class-validator: specifier: ^0.14.0 version: 0.14.0 - cors: - specifier: ^2.8.5 - version: 2.8.5 fastify: specifier: ^4.21.0 version: 4.21.0 - fastify-multer: - specifier: ^2.0.3 - version: 2.0.3 - helmet: - specifier: ^7.0.0 - version: 7.0.0 - hpp: - specifier: ^0.2.3 - version: 0.2.3 - middie: - specifier: ^7.1.0 - version: 7.1.0 - minimist: - specifier: ^1.2.8 - version: 1.2.8 reflect-metadata: specifier: ^0.1.13 version: 0.1.13 @@ -404,15 +416,6 @@ importers: specifier: ^3.10.0 version: 3.10.0 devDependencies: - '@types/body-parser': - specifier: ^1.19.2 - version: 1.19.2 - '@types/cors': - specifier: ^2.8.13 - version: 2.8.13 - '@types/hpp': - specifier: ^0.2.2 - version: 0.2.2 '@types/jest': specifier: ^28.1.6 version: 28.1.6 @@ -482,15 +485,18 @@ importers: koa-bodyparser: specifier: ^4.4.1 version: 4.4.1 + koa-cookies: + specifier: ^4.0.2 + version: 4.0.2 koa-helmet: specifier: ^7.0.2 version: 7.0.2 + koa-session: + specifier: ^6.4.0 + version: 6.4.0 koa2-swagger-ui: specifier: ^5.8.0 version: 5.8.0 - minimist: - specifier: ^1.2.8 - version: 1.2.8 reflect-metadata: specifier: ^0.1.13 version: 0.1.13 @@ -547,22 +553,22 @@ importers: specifier: 1.0.0 version: link:../../packages/extension body-parser: - specifier: ^1.20.2 + specifier: '>=1.20.2' version: 1.20.2 cookie: - specifier: ^0.6.0 + specifier: '>=0.6.0' version: 0.6.0 cors: - specifier: ^2.8.5 + specifier: '>=2.8.5' version: 2.8.5 express: specifier: '>=4.18.2' version: 4.18.2 express-session: - specifier: ^1.17.3 + specifier: '>=1.17.3' version: 1.17.3 multer: - specifier: ^1.4.5-lts.1 + specifier: '>=1.4.5-lts.1' version: 1.4.5-lts.1 template-url: specifier: ^1.0.0 @@ -590,19 +596,19 @@ importers: servers/fastify-server: dependencies: '@fastify/cookie': - specifier: ^9.0.4 + specifier: '>=9.0.4' version: 9.0.4 '@fastify/cors': - specifier: ^8.4.1 + specifier: '>=8.4.1' version: 8.4.1 '@fastify/multipart': - specifier: ^7.7.3 + specifier: '>=7.7.3' version: 7.7.3 '@fastify/session': - specifier: ^10.4.0 + specifier: '>=10.4.0' version: 10.4.0 '@fastify/view': - specifier: ^8.0.0 + specifier: '>=8.0.0' version: 8.0.0 '@node-boot/context': specifier: 1.0.0 @@ -622,9 +628,6 @@ importers: fastify: specifier: '>=4.21.0' version: 4.21.0 - handlebars: - specifier: ^4.7.8 - version: 4.7.8 template-url: specifier: ^1.0.0 version: 1.0.0 @@ -3063,16 +3066,6 @@ packages: typedarray: 0.0.6 dev: false - /concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} - dependencies: - buffer-from: 1.1.2 - inherits: 2.0.4 - readable-stream: 3.6.2 - typedarray: 0.0.6 - dev: false - /content-disposition@0.5.4: resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} engines: {node: '>= 0.6'} @@ -3701,30 +3694,6 @@ packages: resolution: {integrity: sha512-cIusKBIt/R/oI6z/1nyfe2FvGKVTohVRfvkOhvx0nCEW+xf5NoCXjAHcWp93uOUBchzYcsvPlrapAdX1uW+YGg==} dev: false - /fastify-multer@2.0.3: - resolution: {integrity: sha512-QnFqrRgxmUwWHTgX9uyQSu0C/hmVCfcxopqjApZ4uaZD5W9MJ+nHUlW4+9q7Yd3BRxDIuHvgiM5mjrh6XG8cAA==} - engines: {node: '>=10.17.0'} - dependencies: - '@fastify/busboy': 1.2.1 - append-field: 1.0.0 - concat-stream: 2.0.0 - fastify-plugin: 2.3.4 - mkdirp: 1.0.4 - on-finished: 2.4.1 - type-is: 1.6.18 - xtend: 4.0.2 - dev: false - - /fastify-plugin@2.3.4: - resolution: {integrity: sha512-I+Oaj6p9oiRozbam30sh39BiuiqBda7yK2nmSPVwDCfIBlKnT8YB3MY+pRQc2Fcd07bf6KPGklHJaQ2Qu81TYQ==} - dependencies: - semver: 7.5.4 - dev: false - - /fastify-plugin@3.0.1: - resolution: {integrity: sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==} - dev: false - /fastify-plugin@4.5.1: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false @@ -5207,21 +5176,6 @@ packages: picomatch: 2.3.1 dev: true - /middie@7.0.0: - resolution: {integrity: sha512-gAX1DrlTOMsDW6MskZrCJQW1j56I7VHqKFdwGSMtZuIbymye2j89R1juPLLCyEMrx0sZR9EZ+VqnWqOh9KH4PQ==} - dependencies: - fastify-plugin: 3.0.1 - path-to-regexp: 6.2.1 - reusify: 1.0.4 - dev: false - - /middie@7.1.0: - resolution: {integrity: sha512-1Fq0i+nGYlPyVv5pBbnXSdkcQ4in1d9pNv2DMwTKGztsqyAng9PN/PNl4PMhccScEWOvj+HT5QtANsA0AuYHFw==} - dependencies: - middie-deprecated: /middie@7.0.0 - process-warning: 1.0.0 - dev: false - /mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -5805,10 +5759,6 @@ packages: requiresBuild: true dev: false - /process-warning@1.0.0: - resolution: {integrity: sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==} - dev: false - /process-warning@2.2.0: resolution: {integrity: sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==} dev: false @@ -6553,7 +6503,6 @@ packages: resolution: {integrity: sha512-NZ8YDKqf58+bWbD3v1NnF9QCveqtovT8e/4fJoc/ZgafJdFSqVEYiXM+OnS6rg9Yrb8z6bwpJ4H6hLk7mMs36Q==} requiresBuild: true dev: false - optional: true /swagger-ui-express@5.0.0(express@4.18.2): resolution: {integrity: sha512-tsU9tODVvhyfkNSvf03E6FAk+z+5cU3lXAzMy6Pv4av2Gt2xA0++fogwC4qo19XuFf6hdxevPuVCSKFuMHJhFA==} @@ -6565,7 +6514,6 @@ packages: express: 4.18.2 swagger-ui-dist: 5.3.2 dev: false - optional: true /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} diff --git a/samples/sample-express/package.json b/samples/sample-express/package.json index 02216703..f64bef75 100644 --- a/samples/sample-express/package.json +++ b/samples/sample-express/package.json @@ -55,6 +55,9 @@ "hpp": "^0.2.3", "minimist": "^1.2.8", "multer": "^1.4.5-lts.1", + "cookie": "^0.6.0", + "express-session": "^1.17.3", + "swagger-ui-express": "^5.0.0", "typedi": "^0.10.0", "winston": "^3.10.0" }, diff --git a/samples/sample-fastify/package.json b/samples/sample-fastify/package.json index dd915361..1cc88ddf 100644 --- a/samples/sample-fastify/package.json +++ b/samples/sample-fastify/package.json @@ -44,23 +44,21 @@ "@node-boot/starter-persistence": "1.0.0", "reflect-metadata": "^0.1.13", "fastify": "^4.21.0", + "@fastify/cookie": "^9.0.4", + "@fastify/session": "^10.4.0", + "@fastify/multipart": "^7.7.3", + "@fastify/view": "^8.0.0", + "@fastify/cors": "^8.4.1", + "@fastify/swagger-ui": "^1.9.3", + "@fastify/swagger": "^8.10.0", "typeorm": "^0.3.17", - "middie": "^7.1.0", - "fastify-multer": "^2.0.3", - "body-parser": "^1.20.2", + "better-sqlite3": "^9.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "cors": "^2.8.5", - "helmet": "^7.0.0", - "hpp": "^0.2.3", - "minimist": "^1.2.8", "typedi": "^0.10.0", "winston": "^3.10.0" }, "devDependencies": { - "@types/body-parser": "^1.19.2", - "@types/cors": "^2.8.13", - "@types/hpp": "^0.2.2", "@types/jest": "^28.1.6", "@types/node": "^20.4.2", "@types/supertest": "^2.0.12" diff --git a/samples/sample-koa/package.json b/samples/sample-koa/package.json index bd3df68c..daccd877 100644 --- a/samples/sample-koa/package.json +++ b/samples/sample-koa/package.json @@ -48,13 +48,14 @@ "@koa/router": "^12.0.0", "koa-bodyparser": "^4.4.1", "@koa/multer": "^3.0.2", + "koa-session": "^6.4.0", + "koa-cookies": "^4.0.2", + "@koa/cors": "^4.0.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", - "@koa/cors": "^4.0.0", "koa-helmet": "^7.0.2", "koa2-swagger-ui": "^5.8.0", "hpp": "^0.2.3", - "minimist": "^1.2.8", "typedi": "^0.10.0", "winston": "^3.10.0" }, diff --git a/servers/express-server/package.json b/servers/express-server/package.json index e2cc345b..f9a47015 100644 --- a/servers/express-server/package.json +++ b/servers/express-server/package.json @@ -35,15 +35,15 @@ "@node-boot/error": "1.0.0", "@node-boot/engine": "1.0.0", "@node-boot/extension": "1.0.0", - "body-parser": "^1.20.2", - "multer": "^1.4.5-lts.1", - "cors": "^2.8.5", - "template-url": "^1.0.0", - "cookie": "^0.6.0", - "express-session": "^1.17.3" + "template-url": "^1.0.0" }, "peerDependencies": { - "express": ">=4.18.2" + "express": ">=4.18.2", + "body-parser": ">=1.20.2", + "multer": ">=1.4.5-lts.1", + "cors": ">=2.8.5", + "cookie": ">=0.6.0", + "express-session": ">=1.17.3" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/servers/fastify-server/package.json b/servers/fastify-server/package.json index 9d4cc24b..66e016ce 100644 --- a/servers/fastify-server/package.json +++ b/servers/fastify-server/package.json @@ -34,15 +34,14 @@ "@node-boot/engine": "1.0.0", "@node-boot/error": "1.0.0", "@node-boot/extension": "1.0.0", - "@fastify/cookie": "^9.0.4", - "@fastify/session": "^10.4.0", - "@fastify/multipart": "^7.7.3", - "@fastify/view": "^8.0.0", - "@fastify/cors": "^8.4.1", - "template-url": "^1.0.0", - "handlebars": "^4.7.8" + "template-url": "^1.0.0" }, "peerDependencies": { - "fastify": ">=4.21.0" + "fastify": ">=4.21.0", + "@fastify/cookie": ">=9.0.4", + "@fastify/session": ">=10.4.0", + "@fastify/multipart": ">=7.7.3", + "@fastify/view": ">=8.0.0", + "@fastify/cors": ">=8.4.1" } } From 71fb1855e8dabe6050cabf04fd0643f87a6a04b0 Mon Sep 17 00:00:00 2001 From: manusant Date: Fri, 29 Dec 2023 03:59:29 +0000 Subject: [PATCH 15/16] update turborepo --- package.json | 2 +- pnpm-lock.yaml | 45 ++++++++++++++++++++++----------------------- 2 files changed, 23 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index 56fe2ab0..0ebddbc5 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "prettier": "^2.8.3", "prettier-plugin-organize-imports": "^3.2.3", "rimraf": "^4.3.1", - "turbo": "^1.10.12", + "turbo": "^1.11.2", "typescript": "^5.1.6" }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 533a33af..09b1f56f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -57,8 +57,8 @@ importers: specifier: ^4.3.1 version: 4.3.1 turbo: - specifier: ^1.10.12 - version: 1.10.12 + specifier: ^1.11.2 + version: 1.11.2 typescript: specifier: ^5.1.6 version: 5.1.6 @@ -6695,65 +6695,64 @@ packages: safe-buffer: 5.2.1 dev: false - /turbo-darwin-64@1.10.12: - resolution: {integrity: sha512-vmDfGVPl5/aFenAbOj3eOx3ePNcWVUyZwYr7taRl0ZBbmv2TzjRiFotO4vrKCiTVnbqjQqAFQWY2ugbqCI1kOQ==} + /turbo-darwin-64@1.11.2: + resolution: {integrity: sha512-toFmRG/adriZY3hOps7nYCfqHAS+Ci6xqgX3fbo82kkLpC6OBzcXnleSwuPqjHVAaRNhVoB83L5njcE9Qwi2og==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.12: - resolution: {integrity: sha512-3JliEESLNX2s7g54SOBqqkqJ7UhcOGkS0ywMr5SNuvF6kWVTbuUq7uBU/sVbGq8RwvK1ONlhPvJne5MUqBCTCQ==} + /turbo-darwin-arm64@1.11.2: + resolution: {integrity: sha512-FCsEDZ8BUSFYEOSC3rrARQrj7x2VOrmVcfrMUIhexTxproRh4QyMxLfr6LALk4ymx6jbDCxWa6Szal8ckldFbA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.12: - resolution: {integrity: sha512-siYhgeX0DidIfHSgCR95b8xPee9enKSOjCzx7EjTLmPqPaCiVebRYvbOIYdQWRqiaKh9yfhUtFmtMOMScUf1gg==} + /turbo-linux-64@1.11.2: + resolution: {integrity: sha512-Vzda/o/QyEske5CxLf0wcu7UUS+7zB90GgHZV4tyN+WZtoouTvbwuvZ3V6b5Wgd3OJ/JwWR0CXDK7Sf4VEMr7A==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.12: - resolution: {integrity: sha512-K/ZhvD9l4SslclaMkTiIrnfcACgos79YcAo4kwc8bnMQaKuUeRpM15sxLpZp3xDjDg8EY93vsKyjaOhdFG2UbA==} + /turbo-linux-arm64@1.11.2: + resolution: {integrity: sha512-bRLwovQRz0yxDZrM4tQEAYV0fBHEaTzUF0JZ8RG1UmZt/CqtpnUrJpYb1VK8hj1z46z9YehARpYCwQ2K0qU4yw==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.12: - resolution: {integrity: sha512-7FSgSwvktWDNOqV65l9AbZwcoueAILeE4L7JvjauNASAjjbuzXGCEq5uN8AQU3U5BOFj4TdXrVmO2dX+lLu8Zg==} + /turbo-windows-64@1.11.2: + resolution: {integrity: sha512-LgTWqkHAKgyVuLYcEPxZVGPInTjjeCnN5KQMdJ4uQZ+xMDROvMFS2rM93iQl4ieDJgidwHCxxCxaU9u8c3d/Kg==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.12: - resolution: {integrity: sha512-gCNXF52dwom1HLY9ry/cneBPOKTBHhzpqhMylcyvJP0vp9zeMQQkt6yjYv+6QdnmELC92CtKNp2FsNZo+z0pyw==} + /turbo-windows-arm64@1.11.2: + resolution: {integrity: sha512-829aVBU7IX0c/B4G7g1VI8KniAGutHhIupkYMgF6xPkYVev2G3MYe6DMS/vsLt9GGM9ulDtdWxWrH5P2ngK8IQ==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.12: - resolution: {integrity: sha512-WM3+jTfQWnB9W208pmP4oeehZcC6JQNlydb/ZHMRrhmQa+htGhWLCzd6Q9rLe0MwZLPpSPFV2/bN5egCLyoKjQ==} + /turbo@1.11.2: + resolution: {integrity: sha512-jPC7LVQJzebs5gWf8FmEvsvXGNyKbN+O9qpvv98xpNaM59aS0/Irhd0H0KbcqnXfsz7ETlzOC3R+xFWthC4Z8A==} hasBin: true - requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.12 - turbo-darwin-arm64: 1.10.12 - turbo-linux-64: 1.10.12 - turbo-linux-arm64: 1.10.12 - turbo-windows-64: 1.10.12 - turbo-windows-arm64: 1.10.12 + turbo-darwin-64: 1.11.2 + turbo-darwin-arm64: 1.11.2 + turbo-linux-64: 1.11.2 + turbo-linux-arm64: 1.11.2 + turbo-windows-64: 1.11.2 + turbo-windows-arm64: 1.11.2 dev: true /type-check@0.4.0: From e7178447780b0aa255faa1781b06a50a03747de6 Mon Sep 17 00:00:00 2001 From: OEHASAN Date: Sat, 30 Dec 2023 01:00:16 +0100 Subject: [PATCH 16/16] feature: enable strict typescript features, code clean-up and remove unused params --- package.json | 2 +- packages/authorization/jest.config.js | 1 - packages/authorization/package.json | 4 +- .../src/decorator/EnableAuthorization.ts | 2 +- packages/config/jest.config.js | 1 - packages/config/package.json | 2 +- packages/context/jest.config.js | 1 - packages/context/package.json | 2 +- packages/core/jest.config.js | 1 - packages/core/package.json | 2 +- .../core/src/decorators/Configurations.ts | 2 +- packages/core/src/decorators/Controllers.ts | 2 +- .../src/decorators/EnableAutoConfiguration.ts | 37 ------------- .../src/decorators/EnableClassTransformer.ts | 6 +-- .../src/decorators/EnableComponentScan.ts | 2 +- .../core/src/decorators/GlobalMiddlewares.ts | 2 +- packages/core/src/decorators/Interceptors.ts | 2 +- packages/core/src/decorators/index.ts | 1 - packages/di/jest.config.js | 1 - packages/di/package.json | 2 +- packages/di/src/decorators/EnableDI.ts | 2 +- packages/di/src/decorators/Inject.ts | 2 - packages/engine/jest.config.js | 1 - packages/engine/package.json | 4 +- packages/engine/src/core/NodeBootDriver.ts | 2 +- packages/error/jest.config.js | 1 - packages/error/package.json | 4 +- packages/extension/jest.config.js | 1 - packages/extension/package.json | 2 +- packages/extension/src/Optional.ts | 4 +- packages/openapi/jest.config.js | 1 - packages/openapi/package.json | 2 +- .../openapi/src/adapter/ExpressOpenApi.ts | 3 +- .../openapi/src/adapter/FastifyOpenApi.ts | 2 +- .../openapi/src/decorator/EnableOpenApi.ts | 6 +-- packages/openapi/src/decorator/OpenAPI.ts | 2 +- pnpm-lock.yaml | 14 ++++- samples/sample-express/jest.config.js | 1 - samples/sample-express/package.json | 2 +- .../src/auth/DefaultAuthorizationResolver.ts | 4 +- .../src/config/SecurityConfiguration.ts | 2 +- .../src/middlewares/ErrorMiddleware.ts | 2 +- .../src/middlewares/LoggingMiddleware.ts | 2 +- .../src/middlewares/validation.middleware.ts | 2 +- .../listeners/GlobalEntityEventListener.ts | 12 ++--- .../src/services/users.service.ts | 2 +- samples/sample-fastify/jest.config.js | 1 - samples/sample-fastify/package.json | 2 +- .../src/auth/DefaultAuthorizationResolver.ts | 5 +- .../src/middlewares/LoggingMiddleware.ts | 2 +- .../listeners/GlobalEntityEventListener.ts | 12 ++--- .../src/services/users.service.ts | 2 +- samples/sample-koa/jest.config.js | 1 - samples/sample-koa/package.json | 2 +- .../src/auth/DefaultAuthorizationChecker.ts | 8 +-- .../src/config/BackendConfigProperties.ts | 10 ++++ .../src/config/SecurityConfiguration.ts | 2 +- samples/sample-koa/src/dtos/users.dto.ts | 6 +-- .../src/middlewares/CustomErrorHandler.ts | 2 +- .../src/middlewares/LoggingMiddleware.ts | 4 +- .../src/middlewares/validation.middleware.ts | 12 +++-- .../listeners/GlobalEntityEventListener.ts | 48 ++++++++--------- .../listeners/UserEntityEventListener.ts | 10 ++-- .../sample-koa/src/services/users.service.ts | 2 +- servers/express-server/jest.config.js | 1 - servers/express-server/package.json | 2 +- .../src/driver/ExpressDriver.ts | 14 +++-- servers/fastify-server/jest.config.js | 1 - servers/fastify-server/package.json | 2 +- .../src/driver/FastifyDriver.ts | 2 +- servers/koa-server/jest.config.js | 1 - servers/koa-server/package.json | 2 +- servers/koa-server/src/driver/KoaDriver.ts | 4 +- starters/actuator/jest.config.js | 1 - starters/actuator/package.json | 13 ++--- .../src/adapter/DefaultActuatorAdapter.ts | 3 -- .../src/adapter/ExpressActuatorAdapter.ts | 28 +++++----- .../src/adapter/FastifyActuatorAdapter.ts | 22 ++++---- .../src/adapter/KoaActuatorAdapter.ts | 2 +- .../actuator/src/decorator/EnableActuator.ts | 2 +- starters/persistence/jest.config.js | 1 - starters/persistence/package.json | 2 +- .../src/adapter/PersistenceLogger.ts | 3 +- .../src/config/PersistenceConfiguration.ts | 4 +- .../src/decorator/DatasourceConfiguration.ts | 2 +- .../src/decorator/EnableRepositories.ts | 2 +- tsconfig.base.json | 52 ++++++++----------- 87 files changed, 203 insertions(+), 251 deletions(-) delete mode 100644 packages/core/src/decorators/EnableAutoConfiguration.ts create mode 100644 samples/sample-koa/src/config/BackendConfigProperties.ts diff --git a/package.json b/package.json index 0ebddbc5..de1de200 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "pnpm": ">=7.5.1" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "turbo run build", "build:clean": "turbo run clean:build", diff --git a/packages/authorization/jest.config.js b/packages/authorization/jest.config.js index ef8af537..84f52636 100644 --- a/packages/authorization/jest.config.js +++ b/packages/authorization/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/authorization/package.json b/packages/authorization/package.json index 1121cf4c..dae4eb9e 100644 --- a/packages/authorization/package.json +++ b/packages/authorization/package.json @@ -15,8 +15,8 @@ "publishConfig": { "access": "public" }, - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "src/index.js", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/authorization/src/decorator/EnableAuthorization.ts b/packages/authorization/src/decorator/EnableAuthorization.ts index a0d6d868..6335ff6c 100644 --- a/packages/authorization/src/decorator/EnableAuthorization.ts +++ b/packages/authorization/src/decorator/EnableAuthorization.ts @@ -10,7 +10,7 @@ export function EnableAuthorization( CurrentUserCheckerClass?: new () => CurrentUserChecker, AuthorizationCheckerClass?: new () => AuthorizationChecker, ): Function { - return function (object: Function) { + return function () { if (AuthorizationCheckerClass) { ApplicationContext.get().authorizationChecker = new AuthorizationCheckerClass(); } diff --git a/packages/config/jest.config.js b/packages/config/jest.config.js index ef8af537..84f52636 100644 --- a/packages/config/jest.config.js +++ b/packages/config/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/config/package.json b/packages/config/package.json index 25e5d960..e0ccc147 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/context/jest.config.js b/packages/context/jest.config.js index ef8af537..84f52636 100644 --- a/packages/context/jest.config.js +++ b/packages/context/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/context/package.json b/packages/context/package.json index 95d74aee..8fc87b0b 100644 --- a/packages/context/package.json +++ b/packages/context/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/core/jest.config.js b/packages/core/jest.config.js index ef8af537..84f52636 100644 --- a/packages/core/jest.config.js +++ b/packages/core/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/core/package.json b/packages/core/package.json index 077e2b25..23a26a23 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/core/src/decorators/Configurations.ts b/packages/core/src/decorators/Configurations.ts index 99d02747..e8e34337 100644 --- a/packages/core/src/decorators/Configurations.ts +++ b/packages/core/src/decorators/Configurations.ts @@ -1,5 +1,5 @@ export function Configurations(configurationClasses: (new (...args: any[]) => any)[]): Function { - return function (target: any) { + return function () { configurationClasses.map(ClassConstructor => new ClassConstructor()); }; } diff --git a/packages/core/src/decorators/Controllers.ts b/packages/core/src/decorators/Controllers.ts index be1d5fec..cef0d739 100644 --- a/packages/core/src/decorators/Controllers.ts +++ b/packages/core/src/decorators/Controllers.ts @@ -1,7 +1,7 @@ import {ApplicationContext} from "@node-boot/context"; export function Controllers(controllers: Function[]): Function { - return function (target: any) { + return function () { ApplicationContext.get().controllerClasses = controllers; }; } diff --git a/packages/core/src/decorators/EnableAutoConfiguration.ts b/packages/core/src/decorators/EnableAutoConfiguration.ts deleted file mode 100644 index 8e994fc7..00000000 --- a/packages/core/src/decorators/EnableAutoConfiguration.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as path from "path"; -import * as fs from "fs"; - -function getProjectRootDirectory(): string { - let currentDir = __dirname; - while (!fs.existsSync(path.join(currentDir, "package.json"))) { - // Navigate up one directory level - currentDir = path.dirname(currentDir); - } - return currentDir; -} - -async function instantiateClasses(rootDir, classes) { - for (const classData of classes) { - const {class: className, path: classPath, arguments: classArguments} = classData; - - // Use dynamic import to handle the class asynchronously. - const module = require(path.join(rootDir, classPath)); - const Class = module[className]; - - const argumentsMetadata = Reflect.getMetadata("design:paramtypes", Class); - - // Check if the class has any constructor arguments (dependencies). - if (argumentsMetadata && argumentsMetadata.length > 0) { - const dependencies = argumentsMetadata.map((argType, index) => { - const argValue = classArguments ? classArguments[index] : undefined; - return typeof argValue !== "undefined" ? argValue : new argType(); - }); - - const instance = new Class(...dependencies); - // Optionally, you can store the instances in a container for later use if needed. - } else { - const instance = new Class(); - // Optionally, you can store the instances in a container for later use if needed. - } - } -} diff --git a/packages/core/src/decorators/EnableClassTransformer.ts b/packages/core/src/decorators/EnableClassTransformer.ts index a03f5d4c..372059f2 100644 --- a/packages/core/src/decorators/EnableClassTransformer.ts +++ b/packages/core/src/decorators/EnableClassTransformer.ts @@ -2,19 +2,19 @@ import {ApplicationContext, TransformerOptions} from "@node-boot/context"; import {ClassTransformOptions} from "class-transformer"; export function ClassToPlainTransform(options: ClassTransformOptions): Function { - return function (target: Function) { + return function () { ApplicationContext.get().classToPlainTransformOptions = options; }; } export function PlainToClassTransform(options: ClassTransformOptions): Function { - return function (target: Function) { + return function () { ApplicationContext.get().plainToClassTransformOptions = options; }; } export function EnableClassTransformer(options?: TransformerOptions): Function { - return function (target: Function) { + return function () { ApplicationContext.get().classTransformer = options?.enabled ?? true; ApplicationContext.get().classToPlainTransformOptions = options?.classToPlain; diff --git a/packages/core/src/decorators/EnableComponentScan.ts b/packages/core/src/decorators/EnableComponentScan.ts index 11502f05..64f1be1f 100644 --- a/packages/core/src/decorators/EnableComponentScan.ts +++ b/packages/core/src/decorators/EnableComponentScan.ts @@ -2,7 +2,7 @@ import {ApplicationContext, ComponentScanOptions} from "@node-boot/context"; import path from "path"; export function EnableComponentScan(options?: ComponentScanOptions): Function { - return function (object: Function) { + return function () { options = options ?? { controllerPaths: ["/controllers"], middlewarePaths: ["/middlewares"], diff --git a/packages/core/src/decorators/GlobalMiddlewares.ts b/packages/core/src/decorators/GlobalMiddlewares.ts index 19f49f6f..dd78e799 100644 --- a/packages/core/src/decorators/GlobalMiddlewares.ts +++ b/packages/core/src/decorators/GlobalMiddlewares.ts @@ -1,7 +1,7 @@ import {ApplicationContext} from "@node-boot/context"; export function GlobalMiddlewares(middlewares: Function[]): Function { - return function (target: any) { + return function () { ApplicationContext.get().globalMiddlewares = middlewares; }; } diff --git a/packages/core/src/decorators/Interceptors.ts b/packages/core/src/decorators/Interceptors.ts index 414c5355..2144de18 100644 --- a/packages/core/src/decorators/Interceptors.ts +++ b/packages/core/src/decorators/Interceptors.ts @@ -1,7 +1,7 @@ import {ApplicationContext} from "@node-boot/context"; export function Interceptors(interceptors: Function[]): Function { - return function (target: any) { + return function () { ApplicationContext.get().interceptorClasses = interceptors; }; } diff --git a/packages/core/src/decorators/index.ts b/packages/core/src/decorators/index.ts index 863e77ed..b61d3d58 100644 --- a/packages/core/src/decorators/index.ts +++ b/packages/core/src/decorators/index.ts @@ -12,7 +12,6 @@ export * from "./CookieParam"; export * from "./CookieParams"; export * from "./Ctx"; export * from "./Delete"; -export * from "./EnableAutoConfiguration"; export * from "./EnableClassTransformer"; export * from "./EnableComponentScan"; export * from "./ErrorHandler"; diff --git a/packages/di/jest.config.js b/packages/di/jest.config.js index ef8af537..84f52636 100644 --- a/packages/di/jest.config.js +++ b/packages/di/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/di/package.json b/packages/di/package.json index 08c357ef..5474e47b 100644 --- a/packages/di/package.json +++ b/packages/di/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/di/src/decorators/EnableDI.ts b/packages/di/src/decorators/EnableDI.ts index d288d783..d72d427d 100644 --- a/packages/di/src/decorators/EnableDI.ts +++ b/packages/di/src/decorators/EnableDI.ts @@ -7,7 +7,7 @@ import {ApplicationContext, IocContainer, UseContainerOptions} from "@node-boot/ * @param options Extra options for the IOC container */ export function EnableDI(iocContainer: IocContainer, options?: UseContainerOptions): Function { - return function (object: Function) { + return function () { ApplicationContext.get().diOptions = { iocContainer, options, diff --git a/packages/di/src/decorators/Inject.ts b/packages/di/src/decorators/Inject.ts index 9af7dc02..1813f88e 100644 --- a/packages/di/src/decorators/Inject.ts +++ b/packages/di/src/decorators/Inject.ts @@ -16,12 +16,10 @@ export function Inject(options?: InjectionOptions): Function { return (target: Object, propertyName: string | Symbol, index?: number) => { // Registering metadata for custom filed injection (used for example in the Persistence Event Subscribers) if (propertyName && typeof propertyName === "string") { - const propertyType = Reflect.getMetadata("design:type", target, propertyName); const injectProperties: string[] = Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, target) || []; injectProperties.push(propertyName); Reflect.defineMetadata(REQUIRES_FIELD_INJECTION_KEY, injectProperties, target); } - // Normal injection decorateInjection(target, propertyName, index, options); }; diff --git a/packages/engine/jest.config.js b/packages/engine/jest.config.js index ef8af537..84f52636 100644 --- a/packages/engine/jest.config.js +++ b/packages/engine/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/engine/package.json b/packages/engine/package.json index 74fb8b76..a0faafde 100644 --- a/packages/engine/package.json +++ b/packages/engine/package.json @@ -16,8 +16,8 @@ "publishConfig": { "access": "public" }, - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "src/index.js", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/engine/src/core/NodeBootDriver.ts b/packages/engine/src/core/NodeBootDriver.ts index 865eea4d..6d5bb72e 100644 --- a/packages/engine/src/core/NodeBootDriver.ts +++ b/packages/engine/src/core/NodeBootDriver.ts @@ -79,7 +79,7 @@ export abstract class NodeBootDriver { */ currentUserChecker?: CurrentUserChecker; - protected transformResult(result: any, actionMetadata: ActionMetadata, action: TAction): any { + protected transformResult(result: any, actionMetadata: ActionMetadata): T { // check if we need to transform result const shouldTransform = this.useClassTransformer && // transform only if class-transformer is enabled diff --git a/packages/error/jest.config.js b/packages/error/jest.config.js index ef8af537..84f52636 100644 --- a/packages/error/jest.config.js +++ b/packages/error/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/error/package.json b/packages/error/package.json index 196b9b69..f289150a 100644 --- a/packages/error/package.json +++ b/packages/error/package.json @@ -15,8 +15,8 @@ "publishConfig": { "access": "public" }, - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "src/index.js", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/extension/jest.config.js b/packages/extension/jest.config.js index ef8af537..84f52636 100644 --- a/packages/extension/jest.config.js +++ b/packages/extension/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/extension/package.json b/packages/extension/package.json index 79c8aa3a..6de12a7e 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/extension/src/Optional.ts b/packages/extension/src/Optional.ts index 522fac6d..66266962 100644 --- a/packages/extension/src/Optional.ts +++ b/packages/extension/src/Optional.ts @@ -196,11 +196,11 @@ export class Optional { return Optional.of(this.value.filter(predicate) as T); } else if (this.value instanceof Map || this.value instanceof Set) { // Filter map or set - const filteredEntries = Array.from(this.value.entries()).filter(([key, value]) => predicate(value)); + const filteredEntries = Array.from(this.value.entries()).filter(([, value]) => predicate(value)); if (this.value instanceof Map) { return Optional.of(new Map(filteredEntries) as T); } else { - return Optional.of(new Set(filteredEntries.map(([key, value]) => value)) as T); + return Optional.of(new Set(filteredEntries.map(([, value]) => value)) as T); } } else { // Filter single value diff --git a/packages/openapi/jest.config.js b/packages/openapi/jest.config.js index ef8af537..84f52636 100644 --- a/packages/openapi/jest.config.js +++ b/packages/openapi/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/packages/openapi/package.json b/packages/openapi/package.json index f14b2761..b229c836 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/packages/openapi/src/adapter/ExpressOpenApi.ts b/packages/openapi/src/adapter/ExpressOpenApi.ts index 9f1800ab..1ac29c94 100644 --- a/packages/openapi/src/adapter/ExpressOpenApi.ts +++ b/packages/openapi/src/adapter/ExpressOpenApi.ts @@ -1,13 +1,14 @@ import {OpenApiAdapter, OpenApiOptions} from "@node-boot/context"; import swaggerUi from "swagger-ui-express"; import {OpenApiSpecAdapter} from "./OpenApiSpecAdapter"; +import {Response} from "express"; export class ExpressOpenApi implements OpenApiAdapter { bind(openApiOptions: OpenApiOptions, server: any, router: any): void { if (swaggerUi?.serve) { const {spec, options} = OpenApiSpecAdapter.adapt(openApiOptions); - router.get(options.swaggerOptions.url, (req, res) => res.json(spec)); + router.get(options.swaggerOptions.url, (_: never, res: Response) => res.json(spec)); server.use("/api-docs", swaggerUi.serve, swaggerUi.setup(spec, options)); } else { throw new Error( diff --git a/packages/openapi/src/adapter/FastifyOpenApi.ts b/packages/openapi/src/adapter/FastifyOpenApi.ts index 9a1f2001..23855965 100644 --- a/packages/openapi/src/adapter/FastifyOpenApi.ts +++ b/packages/openapi/src/adapter/FastifyOpenApi.ts @@ -6,7 +6,7 @@ export class FastifyOpenApi implements OpenApiAdapter { bind(openApiOptions: OpenApiOptions, server: FastifyInstance, router: FastifyInstance): void { const {spec, options} = OpenApiSpecAdapter.adapt(openApiOptions); - router.get(options.swaggerOptions.url, async (request, reply) => { + router.get(options.swaggerOptions.url, async (_, reply) => { reply.send(spec); }); server.register(require("@fastify/swagger"), { diff --git a/packages/openapi/src/decorator/EnableOpenApi.ts b/packages/openapi/src/decorator/EnableOpenApi.ts index ee88a2f6..65ccca1a 100644 --- a/packages/openapi/src/decorator/EnableOpenApi.ts +++ b/packages/openapi/src/decorator/EnableOpenApi.ts @@ -7,10 +7,10 @@ import {FastifyOpenApi} from "../adapter/FastifyOpenApi"; /** * Defines the configurations to enable Swagger Open API * - * @param openApi The OpenAPI definitions and base config + * @param _ The OpenAPI definitions and base config */ -export function EnableOpenApi(openApi: Partial = {}): Function { - return function (object: Function) { +export function EnableOpenApi(_: Partial = {}): Function { + return function () { ApplicationContext.get().openApi = new (class implements OpenApiBridgeAdapter { bind(serverType: string): OpenApiAdapter { switch (serverType) { diff --git a/packages/openapi/src/decorator/OpenAPI.ts b/packages/openapi/src/decorator/OpenAPI.ts index c4d3744d..297223bd 100644 --- a/packages/openapi/src/decorator/OpenAPI.ts +++ b/packages/openapi/src/decorator/OpenAPI.ts @@ -11,7 +11,7 @@ import {OpenAPI as InnerOpenAPI} from "routing-controllers-openapi"; * returning an updated Operation. */ export function OpenAPI(...args: Parameters) { - return (...innerArgs: [Function] | [object, string, PropertyDescriptor]) => { + return (...innerArgs: [Function] | [object, string, PropertyDescriptor]) => { InnerOpenAPI(...args)(...innerArgs); }; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09b1f56f..3a7832e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -724,6 +724,9 @@ importers: specifier: '>=2.14.2' version: 2.14.2 devDependencies: + '@types/express': + specifier: ^4.17.21 + version: 4.17.21 '@types/koa': specifier: ^2.13.11 version: 2.13.11 @@ -1981,7 +1984,7 @@ packages: resolution: {integrity: sha512-h7BcvPUogWbKCzBR2lY4oqaZbO3jXZksexYJVFvkrFeLgbZjQkU4x8pRq6eg2MHXQhY0McQdqmmsxRWlVAHooA==} dependencies: '@types/connect': 3.4.35 - '@types/express': 4.17.17 + '@types/express': 4.17.21 '@types/keygrip': 1.0.2 '@types/node': 18.15.3 @@ -2018,6 +2021,15 @@ packages: '@types/express-serve-static-core': 4.17.35 '@types/qs': 6.9.7 '@types/serve-static': 1.15.2 + dev: true + + /@types/express@4.17.21: + resolution: {integrity: sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==} + dependencies: + '@types/body-parser': 1.19.2 + '@types/express-serve-static-core': 4.17.35 + '@types/qs': 6.9.7 + '@types/serve-static': 1.15.2 /@types/graceful-fs@4.1.6: resolution: {integrity: sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==} diff --git a/samples/sample-express/jest.config.js b/samples/sample-express/jest.config.js index ef8af537..84f52636 100644 --- a/samples/sample-express/jest.config.js +++ b/samples/sample-express/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/samples/sample-express/package.json b/samples/sample-express/package.json index f64bef75..d7937d21 100644 --- a/samples/sample-express/package.json +++ b/samples/sample-express/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "create:migration": "typeorm migration:create ./src/persistence/migrations/migration", "start": "pnpm run build && node dist/server.js", diff --git a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts index fdb595a0..bc3cf5ef 100644 --- a/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-express/src/auth/DefaultAuthorizationResolver.ts @@ -9,7 +9,7 @@ export class DefaultAuthorizationResolver implements AuthorizationChecker, roles: string[]): Promise { + async check(_: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check @@ -18,8 +18,6 @@ export class DefaultAuthorizationResolver implements AuthorizationChecker) { + public security({application}: BeansContext) { application.use(hpp()); application.use(helmet()); application.disable("x-powered-by"); diff --git a/samples/sample-express/src/middlewares/ErrorMiddleware.ts b/samples/sample-express/src/middlewares/ErrorMiddleware.ts index c46490dd..5fd02e08 100644 --- a/samples/sample-express/src/middlewares/ErrorMiddleware.ts +++ b/samples/sample-express/src/middlewares/ErrorMiddleware.ts @@ -11,7 +11,7 @@ export class ErrorMiddleware implements ErrorHandlerInterface): any { - const {request, response, next} = action; + const {request, next} = action; try { const status: number = error.httpCode || 500; const message: string = error.message || "Something went wrong"; diff --git a/samples/sample-express/src/middlewares/LoggingMiddleware.ts b/samples/sample-express/src/middlewares/LoggingMiddleware.ts index 372b7ead..166e405e 100644 --- a/samples/sample-express/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-express/src/middlewares/LoggingMiddleware.ts @@ -10,7 +10,7 @@ export class LoggingMiddleware implements MiddlewareInterface private logger: Logger; use(action: Action): any { - const {request, response, next} = action; + const {next} = action; this.logger.info(`Logging Middleware: Incoming request`); next?.(); } diff --git a/samples/sample-express/src/middlewares/validation.middleware.ts b/samples/sample-express/src/middlewares/validation.middleware.ts index f7957759..6173fd3e 100644 --- a/samples/sample-express/src/middlewares/validation.middleware.ts +++ b/samples/sample-express/src/middlewares/validation.middleware.ts @@ -17,7 +17,7 @@ export const ValidationMiddleware = ( whitelist = false, forbidNonWhitelisted = false, ) => { - return (req: Request, res: Response, next: NextFunction) => { + return (req: Request, _: Response, next: NextFunction) => { const dto = plainToInstance(type, req.body); validateOrReject(dto, { skipMissingProperties, diff --git a/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts index 71a478c8..d52c5d3f 100644 --- a/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts +++ b/samples/sample-express/src/persistence/listeners/GlobalEntityEventListener.ts @@ -98,42 +98,42 @@ export class GlobalEntityEventListener implements EntitySubscriberInterface { /** * Called before transaction start. */ - beforeTransactionStart(event: TransactionStartEvent) { + beforeTransactionStart(_: TransactionStartEvent) { this.logger.info(`BEFORE TRANSACTION STARTED`); } /** * Called after transaction start. */ - afterTransactionStart(event: TransactionStartEvent) { + afterTransactionStart(_: TransactionStartEvent) { this.logger.info(`AFTER TRANSACTION STARTED`); } /** * Called before transaction commit. */ - beforeTransactionCommit(event: TransactionCommitEvent) { + beforeTransactionCommit(_: TransactionCommitEvent) { this.logger.info(`BEFORE TRANSACTION COMMITTED`); } /** * Called after transaction commit. */ - afterTransactionCommit(event: TransactionCommitEvent) { + afterTransactionCommit(_: TransactionCommitEvent) { this.logger.info(`AFTER TRANSACTION COMMITTED`); } /** * Called before transaction rollback. */ - beforeTransactionRollback(event: TransactionRollbackEvent) { + beforeTransactionRollback(_: TransactionRollbackEvent) { this.logger.info(`BEFORE TRANSACTION ROLLBACK`); } /** * Called after transaction rollback. */ - afterTransactionRollback(event: TransactionRollbackEvent) { + afterTransactionRollback(_: TransactionRollbackEvent) { this.logger.info(`AFTER TRANSACTION ROLLBACK`); } } diff --git a/samples/sample-express/src/services/users.service.ts b/samples/sample-express/src/services/users.service.ts index a41f690e..6e02c56f 100644 --- a/samples/sample-express/src/services/users.service.ts +++ b/samples/sample-express/src/services/users.service.ts @@ -84,7 +84,7 @@ export class UserService { await Optional.of(user) .orElseThrow(() => new HttpError(409, "User doesn't exist")) - .runAsync(user => this.userRepository.delete({id: userId})); + .runAsync(() => this.userRepository.delete({id: userId})); throw new Error("Error after deleting that should rollback transaction"); } diff --git a/samples/sample-fastify/jest.config.js b/samples/sample-fastify/jest.config.js index ef8af537..84f52636 100644 --- a/samples/sample-fastify/jest.config.js +++ b/samples/sample-fastify/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/samples/sample-fastify/package.json b/samples/sample-fastify/package.json index 1cc88ddf..a2fdbb29 100644 --- a/samples/sample-fastify/package.json +++ b/samples/sample-fastify/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "start": "pnpm run build && node dist/server.js", "start:prod": "pnpm run build && NODE_ENV=production node dist/server.js", diff --git a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts index 2b22a17e..28e37e92 100644 --- a/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts +++ b/samples/sample-fastify/src/auth/DefaultAuthorizationResolver.ts @@ -10,7 +10,7 @@ export class DefaultAuthorizationResolver implements AuthorizationChecker, roles: string[]): Promise { + async check(_: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check @@ -18,9 +18,6 @@ export class DefaultAuthorizationResolver implements AuthorizationChecker, payload?: any): void { + use(_: Action): void { this.logger.info(`Logging Middleware: Incoming request`); //done(); } diff --git a/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts index 71a478c8..d52c5d3f 100644 --- a/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts +++ b/samples/sample-fastify/src/persistence/listeners/GlobalEntityEventListener.ts @@ -98,42 +98,42 @@ export class GlobalEntityEventListener implements EntitySubscriberInterface { /** * Called before transaction start. */ - beforeTransactionStart(event: TransactionStartEvent) { + beforeTransactionStart(_: TransactionStartEvent) { this.logger.info(`BEFORE TRANSACTION STARTED`); } /** * Called after transaction start. */ - afterTransactionStart(event: TransactionStartEvent) { + afterTransactionStart(_: TransactionStartEvent) { this.logger.info(`AFTER TRANSACTION STARTED`); } /** * Called before transaction commit. */ - beforeTransactionCommit(event: TransactionCommitEvent) { + beforeTransactionCommit(_: TransactionCommitEvent) { this.logger.info(`BEFORE TRANSACTION COMMITTED`); } /** * Called after transaction commit. */ - afterTransactionCommit(event: TransactionCommitEvent) { + afterTransactionCommit(_: TransactionCommitEvent) { this.logger.info(`AFTER TRANSACTION COMMITTED`); } /** * Called before transaction rollback. */ - beforeTransactionRollback(event: TransactionRollbackEvent) { + beforeTransactionRollback(_: TransactionRollbackEvent) { this.logger.info(`BEFORE TRANSACTION ROLLBACK`); } /** * Called after transaction rollback. */ - afterTransactionRollback(event: TransactionRollbackEvent) { + afterTransactionRollback(_: TransactionRollbackEvent) { this.logger.info(`AFTER TRANSACTION ROLLBACK`); } } diff --git a/samples/sample-fastify/src/services/users.service.ts b/samples/sample-fastify/src/services/users.service.ts index bc546b63..666f291d 100644 --- a/samples/sample-fastify/src/services/users.service.ts +++ b/samples/sample-fastify/src/services/users.service.ts @@ -83,7 +83,7 @@ export class UserService { await Optional.of(user) .orElseThrow(() => new HttpError(409, "User doesn't exist")) - .runAsync(user => this.userRepository.delete({id: userId})); + .runAsync(() => this.userRepository.delete({id: userId})); throw new Error("Error after deleting that should rollback transaction"); } diff --git a/samples/sample-koa/jest.config.js b/samples/sample-koa/jest.config.js index ef8af537..84f52636 100644 --- a/samples/sample-koa/jest.config.js +++ b/samples/sample-koa/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/samples/sample-koa/package.json b/samples/sample-koa/package.json index daccd877..1c866414 100644 --- a/samples/sample-koa/package.json +++ b/samples/sample-koa/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "start": "pnpm run build && node dist/server.js", "start:prod": "pnpm run build && NODE_ENV=production node dist/server.js", diff --git a/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts b/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts index ac1dab01..aeb7cc22 100644 --- a/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts +++ b/samples/sample-koa/src/auth/DefaultAuthorizationChecker.ts @@ -9,7 +9,7 @@ export class DefaultAuthorizationChecker implements AuthorizationChecker, roles: string[]): Promise { + async check(_: Action, roles: string[]): Promise { // here you can use request/response objects from action // also if decorator defines roles it needs to access the action // you can use them to provide granular access check @@ -18,14 +18,10 @@ export class DefaultAuthorizationChecker implements AuthorizationChecker user.roles.indexOf(role) !== -1)) return true; - return false; + return !!(user && roles.find(role => user.roles.indexOf(role) !== -1)); } } diff --git a/samples/sample-koa/src/config/BackendConfigProperties.ts b/samples/sample-koa/src/config/BackendConfigProperties.ts new file mode 100644 index 00000000..61df9bab --- /dev/null +++ b/samples/sample-koa/src/config/BackendConfigProperties.ts @@ -0,0 +1,10 @@ +import {ConfigurationProperties} from "@node-boot/config"; + +@ConfigurationProperties({ + configPath: "backend", + configName: "backend-config", +}) +export class BackendConfigProperties { + baseUrl!: string; + allowInsecureCookie!: boolean; +} diff --git a/samples/sample-koa/src/config/SecurityConfiguration.ts b/samples/sample-koa/src/config/SecurityConfiguration.ts index 932a386f..938f943a 100644 --- a/samples/sample-koa/src/config/SecurityConfiguration.ts +++ b/samples/sample-koa/src/config/SecurityConfiguration.ts @@ -7,7 +7,7 @@ import cors from "@koa/cors"; @Configuration() export class SecurityConfiguration { @Bean() - public security({application, iocContainer}: BeansContext) { + public security({application}: BeansContext) { application.use( helmet({ // FIXME - Disabling contentSecurityPolicy if @EnableOpenApi is applied diff --git a/samples/sample-koa/src/dtos/users.dto.ts b/samples/sample-koa/src/dtos/users.dto.ts index 72edaaeb..b19e57d4 100644 --- a/samples/sample-koa/src/dtos/users.dto.ts +++ b/samples/sample-koa/src/dtos/users.dto.ts @@ -2,13 +2,13 @@ import {IsEmail, IsString, IsNotEmpty, MinLength, MaxLength} from "class-validat export class CreateUserDto { @IsEmail() - public email: string; + public email!: string; @IsString() @IsNotEmpty() @MinLength(9) @MaxLength(32) - public password: string; + public password!: string; } export class UpdateUserDto { @@ -16,5 +16,5 @@ export class UpdateUserDto { @IsNotEmpty() @MinLength(9) @MaxLength(32) - public password: string; + public password!: string; } diff --git a/samples/sample-koa/src/middlewares/CustomErrorHandler.ts b/samples/sample-koa/src/middlewares/CustomErrorHandler.ts index df82dddc..bb41256f 100644 --- a/samples/sample-koa/src/middlewares/CustomErrorHandler.ts +++ b/samples/sample-koa/src/middlewares/CustomErrorHandler.ts @@ -11,7 +11,7 @@ export class CustomErrorHandler implements ErrorHandlerInterface): void { - const {request, response, next} = action; + const {request, next} = action; try { const status: number = error.httpCode || 500; const message: string = error.message || "Something went wrong"; diff --git a/samples/sample-koa/src/middlewares/LoggingMiddleware.ts b/samples/sample-koa/src/middlewares/LoggingMiddleware.ts index 709d1309..7a288960 100644 --- a/samples/sample-koa/src/middlewares/LoggingMiddleware.ts +++ b/samples/sample-koa/src/middlewares/LoggingMiddleware.ts @@ -9,7 +9,7 @@ export class LoggingMiddleware implements MiddlewareInterface @Inject() private logger: Logger; - async use(action: Action, payload?: unknown): Promise { + async use(action: Action): Promise { this.logger.info(`Logging Middleware: Incoming request`); action.context; return action @@ -18,7 +18,7 @@ export class LoggingMiddleware implements MiddlewareInterface console.log("do something after execution"); }) .catch(error => { - console.log("error handling is also here"); + console.log("error handling is also here", error); }); } } diff --git a/samples/sample-koa/src/middlewares/validation.middleware.ts b/samples/sample-koa/src/middlewares/validation.middleware.ts index 824ca6ab..623f7471 100644 --- a/samples/sample-koa/src/middlewares/validation.middleware.ts +++ b/samples/sample-koa/src/middlewares/validation.middleware.ts @@ -1,6 +1,9 @@ import {plainToInstance} from "class-transformer"; import {validateOrReject, ValidationError} from "class-validator"; import {HttpException} from "../exceptions/httpException"; +import {Next, Request} from "koa"; + +type ReqWithBody = Request & {body?: unknown}; /** * @name ValidationMiddleware @@ -16,8 +19,8 @@ export const ValidationMiddleware = ( whitelist = false, forbidNonWhitelisted = false, ) => { - return (req: any, res: any, next: any) => { - const dto = plainToInstance(type, req.body); + return (req: ReqWithBody, _res: never, next: Next) => { + const dto: object = plainToInstance(type, req.body); validateOrReject(dto, { skipMissingProperties, whitelist, @@ -25,13 +28,14 @@ export const ValidationMiddleware = ( }) .then(() => { req.body = dto; - next(); + return next(); }) .catch((errors: ValidationError[]) => { const message = errors .map((error: ValidationError) => Object.values(error.constraints ?? {})) .join(", "); - next(new HttpException(400, message)); + // void next(new HttpException(400, message)); + throw new HttpException(400, message); }); }; }; diff --git a/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts b/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts index 71a478c8..6f2e3a1f 100644 --- a/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts +++ b/samples/sample-koa/src/persistence/listeners/GlobalEntityEventListener.ts @@ -16,124 +16,124 @@ import {Inject} from "@node-boot/di"; @EntityEventSubscriber() export class GlobalEntityEventListener implements EntitySubscriberInterface { @Inject() - private logger: Logger; + private logger?: Logger; /** * Called after entity is loaded. */ afterLoad(entity: any) { - this.logger.info(`AFTER ENTITY LOADED: `, entity); + this.logger?.info(`AFTER ENTITY LOADED: `, entity); } /** * Called before entity insertion. */ beforeInsert(event: InsertEvent) { - this.logger.info(`BEFORE ENTITY INSERTED: `, event.entity); + this.logger?.info(`BEFORE ENTITY INSERTED: `, event.entity); } /** * Called after entity insertion. */ afterInsert(event: InsertEvent) { - this.logger.info(`AFTER ENTITY INSERTED: `, event.entity); + this.logger?.info(`AFTER ENTITY INSERTED: `, event.entity); } /** * Called before entity update. */ beforeUpdate(event: UpdateEvent) { - this.logger.info(`BEFORE ENTITY UPDATED: `, event.entity); + this.logger?.info(`BEFORE ENTITY UPDATED: `, event.entity); } /** * Called after entity update. */ afterUpdate(event: UpdateEvent) { - this.logger.info(`AFTER ENTITY UPDATED: `, event.entity); + this.logger?.info(`AFTER ENTITY UPDATED: `, event.entity); } /** * Called before entity removal. */ beforeRemove(event: RemoveEvent) { - this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + this.logger?.info(`BEFORE ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); } /** * Called after entity removal. */ afterRemove(event: RemoveEvent) { - this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); + this.logger?.info(`AFTER ENTITY WITH ID ${event.entityId} REMOVED: `, event.entity); } /** * Called before entity removal. */ beforeSoftRemove(event: SoftRemoveEvent) { - this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + this.logger?.info(`BEFORE ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); } /** * Called after entity removal. */ afterSoftRemove(event: SoftRemoveEvent) { - this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); + this.logger?.info(`AFTER ENTITY WITH ID ${event.entityId} SOFT REMOVED: `, event.entity); } /** * Called before entity recovery. */ beforeRecover(event: RecoverEvent) { - this.logger.info(`BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + this.logger?.info(`BEFORE ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); } /** * Called after entity recovery. */ afterRecover(event: RecoverEvent) { - this.logger.info(`AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); + this.logger?.info(`AFTER ENTITY WITH ID ${event.entityId} RECOVERED: `, event.entity); } /** * Called before transaction start. */ - beforeTransactionStart(event: TransactionStartEvent) { - this.logger.info(`BEFORE TRANSACTION STARTED`); + beforeTransactionStart(_: TransactionStartEvent) { + this.logger?.info(`BEFORE TRANSACTION STARTED`); } /** * Called after transaction start. */ - afterTransactionStart(event: TransactionStartEvent) { - this.logger.info(`AFTER TRANSACTION STARTED`); + afterTransactionStart(_: TransactionStartEvent) { + this.logger?.info(`AFTER TRANSACTION STARTED`); } /** * Called before transaction commit. */ - beforeTransactionCommit(event: TransactionCommitEvent) { - this.logger.info(`BEFORE TRANSACTION COMMITTED`); + beforeTransactionCommit(_: TransactionCommitEvent) { + this.logger?.info(`BEFORE TRANSACTION COMMITTED`); } /** * Called after transaction commit. */ - afterTransactionCommit(event: TransactionCommitEvent) { - this.logger.info(`AFTER TRANSACTION COMMITTED`); + afterTransactionCommit(_: TransactionCommitEvent) { + this.logger?.info(`AFTER TRANSACTION COMMITTED`); } /** * Called before transaction rollback. */ - beforeTransactionRollback(event: TransactionRollbackEvent) { - this.logger.info(`BEFORE TRANSACTION ROLLBACK`); + beforeTransactionRollback(_: TransactionRollbackEvent) { + this.logger?.info(`BEFORE TRANSACTION ROLLBACK`); } /** * Called after transaction rollback. */ - afterTransactionRollback(event: TransactionRollbackEvent) { - this.logger.info(`AFTER TRANSACTION ROLLBACK`); + afterTransactionRollback(_: TransactionRollbackEvent) { + this.logger?.info(`AFTER TRANSACTION ROLLBACK`); } } diff --git a/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts b/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts index 434e3488..777e9de2 100644 --- a/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts +++ b/samples/sample-koa/src/persistence/listeners/UserEntityEventListener.ts @@ -14,10 +14,10 @@ import {GreetingService} from "../../services/greeting.service"; @EntityEventSubscriber() export class UserEntityEventListener implements EntitySubscriberInterface { @Inject() - private logger: Logger; + private logger?: Logger; @Inject() - private greetingService: GreetingService; + private greetingService?: GreetingService; /** * Indicates that this subscriber only listen to User events. @@ -30,11 +30,11 @@ export class UserEntityEventListener implements EntitySubscriberInterface * Called before user insertion. */ beforeInsert(event: InsertEvent) { - this.logger.info(`BEFORE USER INSERTED: `, event.entity); + this.logger?.info(`BEFORE USER INSERTED: `, event.entity); } afterInsert(event: InsertEvent): Promise | void { - this.logger.info(`AFTER USER INSERTED: `, event.entity); - this.greetingService.sayHello(event.entity); + this.logger?.info(`AFTER USER INSERTED: `, event.entity); + this.greetingService?.sayHello(event.entity); } } diff --git a/samples/sample-koa/src/services/users.service.ts b/samples/sample-koa/src/services/users.service.ts index bc546b63..666f291d 100644 --- a/samples/sample-koa/src/services/users.service.ts +++ b/samples/sample-koa/src/services/users.service.ts @@ -83,7 +83,7 @@ export class UserService { await Optional.of(user) .orElseThrow(() => new HttpError(409, "User doesn't exist")) - .runAsync(user => this.userRepository.delete({id: userId})); + .runAsync(() => this.userRepository.delete({id: userId})); throw new Error("Error after deleting that should rollback transaction"); } diff --git a/servers/express-server/jest.config.js b/servers/express-server/jest.config.js index ef8af537..84f52636 100644 --- a/servers/express-server/jest.config.js +++ b/servers/express-server/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/servers/express-server/package.json b/servers/express-server/package.json index f9a47015..664a9b49 100644 --- a/servers/express-server/package.json +++ b/servers/express-server/package.json @@ -17,7 +17,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/servers/express-server/src/driver/ExpressDriver.ts b/servers/express-server/src/driver/ExpressDriver.ts index e42d19d6..f0bdef25 100644 --- a/servers/express-server/src/driver/ExpressDriver.ts +++ b/servers/express-server/src/driver/ExpressDriver.ts @@ -39,12 +39,6 @@ type ExpressServerOptions = { configs?: ExpressServerConfigs; express?: Application; }; - -type CookieOptions = { - secret?: string | string[]; - options?: CookieParseOptions; -}; - /** * Integration with express framework. */ @@ -196,7 +190,11 @@ export class ExpressDriver extends NodeBootDriver { // This causes a double execution on our side. // * Multiple routes match the request (e.g. GET /users/me matches both @All(/users/me) and @Get(/users/:id)). // The following middleware only starts an action processing if the request has not been processed before. - const routeGuard = function routeGuard(request: any, response: any, next: Function) { + const routeGuard = function routeGuard( + request: Request & {routingControllersStarted?: boolean}, + _: unknown, + next: Function, + ) { if (!request.routingControllersStarted) { request.routingControllersStarted = true; return next(); @@ -280,7 +278,7 @@ export class ExpressDriver extends NodeBootDriver { } // transform result if needed - result = this.transformResult(result, actionMetadata, action); + result = this.transformResult(result, actionMetadata); // set http status code if (result === undefined && actionMetadata.undefinedResultCode) { diff --git a/servers/fastify-server/jest.config.js b/servers/fastify-server/jest.config.js index ef8af537..84f52636 100644 --- a/servers/fastify-server/jest.config.js +++ b/servers/fastify-server/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/servers/fastify-server/package.json b/servers/fastify-server/package.json index 66e016ce..08159ec7 100644 --- a/servers/fastify-server/package.json +++ b/servers/fastify-server/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/servers/fastify-server/src/driver/FastifyDriver.ts b/servers/fastify-server/src/driver/FastifyDriver.ts index 98412570..60c43029 100644 --- a/servers/fastify-server/src/driver/FastifyDriver.ts +++ b/servers/fastify-server/src/driver/FastifyDriver.ts @@ -165,7 +165,7 @@ export class FastifyDriver extends NodeBootDriver done()) + .then(() => done()) .catch((error: any) => { this.handleError(error, undefined, { request, diff --git a/servers/koa-server/jest.config.js b/servers/koa-server/jest.config.js index ef8af537..84f52636 100644 --- a/servers/koa-server/jest.config.js +++ b/servers/koa-server/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/servers/koa-server/package.json b/servers/koa-server/package.json index 61f516ab..b66fe71a 100644 --- a/servers/koa-server/package.json +++ b/servers/koa-server/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/servers/koa-server/src/driver/KoaDriver.ts b/servers/koa-server/src/driver/KoaDriver.ts index 38b63307..4b800ff8 100644 --- a/servers/koa-server/src/driver/KoaDriver.ts +++ b/servers/koa-server/src/driver/KoaDriver.ts @@ -65,7 +65,7 @@ export class KoaDriver extends NodeBootDriver { () => this.logger.warn(`CORS is not configured`), ) .ifCookies( - options => this.app.use(parseCookie()), // always add all cookies to ctx.cookies + () => this.app.use(parseCookie()), // always add all cookies to ctx.cookies () => this.logger.warn(`Cookies is not configured`), ) .ifSession( @@ -265,7 +265,7 @@ export class KoaDriver extends NodeBootDriver { } // transform result if needed - result = this.transformResult(result, action, options); + result = this.transformResult(result, action); if (action.redirect) { // if redirect is set then do it diff --git a/starters/actuator/jest.config.js b/starters/actuator/jest.config.js index ef8af537..84f52636 100644 --- a/starters/actuator/jest.config.js +++ b/starters/actuator/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/starters/actuator/package.json b/starters/actuator/package.json index 3b337ff2..43549eb2 100644 --- a/starters/actuator/package.json +++ b/starters/actuator/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", @@ -29,22 +29,23 @@ "typecheck": "tsc" }, "dependencies": { - "@node-boot/context": "1.0.0", "@node-boot/config": "1.0.0", + "@node-boot/context": "1.0.0", + "dayjs": "^1.11.9", "prom-client": "^14.2.0", - "properties-reader": "^2.3.0", - "dayjs": "^1.11.9" + "properties-reader": "^2.3.0" }, "peerDependencies": { "routing-controllers": ">=0.10.4" }, "optionalDependencies": { - "koa": ">=2.14.2", "@koa/router": ">=12.0.0", "express": ">=4.18.2", - "fastify": ">=4.21.0" + "fastify": ">=4.21.0", + "koa": ">=2.14.2" }, "devDependencies": { + "@types/express": "^4.17.21", "@types/koa": "^2.13.11", "@types/koa__router": "^12.0.4" } diff --git a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts index 315042c0..2161753e 100644 --- a/starters/actuator/src/adapter/DefaultActuatorAdapter.ts +++ b/starters/actuator/src/adapter/DefaultActuatorAdapter.ts @@ -9,8 +9,6 @@ import {ConfigService} from "@node-boot/config"; import {KoaActuatorAdapter} from "./KoaActuatorAdapter"; export class DefaultActuatorAdapter implements ActuatorAdapter { - private metricsContext: MetricsContext; - constructor( private readonly register = new Prometheus.Registry(), private readonly infoService: InfoService = new InfoService(), @@ -43,7 +41,6 @@ export class DefaultActuatorAdapter implements ActuatorAdapter { http_request_duration_milliseconds, http_request_counter, }; - this.metricsContext = context; return context; } diff --git a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts index 66ed3000..0dc4d617 100644 --- a/starters/actuator/src/adapter/ExpressActuatorAdapter.ts +++ b/starters/actuator/src/adapter/ExpressActuatorAdapter.ts @@ -1,10 +1,12 @@ import {ActuatorAdapter, ActuatorOptions} from "@node-boot/context"; -import express from "express"; +import {Router, Application, Response} from "express"; import {InfoService} from "../service/InfoService"; import {MetricsContext} from "../types"; import {MetadataService} from "../service/MetadataService"; import {ConfigService} from "@node-boot/config"; +type ResWithEpoch = Response & {locals: {startEpoch: number}}; + export class ExpressActuatorAdapter implements ActuatorAdapter { constructor( private readonly context: MetricsContext, @@ -13,8 +15,8 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { private readonly configService?: ConfigService, ) {} - bind(options: ActuatorOptions, server: express.Application, router: express.Application): void { - router.use((req, res, next) => { + bind(_options: ActuatorOptions, _server: Application, router: Router): void { + router.use((req, res: ResWithEpoch, next) => { // Start a timer for every request made res.locals.startEpoch = Date.now(); @@ -22,7 +24,7 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { const responseTimeInMilliseconds = Date.now() - res.locals.startEpoch; this.context.http_request_duration_milliseconds - .labels(req.method, req.path, res.statusCode) + .labels(req.method, req.path, res.statusCode.toString()) .observe(responseTimeInMilliseconds); }); @@ -37,40 +39,40 @@ export class ExpressActuatorAdapter implements ActuatorAdapter { next(); }); - router.get("/actuator", (req, res) => { + router.get("/actuator", (_, res) => { res.status(200).json(this.metadataService.getActuatorEndpoints()); }); - router.get("/actuator/info", (req, res) => { + router.get("/actuator/info", (_, res) => { this.infoService.getInfo().then(data => res.status(200).json(data)); }); - router.get("/actuator/config", (req, res) => { + router.get("/actuator/config", (_, res) => { res.status(200).json(this.configService?.get() ?? {}); }); - router.get("/actuator/memory", (req, res) => { + router.get("/actuator/memory", (_, res) => { this.infoService.getMemory().then(data => res.status(200).json(data)); }); - router.get("/actuator/metrics", (req, res) => { + router.get("/actuator/metrics", (_, res) => { this.context.register.getMetricsAsJSON().then(data => res.status(200).json(data)); }); - router.get("/actuator/prometheus", (req, res) => { + router.get("/actuator/prometheus", (_, res) => { res.setHeader("Content-Type", this.context.register.contentType); this.context.register.metrics().then(data => res.status(200).send(data)); }); - router.get("/actuator/controllers", (req, res) => { + router.get("/actuator/controllers", (_, res) => { res.status(200).json(this.metadataService.getControllers()); }); - router.get("/actuator/interceptors", (req, res) => { + router.get("/actuator/interceptors", (_, res) => { res.status(200).json(this.metadataService.getInterceptors()); }); - router.get("/actuator/middlewares", (req, res) => { + router.get("/actuator/middlewares", (_, res) => { res.status(200).json(this.metadataService.getMiddlewares()); }); } diff --git a/starters/actuator/src/adapter/FastifyActuatorAdapter.ts b/starters/actuator/src/adapter/FastifyActuatorAdapter.ts index bad5d903..3e317905 100644 --- a/starters/actuator/src/adapter/FastifyActuatorAdapter.ts +++ b/starters/actuator/src/adapter/FastifyActuatorAdapter.ts @@ -13,8 +13,8 @@ export class FastifyActuatorAdapter implements ActuatorAdapter { private readonly configService?: ConfigService, ) {} - bind(options: ActuatorOptions, server: FastifyInstance, router: FastifyInstance): void { - router.addHook("onRequest", (request, reply, done) => { + bind(_: ActuatorOptions, _instance: FastifyInstance, router: FastifyInstance): void { + router.addHook("onRequest", (request, _reply, done) => { // Start a timer for every request made request.log.info({event: "onRequest"}, "Request received"); request["locals"].startEpoch = Date.now(); @@ -46,38 +46,38 @@ export class FastifyActuatorAdapter implements ActuatorAdapter { done(); }); - router.get("/actuator", (req, res) => { + router.get("/actuator", (_, res) => { res.status(200); res.send(this.metadataService.getActuatorEndpoints()); }); - router.get("/actuator/info", (req, res) => { + router.get("/actuator/info", (_, res) => { this.infoService.getInfo().then(data => { res.status(200); res.send(data); }); }); - router.get("/actuator/config", (req, res) => { + router.get("/actuator/config", (_, res) => { res.status(200); res.send(this.configService?.get() ?? {}); }); - router.get("/actuator/memory", (req, res) => { + router.get("/actuator/memory", (_, res) => { this.infoService.getMemory().then(data => { res.status(200); res.send(data); }); }); - router.get("/actuator/metrics", (req, res) => { + router.get("/actuator/metrics", (_, res) => { this.context.register.getMetricsAsJSON().then(data => { res.status(200); res.send(data); }); }); - router.get("/actuator/prometheus", (req, res) => { + router.get("/actuator/prometheus", (_, res) => { res.type(this.context.register.contentType); this.context.register.metrics().then(data => { res.status(200); @@ -85,17 +85,17 @@ export class FastifyActuatorAdapter implements ActuatorAdapter { }); }); - router.get("/actuator/controllers", (req, res) => { + router.get("/actuator/controllers", (_, res) => { res.status(200); res.send(this.metadataService.getControllers()); }); - router.get("/actuator/interceptors", (req, res) => { + router.get("/actuator/interceptors", (_, res) => { res.status(200); res.send(this.metadataService.getInterceptors()); }); - router.get("/actuator/middlewares", (req, res) => { + router.get("/actuator/middlewares", (_, res) => { res.status(200); res.send(this.metadataService.getMiddlewares()); }); diff --git a/starters/actuator/src/adapter/KoaActuatorAdapter.ts b/starters/actuator/src/adapter/KoaActuatorAdapter.ts index 45d4ffb5..8ff0817a 100644 --- a/starters/actuator/src/adapter/KoaActuatorAdapter.ts +++ b/starters/actuator/src/adapter/KoaActuatorAdapter.ts @@ -14,7 +14,7 @@ export class KoaActuatorAdapter implements ActuatorAdapter { private readonly configService?: ConfigService, ) {} - bind(options: ActuatorOptions, server: Koa, router: Router): void { + bind(_options: ActuatorOptions, _server: Koa, router: Router): void { router.use(async (ctx, next) => { // Start a timer for every request made // Start a timer for every request made diff --git a/starters/actuator/src/decorator/EnableActuator.ts b/starters/actuator/src/decorator/EnableActuator.ts index 1abf8f36..d2c70567 100644 --- a/starters/actuator/src/decorator/EnableActuator.ts +++ b/starters/actuator/src/decorator/EnableActuator.ts @@ -2,7 +2,7 @@ import {ApplicationContext} from "@node-boot/context"; import {DefaultActuatorAdapter} from "../adapter"; export function EnableActuator(): Function { - return function (object: Function) { + return function () { ApplicationContext.get().actuatorAdapter = new DefaultActuatorAdapter(); }; } diff --git a/starters/persistence/jest.config.js b/starters/persistence/jest.config.js index ef8af537..84f52636 100644 --- a/starters/persistence/jest.config.js +++ b/starters/persistence/jest.config.js @@ -1,4 +1,3 @@ -/** @type {import('jest').Config} */ module.exports = { transform: { "^.+\\.(t|j)sx?$": "@swc/jest", diff --git a/starters/persistence/package.json b/starters/persistence/package.json index 5d0b3576..a5ca40c1 100644 --- a/starters/persistence/package.json +++ b/starters/persistence/package.json @@ -16,7 +16,7 @@ "access": "public" }, "main": "dist/index.js", - "types": "dist/index.d.ts", + "types": "src/index.ts", "scripts": { "build": "tsc -p tsconfig.build.json", "clean:build": "rimraf ./dist", diff --git a/starters/persistence/src/adapter/PersistenceLogger.ts b/starters/persistence/src/adapter/PersistenceLogger.ts index f819da26..e1cb390d 100644 --- a/starters/persistence/src/adapter/PersistenceLogger.ts +++ b/starters/persistence/src/adapter/PersistenceLogger.ts @@ -1,5 +1,4 @@ import {AbstractLogger, LogLevel, LogMessage} from "typeorm"; -import {QueryRunner} from "typeorm/query-runner/QueryRunner"; import {Logger} from "winston"; export class PersistenceLogger extends AbstractLogger { @@ -10,7 +9,7 @@ export class PersistenceLogger extends AbstractLogger { /** * Write log to specific output. */ - protected writeLog(level: LogLevel, logMessage: LogMessage | LogMessage[], queryRunner?: QueryRunner) { + protected writeLog(level: LogLevel, logMessage: LogMessage | LogMessage[]) { const messages = this.prepareLogMessages(logMessage, { highlightSql: false, }); diff --git a/starters/persistence/src/config/PersistenceConfiguration.ts b/starters/persistence/src/config/PersistenceConfiguration.ts index 89e00f73..11b05e17 100644 --- a/starters/persistence/src/config/PersistenceConfiguration.ts +++ b/starters/persistence/src/config/PersistenceConfiguration.ts @@ -33,7 +33,7 @@ export class PersistenceConfiguration { * @return dataSource (DataSource): The configured and initialized DataSource object for the persistence layer. * */ @Bean() - public dataSource({iocContainer, logger, config}: BeansContext): DataSource { + public dataSource({iocContainer, logger}: BeansContext): DataSource { logger.info("Configuring persistence DataSource"); const datasourceConfig = iocContainer.get("datasource-config") as DataSourceOptions; @@ -112,7 +112,7 @@ export class PersistenceConfiguration { for (const fieldToInject of Reflect.getMetadata(REQUIRES_FIELD_INJECTION_KEY, subscriber) || []) { // Extract type metadata for field injection. This is useful for custom injection in some modules const propertyType = Reflect.getMetadata("design:type", subscriber, fieldToInject); - subscriber[fieldToInject] = iocContainer.get(propertyType); + subscriber[fieldToInject as never] = iocContainer.get(propertyType) as never; } } logger.info(`${subscribers.length} persistence event subscribers successfully injected`); diff --git a/starters/persistence/src/decorator/DatasourceConfiguration.ts b/starters/persistence/src/decorator/DatasourceConfiguration.ts index 800de320..84029f4b 100644 --- a/starters/persistence/src/decorator/DatasourceConfiguration.ts +++ b/starters/persistence/src/decorator/DatasourceConfiguration.ts @@ -2,7 +2,7 @@ import {PersistenceContext} from "../PersistenceContext"; import {NodeBootDataSourceOptions} from "../property/NodeBootDataSourceOptions"; export function DatasourceConfiguration(options: NodeBootDataSourceOptions): ClassDecorator { - return (target: Function) => { + return () => { PersistenceContext.get().databaseConnectionOverrides = options; }; } diff --git a/starters/persistence/src/decorator/EnableRepositories.ts b/starters/persistence/src/decorator/EnableRepositories.ts index fabc96a2..f7dceee8 100644 --- a/starters/persistence/src/decorator/EnableRepositories.ts +++ b/starters/persistence/src/decorator/EnableRepositories.ts @@ -5,7 +5,7 @@ import {TransactionConfiguration} from "../config/TransactionConfiguration"; import {QueryCacheConfiguration} from "../config/QueryCacheConfiguration"; export const EnableRepositories = (): ClassDecorator => { - return (target: Function) => { + return () => { // Register repositories adapter ApplicationContext.get().repositoriesAdapter = new DefaultRepositoriesAdapter(); diff --git a/tsconfig.base.json b/tsconfig.base.json index cf501e54..24b3def6 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,32 +1,26 @@ { - "extends": "@tsconfig/node-lts-strictest/tsconfig.json", - "compilerOptions": { - /* Basic Options */ - "incremental": true, - "declaration": true, - "allowJs": true, - "noEmit": true, - "strict": true, + "extends": "@tsconfig/node-lts-strictest/tsconfig.json", + "compilerOptions": { + /* Basic Options */ + "incremental": true, + "declaration": true, + "allowJs": true, + "noEmit": true, + "strict": true, - /* Strict Type-Checking Options */ - "noImplicitAny": false, - "strictNullChecks": true, - "strictFunctionTypes": true, - "strictBindCallApply": true, - "strictPropertyInitialization": false, - "noImplicitThis": true, - "alwaysStrict": true, - "resolveJsonModule": true, - "preserveValueImports": false, - "importsNotUsedAsValues": "remove", - "skipLibCheck": true, - "exactOptionalPropertyTypes": false, - "noUnusedParameters": false, - "noUnusedLocals": false, - "esModuleInterop": true, - - /* Experimental Options */ - "experimentalDecorators": true, - "emitDecoratorMetadata": true - } + "strictPropertyInitialization": false, + /* Strict Type-Checking Options */ + "noImplicitThis": true, + "alwaysStrict": true, + "resolveJsonModule": true, + "preserveValueImports": false, + "importsNotUsedAsValues": "remove", + "skipLibCheck": true, + "exactOptionalPropertyTypes": false, + "esModuleInterop": true, + "noImplicitAny": false, + /* Experimental Options */ + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } }