diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts index 9e7935792..dd87ef14a 100644 --- a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery.ts @@ -102,6 +102,7 @@ export const useGenerateCombinedFindManyRecordsQuery = ({ } pageInfo { hasNextPage + hasPreviousPage startCursor endCursor } diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 4db2eefa8..cd1851625 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -29,8 +29,6 @@ import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer'; import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer'; import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; -import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext'; -import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { FieldMetadataType, @@ -302,27 +300,25 @@ export const RecordShowContainer = ({ ); return ( - - - - {!isMobile && summaryCard} - {!isMobile && fieldsBox} - - } - fieldsBox={fieldsBox} - loading={isPrefetchLoading || loading || recordLoading} - /> - - + + + {!isMobile && summaryCard} + {!isMobile && fieldsBox} + + } + fieldsBox={fieldsBox} + loading={isPrefetchLoading || loading || recordLoading} + /> + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts index d252e80ca..cea8c47db 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPagePagination.ts @@ -5,7 +5,6 @@ import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { useRecordIdsFromFindManyCacheRootQuery } from '@/object-record/record-show/hooks/useRecordIdsFromFindManyCacheRootQuery'; @@ -37,10 +36,6 @@ export const useRecordShowPagePagination = ( const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); - const recordGqlFields = generateDepthOneRecordGqlFields({ - objectMetadataItem, - }); - const { filter, orderBy } = useQueryVariablesFromActiveFieldsOfViewOrDefaultView({ objectMetadataItem, @@ -55,7 +50,7 @@ export const useRecordShowPagePagination = ( orderBy, limit: 1, objectNameSingular, - recordGqlFields, + recordGqlFields: { id: true }, }); const cursorFromRequest = currentRecordsPageInfo?.endCursor; @@ -77,7 +72,7 @@ export const useRecordShowPagePagination = ( } : undefined, objectNameSingular, - recordGqlFields, + recordGqlFields: { id: true }, onCompleted: (_, pagination) => { setTotalCountBefore(pagination?.totalCount ?? 0); }, @@ -97,7 +92,7 @@ export const useRecordShowPagePagination = ( } : undefined, objectNameSingular, - recordGqlFields, + recordGqlFields: { id: true }, onCompleted: (_, pagination) => { setTotalCountAfter(pagination?.totalCount ?? 0); }, diff --git a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx index 981afe71f..ffad33185 100644 --- a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx +++ b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx @@ -3,10 +3,10 @@ import { useRecoilValue } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { Favorite } from '@/favorites/types/Favorite'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCombinedFindManyRecords } from '@/object-record/multiple-objects/hooks/useCombinedFindManyRecords'; +import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery'; -import { FIND_ALL_FAVORITES_OPERATION_SIGNATURE } from '@/prefetch/query-keys/FindAllFavoritesOperationSignature'; -import { FIND_ALL_VIEWS_OPERATION_SIGNATURE } from '@/prefetch/query-keys/FindAllViewsOperationSignature'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { View } from '@/views/types/View'; import { isDefined } from '~/utils/isDefined'; @@ -24,11 +24,20 @@ export const PrefetchRunQueriesEffect = () => { prefetchKey: PrefetchKey.AllFavorites, }); + const { objectMetadataItems } = useObjectMetadataItems(); + + const operationSignatures = Object.values(PREFETCH_CONFIG).map( + ({ objectNameSingular, operationSignatureFactory }) => { + const objectMetadataItem = objectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + return operationSignatureFactory({ objectMetadataItem }); + }, + ); + const { result } = useCombinedFindManyRecords({ - operationSignatures: [ - FIND_ALL_VIEWS_OPERATION_SIGNATURE, - FIND_ALL_FAVORITES_OPERATION_SIGNATURE, - ], + operationSignatures, skip: !currentUser, }); diff --git a/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts index e23d51f68..f76ba6637 100644 --- a/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts +++ b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts @@ -1,10 +1,22 @@ -import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; -import { FIND_ALL_FAVORITES_OPERATION_SIGNATURE } from '@/prefetch/query-keys/FindAllFavoritesOperationSignature'; -import { FIND_ALL_VIEWS_OPERATION_SIGNATURE } from '@/prefetch/query-keys/FindAllViewsOperationSignature'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; +import { findAllFavoritesOperationSignatureFactory } from '@/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory'; +import { findAllViewsOperationSignatureFactory } from '@/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -export const PREFETCH_CONFIG: Record = +export const PREFETCH_CONFIG: Record< + PrefetchKey, { - ALL_VIEWS: FIND_ALL_VIEWS_OPERATION_SIGNATURE, - ALL_FAVORITES: FIND_ALL_FAVORITES_OPERATION_SIGNATURE, - }; + objectNameSingular: CoreObjectNameSingular; + operationSignatureFactory: RecordGqlOperationSignatureFactory; + } +> = { + ALL_VIEWS: { + objectNameSingular: CoreObjectNameSingular.View, + operationSignatureFactory: findAllViewsOperationSignatureFactory, + }, + ALL_FAVORITES: { + objectNameSingular: CoreObjectNameSingular.Favorite, + operationSignatureFactory: findAllFavoritesOperationSignatureFactory, + }, +}; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts index 855b337ea..a2b18f3ca 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts @@ -2,7 +2,6 @@ import { useSetRecoilState } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; -import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; @@ -18,10 +17,16 @@ export const usePrefetchRunQuery = ({ const setPrefetchDataIsLoaded = useSetRecoilState( prefetchIsLoadedFamilyState(prefetchKey), ); + + const { operationSignatureFactory, objectNameSingular } = + PREFETCH_CONFIG[prefetchKey]; + const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: PREFETCH_CONFIG[prefetchKey].objectNameSingular, + objectNameSingular, }); + const operationSignature = operationSignatureFactory({ objectMetadataItem }); + const { upsertFindManyRecordsQueryInCache } = useUpsertFindManyRecordsQueryInCache({ objectMetadataItem: objectMetadataItem, @@ -30,10 +35,8 @@ export const usePrefetchRunQuery = ({ const upsertRecordsInCache = (records: T[]) => { setPrefetchDataIsLoaded(false); upsertFindManyRecordsQueryInCache({ - queryVariables: PREFETCH_CONFIG[prefetchKey].variables, - recordGqlFields: - PREFETCH_CONFIG[prefetchKey].fields ?? - generateDepthOneRecordGqlFields({ objectMetadataItem }), + queryVariables: operationSignature.variables, + recordGqlFields: operationSignature.fields, objectRecordsToOverwrite: records, computeReferences: false, }); diff --git a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts index 5db8f0d07..b951dc417 100644 --- a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts +++ b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts @@ -2,7 +2,6 @@ import { useRecoilValue } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; @@ -17,21 +16,18 @@ export const usePrefetchedData = ( prefetchIsLoadedFamilyState(prefetchKey), ); - const prefetchQueryKey = PREFETCH_CONFIG[prefetchKey]; + const { operationSignatureFactory, objectNameSingular } = + PREFETCH_CONFIG[prefetchKey]; const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: prefetchQueryKey.objectNameSingular, + objectNameSingular, }); const { records } = useFindManyRecords({ skip: !isDataPrefetched, - objectNameSingular: prefetchQueryKey.objectNameSingular, + objectNameSingular: objectNameSingular, recordGqlFields: - prefetchQueryKey.fields ?? - generateDepthOneRecordGqlFields({ - objectMetadataItem, - }), - filter, + operationSignatureFactory({ objectMetadataItem }).fields ?? filter, }); return { diff --git a/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts new file mode 100644 index 000000000..a763710a0 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts @@ -0,0 +1,15 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; +import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; + +export const findAllFavoritesOperationSignatureFactory: RecordGqlOperationSignatureFactory = + ({ objectMetadataItem }: { objectMetadataItem: ObjectMetadataItem }) => ({ + objectNameSingular: CoreObjectNameSingular.Favorite, + variables: {}, + fields: { + ...generateDepthOneRecordGqlFields({ + objectMetadataItem, + }), + }, + }); diff --git a/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts new file mode 100644 index 000000000..3a4430a2c --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts @@ -0,0 +1,24 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; + +export const findAllViewsOperationSignatureFactory: RecordGqlOperationSignatureFactory = + () => ({ + objectNameSingular: CoreObjectNameSingular.View, + variables: {}, + fields: { + id: true, + createdAt: true, + updatedAt: true, + isCompact: true, + objectMetadataId: true, + position: true, + type: true, + kanbanFieldMetadataId: true, + name: true, + icon: true, + key: true, + viewFilters: true, + viewSorts: true, + viewFields: true, + }, + }); diff --git a/packages/twenty-front/src/modules/prefetch/query-keys/FindAllFavoritesOperationSignature.ts b/packages/twenty-front/src/modules/prefetch/query-keys/FindAllFavoritesOperationSignature.ts deleted file mode 100644 index 32364260b..000000000 --- a/packages/twenty-front/src/modules/prefetch/query-keys/FindAllFavoritesOperationSignature.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; - -export const FIND_ALL_FAVORITES_OPERATION_SIGNATURE: RecordGqlOperationSignature = - { - objectNameSingular: CoreObjectNameSingular.Favorite, - variables: {}, - }; diff --git a/packages/twenty-front/src/modules/prefetch/query-keys/FindAllViewsOperationSignature.ts b/packages/twenty-front/src/modules/prefetch/query-keys/FindAllViewsOperationSignature.ts deleted file mode 100644 index a0a14f3d5..000000000 --- a/packages/twenty-front/src/modules/prefetch/query-keys/FindAllViewsOperationSignature.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; - -export const FIND_ALL_VIEWS_OPERATION_SIGNATURE: RecordGqlOperationSignature = { - objectNameSingular: CoreObjectNameSingular.View, - variables: {}, - fields: { - id: true, - createdAt: true, - updatedAt: true, - isCompact: true, - objectMetadataId: true, - position: true, - type: true, - kanbanFieldMetadataId: true, - name: true, - icon: true, - key: true, - viewFilters: true, - viewSorts: true, - viewFields: true, - }, -}; diff --git a/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts b/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts index b06cf94a6..38069ee05 100644 --- a/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts +++ b/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts @@ -6,5 +6,5 @@ export const prefetchIsLoadedFamilyState = createFamilyState< PrefetchKey >({ key: 'prefetchIsLoadedFamilyState', - defaultValue: true, + defaultValue: false, }); diff --git a/packages/twenty-front/src/modules/ui/layout/states/ShowPageRecoilScopeContext.ts b/packages/twenty-front/src/modules/ui/layout/states/ShowPageRecoilScopeContext.ts deleted file mode 100644 index f7d666788..000000000 --- a/packages/twenty-front/src/modules/ui/layout/states/ShowPageRecoilScopeContext.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { createContext } from 'react'; - -/* istanbul ignore next */ -export const ShowPageRecoilScopeContext = createContext(null); diff --git a/packages/twenty-server/src/command/command-logger.ts b/packages/twenty-server/src/command/command-logger.ts index 5936c6421..f05a4e930 100644 --- a/packages/twenty-server/src/command/command-logger.ts +++ b/packages/twenty-server/src/command/command-logger.ts @@ -1,3 +1,4 @@ +/* eslint-disable no-console */ import { Injectable } from '@nestjs/common'; import { existsSync } from 'fs'; diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index b701093e7..af786fd2b 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -1,3 +1,5 @@ +import { Logger } from '@nestjs/common'; + import { Command, CommandRunner } from 'nest-commander'; import { EntityManager } from 'typeorm'; @@ -43,6 +45,7 @@ import { WorkspaceSyncMetadataService } from 'src/engine/workspace-manager/works }) export class DataSeedWorkspaceCommand extends CommandRunner { workspaceIds = [SEED_APPLE_WORKSPACE_ID, SEED_TWENTY_WORKSPACE_ID]; + private readonly logger = new Logger(DataSeedWorkspaceCommand.name); constructor( private readonly dataSourceService: DataSourceService, @@ -86,7 +89,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner { }); } } catch (error) { - console.error(error); + this.logger.error(error); return; } @@ -197,7 +200,7 @@ export class DataSeedWorkspaceCommand extends CommandRunner { }, ); } catch (error) { - console.error(error); + this.logger.error(error); } await this.typeORMService.disconnectFromDataSource(dataSourceMetadata.id); diff --git a/packages/twenty-server/src/database/commands/database-command.module.ts b/packages/twenty-server/src/database/commands/database-command.module.ts index c12d651e5..b06b92e6c 100644 --- a/packages/twenty-server/src/database/commands/database-command.module.ts +++ b/packages/twenty-server/src/database/commands/database-command.module.ts @@ -9,7 +9,6 @@ import { DataSeedWorkspaceCommand } from 'src/database/commands/data-seed-dev-wo import { ConfirmationQuestion } from 'src/database/commands/questions/confirmation.question'; import { UpgradeTo0_23CommandModule } from 'src/database/commands/upgrade-version/0-23/0-23-upgrade-version.module'; import { UpgradeVersionModule } from 'src/database/commands/upgrade-version/upgrade-version.module'; -import { WorkspaceAddTotalCountCommand } from 'src/database/commands/workspace-add-total-count.command'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; @@ -51,7 +50,6 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp providers: [ DataSeedWorkspaceCommand, DataSeedDemoWorkspaceCommand, - WorkspaceAddTotalCountCommand, ConfirmationQuestion, StartDataSeedDemoWorkspaceCronCommand, StopDataSeedDemoWorkspaceCronCommand, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts index 2d38b76a3..7b44ddf54 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-23/0-23-backfill-new-onboarding-user-vars.ts @@ -5,7 +5,6 @@ import chalk from 'chalk'; import { Command, CommandRunner, Option } from 'nest-commander'; import { Repository } from 'typeorm'; -import { UpdateFileFolderStructureCommand } from 'src/database/commands/upgrade-version/0-23/0-23-update-file-folder-structure.command'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { Workspace, @@ -21,7 +20,9 @@ interface BackfillNewOnboardingUserVarsCommandOptions { description: 'Backfill new onboarding user vars for existing workspaces', }) export class BackfillNewOnboardingUserVarsCommand extends CommandRunner { - private readonly logger = new Logger(UpdateFileFolderStructureCommand.name); + private readonly logger = new Logger( + BackfillNewOnboardingUserVarsCommand.name, + ); constructor( @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, @@ -43,22 +44,13 @@ export class BackfillNewOnboardingUserVarsCommand extends CommandRunner { _passedParam: string[], options: BackfillNewOnboardingUserVarsCommandOptions, ): Promise { - let workspaces; - - if (options.workspaceId) { - workspaces = await this.workspaceRepository.find({ - where: { - activationStatus: WorkspaceActivationStatus.ACTIVE, - id: options.workspaceId, - }, - relations: ['users'], - }); - } else { - workspaces = await this.workspaceRepository.find({ - where: { activationStatus: WorkspaceActivationStatus.ACTIVE }, - relations: ['users'], - }); - } + const workspaces = await this.workspaceRepository.find({ + where: { + activationStatus: WorkspaceActivationStatus.PENDING_CREATION, + ...(options.workspaceId && { id: options.workspaceId }), + }, + relations: ['users'], + }); if (!workspaces.length) { this.logger.log(chalk.yellow('No workspace found')); @@ -75,19 +67,19 @@ export class BackfillNewOnboardingUserVarsCommand extends CommandRunner { chalk.green(`Running command on workspace ${workspace.id}`), ); - await this.onboardingService.toggleOnboardingInviteTeamCompletion({ + await this.onboardingService.setOnboardingInviteTeamPending({ workspaceId: workspace.id, value: true, }); for (const user of workspace.users) { - await this.onboardingService.toggleOnboardingConnectAccountCompletion({ + await this.onboardingService.setOnboardingCreateProfileCompletion({ userId: user.id, workspaceId: workspace.id, value: true, }); - await this.onboardingService.toggleOnboardingCreateProfileCompletion({ + await this.onboardingService.setOnboardingConnectAccountPending({ userId: user.id, workspaceId: workspace.id, value: true, diff --git a/packages/twenty-server/src/database/commands/workspace-add-total-count.command.ts b/packages/twenty-server/src/database/commands/workspace-add-total-count.command.ts deleted file mode 100644 index cdf8aa293..000000000 --- a/packages/twenty-server/src/database/commands/workspace-add-total-count.command.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Command, CommandRunner } from 'nest-commander'; -import chalk from 'chalk'; - -import { TypeORMService } from 'src/database/typeorm/typeorm.service'; - -@Command({ - name: 'workspace:add-total-count', - description: 'Add pg_graphql total count directive to all workspace tables', -}) -export class WorkspaceAddTotalCountCommand extends CommandRunner { - constructor(private readonly typeORMService: TypeORMService) { - super(); - } - - async run(): Promise { - const mainDataSource = this.typeORMService.getMainDataSource(); - - try { - await mainDataSource.query(` - DO $$ - DECLARE - schema_cursor CURSOR FOR SELECT schema_name FROM information_schema.schemata WHERE schema_name LIKE 'workspace_%'; - schema_name text; - table_rec record; - BEGIN - OPEN schema_cursor; - LOOP - FETCH schema_cursor INTO schema_name; - EXIT WHEN NOT FOUND; - - FOR table_rec IN SELECT t.table_name FROM information_schema.tables t WHERE t.table_schema = schema_name - LOOP - EXECUTE 'COMMENT ON TABLE ' || quote_ident(schema_name) || '.' || quote_ident(table_rec.table_name) || ' IS e''@graphql({"totalCount": {"enabled": true}})'';'; - END LOOP; - END LOOP; - CLOSE schema_cursor; - END $$; - `); - - console.log( - chalk.green('Total count directive added to all workspace tables'), - ); - } catch (error) { - console.log( - chalk.red('Error adding total count directive to all workspace tables'), - ); - } - } -} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum copy.ts similarity index 100% rename from packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum.ts rename to packages/twenty-server/src/database/typeorm/core/migrations/1722256203540-updateActivationStatusEnum copy.ts diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1722256203541-updateActivationStatusEnumPendingCreation.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203541-updateActivationStatusEnumPendingCreation.ts new file mode 100644 index 000000000..dea97eb77 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1722256203541-updateActivationStatusEnumPendingCreation.ts @@ -0,0 +1,69 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateActivationStatusEnumPendingCreationStatus1722256203541 + implements MigrationInterface +{ + name = 'UpdateActivationStatusEnumPendingCreationStatus1722256203541'; + + public async up(queryRunner: QueryRunner): Promise { + // Set current column as text + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE text USING "activationStatus"::text`, + ); + + // Drop default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + + // Drop the old enum type + await queryRunner.query( + `DROP TYPE "core"."workspace_activationStatus_enum"`, + ); + + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationStatus_enum" AS ENUM('PENDING_CREATION', 'ONGOING_CREATION', 'ACTIVE', 'INACTIVE')`, + ); + + // Re-apply the enum type + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE "core"."workspace_activationStatus_enum" USING "activationStatus"::"core"."workspace_activationStatus_enum"`, + ); + + // Update default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + // Set current column as text + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE text USING "activationStatus"::text`, + ); + + // Drop default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" DROP DEFAULT`, + ); + + // Drop the old enum type + await queryRunner.query( + `DROP TYPE "core"."workspace_activationStatus_enum"`, + ); + + await queryRunner.query( + `CREATE TYPE "core"."workspace_activationStatus_enum" AS ENUM('PENDING_CREATION', 'ACTIVE', 'INACTIVE')`, + ); + + // Re-apply the enum type + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DATA TYPE "core"."workspace_activationStatus_enum" USING "activationStatus"::"core"."workspace_activationStatus_enum"`, + ); + + // Update default value + await queryRunner.query( + `ALTER TABLE "core"."workspace" ALTER COLUMN "activationStatus" SET DEFAULT 'INACTIVE'`, + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts index 465f9a0a4..f487b3b25 100644 --- a/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai-sql-query/ai-sql-query.service.ts @@ -1,22 +1,22 @@ import { Injectable, Logger } from '@nestjs/common'; -import { RunnableSequence } from '@langchain/core/runnables'; import { StructuredOutputParser } from '@langchain/core/output_parsers'; +import { RunnableSequence } from '@langchain/core/runnables'; +import groupBy from 'lodash.groupby'; import { DataSource, QueryFailedError } from 'typeorm'; +import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; -import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions'; -import groupBy from 'lodash.groupby'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { sqlGenerationPromptTemplate } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates'; +import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto'; import { LLMChatModelService } from 'src/engine/integrations/llm-chat-model/llm-chat-model.service'; import { LLMTracingService } from 'src/engine/integrations/llm-tracing/llm-tracing.service'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { StandardObjectFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-object.factory'; -import { AISQLQueryResult } from 'src/engine/core-modules/ai-sql-query/dtos/ai-sql-query-result.dto'; -import { sqlGenerationPromptTemplate } from 'src/engine/core-modules/ai-sql-query/ai-sql-query.prompt-templates'; @Injectable() export class AISQLQueryService { @@ -31,9 +31,9 @@ export class AISQLQueryService { private getLabelIdentifierName( objectMetadata: ObjectMetadataEntity, - dataSourceId, - workspaceId, - workspaceFeatureFlagsMap, + _dataSourceId, + _workspaceId, + _workspaceFeatureFlagsMap, ): string | undefined { const customObjectLabelIdentifierFieldMetadata = objectMetadata.fields.find( (fieldMetadata) => diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 3527d6652..2dff70629 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -93,10 +93,10 @@ export class GoogleAPIsAuthController { workspaceId, ); - await onboardingServiceInstance.toggleOnboardingConnectAccountCompletion({ + await onboardingServiceInstance.setOnboardingConnectAccountPending({ userId, workspaceId, - value: true, + value: false, }); } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts index 7313ebf0f..e3bb7af39 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts @@ -1,15 +1,14 @@ +import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; -import { HttpService } from '@nestjs/axios'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; -import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; +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 { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; describe('SignInUpService', () => { let service: SignInUpService; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 0cc3f2f03..839a69427 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -175,6 +175,18 @@ export class SignInUpService { await this.userWorkspaceService.create(user.id, workspace.id); await this.userWorkspaceService.createWorkspaceMember(workspace.id, user); + await this.onboardingService.setOnboardingConnectAccountPending({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); + + await this.onboardingService.setOnboardingCreateProfileCompletion({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); + return user; } @@ -222,13 +234,22 @@ export class SignInUpService { await this.userWorkspaceService.create(user.id, workspace.id); - if (user.firstName !== '' || user.lastName === '') { - await this.onboardingService.toggleOnboardingCreateProfileCompletion({ - userId: user.id, - workspaceId: workspace.id, - value: true, - }); - } + await this.onboardingService.setOnboardingConnectAccountPending({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); + + await this.onboardingService.setOnboardingCreateProfileCompletion({ + userId: user.id, + workspaceId: workspace.id, + value: true, + }); + + await this.onboardingService.setOnboardingInviteTeamPending({ + workspaceId: workspace.id, + value: true, + }); return user; } diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index cb66d9109..c1c6760df 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -9,13 +9,16 @@ import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/ import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingWebhookService } from 'src/engine/core-modules/billing/services/billing-webhook.service'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Module({ imports: [ + FeatureFlagModule, StripeModule, UserWorkspaceModule, TypeOrmModule.forFeature( @@ -35,11 +38,13 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; BillingPortalWorkspaceService, BillingResolver, BillingWorkspaceMemberListener, + BillingService, ], exports: [ BillingSubscriptionService, BillingPortalWorkspaceService, BillingWebhookService, + BillingService, ], }) export class BillingModule {} diff --git a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts index 509d5250b..5199137d7 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/entities/billing-subscription.entity.ts @@ -1,5 +1,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import Stripe from 'stripe'; import { Column, CreateDateColumn, @@ -11,12 +13,10 @@ import { Relation, UpdateDateColumn, } from 'typeorm'; -import Stripe from 'stripe'; -import { IDField } from '@ptc-org/nestjs-query-graphql'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; export enum SubscriptionStatus { Active = 'active', @@ -76,7 +76,7 @@ export class BillingSubscription { enum: Object.values(SubscriptionStatus), nullable: false, }) - status: Stripe.Subscription.Status; + status: SubscriptionStatus; @Field(() => SubscriptionInterval, { nullable: true }) @Column({ diff --git a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts index 14929639f..45d723329 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/jobs/update-subscription.job.ts @@ -33,7 +33,7 @@ export class UpdateSubscriptionJob { try { const billingSubscriptionItem = - await this.billingSubscriptionService.getCurrentBillingSubscriptionItem( + await this.billingSubscriptionService.getCurrentBillingSubscriptionItemOrThrow( data.workspaceId, ); diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index 506a97dd7..ca4685f07 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -70,12 +70,18 @@ export class BillingPortalWorkspaceService { returnUrlPath?: string, ) { const currentSubscriptionItem = - await this.billingSubscriptionService.getCurrentBillingSubscription({ - workspaceId, - }); + await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( + { + workspaceId, + }, + ); const stripeCustomerId = currentSubscriptionItem.stripeCustomerId; + if (!stripeCustomerId) { + throw new Error('Error: missing stripeCustomerId'); + } + const frontBaseUrl = this.environmentService.get('FRONT_BASE_URL'); const returnUrl = returnUrlPath ? frontBaseUrl + returnUrlPath diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts index 9b0575e87..15fb1df71 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-subscription.service.ts @@ -79,7 +79,7 @@ export class BillingSubscriptionService { ); } - async getCurrentBillingSubscription(criteria: { + async getCurrentBillingSubscriptionOrThrow(criteria: { workspaceId?: string; stripeCustomerId?: string; }) { @@ -97,21 +97,15 @@ export class BillingSubscriptionService { return notCanceledSubscriptions?.[0]; } - async getCurrentBillingSubscriptionItem( + async getCurrentBillingSubscriptionItemOrThrow( workspaceId: string, stripeProductId = this.environmentService.get( 'BILLING_STRIPE_BASE_PLAN_PRODUCT_ID', ), ) { - const billingSubscription = await this.getCurrentBillingSubscription({ - workspaceId, - }); - - if (!billingSubscription) { - throw new Error( - `Cannot find billingSubscriptionItem for product ${stripeProductId} for workspace ${workspaceId}`, - ); - } + const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( + { workspaceId }, + ); const billingSubscriptionItem = billingSubscription.billingSubscriptionItems.filter( @@ -129,9 +123,10 @@ export class BillingSubscriptionService { } async deleteSubscription(workspaceId: string) { - const subscriptionToCancel = await this.getCurrentBillingSubscription({ - workspaceId, - }); + const subscriptionToCancel = + await this.getCurrentBillingSubscriptionOrThrow({ + workspaceId, + }); if (subscriptionToCancel) { await this.stripeService.cancelSubscription( @@ -142,9 +137,9 @@ export class BillingSubscriptionService { } async handleUnpaidInvoices(data: Stripe.SetupIntentSucceededEvent.Data) { - const billingSubscription = await this.getCurrentBillingSubscription({ - stripeCustomerId: data.object.customer as string, - }); + const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( + { stripeCustomerId: data.object.customer as string }, + ); if (billingSubscription?.status === 'unpaid') { await this.stripeService.collectLastInvoice( @@ -154,9 +149,9 @@ export class BillingSubscriptionService { } async applyBillingSubscription(user: User) { - const billingSubscription = await this.getCurrentBillingSubscription({ - workspaceId: user.defaultWorkspaceId, - }); + const billingSubscription = await this.getCurrentBillingSubscriptionOrThrow( + { workspaceId: user.defaultWorkspaceId }, + ); const newInterval = billingSubscription?.interval === SubscriptionInterval.Year @@ -164,7 +159,9 @@ export class BillingSubscriptionService { : SubscriptionInterval.Year; const billingSubscriptionItem = - await this.getCurrentBillingSubscriptionItem(user.defaultWorkspaceId); + await this.getCurrentBillingSubscriptionItemOrThrow( + user.defaultWorkspaceId, + ); const productPrice = await this.stripeService.getStripePrice( AvailableProduct.BasePlan, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts index 347eea6de..7d1b5df5a 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-webhook.service.ts @@ -46,7 +46,7 @@ export class BillingWebhookService { workspaceId: workspaceId, stripeCustomerId: data.object.customer as string, stripeSubscriptionId: data.object.id, - status: data.object.status, + status: data.object.status as SubscriptionStatus, interval: data.object.items.data[0].plan.interval, }, { diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts new file mode 100644 index 000000000..e86895f9f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing.service.ts @@ -0,0 +1,55 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { isDefined } from 'class-validator'; + +import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; + +@Injectable() +export class BillingService { + protected readonly logger = new Logger(BillingService.name); + constructor( + private readonly environmentService: EnvironmentService, + private readonly billingSubscriptionService: BillingSubscriptionService, + private readonly isFeatureEnabledService: IsFeatureEnabledService, + ) {} + + isBillingEnabled() { + return this.environmentService.get('IS_BILLING_ENABLED'); + } + + async hasWorkspaceActiveSubscriptionOrFreeAccess(workspaceId: string) { + const isBillingEnabled = this.isBillingEnabled(); + + if (!isBillingEnabled) { + return true; + } + + const isFreeAccessEnabled = + await this.isFeatureEnabledService.isFeatureEnabled( + FeatureFlagKey.IsFreeAccessEnabled, + workspaceId, + ); + + if (isFreeAccessEnabled) { + return true; + } + + const currentBillingSubscription = + await this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( + { workspaceId }, + ); + + return ( + isDefined(currentBillingSubscription) && + [ + SubscriptionStatus.Active, + SubscriptionStatus.Trialing, + SubscriptionStatus.PastDue, + ].includes(currentBillingSubscription.status) + ); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts index c98e8c53b..7dba53b26 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.service.ts @@ -141,28 +141,30 @@ export class StripeService { ); } - formatProductPrices(prices: Stripe.Price[]) { - const result: Record = {}; + formatProductPrices(prices: Stripe.Price[]): ProductPriceEntity[] { + const productPrices: ProductPriceEntity[] = Object.values( + prices + .filter((item) => item.recurring?.interval && item.unit_amount) + .reduce((acc, item: Stripe.Price) => { + const interval = item.recurring?.interval; - prices.forEach((item) => { - const interval = item.recurring?.interval; + if (!interval || !item.unit_amount) { + return acc; + } - if (!interval || !item.unit_amount) { - return; - } - if ( - !result[interval] || - item.created > (result[interval]?.created || 0) - ) { - result[interval] = { - unitAmount: item.unit_amount, - recurringInterval: interval, - created: item.created, - stripePriceId: item.id, - }; - } - }); + if (!acc[interval] || item.created > acc[interval].created) { + acc[interval] = { + unitAmount: item.unit_amount, + recurringInterval: interval, + created: item.created, + stripePriceId: item.id, + }; + } - return Object.values(result).sort((a, b) => a.unitAmount - b.unitAmount); + return acc satisfies Record; + }, {}), + ); + + return productPrices.sort((a, b) => a.unitAmount - b.unitAmount); } } diff --git a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts index b17f04fef..585551291 100644 --- a/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/calendar/timeline-calendar-event.resolver.ts @@ -1,16 +1,13 @@ import { UseGuards } from '@nestjs/common'; -import { Query, Args, ArgsType, Field, Int, Resolver } from '@nestjs/graphql'; +import { Args, ArgsType, Field, Int, Query, Resolver } from '@nestjs/graphql'; import { Max } from 'class-validator'; -import { User } from 'src/engine/core-modules/user/user.entity'; -import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; -import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { TIMELINE_CALENDAR_EVENTS_MAX_PAGE_SIZE } from 'src/engine/core-modules/calendar/constants/calendar.constants'; import { TimelineCalendarEventsWithTotal } from 'src/engine/core-modules/calendar/dtos/timeline-calendar-events-with-total.dto'; import { TimelineCalendarEventService } from 'src/engine/core-modules/calendar/timeline-calendar-event.service'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; -import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard'; @ArgsType() class GetTimelineCalendarEventsFromPersonIdArgs { @@ -43,12 +40,10 @@ class GetTimelineCalendarEventsFromCompanyIdArgs { export class TimelineCalendarEventResolver { constructor( private readonly timelineCalendarEventService: TimelineCalendarEventService, - private readonly userService: UserService, ) {} @Query(() => TimelineCalendarEventsWithTotal) async getTimelineCalendarEventsFromPersonId( - @AuthUser() user: User, @Args() { personId, page, pageSize }: GetTimelineCalendarEventsFromPersonIdArgs, ) { @@ -64,7 +59,6 @@ export class TimelineCalendarEventResolver { @Query(() => TimelineCalendarEventsWithTotal) async getTimelineCalendarEventsFromCompanyId( - @AuthUser() user: User, @Args() { companyId, page, pageSize }: GetTimelineCalendarEventsFromCompanyIdArgs, ) { diff --git a/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts b/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts index 43a4ea2cb..79ebee2c8 100644 --- a/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts +++ b/packages/twenty-server/src/engine/core-modules/jwt/services/jwt-wrapper.service.ts @@ -7,7 +7,12 @@ import * as jwt from 'jsonwebtoken'; export class JwtWrapperService { constructor(private readonly jwtService: JwtService) {} - sign(payload: string, options?: JwtSignOptions): string { + sign(payload: string | object, options?: JwtSignOptions): string { + // Typescript does not handle well the overloads of the sign method, helping it a little bit + if (typeof payload === 'object') { + return this.jwtService.sign(payload, options); + } + return this.jwtService.sign(payload, options); } diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts index 97fe28e50..fcc0a89a7 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.resolver.ts @@ -19,10 +19,10 @@ export class OnboardingResolver { @AuthUser() user: User, @AuthWorkspace() workspace: Workspace, ): Promise { - await this.onboardingService.toggleOnboardingConnectAccountCompletion({ + await this.onboardingService.setOnboardingConnectAccountPending({ userId: user.id, workspaceId: workspace.id, - value: true, + value: false, }); return { success: true }; diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts index 7c2fa2406..cd4adb8e0 100644 --- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts +++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts @@ -1,66 +1,40 @@ import { Injectable } from '@nestjs/common'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; -import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { IsFeatureEnabledService } from 'src/engine/core-modules/feature-flag/services/is-feature-enabled.service'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum'; import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceActivationStatus } from 'src/engine/core-modules/workspace/workspace.entity'; -import { EnvironmentService } from 'src/engine/integrations/environment/environment.service'; -import { isDefined } from 'src/utils/is-defined'; export enum OnboardingStepKeys { - ONBOARDING_CONNECT_ACCOUNT_COMPLETE = 'ONBOARDING_CONNECT_ACCOUNT_COMPLETE', - ONBOARDING_INVITE_TEAM_COMPLETE = 'ONBOARDING_INVITE_TEAM_COMPLETE', - ONBOARDING_CREATE_PROFILE_COMPLETE = 'ONBOARDING_CREATE_PROFILE_COMPLETE', + ONBOARDING_CONNECT_ACCOUNT_PENDING = 'ONBOARDING_CONNECT_ACCOUNT_PENDING', + ONBOARDING_INVITE_TEAM_PENDING = 'ONBOARDING_INVITE_TEAM_PENDING', + ONBOARDING_CREATE_PROFILE_PENDING = 'ONBOARDING_CREATE_PROFILE_PENDING', } export type OnboardingKeyValueTypeMap = { - [OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE]: boolean; - [OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE]: boolean; - [OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE]: boolean; + [OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING]: boolean; + [OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING]: boolean; + [OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING]: boolean; }; @Injectable() export class OnboardingService { constructor( - private readonly billingSubscriptionService: BillingSubscriptionService, - private readonly environmentService: EnvironmentService, - private readonly isFeatureEnabledService: IsFeatureEnabledService, + private readonly billingService: BillingService, private readonly userVarsService: UserVarsService, ) {} private async isSubscriptionIncompleteOnboardingStatus(user: User) { - const isBillingEnabled = this.environmentService.get('IS_BILLING_ENABLED'); - - if (!isBillingEnabled) { - return false; - } - - const isFreeAccessEnabled = - await this.isFeatureEnabledService.isFeatureEnabled( - FeatureFlagKey.IsFreeAccessEnabled, + const hasSubscription = + await this.billingService.hasWorkspaceActiveSubscriptionOrFreeAccess( user.defaultWorkspaceId, ); - if (isFreeAccessEnabled) { - return false; - } - - const currentBillingSubscription = - await this.billingSubscriptionService.getCurrentBillingSubscription({ - workspaceId: user.defaultWorkspaceId, - }); - - return ( - !isDefined(currentBillingSubscription) || - currentBillingSubscription?.status === SubscriptionStatus.Incomplete - ); + return !hasSubscription; } - private async isWorkspaceActivationOnboardingStatus(user: User) { + private isWorkspaceActivationPending(user: User) { return ( user.defaultWorkspace.activationStatus === WorkspaceActivationStatus.PENDING_CREATION @@ -72,7 +46,7 @@ export class OnboardingService { return OnboardingStatus.PLAN_REQUIRED; } - if (await this.isWorkspaceActivationOnboardingStatus(user)) { + if (this.isWorkspaceActivationPending(user)) { return OnboardingStatus.WORKSPACE_ACTIVATION; } @@ -81,33 +55,33 @@ export class OnboardingService { workspaceId: user.defaultWorkspaceId, }); - const isProfileCreationComplete = - userVars.get(OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE) === + const isProfileCreationPending = + userVars.get(OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING) === true; - const isConnectAccountComplete = - userVars.get(OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE) === + const isConnectAccountPending = + userVars.get(OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING) === true; - const isInviteTeamComplete = - userVars.get(OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE) === true; + const isInviteTeamPending = + userVars.get(OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING) === true; - if (!isProfileCreationComplete) { + if (isProfileCreationPending) { return OnboardingStatus.PROFILE_CREATION; } - if (!isConnectAccountComplete) { + if (isConnectAccountPending) { return OnboardingStatus.SYNC_EMAIL; } - if (!isInviteTeamComplete) { + if (isInviteTeamPending) { return OnboardingStatus.INVITE_TEAM; } return OnboardingStatus.COMPLETED; } - async toggleOnboardingConnectAccountCompletion({ + async setOnboardingConnectAccountPending({ userId, workspaceId, value, @@ -116,29 +90,48 @@ export class OnboardingService { workspaceId: string; value: boolean; }) { + if (!value) { + await this.userVarsService.delete({ + userId, + workspaceId, + key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING, + }); + + return; + } + await this.userVarsService.set({ userId, workspaceId: workspaceId, - key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_COMPLETE, - value, + key: OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING, + value: true, }); } - async toggleOnboardingInviteTeamCompletion({ + async setOnboardingInviteTeamPending({ workspaceId, value, }: { workspaceId: string; value: boolean; }) { + if (!value) { + await this.userVarsService.delete({ + workspaceId, + key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING, + }); + + return; + } + await this.userVarsService.set({ workspaceId, - key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_COMPLETE, - value, + key: OnboardingStepKeys.ONBOARDING_INVITE_TEAM_PENDING, + value: true, }); } - async toggleOnboardingCreateProfileCompletion({ + async setOnboardingCreateProfileCompletion({ userId, workspaceId, value, @@ -147,11 +140,21 @@ export class OnboardingService { workspaceId: string; value: boolean; }) { + if (!value) { + await this.userVarsService.delete({ + userId, + workspaceId, + key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING, + }); + + return; + } + await this.userVarsService.set({ userId, workspaceId, - key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_COMPLETE, - value, + key: OnboardingStepKeys.ONBOARDING_CREATE_PROFILE_PENDING, + value: true, }); } } 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 b31b8a2f9..4e57e1d2b 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 @@ -47,6 +47,32 @@ export class WorkspaceService extends TypeOrmQueryService { throw new BadRequestException("'displayName' not provided"); } + const existingWorkspace = await this.workspaceRepository.findOneBy({ + id: user.defaultWorkspace.id, + }); + + if (!existingWorkspace) { + throw new Error('Workspace not found'); + } + + if ( + existingWorkspace.activationStatus === + WorkspaceActivationStatus.ONGOING_CREATION + ) { + throw new Error('Workspace is already being created'); + } + + if ( + existingWorkspace.activationStatus !== + WorkspaceActivationStatus.PENDING_CREATION + ) { + throw new Error('Worspace is not pending creation'); + } + + await this.workspaceRepository.update(user.defaultWorkspace.id, { + activationStatus: WorkspaceActivationStatus.ONGOING_CREATION, + }); + await this.workspaceManagerService.init(user.defaultWorkspace.id); await this.userWorkspaceService.createWorkspaceMember( user.defaultWorkspace.id, @@ -142,9 +168,9 @@ export class WorkspaceService extends TypeOrmQueryService { }); } - await this.onboardingService.toggleOnboardingInviteTeamCompletion({ + await this.onboardingService.setOnboardingInviteTeamPending({ workspaceId: workspace.id, - value: true, + value: false, }); return { success: true }; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts index 324c0d929..fb8412d32 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace-workspace-member.listener.ts @@ -25,9 +25,6 @@ export class WorkspaceWorkspaceMemberListener { async handleUpdateEvent( payload: ObjectRecordUpdateEvent, ) { - const { firstName: firstNameBefore, lastName: lastNameBefore } = - payload.properties.before.name; - const { firstName: firstNameAfter, lastName: lastNameAfter } = payload.properties.after.name; @@ -39,10 +36,10 @@ export class WorkspaceWorkspaceMemberListener { return; } - await this.onboardingService.toggleOnboardingCreateProfileCompletion({ + await this.onboardingService.setOnboardingCreateProfileCompletion({ userId: payload.userId, workspaceId: payload.workspaceId, - value: true, + value: false, }); } 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 88a3bf7c8..0b4a3e048 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 @@ -21,6 +21,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; export enum WorkspaceActivationStatus { + ONGOING_CREATION = 'ONGOING_CREATION', PENDING_CREATION = 'PENDING_CREATION', ACTIVE = 'ACTIVE', INACTIVE = 'INACTIVE', 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 cc390dfa9..1bb019344 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 @@ -118,9 +118,9 @@ export class WorkspaceResolver { async currentBillingSubscription( @Parent() workspace: Workspace, ): Promise { - return this.billingSubscriptionService.getCurrentBillingSubscription({ - workspaceId: workspace.id, - }); + return this.billingSubscriptionService.getCurrentBillingSubscriptionOrThrow( + { workspaceId: workspace.id }, + ); } @ResolveField(() => Number) diff --git a/packages/twenty-server/src/engine/integrations/llm-tracing/drivers/console.driver.ts b/packages/twenty-server/src/engine/integrations/llm-tracing/drivers/console.driver.ts index 47e126324..6c1ae4015 100644 --- a/packages/twenty-server/src/engine/integrations/llm-tracing/drivers/console.driver.ts +++ b/packages/twenty-server/src/engine/integrations/llm-tracing/drivers/console.driver.ts @@ -1,6 +1,7 @@ +/* eslint-disable no-console */ import { BaseCallbackHandler } from '@langchain/core/callbacks/base'; -import { ConsoleCallbackHandler } from '@langchain/core/tracers/console'; import { Run } from '@langchain/core/tracers/base'; +import { ConsoleCallbackHandler } from '@langchain/core/tracers/console'; import { LLMTracingDriver } from 'src/engine/integrations/llm-tracing/drivers/interfaces/llm-tracing-driver.interface'; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts index e62b56ef9..0b8bdf7cf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-health/workspace-health.service.ts @@ -1,30 +1,32 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import { DataSource } from 'typeorm'; +import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; import { WorkspaceHealthIssue } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-issue.interface'; import { WorkspaceHealthMode, WorkspaceHealthOptions, } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-options.interface'; -import { WorkspaceHealthFixKind } from 'src/engine/workspace-manager/workspace-health/interfaces/workspace-health-fix-kind.interface'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; -import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; -import { RelationMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service'; -import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; -import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; +import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; +import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; +import { DatabaseStructureService } from 'src/engine/workspace-manager/workspace-health/services/database-structure.service'; +import { FieldMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/field-metadata-health.service'; +import { ObjectMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/object-metadata-health.service'; +import { RelationMetadataHealthService } from 'src/engine/workspace-manager/workspace-health/services/relation-metadata.health.service'; import { WorkspaceFixService } from 'src/engine/workspace-manager/workspace-health/services/workspace-fix.service'; +import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; @Injectable() export class WorkspaceHealthService { + private readonly logger = new Logger(WorkspaceHealthService.name); + constructor( @InjectDataSource('metadata') private readonly metadataDataSource: DataSource, @@ -188,7 +190,7 @@ export class WorkspaceHealthService { ); } catch (error) { await queryRunner.rollbackTransaction(); - console.error('Fix of issues failed with:', error); + this.logger.error('Fix of issues failed with:', error); } finally { await queryRunner.release(); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index e09a41d3a..d05ff13ea 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { QueryRunner, @@ -32,6 +32,8 @@ import { customTableDefaultColumns } from './utils/custom-table-default-column.u @Injectable() export class WorkspaceMigrationRunnerService { + private readonly logger = new Logger(WorkspaceMigrationRunnerService.name); + constructor( private readonly workspaceDataSourceService: WorkspaceDataSourceService, private readonly workspaceMigrationService: WorkspaceMigrationService, @@ -87,7 +89,7 @@ export class WorkspaceMigrationRunnerService { await queryRunner.commitTransaction(); } catch (error) { - console.error('Error executing migration', error); + this.logger.error('Error executing migration', error); await queryRunner.rollbackTransaction(); throw error; } finally { diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts index 64616dd13..dc2a92dd7 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook.ts @@ -4,10 +4,10 @@ import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-que import { FindOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; @WorkspaceQueryHook({ key: `calendarEvent.findOne`, @@ -39,13 +39,12 @@ export class CalendarEventFindOnePreQueryHook 'calendarChannelEventAssociation', ); - // TODO: Re-implement this using twenty ORM const calendarChannelCalendarEventAssociations = await calendarChannelEventAssociationRepository.find({ where: { calendarEventId: payload?.filter?.id?.eq, }, - relations: ['calendarChannel.connectedAccount'], + relations: ['calendarChannel', 'calendarChannel.connectedAccount'], }); if (calendarChannelCalendarEventAssociations.length === 0) { diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts index 1b083eaa4..041c3a648 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts @@ -1,15 +1,11 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import groupBy from 'lodash.groupby'; -import { Any } from 'typeorm'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; -import { - CalendarChannelVisibility, - CalendarChannelWorkspaceEntity, -} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; +import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; @@ -30,20 +26,9 @@ export class CanAccessCalendarEventService { workspaceId: string, calendarChannelCalendarEventAssociations: CalendarChannelEventAssociationWorkspaceEntity[], ) { - const calendarRepository = - await this.twentyORMManager.getRepository( - 'calendarChannel', - ); - - const calendarChannels = await calendarRepository.find({ - where: { - id: Any( - calendarChannelCalendarEventAssociations.map( - (association) => association.calendarChannel.id, - ), - ), - }, - }); + const calendarChannels = calendarChannelCalendarEventAssociations.map( + (association) => association.calendarChannel, + ); const calendarChannelsGroupByVisibility = groupBy( calendarChannels, diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts index a7dde2e18..506bf3d5a 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/listeners/message-participant-workspace-member.listener.ts @@ -1,6 +1,13 @@ import { Injectable } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { + Workspace, + WorkspaceActivationStatus, +} from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectRecordCreateEvent } from 'src/engine/integrations/event-emitter/types/object-record-create.event'; import { ObjectRecordUpdateEvent } from 'src/engine/integrations/event-emitter/types/object-record-update.event'; import { objectRecordChangedProperties as objectRecordUpdateEventChangedProperties } from 'src/engine/integrations/event-emitter/utils/object-record-changed-properties.util'; @@ -8,12 +15,12 @@ import { InjectMessageQueue } from 'src/engine/integrations/message-queue/decora import { MessageQueue } from 'src/engine/integrations/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/integrations/message-queue/services/message-queue.service'; import { - MessageParticipantMatchParticipantJobData, MessageParticipantMatchParticipantJob, + MessageParticipantMatchParticipantJobData, } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-match-participant.job'; import { - MessageParticipantUnmatchParticipantJobData, MessageParticipantUnmatchParticipantJob, + MessageParticipantUnmatchParticipantJobData, } from 'src/modules/messaging/message-participant-manager/jobs/message-participant-unmatch-participant.job'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -22,12 +29,25 @@ export class MessageParticipantWorkspaceMemberListener { constructor( @InjectMessageQueue(MessageQueue.messagingQueue) private readonly messageQueueService: MessageQueueService, + @InjectRepository(Workspace, 'core') + private readonly workspaceRepository: Repository, ) {} @OnEvent('workspaceMember.created') async handleCreatedEvent( payload: ObjectRecordCreateEvent, ) { + const workspace = await this.workspaceRepository.findOneBy({ + id: payload.workspaceId, + }); + + if ( + !workspace || + workspace.activationStatus !== WorkspaceActivationStatus.ACTIVE + ) { + return; + } + if (payload.properties.after.userEmail === null) { return; } diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts index d905c3d15..d31454791 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/message-participant-manager.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; @@ -22,7 +23,7 @@ import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-o @Module({ imports: [ - TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + TypeOrmModule.forFeature([FeatureFlagEntity, Workspace], 'core'), AnalyticsModule, ContactCreationManagerModule, WorkspaceDataSourceModule, diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index 548fbea14..142b000b9 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -13,6 +13,7 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator'; import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; @@ -90,6 +91,7 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { ], defaultValue: "'NEW'", }) + @WorkspaceIndex() stage: string; @WorkspaceField({ diff --git a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts index 1285f8ed7..159fb8622 100644 --- a/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts +++ b/packages/twenty-server/src/modules/workflow/common/workflow-common.module.ts @@ -1,4 +1,4 @@ -import { Global, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { WorkflowCommonService } from 'src/modules/workflow/common/workflow-common.services';