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:
Paul Rastoin
2025-11-28 18:06:11 +01:00
committed by GitHub
parent 23a7611aac
commit ea3c5d2d45
131 changed files with 6082 additions and 1319 deletions
@@ -9,6 +9,7 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { FieldPermissionService } from 'src/engine/metadata-modules/object-permission/field-permission/field-permission.service';
import { ObjectPermissionService } from 'src/engine/metadata-modules/object-permission/object-permission.service';
import { RoleTargetService } from 'src/engine/metadata-modules/role-target/services/role-target.service';
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';
@@ -38,6 +39,7 @@ export class DevSeederPermissionsService {
private readonly roleRepository: Repository<RoleEntity>,
private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService,
private readonly fieldPermissionService: FieldPermissionService,
private readonly roleTargetService: RoleTargetService,
@InjectDataSource()
private readonly coreDataSource: DataSource,
) {}
@@ -63,28 +65,15 @@ export class DevSeederPermissionsService {
}
try {
await this.coreDataSource
.createQueryBuilder()
.insert()
.into('core.roleTargets', ['roleId', 'apiKeyId', 'workspaceId'])
.orIgnore()
.values([
{
roleId: adminRole.id,
apiKeyId: API_KEY_DATA_SEED_IDS.ID_1,
workspaceId: workspaceId,
},
])
.execute();
await this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
await this.roleTargetService.create({
createRoleTargetInput: {
roleId: adminRole.id,
targetId: API_KEY_DATA_SEED_IDS.ID_1,
targetMetadataForeignKey: 'apiKeyId',
applicationId: twentyStandardApplication.id,
},
workspaceId,
});
await this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
},
);
} catch (error) {
this.logger.error(
`Could not assign role to test API key: ${error.message}`,
@@ -116,8 +105,10 @@ export class DevSeederPermissionsService {
roleId: guestRole.id,
});
const limitedRole =
await this.createLimitedRoleForSeedWorkspace(workspaceId);
const limitedRole = await this.createLimitedRoleForSeedWorkspace({
workspaceId,
applicationId: twentyStandardApplication.id,
});
await this.userRoleService.assignRoleToUserWorkspace({
workspaceId,
@@ -164,8 +155,15 @@ export class DevSeederPermissionsService {
}
}
private async createLimitedRoleForSeedWorkspace(workspaceId: string) {
private async createLimitedRoleForSeedWorkspace({
applicationId,
workspaceId,
}: {
workspaceId: string;
applicationId: string;
}) {
const customRole = await this.roleService.createRole({
applicationId,
workspaceId,
input: {
label: 'Object-restricted',
@@ -13,6 +13,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
import { RoleTargetModule } from 'src/engine/metadata-modules/role-target/role-target.module';
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';
@@ -34,6 +35,7 @@ import { WorkspaceSyncMetadataModule } from 'src/engine/workspace-manager/worksp
TypeORMModule,
DataSourceModule,
RoleModule,
RoleTargetModule,
UserRoleModule,
ApiKeyModule,
ApplicationModule,
@@ -28,6 +28,8 @@ export const fromWorkspaceMigrationBuilderExceptionToMetadataValidationResponseE
invalidRouteTrigger: 0,
invalidFieldMetadata: 0,
invalidViewGroup: 0,
invalidRole: 0,
invalidRoleTarget: 0,
totalErrors: 0,
},
errors: translatedReport,
@@ -0,0 +1,38 @@
import {
type CallHandler,
type ExecutionContext,
Injectable,
type NestInterceptor,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { type Observable, catchError } from 'rxjs';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { WorkspaceMigrationBuilderExceptionV2 } from 'src/engine/workspace-manager/workspace-migration-v2/exceptions/workspace-migration-builder-exception-v2';
import { workspaceMigrationBuilderExceptionV2Formatter } from 'src/engine/workspace-manager/workspace-migration-v2/interceptors/workspace-migration-builder-exception-v2-formatter';
@Injectable()
export class WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor
implements NestInterceptor
{
constructor(private readonly i18nService: I18nService) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
const gqlContext = GqlExecutionContext.create(context);
const ctx = gqlContext.getContext();
const locale = ctx.req?.locale ?? SOURCE_LOCALE;
const i18n = this.i18nService.getI18nInstance(locale);
return next.handle().pipe(
catchError((error) => {
if (error instanceof WorkspaceMigrationBuilderExceptionV2) {
workspaceMigrationBuilderExceptionV2Formatter(error, i18n);
}
throw error;
}),
);
}
}
@@ -19,6 +19,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service';
import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -39,7 +41,9 @@ export class WorkspaceMigrationBuildOrchestratorService {
private readonly workspaceMigrationV2DatabaseEventTriggerActionsBuilderService: WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
private readonly workspaceMigrationV2CronTriggerActionsBuilderService: WorkspaceMigrationV2CronTriggerActionsBuilderService,
private readonly workspaceMigrationV2RouteTriggerActionsBuilderService: WorkspaceMigrationV2RouteTriggerActionsBuilderService,
private readonly workspaceMigrationV2RoleTargetActionsBuilderService: WorkspaceMigrationV2RoleTargetActionsBuilderService,
private readonly workspaceMigrationV2FieldActionsBuilderService: WorkspaceMigrationV2FieldActionsBuilderService,
private readonly workspaceMigrationV2RoleActionsBuilderService: WorkspaceMigrationV2RoleActionsBuilderService,
) {}
private setupOptimisticCache({
@@ -125,6 +129,8 @@ export class WorkspaceMigrationBuildOrchestratorService {
flatFieldMetadataMaps,
flatViewFilterMaps,
flatViewGroupMaps,
flatRoleMaps,
flatRoleTargetMaps,
} = fromToAllFlatEntityMaps;
if (isDefined(flatObjectMetadataMaps)) {
@@ -513,6 +519,65 @@ export class WorkspaceMigrationBuildOrchestratorService {
}
}
if (isDefined(flatRoleMaps)) {
const { from: fromFlatRoleMaps, to: toFlatRoleMaps } = flatRoleMaps;
const roleResult =
this.workspaceMigrationV2RoleActionsBuilderService.validateAndBuild({
from: fromFlatRoleMaps,
to: toFlatRoleMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: undefined,
workspaceId,
});
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
roleResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (roleResult.status === 'fail') {
orchestratorFailureReport.role.push(...roleResult.errors);
} else {
orchestratorActionsReport.role = roleResult.actions;
}
}
if (isDefined(flatRoleTargetMaps)) {
const { from: fromFlatRoleTargetMaps, to: toFlatRoleTargetMaps } =
flatRoleTargetMaps;
const roleTargetResult =
this.workspaceMigrationV2RoleTargetActionsBuilderService.validateAndBuild(
{
from: fromFlatRoleTargetMaps,
to: toFlatRoleTargetMaps,
buildOptions,
dependencyOptimisticFlatEntityMaps: {
flatRoleMaps: optimisticAllFlatEntityMaps.flatRoleMaps,
},
workspaceId,
},
);
this.mergeFlatEntityMapsAndRelatedFlatEntityMapsInAllFlatEntityMapsThroughMutation(
{
allFlatEntityMaps: optimisticAllFlatEntityMaps,
flatEntityMapsAndRelatedFlatEntityMaps:
roleTargetResult.optimisticFlatEntityMapsAndRelatedFlatEntityMaps,
},
);
if (roleTargetResult.status === 'fail') {
orchestratorFailureReport.roleTarget.push(...roleTargetResult.errors);
} else {
orchestratorActionsReport.roleTarget = roleTargetResult.actions;
}
}
const allErrors = Object.values(orchestratorFailureReport);
if (allErrors.some((report) => report.length > 0)) {
@@ -585,9 +650,21 @@ export class WorkspaceMigrationBuildOrchestratorService {
///
// Route triggers
...orchestratorActionsReport.routeTrigger.deleted,
...orchestratorActionsReport.routeTrigger.created,
...orchestratorActionsReport.routeTrigger.updated,
...aggregatedOrchestratorActionsReport.routeTrigger.deleted,
...aggregatedOrchestratorActionsReport.routeTrigger.created,
...aggregatedOrchestratorActionsReport.routeTrigger.updated,
///
// Roles
...aggregatedOrchestratorActionsReport.role.deleted,
...aggregatedOrchestratorActionsReport.role.created,
...aggregatedOrchestratorActionsReport.role.updated,
///
// Role targets
...aggregatedOrchestratorActionsReport.roleTarget.deleted,
...aggregatedOrchestratorActionsReport.roleTarget.created,
...aggregatedOrchestratorActionsReport.roleTarget.updated,
///
],
workspaceId,
@@ -0,0 +1,26 @@
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
export type CreateRoleTargetAction = {
type: 'create_role_target';
roleTarget: FlatRoleTarget;
};
export type UpdateRoleTargetAction = {
type: 'update_role_target';
roleTargetId: string;
updates: FlatEntityPropertiesUpdates<'roleTarget'>;
};
export type DeleteRoleTargetAction = {
type: 'delete_role_target';
roleTargetId: string;
};
export type WorkspaceMigrationRoleTargetActionV2 =
| CreateRoleTargetAction
| UpdateRoleTargetAction
| DeleteRoleTargetAction;
export type WorkspaceMigrationRoleTargetActionTypeV2 =
WorkspaceMigrationRoleTargetActionV2['type'];
@@ -0,0 +1,105 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service';
@Injectable()
export class WorkspaceMigrationV2RoleTargetActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.roleTarget
> {
constructor(
private readonly flatRoleTargetValidatorService: FlatRoleTargetValidatorService,
) {
super(ALL_METADATA_NAME.roleTarget);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'created'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleTargetToValidate } = args;
return {
status: 'success',
action: {
type: 'create_role_target',
roleTarget: flatRoleTargetToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'deleted'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleTargetToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_role_target',
roleTargetId: flatRoleTargetToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.roleTarget>,
): FlatEntityValidationReturnType<
typeof ALL_METADATA_NAME.roleTarget,
'updated'
> {
const validationResult =
this.flatRoleTargetValidatorService.validateFlatRoleTargetUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateRoleTargetAction: UpdateRoleTargetAction = {
type: 'update_role_target',
roleTargetId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateRoleTargetAction,
};
}
}
@@ -0,0 +1,26 @@
import { type FlatEntityPropertiesUpdates } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-properties-updates.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
export type CreateRoleAction = {
type: 'create_role';
role: FlatRole;
};
export type UpdateRoleAction = {
type: 'update_role';
roleId: string;
updates: FlatEntityPropertiesUpdates<'role'>;
};
export type DeleteRoleAction = {
type: 'delete_role';
roleId: string;
};
export type WorkspaceMigrationRoleActionV2 =
| CreateRoleAction
| UpdateRoleAction
| DeleteRoleAction;
export type WorkspaceMigrationRoleActionTypeV2 =
WorkspaceMigrationRoleActionV2['type'];
@@ -0,0 +1,96 @@
import { Injectable } from '@nestjs/common';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceEntityMigrationBuilderV2Service } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/services/workspace-entity-migration-builder-v2.service';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { FlatEntityValidationReturnType } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-result.type';
import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service';
@Injectable()
export class WorkspaceMigrationV2RoleActionsBuilderService extends WorkspaceEntityMigrationBuilderV2Service<
typeof ALL_METADATA_NAME.role
> {
constructor(
private readonly flatRoleValidatorService: FlatRoleValidatorService,
) {
super(ALL_METADATA_NAME.role);
}
protected validateFlatEntityCreation(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'created'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleCreation(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleToValidate } = args;
return {
status: 'success',
action: {
type: 'create_role',
role: flatRoleToValidate,
},
};
}
protected validateFlatEntityDeletion(
args: FlatEntityValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'deleted'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleDeletion(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityToValidate: flatRoleToValidate } = args;
return {
status: 'success',
action: {
type: 'delete_role',
roleId: flatRoleToValidate.id,
},
};
}
protected validateFlatEntityUpdate(
args: FlatEntityUpdateValidationArgs<typeof ALL_METADATA_NAME.role>,
): FlatEntityValidationReturnType<typeof ALL_METADATA_NAME.role, 'updated'> {
const validationResult =
this.flatRoleValidatorService.validateFlatRoleUpdate(args);
if (validationResult.errors.length > 0) {
return {
status: 'fail',
...validationResult,
};
}
const { flatEntityId, flatEntityUpdates } = args;
const updateRoleAction: UpdateRoleAction = {
type: 'update_role',
roleId: flatEntityId,
updates: flatEntityUpdates,
};
return {
status: 'success',
action: updateRoleAction,
};
}
}
@@ -0,0 +1,179 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateFlatRoleTargetAssignationAvailability } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-assignation-availability.util';
import { validateFlatRoleTargetTargetsOnlyOneEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-flat-role-target-targets-only-one-entity.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatRoleTargetValidatorService {
validateFlatRoleTargetCreation({
flatEntityToValidate: flatRoleTargetToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
flatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'create_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatRoleTargetToValidate.id,
},
};
const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.id,
flatEntityMaps: optimisticFlatRoleTargetMaps,
});
if (isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role target with this id already exists`,
userFriendlyMessage: msg`Role target with this id already exists`,
});
}
validationResult.errors.push(
...validateFlatRoleTargetTargetsOnlyOneEntity({
flatRoleTarget: flatRoleTargetToValidate,
}),
);
const referencedRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.roleId,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
} else {
validationResult.errors.push(
...validateFlatRoleTargetAssignationAvailability({
flatRole: referencedRole,
flatRoleTarget: flatRoleTargetToValidate,
}),
);
}
return validationResult;
}
validateFlatRoleTargetDeletion({
flatEntityToValidate: flatRoleTargetToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'delete_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatRoleTargetToValidate.id,
},
};
const existingRoleTarget = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: flatRoleTargetToValidate.id,
flatEntityMaps: optimisticFlatRoleTargetMaps,
});
if (!isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND,
message: t`Role target not found`,
userFriendlyMessage: msg`Role target not found`,
});
return validationResult;
}
return validationResult;
}
validateFlatRoleTargetUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleTargetMaps: optimisticFlatRoleTargetMaps,
flatRoleMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.roleTarget
>): FailedFlatEntityValidation<FlatRoleTarget> {
const validationResult: FailedFlatEntityValidation<FlatRoleTarget> = {
type: 'update_role_target',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const existingRoleTarget = optimisticFlatRoleTargetMaps.byId[flatEntityId];
if (!isDefined(existingRoleTarget)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_NOT_FOUND,
message: t`Role target not found`,
userFriendlyMessage: msg`Role target not found`,
});
return validationResult;
}
const updatedFlatRoleTarget = {
...existingRoleTarget,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
validationResult.errors.push(
...validateFlatRoleTargetTargetsOnlyOneEntity({
flatRoleTarget: updatedFlatRoleTarget,
}),
);
const referencedRole = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: updatedFlatRoleTarget.roleId,
flatEntityMaps: flatRoleMaps,
});
if (!isDefined(referencedRole)) {
validationResult.errors.push({
code: RoleTargetExceptionCode.INVALID_ROLE_TARGET_DATA,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateFlatRoleTargetAssignationAvailability({
flatRole: referencedRole,
flatRoleTarget: updatedFlatRoleTarget,
}),
);
return validationResult;
}
}
@@ -0,0 +1,176 @@
import { Injectable } from '@nestjs/common';
import { msg, t } from '@lingui/core/macro';
import { type ALL_METADATA_NAME } from 'twenty-shared/metadata';
import { isDefined } from 'twenty-shared/utils';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { findFlatEntityPropertyUpdate } from 'src/engine/workspace-manager/workspace-migration-v2/utils/find-flat-entity-property-update.util';
import { type FailedFlatEntityValidation } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
import { type FlatEntityUpdateValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-update-validation-args.type';
import { type FlatEntityValidationArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/types/flat-entity-validation-args.type';
import { validateRoleIsEditable } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-is-editable.util';
import { validateRoleLabelUniqueness } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-label-uniqueness.util';
import { validateRoleReadWritePermissionsConsistency } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-read-write-permissions-consistency.util';
import { validateRoleRequiredPropertiesAreDefined } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/utils/validate-role-required-properties-are-defined.util';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class FlatRoleValidatorService {
public validateFlatRoleCreation({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'create_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
label: flatEntityToValidate.label,
},
};
const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateRoleRequiredPropertiesAreDefined({
flatRole: flatEntityToValidate,
}),
);
validationResult.errors.push(
...validateRoleLabelUniqueness({
label: flatEntityToValidate.label,
existingFlatRoles: existingRoles,
}),
);
validationResult.errors.push(
...validateRoleReadWritePermissionsConsistency({
flatRole: flatEntityToValidate,
}),
);
return validationResult;
}
public validateFlatRoleDeletion({
flatEntityToValidate,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'delete_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityToValidate.id,
label: flatEntityToValidate.label,
},
};
const existingRole = optimisticFlatRoleMaps.byId[flatEntityToValidate.id];
if (!isDefined(existingRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateRoleIsEditable({
flatRole: existingRole,
}),
);
return validationResult;
}
public validateFlatRoleUpdate({
flatEntityId,
flatEntityUpdates,
optimisticFlatEntityMapsAndRelatedFlatEntityMaps: {
flatRoleMaps: optimisticFlatRoleMaps,
},
}: FlatEntityUpdateValidationArgs<
typeof ALL_METADATA_NAME.role
>): FailedFlatEntityValidation<FlatRole> {
const validationResult: FailedFlatEntityValidation<FlatRole> = {
type: 'update_role',
errors: [],
flatEntityMinimalInformation: {
id: flatEntityId,
},
};
const fromFlatRole = optimisticFlatRoleMaps.byId[flatEntityId];
if (!isDefined(fromFlatRole)) {
validationResult.errors.push({
code: PermissionsExceptionCode.ROLE_NOT_FOUND,
message: t`Role not found`,
userFriendlyMessage: msg`Role not found`,
});
return validationResult;
}
validationResult.errors.push(
...validateRoleIsEditable({
flatRole: fromFlatRole,
}),
);
const toFlatRole = {
...fromFlatRole,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity({
updates: flatEntityUpdates,
}),
};
validationResult.errors.push(
...validateRoleRequiredPropertiesAreDefined({
flatRole: toFlatRole,
}),
);
const flatRoleLabelUpdate = findFlatEntityPropertyUpdate({
flatEntityUpdates,
property: 'label',
});
if (isDefined(flatRoleLabelUpdate)) {
const existingRoles = Object.values(optimisticFlatRoleMaps.byId).filter(
isDefined,
);
validationResult.errors.push(
...validateRoleLabelUniqueness({
label: flatRoleLabelUpdate.to,
existingFlatRoles: existingRoles,
}),
);
}
validationResult.errors.push(
...validateRoleReadWritePermissionsConsistency({
flatRole: toFlatRole,
}),
);
return validationResult;
}
}
@@ -0,0 +1,48 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'class-validator';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
type ValidateFlatRoleTargetAssignationAvailabilityArgs = {
flatRole: FlatRole;
flatRoleTarget: FlatRoleTarget;
};
export const validateFlatRoleTargetAssignationAvailability = ({
flatRole,
flatRoleTarget,
}: ValidateFlatRoleTargetAssignationAvailabilityArgs): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
const roleLabel = flatRole.label;
if (isDefined(flatRoleTarget.agentId)) {
if (!flatRole.canBeAssignedToAgents) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to agents`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to agents`,
});
}
} else if (isDefined(flatRoleTarget.userWorkspaceId)) {
if (!flatRole.canBeAssignedToUsers) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to users`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to users`,
});
}
} else if (isDefined(flatRoleTarget.apiKeyId)) {
if (!flatRole.canBeAssignedToApiKeys) {
errors.push({
code: RoleTargetExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_ENTITY,
message: t`Role "${roleLabel}" cannot be assigned to API keys`,
userFriendlyMessage: msg`Role "${roleLabel}" cannot be assigned to API keys`,
});
}
}
return errors;
};
@@ -0,0 +1,30 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { type FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
import { RoleTargetExceptionCode } from 'src/engine/metadata-modules/role/exceptions/role-target.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateFlatRoleTargetTargetsOnlyOneEntity = ({
flatRoleTarget,
}: {
flatRoleTarget: FlatRoleTarget;
}) => {
const errors: FlatEntityValidationError[] = [];
const definedIdentifiersCount = [
isDefined(flatRoleTarget.apiKeyId),
isDefined(flatRoleTarget.userWorkspaceId),
isDefined(flatRoleTarget.agentId),
].filter(Boolean).length;
if (definedIdentifiersCount !== 1) {
errors.push({
code: RoleTargetExceptionCode.ROLE_TARGET_MISSING_IDENTIFIER,
message: t`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`,
userFriendlyMessage: msg`Role target must have exactly one of: apiKeyId, userWorkspaceId, or agentId`,
});
}
return errors;
};
@@ -0,0 +1,26 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleIsEditable = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (!flatRole.isEditable) {
errors.push({
code: PermissionsExceptionCode.ROLE_NOT_EDITABLE,
message: PermissionsExceptionMessage.ROLE_NOT_EDITABLE,
userFriendlyMessage: msg`This role cannot be modified because it is a system role. Only custom roles can be edited.`,
});
}
return errors;
};
@@ -0,0 +1,28 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleLabelUniqueness = ({
label,
existingFlatRoles,
}: {
label: string;
existingFlatRoles: FlatRole[];
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (existingFlatRoles.some((role) => role.label === label)) {
errors.push({
code: PermissionsExceptionCode.ROLE_LABEL_ALREADY_EXISTS,
message: PermissionsExceptionMessage.ROLE_LABEL_ALREADY_EXISTS,
userFriendlyMessage: msg`A role with this label already exists.`,
});
}
return errors;
};
@@ -0,0 +1,32 @@
import { msg } from '@lingui/core/macro';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleReadWritePermissionsConsistency = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] => {
const errors: FlatEntityValidationError[] = [];
if (
flatRole.canReadAllObjectRecords === false &&
(flatRole.canUpdateAllObjectRecords ||
flatRole.canSoftDeleteAllObjectRecords ||
flatRole.canDestroyAllObjectRecords)
) {
errors.push({
code: PermissionsExceptionCode.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
message:
PermissionsExceptionMessage.CANNOT_GIVE_WRITING_PERMISSION_WITHOUT_READING_PERMISSION,
userFriendlyMessage: msg`You cannot grant edit permissions without also granting read permissions. Please enable read access first.`,
});
}
return errors;
};
@@ -0,0 +1,28 @@
import { msg, t } from '@lingui/core/macro';
import { isDefined } from 'twenty-shared/utils';
import { FLAT_ROLE_REQUIRED_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-required-properties.constants';
import { type FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { PermissionsExceptionCode } from 'src/engine/metadata-modules/permissions/permissions.exception';
import { type FlatEntityValidationError } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/types/failed-flat-entity-validation.type';
export const validateRoleRequiredPropertiesAreDefined = ({
flatRole,
}: {
flatRole: FlatRole;
}): FlatEntityValidationError[] =>
FLAT_ROLE_REQUIRED_PROPERTIES.flatMap<FlatEntityValidationError>(
(property) => {
if (isDefined(flatRole[property])) {
return [];
}
return [
{
code: PermissionsExceptionCode.INVALID_ARG,
message: t`Property ${property} is required for role`,
userFriendlyMessage: msg`Some of the information provided is invalid. Please check your input and try again.`,
},
];
},
);
@@ -7,6 +7,8 @@ import { FlatDatabaseEventTriggerValidatorService } from 'src/engine/workspace-m
import { FlatFieldMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-field-metadata-validator.service';
import { FlatIndexValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-index-metadata-validator.service';
import { FlatObjectMetadataValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-object-metadata-validator.service';
import { FlatRoleTargetValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-target-validator.service';
import { FlatRoleValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-role-validator.service';
import { FlatRouteTriggerValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-route-trigger-validator.service';
import { FlatServerlessFunctionValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-serverless-function-validator.service';
import { FlatViewFieldValidatorService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/validators/services/flat-view-field-validator.service';
@@ -29,6 +31,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatCronTriggerValidatorService,
FlatFieldMetadataTypeValidatorService,
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
],
exports: [
FlatViewValidatorService,
@@ -43,6 +47,8 @@ import { FlatViewValidatorService } from 'src/engine/workspace-manager/workspace
FlatCronTriggerValidatorService,
FlatFieldMetadataTypeValidatorService,
FlatRouteTriggerValidatorService,
FlatRoleValidatorService,
FlatRoleTargetValidatorService,
],
})
export class WorkspaceMigrationBuilderValidatorsModule {}
@@ -7,6 +7,8 @@ import { WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService } from 's
import { WorkspaceMigrationV2FieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/field/workspace-migration-v2-field-actions-builder.service';
import { WorkspaceMigrationV2IndexActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/index/workspace-migration-v2-index-actions-builder.service';
import { WorkspaceMigrationV2ObjectActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/object/workspace-migration-v2-object-actions-builder.service';
import { WorkspaceMigrationV2RoleTargetActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/workspace-migration-v2-role-target-actions-builder.service';
import { WorkspaceMigrationV2RoleActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/workspace-migration-v2-role-actions-builder.service';
import { WorkspaceMigrationV2RouteTriggerActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/route-trigger/workspace-migration-v2-route-trigger-actions-builder.service';
import { WorkspaceMigrationV2ServerlessFunctionActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/serverless-function/workspace-migration-v2-serverless-function-actions-builder.service';
import { WorkspaceMigrationV2ViewFieldActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/view-field/workspace-migration-v2-view-field-actions-builder.service';
@@ -30,6 +32,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2FieldActionsBuilderService,
WorkspaceMigrationV2CronTriggerActionsBuilderService,
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
],
exports: [
WorkspaceMigrationV2ViewActionsBuilderService,
@@ -43,6 +47,8 @@ import { WorkspaceMigrationBuilderValidatorsModule } from 'src/engine/workspace-
WorkspaceMigrationV2DatabaseEventTriggerActionsBuilderService,
WorkspaceMigrationV2CronTriggerActionsBuilderService,
WorkspaceMigrationV2RouteTriggerActionsBuilderService,
WorkspaceMigrationV2RoleActionsBuilderService,
WorkspaceMigrationV2RoleTargetActionsBuilderService,
FlatFieldMetadataTypeValidatorService,
],
})
@@ -0,0 +1,55 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { CreateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_role_target',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTarget } = action;
const updatedFlatRoleTargetMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: roleTarget,
flatEntityMaps: flatRoleTargetMaps,
});
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateRoleTargetAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleTarget } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
await roleTargetRepository.insert({
...roleTarget,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,61 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { DeleteRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_role_target',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTargetId } = action;
const updatedFlatRoleTargetMaps = deleteFlatEntityFromFlatEntityMapsOrThrow(
{
entityToDeleteId: roleTargetId,
flatEntityMaps: flatRoleTargetMaps,
},
);
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteRoleTargetAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleTargetId } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
await roleTargetRepository.delete({
id: roleTargetId,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,70 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { RoleTargetsEntity } from 'src/engine/metadata-modules/role/role-targets.entity';
import { UpdateRoleTargetAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role-target/types/workspace-migration-role-target-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateRoleTargetActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_role_target',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateRoleTargetAction>): Partial<AllFlatEntityMaps> {
const { flatRoleTargetMaps } = allFlatEntityMaps;
const { roleTargetId } = action;
const existingRoleTarget = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: roleTargetId,
flatEntityMaps: flatRoleTargetMaps,
});
const updatedRoleTarget = {
...existingRoleTarget,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
};
const updatedFlatRoleTargetMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedRoleTarget,
flatEntityMaps: flatRoleTargetMaps,
});
return {
flatRoleTargetMaps: updatedFlatRoleTargetMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateRoleTargetAction>,
): Promise<void> {
const { action, queryRunner } = context;
const { roleTargetId } = action;
const roleTargetRepository =
queryRunner.manager.getRepository<RoleTargetsEntity>(RoleTargetsEntity);
const update = fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action);
await roleTargetRepository.update(roleTargetId, update);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateRoleTargetAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,59 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { addFlatEntityToFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/add-flat-entity-to-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { CreateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class CreateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'create_role',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<CreateRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { role } = action;
const updatedFlatRoleMaps = addFlatEntityToFlatEntityMapsOrThrow({
flatEntity: role,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { role } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.insert({
...role,
workspaceId,
});
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<CreateRoleAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,56 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { deleteFlatEntityFromFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/delete-flat-entity-from-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { DeleteRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
@Injectable()
export class DeleteRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'delete_role',
) {
constructor() {
super();
}
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<DeleteRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { roleId } = action;
const updatedFlatRoleMaps = deleteFlatEntityFromFlatEntityMapsOrThrow({
entityToDeleteId: roleId,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<DeleteRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleId } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.delete({ id: roleId, workspaceId });
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<DeleteRoleAction>,
): Promise<void> {
return;
}
}
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import {
OptimisticallyApplyActionOnAllFlatEntityMapsArgs,
WorkspaceMigrationRunnerActionHandler,
} from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/interfaces/workspace-migration-runner-action-handler-service.interface';
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { replaceFlatEntityInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/replace-flat-entity-in-flat-entity-maps-or-throw.util';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { UpdateRoleAction } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-builder-v2/builders/role/types/workspace-migration-role-action-v2.type';
import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/types/workspace-migration-action-runner-args.type';
import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/utils/from-flat-entity-properties-updates-to-partial-flat-entity';
@Injectable()
export class UpdateRoleActionHandlerService extends WorkspaceMigrationRunnerActionHandler(
'update_role',
) {
optimisticallyApplyActionOnAllFlatEntityMaps({
action,
allFlatEntityMaps,
}: OptimisticallyApplyActionOnAllFlatEntityMapsArgs<UpdateRoleAction>): Partial<AllFlatEntityMaps> {
const { flatRoleMaps } = allFlatEntityMaps;
const { roleId } = action;
const existingRole = findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: roleId,
flatEntityMaps: flatRoleMaps,
});
const updatedRole = {
...existingRole,
...fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
};
const updatedFlatRoleMaps = replaceFlatEntityInFlatEntityMapsOrThrow({
flatEntity: updatedRole,
flatEntityMaps: flatRoleMaps,
});
return {
flatRoleMaps: updatedFlatRoleMaps,
};
}
async executeForMetadata(
context: WorkspaceMigrationActionRunnerArgs<UpdateRoleAction>,
): Promise<void> {
const { action, queryRunner, workspaceId } = context;
const { roleId } = action;
const roleRepository =
queryRunner.manager.getRepository<RoleEntity>(RoleEntity);
await roleRepository.update(
{ id: roleId, workspaceId },
fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action),
);
}
async executeForWorkspaceSchema(
_context: WorkspaceMigrationActionRunnerArgs<UpdateRoleAction>,
): Promise<void> {
return;
}
}
@@ -15,6 +15,12 @@ import { DeleteIndexActionHandlerService } from 'src/engine/workspace-manager/wo
import { CreateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/create-object-action-handler.service';
import { DeleteObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/delete-object-action-handler.service';
import { UpdateObjectActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/object/services/update-object-action-handler.service';
import { CreateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/create-role-target-action-handler.service';
import { DeleteRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/delete-role-target-action-handler.service';
import { UpdateRoleTargetActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role-target/services/update-role-target-action-handler.service';
import { CreateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/create-role-action-handler.service';
import { DeleteRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/delete-role-action-handler.service';
import { UpdateRoleActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/role/services/update-role-action-handler.service';
import { CreateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/create-route-trigger-action-handler.service';
import { DeleteRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/delete-route-trigger-action-handler.service';
import { UpdateRouteTriggerActionHandlerService } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-runner-v2/action-handlers/route-trigger/services/update-route-trigger-action-handler.service';
@@ -79,6 +85,14 @@ import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/wor
CreateRouteTriggerActionHandlerService,
DeleteRouteTriggerActionHandlerService,
UpdateRouteTriggerActionHandlerService,
CreateRoleActionHandlerService,
UpdateRoleActionHandlerService,
DeleteRoleActionHandlerService,
CreateRoleTargetActionHandlerService,
DeleteRoleTargetActionHandlerService,
UpdateRoleTargetActionHandlerService,
],
})
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
@@ -60,11 +60,6 @@ export class WorkspaceMigrationRunnerV2Service {
workspaceId,
),
);
asyncOperations.push(
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
}),
);
}
const shouldInvalidFindCoreViewsGraphqlCacheOperation = actions.some(
@@ -103,6 +98,43 @@ export class WorkspaceMigrationRunnerV2Service {
);
}
const shouldInvalidateRoleMapCache = actions.some((action) => {
switch (action.type) {
case 'create_role':
case 'delete_role':
case 'update_role':
case 'create_role_target':
case 'delete_role_target':
case 'update_role_target': {
return true;
}
default: {
return false;
}
}
});
if (
shouldIncrementMetadataGraphqlSchemaVersion ||
shouldInvalidateRoleMapCache
) {
asyncOperations.push(
...[
this.workspacePermissionsCacheService.recomputeApiKeyRoleMapCache({
workspaceId,
}),
this.workspacePermissionsCacheService.recomputeRolesPermissionsCache({
workspaceId,
}),
this.workspacePermissionsCacheService.recomputeUserWorkspaceRoleMapCache(
{
workspaceId,
},
),
],
);
}
return asyncOperations;
}
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { type WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface';
import { FlatRole } from 'src/engine/metadata-modules/flat-role/types/flat-role.type';
import { fromStandardRoleDefinitionToFlatRole } from 'src/engine/metadata-modules/flat-role/utils/from-standard-role-definition-to-flat-role.util';
import { type RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { type StandardRoleDefinition } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-roles/types/standard-role-definition.interface';
@@ -12,8 +13,8 @@ export class StandardRoleFactory {
roleDefinitions: StandardRoleDefinition[],
context: WorkspaceSyncContext,
existingRoles: RoleEntity[],
): Partial<RoleEntity>[] {
const computedRoles: Partial<RoleEntity>[] = [];
): FlatRole[] {
const computedRoles: FlatRole[] = [];
for (const roleDefinition of roleDefinitions) {
const existingRole = existingRoles.find(
@@ -30,6 +31,7 @@ export class StandardRoleFactory {
...flatRole,
id: existingRole.id,
universalIdentifier: roleDefinition.standardId || existingRole.id,
createdAt: existingRole.createdAt,
});
} else {
computedRoles.push({
@@ -38,7 +38,12 @@ export class WorkspaceSyncRoleService {
workspaceId: context.workspaceId,
standardId: Not(IsNull()),
},
relations: ['permissionFlags'],
relations: [
'permissionFlags',
'roleTargets',
'objectPermissions',
'fieldPermissions',
],
});
const targetStandardRoles = this.standardRoleFactory.create(
@@ -49,7 +54,7 @@ export class WorkspaceSyncRoleService {
const roleComparatorResults = this.workspaceRoleComparator.compare({
fromFlatRoles: existingStandardRoleEntities.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles.map(fromRoleEntityToFlatRole),
toFlatRoles: targetStandardRoles,
});
for (const roleComparatorResult of roleComparatorResults) {
@@ -3,7 +3,16 @@ import { type PermissionFlagType } from 'src/engine/metadata-modules/permissions
export type StandardRoleDefinition = Omit<
FlatRole,
'id' | 'workspaceId' | 'universalIdentifier' | 'standardId'
| 'id'
| 'workspaceId'
| 'universalIdentifier'
| 'standardId'
| 'roleTargetIds'
| 'objectPermissionIds'
| 'permissionFlagIds'
| 'fieldPermissionIds'
| 'createdAt'
| 'updatedAt'
> & {
standardId: string;
permissionFlags?: PermissionFlagType[];