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
This commit is contained in:
Weiko
2025-12-01 17:08:21 +01:00
committed by GitHub
parent 4e0545ebc5
commit 1eb2e44058
77 changed files with 1306 additions and 1863 deletions
@@ -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(
@@ -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,
@@ -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({
@@ -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: [
@@ -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<RoleEntity> = {
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<RoleEntity> = {
...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>(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,
});
});
});
});
@@ -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<ApiKeyEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly roleTargetService: RoleTargetService,
) {}
@@ -62,12 +62,12 @@ export class ApiKeyRoleService {
apiKeyId: string,
workspaceId: string,
): Promise<string> {
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(
@@ -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,
@@ -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],
@@ -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<ApplicationEntity>,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceFlatApplicationMapCacheService: WorkspaceFlatApplicationMapCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
@InjectRepository(WorkspaceEntity)
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
@@ -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<ApplicationEntity> {
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,
);
}
}
@@ -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<AllMetadataName>,
) // TODO prastoin introduce SyncableMetadata notion
export class WorkspaceFlatApplicationMapCacheService extends WorkspaceFlatMapCacheService<FlatApplicationCacheMaps> {
@WorkspaceCache('flatApplicationMaps')
export class WorkspaceFlatApplicationMapCacheService extends WorkspaceCacheProvider<FlatApplicationCacheMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatApplicationCacheMaps> {
async computeForCache(
workspaceId: string,
): Promise<FlatApplicationCacheMaps> {
const applicationEntities = await this.applicationRepository.find({
where: {
workspaceId,
@@ -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,
@@ -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) {
@@ -28,6 +28,22 @@ export class CacheStorageService {
return this.cache.del(this.getKey(key));
}
async mdel(keys: string[]): Promise<void> {
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<T = unknown>(keys: string[]): Promise<(T | undefined)[]> {
if (this.isRedisCache()) {
const prefixedKeys = keys.map((k) => this.getKey(k));
@@ -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],
@@ -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 () => {
@@ -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<FeatureFlagEntity>,
private readonly workspaceFeatureFlagsMapCacheService: WorkspaceFeatureFlagsMapCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
) {}
public async isFeatureEnabled(
@@ -36,10 +36,10 @@ export class FeatureFlagService {
public async getWorkspaceFeatureFlags(
workspaceId: string,
): Promise<FeatureFlagDTO[]> {
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<FeatureFlagMap> {
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;
}
@@ -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],
@@ -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();
@@ -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<ToolSet> {
const tools: ToolSet = {};
const { data: rolesPermissions } =
await this.workspacePermissionsCacheService.getRolesPermissionsFromCache({
workspaceId,
});
const { rolesPermissions } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'rolesPermissions',
]);
let objectPermissions;
@@ -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<FlatCronTrigger>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(CronTriggerEntity)
private readonly cronTriggerRepository: Repository<CronTriggerEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatCronTrigger>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatCronTrigger>> {
const cronTriggers = await this.cronTriggerRepository.find({
where: {
workspaceId,
@@ -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<FlatDatabaseEventTrigger>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(DatabaseEventTriggerEntity)
private readonly databaseEventTriggerRepository: Repository<DatabaseEventTriggerEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatDatabaseEventTrigger>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatDatabaseEventTrigger>> {
const databaseEventTriggers =
await this.databaseEventTriggerRepository.find({
where: {
@@ -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],
})
@@ -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<AllFlatEntityMaps[K[number]]>;
flatMapKey: K[number];
}) => Promise<void>;
}): Promise<void> {
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<Pick<AllFlatEntityMaps, T[number]>> {
let pickedFlatEntityMaps = {} as Pick<AllFlatEntityMaps, T[number]>;
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<void> {
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<void> {
await this.executeActionForManyOrAllFlatEntity({
action: async ({ service }) => await service.flushCache({ workspaceId }),
flatMapsKeys,
});
await this.workspaceCacheService.flush(
workspaceId,
flatMapsKeys ?? ALL_FLAT_ENTITY_MAPS_PROPERTIES,
);
}
}
@@ -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<FlatFieldMetadata>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(FieldMetadataEntity)
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
@InjectRepository(ViewFieldEntity)
@@ -39,14 +35,12 @@ export class WorkspaceFlatFieldMetadataMapCacheService extends WorkspaceFlatMapC
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatFieldMetadata>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatFieldMetadata>> {
const [fieldMetadatas, viewFields, viewFilters, viewGroups, views] =
await Promise.all([
this.fieldMetadataRepository.find({
@@ -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<FlatIndexMetadata>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(IndexMetadataEntity)
private readonly indexMetadataRepository: Repository<IndexMetadataEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatIndexMetadata>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatIndexMetadata>> {
const indexes = await this.indexMetadataRepository.find({
where: {
workspaceId,
@@ -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<FlatObjectMetadata>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(FieldMetadataEntity)
@@ -37,14 +32,12 @@ export class WorkspaceFlatObjectMetadataMapCacheService extends WorkspaceFlatMap
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatObjectMetadata>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatObjectMetadata>> {
const [objectMetadatas, fields, indexMetadatas, views] = await Promise.all([
this.objectMetadataRepository.find({
where: { workspaceId },
@@ -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<FlatRoleTargetMaps> {
@WorkspaceCache('flatRoleTargetMaps')
export class WorkspaceFlatRoleTargetMapCacheService extends WorkspaceCacheProvider<FlatRoleTargetMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatRoleTargetMaps> {
async computeForCache(workspaceId: string): Promise<FlatRoleTargetMaps> {
const roleTargets = await this.roleTargetsRepository.find({
where: {
workspaceId,
@@ -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<FlatViewFieldMaps> {
@WorkspaceCache('flatViewFieldMaps')
export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvider<FlatViewFieldMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewFieldEntity)
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewFieldMaps> {
async computeForCache(workspaceId: string): Promise<FlatViewFieldMaps> {
const existingViewFields = await this.viewFieldRepository.find({
where: {
workspaceId,
@@ -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<FlatViewFilterMaps> {
@WorkspaceCache('flatViewFilterMaps')
export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvider<FlatViewFilterMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewFilterEntity)
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewFilterMaps> {
async computeForCache(workspaceId: string): Promise<FlatViewFilterMaps> {
const viewFilters = await this.viewFilterRepository.find({
where: {
workspaceId,
@@ -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<FlatViewGroupMaps> {
@WorkspaceCache('flatViewGroupMaps')
export class WorkspaceFlatViewGroupMapCacheService extends WorkspaceCacheProvider<FlatViewGroupMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewGroupMaps> {
async computeForCache(workspaceId: string): Promise<FlatViewGroupMaps> {
const existingViewGroups = await this.viewGroupRepository.find({
where: {
workspaceId,
@@ -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<FlatViewMaps> {
@WorkspaceCache('flatViewMaps')
export class WorkspaceFlatViewMapCacheService extends WorkspaceCacheProvider<FlatViewMaps> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ViewEntity)
private readonly viewRepository: Repository<ViewEntity>,
@InjectRepository(ViewFieldEntity)
@@ -33,14 +29,10 @@ export class WorkspaceFlatViewMapCacheService extends WorkspaceFlatMapCacheServi
@InjectRepository(ViewGroupEntity)
private readonly viewGroupRepository: Repository<ViewGroupEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatViewMaps> {
async computeForCache(workspaceId: string): Promise<FlatViewMaps> {
const [views, viewFields, viewFilters, viewGroups] = await Promise.all([
this.viewRepository.find({
where: { workspaceId },
@@ -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,
@@ -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<ObjectMetadataEnt
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
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<ObjectMetadataEnt
}
if (isDefined(updateObjectInput.update.labelIdentifierFieldMetadataId)) {
await this.workspacePermissionsCacheService.recomputeRolesPermissionsCache(
{
workspaceId,
},
);
await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
'rolesPermissions',
]);
}
return updatedFlatObjectMetadata;
@@ -14,7 +14,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';
describe('ObjectPermissionService', () => {
let service: ObjectPermissionService;
@@ -22,7 +22,7 @@ describe('ObjectPermissionService', () => {
Repository<ObjectPermissionEntity>
>;
let roleRepository: jest.Mocked<Repository<RoleEntity>>;
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
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 () => {
@@ -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<Repository<RoleEntity>>;
let fieldMetadataRepository: jest.Mocked<Repository<FieldMetadataEntity>>;
let workspacePermissionsCacheService: jest.Mocked<WorkspacePermissionsCacheService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let workspaceManyOrAllFlatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
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([
{
@@ -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<FieldMetadataEntity>,
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionsRepository: Repository<FieldPermissionEntity>,
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: {
@@ -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],
@@ -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<RoleEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
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: {
@@ -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],
@@ -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<RoleEntity>,
@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',
]);
}
}
@@ -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: {},
},
],
@@ -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],
@@ -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<RoleEntity>,
@@ -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] ?? {};
@@ -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,
@@ -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<FlatRole>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity)
@@ -39,14 +35,12 @@ export class WorkspaceFlatRoleMapCacheService extends WorkspaceFlatMapCacheServi
@InjectRepository(FieldPermissionEntity)
private readonly fieldPermissionRepository: Repository<FieldPermissionEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatRole>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatRole>> {
const [
roles,
roleTargets,
@@ -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<FlatRouteTrigger>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatRouteTrigger>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatRouteTrigger>> {
const routeTriggers = await this.routeTriggerRepository.find({
where: {
workspaceId,
@@ -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<FlatServerlessFunction>
> {
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
cacheStorageService: CacheStorageService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
@InjectRepository(DatabaseEventTriggerEntity)
@@ -36,14 +32,12 @@ export class WorkspaceFlatServerlessFunctionMapCacheService extends WorkspaceFla
@InjectRepository(RouteTriggerEntity)
private readonly routeTriggerRepository: Repository<RouteTriggerEntity>,
) {
super(cacheStorageService);
super();
}
protected async computeFlatMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FlatEntityMaps<FlatServerlessFunction>> {
async computeForCache(
workspaceId: string,
): Promise<FlatEntityMaps<FlatServerlessFunction>> {
const [
serverlessFunctions,
routeTriggers,
@@ -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],
@@ -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<string[]> {
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);
}
@@ -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],
})
@@ -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<FeatureFlagMap> {
constructor(
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
@InjectRepository(FeatureFlagEntity)
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
private readonly getFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
string,
FeatureFlagMap
>,
) {}
async getWorkspaceFeatureFlagsMap({
workspaceId,
}: {
workspaceId: string;
}): Promise<FeatureFlagMap> {
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<void> {
const freshFeatureFlagMap =
await this.getFeatureFlagsMapFromDatabase(workspaceId);
await this.workspaceCacheStorageService.setFeatureFlagsMap(
workspaceId,
freshFeatureFlagMap,
);
}
private async getFeatureFlagsMapFromDatabase(workspaceId: string) {
async computeForCache(workspaceId: string): Promise<FeatureFlagMap> {
const workspaceFeatureFlags = await this.featureFlagRepository.find({
where: { workspaceId },
});
@@ -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<string, string>
> {
constructor(
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<Record<string, string>> {
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<string, string>,
);
}
}
@@ -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<string> {
const rolesPermissionsVersion = v4();
await this.cacheStorageService.set<string>(
`${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<ObjectsPermissionsByRoleId>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`,
permissions,
TTL_INFINITE,
),
this.setRolesPermissionsVersion(workspaceId),
]);
return { newRolesPermissionsVersion };
}
getRolesPermissions(
workspaceId: string,
): Promise<ObjectsPermissionsByRoleId | undefined> {
return this.cacheStorageService.get<ObjectsPermissionsByRoleId>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissions}:${workspaceId}`,
);
}
getRolesPermissionsVersion(workspaceId: string): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsRolesPermissionsVersion}:${workspaceId}`,
);
}
async setUserWorkspaceRoleMap(
workspaceId: string,
userWorkspaceRoleMap: UserWorkspaceRoleMap,
): Promise<void> {
await Promise.all([
this.cacheStorageService.set<UserWorkspaceRoleMap>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
userWorkspaceRoleMap,
TTL_INFINITE,
),
this.setUserWorkspaceRoleMapVersion(workspaceId),
]);
}
async setUserWorkspaceRoleMapVersion(workspaceId: string) {
const userWorkspaceRoleMapVersion = v4();
await this.cacheStorageService.set<string>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
userWorkspaceRoleMapVersion,
TTL_INFINITE,
);
return userWorkspaceRoleMapVersion;
}
getUserWorkspaceRoleMap(
workspaceId: string,
): Promise<Record<string, string> | undefined> {
return this.cacheStorageService.get<Record<string, string>>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
);
}
getUserWorkspaceRoleMapVersion(
workspaceId: string,
): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMapVersion}:${workspaceId}`,
);
}
removeUserWorkspaceRoleMap(workspaceId: string) {
return this.cacheStorageService.del(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsUserWorkspaceRoleMap}:${workspaceId}`,
);
}
async setApiKeyRoleMap(
workspaceId: string,
apiKeyRoleMap: Record<string, string>,
): Promise<void> {
await Promise.all([
this.cacheStorageService.set<Record<string, string>>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMap}:${workspaceId}`,
apiKeyRoleMap,
TTL_INFINITE,
),
this.setApiKeyRoleMapVersion(workspaceId),
]);
}
async getApiKeyRoleMap(
workspaceId: string,
): Promise<Record<string, string> | undefined> {
return this.cacheStorageService.get<Record<string, string>>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMap}:${workspaceId}`,
);
}
async getApiKeyRoleMapVersion(
workspaceId: string,
): Promise<string | undefined> {
return this.cacheStorageService.get<string>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMapVersion}:${workspaceId}`,
);
}
async removeApiKeyRoleMap(workspaceId: string): Promise<void> {
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<string>(
`${WORKSPACE_CACHE_KEYS.MetadataPermissionsApiKeyRoleMapVersion}:${workspaceId}`,
apiKeyRoleMapVersion,
TTL_INFINITE,
);
return apiKeyRoleMapVersion;
}
}
@@ -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 {}
@@ -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<T, U> = {
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<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
private readonly workspacePermissionsCacheStorageService: WorkspacePermissionsCacheStorageService,
private readonly getRolesPermissionsFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
string,
ObjectsPermissionsByRoleId
>,
private readonly getUserWorkspaceRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
string,
UserWorkspaceRoleMap
>,
private readonly getApiKeyRoleMapFromCacheWithRecomputeService: GetDataFromCacheWithRecomputeService<
string,
Record<string, string>
>,
) {}
async recomputeRolesPermissionsCache({
workspaceId,
roleIds,
}: {
workspaceId: string;
roleIds?: string[];
}): Promise<void> {
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<void> {
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<CacheResult<string, ObjectsPermissionsByRoleId>> {
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<CacheResult<string, UserWorkspaceRoleMap>> {
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<ObjectsPermissionsByRoleId> {
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<ObjectMetadataEntity[]> {
const workspaceObjectMetadata = await this.objectMetadataRepository.find({
where: {
workspaceId,
},
select: [
'id',
'isSystem',
'standardId',
'labelIdentifierFieldMetadataId',
],
});
return workspaceObjectMetadata;
}
private async getUserWorkspaceRoleMapFromDatabase({
workspaceId,
}: {
workspaceId: string;
}): Promise<UserWorkspaceRoleMap> {
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<void> {
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<CacheResult<string, Record<string, string>>> {
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<Record<string, string>> {
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<string, string>,
);
}
}
@@ -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<ObjectsPermissionsByRoleId> {
constructor(
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
) {
super();
}
async computeForCache(
workspaceId: string,
): Promise<ObjectsPermissionsByRoleId> {
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<ObjectMetadataEntity[]> {
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
);
}
}
@@ -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<UserWorkspaceRoleMap> {
constructor(
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
) {
super();
}
async computeForCache(workspaceId: string): Promise<UserWorkspaceRoleMap> {
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);
}
}
@@ -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<T, U> = {
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<WorkspaceEntity>,
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<CacheResult<string, ObjectsPermissionsByRoleId>> {
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<T>({
workspaceDataSource,
currentVersion,
@@ -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,
@@ -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<T extends ObjectLiteral>(
@@ -90,31 +86,24 @@ export class GlobalWorkspaceOrmManager {
): Promise<ORMWorkspaceContext> {
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,
@@ -96,6 +96,12 @@ export class PromiseMemoizer<T> {
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> | void): Promise<void> {
@@ -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,
@@ -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);
@@ -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<T>;
}
@@ -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<CacheStorageService>;
let discoveryService: jest.Mocked<DiscoveryService>;
let reflector: jest.Mocked<Reflector>;
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>(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([]);
});
});
});
@@ -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<unknown>
WorkspaceLocalCacheEntry<CacheDataType>
>();
private readonly workspaceCacheProviders = new Map<
string,
WorkspaceCacheProvider<unknown>
WorkspaceCacheKeyName,
WorkspaceCacheProvider<CacheDataType>
>();
private readonly memoizer = new PromiseMemoizer<Record<string, unknown>>(
MEMOIZER_TTL_MS,
);
private readonly memoizer = new PromiseMemoizer<
Partial<WorkspaceCacheDataMap>
>(MEMOIZER_TTL_MS);
constructor(
@InjectCacheStorage(CacheStorageNamespace.EngineWorkspace)
@@ -47,41 +56,49 @@ export class WorkspaceCacheService implements OnModuleInit {
continue;
}
const workspaceCacheKey = this.reflector.get<string>(
const workspaceCacheKeyName = this.reflector.get<WorkspaceCacheKeyName>(
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<string, unknown> = Record<string, unknown>,
>(workspaceId: string, workspaceCacheKeys: string[]): Promise<T> {
async getOrRecompute<const K extends WorkspaceCacheKeyName[]>(
workspaceId: string,
workspaceCacheKeyNames: K,
): Promise<WorkspaceCacheResult<K>> {
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<string, unknown> = {};
const freshResult: Partial<WorkspaceCacheDataMap> = {};
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<K>;
}
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<void> {
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<void> {
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<void> {
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<void> {
const computed = await this.computeAndStoreInRedis(
workspaceId,
workspaceCacheKeys,
);
for (const { workspaceCacheKeyName, data, hash } of computed) {
this.setInLocalCache(workspaceId, workspaceCacheKeyName, data, hash);
}
}
private partitionKeysByTTLStaleness<K extends WorkspaceCacheKeyName>(
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<Record<string, unknown>> {
const result: Record<string, unknown> = {};
workspaceCacheKeys: WorkspaceCacheKeyName[],
): Promise<Partial<WorkspaceCacheDataMap>> {
const result: Partial<WorkspaceCacheDataMap> = {};
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<string>(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<unknown>(dataKeys);
const redisData = await this.cacheStorage.mget<CacheDataType>(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<unknown> {
const provider = this.workspaceCacheProviders.get(workspaceCacheKey);
workspaceCacheKeyName: WorkspaceCacheKeyName,
): WorkspaceCacheProvider<CacheDataType> {
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 {
@@ -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<WorkspaceCacheKeyName, string>;
type AdditionalCacheDataMap = {
featureFlagsMap: Record<FeatureFlagKey, boolean>;
rolesPermissions: ObjectsPermissionsByRoleId;
userWorkspaceRoleMap: UserWorkspaceRoleMap;
apiKeyRoleMap: Record<string, string>;
flatApplicationMaps: FlatApplicationCacheMaps;
};
export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMap;
export type WorkspaceCacheKeyName = keyof WorkspaceCacheDataMap;
export type WorkspaceCacheResult<K extends WorkspaceCacheKeyName[]> = {
[P in K[number]]: WorkspaceCacheDataMap[P];
};
@@ -1,4 +1,4 @@
export type WorkspaceContextLocalCacheEntry<T> = {
export type WorkspaceLocalCacheEntry<T> = {
data: T;
hash: string;
lastCheckedAt: number;
@@ -1,6 +0,0 @@
import { Injectable } from '@nestjs/common';
@Injectable()
export abstract class WorkspaceCacheProvider<T> {
abstract computeForCache(workspaceId: string): Promise<T>;
}
@@ -9,4 +9,4 @@ import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/works
providers: [WorkspaceCacheService],
exports: [WorkspaceCacheService],
})
export class WorkspaceContextCacheModule {}
export class WorkspaceCacheModule {}
@@ -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<ObjectMetadataEntity>,
@InjectRepository(RoleEntity)
private readonly roleRepository: Repository<RoleEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly workspaceCacheService: WorkspaceCacheService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly roleTargetService: RoleTargetService,
@InjectDataSource()
@@ -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: [
@@ -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(
@@ -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',
]),
);
}
@@ -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,
@@ -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([
@@ -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<RoleEntity>;
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<Repository<RoleEntity>>(
getRepositoryToken(RoleEntity),
);
const workspacePermissionsCacheService =
module.get<WorkspacePermissionsCacheService>(
WorkspacePermissionsCacheService,
);
const twentyORMGlobalManager = module.get<TwentyORMGlobalManager>(
TwentyORMGlobalManager,
);
@@ -314,13 +310,17 @@ export const createAgentToolTestModule =
},
} as any);
const workspaceCacheService = module.get<WorkspaceCacheService>(
WorkspaceCacheService,
);
return {
module,
agentToolService,
agentService,
objectMetadataService,
roleRepository,
workspacePermissionsCacheService,
workspaceCacheService,
twentyORMGlobalManager,
testAgent,
testRole,