Update store on data model mutation (#18684)
- enrich SSE events with relations - remove queries from sse metadata events - on sse event, manage store - on object/field metadata changes, manage store TODO left: - fix other metadata items --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions <github-actions@twenty.com> Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
co-authored by
github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
github-actions
Charles Bochet
parent
e62dfae741
commit
994215e0dc
File diff suppressed because one or more lines are too long
+42
-9
@@ -1,11 +1,14 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { patchMetadataStoreFromSSEEvent } from '@/metadata-store/utils/patchMetadataStoreFromSSEEvent';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type MetadataEntityKey } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
|
||||
import { mapAllMetadataNameToEntityKey } from '@/metadata-store/utils/mapAllMetadataNameToEntityKey';
|
||||
import { useStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type AnyMetadataEntity = MetadataEntityTypeMap[MetadataEntityKey];
|
||||
|
||||
export const MetadataStoreSSEEffect = () => {
|
||||
const store = useStore();
|
||||
const { addToDraft, removeFromDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
@@ -15,12 +18,42 @@ export const MetadataStoreSSEEffect = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
patchMetadataStoreFromSSEEvent({
|
||||
store,
|
||||
entityKey,
|
||||
operation: eventDetail.operation,
|
||||
updatedCollectionHash: eventDetail.updatedCollectionHash,
|
||||
});
|
||||
const collectionHash = eventDetail.updatedCollectionHash;
|
||||
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
addToDraft({
|
||||
key: entityKey,
|
||||
items: [
|
||||
eventDetail.operation
|
||||
.createdRecord as unknown as AnyMetadataEntity,
|
||||
],
|
||||
collectionHash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
addToDraft({
|
||||
key: entityKey,
|
||||
items: [
|
||||
eventDetail.operation
|
||||
.updatedRecord as unknown as AnyMetadataEntity,
|
||||
],
|
||||
collectionHash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
removeFromDraft({
|
||||
key: entityKey,
|
||||
itemIds: [eventDetail.operation.deletedRecordId],
|
||||
collectionHash,
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
applyChanges();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+17
-13
@@ -6,7 +6,7 @@ import { useCallback } from 'react';
|
||||
const MOCKED_COLLECTION_HASH = 'mocked';
|
||||
|
||||
export const useLoadMockedMinimalMetadata = () => {
|
||||
const { updateDraft, applyChanges, resetMetadataStore } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges, resetMetadataStore } = useMetadataStore();
|
||||
|
||||
const loadMockedMinimalMetadata = useCallback(async () => {
|
||||
resetMetadataStore();
|
||||
@@ -25,9 +25,9 @@ export const useLoadMockedMinimalMetadata = () => {
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('indexMetadataItems', flatIndexes, MOCKED_COLLECTION_HASH);
|
||||
|
||||
const {
|
||||
flatViews,
|
||||
@@ -39,26 +39,30 @@ export const useLoadMockedMinimalMetadata = () => {
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(mockedViews);
|
||||
|
||||
updateDraft('views', flatViews, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
|
||||
updateDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
|
||||
updateDraft(
|
||||
replaceDraft('views', flatViews, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('viewFields', flatViewFields, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('viewFilters', flatViewFilters, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('viewSorts', flatViewSorts, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft('viewGroups', flatViewGroups, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft(
|
||||
'viewFilterGroups',
|
||||
flatViewFilterGroups,
|
||||
MOCKED_COLLECTION_HASH,
|
||||
);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups, MOCKED_COLLECTION_HASH);
|
||||
replaceDraft(
|
||||
'viewFieldGroups',
|
||||
flatViewFieldGroups,
|
||||
MOCKED_COLLECTION_HASH,
|
||||
);
|
||||
|
||||
updateDraft(
|
||||
replaceDraft(
|
||||
'navigationMenuItems',
|
||||
mockedNavigationMenuItems,
|
||||
MOCKED_COLLECTION_HASH,
|
||||
);
|
||||
|
||||
applyChanges();
|
||||
}, [updateDraft, applyChanges, resetMetadataStore]);
|
||||
}, [replaceDraft, applyChanges, resetMetadataStore]);
|
||||
|
||||
return { loadMockedMinimalMetadata };
|
||||
};
|
||||
|
||||
+20
-17
@@ -54,7 +54,7 @@ const hasOverlap = (
|
||||
export const useLoadStaleMetadataEntities = () => {
|
||||
const client = useApolloClient();
|
||||
const store = useStore();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const loadStaleMetadataEntities = useCallback(
|
||||
async (staleEntityKeys: MetadataEntityKey[]) => {
|
||||
@@ -80,9 +80,9 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(compositeObjects);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects);
|
||||
updateDraft('fieldMetadataItems', flatFields);
|
||||
updateDraft('indexMetadataItems', flatIndexes);
|
||||
replaceDraft('objectMetadataItems', flatObjects);
|
||||
replaceDraft('fieldMetadataItems', flatFields);
|
||||
replaceDraft('indexMetadataItems', flatIndexes);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -116,13 +116,13 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(allViews);
|
||||
|
||||
updateDraft('views', flatViews);
|
||||
updateDraft('viewFields', flatViewFields);
|
||||
updateDraft('viewFilters', flatViewFilters);
|
||||
updateDraft('viewSorts', flatViewSorts);
|
||||
updateDraft('viewGroups', flatViewGroups);
|
||||
updateDraft('viewFilterGroups', flatViewFilterGroups);
|
||||
updateDraft('viewFieldGroups', flatViewFieldGroups);
|
||||
replaceDraft('views', flatViews);
|
||||
replaceDraft('viewFields', flatViewFields);
|
||||
replaceDraft('viewFilters', flatViewFilters);
|
||||
replaceDraft('viewSorts', flatViewSorts);
|
||||
replaceDraft('viewGroups', flatViewGroups);
|
||||
replaceDraft('viewFilterGroups', flatViewFilterGroups);
|
||||
replaceDraft('viewFieldGroups', flatViewFieldGroups);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -148,9 +148,9 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
flatPageLayoutWidgets,
|
||||
} = splitPageLayoutWithRelated(transformed);
|
||||
|
||||
updateDraft('pageLayouts', flatPageLayouts);
|
||||
updateDraft('pageLayoutTabs', flatPageLayoutTabs);
|
||||
updateDraft('pageLayoutWidgets', flatPageLayoutWidgets);
|
||||
replaceDraft('pageLayouts', flatPageLayouts);
|
||||
replaceDraft('pageLayoutTabs', flatPageLayoutTabs);
|
||||
replaceDraft('pageLayoutWidgets', flatPageLayoutWidgets);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -171,7 +171,10 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
logicFunctionsState.atom,
|
||||
result.data.findManyLogicFunctions,
|
||||
);
|
||||
updateDraft('logicFunctions', result.data.findManyLogicFunctions);
|
||||
replaceDraft(
|
||||
'logicFunctions',
|
||||
result.data.findManyLogicFunctions,
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -188,7 +191,7 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
updateDraft(
|
||||
replaceDraft(
|
||||
'navigationMenuItems',
|
||||
result.data.navigationMenuItems,
|
||||
);
|
||||
@@ -199,7 +202,7 @@ export const useLoadStaleMetadataEntities = () => {
|
||||
await Promise.all(fetchPromises);
|
||||
applyChanges();
|
||||
},
|
||||
[client, store, updateDraft, applyChanges],
|
||||
[client, store, replaceDraft, applyChanges],
|
||||
);
|
||||
|
||||
return { loadStaleMetadataEntities };
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
|
||||
import { useStore, type createStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
type JotaiStore = ReturnType<typeof createStore>;
|
||||
@@ -58,7 +59,7 @@ const changeMetadataEntityAsUpToDate = (
|
||||
export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
const replaceDraft = useCallback(
|
||||
<K extends MetadataEntityKey>(
|
||||
key: K,
|
||||
data: MetadataEntityTypeMap[K][],
|
||||
@@ -123,9 +124,159 @@ export const useMetadataStore = () => {
|
||||
return { hasPersistedAnyMetadataEntity };
|
||||
}, [store]);
|
||||
|
||||
const addToDraft = useCallback(
|
||||
<K extends MetadataEntityKey>({
|
||||
key,
|
||||
items,
|
||||
collectionHash,
|
||||
}: {
|
||||
key: K;
|
||||
items: MetadataEntityTypeMap[K][];
|
||||
collectionHash?: string;
|
||||
}) => {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
const baseItems = (
|
||||
entry.status === 'draft-pending' ? entry.draft : entry.current
|
||||
) as Array<{ id: string }>;
|
||||
|
||||
let newItems = [...baseItems];
|
||||
|
||||
for (const item of items) {
|
||||
const newItem = item as unknown as { id: string };
|
||||
const existingIndex = newItems.findIndex(
|
||||
(existing) => existing.id === newItem.id,
|
||||
);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
if (!isDeeplyEqual(newItems[existingIndex], newItem)) {
|
||||
newItems[existingIndex] = newItem;
|
||||
}
|
||||
} else {
|
||||
newItems = [...newItems, newItem];
|
||||
}
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
...entry,
|
||||
draft: newItems,
|
||||
status: 'draft-pending' as const,
|
||||
...(collectionHash !== undefined
|
||||
? { draftCollectionHash: collectionHash }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const updateInDraft = useCallback(
|
||||
<K extends MetadataEntityKey>(
|
||||
key: K,
|
||||
items: MetadataEntityTypeMap[K][],
|
||||
collectionHash?: string,
|
||||
) => {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
const baseItems = (
|
||||
entry.status === 'draft-pending' ? entry.draft : entry.current
|
||||
) as Array<{ id: string }>;
|
||||
|
||||
let hasChanged = false;
|
||||
const newItems = baseItems.map((existing) => {
|
||||
const updatedItem = (items as unknown as Array<{ id: string }>).find(
|
||||
(item) => item.id === existing.id,
|
||||
);
|
||||
|
||||
if (!isDefined(updatedItem)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
const merged = { ...existing, ...updatedItem };
|
||||
|
||||
if (isDeeplyEqual(existing, merged)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
hasChanged = true;
|
||||
|
||||
return merged;
|
||||
});
|
||||
|
||||
if (!hasChanged && collectionHash === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
...entry,
|
||||
draft: hasChanged ? newItems : baseItems,
|
||||
status: 'draft-pending' as const,
|
||||
...(collectionHash !== undefined
|
||||
? { draftCollectionHash: collectionHash }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const removeFromDraft = useCallback(
|
||||
<K extends MetadataEntityKey>({
|
||||
key,
|
||||
itemIds,
|
||||
collectionHash,
|
||||
}: {
|
||||
key: K;
|
||||
itemIds: string[];
|
||||
collectionHash?: string;
|
||||
}) => {
|
||||
if (itemIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = store.get(metadataStoreState.atomFamily(key));
|
||||
const baseItems = (
|
||||
entry.status === 'draft-pending' ? entry.draft : entry.current
|
||||
) as Array<{ id: string }>;
|
||||
|
||||
const idsToDelete = new Set(itemIds);
|
||||
const newItems = baseItems.filter(
|
||||
(existing) => !idsToDelete.has(existing.id),
|
||||
);
|
||||
|
||||
if (
|
||||
newItems.length === baseItems.length &&
|
||||
collectionHash === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.set(metadataStoreState.atomFamily(key), {
|
||||
...entry,
|
||||
draft: newItems,
|
||||
status: 'draft-pending' as const,
|
||||
...(collectionHash !== undefined
|
||||
? { draftCollectionHash: collectionHash }
|
||||
: {}),
|
||||
});
|
||||
},
|
||||
[store],
|
||||
);
|
||||
|
||||
const reset = useCallback(() => {
|
||||
resetMetadataStore(store);
|
||||
}, [store]);
|
||||
|
||||
return { updateDraft, applyChanges, resetMetadataStore: reset };
|
||||
return {
|
||||
replaceDraft,
|
||||
applyChanges,
|
||||
addToDraft,
|
||||
updateInDraft,
|
||||
removeFromDraft,
|
||||
resetMetadataStore: reset,
|
||||
};
|
||||
};
|
||||
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
import {
|
||||
metadataStoreState,
|
||||
type MetadataEntityKey,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { type createStore } from 'jotai';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type JotaiStore = ReturnType<typeof createStore>;
|
||||
|
||||
type SSEEventOperation =
|
||||
| {
|
||||
type: 'create';
|
||||
createdRecord: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'update';
|
||||
updatedRecord: Record<string, unknown>;
|
||||
}
|
||||
| {
|
||||
type: 'delete';
|
||||
deletedRecordId: string;
|
||||
};
|
||||
|
||||
export const patchMetadataStoreFromSSEEvent = ({
|
||||
store,
|
||||
entityKey,
|
||||
operation,
|
||||
updatedCollectionHash,
|
||||
}: {
|
||||
store: JotaiStore;
|
||||
entityKey: MetadataEntityKey;
|
||||
operation: SSEEventOperation;
|
||||
updatedCollectionHash?: string;
|
||||
}) => {
|
||||
const entry = store.get(metadataStoreState.atomFamily(entityKey));
|
||||
const currentItems = entry.current as Array<{ id: string }>;
|
||||
|
||||
const hashUpdate = isDefined(updatedCollectionHash)
|
||||
? { currentCollectionHash: updatedCollectionHash }
|
||||
: {};
|
||||
|
||||
switch (operation.type) {
|
||||
case 'create': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: [...currentItems, operation.createdRecord],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: currentItems.map((item) =>
|
||||
item.id === (operation.updatedRecord as { id: string }).id
|
||||
? { ...item, ...operation.updatedRecord }
|
||||
: item,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
store.set(metadataStoreState.atomFamily(entityKey), {
|
||||
...entry,
|
||||
...hashUpdate,
|
||||
current: currentItems.filter(
|
||||
(item) => item.id !== operation.deletedRecordId,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
+2
-2
@@ -1,12 +1,12 @@
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { NavigationMenuItemType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { CreateNavigationMenuItemDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useNavigationMenuItemsData } from '@/navigation-menu-item/display/hooks/useNavigationMenuItemsData';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
export const useCreateNavigationMenuItem = () => {
|
||||
const { navigationMenuItems, currentWorkspaceMemberId } =
|
||||
|
||||
+3
-3
@@ -14,7 +14,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
const store = useStore();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
const cache = apolloCoreClient.cache;
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const removeNavigationMenuItemsByTargetRecordIds = useCallback(
|
||||
(targetRecordIds: string[]) => {
|
||||
@@ -31,7 +31,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
!targetRecordIdsSet.has(item.targetRecordId),
|
||||
);
|
||||
|
||||
updateDraft('navigationMenuItems', updatedNavigationMenuItems);
|
||||
replaceDraft('navigationMenuItems', updatedNavigationMenuItems);
|
||||
applyChanges();
|
||||
|
||||
cache.updateQuery(
|
||||
@@ -48,7 +48,7 @@ export const useRemoveNavigationMenuItemByTargetRecordId = () => {
|
||||
},
|
||||
);
|
||||
},
|
||||
[cache, store, updateDraft, applyChanges],
|
||||
[cache, store, replaceDraft, applyChanges],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,6 +19,71 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
|
||||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
isLabelSyncedWithName
|
||||
fieldsList {
|
||||
id
|
||||
universalIdentifier
|
||||
type
|
||||
name
|
||||
label
|
||||
description
|
||||
icon
|
||||
isCustom
|
||||
isActive
|
||||
isSystem
|
||||
isUIReadOnly
|
||||
isNullable
|
||||
isUnique
|
||||
createdAt
|
||||
updatedAt
|
||||
defaultValue
|
||||
options
|
||||
settings
|
||||
isLabelSyncedWithName
|
||||
morphId
|
||||
applicationId
|
||||
relation {
|
||||
type
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
morphRelations {
|
||||
type
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -46,6 +111,48 @@ export const CREATE_ONE_FIELD_METADATA_ITEM = gql`
|
||||
object {
|
||||
id
|
||||
}
|
||||
relation {
|
||||
type
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
morphRelations {
|
||||
type
|
||||
sourceObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
targetObjectMetadata {
|
||||
id
|
||||
nameSingular
|
||||
namePlural
|
||||
}
|
||||
sourceFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
targetFieldMetadata {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+6
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
CreateOneObjectMetadataItemDocument,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
FindManyViewsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
@@ -7,6 +8,9 @@ export const query = CreateOneObjectMetadataItemDocument;
|
||||
|
||||
export const findManyViewsQuery = FindManyViewsDocument;
|
||||
|
||||
export const findManyNavigationMenuItemsQuery =
|
||||
FindManyNavigationMenuItemsDocument;
|
||||
|
||||
export const variables = {
|
||||
input: {
|
||||
object: {
|
||||
@@ -35,4 +39,6 @@ export const responseData = {
|
||||
updatedAt: '',
|
||||
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
isLabelSyncedWithName: false,
|
||||
fieldsList: [],
|
||||
};
|
||||
|
||||
+12
@@ -4,6 +4,7 @@ import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreat
|
||||
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import {
|
||||
findManyNavigationMenuItemsQuery,
|
||||
findManyViewsQuery,
|
||||
query,
|
||||
responseData,
|
||||
@@ -63,6 +64,17 @@ const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
request: {
|
||||
query: findManyNavigationMenuItemsQuery,
|
||||
variables: {},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
navigationMenuItems: [],
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
|
||||
+21
@@ -5,10 +5,13 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
|
||||
export const useCreateOneFieldMetadataItem = () => {
|
||||
@@ -18,6 +21,7 @@ export const useCreateOneFieldMetadataItem = () => {
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { addToDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const createOneFieldMetadataItem = async (
|
||||
input: CreateFieldInput,
|
||||
@@ -35,6 +39,23 @@ export const useCreateOneFieldMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const createdField = response.data?.createOneField;
|
||||
|
||||
if (isDefined(createdField)) {
|
||||
const { __typename, object, ...fieldData } = createdField;
|
||||
|
||||
addToDraft({
|
||||
key: 'fieldMetadataItems',
|
||||
items: [
|
||||
{
|
||||
...fieldData,
|
||||
objectMetadataId: object?.id ?? input.objectMetadataId,
|
||||
} as FlatFieldMetadataItem,
|
||||
],
|
||||
});
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response,
|
||||
|
||||
+78
-1
@@ -1,22 +1,32 @@
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import {
|
||||
type CreateObjectInput,
|
||||
CreateOneObjectMetadataItemDocument,
|
||||
FindManyNavigationMenuItemsDocument,
|
||||
FindManyViewsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { splitViewWithRelated } from '@/metadata-store/utils/splitViewWithRelated';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useCreateOneObjectMetadataItem = () => {
|
||||
const [createOneObjectMetadataItemMutation] = useMutation(
|
||||
CreateOneObjectMetadataItemDocument,
|
||||
);
|
||||
|
||||
const client = useApolloClient();
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { addToDraft, replaceDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const createOneObjectMetadataItem = async (
|
||||
input: CreateObjectInput,
|
||||
@@ -32,6 +42,73 @@ export const useCreateOneObjectMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const createdObject = createdObjectMetadata.data?.createOneObject;
|
||||
|
||||
if (isDefined(createdObject)) {
|
||||
const {
|
||||
__typename: _objectTypename,
|
||||
fieldsList,
|
||||
...objectData
|
||||
} = createdObject;
|
||||
|
||||
addToDraft({
|
||||
key: 'objectMetadataItems',
|
||||
items: [objectData as FlatObjectMetadataItem],
|
||||
});
|
||||
|
||||
const flatFields = fieldsList.map((field) => {
|
||||
const { __typename: _fieldTypename, ...fieldData } = field;
|
||||
|
||||
return {
|
||||
...fieldData,
|
||||
objectMetadataId: createdObject.id,
|
||||
} as FlatFieldMetadataItem;
|
||||
});
|
||||
|
||||
addToDraft({ key: 'fieldMetadataItems', items: flatFields });
|
||||
|
||||
applyChanges();
|
||||
|
||||
const [viewsResult, navItemsResult] = await Promise.all([
|
||||
client.query({
|
||||
query: FindManyViewsDocument,
|
||||
variables: { objectMetadataId: createdObject.id },
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
fetchPolicy: 'network-only',
|
||||
}),
|
||||
]);
|
||||
|
||||
const fetchedViews = viewsResult.data?.getViews ?? [];
|
||||
|
||||
const {
|
||||
flatViews,
|
||||
flatViewFields,
|
||||
flatViewFilters,
|
||||
flatViewSorts,
|
||||
flatViewGroups,
|
||||
flatViewFilterGroups,
|
||||
flatViewFieldGroups,
|
||||
} = splitViewWithRelated(fetchedViews);
|
||||
|
||||
addToDraft({ key: 'views', items: flatViews });
|
||||
addToDraft({ key: 'viewFields', items: flatViewFields });
|
||||
addToDraft({ key: 'viewFilters', items: flatViewFilters });
|
||||
addToDraft({ key: 'viewSorts', items: flatViewSorts });
|
||||
addToDraft({ key: 'viewGroups', items: flatViewGroups });
|
||||
addToDraft({ key: 'viewFilterGroups', items: flatViewFilterGroups });
|
||||
addToDraft({ key: 'viewFieldGroups', items: flatViewFieldGroups });
|
||||
|
||||
replaceDraft(
|
||||
'navigationMenuItems',
|
||||
navItemsResult.data?.navigationMenuItems ?? [],
|
||||
);
|
||||
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response: createdObjectMetadata,
|
||||
|
||||
+6
-1
@@ -3,6 +3,7 @@ import { DeleteOneFieldMetadataItemDocument } from '~/generated-metadata/graphql
|
||||
|
||||
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { recordIndexGroupAggregateFieldMetadataItemComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateFieldMetadataItemComponentState';
|
||||
import { recordIndexGroupAggregateOperationComponentState } from '@/object-record/record-index/states/recordIndexGroupAggregateOperationComponentState';
|
||||
@@ -21,6 +22,7 @@ export const useDeleteOneFieldMetadataItem = () => {
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { removeFromDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const setRecordIndexGroupAggregateOperation = useSetAtomComponentState(
|
||||
recordIndexGroupAggregateOperationComponentState,
|
||||
@@ -63,7 +65,10 @@ export const useDeleteOneFieldMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: see if we can remove this lin altogether
|
||||
removeFromDraft({ key: 'fieldMetadataItems', itemIds: [idToDelete] });
|
||||
applyChanges();
|
||||
|
||||
// TODO: see if we can remove this line altogether
|
||||
await resetRecordIndexKanbanAggregateOperation(idToDelete);
|
||||
|
||||
return {
|
||||
|
||||
+5
@@ -2,6 +2,7 @@ import { useMutation } from '@apollo/client/react';
|
||||
import { DeleteOneObjectMetadataItemDocument } from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
@@ -15,6 +16,7 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { removeFromDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const deleteOneObjectMetadataItem = async (
|
||||
idToDelete: string,
|
||||
@@ -30,6 +32,9 @@ export const useDeleteOneObjectMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
removeFromDraft({ key: 'objectMetadataItems', itemIds: [idToDelete] });
|
||||
applyChanges();
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response,
|
||||
|
||||
+5
-5
@@ -3,7 +3,7 @@ import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/split
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const useLoadMockedObjectMetadataItems = () => {
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const loadMockedObjectMetadataItems = useCallback(async () => {
|
||||
const { generatedMockObjectMetadataItems } = await import(
|
||||
@@ -13,11 +13,11 @@ export const useLoadMockedObjectMetadataItems = () => {
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects);
|
||||
updateDraft('fieldMetadataItems', flatFields);
|
||||
updateDraft('indexMetadataItems', flatIndexes);
|
||||
replaceDraft('objectMetadataItems', flatObjects);
|
||||
replaceDraft('fieldMetadataItems', flatFields);
|
||||
replaceDraft('indexMetadataItems', flatIndexes);
|
||||
applyChanges();
|
||||
}, [updateDraft, applyChanges]);
|
||||
}, [replaceDraft, applyChanges]);
|
||||
|
||||
return {
|
||||
loadMockedObjectMetadataItems,
|
||||
|
||||
+18
@@ -5,12 +5,15 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { lastFieldMetadataItemUpdateState } from '@/object-metadata/states/lastFieldMetadataItemUpdateState';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@@ -22,6 +25,7 @@ export const useUpdateOneFieldMetadataItem = () => {
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { updateInDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const setLastFieldMetadataItemUpdate = useSetAtomState(
|
||||
lastFieldMetadataItemUpdateState,
|
||||
@@ -58,6 +62,20 @@ export const useUpdateOneFieldMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const updatedField = response.data?.updateOneField;
|
||||
|
||||
if (isDefined(updatedField)) {
|
||||
const { __typename, object, ...fieldData } = updatedField;
|
||||
|
||||
updateInDraft('fieldMetadataItems', [
|
||||
{
|
||||
...fieldData,
|
||||
objectMetadataId: object?.id ?? objectMetadataId,
|
||||
} as FlatFieldMetadataItem,
|
||||
]);
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
setLastFieldMetadataItemUpdate({
|
||||
fieldMetadataItemId: fieldMetadataIdToUpdate,
|
||||
objectMetadataId,
|
||||
|
||||
+15
@@ -5,10 +5,13 @@ import {
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import { useMetadataErrorHandler } from '@/metadata-error-handler/hooks/useMetadataErrorHandler';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { type MetadataRequestResult } from '@/object-metadata/types/MetadataRequestResult.type';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { CrudOperationType } from 'twenty-shared/types';
|
||||
|
||||
// TODO: Slice the Apollo store synchronously in the update function instead of subscribing, so we can use update after read in the same function call
|
||||
@@ -19,6 +22,7 @@ export const useUpdateOneObjectMetadataItem = () => {
|
||||
|
||||
const { handleMetadataError } = useMetadataErrorHandler();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const { updateInDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
const updateOneObjectMetadataItem = async ({
|
||||
idToUpdate,
|
||||
@@ -39,6 +43,17 @@ export const useUpdateOneObjectMetadataItem = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const updatedObject = response.data?.updateOneObject;
|
||||
|
||||
if (isDefined(updatedObject)) {
|
||||
const { __typename, ...objectData } = updatedObject;
|
||||
|
||||
updateInDraft('objectMetadataItems', [
|
||||
objectData as FlatObjectMetadataItem,
|
||||
]);
|
||||
applyChanges();
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'successful',
|
||||
response,
|
||||
|
||||
+5
-5
@@ -53,7 +53,7 @@ const RelationFieldValueSetterEffect = () => {
|
||||
'recordTableId',
|
||||
);
|
||||
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useEffect(() => {
|
||||
setRecordStore(mockPerformance.entityValue);
|
||||
@@ -74,14 +74,14 @@ const RelationFieldValueSetterEffect = () => {
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects);
|
||||
updateDraft('fieldMetadataItems', flatFields);
|
||||
updateDraft('indexMetadataItems', flatIndexes);
|
||||
replaceDraft('objectMetadataItems', flatObjects);
|
||||
replaceDraft('fieldMetadataItems', flatFields);
|
||||
replaceDraft('indexMetadataItems', flatIndexes);
|
||||
applyChanges();
|
||||
}, [
|
||||
setRecordStore,
|
||||
setRelationRecordStore,
|
||||
updateDraft,
|
||||
replaceDraft,
|
||||
applyChanges,
|
||||
setCurrentRecordFields,
|
||||
]);
|
||||
|
||||
+10
-12
@@ -20,19 +20,17 @@ export const ON_EVENT_SUBSCRIPTION = gql`
|
||||
}
|
||||
queryIds
|
||||
}
|
||||
metadataEventsWithQueryIds {
|
||||
metadataEvent {
|
||||
type
|
||||
metadataName
|
||||
recordId
|
||||
properties {
|
||||
updatedFields
|
||||
before
|
||||
after
|
||||
diff
|
||||
}
|
||||
metadataEvents {
|
||||
type
|
||||
metadataName
|
||||
recordId
|
||||
updatedCollectionHash
|
||||
properties {
|
||||
updatedFields
|
||||
before
|
||||
after
|
||||
diff
|
||||
}
|
||||
queryIds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-7
@@ -4,7 +4,6 @@ import { useCallback } from 'react';
|
||||
import {
|
||||
type AllMetadataName,
|
||||
type MetadataEvent,
|
||||
type MetadataEventWithQueryIds,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const groupSseMetadataEventsByMetadataName = (
|
||||
@@ -27,13 +26,9 @@ export const useDispatchMetadataEventsFromSseToBrowserEvents = <
|
||||
T extends Record<string, unknown>,
|
||||
>() => {
|
||||
const dispatchMetadataEventsFromSseToBrowserEvents = useCallback(
|
||||
(metadataEventsWithQueryIds: MetadataEventWithQueryIds[]) => {
|
||||
const sseMetadataEvents = metadataEventsWithQueryIds.map(
|
||||
(item) => item.metadataEvent,
|
||||
);
|
||||
|
||||
(metadataEvents: MetadataEvent[]) => {
|
||||
const eventsByMetadataName =
|
||||
groupSseMetadataEventsByMetadataName(sseMetadataEvents);
|
||||
groupSseMetadataEventsByMetadataName(metadataEvents);
|
||||
|
||||
for (const [metadataName, events] of eventsByMetadataName) {
|
||||
const metadataOperationBrowserEvents =
|
||||
|
||||
+7
-5
@@ -98,8 +98,7 @@ export const useTriggerEventStreamCreation = () => {
|
||||
const objectRecordEventsWithQueryIds =
|
||||
eventSubscription?.objectRecordEventsWithQueryIds ?? [];
|
||||
|
||||
const metadataEventsWithQueryIds =
|
||||
eventSubscription?.metadataEventsWithQueryIds ?? [];
|
||||
const metadataEvents = eventSubscription?.metadataEvents ?? [];
|
||||
|
||||
const objectRecordEvents = objectRecordEventsWithQueryIds.map(
|
||||
(item) => item.objectRecordEvent,
|
||||
@@ -113,9 +112,7 @@ export const useTriggerEventStreamCreation = () => {
|
||||
objectRecordEventsWithQueryIds,
|
||||
);
|
||||
|
||||
dispatchMetadataEventsFromSseToBrowserEvents(
|
||||
metadataEventsWithQueryIds,
|
||||
);
|
||||
dispatchMetadataEventsFromSseToBrowserEvents(metadataEvents);
|
||||
},
|
||||
error: (error) => {
|
||||
captureException(error);
|
||||
@@ -169,6 +166,11 @@ export const useTriggerEventStreamCreation = () => {
|
||||
dispatchObjectRecordEventsFromSseToBrowserEvents(
|
||||
objectRecordEventsWithQueryIds,
|
||||
);
|
||||
|
||||
const metadataEvents =
|
||||
result?.data?.onEventSubscription?.metadataEvents ?? [];
|
||||
|
||||
dispatchMetadataEventsFromSseToBrowserEvents(metadataEvents);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -12,7 +12,7 @@ export const JestObjectMetadataItemSetter = ({
|
||||
children: ReactNode;
|
||||
objectMetadataItems?: ObjectMetadataItem[];
|
||||
}) => {
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
const { replaceDraft, applyChanges } = useMetadataStore();
|
||||
const [isLoaded, setIsLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -20,12 +20,12 @@ export const JestObjectMetadataItemSetter = ({
|
||||
const { flatObjects, flatFields, flatIndexes } =
|
||||
splitObjectMetadataItemWithRelated(items);
|
||||
|
||||
updateDraft('objectMetadataItems', flatObjects);
|
||||
updateDraft('fieldMetadataItems', flatFields);
|
||||
updateDraft('indexMetadataItems', flatIndexes);
|
||||
replaceDraft('objectMetadataItems', flatObjects);
|
||||
replaceDraft('fieldMetadataItems', flatFields);
|
||||
replaceDraft('indexMetadataItems', flatIndexes);
|
||||
applyChanges();
|
||||
setIsLoaded(true);
|
||||
}, [objectMetadataItems, updateDraft, applyChanges]);
|
||||
}, [objectMetadataItems, replaceDraft, applyChanges]);
|
||||
|
||||
return isLoaded ? <>{children}</> : null;
|
||||
};
|
||||
|
||||
@@ -1282,15 +1282,10 @@ type ObjectRecordEventWithQueryIds {
|
||||
objectRecordEvent: ObjectRecordEvent!
|
||||
}
|
||||
|
||||
type MetadataEventWithQueryIds {
|
||||
queryIds: [String!]!
|
||||
metadataEvent: MetadataEvent!
|
||||
}
|
||||
|
||||
type EventSubscription {
|
||||
eventStreamId: String!
|
||||
objectRecordEventsWithQueryIds: [ObjectRecordEventWithQueryIds!]!
|
||||
metadataEventsWithQueryIds: [MetadataEventWithQueryIds!]!
|
||||
metadataEvents: [MetadataEvent!]!
|
||||
}
|
||||
|
||||
type OnDbEvent {
|
||||
|
||||
@@ -1004,16 +1004,10 @@ export interface ObjectRecordEventWithQueryIds {
|
||||
__typename: 'ObjectRecordEventWithQueryIds'
|
||||
}
|
||||
|
||||
export interface MetadataEventWithQueryIds {
|
||||
queryIds: Scalars['String'][]
|
||||
metadataEvent: MetadataEvent
|
||||
__typename: 'MetadataEventWithQueryIds'
|
||||
}
|
||||
|
||||
export interface EventSubscription {
|
||||
eventStreamId: Scalars['String']
|
||||
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIds[]
|
||||
metadataEventsWithQueryIds: MetadataEventWithQueryIds[]
|
||||
metadataEvents: MetadataEvent[]
|
||||
__typename: 'EventSubscription'
|
||||
}
|
||||
|
||||
@@ -3941,17 +3935,10 @@ export interface ObjectRecordEventWithQueryIdsGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface MetadataEventWithQueryIdsGenqlSelection{
|
||||
queryIds?: boolean | number
|
||||
metadataEvent?: MetadataEventGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EventSubscriptionGenqlSelection{
|
||||
eventStreamId?: boolean | number
|
||||
objectRecordEventsWithQueryIds?: ObjectRecordEventWithQueryIdsGenqlSelection
|
||||
metadataEventsWithQueryIds?: MetadataEventWithQueryIdsGenqlSelection
|
||||
metadataEvents?: MetadataEventGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -6869,14 +6856,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const MetadataEventWithQueryIds_possibleTypes: string[] = ['MetadataEventWithQueryIds']
|
||||
export const isMetadataEventWithQueryIds = (obj?: { __typename?: any } | null): obj is MetadataEventWithQueryIds => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isMetadataEventWithQueryIds"')
|
||||
return MetadataEventWithQueryIds_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EventSubscription_possibleTypes: string[] = ['EventSubscription']
|
||||
export const isEventSubscription = (obj?: { __typename?: any } | null): obj is EventSubscription => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEventSubscription"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -171,6 +171,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint:diff-with-main": {
|
||||
"executor": "nx:run-commands",
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "FILES=$(git diff --name-only --relative --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware -c .oxlintrc.json $FILES && (prettier --check $FILES || (echo 'ERROR: Prettier formatting check failed! Fix with: npx nx lint:diff-with-main twenty-server --configuration=fix' && false)))"
|
||||
},
|
||||
"configurations": {
|
||||
"fix": {
|
||||
"command": "FILES=$(git diff --name-only --relative --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware --fix -c .oxlintrc.json $FILES && prettier --write $FILES)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {},
|
||||
"test:debug": {
|
||||
"executor": "nx:run-commands",
|
||||
|
||||
@@ -10,7 +10,6 @@ import { type IndexMetadataInterface } from 'src/engine/metadata-modules/index-m
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
|
||||
import { filterMorphRelationDuplicateFields } from 'src/engine/dataloaders/utils/filter-morph-relation-duplicate-fields.util';
|
||||
import { pickMorphGroupSurvivor } from 'src/engine/dataloaders/utils/pick-morph-group-survivor.util';
|
||||
import { FIELD_METADATA_STANDARD_OVERRIDES_PROPERTIES } from 'src/engine/metadata-modules/field-metadata/constants/field-metadata-standard-overrides-properties.constant';
|
||||
import { type FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto';
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
@@ -21,11 +20,10 @@ import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
import { fromFlatFieldMetadataToFieldMetadataDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-flat-field-metadata-to-field-metadata-dto.util';
|
||||
import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { resolveMorphRelationsFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-morph-relations-from-flat-field-metadata.util';
|
||||
import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
|
||||
import { fromFlatObjectMetadataToObjectMetadataDto } from 'src/engine/metadata-modules/flat-object-metadata/utils/from-flat-object-metadata-to-object-metadata-dto.util';
|
||||
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
|
||||
import { fromFlatViewFieldGroupToViewFieldGroupDto } from 'src/engine/metadata-modules/view-field-group/utils/from-flat-view-field-group-to-view-field-group-dto.util';
|
||||
@@ -158,7 +156,6 @@ export class DataloaderService {
|
||||
private createRelationLoader() {
|
||||
return new DataLoader<RelationLoaderPayload, RelationDTO | null>(
|
||||
async (dataLoaderParams: RelationLoaderPayload[]) => {
|
||||
const relationDtos: Array<RelationDTO | null> = [];
|
||||
const workspaceId = dataLoaderParams[0].workspaceId;
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
@@ -168,7 +165,7 @@ export class DataloaderService {
|
||||
},
|
||||
);
|
||||
|
||||
for (const { fieldMetadataId } of dataLoaderParams) {
|
||||
return dataLoaderParams.map(({ fieldMetadataId }) => {
|
||||
const sourceFlatFieldMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: fieldMetadataId,
|
||||
@@ -181,85 +178,15 @@ export class DataloaderService {
|
||||
FieldMetadataType.RELATION,
|
||||
)
|
||||
) {
|
||||
relationDtos.push(null);
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: sourceFlatFieldMetadata.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const targetFlatFieldMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
relationDtos.push(null);
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: targetFlatFieldMetadata.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
isFlatFieldMetadataOfType(
|
||||
targetFlatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const morphNameFromMorphFieldMetadataName =
|
||||
getMorphNameFromMorphFieldMetadataName({
|
||||
morphRelationFlatFieldMetadata: targetFlatFieldMetadata,
|
||||
nameSingular: sourceFlatObjectMetadata.nameSingular,
|
||||
namePlural: sourceFlatObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
const allMorphFlatFieldMetadatas = [
|
||||
targetFlatFieldMetadata,
|
||||
...findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: targetFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata: targetFlatObjectMetadata,
|
||||
}),
|
||||
];
|
||||
|
||||
const survivorMorphField = pickMorphGroupSurvivor(
|
||||
allMorphFlatFieldMetadatas,
|
||||
);
|
||||
|
||||
relationDtos.push(
|
||||
fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatFieldMetadata: {
|
||||
...survivorMorphField,
|
||||
name: morphNameFromMorphFieldMetadataName,
|
||||
},
|
||||
targetFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
relationDtos.push(
|
||||
fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata,
|
||||
targetFlatFieldMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return relationDtos;
|
||||
return resolveRelationFromFlatFieldMetadata({
|
||||
sourceFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -275,9 +202,8 @@ export class DataloaderService {
|
||||
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
const relationDtos: Array<RelationDTO[] | null> = [];
|
||||
|
||||
for (const { fieldMetadataId } of dataLoaderParams) {
|
||||
return dataLoaderParams.map(({ fieldMetadataId }) => {
|
||||
const morphFlatFieldMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: fieldMetadataId,
|
||||
@@ -290,70 +216,15 @@ export class DataloaderService {
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
relationDtos.push(null);
|
||||
continue;
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: morphFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
const relatedMorphFlatFieldMetadatas =
|
||||
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: morphFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata: sourceFlatObjectMetadata,
|
||||
});
|
||||
const allMorphFlatFieldMetadatas = [
|
||||
return resolveMorphRelationsFromFlatFieldMetadata({
|
||||
morphFlatFieldMetadata,
|
||||
...relatedMorphFlatFieldMetadatas,
|
||||
];
|
||||
|
||||
relationDtos.push(
|
||||
allMorphFlatFieldMetadatas.flatMap((sourceFlatFieldMetadata) => {
|
||||
const targetFlatFieldMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetFlatObjectMetadata =
|
||||
findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId:
|
||||
sourceFlatFieldMetadata.relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const morphNameFromMorphFieldMetadataName =
|
||||
getMorphNameFromMorphFieldMetadataName({
|
||||
morphRelationFlatFieldMetadata: sourceFlatFieldMetadata,
|
||||
nameSingular: targetFlatObjectMetadata.nameSingular,
|
||||
namePlural: targetFlatObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
return fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata: {
|
||||
...sourceFlatFieldMetadata,
|
||||
name: morphNameFromMorphFieldMetadataName,
|
||||
},
|
||||
targetFlatFieldMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
});
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
return relationDtos;
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
|
||||
|
||||
type ResolveMorphRelationsFromFlatFieldMetadataArgs = {
|
||||
morphFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.MORPH_RELATION>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
};
|
||||
|
||||
export const resolveMorphRelationsFromFlatFieldMetadata = ({
|
||||
morphFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}: ResolveMorphRelationsFromFlatFieldMetadataArgs): RelationDTO[] => {
|
||||
const sourceFlatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
flatEntityId: morphFlatFieldMetadata.objectMetadataId,
|
||||
});
|
||||
|
||||
const relatedMorphFlatFieldMetadatas =
|
||||
findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: morphFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata: sourceFlatObjectMetadata,
|
||||
});
|
||||
|
||||
const allMorphFlatFieldMetadatas = [
|
||||
morphFlatFieldMetadata,
|
||||
...relatedMorphFlatFieldMetadatas,
|
||||
];
|
||||
|
||||
return allMorphFlatFieldMetadatas.flatMap((sourceFlatFieldMetadata) => {
|
||||
const targetFlatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: sourceFlatFieldMetadata.relationTargetObjectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const morphNameFromMorphFieldMetadataName =
|
||||
getMorphNameFromMorphFieldMetadataName({
|
||||
morphRelationFlatFieldMetadata: sourceFlatFieldMetadata,
|
||||
nameSingular: targetFlatObjectMetadata.nameSingular,
|
||||
namePlural: targetFlatObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
return fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata: {
|
||||
...sourceFlatFieldMetadata,
|
||||
name: morphNameFromMorphFieldMetadataName,
|
||||
},
|
||||
targetFlatFieldMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
});
|
||||
});
|
||||
};
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
|
||||
import { pickMorphGroupSurvivor } from 'src/engine/dataloaders/utils/pick-morph-group-survivor.util';
|
||||
import { RelationDTO } from 'src/engine/metadata-modules/field-metadata/dtos/relation.dto';
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { findAllOthersMorphRelationFlatFieldMetadatasOrThrow } from 'src/engine/metadata-modules/flat-field-metadata/utils/find-all-others-morph-relation-flat-field-metadatas-or-throw.util';
|
||||
import { fromMorphOrRelationFlatFieldMetadataToRelationDto } from 'src/engine/metadata-modules/flat-field-metadata/utils/from-morph-or-relation-flat-field-metadata-to-relation-dto.util';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { isMorphOrRelationFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-morph-or-relation-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { getMorphNameFromMorphFieldMetadataName } from 'src/engine/metadata-modules/flat-object-metadata/utils/get-morph-name-from-morph-field-metadata-name.util';
|
||||
|
||||
type ResolveRelationFromFlatFieldMetadataArgs = {
|
||||
sourceFlatFieldMetadata: FlatFieldMetadata<FieldMetadataType.RELATION>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
};
|
||||
|
||||
export const resolveRelationFromFlatFieldMetadata = ({
|
||||
sourceFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}: ResolveRelationFromFlatFieldMetadataArgs): RelationDTO | null => {
|
||||
const sourceFlatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: sourceFlatFieldMetadata.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
const targetFlatFieldMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: sourceFlatFieldMetadata.relationTargetFieldMetadataId,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isMorphOrRelationFlatFieldMetadata(targetFlatFieldMetadata)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const targetFlatObjectMetadata = findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: targetFlatFieldMetadata.objectMetadataId,
|
||||
flatEntityMaps: flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
if (
|
||||
isFlatFieldMetadataOfType(
|
||||
targetFlatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const morphNameFromMorphFieldMetadataName =
|
||||
getMorphNameFromMorphFieldMetadataName({
|
||||
morphRelationFlatFieldMetadata: targetFlatFieldMetadata,
|
||||
nameSingular: sourceFlatObjectMetadata.nameSingular,
|
||||
namePlural: sourceFlatObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
const allMorphFlatFieldMetadatas = [
|
||||
targetFlatFieldMetadata,
|
||||
...findAllOthersMorphRelationFlatFieldMetadatasOrThrow({
|
||||
flatFieldMetadata: targetFlatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadata: targetFlatObjectMetadata,
|
||||
}),
|
||||
];
|
||||
|
||||
const survivorMorphField = pickMorphGroupSurvivor(
|
||||
allMorphFlatFieldMetadatas,
|
||||
);
|
||||
|
||||
return fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
targetFlatFieldMetadata: {
|
||||
...survivorMorphField,
|
||||
name: morphNameFromMorphFieldMetadataName,
|
||||
},
|
||||
targetFlatObjectMetadata,
|
||||
});
|
||||
}
|
||||
|
||||
return fromMorphOrRelationFlatFieldMetadataToRelationDto({
|
||||
sourceFlatFieldMetadata,
|
||||
targetFlatFieldMetadata,
|
||||
targetFlatObjectMetadata,
|
||||
sourceFlatObjectMetadata,
|
||||
});
|
||||
};
|
||||
@@ -12,15 +12,6 @@ export class ObjectRecordEventWithQueryIdsDTO {
|
||||
objectRecordEvent: ObjectRecordEventDTO;
|
||||
}
|
||||
|
||||
@ObjectType('MetadataEventWithQueryIds')
|
||||
export class MetadataEventWithQueryIdsDTO {
|
||||
@Field(() => [String])
|
||||
queryIds: string[];
|
||||
|
||||
@Field(() => MetadataEventDTO)
|
||||
metadataEvent: MetadataEventDTO;
|
||||
}
|
||||
|
||||
@ObjectType('EventSubscription')
|
||||
export class EventSubscriptionDTO {
|
||||
@Field(() => String)
|
||||
@@ -29,6 +20,6 @@ export class EventSubscriptionDTO {
|
||||
@Field(() => [ObjectRecordEventWithQueryIdsDTO])
|
||||
objectRecordEventsWithQueryIds: ObjectRecordEventWithQueryIdsDTO[];
|
||||
|
||||
@Field(() => [MetadataEventWithQueryIdsDTO])
|
||||
metadataEventsWithQueryIds: MetadataEventWithQueryIdsDTO[];
|
||||
@Field(() => [MetadataEventDTO])
|
||||
metadataEvents: MetadataEventDTO[];
|
||||
}
|
||||
|
||||
@@ -6,8 +6,5 @@ export type EventStreamPayload = {
|
||||
queryIds: string[];
|
||||
objectRecordEvent: ObjectRecordSubscriptionEvent;
|
||||
}[];
|
||||
metadataEventsWithQueryIds: {
|
||||
queryIds: string[];
|
||||
metadataEvent: MetadataEvent;
|
||||
}[];
|
||||
metadataEvents: MetadataEvent[];
|
||||
};
|
||||
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { FieldMetadataType } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { isFlatFieldMetadataOfType } from 'src/engine/metadata-modules/flat-field-metadata/utils/is-flat-field-metadata-of-type.util';
|
||||
import { resolveMorphRelationsFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-morph-relations-from-flat-field-metadata.util';
|
||||
import { resolveRelationFromFlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/utils/resolve-relation-from-flat-field-metadata.util';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
|
||||
type EnrichFieldMetadataEventArgs = {
|
||||
record: Record<string, unknown>;
|
||||
flatFieldMetadataMaps: FlatEntityMaps<FlatFieldMetadata>;
|
||||
flatObjectMetadataMaps: FlatEntityMaps<FlatObjectMetadata>;
|
||||
};
|
||||
|
||||
export const enrichFieldMetadataEventWithRelations = ({
|
||||
record,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
}: EnrichFieldMetadataEventArgs): Record<string, unknown> => {
|
||||
const flatFieldMetadata = findFlatEntityByIdInFlatEntityMaps({
|
||||
flatEntityId: record.id as string,
|
||||
flatEntityMaps: flatFieldMetadataMaps,
|
||||
});
|
||||
|
||||
if (!isDefined(flatFieldMetadata)) {
|
||||
return record;
|
||||
}
|
||||
|
||||
try {
|
||||
if (
|
||||
isFlatFieldMetadataOfType(flatFieldMetadata, FieldMetadataType.RELATION)
|
||||
) {
|
||||
const relation = resolveRelationFromFlatFieldMetadata({
|
||||
sourceFlatFieldMetadata: flatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
return isDefined(relation) ? { ...record, relation } : record;
|
||||
}
|
||||
|
||||
if (
|
||||
isFlatFieldMetadataOfType(
|
||||
flatFieldMetadata,
|
||||
FieldMetadataType.MORPH_RELATION,
|
||||
)
|
||||
) {
|
||||
const morphRelations = resolveMorphRelationsFromFlatFieldMetadata({
|
||||
morphFlatFieldMetadata: flatFieldMetadata,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
return { ...record, morphRelations };
|
||||
}
|
||||
} catch {
|
||||
return record;
|
||||
}
|
||||
|
||||
return record;
|
||||
};
|
||||
+2
-2
@@ -90,7 +90,7 @@ export class WorkspaceEventEmitterResolver {
|
||||
return {
|
||||
eventStreamId: variables.eventStreamId,
|
||||
objectRecordEventsWithQueryIds: payload.objectRecordEventsWithQueryIds,
|
||||
metadataEventsWithQueryIds: payload.metadataEventsWithQueryIds,
|
||||
metadataEvents: payload.metadataEvents,
|
||||
};
|
||||
},
|
||||
})
|
||||
@@ -143,7 +143,7 @@ export class WorkspaceEventEmitterResolver {
|
||||
return wrapAsyncIteratorWithLifecycle(iterator, {
|
||||
initialValue: {
|
||||
objectRecordEventsWithQueryIds: [],
|
||||
metadataEventsWithQueryIds: [],
|
||||
metadataEvents: [],
|
||||
},
|
||||
onHeartbeat: () =>
|
||||
this.eventStreamService.refreshEventStreamTTL({
|
||||
|
||||
+61
-18
@@ -31,6 +31,7 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-metadata/types/flat-field-metadata.type';
|
||||
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
|
||||
import { enrichFieldMetadataEventWithRelations } from 'src/engine/workspace-event-emitter/utils/enrich-field-metadata-event-with-relations.util';
|
||||
import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/types/user-workspace-role-map';
|
||||
import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type';
|
||||
import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type';
|
||||
@@ -130,23 +131,29 @@ export class WorkspaceEventEmitterService {
|
||||
? await this.fetchObjectRecordStreamContext(workspaceId)
|
||||
: undefined;
|
||||
|
||||
const enrichedMetadataEventBatch = isMetadata
|
||||
? await this.enrichFieldMetadataEventsWithRelations(
|
||||
eventBatch as MetadataEventBatch,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
for (const [streamChannelId, streamData] of streamsData) {
|
||||
if (!isDefined(streamData)) {
|
||||
streamIdsToRemove.push(streamChannelId);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Object.keys(streamData.queries).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMetadata) {
|
||||
if (isMetadata && isDefined(enrichedMetadataEventBatch)) {
|
||||
await this.processMetadataStreamEvents(
|
||||
streamChannelId,
|
||||
streamData,
|
||||
eventBatch as MetadataEventBatch,
|
||||
enrichedMetadataEventBatch,
|
||||
);
|
||||
} else {
|
||||
} else if (!isMetadata) {
|
||||
if (Object.keys(streamData.queries).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isDefined(objectRecordStreamContext)) {
|
||||
continue;
|
||||
}
|
||||
@@ -186,19 +193,14 @@ export class WorkspaceEventEmitterService {
|
||||
return;
|
||||
}
|
||||
|
||||
const metadataEventsWithQueryIds = metadataEventBatch.events.map(
|
||||
(metadataEvent) => ({
|
||||
queryIds: [] as string[],
|
||||
metadataEvent: {
|
||||
...metadataEvent,
|
||||
updatedCollectionHash: metadataEventBatch.updatedCollectionHash,
|
||||
},
|
||||
}),
|
||||
);
|
||||
const metadataEvents = metadataEventBatch.events.map((metadataEvent) => ({
|
||||
...metadataEvent,
|
||||
updatedCollectionHash: metadataEventBatch.updatedCollectionHash,
|
||||
}));
|
||||
|
||||
const payload: EventStreamPayload = {
|
||||
objectRecordEventsWithQueryIds: [],
|
||||
metadataEventsWithQueryIds,
|
||||
metadataEvents,
|
||||
};
|
||||
|
||||
await this.subscriptionService.publishToEventStream({
|
||||
@@ -208,6 +210,47 @@ export class WorkspaceEventEmitterService {
|
||||
});
|
||||
}
|
||||
|
||||
private async enrichFieldMetadataEventsWithRelations(
|
||||
metadataEventBatch: MetadataEventBatch,
|
||||
): Promise<MetadataEventBatch> {
|
||||
if (metadataEventBatch.metadataName !== 'fieldMetadata') {
|
||||
return metadataEventBatch;
|
||||
}
|
||||
|
||||
const { flatFieldMetadataMaps, flatObjectMetadataMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId: metadataEventBatch.workspaceId,
|
||||
flatMapsKeys: ['flatFieldMetadataMaps', 'flatObjectMetadataMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
const enrichedEvents = metadataEventBatch.events.map((event) => {
|
||||
if (
|
||||
!('after' in event.properties) ||
|
||||
!isDefined(event.properties.after)
|
||||
) {
|
||||
return event;
|
||||
}
|
||||
|
||||
const enrichedAfter = enrichFieldMetadataEventWithRelations({
|
||||
record: event.properties.after as Record<string, unknown>,
|
||||
flatFieldMetadataMaps,
|
||||
flatObjectMetadataMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
...event,
|
||||
properties: {
|
||||
...event.properties,
|
||||
after: enrichedAfter,
|
||||
},
|
||||
} as typeof event;
|
||||
});
|
||||
|
||||
return { ...metadataEventBatch, events: enrichedEvents };
|
||||
}
|
||||
|
||||
private async processObjectRecordStreamEvents(
|
||||
streamChannelId: string,
|
||||
streamData: EventStreamData,
|
||||
@@ -315,7 +358,7 @@ export class WorkspaceEventEmitterService {
|
||||
|
||||
const payload: EventStreamPayload = {
|
||||
objectRecordEventsWithQueryIds: matchedEvents,
|
||||
metadataEventsWithQueryIds: [],
|
||||
metadataEvents: [],
|
||||
};
|
||||
|
||||
await this.subscriptionService.publishToEventStream({
|
||||
|
||||
Reference in New Issue
Block a user