needle-innovision / nestjs-tenancy

Multi-tenancy approach for nestjs - currently supported only for mongodb with mongoose
MIT License
187 stars 58 forks source link

Change tenant on the fly #11

Open samuelpares opened 2 years ago

samuelpares commented 2 years ago

How can I change the tenant on the fly?

Use case: My tenant can have multiple users. The users will be scoped in the tenant database. So, when I'm creating a new Tenant in the default database, there will be in the payload as well the first user (tenant owner). So, right after creating the tenant in the default database, I want to change to the tenant database to insert the user. Is there a way to do this in the same request for the tenant creation or do I need to to in another request, this time passing the tenant to the request?

sandeepsuvit commented 2 years ago

@samuelpares thanks for writing in. I am guessing the current way this library is developed is by handling the switching per request since the toggling of tenants depends on the identifier in the header. But if you need to switch to a tenant database right after creation that has to be custom handled at your service level by opening a connection to the tenant db or like you mentioned by making another request with the tenant identifier attached.

Do you see a better solution to this? Happy to hear your opinion aswell :)

Daniel-Boll commented 2 years ago

I think a good solution would be in a scenario where I can within a request change the current tenant, think of.

@Get(':tenantId')
async changeTenantOnRequest(@Param('tenantId') tenantId: string) {
    changeTenantHere(tenantId);
    // Now models are connected to this model
}

The only way of achieving this is through a manual connection to mongoose?

samuelpares commented 2 years ago

@sandeepsuvit, first of all, thanks for this nice lib, it's helping me a lot writing multitenant apps.

As for my opnion, since I'm very new to nestjs, I can't say if it is possible, but a good solution would be to have a method I could call passing the db name to change, as Daniel said above. This would solve my use case, and another one I faced now: allow me to use it in a Bull Queue processor.. today I'm importing mongoose again and doing everything manually.

sandeepsuvit commented 2 years ago

@samuelpares thanks for your kind words :)

The way this library is architected is by switching the db context at two levels

I assume making another call to switch the db post that (in an HTTP request response cycle) may not be required since the tenant connection would have been resolved by then and made available to you as we can see here

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`);
        }

        // Extract the tenant identifier
        const {
            tenantIdentifier = null,
            isTenantFromSubdomain = false,
        } = moduleOptions;

        // 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);
        }
    }

But as per your usecase switching the db context in a Queue processor is something the library doesn't support simply because we don't have access to the request scope there since the queue process is normally an asynchronous operation that doesn't fall under the http request response lifecycle.

My one suggestion is to have a common service TenantConnectionService created on your side to manage this manual switching which can be reused in other so called queue operations.

If you have any other suggestions other than what i proposed above i would be happy to hear that.

samuelpares commented 2 years ago

It sounds like good approach, but I don't have the expertise to implement it. So I added to my job data the database name. In the processor function I get the database name and create a new mongoose connection. Although it's not the best solution, it works fine.

Another situation I have now: cron jobs! I'm creating dynamic jobs using the SchedulerRegistry.addCronJob(name, job). So in my service I created the function:

  addCronJob(name: string, dateTime: Date) {
    const job = new CronJob(dateTime, async () => {
      console.log(`time (${dateTime}) for job ${name} to run!`);
      const a = await this.findOne('61ed5f605c128a50a8991809');
      console.log(a);
    });

    this.schedulerRegistry.addCronJob(name, job);
    job.start();

    console.log('job added');
  }

The await this.findOne() is using the model injected with tenancy.. Creating the job to be executed within 10 seconds, worked fine, in the right connection. I imagine that if schedule it to a week for now, it would not work, right?