From 1eb2e44058206d945480e51e67b702b455e36070 Mon Sep 17 00:00:00 2001 From: Weiko Date: Mon, 1 Dec 2025 17:08:21 +0100 Subject: [PATCH] Refactor workspace cache service (#16208) ## Context We've recently introduced a new workspace cache service which now acts as a cache access and local storage for all workspace related data, deprecating the individual specific services. - Better performance through multiple caching/fetching strategies - Consistent data access patterns across the codebase - Reduced redis queries through MGET/MSET/PIPELINE with multiple cache keys --- .../common-base-query-runner.service.ts | 17 +- .../api/common/core-common-api.module.ts | 2 + .../core/handlers/rest-api-base.handler.ts | 16 +- .../api/rest/core/rest-api-core.module.ts | 2 + .../api-key/api-key-role.service.spec.ts | 458 ------------------ .../api-key/api-key-role.service.ts | 14 +- .../core-modules/api-key/api-key.module.ts | 2 + .../application/application.module.ts | 2 + .../application/application.service.ts | 50 +- ...pace-flat-application-map-cache.service.ts | 28 +- .../engine/core-modules/auth/auth.module.ts | 2 + .../auth/services/sign-in-up.service.ts | 10 +- .../services/cache-storage.service.ts | 16 + .../feature-flag/feature-flag.module.ts | 2 + .../__tests__/feature-flag.service.spec.ts | 56 ++- .../services/feature-flag.service.ts | 32 +- .../ai/ai-tools/ai-tools.module.ts | 2 + .../services/__tests__/tool.service.spec.ts | 19 +- .../ai/ai-tools/services/tool.service.ts | 12 +- ...ace-flat-cron-trigger-map-cache.service.ts | 24 +- ...atabase-event-trigger-map-cache.service.ts | 24 +- ...ny-or-all-flat-entity-maps-cache.module.ts | 3 +- ...y-or-all-flat-entity-maps-cache.service.ts | 95 +--- ...e-flat-field-metadata-map-cache.service.ts | 24 +- .../workspace-flat-index-map-cache.service.ts | 24 +- ...-flat-object-metadata-map-cache.service.ts | 25 +- ...pace-flat-role-target-map-cache.service.ts | 22 +- ...space-flat-view-field-map-cache.service.ts | 22 +- ...pace-flat-view-filter-map-cache.service.ts | 22 +- ...space-flat-view-group-map-cache.service.ts | 22 +- .../workspace-flat-view-map-cache.service.ts | 22 +- .../object-metadata/object-metadata.module.ts | 2 + .../object-metadata.service.ts | 12 +- .../object-permission.service.spec.ts | 23 +- .../field-permissions.service.spec.ts | 40 +- .../field-permission.service.ts | 21 +- .../object-permission.module.ts | 2 + .../object-permission.service.ts | 13 +- .../permission-flag/permission-flag.module.ts | 2 + .../permission-flag.service.ts | 13 +- .../__tests__/permissions.service.spec.ts | 4 +- .../permissions/permissions.module.ts | 2 + .../permissions/permissions.service.ts | 12 +- .../metadata-modules/role/role.module.ts | 5 +- .../workspace-flat-role-map-cache.service.ts | 24 +- ...ce-flat-route-trigger-map-cache.service.ts | 24 +- ...t-serverless-function-map-cache.service.ts | 24 +- .../user-role/user-role.module.ts | 2 + .../user-role/user-role.service.ts | 26 +- ...orkspace-feature-flags-map-cache.module.ts | 7 +- ...rkspace-feature-flags-map-cache.service.ts | 67 +-- ...orkspace-api-key-role-map-cache.service.ts | 42 ++ ...space-permissions-cache-storage.service.ts | 167 ------- .../workspace-permissions-cache.module.ts | 20 +- .../workspace-permissions-cache.service.ts | 411 +--------------- ...rkspace-roles-permissions-cache.service.ts | 176 +++++++ ...e-user-workspace-role-map-cache.service.ts | 39 ++ .../factories/workspace-datasource.factory.ts | 73 +-- .../global-workspace-datasource.module.ts | 2 + .../global-workspace-orm.manager.ts | 43 +- .../storage/promise-memoizer.storage.ts | 6 + .../engine/twenty-orm/twenty-orm.module.ts | 2 + .../decorators/workspace-cache.decorator.ts | 6 +- .../workspace-cache-provider.service.ts | 15 + .../__tests__/workspace-cache.service.spec.ts | 338 +++++++++++++ .../services/workspace-cache.service.ts | 292 ++++++----- .../types/workspace-cache-key.type.ts | 43 ++ ...ts => workspace-local-cache-entry.type.ts} | 2 +- .../workspace-cache-provider.service.ts | 6 - .../workspace-cache/workspace-cache.module.ts | 2 +- .../dev-seeder-permissions.service.ts | 4 +- .../dev-seeder/dev-seeder.module.ts | 2 + .../dev-seeder/services/dev-seeder.service.ts | 10 +- .../workspace-migration-runner-v2.service.ts | 22 +- .../workspace-migration-runner-v2.module.ts | 2 + .../agent-tool.service.integration-spec.ts | 30 +- .../agent/utils/agent-tool-test-utils.ts | 18 +- 77 files changed, 1306 insertions(+), 1863 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-api-key-role-map-cache.service.ts delete mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-roles-permissions-cache.service.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-user-workspace-role-map-cache.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/interfaces/workspace-cache-provider.service.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/services/__tests__/workspace-cache.service.spec.ts create mode 100644 packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts rename packages/twenty-server/src/engine/workspace-cache/types/{workspace-context-cache-entry.type.ts => workspace-local-cache-entry.type.ts} (51%) delete mode 100644 packages/twenty-server/src/engine/workspace-cache/workspace-cache-provider.service.ts diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index d84d3c60632..30055e0b10a 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -47,8 +47,8 @@ import { } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; @@ -76,7 +76,7 @@ export abstract class CommonBaseQueryRunnerService< @Inject() protected readonly apiKeyRoleService: ApiKeyRoleService; @Inject() - protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; + protected readonly workspaceCacheService: WorkspaceCacheService; @Inject() protected readonly commonResultGettersService: CommonResultGettersService; @Inject() @@ -350,15 +350,12 @@ export abstract class CommonBaseQueryRunnerService< roleId = userWorkspaceRoleId; } - const objectMetadataPermissions = - await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( - { - workspaceId: workspaceId, - roleIds: [roleId], - }, - ); + const { rolesPermissions } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'rolesPermissions', + ]); - return { roleId, objectsPermissions: objectMetadataPermissions[roleId] }; + return { roleId, objectsPermissions: rolesPermissions[roleId] }; } private async prepareExtendedQueryRunnerContext( diff --git a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts index fa0916b327d..f468c34600b 100644 --- a/packages/twenty-server/src/engine/api/common/core-common-api.module.ts +++ b/packages/twenty-server/src/engine/api/common/core-common-api.module.ts @@ -25,6 +25,7 @@ import { ViewFilterModule } from 'src/engine/metadata-modules/view-filter/view-f import { ViewModule } from 'src/engine/metadata-modules/view/view.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ @@ -45,6 +46,7 @@ import { GlobalWorkspaceDataSourceModule } from 'src/engine/twenty-orm/global-wo RecordTransformerModule, GlobalWorkspaceDataSourceModule, FeatureFlagModule, + WorkspaceCacheModule, ], providers: [ ProcessNestedRelationsHelper, diff --git a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts index 84a1b7b9897..4814ad675f5 100644 --- a/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts +++ b/packages/twenty-server/src/engine/api/rest/core/handlers/rest-api-base.handler.ts @@ -33,8 +33,8 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; @@ -58,7 +58,7 @@ export abstract class RestApiBaseHandler { @Inject() protected readonly twentyORMManager: TwentyORMManager; @Inject() - protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService; + protected readonly workspaceCacheService: WorkspaceCacheService; @Inject() protected readonly createdByFromAuthContextService: CreatedByFromAuthContextService; @Inject() @@ -115,15 +115,13 @@ export abstract class RestApiBaseHandler { roleId = userWorkspaceRoleId; } - const objectMetadataPermissions = - await this.workspacePermissionsCacheService.getObjectRecordPermissionsForRoles( - { - workspaceId: authContext.workspace.id, - roleIds: [roleId], - }, + const { rolesPermissions } = + await this.workspaceCacheService.getOrRecompute( + authContext.workspace.id, + ['rolesPermissions'], ); - return { objectsPermissions: objectMetadataPermissions[roleId] }; + return { objectsPermissions: rolesPermissions[roleId] }; }; async computeSelectedFields({ diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts index e4822c1d8ad..901cbc62bbd 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts @@ -32,6 +32,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceFlatMapCacheModule } from 'src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module'; const restApiCoreResolvers = [ @@ -69,6 +70,7 @@ const restApiCoreResolvers = [ FeatureFlagModule, CoreCommonApiModule, WorkspaceDomainsModule, + WorkspaceCacheModule, ], controllers: [RestApiCoreController], providers: [ diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts deleted file mode 100644 index b7fa79bde5d..00000000000 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.spec.ts +++ /dev/null @@ -1,458 +0,0 @@ -import { Test, type TestingModule } from '@nestjs/testing'; -import { getDataSourceToken, getRepositoryToken } from '@nestjs/typeorm'; - -import { In } from 'typeorm'; - -import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; -import { - ApiKeyException, - ApiKeyExceptionCode, -} from 'src/engine/core-modules/api-key/api-key.exception'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service'; -import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; - -import { ApiKeyRoleService } from './api-key-role.service'; - -describe('ApiKeyRoleService', () => { - let service: ApiKeyRoleService; - let mockRoleTargetsRepository: any; - let mockRoleRepository: any; - let mockWorkspaceRepository: any; - let mockApiKeyRepository: any; - let mockDataSource: any; - let mockWorkspacePermissionsCacheService: any; - let mockRoleTargetService: any; - - const mockWorkspaceId = 'workspace-123'; - const mockApiKeyId = 'api-key-456'; - const mockRoleId = 'role-789'; - const mockNewRoleId = 'role-999'; - - const mockApiKey: ApiKeyEntity = { - id: mockApiKeyId, - name: 'Test API Key', - expiresAt: new Date('2025-12-31'), - revokedAt: undefined, - workspaceId: mockWorkspaceId, - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-01'), - workspace: {} as any, - }; - - const mockRole: Partial = { - id: mockRoleId, - label: 'Admin', - icon: 'admin-icon', - description: 'Admin role', - isEditable: true, - workspaceId: mockWorkspaceId, - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-01'), - canUpdateAllSettings: true, - canAccessAllTools: true, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - canBeAssignedToAgents: false, - canBeAssignedToUsers: true, - canBeAssignedToApiKeys: true, - }; - - const mockNewRole: Partial = { - ...mockRole, - id: mockNewRoleId, - label: 'Member', - }; - - const mockRoleTarget = { - id: 'role-target-123', - roleId: mockRoleId, - apiKeyId: mockApiKeyId, - workspaceId: mockWorkspaceId, - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-01'), - role: mockRole, - apiKey: mockApiKey, - } as RoleTargetsEntity; - - beforeEach(async () => { - mockRoleTargetsRepository = { - save: jest.fn(), - delete: jest.fn(), - find: jest.fn(), - findOne: jest.fn(), - }; - - mockRoleRepository = { - findOne: jest.fn(), - }; - - mockWorkspaceRepository = { - findOne: jest.fn(), - }; - - mockApiKeyRepository = { - findOne: jest.fn(), - }; - - mockDataSource = { - transaction: jest.fn(), - }; - - mockWorkspacePermissionsCacheService = { - recomputeApiKeyRoleMapCache: jest.fn(), - getApiKeyRoleMapFromCache: jest.fn(), - }; - - mockRoleTargetService = { - create: jest.fn(), - delete: jest.fn(), - }; - - const module: TestingModule = await Test.createTestingModule({ - providers: [ - ApiKeyRoleService, - { - provide: getRepositoryToken(RoleTargetsEntity), - useValue: mockRoleTargetsRepository, - }, - { - provide: getRepositoryToken(RoleEntity), - useValue: mockRoleRepository, - }, - { - provide: getRepositoryToken(WorkspaceEntity), - useValue: mockWorkspaceRepository, - }, - { - provide: getRepositoryToken(ApiKeyEntity), - useValue: mockApiKeyRepository, - }, - { - provide: getDataSourceToken(), - useValue: mockDataSource, - }, - { - provide: WorkspacePermissionsCacheService, - useValue: mockWorkspacePermissionsCacheService, - }, - { - provide: RoleTargetService, - useValue: mockRoleTargetService, - }, - ], - }).compile(); - - service = module.get(ApiKeyRoleService); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('assignRoleToApiKey', () => { - it('should assign a new role to API key using roleTargetService', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockNewRole); - mockRoleTargetsRepository.findOne.mockResolvedValue(null); - mockRoleTargetService.create.mockResolvedValue(undefined); - - await service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }); - - expect(mockRoleTargetService.create).toHaveBeenCalledWith({ - createRoleTargetInput: { - roleId: mockNewRoleId, - targetId: mockApiKeyId, - targetMetadataForeignKey: 'apiKeyId', - }, - workspaceId: mockWorkspaceId, - }); - }); - - it('should skip assignment if role is already assigned', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockRole); - mockRoleTargetsRepository.findOne.mockResolvedValue(mockRoleTarget); - - await service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: mockRoleId, - workspaceId: mockWorkspaceId, - }); - - expect(mockRoleTargetService.create).not.toHaveBeenCalled(); - }); - - it('should throw exception if API key not found', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(null); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: 'non-existent', - roleId: mockRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toThrow(ApiKeyException); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: 'non-existent', - roleId: mockRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toMatchObject({ - code: ApiKeyExceptionCode.API_KEY_NOT_FOUND, - }); - }); - - it('should throw exception if role not found', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(null); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: 'non-existent-role', - workspaceId: mockWorkspaceId, - }), - ).rejects.toThrow(ApiKeyException); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: 'non-existent-role', - workspaceId: mockWorkspaceId, - }), - ).rejects.toMatchObject({ - code: ApiKeyExceptionCode.API_KEY_NOT_FOUND, - }); - }); - }); - - describe('getRoleIdForApiKey', () => { - it('should return role ID from cache', async () => { - const mockCacheData = { - data: { - [mockApiKeyId]: mockRoleId, - }, - }; - - mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache.mockResolvedValue( - mockCacheData, - ); - - const result = await service.getRoleIdForApiKey( - mockApiKeyId, - mockWorkspaceId, - ); - - expect( - mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache, - ).toHaveBeenCalledWith({ - workspaceId: mockWorkspaceId, - }); - expect(result).toBe(mockRoleId); - }); - - it('should throw exception if API key has no role in cache', async () => { - const mockCacheData = { - data: {}, - }; - - mockWorkspacePermissionsCacheService.getApiKeyRoleMapFromCache.mockResolvedValue( - mockCacheData, - ); - - await expect( - service.getRoleIdForApiKey(mockApiKeyId, mockWorkspaceId), - ).rejects.toThrow(ApiKeyException); - - await expect( - service.getRoleIdForApiKey(mockApiKeyId, mockWorkspaceId), - ).rejects.toMatchObject({ - code: ApiKeyExceptionCode.API_KEY_NO_ROLE_ASSIGNED, - }); - }); - }); - - describe('getRolesByApiKeys', () => { - it('should return empty map for empty API key IDs', async () => { - const result = await service.getRolesByApiKeys({ - apiKeyIds: [], - workspaceId: mockWorkspaceId, - }); - - expect(result).toEqual(new Map()); - expect(mockRoleTargetsRepository.find).not.toHaveBeenCalled(); - }); - - it('should return roles map for given API key IDs', async () => { - const mockApiKeyIds = [mockApiKeyId, 'another-api-key']; - const mockRoleTargets = [ - { - apiKeyId: mockApiKeyId, - role: mockRole, - }, - { - apiKeyId: 'another-api-key', - role: mockNewRole, - }, - ]; - - mockRoleTargetsRepository.find.mockResolvedValue(mockRoleTargets); - - const result = await service.getRolesByApiKeys({ - apiKeyIds: mockApiKeyIds, - workspaceId: mockWorkspaceId, - }); - - expect(mockRoleTargetsRepository.find).toHaveBeenCalledWith({ - where: { - apiKeyId: In(mockApiKeyIds), - workspaceId: mockWorkspaceId, - }, - relations: ['role'], - }); - - expect(result.size).toBe(2); - expect(result.get(mockApiKeyId)).toEqual({ - id: mockRole.id, - label: mockRole.label, - icon: mockRole.icon, - description: mockRole.description, - isEditable: mockRole.isEditable, - roleTargets: mockRole.roleTargets, - canUpdateAllSettings: true, - canAccessAllTools: true, - canReadAllObjectRecords: true, - canUpdateAllObjectRecords: true, - canSoftDeleteAllObjectRecords: true, - canDestroyAllObjectRecords: true, - canBeAssignedToAgents: false, - canBeAssignedToUsers: true, - canBeAssignedToApiKeys: true, - standardId: undefined, - }); - }); - - it('should handle role targets with missing apiKeyId or role gracefully', async () => { - const mockRoleTargets = [ - { - apiKeyId: null, - role: mockRole, - }, - { - apiKeyId: mockApiKeyId, - role: null, - }, - { - apiKeyId: 'valid-api-key', - role: mockRole, - }, - ]; - - mockRoleTargetsRepository.find.mockResolvedValue(mockRoleTargets); - - const result = await service.getRolesByApiKeys({ - apiKeyIds: [mockApiKeyId, 'valid-api-key'], - workspaceId: mockWorkspaceId, - }); - - expect(result.size).toBe(1); - expect(result.has('valid-api-key')).toBe(true); - }); - }); - - describe('validateAssignRoleInput', () => { - it('should validate successful role assignment inputs', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockNewRole); - mockRoleTargetsRepository.findOne.mockResolvedValue(null); - - const validateMethod = (service as any).validateAssignRoleInput; - const result = await validateMethod.call(service, { - apiKeyId: mockApiKeyId, - workspaceId: mockWorkspaceId, - roleId: mockNewRoleId, - }); - - expect(result.roleToAssignIsSameAsCurrentRole).toBe(false); - }); - - it('should detect same role assignment', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockRole); - mockRoleTargetsRepository.findOne.mockResolvedValue(mockRoleTarget); - - const validateMethod = (service as any).validateAssignRoleInput; - const result = await validateMethod.call(service, { - apiKeyId: mockApiKeyId, - workspaceId: mockWorkspaceId, - roleId: mockRoleId, - }); - - expect(result.roleToAssignIsSameAsCurrentRole).toBe(true); - }); - }); - - describe('error handling', () => { - it('should handle roleTargetService failures gracefully', async () => { - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(mockNewRole); - mockRoleTargetsRepository.findOne.mockResolvedValue(null); - - mockRoleTargetService.create.mockRejectedValue( - new Error('Role target creation failed'), - ); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toThrow('Role target creation failed'); - }); - - it('should throw exception if role cannot be assigned to API keys', async () => { - const roleNotForApiKeys = { - ...mockNewRole, - canBeAssignedToApiKeys: false, - }; - - mockApiKeyRepository.findOne.mockResolvedValue(mockApiKey); - mockRoleRepository.findOne.mockResolvedValue(roleNotForApiKeys); - mockRoleTargetsRepository.findOne.mockResolvedValue(null); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toThrow(ApiKeyException); - - await expect( - service.assignRoleToApiKey({ - apiKeyId: mockApiKeyId, - roleId: mockNewRoleId, - workspaceId: mockWorkspaceId, - }), - ).rejects.toMatchObject({ - code: ApiKeyExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_API_KEYS, - }); - }); - }); -}); diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts index 41c0a0f6290..e5fb70cf7fb 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key-role.service.ts @@ -13,7 +13,7 @@ import { type RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class ApiKeyRoleService { @@ -25,7 +25,7 @@ export class ApiKeyRoleService { @InjectRepository(ApiKeyEntity) private readonly apiKeyRepository: Repository, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly roleTargetService: RoleTargetService, ) {} @@ -62,12 +62,12 @@ export class ApiKeyRoleService { apiKeyId: string, workspaceId: string, ): Promise { - const apiKeyRoleMap = - await this.workspacePermissionsCacheService.getApiKeyRoleMapFromCache({ - workspaceId, - }); + const { apiKeyRoleMap } = await this.workspaceCacheService.getOrRecompute( + workspaceId, + ['apiKeyRoleMap'], + ); - const roleId = apiKeyRoleMap.data[apiKeyId]; + const roleId = apiKeyRoleMap[apiKeyId]; if (!roleId) { throw new ApiKeyException( diff --git a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts index 22c9a36ebb0..8285a25f406 100644 --- a/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts +++ b/packages/twenty-server/src/engine/core-modules/api-key/api-key.module.ts @@ -15,6 +15,7 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { ApiKeyController } from './controllers/api-key.controller'; @@ -28,6 +29,7 @@ import { ApiKeyController } from './controllers/api-key.controller'; ]), JwtModule, WorkspacePermissionsCacheModule, + WorkspaceCacheModule, WorkspaceCacheStorageModule, FeatureFlagModule, RoleTargetModule, diff --git a/packages/twenty-server/src/engine/core-modules/application/application.module.ts b/packages/twenty-server/src/engine/core-modules/application/application.module.ts index 43d8be30a87..7d351c5f174 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.module.ts @@ -7,11 +7,13 @@ import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ TypeOrmModule.forFeature([ApplicationEntity, AgentEntity, WorkspaceEntity]), WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceCacheModule, ], exports: [ApplicationService, WorkspaceFlatApplicationMapCacheService], providers: [ApplicationService, WorkspaceFlatApplicationMapCacheService], diff --git a/packages/twenty-server/src/engine/core-modules/application/application.service.ts b/packages/twenty-server/src/engine/core-modules/application/application.service.ts index 5cd6a57cd8c..619aac3032c 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.service.ts @@ -11,17 +11,16 @@ import { ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/core-modules/application/constants/twenty-standard-applications'; -import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules/application/services/workspace-flat-application-map-cache.service'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class ApplicationService { constructor( @InjectRepository(ApplicationEntity) private readonly applicationRepository: Repository, - private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly workspaceFlatApplicationMapCacheService: WorkspaceFlatApplicationMapCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, ) {} @@ -50,12 +49,11 @@ export class ApplicationService { ); } - const flatApplicationMaps = - await this.workspaceFlatApplicationMapCacheService.getExistingOrRecomputeFlatMaps( - { - workspaceId: workspace.id, - }, - ); + const { flatApplicationMaps } = + await this.workspaceCacheService.getOrRecompute(workspace.id, [ + 'flatApplicationMaps', + ]); + const twentyStandardApplicationId = flatApplicationMaps.idByUniversalIdentifier[ TWENTY_STANDARD_APPLICATION.universalIdentifier @@ -169,9 +167,9 @@ export class ApplicationService { ); if (!skipCacheInvalidation) { - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId, - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatApplicationMaps', + ]); } return twentyStandardApplication; @@ -221,9 +219,9 @@ export class ApplicationService { const savedApplication = await this.applicationRepository.save(application); - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId: data.workspaceId, - }); + await this.workspaceCacheService.invalidateAndRecompute(data.workspaceId, [ + 'flatApplicationMaps', + ]); return savedApplication; } @@ -234,9 +232,10 @@ export class ApplicationService { ): Promise { await this.applicationRepository.update({ id }, data); - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId: data.workspaceId as string, - }); + await this.workspaceCacheService.invalidateAndRecompute( + data.workspaceId as string, + ['flatApplicationMaps'], + ); const updatedApplication = await this.findById(id); @@ -262,14 +261,13 @@ export class ApplicationService { workspaceId, }); - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId, - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatApplicationMaps', + ]); - await this.workspaceManyOrAllFlatEntityMapsCacheService.invalidateFlatEntityMaps( - { - workspaceId, - }, + await this.workspaceCacheService.invalidateAndRecompute( + workspaceId, + ALL_FLAT_ENTITY_MAPS_PROPERTIES, ); } } diff --git a/packages/twenty-server/src/engine/core-modules/application/services/workspace-flat-application-map-cache.service.ts b/packages/twenty-server/src/engine/core-modules/application/services/workspace-flat-application-map-cache.service.ts index f58d6b35332..13953d9c17d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/services/workspace-flat-application-map-cache.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/services/workspace-flat-application-map-cache.service.ts @@ -1,38 +1,28 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { AllMetadataName } from 'twenty-shared/metadata'; import { Repository } from 'typeorm'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; import { fromApplicationEntityToFlatApplication } from 'src/engine/core-modules/application/utils/from-application-entity-to-flat-application.util'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { MetadataToFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/types/metadata-to-flat-entity-maps-key'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; @Injectable() -@WorkspaceFlatMapCache( - 'flatApplicationMaps' as MetadataToFlatEntityMapsKey, -) // TODO prastoin introduce SyncableMetadata notion -export class WorkspaceFlatApplicationMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatApplicationMaps') +export class WorkspaceFlatApplicationMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ApplicationEntity) private readonly applicationRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache( + workspaceId: string, + ): Promise { const applicationEntities = await this.applicationRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 163a1e75076..1a99cf36383 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -59,6 +59,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; @@ -115,6 +116,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; SubdomainManagerModule, DomainServerConfigModule, ApplicationModule, + WorkspaceCacheModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index ddabe788a7a..1ebc745374d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -11,7 +11,6 @@ import { v4 } from 'uuid'; import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants'; import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules/application/services/workspace-flat-application-map-cache.service'; import { AuthException, AuthExceptionCode, @@ -39,6 +38,7 @@ import { UserEntity } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email'; import { isWorkEmail } from 'src/utils/is-work-email'; @@ -60,7 +60,7 @@ export class SignInUpService { private readonly subdomainManagerService: SubdomainManagerService, private readonly userService: UserService, private readonly metricsService: MetricsService, - private readonly workspaceFlatApplicationMapCacheService: WorkspaceFlatApplicationMapCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly applicationService: ApplicationService, @InjectDataSource() private readonly dataSource: DataSource, @@ -526,9 +526,9 @@ export class SignInUpService { ); await queryRunner.commitTransaction(); - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId, - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatApplicationMaps', + ]); return { user, workspace }; } catch (error) { diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts index ca4ed172ede..8a1003d5ee2 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/services/cache-storage.service.ts @@ -28,6 +28,22 @@ export class CacheStorageService { return this.cache.del(this.getKey(key)); } + async mdel(keys: string[]): Promise { + if (keys.length === 0) { + return; + } + + if (this.isRedisCache()) { + const prefixedKeys = keys.map((k) => this.getKey(k)); + + await (this.cache as RedisCache).store.client.del(prefixedKeys); + + return; + } + + await Promise.all(keys.map((k) => this.del(k))); + } + async mget(keys: string[]): Promise<(T | undefined)[]> { if (this.isRedisCache()) { const prefixedKeys = keys.map((k) => this.getKey(k)); diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts index ce68a960c5d..347c83dc6de 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/feature-flag.module.ts @@ -8,6 +8,7 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor }), WorkspaceFeatureFlagsMapCacheModule, WorkspacePermissionsCacheModule, + WorkspaceCacheModule, ], exports: [FeatureFlagService], providers: [FeatureFlagService], diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/__tests__/feature-flag.service.spec.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/__tests__/feature-flag.service.spec.ts index 4f1441fdda4..89245eba629 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/__tests__/feature-flag.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/services/__tests__/feature-flag.service.spec.ts @@ -10,8 +10,8 @@ import { import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; import { publicFeatureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate'; -import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; jest.mock( 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate', @@ -31,9 +31,9 @@ describe('FeatureFlagService', () => { save: jest.fn(), }; - const mockWorkspaceFeatureFlagsMapCacheService = { - getWorkspaceFeatureFlagsMap: jest.fn(), - recomputeFeatureFlagsMapCache: jest.fn(), + const mockWorkspaceCacheService = { + getOrRecompute: jest.fn(), + invalidateAndRecompute: jest.fn(), }; const mockWorkspacePermissionsCacheService = { @@ -59,8 +59,8 @@ describe('FeatureFlagService', () => { useValue: mockFeatureFlagRepository, }, { - provide: WorkspaceFeatureFlagsMapCacheService, - useValue: mockWorkspaceFeatureFlagsMapCacheService, + provide: WorkspaceCacheService, + useValue: mockWorkspaceCacheService, }, { provide: WorkspacePermissionsCacheService, @@ -79,29 +79,28 @@ describe('FeatureFlagService', () => { describe('isFeatureEnabled', () => { it('should return true when feature flag is enabled', async () => { // Prepare - mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue( - { + mockWorkspaceCacheService.getOrRecompute.mockResolvedValue({ + featureFlagsMap: { [featureFlag]: true, }, - ); + }); // Act const result = await service.isFeatureEnabled(featureFlag, workspaceId); // Assert expect(result).toBe(true); - expect( - mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap, - ).toHaveBeenCalledWith({ + expect(mockWorkspaceCacheService.getOrRecompute).toHaveBeenCalledWith( workspaceId, - }); + ['featureFlagsMap'], + ); }); it('should return false when feature flag is not found', async () => { // Prepare - mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue( - {}, - ); + mockWorkspaceCacheService.getOrRecompute.mockResolvedValue({ + featureFlagsMap: {}, + }); // Act const result = await service.isFeatureEnabled(featureFlag, workspaceId); @@ -128,11 +127,11 @@ describe('FeatureFlagService', () => { describe('getWorkspaceFeatureFlags', () => { it('should return all feature flags for a workspace', async () => { // Prepare - mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap.mockResolvedValue( - { + mockWorkspaceCacheService.getOrRecompute.mockResolvedValue({ + featureFlagsMap: { [FeatureFlagKey.IS_AI_ENABLED]: false, }, - ); + }); const mockFeatureFlags = [ { key: FeatureFlagKey.IS_AI_ENABLED, value: false }, ]; @@ -142,9 +141,10 @@ describe('FeatureFlagService', () => { // Assert expect(result).toEqual(mockFeatureFlags); - expect( - mockWorkspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap, - ).toHaveBeenCalledWith({ workspaceId }); + expect(mockWorkspaceCacheService.getOrRecompute).toHaveBeenCalledWith( + workspaceId, + ['featureFlagsMap'], + ); }); }); @@ -173,6 +173,9 @@ describe('FeatureFlagService', () => { const keys = [FeatureFlagKey.IS_AI_ENABLED]; mockFeatureFlagRepository.upsert.mockResolvedValue({}); + mockWorkspaceCacheService.invalidateAndRecompute.mockResolvedValue( + undefined, + ); // Act await service.enableFeatureFlags(keys, workspaceId); @@ -185,6 +188,9 @@ describe('FeatureFlagService', () => { skipUpdateIfNoValuesChanged: true, }, ); + expect( + mockWorkspaceCacheService.invalidateAndRecompute, + ).toHaveBeenCalledWith(workspaceId, ['featureFlagsMap']); }); }); @@ -199,6 +205,9 @@ describe('FeatureFlagService', () => { }; mockFeatureFlagRepository.save.mockResolvedValue(mockFeatureFlag); + mockWorkspaceCacheService.invalidateAndRecompute.mockResolvedValue( + undefined, + ); ( featureFlagValidator.assertIsFeatureFlagKey as jest.Mock @@ -218,6 +227,9 @@ describe('FeatureFlagService', () => { value, workspaceId, }); + expect( + mockWorkspaceCacheService.invalidateAndRecompute, + ).toHaveBeenCalledWith(workspaceId, ['featureFlagsMap']); }); it('should throw an exception when feature flag key is invalid', async () => { diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts index 5e2268b7c53..66e9a68340c 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts @@ -14,14 +14,14 @@ import { } from 'src/engine/core-modules/feature-flag/feature-flag.exception'; import { featureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/feature-flag.validate'; import { publicFeatureFlagValidator } from 'src/engine/core-modules/feature-flag/validates/is-public-feature-flag.validate'; -import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class FeatureFlagService { constructor( @InjectRepository(FeatureFlagEntity) private readonly featureFlagRepository: Repository, - private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} public async isFeatureEnabled( @@ -36,10 +36,10 @@ export class FeatureFlagService { public async getWorkspaceFeatureFlags( workspaceId: string, ): Promise { - const workspaceFeatureFlagsMap = - await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap( - { workspaceId }, - ); + const { featureFlagsMap: workspaceFeatureFlagsMap } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'featureFlagsMap', + ]); return Object.entries(workspaceFeatureFlagsMap).map(([key, value]) => ({ key: key as FeatureFlagKey, @@ -50,10 +50,10 @@ export class FeatureFlagService { public async getWorkspaceFeatureFlagsMap( workspaceId: string, ): Promise { - const workspaceFeatureFlagsMap = - await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMap( - { workspaceId }, - ); + const { featureFlagsMap: workspaceFeatureFlagsMap } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'featureFlagsMap', + ]); return workspaceFeatureFlagsMap; } @@ -71,9 +71,9 @@ export class FeatureFlagService { }, ); - await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( - { workspaceId }, - ); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'featureFlagsMap', + ]); } } @@ -126,9 +126,9 @@ export class FeatureFlagService { const result = await this.featureFlagRepository.save(featureFlagToSave); - await this.workspaceFeatureFlagsMapCacheService.recomputeFeatureFlagsMapCache( - { workspaceId }, - ); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'featureFlagsMap', + ]); return result; } diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts index 429f846c97c..d1f0bf2a008 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/ai-tools.module.ts @@ -17,6 +17,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { MessagingModule } from 'src/modules/messaging/messaging.module'; @Global() @@ -35,6 +36,7 @@ import { MessagingModule } from 'src/modules/messaging/messaging.module'; MessagingModule, PermissionsModule, ToolModule, + WorkspaceCacheModule, ], providers: [ToolService, ToolAdapterService, SearchArticlesTool], exports: [ToolService, ToolAdapterService, SearchArticlesTool], diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts index 8aa0ebe589b..d214666c805 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/__tests__/tool.service.spec.ts @@ -7,9 +7,9 @@ import { UpdateRecordService } from 'src/engine/core-modules/record-crud/service import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { ToolService } from 'src/engine/metadata-modules/ai/ai-tools/services/tool.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { getMockObjectMetadataEntity } from 'src/utils/__test__/get-object-metadata-entity.mock'; // Minimal mock repository type @@ -28,7 +28,7 @@ describe('ToolService', () => { const roleId = 'role_1'; let service: ToolService; - let permissionsCacheService: WorkspacePermissionsCacheService; + let workspaceCacheService: WorkspaceCacheService; const testObject = getMockObjectMetadataEntity({ workspaceId: '', @@ -75,10 +75,10 @@ describe('ToolService', () => { }, }, { - provide: WorkspacePermissionsCacheService, + provide: WorkspaceCacheService, useValue: { - getRolesPermissionsFromCache: jest.fn().mockResolvedValue({ - data: { + getOrRecompute: jest.fn().mockResolvedValue({ + rolesPermissions: { [roleId]: { [testObject.id]: { canReadObjectRecords: true, @@ -135,16 +135,17 @@ describe('ToolService', () => { }).compile(); service = moduleRef.get(ToolService); - permissionsCacheService = moduleRef.get(WorkspacePermissionsCacheService); + workspaceCacheService = moduleRef.get(WorkspaceCacheService); }); describe('listTools', () => { it('should return tools based on role permissions', async () => { const tools = await service.listTools({ unionOf: [roleId] }, workspaceId); - expect( - permissionsCacheService.getRolesPermissionsFromCache, - ).toHaveBeenCalledWith({ workspaceId }); + expect(workspaceCacheService.getOrRecompute).toHaveBeenCalledWith( + workspaceId, + ['rolesPermissions'], + ); // Verify tool keys expect(tools['create_testObject']).toBeDefined(); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts index 04c346af3ed..c5a9d6f7bb8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-tools/services/tool.service.ts @@ -21,8 +21,8 @@ import { ToolOperation, } from 'src/engine/metadata-modules/ai/ai-chat-router/types/tool-hints.interface'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; import { computePermissionIntersection } from 'src/engine/twenty-orm/utils/compute-permission-intersection.util'; @@ -32,7 +32,7 @@ export class ToolService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - protected readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + protected readonly workspaceCacheService: WorkspaceCacheService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly createRecordService: CreateRecordService, private readonly updateRecordService: UpdateRecordService, @@ -51,10 +51,10 @@ export class ToolService { ): Promise { const tools: ToolSet = {}; - const { data: rolesPermissions } = - await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({ - workspaceId, - }); + const { rolesPermissions } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'rolesPermissions', + ]); let objectPermissions; diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/workspace-flat-cron-trigger-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/workspace-flat-cron-trigger-map-cache.service.ts index 3cdd4418f46..6ff16f500a9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/workspace-flat-cron-trigger-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/workspace-flat-cron-trigger-map-cache.service.ts @@ -4,9 +4,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { removePropertiesFromRecord } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { CRON_TRIGGER_ENTITY_RELATION_PROPERTIES, CronTriggerEntity, @@ -14,29 +13,24 @@ import { import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatCronTriggerMaps') -export class WorkspaceFlatCronTriggerMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatCronTriggerMaps') +export class WorkspaceFlatCronTriggerMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(CronTriggerEntity) private readonly cronTriggerRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const cronTriggers = await this.cronTriggerRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/workspace-flat-database-event-trigger-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/workspace-flat-database-event-trigger-map-cache.service.ts index 1182277e3cd..aa5f7a214e1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/workspace-flat-database-event-trigger-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/workspace-flat-database-event-trigger-map-cache.service.ts @@ -4,9 +4,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { removePropertiesFromRecord } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { DATABASE_EVENT_TRIGGER_ENTITY_RELATION_PROPERTIES, DatabaseEventTriggerEntity, @@ -14,29 +13,24 @@ import { import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatDatabaseEventTriggerMaps') -export class WorkspaceFlatDatabaseEventTriggerMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatDatabaseEventTriggerMaps') +export class WorkspaceFlatDatabaseEventTriggerMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(DatabaseEventTriggerEntity) private readonly databaseEventTriggerRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const databaseEventTriggers = await this.databaseEventTriggerRepository.find({ where: { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts index bd5f9c63a23..5497448f8a0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceFlatMapCacheModule } from 'src/engine/workspace-flat-map-cache/workspace-flat-map-cache.module'; @Module({ - imports: [WorkspaceFlatMapCacheModule], + imports: [WorkspaceFlatMapCacheModule, WorkspaceCacheModule], providers: [WorkspaceManyOrAllFlatEntityMapsCacheService], exports: [WorkspaceManyOrAllFlatEntityMapsCacheService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service.ts index bb89f2d72ea..f1314804613 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service.ts @@ -1,66 +1,12 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { isDefined } from 'twenty-shared/utils'; +import { Injectable } from '@nestjs/common'; import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant'; import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; -import { WorkspaceFlatMapCacheRegistryService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache-registry.service'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class WorkspaceManyOrAllFlatEntityMapsCacheService { - private readonly logger = new Logger( - WorkspaceManyOrAllFlatEntityMapsCacheService.name, - ); - - constructor( - private readonly cacheRegistry: WorkspaceFlatMapCacheRegistryService, - ) {} - - private async executeActionForManyOrAllFlatEntity< - K extends (keyof AllFlatEntityMaps)[] = (keyof AllFlatEntityMaps)[], - >({ - action, - flatMapsKeys, - }: { - flatMapsKeys: K | undefined; - action: (args: { - service: WorkspaceFlatMapCacheService; - flatMapKey: K[number]; - }) => Promise; - }): Promise { - const keysToProcess = isDefined(flatMapsKeys) - ? flatMapsKeys - : ALL_FLAT_ENTITY_MAPS_PROPERTIES; - - const results = await Promise.allSettled( - keysToProcess.map(async (flatMapKey) => { - try { - const service = this.cacheRegistry.getCacheServiceOrThrow( - flatMapKey as K[number], - ); - - return await action({ - flatMapKey: flatMapKey, - service, - }); - } catch (error) { - this.logger.error( - `Failed to run action on flat entity maps of ${flatMapKey}`, - error, - ); - throw error; - } - }), - ); - - const failures = results.filter((result) => result.status === 'rejected'); - - if (failures.length > 0) { - this.logger.error(`${failures.length} operations failed`); - throw new Error(`Failed to process ${failures.length} flat entity maps`); - } - } + constructor(private readonly workspaceCacheService: WorkspaceCacheService) {} public async getOrRecomputeManyOrAllFlatEntityMaps< T extends (keyof AllFlatEntityMaps)[] = (keyof AllFlatEntityMaps)[], @@ -71,20 +17,10 @@ export class WorkspaceManyOrAllFlatEntityMapsCacheService { workspaceId: string; flatMapsKeys?: T; }): Promise> { - let pickedFlatEntityMaps = {} as Pick; - - await this.executeActionForManyOrAllFlatEntity({ - action: async ({ service, flatMapKey }) => { - const cacheResult = await service.getExistingOrRecomputeFlatMaps({ - workspaceId, - }); - - pickedFlatEntityMaps[flatMapKey] = cacheResult; - }, - flatMapsKeys, - }); - - return pickedFlatEntityMaps; + return await this.workspaceCacheService.getOrRecompute( + workspaceId, + flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES, + ); } public async invalidateFlatEntityMaps< @@ -96,11 +32,10 @@ export class WorkspaceManyOrAllFlatEntityMapsCacheService { workspaceId: string; flatMapsKeys?: T; }): Promise { - await this.executeActionForManyOrAllFlatEntity({ - action: async ({ service }) => - await service.invalidateCache({ workspaceId }), - flatMapsKeys, - }); + await this.workspaceCacheService.invalidateAndRecompute( + workspaceId, + flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES, + ); } public async flushFlatEntityMaps< @@ -112,9 +47,9 @@ export class WorkspaceManyOrAllFlatEntityMapsCacheService { workspaceId: string; flatMapsKeys?: T; }): Promise { - await this.executeActionForManyOrAllFlatEntity({ - action: async ({ service }) => await service.flushCache({ workspaceId }), - flatMapsKeys, - }); + await this.workspaceCacheService.flush( + workspaceId, + flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES, + ); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts index 7408bfdd80b..d9f491635fe 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/services/workspace-flat-field-metadata-map-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -15,19 +14,16 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatFieldMetadataMaps') -export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatFieldMetadataMaps') +export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(FieldMetadataEntity) private readonly fieldMetadataRepository: Repository, @InjectRepository(ViewFieldEntity) @@ -39,14 +35,12 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapC @InjectRepository(ViewEntity) private readonly viewRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const [fieldMetadatas, viewFields, viewFilters, viewGroups, views] = await Promise.all([ this.fieldMetadataRepository.find({ diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts index 51e397010f3..69def243c91 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-index-metadata/services/workspace-flat-index-map-cache.service.ts @@ -3,37 +3,31 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { FlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/types/flat-index-metadata.type'; import { fromIndexMetadataEntityToFlatIndexMetadata } from 'src/engine/metadata-modules/flat-index-metadata/utils/from-index-metadata-entity-to-flat-index-metadata.util'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatIndexMaps') -export class WorkspaceFlatIndexMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatIndexMaps') +export class WorkspaceFlatIndexMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(IndexMetadataEntity) private readonly indexMetadataRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const indexes = await this.indexMetadataRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts index bc2f8da3797..2d1bda0a58d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-object-metadata/services/workspace-flat-object-metadata-map-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; @@ -14,20 +13,16 @@ import { fromObjectMetadataEntityToFlatObjectMetadata } from 'src/engine/metadat import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatObjectMetadataMaps') -export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatObjectMetadataMaps') +export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, - @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, @InjectRepository(FieldMetadataEntity) @@ -37,14 +32,12 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceFlatMap @InjectRepository(ViewEntity) private readonly viewRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const [objectMetadatas, fields, indexMetadatas, views] = await Promise.all([ this.objectMetadataRepository.find({ where: { workspaceId }, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts index 0c62a21fae6..5441837c9ed 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-role-target/services/workspace-flat-role-target-map-cache.service.ts @@ -3,34 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatRoleTargetMaps } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target-maps.type'; import { fromRoleTargetsEntityToFlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/utils/from-role-target-entity-to-flat-role-target.util'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatRoleTargetMaps') -export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatRoleTargetMaps') +export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(RoleTargetsEntity) private readonly roleTargetsRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache(workspaceId: string): Promise { const roleTargets = await this.roleTargetsRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts index 9d52493455d..95e98721fd0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts @@ -3,34 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatViewFieldMaps } from 'src/engine/metadata-modules/flat-view-field/types/flat-view-field-maps.type'; import { fromViewFieldEntityToFlatViewField } from 'src/engine/metadata-modules/flat-view-field/utils/from-view-field-entity-to-flat-view-field.util'; import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities/view-field.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatViewFieldMaps') -export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatViewFieldMaps') +export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ViewFieldEntity) private readonly viewFieldRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache(workspaceId: string): Promise { const existingViewFields = await this.viewFieldRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts index 3639f693100..83c8421d5ec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts @@ -3,34 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatViewFilterMaps } from 'src/engine/metadata-modules/flat-view-filter/types/flat-view-filter-maps.type'; import { fromViewFilterEntityToFlatViewFilter } from 'src/engine/metadata-modules/flat-view-filter/utils/from-view-filter-entity-to-flat-view-filter.util'; import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatViewFilterMaps') -export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatViewFilterMaps') +export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ViewFilterEntity) private readonly viewFilterRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache(workspaceId: string): Promise { const viewFilters = await this.viewFilterRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts index 838c5770de8..fc6c40cbfa8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-group/services/workspace-flat-view-group-map-cache.service.ts @@ -3,34 +3,26 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatViewGroupMaps } from 'src/engine/metadata-modules/flat-view-group/types/flat-view-group-maps.type'; import { fromViewGroupEntityToFlatViewGroup } from 'src/engine/metadata-modules/flat-view-group/utils/from-view-group-entity-to-flat-view-group.util'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatViewGroupMaps') -export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatViewGroupMaps') +export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ViewGroupEntity) private readonly viewGroupRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache(workspaceId: string): Promise { const existingViewGroups = await this.viewGroupRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts index d96f193e382..0f60ebf3483 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view/services/workspace-flat-view-map-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatViewMaps } from 'src/engine/metadata-modules/flat-view/types/flat-view-maps.type'; import { fromViewEntityToFlatView } from 'src/engine/metadata-modules/flat-view/utils/from-view-entity-to-flat-view.util'; @@ -13,17 +12,14 @@ import { ViewFieldEntity } from 'src/engine/metadata-modules/view-field/entities import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity'; import { ViewGroupEntity } from 'src/engine/metadata-modules/view-group/entities/view-group.entity'; import { ViewEntity } from 'src/engine/metadata-modules/view/entities/view.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatViewMaps') -export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheService { +@WorkspaceCache('flatViewMaps') +export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ViewEntity) private readonly viewRepository: Repository, @InjectRepository(ViewFieldEntity) @@ -33,14 +29,10 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheServi @InjectRepository(ViewGroupEntity) private readonly viewGroupRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { + async computeForCache(workspaceId: string): Promise { const [views, viewFields, viewFilters, viewGroups] = await Promise.all([ this.viewRepository.find({ where: { workspaceId }, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index c806476b942..f1c83ba05cc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -35,6 +35,7 @@ import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/work import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service'; @@ -66,6 +67,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa ViewModule, ViewFieldModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceCacheModule, ], services: [ ObjectMetadataService, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index 914230fdc88..b2739ea1a6c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -28,8 +28,8 @@ import { } from 'src/engine/metadata-modules/object-metadata/object-metadata.exception'; import { ViewKey } from 'src/engine/metadata-modules/view/enums/view-key.enum'; import { ViewType } from 'src/engine/metadata-modules/view/enums/view-type.enum'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { DEFAULT_VIEW_FIELD_SIZE } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/constants/DEFAULT_VIEW_FIELD_SIZE'; import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration-v2/services/workspace-migration-validate-build-and-run-service'; @@ -42,7 +42,7 @@ export class ObjectMetadataService extends TypeOrmQueryService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly applicationService: ApplicationService, ) { @@ -155,11 +155,9 @@ export class ObjectMetadataService extends TypeOrmQueryService { let service: ObjectPermissionService; @@ -22,7 +22,7 @@ describe('ObjectPermissionService', () => { Repository >; let roleRepository: jest.Mocked>; - let workspacePermissionsCacheService: jest.Mocked; + let workspaceCacheService: jest.Mocked; let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked; beforeEach(async () => { @@ -49,9 +49,10 @@ describe('ObjectPermissionService', () => { }, }, { - provide: WorkspacePermissionsCacheService, + provide: WorkspaceCacheService, useValue: { - recomputeRolesPermissionsCache: jest.fn(), + invalidate: jest.fn(), + invalidateAndRecompute: jest.fn(), }, }, { @@ -68,9 +69,7 @@ describe('ObjectPermissionService', () => { getRepositoryToken(ObjectPermissionEntity), ); roleRepository = module.get(getRepositoryToken(RoleEntity)); - workspacePermissionsCacheService = module.get( - WorkspacePermissionsCacheService, - ); + workspaceCacheService = module.get(WorkspaceCacheService); workspaceManyOrAllFlatEntityMapsCacheService = module.get( WorkspaceManyOrAllFlatEntityMapsCacheService, ); @@ -145,7 +144,7 @@ describe('ObjectPermissionService', () => { // Verify that upsert was never called expect(objectPermissionRepository.upsert).not.toHaveBeenCalled(); expect( - workspacePermissionsCacheService.recomputeRolesPermissionsCache, + workspaceCacheService.invalidateAndRecompute, ).not.toHaveBeenCalled(); }); @@ -230,12 +229,10 @@ describe('ObjectPermissionService', () => { conflictPaths: ['objectMetadataId', 'roleId'], }, ); - expect( - workspacePermissionsCacheService.recomputeRolesPermissionsCache, - ).toHaveBeenCalledWith({ + expect(workspaceCacheService.invalidateAndRecompute).toHaveBeenCalledWith( workspaceId, - roleIds: [roleId], - }); + ['rolesPermissions'], + ); }); it('should throw PermissionsException when object metadata is not found', async () => { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts index 3d15684fba4..82c744d5e80 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/__tests__/field-permissions.service.spec.ts @@ -20,7 +20,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { getMockFieldMetadataEntity } from 'src/utils/__test__/get-field-metadata-entity.mock'; describe('FieldPermissionService', () => { @@ -30,7 +30,7 @@ describe('FieldPermissionService', () => { >; let roleRepository: jest.Mocked>; let fieldMetadataRepository: jest.Mocked>; - let workspacePermissionsCacheService: jest.Mocked; + let workspaceCacheService: jest.Mocked; let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked; const testWorkspaceId = '20202020-0000-0000-0000-000000000000'; @@ -91,10 +91,11 @@ describe('FieldPermissionService', () => { }, }, { - provide: WorkspacePermissionsCacheService, + provide: WorkspaceCacheService, useValue: { - getRolesPermissionsFromCache: jest.fn(), - recomputeRolesPermissionsCache: jest.fn(), + getOrRecompute: jest.fn(), + invalidate: jest.fn(), + invalidateAndRecompute: jest.fn(), }, }, { @@ -120,9 +121,7 @@ describe('FieldPermissionService', () => { fieldMetadataRepository = module.get( getRepositoryToken(FieldMetadataEntity), ); - workspacePermissionsCacheService = module.get( - WorkspacePermissionsCacheService, - ); + workspaceCacheService = module.get(WorkspaceCacheService); workspaceManyOrAllFlatEntityMapsCacheService = module.get( WorkspaceManyOrAllFlatEntityMapsCacheService, ); @@ -133,12 +132,9 @@ describe('FieldPermissionService', () => { fieldTextMock, fieldRelationMock, ]); - workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue( - { - version: '1', - data: mockRolesPermissions, - }, - ); + (workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({ + rolesPermissions: mockRolesPermissions, + } as any); const testFieldMetadata = getMockFieldMetadataEntity({ ...fieldTextMock, label: 'Test Field', @@ -240,11 +236,8 @@ describe('FieldPermissionService', () => { ); expect( - workspacePermissionsCacheService.recomputeRolesPermissionsCache, - ).toHaveBeenCalledWith({ - workspaceId: testWorkspaceId, - roleIds: [testRoleId], - }); + workspaceCacheService.invalidateAndRecompute, + ).toHaveBeenCalledWith(testWorkspaceId, ['rolesPermissions']); }); it('should delete field permissions when both canReadFieldValue and canUpdateFieldValue are null', async () => { @@ -533,12 +526,9 @@ describe('FieldPermissionService', () => { }); it('should throw error when object permission is not found', async () => { - workspacePermissionsCacheService.getRolesPermissionsFromCache.mockResolvedValue( - { - version: '1', - data: {}, - }, - ); + (workspaceCacheService.getOrRecompute as jest.Mock).mockResolvedValue({ + rolesPermissions: {}, + } as any); const input = createUpsertInput([ { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts index 9165e191df2..6cc0123f6ae 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/field-permission/field-permission.service.ts @@ -14,8 +14,8 @@ import { } from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { isFieldMetadataTypeRelation } from 'src/engine/metadata-modules/field-metadata/utils/is-field-metadata-type-relation.util'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; 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 { type UpsertFieldPermissionsInput } from 'src/engine/metadata-modules/object-permission/dtos/upsert-field-permissions.input'; @@ -26,7 +26,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class FieldPermissionService { @@ -37,7 +37,7 @@ export class FieldPermissionService { private readonly fieldMetadataRepository: Repository, @InjectRepository(FieldPermissionEntity) private readonly fieldPermissionsRepository: Repository, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -53,10 +53,10 @@ export class FieldPermissionService { workspaceId, }); - const { data: rolesPermissions } = - await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({ - workspaceId, - }); + const { rolesPermissions } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'rolesPermissions', + ]); await this.validateRoleIsEditableOrThrow({ role, @@ -161,10 +161,9 @@ export class FieldPermissionService { }); } - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - roleIds: [input.roleId], - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + ]); return this.fieldPermissionsRepository.find({ where: { diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts index 78df3012cde..961aeb287f1 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.module.ts @@ -10,6 +10,7 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor ]), WorkspacePermissionsCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceCacheModule, ], providers: [ObjectPermissionService, FieldPermissionService], exports: [ObjectPermissionService, FieldPermissionService], diff --git a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts index 7f281a903de..25a1e5a2732 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-permission/object-permission.service.ts @@ -17,7 +17,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; export class ObjectPermissionService { constructor( @@ -27,7 +27,7 @@ export class ObjectPermissionService { private readonly roleRepository: Repository, @InjectRepository(ObjectMetadataEntity) private readonly objectMetadataRepository: Repository, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @@ -107,12 +107,9 @@ export class ObjectPermissionService { throw new Error('Failed to upsert object permission'); } - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - roleIds: [input.roleId], - }, - ); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + ]); return this.objectPermissionRepository.find({ where: { diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts index 9b54eb26d74..ac2bfed39e4 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.module.ts @@ -5,11 +5,13 @@ import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-fla import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ TypeOrmModule.forFeature([PermissionFlagEntity, RoleEntity]), WorkspacePermissionsCacheModule, + WorkspaceCacheModule, ], providers: [PermissionFlagService], diff --git a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts index b016cd0ba2a..a01e433a1f9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permission-flag/permission-flag.service.ts @@ -13,7 +13,7 @@ import { PermissionsExceptionMessage, } from 'src/engine/metadata-modules/permissions/permissions.exception'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; export class PermissionFlagService { constructor( @@ -21,7 +21,7 @@ export class PermissionFlagService { private readonly roleRepository: Repository, @InjectDataSource() private readonly coreDataSource: DataSource, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} public async upsertPermissionFlags({ @@ -128,12 +128,9 @@ export class PermissionFlagService { } finally { await queryRunner.release(); - await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache( - { - workspaceId, - roleIds: [input.roleId], - }, - ); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + ]); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/__tests__/permissions.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/__tests__/permissions.service.spec.ts index 376bc071c70..22ed785579d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/__tests__/permissions.service.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/__tests__/permissions.service.spec.ts @@ -6,7 +6,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/api-key-role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; describe('PermissionsService', () => { let service: PermissionsService; @@ -28,7 +28,7 @@ describe('PermissionsService', () => { useValue: {}, }, { - provide: WorkspacePermissionsCacheService, + provide: WorkspaceCacheService, useValue: {}, }, ], diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts index 4161720e590..fb5d4b3dd44 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.module.ts @@ -12,6 +12,7 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/wor TypeOrmModule.forFeature([UserWorkspaceEntity]), UserRoleModule, WorkspacePermissionsCacheModule, + WorkspaceCacheModule, RoleTargetModule, ], providers: [ApiKeyRoleService, PermissionsService], diff --git a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts index de59db36fa0..533af71a18e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/permissions/permissions.service.ts @@ -16,14 +16,14 @@ import { import { type UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class PermissionsService { constructor( private readonly userRoleService: UserRoleService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly apiKeyRoleService: ApiKeyRoleService, @InjectRepository(RoleEntity) private readonly roleRepository: Repository, @@ -77,10 +77,10 @@ export class PermissionsService { defaultSettingsPermissions, ); - const { data: rolesPermissions } = - await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({ - workspaceId, - }); + const { rolesPermissions } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'rolesPermissions', + ]); const objectsPermissions = rolesPermissions[roleOfUserWorkspace.id] ?? {}; diff --git a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts index 01f0c1b1594..6377a9c0db7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/role.module.ts @@ -22,7 +22,7 @@ import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { WorkspaceFlatRoleMapCacheService } from 'src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-graphql-api-exception.interceptor'; import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module'; @@ -44,11 +44,12 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa ObjectPermissionModule, PermissionFlagModule, WorkspacePermissionsCacheModule, - WorkspaceCacheStorageModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationV2Module, UserWorkspaceModule, FileModule, + ApplicationModule, + WorkspaceCacheModule, ], providers: [ RoleService, diff --git a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts index 34da1865417..dd58897de46 100644 --- a/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/role/services/workspace-flat-role-map-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type'; @@ -15,19 +14,16 @@ import { ObjectPermissionEntity } from 'src/engine/metadata-modules/object-permi import { PermissionFlagEntity } from 'src/engine/metadata-modules/permission-flag/permission-flag.entity'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatRoleMaps') -export class WorkspaceFlatRoleMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatRoleMaps') +export class WorkspaceFlatRoleMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(RoleEntity) private readonly roleRepository: Repository, @InjectRepository(RoleTargetsEntity) @@ -39,14 +35,12 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceFlatMapCacheServi @InjectRepository(FieldPermissionEntity) private readonly fieldPermissionRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const [ roles, roleTargets, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/workspace-flat-route-trigger-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/workspace-flat-route-trigger-map-cache.service.ts index 2105d57252a..32e8f43c3e2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/workspace-flat-route-trigger-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/workspace-flat-route-trigger-map-cache.service.ts @@ -4,9 +4,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { removePropertiesFromRecord } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; import { @@ -14,29 +13,24 @@ import { RouteTriggerEntity, } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatRouteTriggerMaps') -export class WorkspaceFlatRouteTriggerMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatRouteTriggerMaps') +export class WorkspaceFlatRouteTriggerMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(RouteTriggerEntity) private readonly routeTriggerRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const routeTriggers = await this.routeTriggerRepository.find({ where: { workspaceId, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts index 81bfc7e937b..1c279cba619 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts @@ -3,9 +3,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant'; @@ -14,19 +13,16 @@ import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/ro import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type'; -import { WorkspaceFlatMapCache } from 'src/engine/workspace-flat-map-cache/decorators/workspace-flat-map-cache.decorator'; -import { WorkspaceFlatMapCacheService } from 'src/engine/workspace-flat-map-cache/services/workspace-flat-map-cache.service'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-flat-map-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration-v2/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; @Injectable() -@WorkspaceFlatMapCache('flatServerlessFunctionMaps') -export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFlatMapCacheService< +@WorkspaceCache('flatServerlessFunctionMaps') +export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceCacheProvider< FlatEntityMaps > { constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - cacheStorageService: CacheStorageService, @InjectRepository(ServerlessFunctionEntity) private readonly serverlessFunctionRepository: Repository, @InjectRepository(DatabaseEventTriggerEntity) @@ -36,14 +32,12 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFla @InjectRepository(RouteTriggerEntity) private readonly routeTriggerRepository: Repository, ) { - super(cacheStorageService); + super(); } - protected async computeFlatMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { + async computeForCache( + workspaceId: string, + ): Promise> { const [ serverlessFunctions, routeTriggers, diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts index 15487c8ee73..f29a3872dbf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.module.ts @@ -7,12 +7,14 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ TypeOrmModule.forFeature([RoleEntity, RoleTargetsEntity]), TypeOrmModule.forFeature([UserWorkspaceEntity]), WorkspacePermissionsCacheModule, + WorkspaceCacheModule, RoleTargetModule, ], providers: [UserRoleService], diff --git a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts index ef166110f6d..601ee2a076b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/user-role/user-role.service.ts @@ -15,6 +15,7 @@ import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { ADMIN_ROLE } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/roles/admin-role'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -27,6 +28,7 @@ export class UserRoleService { private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly roleTargetService: RoleTargetService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} public async assignRoleToUserWorkspace({ @@ -69,14 +71,12 @@ export class UserRoleService { return; } - const userWorkspaceRoleMap = - await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache( - { - workspaceId, - }, - ); + const { userWorkspaceRoleMap } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'userWorkspaceRoleMap', + ]); - return userWorkspaceRoleMap.data[userWorkspaceId]; + return userWorkspaceRoleMap[userWorkspaceId]; } public async getRolesByUserWorkspaces({ @@ -160,14 +160,12 @@ export class UserRoleService { roleId: string, workspaceId: string, ): Promise { - const userWorkspaceRoleMap = - await this.workspacePermissionsCacheService.getUserWorkspaceRoleMapFromCache( - { - workspaceId, - }, - ); + const { userWorkspaceRoleMap } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'userWorkspaceRoleMap', + ]); - return Object.entries(userWorkspaceRoleMap.data) + return Object.entries(userWorkspaceRoleMap) .filter(([_, roleIdFromMap]) => roleIdFromMap === roleId) .map(([userWorkspaceId]) => userWorkspaceId); } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module.ts index 881c37a1d4f..2e34584bc49 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module.ts @@ -2,15 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; @Module({ - imports: [ - TypeOrmModule.forFeature([WorkspaceEntity, FeatureFlagEntity]), - WorkspaceCacheStorageModule, - ], + imports: [TypeOrmModule.forFeature([FeatureFlagEntity])], providers: [WorkspaceFeatureFlagsMapCacheService], exports: [WorkspaceFeatureFlagsMapCacheService], }) diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts index b1b27bd1368..e38351eaf76 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service.ts @@ -1,76 +1,25 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; import { type FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; -import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - -const FEATURE_FLAG_MAP = 'Feature flag map'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; @Injectable() -export class WorkspaceFeatureFlagsMapCacheService { - logger = new Logger(WorkspaceFeatureFlagsMapCacheService.name); - +@WorkspaceCache('featureFlagsMap') +export class WorkspaceFeatureFlagsMapCacheService extends WorkspaceCacheProvider { constructor( - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, @InjectRepository(FeatureFlagEntity) private readonly featureFlagRepository: Repository, - private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< - string, - FeatureFlagMap - >, - ) {} - - async getWorkspaceFeatureFlagsMap({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - const { data: workspaceFeatureFlagsMap } = - await this.getWorkspaceFeatureFlagsMapAndVersion({ workspaceId }); - - return workspaceFeatureFlagsMap; + ) { + super(); } - async getWorkspaceFeatureFlagsMapAndVersion({ - workspaceId, - }: { - workspaceId: string; - }) { - return this.getFromCacheWithRecomputeService.getFromCacheWithRecompute({ - workspaceId, - getCacheData: () => - this.workspaceCacheStorageService.getFeatureFlagsMap(workspaceId), - getCacheVersion: () => - this.workspaceCacheStorageService.getFeatureFlagsMapVersionFromCache( - workspaceId, - ), - recomputeCache: (params) => this.recomputeFeatureFlagsMapCache(params), - cachedEntityName: FEATURE_FLAG_MAP, - exceptionCode: TwentyORMExceptionCode.FEATURE_FLAG_MAP_VERSION_NOT_FOUND, - }); - } - - async recomputeFeatureFlagsMapCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - const freshFeatureFlagMap = - await this.getFeatureFlagsMapFromDatabase(workspaceId); - - await this.workspaceCacheStorageService.setFeatureFlagsMap( - workspaceId, - freshFeatureFlagMap, - ); - } - - private async getFeatureFlagsMapFromDatabase(workspaceId: string) { + async computeForCache(workspaceId: string): Promise { const workspaceFeatureFlags = await this.featureFlagRepository.find({ where: { workspaceId }, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-api-key-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-api-key-role-map-cache.service.ts new file mode 100644 index 00000000000..3e8376bb228 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-api-key-role-map-cache.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { IsNull, Not, Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('apiKeyRoleMap') +export class WorkspaceApiKeyRoleMapCacheService extends WorkspaceCacheProvider< + Record +> { + constructor( + @InjectRepository(RoleTargetsEntity) + private readonly roleTargetsRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise> { + const roleTargetsMap = await this.roleTargetsRepository.find({ + where: { + workspaceId, + apiKeyId: Not(IsNull()), + }, + }); + + return roleTargetsMap.reduce( + (acc, roleTarget) => { + if (roleTarget.apiKeyId) { + acc[roleTarget.apiKeyId] = roleTarget.roleId; + } + + return acc; + }, + {} as Record, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts deleted file mode 100644 index 162ccd29348..00000000000 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; -import { v4 } from 'uuid'; - -import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; -import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; -import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; -import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; -import { WORKSPACE_CACHE_KEYS } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; - -const TTL_INFINITE = 0; - -@Injectable() -export class WorkspacePermissionsCacheStorageService { - logger = new Logger(WorkspacePermissionsCacheStorageService.name); - - constructor( - @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) - private readonly cacheStorageService: CacheStorageService, - ) {} - - async setRolesPermissionsVersion(workspaceId: string): Promise { - const rolesPermissionsVersion = v4(); - - await this.cacheStorageService.set( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, - rolesPermissionsVersion, - TTL_INFINITE, - ); - - return rolesPermissionsVersion; - } - - async setRolesPermissions( - workspaceId: string, - permissions: ObjectsPermissionsByRoleId, - ): Promise<{ - newRolesPermissionsVersion: string; - }> { - const [, newRolesPermissionsVersion] = await Promise.all([ - this.cacheStorageService.set( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`, - permissions, - TTL_INFINITE, - ), - this.setRolesPermissionsVersion(workspaceId), - ]); - - return { newRolesPermissionsVersion }; - } - - getRolesPermissions( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`, - ); - } - - getRolesPermissionsVersion(workspaceId: string): Promise { - return this.cacheStorageService.get( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`, - ); - } - - async setUserWorkspaceRoleMap( - workspaceId: string, - userWorkspaceRoleMap: UserWorkspaceRoleMap, - ): Promise { - await Promise.all([ - this.cacheStorageService.set( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, - userWorkspaceRoleMap, - TTL_INFINITE, - ), - this.setUserWorkspaceRoleMapVersion(workspaceId), - ]); - } - - async setUserWorkspaceRoleMapVersion(workspaceId: string) { - const userWorkspaceRoleMapVersion = v4(); - - await this.cacheStorageService.set( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, - userWorkspaceRoleMapVersion, - TTL_INFINITE, - ); - - return userWorkspaceRoleMapVersion; - } - - getUserWorkspaceRoleMap( - workspaceId: string, - ): Promise | undefined> { - return this.cacheStorageService.get>( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, - ); - } - - getUserWorkspaceRoleMapVersion( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`, - ); - } - - removeUserWorkspaceRoleMap(workspaceId: string) { - return this.cacheStorageService.del( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`, - ); - } - - async setApiKeyRoleMap( - workspaceId: string, - apiKeyRoleMap: Record, - ): Promise { - await Promise.all([ - this.cacheStorageService.set>( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMap}:${workspaceId}`, - apiKeyRoleMap, - TTL_INFINITE, - ), - this.setApiKeyRoleMapVersion(workspaceId), - ]); - } - - async getApiKeyRoleMap( - workspaceId: string, - ): Promise | undefined> { - return this.cacheStorageService.get>( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMap}:${workspaceId}`, - ); - } - - async getApiKeyRoleMapVersion( - workspaceId: string, - ): Promise { - return this.cacheStorageService.get( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMapVersion}:${workspaceId}`, - ); - } - - async removeApiKeyRoleMap(workspaceId: string): Promise { - await Promise.all([ - this.cacheStorageService.del( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMap}:${workspaceId}`, - ), - this.cacheStorageService.del( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMapVersion}:${workspaceId}`, - ), - ]); - } - - private async setApiKeyRoleMapVersion(workspaceId: string) { - const apiKeyRoleMapVersion = v4(); - - await this.cacheStorageService.set( - `${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMapVersion}:${workspaceId}`, - apiKeyRoleMapVersion, - TTL_INFINITE, - ); - - return apiKeyRoleMapVersion; - } -} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts index 01d510141be..0283467440c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module.ts @@ -1,34 +1,36 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspaceFeatureFlagsMapCacheModule } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.module'; -import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; -import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceApiKeyRoleMapCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-api-key-role-map-cache.service'; +import { WorkspaceRolesPermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-roles-permissions-cache.service'; +import { WorkspaceUserWorkspaceRoleMapCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-user-workspace-role-map-cache.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspacePermissionsCacheService } from './workspace-permissions-cache.service'; @Module({ imports: [ - TypeOrmModule.forFeature([WorkspaceEntity]), TypeOrmModule.forFeature([ ObjectMetadataEntity, RoleEntity, RoleTargetsEntity, ]), - WorkspaceCacheStorageModule, - WorkspaceFeatureFlagsMapCacheModule, + WorkspaceCacheModule, ], providers: [ WorkspacePermissionsCacheService, - WorkspacePermissionsCacheStorageService, + WorkspaceRolesPermissionsCacheService, + WorkspaceUserWorkspaceRoleMapCacheService, + WorkspaceApiKeyRoleMapCacheService, ], exports: [ WorkspacePermissionsCacheService, - WorkspacePermissionsCacheStorageService, + WorkspaceRolesPermissionsCacheService, + WorkspaceUserWorkspaceRoleMapCacheService, + WorkspaceApiKeyRoleMapCacheService, ], }) export class WorkspacePermissionsCacheModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts index 2f0b7133661..be5805a7520 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service.ts @@ -1,170 +1,12 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { Injectable } from '@nestjs/common'; -import { Record } from 'cloudflare/core'; -import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; -import { - type ObjectsPermissions, - type ObjectsPermissionsByRoleId, - type RestrictedFieldsPermissions, -} from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { In, IsNull, Not, Repository } from 'typeorm'; -import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; -import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; -import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; -import { TwentyORMExceptionCode } from 'src/engine/twenty-orm/exceptions/twenty-orm.exception'; -import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service'; - -type CacheResult = { - version: T; - data: U; -}; - -export const USER_WORKSPACE_ROLE_MAP = 'User workspace role map'; -export const ROLES_PERMISSIONS = 'Roles permissions'; -const WORKFLOW_STANDARD_OBJECT_IDS = [ - STANDARD_OBJECT_IDS.workflow, - STANDARD_OBJECT_IDS.workflowRun, - STANDARD_OBJECT_IDS.workflowVersion, -] as const; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class WorkspacePermissionsCacheService { - logger = new Logger(WorkspacePermissionsCacheService.name); - - constructor( - @InjectRepository(ObjectMetadataEntity) - private readonly objectMetadataRepository: Repository, - @InjectRepository(RoleEntity) - private readonly roleRepository: Repository, - @InjectRepository(RoleTargetsEntity) - private readonly roleTargetsRepository: Repository, - private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, - private readonly getRolesPermissionsFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< - string, - ObjectsPermissionsByRoleId - >, - private readonly getUserWorkspaceRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< - string, - UserWorkspaceRoleMap - >, - private readonly getApiKeyRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< - string, - Record - >, - ) {} - - async recomputeRolesPermissionsCache({ - workspaceId, - roleIds, - }: { - workspaceId: string; - roleIds?: string[]; - }): Promise { - let currentRolesPermissions: ObjectsPermissionsByRoleId | undefined; - - if (roleIds) { - currentRolesPermissions = - await this.workspacePermissionsCacheStorageService.getRolesPermissions( - workspaceId, - ); - } - - const recomputedRolesPermissions = - await this.getObjectRecordPermissionsForRoles({ - workspaceId, - roleIds, - }); - - const freshObjectRecordsPermissionsByRoleId = roleIds - ? { ...currentRolesPermissions, ...recomputedRolesPermissions } - : recomputedRolesPermissions; - - await this.workspacePermissionsCacheStorageService.setRolesPermissions( - workspaceId, - freshObjectRecordsPermissionsByRoleId, - ); - } - - async recomputeUserWorkspaceRoleMapCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - try { - const freshUserWorkspaceRoleMap = - await this.getUserWorkspaceRoleMapFromDatabase({ - workspaceId, - }); - - await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMap( - workspaceId, - freshUserWorkspaceRoleMap, - ); - await this.workspacePermissionsCacheStorageService.setUserWorkspaceRoleMapVersion( - workspaceId, - ); - } catch { - // Flush stale userWorkspaceRoleMap - await this.workspacePermissionsCacheStorageService.removeUserWorkspaceRoleMap( - workspaceId, - ); - } - } - - async getRolesPermissionsFromCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { - return this.getRolesPermissionsFromCacheWithRecomputeService.getFromCacheWithRecompute( - { - workspaceId, - getCacheData: () => - this.workspacePermissionsCacheStorageService.getRolesPermissions( - workspaceId, - ), - getCacheVersion: () => - this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion( - workspaceId, - ), - recomputeCache: (params) => this.recomputeRolesPermissionsCache(params), - cachedEntityName: ROLES_PERMISSIONS, - exceptionCode: - TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND, - }, - ); - } - - async getUserWorkspaceRoleMapFromCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { - return this.getUserWorkspaceRoleMapFromCacheWithRecomputeService.getFromCacheWithRecompute( - { - workspaceId, - getCacheData: () => - this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMap( - workspaceId, - ), - getCacheVersion: () => - this.workspacePermissionsCacheStorageService.getUserWorkspaceRoleMapVersion( - workspaceId, - ), - recomputeCache: (params) => - this.recomputeUserWorkspaceRoleMapCache(params), - cachedEntityName: USER_WORKSPACE_ROLE_MAP, - exceptionCode: - TwentyORMExceptionCode.USER_WORKSPACE_ROLE_MAP_VERSION_NOT_FOUND, - }, - ); - } + constructor(private readonly workspaceCacheService: WorkspaceCacheService) {} async getRoleIdFromUserWorkspaceId({ workspaceId, @@ -177,250 +19,11 @@ export class WorkspacePermissionsCacheService { return; } - const { data: userWorkspaceRoleMap } = - await this.getUserWorkspaceRoleMapFromCache({ - workspaceId, - }); + const { userWorkspaceRoleMap } = + await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'userWorkspaceRoleMap', + ]); return userWorkspaceRoleMap[userWorkspaceId]; } - - async getObjectRecordPermissionsForRoles({ - workspaceId, - roleIds, - }: { - workspaceId: string; - roleIds?: string[]; - }): Promise { - let roles: RoleEntity[] = []; - - roles = await this.roleRepository.find({ - where: { - workspaceId, - ...(roleIds ? { id: In(roleIds) } : {}), - }, - relations: ['objectPermissions', 'permissionFlags', 'fieldPermissions'], - }); - - const workspaceObjectMetadataCollection = - await this.getWorkspaceObjectMetadataCollection(workspaceId); - - const permissionsByRoleId: ObjectsPermissionsByRoleId = {}; - - for (const role of roles) { - const objectRecordsPermissions: ObjectsPermissions = {}; - - for (const objectMetadata of workspaceObjectMetadataCollection) { - const { id: objectMetadataId, isSystem, standardId } = objectMetadata; - - let canRead = role.canReadAllObjectRecords; - let canUpdate = role.canUpdateAllObjectRecords; - let canSoftDelete = role.canSoftDeleteAllObjectRecords; - let canDestroy = role.canDestroyAllObjectRecords; - const restrictedFields: RestrictedFieldsPermissions = {}; - - if ( - standardId && - WORKFLOW_STANDARD_OBJECT_IDS.includes( - standardId as (typeof WORKFLOW_STANDARD_OBJECT_IDS)[number], - ) - ) { - const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role); - - canRead = hasWorkflowsPermissions; - canUpdate = hasWorkflowsPermissions; - canSoftDelete = hasWorkflowsPermissions; - canDestroy = hasWorkflowsPermissions; - } else { - const objectRecordPermissionsOverride = role.objectPermissions.find( - (objectPermission) => - objectPermission.objectMetadataId === objectMetadataId, - ); - - const getPermissionValue = ( - overrideValue: boolean | undefined, - defaultValue: boolean, - ) => (isSystem ? true : (overrideValue ?? defaultValue)); - - canRead = getPermissionValue( - objectRecordPermissionsOverride?.canReadObjectRecords, - canRead, - ); - canUpdate = getPermissionValue( - objectRecordPermissionsOverride?.canUpdateObjectRecords, - canUpdate, - ); - canSoftDelete = getPermissionValue( - objectRecordPermissionsOverride?.canSoftDeleteObjectRecords, - canSoftDelete, - ); - canDestroy = getPermissionValue( - objectRecordPermissionsOverride?.canDestroyObjectRecords, - canDestroy, - ); - - const fieldPermissions = role.fieldPermissions.filter( - (fieldPermission) => - fieldPermission.objectMetadataId === objectMetadataId, - ); - - for (const fieldPermission of fieldPermissions) { - const isFieldLabelIdentifier = - fieldPermission.fieldMetadataId === - objectMetadata.labelIdentifierFieldMetadataId; - - if ( - isDefined(fieldPermission.canReadFieldValue) || - isDefined(fieldPermission.canUpdateFieldValue) - ) { - restrictedFields[fieldPermission.fieldMetadataId] = { - canRead: isFieldLabelIdentifier - ? true - : fieldPermission.canReadFieldValue, - canUpdate: fieldPermission.canUpdateFieldValue, - }; - } - } - } - - objectRecordsPermissions[objectMetadataId] = { - canReadObjectRecords: canRead, - canUpdateObjectRecords: canUpdate, - canSoftDeleteObjectRecords: canSoftDelete, - canDestroyObjectRecords: canDestroy, - restrictedFields, - }; - } - - permissionsByRoleId[role.id] = objectRecordsPermissions; - } - - return permissionsByRoleId; - } - - private async getWorkspaceObjectMetadataCollection( - workspaceId: string, - ): Promise { - const workspaceObjectMetadata = await this.objectMetadataRepository.find({ - where: { - workspaceId, - }, - select: [ - 'id', - 'isSystem', - 'standardId', - 'labelIdentifierFieldMetadataId', - ], - }); - - return workspaceObjectMetadata; - } - - private async getUserWorkspaceRoleMapFromDatabase({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - const roleTargetsMap = await this.roleTargetsRepository.find({ - where: { - workspaceId, - userWorkspaceId: Not(IsNull()), - }, - }); - - const initialAccumulator: UserWorkspaceRoleMap = {}; - - return roleTargetsMap.reduce( - (acc, roleTarget) => ({ - ...acc, - [roleTarget.userWorkspaceId as string]: roleTarget.roleId, - }), - initialAccumulator, - ); - } - - private hasWorkflowsPermissions(role: RoleEntity): boolean { - const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings; - const hasWorkflowsPermissionsFromSettingPermissions = isDefined( - role.permissionFlags.find( - (permissionFlag) => - permissionFlag.flag === PermissionFlagType.WORKFLOWS, - ), - ); - - return ( - hasWorkflowsPermissionFromRole || - hasWorkflowsPermissionsFromSettingPermissions - ); - } - - async recomputeApiKeyRoleMapCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise { - try { - const freshApiKeyRoleMap = await this.getApiKeyRoleMapFromDatabase({ - workspaceId, - }); - - await this.workspacePermissionsCacheStorageService.setApiKeyRoleMap( - workspaceId, - freshApiKeyRoleMap, - ); - } catch { - // Flush stale apiKeyRoleMap - await this.workspacePermissionsCacheStorageService.removeApiKeyRoleMap( - workspaceId, - ); - } - } - - async getApiKeyRoleMapFromCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise>> { - return this.getApiKeyRoleMapFromCacheWithRecomputeService.getFromCacheWithRecompute( - { - workspaceId, - getCacheData: () => - this.workspacePermissionsCacheStorageService.getApiKeyRoleMap( - workspaceId, - ), - getCacheVersion: () => - this.workspacePermissionsCacheStorageService.getApiKeyRoleMapVersion( - workspaceId, - ), - recomputeCache: (params) => this.recomputeApiKeyRoleMapCache(params), - cachedEntityName: 'API_KEY_ROLE_MAP', - exceptionCode: - TwentyORMExceptionCode.API_KEY_ROLE_MAP_VERSION_NOT_FOUND, - }, - ); - } - - private async getApiKeyRoleMapFromDatabase({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { - const roleTargetsMap = await this.roleTargetsRepository.find({ - where: { - workspaceId, - apiKeyId: Not(IsNull()), - }, - }); - - return roleTargetsMap.reduce( - (acc, roleTarget) => { - if (roleTarget.apiKeyId) { - acc[roleTarget.apiKeyId] = roleTarget.roleId; - } - - return acc; - }, - {} as Record, - ); - } } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-roles-permissions-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-roles-permissions-cache.service.ts new file mode 100644 index 00000000000..f12f49aace9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-roles-permissions-cache.service.ts @@ -0,0 +1,176 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { STANDARD_OBJECT_IDS } from 'twenty-shared/metadata'; +import { + type ObjectsPermissions, + type ObjectsPermissionsByRoleId, + type RestrictedFieldsPermissions, +} from 'twenty-shared/types'; +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; +import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +const WORKFLOW_STANDARD_OBJECT_IDS = [ + STANDARD_OBJECT_IDS.workflow, + STANDARD_OBJECT_IDS.workflowRun, + STANDARD_OBJECT_IDS.workflowVersion, +] as const; + +@Injectable() +@WorkspaceCache('rolesPermissions') +export class WorkspaceRolesPermissionsCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ObjectMetadataEntity) + private readonly objectMetadataRepository: Repository, + @InjectRepository(RoleEntity) + private readonly roleRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const roles = await this.roleRepository.find({ + where: { + workspaceId, + }, + relations: ['objectPermissions', 'permissionFlags', 'fieldPermissions'], + }); + + const workspaceObjectMetadataCollection = + await this.getWorkspaceObjectMetadataCollection(workspaceId); + + const permissionsByRoleId: ObjectsPermissionsByRoleId = {}; + + for (const role of roles) { + const objectRecordsPermissions: ObjectsPermissions = {}; + + for (const objectMetadata of workspaceObjectMetadataCollection) { + const { id: objectMetadataId, isSystem, standardId } = objectMetadata; + + let canRead = role.canReadAllObjectRecords; + let canUpdate = role.canUpdateAllObjectRecords; + let canSoftDelete = role.canSoftDeleteAllObjectRecords; + let canDestroy = role.canDestroyAllObjectRecords; + const restrictedFields: RestrictedFieldsPermissions = {}; + + if ( + standardId && + WORKFLOW_STANDARD_OBJECT_IDS.includes( + standardId as (typeof WORKFLOW_STANDARD_OBJECT_IDS)[number], + ) + ) { + const hasWorkflowsPermissions = this.hasWorkflowsPermissions(role); + + canRead = hasWorkflowsPermissions; + canUpdate = hasWorkflowsPermissions; + canSoftDelete = hasWorkflowsPermissions; + canDestroy = hasWorkflowsPermissions; + } else { + const objectRecordPermissionsOverride = role.objectPermissions.find( + (objectPermission) => + objectPermission.objectMetadataId === objectMetadataId, + ); + + const getPermissionValue = ( + overrideValue: boolean | undefined, + defaultValue: boolean, + ) => (isSystem ? true : (overrideValue ?? defaultValue)); + + canRead = getPermissionValue( + objectRecordPermissionsOverride?.canReadObjectRecords, + canRead, + ); + canUpdate = getPermissionValue( + objectRecordPermissionsOverride?.canUpdateObjectRecords, + canUpdate, + ); + canSoftDelete = getPermissionValue( + objectRecordPermissionsOverride?.canSoftDeleteObjectRecords, + canSoftDelete, + ); + canDestroy = getPermissionValue( + objectRecordPermissionsOverride?.canDestroyObjectRecords, + canDestroy, + ); + + const fieldPermissions = role.fieldPermissions.filter( + (fieldPermission) => + fieldPermission.objectMetadataId === objectMetadataId, + ); + + for (const fieldPermission of fieldPermissions) { + const isFieldLabelIdentifier = + fieldPermission.fieldMetadataId === + objectMetadata.labelIdentifierFieldMetadataId; + + if ( + isDefined(fieldPermission.canReadFieldValue) || + isDefined(fieldPermission.canUpdateFieldValue) + ) { + restrictedFields[fieldPermission.fieldMetadataId] = { + canRead: isFieldLabelIdentifier + ? true + : fieldPermission.canReadFieldValue, + canUpdate: fieldPermission.canUpdateFieldValue, + }; + } + } + } + + objectRecordsPermissions[objectMetadataId] = { + canReadObjectRecords: canRead, + canUpdateObjectRecords: canUpdate, + canSoftDeleteObjectRecords: canSoftDelete, + canDestroyObjectRecords: canDestroy, + restrictedFields, + }; + } + + permissionsByRoleId[role.id] = objectRecordsPermissions; + } + + return permissionsByRoleId; + } + + private async getWorkspaceObjectMetadataCollection( + workspaceId: string, + ): Promise { + const workspaceObjectMetadata = await this.objectMetadataRepository.find({ + where: { + workspaceId, + }, + select: [ + 'id', + 'isSystem', + 'standardId', + 'labelIdentifierFieldMetadataId', + ], + }); + + return workspaceObjectMetadata; + } + + private hasWorkflowsPermissions(role: RoleEntity): boolean { + const hasWorkflowsPermissionFromRole = role.canUpdateAllSettings; + const hasWorkflowsPermissionsFromSettingPermissions = isDefined( + role.permissionFlags.find( + (permissionFlag) => + permissionFlag.flag === PermissionFlagType.WORKFLOWS, + ), + ); + + return ( + hasWorkflowsPermissionFromRole || + hasWorkflowsPermissionsFromSettingPermissions + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-user-workspace-role-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-user-workspace-role-map-cache.service.ts new file mode 100644 index 00000000000..93a24fd7756 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-permissions-cache/workspace-user-workspace-role-map-cache.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined } from 'twenty-shared/utils'; +import { IsNull, Not, Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity'; +import { UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('userWorkspaceRoleMap') +export class WorkspaceUserWorkspaceRoleMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(RoleTargetsEntity) + private readonly roleTargetsRepository: Repository, + ) { + super(); + } + + async computeForCache(workspaceId: string): Promise { + const roleTargetsMap = await this.roleTargetsRepository.find({ + where: { + workspaceId, + userWorkspaceId: Not(IsNull()), + }, + }); + + return roleTargetsMap.reduce((acc, roleTarget) => { + if (isDefined(roleTarget.userWorkspaceId)) { + acc[roleTarget.userWorkspaceId] = roleTarget.roleId; + } + + return acc; + }, {} as UserWorkspaceRoleMap); + } +} diff --git a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts index 993dc201a6f..e1bf939dfe6 100644 --- a/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts +++ b/packages/twenty-server/src/engine/twenty-orm/factories/workspace-datasource.factory.ts @@ -1,6 +1,8 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import crypto from 'crypto'; + import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { EntitySchema, Repository } from 'typeorm'; @@ -12,12 +14,6 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; -import { WorkspacePermissionsCacheStorageService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache-storage.service'; -import { - ROLES_PERMISSIONS, - WorkspacePermissionsCacheService, -} from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceDataSource } from 'src/engine/twenty-orm/datasource/workspace.datasource'; import { TwentyORMException, @@ -26,15 +22,10 @@ import { import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.storage'; import { type CacheKey } from 'src/engine/twenty-orm/storage/types/cache-key.type'; -import { GetDataFromCacheWithRecomputeService } from 'src/engine/workspace-cache-storage/services/get-data-from-cache-with-recompute.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; -type CacheResult = { - version: T; - data: U; -}; - const TWENTY_MINUTES_IN_MS = 120_000; @Injectable() @@ -48,16 +39,10 @@ export class WorkspaceDatasourceFactory { private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly entitySchemaFactory: EntitySchemaFactory, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, - private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService, - private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, @InjectRepository(WorkspaceEntity) private readonly workspaceRepository: Repository, private readonly workspaceEventEmitter: WorkspaceEventEmitter, - private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService< - string, - ObjectsPermissionsByRoleId - >, ) {} private async safelyDestroyDataSource( @@ -88,17 +73,23 @@ export class WorkspaceDatasourceFactory { const dataSourceMetadataVersion = await this.getWorkspaceMetadataVersionFromCacheOrFromDB(workspaceId); - const { data: cachedFeatureFlagMap, version: cachedFeatureFlagMapVersion } = - await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMapAndVersion( - { workspaceId }, - ); - const { - data: cachedRolesPermissions, - version: cachedRolesPermissionsVersion, - } = await this.getRolesPermissionsFromCache({ - workspaceId, - }); + featureFlagsMap: cachedFeatureFlagMap, + rolesPermissions: cachedRolesPermissions, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'featureFlagsMap', + 'rolesPermissions', + ]); + + const cachedFeatureFlagMapVersion = crypto + .createHash('sha256') + .update(JSON.stringify(cachedFeatureFlagMap)) + .digest('hex'); + + const cachedRolesPermissionsVersion = crypto + .createHash('sha256') + .update(JSON.stringify(cachedRolesPermissions)) + .digest('hex'); const cacheKey: CacheKey = `${workspaceId}-${dataSourceMetadataVersion}`; @@ -251,30 +242,6 @@ export class WorkspaceDatasourceFactory { return workspaceDataSource; } - private async getRolesPermissionsFromCache({ - workspaceId, - }: { - workspaceId: string; - }): Promise> { - return this.getFromCacheWithRecomputeService.getFromCacheWithRecompute({ - workspaceId, - getCacheData: () => - this.workspacePermissionsCacheStorageService.getRolesPermissions( - workspaceId, - ), - getCacheVersion: () => - this.workspacePermissionsCacheStorageService.getRolesPermissionsVersion( - workspaceId, - ), - recomputeCache: () => - this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - }), - cachedEntityName: ROLES_PERMISSIONS, - exceptionCode: TwentyORMExceptionCode.ROLES_PERMISSIONS_VERSION_NOT_FOUND, - }); - } - private updateWorkspaceDataSourceIfNeeded({ workspaceDataSource, currentVersion, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts index beb3297d038..80054cb08fa 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.module.ts @@ -14,6 +14,7 @@ import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-sche import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; @Global() @@ -28,6 +29,7 @@ import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/ FeatureFlagModule, TwentyConfigModule, WorkspaceEventEmitterModule, + WorkspaceCacheModule, ], providers: [ GlobalWorkspaceDataSourceService, diff --git a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts index cd3cc85cd49..0ab82a1a9bc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts +++ b/packages/twenty-server/src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager.ts @@ -12,8 +12,6 @@ import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/typ 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 { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util'; -import { WorkspaceFeatureFlagsMapCacheService } from 'src/engine/metadata-modules/workspace-feature-flags-map-cache/workspace-feature-flags-map-cache.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { EntitySchemaFactory } from 'src/engine/twenty-orm/factories/entity-schema.factory'; import { GlobalWorkspaceDataSourceService } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-datasource.service'; import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository'; @@ -22,7 +20,7 @@ import { withWorkspaceContext, } from 'src/engine/twenty-orm/storage/orm-workspace-context.storage'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; -import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; @Injectable() @@ -30,10 +28,8 @@ export class GlobalWorkspaceOrmManager { constructor( private readonly globalWorkspaceDataSourceService: GlobalWorkspaceDataSourceService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, - private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly entitySchemaFactory: EntitySchemaFactory, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async getRepository( @@ -90,31 +86,24 @@ export class GlobalWorkspaceOrmManager { ): Promise { const workspaceId = authContext.workspace.id; - const { flatObjectMetadataMaps, flatFieldMetadataMaps, flatIndexMaps } = - await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( - { - workspaceId, - flatMapsKeys: [ - 'flatObjectMetadataMaps', - 'flatFieldMetadataMaps', - 'flatIndexMaps', - ], - }, - ); + const { + flatObjectMetadataMaps, + flatFieldMetadataMaps, + flatIndexMaps, + featureFlagsMap, + rolesPermissions: permissionsPerRoleId, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatObjectMetadataMaps', + 'flatFieldMetadataMaps', + 'flatIndexMaps', + 'featureFlagsMap', + 'rolesPermissions', + 'userWorkspaceRoleMap', + ]); const { idByNameSingular: objectIdByNameSingular } = buildObjectIdByNameMaps(flatObjectMetadataMaps); - const { data: featureFlagsMap } = - await this.workspaceFeatureFlagsMapCacheService.getWorkspaceFeatureFlagsMapAndVersion( - { workspaceId }, - ); - - const { data: permissionsPerRoleId } = - await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({ - workspaceId, - }); - const entitySchemas = this.buildEntitySchemas( workspaceId, flatObjectMetadataMaps, diff --git a/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts b/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts index e56e9b0b7ea..e99fea5bf3b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts +++ b/packages/twenty-server/src/engine/twenty-orm/storage/promise-memoizer.storage.ts @@ -96,6 +96,12 @@ export class PromiseMemoizer { await this.clearKey(cacheKey, onDelete); } } + + for (const cacheKey of [...this.pending.keys()]) { + if (cacheKey.startsWith(cacheKeyPrefix)) { + this.pending.delete(cacheKey); + } + } } async clearAll(onDelete?: (value: T) => Promise | void): Promise { diff --git a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts index 92c29eb2553..d84bc185d92 100644 --- a/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts +++ b/packages/twenty-server/src/engine/twenty-orm/twenty-orm.module.ts @@ -17,6 +17,7 @@ import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/s import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module'; @@ -37,6 +38,7 @@ import { PgPoolSharedModule } from './pg-shared-pool/pg-shared-pool.module'; FeatureFlagModule, TwentyConfigModule, PgPoolSharedModule, + WorkspaceCacheModule, ], providers: [ ...entitySchemaFactories, diff --git a/packages/twenty-server/src/engine/workspace-cache/decorators/workspace-cache.decorator.ts b/packages/twenty-server/src/engine/workspace-cache/decorators/workspace-cache.decorator.ts index 5928bdcac0d..04aaa7dea21 100644 --- a/packages/twenty-server/src/engine/workspace-cache/decorators/workspace-cache.decorator.ts +++ b/packages/twenty-server/src/engine/workspace-cache/decorators/workspace-cache.decorator.ts @@ -1,6 +1,8 @@ import { SetMetadata } from '@nestjs/common'; +import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type'; + export const WORKSPACE_CACHE_KEY = 'WORKSPACE_CACHE_KEY'; -export const WorkspaceCache = (workspaceCacheKey: string) => - SetMetadata(WORKSPACE_CACHE_KEY, workspaceCacheKey); +export const WorkspaceCache = (workspaceCacheKeyName: WorkspaceCacheKeyName) => + SetMetadata(WORKSPACE_CACHE_KEY, workspaceCacheKeyName); diff --git a/packages/twenty-server/src/engine/workspace-cache/interfaces/workspace-cache-provider.service.ts b/packages/twenty-server/src/engine/workspace-cache/interfaces/workspace-cache-provider.service.ts new file mode 100644 index 00000000000..99884c1eced --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/interfaces/workspace-cache-provider.service.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@nestjs/common'; + +import { + type WorkspaceCacheDataMap, + type WorkspaceCacheKeyName, +} from 'src/engine/workspace-cache/types/workspace-cache-key.type'; + +type WorkspaceCacheDataType = WorkspaceCacheDataMap[WorkspaceCacheKeyName]; + +@Injectable() +export abstract class WorkspaceCacheProvider< + T extends WorkspaceCacheDataType = WorkspaceCacheDataType, +> { + abstract computeForCache(workspaceId: string): Promise; +} diff --git a/packages/twenty-server/src/engine/workspace-cache/services/__tests__/workspace-cache.service.spec.ts b/packages/twenty-server/src/engine/workspace-cache/services/__tests__/workspace-cache.service.spec.ts new file mode 100644 index 00000000000..c3216ab8fba --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/services/__tests__/workspace-cache.service.spec.ts @@ -0,0 +1,338 @@ +import { DiscoveryService, Reflector } from '@nestjs/core'; +import { Test, type TestingModule } from '@nestjs/testing'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { type CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { WORKSPACE_CACHE_KEY } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; + +const WORKSPACE_ID = 'test-workspace-id'; + +class MockFeatureFlagsCacheProvider extends WorkspaceCacheProvider<{ + testData: string; +}> { + async computeForCache(_workspaceId: string) { + return { testData: 'computed-value' }; + } +} + +class MockRolesPermissionsCacheProvider extends WorkspaceCacheProvider<{ + testData: string; +}> { + async computeForCache(_workspaceId: string) { + return { testData: 'computed-value' }; + } +} + +describe('WorkspaceCacheService', () => { + let service: WorkspaceCacheService; + let cacheStorageService: jest.Mocked; + let discoveryService: jest.Mocked; + let reflector: jest.Mocked; + let mockProvider: MockFeatureFlagsCacheProvider; + + beforeEach(async () => { + jest.useFakeTimers(); + + mockProvider = new MockFeatureFlagsCacheProvider(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + WorkspaceCacheService, + { + provide: CacheStorageNamespace.EngineWorkspace, + useValue: { + mget: jest.fn(), + mset: jest.fn(), + mdel: jest.fn(), + }, + }, + { + provide: DiscoveryService, + useValue: { + getProviders: jest.fn(), + }, + }, + { + provide: Reflector, + useValue: { + get: jest.fn(), + }, + }, + ], + }).compile(); + + service = module.get(WorkspaceCacheService); + cacheStorageService = module.get(CacheStorageNamespace.EngineWorkspace); + discoveryService = module.get(DiscoveryService); + reflector = module.get(Reflector); + }); + + afterEach(() => { + jest.useRealTimers(); + jest.clearAllMocks(); + }); + + describe('onModuleInit', () => { + it('should register workspace cache providers', async () => { + discoveryService.getProviders.mockReturnValue([ + { instance: mockProvider }, + ] as any); + + reflector.get.mockImplementation((key, target) => { + if ( + key === WORKSPACE_CACHE_KEY && + target === MockFeatureFlagsCacheProvider + ) { + return 'featureFlagsMap'; + } + + return undefined; + }); + + await service.onModuleInit(); + + expect(discoveryService.getProviders).toHaveBeenCalled(); + expect(reflector.get).toHaveBeenCalled(); + }); + + it('should skip non-object instances', async () => { + discoveryService.getProviders.mockReturnValue([ + { instance: null }, + { instance: undefined }, + { instance: 'string-value' }, + ] as any); + + await service.onModuleInit(); + + expect(reflector.get).not.toHaveBeenCalled(); + }); + + it('should skip instances without workspace cache key metadata', async () => { + discoveryService.getProviders.mockReturnValue([ + { instance: mockProvider }, + ] as any); + + reflector.get.mockReturnValue(undefined); + + await service.onModuleInit(); + + expect(reflector.get).toHaveBeenCalled(); + }); + }); + + describe('getOrRecompute', () => { + beforeEach(async () => { + discoveryService.getProviders.mockReturnValue([ + { instance: mockProvider }, + ] as any); + + reflector.get.mockImplementation((key, target) => { + if ( + key === WORKSPACE_CACHE_KEY && + target === MockFeatureFlagsCacheProvider + ) { + return 'featureFlagsMap'; + } + + return undefined; + }); + + await service.onModuleInit(); + }); + + it('should compute and cache data when redis cache is empty', async () => { + cacheStorageService.mget.mockResolvedValue([undefined]); + cacheStorageService.mset.mockResolvedValue(undefined); + + jest.spyOn(mockProvider, 'computeForCache').mockResolvedValue({ + testData: 'fresh-computed-value', + }); + + const result = await service.getOrRecompute(WORKSPACE_ID, [ + 'featureFlagsMap', + ]); + + expect(result).toEqual({ + featureFlagsMap: { testData: 'fresh-computed-value' }, + }); + expect(mockProvider.computeForCache).toHaveBeenCalledWith(WORKSPACE_ID); + expect(cacheStorageService.mset).toHaveBeenCalled(); + }); + + it('should return data from redis when available', async () => { + const cachedData = { FLAG_A: true, FLAG_B: false }; + + cacheStorageService.mget + .mockResolvedValueOnce([undefined]) + .mockResolvedValueOnce([cachedData]); + + const result = await service.getOrRecompute(WORKSPACE_ID, [ + 'featureFlagsMap', + ]); + + expect(result).toEqual({ featureFlagsMap: cachedData }); + }); + + it('should use local cache when within TTL staleness window', async () => { + cacheStorageService.mget.mockResolvedValue([undefined]); + cacheStorageService.mset.mockResolvedValue(undefined); + + jest.spyOn(mockProvider, 'computeForCache').mockResolvedValue({ + testData: 'computed-value', + }); + + await service.getOrRecompute(WORKSPACE_ID, ['featureFlagsMap']); + + jest.advanceTimersByTime(50); + + const result = await service.getOrRecompute(WORKSPACE_ID, [ + 'featureFlagsMap', + ]); + + expect(result).toEqual({ + featureFlagsMap: { testData: 'computed-value' }, + }); + + expect(mockProvider.computeForCache).toHaveBeenCalledTimes(1); + }); + + it('should recheck redis when local cache exceeds TTL staleness window', async () => { + const initialData = { testData: 'initial-value' }; + const updatedData = { testData: 'updated-value' }; + + cacheStorageService.mget.mockResolvedValue([undefined]); + cacheStorageService.mset.mockResolvedValue(undefined); + + jest + .spyOn(mockProvider, 'computeForCache') + .mockResolvedValue(initialData); + + await service.getOrRecompute(WORKSPACE_ID, ['featureFlagsMap']); + + // Advance past the local staleness TTL (100ms) and memoizer TTL (10s) + jest.advanceTimersByTime(15_000); + + cacheStorageService.mget.mockResolvedValue([updatedData]); + + jest + .spyOn(mockProvider, 'computeForCache') + .mockResolvedValue(updatedData); + + await service.getOrRecompute(WORKSPACE_ID, ['featureFlagsMap']); + + // Each getOrRecompute call triggers 2 mget calls (hash check + data fetch) + expect(cacheStorageService.mget).toHaveBeenCalledTimes(4); + }); + }); + + describe('invalidateAndRecompute', () => { + beforeEach(async () => { + discoveryService.getProviders.mockReturnValue([ + { instance: mockProvider }, + ] as any); + + reflector.get.mockImplementation((key, target) => { + if ( + key === WORKSPACE_CACHE_KEY && + target === MockFeatureFlagsCacheProvider + ) { + return 'featureFlagsMap'; + } + + return undefined; + }); + + await service.onModuleInit(); + }); + + it('should delete from redis and local cache, then recompute', async () => { + cacheStorageService.mdel.mockResolvedValue(undefined); + cacheStorageService.mset.mockResolvedValue(undefined); + + jest.spyOn(mockProvider, 'computeForCache').mockResolvedValue({ + testData: 'recomputed-value', + }); + + await service.invalidateAndRecompute(WORKSPACE_ID, ['featureFlagsMap']); + + expect(cacheStorageService.mdel).toHaveBeenCalledWith([ + 'feature-flag:feature-flags-map:test-workspace-id:data', + 'feature-flag:feature-flags-map:test-workspace-id:hash', + ]); + expect(mockProvider.computeForCache).toHaveBeenCalledWith(WORKSPACE_ID); + expect(cacheStorageService.mset).toHaveBeenCalled(); + }); + + it('should invalidate multiple cache keys at once', async () => { + const secondMockProvider = new MockRolesPermissionsCacheProvider(); + + discoveryService.getProviders.mockReturnValue([ + { instance: mockProvider }, + { instance: secondMockProvider }, + ] as any); + + reflector.get.mockImplementation((key, target) => { + if (key === WORKSPACE_CACHE_KEY) { + if (target === MockFeatureFlagsCacheProvider) { + return 'featureFlagsMap'; + } + if (target === MockRolesPermissionsCacheProvider) { + return 'rolesPermissions'; + } + } + + return undefined; + }); + + await service.onModuleInit(); + + cacheStorageService.mdel.mockResolvedValue(undefined); + cacheStorageService.mset.mockResolvedValue(undefined); + + jest.spyOn(mockProvider, 'computeForCache').mockResolvedValue({ + testData: 'recomputed-value', + }); + jest.spyOn(secondMockProvider, 'computeForCache').mockResolvedValue({ + testData: 'recomputed-value', + }); + + await service.invalidateAndRecompute(WORKSPACE_ID, [ + 'featureFlagsMap', + 'rolesPermissions', + ]); + + expect(cacheStorageService.mdel).toHaveBeenCalledWith( + expect.arrayContaining([ + 'feature-flag:feature-flags-map:test-workspace-id:data', + 'feature-flag:feature-flags-map:test-workspace-id:hash', + 'metadata:permissions:roles-permissions:test-workspace-id:data', + 'metadata:permissions:roles-permissions:test-workspace-id:hash', + ]), + ); + }); + }); + + describe('flush', () => { + it('should delete from redis and local cache', async () => { + cacheStorageService.mdel.mockResolvedValue(undefined); + + await service.flush(WORKSPACE_ID, ['featureFlagsMap']); + + expect(cacheStorageService.mdel).toHaveBeenCalledWith([ + 'feature-flag:feature-flags-map:test-workspace-id:data', + 'feature-flag:feature-flags-map:test-workspace-id:hash', + ]); + }); + + it('should handle empty cache keys array', async () => { + cacheStorageService.mdel.mockResolvedValue(undefined); + + await service.flush(WORKSPACE_ID, []); + + expect(cacheStorageService.mdel).toHaveBeenCalledWith([]); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts index f70aa7252d0..0828bd37144 100644 --- a/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts +++ b/packages/twenty-server/src/engine/workspace-cache/services/workspace-cache.service.ts @@ -5,30 +5,39 @@ import crypto from 'crypto'; import { isDefined } from 'twenty-shared/utils'; +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; import { PromiseMemoizer } from 'src/engine/twenty-orm/storage/promise-memoizer.storage'; import { WORKSPACE_CACHE_KEY } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; -import { WorkspaceContextLocalCacheEntry } from 'src/engine/workspace-cache/types/workspace-context-cache-entry.type'; -import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/workspace-cache-provider.service'; +import { + WORKSPACE_CACHE_KEYS_V2, + type WorkspaceCacheDataMap, + type WorkspaceCacheKeyName, + type WorkspaceCacheResult, +} from 'src/engine/workspace-cache/types/workspace-cache-key.type'; +import { type WorkspaceLocalCacheEntry } from 'src/engine/workspace-cache/types/workspace-local-cache-entry.type'; -const LOCAL_STALENESS_TTL_MS = 30_000; +const LOCAL_STALENESS_TTL_MS = 100; const MEMOIZER_TTL_MS = 10_000; +type CacheDataType = WorkspaceCacheDataMap[WorkspaceCacheKeyName]; + @Injectable() export class WorkspaceCacheService implements OnModuleInit { private readonly localCache = new Map< string, - WorkspaceContextLocalCacheEntry + WorkspaceLocalCacheEntry >(); private readonly workspaceCacheProviders = new Map< - string, - WorkspaceCacheProvider + WorkspaceCacheKeyName, + WorkspaceCacheProvider >(); - private readonly memoizer = new PromiseMemoizer>( - MEMOIZER_TTL_MS, - ); + private readonly memoizer = new PromiseMemoizer< + Partial + >(MEMOIZER_TTL_MS); constructor( @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) @@ -47,41 +56,49 @@ export class WorkspaceCacheService implements OnModuleInit { continue; } - const workspaceCacheKey = this.reflector.get( + const workspaceCacheKeyName = this.reflector.get( WORKSPACE_CACHE_KEY, instance.constructor, ); if ( - isDefined(workspaceCacheKey) && + isDefined(workspaceCacheKeyName) && instance instanceof WorkspaceCacheProvider ) { - this.workspaceCacheProviders.set(workspaceCacheKey, instance); + this.workspaceCacheProviders.set(workspaceCacheKeyName, instance); } } } - async getOrRecompute< - T extends Record = Record, - >(workspaceId: string, workspaceCacheKeys: string[]): Promise { + async getOrRecompute( + workspaceId: string, + workspaceCacheKeyNames: K, + ): Promise> { const memoKey = - `${workspaceId}-${[...workspaceCacheKeys].sort().join(',')}` as const; + `${workspaceId}-${[...workspaceCacheKeyNames].sort().join(',')}` as const; const result = await this.memoizer.memoizePromiseAndExecute( memoKey, async () => { - const freshResult: Record = {}; + const freshResult: Partial = {}; const { freshKeys, staleKeys } = this.partitionKeysByTTLStaleness( workspaceId, - workspaceCacheKeys, + workspaceCacheKeyNames, ); - for (const workspaceCacheKey of freshKeys) { - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + for (const workspaceCacheKeyName of freshKeys) { + const localKey = this.buildCacheKey( + workspaceId, + workspaceCacheKeyName, + ); const cached = this.localCache.get(localKey); - freshResult[workspaceCacheKey] = cached?.data; + if (isDefined(cached)) { + Object.assign(freshResult, { + [workspaceCacheKeyName]: cached.data, + }); + } } if (staleKeys.length === 0) { @@ -97,48 +114,84 @@ export class WorkspaceCacheService implements OnModuleInit { }, ); - return result as T; + return result as WorkspaceCacheResult; } - invalidate(workspaceId: string, workspaceCacheKeys?: string[]): void { - if (!isDefined(workspaceCacheKeys) || workspaceCacheKeys.length === 0) { - const allKeys = Array.from(this.localCache.keys()); + async invalidateAndRecompute( + workspaceId: string, + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): Promise { + await this.memoizer.clearKeys(`${workspaceId}-`); - for (const key of allKeys) { - if (key.startsWith(this.getCacheKey(workspaceId, ''))) { - this.localCache.delete(key); - } - } + await this.flush(workspaceId, workspaceCacheKeys); + await this.recomputeCache(workspaceId, workspaceCacheKeys); + } - return; - } + async flush( + workspaceId: string, + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): Promise { + await this.deleteFromRedis(workspaceId, workspaceCacheKeys); + this.deleteFromLocalCache(workspaceId, workspaceCacheKeys); + } - for (const workspaceCacheKey of workspaceCacheKeys) { - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + private deleteFromLocalCache( + workspaceId: string, + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): void { + for (const workspaceCacheKeyName of workspaceCacheKeys) { + const localKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); this.localCache.delete(localKey); } } - private partitionKeysByTTLStaleness( + private async deleteFromRedis( workspaceId: string, - workspaceCacheKeys: string[], - ): { freshKeys: string[]; staleKeys: string[] } { - const freshKeys: string[] = []; - const staleKeys: string[] = []; + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): Promise { + const keysToDelete = workspaceCacheKeys.flatMap((workspaceCacheKeyName) => { + const baseKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); + + return [`${baseKey}:data`, `${baseKey}:hash`]; + }); + + await this.cacheStorage.mdel(keysToDelete); + } + + private async recomputeCache( + workspaceId: string, + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): Promise { + const computed = await this.computeAndStoreInRedis( + workspaceId, + workspaceCacheKeys, + ); + + for (const { workspaceCacheKeyName, data, hash } of computed) { + this.setInLocalCache(workspaceId, workspaceCacheKeyName, data, hash); + } + } + + private partitionKeysByTTLStaleness( + workspaceId: string, + workspaceCacheKeys: readonly K[], + ): { freshKeys: K[]; staleKeys: K[] } { + const freshKeys: K[] = []; + const staleKeys: K[] = []; const now = Date.now(); - for (const workspaceCacheKey of workspaceCacheKeys) { - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + for (const workspaceCacheKeyName of workspaceCacheKeys) { + const localKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); const cached = this.localCache.get(localKey); if ( isDefined(cached) && now - cached.lastCheckedAt < LOCAL_STALENESS_TTL_MS ) { - freshKeys.push(workspaceCacheKey); + freshKeys.push(workspaceCacheKeyName); } else { - staleKeys.push(workspaceCacheKey); + staleKeys.push(workspaceCacheKeyName); } } @@ -147,25 +200,25 @@ export class WorkspaceCacheService implements OnModuleInit { private async resolveStaleKeys( workspaceId: string, - workspaceCacheKeys: string[], - ): Promise> { - const result: Record = {}; + workspaceCacheKeys: WorkspaceCacheKeyName[], + ): Promise> { + const result: Partial = {}; const { validFromLocal, needsRedisCheck } = await this.partitionKeysByLocalStaleness(workspaceId, workspaceCacheKeys); - for (const workspaceCacheKey of validFromLocal) { - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + for (const workspaceCacheKeyName of validFromLocal) { + const localKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); const localEntry = this.localCache.get(localKey); if (!isDefined(localEntry)) { continue; } - result[workspaceCacheKey] = localEntry.data; - this.updateLocalCache( + Object.assign(result, { [workspaceCacheKeyName]: localEntry.data }); + this.setInLocalCache( workspaceId, - workspaceCacheKey, + workspaceCacheKeyName, localEntry.data, localEntry.hash, ); @@ -180,9 +233,9 @@ export class WorkspaceCacheService implements OnModuleInit { cacheKeysToRecomputeFromProviders: needsCompute, } = await this.fetchDataFromRedis(workspaceId, needsRedisCheck); - for (const { workspaceCacheKey, data, hash } of validFromRedis) { - result[workspaceCacheKey] = data; - this.updateLocalCache(workspaceId, workspaceCacheKey, data, hash); + for (const { workspaceCacheKeyName, data, hash } of validFromRedis) { + Object.assign(result, { [workspaceCacheKeyName]: data }); + this.setInLocalCache(workspaceId, workspaceCacheKeyName, data, hash); } if (needsCompute.length === 0) { @@ -194,9 +247,9 @@ export class WorkspaceCacheService implements OnModuleInit { needsCompute, ); - for (const { workspaceCacheKey, data, hash } of computed) { - result[workspaceCacheKey] = data; - this.updateLocalCache(workspaceId, workspaceCacheKey, data, hash); + for (const { workspaceCacheKeyName, data, hash } of computed) { + Object.assign(result, { [workspaceCacheKeyName]: data }); + this.setInLocalCache(workspaceId, workspaceCacheKeyName, data, hash); } return result; @@ -204,24 +257,24 @@ export class WorkspaceCacheService implements OnModuleInit { private async partitionKeysByLocalStaleness( workspaceId: string, - workspaceCacheKeys: string[], + workspaceCacheKeys: WorkspaceCacheKeyName[], ): Promise<{ - validFromLocal: string[]; - needsRedisCheck: string[]; + validFromLocal: WorkspaceCacheKeyName[]; + needsRedisCheck: WorkspaceCacheKeyName[]; }> { - const validFromLocal: string[] = []; - const needsRedisCheck: string[] = []; + const validFromLocal: WorkspaceCacheKeyName[] = []; + const needsRedisCheck: WorkspaceCacheKeyName[] = []; - const hashKeys = workspaceCacheKeys.map((workspaceCacheKey) => { - return `${workspaceId}:${workspaceCacheKey}:hash`; - }); + const hashKeys = workspaceCacheKeys.map( + (workspaceCacheKeyName) => + `${this.buildCacheKey(workspaceId, workspaceCacheKeyName)}:hash`, + ); const redisHashes = await this.cacheStorage.mget(hashKeys); - for (let i = 0; i < workspaceCacheKeys.length; i++) { - const workspaceCacheKey = workspaceCacheKeys[i]; - const redisHash = redisHashes[i]; - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + for (const [index, workspaceCacheKeyName] of workspaceCacheKeys.entries()) { + const redisHash = redisHashes[index]; + const localKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); const localEntry = this.localCache.get(localKey); if ( @@ -229,9 +282,9 @@ export class WorkspaceCacheService implements OnModuleInit { isDefined(redisHash) && localEntry.hash === redisHash ) { - validFromLocal.push(workspaceCacheKey); + validFromLocal.push(workspaceCacheKeyName); } else { - needsRedisCheck.push(workspaceCacheKey); + needsRedisCheck.push(workspaceCacheKeyName); } } @@ -240,38 +293,38 @@ export class WorkspaceCacheService implements OnModuleInit { private async fetchDataFromRedis( workspaceId: string, - workspaceCacheKeys: string[], + workspaceCacheKeys: WorkspaceCacheKeyName[], ): Promise<{ validDataFromRedis: Array<{ - workspaceCacheKey: string; - data: unknown; + workspaceCacheKeyName: WorkspaceCacheKeyName; + data: CacheDataType; hash: string; }>; - cacheKeysToRecomputeFromProviders: string[]; + cacheKeysToRecomputeFromProviders: WorkspaceCacheKeyName[]; }> { const validDataFromRedis: Array<{ - workspaceCacheKey: string; - data: unknown; + workspaceCacheKeyName: WorkspaceCacheKeyName; + data: CacheDataType; hash: string; }> = []; - const cacheKeysToRecomputeFromProviders: string[] = []; + const cacheKeysToRecomputeFromProviders: WorkspaceCacheKeyName[] = []; - const dataKeys = workspaceCacheKeys.map((workspaceCacheKey) => { - return `${this.getCacheKey(workspaceId, workspaceCacheKey)}:data`; - }); + const dataKeys = workspaceCacheKeys.map( + (workspaceCacheKeyName) => + `${this.buildCacheKey(workspaceId, workspaceCacheKeyName)}:data`, + ); - const redisData = await this.cacheStorage.mget(dataKeys); + const redisData = await this.cacheStorage.mget(dataKeys); - for (let i = 0; i < workspaceCacheKeys.length; i++) { - const workspaceCacheKey = workspaceCacheKeys[i]; - const data = redisData[i]; + for (const [index, workspaceCacheKeyName] of workspaceCacheKeys.entries()) { + const data = redisData[index]; if (isDefined(data)) { const hash = this.generateHash(data); - validDataFromRedis.push({ workspaceCacheKey, data, hash }); + validDataFromRedis.push({ workspaceCacheKeyName, data, hash }); } else { - cacheKeysToRecomputeFromProviders.push(workspaceCacheKey); + cacheKeysToRecomputeFromProviders.push(workspaceCacheKeyName); } } @@ -282,13 +335,13 @@ export class WorkspaceCacheService implements OnModuleInit { } private getProviderOrThrow( - workspaceCacheKey: string, - ): WorkspaceCacheProvider { - const provider = this.workspaceCacheProviders.get(workspaceCacheKey); + workspaceCacheKeyName: WorkspaceCacheKeyName, + ): WorkspaceCacheProvider { + const provider = this.workspaceCacheProviders.get(workspaceCacheKeyName); if (!isDefined(provider)) { throw new Error( - `Cache provider with key "${workspaceCacheKey}" not found`, + `Cache provider with key name "${workspaceCacheKeyName}" not found`, ); } @@ -297,53 +350,57 @@ export class WorkspaceCacheService implements OnModuleInit { private async computeAndStoreInRedis( workspaceId: string, - workspaceCacheKeys: string[], + workspaceCacheKeys: WorkspaceCacheKeyName[], ): Promise< - Array<{ workspaceCacheKey: string; data: unknown; hash: string }> + Array<{ + workspaceCacheKeyName: WorkspaceCacheKeyName; + data: CacheDataType; + hash: string; + }> > { const computePromises = workspaceCacheKeys.map( - async (workspaceCacheKey) => { - const provider = this.getProviderOrThrow(workspaceCacheKey); + async (workspaceCacheKeyName) => { + const provider = this.getProviderOrThrow(workspaceCacheKeyName); const data = await provider.computeForCache(workspaceId); - return { workspaceCacheKey, data }; + return { workspaceCacheKeyName, data }; }, ); const computed = await Promise.all(computePromises); - const redisEntries = computed.flatMap(({ workspaceCacheKey, data }) => { + const redisEntries: Array<{ key: string; value: unknown }> = []; + + for (const { workspaceCacheKeyName, data } of computed) { const hash = this.generateHash(data); - return [ - { - key: `${this.getCacheKey(workspaceId, workspaceCacheKey)}:data`, - value: data, - }, - { - key: `${this.getCacheKey(workspaceId, workspaceCacheKey)}:hash`, - value: hash, - }, - ]; - }); + redisEntries.push({ + key: `${this.buildCacheKey(workspaceId, workspaceCacheKeyName)}:data`, + value: data, + }); + redisEntries.push({ + key: `${this.buildCacheKey(workspaceId, workspaceCacheKeyName)}:hash`, + value: hash, + }); + } await this.cacheStorage.mset(redisEntries); - return computed.map(({ workspaceCacheKey, data }) => ({ - workspaceCacheKey, + return computed.map(({ workspaceCacheKeyName, data }) => ({ + workspaceCacheKeyName, data, hash: this.generateHash(data), })); } - private updateLocalCache( + private setInLocalCache( workspaceId: string, - workspaceCacheKey: string, - data: unknown, + workspaceCacheKeyName: WorkspaceCacheKeyName, + data: CacheDataType, hash: string, ): void { - const localKey = this.getCacheKey(workspaceId, workspaceCacheKey); + const localKey = this.buildCacheKey(workspaceId, workspaceCacheKeyName); this.localCache.set(localKey, { data, @@ -352,8 +409,11 @@ export class WorkspaceCacheService implements OnModuleInit { }); } - private getCacheKey(workspaceId: string, workspaceCacheKey: string): string { - return `${workspaceId}:${workspaceCacheKey}`; + private buildCacheKey( + workspaceId: string, + workspaceCacheKeyName: WorkspaceCacheKeyName, + ): string { + return `${WORKSPACE_CACHE_KEYS_V2[workspaceCacheKeyName]}:${workspaceId}`; } private generateHash(data: unknown): string { diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts new file mode 100644 index 00000000000..8b435058a4a --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -0,0 +1,43 @@ +import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; + +import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; +import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; +import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/workspace-permissions-cache/types/user-workspace-role-map.type'; + +export const WORKSPACE_CACHE_KEYS_V2 = { + flatObjectMetadataMaps: 'flat-maps:object-metadata', + flatFieldMetadataMaps: 'flat-maps:field-metadata', + flatIndexMaps: 'flat-maps:index', + flatViewMaps: 'flat-maps:view', + flatViewFieldMaps: 'flat-maps:view-field', + flatViewGroupMaps: 'flat-maps:view-group', + flatViewFilterMaps: 'flat-maps:view-filter', + flatServerlessFunctionMaps: 'flat-maps:serverless-function', + flatCronTriggerMaps: 'flat-maps:cron-trigger', + flatDatabaseEventTriggerMaps: 'flat-maps:database-event-trigger', + flatRouteTriggerMaps: 'flat-maps:route-trigger', + featureFlagsMap: 'feature-flag:feature-flags-map', + rolesPermissions: 'metadata:permissions:roles-permissions', + userWorkspaceRoleMap: 'metadata:permissions:user-workspace-role-map', + apiKeyRoleMap: 'metadata:permissions:api-key-role-map', + flatApplicationMaps: 'flat-maps:flatApplicationMaps', + flatRoleMaps: 'flat-maps:role', + flatRoleTargetMaps: 'flat-maps:role-target', +} as const satisfies Record; + +type AdditionalCacheDataMap = { + featureFlagsMap: Record; + rolesPermissions: ObjectsPermissionsByRoleId; + userWorkspaceRoleMap: UserWorkspaceRoleMap; + apiKeyRoleMap: Record; + flatApplicationMaps: FlatApplicationCacheMaps; +}; + +export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMap; + +export type WorkspaceCacheKeyName = keyof WorkspaceCacheDataMap; + +export type WorkspaceCacheResult = { + [P in K[number]]: WorkspaceCacheDataMap[P]; +}; diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-cache-entry.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-local-cache-entry.type.ts similarity index 51% rename from packages/twenty-server/src/engine/workspace-cache/types/workspace-context-cache-entry.type.ts rename to packages/twenty-server/src/engine/workspace-cache/types/workspace-local-cache-entry.type.ts index e051eafd53f..b8811a85a46 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-context-cache-entry.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-local-cache-entry.type.ts @@ -1,4 +1,4 @@ -export type WorkspaceContextLocalCacheEntry = { +export type WorkspaceLocalCacheEntry = { data: T; hash: string; lastCheckedAt: number; diff --git a/packages/twenty-server/src/engine/workspace-cache/workspace-cache-provider.service.ts b/packages/twenty-server/src/engine/workspace-cache/workspace-cache-provider.service.ts deleted file mode 100644 index 5bb4553abe4..00000000000 --- a/packages/twenty-server/src/engine/workspace-cache/workspace-cache-provider.service.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -@Injectable() -export abstract class WorkspaceCacheProvider { - abstract computeForCache(workspaceId: string): Promise; -} diff --git a/packages/twenty-server/src/engine/workspace-cache/workspace-cache.module.ts b/packages/twenty-server/src/engine/workspace-cache/workspace-cache.module.ts index eb14fb2ebe2..38683acafd8 100644 --- a/packages/twenty-server/src/engine/workspace-cache/workspace-cache.module.ts +++ b/packages/twenty-server/src/engine/workspace-cache/workspace-cache.module.ts @@ -9,4 +9,4 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works providers: [WorkspaceCacheService], exports: [WorkspaceCacheService], }) -export class WorkspaceContextCacheModule {} +export class WorkspaceCacheModule {} diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts index 452d5a1fee9..bf03a9bba4a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service.ts @@ -13,7 +13,7 @@ import { RoleTargetService } from 'src/engine/metadata-modules/role-target/servi import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { RoleService } from 'src/engine/metadata-modules/role/role.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { SEED_APPLE_WORKSPACE_ID, SEED_YCOMBINATOR_WORKSPACE_ID, @@ -37,7 +37,7 @@ export class DevSeederPermissionsService { private readonly objectMetadataRepository: Repository, @InjectRepository(RoleEntity) private readonly roleRepository: Repository, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly fieldPermissionService: FieldPermissionService, private readonly roleTargetService: RoleTargetService, @InjectDataSource() diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts index ab0dffee93d..1b2362f7d03 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/dev-seeder.module.ts @@ -18,6 +18,7 @@ import { RoleModule } from 'src/engine/metadata-modules/role/role.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service'; import { DevSeederDataService } from 'src/engine/workspace-manager/dev-seeder/data/services/dev-seeder-data.service'; @@ -46,6 +47,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp ObjectPermissionModule, WorkspacePermissionsCacheModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, + WorkspaceCacheModule, ], exports: [DevSeederService], providers: [ diff --git a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts index 02e078a030c..4342b5abcb0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/dev-seeder/services/dev-seeder.service.ts @@ -6,7 +6,6 @@ import { DataSource } from 'typeorm'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { TWENTY_STANDARD_APPLICATION } from 'src/engine/core-modules/application/constants/twenty-standard-applications'; -import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules/application/services/workspace-flat-application-map-cache.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -14,6 +13,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { SeededWorkspacesIds } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant'; import { DevSeederPermissionsService } from 'src/engine/workspace-manager/dev-seeder/core/services/dev-seeder-permissions.service'; @@ -39,7 +39,7 @@ export class DevSeederService { private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly devSeederDataService: DevSeederDataService, private readonly applicationService: ApplicationService, - private readonly workspaceFlatApplicationMapCacheService: WorkspaceFlatApplicationMapCacheService, + private readonly workspaceCacheService: WorkspaceCacheService, @InjectDataSource() private readonly coreDataSource: DataSource, ) {} @@ -61,9 +61,9 @@ export class DevSeederService { workspaceId, ); - await this.workspaceFlatApplicationMapCacheService.invalidateCache({ - workspaceId, - }); + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'flatApplicationMaps', + ]); const dataSourceMetadata = await this.dataSourceService.createDataSourceMetadata( diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts index 4fb46218839..3bf42293e6d 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service.ts @@ -12,8 +12,8 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadat import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type'; import { FIND_ALL_CORE_VIEWS_GRAPHQL_OPERATION } from 'src/engine/metadata-modules/view/constants/find-all-core-views-graphql-operation.constant'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkspaceMigrationV2 } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/workspace-migration-v2'; import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service'; @@ -25,8 +25,8 @@ export class WorkspaceMigrationRunnerV2Service { private readonly coreDataSource: DataSource, private readonly workspaceMigrationRunnerActionHandlerRegistry: WorkspaceMigrationRunnerActionHandlerRegistryService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, - private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, + private readonly workspaceCacheService: WorkspaceCacheService, private readonly logger: LoggerService, ) {} @@ -119,19 +119,11 @@ export class WorkspaceMigrationRunnerV2Service { shouldInvalidateRoleMapCache ) { asyncOperations.push( - ...[ - this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({ - workspaceId, - }), - this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({ - workspaceId, - }), - this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache( - { - workspaceId, - }, - ), - ], + this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'rolesPermissions', + 'userWorkspaceRoleMap', + 'apiKeyRoleMap', + ]), ); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts index 4d2382661c5..80afb0db0a6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/workspace-migration-runner-v2.module.ts @@ -8,6 +8,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata import { WorkspaceMetadataVersionModule } from 'src/engine/metadata-modules/workspace-metadata-version/workspace-metadata-version.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceSchemaMigrationRunnerActionHandlersModule } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/workspace-schema-migration-runner-action-handlers.module'; import { WorkspaceMigrationRunnerActionHandlerRegistryService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/registry/workspace-migration-runner-action-handler-registry.service'; import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/services/workspace-migration-runner-v2.service'; @@ -23,6 +24,7 @@ import { WorkspaceMigrationRunnerV2Service } from 'src/engine/workspace-manager/ WorkspaceManyOrAllFlatEntityMapsCacheModule, DiscoveryModule, WorkspaceCacheStorageModule, + WorkspaceCacheModule, ], providers: [ WorkspaceMigrationRunnerV2Service, diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts index 608d742e70a..7906829c9a3 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts @@ -27,12 +27,9 @@ describe('AgentToolGeneratorService Integration', () => { .spyOn(context.roleRepository, 'find') .mockResolvedValue([roleWithFullPermissions]); jest - .spyOn( - context.workspacePermissionsCacheService, - 'getRolesPermissionsFromCache', - ) + .spyOn(context.workspaceCacheService, 'getOrRecompute') .mockResolvedValue({ - data: { + rolesPermissions: { [context.testRoleId]: { [context.testObjectMetadata.id]: { canReadObjectRecords: true, @@ -43,8 +40,7 @@ describe('AgentToolGeneratorService Integration', () => { }, }, }, - version: '1.0', - }); + } as any); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') .mockResolvedValue([ @@ -75,12 +71,9 @@ describe('AgentToolGeneratorService Integration', () => { .spyOn(context.roleRepository, 'find') .mockResolvedValue([context.testRole]); jest - .spyOn( - context.workspacePermissionsCacheService, - 'getRolesPermissionsFromCache', - ) + .spyOn(context.workspaceCacheService, 'getOrRecompute') .mockResolvedValue({ - data: { + rolesPermissions: { [context.testRoleId]: { [context.testObjectMetadata.id]: { canReadObjectRecords: true, @@ -91,8 +84,7 @@ describe('AgentToolGeneratorService Integration', () => { }, }, }, - version: '1.0', - }); + } as any); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') .mockResolvedValue([ @@ -136,12 +128,9 @@ describe('AgentToolGeneratorService Integration', () => { .spyOn(context.roleRepository, 'find') .mockResolvedValue([context.testRole]); jest - .spyOn( - context.workspacePermissionsCacheService, - 'getRolesPermissionsFromCache', - ) + .spyOn(context.workspaceCacheService, 'getOrRecompute') .mockResolvedValue({ - data: { + rolesPermissions: { [context.testRoleId]: { [workflowObject.id]: { canReadObjectRecords: true, @@ -152,8 +141,7 @@ describe('AgentToolGeneratorService Integration', () => { }, }, }, - version: '1.0', - }); + } as any); jest .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') .mockResolvedValue([ diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index 963602edf45..7272d9e1b72 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -23,8 +23,8 @@ import { type ObjectMetadataEntity } from 'src/engine/metadata-modules/object-me import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { MessagingSendMessageService } from 'src/modules/messaging/message-import-manager/services/messaging-send-message.service'; import { WorkflowToolWorkspaceService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service'; @@ -36,7 +36,7 @@ export interface AgentToolTestContext { agentService: AgentService; objectMetadataService: ObjectMetadataService; roleRepository: Repository; - workspacePermissionsCacheService: WorkspacePermissionsCacheService; + workspaceCacheService: WorkspaceCacheService; twentyORMGlobalManager: TwentyORMGlobalManager; testAgent: AgentEntity & { roleId: string | null }; testRole: RoleEntity; @@ -89,9 +89,9 @@ export const createAgentToolTestModule = }, }, { - provide: WorkspacePermissionsCacheService, + provide: WorkspaceCacheService, useValue: { - getRolesPermissionsFromCache: jest.fn(), + getOrRecompute: jest.fn(), }, }, { @@ -205,10 +205,6 @@ export const createAgentToolTestModule = const roleRepository = module.get>( getRepositoryToken(RoleEntity), ); - const workspacePermissionsCacheService = - module.get( - WorkspacePermissionsCacheService, - ); const twentyORMGlobalManager = module.get( TwentyORMGlobalManager, ); @@ -314,13 +310,17 @@ export const createAgentToolTestModule = }, } as any); + const workspaceCacheService = module.get( + WorkspaceCacheService, + ); + return { module, agentToolService, agentService, objectMetadataService, roleRepository, - workspacePermissionsCacheService, + workspaceCacheService, twentyORMGlobalManager, testAgent, testRole,