feat: retrieve default resource views.

This commit is contained in:
a.bouhuolia
2021-08-04 15:14:21 +02:00
parent 2822270ac3
commit 4eb20162bb
3 changed files with 50 additions and 460 deletions

View File

@@ -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);
}
};
}

View File

@@ -13,4 +13,8 @@ export default (Model) =>
static getDefaultViewBySlug(viewSlug) {
return this.defaultViews.find((view) => view.slug === viewSlug) || null;
}
static getDefaultViews() {
return this.defaultViews;
}
};

View File

@@ -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<IView[]> {
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<IView> {
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<IView> {
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<IView>}
*/
public async newView(tenantId: number, viewDTO: IViewDTO): Promise<IView> {
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<IView>}
*/
public async editView(tenantId: number, viewId: number, viewEditDTO: IViewEditDTO): Promise<IView> {
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<IView>}
*/
private async getViewByIdOrThrowError(tenantId: number, viewId: number): Promise<IView> {
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<void>}
*/
public async deleteView(tenantId: number, viewId: number): Promise<void> {
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 });
}
}
}