Migrate role and role target to v2 (#16009)
# Introduction close https://github.com/twentyhq/core-team-issues/issues/1930 close https://github.com/twentyhq/core-team-issues/issues/1929 Migrating role and roleTarget entities to the v2 core engine, allowing v2 caching leverage and allow migrating agent to v2 that needs role target in prior After agent we should be able to pass twenty standard app totally though workspace migration ## Role target assignation Please note that role target have 3 creation entrypoints: - Agent - User workspace - ApiKey Refactored all 3 of them to pass through a new role-target.service.ts that consumes the v2 under the hood. --------- Co-authored-by: Weiko <corentin@twenty.com>
This commit is contained in:
+60
-59
@@ -9,6 +9,8 @@ import {
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { type ModelId } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
|
||||
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
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';
|
||||
|
||||
@@ -19,6 +21,7 @@ describe('AiAgentRoleService', () => {
|
||||
let agentRepository: Repository<AgentEntity>;
|
||||
let roleRepository: Repository<RoleEntity>;
|
||||
let roleTargetsRepository: Repository<RoleTargetsEntity>;
|
||||
let roleTargetService: RoleTargetService;
|
||||
|
||||
const testWorkspaceId = 'test-workspace-id';
|
||||
let testAgent: AgentEntity;
|
||||
@@ -52,6 +55,13 @@ describe('AiAgentRoleService', () => {
|
||||
find: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: RoleTargetService,
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -65,6 +75,7 @@ describe('AiAgentRoleService', () => {
|
||||
roleTargetsRepository = module.get<Repository<RoleTargetsEntity>>(
|
||||
getRepositoryToken(RoleTargetsEntity),
|
||||
);
|
||||
roleTargetService = module.get<RoleTargetService>(RoleTargetService);
|
||||
|
||||
// Setup test data
|
||||
testAgent = {
|
||||
@@ -122,24 +133,12 @@ describe('AiAgentRoleService', () => {
|
||||
describe('assignRoleToAgent', () => {
|
||||
it('should successfully assign a role to an agent', async () => {
|
||||
// Arrange
|
||||
const newRoleTarget = {
|
||||
id: 'new-role-target-id',
|
||||
roleId: testRole.id,
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as RoleTargetsEntity;
|
||||
|
||||
jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent);
|
||||
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole);
|
||||
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'save')
|
||||
.mockResolvedValue(newRoleTarget);
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 0 } as any);
|
||||
.spyOn(roleTargetService, 'create')
|
||||
.mockResolvedValue({} as FlatRoleTarget);
|
||||
|
||||
// Act
|
||||
await service.assignRoleToAgent({
|
||||
@@ -162,38 +161,24 @@ describe('AiAgentRoleService', () => {
|
||||
workspaceId: testWorkspaceId,
|
||||
},
|
||||
});
|
||||
expect(roleTargetsRepository.save).toHaveBeenCalledWith({
|
||||
roleId: testRole.id,
|
||||
agentId: testAgent.id,
|
||||
expect(roleTargetService.create).toHaveBeenCalledWith({
|
||||
createRoleTargetInput: {
|
||||
roleId: testRole.id,
|
||||
targetId: testAgent.id,
|
||||
targetMetadataForeignKey: 'agentId',
|
||||
},
|
||||
workspaceId: testWorkspaceId,
|
||||
});
|
||||
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
id: expect.any(Object), // Not(newRoleTarget.id)
|
||||
});
|
||||
});
|
||||
|
||||
it('should replace existing role when assigning a new role to an agent', async () => {
|
||||
// Arrange
|
||||
const newRoleTarget = {
|
||||
id: 'new-role-target-id',
|
||||
roleId: testRole2.id,
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as RoleTargetsEntity;
|
||||
|
||||
jest.spyOn(agentRepository, 'findOne').mockResolvedValue(testAgent);
|
||||
jest.spyOn(roleRepository, 'findOne').mockResolvedValue(testRole2);
|
||||
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'save')
|
||||
.mockResolvedValue(newRoleTarget);
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 1 } as any);
|
||||
.spyOn(roleTargetService, 'create')
|
||||
.mockResolvedValue({} as FlatRoleTarget);
|
||||
|
||||
// Act
|
||||
await service.assignRoleToAgent({
|
||||
@@ -203,16 +188,14 @@ describe('AiAgentRoleService', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(roleTargetsRepository.save).toHaveBeenCalledWith({
|
||||
roleId: testRole2.id,
|
||||
agentId: testAgent.id,
|
||||
expect(roleTargetService.create).toHaveBeenCalledWith({
|
||||
createRoleTargetInput: {
|
||||
roleId: testRole2.id,
|
||||
targetId: testAgent.id,
|
||||
targetMetadataForeignKey: 'agentId',
|
||||
},
|
||||
workspaceId: testWorkspaceId,
|
||||
});
|
||||
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
id: expect.any(Object), // Not(newRoleTarget.id)
|
||||
});
|
||||
});
|
||||
|
||||
it('should not create duplicate role target when assigning the same role', async () => {
|
||||
@@ -240,8 +223,7 @@ describe('AiAgentRoleService', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(roleTargetsRepository.save).not.toHaveBeenCalled();
|
||||
expect(roleTargetsRepository.delete).not.toHaveBeenCalled();
|
||||
expect(roleTargetService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw AgentException when agent does not exist', async () => {
|
||||
@@ -330,9 +312,19 @@ describe('AiAgentRoleService', () => {
|
||||
describe('removeRoleFromAgent', () => {
|
||||
it('should successfully remove role from agent', async () => {
|
||||
// Arrange
|
||||
const existingRoleTarget = {
|
||||
id: 'existing-role-target-id',
|
||||
roleId: testRole.id,
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
} as RoleTargetsEntity;
|
||||
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 1 } as any);
|
||||
.spyOn(roleTargetsRepository, 'findOne')
|
||||
.mockResolvedValue(existingRoleTarget);
|
||||
jest.spyOn(roleTargetService, 'delete').mockResolvedValue(undefined);
|
||||
|
||||
// Act
|
||||
await service.removeRoleFromAgent({
|
||||
@@ -341,29 +333,38 @@ describe('AiAgentRoleService', () => {
|
||||
});
|
||||
|
||||
// Assert
|
||||
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
|
||||
agentId: testAgent.id,
|
||||
expect(roleTargetsRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
},
|
||||
});
|
||||
expect(roleTargetService.delete).toHaveBeenCalledWith({
|
||||
id: existingRoleTarget.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not throw error when removing role from agent that has no role', async () => {
|
||||
it('should throw error when removing role from agent that has no role', async () => {
|
||||
// Arrange
|
||||
jest
|
||||
.spyOn(roleTargetsRepository, 'delete')
|
||||
.mockResolvedValue({ affected: 0 } as any);
|
||||
jest.spyOn(roleTargetsRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
// Act & Assert - Should not throw
|
||||
// Act & Assert
|
||||
await expect(
|
||||
service.removeRoleFromAgent({
|
||||
workspaceId: testWorkspaceId,
|
||||
agentId: testAgent.id,
|
||||
}),
|
||||
).resolves.not.toThrow();
|
||||
).rejects.toThrow(AgentException);
|
||||
|
||||
expect(roleTargetsRepository.delete).toHaveBeenCalledWith({
|
||||
agentId: testAgent.id,
|
||||
workspaceId: testWorkspaceId,
|
||||
await expect(
|
||||
service.removeRoleFromAgent({
|
||||
workspaceId: testWorkspaceId,
|
||||
agentId: testAgent.id,
|
||||
}),
|
||||
).rejects.toMatchObject({
|
||||
code: AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
message: `Role target not found for agent ${testAgent.id}`,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+2
@@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
|
||||
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
|
||||
@@ -10,6 +11,7 @@ import { AiAgentRoleService } from './ai-agent-role.service';
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AgentEntity, RoleEntity, RoleTargetsEntity]),
|
||||
RoleTargetModule,
|
||||
],
|
||||
providers: [AiAgentRoleService],
|
||||
exports: [AiAgentRoleService],
|
||||
|
||||
+25
-12
@@ -9,6 +9,7 @@ import {
|
||||
AgentExceptionCode,
|
||||
} from 'src/engine/metadata-modules/ai/ai-agent/agent.exception';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.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';
|
||||
|
||||
@@ -21,6 +22,7 @@ export class AiAgentRoleService {
|
||||
private readonly roleRepository: Repository<RoleEntity>,
|
||||
@InjectRepository(RoleTargetsEntity)
|
||||
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
|
||||
private readonly roleTargetService: RoleTargetService,
|
||||
) {}
|
||||
|
||||
public async assignRoleToAgent({
|
||||
@@ -42,17 +44,14 @@ export class AiAgentRoleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRoleTarget = await this.roleTargetsRepository.save({
|
||||
roleId,
|
||||
agentId,
|
||||
await this.roleTargetService.create({
|
||||
createRoleTargetInput: {
|
||||
roleId,
|
||||
targetId: agentId,
|
||||
targetMetadataForeignKey: 'agentId',
|
||||
},
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
await this.roleTargetsRepository.delete({
|
||||
agentId,
|
||||
workspaceId,
|
||||
id: Not(newRoleTarget.id),
|
||||
});
|
||||
}
|
||||
|
||||
public async assignStandardRoleToAgent({
|
||||
@@ -68,7 +67,7 @@ export class AiAgentRoleService {
|
||||
where: { standardId: standardRoleId, workspaceId },
|
||||
});
|
||||
|
||||
if (!role) {
|
||||
if (!isDefined(role)) {
|
||||
throw new AgentException(
|
||||
`Standard role with standard ID ${standardRoleId} not found in workspace`,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
@@ -89,8 +88,22 @@ export class AiAgentRoleService {
|
||||
workspaceId: string;
|
||||
agentId: string;
|
||||
}): Promise<void> {
|
||||
await this.roleTargetsRepository.delete({
|
||||
agentId,
|
||||
const existingRoleTarget = await this.roleTargetsRepository.findOne({
|
||||
where: {
|
||||
agentId,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(existingRoleTarget)) {
|
||||
throw new AgentException(
|
||||
`Role target not found for agent ${agentId}`,
|
||||
AgentExceptionCode.ROLE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
await this.roleTargetService.delete({
|
||||
id: existingRoleTarget.id,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
|
||||
@@ -9,7 +9,6 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
|
||||
import { FileModule } from 'src/engine/core-modules/file/file.module';
|
||||
import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
|
||||
import { AiBillingModule } from 'src/engine/metadata-modules/ai/ai-billing/ai-billing.module';
|
||||
import { AiChatRouterModule } from 'src/engine/metadata-modules/ai/ai-chat-router/ai-chat-router.module';
|
||||
@@ -54,7 +53,6 @@ import { AgentToolGeneratorService } from './services/agent-tool-generator.servi
|
||||
TokenModule,
|
||||
WorkspaceDomainsModule,
|
||||
WorkflowToolsModule,
|
||||
forwardRef(() => UserModule),
|
||||
UserWorkspaceModule,
|
||||
UserRoleModule,
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user