Compare commits

...

1 Commits

Author SHA1 Message Date
Vinay Arvind Badgujar ffcf608897 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 <charles@twenty.com>
Co-authored-by: prastoin <paul@twenty.com>
2025-09-05 10:43:58 +02:00
72 changed files with 914 additions and 2581 deletions
@@ -4337,7 +4337,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'];
@@ -4345,7 +4345,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'];
@@ -4367,7 +4367,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; }>;
@@ -7944,6 +7944,9 @@ export const CreateOneFieldMetadataItemDocument = gql`
defaultValue
options
isLabelSyncedWithName
object {
id
}
}
}
`;
@@ -7990,6 +7993,9 @@ export const UpdateOneFieldMetadataItemDocument = gql`
updatedAt
settings
isLabelSyncedWithName
object {
id
}
}
}
`;
@@ -8131,6 +8137,9 @@ export const DeleteOneFieldMetadataItemDocument = gql`
createdAt
updatedAt
settings
object {
id
}
}
}
`;
@@ -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
}
}
}
`;
@@ -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
}
}
}
`,
@@ -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,
@@ -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;
};
@@ -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;
};
@@ -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;
};
@@ -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;
};
@@ -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,
};
};
@@ -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;
};
@@ -32,7 +32,7 @@ export const visibleRecordFieldsComponentSelector = createComponentSelector({
),
);
return filteredVisibleAndReadableRecordFields.toSorted(
return [...filteredVisibleAndReadableRecordFields].sort(
sortByProperty('position'),
);
},
@@ -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,
})
}
/>
) : (
<LightIconButton
@@ -44,7 +44,10 @@ export const EditableFilterChip = ({
: recordFilter.label;
const labelKey = `${fieldNameLabel}`;
const labelValue = getRecordFilterLabelValue(recordFilter);
const labelValue = getRecordFilterLabelValue({
recordFilter,
fieldMetadataOptions: fieldMetadataItem.options ?? [],
});
return (
<SortOrFilterChip
@@ -16,7 +16,7 @@ import { type ViewFilter } from '@/views/types/ViewFilter';
import { convertViewFilterOperandToCore } from '@/views/utils/convertViewFilterOperandToCore';
import { useApolloClient } from '@apollo/client';
import { isNull } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, parseJson } from 'twenty-shared/utils';
import { type CoreViewFilter } from '~/generated/graphql';
export const usePersistViewFilterRecords = () => {
@@ -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,
@@ -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,
}),
};
};
@@ -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],
@@ -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,
};
};
@@ -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}`;
@@ -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<void> {
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}`,
);
}
}
@@ -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<ViewFieldEntity | null> {
return this.viewFieldService.findById(id, workspace.id);
): Promise<ViewFieldEntity> {
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')
@@ -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<ViewFilterGroupDTO | null> {
return this.viewFilterGroupService.findById(id, workspace.id);
): Promise<ViewFilterGroupDTO> {
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()
@@ -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<ViewFilterDTO | null> {
return this.viewFilterService.findById(id, workspace.id);
): Promise<ViewFilterDTO> {
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()
@@ -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<ViewGroupDTO | null> {
return this.viewGroupService.findById(id, workspace.id);
): Promise<ViewGroupDTO> {
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()
@@ -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<ViewSortDTO | null> {
return this.viewSortService.findById(id, workspace.id);
): Promise<ViewSortDTO> {
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()
@@ -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<ViewDTO | null> {
return this.viewService.findById(id, workspace.id);
): Promise<ViewDTO> {
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()
@@ -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.`;
}
};
@@ -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(
@@ -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,
@@ -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[] = [
@@ -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<T> = {
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<ViewGroupWorkspaceEntity>(
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<void> {
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<void> {
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<ViewFilterWorkspaceEntity>(
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<string[]>(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<ViewGroupWorkspaceEntity>,
view: ViewEntity,
): Promise<void> {
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<ViewWorkspaceEntity, 'viewGroups' | 'viewFilters'>,
): Promise<ViewWorkspaceEntity[]> {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
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,
});
}
}
}
@@ -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<FieldMetadataEntit
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
private readonly fieldMetadataRelatedRecordsService: FieldMetadataRelatedRecordsService,
private readonly viewService: ViewService,
private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService,
private readonly featureFlagService: FeatureFlagService,
private readonly fieldMetadataValidationService: FieldMetadataValidationService,
@@ -468,6 +466,10 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
);
}
await this.fieldMetadataRelatedRecordsService.resetViewKanbanAggregateOperation(
fieldMetadata,
);
if (isFieldMetadataTypeRelation(fieldMetadata)) {
const fieldMetadataIdsToDelete: string[] = [];
const isRelationTargetMorphRelation = isFieldMetadataTypeMorphRelation(
@@ -607,11 +609,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit
await queryRunner.commitTransaction();
await this.viewService.resetKanbanAggregateOperationByFieldMetadataId({
workspaceId,
fieldMetadataId: fieldMetadata.id,
});
await this.workspaceMetadataVersionService.incrementMetadataVersion(
workspaceId,
);
@@ -12,6 +12,7 @@ import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { CoreViewModule } from 'src/engine/core-modules/view/view.module';
import { SettingsPermissionsGuard } from 'src/engine/guards/settings-permissions.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
@@ -70,6 +71,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
WorkspaceDataSourceModule,
FeatureFlagModule,
WorkspaceMigrationV2Module,
CoreViewModule,
],
services: [
ObjectMetadataService,
@@ -1,16 +1,26 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
import { ViewDTO } from 'src/engine/core-modules/view/dtos/view.dto';
import { ViewEntity } from 'src/engine/core-modules/view/entities/view.entity';
import { ViewKey } from 'src/engine/core-modules/view/enums/view-key.enum';
import { ViewType } from 'src/engine/core-modules/view/enums/view-type.enum';
import { ViewFieldService } from 'src/engine/core-modules/view/services/view-field.service';
import { ViewService } from 'src/engine/core-modules/view/services/view.service';
import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import { type FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity';
import { type ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity';
import { type ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity';
@Injectable()
export class ObjectMetadataRelatedRecordsService {
constructor(
private readonly viewService: ViewService,
private readonly viewFieldService: ViewFieldService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
) {}
@@ -25,19 +35,14 @@ export class ObjectMetadataRelatedRecordsService {
private async createView(
objectMetadata: ObjectMetadataEntity,
): Promise<ViewWorkspaceEntity> {
const viewRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewWorkspaceEntity>(
objectMetadata.workspaceId,
'view',
);
return await viewRepository.save({
): Promise<ViewDTO> {
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<void> {
const viewFieldRepository =
await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFieldWorkspaceEntity>(
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<ViewWorkspaceEntity>(
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<ViewWorkspaceEntity>(
workspaceId,
'view',
{
shouldBypassPermissionChecks: true,
},
);
await viewRepository.delete({
await this.viewRepository.delete({
objectMetadataId: objectMetadata.id,
workspaceId,
});
}
}
@@ -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,
@@ -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<void> {
@@ -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 = [];
@@ -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<T extends EntityWithId> = {
create: (workspaceId: string, entity: T) => Promise<void>;
update: (
workspaceId: string,
entity: T,
diff?: Partial<ObjectRecordDiff<T>>,
) => Promise<void>;
delete: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
destroy: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
restore: (workspaceId: string, entity: Pick<T, 'id'>) => Promise<void>;
};
@Injectable()
export abstract class BaseViewSyncListener<T extends EntityWithId> {
@Inject(FeatureFlagService)
protected readonly featureFlagService: FeatureFlagService;
@Inject(ExceptionHandlerService)
protected readonly exceptionHandlerService: ExceptionHandlerService;
protected readonly logger: Logger;
constructor(
protected readonly syncOperations: SyncOperations<T>,
loggerName: string,
protected readonly entityTypeName: string,
) {
this.logger = new Logger(loggerName);
}
protected async handleCreated(
batchEvent: WorkspaceEventBatch<ObjectRecordCreateEvent<T>>,
): Promise<void> {
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<ObjectRecordUpdateEvent<T>>,
): Promise<void> {
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<ObjectRecordDeleteEvent<T>>,
): Promise<void> {
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<ObjectRecordDestroyEvent<T>>,
): Promise<void> {
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<ObjectRecordRestoreEvent<T>>,
): Promise<void> {
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<boolean> {
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,
},
});
}
}
@@ -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<ViewFieldWorkspaceEntity> {
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<ViewFieldWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.UPDATED)
async handleViewFieldUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewFieldWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DELETED)
async handleViewFieldDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewFieldWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.DESTROYED)
async handleViewFieldDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewFieldWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('viewField', DatabaseEventAction.RESTORED)
async handleViewFieldRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewFieldWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<ViewFilterGroupWorkspaceEntity> {
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<ViewFilterGroupWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.UPDATED)
async handleViewFilterGroupUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewFilterGroupWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DELETED)
async handleViewFilterGroupDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewFilterGroupWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.DESTROYED)
async handleViewFilterGroupDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewFilterGroupWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('viewFilterGroup', DatabaseEventAction.RESTORED)
async handleViewFilterGroupRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewFilterGroupWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<ViewFilterWorkspaceEntity> {
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<ViewFilterWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.UPDATED)
async handleViewFilterUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewFilterWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DELETED)
async handleViewFilterDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewFilterWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.DESTROYED)
async handleViewFilterDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewFilterWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('viewFilter', DatabaseEventAction.RESTORED)
async handleViewFilterRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewFilterWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<ViewGroupWorkspaceEntity> {
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<ViewGroupWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.UPDATED)
async handleViewGroupUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewGroupWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DELETED)
async handleViewGroupDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewGroupWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.DESTROYED)
async handleViewGroupDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewGroupWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('viewGroup', DatabaseEventAction.RESTORED)
async handleViewGroupRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewGroupWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<ViewSortWorkspaceEntity> {
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<ViewSortWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.UPDATED)
async handleViewSortUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewSortWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DELETED)
async handleViewSortDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewSortWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.DESTROYED)
async handleViewSortDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewSortWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('viewSort', DatabaseEventAction.RESTORED)
async handleViewSortRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewSortWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<ViewWorkspaceEntity> {
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<ViewWorkspaceEntity>
>,
) {
return this.handleCreated(batchEvent);
}
@OnDatabaseBatchEvent('view', DatabaseEventAction.UPDATED)
async handleViewUpdated(
batchEvent: WorkspaceEventBatch<
ObjectRecordUpdateEvent<ViewWorkspaceEntity>
>,
) {
return this.handleUpdated(batchEvent);
}
@OnDatabaseBatchEvent('view', DatabaseEventAction.DELETED)
async handleViewDeleted(
batchEvent: WorkspaceEventBatch<
ObjectRecordDeleteEvent<ViewWorkspaceEntity>
>,
) {
return this.handleDeleted(batchEvent);
}
@OnDatabaseBatchEvent('view', DatabaseEventAction.DESTROYED)
async handleViewDestroyed(
batchEvent: WorkspaceEventBatch<
ObjectRecordDestroyEvent<ViewWorkspaceEntity>
>,
) {
return this.handleDestroyed(batchEvent);
}
@OnDatabaseBatchEvent('view', DatabaseEventAction.RESTORED)
async handleViewRestored(
batchEvent: WorkspaceEventBatch<
ObjectRecordRestoreEvent<ViewWorkspaceEntity>
>,
) {
return this.handleRestored(batchEvent);
}
}
@@ -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<DeleteManyResolverArgs> {
throw new GraphqlQueryRunnerException(
'Method not implemented',
GraphqlQueryRunnerExceptionCode.NOT_IMPLEMENTED,
);
}
}
@@ -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<DeleteOneResolverArgs> {
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;
}
}
@@ -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<ViewFieldEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
): Partial<ViewFieldEntity> {
const updateData: Record<string, unknown> = {};
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<ViewFieldEntity>;
}
public async createCoreViewField(
workspaceId: string,
workspaceViewField: ViewFieldWorkspaceEntity,
): Promise<void> {
const coreViewField: Partial<ViewFieldEntity> = {
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<ViewFieldWorkspaceEntity, 'id'>,
diff?: Partial<ObjectRecordDiff<ViewFieldWorkspaceEntity>>,
): Promise<void> {
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<ViewFieldWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFieldRepository.softDelete({
id: workspaceViewField.id,
workspaceId,
});
}
public async destroyCoreViewField(
workspaceId: string,
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFieldRepository.delete({
id: workspaceViewField.id,
workspaceId,
});
}
public async restoreCoreViewField(
workspaceId: string,
workspaceViewField: Pick<ViewFieldWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFieldRepository.restore({
id: workspaceViewField.id,
workspaceId,
});
}
}
@@ -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<ViewFilterGroupEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
): Partial<ViewFilterGroupEntity> {
const updateData: Record<string, unknown> = {};
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<ViewFilterGroupEntity>;
}
public async createCoreViewFilterGroup(
workspaceId: string,
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
): Promise<void> {
const coreViewFilterGroup: Partial<ViewFilterGroupEntity> = {
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<ViewFilterGroupWorkspaceEntity, 'id'>,
diff?: Partial<ObjectRecordDiff<ViewFilterGroupWorkspaceEntity>>,
): Promise<void> {
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<ViewFilterGroupWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFilterGroupRepository.softDelete({
id: workspaceViewFilterGroup.id,
workspaceId,
});
}
public async destroyCoreViewFilterGroup(
workspaceId: string,
workspaceViewFilterGroup: Pick<ViewFilterGroupWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFilterGroupRepository.delete({
id: workspaceViewFilterGroup.id,
workspaceId,
});
}
public async restoreCoreViewFilterGroup(
workspaceId: string,
workspaceViewFilterGroup: ViewFilterGroupWorkspaceEntity,
): Promise<void> {
await this.coreViewFilterGroupRepository.restore({
id: workspaceViewFilterGroup.id,
workspaceId,
});
}
}
@@ -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<ViewFilterEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
): Partial<ViewFilterEntity> {
const updateData: Record<string, unknown> = {};
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<ViewFilterEntity>;
}
public async createCoreViewFilter(
workspaceId: string,
workspaceViewFilter: ViewFilterWorkspaceEntity,
): Promise<void> {
if (!workspaceViewFilter.viewId) {
return;
}
const coreViewFilter: Partial<ViewFilterEntity> = {
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<ObjectRecordDiff<ViewFilterWorkspaceEntity>>,
): Promise<void> {
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<ViewFilterWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFilterRepository.softDelete({
id: workspaceViewFilter.id,
workspaceId,
});
}
public async destroyCoreViewFilter(
workspaceId: string,
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFilterRepository.delete({
id: workspaceViewFilter.id,
workspaceId,
});
}
public async restoreCoreViewFilter(
workspaceId: string,
workspaceViewFilter: Pick<ViewFilterWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewFilterRepository.restore({
id: workspaceViewFilter.id,
workspaceId,
});
}
}
@@ -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<ViewGroupEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
): Partial<ViewGroupEntity> {
const updateData: Record<string, unknown> = {};
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<ViewGroupEntity>;
}
public async createCoreViewGroup(
workspaceId: string,
workspaceViewGroup: ViewGroupWorkspaceEntity,
): Promise<void> {
if (!workspaceViewGroup.viewId) {
return;
}
const coreViewGroup: Partial<ViewGroupEntity> = {
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<ObjectRecordDiff<ViewGroupWorkspaceEntity>>,
): Promise<void> {
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<ViewGroupWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewGroupRepository.softDelete({
id: workspaceViewGroup.id,
workspaceId,
});
}
public async destroyCoreViewGroup(
workspaceId: string,
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewGroupRepository.delete({
id: workspaceViewGroup.id,
workspaceId,
});
}
public async restoreCoreViewGroup(
workspaceId: string,
workspaceViewGroup: Pick<ViewGroupWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewGroupRepository.restore({
id: workspaceViewGroup.id,
workspaceId,
});
}
}
@@ -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<ViewSortEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
): Partial<ViewSortEntity> {
const updateData: Record<string, unknown> = {};
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<ViewSortEntity>;
}
public async createCoreViewSort(
workspaceId: string,
workspaceViewSort: ViewSortWorkspaceEntity,
): Promise<void> {
if (!workspaceViewSort.viewId) {
return;
}
const direction =
workspaceViewSort.direction.toUpperCase() as ViewSortDirection;
const coreViewSort: Partial<ViewSortEntity> = {
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<ObjectRecordDiff<ViewSortWorkspaceEntity>>,
): Promise<void> {
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<ViewSortWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewSortRepository.softDelete({
id: workspaceViewSort.id,
workspaceId,
});
}
public async destroyCoreViewSort(
workspaceId: string,
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewSortRepository.delete({
id: workspaceViewSort.id,
workspaceId,
});
}
public async restoreCoreViewSort(
workspaceId: string,
workspaceViewSort: Pick<ViewSortWorkspaceEntity, 'id'>,
): Promise<void> {
await this.coreViewSortRepository.restore({
id: workspaceViewSort.id,
workspaceId,
});
}
}
@@ -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<ViewEntity>,
) {}
private parseUpdateDataFromDiff(
diff: Partial<ObjectRecordDiff<ViewWorkspaceEntity>>,
): Partial<ViewEntity> {
const updateData: Record<string, unknown> = {};
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<ViewEntity>;
}
public async createCoreView(
workspaceId: string,
workspaceView: ViewWorkspaceEntity,
): Promise<void> {
let viewName = workspaceView.name;
if (workspaceView.key === 'INDEX') {
// All INDEX views use the template for consistency
viewName = 'All {objectLabelPlural}';
}
const coreView: Partial<ViewEntity> = {
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<ObjectRecordDiff<ViewWorkspaceEntity>>,
): Promise<void> {
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<void> {
await this.coreViewRepository.softDelete({
id: workspaceView.id,
workspaceId,
});
}
public async destroyCoreView(
workspaceId: string,
workspaceView: ViewWorkspaceEntity,
): Promise<void> {
await this.coreViewRepository.delete({
id: workspaceView.id,
workspaceId,
});
}
public async restoreCoreView(
workspaceId: string,
workspaceView: ViewWorkspaceEntity,
): Promise<void> {
await this.coreViewRepository.restore({
id: workspaceView.id,
workspaceId,
});
}
}
@@ -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,
},
);
}
}
@@ -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 {}
@@ -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 () => {
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
@@ -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',
},
});
@@ -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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"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<String>,
"value": "["OPTION_0","OPTION_1","OPTION_2"]",
}
[
"OPTION_0",
"OPTION_1",
"OPTION_2",
]
`;
@@ -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);
});
});
});
@@ -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<string[]>(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: {
@@ -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({
@@ -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);
});
});
});
@@ -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);
});
});
});
@@ -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);
});
});
});
@@ -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);
});
});
});
@@ -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 () => {
@@ -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);
});
});
});
@@ -32,3 +32,16 @@ export const assertRestApiErrorResponse = <T = Record<string, unknown>>(
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);
}
};
@@ -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<ViewEntity | null> => {
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<ViewFilterEntity | null> => {
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<ViewEntity> = {},
): Promise<ViewEntity> => {