refactoring: media system.

This commit is contained in:
Ahmed Bouhuolia
2020-10-07 21:30:16 +02:00
parent 681fa0560e
commit 15dca0a7d2
14 changed files with 572 additions and 173 deletions

View File

@@ -7,6 +7,7 @@ import ExpensesService from "services/Expenses/ExpensesService";
import { IExpenseDTO } from 'interfaces';
import { ServiceError } from "exceptions";
import DynamicListingService from 'services/DynamicListing/DynamicListService';
import { takeWhile } from "lodash";
@Service()
export default class ExpensesController extends BaseController {
@@ -73,10 +74,19 @@ export default class ExpensesController extends BaseController {
'/', [
...this.expensesListSchema,
],
this.validationResult,
asyncMiddleware(this.getExpensesList.bind(this)),
this.dynamicListService.handlerErrorsToResponse,
this.catchServiceErrors,
);
router.get(
'/:id', [
this.expenseParamSchema,
],
this.validationResult,
asyncMiddleware(this.getExpense.bind(this)),
this.catchServiceErrors,
);
return router;
}
@@ -280,6 +290,24 @@ export default class ExpensesController extends BaseController {
}
}
/**
* Retrieve expense details.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async getExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: expenseId } = req.params;
try {
const expense = await this.expensesService.getExpense(tenantId, expenseId);
return res.status(200).send({ expense });
} catch (error) {
next(error);
}
}
/**
* Transform service errors to api response errors.
* @param {Response} res

View File

@@ -1,163 +0,0 @@
import express from 'express';
import {
param,
query,
validationResult,
} from 'express-validator';
import Container from 'typedi';
import fs from 'fs';
import { difference } from 'lodash';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
const fsPromises = fs.promises;
export default {
/**
* Router constructor.
*/
router() {
const router = express.Router();
router.post('/upload',
this.upload.validation,
asyncMiddleware(this.upload.handler));
router.delete('/',
this.delete.validation,
asyncMiddleware(this.delete.handler));
router.get('/',
this.get.validation,
asyncMiddleware(this.get.handler));
return router;
},
/**
* Retrieve all or the given attachment ids.
*/
get: {
validation: [
query('ids'),
],
async handler(req, res) {
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { Media } = req.models;
const media = await Media.query().onBuild((builder) => {
if (req.query.ids) {
const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
builder.whereIn('id', ids);
}
});
return res.status(200).send({ media });
},
},
/**
* Uploads the given attachment file.
*/
upload: {
validation: [
// check('attachment').exists(),
],
async handler(req, res) {
const Logger = Container.get('logger');
if (!req.files.attachment) {
return res.status(400).send({
errors: [{ type: 'ATTACHMENT.NOT.FOUND', code: 200 }],
});
}
const publicPath = 'storage/app/public/';
const attachmentsMimes = ['image/png', 'image/jpeg'];
const { attachment } = req.files;
const { Media } = req.models;
const errorReasons = [];
// Validate the attachment.
if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
errorReasons.push({ type: 'ATTACHMENT.MINETYPE.NOT.SUPPORTED', code: 160 });
}
// Catch all error reasons to response 400.
if (errorReasons.length > 0) {
return res.status(400).send({ errors: errorReasons });
}
try {
await attachment.mv(`${publicPath}${req.organizationId}/${attachment.md5}.png`);
Logger.info('[attachment] uploaded successfully');
} catch (error) {
Logger.info('[attachment] uploading failed.', { error });
}
const media = await Media.query().insert({
attachment_file: `${attachment.md5}.png`,
});
return res.status(200).send({ media });
},
},
/**
* Deletes the given attachment ids from file system and database.
*/
delete: {
validation: [
query('ids').exists().isArray(),
query('ids.*').exists().isNumeric().toInt(),
],
async handler(req, res) {
const Logger = Container.get('logger');
const validationErrors = validationResult(req);
if (!validationErrors.isEmpty()) {
return res.boom.badData(null, {
code: 'validation_error', ...validationErrors,
});
}
const { Media, MediaLink } = req.models;
const ids = Array.isArray(req.query.ids) ? req.query.ids : [req.query.ids];
const media = await Media.query().whereIn('id', ids);
const mediaIds = media.map((m) => m.id);
const notFoundMedia = difference(ids, mediaIds);
if (notFoundMedia.length) {
return res.status(400).send({
errors: [{ type: 'MEDIA.IDS.NOT.FOUND', code: 200, ids: notFoundMedia }],
});
}
const publicPath = 'storage/app/public/';
const tenantPath = `${publicPath}${req.organizationId}`;
const unlinkOpers = [];
media.forEach((mediaModel) => {
const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
unlinkOpers.push(oper);
});
await Promise.all(unlinkOpers).then((resolved) => {
resolved.forEach(() => {
Logger.info('[attachment] file has been deleted.');
});
})
.catch((errors) => {
errors.forEach((error) => {
Logger.info('[attachment] Delete item attachment file delete failed.', { error });
})
});
await MediaLink.query().whereIn('media_id', mediaIds).delete();
await Media.query().whereIn('id', mediaIds).delete();
return res.status(200).send();
},
},
};

