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:
Charles Bochet
2026-03-14 12:54:19 +01:00
committed by GitHub
parent 48172d60fd
commit 40ff109179
64 changed files with 657 additions and 315 deletions
@@ -8,8 +8,8 @@ import {
variables,
} from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { isDefined } from 'twenty-shared/utils';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
@@ -28,8 +28,8 @@ const mocks = [
];
const Wrapper = ({ children }: { children: ReactNode }) => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
@@ -2,8 +2,8 @@ import { renderHook } from '@testing-library/react';
import { Provider as JotaiProvider } from 'jotai';
import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
const Wrapper = ({ children }: { children: React.ReactNode }) => (
@@ -12,8 +12,8 @@ const Wrapper = ({ children }: { children: React.ReactNode }) => (
describe('useGetObjectRecordIdentifierByNameSingular', () => {
beforeEach(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
});
@@ -4,8 +4,8 @@ import { type ReactNode } from 'react';
import { Provider as JotaiProvider } from 'jotai';
import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { setTestObjectMetadataItemsInMetadataStore } from '~/testing/utils/setTestObjectMetadataItemsInMetadataStore';
import { generatedMockObjectMetadataItems } from '~/testing/utils/generatedMockObjectMetadataItems';
const Wrapper = ({ children }: { children: ReactNode }) => (
@@ -16,8 +16,8 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
describe('useGetRelationMetadata', () => {
beforeEach(() => {
jotaiStore.set(
objectMetadataItemsState.atom,
setTestObjectMetadataItemsInMetadataStore(
jotaiStore,
generatedMockObjectMetadataItems,
);
});
@@ -50,11 +50,11 @@ describe('useGetRelationMetadata', () => {
(field) => field.name === 'pointOfContact',
);
expect(relationObjectMetadataItem).toEqual(
expectedRelationObjectMetadataItem,
expect(relationObjectMetadataItem).toMatchObject(
expectedRelationObjectMetadataItem!,
);
expect(relationFieldMetadataItem).toEqual(
expectedRelationFieldMetadataItem,
expect(relationFieldMetadataItem).toMatchObject(
expectedRelationFieldMetadataItem!,
);
expect(relationType).toBe('ONE_TO_MANY');
});
@@ -1,9 +1,12 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useMemo } from 'react';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
export const useFilteredObjectMetadataItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
const activeNonSystemObjectMetadataItems = useMemo(
() =>
@@ -1,32 +1,30 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { useCallback } from 'react';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { useStore } from 'jotai';
import { useCallback } from 'react';
export const useLoadMockedObjectMetadataItems = () => {
const store = useStore();
const { updateDraft, applyChanges } = useMetadataStore();
const loadMockedObjectMetadataItems = useCallback(async () => {
const { generatedMockObjectMetadataItems } = await import(
'~/testing/utils/generatedMockObjectMetadataItems'
);
if (
!isDeeplyEqual(
store.get(objectMetadataItemsState.atom),
generatedMockObjectMetadataItems,
)
) {
store.set(
objectMetadataItemsState.atom,
generatedMockObjectMetadataItems,
);
}
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [store]);
}, [store, updateDraft, applyChanges]);
return {
loadMockedObjectMetadataItems,
@@ -1,6 +1,6 @@
import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError';
import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useAtomFamilySelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilySelectorValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -18,7 +18,10 @@ export const useObjectMetadataItem = ({
},
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
if (!isDefined(objectMetadataItem)) {
throw new ObjectMetadataItemNotFoundError(
@@ -1,9 +1,11 @@
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
export const useObjectMetadataItems = () => {
const objectMetadataItems = useAtomStateValue(objectMetadataItemsState);
const objectMetadataItemsWithFields = useAtomStateValue(
objectMetadataItemsWithFieldsSelector,
);
const objectMetadataItems = objectMetadataItemsWithFields;
return {
objectMetadataItems,
@@ -1,75 +1,22 @@
import { isAppEffectRedirectEnabledState } from '@/app/states/isAppEffectRedirectEnabledState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useMetadataStore } from '@/metadata-store/hooks/useMetadataStore';
import { splitObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { type FetchPolicy } from '@apollo/client';
import { useApolloClient } from '@apollo/client/react';
import { useCallback } from 'react';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
export const useRefreshObjectMetadataItems = (
fetchPolicy: FetchPolicy = 'network-only',
) => {
const store = useStore();
const client = useApolloClient();
const { updateDraft, applyChanges } = useMetadataStore();
const replaceObjectMetadataItemIfDifferent = useCallback(
(
toSetObjectMetadataItems: Omit<
ObjectMetadataItem,
'readableFields' | 'updatableFields'
>[],
) => {
const currentUserWorkspace = store.get(currentUserWorkspaceState.atom);
if (!isDefined(currentUserWorkspace)) {
return;
}
const objectPermissionsByObjectMetadataId =
currentUserWorkspace.objectsPermissions.reduce(
(acc, objectPermission) => {
acc[objectPermission.objectMetadataId] = objectPermission;
return acc;
},
{} as Record<
string,
ObjectPermissions & { objectMetadataId: string }
>,
);
const newObjectMetadataItems = enrichObjectMetadataItemsWithPermissions({
objectMetadataItems: toSetObjectMetadataItems,
objectPermissionsByObjectMetadataId,
});
if (
!isDeeplyEqual(
store.get(objectMetadataItemsState.atom),
newObjectMetadataItems,
) &&
newObjectMetadataItems.length > 0
) {
store.set(objectMetadataItemsState.atom, newObjectMetadataItems);
}
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
return newObjectMetadataItems;
},
[store],
);
const refreshObjectMetadataItems = async () => {
const refreshObjectMetadataItems = useCallback(async () => {
const objectMetadataItemsResult =
await client.query<ObjectMetadataItemsQuery>({
query: FIND_MANY_OBJECT_METADATA_ITEMS,
@@ -77,13 +24,23 @@ export const useRefreshObjectMetadataItems = (
fetchPolicy,
});
const objectMetadataItems =
const compositeObjects =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: objectMetadataItemsResult.data,
});
return replaceObjectMetadataItemIfDifferent(objectMetadataItems);
};
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(compositeObjects);
updateDraft('objectMetadataItems', flatObjects);
updateDraft('fieldMetadataItems', flatFields);
updateDraft('indexMetadataItems', flatIndexes);
applyChanges();
if (store.get(isAppEffectRedirectEnabledState.atom) === false) {
store.set(isAppEffectRedirectEnabledState.atom, true);
}
}, [client, fetchPolicy, store, updateDraft, applyChanges]);
return {
refreshObjectMetadataItems,
@@ -0,0 +1,13 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const activeObjectNameSingularsSelector = createAtomSelector<string[]>({
key: 'activeObjectNameSingularsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
return flatObjects
.filter((flatObject) => flatObject.isActive)
.map((flatObject) => flatObject.nameSingular);
},
});
@@ -1,5 +1,5 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { getFilterFilterableFieldMetadataItems } from '@/object-metadata/utils/getFilterFilterableFieldMetadataItems';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -17,7 +17,7 @@ export const availableFieldMetadataItemsForFilterFamilySelector =
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const currentWorkspace = get(currentWorkspaceState);
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { filterSortableFieldMetadataItems } from '@/object-metadata/utils/filterSortableFieldMetadataItems';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -13,7 +13,7 @@ export const availableFieldMetadataItemsForSortFamilySelector =
get:
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(item) => item.id === objectMetadataItemId,
@@ -1,5 +1,5 @@
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@@ -8,7 +8,7 @@ export const fieldMetadataItemByIdSelector = createAtomFamilySelector({
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const flattenedFieldMetadataItems = get(
flattenedFieldMetadataItemsSelector,
);
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@@ -7,7 +7,7 @@ export const flattenedFieldMetadataItemsSelector = createAtomSelector<
>({
key: 'flattenedFieldMetadataItemsSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.fields,
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@@ -7,7 +7,7 @@ export const flattenedReadableFieldMetadataItemsSelector = createAtomSelector<
>({
key: 'flattenedReadableFieldMetadataItemsSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectMetadataItems.flatMap(
(objectMetadataItem) => objectMetadataItem.readableFields,
@@ -1,7 +1,7 @@
import { availableFieldMetadataItemsForFilterFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForFilterFamilySelector';
import { availableFieldMetadataItemsForSortFamilySelector } from '@/object-metadata/states/availableFieldMetadataItemsForSortFamilySelector';
import { flattenedFieldMetadataItemsSelector } from '@/object-metadata/states/flattenedFieldMetadataItemsSelector';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@@ -11,7 +11,7 @@ export const isFieldMetadataItemFilterableAndSortableSelector =
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const flattenedFieldMetadataItems = get(
flattenedFieldMetadataItemsSelector,
);
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { findById, isDefined } from 'twenty-shared/utils';
@@ -9,7 +9,7 @@ export const isFieldMetadataItemLabelIdentifierSelector =
get:
({ fieldMetadataItemId }: { fieldMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const foundObjectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -0,0 +1,18 @@
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
export const isSystemObjectByNameSingularFamilySelector =
createAtomFamilySelector<boolean, string>({
key: 'isSystemObjectByNameSingularFamilySelector',
get:
(nameSingular: string) =>
({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
return (
flatObjects.find(
(flatObject) => flatObject.nameSingular === nameSingular,
)?.isSystem ?? false
);
},
});
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
import { isDefined } from 'twenty-shared/utils';
@@ -9,7 +9,7 @@ export const labelIdentifierFieldMetadataItemSelector =
get:
({ objectMetadataItemId }: { objectMetadataItemId: string }) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
const objectMetadataItem = objectMetadataItems.find(
(objectMetadataItem) =>
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -15,7 +15,7 @@ export const objectMetadataItemFamilySelector = createAtomFamilySelector<
get:
({ objectNameType, objectName }: ObjectMetadataItemSelector) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
if (objectNameType === 'singular') {
return (
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@@ -7,7 +7,7 @@ export const objectMetadataItemsByNamePluralMapSelector = createAtomSelector<
>({
key: 'objectMetadataItemsByNamePluralMapSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return new Map(
objectMetadataItems.map((objectMetadataItem) => [
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
@@ -7,7 +7,7 @@ export const objectMetadataItemsByNameSingularMapSelector = createAtomSelector<
>({
key: 'objectMetadataItemsByNameSingularMapSelector',
get: ({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return new Map(
objectMetadataItems.map((objectMetadataItem) => [
@@ -1,4 +1,4 @@
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomFamilySelector } from '@/ui/utilities/state/jotai/utils/createAtomFamilySelector';
@@ -8,7 +8,7 @@ export const objectMetadataItemsBySingularNameSelector =
get:
(objectNameSingulars: string[]) =>
({ get }) => {
const objectMetadataItems = get(objectMetadataItemsState);
const objectMetadataItems = get(objectMetadataItemsWithFieldsSelector);
return objectNameSingulars.flatMap((objectNameSingular) => {
const found = objectMetadataItems.find(
@@ -1,7 +1,10 @@
import { objectMetadataItemsWithFieldsSelector } from '@/object-metadata/states/objectMetadataItemsWithFieldsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
export const objectMetadataItemsState = createAtomState<ObjectMetadataItem[]>({
export const objectMetadataItemsState = createAtomSelector<
ObjectMetadataItem[]
>({
key: 'objectMetadataItemsState',
defaultValue: [],
get: ({ get }) => get(objectMetadataItemsWithFieldsSelector),
});
@@ -0,0 +1,95 @@
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { fieldMetadataItemsSelector } from '@/metadata-store/states/fieldMetadataItemsSelector';
import { indexMetadataItemsSelector } from '@/metadata-store/states/indexMetadataItemsSelector';
import { objectMetadataItemsSelector } from '@/metadata-store/states/objectMetadataItemsSelector';
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { getNonReadableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonReadableFieldMetadataIdsFromObjectPermissions';
import { getNonUpdatableFieldMetadataIdsFromObjectPermissions } from '@/object-metadata/utils/getNonUpdatableFieldMetadataIdsFromObjectPermissions';
import { getObjectPermissionsFromMapByObjectMetadataId } from '@/settings/roles/role-permissions/objects-permissions/utils/getObjectPermissionsFromMapByObjectMetadataId';
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
export const objectMetadataItemsWithFieldsSelector = createAtomSelector<
ObjectMetadataItem[]
>({
key: 'objectMetadataItemsWithFieldsSelector',
get: ({ get }) => {
const flatObjects = get(objectMetadataItemsSelector);
const allFlatFields = get(fieldMetadataItemsSelector);
const allFlatIndexes = get(indexMetadataItemsSelector);
const currentUserWorkspace = get(currentUserWorkspaceState);
const fieldsByObjectId = new Map<
string,
(typeof allFlatFields)[number][]
>();
for (const field of allFlatFields) {
const existing = fieldsByObjectId.get(field.objectMetadataId);
if (isDefined(existing)) {
existing.push(field);
} else {
fieldsByObjectId.set(field.objectMetadataId, [field]);
}
}
const indexesByObjectId = new Map<
string,
(typeof allFlatIndexes)[number][]
>();
for (const index of allFlatIndexes) {
const existing = indexesByObjectId.get(index.objectMetadataId);
if (isDefined(existing)) {
existing.push(index);
} else {
indexesByObjectId.set(index.objectMetadataId, [index]);
}
}
const objectPermissionsByObjectMetadataId =
currentUserWorkspace?.objectsPermissions.reduce(
(accumulator, objectPermission) => {
accumulator[objectPermission.objectMetadataId] = objectPermission;
return accumulator;
},
{} as Record<string, ObjectPermissions & { objectMetadataId: string }>,
) ?? {};
return flatObjects.map((flatObject) => {
const fields = fieldsByObjectId.get(flatObject.id) ?? [];
const indexMetadatas = indexesByObjectId.get(flatObject.id) ?? [];
const objectPermissions = getObjectPermissionsFromMapByObjectMetadataId({
objectPermissionsByObjectMetadataId,
objectMetadataId: flatObject.id,
});
const nonReadableFieldMetadataIds =
getNonReadableFieldMetadataIdsFromObjectPermissions({
objectPermissions,
});
const nonUpdatableFieldMetadataIds =
getNonUpdatableFieldMetadataIdsFromObjectPermissions({
objectPermissions,
});
return {
...flatObject,
fields,
indexMetadatas,
readableFields: fields.filter(
(field) => !nonReadableFieldMetadataIds.includes(field.id),
),
updatableFields: fields.filter(
(field) => !nonUpdatableFieldMetadataIds.includes(field.id),
),
} satisfies ObjectMetadataItem;
});
},
});