From 34f6515907127029618b0a762aa3e4ba7fc3eb08 Mon Sep 17 00:00:00 2001 From: Sandeep K Nair Date: Fri, 28 Oct 2022 12:09:51 +0530 Subject: [PATCH] Fixes - Upgraded to version 9 - Fixed linting issues - Fixed formatting issues --- .eslintrc.js | 4 +- .gitignore | 2 +- .npmignore | 2 + .prettierrc | 4 +- README.md | 11 +- index.d.ts | 2 +- index.js | 23 +- lib/decorators/index.ts | 2 +- lib/decorators/tenancy.decorator.ts | 6 +- lib/factories/index.ts | 2 +- lib/factories/tenancy.factory.ts | 78 +- lib/index.ts | 2 +- lib/interfaces/index.ts | 2 +- lib/interfaces/model-definition.interface.ts | 6 +- lib/interfaces/tenancy-options.interface.ts | 112 +-- lib/tenancy-core.module.ts | 984 ++++++++++--------- lib/tenancy-feature.module.ts | 18 +- lib/tenancy.constants.ts | 2 +- lib/tenancy.module.ts | 14 +- lib/types/index.ts | 2 +- lib/types/tenancy-types.ts | 2 +- lib/utils/index.ts | 2 +- lib/utils/tenancy.utils.ts | 10 +- package.json | 61 +- tsconfig.build.json | 7 +- tsconfig.json | 13 +- 26 files changed, 711 insertions(+), 662 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 0d69b8a..006c345 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -6,10 +6,8 @@ module.exports = { }, plugins: ['@typescript-eslint/eslint-plugin'], extends: [ - 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', - 'prettier', - 'prettier/@typescript-eslint', + 'plugin:prettier/recommended', ], root: true, env: { diff --git a/.gitignore b/.gitignore index d7c2d60..49be6dd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,4 @@ npm-debug.log /.nyc_output # dist -dist \ No newline at end of file +dist diff --git a/.npmignore b/.npmignore index a7842cf..2fdc1f6 100644 --- a/.npmignore +++ b/.npmignore @@ -4,5 +4,7 @@ index.ts package-lock.json .eslintrc.js tsconfig.json +tsconfig.build.json +.prettierrc .prettierrc .commitlintrc.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index dcb7279..cd6a989 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,4 +1,4 @@ { - "singleQuote": true, - "trailingComma": "all" + "trailingComma": "all", + "singleQuote": true } \ No newline at end of file diff --git a/README.md b/README.md index aefe873..41c5f7d 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,8 @@ npm i --save @needle-innovision/nestjs-tenancy | Nest versions | Command | |---------------|---------------------------------------------------------| -| v8.x | `npm i --save @needle-innovision/nestjs-tenancy` | +| v7.x | `npm i --save @needle-innovision/nestjs-tenancy | +| v8.x | `npm i --save @needle-innovision/nestjs-tenancy@2.0.0 | | v6.x or v7.x | `npm i --save @needle-innovision/nestjs-tenancy@1.0.21` | ## Basic usage @@ -329,10 +330,10 @@ we can make use of the property in `TenancyModuleOptions` which is `forceCreateC ## Requirements -1. @nest/mongoose ^6.x.x || ^8.x.x -2. @nestjs/common ^6.x.x || ^8.x.x -3. @nestjs/core ^6.x.x || ^8.x.x -4. mongoose ^5.7.12 (with typings `@types/mongoose`) || ^6.3.8 +1. @nest/mongoose ^6.x.x || ^8.x.x || ^9.x.x +2. @nestjs/common ^6.x.x || ^8.x.x || ^9.x.x +3. @nestjs/core ^6.x.x || ^8.x.x || ^9.x.x +4. mongoose ^5.7.12 (with typings `@types/mongoose`) || ^6.3.8 || ^6.7.0 ## Test diff --git a/index.d.ts b/index.d.ts index ba59932..5703fb5 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1 +1 @@ -export * from './dist'; \ No newline at end of file +export * from './dist'; diff --git a/index.js b/index.js index 93e507b..0bb0f5f 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,17 @@ "use strict"; - -function __export(m) { - for (var p in m) - if (!exports.hasOwnProperty(p)) exports[p] = m[p]; -} -exports.__esModule = true; -__export(require("./dist")); \ No newline at end of file +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./dist"), exports); diff --git a/lib/decorators/index.ts b/lib/decorators/index.ts index 19f82ae..727f420 100644 --- a/lib/decorators/index.ts +++ b/lib/decorators/index.ts @@ -1 +1 @@ -export * from './tenancy.decorator'; \ No newline at end of file +export * from './tenancy.decorator'; diff --git a/lib/decorators/tenancy.decorator.ts b/lib/decorators/tenancy.decorator.ts index 144440b..11d3387 100644 --- a/lib/decorators/tenancy.decorator.ts +++ b/lib/decorators/tenancy.decorator.ts @@ -6,11 +6,13 @@ import { getTenantConnectionToken, getTenantModelToken } from '../utils'; * * @param model any */ -export const InjectTenancyModel = (model: string) => Inject(getTenantModelToken(model)); +export const InjectTenancyModel = (model: string) => + Inject(getTenantModelToken(model)); /** * Get the instance of the tenant connection * * @param name any */ -export const InjectTenancyConnection = (name?: string) => Inject(getTenantConnectionToken(name)); +export const InjectTenancyConnection = (name?: string) => + Inject(getTenantConnectionToken(name)); diff --git a/lib/factories/index.ts b/lib/factories/index.ts index 2b70741..0a6fa39 100644 --- a/lib/factories/index.ts +++ b/lib/factories/index.ts @@ -1 +1 @@ -export * from './tenancy.factory'; \ No newline at end of file +export * from './tenancy.factory'; diff --git a/lib/factories/tenancy.factory.ts b/lib/factories/tenancy.factory.ts index 3be94f4..8cc1880 100644 --- a/lib/factories/tenancy.factory.ts +++ b/lib/factories/tenancy.factory.ts @@ -1,48 +1,54 @@ import { Provider } from '@nestjs/common'; import { Connection } from 'mongoose'; import { ModelDefinition } from '../interfaces'; -import { CONNECTION_MAP, MODEL_DEFINITION_MAP, TENANT_CONNECTION } from '../tenancy.constants'; +import { + CONNECTION_MAP, + MODEL_DEFINITION_MAP, + TENANT_CONNECTION, +} from '../tenancy.constants'; import { ConnectionMap, ModelDefinitionMap } from '../types'; import { getTenantModelDefinitionToken, getTenantModelToken } from '../utils'; -export const createTenancyProviders = (definitions: ModelDefinition[]): Provider[] => { - const providers: Provider[] = []; +export const createTenancyProviders = ( + definitions: ModelDefinition[], +): Provider[] => { + const providers: Provider[] = []; - for (const definition of definitions) { - // Extract the definition data - const { name, schema, collection } = definition; + for (const definition of definitions) { + // Extract the definition data + const { name, schema, collection } = definition; - providers.push({ - provide: getTenantModelDefinitionToken(name), - useFactory: ( - modelDefinitionMap: ModelDefinitionMap, - connectionMap: ConnectionMap, - ) => { - const exists = modelDefinitionMap.has(name); - if (!exists) { - modelDefinitionMap.set(name, { ...definition }); + providers.push({ + provide: getTenantModelDefinitionToken(name), + useFactory: ( + modelDefinitionMap: ModelDefinitionMap, + connectionMap: ConnectionMap, + ) => { + const exists = modelDefinitionMap.has(name); + if (!exists) { + modelDefinitionMap.set(name, { ...definition }); - connectionMap.forEach((connection: Connection) => { - connection.model(name, schema, collection); - }); - } - }, - inject: [ - MODEL_DEFINITION_MAP, - CONNECTION_MAP, - ], - }); + connectionMap.forEach((connection: Connection) => { + connection.model(name, schema, collection); + }); + } + }, + inject: [MODEL_DEFINITION_MAP, CONNECTION_MAP], + }); - // Creating Models with connections attached - providers.push({ - provide: getTenantModelToken(name), - useFactory(tenantConnection: Connection) { - return tenantConnection.models[name] || tenantConnection.model(name, schema, collection); - }, - inject: [TENANT_CONNECTION], - }); - } + // Creating Models with connections attached + providers.push({ + provide: getTenantModelToken(name), + useFactory(tenantConnection: Connection) { + return ( + tenantConnection.models[name] || + tenantConnection.model(name, schema, collection) + ); + }, + inject: [TENANT_CONNECTION], + }); + } - // Return the list of providers mapping - return providers; + // Return the list of providers mapping + return providers; }; diff --git a/lib/index.ts b/lib/index.ts index 4ca7cd9..de8ad9e 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -4,4 +4,4 @@ export * from './interfaces'; export * from './decorators'; export * from './utils'; export * from './factories'; -export * from './tenancy.module'; \ No newline at end of file +export * from './tenancy.module'; diff --git a/lib/interfaces/index.ts b/lib/interfaces/index.ts index ad828fe..9dda901 100644 --- a/lib/interfaces/index.ts +++ b/lib/interfaces/index.ts @@ -1,2 +1,2 @@ export * from './model-definition.interface'; -export * from './tenancy-options.interface'; \ No newline at end of file +export * from './tenancy-options.interface'; diff --git a/lib/interfaces/model-definition.interface.ts b/lib/interfaces/model-definition.interface.ts index fab5854..e874832 100644 --- a/lib/interfaces/model-definition.interface.ts +++ b/lib/interfaces/model-definition.interface.ts @@ -1,7 +1,7 @@ import { Schema } from 'mongoose'; export interface ModelDefinition { - name: string; - schema: Schema; - collection?: string; + name: string; + schema: Schema; + collection?: string; } diff --git a/lib/interfaces/tenancy-options.interface.ts b/lib/interfaces/tenancy-options.interface.ts index 4291a08..1b5267f 100644 --- a/lib/interfaces/tenancy-options.interface.ts +++ b/lib/interfaces/tenancy-options.interface.ts @@ -7,43 +7,43 @@ import { ModuleMetadata, Type } from '@nestjs/common/interfaces'; * @interface TenancyModuleOptions */ export interface TenancyModuleOptions extends Record { - /** - * If `true`, tenant id will be extracted from the subdomain - */ - isTenantFromSubdomain?: boolean; + /** + * If `true`, tenant id will be extracted from the subdomain + */ + isTenantFromSubdomain?: boolean; - /** - * Tenant id will be extracted using the keyword from the request header - */ - tenantIdentifier?: string; + /** + * Tenant id will be extracted using the keyword from the request header + */ + tenantIdentifier?: string; - /** - * URI for the tenant database - */ - uri: (uri: string) => Promise | string; + /** + * URI for the tenant database + */ + uri: (uri: string) => Promise | string; - /** - * Used for applying custom validations - */ - validator?: (tenantId: string) => TenancyValidator; + /** + * Used for applying custom validations + */ + validator?: (tenantId: string) => TenancyValidator; - /** - * Options for the database - */ - options?: any; + /** + * Options for the database + */ + options?: any; - /** - * Whitelist following subdomains - */ - whitelist?: any; + /** + * Whitelist following subdomains + */ + whitelist?: any; - /** - * Option to create the collections that are mapped to the tenant module - * automatically while requesting for the tenant connection for the - * first time. This option is useful in case on mongo transactions, where - * transactions doens't create a collection if it does't exist already. - */ - forceCreateCollections?: boolean; + /** + * Option to create the collections that are mapped to the tenant module + * automatically while requesting for the tenant connection for the + * first time. This option is useful in case on mongo transactions, where + * transactions doens't create a collection if it does't exist already. + */ + forceCreateCollections?: boolean; } /** @@ -56,7 +56,7 @@ export interface TenancyModuleOptions extends Record { * @interface TenancyOptionsFactory */ export interface TenancyOptionsFactory { - createTenancyOptions():Promise | TenancyModuleOptions; + createTenancyOptions(): Promise | TenancyModuleOptions; } /** @@ -65,11 +65,14 @@ export interface TenancyOptionsFactory { * @export * @interface TenancyModuleAsyncOptions */ -export interface TenancyModuleAsyncOptions extends Pick { - useExisting?: Type; - useClass?: Type; - useFactory?: (...args: any[]) => Promise | TenancyModuleOptions; - inject?: any[]; +export interface TenancyModuleAsyncOptions + extends Pick { + useExisting?: Type; + useClass?: Type; + useFactory?: ( + ...args: any[] + ) => Promise | TenancyModuleOptions; + inject?: any[]; } /** @@ -82,22 +85,21 @@ export interface TenancyModuleAsyncOptions extends Pick; -} \ No newline at end of file + /** + * This call will be invoked internally by the library + * + * @memberof TenancyValidator + */ + validate(): Promise; +} diff --git a/lib/tenancy-core.module.ts b/lib/tenancy-core.module.ts index 11cec0c..c00bbdb 100644 --- a/lib/tenancy-core.module.ts +++ b/lib/tenancy-core.module.ts @@ -1,511 +1,545 @@ -import { BadRequestException, DynamicModule, Global, Module, OnApplicationShutdown, Provider, Scope } from '@nestjs/common'; +import { + BadRequestException, + DynamicModule, + Global, + Module, + OnApplicationShutdown, + Provider, + Scope, +} from '@nestjs/common'; import { Type } from '@nestjs/common/interfaces'; import { HttpAdapterHost, ModuleRef, REQUEST } from '@nestjs/core'; import { Request } from 'express'; import { Connection, createConnection, Model } from 'mongoose'; import { ConnectionOptions } from 'tls'; -import { TenancyModuleAsyncOptions, TenancyModuleOptions, TenancyOptionsFactory } from './interfaces'; -import { CONNECTION_MAP, DEFAULT_HTTP_ADAPTER_HOST, MODEL_DEFINITION_MAP, TENANT_CONNECTION, TENANT_CONTEXT, TENANT_MODULE_OPTIONS } from './tenancy.constants'; +import { + TenancyModuleAsyncOptions, + TenancyModuleOptions, + TenancyOptionsFactory, +} from './interfaces'; +import { + CONNECTION_MAP, + DEFAULT_HTTP_ADAPTER_HOST, + MODEL_DEFINITION_MAP, + TENANT_CONNECTION, + TENANT_CONTEXT, + TENANT_MODULE_OPTIONS, +} from './tenancy.constants'; import { ConnectionMap, ModelDefinitionMap } from './types'; @Global() @Module({}) export class TenancyCoreModule implements OnApplicationShutdown { - constructor( - private readonly moduleRef: ModuleRef, - ) { } - - /** - * Register for synchornous modules - * - * @static - * @param {TenancyModuleOptions} options - * @returns {DynamicModule} - * @memberof TenancyCoreModule - */ - static register(options: TenancyModuleOptions): DynamicModule { - - /* Module options */ - const tenancyModuleOptionsProvider = { - provide: TENANT_MODULE_OPTIONS, - useValue: { ...options }, - }; - - /* Connection Map */ - const connectionMapProvider = this.createConnectionMapProvider(); - - /* Model Definition Map */ - const modelDefinitionMapProvider = this.createModelDefinitionMapProvider(); - - /* Tenant Context */ - const tenantContextProvider = this.createTenantContextProvider(); - - /* Http Adaptor */ - const httpAdapterHost = this.createHttpAdapterProvider(); - - /* Tenant Connection */ - const tenantConnectionProvider = { - provide: TENANT_CONNECTION, - useFactory: async ( - tenantId: string, - moduleOptions: TenancyModuleOptions, - connMap: ConnectionMap, - modelDefMap: ModelDefinitionMap, - ): Promise => { - return await this.getConnection(tenantId, moduleOptions, connMap, modelDefMap); - }, - inject: [ - TENANT_CONTEXT, - TENANT_MODULE_OPTIONS, - CONNECTION_MAP, - MODEL_DEFINITION_MAP, - ], - }; - - const providers = [ - tenancyModuleOptionsProvider, - tenantContextProvider, - connectionMapProvider, - modelDefinitionMapProvider, - tenantConnectionProvider, - httpAdapterHost, - ]; - - return { - module: TenancyCoreModule, - providers, - exports: providers, - }; - } - - /** - * Register for asynchronous modules - * - * @static - * @param {TenancyModuleAsyncOptions} options - * @returns {DynamicModule} - * @memberof TenancyCoreModule - */ - static registerAsync(options: TenancyModuleAsyncOptions): DynamicModule { - - /* Connection Map */ - const connectionMapProvider = this.createConnectionMapProvider(); - - /* Model Definition Map */ - const modelDefinitionMapProvider = this.createModelDefinitionMapProvider(); - - /* Tenant Context */ - const tenantContextProvider = this.createTenantContextProvider(); - - /* Http Adaptor */ - const httpAdapterHost = this.createHttpAdapterProvider(); - - /* Tenant Connection */ - const tenantConnectionProvider = { - provide: TENANT_CONNECTION, - useFactory: async ( - tenantId: string, - moduleOptions: TenancyModuleOptions, - connMap: ConnectionMap, - modelDefMap: ModelDefinitionMap, - ): Promise => { - return await this.getConnection(tenantId, moduleOptions, connMap, modelDefMap); - }, - inject: [ - TENANT_CONTEXT, - TENANT_MODULE_OPTIONS, - CONNECTION_MAP, - MODEL_DEFINITION_MAP, - ] - }; - - /* Asyc providers */ - const asyncProviders = this.createAsyncProviders(options); - - const providers = [ - ...asyncProviders, - tenantContextProvider, - connectionMapProvider, - modelDefinitionMapProvider, - tenantConnectionProvider, - httpAdapterHost, - ]; - - return { - module: TenancyCoreModule, - imports: options.imports, - providers: providers, - exports: providers - }; - } - - /** - * Override method from `OnApplicationShutdown` - * - * @memberof TenantCoreModule - */ - async onApplicationShutdown() { - // Map of all connections - const connectionMap: ConnectionMap = this.moduleRef.get(CONNECTION_MAP); - - // Remove all stray connections - await Promise.all( - [...connectionMap.values()].map(connection => connection.close()), + constructor(private readonly moduleRef: ModuleRef) {} + + /** + * Register for synchornous modules + * + * @static + * @param {TenancyModuleOptions} options + * @returns {DynamicModule} + * @memberof TenancyCoreModule + */ + static register(options: TenancyModuleOptions): DynamicModule { + /* Module options */ + const tenancyModuleOptionsProvider = { + provide: TENANT_MODULE_OPTIONS, + useValue: { ...options }, + }; + + /* Connection Map */ + const connectionMapProvider = this.createConnectionMapProvider(); + + /* Model Definition Map */ + const modelDefinitionMapProvider = this.createModelDefinitionMapProvider(); + + /* Tenant Context */ + const tenantContextProvider = this.createTenantContextProvider(); + + /* Http Adaptor */ + const httpAdapterHost = this.createHttpAdapterProvider(); + + /* Tenant Connection */ + const tenantConnectionProvider = { + provide: TENANT_CONNECTION, + useFactory: async ( + tenantId: string, + moduleOptions: TenancyModuleOptions, + connMap: ConnectionMap, + modelDefMap: ModelDefinitionMap, + ): Promise => { + return await this.getConnection( + tenantId, + moduleOptions, + connMap, + modelDefMap, ); - } - - /** - * Get Tenant id from the request - * - * @private - * @static - * @param {Request} req - * @param {TenancyModuleOptions} moduleOptions - * @param {HttpAdapterHost} adapterHost - * @returns {string} - * @memberof TenancyCoreModule - */ - private static getTenant( - req: Request, + }, + inject: [ + TENANT_CONTEXT, + TENANT_MODULE_OPTIONS, + CONNECTION_MAP, + MODEL_DEFINITION_MAP, + ], + }; + + const providers = [ + tenancyModuleOptionsProvider, + tenantContextProvider, + connectionMapProvider, + modelDefinitionMapProvider, + tenantConnectionProvider, + httpAdapterHost, + ]; + + return { + module: TenancyCoreModule, + providers, + exports: providers, + }; + } + + /** + * Register for asynchronous modules + * + * @static + * @param {TenancyModuleAsyncOptions} options + * @returns {DynamicModule} + * @memberof TenancyCoreModule + */ + static registerAsync(options: TenancyModuleAsyncOptions): DynamicModule { + /* Connection Map */ + const connectionMapProvider = this.createConnectionMapProvider(); + + /* Model Definition Map */ + const modelDefinitionMapProvider = this.createModelDefinitionMapProvider(); + + /* Tenant Context */ + const tenantContextProvider = this.createTenantContextProvider(); + + /* Http Adaptor */ + const httpAdapterHost = this.createHttpAdapterProvider(); + + /* Tenant Connection */ + const tenantConnectionProvider = { + provide: TENANT_CONNECTION, + useFactory: async ( + tenantId: string, moduleOptions: TenancyModuleOptions, - adapterHost: HttpAdapterHost, - ): string { - // Check if the adaptor is fastify - const isFastifyAdaptor = this.adapterIsFastify(adapterHost); - - if (!moduleOptions) { - throw new BadRequestException(`Tenant options are mandatory`); - } - - // Extract the tenant idetifier - const { - tenantIdentifier = null, - isTenantFromSubdomain = false, - } = moduleOptions; - - // Pull the tenant id from the subdomain - if (isTenantFromSubdomain) { + connMap: ConnectionMap, + modelDefMap: ModelDefinitionMap, + ): Promise => { + return await this.getConnection( + tenantId, + moduleOptions, + connMap, + modelDefMap, + ); + }, + inject: [ + TENANT_CONTEXT, + TENANT_MODULE_OPTIONS, + CONNECTION_MAP, + MODEL_DEFINITION_MAP, + ], + }; + + /* Asyc providers */ + const asyncProviders = this.createAsyncProviders(options); + + const providers = [ + ...asyncProviders, + tenantContextProvider, + connectionMapProvider, + modelDefinitionMapProvider, + tenantConnectionProvider, + httpAdapterHost, + ]; + + return { + module: TenancyCoreModule, + imports: options.imports, + providers: providers, + exports: providers, + }; + } + + /** + * Override method from `OnApplicationShutdown` + * + * @memberof TenantCoreModule + */ + async onApplicationShutdown() { + // Map of all connections + const connectionMap: ConnectionMap = this.moduleRef.get(CONNECTION_MAP); + + // Remove all stray connections + await Promise.all( + [...connectionMap.values()].map((connection) => connection.close()), + ); + } + + /** + * Get Tenant id from the request + * + * @private + * @static + * @param {Request} req + * @param {TenancyModuleOptions} moduleOptions + * @param {HttpAdapterHost} adapterHost + * @returns {string} + * @memberof TenancyCoreModule + */ + private static getTenant( + req: Request, + moduleOptions: TenancyModuleOptions, + adapterHost: HttpAdapterHost, + ): string { + // Check if the adaptor is fastify + const isFastifyAdaptor = this.adapterIsFastify(adapterHost); + + if (!moduleOptions) { + throw new BadRequestException(`Tenant options are mandatory`); + } - return this.getTenantFromSubdomain(isFastifyAdaptor, req); + // Extract the tenant idetifier + const { tenantIdentifier = null, isTenantFromSubdomain = false } = + moduleOptions; - } else { - // Validate if tenant identifier token is present - if (!tenantIdentifier) { - throw new BadRequestException(`${tenantIdentifier} is mandatory`); - } + // Pull the tenant id from the subdomain + if (isTenantFromSubdomain) { + return this.getTenantFromSubdomain(isFastifyAdaptor, req); + } else { + // Validate if tenant identifier token is present + if (!tenantIdentifier) { + throw new BadRequestException(`${tenantIdentifier} is mandatory`); + } - return this.getTenantFromRequest(isFastifyAdaptor, req, tenantIdentifier); - } + return this.getTenantFromRequest(isFastifyAdaptor, req, tenantIdentifier); } - - /** - * Get the Tenant information from the request object - * - * @private - * @static - * @param {boolean} isFastifyAdaptor - * @param {Request} req - * @param {string} tenantIdentifier - * @returns - * @memberof TenancyCoreModule - */ - private static getTenantFromRequest(isFastifyAdaptor: boolean, req: Request, tenantIdentifier: string) { - let tenantId = ''; - - if (isFastifyAdaptor) { // For Fastify - // Get the tenant id from the header - tenantId = req.headers[`${tenantIdentifier || ''}`.toLowerCase()]?.toString() || ''; - } else { // For Express - Default - // Get the tenant id from the request - tenantId = req.get(`${tenantIdentifier}`) || ''; - } - - // Validate if tenant id is present - if (this.isEmpty(tenantId)) { - throw new BadRequestException(`${tenantIdentifier} is not supplied`); - } - - return tenantId; + } + + /** + * Get the Tenant information from the request object + * + * @private + * @static + * @param {boolean} isFastifyAdaptor + * @param {Request} req + * @param {string} tenantIdentifier + * @returns + * @memberof TenancyCoreModule + */ + private static getTenantFromRequest( + isFastifyAdaptor: boolean, + req: Request, + tenantIdentifier: string, + ) { + let tenantId = ''; + + if (isFastifyAdaptor) { + // For Fastify + // Get the tenant id from the header + tenantId = + req.headers[`${tenantIdentifier || ''}`.toLowerCase()]?.toString() || + ''; + } else { + // For Express - Default + // Get the tenant id from the request + tenantId = req.get(`${tenantIdentifier}`) || ''; } - /** - * Get the Tenant information from the request header - * - * @private - * @static - * @param {boolean} isFastifyAdaptor - * @param {Request} req - * @returns - * @memberof TenancyCoreModule - */ - private static getTenantFromSubdomain(isFastifyAdaptor: boolean, req: Request) { - let tenantId = ''; - - if (isFastifyAdaptor) { // For Fastify - const subdomains = this.getSubdomainsForFastify(req); - - if (subdomains instanceof Array && subdomains.length > 0) { - tenantId = subdomains[subdomains.length - 1]; - } - } else { // For Express - Default - // Check for multi-level subdomains and return only the first name - if (req.subdomains instanceof Array && req.subdomains.length > 0) { - tenantId = req.subdomains[req.subdomains.length - 1]; - } - } - - // Validate if tenant identifier token is present - if (this.isEmpty(tenantId)) { - throw new BadRequestException(`Tenant ID is mandatory`); - } - - return tenantId; + // Validate if tenant id is present + if (this.isEmpty(tenantId)) { + throw new BadRequestException(`${tenantIdentifier} is not supplied`); } - /** - * Get the connection for the tenant - * - * @private - * @static - * @param {String} tenantId - * @param {TenancyModuleOptions} moduleOptions - * @param {ConnectionMap} connMap - * @param {ModelDefinitionMap} modelDefMap - * @returns {Promise} - * @memberof TenancyCoreModule - */ - private static async getConnection( - tenantId: string, - moduleOptions: TenancyModuleOptions, - connMap: ConnectionMap, - modelDefMap: ModelDefinitionMap, - ): Promise { - // Check if validator is set, if so call the `validate` method on it - if (moduleOptions.validator) { - await moduleOptions.validator(tenantId).validate(); - } - - // Check if tenantId exist in the connection map - const exists = connMap.has(tenantId); - - // Return the connection if exist - if (exists) { - const connection = connMap.get(tenantId) as Connection; - - if (moduleOptions.forceCreateCollections) { - // For transactional support the Models/Collections has exist in the - // tenant database, otherwise it will throw error - await Promise.all( - Object.entries(connection.models).map(([k, m]) => m.createCollection()) - ); - } - - return connection; - } - - // Otherwise create a new connection - const uri = await Promise.resolve(moduleOptions.uri(tenantId)) - // Connection options - var connectionOptions: ConnectionOptions = { - useNewUrlParser: true, - useUnifiedTopology: true, - ...moduleOptions.options(), - }; - - // Create the connection - const connection = createConnection(uri, connectionOptions); - - // Attach connection to the models passed in the map - modelDefMap.forEach(async (definition: any) => { - const { name, schema, collection } = definition; - - const modelCreated: Model = connection.model(name, schema, collection); - - if (moduleOptions.forceCreateCollections) { - // For transactional support the Models/Collections has exist in the - // tenant database, otherwise it will throw error - await modelCreated.createCollection(); - } - }); - - // Add the new connection to the map - connMap.set(tenantId, connection); - - return connection; + return tenantId; + } + + /** + * Get the Tenant information from the request header + * + * @private + * @static + * @param {boolean} isFastifyAdaptor + * @param {Request} req + * @returns + * @memberof TenancyCoreModule + */ + private static getTenantFromSubdomain( + isFastifyAdaptor: boolean, + req: Request, + ) { + let tenantId = ''; + + if (isFastifyAdaptor) { + // For Fastify + const subdomains = this.getSubdomainsForFastify(req); + + if (subdomains instanceof Array && subdomains.length > 0) { + tenantId = subdomains[subdomains.length - 1]; + } + } else { + // For Express - Default + // Check for multi-level subdomains and return only the first name + if (req.subdomains instanceof Array && req.subdomains.length > 0) { + tenantId = req.subdomains[req.subdomains.length - 1]; + } } - /** - * Create connection map provider - * - * @private - * @static - * @returns {Provider} - * @memberof TenancyCoreModule - */ - private static createConnectionMapProvider(): Provider { - return { - provide: CONNECTION_MAP, - useFactory: (): ConnectionMap => new Map(), - } + // Validate if tenant identifier token is present + if (this.isEmpty(tenantId)) { + throw new BadRequestException(`Tenant ID is mandatory`); } - /** - * Create model definition map provider - * - * @private - * @static - * @returns {Provider} - * @memberof TenancyCoreModule - */ - private static createModelDefinitionMapProvider(): Provider { - return { - provide: MODEL_DEFINITION_MAP, - useFactory: (): ModelDefinitionMap => new Map(), - } + return tenantId; + } + + /** + * Get the connection for the tenant + * + * @private + * @static + * @param {String} tenantId + * @param {TenancyModuleOptions} moduleOptions + * @param {ConnectionMap} connMap + * @param {ModelDefinitionMap} modelDefMap + * @returns {Promise} + * @memberof TenancyCoreModule + */ + private static async getConnection( + tenantId: string, + moduleOptions: TenancyModuleOptions, + connMap: ConnectionMap, + modelDefMap: ModelDefinitionMap, + ): Promise { + // Check if validator is set, if so call the `validate` method on it + if (moduleOptions.validator) { + await moduleOptions.validator(tenantId).validate(); } - /** - * Create tenant context provider - * - * @private - * @static - * @returns {Provider} - * @memberof TenancyCoreModule - */ - private static createTenantContextProvider(): Provider { - return { - provide: TENANT_CONTEXT, - scope: Scope.REQUEST, - useFactory: ( - req: Request, - moduleOptions: TenancyModuleOptions, - adapterHost: HttpAdapterHost, - ) => this.getTenant(req, moduleOptions, adapterHost), - inject: [ - REQUEST, - TENANT_MODULE_OPTIONS, - DEFAULT_HTTP_ADAPTER_HOST, - ] - } - } + // Check if tenantId exist in the connection map + const exists = connMap.has(tenantId); - /** - * Create options providers - * - * @private - * @static - * @param {TenancyModuleAsyncOptions} options - * @returns {Provider[]} - * @memberof TenancyCoreModule - */ - private static createAsyncProviders( - options: TenancyModuleAsyncOptions, - ): Provider[] { - if (options.useExisting || options.useFactory) { - return [this.createAsyncOptionsProvider(options)]; - } - - const useClass = options.useClass as Type; - - return [ - this.createAsyncOptionsProvider(options), - { - provide: useClass, - useClass, - }, - ]; - } + // Return the connection if exist + if (exists) { + const connection = connMap.get(tenantId) as Connection; - /** - * Create options provider - * - * @private - * @static - * @param {TenancyModuleAsyncOptions} options - * @returns {Provider} - * @memberof TenancyCoreModule - */ - private static createAsyncOptionsProvider( - options: TenancyModuleAsyncOptions, - ): Provider { - if (options.useFactory) { - return { - provide: TENANT_MODULE_OPTIONS, - useFactory: options.useFactory, - inject: options.inject || [], - }; - } - - const inject = [ - (options.useClass || options.useExisting) as Type, - ]; - - return { - provide: TENANT_MODULE_OPTIONS, - useFactory: async (optionsFactory: TenancyOptionsFactory) => - await optionsFactory.createTenancyOptions(), - inject, - }; - } + if (moduleOptions.forceCreateCollections) { + // For transactional support the Models/Collections has exist in the + // tenant database, otherwise it will throw error + await Promise.all( + Object.entries(connection.models).map(([k, m]) => + m.createCollection(), + ), + ); + } - /** - * Create Http Adapter provider - * - * @private - * @static - * @returns {Provider} - * @memberof TenancyCoreModule - */ - private static createHttpAdapterProvider(): Provider { - return { - provide: DEFAULT_HTTP_ADAPTER_HOST, - useFactory: (adapterHost: HttpAdapterHost) => adapterHost, - inject: [ - HttpAdapterHost - ], - }; + return connection; } - /** - * Check if the object is empty or not - * - * @private - * @param {*} obj - * @returns - * @memberof TenancyCoreModule - */ - private static isEmpty(obj: any) { - return !obj || !Object.keys(obj).some(x => obj[x] !== void 0); + // Otherwise create a new connection + const uri = await Promise.resolve(moduleOptions.uri(tenantId)); + // Connection options + const connectionOptions: ConnectionOptions = { + useNewUrlParser: true, + useUnifiedTopology: true, + ...moduleOptions.options(), + }; + + // Create the connection + const connection = createConnection(uri, connectionOptions); + + // Attach connection to the models passed in the map + modelDefMap.forEach(async (definition: any) => { + const { name, schema, collection } = definition; + + const modelCreated: Model = connection.model( + name, + schema, + collection, + ); + + if (moduleOptions.forceCreateCollections) { + // For transactional support the Models/Collections has exist in the + // tenant database, otherwise it will throw error + await modelCreated.createCollection(); + } + }); + + // Add the new connection to the map + connMap.set(tenantId, connection); + + return connection; + } + + /** + * Create connection map provider + * + * @private + * @static + * @returns {Provider} + * @memberof TenancyCoreModule + */ + private static createConnectionMapProvider(): Provider { + return { + provide: CONNECTION_MAP, + useFactory: (): ConnectionMap => new Map(), + }; + } + + /** + * Create model definition map provider + * + * @private + * @static + * @returns {Provider} + * @memberof TenancyCoreModule + */ + private static createModelDefinitionMapProvider(): Provider { + return { + provide: MODEL_DEFINITION_MAP, + useFactory: (): ModelDefinitionMap => new Map(), + }; + } + + /** + * Create tenant context provider + * + * @private + * @static + * @returns {Provider} + * @memberof TenancyCoreModule + */ + private static createTenantContextProvider(): Provider { + return { + provide: TENANT_CONTEXT, + scope: Scope.REQUEST, + useFactory: ( + req: Request, + moduleOptions: TenancyModuleOptions, + adapterHost: HttpAdapterHost, + ) => this.getTenant(req, moduleOptions, adapterHost), + inject: [REQUEST, TENANT_MODULE_OPTIONS, DEFAULT_HTTP_ADAPTER_HOST], + }; + } + + /** + * Create options providers + * + * @private + * @static + * @param {TenancyModuleAsyncOptions} options + * @returns {Provider[]} + * @memberof TenancyCoreModule + */ + private static createAsyncProviders( + options: TenancyModuleAsyncOptions, + ): Provider[] { + if (options.useExisting || options.useFactory) { + return [this.createAsyncOptionsProvider(options)]; } - /** - * Check if the adapter is a fastify instance or not - * - * @private - * @static - * @param {HttpAdapterHost} adapterHost - * @returns {boolean} - * @memberof TenancyCoreModule - */ - private static adapterIsFastify(adapterHost: HttpAdapterHost): boolean { - return adapterHost.httpAdapter.getType() === 'fastify'; + const useClass = options.useClass as Type; + + return [ + this.createAsyncOptionsProvider(options), + { + provide: useClass, + useClass, + }, + ]; + } + + /** + * Create options provider + * + * @private + * @static + * @param {TenancyModuleAsyncOptions} options + * @returns {Provider} + * @memberof TenancyCoreModule + */ + private static createAsyncOptionsProvider( + options: TenancyModuleAsyncOptions, + ): Provider { + if (options.useFactory) { + return { + provide: TENANT_MODULE_OPTIONS, + useFactory: options.useFactory, + inject: options.inject || [], + }; } - /** - * Get the subdomains for fastify adaptor - * - * @private - * @static - * @param {Request} req - * @returns {string[]} - * @memberof TenancyCoreModule - */ - private static getSubdomainsForFastify(req: Request): string[] { - let host = req?.headers?.host || ''; - - host = host.split(':')[0]; - host = host.trim(); - - return host.split('.').reverse(); - } + const inject = [ + (options.useClass || options.useExisting) as Type, + ]; + + return { + provide: TENANT_MODULE_OPTIONS, + useFactory: async (optionsFactory: TenancyOptionsFactory) => + await optionsFactory.createTenancyOptions(), + inject, + }; + } + + /** + * Create Http Adapter provider + * + * @private + * @static + * @returns {Provider} + * @memberof TenancyCoreModule + */ + private static createHttpAdapterProvider(): Provider { + return { + provide: DEFAULT_HTTP_ADAPTER_HOST, + useFactory: (adapterHost: HttpAdapterHost) => adapterHost, + inject: [HttpAdapterHost], + }; + } + + /** + * Check if the object is empty or not + * + * @private + * @param {*} obj + * @returns + * @memberof TenancyCoreModule + */ + private static isEmpty(obj: any) { + return !obj || !Object.keys(obj).some((x) => obj[x] !== void 0); + } + + /** + * Check if the adapter is a fastify instance or not + * + * @private + * @static + * @param {HttpAdapterHost} adapterHost + * @returns {boolean} + * @memberof TenancyCoreModule + */ + private static adapterIsFastify(adapterHost: HttpAdapterHost): boolean { + return adapterHost.httpAdapter.getType() === 'fastify'; + } + + /** + * Get the subdomains for fastify adaptor + * + * @private + * @static + * @param {Request} req + * @returns {string[]} + * @memberof TenancyCoreModule + */ + private static getSubdomainsForFastify(req: Request): string[] { + let host = req?.headers?.host || ''; + + host = host.split(':')[0]; + host = host.trim(); + + return host.split('.').reverse(); + } } diff --git a/lib/tenancy-feature.module.ts b/lib/tenancy-feature.module.ts index e6b6605..c26c190 100644 --- a/lib/tenancy-feature.module.ts +++ b/lib/tenancy-feature.module.ts @@ -5,15 +5,13 @@ import { ModelDefinition } from './interfaces'; @Global() @Module({}) export class TenancyFeatureModule { + static register(models: ModelDefinition[]): DynamicModule { + const providers = createTenancyProviders(models); - static register(models: ModelDefinition[]): DynamicModule { - const providers = createTenancyProviders(models); - - return { - module: TenancyFeatureModule, - providers, - exports: providers, - }; - } - + return { + module: TenancyFeatureModule, + providers, + exports: providers, + }; + } } diff --git a/lib/tenancy.constants.ts b/lib/tenancy.constants.ts index 7993427..61a9bb3 100644 --- a/lib/tenancy.constants.ts +++ b/lib/tenancy.constants.ts @@ -1,5 +1,5 @@ export const DEFAULT_TENANT_DB_CONNECTION = 'TenantConnection'; -export const DEFAULT_HTTP_ADAPTER_HOST = 'DefaultHttpAdapterHost' +export const DEFAULT_HTTP_ADAPTER_HOST = 'DefaultHttpAdapterHost'; export const TENANT_CONTEXT = 'TenantContext'; export const MODEL_DEFINITION_MAP = 'ModelDefinitionMap'; diff --git a/lib/tenancy.module.ts b/lib/tenancy.module.ts index ab92c70..95a65e7 100644 --- a/lib/tenancy.module.ts +++ b/lib/tenancy.module.ts @@ -1,11 +1,15 @@ import { DynamicModule, Module } from '@nestjs/common'; -import { ModelDefinition, TenancyModuleAsyncOptions, TenancyModuleOptions } from './interfaces'; +import { + ModelDefinition, + TenancyModuleAsyncOptions, + TenancyModuleOptions, +} from './interfaces'; import { TenancyCoreModule } from './tenancy-core.module'; import { TenancyFeatureModule } from './tenancy-feature.module'; /** * Module to help with multi tenancy - * + * * For root configutaion: * ```ts * TenancyModule.forRoot({ @@ -14,7 +18,7 @@ import { TenancyFeatureModule } from './tenancy-feature.module'; * uri: (tenantId: string) => `mongodb://localhost/tenant-${tenantId}`, * }) * ``` - * + * * For root async configuration: * ```ts * TenancyModule.forRootAsync({ @@ -22,7 +26,7 @@ import { TenancyFeatureModule } from './tenancy-feature.module'; * inject: [ConfigService], * }) *``` - * + * * For feature configurations: * ```ts * TenancyModule.forFeature([{ name: 'Account', schema: AccountSchema }]) @@ -32,7 +36,6 @@ import { TenancyFeatureModule } from './tenancy-feature.module'; */ @Module({}) export class TenancyModule { - /** * For root synchronous imports * @@ -77,5 +80,4 @@ export class TenancyModule { imports: [TenancyFeatureModule.register(models)], }; } - } diff --git a/lib/types/index.ts b/lib/types/index.ts index 02eb0b3..1807615 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1 +1 @@ -export * from './tenancy-types'; \ No newline at end of file +export * from './tenancy-types'; diff --git a/lib/types/tenancy-types.ts b/lib/types/tenancy-types.ts index a7df5e0..80d6d43 100644 --- a/lib/types/tenancy-types.ts +++ b/lib/types/tenancy-types.ts @@ -1,3 +1,3 @@ export type ModelDefinitionMap = Record; -export type ConnectionMap = Record; \ No newline at end of file +export type ConnectionMap = Record; diff --git a/lib/utils/index.ts b/lib/utils/index.ts index a740df9..00b0cab 100644 --- a/lib/utils/index.ts +++ b/lib/utils/index.ts @@ -1 +1 @@ -export * from './tenancy.utils'; \ No newline at end of file +export * from './tenancy.utils'; diff --git a/lib/utils/tenancy.utils.ts b/lib/utils/tenancy.utils.ts index 5e78942..c227a6c 100644 --- a/lib/utils/tenancy.utils.ts +++ b/lib/utils/tenancy.utils.ts @@ -8,7 +8,7 @@ import { DEFAULT_TENANT_DB_CONNECTION } from '../tenancy.constants'; * @returns */ export function getTenantModelToken(model: string) { - return `${model}Model`; + return `${model}Model`; } /** @@ -19,7 +19,7 @@ export function getTenantModelToken(model: string) { * @returns */ export function getTenantModelDefinitionToken(model: string) { - return `${model}Definition`; + return `${model}Definition`; } /** @@ -30,7 +30,7 @@ export function getTenantModelDefinitionToken(model: string) { * @returns */ export function getTenantConnectionToken(name?: string) { - return name && name !== DEFAULT_TENANT_DB_CONNECTION - ? `${name}TenantConnection` - : DEFAULT_TENANT_DB_CONNECTION; + return name && name !== DEFAULT_TENANT_DB_CONNECTION + ? `${name}TenantConnection` + : DEFAULT_TENANT_DB_CONNECTION; } diff --git a/package.json b/package.json index 2a97eb3..ea82627 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "lint": "eslint \"lib/**/*.ts\" --fix", "format": "prettier \"lib/**/*.ts\" --write", - "build": "rm -rf dist && tsc -p tsconfig.json", + "build": "rm -rf dist && tsc -p tsconfig.build.json", "precommit": "lint-staged", "prepublish:npm": "npm run build", "publish:npm": "npm publish --access public", @@ -21,45 +21,44 @@ "test:e2e:dev": "jest --config ./tests/jest-e2e.json --runInBand --watch" }, "devDependencies": { - "@nestjs/cli": "^8.1.3", - "@nestjs/common": "^8.4.7", - "@nestjs/core": "^8.4.7", - "@nestjs/mongoose": "^9.1.1", - "@nestjs/platform-express": "^8.4.7", - "@nestjs/schematics": "^8.0.4", - "@nestjs/testing": "^8.1.1", - "@types/express": "^4.17.13", - "@types/jest": "^27.0.2", - "@types/node": "16.11.40", - "@types/supertest": "^2.0.11", - "@typescript-eslint/eslint-plugin": "^4.29.2", - "@typescript-eslint/parser": "^4.29.2", + "@nestjs/cli": "^9.1.5", + "@nestjs/common": "^9.1.6", + "@nestjs/core": "^9.1.6", + "@nestjs/mongoose": "^9.2.0", + "@nestjs/platform-express": "^9.1.6", + "@nestjs/schematics": "^9.0.3", + "@nestjs/testing": "^9.1.6", + "@types/express": "^4.17.14", + "@types/jest": "^29.2.0", + "@types/node": "18.11.5", + "@types/supertest": "^2.0.12", + "@typescript-eslint/eslint-plugin": "^4.33.0", + "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-import": "2.26.0", - "eslint-plugin-prettier": "4.0.0", - "jest": "28.1.1", - "mongoose": "6.3.8", - "prettier": "2.7.0", + "eslint-plugin-prettier": "4.2.1", + "jest": "29.2.2", + "mongoose": "6.7.0", + "prettier": "2.7.1", "reflect-metadata": "^0.1.13", - "release-it": "^13.6.1", - "rxjs": "7.5.5", - "supertest": "6.2.3", - "ts-jest": "28.0.5", + "release-it": "^15.5.0", + "rxjs": "7.5.7", + "supertest": "6.3.1", + "ts-jest": "29.0.3", "ts-loader": "^9.2.6", - "ts-node": "^10.3.0", + "ts-node": "^10.9.1", "tsconfig-paths": "^3.11.0", - "typescript": "^4.4.4" + "typescript": "^4.8.4" }, "peerDependencies": { - "@nestjs/common": "^8.0.0", - "@nestjs/core": "^8.0.0", + "@nestjs/common": "^8.0.0 || ^9.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0", "mongoose": "^6.3.8", - "reflect-metadata": "^0.1.13" + "reflect-metadata": "^0.1.13", + "rxjs": "^7.0.0" }, "lint-staged": { - "*.ts": [ - "prettier --write" - ] + "**/*.{ts,json}": [] } -} +} \ No newline at end of file diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..1e5a288 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,9 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./lib" + }, + "include": ["lib/**/*"], + "exclude": ["node_modules", "tests", "dist", "**/*spec.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 72faf96..219942a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,16 +9,7 @@ "experimentalDecorators": true, "target": "es6", "sourceMap": false, - "outDir": "./dist", - "rootDir": "./lib", "strict": true, "strictPropertyInitialization": false - }, - "include": [ - "lib/**/*" - ], - "exclude": [ - "node_modules", - "**/*.spec.ts" - ] -} \ No newline at end of file + } +}