Standard Agent, Role, Role target (#16499)

# Introduction
In this pullrequest have been migrated to the flat standard entities:
Role, Agent and RoleTargets.

## What happens
- Removed createStandardMorph tool util in favor of dynamic typing of
`createMorphOrRelationStandardField`
- Implemented a command to remove standard agents and their role that
has been removed in https://github.com/twentyhq/twenty/pull/16513 also
added a default role target to data manipulator role to the only
remaining agent
- Implemented an agent deleteMany service handler
This commit is contained in:
Paul Rastoin
2025-12-16 14:17:31 +01:00
committed by GitHub
parent 553efe0cee
commit 75bba5a8ee
66 changed files with 1046 additions and 434 deletions
@@ -251,27 +251,58 @@ export class AgentService {
id: string,
workspaceId: string,
): Promise<FlatAgentWithRoleId> {
const {
flatAgentMaps: existingFlatAgentMaps,
flatRoleTargetByAgentIdMaps,
} = await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
const agentToDelete = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: existingFlatAgentMaps,
const deletedAgents = await this.deleteManyAgents({
ids: [id],
workspaceId,
});
if (!isDefined(agentToDelete)) {
if (deletedAgents.length !== 1) {
throw new AgentException(
`Agent not found`,
'Could not retrieve deleted agent',
AgentExceptionCode.AGENT_NOT_FOUND,
);
}
const roleId = flatRoleTargetByAgentIdMaps[agentToDelete.id]?.roleId;
const [deletedAgent] = deletedAgents;
return deletedAgent;
}
async deleteManyAgents({
ids,
workspaceId,
isSystemBuild = false,
}: {
ids: string[];
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<FlatAgentWithRoleId[]> {
if (ids.length === 0) {
return [];
}
const { flatAgentMaps, flatRoleTargetByAgentIdMaps } =
await this.workspaceCacheService.getOrRecompute(workspaceId, [
'flatAgentMaps',
'flatRoleTargetByAgentIdMaps',
]);
const agentsToDelete = ids
.map((id) =>
findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: flatAgentMaps,
}),
)
.filter(isDefined);
if (agentsToDelete.length === 0) {
return [];
}
const roleTargetsToDelete = agentsToDelete
.map((agent) => flatRoleTargetByAgentIdMaps[agent.id])
.filter(isDefined);
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
@@ -279,26 +310,31 @@ export class AgentService {
allFlatEntityOperationByMetadataName: {
agent: {
flatEntityToCreate: [],
flatEntityToDelete: [agentToDelete],
flatEntityToDelete: agentsToDelete,
flatEntityToUpdate: [],
},
roleTarget: {
flatEntityToCreate: [],
flatEntityToDelete: roleTargetsToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
isSystemBuild,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting agent',
`Multiple validation errors occurred while deleting agent${ids.length > 1 ? 's' : ''}`,
);
}
return {
...agentToDelete,
roleId: roleId ?? null,
};
return agentsToDelete.map((agent) => ({
...agent,
roleId: flatRoleTargetByAgentIdMaps[agent.id]?.roleId ?? null,
}));
}
async searchAgents(
@@ -52,8 +52,11 @@ export const ALL_METADATA_REQUIRED_METADATA_FOR_VALIDATION = {
role: {},
roleTarget: {
role: true,
agent: true,
},
agent: {
role: true,
},
agent: {},
pageLayout: {},
pageLayoutTab: {
pageLayout: true,
@@ -8,6 +8,7 @@ import { Repository } from 'typeorm';
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 { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { getFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/get-flat-entities-by-universal-identifier.util';
import { fromCreateRoleInputToFlatRoleToCreate } from 'src/engine/metadata-modules/flat-role/utils/from-create-role-input-to-flat-role-to-create.util';
import { fromDeleteRoleInputToFlatRoleOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-delete-role-input-to-flat-role-or-throw.util';
import { fromUpdateRoleInputToFlatRoleToUpdateOrThrow } from 'src/engine/metadata-modules/flat-role/utils/from-update-role-input-to-flat-role-to-update-or-throw.util';
@@ -25,7 +26,6 @@ import { fromFlatRoleToRoleDto } from 'src/engine/metadata-modules/role/utils/fr
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
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';
import { getFlatEntityByUniversalIdentifier } from 'src/engine/metadata-modules/flat-entity/utils/get-flat-entities-by-universal-identifier.util';
@Injectable()
export class RoleService {
@@ -214,6 +214,38 @@ export class RoleService {
roleId: string;
workspaceId: string;
}): Promise<RoleDTO> {
const deletedRoles = await this.deleteManyRoles({
ids: [roleId],
workspaceId,
isSystemBuild: false,
});
const [deletedRole] = deletedRoles;
return deletedRole;
}
public async deleteManyRoles({
ids,
workspaceId,
isSystemBuild = false,
}: {
ids: string[];
workspaceId: string;
isSystemBuild?: boolean;
}): Promise<RoleDTO[]> {
if (ids.length === 0) {
return [];
}
const { flatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const workspace = await this.workspaceRepository.findOne({
where: {
id: workspaceId,
@@ -232,58 +264,56 @@ export class RoleService {
);
}
if (defaultRoleId === roleId) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
{
userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`,
},
);
const rolesToDelete = [];
for (const roleId of ids) {
const flatRoleToDelete = fromDeleteRoleInputToFlatRoleOrThrow({
flatRoleMaps,
roleId,
});
if (defaultRoleId === roleId) {
throw new PermissionsException(
PermissionsExceptionMessage.DEFAULT_ROLE_CANNOT_BE_DELETED,
PermissionsExceptionCode.DEFAULT_ROLE_CANNOT_BE_DELETED,
{
userFriendlyMessage: msg`The default role cannot be deleted as it is required for the workspace to function properly.`,
},
);
}
await this.assignDefaultRoleToMembersWithRoleToDelete({
roleId,
workspaceId,
defaultRoleId,
});
rolesToDelete.push(flatRoleToDelete);
}
await this.assignDefaultRoleToMembersWithRoleToDelete({
roleId,
workspaceId,
defaultRoleId,
});
const { flatRoleMaps: existingFlatRoleMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatRoleMaps'],
},
);
const flatRoleToDelete = fromDeleteRoleInputToFlatRoleOrThrow({
flatRoleMaps: existingFlatRoleMaps,
roleId,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
role: {
flatEntityToCreate: [],
flatEntityToDelete: [flatRoleToDelete],
flatEntityToDelete: rolesToDelete,
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
isSystemBuild,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderExceptionV2(
validateAndBuildResult,
'Multiple validation errors occurred while deleting role',
`Multiple validation errors occurred while deleting role${ids.length > 1 ? 's' : ''}`,
);
}
return fromFlatRoleToRoleDto(flatRoleToDelete);
return rolesToDelete.map(fromFlatRoleToRoleDto);
}
public async createMemberRole({