Remove workflow feature flag (#12732)

Removing workflows from the lab
This commit is contained in:
Thomas Trompette
2025-06-19 15:26:00 +02:00
committed by GitHub
parent cbc0d06a2f
commit f9da3735de
25 changed files with 28 additions and 186 deletions

View File

@ -7,19 +7,11 @@ type FeatureFlagMetadata = {
};
export type PublicFeatureFlag = {
key: Extract<FeatureFlagKey, FeatureFlagKey.IS_WORKFLOW_ENABLED>;
key: FeatureFlagKey;
metadata: FeatureFlagMetadata;
};
export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [
{
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
metadata: {
label: 'Workflows',
description: 'Create custom workflows to automate your work.',
imagePath: 'https://twenty.com/images/lab/is-workflow-enabled.png',
},
},
...(process.env.CLOUDFLARE_API_KEY
? [
// {

View File

@ -2,7 +2,6 @@ export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED',

View File

@ -41,7 +41,7 @@ describe('FeatureFlagService', () => {
};
const workspaceId = 'workspace-id';
const featureFlag = FeatureFlagKey.IS_WORKFLOW_ENABLED;
const featureFlag = FeatureFlagKey.IS_AI_ENABLED;
beforeEach(async () => {
jest.clearAllMocks();
@ -130,12 +130,10 @@ describe('FeatureFlagService', () => {
// Prepare
mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue(
{
[FeatureFlagKey.IS_WORKFLOW_ENABLED]: true,
[FeatureFlagKey.IS_AI_ENABLED]: false,
},
);
const mockFeatureFlags = [
{ key: FeatureFlagKey.IS_WORKFLOW_ENABLED, value: true },
{ key: FeatureFlagKey.IS_AI_ENABLED, value: false },
];
@ -154,7 +152,6 @@ describe('FeatureFlagService', () => {
it('should return a map of feature flags for a workspace', async () => {
// Prepare
const mockFeatureFlags = [
{ key: FeatureFlagKey.IS_WORKFLOW_ENABLED, value: true, workspaceId },
{ key: FeatureFlagKey.IS_AI_ENABLED, value: false, workspaceId },
];
@ -165,7 +162,6 @@ describe('FeatureFlagService', () => {
// Assert
expect(result).toEqual({
[FeatureFlagKey.IS_WORKFLOW_ENABLED]: true,
[FeatureFlagKey.IS_AI_ENABLED]: false,
});
});
@ -174,10 +170,7 @@ describe('FeatureFlagService', () => {
describe('enableFeatureFlags', () => {
it('should enable multiple feature flags for a workspace', async () => {
// Prepare
const keys = [
FeatureFlagKey.IS_WORKFLOW_ENABLED,
FeatureFlagKey.IS_AI_ENABLED,
];
const keys = [FeatureFlagKey.IS_AI_ENABLED];
mockFeatureFlagRepository.upsert.mockResolvedValue({});

View File

@ -6,7 +6,7 @@ describe('featureFlagValidator', () => {
it('should not throw error if featureFlagKey is valid', () => {
expect(() =>
featureFlagValidator.assertIsFeatureFlagKey(
'IS_WORKFLOW_ENABLED',
'IS_AI_ENABLED',
new CustomException('Error', 'Error'),
),
).not.toThrow();

View File

@ -1,8 +1,8 @@
import { OnModuleDestroy } from '@nestjs/common';
import { JobsOptions, MetricsTime, Queue, QueueOptions, Worker } from 'bullmq';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
QueueCronJobOptions,

View File

@ -1042,7 +1042,7 @@ export class ConfigVariables {
type: ConfigVariableType.NUMBER,
})
@CastToPositiveNumber()
WORKFLOW_EXEC_THROTTLE_LIMIT = 100;
WORKFLOW_EXEC_THROTTLE_LIMIT = 10;
@ConfigVariablesMetadata({
group: ConfigVariablesGroup.RateLimiting,

View File

@ -10,7 +10,6 @@ export class ServerlessFunctionException extends CustomException {
export enum ServerlessFunctionExceptionCode {
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
SERVERLESS_FUNCTION_VERSION_NOT_FOUND = 'SERVERLESS_FUNCTION_VERSION_NOT_FOUND',
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING',

View File

@ -5,7 +5,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import graphqlTypeJson from 'graphql-type-json';
import { Repository } from 'typeorm';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@ -19,10 +18,6 @@ import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverles
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
ServerlessFunctionException,
ServerlessFunctionExceptionCode,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
@ -37,29 +32,12 @@ export class ServerlessFunctionResolver {
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
) {}
async checkFeatureFlag(workspaceId: string) {
const isWorkflowEnabled = await this.featureFlagRepository.findOneBy({
workspaceId,
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
value: true,
});
if (!isWorkflowEnabled) {
throw new ServerlessFunctionException(
`IS_WORKFLOW_ENABLED feature flag is not set to true for this workspace`,
ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID,
);
}
}
@Query(() => ServerlessFunctionDTO)
async findOneServerlessFunction(
@Args('input') { id }: ServerlessFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionRepository.findOneOrFail({
where: {
id,
@ -76,8 +54,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.findManyServerlessFunctions({
workspaceId,
});
@ -87,13 +63,8 @@ export class ServerlessFunctionResolver {
}
@Query(() => graphqlTypeJson)
async getAvailablePackages(
@Args('input') { id }: ServerlessFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
async getAvailablePackages(@Args('input') { id }: ServerlessFunctionIdInput) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.getAvailablePackages(id);
} catch (error) {
serverlessFunctionGraphQLApiExceptionHandler(error);
@ -106,8 +77,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.getServerlessFunctionSourceCode(
workspaceId,
input.id,
@ -124,8 +93,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.deleteOneServerlessFunction({
id: input.id,
workspaceId,
@ -142,8 +109,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.updateOneServerlessFunction(
input,
workspaceId,
@ -160,8 +125,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.createOneServerlessFunction(
input,
workspaceId,
@ -177,7 +140,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
const { id, payload, version } = input;
return await this.serverlessFunctionService.executeOneServerlessFunction(
@ -197,7 +159,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try {
await this.checkFeatureFlag(workspaceId);
const { id } = input;
return await this.serverlessFunctionService.publishOneServerlessFunction(

View File

@ -19,7 +19,6 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
throw new ConflictError(error.message);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_BUILDING:
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_EXECUTION_LIMIT_REACHED:
throw new ForbiddenError(error.message);
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CODE_UNCHANGED:

View File

@ -30,11 +30,6 @@ export const seedFeatureFlags = async (
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
workspaceId: workspaceId,
value: true,
},
{
key: FeatureFlagKey.IS_UNIQUE_INDEXES_ENABLED,
workspaceId: workspaceId,

View File

@ -1,17 +1,15 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AutomatedTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { AutomatedTriggerWorkspaceService } from 'src/modules/workflow/workflow-trigger/automated-trigger/automated-trigger.workspace-service';
import { CronTriggerCronCommand } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/commands/cron-trigger.cron.command';
import { CronTriggerCronJob } from 'src/modules/workflow/workflow-trigger/automated-trigger/crons/jobs/cron-trigger.cron.job';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { DatabaseEventTriggerListener } from 'src/modules/workflow/workflow-trigger/automated-trigger/listeners/database-event-trigger.listener';
@Module({
imports: [
FeatureFlagModule,
TypeOrmModule.forFeature([Workspace], 'core'),
WorkflowCommonModule,
],

View File

@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
@ -12,7 +11,6 @@ describe('DatabaseEventTriggerListener', () => {
let listener: DatabaseEventTriggerListener;
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let messageQueueService: jest.Mocked<MessageQueueService>;
let featureFlagService: jest.Mocked<FeatureFlagService>;
const mockRepository = {
find: jest.fn(),
@ -27,10 +25,6 @@ describe('DatabaseEventTriggerListener', () => {
add: jest.fn(),
} as any;
featureFlagService = {
isFeatureEnabled: jest.fn().mockResolvedValue(true),
} as any;
const module: TestingModule = await Test.createTestingModule({
providers: [
DatabaseEventTriggerListener,
@ -42,10 +36,6 @@ describe('DatabaseEventTriggerListener', () => {
provide: MessageQueueService,
useValue: messageQueueService,
},
{
provide: FeatureFlagService,
useValue: featureFlagService,
},
{
provide: 'MESSAGE_QUEUE_workflow-queue',
useValue: messageQueueService,
@ -303,14 +293,6 @@ describe('DatabaseEventTriggerListener', () => {
);
});
it('should ignore events when feature flag is disabled', async () => {
featureFlagService.isFeatureEnabled.mockResolvedValueOnce(false);
await listener.handleObjectRecordUpdateEvent(mockPayload);
expect(messageQueueService.add).not.toHaveBeenCalled();
});
it('should handle multiple events in a batch', async () => {
const batchPayload = {
...mockPayload,

View File

@ -10,8 +10,6 @@ import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/t
import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event';
import { ObjectRecordNonDestructiveEvent } from 'src/engine/core-modules/event-emitter/types/object-record-non-destructive-event';
import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@ -36,7 +34,6 @@ export class DatabaseEventTriggerListener {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService,
private readonly isFeatureFlagEnabledService: FeatureFlagService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {}
@ -231,13 +228,7 @@ export class DatabaseEventTriggerListener {
return true;
}
const isWorkflowEnabled =
await this.isFeatureFlagEnabledService.isFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
workspaceId,
);
return !isWorkflowEnabled;
return false;
}
private async handleEvent({