needle-innovision / nestjs-tenancy

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

How do I exclude a controller/service method from the tenant check #8

Closed alexonozor closed 3 years ago

alexonozor commented 3 years ago

Hi there, I would like to exclude a particular service/controller method from been validated by the tenantId validation that comes from the header.

sandeepsuvit commented 3 years ago

Hi @alexonozor I didn't really catch the usecase there. Cloud you give some examples of your usecase here. Ideally this tenancy module acts on the connection object via the request context properties which you inject via InjectTenancyModel and get the connection of that particular document which belongs to the tenant. The services and controller doesn't actually matter. You can still use your normal mongoose InjectModel in your service's for your default connection though.

alexonozor commented 3 years ago

Ok my Tenant service I added

tenant.service.ts

export class TenantsService {
  constructor(
     @InjectTenancyModel('Tenant') private tenantModel: Model<any>,
     @InjectModel('Tenant') private tenantFreeModel: Model<any>,
    private staffService: StaffsService
    ) { }

    // I want to call the search without the validation it still shows errors 404. "message": "X-TENANT-ID is not supplied",

    async search(params): Promise<any[]> {
    return this.tenantFreeModel.find({ "tenantId": { "$regex": params, "$options": "i" }})
      .select("_id name email photo")
  }

   async delete(tenantId: string): Promise<any> {
    return this.tenantModel.deleteOne({ _id: tenantId });
  }
  }

tenant.module.ts


import { Module } from '@nestjs/common';
import { TenantsController } from './tenants.controller';
import { TenantsService } from './tenants.service';
import { TenantSchema } from '../schemas/tanant.schema';
import { TenancyModule } from '@needle-innovision/nestjs-tenancy';
import { ConfigService } from '@nestjs/config';
import { StaffsService } from 'src/staffs/staffs.service';

@Module({
  imports: [
    TenancyModule.forFeature([{ name: 'Tenant', schema: TenantSchema }
  ])],
  controllers: [TenantsController],
  providers: [TenantsService, StaffsService, ConfigService],
  exports: [TenantsService]
})
export class TenantModule {}

app.module.ts

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true, load: [configuration] }),
    TenancyModule.forRoot({
      tenantIdentifier: 'X-TENANT-ID',
      options: () => { },
      uri: (tenantId: string) => `${process.env.DATABASE_URL}/${tenantId}`,
    }),
    InvitationModule,
    UsersModule,
    ShippingsModule,
    CategoriesModule,
    AuthModule,
    FoodsModule,
    CartsModule,
    OrdersModule,
    FavoritesModule,
    StaffsModule,
    TenantModule,
    MailModule,
    CollectionsModule
  ],
  providers: [ConfigService]
})
export class AppModule {
  constructor() {
    console.log(console.log(process.env.DATABASE_URL))
  }
}
sandeepsuvit commented 3 years ago

Hi @alexonozor sorry for the late reply,

I am bit confused the way you have arranged the Tenant model in the TenantsService. Sorry if i misunderstood your implementation. Here is what i normally use it for your reference.

  1. My App Module - app.module.ts would look like this

@Module({
  imports: [
    // Load the default configuration file
    ConfigModule.forRoot({ load: configuration, isGlobal: true }),
    // Mongoose default connection for master data
    MongooseModule.forRootAsync({
      useFactory: async (cfs: ConfigService) => cfs.get(`database`),
      inject: [ConfigService],
    }),
    // Multi tenancy configuration
    TenancyModule.forRootAsync({
      imports: [MasterDatastoreModule], // For accessing Custom Tenancy Validator exported via `MasterDatastoreModule`
      useFactory: async (cfs: ConfigService, tVal: CustomTenantValidator) => {
        return {
          ...cfs.get(`tenant`),  // Contains connection properties for tenant db
          // Custom validator to check if the tenant exist in common database
          validator: (tenantId: string) => tVal.setTenantId(tenantId),
        }
      },
      inject: [ConfigService, CustomTenantValidator],
    }),
    MasterDatastoreModule,
    TenantDataModule,
    ...other modules
  ],
  controllers: [AppController],
  providers: [
    AppService,
  ],
})
export class AppModule {}
  1. My Master and Tenant modules

// Master module
@Module({
  imports: [
    // Register all master level entities here
    MongooseModule.forFeature([
      { name: 'Account', schema: AccountSchema },
    ]),
  ],
  controllers: [],
  providers: [
    MongoConnectionService,
    CustomTenantValidator,
  ],
  exports: [
    MongooseModule,
    DbUtilsService,
    CustomTenantValidator,
  ],
})
export class MasterDatastoreModule {}

// Tenant Module that exports tenant db related schemas

@Module({
    imports: [
        // Register all tenant level entities here
        TenancyModule.forFeature([
            // Module entities
            { name: 'Project', schema: ProjectSchema },
        ]),
    ],
    providers: [
    ],
    exports: [
    ]
})
export class TenantDatastoreModule {}
  1. My TenantsModule

@Module({
  imports: [
    TenantDatastoreModule,
    MasterDatastoreModule
  ],
  controllers: [TenantsController],
  providers: [TenantsService],
  exports: [TenantsService]
})
export class TenantsModule {}
  1. Within TenantsService i would have operations related to all tenants that are in the master db

@Injectable()
export class TenantsService {
    constructor(
        @InjectModel('Account')
        private readonly accountModel: Model<IAccountModel>,
        @InjectModel('AccountOwner')
        private readonly accountOwnerModel: Model<IAccountOwnerModel>,
        @InjectModel('AccountSubscription')
        private readonly accountSubscriptionModel: Model<IAccountSubscriptionModel>
    ) { }

// Example method inside this service
private async createAccountOwner(
        ownerDto: CreateCustomerDto,
    ): Promise<IAccountOwnerModel> {
        try {
            const owner = new AccountOwnerDto({
                firstName: ownerDto.firstName,
                lastName: ownerDto.lastName,
                email: ownerDto.email,
                phone: ownerDto.phone,
            });

            const ownerModel = new this.accountOwnerModel(owner);

            // Persist the details to master db
            return await ownerModel.save();
        } catch (error) {
            throw new InternalServerErrorException(error);
        }
    }
 }
  1. Module and Services on the Tenant db side

@Module({
  imports: [
    TenantDatastoreModule,
  ],
  controllers: [ProjectsController],
  providers: [ProjectsService],
  exports: [ProjectsService]
})
export class ProjectsModule {}

@Injectable()
export class ProjectsService {
    constructor(
        @InjectTenancyModel('Project')
        private readonly projectModel: PaginateModel<IProjectModel>
    ) {}

async findAllProjects(
        page: number,
        limit: number,
        userId: string,
    ): Promise<PaginateResult<IProjectModel>> {
        // Useful to create query condition
        const query = {};

        // Params for pagination
        const options = {
            // sort: { createdOn: -1 },
            page: Number(page),
            limit: Number(limit),
            populate: { 
                path: 'owner',
                select: BaseUserDetails,
                populate: {
                    path: 'profile',
                    select: BaseProfileDetails,
                }
            }
        };

        // Fetch the record from the tenant db
        return await this.projectModel.paginate(query, options);
    }

}

If this still doesn't help you i would recommend you to create a sample github project with your issue and share it with me to debug it.

alexonozor commented 3 years ago

Thanks, it works.

cesc1802 commented 2 years ago

could you share the full project you already apply this library?