How can i setup multitenant in NESTJS
Asked Answered
G

3

11

I want to connect to any database based on the subdomain (multi-tenant), but i'm not sure how can i do it.

My code runs when the app is started, but i don't know how to change the Datasource based on subdomain.

PS: I created middleware on each request, but I don't know how to change the source.

I have the following code for my DB:

import { connect, createConnection } from 'mongoose';
import { SERVER_CONFIG, DB_CONNECTION_TOKEN } from '../server.constants';

 const opts = {
    useCreateIndex: true,
    useNewUrlParser: true,
    keepAlive: true,
    socketTimeoutMS: 30000,
    poolSize: 100,
    reconnectTries: Number.MAX_VALUE,
    reconnectInterval: 500,
    autoReconnect: true,
  };
export const databaseProviders = [
  {
    provide: DB_CONNECTION_TOKEN,
    useFactory: async () => {
      try {
        console.log(`Connecting to ${ SERVER_CONFIG.db }`);
        return await createConnection(`${SERVER_CONFIG.db}`, opts);
      } catch (ex) {
        console.log(ex);
      }

    },
  }
];

I want to change my datasource in each request based on subdomain (multi-tenant)

Gizzard answered 10/5, 2019 at 22:49 Comment(0)
S
24

Here is a solution that i used with mongoose

  1. TenantsService used to manage all tenants in the application
@Injectable()
export class TenantsService {
    constructor(
        @InjectModel('Tenant') private readonly tenantModel: Model<ITenant>,
    ) {}

