feat: workflow agent node permissions tab (#16092)

This commit is contained in:
Abdul Rahman
2025-11-28 02:57:33 +05:30
committed by GitHub
parent 41a07006ef
commit a343bc1aee
32 changed files with 2191 additions and 420 deletions
@@ -1,8 +1,11 @@
import { Test, type TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.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 { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
@@ -21,8 +24,11 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let twentyORMGlobalManager: jest.Mocked<TwentyORMGlobalManager>;
let serverlessFunctionService: jest.Mocked<ServerlessFunctionService>;
let agentRepository: jest.Mocked<any>;
let roleTargetsRepository: jest.Mocked<any>;
let roleRepository: jest.Mocked<any>;
let objectMetadataRepository: jest.Mocked<any>;
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
beforeEach(async () => {
serverlessFunctionService = {
@@ -38,6 +44,16 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
delete: jest.fn(),
};
roleTargetsRepository = {
findOne: jest.fn(),
count: jest.fn(),
};
roleRepository = {
findOne: jest.fn(),
delete: jest.fn(),
};
objectMetadataRepository = {
findOne: jest.fn(),
};
@@ -46,6 +62,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
getObjectMetadataItemWithFieldsMaps: jest.fn(),
} as unknown as jest.Mocked<WorkflowCommonWorkspaceService>;
aiAgentRoleService = {
deleteAgentOnlyRoleIfUnused: jest.fn(),
} as unknown as jest.Mocked<AiAgentRoleService>;
twentyORMGlobalManager = {
getRepositoryForWorkspace: jest.fn(),
} as unknown as jest.Mocked<TwentyORMGlobalManager>;
@@ -65,6 +85,14 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: getRepositoryToken(AgentEntity),
useValue: agentRepository,
},
{
provide: getRepositoryToken(RoleTargetsEntity),
useValue: roleTargetsRepository,
},
{
provide: getRepositoryToken(RoleEntity),
useValue: roleRepository,
},
{
provide: getRepositoryToken(ObjectMetadataEntity),
useValue: objectMetadataRepository,
@@ -73,6 +101,10 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
provide: WorkflowCommonWorkspaceService,
useValue: workflowCommonWorkspaceService,
},
{
provide: AiAgentRoleService,
useValue: aiAgentRoleService,
},
{
provide: ScopedWorkspaceContextFactory,
useValue: {},
@@ -154,6 +186,50 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
workspaceId: mockWorkspaceId,
});
});
it('should delete attached role when it is agent-only and unassigned elsewhere', async () => {
const step = {
id: 'step-id',
name: 'AI Agent Step',
type: WorkflowActionType.AI_AGENT,
valid: true,
nextStepIds: [],
settings: {
input: {
agentId: 'agent-id',
prompt: '',
},
outputSchema: {},
errorHandlingOptions: {
continueOnFailure: { value: false },
retryOnFailure: { value: false },
},
},
} as unknown as WorkflowAction;
agentRepository.findOne.mockResolvedValue({ id: 'agent-id' });
roleTargetsRepository.findOne.mockResolvedValue({
id: 'role-target-id',
roleId: 'role-id',
});
await service.runWorkflowVersionStepDeletionSideEffects({
step,
workspaceId: mockWorkspaceId,
});
expect(agentRepository.delete).toHaveBeenCalledWith({
id: 'agent-id',
workspaceId: mockWorkspaceId,
});
expect(
aiAgentRoleService.deleteAgentOnlyRoleIfUnused,
).toHaveBeenCalledWith({
roleId: 'role-id',
roleTargetId: 'role-target-id',
workspaceId: mockWorkspaceId,
});
});
});
describe('runStepCreationSideEffectsAndBuildStep', () => {
@@ -9,9 +9,11 @@ import { v4 } from 'uuid';
import { getFlatFieldsFromFlatObjectMetadata } from 'src/engine/api/graphql/workspace-schema-builder/utils/get-flat-fields-for-flat-object-metadata.util';
import { BASE_TYPESCRIPT_PROJECT_INPUT_SCHEMA } from 'src/engine/core-modules/serverless/drivers/constants/base-typescript-project-input-schema';
import { type WorkflowStepPositionInput } from 'src/engine/core-modules/workflow/dtos/update-workflow-step-position-input.dto';
import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.service';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
import {
@@ -53,9 +55,12 @@ export class WorkflowVersionStepOperationsWorkspaceService {
private readonly serverlessFunctionService: ServerlessFunctionService,
@InjectRepository(AgentEntity)
private readonly agentRepository: Repository<AgentEntity>,
@InjectRepository(RoleTargetsEntity)
private readonly roleTargetsRepository: Repository<RoleTargetsEntity>,
@InjectRepository(ObjectMetadataEntity)
private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>,
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
private readonly aiAgentRoleService: AiAgentRoleService,
) {}
async runWorkflowVersionStepDeletionSideEffects({
@@ -90,7 +95,22 @@ export class WorkflowVersionStepOperationsWorkspaceService {
});
if (isDefined(agent)) {
const roleTarget = await this.roleTargetsRepository.findOne({
where: {
agentId: agent.id,
workspaceId,
},
});
await this.agentRepository.delete({ id: agent.id, workspaceId });
if (isDefined(roleTarget?.roleId) && isDefined(roleTarget?.id)) {
await this.aiAgentRoleService.deleteAgentOnlyRoleIfUnused({
roleId: roleTarget.roleId,
roleTargetId: roleTarget.id,
workspaceId,
});
}
}
break;
}
@@ -2,8 +2,11 @@ import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { AiAgentRoleModule } from 'src/engine/metadata-modules/ai/ai-agent-role/ai-agent-role.module';
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.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 { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { WorkflowSchemaModule } from 'src/modules/workflow/workflow-builder/workflow-schema/workflow-schema.module';
@@ -19,7 +22,13 @@ import { WorkflowVersionStepWorkspaceService } from 'src/modules/workflow/workfl
WorkflowSchemaModule,
ServerlessFunctionModule,
WorkflowCommonModule,
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity, AgentEntity]),
AiAgentRoleModule,
NestjsQueryTypeOrmModule.forFeature([
ObjectMetadataEntity,
AgentEntity,
RoleTargetsEntity,
RoleEntity,
]),
],
providers: [
WorkflowVersionStepWorkspaceService,