diff --git a/server/src/api/controllers/Views.ts b/server/src/api/controllers/Views.ts index 3d588d13b..92db2dcc2 100644 --- a/server/src/api/controllers/Views.ts +++ b/server/src/api/controllers/Views.ts @@ -8,216 +8,68 @@ import { IViewDTO, IViewEditDTO } from 'interfaces'; import { ServiceError } from 'exceptions'; @Service() -export default class ViewsController extends BaseController{ +export default class ViewsController extends BaseController { @Inject() viewsService: ViewsService; - + /** * Router constructor. */ router() { const router = Router(); - router.get('/resource/:resource_model', [ - ...this.viewsListSchemaValidation, - ], + router.get( + '/resource/:resource_model', + [...this.viewsListSchemaValidation], this.validationResult, asyncMiddleware(this.listResourceViews.bind(this)), - this.handlerServiceErrors, - ); - router.post('/', [ - ...this.viewDTOSchemaValidation, - ], - this.validationResult, - asyncMiddleware(this.createView.bind(this)), this.handlerServiceErrors ); - router.post('/:id', [ - ...this.viewParamSchemaValidation, - ...this.viewEditDTOSchemaValidation, - ], - this.validationResult, - asyncMiddleware(this.editView.bind(this)), - this.handlerServiceErrors, - ); - router.delete('/:id', [ - ...this.viewParamSchemaValidation - ], - this.validationResult, - asyncMiddleware(this.deleteView.bind(this)), - this.handlerServiceErrors, - ); - router.get('/:id', [ - ...this.viewParamSchemaValidation - ], - this.validationResult, - asyncMiddleware(this.getView.bind(this)), - this.handlerServiceErrors, - ); return router; } /** - * New view DTO schema validation. + * Custom views list validation schema. */ - get viewDTOSchemaValidation() { - return [ - check('resource_model').exists().escape().trim(), - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - - check('roles').isArray({ min: 1 }), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - - check('columns').exists().isArray({ min: 1 }), - check('columns.*.field_key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - ]; - } - - /** - * Edit view DTO schema validation. - */ - get viewEditDTOSchemaValidation() { - return [ - check('name').exists().escape().trim(), - check('logic_expression').exists().trim().escape(), - - check('roles').isArray({ min: 1 }), - check('roles.*.field_key').exists().escape().trim(), - check('roles.*.comparator').exists(), - check('roles.*.value').exists(), - check('roles.*.index').exists().isNumeric().toInt(), - - check('columns').exists().isArray({ min: 1 }), - check('columns.*.field_key').exists().escape().trim(), - check('columns.*.index').exists().isNumeric().toInt(), - ]; - } - - get viewParamSchemaValidation() { - return [ - param('id').exists().isNumeric().toInt(), - ]; - } - get viewsListSchemaValidation() { - return [ - param('resource_model').exists().trim().escape(), - ] + return [param('resource_model').exists().trim().escape()]; } /** * List all views that associated with the given resource. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - + * @param {Request} req - + * @param {Response} res - + * @param {NextFunction} next - */ async listResourceViews(req: Request, res: Response, next: NextFunction) { const { tenantId } = req; const { resource_model: resourceModel } = req.params; try { - const views = await this.viewsService.listResourceViews(tenantId, resourceModel); + const views = await this.viewsService.listResourceViews( + tenantId, + resourceModel + ); + return res.status(200).send({ views }); } catch (error) { next(error); } } - /** - * Retrieve view details with assocaited roles and columns. - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next - */ - async getView(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: viewId } = req.params; - - try { - const view = await this.viewsService.getView(tenantId, viewId); - return res.status(200).send({ view }); - } catch (error) { - next(error); - } - } - - /** - * Creates a new view. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - - */ - async createView(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const viewDTO: IViewDTO = this.matchedBodyData(req); - - try { - const view = await this.viewsService.newView(tenantId, viewDTO); - return res.status(200).send({ - id: view.id, - message: 'The view has been created successfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Edits views metadata. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - - */ - async editView(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: viewId } = req.params; - const viewEditDTO: IViewEditDTO = this.matchedBodyData(req); - - try { - await this.viewsService.editView(tenantId, viewId, viewEditDTO); - return res.status(200).send({ - id: viewId, - message: 'The given view has been edited succcessfully.', - }); - } catch (error) { - next(error); - } - } - - /** - * Deletes the given view. - * @param {Request} req - - * @param {Response} res - - * @param {NextFunction} next - - */ - async deleteView(req: Request, res: Response, next: NextFunction) { - const { tenantId } = req; - const { id: viewId } = req.params; - - try { - await this.viewsService.deleteView(tenantId, viewId); - return res.status(200).send({ - id: viewId, - message: 'The view has been deleted successfully.', - }); - } catch (error) { - next(error); - } - } - /** * Handles service errors. - * @param {Error} error - * @param {Request} req - * @param {Response} res - * @param {NextFunction} next + * @param {Error} error + * @param {Request} req + * @param {Response} res + * @param {NextFunction} next */ - handlerServiceErrors(error: Error, req: Request, res: Response, next: NextFunction) { + handlerServiceErrors( + error: Error, + req: Request, + res: Response, + next: NextFunction + ) { if (error instanceof ServiceError) { if (error.errorType === 'VIEW_NAME_NOT_UNIQUE') { return res.boom.badRequest(null, { @@ -226,7 +78,7 @@ export default class ViewsController extends BaseController{ } if (error.errorType === 'RESOURCE_MODEL_NOT_FOUND') { return res.boom.badRequest(null, { - errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150, }], + errors: [{ type: 'RESOURCE_MODEL_NOT_FOUND', code: 150 }], }); } if (error.errorType === 'INVALID_LOGIC_EXPRESSION') { @@ -253,18 +105,18 @@ export default class ViewsController extends BaseController{ return res.boom.badRequest(null, { errors: [{ type: 'PREDEFINED_VIEW', code: 200 }], }); - } + } if (error.errorType === 'RESOURCE_FIELDS_KEYS_NOT_FOUND') { return res.boom.badRequest(null, { errors: [{ type: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', code: 300 }], - }) + }); } if (error.errorType === 'RESOURCE_COLUMNS_KEYS_NOT_FOUND') { return res.boom.badRequest(null, { errors: [{ type: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', code: 310 }], - }) + }); } } next(error); } -}; +} diff --git a/server/src/models/CustomViewBaseModel.js b/server/src/models/CustomViewBaseModel.js index c5a2ee934..a54520023 100644 --- a/server/src/models/CustomViewBaseModel.js +++ b/server/src/models/CustomViewBaseModel.js @@ -13,4 +13,8 @@ export default (Model) => static getDefaultViewBySlug(viewSlug) { return this.defaultViews.find((view) => view.slug === viewSlug) || null; } + + static getDefaultViews() { + return this.defaultViews; + } }; diff --git a/server/src/services/Views/ViewsService.ts b/server/src/services/Views/ViewsService.ts index 334f37cd0..0c0feda7c 100644 --- a/server/src/services/Views/ViewsService.ts +++ b/server/src/services/Views/ViewsService.ts @@ -1,29 +1,11 @@ -import { Service, Inject } from "typedi"; -import { difference } from 'lodash'; -import { ServiceError } from 'exceptions'; +import { Service, Inject } from 'typedi'; import { IViewsService, - IViewDTO, IView, - IViewEditDTO, IModel, - IViewColumnDTO, - IViewRoleDTO, } from 'interfaces'; -import { getModelFieldsKeys } from 'lib/ViewRolesBuilder'; import TenancyService from 'services/Tenancy/TenancyService'; -import ResourceService from "services/Resource/ResourceService"; -import { validateRolesLogicExpression } from 'lib/ViewRolesBuilder'; - -const ERRORS = { - VIEW_NOT_FOUND: 'VIEW_NOT_FOUND', - VIEW_PREDEFINED: 'VIEW_PREDEFINED', - VIEW_NAME_NOT_UNIQUE: 'VIEW_NAME_NOT_UNIQUE', - LOGIC_EXPRESSION_INVALID: 'INVALID_LOGIC_EXPRESSION', - RESOURCE_FIELDS_KEYS_NOT_FOUND: 'RESOURCE_FIELDS_KEYS_NOT_FOUND', - RESOURCE_COLUMNS_KEYS_NOT_FOUND: 'RESOURCE_COLUMNS_KEYS_NOT_FOUND', - RESOURCE_MODEL_NOT_FOUND: 'RESOURCE_MODEL_NOT_FOUND' -}; +import ResourceService from 'services/Resource/ResourceService'; @Service() export default class ViewsService implements IViewsService { @@ -38,281 +20,33 @@ export default class ViewsService implements IViewsService { /** * Listing resource views. - * @param {number} tenantId - - * @param {string} resourceModel - + * @param {number} tenantId - + * @param {string} resourceModel - */ public async listResourceViews( tenantId: number, - resourceModelName: string, + resourceModelName: string ): Promise { - this.logger.info('[views] trying to retrieve resource views.', { tenantId, resourceModelName }); - // Validate the resource model name is valid. - const resourceModel = this.getResourceModelOrThrowError(tenantId, resourceModelName); + const resourceModel = this.getResourceModelOrThrowError( + tenantId, + resourceModelName + ); + // Default views. + const defaultViews = resourceModel.getDefaultViews(); - const { viewRepository } = this.tenancy.repositories(tenantId); - return viewRepository.allByResource(resourceModel.name, 'roles'); - } - - /** - * Validate model resource conditions fields existance. - * @param {string} resourceName - * @param {IViewRoleDTO[]} viewRoles - */ - private validateResourceRolesFieldsExistance( - ResourceModel: IModel, - viewRoles: IViewRoleDTO[], - ) { - const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); - - const fieldsKeys = viewRoles.map(viewRole => viewRole.fieldKey); - const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); - - if (notFoundFieldsKeys.length > 0) { - throw new ServiceError(ERRORS.RESOURCE_FIELDS_KEYS_NOT_FOUND); - } - return notFoundFieldsKeys; - } - - /** - * Validates model resource columns existance. - * @param {string} resourceName - * @param {IViewColumnDTO[]} viewColumns - */ - private validateResourceColumnsExistance( - ResourceModel: IModel, - viewColumns: IViewColumnDTO[], - ) { - const resourceFieldsKeys = getModelFieldsKeys(ResourceModel); - - const fieldsKeys = viewColumns.map((viewColumn: IViewColumnDTO) => viewColumn.fieldKey); - const notFoundFieldsKeys = difference(fieldsKeys, resourceFieldsKeys); - - if (notFoundFieldsKeys.length > 0) { - throw new ServiceError(ERRORS.RESOURCE_COLUMNS_KEYS_NOT_FOUND); - } - return notFoundFieldsKeys; - } - - /** - * Retrieve the given view details with associated conditions and columns. - * @param {number} tenantId - Tenant id. - * @param {number} viewId - View id. - */ - public getView(tenantId: number, viewId: number): Promise { - this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); - return this.getViewOrThrowError(tenantId, viewId); - } - - /** - * Retrieve view or throw not found error. - * @param {number} tenantId - Tenant id. - * @param {number} viewId - View id. - */ - private async getViewOrThrowError(tenantId: number, viewId: number): Promise { - const { viewRepository } = this.tenancy.repositories(tenantId); - - this.logger.info('[view] trying to get view from storage.', { tenantId, viewId }); - const view = await viewRepository.findOneById(viewId); - - if (!view) { - this.logger.info('[view] view not found.', { tenantId, viewId }); - throw new ServiceError(ERRORS.VIEW_NOT_FOUND); - } - return view; + return defaultViews; } /** * Retrieve resource model from resource name or throw not found error. - * @param {number} tenantId - * @param {number} resourceModel + * @param {number} tenantId + * @param {number} resourceModel */ private getResourceModelOrThrowError( tenantId: number, - resourceModel: string, + resourceModel: string ): IModel { return this.resourceService.getResourceModel(tenantId, resourceModel); } - - /** - * Validates view name uniqiness in the given resource. - * @param {number} tenantId - * @param {stirng} resourceModel - * @param {string} viewName - * @param {number} notViewId - */ - private async validateViewNameUniquiness( - tenantId: number, - resourceModel: string, - viewName: string, - notViewId?: number - ): void { - const { View } = this.tenancy.models(tenantId); - - this.logger.info('[views] trying to validate view name uniqiness.', { - tenantId, resourceModel, viewName, - }); - const foundViews = await View.query() - .where('resource_model', resourceModel) - .where('name', viewName) - .onBuild((builder) => { - if (notViewId) { - builder.whereNot('id', notViewId); - } - }); - - if (foundViews.length > 0) { - throw new ServiceError(ERRORS.VIEW_NAME_NOT_UNIQUE); - } - } - - /** - * Creates a new custom view to specific resource. - * ----–––––– - * Precedures. - * ----–––––– - * - Validate resource fields existance. - * - Validate resource columns existance. - * - Validate view logic expression. - * - Store view to the storage. - * - Store view columns to the storage. - * - Store view roles/conditions to the storage. - * --------- - * @param {number} tenantId - Tenant id. - * @param {IViewDTO} viewDTO - New view DTO. - * - * @return {Promise} - */ - public async newView(tenantId: number, viewDTO: IViewDTO): Promise { - const { viewRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[views] trying to create a new view.', { tenantId, viewDTO }); - - // Validate the resource name is exists and resourcable. - const ResourceModel = this.getResourceModelOrThrowError(tenantId, viewDTO.resourceModel); - - // Validate view name uniquiness. - await this.validateViewNameUniquiness(tenantId, viewDTO.resourceModel, viewDTO.name); - - // Validate the given fields keys exist on the storage. - this.validateResourceRolesFieldsExistance(ResourceModel, viewDTO.roles); - - // Validate the given columnable fields keys exists on the storage. - this.validateResourceColumnsExistance(ResourceModel, viewDTO.columns); - - // Validates the view conditional logic expression. - if (!validateRolesLogicExpression(viewDTO.logicExpression, viewDTO.roles)) { - throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); - } - // Save view details. - this.logger.info('[views] trying to insert to storage.', { tenantId, viewDTO }) - const view = await viewRepository.create({ - predefined: false, - name: viewDTO.name, - rolesLogicExpression: viewDTO.logicExpression, - resourceModel: ResourceModel.name, - roles: viewDTO.roles, - columns: viewDTO.columns, - }); - this.logger.info('[views] inserted to the storage successfully.', { tenantId, viewDTO }); - return view; - } - - /** - * Edits view details, roles and columns on the storage. - * -------- - * Precedures. - * -------- - * - Validate view existance. - * - Validate view resource fields existance. - * - Validate view resource columns existance. - * - Validate view logic expression. - * - Delete old view columns and roles. - * - Re-save view columns and roles. - * - * @param {number} tenantId - - * @param {number} viewId - - * @param {IViewEditDTO} viewEditDTO - - * @return {Promise} - */ - public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise { - const { viewRepository } = this.tenancy.repositories(tenantId); - this.logger.info('[view] trying to edit custom view.', { tenantId, viewId }); - - // Retrieve view details or throw not found error. - const oldView = await this.getViewOrThrowError(tenantId, viewId); - - // Validate the resource name is exists and resourcable. - const ResourceModel = this.getResourceModelOrThrowError(tenantId, oldView.resourceModel); - - // Validate view name uniquiness. - await this.validateViewNameUniquiness(tenantId, oldView.resourceModel, viewEditDTO.name, viewId); - - // Validate the given fields keys exist on the storage. - this.validateResourceRolesFieldsExistance(ResourceModel, viewEditDTO.roles); - - // Validate the given columnable fields keys exists on the storage. - this.validateResourceColumnsExistance(ResourceModel, viewEditDTO.columns); - - // Validates the view conditional logic expression. - if (!validateRolesLogicExpression(viewEditDTO.logicExpression, viewEditDTO.roles)) { - throw new ServiceError(ERRORS.LOGIC_EXPRESSION_INVALID); - } - // Update view details. - this.logger.info('[views] trying to update view details.', { tenantId, viewId }); - const view = await viewRepository.upsertGraph({ - id: viewId, - predefined: false, - name: viewEditDTO.name, - rolesLogicExpression: viewEditDTO.logicExpression, - roles: viewEditDTO.roles, - columns: viewEditDTO.columns, - }) - this.logger.info('[view] edited successfully.', { tenantId, viewId }); - - return view; - } - - /** - * Retrieve views details of the given id or throw not found error. - * @private - * @param {number} tenantId - * @param {number} viewId - * @return {Promise} - */ - private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise { - const { View } = this.tenancy.models(tenantId); - - this.logger.info('[views] get stored view.', { tenantId, viewId }); - const view = await View.query().findById(viewId); - - if (!view) { - this.logger.info('[views] the given id not found.', { tenantId, viewId }); - throw new ServiceError(ERRORS.VIEW_NOT_FOUND); - } - return view; - } - - /** - * Deletes the given view with associated roles and columns. - * @param {number} tenantId - Tenant id. - * @param {number} viewId - View id. - * @return {Promise} - */ - public async deleteView(tenantId: number, viewId: number): Promise { - const { View } = this.tenancy.models(tenantId); - - this.logger.info('[views] trying to delete the given view.', { tenantId, viewId }); - const view = await this.getViewByIdOrThrowError(tenantId, viewId); - - if (view.predefined) { - this.logger.info('[views] cannot delete predefined.', { tenantId, viewId }); - throw new ServiceError(ERRORS.VIEW_PREDEFINED); - } - await Promise.all([ - view.$relatedQuery('roles').delete(), - view.$relatedQuery('columns').delete(), - ]); - await View.query().where('id', view.id).delete(); - this.logger.info('[views] deleted successfully.', { tenantId, viewId }); - } -} \ No newline at end of file +}