fix: migrate driver modules to DriverFactoryBase lazy-loading pattern (#18731)
## Summary - Migrates `LogicFunctionModule`, `CodeInterpreterModule`, and `CaptchaModule` from the `forRootAsync` + injection token pattern to the `DriverFactoryBase` lazy-loading pattern (matching `EmailModule` and `FileStorageModule`) - Fixes #18724 where `LOGIC_FUNCTION_TYPE` was not respected in worker processes because the driver was created at module boot time before the DB config cache was loaded - Removes `isEnvOnly` from `LOGIC_FUNCTION_TYPE`, `CODE_INTERPRETER_TYPE`, `CAPTCHA_DRIVER`, `IS_MULTIWORKSPACE_ENABLED`, and `FRONTEND_URL` — these can now be safely configured via the database at runtime ## How it works Each migrated module now uses a `DriverFactory` (extending `DriverFactoryBase`) instead of a module-level async factory + Symbol injection token: 1. **Lazy creation**: `getCurrentDriver()` creates the driver on first call, after `DatabaseConfigDriver.onModuleInit()` has loaded the DB cache 2. **Auto-recreation**: If config changes in the DB, the next `getCurrentDriver()` call detects the key mismatch and creates a new driver instance 3. **Unified config**: Both server and worker read from the same database — driver config only needs to be set once ### Files deleted (old pattern) - `logic-function-module.factory.ts`, `logic-function-drivers.module.ts`, `logic-function-driver.constants.ts` - `code-interpreter-module.factory.ts` - `captcha.module-factory.ts`, `captcha-driver.constants.ts` ### Files created (new pattern) - `logic-function-driver.factory.ts` - `code-interpreter-driver.factory.ts` - `captcha-driver.factory.ts` Net: **-150 lines** ## Test plan - [x] `npx nx typecheck twenty-server` passes - [x] `npx nx lint:diff-with-main twenty-server` passes - [ ] Integration tests pass (`npx nx run twenty-server:test:integration:with-db-reset`) - [ ] Verify logic functions execute in workflow runs (the original bug) - [ ] Verify code interpreter works in workflow code steps - [ ] Verify captcha validation works on sign-up (when captcha is configured) Made with [Cursor](https://cursor.com)
This commit is contained in:
+72
@@ -0,0 +1,72 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type CodeInterpreterDriver } from 'src/engine/core-modules/code-interpreter/drivers/interfaces/code-interpreter-driver.interface';
|
||||
|
||||
import { CodeInterpreterDriverType } from 'src/engine/core-modules/code-interpreter/code-interpreter.interface';
|
||||
import { DisabledDriver } from 'src/engine/core-modules/code-interpreter/drivers/disabled.driver';
|
||||
import { E2BDriver } from 'src/engine/core-modules/code-interpreter/drivers/e2b.driver';
|
||||
import { LocalDriver } from 'src/engine/core-modules/code-interpreter/drivers/local.driver';
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
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 CodeInterpreterDriverFactory extends DriverFactoryBase<CodeInterpreterDriver> {
|
||||
constructor(twentyConfigService: TwentyConfigService) {
|
||||
super(twentyConfigService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
const driverType = this.twentyConfigService.get('CODE_INTERPRETER_TYPE');
|
||||
|
||||
if (driverType === CodeInterpreterDriverType.E_2_B) {
|
||||
return `e2b|${this.getConfigGroupHash(ConfigVariablesGroup.CODE_INTERPRETER_CONFIG)}`;
|
||||
}
|
||||
|
||||
return driverType;
|
||||
}
|
||||
|
||||
protected createDriver(): CodeInterpreterDriver {
|
||||
const driverType = this.twentyConfigService.get('CODE_INTERPRETER_TYPE');
|
||||
const timeoutMs = this.twentyConfigService.get(
|
||||
'CODE_INTERPRETER_TIMEOUT_MS',
|
||||
);
|
||||
|
||||
switch (driverType) {
|
||||
case CodeInterpreterDriverType.DISABLED:
|
||||
return new DisabledDriver(
|
||||
'Code interpreter is disabled. Set CODE_INTERPRETER_TYPE to LOCAL (development only) or E2B to enable it.',
|
||||
);
|
||||
|
||||
case CodeInterpreterDriverType.LOCAL: {
|
||||
const nodeEnv = this.twentyConfigService.get('NODE_ENV');
|
||||
|
||||
if (nodeEnv === NodeEnvironment.PRODUCTION) {
|
||||
return new DisabledDriver(
|
||||
'LOCAL code interpreter driver is not allowed in production. Use E2B driver instead by setting CODE_INTERPRETER_TYPE=E2B and providing E2B_API_KEY.',
|
||||
);
|
||||
}
|
||||
|
||||
return new LocalDriver({ timeoutMs });
|
||||
}
|
||||
|
||||
case CodeInterpreterDriverType.E_2_B: {
|
||||
const apiKey = this.twentyConfigService.get('E2B_API_KEY');
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'E2B_API_KEY is required when CODE_INTERPRETER_TYPE is E2B',
|
||||
);
|
||||
}
|
||||
|
||||
return new E2BDriver({ apiKey, timeoutMs });
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid code interpreter driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
|
||||
|
||||
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
import {
|
||||
CodeInterpreterDriverType,
|
||||
type CodeInterpreterModuleOptions,
|
||||
} from './code-interpreter.interface';
|
||||
|
||||
export const codeInterpreterModuleFactory = async (
|
||||
twentyConfigService: TwentyConfigService,
|
||||
): Promise<CodeInterpreterModuleOptions> => {
|
||||
const driverType = twentyConfigService.get('CODE_INTERPRETER_TYPE');
|
||||
const timeoutMs = twentyConfigService.get('CODE_INTERPRETER_TIMEOUT_MS');
|
||||
|
||||
switch (driverType) {
|
||||
case CodeInterpreterDriverType.LOCAL: {
|
||||
const nodeEnv = twentyConfigService.get('NODE_ENV');
|
||||
|
||||
if (nodeEnv === NodeEnvironment.PRODUCTION) {
|
||||
return {
|
||||
type: CodeInterpreterDriverType.DISABLED,
|
||||
options: {
|
||||
reason:
|
||||
'LOCAL code interpreter driver is not allowed in production. Use E2B driver instead by setting CODE_INTERPRETER_TYPE=E2B and providing E2B_API_KEY.',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: CodeInterpreterDriverType.LOCAL,
|
||||
options: { timeoutMs },
|
||||
};
|
||||
}
|
||||
case CodeInterpreterDriverType.E_2_B: {
|
||||
const apiKey = twentyConfigService.get('E2B_API_KEY');
|
||||
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
'E2B_API_KEY is required when CODE_INTERPRETER_TYPE is E2B',
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
type: CodeInterpreterDriverType.E_2_B,
|
||||
options: {
|
||||
apiKey,
|
||||
timeoutMs,
|
||||
},
|
||||
};
|
||||
}
|
||||
case CodeInterpreterDriverType.DISABLED: {
|
||||
return {
|
||||
type: CodeInterpreterDriverType.DISABLED,
|
||||
options: {
|
||||
reason:
|
||||
'Code interpreter is disabled. Set CODE_INTERPRETER_TYPE to LOCAL (development only) or E2B to enable it.',
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid code interpreter driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
-2
@@ -1,3 +1 @@
|
||||
export const CODE_INTERPRETER_DRIVER = Symbol('CODE_INTERPRETER_DRIVER');
|
||||
|
||||
export const DEFAULT_CODE_INTERPRETER_TIMEOUT_MS = 300_000;
|
||||
|
||||
-32
@@ -1,37 +1,5 @@
|
||||
import { type FactoryProvider, type ModuleMetadata } from '@nestjs/common';
|
||||
|
||||
import { type E2BDriverOptions } from './drivers/e2b.driver';
|
||||
import { type LocalDriverOptions } from './drivers/local.driver';
|
||||
|
||||
export enum CodeInterpreterDriverType {
|
||||
LOCAL = 'LOCAL',
|
||||
E_2_B = 'E_2_B',
|
||||
DISABLED = 'DISABLED',
|
||||
}
|
||||
|
||||
export type LocalDriverFactoryOptions = {
|
||||
type: CodeInterpreterDriverType.LOCAL;
|
||||
options: LocalDriverOptions;
|
||||
};
|
||||
|
||||
export type E2BDriverFactoryOptions = {
|
||||
type: CodeInterpreterDriverType.E_2_B;
|
||||
options: E2BDriverOptions;
|
||||
};
|
||||
|
||||
export type DisabledDriverFactoryOptions = {
|
||||
type: CodeInterpreterDriverType.DISABLED;
|
||||
options: { reason: string };
|
||||
};
|
||||
|
||||
export type CodeInterpreterModuleOptions =
|
||||
| LocalDriverFactoryOptions
|
||||
| E2BDriverFactoryOptions
|
||||
| DisabledDriverFactoryOptions;
|
||||
|
||||
export type CodeInterpreterModuleAsyncOptions = {
|
||||
useFactory: (
|
||||
...args: unknown[]
|
||||
) => CodeInterpreterModuleOptions | Promise<CodeInterpreterModuleOptions>;
|
||||
} & Pick<ModuleMetadata, 'imports'> &
|
||||
Pick<FactoryProvider, 'inject'>;
|
||||
|
||||
+6
-32
@@ -1,42 +1,16 @@
|
||||
import { type DynamicModule, Global } from '@nestjs/common';
|
||||
|
||||
import { CODE_INTERPRETER_DRIVER } from './code-interpreter.constants';
|
||||
import {
|
||||
CodeInterpreterDriverType,
|
||||
type CodeInterpreterModuleAsyncOptions,
|
||||
} from './code-interpreter.interface';
|
||||
import { CodeInterpreterService } from './code-interpreter.service';
|
||||
|
||||
import { DisabledDriver } from './drivers/disabled.driver';
|
||||
import { E2BDriver } from './drivers/e2b.driver';
|
||||
import { LocalDriver } from './drivers/local.driver';
|
||||
import { CodeInterpreterDriverFactory } from 'src/engine/core-modules/code-interpreter/code-interpreter-driver.factory';
|
||||
import { CodeInterpreterService } from 'src/engine/core-modules/code-interpreter/code-interpreter.service';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
|
||||
@Global()
|
||||
export class CodeInterpreterModule {
|
||||
static forRootAsync(
|
||||
options: CodeInterpreterModuleAsyncOptions,
|
||||
): DynamicModule {
|
||||
const provider = {
|
||||
provide: CODE_INTERPRETER_DRIVER,
|
||||
useFactory: async (...args: unknown[]) => {
|
||||
const config = await options.useFactory(...args);
|
||||
|
||||
switch (config.type) {
|
||||
case CodeInterpreterDriverType.LOCAL:
|
||||
return new LocalDriver(config.options);
|
||||
case CodeInterpreterDriverType.E_2_B:
|
||||
return new E2BDriver(config.options);
|
||||
case CodeInterpreterDriverType.DISABLED:
|
||||
return new DisabledDriver(config.options.reason);
|
||||
}
|
||||
},
|
||||
inject: options.inject ?? [],
|
||||
};
|
||||
|
||||
static forRoot(): DynamicModule {
|
||||
return {
|
||||
module: CodeInterpreterModule,
|
||||
imports: options.imports ?? [],
|
||||
providers: [CodeInterpreterService, provider],
|
||||
imports: [TwentyConfigModule],
|
||||
providers: [CodeInterpreterDriverFactory, CodeInterpreterService],
|
||||
exports: [CodeInterpreterService],
|
||||
};
|
||||
}
|
||||
|
||||
+7
-6
@@ -1,19 +1,18 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { CODE_INTERPRETER_DRIVER } from './code-interpreter.constants';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { CodeInterpreterDriverFactory } from 'src/engine/core-modules/code-interpreter/code-interpreter-driver.factory';
|
||||
import {
|
||||
type CodeExecutionResult,
|
||||
type CodeInterpreterDriver,
|
||||
type ExecutionContext,
|
||||
type InputFile,
|
||||
type StreamCallbacks,
|
||||
} from './drivers/interfaces/code-interpreter-driver.interface';
|
||||
} from 'src/engine/core-modules/code-interpreter/drivers/interfaces/code-interpreter-driver.interface';
|
||||
|
||||
@Injectable()
|
||||
export class CodeInterpreterService implements CodeInterpreterDriver {
|
||||
constructor(
|
||||
@Inject(CODE_INTERPRETER_DRIVER) private driver: CodeInterpreterDriver,
|
||||
private readonly codeInterpreterDriverFactory: CodeInterpreterDriverFactory,
|
||||
) {}
|
||||
|
||||
execute(
|
||||
@@ -22,6 +21,8 @@ export class CodeInterpreterService implements CodeInterpreterDriver {
|
||||
context?: ExecutionContext,
|
||||
callbacks?: StreamCallbacks,
|
||||
): Promise<CodeExecutionResult> {
|
||||
return this.driver.execute(code, files, context, callbacks);
|
||||
const driver = this.codeInterpreterDriverFactory.getCurrentDriver();
|
||||
|
||||
return driver.execute(code, files, context, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user