Refactor WorkspaceMemberDto transpilation (#12110)

# Introduction

In a nutshell this PR introduces a `workspaceMemberEntity` to
`workspaceMemberDto` transpilation which was not done but commented as
`// TODO` across the `user resolver`.
Also passed on the `Roles` and `UserWorkspacePermissions` transpilation
We now also compute the roles for the `workspaceMember` resolver ( not
only the `workspaceMembers` )
Some refactor

In the following days about to create a PR that introduces integration
testing on the user resolver

## Conclusion
As always any suggestions are more than welcomed ! Please let me know !

## Misc

Following https://github.com/twentyhq/twenty/pull/11914

closing https://github.com/twentyhq/core-team-issues/issues/1011
This commit is contained in:
Paul Rastoin
2025-06-13 11:01:25 +02:00
committed by GitHub
parent 7c4ddb9448
commit 4182a3d306
11 changed files with 407 additions and 252 deletions

View File

@ -1,63 +0,0 @@
import { Injectable } from '@nestjs/common';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class DeletedWorkspaceMemberTranspiler {
constructor(private readonly fileService: FileService) {}
generateSignedAvatarUrl({
workspaceId,
workspaceMember,
}: {
workspaceMember: Pick<WorkspaceMemberWorkspaceEntity, 'avatarUrl' | 'id'>;
workspaceId: string;
}): string {
return this.fileService.signFileUrl({
url: workspaceMember.avatarUrl,
workspaceId,
});
}
toDeletedWorkspaceMemberDto(
workspaceMember: WorkspaceMemberWorkspaceEntity,
userWorkspaceId?: string,
): DeletedWorkspaceMember {
const {
avatarUrl: avatarUrlFromEntity,
id,
name,
userEmail,
} = workspaceMember;
const avatarUrl =
userWorkspaceId && avatarUrlFromEntity
? this.generateSignedAvatarUrl({
workspaceId: userWorkspaceId,
workspaceMember: {
avatarUrl: avatarUrlFromEntity,
id,
},
})
: null;
return {
id,
name,
userEmail,
avatarUrl,
userWorkspaceId: userWorkspaceId ?? null,
} satisfies DeletedWorkspaceMember;
}
toDeletedWorkspaceMemberDtos(
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
userWorkspaceId?: string,
): DeletedWorkspaceMember[] {
return workspaceMembers.map((workspaceMember) =>
this.toDeletedWorkspaceMemberDto(workspaceMember, userWorkspaceId),
);
}
}

View File

@ -0,0 +1,136 @@
import { Injectable } from '@nestjs/common';
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
import { fromRoleEntitiesToRoleDtos } from 'src/engine/metadata-modules/role/utils/fromRoleEntityToRoleDto.util';
import {
WorkspaceMemberDateFormatEnum,
WorkspaceMemberTimeFormatEnum,
WorkspaceMemberWorkspaceEntity,
} from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
export type ToWorkspaceMemberDtoArgs = {
workspaceMemberEntity: WorkspaceMemberWorkspaceEntity;
userWorkspaceRoles: RoleEntity[];
userWorkspace: UserWorkspace;
};
@Injectable()
export class WorkspaceMemberTranspiler {
constructor(private readonly fileService: FileService) {}
generateSignedAvatarUrl({
workspaceId,
workspaceMember,
}: {
workspaceMember: Pick<WorkspaceMemberWorkspaceEntity, 'avatarUrl' | 'id'>;
workspaceId: string;
}): string {
if (
!isDefined(workspaceMember.avatarUrl) ||
!isNonEmptyString(workspaceMember.avatarUrl)
) {
return '';
}
return this.fileService.signFileUrl({
url: workspaceMember.avatarUrl,
workspaceId,
});
}
toWorkspaceMemberDto({
userWorkspace,
workspaceMemberEntity,
userWorkspaceRoles,
}: ToWorkspaceMemberDtoArgs): WorkspaceMember {
const {
avatarUrl: avatarUrlFromEntity,
id,
name,
userEmail,
colorScheme,
locale,
timeFormat,
timeZone,
dateFormat,
} = workspaceMemberEntity;
const avatarUrl = this.generateSignedAvatarUrl({
workspaceId: userWorkspace.id,
workspaceMember: {
avatarUrl: avatarUrlFromEntity,
id,
},
});
const roles = fromRoleEntitiesToRoleDtos(userWorkspaceRoles);
return {
id,
name,
userEmail,
avatarUrl,
userWorkspaceId: userWorkspace.id,
colorScheme,
dateFormat: dateFormat as WorkspaceMemberDateFormatEnum,
locale,
timeFormat: timeFormat as WorkspaceMemberTimeFormatEnum,
timeZone,
roles,
} satisfies WorkspaceMember;
}
toWorkspaceMemberDtos(
allWorkspaceEntitiesBundles: ToWorkspaceMemberDtoArgs[],
) {
return allWorkspaceEntitiesBundles.map((bundle) =>
this.toWorkspaceMemberDto(bundle),
);
}
toDeletedWorkspaceMemberDto(
workspaceMember: WorkspaceMemberWorkspaceEntity,
userWorkspaceId?: string,
): DeletedWorkspaceMember {
const {
avatarUrl: avatarUrlFromEntity,
id,
name,
userEmail,
} = workspaceMember;
const avatarUrl = userWorkspaceId
? this.generateSignedAvatarUrl({
workspaceId: userWorkspaceId,
workspaceMember: {
avatarUrl: avatarUrlFromEntity,
id,
},
})
: null;
return {
id,
name,
userEmail,
avatarUrl,
userWorkspaceId: userWorkspaceId ?? null,
} satisfies DeletedWorkspaceMember;
}
toDeletedWorkspaceMemberDtos(
workspaceMembers: WorkspaceMemberWorkspaceEntity[],
userWorkspaceId?: string,
): DeletedWorkspaceMember[] {
return workspaceMembers.map((workspaceMember) =>
this.toDeletedWorkspaceMemberDto(workspaceMember, userWorkspaceId),
);
}
}

View File

@ -16,7 +16,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service';
import { WorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/workspace-member-transpiler.service';
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
import { User } from 'src/engine/core-modules/user/user.entity';
import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
@ -54,12 +54,12 @@ import { UserService } from './services/user.service';
PermissionsModule,
UserWorkspaceModule,
],
exports: [UserService, DeletedWorkspaceMemberTranspiler],
exports: [UserService, WorkspaceMemberTranspiler],
providers: [
UserService,
UserResolver,
TypeORMService,
DeletedWorkspaceMemberTranspiler,
WorkspaceMemberTranspiler,
],
})
export class UserModule {}

