From cd4263f7fdb90ec8150a57c98b8ed56a3f4d34d8 Mon Sep 17 00:00:00 2001 From: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 30 Jul 2024 12:36:39 +0200 Subject: [PATCH] 6431 create new field activationStatus inside workspace table (#6439) Closes #6431 - create new field `activationStatus` - create migration commands - add logic to update `activationStatus` on workspace activation and on stripe subscriptionStatus change --------- Co-authored-by: Charles Bochet --- .../twenty-front/src/generated/graphql.tsx | 15 ++- .../ObjectMetadataItemsLoadEffect.tsx | 3 +- .../hooks/useObjectMetadataItem.ts | 3 +- .../hooks/useObjectNamePluralFromSingular.ts | 3 +- .../hooks/useObjectNameSingularFromPlural.ts | 3 +- .../hooks/useLoadRecordIndexTable.ts | 3 +- .../__tests__/useSubscriptionStatus.test.ts | 9 +- .../src/testing/mock-data/users.ts | 3 +- ...set-workspace-activation-status.command.ts | 124 ++++++++++++++++++ .../0-23/0-23-upgrade-version.command.ts | 3 + .../0-23/0-23-upgrade-version.module.ts | 4 + .../1722256203539-addActivationStatus.ts | 23 ++++ .../billing/billing.workspace-service.ts | 24 +++- .../workspace/services/workspace.service.ts | 6 +- .../workspace/workspace.entity.ts | 30 +++-- .../workspace/workspace.resolver.ts | 16 ++- 16 files changed, 242 insertions(+), 30 deletions(-) create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1722256203539-addActivationStatus.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 20e35079e..9876c60cd 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -964,7 +964,7 @@ export type Verify = { export type Workspace = { __typename?: 'Workspace'; - activationStatus: Scalars['String']; + activationStatus: WorkspaceActivationStatus; allowImpersonation: Scalars['Boolean']; billingSubscriptions?: Maybe>; createdAt: Scalars['DateTime']; @@ -993,6 +993,11 @@ export type WorkspaceFeatureFlagsArgs = { sorting?: Array; }; +export enum WorkspaceActivationStatus { + Active = 'ACTIVE', + Inactive = 'INACTIVE' +} + export type WorkspaceEdge = { __typename?: 'WorkspaceEdge'; /** Cursor for this node. */ @@ -1237,7 +1242,7 @@ export type ImpersonateMutationVariables = Exact<{ }>; -export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type RenewTokenMutationVariables = Exact<{ appToken: Scalars['String']; @@ -1269,7 +1274,7 @@ export type VerifyMutationVariables = Exact<{ }>; -export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type CheckUserExistsQueryVariables = Exact<{ email: Scalars['String']; @@ -1330,7 +1335,7 @@ export type GetAisqlQueryQueryVariables = Exact<{ export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } }; -export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; +export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }; export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>; @@ -1347,7 +1352,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>; -export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; +export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } }; export type AddUserToWorkspaceMutationVariables = Exact<{ inviteHash: Scalars['String']; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index c8357ccc7..1eb2eff27 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -6,6 +6,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -25,7 +26,7 @@ export const ObjectMetadataItemsLoadEffect = () => { useEffect(() => { const toSetObjectMetadataItems = isUndefinedOrNull(currentUser) || - currentWorkspace?.activationStatus !== 'active' + currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active ? getObjectMetadataItemsMock() : newObjectMetadataItems; if ( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 91e4107fd..4d2a9bdc8 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -7,6 +7,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; import { isDefined } from '~/utils/isDefined'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; export const useObjectMetadataItem = ({ @@ -26,7 +27,7 @@ export const useObjectMetadataItem = ({ let objectMetadataItems = useRecoilValue(objectMetadataItemsState); - if (currentWorkspace?.activationStatus !== 'active') { + if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts index 502bd2938..ad9d8641b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const useObjectNamePluralFromSingular = ({ @@ -20,7 +21,7 @@ export const useObjectNamePluralFromSingular = ({ }), ); - if (currentWorkspace?.activationStatus !== 'active') { + if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts index 1ec11b9bb..3b4a24494 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const useObjectNameSingularFromPlural = ({ @@ -21,7 +22,7 @@ export const useObjectNameSingularFromPlural = ({ }), ); - if (currentWorkspace?.activationStatus !== 'active') { + if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) { objectMetadataItem = mockObjectMetadataItems.find( (objectMetadataItem) => diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 99f613521..dbf4eada7 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -9,6 +9,7 @@ import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hook import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; +import { WorkspaceActivationStatus } from '~/generated/graphql'; export const useFindManyParams = ( objectNameSingular: string, @@ -65,7 +66,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { return { records: - currentWorkspace?.activationStatus === 'active' + currentWorkspace?.activationStatus === WorkspaceActivationStatus.Active ? records : SIGN_IN_BACKGROUND_MOCK_COMPANIES, totalCount: totalCount, diff --git a/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts b/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts index 3ba84ddb7..d7840ca27 100644 --- a/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts +++ b/packages/twenty-front/src/modules/workspace/hooks/__tests__/useSubscriptionStatus.test.ts @@ -1,5 +1,5 @@ -import { act } from 'react-dom/test-utils'; import { renderHook } from '@testing-library/react'; +import { act } from 'react-dom/test-utils'; import { RecoilRoot, useSetRecoilState } from 'recoil'; import { v4 } from 'uuid'; @@ -8,12 +8,15 @@ import { currentWorkspaceState, } from '@/auth/states/currentWorkspaceState'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; -import { SubscriptionStatus } from '~/generated/graphql'; +import { + SubscriptionStatus, + WorkspaceActivationStatus, +} from '~/generated/graphql'; const currentWorkspace = { id: '1', currentBillingSubscription: { status: SubscriptionStatus.Incomplete }, - activationStatus: 'active', + activationStatus: WorkspaceActivationStatus.Active, allowImpersonation: true, } as CurrentWorkspace; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 1839303e9..2bbe2185f 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -5,6 +5,7 @@ import { SubscriptionStatus, User, Workspace, + WorkspaceActivationStatus, } from '~/generated/graphql'; type MockedUser = Pick< @@ -37,7 +38,7 @@ export const mockDefaultWorkspace: Workspace = { inviteHash: 'twenty.com-invite-hash', logo: workspaceLogoUrl, allowImpersonation: true, - activationStatus: 'active', + activationStatus: WorkspaceActivationStatus.Active, featureFlags: [ { id: '1492de61-5018-4368-8923-4f1eeaf988c4', diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts new file mode 100644 index 000000000..57700e9b7 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command.ts @@ -0,0 +1,124 @@ +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command, CommandRunner, Option } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { BillingService } from 'src/engine/core-modules/billing/billing.service'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/workspace-cache-version/workspace-cache-version.service'; + +interface SetWorkspaceActivationStatusCommandOptions { + workspaceId?: string; +} + +@Command({ + name: 'migrate-0.23:set-workspace-activation-status', + description: 'Set workspace activation status', +}) +export class SetWorkspaceActivationStatusCommand extends CommandRunner { + private readonly logger = new Logger( + SetWorkspaceActivationStatusCommand.name, + ); + constructor( + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, + private readonly typeORMService: TypeORMService, + private readonly dataSourceService: DataSourceService, + private readonly workspaceCacheVersionService: WorkspaceCacheVersionService, + private readonly billingService: BillingService, + ) { + super(); + } + + @Option({ + flags: '-w, --workspace-id [workspace_id]', + description: 'workspace id. Command runs on all workspaces if not provided', + required: false, + }) + parseWorkspaceId(value: string): string { + return value; + } + + async run( + _passedParam: string[], + options: SetWorkspaceActivationStatusCommandOptions, + ): Promise { + let activeSubscriptionWorkspaceIds: string[] = []; + + if (options.workspaceId) { + activeSubscriptionWorkspaceIds = [options.workspaceId]; + } else { + activeSubscriptionWorkspaceIds = + await this.billingService.getActiveSubscriptionWorkspaceIds(); + } + + if (!activeSubscriptionWorkspaceIds.length) { + this.logger.log(chalk.yellow('No workspace found')); + + return; + } else { + this.logger.log( + chalk.green( + `Running command on ${activeSubscriptionWorkspaceIds.length} workspaces`, + ), + ); + } + + for (const workspaceId of activeSubscriptionWorkspaceIds) { + try { + const dataSourceMetadatas = + await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId( + workspaceId, + ); + + for (const dataSourceMetadata of dataSourceMetadatas) { + const workspaceDataSource = + await this.typeORMService.connectToDataSource(dataSourceMetadata); + + if (workspaceDataSource) { + const queryRunner = workspaceDataSource.createQueryRunner(); + + await queryRunner.connect(); + await queryRunner.startTransaction(); + + try { + await this.workspaceRepository.update( + { id: workspaceId }, + { activationStatus: WorkspaceActivationStatus.ACTIVE }, + ); + + await queryRunner.commitTransaction(); + } catch (error) { + await queryRunner.rollbackTransaction(); + this.logger.log( + chalk.red(`Running command on workspace ${workspaceId} failed`), + ); + throw error; + } finally { + await queryRunner.release(); + } + } + } + + await this.workspaceCacheVersionService.incrementVersion(workspaceId); + + this.logger.log( + chalk.green(`Running command on workspace ${workspaceId} done`), + ); + } catch (error) { + this.logger.error( + `Migration failed for workspace ${workspaceId}: ${error.message}`, + ); + } + } + + this.logger.log(chalk.green(`Command completed!`)); + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts index 2057f9fee..2d5d21372 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command.ts @@ -3,6 +3,7 @@ import { Command, CommandRunner, Option } from 'nest-commander'; import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; +import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; interface Options { workspaceId?: string; @@ -17,6 +18,7 @@ export class UpgradeTo0_23Command extends CommandRunner { private readonly migrateLinkFieldsToLinks: MigrateLinkFieldsToLinksCommand, private readonly migrateDomainNameFromTextToLinks: MigrateDomainNameFromTextToLinksCommand, private readonly migrateMessageChannelSyncStatusEnumCommand: MigrateMessageChannelSyncStatusEnumCommand, + private readonly setWorkspaceActivationStatusCommand: SetWorkspaceActivationStatusCommand, ) { super(); } @@ -38,5 +40,6 @@ export class UpgradeTo0_23Command extends CommandRunner { _passedParam, options, ); + await this.setWorkspaceActivationStatusCommand.run(_passedParam, options); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts index cba2f6872..1ff3602b8 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module.ts @@ -4,8 +4,10 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { MigrateDomainNameFromTextToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-domain-to-links.command'; import { MigrateLinkFieldsToLinksCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-link-fields-to-links.command'; import { MigrateMessageChannelSyncStatusEnumCommand } from 'src/database/commands/upgrade-version/0-23/0-23-migrate-message-channel-sync-status-enum.command'; +import { SetWorkspaceActivationStatusCommand } from 'src/database/commands/upgrade-version/0-23/0-23-set-workspace-activation-status.command'; import { UpgradeTo0_23Command } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.command'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; @@ -26,11 +28,13 @@ import { ViewModule } from 'src/modules/view/view.module'; TypeOrmModule.forFeature([ObjectMetadataEntity], 'metadata'), TypeORMModule, ViewModule, + BillingModule, ], providers: [ MigrateLinkFieldsToLinksCommand, MigrateDomainNameFromTextToLinksCommand, MigrateMessageChannelSyncStatusEnumCommand, + SetWorkspaceActivationStatusCommand, UpgradeTo0_23Command, ], }) diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1722256203539-addActivationStatus.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203539-addActivationStatus.ts new file mode 100644 index 000000000..81b6e7961 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203539-addActivationStatus.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddActivationStatus1722256203539 implements MigrationInterface { + name = 'AddActivationStatus1722256203539'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationstatus_enum" AS ENUM('ACTIVE', 'INACTIVE')`, + ); + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD "activationStatus" "core"."workspace_activationstatus_enum" NOT NULL DEFAULT 'INACTIVE'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP COLUMN "activationStatus"`, + ); + await queryRunner.query( + `DROP TYPE "core"."workspace_activationstatus_enum"`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts index f0d1e4090..f03935858 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.workspace-service.ts @@ -19,7 +19,10 @@ import { } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { User } from 'src/engine/core-modules/user/user.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { assert } from 'src/utils/assert'; @@ -329,5 +332,24 @@ export class BillingWorkspaceService { workspaceId, key: FeatureFlagKeys.IsFreeAccessEnabled, }); + + if ( + data.object.status === SubscriptionStatus.Canceled || + data.object.status === SubscriptionStatus.Unpaid + ) { + await this.workspaceRepository.update(workspaceId, { + activationStatus: WorkspaceActivationStatus.INACTIVE, + }); + } + + if ( + data.object.status === SubscriptionStatus.Active || + data.object.status === SubscriptionStatus.Trialing || + data.object.status === SubscriptionStatus.PastDue + ) { + await this.workspaceRepository.update(workspaceId, { + activationStatus: WorkspaceActivationStatus.ACTIVE, + }); + } } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index e94a3596f..8b176e554 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -15,7 +15,10 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use import { User } from 'src/engine/core-modules/user/user.entity'; import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input'; import { SendInviteLink } from 'src/engine/core-modules/workspace/dtos/send-invite-link.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/integrations/email/email.service'; import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; @@ -44,6 +47,7 @@ export class WorkspaceService extends TypeOrmQueryService { } await this.workspaceRepository.update(user.defaultWorkspace.id, { displayName: data.displayName, + activationStatus: WorkspaceActivationStatus.ACTIVE, }); await this.workspaceManagerService.init(user.defaultWorkspace.id); await this.userWorkspaceService.createWorkspaceMember( diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index 6b4d2b425..5d54ec231 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -1,4 +1,4 @@ -import { Field, ObjectType } from '@nestjs/graphql'; +import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField, UnPagedRelation } from '@ptc-org/nestjs-query-graphql'; import { @@ -11,14 +11,23 @@ import { UpdateDateColumn, } from 'typeorm'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; -import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; +import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity'; +import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; + +export enum WorkspaceActivationStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', +} + +registerEnumType(WorkspaceActivationStatus, { + name: 'WorkspaceActivationStatus', +}); @Entity({ name: 'workspace', schema: 'core' }) @ObjectType('Workspace') @@ -87,8 +96,13 @@ export class Workspace { @Field({ nullable: true }) workspaceMembersCount: number; - @Field() - activationStatus: 'active' | 'inactive'; + @Field(() => WorkspaceActivationStatus) + @Column({ + type: 'enum', + enum: WorkspaceActivationStatus, + default: WorkspaceActivationStatus.INACTIVE, + }) + activationStatus: WorkspaceActivationStatus; @OneToMany( () => BillingSubscription, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 2ee565590..e1ae3e27e 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -29,7 +29,7 @@ import { WorkspaceCacheVersionService } from 'src/engine/metadata-modules/worksp import { assert } from 'src/utils/assert'; import { streamToBuffer } from 'src/utils/stream-to-buffer'; -import { Workspace } from './workspace.entity'; +import { Workspace, WorkspaceActivationStatus } from './workspace.entity'; import { WorkspaceService } from './services/workspace.service'; @@ -100,15 +100,19 @@ export class WorkspaceResolver { return this.workspaceService.deleteWorkspace(id); } - @ResolveField(() => String) + @ResolveField(() => WorkspaceActivationStatus) async activationStatus( @Parent() workspace: Workspace, - ): Promise<'active' | 'inactive'> { - if (await this.workspaceService.isWorkspaceActivated(workspace.id)) { - return 'active'; + ): Promise { + if (workspace.activationStatus) { + return workspace.activationStatus; } - return 'inactive'; + if (await this.workspaceService.isWorkspaceActivated(workspace.id)) { + return WorkspaceActivationStatus.ACTIVE; + } + + return WorkspaceActivationStatus.INACTIVE; } @ResolveField(() => String, { nullable: true })