From 5824637d1aa63071b94cd156c89eb98668c428ed Mon Sep 17 00:00:00 2001 From: Vinay Arvind Badgujar <90821253+vinaybadgujar102@users.noreply.github.com> Date: Fri, 5 Sep 2025 14:13:03 +0530 Subject: [PATCH] fix - newly added object not visible in left menu (#14202) resolves #14190 added refreshCoreViews() call after object creation to immediately update core views state, ensuring new objects appear in the navigation drawer without requiring a refresh --------- Co-authored-by: Charles Bochet Co-authored-by: prastoin --- .../src/generated-metadata/graphql.ts | 15 +- .../object-metadata/graphql/mutations.ts | 9 + .../hooks/__mocks__/useFieldMetadataItem.ts | 49 +-- .../__tests__/useFieldMetadataItem.test.tsx | 37 +-- .../hooks/useCreateOneFieldMetadataItem.ts | 8 +- .../hooks/useCreateOneObjectMetadataItem.ts | 15 +- .../hooks/useDeleteOneFieldMetadataItem.ts | 20 +- .../hooks/useDeleteOneObjectMetadataItem.ts | 5 + .../hooks/useFieldMetadataItem.ts | 7 +- .../hooks/useUpdateOneFieldMetadataItem.ts | 76 +---- .../visibleRecordFieldsComponentSelector.ts | 2 +- .../SettingsObjectFieldItemTableRow.tsx | 17 +- .../views/components/EditableFilterChip.tsx | 5 +- .../internal/usePersistViewFilterRecords.ts | 4 +- ...eComputeRecordRelationFilterLabelValue.tsx | 12 +- .../useRefreshCoreViewsByObjectMetadataId.ts | 93 +++++- .../modules/views/hooks/useRefreshViews.ts | 25 -- .../views/utils/getRecordFilterLabelValue.ts | 23 +- .../migrate-views-to-core.command.ts | 17 +- .../view/controllers/view-field.controller.ts | 28 +- .../view-filter-group.controller.ts | 32 +- .../controllers/view-filter.controller.ts | 30 +- .../view/controllers/view-group.controller.ts | 28 +- .../view/controllers/view-sort.controller.ts | 28 +- .../view/controllers/view.controller.ts | 28 +- .../view/exceptions/view-field.exception.ts | 5 + .../view/services/view-field.service.ts | 24 +- .../field-metadata/field-metadata.module.ts | 4 +- .../field-metadata-related-records.spec.ts | 18 +- .../field-metadata-related-records.service.ts | 291 +++++++++--------- .../services/field-metadata.service.ts | 11 +- .../object-metadata/object-metadata.module.ts | 2 + ...object-metadata-related-records.service.ts | 64 ++-- .../core/utils/seed-feature-flags.util.ts | 10 - .../workspace-manager.service.ts | 18 +- .../constants/default-feature-flags.ts | 7 +- .../view/listeners/base-view-sync.listener.ts | 209 ------------- .../view/listeners/view-field.listener.ts | 81 ----- .../listeners/view-filter-group.listener.ts | 86 ------ .../view/listeners/view-filter.listener.ts | 86 ------ .../view/listeners/view-group.listener.ts | 81 ----- .../view/listeners/view-sort.listener.ts | 81 ----- .../modules/view/listeners/view.listener.ts | 76 ----- .../view-delete-many.pre-query.hook.ts | 27 -- .../view-delete-one.pre-query.hook.ts | 56 ---- .../view/services/view-field-sync.service.ts | 104 ------- .../view-filter-group-sync.service.ts | 110 ------- .../view/services/view-filter-sync.service.ts | 133 -------- .../view/services/view-group-sync.service.ts | 112 ------- .../view/services/view-sort-sync.service.ts | 120 -------- .../view/services/view-sync.service.ts | 146 --------- .../src/modules/view/services/view.service.ts | 147 --------- .../src/modules/view/view.module.ts | 33 +- ...ct-records-permissions.integration-spec.ts | 49 +-- .../view-field-resolver.integration-spec.ts | 8 +- ...-filter-group-resolver.integration-spec.ts | 8 +- .../view-filter-resolver.integration-spec.ts | 8 +- .../view-group-resolver.integration-spec.ts | 8 +- .../view/view-resolver.integration-spec.ts | 8 +- .../view-sort-resolver.integration-spec.ts | 8 +- ...ta-related-record.integration-spec.ts.snap | 206 ++++++------- ...ete-one-field-metadata.integration-spec.ts | 80 ++--- ...etadata-related-record.integration-spec.ts | 172 ++++------- .../utils/update-one-field-metadata.util.ts | 4 +- .../suites/view-field.integration-spec.ts | 16 +- .../view-filter-group.integration-spec.ts | 16 +- .../suites/view-filter.integration-spec.ts | 30 +- .../suites/view-group.integration-spec.ts | 28 +- .../rest/suites/view-sort.integration-spec.ts | 16 +- .../rest/suites/view.integration-spec.ts | 30 +- .../rest/utils/rest-test-assertions.util.ts | 13 + .../rest/utils/view-rest-api.util.ts | 32 ++ 72 files changed, 914 insertions(+), 2581 deletions(-) delete mode 100644 packages/twenty-front/src/modules/views/hooks/useRefreshViews.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/base-view-sync.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view-field.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view-filter-group.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view-filter.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view-group.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view-sort.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/listeners/view.listener.ts delete mode 100644 packages/twenty-server/src/modules/view/pre-hooks/view-delete-many.pre-query.hook.ts delete mode 100644 packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-field-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-filter-group-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-filter-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-group-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-sort-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view-sync.service.ts delete mode 100644 packages/twenty-server/src/modules/view/services/view.service.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 37af70989f..0deb3931c5 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -4411,7 +4411,7 @@ export type CreateOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, defaultValue?: any | null, options?: any | null, isLabelSyncedWithName?: boolean | null } }; +export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, defaultValue?: any | null, options?: any | null, isLabelSyncedWithName?: boolean | null, object?: { __typename?: 'Object', id: string } | null } }; export type UpdateOneFieldMetadataItemMutationVariables = Exact<{ idToUpdate: Scalars['UUID']; @@ -4419,7 +4419,7 @@ export type UpdateOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, isLabelSyncedWithName?: boolean | null } }; +export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, isLabelSyncedWithName?: boolean | null, object?: { __typename?: 'Object', id: string } | null } }; export type UpdateOneObjectMetadataItemMutationVariables = Exact<{ idToUpdate: Scalars['UUID']; @@ -4441,7 +4441,7 @@ export type DeleteOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null } }; +export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isUnique?: boolean | null, isNullable?: boolean | null, createdAt: string, updatedAt: string, settings?: any | null, object?: { __typename?: 'Object', id: string } | null } }; export type ObjectMetadataItemsQueryVariables = Exact<{ [key: string]: never; }>; @@ -8031,6 +8031,9 @@ export const CreateOneFieldMetadataItemDocument = gql` defaultValue options isLabelSyncedWithName + object { + id + } } } `; @@ -8077,6 +8080,9 @@ export const UpdateOneFieldMetadataItemDocument = gql` updatedAt settings isLabelSyncedWithName + object { + id + } } } `; @@ -8218,6 +8224,9 @@ export const DeleteOneFieldMetadataItemDocument = gql` createdAt updatedAt settings + object { + id + } } } `; diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts b/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts index 3696cd8d46..1dddfab5e6 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/mutations.ts @@ -41,6 +41,9 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql` defaultValue options isLabelSyncedWithName + object { + id + } } } `; @@ -65,6 +68,9 @@ export const UPDATE_ONE_FIELD_METADATA_ITEM = gql` updatedAt settings isLabelSyncedWithName + object { + id + } } } `; @@ -132,6 +138,9 @@ export const DELETE_ONE_FIELD_METADATA_ITEM = gql` createdAt updatedAt settings + object { + id + } } } `; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index b132423526..edb521dcc8 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -22,48 +22,9 @@ export const queries = { createdAt updatedAt settings - } - } - `, - findManyViewsQuery: gql` - query FindManyViews( - $filter: ViewFilterInput - $orderBy: [ViewOrderByInput] - $lastCursor: String - $limit: Int - ) { - views( - filter: $filter - orderBy: $orderBy - first: $limit - after: $lastCursor - ) { - edges { - node { - __typename - id - viewGroups { - edges { - node { - __typename - fieldMetadataId - fieldValue - id - isVisible - position - } - } - } - } - cursor + object { + id } - pageInfo { - hasNextPage - hasPreviousPage - startCursor - endCursor - } - totalCount } } `, @@ -86,6 +47,9 @@ export const queries = { updatedAt settings isLabelSyncedWithName + object { + id + } } } `, @@ -108,6 +72,9 @@ export const queries = { defaultValue options isLabelSyncedWithName + object { + id + } } } `, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx index b7a488047e..30dd1b57c8 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx @@ -74,31 +74,6 @@ const fieldRelationMetadataItem: FieldMetadataItem = { }; const mocks = [ - { - request: { - query: queries.findManyViewsQuery, - variables: { - filter: { - objectMetadataId: { eq: '25611fce-6637-4089-b0ca-91afeec95784' }, - }, - }, - }, - result: jest.fn(() => ({ - data: { - views: { - __typename: 'ViewConnection', - totalCount: 0, - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - startCursor: '', - endCursor: '', - }, - edges: [], - }, - }, - })), - }, { request: { query: GET_CURRENT_USER, @@ -219,7 +194,10 @@ describe('useFieldMetadataItem', () => { }); await act(async () => { - const res = await result.current.deleteMetadataField(fieldMetadataItem); + const res = await result.current.deleteMetadataField({ + idToDelete: fieldMetadataItem.id, + objectMetadataId, + }); expect(res.data).toEqual({ deleteOneField: responseData.default, @@ -233,9 +211,10 @@ describe('useFieldMetadataItem', () => { }); await act(async () => { - const res = await result.current.deleteMetadataField( - fieldRelationMetadataItem, - ); + const res = await result.current.deleteMetadataField({ + idToDelete: fieldRelationMetadataItem.id, + objectMetadataId, + }); expect(res.data).toEqual({ deleteOneField: responseData.fieldRelation, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts index 25dbcae550..e4c232b532 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneFieldMetadataItem.ts @@ -9,7 +9,7 @@ import { import { CREATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; -import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; export const useCreateOneFieldMetadataItem = () => { const { refreshObjectMetadataItems } = @@ -20,7 +20,8 @@ export const useCreateOneFieldMetadataItem = () => { CreateOneFieldMetadataItemMutationVariables >(CREATE_ONE_FIELD_METADATA_ITEM); - const { refreshCachedViews } = useRefreshCachedViews(); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); const createOneFieldMetadataItem = async (input: CreateFieldInput) => { const result = await mutate({ @@ -33,8 +34,7 @@ export const useCreateOneFieldMetadataItem = () => { await refreshObjectMetadataItems(); - await refreshCachedViews(); - + await refreshCoreViewsByObjectMetadataId(input.objectMetadataId); return result; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index a586b76ebd..884b7cd884 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -9,13 +9,14 @@ import { import { CREATE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; -import { useRefreshCachedViews } from '@/views/hooks/useRefreshViews'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; +import { isDefined } from 'twenty-shared/utils'; export const useCreateOneObjectMetadataItem = () => { - const { refreshCachedViews } = useRefreshCachedViews(); - const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); const [mutate] = useMutation< CreateOneObjectMetadataItemMutation, @@ -30,7 +31,13 @@ export const useCreateOneObjectMetadataItem = () => { }); await refreshObjectMetadataItems(); - refreshCachedViews(); + + if (isDefined(createdObjectMetadata.data?.createOneObject?.id)) { + await refreshCoreViewsByObjectMetadataId( + createdObjectMetadata.data.createOneObject.id, + ); + } + return createdObjectMetadata; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts index 1b60307a4e..9e6eeb8628 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneFieldMetadataItem.ts @@ -5,10 +5,10 @@ import { type DeleteOneFieldMetadataItemMutationVariables, } from '~/generated-metadata/graphql'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; import { recordIndexKanbanAggregateOperationState } from '@/object-record/record-index/states/recordIndexKanbanAggregateOperationState'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; import { useRecoilState } from 'recoil'; import { DELETE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; @@ -20,14 +20,14 @@ export const useDeleteOneFieldMetadataItem = () => { const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); const [ recordIndexKanbanAggregateOperation, setRecordIndexKanbanAggregateOperation, ] = useRecoilState(recordIndexKanbanAggregateOperationState); - const apolloCoreClient = useApolloCoreClient(); - const resetRecordIndexKanbanAggregateOperation = async ( idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'], ) => { @@ -37,14 +37,15 @@ export const useDeleteOneFieldMetadataItem = () => { fieldMetadataId: null, }); } - await apolloCoreClient.refetchQueries({ - include: ['FindManyViews'], - }); }; - const deleteOneFieldMetadataItem = async ( - idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete'], - ) => { + const deleteOneFieldMetadataItem = async ({ + idToDelete, + objectMetadataId, + }: { + idToDelete: DeleteOneFieldMetadataItemMutationVariables['idToDelete']; + objectMetadataId: string; + }) => { const result = await mutate({ variables: { idToDelete, @@ -54,6 +55,7 @@ export const useDeleteOneFieldMetadataItem = () => { await resetRecordIndexKanbanAggregateOperation(idToDelete); await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(objectMetadataId); return result; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts index 85fba6c9dc..3fbc174ac6 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useDeleteOneObjectMetadataItem.ts @@ -8,6 +8,7 @@ import { import { DELETE_ONE_OBJECT_METADATA_ITEM } from '../graphql/mutations'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; export const useDeleteOneObjectMetadataItem = () => { const [mutate] = useMutation< @@ -18,6 +19,9 @@ export const useDeleteOneObjectMetadataItem = () => { const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); + const deleteOneObjectMetadataItem = async ( idToDelete: DeleteOneObjectMetadataItemMutationVariables['idToDelete'], ) => { @@ -28,6 +32,7 @@ export const useDeleteOneObjectMetadataItem = () => { }); await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(idToDelete); return result; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts index d62231308a..ec22245ae5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useFieldMetadataItem.ts @@ -1,6 +1,5 @@ import { type Field } from '~/generated-metadata/graphql'; -import { type FieldMetadataItem } from '../types/FieldMetadataItem'; import { formatFieldMetadataItemInput } from '../utils/formatFieldMetadataItemInput'; import { type RelationCreationPayload } from 'twenty-shared/types'; @@ -65,14 +64,10 @@ export const useFieldMetadataItem = () => { updatePayload: { isActive: false }, }); - const deleteMetadataField = (metadataField: FieldMetadataItem) => { - return deleteOneFieldMetadataItem(metadataField.id); - }; - return { activateMetadataField, createMetadataField, deactivateMetadataField, - deleteMetadataField, + deleteMetadataField: deleteOneFieldMetadataItem, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts index a8cd46c309..e904c04942 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useUpdateOneFieldMetadataItem.ts @@ -1,4 +1,4 @@ -import { useApolloClient, useMutation } from '@apollo/client'; +import { useMutation } from '@apollo/client'; import { type UpdateOneFieldMetadataItemMutation, @@ -7,43 +7,15 @@ import { import { UPDATE_ONE_FIELD_METADATA_ITEM } from '../graphql/mutations'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient'; import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; -import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; -import { useSetRecoilState } from 'recoil'; - -import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; -import { type RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -import { useSetRecordGroups } from '@/object-record/record-group/hooks/useSetRecordGroups'; -import { isDefined } from 'twenty-shared/utils'; +import { useRefreshCoreViewsByObjectMetadataId } from '@/views/hooks/useRefreshCoreViewsByObjectMetadataId'; export const useUpdateOneFieldMetadataItem = () => { - const apolloClient = useApolloClient(); - const apolloCoreClient = useApolloCoreClient(); const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems('network-only'); - const { setRecordGroupsFromViewGroups } = useSetRecordGroups(); - const cache = useApolloClient().cache; - - const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); - - const { findManyRecordsQuery: findManyViewsQuery } = useFindManyRecordsQuery({ - objectNameSingular: CoreObjectNameSingular.View, - recordGqlFields: { - id: true, - viewGroups: { - id: true, - fieldMetadataId: true, - isVisible: true, - fieldValue: true, - position: true, - }, - }, - }); + const { refreshCoreViewsByObjectMetadataId } = + useRefreshCoreViewsByObjectMetadataId(); const [mutate] = useMutation< UpdateOneFieldMetadataItemMutation, @@ -76,44 +48,8 @@ export const useUpdateOneFieldMetadataItem = () => { }, }); - const objectMetadataItemsRefreshed = await refreshObjectMetadataItems(); - - const { data } = await apolloClient.query({ query: GET_CURRENT_USER }); - setCurrentWorkspace(data?.currentUser?.currentWorkspace); - - const { data: viewConnection } = await apolloCoreClient.query<{ - views: RecordGqlConnection; - }>({ - query: findManyViewsQuery, - variables: { - filter: { - objectMetadataId: { - eq: objectMetadataId, - }, - }, - }, - fetchPolicy: 'network-only', - }); - - const viewRecords = getRecordsFromRecordConnection({ - recordConnection: viewConnection?.views, - }); - - for (const view of viewRecords) { - const correspondingObjectMetadataItemRefreshed = - objectMetadataItemsRefreshed?.find( - (item) => item.id === objectMetadataId, - ); - - if (isDefined(correspondingObjectMetadataItemRefreshed)) { - setRecordGroupsFromViewGroups( - view.id, - view.viewGroups, - correspondingObjectMetadataItemRefreshed, - ); - } - cache.evict({ id: `Views:${view.id}` }); - } + await refreshObjectMetadataItems(); + await refreshCoreViewsByObjectMetadataId(objectMetadataId); return result; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts index 3417f3a352..99e9f2c8ea 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/states/visibleRecordFieldsComponentSelector.ts @@ -32,7 +32,7 @@ export const visibleRecordFieldsComponentSelector = createComponentSelector({ ), ); - return filteredVisibleAndReadableRecordFields.toSorted( + return [...filteredVisibleAndReadableRecordFields].sort( sortByProperty('position'), ); }, diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 5c46df17f4..3e2bd584a8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -1,3 +1,4 @@ +import { useDeleteOneFieldMetadataItem } from '@/object-metadata/hooks/useDeleteOneFieldMetadataItem'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; @@ -108,11 +109,10 @@ export const SettingsObjectFieldItemTableRow = ({ fieldName: fieldMetadataItem.name, }); - const { - activateMetadataField, - deactivateMetadataField, - deleteMetadataField, - } = useFieldMetadataItem(); + const { activateMetadataField, deactivateMetadataField } = + useFieldMetadataItem(); + + const { deleteOneFieldMetadataItem } = useDeleteOneFieldMetadataItem(); const handleDisableField = async ( activeFieldMetadatItem: FieldMetadataItem, @@ -293,7 +293,12 @@ export const SettingsObjectFieldItemTableRow = ({ onActivate={() => activateMetadataField(fieldMetadataItem.id, objectMetadataItem.id) } - onDelete={() => deleteMetadataField(fieldMetadataItem)} + onDelete={() => + deleteOneFieldMetadataItem({ + idToDelete: fieldMetadataItem.id, + objectMetadataId: objectMetadataItem.id, + }) + } /> ) : ( { @@ -86,7 +86,7 @@ export const usePersistViewFilterRecords = () => { variables: { id: viewFilter.id, input: { - value: viewFilter.value, + value: parseJson(viewFilter.value), operand: convertViewFilterOperandToCore(viewFilter.operand), positionInViewFilterGroup: viewFilter.positionInViewFilterGroup, viewFilterGroupId: viewFilter.viewFilterGroupId, diff --git a/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx index 5b8e9d6859..a8a0f90a41 100644 --- a/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx +++ b/packages/twenty-front/src/modules/views/hooks/useComputeRecordRelationFilterLabelValue.tsx @@ -66,7 +66,7 @@ export const useComputeRecordRelationFilterLabelValue = ({ }); if (loading) { - return { labelValue: t`Loading...` }; + return { labelValue: t`: Loading...` }; } const labelValueItems = [ @@ -83,9 +83,13 @@ export const useComputeRecordRelationFilterLabelValue = ({ labelValue: labelValueItems.length > 0 ? getRecordFilterLabelValue({ - ...recordFilter, - displayValue: filterDisplayValue, + recordFilter: { + ...recordFilter, + displayValue: filterDisplayValue, + }, }) - : getRecordFilterLabelValue(recordFilter), + : getRecordFilterLabelValue({ + recordFilter, + }), }; }; diff --git a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts index 6eadeff93e..73bba01984 100644 --- a/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts +++ b/packages/twenty-front/src/modules/views/hooks/useRefreshCoreViewsByObjectMetadataId.ts @@ -1,4 +1,15 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { currentRecordFieldsComponentState } from '@/object-record/record-field/states/currentRecordFieldsComponentState'; +import { currentRecordFiltersComponentState } from '@/object-record/record-filter/states/currentRecordFiltersComponentState'; +import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState'; +import { getRecordIndexIdFromObjectNamePluralAndViewId } from '@/object-record/utils/getRecordIndexIdFromObjectNamePluralAndViewId'; import { coreViewsByObjectMetadataIdFamilySelector } from '@/views/states/coreViewsByObjectMetadataIdFamilySelector'; +import { convertCoreViewToView } from '@/views/utils/convertCoreViewToView'; +import { getFilterableFieldsWithVectorSearch } from '@/views/utils/getFilterableFieldsWithVectorSearch'; + +import { mapViewFieldToRecordField } from '@/views/utils/mapViewFieldToRecordField'; +import { mapViewFiltersToFilters } from '@/views/utils/mapViewFiltersToFilters'; +import { mapViewSortsToSorts } from '@/views/utils/mapViewSortsToSorts'; import { useRecoilCallback } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { useFindManyCoreViewsLazyQuery } from '~/generated/graphql'; @@ -17,6 +28,22 @@ export const useRefreshCoreViewsByObjectMetadataId = () => { fetchPolicy: 'network-only', }); + if (!isDefined(result.data?.getCoreViews)) { + return; + } + + const objectMetadataItems = snapshot + .getLoadable(objectMetadataItemsState) + .getValue(); + + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => objectMetadataItem.id === objectMetadataId, + ); + + if (!isDefined(objectMetadataItem)) { + return; + } + const coreViewsForObjectMetadataId = snapshot .getLoadable( coreViewsByObjectMetadataIdFamilySelector(objectMetadataId), @@ -24,13 +51,69 @@ export const useRefreshCoreViewsByObjectMetadataId = () => { .getValue(); if ( - isDefined(result.data?.getCoreViews) && - !isDeeplyEqual(coreViewsForObjectMetadataId, result.data.getCoreViews) + isDeeplyEqual(coreViewsForObjectMetadataId, result.data.getCoreViews) ) { - set( - coreViewsByObjectMetadataIdFamilySelector(objectMetadataId), - result.data.getCoreViews, + return; + } + + set( + coreViewsByObjectMetadataIdFamilySelector(objectMetadataId), + result.data.getCoreViews, + ); + + for (const coreView of result.data.getCoreViews) { + const existingView = coreViewsForObjectMetadataId.find( + (coreViewForObjectMetadata) => + coreViewForObjectMetadata.id === coreView.id, ); + + if (!isDefined(existingView)) { + continue; + } + + if (!isDeeplyEqual(coreView.viewFields, existingView.viewFields)) { + const view = convertCoreViewToView(coreView); + set( + currentRecordFieldsComponentState.atomFamily({ + instanceId: getRecordIndexIdFromObjectNamePluralAndViewId( + objectMetadataItem.namePlural, + view.id, + ), + }), + view.viewFields + .filter(isDefined) + .map((viewField) => mapViewFieldToRecordField(viewField)), + ); + } + + if (!isDeeplyEqual(coreView.viewFilters, existingView.viewFilters)) { + const view = convertCoreViewToView(coreView); + set( + currentRecordFiltersComponentState.atomFamily({ + instanceId: getRecordIndexIdFromObjectNamePluralAndViewId( + objectMetadataItem.namePlural, + view.id, + ), + }), + mapViewFiltersToFilters( + view.viewFilters, + getFilterableFieldsWithVectorSearch(objectMetadataItem), + ), + ); + } + + if (!isDeeplyEqual(coreView.viewSorts, existingView.viewSorts)) { + const view = convertCoreViewToView(coreView); + set( + currentRecordSortsComponentState.atomFamily({ + instanceId: getRecordIndexIdFromObjectNamePluralAndViewId( + objectMetadataItem.namePlural, + view.id, + ), + }), + mapViewSortsToSorts(view.viewSorts), + ); + } } }, [findManyCoreViewsLazy], diff --git a/packages/twenty-front/src/modules/views/hooks/useRefreshViews.ts b/packages/twenty-front/src/modules/views/hooks/useRefreshViews.ts deleted file mode 100644 index 9249eff7e4..0000000000 --- a/packages/twenty-front/src/modules/views/hooks/useRefreshViews.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; -import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory'; - -export const useRefreshCachedViews = () => { - const { objectMetadataItems } = useObjectMetadataItems(); - - const findAllViewsOperationSignature = findAllViewsOperationSignatureFactory({ - objectMetadataItem: objectMetadataItems.find( - (item) => item.nameSingular === CoreObjectNameSingular.View, - ), - }); - - const { findManyRecordsLazy: refreshCachedViews } = useLazyFindManyRecords({ - objectNameSingular: CoreObjectNameSingular.View, - filter: findAllViewsOperationSignature.variables.filter, - recordGqlFields: findAllViewsOperationSignature.fields, - fetchPolicy: 'network-only', - }); - - return { - refreshCachedViews, - }; -}; diff --git a/packages/twenty-front/src/modules/views/utils/getRecordFilterLabelValue.ts b/packages/twenty-front/src/modules/views/utils/getRecordFilterLabelValue.ts index 055e02fb59..55894e1d04 100644 --- a/packages/twenty-front/src/modules/views/utils/getRecordFilterLabelValue.ts +++ b/packages/twenty-front/src/modules/views/utils/getRecordFilterLabelValue.ts @@ -1,10 +1,17 @@ +import { type FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; import { type RecordFilter } from '@/object-record/record-filter/types/RecordFilter'; import { RecordFilterOperand } from '@/object-record/record-filter/types/RecordFilterOperand'; import { isEmptinessOperand } from '@/object-record/record-filter/utils/isEmptinessOperand'; import { isRecordFilterConsideredEmpty } from '@/object-record/record-filter/utils/isRecordFilterConsideredEmpty'; -export const getRecordFilterLabelValue = (recordFilter: RecordFilter) => { +export const getRecordFilterLabelValue = ({ + recordFilter, + fieldMetadataOptions, +}: { + recordFilter: RecordFilter; + fieldMetadataOptions?: FieldMetadataItemOption[]; +}) => { const operandLabelShort = getOperandLabelShort(recordFilter.operand); const operandIsEmptiness = isEmptinessOperand(recordFilter.operand); const recordFilterIsEmpty = isRecordFilterConsideredEmpty(recordFilter); @@ -22,6 +29,20 @@ export const getRecordFilterLabelValue = (recordFilter: RecordFilter) => { return `${operandLabelShort} ${recordFilter.displayValue}`; } } + if (recordFilter.type === 'SELECT' || recordFilter.type === 'MULTI_SELECT') { + const valueArray = JSON.parse(recordFilter.value); + + if (!Array.isArray(valueArray)) { + return ''; + } + + const optionLabels = valueArray.map( + (value) => + fieldMetadataOptions?.find((option) => option.value === value)?.label, + ); + + return `${operandLabelShort} ${optionLabels.join(', ')}`; + } if (!operandIsEmptiness && !recordFilterIsEmpty) { return `${operandLabelShort} ${recordFilter.displayValue}`; diff --git a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts b/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts index 1199c73503..0c7f4661f8 100644 --- a/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts +++ b/packages/twenty-server/src/database/commands/views-migration/migrate-views-to-core.command.ts @@ -8,7 +8,6 @@ import { ActiveOrSuspendedWorkspacesMigrationCommandRunner, type RunOnWorkspaceArgs, } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity'; @@ -77,11 +76,10 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat if (options.dryRun) { this.logger.log( - `DRY RUN: Would enable IS_CORE_VIEW_SYNCING_ENABLED feature flag for workspace ${workspaceId}`, + `DRY RUN: Would migrate views to core schema for workspace ${workspaceId}`, ); } else { await queryRunner.commitTransaction(); - await this.enableCoreViewSyncingFeatureFlag(workspaceId); this.logger.log( `Successfully migrated views to core schema for workspace ${workspaceId}`, ); @@ -670,17 +668,4 @@ export class MigrateViewsToCoreCommand extends ActiveOrSuspendedWorkspacesMigrat await repository.insert(coreViewFilterGroup); } } - - private async enableCoreViewSyncingFeatureFlag( - workspaceId: string, - ): Promise { - await this.featureFlagService.enableFeatureFlags( - [FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED], - workspaceId, - ); - - this.logger.log( - `Enabled IS_CORE_VIEW_SYNCING_ENABLED feature flag for workspace ${workspaceId}`, - ); - } } diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts index dfb1cd0e30..7673713fc1 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-field.controller.ts @@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-field.input'; import { UpdateViewFieldInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-field.input'; import { type ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; +import { + generateViewFieldExceptionMessage, + generateViewFieldUserFriendlyExceptionMessage, + ViewFieldException, + ViewFieldExceptionCode, + ViewFieldExceptionMessageKey, +} from 'src/engine/core-modules/view/exceptions/view-field.exception'; import { ViewFieldRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-field-rest-api-exception.filter'; import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -44,8 +51,25 @@ export class ViewFieldController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewFieldService.findById(id, workspace.id); + ): Promise { + const viewField = await this.viewFieldService.findById(id, workspace.id); + + if (!isDefined(viewField)) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + id, + ), + ViewFieldExceptionCode.VIEW_FIELD_NOT_FOUND, + { + userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, + ), + }, + ); + } + + return viewField; } @Patch(':id') diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter-group.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter-group.controller.ts index 2147cbff82..d5eb445338 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter-group.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter-group.controller.ts @@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter-group.input'; import { UpdateViewFilterGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter-group.input'; import { type ViewFilterGroupDTO } from 'src/engine/core-modules/view/dtos/view-filter-group.dto'; +import { + generateViewFilterGroupExceptionMessage, + generateViewFilterGroupUserFriendlyExceptionMessage, + ViewFilterGroupException, + ViewFilterGroupExceptionCode, + ViewFilterGroupExceptionMessageKey, +} from 'src/engine/core-modules/view/exceptions/view-filter-group.exception'; import { ViewFilterGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-group-rest-api-exception.filter'; import { ViewFilterGroupService } from 'src/engine/core-modules/view/services/view-filter-group.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -46,8 +53,29 @@ export class ViewFilterGroupController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewFilterGroupService.findById(id, workspace.id); + ): Promise { + const viewFilterGroup = await this.viewFilterGroupService.findById( + id, + workspace.id, + ); + + if (!isDefined(viewFilterGroup)) { + throw new ViewFilterGroupException( + generateViewFilterGroupExceptionMessage( + ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND, + id, + ), + ViewFilterGroupExceptionCode.VIEW_FILTER_GROUP_NOT_FOUND, + { + userFriendlyMessage: + generateViewFilterGroupUserFriendlyExceptionMessage( + ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND, + ), + }, + ); + } + + return viewFilterGroup; } @Post() diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter.controller.ts index c7545d6803..deed52dc6c 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-filter.controller.ts @@ -15,7 +15,14 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-filter.input'; import { UpdateViewFilterInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-filter.input'; -import { type ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto'; +import { ViewFilterDTO } from 'src/engine/core-modules/view/dtos/view-filter.dto'; +import { + generateViewFilterExceptionMessage, + generateViewFilterUserFriendlyExceptionMessage, + ViewFilterException, + ViewFilterExceptionCode, + ViewFilterExceptionMessageKey, +} from 'src/engine/core-modules/view/exceptions/view-filter.exception'; import { ViewFilterRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-filter-rest-api-exception.filter'; import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -44,8 +51,25 @@ export class ViewFilterController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewFilterService.findById(id, workspace.id); + ): Promise { + const viewFilter = await this.viewFilterService.findById(id, workspace.id); + + if (!isDefined(viewFilter)) { + throw new ViewFilterException( + generateViewFilterExceptionMessage( + ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND, + id, + ), + ViewFilterExceptionCode.VIEW_FILTER_NOT_FOUND, + { + userFriendlyMessage: generateViewFilterUserFriendlyExceptionMessage( + ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND, + ), + }, + ); + } + + return viewFilter; } @Post() diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-group.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-group.controller.ts index 531c72a43c..d2779de7d2 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-group.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-group.controller.ts @@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-group.input'; import { UpdateViewGroupInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-group.input'; import { type ViewGroupDTO } from 'src/engine/core-modules/view/dtos/view-group.dto'; +import { + generateViewGroupExceptionMessage, + generateViewGroupUserFriendlyExceptionMessage, + ViewGroupException, + ViewGroupExceptionCode, + ViewGroupExceptionMessageKey, +} from 'src/engine/core-modules/view/exceptions/view-group.exception'; import { ViewGroupRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-group-rest-api-exception.filter'; import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -44,8 +51,25 @@ export class ViewGroupController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewGroupService.findById(id, workspace.id); + ): Promise { + const viewGroup = await this.viewGroupService.findById(id, workspace.id); + + if (!isDefined(viewGroup)) { + throw new ViewGroupException( + generateViewGroupExceptionMessage( + ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND, + id, + ), + ViewGroupExceptionCode.VIEW_GROUP_NOT_FOUND, + { + userFriendlyMessage: generateViewGroupUserFriendlyExceptionMessage( + ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND, + ), + }, + ); + } + + return viewGroup; } @Post() diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view-sort.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view-sort.controller.ts index 98ed12e9b9..a908b41b6c 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view-sort.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view-sort.controller.ts @@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/create-view-sort.input'; import { UpdateViewSortInput } from 'src/engine/core-modules/view/dtos/inputs/update-view-sort.input'; import { type ViewSortDTO } from 'src/engine/core-modules/view/dtos/view-sort.dto'; +import { + ViewSortException, + ViewSortExceptionCode, + ViewSortExceptionMessageKey, + generateViewSortExceptionMessage, + generateViewSortUserFriendlyExceptionMessage, +} from 'src/engine/core-modules/view/exceptions/view-sort.exception'; import { ViewSortRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-sort-rest-api-exception.filter'; import { ViewSortService } from 'src/engine/core-modules/view/services/view-sort.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -44,8 +51,25 @@ export class ViewSortController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewSortService.findById(id, workspace.id); + ): Promise { + const viewSort = await this.viewSortService.findById(id, workspace.id); + + if (!isDefined(viewSort)) { + throw new ViewSortException( + generateViewSortExceptionMessage( + ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, + id, + ), + ViewSortExceptionCode.VIEW_SORT_NOT_FOUND, + { + userFriendlyMessage: generateViewSortUserFriendlyExceptionMessage( + ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, + ), + }, + ); + } + + return viewSort; } @Post() diff --git a/packages/twenty-server/src/engine/core-modules/view/controllers/view.controller.ts b/packages/twenty-server/src/engine/core-modules/view/controllers/view.controller.ts index 51a7e3a711..06d33430e4 100644 --- a/packages/twenty-server/src/engine/core-modules/view/controllers/view.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/view/controllers/view.controller.ts @@ -16,6 +16,13 @@ import { isDefined } from 'twenty-shared/utils'; import { CreateViewInput } from 'src/engine/core-modules/view/dtos/inputs/create-view.input'; import { UpdateViewInput } from 'src/engine/core-modules/view/dtos/inputs/update-view.input'; import { type ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto'; +import { + generateViewExceptionMessage, + generateViewUserFriendlyExceptionMessage, + ViewException, + ViewExceptionCode, + ViewExceptionMessageKey, +} from 'src/engine/core-modules/view/exceptions/view.exception'; import { ViewRestApiExceptionFilter } from 'src/engine/core-modules/view/filters/view-rest-api-exception.filter'; import { ViewService } from 'src/engine/core-modules/view/services/view.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -47,8 +54,25 @@ export class ViewController { async findOne( @Param('id') id: string, @AuthWorkspace() workspace: Workspace, - ): Promise { - return this.viewService.findById(id, workspace.id); + ): Promise { + const view = await this.viewService.findById(id, workspace.id); + + if (!isDefined(view)) { + throw new ViewException( + generateViewExceptionMessage( + ViewExceptionMessageKey.VIEW_NOT_FOUND, + id, + ), + ViewExceptionCode.VIEW_NOT_FOUND, + { + userFriendlyMessage: generateViewUserFriendlyExceptionMessage( + ViewExceptionMessageKey.VIEW_NOT_FOUND, + ), + }, + ); + } + + return view; } @Post() diff --git a/packages/twenty-server/src/engine/core-modules/view/exceptions/view-field.exception.ts b/packages/twenty-server/src/engine/core-modules/view/exceptions/view-field.exception.ts index e996e9f378..43e996104d 100644 --- a/packages/twenty-server/src/engine/core-modules/view/exceptions/view-field.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/view/exceptions/view-field.exception.ts @@ -25,6 +25,7 @@ export enum ViewFieldExceptionMessageKey { VIEW_FIELD_NOT_FOUND = 'VIEW_FIELD_NOT_FOUND', INVALID_VIEW_FIELD_DATA = 'INVALID_VIEW_FIELD_DATA', FIELD_METADATA_ID_REQUIRED = 'FIELD_METADATA_ID_REQUIRED', + VIEW_FIELD_ALREADY_EXISTS = 'VIEW_FIELD_ALREADY_EXISTS', } export const generateViewFieldExceptionMessage = ( @@ -42,6 +43,8 @@ export const generateViewFieldExceptionMessage = ( return `Invalid view field data${id ? ` for view field id: ${id}` : ''}`; case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED: return 'FieldMetadataId is required'; + case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS: + return 'View field already exists'; default: assertUnreachable(key); } @@ -57,5 +60,7 @@ export const generateViewFieldUserFriendlyExceptionMessage = ( return t`ViewId is required to create a view field.`; case ViewFieldExceptionMessageKey.FIELD_METADATA_ID_REQUIRED: return t`FieldMetadataId is required to create a view field.`; + case ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS: + return t`View field already exists.`; } }; diff --git a/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts b/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts index 49cf00982a..f7e954131e 100644 --- a/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts +++ b/packages/twenty-server/src/engine/core-modules/view/services/view-field.service.ts @@ -107,9 +107,29 @@ export class ViewFieldService { ); } - const viewField = this.viewFieldRepository.create(viewFieldData); + try { + const viewField = this.viewFieldRepository.create(viewFieldData); - return this.viewFieldRepository.save(viewField); + return await this.viewFieldRepository.save(viewField); + } catch (error) { + if ( + error.message.includes('duplicate key value violates unique constraint') + ) { + throw new ViewFieldException( + generateViewFieldExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS, + ), + ViewFieldExceptionCode.INVALID_VIEW_FIELD_DATA, + { + userFriendlyMessage: generateViewFieldUserFriendlyExceptionMessage( + ViewFieldExceptionMessageKey.VIEW_FIELD_ALREADY_EXISTS, + ), + }, + ); + } + + throw error; + } } async update( diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 3819c166f9..a9909c17df 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -10,6 +10,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { CoreViewModule } from 'src/engine/core-modules/view/view.module'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; @@ -35,7 +36,6 @@ import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace- import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; -import { ViewModule } from 'src/modules/view/view.module'; import { FieldMetadataEntity } from './field-metadata.entity'; @@ -60,7 +60,7 @@ import { FieldMetadataService } from './services/field-metadata.service'; TypeORMModule, ActorModule, FeatureFlagModule, - ViewModule, + CoreViewModule, PermissionsModule, WorkspaceMetadataCacheModule, WorkspaceMigrationV2Module, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts index b9dee7309d..836a662f01 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/__tests__/field-metadata-related-records.spec.ts @@ -1,8 +1,11 @@ import { type EachTestingContext } from 'twenty-shared/testing'; +import { type ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; +import { type ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service'; +import { type ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service'; +import { type ViewService } from 'src/engine/core-modules/view/services/view.service'; import { type FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; import { FieldMetadataRelatedRecordsService } from 'src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service'; -import { type TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; type GetOptionsDifferencesTestContext = EachTestingContext<{ oldOptions: FieldMetadataDefaultOption[]; @@ -21,11 +24,18 @@ type GetOptionsDifferencesTestContext = EachTestingContext<{ describe('FieldMetadataRelatedRecordsService', () => { describe('getOptionsDifferences', () => { let service: FieldMetadataRelatedRecordsService; - let twentyORMGlobalManager: TwentyORMGlobalManager; + let viewService: ViewService = {} as ViewService; + let viewFieldService: ViewFieldService = {} as ViewFieldService; + let viewFilterService: ViewFilterService = {} as ViewFilterService; + let viewGroupService: ViewGroupService = {} as ViewGroupService; beforeEach(() => { - twentyORMGlobalManager = {} as TwentyORMGlobalManager; - service = new FieldMetadataRelatedRecordsService(twentyORMGlobalManager); + service = new FieldMetadataRelatedRecordsService( + viewService, + viewFieldService, + viewFilterService, + viewGroupService, + ); }); const testCases: GetOptionsDifferencesTestContext[] = [ diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts index 3355127384..8c0406b7b4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata-related-records.service.ts @@ -1,11 +1,16 @@ import { Injectable } from '@nestjs/common'; -import isEmpty from 'lodash.isempty'; -import { MAX_OPTIONS_TO_DISPLAY } from 'twenty-shared/constants'; +import { isNonEmptyString } from '@sniptt/guards'; import { isDefined, parseJson } from 'twenty-shared/utils'; -import { In } from 'typeorm'; import { settings } from 'src/engine/constants/settings'; +import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity'; +import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; +import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; +import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service'; +import { ViewFilterService } from 'src/engine/core-modules/view/services/view-filter.service'; +import { ViewGroupService } from 'src/engine/core-modules/view/services/view-group.service'; +import { ViewService } from 'src/engine/core-modules/view/services/view.service'; import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, @@ -17,12 +22,6 @@ import { } from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; import { isSelectFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-select-field-metadata-type.util'; import { type SelectOrMultiSelectFieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/utils/is-select-or-multi-select-field-metadata.util'; -import { type WorkspaceEntityManager } from 'src/engine/twenty-orm/entity-manager/workspace-entity-manager'; -import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; -import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; -import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; type Differences = { created: T[]; @@ -37,7 +36,10 @@ type GetOptionsDifferences = Differences< @Injectable() export class FieldMetadataRelatedRecordsService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly viewService: ViewService, + private readonly viewFieldService: ViewFieldService, + private readonly viewFilterService: ViewFilterService, + private readonly viewGroupService: ViewGroupService, ) {} public async updateRelatedViewGroups( @@ -51,9 +53,9 @@ export class FieldMetadataRelatedRecordsService { ) { return; } - const views = await this.getFieldMetadataViewWithRelation( - newFieldMetadata, - 'viewGroups', + const objectMetadataViews = await this.viewService.findByObjectMetadataId( + newFieldMetadata.workspaceId, + newFieldMetadata.objectMetadataId, ); const { created, updated, deleted } = this.getOptionsDifferences( @@ -61,38 +63,38 @@ export class FieldMetadataRelatedRecordsService { newFieldMetadata.options ?? [], ); - const viewGroupRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - newFieldMetadata.workspaceId, - 'viewGroup', - ); - - for (const view of views) { + for (const view of objectMetadataViews) { if (view.viewGroups.length === 0) { continue; } + const valuesToDelete = deleted.map((option) => option.value); if (valuesToDelete.length > 0) { - await viewGroupRepository.delete({ - fieldMetadataId: newFieldMetadata.id, - fieldValue: In(valuesToDelete), - }); + for (const valueToDelete of valuesToDelete) { + const viewGroupsToDelete = view.viewGroups.filter( + (group) => group.fieldValue === valueToDelete, + ); + + for (const viewGroup of viewGroupsToDelete) { + await this.viewGroupService.destroy( + viewGroup.id, + newFieldMetadata.workspaceId, + ); + } + } } const maxPosition = this.getMaxPosition(view.viewGroups); - const viewGroupsToCreate = created.map((option, index) => - viewGroupRepository.create({ + for (const [index, option] of created.entries()) { + this.viewGroupService.create({ fieldMetadataId: newFieldMetadata.id, fieldValue: option.value, position: maxPosition + index, isVisible: true, viewId: view.id, - }), - ); - - if (viewGroupsToCreate.length > 0) { - await viewGroupRepository.insert(viewGroupsToCreate); + workspaceId: newFieldMetadata.workspaceId, + }); } for (const { old: oldOption, new: newOption } of updated) { @@ -106,37 +108,54 @@ export class FieldMetadataRelatedRecordsService { ); } - await viewGroupRepository.update( - { id: existingViewGroup.id }, + this.viewGroupService.update( + existingViewGroup.id, + newFieldMetadata.workspaceId, { fieldValue: newOption.value }, ); } - await this.syncNoValueViewGroup( - newFieldMetadata, - view, - viewGroupRepository, - ); + await this.syncNoValueViewGroup(newFieldMetadata, view); } } - private computeViewFilterDisplayValue( - newViewFilterOptions: FieldMetadataDefaultOption[], - ): string { - if (newViewFilterOptions.length > MAX_OPTIONS_TO_DISPLAY) { - return `${newViewFilterOptions.length} options`; - } + public async resetViewKanbanAggregateOperation( + fieldMetadata: Pick< + FieldMetadataEntity, + 'id' | 'workspaceId' | 'objectMetadataId' + >, + ): Promise { + const views = await this.viewService.findByObjectMetadataId( + fieldMetadata.workspaceId, + fieldMetadata.objectMetadataId, + ); - return newViewFilterOptions.map((option) => option.label).join(', '); + const viewsHavingFieldAsAggregateOperation = views.filter( + (view) => + view.kanbanAggregateOperationFieldMetadataId === fieldMetadata.id, + ); + + for (const view of viewsHavingFieldAsAggregateOperation) { + await this.viewService.update(view.id, fieldMetadata.workspaceId, { + kanbanAggregateOperationFieldMetadataId: null, + kanbanAggregateOperation: null, + }); + } } public async updateRelatedViewFilters( oldFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, newFieldMetadata: SelectOrMultiSelectFieldMetadataEntity, ): Promise { - const views = await this.getFieldMetadataViewWithRelation( - newFieldMetadata, - 'viewFilters', + const views = await this.viewService.findByObjectMetadataId( + newFieldMetadata.workspaceId, + newFieldMetadata.objectMetadataId, + ); + + const fieldMetadataViews = views.filter((view) => + view.viewFilters.some( + (filter) => filter.fieldMetadataId === newFieldMetadata.id, + ), ); const alsoCompareLabel = true; @@ -156,29 +175,29 @@ export class FieldMetadataRelatedRecordsService { return; } - const viewFilterRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - newFieldMetadata.workspaceId, - 'viewFilter', - ); - - for (const filter of views) { - if (filter.viewFilters.length === 0) { + for (const view of fieldMetadataViews) { + if (view.viewFilters.length === 0) { continue; } - for (const viewFilter of filter.viewFilters) { - const viewFilterValue = parseJson(viewFilter.value); + for (const viewFilter of view.viewFilters) { + if (!isDefined(viewFilter.value)) { + continue; + } - // Note below assertion could be removed after https://github.com/twentyhq/core-team-issues/issues/1009 completion - if (!isDefined(viewFilterValue) || !Array.isArray(viewFilterValue)) { + // TODO: all view filter value should be stored as JSON, this is ongoing work (we are missing a command to migrate the data) + const parsedValue = isNonEmptyString(viewFilter.value) + ? parseJson(viewFilter.value) + : viewFilter.value; + + if (!isDefined(parsedValue) || !Array.isArray(parsedValue)) { throw new FieldMetadataException( `Unexpected invalid view filter value for filter ${viewFilter.id}`, FieldMetadataExceptionCode.INTERNAL_SERVER_ERROR, ); } - const viewFilterOptions = viewFilterValue + const viewFilterOptions = parsedValue .map((value) => { if (!isDefined(oldFieldMetadata.options)) { return undefined; @@ -198,7 +217,10 @@ export class FieldMetadataRelatedRecordsService { ); if (afterDeleteViewFilterOptions.length === 0) { - await viewFilterRepository.delete({ id: viewFilter.id }); + await this.viewFilterService.destroy( + viewFilter.id, + newFieldMetadata.workspaceId, + ); continue; } @@ -213,18 +235,15 @@ export class FieldMetadataRelatedRecordsService { : viewFilterOption; }); - const displayValue = this.computeViewFilterDisplayValue( - afterUpdateAndDeleteViewFilterOptions, - ); - const value = JSON.stringify( - afterUpdateAndDeleteViewFilterOptions.map((option) => option.value), + const value = afterUpdateAndDeleteViewFilterOptions.map( + (option) => option.value, ); - await viewFilterRepository.update( - { id: viewFilter.id }, + await this.viewFilterService.update( + viewFilter.id, + newFieldMetadata.workspaceId, { value, - displayValue, }, ); } @@ -233,8 +252,7 @@ export class FieldMetadataRelatedRecordsService { async syncNoValueViewGroup( fieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - view: ViewWorkspaceEntity, - viewGroupRepository: WorkspaceRepository, + view: ViewEntity, ): Promise { const noValueGroup = view.viewGroups.find( (group) => group.fieldValue === '', @@ -242,17 +260,20 @@ export class FieldMetadataRelatedRecordsService { if (fieldMetadata.isNullable && !noValueGroup) { const maxPosition = this.getMaxPosition(view.viewGroups); - const newGroup = viewGroupRepository.create({ + + this.viewGroupService.create({ fieldMetadataId: fieldMetadata.id, fieldValue: '', position: maxPosition + 1, isVisible: true, viewId: view.id, + workspaceId: fieldMetadata.workspaceId, }); - - await viewGroupRepository.insert(newGroup); } else if (!fieldMetadata.isNullable && noValueGroup) { - await viewGroupRepository.delete({ id: noValueGroup.id }); + await this.viewGroupService.destroy( + noValueGroup.id, + fieldMetadata.workspaceId, + ); } } @@ -306,27 +327,7 @@ export class FieldMetadataRelatedRecordsService { return differences; } - private async getFieldMetadataViewWithRelation( - fieldMetadata: SelectOrMultiSelectFieldMetadataEntity, - relation: keyof Pick, - ): Promise { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - fieldMetadata.workspaceId, - 'view', - ); - - return viewRepository.find({ - where: { - [relation]: { - fieldMetadataId: fieldMetadata.id, - }, - }, - relations: [relation], - }); - } - - private getMaxPosition(viewGroups: ViewGroupWorkspaceEntity[]): number { + private getMaxPosition(viewGroups: ViewGroupEntity[]): number { return viewGroups.reduce((max, group) => Math.max(max, group.position), 0); } @@ -334,69 +335,53 @@ export class FieldMetadataRelatedRecordsService { createdFieldMetadatas: FieldMetadataEntity[], workspaceId: string, ) { - const workspaceDataSource = - await this.twentyORMGlobalManager.getDataSourceForWorkspace({ - workspaceId, - }); + const views = await this.viewService.findByWorkspaceId(workspaceId); - await workspaceDataSource.transaction( - async (workspaceEntityManager: WorkspaceEntityManager) => { - const viewsRepository = workspaceEntityManager.getRepository('view', { - shouldBypassPermissionChecks: true, - }); + for (const createdFieldMetadata of createdFieldMetadatas) { + const objectViews = views.filter( + (view) => + view.objectMetadataId === createdFieldMetadata.objectMetadataId, + ); - const viewFieldsRepository = workspaceEntityManager.getRepository( - 'viewField', - { - shouldBypassPermissionChecks: true, - }, - ); + if (objectViews.length === 0) { + return; + } - for (const createdFieldMetadata of createdFieldMetadatas) { - const views = await viewsRepository.find({ - where: { - objectMetadataId: createdFieldMetadata.objectMetadataId, - }, - }); + const indexView = objectViews.find((view) => view.key === ViewKey.INDEX); - if (!isEmpty(views)) { - const view = views[0]; - const existingViewFields = await viewFieldsRepository.find({ - where: { - viewId: view.id, - }, - }); + if (!indexView) { + return; + } + const isVisible = + indexView.viewFields.length < settings.maxVisibleViewFields; - const isVisible = - existingViewFields.length < settings.maxVisibleViewFields; + const createdFieldIsAlreadyInView = indexView.viewFields.some( + (existingViewField) => + existingViewField.fieldMetadataId === createdFieldMetadata.id, + ); - const createdFieldIsAlreadyInView = existingViewFields.some( - (existingViewField) => - existingViewField.fieldMetadataId === createdFieldMetadata.id, - ); + if (createdFieldIsAlreadyInView) { + continue; + } - if (!createdFieldIsAlreadyInView) { - const lastPosition = existingViewFields - .map((viewField) => viewField.position) - .reduce((acc, position) => { - if (position > acc) { - return position; - } - - return acc; - }, -1); - - await viewFieldsRepository.insert({ - fieldMetadataId: createdFieldMetadata.id, - position: lastPosition + 1, - isVisible, - size: 180, - viewId: view.id, - }); - } + const lastPosition = indexView.viewFields + .map((viewField) => viewField.position) + .reduce((acc, position) => { + if (position > acc) { + return position; } - } - }, - ); + + return acc; + }, -1); + + await this.viewFieldService.create({ + fieldMetadataId: createdFieldMetadata.id, + position: lastPosition + 1, + isVisible, + size: 180, + viewId: indexView.id, + workspaceId: createdFieldMetadata.workspaceId, + }); + } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts index 86768dede0..5e4131b117 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/services/field-metadata.service.ts @@ -72,7 +72,6 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; -import { ViewService } from 'src/modules/view/services/view.service'; type GenerateMigrationArgs = { fieldMetadata: FieldMetadataEntity< @@ -95,7 +94,6 @@ export class FieldMetadataService extends TypeOrmQueryService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} @@ -25,19 +35,14 @@ export class ObjectMetadataRelatedRecordsService { private async createView( objectMetadata: ObjectMetadataEntity, - ): Promise { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - objectMetadata.workspaceId, - 'view', - ); - - return await viewRepository.save({ + ): Promise { + return await this.viewService.create({ objectMetadataId: objectMetadata.id, - type: 'table', + type: ViewType.TABLE, name: `All ${objectMetadata.labelPlural}`, - key: 'INDEX', + key: ViewKey.INDEX, icon: 'IconList', + workspaceId: objectMetadata.workspaceId, }); } @@ -45,12 +50,6 @@ export class ObjectMetadataRelatedRecordsService { objectMetadata: ObjectMetadataEntity, viewId: string, ): Promise { - const viewFieldRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - objectMetadata.workspaceId, - 'viewField', - ); - const viewFields = objectMetadata.fields .filter((field) => field.name !== 'id' && field.name !== 'deletedAt') .map((field, index) => ({ @@ -59,9 +58,12 @@ export class ObjectMetadataRelatedRecordsService { isVisible: true, size: 180, viewId: viewId, + workspaceId: objectMetadata.workspaceId, })); - await viewFieldRepository.insert(viewFields); + for (const viewField of viewFields) { + await this.viewFieldService.create(viewField); + } } public async createViewWorkspaceFavorite( @@ -91,14 +93,12 @@ export class ObjectMetadataRelatedRecordsService { >, workspaceId: string, ) { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + await this.viewRepository.update( + { + objectMetadataId: updatedObjectMetadata.id, + key: ViewKey.INDEX, workspaceId, - 'view', - ); - - await viewRepository.update( - { objectMetadataId: updatedObjectMetadata.id, key: 'INDEX' }, + }, { name: `All ${updatedObjectMetadata.labelPlural}`, ...(isDefined(updatedObjectMetadata.icon) @@ -112,17 +112,9 @@ export class ObjectMetadataRelatedRecordsService { objectMetadata: ObjectMetadataEntity, workspaceId: string, ) { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'view', - { - shouldBypassPermissionChecks: true, - }, - ); - - await viewRepository.delete({ + await this.viewRepository.delete({ objectMetadataId: objectMetadata.id, + workspaceId, }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts index a7436e08da..95edbc0ed7 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util.ts @@ -55,11 +55,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, - { - key: FeatureFlagKey.IS_CORE_VIEW_ENABLED, - workspaceId: workspaceId, - value: true, - }, { key: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, workspaceId: workspaceId, @@ -70,11 +65,6 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, - { - key: FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED, - workspaceId: workspaceId, - value: true, - }, { key: FeatureFlagKey.IS_PAGE_LAYOUT_ENABLED, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts index 5969348c7e..3f190f81c8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.service.ts @@ -137,17 +137,13 @@ export class WorkspaceManagerService { featureFlags, ); - if (featureFlags[FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED]) { - this.logger.log(`Prefilling core views for workspace ${workspaceId}`); - - await prefillCoreViews({ - coreDataSource: this.coreDataSource, - workspaceId, - objectMetadataItems: createdObjectMetadata, - schemaName: dataSourceMetadata.schema, - featureFlags, - }); - } + await prefillCoreViews({ + coreDataSource: this.coreDataSource, + workspaceId, + objectMetadataItems: createdObjectMetadata, + schemaName: dataSourceMetadata.schema, + featureFlags, + }); } public async delete(workspaceId: string): Promise { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts index eee7e063d2..4b179f5cd9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts @@ -1,6 +1 @@ -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; - -export const DEFAULT_FEATURE_FLAGS = [ - FeatureFlagKey.IS_CORE_VIEW_ENABLED, - FeatureFlagKey.IS_CORE_VIEW_SYNCING_ENABLED, -]; +export const DEFAULT_FEATURE_FLAGS = []; diff --git a/packages/twenty-server/src/modules/view/listeners/base-view-sync.listener.ts b/packages/twenty-server/src/modules/view/listeners/base-view-sync.listener.ts deleted file mode 100644 index 103df972b9..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/base-view-sync.listener.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { Inject, Injectable, Logger } from '@nestjs/common'; - -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; -import { type WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { - ViewException, - ViewExceptionCode, -} from 'src/modules/view/views.exception'; - -type EntityWithId = { id: string }; - -type SyncOperations = { - create: (workspaceId: string, entity: T) => Promise; - update: ( - workspaceId: string, - entity: T, - diff?: Partial>, - ) => Promise; - delete: (workspaceId: string, entity: Pick) => Promise; - destroy: (workspaceId: string, entity: Pick) => Promise; - restore: (workspaceId: string, entity: Pick) => Promise; -}; - -@Injectable() -export abstract class BaseViewSyncListener { - @Inject(FeatureFlagService) - protected readonly featureFlagService: FeatureFlagService; - - @Inject(ExceptionHandlerService) - protected readonly exceptionHandlerService: ExceptionHandlerService; - - protected readonly logger: Logger; - - constructor( - protected readonly syncOperations: SyncOperations, - loggerName: string, - protected readonly entityTypeName: string, - ) { - this.logger = new Logger(loggerName); - } - - protected async handleCreated( - batchEvent: WorkspaceEventBatch>, - ): Promise { - const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId); - - if (!isEnabled) { - return; - } - - for (const event of batchEvent.events) { - try { - await this.syncOperations.create( - batchEvent.workspaceId, - event.properties.after, - ); - } catch (error) { - this.captureException( - error, - batchEvent.workspaceId, - 'create', - event.properties.after.id, - ); - } - } - } - - protected async handleUpdated( - batchEvent: WorkspaceEventBatch>, - ): Promise { - const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId); - - if (!isEnabled) { - return; - } - - for (const event of batchEvent.events) { - try { - await this.syncOperations.update( - batchEvent.workspaceId, - event.properties.after, - event.properties.diff, - ); - } catch (error) { - this.captureException( - error, - batchEvent.workspaceId, - 'update', - event.properties.after.id, - ); - } - } - } - - protected async handleDeleted( - batchEvent: WorkspaceEventBatch>, - ): Promise { - const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId); - - if (!isEnabled) { - return; - } - - for (const event of batchEvent.events) { - try { - await this.syncOperations.delete( - batchEvent.workspaceId, - event.properties.before, - ); - } catch (error) { - this.captureException( - error, - batchEvent.workspaceId, - 'delete', - event.properties.before.id, - ); - } - } - } - - protected async handleDestroyed( - batchEvent: WorkspaceEventBatch>, - ): Promise { - const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId); - - if (!isEnabled) { - return; - } - - for (const event of batchEvent.events) { - try { - await this.syncOperations.destroy( - batchEvent.workspaceId, - event.properties.before, - ); - } catch (error) { - this.captureException( - error, - batchEvent.workspaceId, - 'destroy', - event.properties.before.id, - ); - } - } - } - - protected async handleRestored( - batchEvent: WorkspaceEventBatch>, - ): Promise { - const isEnabled = await this.isFeatureFlagEnabled(batchEvent.workspaceId); - - if (!isEnabled) { - return; - } - - for (const event of batchEvent.events) { - try { - await this.syncOperations.restore( - batchEvent.workspaceId, - event.properties.after, - ); - } catch (error) { - this.captureException( - error, - batchEvent.workspaceId, - 'restore', - event.properties.after.id, - ); - } - } - } - - private async isFeatureFlagEnabled(workspaceId: string): Promise { - const featureFlags = - await this.featureFlagService.getWorkspaceFeatureFlagsMap(workspaceId); - - return featureFlags.IS_CORE_VIEW_SYNCING_ENABLED; - } - - private captureException( - error: Error, - workspaceId: string, - operation: string, - entityId: string, - ) { - const viewException = new ViewException( - `Failed to sync ${this.entityTypeName} ${entityId} to core: ${error.message}`, - ViewExceptionCode.CORE_VIEW_SYNC_ERROR, - ); - - this.exceptionHandlerService.captureExceptions([viewException], { - workspace: { - id: workspaceId, - }, - additionalData: { - entityId: entityId, - entityType: this.entityTypeName, - operation: operation, - }, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view-field.listener.ts b/packages/twenty-server/src/modules/view/listeners/view-field.listener.ts deleted file mode 100644 index 91292c9516..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view-field.listener.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service'; -import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewFieldListener extends BaseViewSyncListener { - constructor(viewFieldSyncService: ViewFieldSyncService) { - super( - { - create: - viewFieldSyncService.createCoreViewField.bind(viewFieldSyncService), - update: - viewFieldSyncService.updateCoreViewField.bind(viewFieldSyncService), - delete: - viewFieldSyncService.deleteCoreViewField.bind(viewFieldSyncService), - destroy: - viewFieldSyncService.destroyCoreViewField.bind(viewFieldSyncService), - restore: - viewFieldSyncService.restoreCoreViewField.bind(viewFieldSyncService), - }, - ViewFieldListener.name, - 'view field', - ); - } - - @OnDatabaseBatchEvent('viewField', DatabaseEventAction.CREATED) - async handleViewFieldCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('viewField', DatabaseEventAction.UPDATED) - async handleViewFieldUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('viewField', DatabaseEventAction.DELETED) - async handleViewFieldDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('viewField', DatabaseEventAction.DESTROYED) - async handleViewFieldDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('viewField', DatabaseEventAction.RESTORED) - async handleViewFieldRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view-filter-group.listener.ts b/packages/twenty-server/src/modules/view/listeners/view-filter-group.listener.ts deleted file mode 100644 index a8c6c9b453..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view-filter-group.listener.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service'; -import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewFilterGroupListener extends BaseViewSyncListener { - constructor(viewFilterGroupSyncService: ViewFilterGroupSyncService) { - super( - { - create: viewFilterGroupSyncService.createCoreViewFilterGroup.bind( - viewFilterGroupSyncService, - ), - update: viewFilterGroupSyncService.updateCoreViewFilterGroup.bind( - viewFilterGroupSyncService, - ), - delete: viewFilterGroupSyncService.deleteCoreViewFilterGroup.bind( - viewFilterGroupSyncService, - ), - destroy: viewFilterGroupSyncService.destroyCoreViewFilterGroup.bind( - viewFilterGroupSyncService, - ), - restore: viewFilterGroupSyncService.restoreCoreViewFilterGroup.bind( - viewFilterGroupSyncService, - ), - }, - ViewFilterGroupListener.name, - 'view filter group', - ); - } - - @OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.CREATED) - async handleViewFilterGroupCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.UPDATED) - async handleViewFilterGroupUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DELETED) - async handleViewFilterGroupDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DESTROYED) - async handleViewFilterGroupDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.RESTORED) - async handleViewFilterGroupRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view-filter.listener.ts b/packages/twenty-server/src/modules/view/listeners/view-filter.listener.ts deleted file mode 100644 index 8117d6531d..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view-filter.listener.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service'; -import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewFilterListener extends BaseViewSyncListener { - constructor(viewFilterSyncService: ViewFilterSyncService) { - super( - { - create: viewFilterSyncService.createCoreViewFilter.bind( - viewFilterSyncService, - ), - update: viewFilterSyncService.updateCoreViewFilter.bind( - viewFilterSyncService, - ), - delete: viewFilterSyncService.deleteCoreViewFilter.bind( - viewFilterSyncService, - ), - destroy: viewFilterSyncService.destroyCoreViewFilter.bind( - viewFilterSyncService, - ), - restore: viewFilterSyncService.restoreCoreViewFilter.bind( - viewFilterSyncService, - ), - }, - ViewFilterListener.name, - 'view filter', - ); - } - - @OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.CREATED) - async handleViewFilterCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.UPDATED) - async handleViewFilterUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DELETED) - async handleViewFilterDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DESTROYED) - async handleViewFilterDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.RESTORED) - async handleViewFilterRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view-group.listener.ts b/packages/twenty-server/src/modules/view/listeners/view-group.listener.ts deleted file mode 100644 index 7903da5722..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view-group.listener.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service'; -import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewGroupListener extends BaseViewSyncListener { - constructor(viewGroupSyncService: ViewGroupSyncService) { - super( - { - create: - viewGroupSyncService.createCoreViewGroup.bind(viewGroupSyncService), - update: - viewGroupSyncService.updateCoreViewGroup.bind(viewGroupSyncService), - delete: - viewGroupSyncService.deleteCoreViewGroup.bind(viewGroupSyncService), - destroy: - viewGroupSyncService.destroyCoreViewGroup.bind(viewGroupSyncService), - restore: - viewGroupSyncService.restoreCoreViewGroup.bind(viewGroupSyncService), - }, - ViewGroupListener.name, - 'view group', - ); - } - - @OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.CREATED) - async handleViewGroupCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.UPDATED) - async handleViewGroupUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DELETED) - async handleViewGroupDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DESTROYED) - async handleViewGroupDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.RESTORED) - async handleViewGroupRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view-sort.listener.ts b/packages/twenty-server/src/modules/view/listeners/view-sort.listener.ts deleted file mode 100644 index f562c533f8..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view-sort.listener.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service'; -import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewSortListener extends BaseViewSyncListener { - constructor(viewSortSyncService: ViewSortSyncService) { - super( - { - create: - viewSortSyncService.createCoreViewSort.bind(viewSortSyncService), - update: - viewSortSyncService.updateCoreViewSort.bind(viewSortSyncService), - delete: - viewSortSyncService.deleteCoreViewSort.bind(viewSortSyncService), - destroy: - viewSortSyncService.destroyCoreViewSort.bind(viewSortSyncService), - restore: - viewSortSyncService.restoreCoreViewSort.bind(viewSortSyncService), - }, - ViewSortListener.name, - 'view sort', - ); - } - - @OnDatabaseBatchEvent('viewSort', DatabaseEventAction.CREATED) - async handleViewSortCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('viewSort', DatabaseEventAction.UPDATED) - async handleViewSortUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DELETED) - async handleViewSortDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DESTROYED) - async handleViewSortDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('viewSort', DatabaseEventAction.RESTORED) - async handleViewSortRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/listeners/view.listener.ts b/packages/twenty-server/src/modules/view/listeners/view.listener.ts deleted file mode 100644 index a2339f5f90..0000000000 --- a/packages/twenty-server/src/modules/view/listeners/view.listener.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { OnDatabaseBatchEvent } from 'src/engine/api/graphql/graphql-query-runner/decorators/on-database-batch-event.decorator'; -import { DatabaseEventAction } from 'src/engine/api/graphql/graphql-query-runner/enums/database-event-action'; -import { type ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; -import { type ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; -import { type ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; -import { type ObjectRecordRestoreEvent } from 'src/engine/core-modules/event-emitter/types/object-record-restore.event'; -import { type ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; -import { WorkspaceEventBatch } from 'src/engine/workspace-event-emitter/types/workspace-event.type'; -import { ViewSyncService } from 'src/modules/view/services/view-sync.service'; -import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; - -import { BaseViewSyncListener } from './base-view-sync.listener'; - -@Injectable() -export class ViewListener extends BaseViewSyncListener { - constructor(viewSyncService: ViewSyncService) { - super( - { - create: viewSyncService.createCoreView.bind(viewSyncService), - update: viewSyncService.updateCoreView.bind(viewSyncService), - delete: viewSyncService.deleteCoreView.bind(viewSyncService), - destroy: viewSyncService.destroyCoreView.bind(viewSyncService), - restore: viewSyncService.restoreCoreView.bind(viewSyncService), - }, - ViewListener.name, - 'view', - ); - } - - @OnDatabaseBatchEvent('view', DatabaseEventAction.CREATED) - async handleViewCreated( - batchEvent: WorkspaceEventBatch< - ObjectRecordCreateEvent - >, - ) { - return this.handleCreated(batchEvent); - } - - @OnDatabaseBatchEvent('view', DatabaseEventAction.UPDATED) - async handleViewUpdated( - batchEvent: WorkspaceEventBatch< - ObjectRecordUpdateEvent - >, - ) { - return this.handleUpdated(batchEvent); - } - - @OnDatabaseBatchEvent('view', DatabaseEventAction.DELETED) - async handleViewDeleted( - batchEvent: WorkspaceEventBatch< - ObjectRecordDeleteEvent - >, - ) { - return this.handleDeleted(batchEvent); - } - - @OnDatabaseBatchEvent('view', DatabaseEventAction.DESTROYED) - async handleViewDestroyed( - batchEvent: WorkspaceEventBatch< - ObjectRecordDestroyEvent - >, - ) { - return this.handleDestroyed(batchEvent); - } - - @OnDatabaseBatchEvent('view', DatabaseEventAction.RESTORED) - async handleViewRestored( - batchEvent: WorkspaceEventBatch< - ObjectRecordRestoreEvent - >, - ) { - return this.handleRestored(batchEvent); - } -} diff --git a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-many.pre-query.hook.ts b/packages/twenty-server/src/modules/view/pre-hooks/view-delete-many.pre-query.hook.ts deleted file mode 100644 index 422b3f7898..0000000000 --- a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-many.pre-query.hook.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; -import { type DeleteManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; - -@WorkspaceQueryHook(`view.deleteMany`) -export class ViewDeleteManyPreQueryHook - implements WorkspacePreQueryHookInstance -{ - constructor() {} - - async execute( - _authContext: AuthContext, - _objectName: string, - _payload: DeleteManyResolverArgs, - ): Promise { - throw new GraphqlQueryRunnerException( - 'Method not implemented', - GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED, - ); - } -} diff --git a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts deleted file mode 100644 index 20f4714084..0000000000 --- a/packages/twenty-server/src/modules/view/pre-hooks/view-delete-one.pre-query.hook.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { type WorkspacePreQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface'; -import { type DeleteOneResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; - -import { - GraphqlQueryRunnerException, - GraphqlQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; -import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator'; -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -@WorkspaceQueryHook(`view.deleteOne`) -export class ViewDeleteOnePreQueryHook - implements WorkspacePreQueryHookInstance -{ - constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - ) {} - - async execute( - authContext: AuthContext, - _objectName: string, - payload: DeleteOneResolverArgs, - ): Promise { - const workspace = authContext.workspace; - - workspaceValidator.assertIsDefinedOrThrow(workspace); - - const targettedViewId = payload.id; - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspace.id, - 'view', - ); - - const view = await viewRepository.findOne({ - where: { id: targettedViewId }, - }); - - if (!view) { - throw new GraphqlQueryRunnerException( - 'View not found', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - if (view.key === 'INDEX') { - throw new GraphqlQueryRunnerException( - 'Cannot delete INDEX view', - GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - return payload; - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-field-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-field-sync.service.ts deleted file mode 100644 index 4a5e22bf78..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-field-sync.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewFieldEntity } from 'src/engine/core-modules/view/entities/view-field.entity'; -import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; - -@Injectable() -export class ViewFieldSyncService { - constructor( - @InjectRepository(ViewFieldEntity) - private readonly coreViewFieldRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - const diffValue = diff[key as keyof ViewFieldWorkspaceEntity]; - - if (isDefined(diffValue)) { - updateData[key] = diffValue.after; - } - } - - return updateData as Partial; - } - - public async createCoreViewField( - workspaceId: string, - workspaceViewField: ViewFieldWorkspaceEntity, - ): Promise { - const coreViewField: Partial = { - id: workspaceViewField.id, - fieldMetadataId: workspaceViewField.fieldMetadataId, - viewId: workspaceViewField.viewId, - position: workspaceViewField.position, - isVisible: workspaceViewField.isVisible, - size: workspaceViewField.size, - workspaceId, - createdAt: new Date(workspaceViewField.createdAt), - updatedAt: new Date(workspaceViewField.updatedAt), - deletedAt: workspaceViewField.deletedAt - ? new Date(workspaceViewField.deletedAt) - : null, - }; - - await this.coreViewFieldRepository.save(coreViewField); - } - - public async updateCoreViewField( - workspaceId: string, - workspaceViewField: Pick, - diff?: Partial>, - ): Promise { - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewFieldRepository.update( - { id: workspaceViewField.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreViewField( - workspaceId: string, - workspaceViewField: Pick, - ): Promise { - await this.coreViewFieldRepository.softDelete({ - id: workspaceViewField.id, - workspaceId, - }); - } - - public async destroyCoreViewField( - workspaceId: string, - workspaceViewField: Pick, - ): Promise { - await this.coreViewFieldRepository.delete({ - id: workspaceViewField.id, - workspaceId, - }); - } - - public async restoreCoreViewField( - workspaceId: string, - workspaceViewField: Pick, - ): Promise { - await this.coreViewFieldRepository.restore({ - id: workspaceViewField.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-filter-group-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-filter-group-sync.service.ts deleted file mode 100644 index 6ebd6aeff6..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-filter-group-sync.service.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewFilterGroupEntity } from 'src/engine/core-modules/view/entities/view-filter-group.entity'; -import { type ViewFilterGroupLogicalOperator } from 'src/engine/core-modules/view/enums/view-filter-group-logical-operator'; -import { type ViewFilterGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter-group.workspace-entity'; - -@Injectable() -export class ViewFilterGroupSyncService { - constructor( - @InjectRepository(ViewFilterGroupEntity) - private readonly coreViewFilterGroupRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - const diffValue = diff[key as keyof ViewFilterGroupWorkspaceEntity]; - - if (isDefined(diffValue)) { - if (key === 'logicalOperator') { - updateData[key] = diffValue.after as ViewFilterGroupLogicalOperator; - } else { - updateData[key] = diffValue.after; - } - } - } - - return updateData as Partial; - } - - public async createCoreViewFilterGroup( - workspaceId: string, - workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity, - ): Promise { - const coreViewFilterGroup: Partial = { - id: workspaceViewFilterGroup.id, - viewId: workspaceViewFilterGroup.viewId, - logicalOperator: - workspaceViewFilterGroup.logicalOperator as ViewFilterGroupLogicalOperator, - parentViewFilterGroupId: workspaceViewFilterGroup.parentViewFilterGroupId, - positionInViewFilterGroup: - workspaceViewFilterGroup.positionInViewFilterGroup, - workspaceId, - createdAt: new Date(workspaceViewFilterGroup.createdAt), - updatedAt: new Date(workspaceViewFilterGroup.updatedAt), - deletedAt: workspaceViewFilterGroup.deletedAt - ? new Date(workspaceViewFilterGroup.deletedAt) - : null, - }; - - await this.coreViewFilterGroupRepository.save(coreViewFilterGroup); - } - - public async updateCoreViewFilterGroup( - workspaceId: string, - workspaceViewFilterGroup: Pick, - diff?: Partial>, - ): Promise { - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewFilterGroupRepository.update( - { id: workspaceViewFilterGroup.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreViewFilterGroup( - workspaceId: string, - workspaceViewFilterGroup: Pick, - ): Promise { - await this.coreViewFilterGroupRepository.softDelete({ - id: workspaceViewFilterGroup.id, - workspaceId, - }); - } - - public async destroyCoreViewFilterGroup( - workspaceId: string, - workspaceViewFilterGroup: Pick, - ): Promise { - await this.coreViewFilterGroupRepository.delete({ - id: workspaceViewFilterGroup.id, - workspaceId, - }); - } - - public async restoreCoreViewFilterGroup( - workspaceId: string, - workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity, - ): Promise { - await this.coreViewFilterGroupRepository.restore({ - id: workspaceViewFilterGroup.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-filter-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-filter-sync.service.ts deleted file mode 100644 index 0651b74f8e..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-filter-sync.service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { type ViewFilterOperand as SharedViewFilterOperand } from 'twenty-shared/types'; -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewFilterEntity } from 'src/engine/core-modules/view/entities/view-filter.entity'; -import { type ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; -import { convertViewFilterOperandToCoreOperand } from 'src/modules/view/utils/convert-view-filter-operand-to-core-operand.util'; -import { convertViewFilterWorkspaceValueToCoreValue } from 'src/modules/view/utils/convert-view-filter-workspace-value-to-core-value'; - -@Injectable() -export class ViewFilterSyncService { - constructor( - @InjectRepository(ViewFilterEntity) - private readonly coreViewFilterRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - if (key === 'displayValue') { - continue; - } - - const diffValue = diff[key as keyof ViewFilterWorkspaceEntity]; - - if (isDefined(diffValue)) { - if (key === 'value' && typeof diffValue.after === 'string') { - updateData[key] = convertViewFilterWorkspaceValueToCoreValue( - diffValue.after, - ); - } else if (key === 'operand' && diffValue.after) { - updateData[key] = convertViewFilterOperandToCoreOperand( - diffValue.after as SharedViewFilterOperand, - ); - } else { - updateData[key] = diffValue.after; - } - } - } - - return updateData as Partial; - } - - public async createCoreViewFilter( - workspaceId: string, - workspaceViewFilter: ViewFilterWorkspaceEntity, - ): Promise { - if (!workspaceViewFilter.viewId) { - return; - } - - const coreViewFilter: Partial = { - id: workspaceViewFilter.id, - fieldMetadataId: workspaceViewFilter.fieldMetadataId, - viewId: workspaceViewFilter.viewId, - operand: convertViewFilterOperandToCoreOperand( - workspaceViewFilter.operand as SharedViewFilterOperand, - ), - value: convertViewFilterWorkspaceValueToCoreValue( - workspaceViewFilter.value, - ), - viewFilterGroupId: workspaceViewFilter.viewFilterGroupId, - workspaceId, - createdAt: new Date(workspaceViewFilter.createdAt), - updatedAt: new Date(workspaceViewFilter.updatedAt), - deletedAt: workspaceViewFilter.deletedAt - ? new Date(workspaceViewFilter.deletedAt) - : null, - }; - - await this.coreViewFilterRepository.save(coreViewFilter); - } - - public async updateCoreViewFilter( - workspaceId: string, - workspaceViewFilter: ViewFilterWorkspaceEntity, - diff?: Partial>, - ): Promise { - if (!workspaceViewFilter.viewId) { - return; - } - - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewFilterRepository.update( - { id: workspaceViewFilter.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreViewFilter( - workspaceId: string, - workspaceViewFilter: Pick, - ): Promise { - await this.coreViewFilterRepository.softDelete({ - id: workspaceViewFilter.id, - workspaceId, - }); - } - - public async destroyCoreViewFilter( - workspaceId: string, - workspaceViewFilter: Pick, - ): Promise { - await this.coreViewFilterRepository.delete({ - id: workspaceViewFilter.id, - workspaceId, - }); - } - - public async restoreCoreViewFilter( - workspaceId: string, - workspaceViewFilter: Pick, - ): Promise { - await this.coreViewFilterRepository.restore({ - id: workspaceViewFilter.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-group-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-group-sync.service.ts deleted file mode 100644 index 8db6f7ef9b..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-group-sync.service.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-group.entity'; -import { type ViewGroupWorkspaceEntity } from 'src/modules/view/standard-objects/view-group.workspace-entity'; - -@Injectable() -export class ViewGroupSyncService { - constructor( - @InjectRepository(ViewGroupEntity) - private readonly coreViewGroupRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - const diffValue = diff[key as keyof ViewGroupWorkspaceEntity]; - - if (isDefined(diffValue)) { - updateData[key] = diffValue.after; - } - } - - return updateData as Partial; - } - - public async createCoreViewGroup( - workspaceId: string, - workspaceViewGroup: ViewGroupWorkspaceEntity, - ): Promise { - if (!workspaceViewGroup.viewId) { - return; - } - - const coreViewGroup: Partial = { - id: workspaceViewGroup.id, - fieldMetadataId: workspaceViewGroup.fieldMetadataId, - viewId: workspaceViewGroup.viewId, - fieldValue: workspaceViewGroup.fieldValue, - isVisible: workspaceViewGroup.isVisible, - position: workspaceViewGroup.position, - workspaceId, - createdAt: new Date(workspaceViewGroup.createdAt), - updatedAt: new Date(workspaceViewGroup.updatedAt), - deletedAt: workspaceViewGroup.deletedAt - ? new Date(workspaceViewGroup.deletedAt) - : null, - }; - - await this.coreViewGroupRepository.save(coreViewGroup); - } - - public async updateCoreViewGroup( - workspaceId: string, - workspaceViewGroup: ViewGroupWorkspaceEntity, - diff?: Partial>, - ): Promise { - if (!workspaceViewGroup.viewId) { - return; - } - - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewGroupRepository.update( - { id: workspaceViewGroup.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreViewGroup( - workspaceId: string, - workspaceViewGroup: Pick, - ): Promise { - await this.coreViewGroupRepository.softDelete({ - id: workspaceViewGroup.id, - workspaceId, - }); - } - - public async destroyCoreViewGroup( - workspaceId: string, - workspaceViewGroup: Pick, - ): Promise { - await this.coreViewGroupRepository.delete({ - id: workspaceViewGroup.id, - workspaceId, - }); - } - - public async restoreCoreViewGroup( - workspaceId: string, - workspaceViewGroup: Pick, - ): Promise { - await this.coreViewGroupRepository.restore({ - id: workspaceViewGroup.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-sort-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-sort-sync.service.ts deleted file mode 100644 index 25cf19a8db..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-sort-sync.service.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity'; -import { type ViewSortDirection } from 'src/engine/core-modules/view/enums/view-sort-direction'; -import { type ViewSortWorkspaceEntity } from 'src/modules/view/standard-objects/view-sort.workspace-entity'; - -@Injectable() -export class ViewSortSyncService { - constructor( - @InjectRepository(ViewSortEntity) - private readonly coreViewSortRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - const diffValue = diff[key as keyof ViewSortWorkspaceEntity]; - - if (isDefined(diffValue)) { - if (key === 'direction') { - updateData[key] = ( - diffValue.after as string - ).toUpperCase() as ViewSortDirection; - } else { - updateData[key] = diffValue.after; - } - } - } - - return updateData as Partial; - } - - public async createCoreViewSort( - workspaceId: string, - workspaceViewSort: ViewSortWorkspaceEntity, - ): Promise { - if (!workspaceViewSort.viewId) { - return; - } - - const direction = - workspaceViewSort.direction.toUpperCase() as ViewSortDirection; - - const coreViewSort: Partial = { - id: workspaceViewSort.id, - fieldMetadataId: workspaceViewSort.fieldMetadataId, - viewId: workspaceViewSort.viewId, - direction: direction, - workspaceId, - createdAt: new Date(workspaceViewSort.createdAt), - updatedAt: new Date(workspaceViewSort.updatedAt), - deletedAt: workspaceViewSort.deletedAt - ? new Date(workspaceViewSort.deletedAt) - : null, - }; - - await this.coreViewSortRepository.save(coreViewSort); - } - - public async updateCoreViewSort( - workspaceId: string, - workspaceViewSort: ViewSortWorkspaceEntity, - diff?: Partial>, - ): Promise { - if (!workspaceViewSort.viewId) { - return; - } - - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewSortRepository.update( - { id: workspaceViewSort.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreViewSort( - workspaceId: string, - workspaceViewSort: Pick, - ): Promise { - await this.coreViewSortRepository.softDelete({ - id: workspaceViewSort.id, - workspaceId, - }); - } - - public async destroyCoreViewSort( - workspaceId: string, - workspaceViewSort: Pick, - ): Promise { - await this.coreViewSortRepository.delete({ - id: workspaceViewSort.id, - workspaceId, - }); - } - - public async restoreCoreViewSort( - workspaceId: string, - workspaceViewSort: Pick, - ): Promise { - await this.coreViewSortRepository.restore({ - id: workspaceViewSort.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view-sync.service.ts b/packages/twenty-server/src/modules/view/services/view-sync.service.ts deleted file mode 100644 index eaecb8c973..0000000000 --- a/packages/twenty-server/src/modules/view/services/view-sync.service.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; - -import { isDefined } from 'twenty-shared/utils'; -import { Repository } from 'typeorm'; - -import { type ObjectRecordDiff } from 'src/engine/core-modules/event-emitter/types/object-record-diff'; -import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; -import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; -import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; -import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; -import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; - -@Injectable() -export class ViewSyncService { - constructor( - @InjectRepository(ViewEntity) - private readonly coreViewRepository: Repository, - ) {} - - private parseUpdateDataFromDiff( - diff: Partial>, - ): Partial { - const updateData: Record = {}; - - for (const key of Object.keys(diff)) { - if (key === 'kanbanFieldMetadataId') { - continue; - } - - const diffValue = diff[key as keyof ViewWorkspaceEntity]; - - if (isDefined(diffValue)) { - if (key === 'openRecordIn') { - updateData[key] = - diffValue.after === 'SIDE_PANEL' - ? ViewOpenRecordIn.SIDE_PANEL - : ViewOpenRecordIn.RECORD_PAGE; - } else if (key === 'type') { - updateData[key] = - diffValue.after === 'table' ? ViewType.TABLE : ViewType.KANBAN; - } else if (key === 'key') { - updateData[key] = - diffValue.after === 'INDEX' || diffValue.after === ViewKey.INDEX - ? ViewKey.INDEX - : null; - } else { - updateData[key] = diffValue.after; - } - } - } - - return updateData as Partial; - } - - public async createCoreView( - workspaceId: string, - workspaceView: ViewWorkspaceEntity, - ): Promise { - let viewName = workspaceView.name; - - if (workspaceView.key === 'INDEX') { - // All INDEX views use the template for consistency - viewName = 'All {objectLabelPlural}'; - } - - const coreView: Partial = { - id: workspaceView.id, - name: viewName, - objectMetadataId: workspaceView.objectMetadataId, - type: workspaceView.type === 'table' ? ViewType.TABLE : ViewType.KANBAN, - key: - workspaceView.key === 'INDEX' || workspaceView.key === ViewKey.INDEX - ? ViewKey.INDEX - : null, - icon: workspaceView.icon, - position: workspaceView.position, - isCompact: workspaceView.isCompact, - isCustom: workspaceView.key !== 'INDEX', - openRecordIn: - workspaceView.openRecordIn === 'SIDE_PANEL' - ? ViewOpenRecordIn.SIDE_PANEL - : ViewOpenRecordIn.RECORD_PAGE, - kanbanAggregateOperation: workspaceView.kanbanAggregateOperation, - kanbanAggregateOperationFieldMetadataId: - workspaceView.kanbanAggregateOperationFieldMetadataId, - workspaceId, - createdAt: new Date(workspaceView.createdAt), - updatedAt: new Date(workspaceView.updatedAt), - deletedAt: workspaceView.deletedAt - ? new Date(workspaceView.deletedAt) - : null, - }; - - await this.coreViewRepository.save(coreView); - } - - public async updateCoreView( - workspaceId: string, - workspaceView: ViewWorkspaceEntity, - diff?: Partial>, - ): Promise { - if (!diff || Object.keys(diff).length === 0) { - return; - } - - const updateData = this.parseUpdateDataFromDiff(diff); - - if (Object.keys(updateData).length > 0) { - await this.coreViewRepository.update( - { id: workspaceView.id, workspaceId }, - updateData, - ); - } - } - - public async deleteCoreView( - workspaceId: string, - workspaceView: ViewWorkspaceEntity, - ): Promise { - await this.coreViewRepository.softDelete({ - id: workspaceView.id, - workspaceId, - }); - } - - public async destroyCoreView( - workspaceId: string, - workspaceView: ViewWorkspaceEntity, - ): Promise { - await this.coreViewRepository.delete({ - id: workspaceView.id, - workspaceId, - }); - } - - public async restoreCoreView( - workspaceId: string, - workspaceView: ViewWorkspaceEntity, - ): Promise { - await this.coreViewRepository.restore({ - id: workspaceView.id, - workspaceId, - }); - } -} diff --git a/packages/twenty-server/src/modules/view/services/view.service.ts b/packages/twenty-server/src/modules/view/services/view.service.ts deleted file mode 100644 index 1ba3e03b15..0000000000 --- a/packages/twenty-server/src/modules/view/services/view.service.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { isDefined } from 'class-validator'; -import isEmpty from 'lodash.isempty'; - -import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; - -@Injectable() -export class ViewService { - private readonly logger = new Logger(ViewService.name); - constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - ) {} - - async addFieldToViews({ - workspaceId, - fieldId, - viewsIds, - positions, - size, - }: { - workspaceId: string; - fieldId: string; - viewsIds: string[]; - positions?: { - [viewId: string]: number; - }[]; - size?: number; - }) { - const viewFieldRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'viewField', - ); - - for (const viewId of viewsIds) { - // @ts-expect-error legacy noImplicitAny - const position = positions?.[viewId]; - const newFieldInThisView = await viewFieldRepository.findBy({ - fieldMetadataId: fieldId, - viewId: viewId as string, - isVisible: true, - }); - - if (!isEmpty(newFieldInThisView)) { - continue; - } - - this.logger.log( - `Adding new field ${fieldId} to view ${viewId} for workspace ${workspaceId}...`, - ); - const newViewField = viewFieldRepository.create({ - viewId: viewId, - fieldMetadataId: fieldId, - isVisible: true, - ...(isDefined(position) && { position: position }), - ...(isDefined(size) && { size: size }), - }); - - await viewFieldRepository.save(newViewField); - this.logger.log( - `New field successfully added to view ${viewId} for workspace ${workspaceId}`, - ); - } - } - - async removeFieldFromViews({ - workspaceId, - fieldId, - }: { - workspaceId: string; - fieldId: string; - }) { - const viewFieldRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'viewField', - ); - const viewsWithField = await viewFieldRepository.find({ - where: { - fieldMetadataId: fieldId, - isVisible: true, - }, - }); - - for (const viewWithField of viewsWithField) { - const viewId = viewWithField.viewId; - - this.logger.log( - `Removing field ${fieldId} from view ${viewId} for workspace ${workspaceId}...`, - ); - await viewFieldRepository.delete({ - viewId: viewWithField.viewId as string, - fieldMetadataId: fieldId, - }); - - this.logger.log( - `Field ${fieldId} successfully removed from view ${viewId} for workspace ${workspaceId}`, - ); - } - } - - async getViewsIdsForObjectMetadataId({ - workspaceId, - objectMetadataId, - }: { - workspaceId: string; - objectMetadataId: string; - }) { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'view', - ); - - return viewRepository - .find({ - where: { - objectMetadataId: objectMetadataId, - }, - }) - .then((views) => views.map((view) => view.id)); - } - - async resetKanbanAggregateOperationByFieldMetadataId({ - workspaceId, - fieldMetadataId, - }: { - workspaceId: string; - fieldMetadataId: string; - }) { - const viewRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'view', - ); - - await viewRepository.update( - { kanbanAggregateOperationFieldMetadataId: fieldMetadataId }, - { - kanbanAggregateOperationFieldMetadataId: null, - kanbanAggregateOperation: AggregateOperations.COUNT, - }, - ); - } -} diff --git a/packages/twenty-server/src/modules/view/view.module.ts b/packages/twenty-server/src/modules/view/view.module.ts index 10938b38b4..cad520d00c 100644 --- a/packages/twenty-server/src/modules/view/view.module.ts +++ b/packages/twenty-server/src/modules/view/view.module.ts @@ -9,20 +9,6 @@ import { ViewGroupEntity } from 'src/engine/core-modules/view/entities/view-grou import { ViewSortEntity } from 'src/engine/core-modules/view/entities/view-sort.entity'; import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { ViewFieldListener } from 'src/modules/view/listeners/view-field.listener'; -import { ViewFilterGroupListener } from 'src/modules/view/listeners/view-filter-group.listener'; -import { ViewFilterListener } from 'src/modules/view/listeners/view-filter.listener'; -import { ViewGroupListener } from 'src/modules/view/listeners/view-group.listener'; -import { ViewSortListener } from 'src/modules/view/listeners/view-sort.listener'; -import { ViewListener } from 'src/modules/view/listeners/view.listener'; -import { ViewDeleteOnePreQueryHook } from 'src/modules/view/pre-hooks/view-delete-one.pre-query.hook'; -import { ViewFieldSyncService } from 'src/modules/view/services/view-field-sync.service'; -import { ViewFilterGroupSyncService } from 'src/modules/view/services/view-filter-group-sync.service'; -import { ViewFilterSyncService } from 'src/modules/view/services/view-filter-sync.service'; -import { ViewGroupSyncService } from 'src/modules/view/services/view-group-sync.service'; -import { ViewSortSyncService } from 'src/modules/view/services/view-sort-sync.service'; -import { ViewSyncService } from 'src/modules/view/services/view-sync.service'; -import { ViewService } from 'src/modules/view/services/view.service'; @Module({ imports: [ @@ -38,22 +24,7 @@ import { ViewService } from 'src/modules/view/services/view.service'; FeatureFlagModule, ], - providers: [ - ViewService, - ViewDeleteOnePreQueryHook, - ViewSyncService, - ViewFieldSyncService, - ViewFilterSyncService, - ViewFilterGroupSyncService, - ViewGroupSyncService, - ViewSortSyncService, - ViewListener, - ViewFieldListener, - ViewFilterListener, - ViewFilterGroupListener, - ViewGroupListener, - ViewSortListener, - ], - exports: [ViewService], + providers: [], + exports: [], }) export class ViewModule {} diff --git a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts index 5636ed2856..5f9cabe82b 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-records-permissions/update-one-object-records-permissions.integration-spec.ts @@ -13,7 +13,8 @@ import { PermissionsExceptionMessage } from 'src/engine/metadata-modules/permiss describe('updateOneObjectRecordsPermissions', () => { const personId = randomUUID(); - let allPetsViewId: string; + let messageId: string; + let originalMessageText: string; beforeAll(async () => { const createPersonOperation = createOneOperationFactory({ @@ -27,34 +28,38 @@ describe('updateOneObjectRecordsPermissions', () => { await makeGraphqlAPIRequest(createPersonOperation); - const findAllPetsViewOperation = findOneOperationFactory({ - objectMetadataSingularName: 'view', - gqlFields: 'id', + const findAllMessagesOperation = findOneOperationFactory({ + objectMetadataSingularName: 'message', + gqlFields: ` + id + text + `, filter: { - name: { - eq: 'All Pets', + subject: { + eq: 'Meeting Request', }, }, }); - const findAllPetsViewResponse = await makeGraphqlAPIRequest( - findAllPetsViewOperation, + const findAllMessagesResponse = await makeGraphqlAPIRequest( + findAllMessagesOperation, ); - allPetsViewId = findAllPetsViewResponse.body.data.view.id; + messageId = findAllMessagesResponse.body.data.message.id; + originalMessageText = findAllMessagesResponse.body.data.message.text; }); afterAll(async () => { - const updateViewOperation = updateOneOperationFactory({ - objectMetadataSingularName: 'view', + const updateMessageOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'message', gqlFields: 'id', - recordId: allPetsViewId, + recordId: messageId, data: { - icon: 'IconList', + text: originalMessageText, }, }); - await makeGraphqlAPIRequest(updateViewOperation); + await makeGraphqlAPIRequest(updateMessageOperation); }); it('should throw a permission error when user does not have permission (guest role)', async () => { @@ -79,23 +84,25 @@ describe('updateOneObjectRecordsPermissions', () => { it('should allow to update a system object record even without update permission (guest role)', async () => { const graphqlOperation = updateOneOperationFactory({ - objectMetadataSingularName: 'view', + objectMetadataSingularName: 'message', gqlFields: ` id - icon + text `, - recordId: allPetsViewId, + recordId: messageId, data: { - icon: 'IconDog', + text: "Hello, I'm fine, thank you!", }, }); const response = await makeGraphqlAPIRequestWithGuestRole(graphqlOperation); expect(response.body.data).toBeDefined(); - expect(response.body.data.updateView).toBeDefined(); - expect(response.body.data.updateView.id).toBe(allPetsViewId); - expect(response.body.data.updateView.icon).toBe('IconDog'); + expect(response.body.data.updateMessage).toBeDefined(); + expect(response.body.data.updateMessage.id).toBe(messageId); + expect(response.body.data.updateMessage.text).toBe( + "Hello, I'm fine, thank you!", + ); }); it('should update an object record when user has permission (admin role)', async () => { diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts index d89b4e86ea..e9cb52e522 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-field-resolver.integration-spec.ts @@ -41,10 +41,10 @@ describe('View Field Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'myFieldTestObject', + namePlural: 'myFieldTestObjects', + labelSingular: 'My Field Test Object', + labelPlural: 'My Field Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-filter-group-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-filter-group-resolver.integration-spec.ts index 55c304d876..7a1b8e8a48 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-filter-group-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-filter-group-resolver.integration-spec.ts @@ -41,10 +41,10 @@ describe('View Filter Group Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'myFilterGroupTestObject', + namePlural: 'myFilterGroupTestObjects', + labelSingular: 'My Filter Group Test Object', + labelPlural: 'My Filter Group Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-filter-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-filter-resolver.integration-spec.ts index 70d962da87..62b95cea45 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-filter-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-filter-resolver.integration-spec.ts @@ -39,10 +39,10 @@ describe('View Filter Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'myFilterTestObject', + namePlural: 'myFilterTestObjects', + labelSingular: 'My Filter Test Object', + labelPlural: 'My Filter Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts index 134e0c171f..674706e3d9 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-group-resolver.integration-spec.ts @@ -41,10 +41,10 @@ describe('View Group Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'myGroupTestObject', + namePlural: 'myGroupTestObjects', + labelSingular: 'My Group Test Object', + labelPlural: 'My Group Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-resolver.integration-spec.ts index b9c080c8ea..ddc3a86018 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-resolver.integration-spec.ts @@ -40,10 +40,10 @@ describe('View Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'myViewTestObject', + namePlural: 'myViewTestObjects', + labelSingular: 'My View Test Object', + labelPlural: 'My View Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts index 575887385c..fd8a3b340a 100644 --- a/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/view/view-sort-resolver.integration-spec.ts @@ -42,10 +42,10 @@ describe('View Sort Resolver', () => { }, } = await createOneObjectMetadata({ input: { - nameSingular: 'myTestObject', - namePlural: 'myTestObjects', - labelSingular: 'My Test Object', - labelPlural: 'My Test Objects', + nameSingular: 'mySortTestObject', + namePlural: 'mySortTestObjects', + labelSingular: 'My Sort Test Object', + labelPlural: 'My Sort Test Objects', icon: 'Icon123', }, }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap index 9cd5891d5a..88e86075be 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/__snapshots__/update-one-field-metadata-related-record.integration-spec.ts.snap @@ -1,49 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`update-one-field-metadata-related-record MULTI_SELECT should delete related view filter if all select field options got deleted 1`] = ` +exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = ` [ - { - "extensions": { - "code": "NOT_FOUND", - "subCode": "RECORD_NOT_FOUND", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Record not found", - "name": "NotFoundError", - }, + "OPTION_0", + "OPTION_1", ] `; -exports[`update-one-field-metadata-related-record MULTI_SELECT should handle adding new options while maintaining existing view filter 1`] = ` -{ - "displayValue": "2 options", - "id": Any, - "value": "["OPTION_0","OPTION_1"]", -} -`; - exports[`update-one-field-metadata-related-record MULTI_SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` -{ - "displayValue": "10 options", - "id": Any, - "value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]", -} +[ + "OPTION_0", + "OPTION_1", + "OPTION_2", + "OPTION_3", + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should handle partial deletion of selected options in view filter 1`] = ` -{ - "displayValue": "6 options", - "id": Any, - "value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]", -} +[ + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should handle reordering of options while maintaining view filter values 1`] = ` -{ - "displayValue": "2 options", - "id": Any, - "value": "["OPTION_0","OPTION_1"]", -} +[ + "OPTION_0", + "OPTION_1", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` @@ -60,81 +54,79 @@ exports[`update-one-field-metadata-related-record MULTI_SELECT should throw erro `; exports[`update-one-field-metadata-related-record MULTI_SELECT should update display value with options label if less than 3 options are selected 1`] = ` -{ - "displayValue": "Option 8, Option 9", - "id": Any, - "value": "["OPTION_8","OPTION_9"]", -} +[ + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should update related multi selected options view filter 1`] = ` -{ - "displayValue": "10 options", - "id": Any, - "value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]", -} +[ + "OPTION_0_UPDATED", + "OPTION_1", + "OPTION_2_UPDATED", + "OPTION_3", + "OPTION_4_UPDATED", + "OPTION_5", + "OPTION_6_UPDATED", + "OPTION_7", + "OPTION_8_UPDATED", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should update related solo selected option view filter 1`] = ` -{ - "displayValue": "Option 5 updated", - "id": Any, - "value": "["OPTION_5_UPDATED"]", -} +[ + "OPTION_5_UPDATED", +] `; exports[`update-one-field-metadata-related-record MULTI_SELECT should update the display value on an option label change only 1`] = ` -{ - "displayValue": "Option 0 updated, Option 1 updated, Option 2 updated", - "id": Any, - "value": "["OPTION_0","OPTION_1","OPTION_2"]", -} -`; - -exports[`update-one-field-metadata-related-record SELECT should delete related view filter if all select field options got deleted 1`] = ` [ - { - "extensions": { - "code": "NOT_FOUND", - "subCode": "RECORD_NOT_FOUND", - "userFriendlyMessage": "An error occurred.", - }, - "message": "Record not found", - "name": "NotFoundError", - }, + "OPTION_0", + "OPTION_1", + "OPTION_2", ] `; exports[`update-one-field-metadata-related-record SELECT should handle adding new options while maintaining existing view filter 1`] = ` -{ - "displayValue": "2 options", - "id": Any, - "value": "["OPTION_0","OPTION_1"]", -} +[ + "OPTION_0", + "OPTION_1", +] `; exports[`update-one-field-metadata-related-record SELECT should handle no changes update of options while maintaining existing view filter values 1`] = ` -{ - "displayValue": "10 options", - "id": Any, - "value": "["OPTION_0","OPTION_1","OPTION_2","OPTION_3","OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]", -} +[ + "OPTION_0", + "OPTION_1", + "OPTION_2", + "OPTION_3", + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record SELECT should handle partial deletion of selected options in view filter 1`] = ` -{ - "displayValue": "6 options", - "id": Any, - "value": "["OPTION_4","OPTION_5","OPTION_6","OPTION_7","OPTION_8","OPTION_9"]", -} +[ + "OPTION_4", + "OPTION_5", + "OPTION_6", + "OPTION_7", + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record SELECT should handle reordering of options while maintaining view filter values 1`] = ` -{ - "displayValue": "2 options", - "id": Any, - "value": "["OPTION_0","OPTION_1"]", -} +[ + "OPTION_0", + "OPTION_1", +] `; exports[`update-one-field-metadata-related-record SELECT should throw error if view filter value is not a stringified JSON array 1`] = ` @@ -151,33 +143,37 @@ exports[`update-one-field-metadata-related-record SELECT should throw error if v `; exports[`update-one-field-metadata-related-record SELECT should update display value with options label if less than 3 options are selected 1`] = ` -{ - "displayValue": "Option 8, Option 9", - "id": Any, - "value": "["OPTION_8","OPTION_9"]", -} +[ + "OPTION_8", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record SELECT should update related multi selected options view filter 1`] = ` -{ - "displayValue": "10 options", - "id": Any, - "value": "["OPTION_0_UPDATED","OPTION_1","OPTION_2_UPDATED","OPTION_3","OPTION_4_UPDATED","OPTION_5","OPTION_6_UPDATED","OPTION_7","OPTION_8_UPDATED","OPTION_9"]", -} +[ + "OPTION_0_UPDATED", + "OPTION_1", + "OPTION_2_UPDATED", + "OPTION_3", + "OPTION_4_UPDATED", + "OPTION_5", + "OPTION_6_UPDATED", + "OPTION_7", + "OPTION_8_UPDATED", + "OPTION_9", +] `; exports[`update-one-field-metadata-related-record SELECT should update related solo selected option view filter 1`] = ` -{ - "displayValue": "Option 5 updated", - "id": Any, - "value": "["OPTION_5_UPDATED"]", -} +[ + "OPTION_5_UPDATED", +] `; exports[`update-one-field-metadata-related-record SELECT should update the display value on an option label change only 1`] = ` -{ - "displayValue": "Option 0 updated, Option 1 updated, Option 2 updated", - "id": Any, - "value": "["OPTION_0","OPTION_1","OPTION_2"]", -} +[ + "OPTION_0", + "OPTION_1", + "OPTION_2", +] `; diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts index 370c16842b..efff621f37 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/delete-one-field-metadata.integration-spec.ts @@ -1,6 +1,3 @@ -import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; -import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { deleteOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/delete-one-field-metadata.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; @@ -10,8 +7,15 @@ import { } from 'test/integration/metadata/suites/object-metadata/constants/test-object-names.constant'; import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; +import { + createTestViewWithRestApi, + findViewByIdWithRestApi, +} from 'test/integration/rest/utils/view-rest-api.util'; +import { generateRecordName } from 'test/integration/utils/generate-record-name'; import { FieldMetadataType } from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { AggregateOperations } from 'src/engine/api/graphql/graphql-query-runner/constants/aggregate-operations.constant'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; describe('deleteOne', () => { @@ -45,28 +49,15 @@ describe('deleteOne', () => { testFieldId = createdFieldData.createOneField.id; - // create view - const graphqlOperation = createOneOperationFactory({ - objectMetadataSingularName: 'View', - gqlFields: ` - id - kanbanAggregateOperationFieldMetadataId - kanbanAggregateOperation - `, - data: { - kanbanAggregateOperationFieldMetadataId: testFieldId, - kanbanAggregateOperation: 'MAX', - objectMetadataId: listingObjectId, - name: 'By Type', - type: ViewType.KANBAN, - icon: 'IconLayoutKanban', - }, + const createdView = await createTestViewWithRestApi({ + name: generateRecordName('By Type'), + objectMetadataId: listingObjectId, + type: ViewType.KANBAN, + kanbanAggregateOperationFieldMetadataId: testFieldId, + kanbanAggregateOperation: AggregateOperations.MAX, + icon: 'IconLayoutKanban', }); - const response = await makeGraphqlAPIRequest(graphqlOperation); - - const createdView = response.body.data.createView; - viewId = createdView.id; }); afterEach(async () => { @@ -76,30 +67,17 @@ describe('deleteOne', () => { }); }); it('should reset kanban aggregate operation when deleting a field used as kanbanAggregateOperationFieldMetadataId', async () => { - // Arrange - // 1. Check that view has expcted kanbanAggregateOperationFieldMetadataId and kanbanAggregateOperation - const findViewOperation = findOneOperationFactory({ - objectMetadataSingularName: 'view', - gqlFields: ` - id - kanbanAggregateOperationFieldMetadataId - kanbanAggregateOperation - `, - filter: { - id: { - eq: viewId, - }, - }, - }); + const viewThatShouldBeUpdated = await findViewByIdWithRestApi(viewId); - const viewResponse = await makeGraphqlAPIRequest(findViewOperation); + if (!isDefined(viewThatShouldBeUpdated)) { + throw new Error('View not found, this should not happen'); + } expect( - viewResponse.body.data.view.kanbanAggregateOperationFieldMetadataId, + viewThatShouldBeUpdated.kanbanAggregateOperationFieldMetadataId, ).toBe(testFieldId); - expect(viewResponse.body.data.view.kanbanAggregateOperation).toBe('MAX'); + expect(viewThatShouldBeUpdated.kanbanAggregateOperation).toBe('MAX'); - // Deactivate field to be able to delete it after await updateOneFieldMetadata({ input: { idToUpdate: testFieldId, @@ -111,26 +89,22 @@ describe('deleteOne', () => { `, }); - // Act const { data } = await deleteOneFieldMetadata({ input: { idToDelete: testFieldId }, }); - // Assert - // 1. Field is deleted expect(data.deleteOneField.id).toBe(testFieldId); - // 2. Kanban aggregate operation has been reset on view using this field as kanbanAggregateOperationFieldMetadataId - const updatedViewResponse = - await makeGraphqlAPIRequest(findViewOperation); + const updatedViewResponse = await findViewByIdWithRestApi(viewId); + + if (!isDefined(updatedViewResponse)) { + throw new Error('View not found, this should not happen'); + } expect( - updatedViewResponse.body.data.view - .kanbanAggregateOperationFieldMetadataId, + updatedViewResponse.kanbanAggregateOperationFieldMetadataId, ).toBeNull(); - expect(updatedViewResponse.body.data.view.kanbanAggregateOperation).toBe( - 'COUNT', - ); + expect(updatedViewResponse.kanbanAggregateOperation).toBe(null); }); }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts index 0341577d95..b87dc5b90e 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/update-one-field-metadata-related-record.integration-spec.ts @@ -1,22 +1,31 @@ import { faker } from '@faker-js/faker'; -import { createOneOperation } from 'test/integration/graphql/utils/create-one-operation.util'; -import { findOneOperation } from 'test/integration/graphql/utils/find-one-operation.util'; import { createOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/create-one-field-metadata.util'; import { updateOneFieldMetadata } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util'; +import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { getMockCreateObjectInput } from 'test/integration/metadata/suites/object-metadata/utils/generate-mock-create-object-metadata-input'; +import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util'; +import { + createTestViewFilterWithRestApi, + createTestViewWithRestApi, + findViewFilterWithRestApi, +} from 'test/integration/rest/utils/view-rest-api.util'; import { type EachTestingContext } from 'twenty-shared/testing'; import { type EnumFieldMetadataType, FieldMetadataType, } from 'twenty-shared/types'; -import { isDefined, parseJson } from 'twenty-shared/utils'; -import { createOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/create-one-object-metadata.util'; +import { isDefined } from 'twenty-shared/utils'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand'; +import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; +import { type ViewFilterValue } from 'src/engine/core-modules/view/types/view-filter-value.type'; import { type FieldMetadataComplexOption, type FieldMetadataDefaultOption, } from 'src/engine/metadata-modules/field-metadata/dtos/options.input'; +import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-workspaces.util'; type Option = FieldMetadataDefaultOption | FieldMetadataComplexOption; @@ -28,7 +37,7 @@ const generateOption = (index: number): Option => ({ }); const generateOptions = (length: number) => Array.from({ length }, (_value, index) => generateOption(index)); -const updateOption = ({ value, label, ...option }: Option) => ({ +const fakeOptionUpdate = ({ value, label, ...option }: Option) => ({ ...option, value: `${value}_UPDATED`, label: `${label} updated`, @@ -39,7 +48,6 @@ const ALL_OPTIONS = generateOptions(10); const isEven = (_value: unknown, index: number) => index % 2 === 0; type ViewFilterUpdate = { - displayValue: string; value: string[]; }; @@ -68,6 +76,13 @@ describe('update-one-field-metadata-related-record', () => { options, type: fieldMetadataType, }: FieldMetadataOptionsAndType) => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: false, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); + const singular = faker.lorem.words(); const plural = singular + faker.lorem.word(); const { @@ -101,25 +116,23 @@ describe('update-one-field-metadata-related-record', () => { `, }); - const { - data: { createOneResponse: createOneView }, - } = await createOneOperation<{ - id: string; - objectMetadataId: string; - type: string; - }>({ - objectMetadataSingularName: 'view', - input: { - id: faker.string.uuid(), - objectMetadataId: createOneObject.id, - type: 'table', - }, + const createdView = await createTestViewWithRestApi({ + id: faker.string.uuid(), + name: 'Test View', + objectMetadataId: createOneObject.id, + type: ViewType.TABLE, }); - return { createOneObject, createOneField, createOneView }; + return { createOneObject, createOneField, createdView }; }; afterEach(async () => { + await updateFeatureFlag({ + expectToFail: false, + featureFlag: FeatureFlagKey.IS_WORKSPACE_MIGRATION_V2_ENABLED, + value: true, + workspaceId: SEED_APPLE_WORKSPACE_ID, + }); if (isDefined(idToDelete)) { await deleteOneObjectMetadata({ input: { idToDelete: idToDelete }, @@ -142,7 +155,7 @@ describe('update-one-field-metadata-related-record', () => { context: { updateOptions: (options) => options.map((option, index) => - isEven(option, index) ? updateOption(option) : option, + isEven(option, index) ? fakeOptionUpdate(option) : option, ), }, }, @@ -150,10 +163,9 @@ describe('update-one-field-metadata-related-record', () => { title: 'should update related solo selected option view filter', context: { createViewFilter: { - displayValue: ALL_OPTIONS[5].label, value: [ALL_OPTIONS[5].value], }, - updateOptions: (options) => [updateOption(options[5])], + updateOptions: (options) => [fakeOptionUpdate(options[5])], }, }, { @@ -168,7 +180,6 @@ describe('update-one-field-metadata-related-record', () => { 'should handle reordering of options while maintaining view filter values', context: { createViewFilter: { - displayValue: '2 options', value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), }, updateOptions: (options) => [...options].reverse(), @@ -190,7 +201,6 @@ describe('update-one-field-metadata-related-record', () => { type: fieldType, }, createViewFilter: { - displayValue: '2 options', value: ALL_OPTIONS.slice(0, 2).map((option) => option.value), }, updateOptions: (options) => [ @@ -199,27 +209,6 @@ describe('update-one-field-metadata-related-record', () => { ], }, }, - { - title: - 'should update display value with options label if less than 3 options are selected', - context: { - updateOptions: (options) => options.slice(8), - }, - }, - { - title: 'should update the display value on an option label change only', - context: { - createViewFilter: { - displayValue: 'Option 3', - value: ALL_OPTIONS.slice(0, 3).map((option) => option.value), - }, - updateOptions: (options) => - options.map((option) => ({ - ...option, - label: `${option.label} updated`, - })), - }, - }, ]; test.each(testCases)( @@ -228,34 +217,20 @@ describe('update-one-field-metadata-related-record', () => { context: { expected, createViewFilter = { - displayValue: '10 options', value: ALL_OPTIONS.map((option) => option.value), }, fieldMetadata = { options: ALL_OPTIONS, type: fieldType }, updateOptions, }, }) => { - const { createOneField, createOneView } = + const { createOneField, createdView } = await createObjectSelectFieldAndView(fieldMetadata); - const { - data: { createOneResponse: createOneViewFilter }, - } = await createOneOperation<{ - id: string; - viewId: string; - fieldMetadataId: string; - operand: string; - value: string; - displayValue: string; - }>({ - objectMetadataSingularName: 'viewFilter', - input: { - id: faker.string.uuid(), - viewId: createOneView.id, - fieldMetadataId: createOneField.id, - operand: 'is', - value: JSON.stringify(createViewFilter.value), - displayValue: createViewFilter.displayValue, - }, + + const createdViewFilter = await createTestViewFilterWithRestApi({ + viewId: createdView.id, + fieldMetadataId: createOneField.id, + operand: ViewFilterOperand.IS, + value: createViewFilter.value, }); const optionsWithIds = createOneField.options; @@ -278,41 +253,31 @@ describe('update-one-field-metadata-related-record', () => { `, }); - const { - data: { findResponse }, - errors, - } = await findOneOperation({ - gqlFields: ` - id - displayValue - value - `, - objectMetadataSingularName: 'viewFilter', - filter: { - id: { eq: createOneViewFilter.id }, - }, - }); + const updatedViewFilter = await findViewFilterWithRestApi( + createdViewFilter.id, + ); if (expected !== undefined) { - expect(findResponse).toBe(expected); - expect(errors).toMatchSnapshot(); + expect(updatedViewFilter).toBe(expected); return; } - const parsedViewFilterValues = parseJson(findResponse.value); + if (!isDefined(updatedViewFilter)) { + throw new Error( + 'updatedViewFilter is not defined but should be at this point', + ); + } - expect(parsedViewFilterValues).not.toBeNull(); - if (parsedViewFilterValues === null) { + expect(updatedViewFilter.value).not.toBeNull(); + if (updatedViewFilter.value === null) { throw new Error('Invariant parsedValue should not be null'); } expect(updatedOptions.map((option) => option.value)).toEqual( - expect.arrayContaining(parsedViewFilterValues), + expect.arrayContaining(updatedViewFilter.value as string[]), ); - expect(findResponse).toMatchSnapshot({ - id: expect.any(String), - }); + expect(updatedViewFilter.value).toMatchSnapshot(); }, ); @@ -335,7 +300,7 @@ describe('update-one-field-metadata-related-record', () => { test.each(failingTestCases)( '$title', async ({ context: { createViewFilterValue } }) => { - const { createOneField, createOneView } = + const { createOneField, createdView } = await createObjectSelectFieldAndView({ options: ALL_OPTIONS, type: fieldType, @@ -343,23 +308,12 @@ describe('update-one-field-metadata-related-record', () => { const viewFilterId = '20202020-e3b5-4fa7-85aa-9b1950fc7bf5'; - await createOneOperation<{ - id: string; - viewId: string; - fieldMetadataId: string; - operand: string; - value: string; - displayValue: string; - }>({ - objectMetadataSingularName: 'viewFilter', - input: { - id: viewFilterId, - viewId: createOneView.id, - fieldMetadataId: createOneField.id, - operand: 'is', - value: createViewFilterValue as unknown as string, - displayValue: '10 options', - }, + await createTestViewFilterWithRestApi({ + id: viewFilterId, + viewId: createdView.id, + fieldMetadataId: createOneField.id, + operand: ViewFilterOperand.IS, + value: createViewFilterValue as unknown as ViewFilterValue, }); const optionsWithIds = createOneField.options; @@ -368,7 +322,7 @@ describe('update-one-field-metadata-related-record', () => { throw new Error('optionsWithIds is not defined'); } const updatePayload = { - options: optionsWithIds.map((option) => updateOption(option)), + options: optionsWithIds.map((option) => fakeOptionUpdate(option)), }; const { errors, data } = await updateOneFieldMetadata({ input: { diff --git a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util.ts b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util.ts index 7fbf805940..9697661e50 100644 --- a/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util.ts +++ b/packages/twenty-server/test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata.util.ts @@ -1,8 +1,8 @@ -import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; import { type UpdateOneFieldFactoryInput, updateOneFieldMetadataQueryFactory, } from 'test/integration/metadata/suites/field-metadata/utils/update-one-field-metadata-query-factory.util'; +import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util'; import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type'; import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type'; import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util'; @@ -22,7 +22,7 @@ export const updateOneFieldMetadata = async ({ gqlFields, }); - const response = await makeGraphqlAPIRequest(graphqlOperation); + const response = await makeMetadataAPIRequest(graphqlOperation); if (expectToFail === true) { warnIfNoErrorButExpectedToFail({ diff --git a/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts index ffa441e73b..ca5e1bf0e9 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-field.integration-spec.ts @@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { + assertRestApiErrorNotFoundResponse, assertRestApiErrorResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; @@ -219,8 +220,7 @@ describe('View Field REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -308,8 +308,7 @@ describe('View Field REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(getResponse); - expect(getResponse.body).toEqual({}); + assertRestApiErrorNotFoundResponse(getResponse); }); it('should return 404 error when deleting non-existent view field', async () => { @@ -319,14 +318,7 @@ describe('View Field REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewFieldExceptionMessage( - ViewFieldExceptionMessageKey.VIEW_FIELD_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FIELD_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/view-filter-group.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-filter-group.integration-spec.ts index 7715c6a717..9d88674ed1 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-filter-group.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-filter-group.integration-spec.ts @@ -6,6 +6,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { + assertRestApiErrorNotFoundResponse, assertRestApiErrorResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; @@ -316,8 +317,7 @@ describe('View Filter Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -464,8 +464,7 @@ describe('View Filter Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); it('should return 404 error when deleting non-existent filter group', async () => { @@ -475,14 +474,7 @@ describe('View Filter Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewFilterGroupExceptionMessage( - ViewFilterGroupExceptionMessageKey.VIEW_FILTER_GROUP_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FILTER_GROUP_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/view-filter.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-filter.integration-spec.ts index bc9f5f0a2f..511422a48e 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-filter.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-filter.integration-spec.ts @@ -7,7 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { - assertRestApiErrorResponse, + assertRestApiErrorNotFoundResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; import { @@ -22,10 +22,6 @@ import { import { FieldMetadataType } from 'twenty-shared/types'; import { ViewFilterOperand } from 'src/engine/core-modules/view/enums/view-filter-operand'; -import { - generateViewFilterExceptionMessage, - ViewFilterExceptionMessageKey, -} from 'src/engine/core-modules/view/exceptions/view-filter.exception'; describe('View Filter REST API', () => { let testObjectMetadataId: string; @@ -232,8 +228,7 @@ describe('View Filter REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -282,14 +277,7 @@ describe('View Filter REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewFilterExceptionMessage( - ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FILTER_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -316,8 +304,7 @@ describe('View Filter REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(getResponse); - expect(getResponse.body).toEqual({}); + assertRestApiErrorNotFoundResponse(getResponse); }); it('should return 404 error when deleting non-existent view filter', async () => { @@ -327,14 +314,7 @@ describe('View Filter REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewFilterExceptionMessage( - ViewFilterExceptionMessageKey.VIEW_FILTER_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_FILTER_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts index 2b3b53f7a3..fe24c3f25f 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-group.integration-spec.ts @@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { + assertRestApiErrorNotFoundResponse, assertRestApiErrorResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; @@ -373,14 +374,7 @@ describe('View Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewGroupExceptionMessage( - ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_GROUP_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -408,14 +402,7 @@ describe('View Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewGroupExceptionMessage( - ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_GROUP_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); it('should return success even when group is already deleted', async () => { @@ -438,14 +425,7 @@ describe('View Group REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - deleteResponse2, - 404, - generateViewGroupExceptionMessage( - ViewGroupExceptionMessageKey.VIEW_GROUP_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_GROUP_ID, - ), - ); + assertRestApiErrorNotFoundResponse(deleteResponse2); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/suites/view-sort.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view-sort.integration-spec.ts index d99d704abc..88f18df320 100644 --- a/packages/twenty-server/test/integration/rest/suites/view-sort.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view-sort.integration-spec.ts @@ -7,6 +7,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { + assertRestApiErrorNotFoundResponse, assertRestApiErrorResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; @@ -199,8 +200,7 @@ describe('View Sort REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -243,14 +243,7 @@ describe('View Sort REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewSortExceptionMessage( - ViewSortExceptionMessageKey.VIEW_SORT_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_SORT_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -276,8 +269,7 @@ describe('View Sort REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(getResponse); - expect(getResponse.body).toEqual({}); + assertRestApiErrorNotFoundResponse(getResponse); }); it('should return 404 error when deleting non-existent view sort', async () => { diff --git a/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts b/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts index b3b1e47d59..6eb6192d7c 100644 --- a/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts +++ b/packages/twenty-server/test/integration/rest/suites/view.integration-spec.ts @@ -3,7 +3,7 @@ import { createOneObjectMetadata } from 'test/integration/metadata/suites/object import { deleteOneObjectMetadata } from 'test/integration/metadata/suites/object-metadata/utils/delete-one-object-metadata.util'; import { makeRestAPIRequest } from 'test/integration/rest/utils/make-rest-api-request.util'; import { - assertRestApiErrorResponse, + assertRestApiErrorNotFoundResponse, assertRestApiSuccessfulResponse, } from 'test/integration/rest/utils/rest-test-assertions.util'; import { @@ -19,10 +19,6 @@ import { import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum'; import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; -import { - ViewExceptionMessageKey, - generateViewExceptionMessage, -} from 'src/engine/core-modules/view/exceptions/view.exception'; describe('View REST API', () => { let testObjectMetadataId: string; @@ -173,8 +169,7 @@ describe('View REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(response); - expect(response.body).toEqual({}); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -231,14 +226,7 @@ describe('View REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewExceptionMessage( - ViewExceptionMessageKey.VIEW_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); @@ -271,8 +259,7 @@ describe('View REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiSuccessfulResponse(getResponse); - expect(getResponse.body).toEqual({}); + assertRestApiErrorNotFoundResponse(getResponse); }); it('should return 404 error when deleting non-existent view', async () => { @@ -282,14 +269,7 @@ describe('View REST API', () => { bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, }); - assertRestApiErrorResponse( - response, - 404, - generateViewExceptionMessage( - ViewExceptionMessageKey.VIEW_NOT_FOUND, - TEST_NOT_EXISTING_VIEW_ID, - ), - ); + assertRestApiErrorNotFoundResponse(response); }); }); }); diff --git a/packages/twenty-server/test/integration/rest/utils/rest-test-assertions.util.ts b/packages/twenty-server/test/integration/rest/utils/rest-test-assertions.util.ts index 7397ec8458..85097b26c5 100644 --- a/packages/twenty-server/test/integration/rest/utils/rest-test-assertions.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/rest-test-assertions.util.ts @@ -32,3 +32,16 @@ export const assertRestApiErrorResponse = >( expect(response.body.message).toContain(expectedErrorMessage); } }; + +export const assertRestApiErrorNotFoundResponse = ( + response: RestResponse<{ statusCode: number; messages: [] }>, + expectedStatus = 404, + expectedErrorMessage?: string, +) => { + expect(response.status).toBe(expectedStatus); + expect(response.body.statusCode).toBe(expectedStatus); + + if (expectedErrorMessage && response.body.message) { + expect(response.body.message).toContain(expectedErrorMessage); + } +}; diff --git a/packages/twenty-server/test/integration/rest/utils/view-rest-api.util.ts b/packages/twenty-server/test/integration/rest/utils/view-rest-api.util.ts index 851c7447a3..6e465a6473 100644 --- a/packages/twenty-server/test/integration/rest/utils/view-rest-api.util.ts +++ b/packages/twenty-server/test/integration/rest/utils/view-rest-api.util.ts @@ -15,6 +15,38 @@ import { type ViewEntity } from 'src/engine/core-modules/view/entities/view.enti import { ViewOpenRecordIn } from 'src/engine/core-modules/view/enums/view-open-record-in'; import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum'; +export const findViewByIdWithRestApi = async ( + viewId: string, +): Promise => { + const response = await makeRestAPIRequest({ + method: 'get', + path: `/metadata/views/${viewId}`, + bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, + }); + + if (response.status === 404) { + return null; + } + + return response.body; +}; + +export const findViewFilterWithRestApi = async ( + viewFilterId: string, +): Promise => { + const response = await makeRestAPIRequest({ + method: 'get', + path: `/metadata/viewFilters/${viewFilterId}`, + bearer: APPLE_JANE_ADMIN_ACCESS_TOKEN, + }); + + if (response.status === 404) { + return null; + } + + return response.body; +}; + export const createTestViewWithRestApi = async ( overrides: Partial = {}, ): Promise => {