feat: migrate objectMetadata reads to granular metadata store (#18643)
## Summary
Consolidates `objectMetadataItems` onto the metadata store as the
**single source of truth**, replacing the previous dual-store approach
(separate `objectMetadataItemsState` atom + untyped
`metadataStoreState`).
### Architecture: three-layer design
```
┌─────────────────────────────────────────────────────────┐
│ Store Layer (granular, typed) │
│ objectMetadataItems → FlatObjectMetadataItem[] │
│ fieldMetadataItems → FlatFieldMetadataItem[] │
│ indexMetadataItems → FlatIndexMetadataItem[] │
└────────────────┬────────────────────────────────────────┘
│ .current (never draft)
┌────────────────▼────────────────────────────────────────┐
│ Selectors (typed read-only) │
│ objectMetadataItemsSelector │
│ fieldMetadataItemsSelector │
│ indexMetadataItemsSelector │
│ metadataStoreStatusFamilySelector │
│ isSystemObjectByNameSingularFamilySelector (narrow) │
│ activeObjectNameSingularsSelector (narrow) │
└────────────────┬────────────────────────────────────────┘
│ joins objects + fields + indexes + permissions
┌────────────────▼────────────────────────────────────────┐
│ Joining Selector │
│ objectMetadataItemsWithFieldsSelector │
│ → produces full ObjectMetadataItem[] with │
│ readableFields / updatableFields from permissions │
│ → 12 existing selectors repointed here │
└─────────────────────────────────────────────────────────┘
```
### Key changes
- **Granular flat types** (`FlatObjectMetadataItem`,
`FlatFieldMetadataItem`, `FlatIndexMetadataItem`) — objects stored
without embedded fields/indexes, matching backend "Flat" naming
convention
- **Typed write API** — `updateDraft` is now generic via
`MetadataEntityTypeMap`, giving compile-time safety on what data shape
goes to each key
- **Write path refactored** — fetch → split into flat entities via
`splitObjectMetadataItemWithRelated` → write to metadata store directly.
No more dual-write through `objectMetadataItemsState`. Permissions
enrichment moved from write path into the joining selector.
- **SSE effects write directly** — `ObjectMetadataItemSSEEffect` and
`FieldMetadataSSEEffect` now patch the store from the SSE event payload
(create/update/delete) instead of triggering a full re-fetch
- **`objectMetadataItemsState` bridge** — converted from writable
`createAtomState` to read-only `createAtomSelector` that delegates to
the joining selector. All 100+ existing consumers continue to work
without code changes.
- **All selectors use Twenty state API** — `createAtomSelector` /
`createAtomFamilySelector` throughout, no raw `atom()`
- **Narrow selectors** for hot paths —
`isSystemObjectByNameSingularFamilySelector` and
`activeObjectNameSingularsSelector` read from flat objects only,
avoiding re-renders when fields/indexes/permissions change. Placed in
`object-metadata/states/` as higher-level business selectors.
- **Test helper** — `setTestObjectMetadataItemsInMetadataStore` for
tests that need to set up composite object metadata through the store
(clearly named as a testing utility)
### Naming conventions
- `ObjectMetadataItemWithRelated` — type for objects with embedded
fields/indexes (input to split utility)
- `FlatObjectMetadataItem` / `FlatFieldMetadataItem` /
`FlatIndexMetadataItem` — granular store types
- Selector names don't expose "Current" — that's an internal detail of
the metadata store API
### Future work
- Optimistic update API (`updateCurrentOptimistically` with rollback)
- Migrate remaining entities (views, pageLayouts, etc.) to the same
pattern
- Gradually remove `objectMetadataItemsState` bridge once all direct
imports are replaced
## Test plan
- [x] `npx nx typecheck twenty-front` passes
- [x] `npx nx lint:diff-with-main twenty-front` passes
- [ ] Verify app loads correctly with metadata from the store
- [ ] Verify SSE updates (object/field changes) propagate correctly
- [ ] Run existing test suites to confirm no regressions
This commit is contained in:
+47
-11
@@ -1,7 +1,6 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
import { AllMetadataName } from '~/generated-metadata/graphql';
|
||||
@@ -11,9 +10,6 @@ export const FieldMetadataSSEEffect = () => {
|
||||
|
||||
const store = useStore();
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
@@ -24,12 +20,52 @@ export const FieldMetadataSSEEffect = () => {
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.fieldMetadata,
|
||||
onMetadataOperationBrowserEvent: async () => {
|
||||
await refreshObjectMetadataItems();
|
||||
onMetadataOperationBrowserEvent: (eventDetail) => {
|
||||
const entry = store.get(
|
||||
metadataStoreState.atomFamily('fieldMetadataItems'),
|
||||
);
|
||||
const currentFields = entry.current as FlatFieldMetadataItem[];
|
||||
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdField = eventDetail.operation
|
||||
.createdRecord as unknown as FlatFieldMetadataItem;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
|
||||
...entry,
|
||||
current: [...currentFields, createdField],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedField = eventDetail.operation
|
||||
.updatedRecord as unknown as FlatFieldMetadataItem;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
|
||||
...entry,
|
||||
current: currentFields.map((field) =>
|
||||
field.id === updatedField.id
|
||||
? { ...field, ...updatedField }
|
||||
: field,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedFieldId = eventDetail.operation
|
||||
.deletedRecordId as string;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('fieldMetadataItems'), {
|
||||
...entry,
|
||||
current: currentFields.filter(
|
||||
(field) => field.id !== deletedFieldId,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
+47
-11
@@ -1,7 +1,6 @@
|
||||
import { useListenToMetadataOperationBrowserEvent } from '@/browser-event/hooks/useListenToMetadataOperationBrowserEvent';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { navigationMenuItemsState } from '@/navigation-menu-item/states/navigationMenuItemsState';
|
||||
import { useListenToEventsForQuery } from '@/sse-db-event/hooks/useListenToEventsForQuery';
|
||||
import { useStore } from 'jotai';
|
||||
@@ -18,9 +17,6 @@ export const ObjectMetadataItemSSEEffect = () => {
|
||||
const store = useStore();
|
||||
const client = useApolloClient();
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useListenToEventsForQuery({
|
||||
queryId,
|
||||
operationSignature: {
|
||||
@@ -31,12 +27,52 @@ export const ObjectMetadataItemSSEEffect = () => {
|
||||
|
||||
useListenToMetadataOperationBrowserEvent({
|
||||
metadataName: AllMetadataName.objectMetadata,
|
||||
onMetadataOperationBrowserEvent: async () => {
|
||||
await refreshObjectMetadataItems();
|
||||
onMetadataOperationBrowserEvent: async (eventDetail) => {
|
||||
const entry = store.get(
|
||||
metadataStoreState.atomFamily('objectMetadataItems'),
|
||||
);
|
||||
const currentObjects = entry.current as FlatObjectMetadataItem[];
|
||||
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
switch (eventDetail.operation.type) {
|
||||
case 'create': {
|
||||
const createdObject = eventDetail.operation
|
||||
.createdRecord as unknown as FlatObjectMetadataItem;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
|
||||
...entry,
|
||||
current: [...currentObjects, createdObject],
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'update': {
|
||||
const updatedObject = eventDetail.operation
|
||||
.updatedRecord as unknown as FlatObjectMetadataItem;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
|
||||
...entry,
|
||||
current: currentObjects.map((object) =>
|
||||
object.id === updatedObject.id
|
||||
? { ...object, ...updatedObject }
|
||||
: object,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'delete': {
|
||||
const deletedObjectId = eventDetail.operation
|
||||
.deletedRecordId as string;
|
||||
|
||||
store.set(metadataStoreState.atomFamily('objectMetadataItems'), {
|
||||
...entry,
|
||||
current: currentObjects.filter(
|
||||
(object) => object.id !== deletedObjectId,
|
||||
),
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
const navigationMenuItemsResult = await client.query({
|
||||
query: FindManyNavigationMenuItemsDocument,
|
||||
|
||||
-11
@@ -1,23 +1,18 @@
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadedState';
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useStore } from 'jotai';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { isWorkspaceActiveOrSuspended } from 'twenty-shared/workspace';
|
||||
|
||||
export const ObjectMetadataProviderInitialEffect = () => {
|
||||
const isCurrentUserLoaded = useAtomStateValue(isCurrentUserLoadedState);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const store = useStore();
|
||||
const [isInitialized, setIsInitialized] = useState(false);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { loadMockedObjectMetadataItems } = useLoadMockedObjectMetadataItems();
|
||||
const { updateDraft, applyChanges } = useMetadataStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isInitialized) {
|
||||
@@ -36,9 +31,6 @@ export const ObjectMetadataProviderInitialEffect = () => {
|
||||
await loadMockedObjectMetadataItems();
|
||||
}
|
||||
|
||||
const loadedItems = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedItems);
|
||||
applyChanges();
|
||||
setIsInitialized(true);
|
||||
};
|
||||
|
||||
@@ -49,9 +41,6 @@ export const ObjectMetadataProviderInitialEffect = () => {
|
||||
currentWorkspace,
|
||||
refreshObjectMetadataItems,
|
||||
loadMockedObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useSetIndexViews } from '@/metadata-store/hooks/useSetIndexViews';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
@@ -24,7 +25,8 @@ export const useFetchAndLoadIndexViews = () => {
|
||||
|
||||
if (isDefined(result.data?.getCoreViews)) {
|
||||
setIndexViews(result.data.getCoreViews);
|
||||
updateDraft('views', result.data.getCoreViews);
|
||||
// TODO: align generated ViewType with app ViewType to remove this cast
|
||||
updateDraft('views', result.data.getCoreViews as unknown as View[]);
|
||||
applyChanges();
|
||||
}
|
||||
}, [client, setIndexViews, updateDraft, applyChanges]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type MetadataEntityKey,
|
||||
type MetadataStoreItem,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { type MetadataEntityTypeMap } from '@/metadata-store/types/MetadataEntityTypeMap';
|
||||
import { useStore, type createStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
@@ -55,7 +56,7 @@ export const useMetadataStore = () => {
|
||||
const store = useStore();
|
||||
|
||||
const updateDraft = useCallback(
|
||||
(key: MetadataEntityKey, data: object[]) => {
|
||||
<K extends MetadataEntityKey>(key: K, data: MetadataEntityTypeMap[K][]) => {
|
||||
const currentEntry = store.get(metadataStoreState.atomFamily(key));
|
||||
|
||||
if (
|
||||
|
||||
+4
-15
@@ -1,7 +1,7 @@
|
||||
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
|
||||
import { useLoadMockedObjectMetadataItems } from '@/object-metadata/hooks/useLoadMockedObjectMetadataItems';
|
||||
import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefreshObjectMetadataItems';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { coreViewsState } from '@/views/states/coreViewState';
|
||||
import { useStore } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
@@ -16,11 +16,9 @@ export const useReloadWorkspaceMetadata = () => {
|
||||
resetMetadataStore();
|
||||
|
||||
await refreshObjectMetadataItems();
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
|
||||
const loadedViews = store.get(coreViewsState.atom);
|
||||
// TODO: align generated ViewType with app ViewType to remove this cast
|
||||
const loadedViews = store.get(coreViewsState.atom) as unknown as View[];
|
||||
updateDraft('views', loadedViews);
|
||||
applyChanges();
|
||||
}, [
|
||||
@@ -35,16 +33,7 @@ export const useReloadWorkspaceMetadata = () => {
|
||||
resetMetadataStore();
|
||||
|
||||
await loadMockedObjectMetadataItems();
|
||||
const loadedObjects = store.get(objectMetadataItemsState.atom);
|
||||
updateDraft('objectMetadataItems', loadedObjects);
|
||||
applyChanges();
|
||||
}, [
|
||||
resetMetadataStore,
|
||||
loadMockedObjectMetadataItems,
|
||||
store,
|
||||
updateDraft,
|
||||
applyChanges,
|
||||
]);
|
||||
}, [resetMetadataStore, loadMockedObjectMetadataItems]);
|
||||
|
||||
return { reloadWorkspaceMetadata, resetToMockedMetadata };
|
||||
};
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const fieldMetadataItemsSelector = createAtomSelector<
|
||||
FlatFieldMetadataItem[]
|
||||
>({
|
||||
key: 'fieldMetadataItemsSelector',
|
||||
get: ({ get }) => {
|
||||
const storeItem = get(metadataStoreState, 'fieldMetadataItems');
|
||||
|
||||
return storeItem.current as FlatFieldMetadataItem[];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const indexMetadataItemsSelector = createAtomSelector<
|
||||
FlatIndexMetadataItem[]
|
||||
>({
|
||||
key: 'indexMetadataItemsSelector',
|
||||
get: ({ get }) => {
|
||||
const storeItem = get(metadataStoreState, 'indexMetadataItems');
|
||||
|
||||
return storeItem.current as FlatIndexMetadataItem[];
|
||||
},
|
||||
});
|
||||
@@ -8,6 +8,7 @@ export type MetadataEntityStoreStatus =
|
||||
export const ALL_METADATA_ENTITY_KEYS = [
|
||||
'objectMetadataItems',
|
||||
'fieldMetadataItems',
|
||||
'indexMetadataItems',
|
||||
'views',
|
||||
'viewFields',
|
||||
'viewFilters',
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import {
|
||||
metadataStoreState,
|
||||
type MetadataEntityKey,
|
||||
type MetadataEntityStoreStatus,
|
||||
} from '@/metadata-store/states/metadataStoreState';
|
||||
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
|
||||
|
||||
export const metadataStoreStatusFamilySelector = createAtomFamilySelector<
|
||||
MetadataEntityStoreStatus,
|
||||
MetadataEntityKey
|
||||
>({
|
||||
key: 'metadataStoreStatusFamilySelector',
|
||||
get:
|
||||
(entityKey: MetadataEntityKey) =>
|
||||
({ get }) => {
|
||||
const storeItem = get(metadataStoreState, entityKey);
|
||||
|
||||
return storeItem.status;
|
||||
},
|
||||
});
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { metadataStoreState } from '@/metadata-store/states/metadataStoreState';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const objectMetadataItemsSelector = createAtomSelector<
|
||||
FlatObjectMetadataItem[]
|
||||
>({
|
||||
key: 'objectMetadataItemsSelector',
|
||||
get: ({ get }) => {
|
||||
const storeItem = get(metadataStoreState, 'objectMetadataItems');
|
||||
|
||||
return storeItem.current as FlatObjectMetadataItem[];
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
|
||||
export type FlatFieldMetadataItem = FieldMetadataItem & {
|
||||
objectMetadataId: string;
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { type IndexMetadataItem } from '@/object-metadata/types/IndexMetadataItem';
|
||||
|
||||
export type FlatIndexMetadataItem = IndexMetadataItem & {
|
||||
objectMetadataId: string;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export type FlatObjectMetadataItem = Omit<
|
||||
ObjectMetadataItem,
|
||||
'fields' | 'readableFields' | 'updatableFields' | 'indexMetadatas'
|
||||
>;
|
||||
@@ -0,0 +1,23 @@
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { type PageLayout } from '@/page-layout/types/PageLayout';
|
||||
import { type LogicFunction } from '@/settings/logic-functions/states/logicFunctionsState';
|
||||
import { type View } from '@/views/types/View';
|
||||
import { type ViewField } from '@/views/types/ViewField';
|
||||
import { type ViewFilter } from '@/views/types/ViewFilter';
|
||||
import { type ViewSort } from '@/views/types/ViewSort';
|
||||
import { type NavigationMenuItem } from '~/generated-metadata/graphql';
|
||||
|
||||
export type MetadataEntityTypeMap = {
|
||||
objectMetadataItems: FlatObjectMetadataItem;
|
||||
fieldMetadataItems: FlatFieldMetadataItem;
|
||||
indexMetadataItems: FlatIndexMetadataItem;
|
||||
views: View;
|
||||
viewFields: ViewField;
|
||||
viewFilters: ViewFilter;
|
||||
viewSorts: ViewSort;
|
||||
pageLayouts: PageLayout;
|
||||
logicFunctions: LogicFunction;
|
||||
navigationMenuItems: NavigationMenuItem;
|
||||
};
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
import { type FlatFieldMetadataItem } from '@/metadata-store/types/FlatFieldMetadataItem';
|
||||
import { type FlatIndexMetadataItem } from '@/metadata-store/types/FlatIndexMetadataItem';
|
||||
import { type FlatObjectMetadataItem } from '@/metadata-store/types/FlatObjectMetadataItem';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export type ObjectMetadataItemWithRelated = Omit<
|
||||
ObjectMetadataItem,
|
||||
'readableFields' | 'updatableFields'
|
||||
>;
|
||||
|
||||
type SplitResult = {
|
||||
flatObjects: FlatObjectMetadataItem[];
|
||||
flatFields: FlatFieldMetadataItem[];
|
||||
flatIndexes: FlatIndexMetadataItem[];
|
||||
};
|
||||
|
||||
export const splitObjectMetadataItemWithRelated = (
|
||||
objectMetadataItemsWithRelated: ObjectMetadataItemWithRelated[],
|
||||
): SplitResult => {
|
||||
const flatObjects: FlatObjectMetadataItem[] = [];
|
||||
const flatFields: FlatFieldMetadataItem[] = [];
|
||||
const flatIndexes: FlatIndexMetadataItem[] = [];
|
||||
|
||||
for (const objectMetadataItemWithRelated of objectMetadataItemsWithRelated) {
|
||||
const {
|
||||
fields = [],
|
||||
indexMetadatas = [],
|
||||
...objectProperties
|
||||
} = objectMetadataItemWithRelated;
|
||||
|
||||
flatObjects.push(objectProperties);
|
||||
|
||||
for (const field of fields) {
|
||||
flatFields.push({
|
||||
...field,
|
||||
objectMetadataId: objectMetadataItemWithRelated.id,
|
||||
});
|
||||
}
|
||||
|
||||
for (const index of indexMetadatas) {
|
||||
flatIndexes.push({
|
||||
...index,
|
||||
objectMetadataId: objectMetadataItemWithRelated.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { flatObjects, flatFields, flatIndexes };
|
||||
};
|
||||
Reference in New Issue
Block a user