View File

@@ -0,0 +1,212 @@
import { Router, Request, Response, NextFunction } from 'express';
import {
param,
query,
check,
} from 'express-validator';
import { camelCase, upperFirst } from 'lodash';
import { Inject, Service } from 'typedi';
import { IMediaLinkDTO } from 'interfaces';
import fs from 'fs';
import asyncMiddleware from 'api/middleware/asyncMiddleware';
import BaseController from './BaseController';
import MediaService from 'services/Media/MediaService';
import { ServiceError } from 'exceptions';
const fsPromises = fs.promises;
@Service()
export default class MediaController extends BaseController {
@Inject()
mediaService: MediaService;
/**
* Router constructor.
*/
router() {
const router = Router();
router.post('/upload', [
...this.uploadValidationSchema,
],
this.validationResult,
asyncMiddleware(this.uploadMedia.bind(this)),
this.handlerServiceErrors,
);
router.post('/:id/link', [
...this.mediaIdParamSchema,
...this.linkValidationSchema,
],
this.validationResult,
asyncMiddleware(this.linkMedia.bind(this)),
this.handlerServiceErrors,
);
router.delete('/', [
...this.deleteValidationSchema,
],
this.validationResult,
asyncMiddleware(this.deleteMedia.bind(this)),
this.handlerServiceErrors,
);
router.get('/:id', [
...this.mediaIdParamSchema,
],
this.validationResult,
asyncMiddleware(this.getMedia.bind(this)),
this.handlerServiceErrors,
);
return router;
}
get uploadValidationSchema() {
return [
// check('attachment'),
check('model_name').optional().trim().escape(),
check('model_id').optional().isNumeric().toInt(),
];
}
get linkValidationSchema() {
return [
check('model_name').exists().trim().escape(),
check('model_id').exists().isNumeric().toInt(),
]
}
get deleteValidationSchema() {
return [
query('ids').exists().isArray(),
query('ids.*').exists().isNumeric().toInt(),
];
}
get mediaIdParamSchema() {
return [
param('id').exists().isNumeric().toInt(),
];
}
/**
* Retrieve all or the given attachment ids.
* @param {Request} req -
* @param {Response} req -
* @param {NextFunction} req -
*/
async getMedia(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: mediaId } = req.params;
try {
const media = await this.mediaService.getMedia(tenantId, mediaId);
return res.status(200).send({ media });
} catch (error) {
next(error);
}
}
/**
* Uploads media.
* @param {Request} req -
* @param {Response} req -
* @param {NextFunction} req -
*/
async uploadMedia(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { attachment } = req.files
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
const modelName = linkMediaDTO.modelName
? upperFirst(camelCase(linkMediaDTO.modelName)) : '';
try {
const media = await this.mediaService.upload(tenantId, attachment, modelName, linkMediaDTO.modelId);
return res.status(200).send({ media_id: media.id });
} catch (error) {
next(error);
}
}
/**
* Deletes the given attachment ids from file system and database.
* @param {Request} req -
* @param {Response} req -
* @param {NextFunction} req -
*/
async deleteMedia(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { ids: mediaIds } = req.query;
try {
await this.mediaService.deleteMedia(tenantId, mediaIds);
return res.status(200).send({
media_ids: mediaIds
});
} catch (error) {
next(error);
}
}
/**
* Links the given media to the specific resource model.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
async linkMedia(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: mediaId } = req.params;
const linkMediaDTO: IMediaLinkDTO = this.matchedBodyData(req);
const modelName = upperFirst(camelCase(linkMediaDTO.modelName));
try {
await this.mediaService.linkMedia(tenantId, mediaId, linkMediaDTO.modelId, modelName);
return res.status(200).send({ media_id: mediaId });
} catch (error) {
next(error);
}
}
/**
* Handler service errors.
* @param {Error} error
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
*/
handlerServiceErrors(error, req: Request, res: Response, next: NextFunction) {
if (error instanceof ServiceError) {
if (error.errorType === 'MINETYPE_NOT_SUPPORTED') {
return res.boom.badRequest(null, {
errors: [{ type: 'MINETYPE_NOT_SUPPORTED', code: 100, }]
});
}
if (error.errorType === 'MEDIA_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'MEDIA_NOT_FOUND', code: 200 }]
});
}
if (error.errorType === 'MODEL_NAME_HAS_NO_MEDIA') {
return res.boom.badRequest(null, {
errors: [{ type: 'MODEL_NAME_HAS_NO_MEDIA', code: 300 }]
});
}
if (error.errorType === 'MODEL_ID_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'MODEL_ID_NOT_FOUND', code: 400 }]
});
}
if (error.errorType === 'MEDIA_IDS_NOT_FOUND') {
return res.boom.badRequest(null, {
errors: [{ type: 'MEDIA_IDS_NOT_FOUND', code: 500 }],
});
}
if (error.errorType === 'MEDIA_LINK_EXISTS') {
return res.boom.badRequest(null, {
errors: [{ type: 'MEDIA_LINK_EXISTS', code: 600 }],
});
}
}
next(error);
}
};