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:
Félix Malfait
2026-03-18 16:00:45 +01:00
committed by GitHub
parent fe4512f80c
commit c6f11d8adb
49 changed files with 420 additions and 769 deletions
@@ -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`,
);
}
}
}
@@ -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`,
);
}
};
@@ -1,3 +1 @@
export const CODE_INTERPRETER_DRIVER = Symbol('CODE_INTERPRETER_DRIVER');
export const DEFAULT_CODE_INTERPRETER_TIMEOUT_MS = 300_000;
@@ -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'>;
@@ -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],
};
}
@@ -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);
}
}