    /**
     * Save tenant data
     *
     * @param {CreateTenantDto} createTenant
     * @returns {Promise<ITenant>}
     * @memberof TenantsService
     */
    async create(createTenant: CreateTenantDto): Promise<ITenant> {
        try {
            const dataToPersist = new this.tenantModel(createTenant);
            // Persist the data
            return await dataToPersist.save();
        } catch (error) {
            throw new HttpException(error, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Find details of a tenant by name
     *
     * @param {string} name
     * @returns {Promise<ITenant>}
     * @memberof TenantsService
     */
    async findByName(name: string): Promise<ITenant> {
        return await this.tenantModel.findOne({ name });
    }
}

  1. TenantAwareMiddleware middleware to get the tenant id from the request context. You can make your own logic here to extract the tenant id, either from request header or from request url subdomain. Request header extraction method is shown here.

If you want to extract the subdomain the same can be done by extracting it from the Request object by calling req.subdomains, which would give you a list of subdomains and then you can get the one you are looking for from that.

@Injectable()
export class TenantAwareMiddleware implements NestMiddleware {
    async use(req: Request, res: Response, next: NextFunction) {
        // Extract from the request object
        const { subdomains, headers } = req;

        // Get the tenant id from header
        const tenantId = headers['X-TENANT-ID'] || headers['x-tenant-id'];

        if (!tenantId) {
            throw new HttpException('`X-TENANT-ID` not provided', HttpStatus.NOT_FOUND);
        }

        // Set the tenant id in the header
        req['tenantId'] = tenantId.toString();

        next();
    }
}
  1. TenantConnection this class is used to create new connection using tenant id and if there is an existing connection available it would return back the same connection (to avoid creating additional connections).
@Injectable()
export class TenantConnection {
    private _tenantId: string;

    constructor(
        private tenantService: TenantsService,
        private configService: ConfigService,
    ) {}

    /**
     * Set the context of the tenant
     *
     * @memberof TenantConnection
     */
    set tenantId(tenantId: string) {
        this._tenantId = tenantId;
    }

    /**
     * Get the connection details
     *
     * @param {ITenant} tenant
     * @returns
     * @memberof TenantConnection
     */
    async getConnection(): Connection {
        // Get the tenant details from the database
        const tenant = await this.tenantService.findByName(this._tenantId);

        // Validation check if tenant exist
        if (!tenant) {
            throw new HttpException('Tenant not found', HttpStatus.NOT_FOUND);
        }

        // Get the underlying mongoose connections
        const connections: Connection[] = mongoose.connections;

        // Find existing connection
        const foundConn = connections.find((con: Connection) => {
            return con.name === `tenantDB_${tenant.name}`;
        });

        // Check if connection exist and is ready to execute
        if (foundConn && foundConn.readyState === 1) {
            return foundConn;
        }

        // Create a new connection
        return await this.createConnection(tenant);
    }

    /**
     * Create new connection
     *
     * @private
     * @param {ITenant} tenant
     * @returns {Connection}
     * @memberof TenantConnection
     */
    private async createConnection(tenant: ITenant): Promise<Connection> {
        // Create or Return a mongo connection
        return await mongoose.createConnection(`${tenant.uri}`, this.configService.get('tenant.dbOptions'));
    }
}

  1. TenantConnectionFactory this is custom provider which gets you the tenant id and also helps in creation of the connection
// Tenant creation factory
export const TenantConnectionFactory = [
    {
        provide: 'TENANT_CONTEXT',
        scope: Scope.REQUEST,
        inject: [REQUEST],
        useFactory: (req: Request): ITenantContext => {
            const { tenantId } = req as any;
            return new TenantContext(tenantId);
        },
    },
    {
        provide: 'TENANT_CONNECTION',
        useFactory: async (context: ITenantContext, connection: TenantConnection): Promise<typeof mongoose>  => {
            // Set tenant context
            connection.tenantId = context.tenantId;

            // Return the connection
            return connection.getConnection();
        },
        inject: ['TENANT_CONTEXT', TenantConnection],
    },
];
  1. TenantsModule - Here you can see the TenantConnectionFactory added as a provider and is being exported to be used inside other modules.
@Module({
  imports: [
    CoreModule,
  ],
  controllers: [TenantsController],
  providers: [
    TenantsService,
    TenantConnection,
    ...TenantConnectionFactory,
  ],
  exports: [
    ...TenantConnectionFactory,
  ],
})
export class TenantsModule {}
  1. TenantModelProviders - Since your tenant models depends on the tenant connection, your models have to defined through a provider and then included inside the module where you initialise them.
export const TenantModelProviders = [
    {
        provide: 'USER_MODEL',
        useFactory: (connection: Connection) => connection.model('User', UserSchema),
        inject: ['TENANT_CONNECTION'],
    },
];
  1. UsersModule - This class will be using the models. You can also see the middleware being configured here to act upon your tenand db routes. This case all the user routes are part of the tenant and will be served by tenant db.
@Module({
  imports: [
    CoreModule,
    TenantsModule,
  ],
  providers: [
    UsersService,
    ...TenantModelProviders,
  ],
  controllers: [UsersController],
})
export class UsersModule implements NestModule {
  configure(context: MiddlewareConsumer) {
    context.apply(TenantAwareMiddleware).forRoutes('/users');
  }
}
  1. UsersService - Example implementation of accessing tenant db from user module
@Injectable()
export class UsersService {

    constructor(
        @Inject('TENANT_CONTEXT') readonly tenantContext: ITenantContext,
        @Inject('USER_MODEL') private userModel: Model<IUser>,
    ) {
        Logger.debug(`Current tenant: ${this.tenantContext.tenantId}`);
    }

    /**
     * Create a new user
     *
     * @param {CreateUserDto} user
     * @returns {Promise<IUser>}
     * @memberof UsersService
     */
    async create(user: CreateUserDto): Promise<IUser> {
        try {
            const dataToPersist = new this.userModel(user);
            // Persist the data
            return await dataToPersist.save();
        } catch (error) {
            throw new HttpException(error, HttpStatus.BAD_REQUEST);
        }
    }

    /**
     * Get the list of all users
     *
     * @returns {Promise<IUser>}
     * @memberof UsersService
     */
    async findAll(): Promise<IUser> {
        return await this.userModel.find({});
    }
}

Schoolbag answered 16/1, 2020 at 8:35 Comment(8)
using the scope request can cause performance to be altered in all services that use the factory? Isn't there a solution that doesn't involve making the services instantiate again?Adamis
@Adamis i guess you can, but the strategy you use would just involve a change in way you get the tenant id. Post that the connection creation process would remain the same.Schoolbag
@SandeepKNair, where is ITenant coming from? Is there an example code somewhere to check out?Blackpoll
@Blackpoll that is just an interface representing your Tenant entity. It's upto you what fields you want to put into it. Hope that clears your doubt. For example export interface ITenant { readonly _id: string; readonly name: string; readonly uri: string; }Schoolbag
@SandeepKNair, thanks for the reply. I have a problem. I can't import this line: import { TenancyValidator } from '@app/tenancy'; So instead I imported this one: import { TenancyValidator } from 'nestjs-tenancy'; However, I get an error: ERROR [ExceptionsHandler] The dialect mongodb+srv is not supported. Supported dialects: mssql, mariadb, mysql, oracle, postgres, db2 and sqlite. Do you know why?Blackpoll
@SandeepKNair This is my test repo: github.com/SamLevinSE/multi_tenBlackpoll
@Blackpoll thanks for pointing it out. That was a mistake in the doc's. The correct import path is import { TenancyValidator } from "@needle-innovision/nestjs-tenancy";. I have corrected this in the docsSchoolbag
@SandeepKNair This solution is not working anymoreConfederation
A
6

We also have a Mulit-Tenancy Setup for our NestJS Setup.
You could have a middleware that decides, depending on the request, which datasource to use. In our example we are using TypeORM which has a pretty good integration in NestJS. There are some useful functions within the TypeORM package.

Middleware

export class AppModule {
  constructor(private readonly connection: Connection) {
  }

  configure(consumer: MiddlewareConsumer): void {
    consumer
      .apply(async (req, res, next) => {
        try {
          getConnection(tenant);
          next();
        } catch (e) {
          const tenantRepository = this.connection.getRepository(tenant);
          const tenant = await tenantRepository.findOne({ name: tenant });
          if (tenant) {
            const createdConnection: Connection = await createConnection(options);
            if (createdConnection) {
              next();
            } else {
              throw new CustomNotFoundException(
                'Database Connection Error',
                'There is a Error with the Database!',
              );
            }
          }
        }
      }).forRoutes('*');
   }

This is an example of our middleware. TypeORM is managing the connections internally. So the first thing you would try is to load the connection for that specific tenant. If there is one, good otherwise just create one. The good thing here is, that once created the connection stays available in the TypeORM connection manager. This way you always have a connection in the routes.
In your routes you need a identification for your tenants. In our case it is just a string which is extracted from the url. Whatever value it is you can bind it to the request object inside your middleware. In your controller you extract that value again and pass it to your services. Then you have to load the repository for your tenant and your good to go.

Service Class

@Injectable()
export class SampleService {

  constructor() {}

  async getTenantRepository(tenant: string): Promise<Repository<Entity>> {
    try {
      const connection: Connection = await getConnection(tenant);
      return connection.getRepository(Property);
    } catch (e) {
      throw new CustomInternalServerError('Internal Server Error', 'Internal Server Error');
    }
  }

  async findOne(params: Dto, tenant: string) {

    const entityRepository: Repository<Entity> = await this.getTenantRepository(tenant);

    return await propertyRepository.findOne({ where: params });

  }

That's what a service looks like in our application.

Hopefully this will inspire you and get you going with your problem :)

Ats answered 15/5, 2019 at 13:8 Comment(1)
I would love to see full GIT repo of it with example of getConnection method as well as MiddlewareConsumer.Antilogism
F
3

You should use a provider with a factory to inject connection details within each service, and switch repos accordingly.

Here is the connection factory (assuming the request contains the tenant id):

const connectionFactory = {
  provide: CONNECTION,
  scope: Scope.REQUEST,
  useFactory: (request: ExpressRequest) => {
    const { tenantId } = request;

    if (tenantId) {
      return getTenantConnection(tenantId);
    }

    return null;
  },
  inject: [REQUEST],
};

The connection for each tenant can be gotten like this:

export function getTenantConnection(tenantId: string): Promise<Connection> {
  const connectionName = `tenant_${tenantId}`;
  const connectionManager = getConnectionManager();

  if (connectionManager.has(connectionName)) {
    const connection = connectionManager.get(connectionName);
    return Promise.resolve(connection.isConnected ? connection : connection.connect());
  }

  return createConnection({
    ...(tenantsOrmconfig as PostgresConnectionOptions),
    name: connectionName,
    schema: connectionName,
  });
}

Then you can use the connection in each service:

@Injectable()
export class CatsService {
  private readonly catsRepository: Repository<Cat>;

  constructor(
    @Inject(CONNECTION) connection: Connection,
  ) {
    this.catsRepository = connection.getRepository(Cat);
  }

  create(createCatDto: CreateCatDto): Promise<Cat> {
    const cat = new Cat();
    cat.name = createCatDto.name;

    return this.catsRepository.save(cat);
  }

  async findAll(): Promise<Cat[]> {
    return this.catsRepository.find();
  }
}

Note that there's a full repo with all related services and setup accessible from this article that does a walkthrough; https://thomasvds.com/schema-based-multitenancy-with-nest-js-type-orm-and-postgres-sql/.

Friedly answered 21/1, 2022 at 11:7 Comment(1)
what is the memory consumption with that solution?Maeve

© 2022 - 2024 — McMap. All rights reserved.