Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: How to use Factory Proxy Providers and How to Register? #199

Closed
rupakkarki27 opened this issue Jan 17, 2025 · 3 comments · Fixed by #216
Closed

Question: How to use Factory Proxy Providers and How to Register? #199

rupakkarki27 opened this issue Jan 17, 2025 · 3 comments · Fixed by #216
Labels
bug Something isn't working

Comments

@rupakkarki27
Copy link

rupakkarki27 commented Jan 17, 2025

Hi @Papooch, thanks for the library.

I am currently trying to migrate from request scoped providers to nestjs-cls to support tenant specific database connections. I was also looking at this issue: #89 to get a context on how to improve the dev experience.

The problem is, the datasource in the factory is an empty object and I get datasource.getRepository is not a function. But, in the ClsModule.forFeatureAsync, the connection is defined on each request.

app.module.ts

@Module({
  imports: [
    TypeOrmModule.forRoot(commonTypeOrmModuleOptions),
    ClsModule.forRoot({
      global: true,
      middleware: { mount: true },
    }),
    ClsModule.forFeatureAsync({
      provide: TENANT_CONNECTION_PROVIDER,
      inject: [CLS_REQ],
      useFactory: async (request: Request) => {
        const tenantId = request.headers['x-tenant-id'];

        if (!tenantId) {
          throw new BadRequestException('Tenant ID is required');
        }
        // connection is defined here - getTenantConnection is just a normal function
        const connection =  getTenantConnection(tenantId);

        return connection;
      },
      global: true,
    }),
  ],
  controllers: [AppController],
  exports: [ClsModule],
})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*')
      .apply(JwtMiddleware)
      .exclude(...JWT_MIDDLEWARE_EXCLUDED_ROUTES)
      .forRoutes({ path: '*', method: RequestMethod.ALL });
  }
}

I also created a higher order function to help with the injection.

export const registerTenantRepository = (
  repoClass: Type<any>,
  entityClass: Type<any>,
) => {
  return ClsModule.forFeatureAsync({
    provide: repoClass,
    inject: [TENANT_CONNECTION_PROVIDER],
    useFactory: async (datasource: DataSource) => {
       // the datasource is {}
      return datasource.getRepository(entityClass);
    },
    global: true,
  });
};

and using it in modules like:

  imports: [
    registerTenantRepository(Repository<User>, User)
  ]

I tried debugging a lot but cannot find out what went wrong. Any help would be appriciated.

@Papooch
Copy link
Owner

Papooch commented Jan 17, 2025

Hi, thanks for reaching out.

I think that you just bumped into a situation, where Proxy providers do not work well (yet) - and that is injecting proxy providers into other proxy providers, there's an open issue that I still haven't got around to implement: #169. (EDIT: It has been implemented)

The root of the issue is that there is no guaranteed order in which Proxy providers resolve. We can leverage Nest's dependency injection to gather the needed dependencies, but we can't use it's dependency resolution algorithm to instantiate them in the correct order (because from Nest's perspective, they are singletons and have already been constructed).

That means that the repoClass Proxy provider is being resolved before or at the same time as TENANT_CONNECTION_PROVIDER.

Until a proper solution is implemented in the library, this can be "worked around" in two ways:

1) Manually resolve proxy providers in the correct order.

  1. In ClsModule.forRoot, set resolveProxyProviders to false
ClsModule.forRoot({
  global: true,
  middleware: {
    mount: true
+   resolveProxyProviders: false
  },
}),

This will disable automatic resolution of Proxy providers in the middleware

  1. Bind an extra middleware that injects ClsService to trigger the resolution:
@Injectable()
export class ManualProxyResolvingMiddleware implements NestMiddleware {
  constructor(private readonly cls: ClsService) {}

  async use(_req: any, _res: any, next: () => any) {
    // resolve the providers that are depended upon by others
    await this.cls.resolveProxyProviders([TENANT_CONNECTION_PROVIDER])
    // resolve the rest of unresolved proxy providers
    await this.cls.resolveProxyProviders()
  }
}

Then your existing Proxy providers should work as expected.

2) Do not use Proxy provider for the DataSource.

If we can put the DataSource instance in the CLS before any Proxy provider needs it, we can retrieve it in the Proxy's factory. This can be done in the setup function:

ClsModule.forRoot({
  global: true,
  middleware: {
    mount: true
    setup: (cls, req) => {
      const tenantId = req.headers['x-tenant-id'];
      if (!tenantId) {
        throw new BadRequestException('Tenant ID is required');
      }
      const connection =  getTenantConnection(tenantId);
      // attach the TENANT_CONNECTION to the CLS
      cls.set(TENANT_CONNECTION, connection);
    }
  },
}),

Then, you refactor your existing Proxy providers into these:

ClsModule.forFeatureAsync({
      provide: TENANT_CONNECTION_PROVIDER,
      inject: [ClsService],
      // this essentially just makes a proxy provider out of the existing CLS value
      useFactory: (cls: ClsService) => cls.get(TENANT_CONNECTION)
      global: true,
    }),

And

export const registerTenantRepository = (
  repoClass: Type<any>,
  entityClass: Type<any>,
) => {
  return ClsModule.forFeatureAsync({
    provide: repoClass,
    inject: [ClsService],
    useFactory: (cls: ClsService) => {
      const datasource = cls.get(TENANT_CONNECTION)
      return datasource.getRepository(entityClass);
    },
    global: true,
  });
};

I hope this helps.


By the way, the way you register your repository providers is dangerous:

registerTenantRepository(Repository<User>, User)

Since TypeScript generic don't exist at runtime, if you register multiple repositories this way, they will all be registered under a token called Repository and each subsequent one will override the previous.

That's the reason I suggested to define a "dummy" abstract class AbcRepository extends Repository<Abc> to get around this limitation.

@Papooch Papooch added the bug Something isn't working label Jan 17, 2025
@rupakkarki27
Copy link
Author

Thanks @Papooch for the detailed explanation and reply. and also thank you for pointing out the way I was registering my providers using generics.

I used the second workaround of not using the proxy provider at all and storing the connection in Cls. This is working as expected and I'm planning to test it out more.

Closing this issue for now. Thank you so much again.

@Papooch
Copy link
Owner

Papooch commented Feb 16, 2025

@rupakkarki27 A proper algorithm for resolving Proxy Providers' dependencies in the correct order has been implemented and published in v5.3.0. The code in your original post should now work without any extra workarounds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants