import { Service, Inject } from 'typedi'; import Knex from 'knex'; import { ICreateRoleDTO, ICreateRolePermissionDTO, IEditRoleDTO, IEditRolePermissionDTO, IRole, IRoleCreatedPayload, IRoleDeletedPayload, IRoleEditedPayload, } from '@/interfaces'; import HasTenancyService from '@/services/Tenancy/TenancyService'; import { ServiceError } from '@/exceptions'; import { AbilitySchema } from './AbilitySchema'; import { getInvalidPermissions } from './utils'; import UnitOfWork from '@/services/UnitOfWork'; import { ERRORS } from './constants'; import { EventPublisher } from '@/lib/EventPublisher/EventPublisher'; import events from '@/subscribers/events'; import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable'; import { RoleTransformer } from './RoleTransformer'; @Service() export default class RolesService { @Inject() private tenancy: HasTenancyService; @Inject() private uow: UnitOfWork; @Inject() private eventPublisher: EventPublisher; @Inject() private transformer: TransformerInjectable; /** * Creates a new role and store it to the storage. * @param {number} tenantId * @param {ICreateRoleDTO} createRoleDTO * @returns */ public createRole = async ( tenantId: number, createRoleDTO: ICreateRoleDTO ) => { const { Role } = this.tenancy.models(tenantId); // Validates the invalid permissions. this.validateInvalidPermissions(createRoleDTO.permissions); // Transformes the permissions DTO. const permissions = this.tranaformPermissionsDTO(createRoleDTO.permissions); // Creates a new role with associated entries under unit-of-work. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Creates a new role to the storage. const role = await Role.query(trx).upsertGraph({ name: createRoleDTO.roleName, description: createRoleDTO.roleDescription, permissions, }); // Triggers `onRoleCreated` event. await this.eventPublisher.emitAsync(events.roles.onCreated, { tenantId, createRoleDTO, role, trx, } as IRoleCreatedPayload); return role; }); }; /** * Edits details of the given role on the storage. * @param {number} tenantId - * @param {number} roleId - * @param {IEditRoleDTO} editRoleDTO - Edit role DTO. */ public editRole = async ( tenantId: number, roleId: number, editRoleDTO: IEditRoleDTO ) => { const { Role } = this.tenancy.models(tenantId); // Validates the invalid permissions. this.validateInvalidPermissions(editRoleDTO.permissions); // Retrieve the given role or throw not found serice error. const oldRole = await this.getRoleOrThrowError(tenantId, roleId); const permissions = this.tranaformEditPermissionsDTO( editRoleDTO.permissions ); // Updates the role on the storage. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Updates the given role to the storage. const role = await Role.query(trx).upsertGraph({ id: roleId, name: editRoleDTO.roleName, description: editRoleDTO.roleDescription, permissions, }); // Triggers `onRoleEdited` event. await this.eventPublisher.emitAsync(events.roles.onEdited, { editRoleDTO, oldRole, role, trx, } as IRoleEditedPayload); return role; }); }; /** * Retrieve the role or throw not found service error. * @param {number} tenantId * @param {number} roleId * @returns {Promise} */ public getRoleOrThrowError = async ( tenantId: number, roleId: number ): Promise => { const { Role } = this.tenancy.models(tenantId); const role = await Role.query().findById(roleId); this.throwRoleNotFound(role); return role; }; /** * Throw role not found service error if the role is not found. * @param {IRole|null} role */ private throwRoleNotFound(role: IRole | null) { if (!role) { throw new ServiceError(ERRORS.ROLE_NOT_FOUND); } } /** * Deletes the given role from the storage. * @param {number} tenantId - * @param {number} roleId - Role id. */ public deleteRole = async ( tenantId: number, roleId: number ): Promise => { const { Role, RolePermission } = this.tenancy.models(tenantId); // Retrieve the given role or throw not found serice error. const oldRole = await this.getRoleOrThrowError(tenantId, roleId); // Validate role is not predefined. this.validateRoleNotPredefined(oldRole); // Validates the given role is not associated to any user. await this.validateRoleNotAssociatedToUser(tenantId, roleId); // Deletes the given role and associated models under unit-of-work envirement. return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => { // Deletes the role associated permissions from the storage. await RolePermission.query(trx).where('roleId', roleId).delete(); // Deletes the role object form the storage. await Role.query(trx).findById(roleId).delete(); // Triggers `onRoleDeleted` event. await this.eventPublisher.emitAsync(events.roles.onDeleted, { oldRole, roleId, tenantId, trx, } as IRoleDeletedPayload); }); }; /** * Retrieve the roles list. * @param {number} tenantId * @param {Promise} */ public listRoles = async (tenantId: number): Promise => { const { Role } = this.tenancy.models(tenantId); const roles = await Role.query().withGraphFetched('permissions'); return this.transformer.transform(tenantId, roles, new RoleTransformer()); }; /** * Retrieve the given role metadata. * @param {number} tenantId * @param {number} roleId - Role id. * @returns {Promise} */ public getRole = async (tenantId: number, roleId: number): Promise => { const { Role } = this.tenancy.models(tenantId); const role = await Role.query() .findById(roleId) .withGraphFetched('permissions'); this.throwRoleNotFound(role); return this.transformer.transform(tenantId, role, new RoleTransformer()); }; /** * Valdiates role is not predefined. * @param {IRole} role - Role object. */ private validateRoleNotPredefined(role: IRole) { if (role.predefined) { throw new ServiceError(ERRORS.ROLE_PREFINED); } } /** * Validates the invalid given permissions. * @param {ICreateRolePermissionDTO[]} permissions - */ public validateInvalidPermissions = ( permissions: ICreateRolePermissionDTO[] ) => { const invalidPerms = getInvalidPermissions(AbilitySchema, permissions); if (invalidPerms.length > 0) { throw new ServiceError(ERRORS.INVALIDATE_PERMISSIONS, null, { invalidPermissions: invalidPerms, }); } }; /** * Transformes new permissions DTO. * @param {ICreateRolePermissionDTO[]} permissions * @returns {ICreateRolePermissionDTO[]} */ private tranaformPermissionsDTO = ( permissions: ICreateRolePermissionDTO[] ) => { return permissions.map((permission: ICreateRolePermissionDTO) => ({ subject: permission.subject, ability: permission.ability, value: permission.value, })); }; /** * Transformes edit permissions DTO. * @param {ICreateRolePermissionDTO[]} permissions * @returns {IEditRolePermissionDTO[]} */ private tranaformEditPermissionsDTO = ( permissions: IEditRolePermissionDTO[] ) => { return permissions.map((permission: IEditRolePermissionDTO) => ({ permissionId: permission.permissionId, subject: permission.subject, ability: permission.ability, value: permission.value, })); }; /** * Validates the given role is not associated to any tenant users. * @param {number} tenantId * @param {number} roleId */ private validateRoleNotAssociatedToUser = async ( tenantId: number, roleId: number ) => { const { User } = this.tenancy.models(tenantId); const userAssociatedRole = await User.query().where('roleId', roleId); // Throw service error if the role has associated users. if (userAssociatedRole.length > 0) { throw new ServiceError(ERRORS.CANNT_DELETE_ROLE_ASSOCIATED_TO_USERS); } }; }