Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: upload and attach documents #461

Merged
merged 11 commits into from
May 30, 2024
5 changes: 5 additions & 0 deletions packages/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
"bigcapital": "./bin/bigcapital.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.576.0",
"@aws-sdk/s3-request-presigner": "^3.583.0",
"@casl/ability": "^5.4.3",
"@hapi/boom": "^7.4.3",
"@lemonsqueezy/lemonsqueezy.js": "^2.2.0",
Expand Down Expand Up @@ -73,12 +75,14 @@
"lru-cache": "^6.0.0",
"mathjs": "^9.4.0",
"memory-cache": "^0.2.0",
"mime-types": "^2.1.35",
"moment": "^2.24.0",
"moment-range": "^4.0.2",
"moment-timezone": "^0.5.43",
"mongodb": "^6.1.0",
"mongoose": "^5.10.0",
"multer": "1.4.5-lts.1",
"multer-s3": "^3.0.1",
"mustache": "^3.0.3",
"mysql": "^2.17.1",
"mysql2": "^1.6.5",
Expand Down Expand Up @@ -113,6 +117,7 @@
},
"devDependencies": {
"@types/lodash": "^4.14.158",
"@types/multer": "^1.4.11",
"@types/ramda": "^0.27.64",
"@typescript-eslint/eslint-plugin": "^5.50.0",
"@typescript-eslint/parser": "^5.50.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import mime from 'mime-types';
import { Service, Inject } from 'typedi';
import { Router, Response } from 'express';
import { body, param } from 'express-validator';
import BaseController from '@/api/controllers/BaseController';
import { Request } from 'express-validator/src/base';
import { AttachmentsApplication } from '@/services/Attachments/AttachmentsApplication';

@Service()
export class AttachmentsController extends BaseController {
@Inject()
private attachmentsApplication: AttachmentsApplication;

/**
* Router constructor.
*/
public router() {
const router = Router();

router.post(
'/',
this.attachmentsApplication.uploadPipeline.single('file'),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate the file should exist.

this.uploadAttachment.bind(this)
);
router.delete(
'/:id',
[param('id').exists()],
this.validationResult,
this.deleteAttachment.bind(this)
);
router.get(
'/:id',
[param('id').exists()],
this.validationResult,
this.getAttachment.bind(this)
);
router.post(
'/:id/link',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated code.

);
router.post(
'/:id/link',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult,
this.linkDocument.bind(this)
);
router.post(
'/:id/unlink',
[body('modelRef').exists(), body('modelId').exists()],
this.validationResult,
this.unlinkDocument.bind(this)
);
router.get(
'/:id/presigned-url',
[param('id').exists()],
this.validationResult,
this.getAttachmentPresignedUrl.bind(this)
);

return router;
}

/**
* Uploads the attachments to S3 and store the file metadata to DB.
* @param {Request} req
* @param {Response} res
* @param {Function} next
* @returns
*/
private async uploadAttachment(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const file = req.file;

try {
const data = await this.attachmentsApplication.upload(tenantId, file);

return res.status(200).send({
status: 200,
message: 'The document has uploaded successfully.',
data,
});
} catch (error) {
next(error);
}
}

/**
*
abouolia marked this conversation as resolved.
Show resolved Hide resolved
* @param {Request} req
* @param {Response} res
* @param next
*/
private async getAttachment(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id } = req.params;

try {
const data = await this.attachmentsApplication.get(tenantId, id);

const byte = await data.Body.transformToByteArray();
const extension = mime.extension(data.ContentType);
const buffer = Buffer.from(byte);

res.set(
'Content-Disposition',
`filename="${req.params.id}.${extension}"`
);
res.set('Content-Type', data.ContentType);
res.send(buffer);
} catch (error) {
next(error);
}
}

/**
* Deletes the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
private async deleteAttachment(req: Request, res: Response, next: Function) {
abouolia marked this conversation as resolved.
Show resolved Hide resolved
const { tenantId } = req;
const { id: documentId } = req.params;

try {
await this.attachmentsApplication.delete(tenantId, documentId);

return res.status(200).send({
status: 200,
message: 'The document has been delete successfully.',
});
} catch (error) {
next(error);
}
}

/**
* Links the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
private async linkDocument(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id: documentId } = req.params;
const { modelRef, modelId } = this.matchedBodyData(req);

try {
await this.attachmentsApplication.link(
tenantId,
documentId,
modelRef,
modelId
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
} catch (error) {
next(error);
}
}

/**
* Links the given document key.
* @param {Request} req
* @param {Response} res
* @param {NextFunction} next
* @returns
*/
private async unlinkDocument(req: Request, res: Response, next: Function) {
const { tenantId } = req;
const { id: documentId } = req.params;
const { modelRef, modelId } = this.matchedBodyData(req);

try {
await this.attachmentsApplication.link(
tenantId,
documentId,
modelRef,
modelId
);
return res.status(200).send({
status: 200,
message: 'The document has been linked successfully.',
});
} catch (error) {
next(error);
}
}

/**
* Retreives the presigned url of the given attachment key.
* @param {Request} req
* @param {Response} res
* @param next
*/
private async getAttachmentPresignedUrl(
req: Request,
res: Response,
next: any
) {
const { id: documentKey } = req.params;

try {
const presignedUrl = await this.attachmentsApplication.getPresignedUrl(
documentKey
);
return res.status(200).send({ presignedUrl });
} catch (error) {
next(error);
}
}
}
24 changes: 19 additions & 5 deletions packages/server/src/api/controllers/Expenses/Expenses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export class ExpensesController extends BaseController {
/**
* Expense DTO schema.
*/
get expenseDTOSchema() {
private get expenseDTOSchema() {
return [
check('reference_no')
.optional({ nullable: true })
Expand Down Expand Up @@ -130,6 +130,9 @@ export class ExpensesController extends BaseController {
.optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down Expand Up @@ -183,6 +186,9 @@ export class ExpensesController extends BaseController {
.optional({ nullable: true })
.isInt({ max: DATATYPES_LENGTH.INT_10 })
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down Expand Up @@ -269,7 +275,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async deleteExpense(req: Request, res: Response, next: NextFunction) {
private async deleteExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId, user } = req;
const { id: expenseId } = req.params;

Expand All @@ -291,7 +297,11 @@ export class ExpensesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async publishExpense(req: Request, res: Response, next: NextFunction) {
private async publishExpense(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId, user } = req;
const { id: expenseId } = req.params;

Expand All @@ -313,7 +323,11 @@ export class ExpensesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async getExpensesList(req: Request, res: Response, next: NextFunction) {
private async getExpensesList(
req: Request,
res: Response,
next: NextFunction
) {
const { tenantId } = req;
const filter = {
sortOrder: 'desc',
Expand Down Expand Up @@ -343,7 +357,7 @@ export class ExpensesController extends BaseController {
* @param {Response} res
* @param {NextFunction} next
*/
async getExpense(req: Request, res: Response, next: NextFunction) {
private async getExpense(req: Request, res: Response, next: NextFunction) {
const { tenantId } = req;
const { id: expenseId } = req.params;

Expand Down
3 changes: 3 additions & 0 deletions packages/server/src/api/controllers/ManualJournals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ export default class ManualJournalsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down
7 changes: 6 additions & 1 deletion packages/server/src/api/controllers/Purchases/Bills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ export default class BillsController extends BaseController {
check('is_inclusive_tax').default(false).isBoolean().toBoolean(),

check('entries').isArray({ min: 1 }),

check('entries.*.index').exists().isNumeric().toInt(),
check('entries.*.item_id').exists().isNumeric().toInt(),
check('entries.*.rate').exists().isNumeric().toFloat(),
Expand Down Expand Up @@ -148,6 +147,9 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down Expand Up @@ -190,6 +192,9 @@ export default class BillsController extends BaseController {
.optional({ nullable: true })
.isBoolean()
.toBoolean(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ export default class BillsPayments extends BaseController {
check('entries.*.index').optional().isNumeric().toInt(),
check('entries.*.bill_id').exists().isNumeric().toInt(),
check('entries.*.payment_amount').exists().isNumeric().toFloat(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down
6 changes: 6 additions & 0 deletions packages/server/src/api/controllers/Purchases/VendorCredit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down Expand Up @@ -228,6 +231,9 @@ export default class VendorCreditController extends BaseController {
.optional({ nullable: true })
.isNumeric()
.toInt(),

check('attachments').isArray().optional(),
check('attachments.*.key').exists().isString(),
];
}

Expand Down
Loading
Loading