From 1109b89cd7a70b5672ca2f96d0e56b4cd1effa89 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 30 Mar 2026 15:01:54 +0200 Subject: [PATCH] fix: use Redis metadata version for GraphQL response cache key (#19111) ## Summary - Fix stale `ObjectMetadataItems` GraphQL response cache after field creation by using `request.workspaceMetadataVersion` (sourced from Redis) instead of `workspace.metadataVersion` (from the potentially stale CoreEntityCacheService) - Make the E2E kanban view test selector more robust with a regex match ## Root Cause The `useCachedMetadata` GraphQL plugin keys cached responses using `workspace.metadataVersion` from the `CoreEntityCacheService`. When a field is created: 1. The migration runner increments `metadataVersion` in DB and Redis 2. But the `CoreEntityCacheService` for `WorkspaceEntity` is **not** invalidated 3. So `request.workspace.metadataVersion` still has the old version 4. The cache key resolves to the old cached response 5. The frontend gets stale metadata without the newly created field This breaks E2E tests (and likely affects users) - after creating a custom field, the metadata isn't visible until the workspace entity cache refreshes. ## Fix Use `request.workspaceMetadataVersion` (populated from Redis by the middleware, always up-to-date) as the primary version for cache keys, falling back to the entity cache version. ## Test plan - [ ] E2E `create-kanban-view` tests should pass (creating a Select field and immediately using it in a Kanban view) - [ ] Verify `ObjectMetadataItems` returns fresh data after field creation (no stale cache) Made with [Cursor](https://cursor.com) --- .../twenty-e2e-testing/tests/create-kanban-view.spec.ts | 9 ++++----- .../services/workspace-metadata-version.service.ts | 7 +++++++ .../workspace-metadata-version.module.ts | 2 ++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts b/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts index 0b7dd41656..52c58e90f0 100644 --- a/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts +++ b/packages/twenty-e2e-testing/tests/create-kanban-view.spec.ts @@ -24,7 +24,7 @@ test('Create Industry Select Field', async ({ page }) => { test('Create Kanban View from Industry Select Field', async ({ page }) => { await page.getByRole('link', { name: 'Opportunities' }).click(); - await page.getByRole('button', { name: 'All Opportunities ยท' }).click(); + await page.getByRole('button', { name: /All Opportunities/ }).click(); await page.getByText('Add view').click(); await page.getByRole('textbox').press('ControlOrMeta+a'); await page.getByRole('textbox').fill('By industry'); @@ -32,12 +32,11 @@ test('Create Kanban View from Industry Select Field', async ({ page }) => { await page.getByText('Kanban').click(); await page.locator('[aria-controls="view-picker-kanban-field-options"]').click(); await page.getByRole('option', { name: 'Industry' }).click(); - // Use exact: true to ensure we only click the button with the label "Create" await page.getByRole('button', { name: 'Create new view' }).click(); - await expect(page.getByText('Food')).toBeVisible(); + await expect(page.getByText('Food')).toBeVisible({ timeout: 30000 }); await expect(page.getByText('Tech')).toBeVisible(); await expect(page.getByText('Travel')).toBeVisible(); - await expect(page.getByText('No value')).toBeVisible(); + await expect(page.getByText('No Value')).toBeVisible(); const byIndustryElements = await page.locator('text=By industry').all(); expect(byIndustryElements.length).toBeGreaterThanOrEqual(1); for (const element of byIndustryElements) { @@ -50,6 +49,6 @@ test('Create Kanban View from Industry Select Field', async ({ page }) => { return req.url().includes('/metadata') && req.method() === 'POST'; })]); - await expect(page.getByText('No value')).not.toBeVisible(); + await expect(page.getByText('No Value')).not.toBeVisible(); }); }) diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts index f095f94884..5070cf13c7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service.ts @@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; +import { CoreEntityCacheService } from 'src/engine/core-entity-cache/services/core-entity-cache.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceMetadataVersionException, @@ -19,6 +20,7 @@ export class WorkspaceMetadataVersionService { @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly coreEntityCacheService: CoreEntityCacheService, ) {} async incrementMetadataVersion(workspaceId: string): Promise { @@ -47,5 +49,10 @@ export class WorkspaceMetadataVersionService { workspaceId, newMetadataVersion, ); + + await this.coreEntityCacheService.invalidate( + 'workspaceEntity', + workspaceId, + ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module.ts index 889d59e5b7..46892d7e3c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { CoreEntityCacheModule } from 'src/engine/core-entity-cache/core-entity-cache.module'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; @@ -9,6 +10,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ @Module({ imports: [ TypeOrmModule.forFeature([WorkspaceEntity]), + CoreEntityCacheModule, WorkspaceCacheStorageModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, ],