View File

@ -13,7 +13,7 @@ import crypto from 'crypto';
import { GraphQLJSONObject } from 'graphql-type-json';
import { FileUpload, GraphQLUpload } from 'graphql-upload';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { In, Repository } from 'typeorm';
@ -24,12 +24,10 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service';
import { SignedFileDTO } from 'src/engine/core-modules/file/file-upload/dtos/signed-file.dto';
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
import { FileService } from 'src/engine/core-modules/file/services/file.service';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import {
OnboardingService,
@ -37,10 +35,14 @@ import {
} from 'src/engine/core-modules/onboarding/onboarding.service';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { DeletedWorkspaceMember } from 'src/engine/core-modules/user/dtos/deleted-workspace-member.dto';
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
import { DeletedWorkspaceMemberTranspiler } from 'src/engine/core-modules/user/services/deleted-workspace-member-transpiler.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import {
ToWorkspaceMemberDtoArgs,
WorkspaceMemberTranspiler,
} from 'src/engine/core-modules/user/services/workspace-member-transpiler.service';
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { userValidator } from 'src/engine/core-modules/user/user.validate';
@ -48,11 +50,10 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ObjectPermissionDTO } from 'src/engine/metadata-modules/object-permission/dtos/object-permission.dto';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions';
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { fromUserWorkspacePermissionsToUserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/utils/fromUserWorkspacePermissionsToUserWorkspacePermissionsDto';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { AccountsToReconnectKeys } from 'src/modules/connected-account/types/accounts-to-reconnect-key-value.type';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@ -77,16 +78,50 @@ export class UserResolver {
private readonly fileUploadService: FileUploadService,
private readonly onboardingService: OnboardingService,
private readonly userVarService: UserVarsService,
private readonly fileService: FileService,
private readonly domainManagerService: DomainManagerService,
@InjectRepository(UserWorkspace, 'core')
private readonly userWorkspaceRepository: Repository<UserWorkspace>,
private readonly userRoleService: UserRoleService,
private readonly permissionsService: PermissionsService,
private readonly deletedWorkspaceMemberTranspiler: DeletedWorkspaceMemberTranspiler,
private readonly featureFlagService: FeatureFlagService,
private readonly workspaceMemberTranspiler: WorkspaceMemberTranspiler,
private readonly userWorkspaceService: UserWorkspaceService,
) {}
private async getUserWorkspacePermissions({
currentUserWorkspace,
workspace,
}: {
workspace: Workspace;
currentUserWorkspace: UserWorkspace;
}): Promise<UserWorkspacePermissions> {
const workspaceIsPendingOrOngoingCreation = [
WorkspaceActivationStatus.PENDING_CREATION,
WorkspaceActivationStatus.ONGOING_CREATION,
].includes(workspace.activationStatus);
if (workspaceIsPendingOrOngoingCreation) {
return this.permissionsService.getDefaultUserWorkspacePermissions();
}
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspace.id,
);
if (!isPermissionsV2Enabled) {
return await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
}
return await this.permissionsService.getUserWorkspacePermissionsV2({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
}
@Query(() => User)
async currentUser(
@AuthUser() { id: userId }: User,
@ -108,75 +143,24 @@ export class UserResolver {
(userWorkspace) => userWorkspace.workspace.id === workspace.id,
);
if (!currentUserWorkspace) {
if (!isDefined(currentUserWorkspace)) {
throw new Error('Current user workspace not found');
}
let settingsPermissions = {};
let objectRecordsPermissions = {};
let objectPermissions: ObjectPermissionDTO[] = [];
if (
![
WorkspaceActivationStatus.PENDING_CREATION,
WorkspaceActivationStatus.ONGOING_CREATION,
].includes(workspace.activationStatus)
) {
const isPermissionsV2Enabled =
await this.featureFlagService.isFeatureEnabled(
FeatureFlagKey.IS_PERMISSIONS_V2_ENABLED,
workspace.id,
);
if (isPermissionsV2Enabled) {
const permissions =
await this.permissionsService.getUserWorkspacePermissionsV2({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
settingsPermissions = permissions.settingsPermissions;
objectPermissions = Object.entries(permissions.objectPermissions).map(
([objectMetadataId, permissions]) => ({
objectMetadataId,
canReadObjectRecords: permissions.canRead,
canUpdateObjectRecords: permissions.canUpdate,
canSoftDeleteObjectRecords: permissions.canSoftDelete,
canDestroyObjectRecords: permissions.canDestroy,
}),
);
objectRecordsPermissions = permissions.objectRecordsPermissions;
} else {
const permissions =
await this.permissionsService.getUserWorkspacePermissions({
userWorkspaceId: currentUserWorkspace.id,
workspaceId: workspace.id,
});
settingsPermissions = permissions.settingsPermissions;
objectRecordsPermissions = permissions.objectRecordsPermissions;
}
}
const grantedSettingsPermissions: SettingPermissionType[] = (
Object.keys(settingsPermissions) as SettingPermissionType[]
)
// @ts-expect-error legacy noImplicitAny
.filter((feature) => settingsPermissions[feature] === true);
const grantedObjectRecordsPermissions = (
Object.keys(objectRecordsPermissions) as PermissionsOnAllObjectRecords[]
)
// @ts-expect-error legacy noImplicitAny
.filter((permission) => objectRecordsPermissions[permission] === true);
currentUserWorkspace.settingsPermissions = grantedSettingsPermissions;
currentUserWorkspace.objectRecordsPermissions =
grantedObjectRecordsPermissions;
currentUserWorkspace.objectPermissions = objectPermissions;
user.currentUserWorkspace = currentUserWorkspace;
const userWorkspacePermissions =
fromUserWorkspacePermissionsToUserWorkspacePermissionsDto(
await this.getUserWorkspacePermissions({
currentUserWorkspace,
workspace,
}),
);
return {
...user,
currentUserWorkspace: {
...currentUserWorkspace,
...userWorkspacePermissions,
},
currentWorkspace: workspace,
};
}
@ -185,18 +169,17 @@ export class UserResolver {
async userVars(
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
): Promise<Record<string, any>> {
): Promise<Record<string, unknown>> {
const userVars = await this.userVarService.getAll({
userId: user.id,
workspaceId: workspace.id,
});
const userVarAllowList = [
const userVarAllowList: string[] = [
OnboardingStepKeys.ONBOARDING_CONNECT_ACCOUNT_PENDING,
AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
AccountsToReconnectKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
] as string[];
];
const filteredMap = new Map(
[...userVars].filter(([key]) => userVarAllowList.includes(key)),
@ -212,20 +195,39 @@ export class UserResolver {
@Parent() user: User,
@AuthWorkspace() workspace: Workspace,
): Promise<WorkspaceMember | null> {
const workspaceMember = await this.userService.loadWorkspaceMember(
const workspaceMemberEntity = await this.userService.loadWorkspaceMember(
user,
workspace,
);
if (workspaceMember && workspaceMember.avatarUrl) {
workspaceMember.avatarUrl = this.fileService.signFileUrl({
url: workspaceMember.avatarUrl,
workspaceId: workspace.id,
});
if (!isDefined(workspaceMemberEntity)) {
throw new Error('Workspace member not found');
}
// TODO Refactor to be transpiled to WorkspaceMember instead
return workspaceMember as WorkspaceMember | null;
const workspaceId = workspace.id;
const userWorkspace =
await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({
userId: workspaceMemberEntity.userId,
workspaceId: workspace.id,
});
const roleOfUserWorkspace =
await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspace.id],
workspaceId,
});
const userWorkspaceRoles = roleOfUserWorkspace.get(userWorkspace.id);
if (!isDefined(userWorkspaceRoles)) {
throw new Error('User workspace roles not found');
}
return this.workspaceMemberTranspiler.toWorkspaceMemberDto({
workspaceMemberEntity,
userWorkspace,
userWorkspaceRoles,
});
}
@ResolveField(() => [WorkspaceMember], {
@ -240,7 +242,6 @@ export class UserResolver {
false,
);
const workspaceMembers: WorkspaceMember[] = [];
const userWorkspaces = await this.userWorkspaceRepository.find({
where: {
userId: In(workspaceMemberEntities.map((entity) => entity.userId)),
@ -248,14 +249,14 @@ export class UserResolver {
},
});
const userWorkspacesByUserId = new Map<string, UserWorkspace>(
const userWorkspacesByUserIdMap = new Map<string, UserWorkspace>(
userWorkspaces.map((userWorkspace) => [
userWorkspace.userId,
userWorkspace,
]),
);
const rolesByUserWorkspaces: Map<string, RoleDTO[]> =
const rolesByUserWorkspacesMap =
await this.userRoleService.getRolesByUserWorkspaces({
userWorkspaceIds: userWorkspaces.map(
(userWorkspace) => userWorkspace.id,
@ -263,54 +264,36 @@ export class UserResolver {
workspaceId: workspace.id,
});
for (const workspaceMemberEntity of workspaceMemberEntities) {
if (workspaceMemberEntity.avatarUrl) {
workspaceMemberEntity.avatarUrl = this.fileService.signFileUrl({
url: workspaceMemberEntity.avatarUrl,
workspaceId: workspace.id,
});
}
const toWorkspaceMemberDtoArgs =
workspaceMemberEntities.map<ToWorkspaceMemberDtoArgs>(
(workspaceMemberEntity) => {
const userWorkspace = userWorkspacesByUserIdMap.get(
workspaceMemberEntity.userId,
);
// TODO Refactor to be transpiled to WorkspaceMember instead
const workspaceMember = workspaceMemberEntity as WorkspaceMember;
if (!isDefined(userWorkspace)) {
throw new Error('User workspace not found');
}
const userWorkspace = userWorkspacesByUserId.get(
workspaceMemberEntity.userId,
const userWorkspaceRoles = rolesByUserWorkspacesMap.get(
userWorkspace.id,
);
if (!isDefined(userWorkspaceRoles)) {
throw new Error('User workspace roles not found');
}
return {
userWorkspace,
userWorkspaceRoles,
workspaceMemberEntity,
};
},
);
// TODO Refactor should not throw ? typed as nullable ?
if (!userWorkspace) {
throw new Error('User workspace not found');
}
workspaceMember.userWorkspaceId = userWorkspace.id;
const workspaceMemberRoles = (
rolesByUserWorkspaces.get(userWorkspace.id) ?? []
).map((roleEntity) => {
return {
id: roleEntity.id,
label: roleEntity.label,
canUpdateAllSettings: roleEntity.canUpdateAllSettings,
description: roleEntity.description,
icon: roleEntity.icon,
isEditable: roleEntity.isEditable,
userWorkspaceRoles: roleEntity.userWorkspaceRoles,
canReadAllObjectRecords: roleEntity.canReadAllObjectRecords,
canUpdateAllObjectRecords: roleEntity.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords:
roleEntity.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: roleEntity.canDestroyAllObjectRecords,
};
});
workspaceMember.roles = workspaceMemberRoles;
workspaceMembers.push(workspaceMember);
}
// TODO: Fix typing disrepency between Entity and DTO
return workspaceMembers;
return this.workspaceMemberTranspiler.toWorkspaceMemberDtos(
toWorkspaceMemberDtoArgs,
);
}
@ResolveField(() => [DeletedWorkspaceMember], {
@ -323,7 +306,7 @@ export class UserResolver {
const workspaceMemberEntities =
await this.userService.loadDeletedWorkspaceMembersOnly(workspace);
return this.deletedWorkspaceMemberTranspiler.toDeletedWorkspaceMemberDtos(
return this.workspaceMemberTranspiler.toDeletedWorkspaceMemberDtos(
workspaceMemberEntities,
workspace.id,
);
@ -375,7 +358,6 @@ export class UserResolver {
@Mutation(() => User)
async deleteUser(@AuthUser() { id: userId }: User) {
// Proceed with user deletion
return this.userService.deleteUser(userId);
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
@ -16,6 +15,7 @@ import {
PermissionsExceptionCode,
PermissionsExceptionMessage,
} from 'src/engine/metadata-modules/permissions/permissions.exception';
import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions';
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
import { WorkspacePermissionsCacheService } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.service';
@ -33,11 +33,7 @@ export class PermissionsService {
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<{
settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
objectPermissions: ObjectRecordsPermissions;
}> {
}): Promise<UserWorkspacePermissions> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
@ -60,7 +56,9 @@ export class PermissionsService {
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce(
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().settingsPermissions;
const settingsPermissions = Object.keys(SettingPermissionType).reduce(
(acc, feature) => ({
...acc,
[feature]:
@ -69,7 +67,7 @@ export class PermissionsService {
(settingPermission) => settingPermission.setting === feature,
),
}),
{} as Record<SettingPermissionType, boolean>,
defaultSettingsPermissions,
);
const { data: rolesPermissions } =
@ -79,37 +77,53 @@ export class PermissionsService {
const objectPermissions = rolesPermissions[roleOfUserWorkspace.id] ?? {};
const objectRecordsPermissionsMap: Record<
PermissionsOnAllObjectRecords,
boolean
> = {
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canDestroyAllObjectRecords ?? false,
};
const objectRecordsPermissions: UserWorkspacePermissions['objectRecordsPermissions'] =
{
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canDestroyAllObjectRecords ?? false,
};
return {
settingsPermissions: settingsPermissionsMap,
objectRecordsPermissions: objectRecordsPermissionsMap,
settingsPermissions,
objectRecordsPermissions,
objectPermissions,
};
}
public getDefaultUserWorkspacePermissions = () =>
({
objectRecordsPermissions: {
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]: false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]: false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]: false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]: false,
},
settingsPermissions: {
[SettingPermissionType.API_KEYS_AND_WEBHOOKS]: false,
[SettingPermissionType.WORKSPACE]: false,
[SettingPermissionType.WORKSPACE_MEMBERS]: false,
[SettingPermissionType.ROLES]: false,
[SettingPermissionType.DATA_MODEL]: false,
[SettingPermissionType.ADMIN_PANEL]: false,
[SettingPermissionType.SECURITY]: false,
[SettingPermissionType.WORKFLOWS]: false,
},
objectPermissions: {},
}) as const satisfies UserWorkspacePermissions;
public async getUserWorkspacePermissions({
userWorkspaceId,
workspaceId,
}: {
userWorkspaceId: string;
workspaceId: string;
}): Promise<{
settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
}> {
}): Promise<UserWorkspacePermissions> {
const [roleOfUserWorkspace] = await this.userRoleService
.getRolesByUserWorkspaces({
userWorkspaceIds: [userWorkspaceId],
@ -132,7 +146,9 @@ export class PermissionsService {
const settingPermissions = roleOfUserWorkspace.settingPermissions ?? [];
const settingsPermissionsMap = Object.keys(SettingPermissionType).reduce(
const defaultSettingsPermissions =
this.getDefaultUserWorkspacePermissions().settingsPermissions;
const settingsPermissions = Object.keys(SettingPermissionType).reduce(
(acc, feature) => ({
...acc,
[feature]:
@ -141,26 +157,25 @@ export class PermissionsService {
(settingPermission) => settingPermission.setting === feature,
),
}),
{} as Record<SettingPermissionType, boolean>,
defaultSettingsPermissions,
);
const objectRecordsPermissionsMap: Record<
PermissionsOnAllObjectRecords,
boolean
> = {
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canDestroyAllObjectRecords ?? false,
};
const objectRecordsPermissions: UserWorkspacePermissions['objectRecordsPermissions'] =
{
[PermissionsOnAllObjectRecords.READ_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canReadAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.UPDATE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canUpdateAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.SOFT_DELETE_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canSoftDeleteAllObjectRecords ?? false,
[PermissionsOnAllObjectRecords.DESTROY_ALL_OBJECT_RECORDS]:
roleOfUserWorkspace.canDestroyAllObjectRecords ?? false,
};
return {
settingsPermissions: settingsPermissionsMap,
objectRecordsPermissions: objectRecordsPermissionsMap,
settingsPermissions,
objectRecordsPermissions,
objectPermissions: {},
};
}

View File

@ -0,0 +1,10 @@
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { ObjectRecordsPermissions } from 'twenty-shared/types';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
export type UserWorkspacePermissions = {
settingsPermissions: Record<SettingPermissionType, boolean>;
objectRecordsPermissions: Record<PermissionsOnAllObjectRecords, boolean>;
objectPermissions: ObjectRecordsPermissions;
};

View File

@ -0,0 +1,6 @@
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
export type UserWorkspacePermissionsDto = Pick<
UserWorkspace,
'objectPermissions' | 'settingsPermissions' | 'objectRecordsPermissions'
>;

View File

@ -0,0 +1,33 @@
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
export const fromRoleEntityToRoleDto = ({
id,
label,
canUpdateAllSettings,
description,
icon,
isEditable,
userWorkspaceRoles,
canReadAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords,
}: RoleEntity): RoleDTO => {
return {
id,
label,
canUpdateAllSettings,
description,
icon,
isEditable,
userWorkspaceRoles,
canReadAllObjectRecords,
canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords,
};
};
export const fromRoleEntitiesToRoleDtos = (roleEntities: RoleEntity[]) =>
roleEntities.map(fromRoleEntityToRoleDto);

View File

@ -0,0 +1,35 @@
import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants';
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
import { UserWorkspacePermissions } from 'src/engine/metadata-modules/permissions/types/user-workspace-permissions';
import { UserWorkspacePermissionsDto } from 'src/engine/metadata-modules/role/dtos/user-workspace-permissions.dto';
export const fromUserWorkspacePermissionsToUserWorkspacePermissionsDto = ({
objectPermissions: rawObjectPermissions,
objectRecordsPermissions: rawObjectRecordsPermissions,
settingsPermissions: rawSettingsPermissions,
}: UserWorkspacePermissions): UserWorkspacePermissionsDto => {
const objectPermissions = Object.entries(rawObjectPermissions).map(
([objectMetadataId, permissions]) => ({
objectMetadataId,
canReadObjectRecords: permissions.canRead,
canUpdateObjectRecords: permissions.canUpdate,
canSoftDeleteObjectRecords: permissions.canSoftDelete,
canDestroyObjectRecords: permissions.canDestroy,
}),
);
const settingsPermissions = (
Object.keys(rawSettingsPermissions) as SettingPermissionType[]
).filter((feature) => rawSettingsPermissions[feature] === true);
const objectRecordsPermissions = (
Object.keys(rawObjectRecordsPermissions) as PermissionsOnAllObjectRecords[]
).filter((feature) => rawObjectRecordsPermissions[feature] === true);
return {
objectPermissions,
objectRecordsPermissions,
settingsPermissions,
};
};

View File

@ -1,8 +1,7 @@
export type ObjectRecordsPermissions = {
[objectMetadataId: string]: {
type ObjectMetadataId = string;
export type ObjectRecordsPermissions = Record<ObjectMetadataId, {
canRead: boolean;
canUpdate: boolean;
canSoftDelete: boolean;
canDestroy: boolean;
};
};
}>;

View File

@ -1,5 +1,7 @@
import { ObjectRecordsPermissions } from '@/types';
import { ObjectRecordsPermissions } from "@/types/ObjectRecordsPermissions";
export type ObjectRecordsPermissionsByRoleId = {
[roleId: string]: ObjectRecordsPermissions;
};
type RoleId = string;
export type ObjectRecordsPermissionsByRoleId = Record<
RoleId,
ObjectRecordsPermissions
>;