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
@@ -1 +0,0 @@
export const LOGIC_FUNCTION_DRIVER = Symbol('LOGIC_FUNCTION_DRIVER');
@@ -1,80 +0,0 @@
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import {
LogicFunctionDriverType,
type LogicFunctionModuleOptions,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { type TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import type { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
export const logicFunctionModuleFactory = async (
twentyConfigService: TwentyConfigService,
logicFunctionResourceService: LogicFunctionResourceService,
): Promise<LogicFunctionModuleOptions> => {
const driverType = twentyConfigService.get('LOGIC_FUNCTION_TYPE');
const options = { logicFunctionResourceService };
switch (driverType) {
case LogicFunctionDriverType.DISABLED: {
return {
type: LogicFunctionDriverType.DISABLED,
};
}
case LogicFunctionDriverType.LOCAL: {
return {
type: LogicFunctionDriverType.LOCAL,
options,
};
}
case LogicFunctionDriverType.LAMBDA: {
const region = twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_REGION');
const accessKeyId = twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID',
);
const secretAccessKey = twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY',
);
const lambdaRole = twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_ROLE');
const subhostingRole = twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_SUBHOSTING_ROLE',
);
const s3BucketName = twentyConfigService.get('STORAGE_S3_NAME');
const layerBucket =
twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_LAYER_BUCKET') ??
s3BucketName ??
'twenty-lambda-layer';
const layerBucketRegion =
twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_LAYER_BUCKET_REGION') ??
region;
return {
type: LogicFunctionDriverType.LAMBDA,
options: {
...options,
credentials: accessKeyId
? {
accessKeyId,
secretAccessKey,
}
: fromNodeProviderChain({
clientConfig: { region },
}),
region,
lambdaRole,
subhostingRole,
layerBucket,
layerBucketRegion,
},
};
}
default:
throw new Error(
`Invalid logic function executor driver type (${driverType}), check your .env file`,
);
}
};
@@ -1,10 +1,6 @@
import type { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type LogicFunctionExecutionStatus } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import type { LocalDriverOptions } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver';
import type { LambdaDriverOptions } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver';
export type LogicFunctionExecuteError = {
errorType: string;
@@ -54,30 +50,3 @@ export enum LogicFunctionDriverType {
LAMBDA = 'LAMBDA',
LOCAL = 'LOCAL',
}
export interface DisabledDriverFactoryOptions {
type: LogicFunctionDriverType.DISABLED;
}
export interface LocalDriverFactoryOptions {
type: LogicFunctionDriverType.LOCAL;
options: LocalDriverOptions;
}
export interface LambdaDriverFactoryOptions {
type: LogicFunctionDriverType.LAMBDA;
options: LambdaDriverOptions;
}
export type LogicFunctionModuleOptions =
| DisabledDriverFactoryOptions
| LocalDriverFactoryOptions
| LambdaDriverFactoryOptions;
export type LogicFunctionModuleAsyncOptions = {
useFactory: (
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
...args: any[]
) => LogicFunctionModuleOptions | Promise<LogicFunctionModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;
@@ -0,0 +1,94 @@
import { Injectable } from '@nestjs/common';
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import {
type LogicFunctionDriver,
LogicFunctionDriverType,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { DisabledDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver';
import { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
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 LogicFunctionDriverFactory extends DriverFactoryBase<LogicFunctionDriver> {
constructor(
twentyConfigService: TwentyConfigService,
private readonly logicFunctionResourceService: LogicFunctionResourceService,
) {
super(twentyConfigService);
}
protected buildConfigKey(): string {
const driverType = this.twentyConfigService.get('LOGIC_FUNCTION_TYPE');
if (driverType === LogicFunctionDriverType.LAMBDA) {
return `lambda|${this.getConfigGroupHash(ConfigVariablesGroup.LOGIC_FUNCTION_CONFIG)}`;
}
return driverType;
}
protected createDriver(): LogicFunctionDriver {
const driverType = this.twentyConfigService.get('LOGIC_FUNCTION_TYPE');
switch (driverType) {
case LogicFunctionDriverType.DISABLED:
return new DisabledDriver();
case LogicFunctionDriverType.LOCAL:
return new LocalDriver({
logicFunctionResourceService: this.logicFunctionResourceService,
});
case LogicFunctionDriverType.LAMBDA: {
const region = this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_REGION',
);
const accessKeyId = this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_ACCESS_KEY_ID',
);
const secretAccessKey = this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_SECRET_ACCESS_KEY',
);
const lambdaRole = this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_ROLE',
);
const subhostingRole = this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_SUBHOSTING_ROLE',
);
const s3BucketName = this.twentyConfigService.get('STORAGE_S3_NAME');
const layerBucket =
this.twentyConfigService.get('LOGIC_FUNCTION_LAMBDA_LAYER_BUCKET') ??
s3BucketName ??
'twenty-lambda-layer';
const layerBucketRegion =
this.twentyConfigService.get(
'LOGIC_FUNCTION_LAMBDA_LAYER_BUCKET_REGION',
) ?? region;
return new LambdaDriver({
logicFunctionResourceService: this.logicFunctionResourceService,
credentials: accessKeyId
? { accessKeyId, secretAccessKey }
: fromNodeProviderChain({ clientConfig: { region } }),
region,
lambdaRole,
subhostingRole,
layerBucket,
layerBucketRegion,
});
}
default:
throw new Error(
`Invalid logic function driver type (${driverType}), check your .env file`,
);
}
}
}
@@ -1,48 +0,0 @@
import { type DynamicModule, Module } from '@nestjs/common';
import {
LogicFunctionDriverType,
LogicFunctionModuleAsyncOptions,
} from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { DisabledDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/disabled.driver';
import { LambdaDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/logic-function/logic-function-drivers/drivers/local.driver';
import { LOGIC_FUNCTION_DRIVER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-driver.constants';
@Module({})
export class LogicFunctionDriversModule {
static forRootAsync(options: LogicFunctionModuleAsyncOptions): DynamicModule {
const provider = {
provide: LOGIC_FUNCTION_DRIVER,
// oxlint-disable-next-line @typescripttypescript/no-explicit-any
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
switch (config?.type) {
case LogicFunctionDriverType.DISABLED:
return new DisabledDriver();
case LogicFunctionDriverType.LOCAL:
return new LocalDriver(config.options);
case LogicFunctionDriverType.LAMBDA:
return new LambdaDriver(config.options);
default: {
const unknownConfig = config as { type?: string };
throw new Error(
`Unknown logic function executor driver type: ${unknownConfig?.type}`,
);
}
}
},
inject: options.inject || [],
};
return {
module: LogicFunctionDriversModule,
imports: options.imports || [],
providers: [provider],
exports: [LOGIC_FUNCTION_DRIVER],
};
}
}
@@ -1,4 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import {
DEFAULT_API_KEY_NAME,
@@ -8,7 +8,6 @@ import {
import { isDefined } from 'twenty-shared/utils';
import {
LogicFunctionDriver,
type LogicFunctionExecuteResult,
type LogicFunctionTranspileParams,
type LogicFunctionTranspileResult,
@@ -19,7 +18,7 @@ import type { FlatApplicationVariable } from 'src/engine/core-modules/applicatio
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
import { LOGIC_FUNCTION_DRIVER } from 'src/engine/core-modules/logic-function/logic-function-drivers/constants/logic-function-driver.constants';
import { LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
import { buildEnvVar } from 'src/engine/core-modules/logic-function/logic-function-executor/utils/build-env-var';
import { SecretEncryptionService } from 'src/engine/core-modules/secret-encryption/secret-encryption.service';
import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service';
@@ -49,8 +48,7 @@ export enum LogicFunctionExecutionExceptionCode {
@Injectable()
export class LogicFunctionExecutorService {
constructor(
@Inject(LOGIC_FUNCTION_DRIVER)
private driver: LogicFunctionDriver,
private readonly logicFunctionDriverFactory: LogicFunctionDriverFactory,
private readonly throttlerService: ThrottlerService,
private readonly twentyConfigService: TwentyConfigService,
private readonly workspaceCacheService: WorkspaceCacheService,
@@ -84,7 +82,9 @@ export class LogicFunctionExecutorService {
_flatLogicFunction: flatLogicFunction,
});
const resultLogicFunction = await this.driver.execute({
const driver = this.logicFunctionDriverFactory.getCurrentDriver();
const resultLogicFunction = await driver.execute({
flatLogicFunction,
flatApplication,
applicationUniversalIdentifier: flatApplication.universalIdentifier,
@@ -106,7 +106,9 @@ export class LogicFunctionExecutorService {
async transpile(
params: LogicFunctionTranspileParams,
): Promise<LogicFunctionTranspileResult> {
return this.driver.transpile(params);
const driver = this.logicFunctionDriverFactory.getCurrentDriver();
return driver.transpile(params);
}
private async throttleExecution(workspaceId: string) {
@@ -1,26 +1,26 @@
import { type DynamicModule, Global, Module } from '@nestjs/common';
import { LogicFunctionModuleAsyncOptions } from 'src/engine/core-modules/logic-function/logic-function-drivers/interfaces/logic-function-driver.interface';
import { LogicFunctionDriversModule } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-drivers.module';
import { LogicFunctionDriverFactory } from 'src/engine/core-modules/logic-function/logic-function-drivers/logic-function-driver.factory';
import { LogicFunctionResourceModule } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.module';
import { LogicFunctionTriggerModule } from 'src/engine/core-modules/logic-function/logic-function-trigger/logic-function-trigger.module';
import { LogicFunctionExecutorModule } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.module';
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
@Global()
@Module({})
export class LogicFunctionModule {
static forRootAsync(options: LogicFunctionModuleAsyncOptions): DynamicModule {
static forRoot(): DynamicModule {
return {
module: LogicFunctionModule,
imports: [
LogicFunctionDriversModule.forRootAsync(options),
TwentyConfigModule,
LogicFunctionResourceModule,
LogicFunctionTriggerModule,
LogicFunctionExecutorModule,
],
providers: [LogicFunctionDriverFactory],
exports: [
LogicFunctionDriversModule,
LogicFunctionDriverFactory,
LogicFunctionResourceModule,
LogicFunctionTriggerModule,
LogicFunctionExecutorModule,