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:
-1
@@ -1 +0,0 @@
|
||||
export const LOGIC_FUNCTION_DRIVER = Symbol('LOGIC_FUNCTION_DRIVER');
|
||||
-80
@@ -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`,
|
||||
);
|
||||
}
|
||||
};
|
||||
-31
@@ -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'>;
|
||||
|
||||
+94
@@ -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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-48
@@ -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],
|
||||
};
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -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) {
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user