mirror of
https://github.com/bigcapitalhq/bigcapital.git
synced 2026-02-15 20:30:33 +00:00
refactoring: media system.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
},
|
||||
};
|
||||
212
server/src/api/controllers/Media.ts
Normal file
212
server/src/api/controllers/Media.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -98,7 +98,7 @@ export default () => {
|
||||
// dashboard.use('/purchases', Container.get(Purchases).router());
|
||||
dashboard.use('/resources', Container.get(Resources).router());
|
||||
dashboard.use('/exchange_rates', Container.get(ExchangeRates).router());
|
||||
dashboard.use('/media', Media.router());
|
||||
dashboard.use('/media', Container.get(Media).router());
|
||||
|
||||
app.use('/', dashboard);
|
||||
|
||||
|
||||
@@ -66,4 +66,5 @@ export interface IExpensesService {
|
||||
publishBulkExpenses(tenantId: number, expensesIds: number[], authorizedUser: ISystemUser): Promise<void>;
|
||||
|
||||
getExpensesList(tenantId: number, expensesFilter: IExpensesFilter): Promise<{ expenses: IExpense[], pagination: IPaginationMeta, filterMeta: IFilterMeta }>;
|
||||
getExpense(tenantId: number, expenseId: number): Promise<IExpense>;
|
||||
}
|
||||
25
server/src/interfaces/Media.ts
Normal file
25
server/src/interfaces/Media.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
export interface IMedia {
|
||||
id?: number,
|
||||
attachmentFile: string,
|
||||
createdAt?: Date,
|
||||
};
|
||||
|
||||
export interface IMediaLink {
|
||||
mediaId: number,
|
||||
modelName: string,
|
||||
modelId: number,
|
||||
};
|
||||
|
||||
export interface IMediaLinkDTO {
|
||||
modelName: string,
|
||||
modelId: number,
|
||||
};
|
||||
|
||||
export interface IMediaService {
|
||||
linkMedia(tenantId: number, mediaId: number, modelId?: number, modelName?: string): Promise<void>;
|
||||
getMedia(tenantId: number, mediaId: number): Promise<IMedia>;
|
||||
deleteMedia(tenantId: number, mediaId: number | number[]): Promise<void>;
|
||||
upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise<IMedia>;
|
||||
}
|
||||
@@ -24,4 +24,5 @@ export * from './Tenancy';
|
||||
export * from './View';
|
||||
export * from './ManualJournal';
|
||||
export * from './Currency';
|
||||
export * from './ExchangeRate';
|
||||
export * from './ExchangeRate';
|
||||
export * from './Media';
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
import { viewRolesBuilder } from 'lib/ViewRolesBuilder';
|
||||
import Media from './Media';
|
||||
|
||||
export default class Expense extends TenantModel {
|
||||
/**
|
||||
@@ -24,6 +25,11 @@ export default class Expense extends TenantModel {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
|
||||
static get media () {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Model modifiers.
|
||||
*/
|
||||
@@ -55,7 +61,6 @@ export default class Expense extends TenantModel {
|
||||
query.where('payment_account_id', accountId);
|
||||
}
|
||||
},
|
||||
|
||||
viewRolesBuilder(query, conditionals, expression) {
|
||||
viewRolesBuilder(conditionals, expression)(query);
|
||||
},
|
||||
@@ -68,6 +73,7 @@ export default class Expense extends TenantModel {
|
||||
static get relationMappings() {
|
||||
const Account = require('models/Account');
|
||||
const ExpenseCategory = require('models/ExpenseCategory');
|
||||
const Media = require('models/Media');
|
||||
|
||||
return {
|
||||
paymentAccount: {
|
||||
@@ -86,6 +92,18 @@ export default class Expense extends TenantModel {
|
||||
to: 'expense_transaction_categories.expenseId',
|
||||
},
|
||||
},
|
||||
media: {
|
||||
relation: Model.ManyToManyRelation,
|
||||
modelClass: Media.default,
|
||||
join: {
|
||||
from: 'expenses_transactions.id',
|
||||
through: {
|
||||
from: 'media_links.model_id',
|
||||
to: 'media_links.media_id',
|
||||
},
|
||||
to: 'media.id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Model } from 'objection';
|
||||
import TenantModel from 'models/TenantModel';
|
||||
|
||||
export default class Media extends TenantModel {
|
||||
@@ -7,4 +8,29 @@ export default class Media extends TenantModel {
|
||||
static get tableName() {
|
||||
return 'media';
|
||||
}
|
||||
|
||||
/**
|
||||
* Model timestamps.
|
||||
*/
|
||||
get timestamps() {
|
||||
return ['createdAt', 'updatedAt'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Relationship mapping.
|
||||
*/
|
||||
static get relationMappings() {
|
||||
const MediaLink = require('models/MediaLink');
|
||||
|
||||
return {
|
||||
links: {
|
||||
relation: Model.HasManyRelation,
|
||||
modelClass: MediaLink.default,
|
||||
join: {
|
||||
from: 'media.id',
|
||||
to: 'media_links.media_id',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
8
server/src/models/ResourcableModel.js
Normal file
8
server/src/models/ResourcableModel.js
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
export default class ResourceableModel {
|
||||
|
||||
static get resourceable() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export default class ExpensesService implements IExpensesService {
|
||||
this.logger.info('[expenses] trying to get the given payment account.', { tenantId, paymentAccountId });
|
||||
|
||||
const { accountRepository } = this.tenancy.repositories(tenantId);
|
||||
const paymentAccount = await accountRepository.getById(paymentAccountId)
|
||||
const paymentAccount = await accountRepository.findById(paymentAccountId)
|
||||
|
||||
if (!paymentAccount) {
|
||||
this.logger.info('[expenses] the given payment account not found.', { tenantId, paymentAccountId });
|
||||
@@ -460,4 +460,24 @@ export default class ExpensesService implements IExpensesService {
|
||||
dynamicFilter.getResponseMeta(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve expense details.
|
||||
* @param {number} tenantId
|
||||
* @param {number} expenseId
|
||||
* @return {Promise<IExpense>}
|
||||
*/
|
||||
public async getExpense(tenantId: number, expenseId: number): Promise<IExpense> {
|
||||
const { Expense } = this.tenancy.models(tenantId);
|
||||
|
||||
const expense = await Expense.query().findById(expenseId)
|
||||
.withGraphFetched('paymentAccount')
|
||||
.withGraphFetched('media')
|
||||
.withGraphFetched('categories');
|
||||
|
||||
if (!expense) {
|
||||
throw new ServiceError(ERRORS.EXPENSE_NOT_FOUND);
|
||||
}
|
||||
return expense;
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
|
||||
const incomeType = await accountTypeRepository.getByKey('income');
|
||||
const foundAccount = await accountRepository.getById(sellAccountId);
|
||||
const foundAccount = await accountRepository.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
|
||||
@@ -130,7 +130,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
|
||||
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
|
||||
const foundAccount = await accountRepository.getById(costAccountId)
|
||||
const foundAccount = await accountRepository.findById(costAccountId)
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
|
||||
@@ -152,7 +152,7 @@ export default class ItemCategoriesService implements IItemCategoriesService {
|
||||
|
||||
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
|
||||
const otherAsset = await accountTypeRepository.getByKey('other_asset');
|
||||
const foundAccount = await accountRepository.getById(inventoryAccountId);
|
||||
const foundAccount = await accountRepository.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
|
||||
|
||||
@@ -85,7 +85,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate cost account existance.', { tenantId, costAccountId });
|
||||
const COGSType = await accountTypeRepository.getByKey('cost_of_goods_sold');
|
||||
const foundAccount = await accountRepository.getById(costAccountId)
|
||||
const foundAccount = await accountRepository.findById(costAccountId)
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] cost account not found.', { tenantId, costAccountId });
|
||||
@@ -106,7 +106,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate sell account existance.', { tenantId, sellAccountId });
|
||||
const incomeType = await accountTypeRepository.getByKey('income');
|
||||
const foundAccount = await accountRepository.getById(sellAccountId);
|
||||
const foundAccount = await accountRepository.findById(sellAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] sell account not found.', { tenantId, sellAccountId });
|
||||
@@ -127,7 +127,7 @@ export default class ItemsService implements IItemsService {
|
||||
|
||||
this.logger.info('[items] validate inventory account existance.', { tenantId, inventoryAccountId });
|
||||
const otherAsset = await accountTypeRepository.getByKey('other_asset');
|
||||
const foundAccount = await accountRepository.getById(inventoryAccountId);
|
||||
const foundAccount = await accountRepository.findById(inventoryAccountId);
|
||||
|
||||
if (!foundAccount) {
|
||||
this.logger.info('[items] inventory account not found.', { tenantId, inventoryAccountId });
|
||||
|
||||
223
server/src/services/Media/MediaService.ts
Normal file
223
server/src/services/Media/MediaService.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import fs from 'fs';
|
||||
import { Service, Inject } from 'typedi';
|
||||
import TenancyService from 'services/Tenancy/TenancyService';
|
||||
import { ServiceError } from "exceptions";
|
||||
import { IMedia, IMediaService } from 'interfaces';
|
||||
import { difference } from 'lodash';
|
||||
|
||||
const fsPromises = fs.promises;
|
||||
|
||||
const ERRORS = {
|
||||
MINETYPE_NOT_SUPPORTED: 'MINETYPE_NOT_SUPPORTED',
|
||||
MEDIA_NOT_FOUND: 'MEDIA_NOT_FOUND',
|
||||
MODEL_NAME_HAS_NO_MEDIA: 'MODEL_NAME_HAS_NO_MEDIA',
|
||||
MODEL_ID_NOT_FOUND: 'MODEL_ID_NOT_FOUND',
|
||||
MEDIA_IDS_NOT_FOUND: 'MEDIA_IDS_NOT_FOUND',
|
||||
MEDIA_LINK_EXISTS: 'MEDIA_LINK_EXISTS'
|
||||
}
|
||||
const publicPath = 'storage/app/public/';
|
||||
const attachmentsMimes = ['image/png', 'image/jpeg'];
|
||||
|
||||
@Service()
|
||||
export default class MediaService implements IMediaService {
|
||||
@Inject('logger')
|
||||
logger: any;
|
||||
|
||||
@Inject()
|
||||
tenancy: TenancyService;
|
||||
|
||||
@Inject('repositories')
|
||||
sysRepositories: any;
|
||||
|
||||
/**
|
||||
* Retrieve media model or throw not found error
|
||||
* @param tenantId
|
||||
* @param mediaId
|
||||
*/
|
||||
async getMediaOrThrowError(tenantId: number, mediaId: number) {
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
const foundMedia = await Media.query().findById(mediaId);
|
||||
|
||||
if (!foundMedia) {
|
||||
throw new ServiceError(ERRORS.MEDIA_NOT_FOUND);
|
||||
}
|
||||
return foundMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retreive media models by the given ids or throw not found error.
|
||||
* @param {number} tenantId
|
||||
* @param {number[]} mediaIds
|
||||
*/
|
||||
async getMediaByIdsOrThrowError(tenantId: number, mediaIds: number[]) {
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
const foundMedia = await Media.query().whereIn('id', mediaIds);
|
||||
|
||||
const storedMediaIds = foundMedia.map((m) => m.id);
|
||||
const notFoundMedia = difference(mediaIds, storedMediaIds);
|
||||
|
||||
if (notFoundMedia.length > 0) {
|
||||
throw new ServiceError(ERRORS.MEDIA_IDS_NOT_FOUND);
|
||||
}
|
||||
return foundMedia;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the model name and id.
|
||||
* @param {number} tenantId
|
||||
* @param {string} modelName
|
||||
* @param {number} modelId
|
||||
*/
|
||||
async validateModelNameAndIdExistance(tenantId: number, modelName: string, modelId: number) {
|
||||
const models = this.tenancy.models(tenantId);
|
||||
this.logger.info('[media] trying to validate model name and id.', { tenantId, modelName, modelId });
|
||||
|
||||
if (!models[modelName]) {
|
||||
this.logger.info('[media] model name not found.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
|
||||
}
|
||||
if (!models[modelName].media) {
|
||||
this.logger.info('[media] model is not media-able.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_NAME_HAS_NO_MEDIA);
|
||||
}
|
||||
|
||||
const foundModel = await models[modelName].query().findById(modelId);
|
||||
|
||||
if (!foundModel) {
|
||||
this.logger.info('[media] model is not found.', { tenantId, modelName, modelId });
|
||||
throw new ServiceError(ERRORS.MODEL_ID_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the media existance.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @param {number} modelId
|
||||
* @param {string} modelName
|
||||
*/
|
||||
async validateMediaLinkExistance(
|
||||
tenantId: number,
|
||||
mediaId: number,
|
||||
modelId: number,
|
||||
modelName: string
|
||||
) {
|
||||
const { MediaLink } = this.tenancy.models(tenantId);
|
||||
|
||||
const foundMediaLinks = await MediaLink.query()
|
||||
.where('media_id', mediaId)
|
||||
.where('model_id', modelId)
|
||||
.where('model_name', modelName);
|
||||
|
||||
if (foundMediaLinks.length > 0) {
|
||||
throw new ServiceError(ERRORS.MEDIA_LINK_EXISTS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Links the given media to the specific media-able model resource.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @param {number} modelId
|
||||
* @param {string} modelType
|
||||
*/
|
||||
async linkMedia(tenantId: number, mediaId: number, modelId: number, modelName: string) {
|
||||
this.logger.info('[media] trying to link media.', { tenantId, mediaId, modelId, modelName });
|
||||
const { MediaLink } = this.tenancy.models(tenantId);
|
||||
await this.validateMediaLinkExistance(tenantId, mediaId, modelId, modelName);
|
||||
|
||||
const media = await this.getMediaOrThrowError(tenantId, mediaId);
|
||||
await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
|
||||
|
||||
await MediaLink.query().insert({ mediaId, modelId, modelName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve media metadata.
|
||||
* @param {number} tenantId - Tenant id.
|
||||
* @param {number} mediaId - Media id.
|
||||
* @return {Promise<IMedia>}
|
||||
*/
|
||||
public async getMedia(tenantId: number, mediaId: number): Promise<IMedia> {
|
||||
this.logger.info('[media] try to get media.', { tenantId, mediaId });
|
||||
return this.getMediaOrThrowError(tenantId, mediaId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given media.
|
||||
* @param {number} tenantId
|
||||
* @param {number} mediaId
|
||||
* @return {Promise<void>}
|
||||
*/
|
||||
public async deleteMedia(tenantId: number, mediaId: number|number[]): Promise<void> {
|
||||
const { Media, MediaLink } = this.tenancy.models(tenantId);
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
|
||||
this.logger.info('[media] trying to delete media.', { tenantId, mediaId });
|
||||
|
||||
const mediaIds = Array.isArray(mediaId) ? mediaId : [mediaId];
|
||||
|
||||
const tenant = await tenantRepository.getById(tenantId);
|
||||
const media = await this.getMediaByIdsOrThrowError(tenantId, mediaIds);
|
||||
|
||||
const tenantPath = `${publicPath}${tenant.organizationId}`;
|
||||
const unlinkOpers = [];
|
||||
|
||||
media.forEach((mediaModel) => {
|
||||
const oper = fsPromises.unlink(`${tenantPath}/${mediaModel.attachmentFile}`);
|
||||
unlinkOpers.push(oper);
|
||||
});
|
||||
await Promise.all(unlinkOpers)
|
||||
.then((resolved) => {
|
||||
resolved.forEach(() => {
|
||||
this.logger.info('[attachment] file has been deleted.');
|
||||
});
|
||||
})
|
||||
.catch((errors) => {
|
||||
this.logger.info('[attachment] Delete item attachment file delete failed.', { errors });
|
||||
});
|
||||
await MediaLink.query().whereIn('media_id', mediaIds).delete();
|
||||
await Media.query().whereIn('id', mediaIds).delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads the given attachment.
|
||||
* @param {number} tenantId -
|
||||
* @param {any} attachment -
|
||||
* @return {Promise<IMedia>}
|
||||
*/
|
||||
public async upload(tenantId: number, attachment: any, modelName?: string, modelId?: number): Promise<IMedia> {
|
||||
const { tenantRepository } = this.sysRepositories;
|
||||
const { Media } = this.tenancy.models(tenantId);
|
||||
|
||||
this.logger.info('[media] trying to upload media.', { tenantId });
|
||||
|
||||
const tenant = await tenantRepository.getById(tenantId);
|
||||
const fileName = `${attachment.md5}.png`;
|
||||
|
||||
// Validate the attachment.
|
||||
if (attachment && attachmentsMimes.indexOf(attachment.mimetype) === -1) {
|
||||
throw new ServiceError(ERRORS.MINETYPE_NOT_SUPPORTED);
|
||||
}
|
||||
if (modelName && modelId) {
|
||||
await this.validateModelNameAndIdExistance(tenantId, modelName, modelId);
|
||||
}
|
||||
try {
|
||||
await attachment.mv(`${publicPath}${tenant.organizationId}/${fileName}`);
|
||||
this.logger.info('[attachment] uploaded successfully');
|
||||
} catch (error) {
|
||||
this.logger.info('[attachment] uploading failed.', { error });
|
||||
}
|
||||
const media = await Media.query().insertGraph({
|
||||
attachmentFile: `${fileName}`,
|
||||
...(modelName && modelId) ? {
|
||||
links: [{
|
||||
modelName,
|
||||
modelId,
|
||||
}]
|
||||
} : {},
|
||||
});
|
||||
this.logger.info('[media] uploaded successfully.', { tenantId, fileName, modelName, modelId });
|
||||
return media;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user