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

@ -326,6 +326,7 @@ export type ClientConfig = {
api: ApiConfig; api: ApiConfig;
authProviders: AuthProviders; authProviders: AuthProviders;
billing: Billing; billing: Billing;
calendarBookingPageId?: Maybe<Scalars['String']['output']>;
canManageFeatureFlags: Scalars['Boolean']['output']; canManageFeatureFlags: Scalars['Boolean']['output'];
captcha: Captcha; captcha: Captcha;
chromeExtensionId?: Maybe<Scalars['String']['output']>; chromeExtensionId?: Maybe<Scalars['String']['output']>;
@ -658,8 +659,7 @@ export enum FeatureFlagKey {
IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED', IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED'
} }
export type Field = { export type Field = {
@ -1001,6 +1001,7 @@ export type Mutation = {
signUp: AvailableWorkspacesAndAccessTokensOutput; signUp: AvailableWorkspacesAndAccessTokensOutput;
signUpInNewWorkspace: SignUpOutput; signUpInNewWorkspace: SignUpOutput;
signUpInWorkspace: SignUpOutput; signUpInWorkspace: SignUpOutput;
skipBookOnboardingStep: OnboardingStepSuccess;
skipSyncEmailOnboardingStep: OnboardingStepSuccess; skipSyncEmailOnboardingStep: OnboardingStepSuccess;
submitFormStep: Scalars['Boolean']['output']; submitFormStep: Scalars['Boolean']['output'];
switchToEnterprisePlan: BillingUpdateOutput; switchToEnterprisePlan: BillingUpdateOutput;
@ -1574,6 +1575,7 @@ export type OnDbEventInput = {
/** Onboarding status */ /** Onboarding status */
export enum OnboardingStatus { export enum OnboardingStatus {
BOOK_ONBOARDING = 'BOOK_ONBOARDING',
COMPLETED = 'COMPLETED', COMPLETED = 'COMPLETED',
INVITE_TEAM = 'INVITE_TEAM', INVITE_TEAM = 'INVITE_TEAM',
PLAN_REQUIRED = 'PLAN_REQUIRED', PLAN_REQUIRED = 'PLAN_REQUIRED',
@ -1945,6 +1947,8 @@ export type Role = {
export type RunWorkflowVersionInput = { export type RunWorkflowVersionInput = {
/** Execution result in JSON format */ /** Execution result in JSON format */
payload?: InputMaybe<Scalars['JSON']['input']>; payload?: InputMaybe<Scalars['JSON']['input']>;
/** Workflow run ID */
workflowRunId?: InputMaybe<Scalars['String']['input']>;
/** Workflow version ID */ /** Workflow version ID */
workflowVersionId: Scalars['String']['input']; workflowVersionId: Scalars['String']['input'];
}; };

View File

@ -615,8 +615,7 @@ export enum FeatureFlagKey {
IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED', IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED', IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_INTEGRATION_ENABLED',
IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED', IS_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED'
IS_WORKFLOW_ENABLED = 'IS_WORKFLOW_ENABLED'
} }
export type Field = { export type Field = {

View File

@ -413,13 +413,11 @@ export const DEFAULT_RECORD_ACTIONS_CONFIG: Record<
shouldBeRegistered: ({ shouldBeRegistered: ({
objectMetadataItem, objectMetadataItem,
viewType, viewType,
isWorkflowEnabled,
getTargetObjectReadPermission, getTargetObjectReadPermission,
}) => }) =>
getTargetObjectReadPermission(CoreObjectNameSingular.Workflow) === true && getTargetObjectReadPermission(CoreObjectNameSingular.Workflow) === true &&
(objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Workflow || (objectMetadataItem?.nameSingular !== CoreObjectNameSingular.Workflow ||
viewType === ActionViewType.SHOW_PAGE) && viewType === ActionViewType.SHOW_PAGE),
isWorkflowEnabled,
availableOn: [ availableOn: [
ActionViewType.INDEX_PAGE_NO_SELECTION, ActionViewType.INDEX_PAGE_NO_SELECTION,
ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION, ActionViewType.INDEX_PAGE_SINGLE_RECORD_SELECTION,

View File

@ -4,18 +4,12 @@ import { ActionType } from '@/action-menu/actions/types/ActionType';
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext'; import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
import { useActiveWorkflowVersionsWithManualTrigger } from '@/workflow/hooks/useActiveWorkflowVersionsWithManualTrigger'; import { useActiveWorkflowVersionsWithManualTrigger } from '@/workflow/hooks/useActiveWorkflowVersionsWithManualTrigger';
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion'; import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { msg } from '@lingui/core/macro'; import { msg } from '@lingui/core/macro';
import { useContext } from 'react'; import { useContext } from 'react';
import { capitalize, isDefined } from 'twenty-shared/utils'; import { capitalize, isDefined } from 'twenty-shared/utils';
import { IconSettingsAutomation } from 'twenty-ui/display'; import { IconSettingsAutomation } from 'twenty-ui/display';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRunWorkflowRecordAgnosticActions = () => { export const useRunWorkflowRecordAgnosticActions = () => {
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
);
const { actionMenuType } = useContext(ActionMenuContext); const { actionMenuType } = useContext(ActionMenuContext);
const { records: activeWorkflowVersions } = const { records: activeWorkflowVersions } =
@ -27,10 +21,6 @@ export const useRunWorkflowRecordAgnosticActions = () => {
const { runWorkflowVersion } = useRunWorkflowVersion(); const { runWorkflowVersion } = useRunWorkflowVersion();
if (!isWorkflowEnabled) {
return [];
}
return activeWorkflowVersions return activeWorkflowVersions
.map((activeWorkflowVersion, index) => { .map((activeWorkflowVersion, index) => {
if (!isDefined(activeWorkflowVersion.workflow)) { if (!isDefined(activeWorkflowVersion.workflow)) {

View File

@ -8,7 +8,6 @@ import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow';
export type ShouldBeRegisteredFunctionParams = { export type ShouldBeRegisteredFunctionParams = {
objectMetadataItem?: ObjectMetadataItem; objectMetadataItem?: ObjectMetadataItem;
objectPermissions: ObjectPermissions; objectPermissions: ObjectPermissions;
isWorkflowEnabled: boolean;
recordFilters?: RecordFilter[]; recordFilters?: RecordFilter[];
isShowPage?: boolean; isShowPage?: boolean;
isSoftDeleteFilterActive?: boolean; isSoftDeleteFilterActive?: boolean;

View File

@ -6,10 +6,8 @@ import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const ActionMenuContextProvider = ({ export const ActionMenuContextProvider = ({
children, children,
@ -19,10 +17,6 @@ export const ActionMenuContextProvider = ({
}: Omit<ActionMenuContextType, 'actions'> & { }: Omit<ActionMenuContextType, 'actions'> & {
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
);
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2( const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValueV2(
contextStoreCurrentObjectMetadataItemIdComponentState, contextStoreCurrentObjectMetadataItemIdComponentState,
); );
@ -39,7 +33,7 @@ export const ActionMenuContextProvider = ({
objectMetadataItem?.nameSingular === CoreObjectNameSingular.WorkflowRun || objectMetadataItem?.nameSingular === CoreObjectNameSingular.WorkflowRun ||
objectMetadataItem?.nameSingular === CoreObjectNameSingular.WorkflowVersion; objectMetadataItem?.nameSingular === CoreObjectNameSingular.WorkflowVersion;
if (isWorkflowEnabled && isDefined(objectMetadataItem) && isWorkflowObject) { if (isDefined(objectMetadataItem) && isWorkflowObject) {
return ( return (
<ActionMenuContextProviderWorkflowObjects <ActionMenuContextProviderWorkflowObjects
isInRightDrawer={isInRightDrawer} isInRightDrawer={isInRightDrawer}
@ -53,7 +47,6 @@ export const ActionMenuContextProvider = ({
} }
if ( if (
isWorkflowEnabled &&
isDefined(objectMetadataItem) && isDefined(objectMetadataItem) &&
(actionMenuType === 'command-menu' || (actionMenuType === 'command-menu' ||
actionMenuType === 'command-menu-show-page-action-menu-dropdown') actionMenuType === 'command-menu-show-page-action-menu-dropdown')

View File

@ -13,10 +13,8 @@ import { useObjectPermissionsForObject } from '@/object-record/hooks/useObjectPe
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState'; import { isSoftDeleteFilterActiveComponentState } from '@/object-record/record-table/states/isSoftDeleteFilterActiveComponentState';
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useContext } from 'react'; import { useContext } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useRecoilCallback, useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useShouldActionBeRegisteredParams = ({ export const useShouldActionBeRegisteredParams = ({
objectMetadataItem, objectMetadataItem,
@ -61,10 +59,6 @@ export const useShouldActionBeRegisteredParams = ({
useRecoilComponentValueV2(contextStoreCurrentViewTypeComponentState) === useRecoilComponentValueV2(contextStoreCurrentViewTypeComponentState) ===
ContextStoreViewType.ShowPage; ContextStoreViewType.ShowPage;
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
);
const numberOfSelectedRecords = useRecoilComponentValueV2( const numberOfSelectedRecords = useRecoilComponentValueV2(
contextStoreNumberOfSelectedRecordsComponentState, contextStoreNumberOfSelectedRecordsComponentState,
); );
@ -101,7 +95,6 @@ export const useShouldActionBeRegisteredParams = ({
isSoftDeleteFilterActive, isSoftDeleteFilterActive,
isShowPage, isShowPage,
selectedRecord, selectedRecord,
isWorkflowEnabled,
numberOfSelectedRecords, numberOfSelectedRecords,
viewType: viewType ?? undefined, viewType: viewType ?? undefined,
getTargetObjectReadPermission: getObjectReadPermission, getTargetObjectReadPermission: getObjectReadPermission,

View File

@ -1,43 +1,22 @@
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata'; import { useMemo } from 'react';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useCallback, useMemo } from 'react';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useFilteredObjectMetadataItems = () => { export const useFilteredObjectMetadataItems = () => {
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
);
const isWorkflowToBeFiltered = useCallback(
(nameSingular: string) => {
return (
!isWorkflowEnabled && isWorkflowRelatedObjectMetadata(nameSingular)
);
},
[isWorkflowEnabled],
);
const activeNonSystemObjectMetadataItems = useMemo( const activeNonSystemObjectMetadataItems = useMemo(
() => () =>
objectMetadataItems.filter( objectMetadataItems.filter(
({ isActive, isSystem, nameSingular }) => ({ isActive, isSystem }) => isActive && !isSystem,
isActive && !isSystem && !isWorkflowToBeFiltered(nameSingular),
), ),
[isWorkflowToBeFiltered, objectMetadataItems], [objectMetadataItems],
); );
const activeObjectMetadataItems = useMemo( const activeObjectMetadataItems = useMemo(
() => () => objectMetadataItems.filter(({ isActive }) => isActive),
objectMetadataItems.filter( [objectMetadataItems],
({ isActive, nameSingular }) =>
isActive && !isWorkflowToBeFiltered(nameSingular),
),
[isWorkflowToBeFiltered, objectMetadataItems],
); );
const alphaSortedActiveNonSystemObjectMetadataItems = const alphaSortedActiveNonSystemObjectMetadataItems =

View File

@ -4,10 +4,7 @@ import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/Object
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { isWorkflowRelatedObjectMetadata } from '@/object-metadata/utils/isWorkflowRelatedObjectMetadata';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier';
export const useObjectMetadataItem = ({ export const useObjectMetadataItem = ({
@ -20,21 +17,8 @@ export const useObjectMetadataItem = ({
}), }),
); );
const isWorkflowEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
);
const isWorkflowToBeFiltered =
!isWorkflowEnabled && isWorkflowRelatedObjectMetadata(objectNameSingular);
const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
if (isWorkflowToBeFiltered) {
throw new Error(
'Workflow is not enabled. If you want to use it, please enable it in the lab.',
);
}
if (!isDefined(objectMetadataItem)) { if (!isDefined(objectMetadataItem)) {
throw new ObjectMetadataItemNotFoundError( throw new ObjectMetadataItemNotFoundError(
objectNameSingular, objectNameSingular,

View File

@ -20,7 +20,6 @@ import {
IconSettings, IconSettings,
} from 'twenty-ui/display'; } from 'twenty-ui/display';
import { FieldMetadataType } from '~/generated-metadata/graphql'; import { FieldMetadataType } from '~/generated-metadata/graphql';
import { FeatureFlagKey } from '~/generated/graphql';
export const useRecordShowContainerTabs = ( export const useRecordShowContainerTabs = (
loading: boolean, loading: boolean,
@ -155,7 +154,7 @@ export const useRecordShowContainerTabs = (
ifMobile: false, ifMobile: false,
ifDesktop: false, ifDesktop: false,
ifInRightDrawer: false, ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IS_WORKFLOW_ENABLED], ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [], ifRequiredObjectsInactive: [],
ifRelationsMissing: [], ifRelationsMissing: [],
}, },
@ -178,7 +177,7 @@ export const useRecordShowContainerTabs = (
ifMobile: false, ifMobile: false,
ifDesktop: false, ifDesktop: false,
ifInRightDrawer: false, ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IS_WORKFLOW_ENABLED], ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [], ifRequiredObjectsInactive: [],
ifRelationsMissing: [], ifRelationsMissing: [],
}, },
@ -200,7 +199,7 @@ export const useRecordShowContainerTabs = (
ifMobile: false, ifMobile: false,
ifDesktop: false, ifDesktop: false,
ifInRightDrawer: false, ifInRightDrawer: false,
ifFeaturesDisabled: [FeatureFlagKey.IS_WORKFLOW_ENABLED], ifFeaturesDisabled: [],
ifRequiredObjectsInactive: [], ifRequiredObjectsInactive: [],
ifRelationsMissing: [], ifRelationsMissing: [],
}, },

View File

@ -4,7 +4,6 @@ import { ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
// This file is not designed to be manually edited. // This file is not designed to be manually edited.
// It's an extract from the dev seeded environment metadata call // It's an extract from the dev seeded environment metadata call
// TODO: automate the generation of this file // TODO: automate the generation of this file
// ⚠️ WARNING ⚠️: Be sure to activate the workflow feature flag (IsWorkflowEnabled) before updating that mock.
// prettier-ignore // prettier-ignore
export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery =

View File

@ -68,10 +68,6 @@ export const mockCurrentWorkspace: Workspace = {
key: FeatureFlagKey.IS_POSTGRESQL_INTEGRATION_ENABLED, key: FeatureFlagKey.IS_POSTGRESQL_INTEGRATION_ENABLED,
value: true, value: true,
}, },
{
key: FeatureFlagKey.IS_WORKFLOW_ENABLED,
value: true,
},
], ],
createdAt: '2023-04-26T10:23:42.33625+00:00', createdAt: '2023-04-26T10:23:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00',

View File

@ -7,19 +7,11 @@ type FeatureFlagMetadata = {
}; };
export type PublicFeatureFlag = { export type PublicFeatureFlag = {
key: Extract<FeatureFlagKey, FeatureFlagKey.IS_WORKFLOW_ENABLED>; key: FeatureFlagKey;
metadata: FeatureFlagMetadata; metadata: FeatureFlagMetadata;
}; };
export const PUBLIC_FEATURE_FLAGS: PublicFeatureFlag[] = [ 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 ...(process.env.CLOUDFLARE_API_KEY
? [ ? [
// { // {

View File

@ -2,7 +2,6 @@ export enum FeatureFlagKey {
IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED', IS_AIRTABLE_INTEGRATION_ENABLED = 'IS_AIRTABLE_INTEGRATION_ENABLED',
IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED', IS_POSTGRESQL_INTEGRATION_ENABLED = 'IS_POSTGRESQL_INTEGRATION_ENABLED',
IS_STRIPE_INTEGRATION_ENABLED = 'IS_STRIPE_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_UNIQUE_INDEXES_ENABLED = 'IS_UNIQUE_INDEXES_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED', IS_PERMISSIONS_V2_ENABLED = 'IS_PERMISSIONS_V2_ENABLED',

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,6 @@ export class ServerlessFunctionException extends CustomException {
export enum ServerlessFunctionExceptionCode { export enum ServerlessFunctionExceptionCode {
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND', SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
SERVERLESS_FUNCTION_VERSION_NOT_FOUND = 'SERVERLESS_FUNCTION_VERSION_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_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY', SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING', SERVERLESS_FUNCTION_BUILDING = 'SERVERLESS_FUNCTION_BUILDING',

View File

@ -5,7 +5,6 @@ import { InjectRepository } from '@nestjs/typeorm';
import graphqlTypeJson from 'graphql-type-json'; import graphqlTypeJson from 'graphql-type-json';
import { Repository } from 'typeorm'; 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 { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; 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 { 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 { 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 { 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 { 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'; 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>, 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) @Query(() => ServerlessFunctionDTO)
async findOneServerlessFunction( async findOneServerlessFunction(
@Args('input') { id }: ServerlessFunctionIdInput, @Args('input') { id }: ServerlessFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionRepository.findOneOrFail({ return await this.serverlessFunctionRepository.findOneOrFail({
where: { where: {
id, id,
@ -76,8 +54,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.findManyServerlessFunctions({ return await this.serverlessFunctionService.findManyServerlessFunctions({
workspaceId, workspaceId,
}); });
@ -87,13 +63,8 @@ export class ServerlessFunctionResolver {
} }
@Query(() => graphqlTypeJson) @Query(() => graphqlTypeJson)
async getAvailablePackages( async getAvailablePackages(@Args('input') { id }: ServerlessFunctionIdInput) {
@Args('input') { id }: ServerlessFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.getAvailablePackages(id); return await this.serverlessFunctionService.getAvailablePackages(id);
} catch (error) { } catch (error) {
serverlessFunctionGraphQLApiExceptionHandler(error); serverlessFunctionGraphQLApiExceptionHandler(error);
@ -106,8 +77,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.getServerlessFunctionSourceCode( return await this.serverlessFunctionService.getServerlessFunctionSourceCode(
workspaceId, workspaceId,
input.id, input.id,
@ -124,8 +93,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.deleteOneServerlessFunction({ return await this.serverlessFunctionService.deleteOneServerlessFunction({
id: input.id, id: input.id,
workspaceId, workspaceId,
@ -142,8 +109,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.updateOneServerlessFunction( return await this.serverlessFunctionService.updateOneServerlessFunction(
input, input,
workspaceId, workspaceId,
@ -160,8 +125,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
return await this.serverlessFunctionService.createOneServerlessFunction( return await this.serverlessFunctionService.createOneServerlessFunction(
input, input,
workspaceId, workspaceId,
@ -177,7 +140,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
const { id, payload, version } = input; const { id, payload, version } = input;
return await this.serverlessFunctionService.executeOneServerlessFunction( return await this.serverlessFunctionService.executeOneServerlessFunction(
@ -197,7 +159,6 @@ export class ServerlessFunctionResolver {
@AuthWorkspace() { id: workspaceId }: Workspace, @AuthWorkspace() { id: workspaceId }: Workspace,
) { ) {
try { try {
await this.checkFeatureFlag(workspaceId);
const { id } = input; const { id } = input;
return await this.serverlessFunctionService.publishOneServerlessFunction( return await this.serverlessFunctionService.publishOneServerlessFunction(

View File

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

View File

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

View File

@ -1,17 +1,15 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm'; 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 { 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 { 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 { 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({ @Module({
imports: [ imports: [
FeatureFlagModule,
TypeOrmModule.forFeature([Workspace], 'core'), TypeOrmModule.forFeature([Workspace], 'core'),
WorkflowCommonModule, WorkflowCommonModule,
], ],

View File

@ -1,6 +1,5 @@
import { Test, TestingModule } from '@nestjs/testing'; 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 { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; 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'; import { AutomatedTriggerType } from 'src/modules/workflow/common/standard-objects/workflow-automated-trigger.workspace-entity';
@ -12,7 +11,6 @@ describe('DatabaseEventTriggerListener', () => {
let listener: DatabaseEventTriggerListener; let listener: DatabaseEventTriggerListener;
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>; let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let messageQueueService: jest.Mocked<MessageQueueService>; let messageQueueService: jest.Mocked<MessageQueueService>;
let featureFlagService: jest.Mocked<FeatureFlagService>;
const mockRepository = { const mockRepository = {
find: jest.fn(), find: jest.fn(),
@ -27,10 +25,6 @@ describe('DatabaseEventTriggerListener', () => {
add: jest.fn(), add: jest.fn(),
} as any; } as any;
featureFlagService = {
isFeatureEnabled: jest.fn().mockResolvedValue(true),
} as any;
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
DatabaseEventTriggerListener, DatabaseEventTriggerListener,
@ -42,10 +36,6 @@ describe('DatabaseEventTriggerListener', () => {
provide: MessageQueueService, provide: MessageQueueService,
useValue: messageQueueService, useValue: messageQueueService,
}, },
{
provide: FeatureFlagService,
useValue: featureFlagService,
},
{ {
provide: 'MESSAGE_QUEUE_workflow-queue', provide: 'MESSAGE_QUEUE_workflow-queue',
useValue: messageQueueService, 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 () => { it('should handle multiple events in a batch', async () => {
const batchPayload = { const batchPayload = {
...mockPayload, ...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 { 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 { 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 { 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 { 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 { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@ -36,7 +34,6 @@ export class DatabaseEventTriggerListener {
private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
@InjectMessageQueue(MessageQueue.workflowQueue) @InjectMessageQueue(MessageQueue.workflowQueue)
private readonly messageQueueService: MessageQueueService, private readonly messageQueueService: MessageQueueService,
private readonly isFeatureFlagEnabledService: FeatureFlagService,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
) {} ) {}
@ -231,13 +228,7 @@ export class DatabaseEventTriggerListener {
return true; return true;
} }
const isWorkflowEnabled = return false;
await this.isFeatureFlagEnabledService.isFeatureEnabled(
FeatureFlagKey.IS_WORKFLOW_ENABLED,
workspaceId,
);
return !isWorkflowEnabled;
} }
private async handleEvent({ private async handleEvent({