fix: prevent localStorage bloat from derived fields on mock metadata (#18809)

## Summary

- On unauthenticated pages (login), mocked object metadata was being
hydrated into the store with `readableFields` and `updatableFields`
attached. These derived arrays duplicate every field per object,
inflating `objectMetadataItems` in localStorage from ~70 KB to ~2 MB.
Combined with other metadata keys, this exceeded Safari's 5 MB quota and
caused `QuotaExceededError`, preventing real data from replacing mock
data on login.
- Extracted flat mock data into a dedicated file
(`generatedMockObjectMetadataItemsWithRelated.ts`) and use it for store
hydration, keeping the enriched version
(`generatedMockObjectMetadataItems`) only for tests.
- Completed `splitObjectMetadataItemWithRelated`'s contract by stripping
`readableFields` and `updatableFields` at runtime (not just via
TypeScript's `Omit`), matching the `FlatObjectMetadataItem` return type
that omits all four relational properties.

## Root cause

1. `MinimalMetadataLoadEffect` loads mocked metadata on unauthenticated
pages.
2. `generatedMockObjectMetadataItems` was produced by
`enrichObjectMetadataItemsWithPermissions`, which attaches
`readableFields` and `updatableFields` (full copies of the `fields`
array).
3. `splitObjectMetadataItemWithRelated` only destructured `fields` and
`indexMetadatas` — `readableFields`/`updatableFields` leaked into
`...objectProperties` at runtime because `Omit` is a compile-time-only
guard.
4. These bloated objects were written to localStorage, consuming ~2 MB
instead of ~70 KB.
5. On login, the intermediate state (old composite mock `current` + new
flat real `draft`) exceeded the 5 MB quota, causing a
`QuotaExceededError` deadlock.

## Test plan

- [ ] Open the app in a fresh Safari private window (empty localStorage)
- [ ] Verify the login page loads without errors
- [ ] Log in and verify metadata loads correctly
- [ ] Check `localStorage` size —
`metadataStoreState__objectMetadataItems` should be ~70 KB, not ~2 MB
- [ ] Run existing tests: `npx nx test twenty-front` — no regressions
from the mock data split


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Félix Malfait
2026-03-22 13:54:44 +01:00
committed by GitHub
parent bdaff0b7e2
commit 96c5728ed0
3 changed files with 16 additions and 11 deletions
@@ -11,11 +11,11 @@ export const useLoadMockedMinimalMetadata = () => {
const loadMockedMinimalMetadata = useCallback(async () => {
resetMetadataStore();
const [
{ generatedMockObjectMetadataItems },
{ generatedMockObjectMetadataItemsWithRelated },
{ mockedViews },
{ mockedNavigationMenuItems },
] = await Promise.all([
import('~/testing/utils/generatedMockObjectMetadataItems'),
import('~/testing/utils/generatedMockObjectMetadataItemsWithRelated'),
import('~/testing/mock-data/generated/metadata/views/mock-views-data'),
import(
'~/testing/mock-data/generated/metadata/navigation-menu-items/mock-navigation-menu-items-data'
@@ -23,7 +23,9 @@ export const useLoadMockedMinimalMetadata = () => {
]);
const { flatObjects, flatFields, flatIndexes } =
splitObjectMetadataItemWithRelated(generatedMockObjectMetadataItems);
splitObjectMetadataItemWithRelated(
generatedMockObjectMetadataItemsWithRelated,
);
replaceDraft('objectMetadataItems', flatObjects, MOCKED_COLLECTION_HASH);
replaceDraft('fieldMetadataItems', flatFields, MOCKED_COLLECTION_HASH);
@@ -1,15 +1,9 @@
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { enrichObjectMetadataItemsWithPermissions } from '@/object-metadata/utils/enrichObjectMetadataItemsWithPermissions';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { type ObjectMetadataItemsQuery } from '~/generated-metadata/graphql';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata';
import { generatedMockObjectMetadataItemsWithRelated } from '~/testing/utils/generatedMockObjectMetadataItemsWithRelated';
export const generatedMockObjectMetadataItems: ObjectMetadataItem[] =
enrichObjectMetadataItemsWithPermissions({
objectMetadataItems: mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems:
mockedStandardObjectMetadataQueryResult as unknown as ObjectMetadataItemsQuery,
}),
objectMetadataItems: generatedMockObjectMetadataItemsWithRelated,
objectPermissionsByObjectMetadataId: {},
});
@@ -0,0 +1,9 @@
import { type ObjectMetadataItemWithRelated } from '@/metadata-store/utils/splitObjectMetadataItemWithRelated';
import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems';
import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/metadata/objects/mock-objects-metadata';
export const generatedMockObjectMetadataItemsWithRelated: ObjectMetadataItemWithRelated[] =
mapPaginatedObjectMetadataItemsToObjectMetadataItems({
pagedObjectMetadataItems: mockedStandardObjectMetadataQueryResult,
});