feat: implement dynamic driver configuration + fix integration test log pollution (#12104)
### Primary Changes: Dynamic Driver Configuration Refactors FileStorageService and EmailSenderService to support dynamic driver configuration changes at runtime without requiring application restarts. **Key Architectural Change**: Instead of conditionally registering drivers at build time based on configuration, we now **register all possible drivers eagerly** and select the appropriate one at runtime. ### What Changed: - **Before**: Modules conditionally registered only the configured driver (e.g., only S3Driver if STORAGE_TYPE=S3) - **After**: All drivers (LocalDriver, S3Driver, SmtpDriver, LoggerDriver) are registered at startup - **Runtime Selection**: Services dynamically choose and instantiate the correct driver based on current configuration ### Secondary Fix: Integration Test Log Cleanup Addresses ConfigStorageService error logs appearing in integration test output by using injected LoggerService for consistent log handling.
This commit is contained in:
@ -0,0 +1,229 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
describe('EmailDriverFactory', () => {
|
||||
let factory: EmailDriverFactory;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
|
||||
const mockTwentyConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EmailDriverFactory,
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: mockTwentyConfigService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
factory = module.get<EmailDriverFactory>(EmailDriverFactory);
|
||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('buildConfigKey', () => {
|
||||
it('should return "logger" for logger driver', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockReturnValue(EmailDriver.Logger);
|
||||
|
||||
const result = factory['buildConfigKey']();
|
||||
|
||||
expect(result).toBe('logger');
|
||||
expect(twentyConfigService.get).toHaveBeenCalledWith('EMAIL_DRIVER');
|
||||
});
|
||||
|
||||
it('should return smtp config key for smtp driver', () => {
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue(EmailDriver.Smtp);
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
const result = factory['buildConfigKey']();
|
||||
|
||||
expect(result).toBe('smtp|smtp-hash-123');
|
||||
expect(twentyConfigService.get).toHaveBeenCalledWith('EMAIL_DRIVER');
|
||||
});
|
||||
|
||||
it('should throw error for unsupported driver', () => {
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('invalid-driver');
|
||||
|
||||
expect(() => factory['buildConfigKey']()).toThrow(
|
||||
'Unsupported email driver: invalid-driver',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createDriver', () => {
|
||||
it('should create logger driver', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockReturnValue(EmailDriver.Logger);
|
||||
|
||||
const driver = factory['createDriver']();
|
||||
|
||||
expect(driver).toBeDefined();
|
||||
expect(driver.constructor.name).toBe('LoggerDriver');
|
||||
});
|
||||
|
||||
it('should create smtp driver with basic configuration', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'EMAIL_DRIVER':
|
||||
return EmailDriver.Smtp;
|
||||
case 'EMAIL_SMTP_HOST':
|
||||
return 'smtp.example.com';
|
||||
case 'EMAIL_SMTP_PORT':
|
||||
return 587;
|
||||
case 'EMAIL_SMTP_USER':
|
||||
return undefined;
|
||||
case 'EMAIL_SMTP_PASSWORD':
|
||||
return undefined;
|
||||
case 'EMAIL_SMTP_NO_TLS':
|
||||
return false;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
const driver = factory['createDriver']();
|
||||
|
||||
expect(driver).toBeDefined();
|
||||
expect(driver.constructor.name).toBe('SmtpDriver');
|
||||
});
|
||||
|
||||
it('should throw error when smtp host is missing', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'EMAIL_DRIVER':
|
||||
return EmailDriver.Smtp;
|
||||
case 'EMAIL_SMTP_HOST':
|
||||
return undefined;
|
||||
case 'EMAIL_SMTP_PORT':
|
||||
return 587;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
expect(() => factory['createDriver']()).toThrow(
|
||||
'SMTP driver requires host and port to be defined',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for invalid driver', () => {
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('invalid-driver');
|
||||
|
||||
expect(() => factory['createDriver']()).toThrow(
|
||||
'Invalid email driver: invalid-driver',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCurrentDriver', () => {
|
||||
it('should return current driver for logger', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockReturnValue(EmailDriver.Logger);
|
||||
|
||||
const driver = factory.getCurrentDriver();
|
||||
|
||||
expect(driver).toBeDefined();
|
||||
expect(driver.constructor.name).toBe('LoggerDriver');
|
||||
});
|
||||
|
||||
it('should reuse driver when config key unchanged', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockReturnValue(EmailDriver.Logger);
|
||||
|
||||
const driver1 = factory.getCurrentDriver();
|
||||
const driver2 = factory.getCurrentDriver();
|
||||
|
||||
expect(driver1).toBe(driver2);
|
||||
});
|
||||
|
||||
it('should create new driver when config key changes', () => {
|
||||
// First call with logger
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockReturnValue(EmailDriver.Logger);
|
||||
|
||||
const driver1 = factory.getCurrentDriver();
|
||||
|
||||
// Second call with smtp
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'EMAIL_DRIVER':
|
||||
return EmailDriver.Smtp;
|
||||
case 'EMAIL_SMTP_HOST':
|
||||
return 'smtp.example.com';
|
||||
case 'EMAIL_SMTP_PORT':
|
||||
return 587;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
const driver2 = factory.getCurrentDriver();
|
||||
|
||||
expect(driver1).not.toBe(driver2);
|
||||
expect(driver1.constructor.name).toBe('LoggerDriver');
|
||||
expect(driver2.constructor.name).toBe('SmtpDriver');
|
||||
});
|
||||
|
||||
it('should throw error for unsupported email driver', () => {
|
||||
jest.spyOn(twentyConfigService, 'get').mockReturnValue('invalid-driver');
|
||||
|
||||
expect(() => factory.getCurrentDriver()).toThrow(
|
||||
'Failed to build config key for EmailDriverFactory. Original error: Unsupported email driver: invalid-driver',
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when driver creation fails after valid config', () => {
|
||||
jest
|
||||
.spyOn(twentyConfigService, 'get')
|
||||
.mockImplementation((key: string) => {
|
||||
switch (key) {
|
||||
case 'EMAIL_DRIVER':
|
||||
return EmailDriver.Smtp;
|
||||
case 'EMAIL_SMTP_HOST':
|
||||
return 'smtp.example.com';
|
||||
case 'EMAIL_SMTP_PORT':
|
||||
return 587;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
jest
|
||||
.spyOn(factory as any, 'getConfigGroupHash')
|
||||
.mockReturnValue('smtp-hash-123');
|
||||
|
||||
jest.spyOn(factory as any, 'createDriver').mockImplementation(() => {
|
||||
throw new Error('Driver creation failed');
|
||||
});
|
||||
|
||||
expect(() => factory.getCurrentDriver()).toThrow(
|
||||
'Failed to create driver for EmailDriverFactory with config key: smtp|smtp-hash-123. Original error: Driver creation failed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,73 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
|
||||
import { EmailSenderService } from 'src/engine/core-modules/email/email-sender.service';
|
||||
|
||||
describe('EmailSenderService', () => {
|
||||
let service: EmailSenderService;
|
||||
let emailDriverFactory: EmailDriverFactory;
|
||||
|
||||
const mockEmailDriverFactory = {
|
||||
getCurrentDriver: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
EmailSenderService,
|
||||
{
|
||||
provide: EmailDriverFactory,
|
||||
useValue: mockEmailDriverFactory,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<EmailSenderService>(EmailSenderService);
|
||||
emailDriverFactory = module.get<EmailDriverFactory>(EmailDriverFactory);
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('send', () => {
|
||||
it('should delegate to the current driver', async () => {
|
||||
const mockDriver = {
|
||||
send: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
|
||||
mockEmailDriverFactory.getCurrentDriver.mockReturnValue(mockDriver);
|
||||
|
||||
const sendMailOptions: SendMailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test',
|
||||
text: 'Test message',
|
||||
};
|
||||
|
||||
await service.send(sendMailOptions);
|
||||
|
||||
expect(emailDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.send).toHaveBeenCalledWith(sendMailOptions);
|
||||
});
|
||||
|
||||
it('should handle driver errors', async () => {
|
||||
const mockDriver = {
|
||||
send: jest.fn().mockRejectedValue(new Error('Driver error')),
|
||||
};
|
||||
|
||||
mockEmailDriverFactory.getCurrentDriver.mockReturnValue(mockDriver);
|
||||
|
||||
const sendMailOptions: SendMailOptions = {
|
||||
to: 'test@example.com',
|
||||
subject: 'Test',
|
||||
text: 'Test message',
|
||||
};
|
||||
|
||||
await expect(service.send(sendMailOptions)).rejects.toThrow(
|
||||
'Driver error',
|
||||
);
|
||||
expect(emailDriverFactory.getCurrentDriver).toHaveBeenCalled();
|
||||
expect(mockDriver.send).toHaveBeenCalledWith(sendMailOptions);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,5 @@
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
export interface EmailDriver {
|
||||
export interface EmailDriverInterface {
|
||||
send(sendMailOptions: SendMailOptions): Promise<void>;
|
||||
}
|
||||
|
||||
@ -2,9 +2,9 @@ import { Logger } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
import { EmailDriverInterface } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
export class LoggerDriver implements EmailDriver {
|
||||
export class LoggerDriver implements EmailDriverInterface {
|
||||
private readonly logger = new Logger(LoggerDriver.name);
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import { createTransport, Transporter, SendMailOptions } from 'nodemailer';
|
||||
import { createTransport, SendMailOptions, Transporter } from 'nodemailer';
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
import { EmailDriverInterface } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
export class SmtpDriver implements EmailDriver {
|
||||
export class SmtpDriver implements EmailDriverInterface {
|
||||
private readonly logger = new Logger(SmtpDriver.name);
|
||||
private transport: Transporter;
|
||||
|
||||
|
||||
@ -0,0 +1,79 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { EmailDriverInterface } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
|
||||
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/enums/email-driver.enum';
|
||||
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class EmailDriverFactory extends DriverFactoryBase<EmailDriverInterface> {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
const driver = this.twentyConfigService.get('EMAIL_DRIVER');
|
||||
|
||||
if (driver === EmailDriver.Logger) {
|
||||
return 'logger';
|
||||
}
|
||||
|
||||
if (driver === EmailDriver.Smtp) {
|
||||
const emailConfigHash = this.getConfigGroupHash(
|
||||
ConfigVariablesGroup.EmailSettings,
|
||||
);
|
||||
|
||||
return `smtp|${emailConfigHash}`;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported email driver: ${driver}`);
|
||||
}
|
||||
|
||||
protected createDriver(): EmailDriverInterface {
|
||||
const driver = this.twentyConfigService.get('EMAIL_DRIVER');
|
||||
|
||||
switch (driver) {
|
||||
case EmailDriver.Logger:
|
||||
return new LoggerDriver();
|
||||
|
||||
case EmailDriver.Smtp: {
|
||||
const host = this.twentyConfigService.get('EMAIL_SMTP_HOST');
|
||||
const port = this.twentyConfigService.get('EMAIL_SMTP_PORT');
|
||||
const user = this.twentyConfigService.get('EMAIL_SMTP_USER');
|
||||
const pass = this.twentyConfigService.get('EMAIL_SMTP_PASSWORD');
|
||||
const noTLS = this.twentyConfigService.get('EMAIL_SMTP_NO_TLS');
|
||||
|
||||
if (!host || !port) {
|
||||
throw new Error('SMTP driver requires host and port to be defined');
|
||||
}
|
||||
|
||||
const options: {
|
||||
host: string;
|
||||
port: number;
|
||||
auth?: { user: string; pass: string };
|
||||
secure?: boolean;
|
||||
ignoreTLS?: boolean;
|
||||
requireTLS?: boolean;
|
||||
} = { host, port };
|
||||
|
||||
if (user && pass) {
|
||||
options.auth = { user, pass };
|
||||
}
|
||||
|
||||
if (noTLS) {
|
||||
options.secure = false;
|
||||
options.ignoreTLS = true;
|
||||
}
|
||||
|
||||
return new SmtpDriver(options);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid email driver: ${driver}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,16 +1,18 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { SendMailOptions } from 'nodemailer';
|
||||
|
||||
import { EmailDriver } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
import { EmailDriverInterface } from 'src/engine/core-modules/email/drivers/interfaces/email-driver.interface';
|
||||
|
||||
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
|
||||
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
|
||||
|
||||
@Injectable()
|
||||
export class EmailSenderService implements EmailDriver {
|
||||
constructor(@Inject(EMAIL_DRIVER) private driver: EmailDriver) {}
|
||||
export class EmailSenderService implements EmailDriverInterface {
|
||||
constructor(private readonly emailDriverFactory: EmailDriverFactory) {}
|
||||
|
||||
async send(sendMailOptions: SendMailOptions): Promise<void> {
|
||||
await this.driver.send(sendMailOptions);
|
||||
const driver = this.emailDriverFactory.getCurrentDriver();
|
||||
|
||||
await driver.send(sendMailOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export const EMAIL_DRIVER = Symbol('EMAIL_DRIVER');
|
||||
@ -1,52 +0,0 @@
|
||||
import {
|
||||
EmailDriver,
|
||||
EmailModuleOptions,
|
||||
} from 'src/engine/core-modules/email/interfaces/email.interface';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
export const emailModuleFactory = (
|
||||
twentyConfigService: TwentyConfigService,
|
||||
): EmailModuleOptions => {
|
||||
const driver = twentyConfigService.get('EMAIL_DRIVER');
|
||||
|
||||
switch (driver) {
|
||||
case EmailDriver.Logger:
|
||||
return {
|
||||
type: EmailDriver.Logger,
|
||||
};
|
||||
|
||||
case EmailDriver.Smtp: {
|
||||
const options: EmailModuleOptions = {
|
||||
type: EmailDriver.Smtp,
|
||||
};
|
||||
|
||||
const host = twentyConfigService.get('EMAIL_SMTP_HOST');
|
||||
const port = twentyConfigService.get('EMAIL_SMTP_PORT');
|
||||
const user = twentyConfigService.get('EMAIL_SMTP_USER');
|
||||
const pass = twentyConfigService.get('EMAIL_SMTP_PASSWORD');
|
||||
const noTLS = twentyConfigService.get('EMAIL_SMTP_NO_TLS');
|
||||
|
||||
if (!host || !port) {
|
||||
throw new Error(
|
||||
`${driver} email driver requires host and port to be defined, check your .env file`,
|
||||
);
|
||||
}
|
||||
|
||||
options.host = host;
|
||||
options.port = port;
|
||||
|
||||
if (user && pass) options.auth = { user, pass };
|
||||
|
||||
if (noTLS) {
|
||||
options.secure = false;
|
||||
options.ignoreTLS = true;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid email driver (${driver}), check your .env file`);
|
||||
}
|
||||
};
|
||||
@ -1,35 +1,17 @@
|
||||
import { DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
EmailDriver,
|
||||
EmailModuleAsyncOptions,
|
||||
} from 'src/engine/core-modules/email/interfaces/email.interface';
|
||||
|
||||
import { LoggerDriver } from 'src/engine/core-modules/email/drivers/logger.driver';
|
||||
import { SmtpDriver } from 'src/engine/core-modules/email/drivers/smtp.driver';
|
||||
import { EmailDriverFactory } from 'src/engine/core-modules/email/email-driver.factory';
|
||||
import { EmailSenderService } from 'src/engine/core-modules/email/email-sender.service';
|
||||
import { EMAIL_DRIVER } from 'src/engine/core-modules/email/email.constants';
|
||||
import { EmailService } from 'src/engine/core-modules/email/email.service';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
|
||||
@Global()
|
||||
export class EmailModule {
|
||||
static forRoot(options: EmailModuleAsyncOptions): DynamicModule {
|
||||
const provider = {
|
||||
provide: EMAIL_DRIVER,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
useFactory: (...args: any[]) => {
|
||||
const config = options.useFactory(...args);
|
||||
|
||||
return config.type === EmailDriver.Smtp
|
||||
? new SmtpDriver(config)
|
||||
: new LoggerDriver();
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: EmailModule,
|
||||
providers: [EmailSenderService, EmailService, provider],
|
||||
imports: [TwentyConfigModule],
|
||||
providers: [EmailDriverFactory, EmailSenderService, EmailService],
|
||||
exports: [EmailSenderService, EmailService],
|
||||
};
|
||||
}
|
||||
|
||||
@ -0,0 +1,4 @@
|
||||
export enum EmailDriver {
|
||||
Logger = 'logger',
|
||||
Smtp = 'smtp',
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
||||
|
||||
export enum EmailDriver {
|
||||
Logger = 'logger',
|
||||
Smtp = 'smtp',
|
||||
}
|
||||
|
||||
export type EmailModuleOptions =
|
||||
| (SMTPConnection.Options & {
|
||||
type: EmailDriver.Smtp;
|
||||
})
|
||||
| {
|
||||
type: EmailDriver.Logger;
|
||||
};
|
||||
|
||||
export type EmailModuleAsyncOptions = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
useFactory: (...args: any[]) => EmailModuleOptions;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
Reference in New Issue
Block a user