Compare commits

...

1 Commits

Author SHA1 Message Date
Charles Bochet f570b56b8b Fix performances on view loading (#14859)
We are experiencing bad performance on Twenty. One of the root cause
hypothesis is that computing `currentUser.currentWorkspace.views` is CPU
consuming. Without views, the GetCurrentUser response is ~1000 lines.
With its ~10000 lines.

As graphql is going through all fields recursively this can be quite
heavy on CPU. We had a similar issues on ObjectMetadataItems 2 years ago
and came with storing the response in redis.

Note: I thought there was also a cache in RAM but this is not the case,
so to invalidate the cache we can just empty redis.

- Extract getting all views from GetCurrentUser and update frontend to
perform both queries
- Add views to cached graphql operations
- invalidate the cache manually on view or related core entities update
/ create / delete / destroy

I have tested a lot on v1
2025-10-02 22:56:38 +02:00
26 changed files with 385 additions and 148 deletions
File diff suppressed because one or more lines are too long
@@ -4512,6 +4512,11 @@ export type UpdateCoreViewSortMutationVariables = Exact<{
export type UpdateCoreViewSortMutation = { __typename?: 'Mutation', updateCoreViewSort: { __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any } };
export type FindAllCoreViewsQueryVariables = Exact<{ [key: string]: never; }>;
export type FindAllCoreViewsQuery = { __typename?: 'Query', getCoreViews: Array<{ __typename?: 'CoreView', id: any, name: string, objectMetadataId: any, type: ViewType, key?: ViewKey | null, icon: string, position: number, isCompact: boolean, openRecordIn: ViewOpenRecordIn, kanbanAggregateOperation?: AggregateOperations | null, kanbanAggregateOperationFieldMetadataId?: any | null, anyFieldFilterValue?: string | null, calendarFieldMetadataId?: any | null, calendarLayout?: ViewCalendarLayout | null, viewFields: Array<{ __typename?: 'CoreViewField', id: any, fieldMetadataId: any, viewId: any, isVisible: boolean, position: number, size: number, aggregateOperation?: AggregateOperations | null, createdAt: string, updatedAt: string, deletedAt?: string | null }>, viewFilters: Array<{ __typename?: 'CoreViewFilter', id: any, fieldMetadataId: any, operand: ViewFilterOperand, value: any, viewFilterGroupId?: any | null, positionInViewFilterGroup?: number | null, subFieldName?: string | null, viewId: any }>, viewFilterGroups: Array<{ __typename?: 'CoreViewFilterGroup', id: any, parentViewFilterGroupId?: any | null, logicalOperator: ViewFilterGroupLogicalOperator, positionInViewFilterGroup?: number | null, viewId: any }>, viewSorts: Array<{ __typename?: 'CoreViewSort', id: any, fieldMetadataId: any, direction: ViewSortDirection, viewId: any }>, viewGroups: Array<{ __typename?: 'CoreViewGroup', id: any, fieldMetadataId: any, isVisible: boolean, fieldValue: string, position: number, viewId: any }> }> };
export type FindManyCoreViewFieldsQueryVariables = Exact<{
viewId: Scalars['String'];
}>;
@@ -5681,6 +5686,40 @@ export function useUpdateCoreViewSortMutation(baseOptions?: Apollo.MutationHookO
export type UpdateCoreViewSortMutationHookResult = ReturnType<typeof useUpdateCoreViewSortMutation>;
export type UpdateCoreViewSortMutationResult = Apollo.MutationResult<UpdateCoreViewSortMutation>;
export type UpdateCoreViewSortMutationOptions = Apollo.BaseMutationOptions<UpdateCoreViewSortMutation, UpdateCoreViewSortMutationVariables>;
export const FindAllCoreViewsDocument = gql`
query FindAllCoreViews {
getCoreViews {
...ViewFragment
}
}
${ViewFragmentFragmentDoc}`;
/**
* __useFindAllCoreViewsQuery__
*
* To run a query within a React component, call `useFindAllCoreViewsQuery` and pass it any options that fit your needs.
* When your component renders, `useFindAllCoreViewsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useFindAllCoreViewsQuery({
* variables: {
* },
* });
*/
export function useFindAllCoreViewsQuery(baseOptions?: Apollo.QueryHookOptions<FindAllCoreViewsQuery, FindAllCoreViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<FindAllCoreViewsQuery, FindAllCoreViewsQueryVariables>(FindAllCoreViewsDocument, options);
}
export function useFindAllCoreViewsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<FindAllCoreViewsQuery, FindAllCoreViewsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<FindAllCoreViewsQuery, FindAllCoreViewsQueryVariables>(FindAllCoreViewsDocument, options);
}
export type FindAllCoreViewsQueryHookResult = ReturnType<typeof useFindAllCoreViewsQuery>;
export type FindAllCoreViewsLazyQueryHookResult = ReturnType<typeof useFindAllCoreViewsLazyQuery>;
export type FindAllCoreViewsQueryResult = Apollo.QueryResult<FindAllCoreViewsQuery, FindAllCoreViewsQueryVariables>;
export const FindManyCoreViewFieldsDocument = gql`
query FindManyCoreViewFields($viewId: String!) {
getCoreViewFields(viewId: $viewId) {
@@ -21,8 +21,8 @@ import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
import { UserThemeProviderEffect } from '@/ui/theme/components/UserThemeProviderEffect';
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { UserAndViewsProviderEffect } from '@/users/components/UserAndViewsProviderEffect';
import { UserProvider } from '@/users/components/UserProvider';
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
import { StrictMode } from 'react';
import { Outlet, useLocation } from 'react-router-dom';
@@ -36,7 +36,7 @@ export const AppRouterProviders = () => {
<ApolloProvider>
<BaseThemeProvider>
<ClientConfigProviderEffect />
<UserProviderEffect />
<UserAndViewsProviderEffect />
<WorkspaceProviderEffect />
<ClientConfigProvider>
<CaptchaProvider>
@@ -22,6 +22,7 @@ import { AppPath, type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type WorkspaceMember,
useFindAllCoreViewsQuery,
useGetCurrentUserQuery,
} from '~/generated-metadata/graphql';
import { dateLocaleState } from '~/localization/states/dateLocaleState';
@@ -29,7 +30,7 @@ import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { isMatchingLocation } from '~/utils/isMatchingLocation';
export const UserProviderEffect = () => {
export const UserAndViewsProviderEffect = () => {
const location = useLocation();
const [isCurrentUserLoaded, setIsCurrentUserLoaded] = useRecoilState(
@@ -82,53 +83,57 @@ export const UserProviderEffect = () => {
currentWorkspaceDeletedMembersState,
);
const { data: queryData, loading: queryLoading } = useGetCurrentUserQuery({
skip:
!isLoggedIn ||
isCurrentUserLoaded ||
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail),
const shouldSkip =
!isLoggedIn ||
isCurrentUserLoaded ||
isMatchingLocation(location, AppPath.Verify) ||
isMatchingLocation(location, AppPath.VerifyEmail);
const { data: userQueryData, loading: userQueryLoading } =
useGetCurrentUserQuery({
skip: shouldSkip,
});
const { data: queryDataCoreViews } = useFindAllCoreViewsQuery({
skip: shouldSkip,
});
useEffect(() => {
if (!queryLoading) {
if (!userQueryLoading) {
setIsCurrentUserLoaded(true);
}
if (!isDefined(queryData?.currentUser)) return;
if (!isDefined(userQueryData?.currentUser)) return;
setCurrentUser(queryData.currentUser);
setCurrentUser(userQueryData.currentUser);
if (isDefined(queryData.currentUser.currentWorkspace)) {
if (isDefined(userQueryData.currentUser.currentWorkspace)) {
setCurrentWorkspace({
...queryData.currentUser.currentWorkspace,
defaultRole: queryData.currentUser.currentWorkspace.defaultRole ?? null,
...userQueryData.currentUser.currentWorkspace,
defaultRole:
userQueryData.currentUser.currentWorkspace.defaultRole ?? null,
defaultAgent:
queryData.currentUser.currentWorkspace.defaultAgent ?? null,
userQueryData.currentUser.currentWorkspace.defaultAgent ?? null,
});
}
if (isDefined(queryData.currentUser.currentUserWorkspace)) {
if (isDefined(userQueryData.currentUser.currentUserWorkspace)) {
setCurrentUserWorkspace({
...queryData.currentUser.currentUserWorkspace,
...userQueryData.currentUser.currentUserWorkspace,
objectsPermissions:
(queryData.currentUser.currentUserWorkspace
(userQueryData.currentUser.currentUserWorkspace
.objectsPermissions as Array<
ObjectPermissions & { objectMetadataId: string }
>) ?? [],
});
}
if (isDefined(queryData.currentUser?.currentWorkspace?.views)) {
setCoreViews(queryData.currentUser.currentWorkspace.views);
}
const {
workspaceMember,
workspaceMembers,
deletedWorkspaceMembers,
availableWorkspaces,
} = queryData.currentUser;
} = userQueryData.currentUser;
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
workspaceMember: WorkspaceMember,
@@ -168,8 +173,8 @@ export const UserProviderEffect = () => {
setAvailableWorkspaces(availableWorkspaces);
}
}, [
queryLoading,
queryData?.currentUser,
userQueryLoading,
userQueryData?.currentUser,
setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspaceMembers,
@@ -183,5 +188,11 @@ export const UserProviderEffect = () => {
setCoreViews,
]);
useEffect(() => {
if (!isDefined(queryDataCoreViews?.getCoreViews)) return;
setCoreViews(queryDataCoreViews.getCoreViews);
}, [queryDataCoreViews?.getCoreViews, setCoreViews]);
return null;
};
@@ -4,14 +4,13 @@ import {
} from '@/auth/graphql/fragments/authFragments';
import { OBJECT_PERMISSION_FRAGMENT } from '@/settings/roles/graphql/fragments/objectPermissionFragment';
import { ROLE_FRAGMENT } from '@/settings/roles/graphql/fragments/roleFragment';
import { BILLING_SUBSCRIPTION_FRAGMENT } from '@/users/graphql/fragments/billingSubscriptionsFragment';
import { CURRENT_BILLING_SUBSCRIPTION_FRAGMENT } from '@/users/graphql/fragments/currentBillingSubscriptionFragement';
import { WORKSPACE_URLS_FRAGMENT } from '@/users/graphql/fragments/workspaceUrlsFragment';
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { DELETED_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/deletedWorkspaceMemberQueryFragment';
import { PARTIAL_WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/partialWorkspaceMemberQueryFragment';
import { WORKSPACE_MEMBER_QUERY_FRAGMENT } from '@/workspace-member/graphql/fragments/workspaceMemberQueryFragment';
import { gql } from '@apollo/client';
import { CURRENT_BILLING_SUBSCRIPTION_FRAGMENT } from '@/users/graphql/fragments/currentBillingSubscriptionFragement';
import { BILLING_SUBSCRIPTION_FRAGMENT } from '@/users/graphql/fragments/billingSubscriptionsFragment';
export const USER_QUERY_FRAGMENT = gql`
fragment UserQueryFragment on User {
@@ -80,9 +79,6 @@ export const USER_QUERY_FRAGMENT = gql`
id
}
isTwoFactorAuthenticationEnforced
views {
...ViewFragment
}
}
availableWorkspaces {
...AvailableWorkspacesFragment
@@ -96,7 +92,6 @@ export const USER_QUERY_FRAGMENT = gql`
${OBJECT_PERMISSION_FRAGMENT}
${WORKSPACE_URLS_FRAGMENT}
${ROLE_FRAGMENT}
${VIEW_FRAGMENT}
${AVAILABLE_WORKSPACES_FOR_AUTH_FRAGMENT}
${AVAILABLE_WORKSPACE_FOR_AUTH_FRAGMENT}
${CURRENT_BILLING_SUBSCRIPTION_FRAGMENT}
@@ -14,7 +14,10 @@ import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ColorScheme } from 'twenty-ui/input';
import { useGetCurrentUserLazyQuery } from '~/generated-metadata/graphql';
import {
useFindAllCoreViewsLazyQuery,
useGetCurrentUserLazyQuery,
} from '~/generated-metadata/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
@@ -37,12 +40,17 @@ export const useLoadCurrentUser = () => {
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const [getCurrentUser] = useGetCurrentUserLazyQuery();
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const loadCurrentUser = useCallback(async () => {
const currentUserResult = await getCurrentUser({
fetchPolicy: 'network-only',
});
const coreViewsResult = await findAllCoreViews({
fetchPolicy: 'network-only',
});
if (isDefined(currentUserResult.error)) {
throw new Error(currentUserResult.error.message);
}
@@ -102,8 +110,8 @@ export const useLoadCurrentUser = () => {
});
}
if (isDefined(workspace) && isDefined(workspace.views)) {
setCoreViews(workspace.views);
if (isDefined(coreViewsResult.data?.getCoreViews)) {
setCoreViews(coreViewsResult.data.getCoreViews);
}
return {
@@ -113,16 +121,17 @@ export const useLoadCurrentUser = () => {
};
}, [
getCurrentUser,
isOnAWorkspace,
setAvailableWorkspaces,
setCoreViews,
findAllCoreViews,
setCurrentUser,
setCurrentUserWorkspace,
setCurrentWorkspace,
setCurrentWorkspaceMember,
isOnAWorkspace,
setCurrentWorkspaceMembers,
setAvailableWorkspaces,
setCurrentUserWorkspace,
setCurrentWorkspaceMember,
initializeFormatPreferences,
setLastAuthenticateWorkspaceDomain,
setCoreViews,
]);
return {
@@ -0,0 +1,11 @@
import { VIEW_FRAGMENT } from '@/views/graphql/fragments/viewFragment';
import { gql } from '@apollo/client';
export const FIND_ALL_CORE_VIEWS = gql`
${VIEW_FRAGMENT}
query FindAllCoreViews {
getCoreViews {
...ViewFragment
}
}
`;
@@ -1,6 +1,7 @@
import { anyFieldFilterValueComponentState } from '@/object-record/record-filter/states/anyFieldFilterValueComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useGetCurrentViewOnly } from '@/views/hooks/useGetCurrentViewOnly';
import { isNonEmptyString } from '@sniptt/guards';
import { compareNonEmptyStrings } from '~/utils/compareNonEmptyStrings';
export const useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter = () => {
@@ -11,8 +12,11 @@ export const useIsViewAnyFieldFilterDifferentFromCurrentAnyFieldFilter = () => {
const viewAnyFieldFilterValue = currentView?.anyFieldFilterValue;
const viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter =
!compareNonEmptyStrings(viewAnyFieldFilterValue, anyFieldFilterValue);
const viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter = isNonEmptyString(
anyFieldFilterValue,
)
? !compareNonEmptyStrings(viewAnyFieldFilterValue, anyFieldFilterValue)
: isNonEmptyString(viewAnyFieldFilterValue);
return { viewAnyFieldFilterDifferentFromCurrentAnyFieldFilter };
};
@@ -15,7 +15,7 @@ import { ClientConfigProviderEffect } from '@/client-config/components/ClientCon
import { ApolloCoreClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloCoreClientMockedProvider';
import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout';
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
import { UserAndViewsProviderEffect } from '@/users/components/UserAndViewsProviderEffect';
import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider';
import { UserProvider } from '~/modules/users/components/UserProvider';
import { mockedApolloClient } from '~/testing/mockedApolloClient';
@@ -85,7 +85,7 @@ const Providers = () => {
<ApolloStorybookDevLogEffect />
<ClientConfigProviderEffect />
<ClientConfigProvider>
<UserProviderEffect />
<UserAndViewsProviderEffect />
<WorkspaceProviderEffect />
<UserProvider>
<ApolloCoreClientMockedProvider>
@@ -44,7 +44,7 @@ export const metadataModuleFactory = async (
useCachedMetadata({
cacheGetter: cacheStorageService.get.bind(cacheStorageService),
cacheSetter: cacheStorageService.set.bind(cacheStorageService),
operationsToCache: ['ObjectMetadataItems'],
operationsToCache: ['ObjectMetadataItems', 'FindAllCoreViews'],
}),
],
path: '/metadata',
@@ -107,7 +107,7 @@ export class CacheStorageService {
do {
const result = await redisClient.scan(cursor, {
MATCH: scanPattern,
MATCH: `${this.namespace}:${scanPattern}`,
COUNT: 100,
});
@@ -53,6 +53,7 @@ describe('ViewFieldService', () => {
provide: ViewService,
useValue: {
findByIdWithRelatedObjectMetadata: jest.fn(),
flushGraphQLCache: jest.fn(),
},
},
],
@@ -13,6 +13,7 @@ import {
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewFilterGroupService', () => {
let viewFilterGroupService: ViewFilterGroupService;
@@ -44,6 +45,12 @@ describe('ViewFilterGroupService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -13,6 +13,7 @@ import {
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewFilterService', () => {
let viewFilterService: ViewFilterService;
@@ -35,6 +36,12 @@ describe('ViewFilterService', () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ViewFilterService,
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
{
provide: getRepositoryToken(ViewFilterEntity),
useValue: {
@@ -12,6 +12,7 @@ import {
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewGroupService', () => {
let viewGroupService: ViewGroupService;
@@ -45,6 +46,12 @@ describe('ViewGroupService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -13,6 +13,7 @@ import {
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
describe('ViewSortService', () => {
let viewSortService: ViewSortService;
@@ -44,6 +45,12 @@ describe('ViewSortService', () => {
delete: jest.fn(),
},
},
{
provide: ViewService,
useValue: {
flushGraphQLCache: jest.fn(),
},
},
],
}).compile();
@@ -15,6 +15,7 @@ import {
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
describe('ViewService', () => {
let viewService: ViewService;
@@ -56,6 +57,12 @@ describe('ViewService', () => {
delete: jest.fn(),
},
},
{
provide: WorkspaceCacheStorageService,
useValue: {
flushGraphQLOperation: jest.fn(),
},
},
{
provide: I18nService,
useValue: {
@@ -95,6 +95,9 @@ export class ViewFieldService {
);
const savedViewField = await this.viewFieldRepository.save(viewField);
await this.viewService.flushGraphQLCache(viewFieldData.workspaceId);
const createdViewField = await this.findById(
savedViewField.id,
viewFieldData.workspaceId,
@@ -223,6 +226,8 @@ export class ViewFieldService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewField, ...updatedViewField };
}
@@ -241,6 +246,8 @@ export class ViewFieldService {
await this.viewFieldRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewField;
}
@@ -259,6 +266,8 @@ export class ViewFieldService {
await this.viewFieldRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewField;
}
@@ -12,12 +12,14 @@ import {
generateViewFilterGroupExceptionMessage,
generateViewFilterGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewFilterGroupService {
constructor(
@InjectRepository(ViewFilterGroupEntity)
private readonly viewFilterGroupRepository: Repository<ViewFilterGroupEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(
@@ -118,7 +120,14 @@ export class ViewFilterGroupService {
const viewFilterGroup =
this.viewFilterGroupRepository.create(viewFilterGroupData);
return this.viewFilterGroupRepository.save(viewFilterGroup);
await this.viewService.flushGraphQLCache(viewFilterGroupData.workspaceId);
const savedViewFilterGroup =
await this.viewFilterGroupRepository.save(viewFilterGroup);
await this.viewService.flushGraphQLCache(viewFilterGroupData.workspaceId);
return savedViewFilterGroup;
}
async update(
@@ -143,6 +152,8 @@ export class ViewFilterGroupService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewFilterGroup, ...updatedViewFilterGroup };
}
@@ -182,6 +193,8 @@ export class ViewFilterGroupService {
await this.viewFilterGroupRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewFilterExceptionMessage,
generateViewFilterUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-filter.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewFilterService {
constructor(
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewFilterEntity[]> {
@@ -109,7 +111,13 @@ export class ViewFilterService {
const viewFilter = this.viewFilterRepository.create(viewFilterData);
return this.viewFilterRepository.save(viewFilter);
await this.viewService.flushGraphQLCache(viewFilterData.workspaceId);
const savedViewFilter = await this.viewFilterRepository.save(viewFilter);
await this.viewService.flushGraphQLCache(viewFilterData.workspaceId);
return savedViewFilter;
}
async update(
@@ -134,6 +142,8 @@ export class ViewFilterService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewFilter, ...updatedViewFilter };
}
@@ -152,6 +162,8 @@ export class ViewFilterService {
await this.viewFilterRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewFilter;
}
@@ -170,6 +182,8 @@ export class ViewFilterService {
await this.viewFilterRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewGroupExceptionMessage,
generateViewGroupUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-group.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewGroupService {
constructor(
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewGroupEntity[]> {
@@ -109,7 +111,13 @@ export class ViewGroupService {
const viewGroup = this.viewGroupRepository.create(viewGroupData);
return this.viewGroupRepository.save(viewGroup);
await this.viewService.flushGraphQLCache(viewGroupData.workspaceId);
const savedViewGroup = await this.viewGroupRepository.save(viewGroup);
await this.viewService.flushGraphQLCache(viewGroupData.workspaceId);
return savedViewGroup;
}
async update(
@@ -134,6 +142,8 @@ export class ViewGroupService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewGroup, ...updatedViewGroup };
}
@@ -152,6 +162,8 @@ export class ViewGroupService {
await this.viewGroupRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewGroup;
}
@@ -170,6 +182,8 @@ export class ViewGroupService {
await this.viewGroupRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -12,12 +12,14 @@ import {
generateViewSortExceptionMessage,
generateViewSortUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view-sort.exception';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
@Injectable()
export class ViewSortService {
constructor(
@InjectRepository(ViewSortEntity)
private readonly viewSortRepository: Repository<ViewSortEntity>,
private readonly viewService: ViewService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewSortEntity[]> {
@@ -105,7 +107,11 @@ export class ViewSortService {
const viewSort = this.viewSortRepository.create(viewSortData);
return this.viewSortRepository.save(viewSort);
const savedViewSort = await this.viewSortRepository.save(viewSort);
await this.viewService.flushGraphQLCache(viewSortData.workspaceId);
return savedViewSort;
}
async update(
@@ -130,6 +136,8 @@ export class ViewSortService {
...updateData,
});
await this.viewService.flushGraphQLCache(workspaceId);
return { ...existingViewSort, ...updatedViewSort };
}
@@ -148,6 +156,8 @@ export class ViewSortService {
await this.viewSortRepository.softDelete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return viewSort;
}
@@ -166,6 +176,8 @@ export class ViewSortService {
await this.viewSortRepository.delete(id);
await this.viewService.flushGraphQLCache(workspaceId);
return true;
}
}
@@ -15,6 +15,7 @@ import {
generateViewExceptionMessage,
generateViewUserFriendlyExceptionMessage,
} from 'src/engine/core-modules/view/exceptions/view.exception';
import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service';
@Injectable()
export class ViewService {
@@ -22,6 +23,7 @@ export class ViewService {
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly i18nService: I18nService,
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
) {}
async findByWorkspaceId(workspaceId: string): Promise<ViewEntity[]> {
@@ -134,7 +136,11 @@ export class ViewService {
isCustom: true,
});
return this.viewRepository.save(view);
const savedView = await this.viewRepository.save(view);
await this.flushGraphQLCache(viewData.workspaceId);
return savedView;
}
async update(
@@ -159,6 +165,8 @@ export class ViewService {
...updateData,
});
await this.flushGraphQLCache(workspaceId);
return { ...existingView, ...updatedView };
}
@@ -177,6 +185,8 @@ export class ViewService {
await this.viewRepository.softDelete(id);
await this.flushGraphQLCache(workspaceId);
return view;
}
@@ -194,6 +204,7 @@ export class ViewService {
}
await this.viewRepository.delete(id);
await this.flushGraphQLCache(workspaceId);
return true;
}
@@ -235,4 +246,11 @@ export class ViewService {
return viewName;
}
async flushGraphQLCache(workspaceId: string): Promise<void> {
await this.workspaceCacheStorageService.flushGraphQLOperation({
operationName: 'FindAllCoreViews',
workspaceId,
});
}
}
@@ -116,6 +116,7 @@ export class FieldMetadataRelatedRecordsService {
await this.syncNoValueViewGroup(newFieldMetadata, view);
}
await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId);
}
public async resetViewKanbanAggregateOperation(
@@ -140,6 +141,7 @@ export class FieldMetadataRelatedRecordsService {
kanbanAggregateOperation: null,
});
}
await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId);
}
public async updateRelatedViewFilters(
@@ -247,6 +249,7 @@ export class FieldMetadataRelatedRecordsService {
);
}
}
await this.viewService.flushGraphQLCache(newFieldMetadata.workspaceId);
}
async syncNoValueViewGroup(
@@ -274,6 +277,7 @@ export class FieldMetadataRelatedRecordsService {
fieldMetadata.workspaceId,
);
}
await this.viewService.flushGraphQLCache(fieldMetadata.workspaceId);
}
public getOptionsDifferences(
@@ -34,6 +34,7 @@ export class ObjectMetadataRelatedRecordsService {
await this.createViewFields(objectMetadata, view.id);
await this.createViewWorkspaceFavorite(objectMetadata.workspaceId, view.id);
await this.viewService.flushGraphQLCache(objectMetadata.workspaceId);
}
public async updateLabelMetadataIdentifierInObjectViews({
@@ -221,6 +221,18 @@ export class WorkspaceCacheStorageService {
);
}
async flushGraphQLOperation({
operationName,
workspaceId,
}: {
operationName: string;
workspaceId: string;
}): Promise<void> {
await this.cacheStorageService.flushByPattern(
`${WorkspaceCacheKeys.GraphQLOperations}:${operationName}:${workspaceId}:*`,
);
}
async flushVersionedMetadata(
workspaceId: string,
metadataVersion?: number,