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:
+7
-10
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-19
@@ -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) {
|
||||
|
||||
+16
@@ -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],
|
||||
|
||||
+34
-22
@@ -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 () => {
|
||||
|
||||
+16
-16
@@ -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],
|
||||
|
||||
+10
-9
@@ -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();
|
||||
|
||||
+6
-6
@@ -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;
|
||||
|
||||
|
||||
+9
-15
@@ -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,
|
||||
|
||||
+9
-15
@@ -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: {
|
||||
|
||||
+2
-1
@@ -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],
|
||||
})
|
||||
|
||||
+15
-80
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-15
@@ -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({
|
||||
|
||||
+9
-15
@@ -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,
|
||||
|
||||
+9
-16
@@ -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 },
|
||||
|
||||
+7
-15
@@ -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,
|
||||
|
||||
+7
-15
@@ -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,
|
||||
|
||||
+7
-15
@@ -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,
|
||||
|
||||
+7
-15
@@ -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,
|
||||
|
||||
+7
-15
@@ -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 },
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+5
-7
@@ -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;
|
||||
|
||||
+10
-13
@@ -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 () => {
|
||||
|
||||
+15
-25
@@ -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([
|
||||
{
|
||||
|
||||
+10
-11
@@ -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: {
|
||||
|
||||
+2
@@ -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],
|
||||
|
||||
+5
-8
@@ -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: {
|
||||
|
||||
+2
@@ -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],
|
||||
|
||||
+5
-8
@@ -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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -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],
|
||||
|
||||
+6
-6
@@ -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,
|
||||
|
||||
+9
-15
@@ -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,
|
||||
|
||||
+9
-15
@@ -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,
|
||||
|
||||
+9
-15
@@ -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);
|
||||
}
|
||||
|
||||
+1
-6
@@ -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],
|
||||
})
|
||||
|
||||
+8
-59
@@ -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 },
|
||||
});
|
||||
|
||||
+42
@@ -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>,
|
||||
);
|
||||
}
|
||||
}
|
||||
-167
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
-9
@@ -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 {}
|
||||
|
||||
+7
-404
@@ -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>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+176
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
+39
@@ -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);
|
||||
}
|
||||
}
|
||||
+20
-53
@@ -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,
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+16
-27
@@ -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,
|
||||
|
||||
+4
-2
@@ -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);
|
||||
|
||||
+15
@@ -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>;
|
||||
}
|
||||
+338
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
+176
-116
@@ -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
-1
@@ -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 {}
|
||||
|
||||
+2
-2
@@ -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: [
|
||||
|
||||
+5
-5
@@ -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(
|
||||
|
||||
+7
-15
@@ -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',
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+9
-21
@@ -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([
|
||||
|
||||
+9
-9
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user