From 1ed77dd5ed3c070007a5da88048c94e09323589c Mon Sep 17 00:00:00 2001 From: Ahmed Bouhuolia Date: Fri, 4 Apr 2025 20:56:31 +0200 Subject: [PATCH] refactor(nestjs): attachments and s3 modules --- .../server-nest/src/modules/App/App.module.ts | 6 +- .../modules/Attachments/Attachment.module.ts | 37 ++++ .../Attachments/AttachmentTransformer.ts | 19 +++ .../Attachments/AttachmentsApplication.ts | 80 +++++++++ .../modules/Attachments/DeleteAttachment.ts | 54 ++++++ .../src/modules/Attachments/GetAttachment.ts | 30 ++++ .../Attachments/GetAttachmentPresignedUrl.ts | 36 ++++ .../src/modules/Attachments/LinkAttachment.ts | 82 +++++++++ .../modules/Attachments/S3UploadPipeline.ts | 59 +++++++ .../modules/Attachments/UnlinkAttachment.ts | 122 ++++++++++++++ .../src/modules/Attachments/UploadDocument.ts | 28 +++ .../Attachments/ValidateAttachments.ts | 32 ++++ .../src/modules/Attachments/_utils.ts | 14 ++ .../src/modules/Attachments/constants.ts | 5 + .../Attachments/events/AttachmentsOnBills.ts | 151 +++++++++++++++++ .../events/AttachmentsOnCreditNote.ts | 155 +++++++++++++++++ .../events/AttachmentsOnExpenses.ts | 151 +++++++++++++++++ .../events/AttachmentsOnManualJournals.ts | 157 +++++++++++++++++ .../events/AttachmentsOnPaymentsMade.ts | 157 +++++++++++++++++ .../events/AttachmentsOnPaymentsReceived.ts | 157 +++++++++++++++++ .../events/AttachmentsOnSaleEstimates.ts | 154 +++++++++++++++++ .../events/AttachmentsOnSaleInvoice.ts | 159 ++++++++++++++++++ .../events/AttachmentsOnSaleReceipts.ts | 157 +++++++++++++++++ .../events/AttachmentsOnVendorCredits.ts | 157 +++++++++++++++++ .../Attachments/models/Document.model.ts | 22 +++ .../Attachments/models/DocumentLink.model.ts | 39 +++++ .../src/modules/Attachments/utils.ts | 9 + .../server-nest/src/modules/S3/S3.module.ts | 29 ++++ 28 files changed, 2257 insertions(+), 1 deletion(-) create mode 100644 packages/server-nest/src/modules/Attachments/Attachment.module.ts create mode 100644 packages/server-nest/src/modules/Attachments/AttachmentTransformer.ts create mode 100644 packages/server-nest/src/modules/Attachments/AttachmentsApplication.ts create mode 100644 packages/server-nest/src/modules/Attachments/DeleteAttachment.ts create mode 100644 packages/server-nest/src/modules/Attachments/GetAttachment.ts create mode 100644 packages/server-nest/src/modules/Attachments/GetAttachmentPresignedUrl.ts create mode 100644 packages/server-nest/src/modules/Attachments/LinkAttachment.ts create mode 100644 packages/server-nest/src/modules/Attachments/S3UploadPipeline.ts create mode 100644 packages/server-nest/src/modules/Attachments/UnlinkAttachment.ts create mode 100644 packages/server-nest/src/modules/Attachments/UploadDocument.ts create mode 100644 packages/server-nest/src/modules/Attachments/ValidateAttachments.ts create mode 100644 packages/server-nest/src/modules/Attachments/_utils.ts create mode 100644 packages/server-nest/src/modules/Attachments/constants.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnBills.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnCreditNote.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnExpenses.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnManualJournals.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsMade.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsReceived.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleEstimates.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleInvoice.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleReceipts.ts create mode 100644 packages/server-nest/src/modules/Attachments/events/AttachmentsOnVendorCredits.ts create mode 100644 packages/server-nest/src/modules/Attachments/models/Document.model.ts create mode 100644 packages/server-nest/src/modules/Attachments/models/DocumentLink.model.ts create mode 100644 packages/server-nest/src/modules/Attachments/utils.ts create mode 100644 packages/server-nest/src/modules/S3/S3.module.ts diff --git a/packages/server-nest/src/modules/App/App.module.ts b/packages/server-nest/src/modules/App/App.module.ts index 62ca63bcd..2f31cb1fc 100644 --- a/packages/server-nest/src/modules/App/App.module.ts +++ b/packages/server-nest/src/modules/App/App.module.ts @@ -77,6 +77,8 @@ import { PaymentServicesModule } from '../PaymentServices/PaymentServices.module import { AuthModule } from '../Auth/Auth.module'; import { TenancyModule } from '../Tenancy/Tenancy.module'; import { LoopsModule } from '../Loops/Loops.module'; +import { AttachmentsModule } from '../Attachments/Attachment.module'; +import { S3Module } from '../S3/S3.module'; @Module({ imports: [ @@ -188,7 +190,9 @@ import { LoopsModule } from '../Loops/Loops.module'; OrganizationModule, TenantDBManagerModule, PaymentServicesModule, - LoopsModule + LoopsModule, + AttachmentsModule, + S3Module ], controllers: [AppController], providers: [ diff --git a/packages/server-nest/src/modules/Attachments/Attachment.module.ts b/packages/server-nest/src/modules/Attachments/Attachment.module.ts new file mode 100644 index 000000000..db8090ecb --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/Attachment.module.ts @@ -0,0 +1,37 @@ +import { Module } from "@nestjs/common"; +import { S3Module } from "../S3/S3.module"; +import { DeleteAttachment } from "./DeleteAttachment"; +import { GetAttachment } from "./GetAttachment"; +import { getAttachmentPresignedUrl } from "./GetAttachmentPresignedUrl"; +import { LinkAttachment } from "./LinkAttachment"; +import { UnlinkAttachment } from "./UnlinkAttachment"; +import { ValidateAttachments } from "./ValidateAttachments"; +import { AttachmentsOnBillPayments } from "./events/AttachmentsOnPaymentsMade"; +import { AttachmentsOnBills } from "./events/AttachmentsOnBills"; +import { AttachmentsOnCreditNote } from "./events/AttachmentsOnCreditNote"; +import { AttachmentsOnExpenses } from "./events/AttachmentsOnExpenses"; +import { AttachmentsOnPaymentsReceived } from "./events/AttachmentsOnPaymentsReceived"; +import { AttachmentsOnManualJournals } from "./events/AttachmentsOnManualJournals"; +import { AttachmentsOnVendorCredits } from "./events/AttachmentsOnVendorCredits"; + + + +@Module({ + imports: [S3Module], + providers: [ + DeleteAttachment, + GetAttachment, + getAttachmentPresignedUrl, + LinkAttachment, + UnlinkAttachment, + ValidateAttachments, + AttachmentsOnBillPayments, + AttachmentsOnBills, + AttachmentsOnCreditNote, + AttachmentsOnExpenses, + AttachmentsOnPaymentsReceived, + AttachmentsOnManualJournals, + AttachmentsOnVendorCredits + ] +}) +export class AttachmentsModule {} \ No newline at end of file diff --git a/packages/server-nest/src/modules/Attachments/AttachmentTransformer.ts b/packages/server-nest/src/modules/Attachments/AttachmentTransformer.ts new file mode 100644 index 000000000..af9a7a8a6 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/AttachmentTransformer.ts @@ -0,0 +1,19 @@ +import { Transformer } from "../Transformer/Transformer"; + +export class AttachmentTransformer extends Transformer{ + /** + * Exclude attributes. + * @returns {string[]} + */ + public excludeAttributes = (): string[] => { + return ['id', 'createdAt']; + }; + + /** + * Includeded attributes. + * @returns {string[]} + */ + public includeAttributes = (): string[] => { + return []; + }; +} diff --git a/packages/server-nest/src/modules/Attachments/AttachmentsApplication.ts b/packages/server-nest/src/modules/Attachments/AttachmentsApplication.ts new file mode 100644 index 000000000..043a28922 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/AttachmentsApplication.ts @@ -0,0 +1,80 @@ +import { Injectable } from '@nestjs/common'; +import { UploadDocument } from './UploadDocument'; +import { DeleteAttachment } from './DeleteAttachment'; +import { GetAttachment } from './GetAttachment'; +import { LinkAttachment } from './LinkAttachment'; +import { UnlinkAttachment } from './UnlinkAttachment'; +import { getAttachmentPresignedUrl } from './GetAttachmentPresignedUrl'; + +@Injectable() +export class AttachmentsApplication { + constructor( + private readonly uploadDocumentService: UploadDocument, + private readonly deleteDocumentService: DeleteAttachment, + private readonly getDocumentService: GetAttachment, + private readonly linkDocumentService: LinkAttachment, + private readonly unlinkDocumentService: UnlinkAttachment, + private readonly getPresignedUrlService: getAttachmentPresignedUrl, + ) {} + + /** + * Saves the metadata of uploaded document to S3 on database. + * @param {number} tenantId + * @param {} file + * @returns {Promise} + */ + public upload(file: any) { + return this.uploadDocumentService.upload(file); + } + + /** + * Deletes the give file attachment file key. + * @param {string} documentKey + * @returns {Promise} + */ + public delete(documentKey: string) { + return this.deleteDocumentService.delete(documentKey); + } + + /** + * Retrieves the document data. + * @param {number} tenantId + * @param {string} documentKey + */ + public get(documentKey: string) { + return this.getDocumentService.getAttachment(documentKey); + } + + /** + * Links the given document to resource model. + * @param {string} filekey + * @param {string} modelRef + * @param {number} modelId + * @returns + */ + public link(filekey: string, modelRef: string, modelId: number) { + return this.linkDocumentService.link(filekey, modelRef, modelId); + } + + /** + * Unlinks the given document from resource model. + * @param {number} tenantId + * @param {string} filekey + * @param {string} modelRef + * @param {number} modelId + * @returns + */ + public unlink(filekey: string, modelRef: string, modelId: number) { + return this.unlinkDocumentService.unlink(filekey, modelRef, modelId); + } + + /** + * Retrieves the presigned url of the given attachment key. + * @param {number} tenantId + * @param {string} key + * @returns {Promise} + */ + public getPresignedUrl(key: string): Promise { + return this.getPresignedUrlService.getPresignedUrl(key); + } +} diff --git a/packages/server-nest/src/modules/Attachments/DeleteAttachment.ts b/packages/server-nest/src/modules/Attachments/DeleteAttachment.ts new file mode 100644 index 000000000..b2f3fc5b3 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/DeleteAttachment.ts @@ -0,0 +1,54 @@ +import { DeleteObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { Knex } from 'knex'; +import { Inject, Injectable } from '@nestjs/common'; +import { UnitOfWork } from '../Tenancy/TenancyDB/UnitOfWork.service'; +import { ConfigService } from '@nestjs/config'; +import { S3_CLIENT } from '../S3/S3.module'; +import { DocumentModel } from './models/Document.model'; +import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { DocumentLinkModel } from './models/DocumentLink.model'; + +@Injectable() +export class DeleteAttachment { + constructor( + private readonly uow: UnitOfWork, + private readonly configService: ConfigService, + + @Inject(S3_CLIENT) + private readonly s3Client: S3Client, + + @Inject(Document.name) + private readonly documentModel: TenantModelProxy, + + @Inject(DocumentLinkModel.name) + private readonly documentLinkModel: TenantModelProxy + ) { + + } + + /** + * Deletes the give file attachment file key. + * @param {string} filekey + */ + async delete(filekey: string): Promise { + const params = { + Bucket: this.configService.get('s3.bucket'), + Key: filekey, + }; + await this.s3Client.send(new DeleteObjectCommand(params)); + + const foundDocument = await this.documentModel().query() + .findOne('key', filekey) + .throwIfNotFound(); + + await this.uow.withTransaction(async (trx: Knex.Transaction) => { + // Delete all document links + await this.documentLinkModel().query(trx) + .where('documentId', foundDocument.id) + .delete(); + + // Delete thedocument. + await this.documentModel().query(trx).findById(foundDocument.id).delete(); + }); + } +} diff --git a/packages/server-nest/src/modules/Attachments/GetAttachment.ts b/packages/server-nest/src/modules/Attachments/GetAttachment.ts new file mode 100644 index 000000000..2ec83fc3f --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/GetAttachment.ts @@ -0,0 +1,30 @@ +import { GetObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { Inject, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { S3_CLIENT } from '../S3/S3.module'; + +@Injectable() +export class GetAttachment { + constructor( + private readonly configService: ConfigService, + + @Inject(S3_CLIENT) + private readonly s3: S3Client + ) { + + } + /** + * Retrieves data of the given document key. + * @param {number} tenantId + * @param {string} filekey + */ + async getAttachment(filekey: string) { + const params = { + Bucket: this.configService.get('s3.bucket'), + Key: filekey, + }; + const data = await this.s3.send(new GetObjectCommand(params)); + + return data; + } +} diff --git a/packages/server-nest/src/modules/Attachments/GetAttachmentPresignedUrl.ts b/packages/server-nest/src/modules/Attachments/GetAttachmentPresignedUrl.ts new file mode 100644 index 000000000..ea4a89434 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/GetAttachmentPresignedUrl.ts @@ -0,0 +1,36 @@ +import { GetObjectCommand } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { Injectable } from '@nestjs/common'; +import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { DocumentModel } from './models/Document.model'; + +@Injectable() +export class getAttachmentPresignedUrl { + constructor( + private readonly documentModel: TenantModelProxy + ) {} + + /** + * Retrieves the presigned url of the given attachment key with the original filename. + * @param {number} tenantId + * @param {string} key + * @returns {string} + */ + async getPresignedUrl(tenantId: number, key: string) { + const foundDocument = await this.documentModel().query().findOne({ key }); + + let ResponseContentDisposition = 'attachment'; + if (foundDocument && foundDocument.originName) { + ResponseContentDisposition += `; filename="${foundDocument.originName}"`; + } + + const command = new GetObjectCommand({ + Bucket: config.s3.bucket, + Key: key, + ResponseContentDisposition, + }); + const signedUrl = await getSignedUrl(s3, command, { expiresIn: 300 }); + + return signedUrl; + } +} diff --git a/packages/server-nest/src/modules/Attachments/LinkAttachment.ts b/packages/server-nest/src/modules/Attachments/LinkAttachment.ts new file mode 100644 index 000000000..92e6f3362 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/LinkAttachment.ts @@ -0,0 +1,82 @@ +import { Inject, Service } from 'typedi'; +import bluebird from 'bluebird'; +import { Knex } from 'knex'; +import { + validateLinkModelEntryExists, + validateLinkModelExists, +} from './Attachments/_utils'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; + +@Service() +export class LinkAttachment { + @Inject() + private tenancy: HasTenancyService; + + /** + * Links the given file key to the given model type and id. + * @param {number} tenantId + * @param {string} filekey + * @param {string} modelRef + * @param {number} modelId + * @returns {Promise} + */ + async link( + tenantId: number, + filekey: string, + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ) { + const { DocumentLink, Document, ...models } = this.tenancy.models(tenantId); + const LinkModel = models[modelRef]; + validateLinkModelExists(LinkModel); + + const foundFile = await Document.query(trx) + .findOne('key', filekey) + .throwIfNotFound(); + + const foundLinkModel = await LinkModel.query(trx).findById(modelId); + validateLinkModelEntryExists(foundLinkModel); + + const foundLinks = await DocumentLink.query(trx) + .where('modelRef', modelRef) + .where('modelId', modelId) + .where('documentId', foundFile.id); + + if (foundLinks.length > 0) { + throw new ServiceError(ERRORS.DOCUMENT_LINK_ALREADY_LINKED); + } + await DocumentLink.query(trx).insert({ + modelRef, + modelId, + documentId: foundFile.id, + }); + } + + /** + * Links the given file keys to the given model type and id. + * @param {number} tenantId + * @param {string[]} filekeys + * @param {string} modelRef + * @param {number} modelId + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + async bulkLink( + tenantId: number, + filekeys: string[], + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ) { + return bluebird.each(filekeys, async (fieldKey: string) => { + try { + await this.link(tenantId, fieldKey, modelRef, modelId, trx); + } catch { + // Ignore catching exceptions in bulk action. + } + }); + } +} diff --git a/packages/server-nest/src/modules/Attachments/S3UploadPipeline.ts b/packages/server-nest/src/modules/Attachments/S3UploadPipeline.ts new file mode 100644 index 000000000..26d00f124 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/S3UploadPipeline.ts @@ -0,0 +1,59 @@ +import { NextFunction, Request, Response } from 'express'; +import multer from 'multer'; +import type { Multer } from 'multer'; +import multerS3 from 'multer-s3'; +import { s3 } from '@/lib/S3/S3'; +import { Service } from 'typedi'; +import config from '@/config'; + +@Service() +export class AttachmentUploadPipeline { + /** + * Middleware to ensure that S3 configuration is properly set before proceeding. + * This function checks if the necessary S3 configuration keys are present and throws an error if any are missing. + * @param req The HTTP request object. + * @param res The HTTP response object. + * @param next The callback to pass control to the next middleware function. + */ + public validateS3Configured(req: Request, res: Response, next: NextFunction) { + if ( + !config.s3.region || + !config.s3.accessKeyId || + !config.s3.secretAccessKey + ) { + const missingKeys = []; + if (!config.s3.region) missingKeys.push('region'); + if (!config.s3.accessKeyId) missingKeys.push('accessKeyId'); + if (!config.s3.secretAccessKey) missingKeys.push('secretAccessKey'); + const missing = missingKeys.join(', '); + + throw new Error(`S3 configuration error: Missing ${missing}`); + } + next(); + } + + /** + * Express middleware for uploading attachments to an S3 bucket. + * It utilizes the multer middleware for handling multipart/form-data, specifically for file uploads. + */ + public uploadPipeline(): Multer { + return multer({ + storage: multerS3({ + s3, + bucket: config.s3.bucket, + contentType: multerS3.AUTO_CONTENT_TYPE, + metadata: function (req, file, cb) { + cb(null, { fieldName: file.fieldname }); + }, + key: function (req, file, cb) { + cb(null, Date.now().toString()); + }, + acl: function(req, file, cb) { + // Conditionally set file to public or private based on isPublic flag + const aclValue = true ? 'public-read' : 'private'; + cb(null, aclValue); // Set ACL based on the isPublic flag + } + }), + }); + } +} diff --git a/packages/server-nest/src/modules/Attachments/UnlinkAttachment.ts b/packages/server-nest/src/modules/Attachments/UnlinkAttachment.ts new file mode 100644 index 000000000..f77360260 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/UnlinkAttachment.ts @@ -0,0 +1,122 @@ +import bluebird from 'bluebird'; +import { + validateLinkModelEntryExists, + validateLinkModelExists, +} from './_utils'; +import { Knex } from 'knex'; +import { difference } from 'lodash'; +import { Injectable } from '@nestjs/common'; + +@Injectable() +export class UnlinkAttachment { + /** + * Unlink the attachments from the model entry. + * @param {number} tenantId + * @param {string} filekey + * @param {string} modelRef + * @param {number} modelId + */ + async unlink( + tenantId: number, + filekey: string, + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ): Promise { + const { DocumentLink, Document, ...models } = this.tenancy.models(tenantId); + + const LinkModel = models[modelRef]; + validateLinkModelExists(LinkModel); + + const foundLinkModel = await LinkModel.query(trx).findById(modelId); + validateLinkModelEntryExists(foundLinkModel); + + const document = await Document.query(trx).findOne('key', filekey); + + // Delete the document link. + await DocumentLink.query(trx) + .where('modelRef', modelRef) + .where('modelId', modelId) + .where('documentId', document.id) + .delete(); + } + + /** + * Bulk unlink the attachments from the model entry. + * @param {number} tenantId + * @param {string} fieldkey + * @param {string} modelRef + * @param {number} modelId + * @returns {Promise} + */ + async bulkUnlink( + tenantId: number, + filekeys: string[], + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ): Promise { + await bluebird.each(filekeys, (fieldKey: string) => { + try { + this.unlink(tenantId, fieldKey, modelRef, modelId, trx); + } catch { + // Ignore catching exceptions on bulk action. + } + }); + } + + /** + * Unlink all the unpresented keys of the given model type and id. + * @param {number} tenantId + * @param {string[]} presentedKeys + * @param {string} modelRef + * @param {number} modelId + * @param {Knex.Transaction} trx + */ + async unlinkUnpresentedKeys( + tenantId: number, + presentedKeys: string[], + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ): Promise { + const { DocumentLink } = this.tenancy.models(tenantId); + + const modelLinks = await DocumentLink.query(trx) + .where('modelRef', modelRef) + .where('modelId', modelId) + .withGraphFetched('document'); + + const modelLinkKeys = modelLinks.map((link) => link.document.key); + const unpresentedKeys = difference(modelLinkKeys, presentedKeys); + + await this.bulkUnlink(tenantId, unpresentedKeys, modelRef, modelId, trx); + } + + /** + * Unlink all attachments of the given model type and id. + * @param {number} tenantId + * @param {string} modelRef + * @param {number} modelId + * @param {Knex.Transaction} trx + * @returns {Promise} + */ + async unlinkAllModelKeys( + tenantId: number, + modelRef: string, + modelId: number, + trx?: Knex.Transaction + ): Promise { + const { DocumentLink } = this.tenancy.models(tenantId); + + // Get all the keys of the modelRef and modelId. + const modelLinks = await DocumentLink.query(trx) + .where('modelRef', modelRef) + .where('modelId', modelId) + .withGraphFetched('document'); + + const modelLinkKeys = modelLinks.map((link) => link.document.key); + + await this.bulkUnlink(tenantId, modelLinkKeys, modelRef, modelId, trx); + } +} diff --git a/packages/server-nest/src/modules/Attachments/UploadDocument.ts b/packages/server-nest/src/modules/Attachments/UploadDocument.ts new file mode 100644 index 000000000..e3512381c --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/UploadDocument.ts @@ -0,0 +1,28 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { TenancyDatabaseModule } from '../Tenancy/TenancyDB/TenancyDB.module'; +import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { DocumentModel } from './models/Document.model'; + +@Injectable() +export class UploadDocument { + constructor( + @Inject(DocumentModel.name) + private readonly documentModel: TenantModelProxy, + ) {} + + /** + * Inserts the document metadata. + * @param {number} tenantId + * @param {} file + * @returns {} + */ + async upload(file: any) { + const insertedDocument = await this.documentModel().query().insert({ + key: file.key, + mimeType: file.mimetype, + size: file.size, + originName: file.originalname, + }); + return insertedDocument; + } +} diff --git a/packages/server-nest/src/modules/Attachments/ValidateAttachments.ts b/packages/server-nest/src/modules/Attachments/ValidateAttachments.ts new file mode 100644 index 000000000..d629ac13b --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/ValidateAttachments.ts @@ -0,0 +1,32 @@ +import { castArray, difference } from 'lodash'; +import HasTenancyService from '../Tenancy/TenancyService'; +import { ServiceError } from '@/exceptions'; +import { Inject, Service } from 'typedi'; +import { TenantModelProxy } from '../System/models/TenantBaseModel'; +import { DocumentModel } from './models/Document.model'; + +@Service() +export class ValidateAttachments { + + constructor( + private readonly documentModel: TenantModelProxy + ) { + + } + /** + * Validates the given file keys existance. + * @param {number} tenantId + * @param {string|string[]} key + */ + async validate(tenantId: number, key: string | string[]) { + const keys = castArray(key); + const documents = await this.documentModel().query().whereIn('key', key); + const documentKeys = documents.map((document) => document.key); + + const notFoundKeys = difference(keys, documentKeys); + + if (notFoundKeys.length > 0) { + throw new ServiceError('DOCUMENT_KEYS_INVALID'); + } + } +} diff --git a/packages/server-nest/src/modules/Attachments/_utils.ts b/packages/server-nest/src/modules/Attachments/_utils.ts new file mode 100644 index 000000000..26f0f59c9 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/_utils.ts @@ -0,0 +1,14 @@ +import { ServiceError } from '@/exceptions'; +import { ERRORS } from './constants'; + +export const validateLinkModelExists = (LinkModel) => { + if (!LinkModel) { + throw new ServiceError(ERRORS.DOCUMENT_LINK_REF_INVALID); + } +}; + +export const validateLinkModelEntryExists = (foundLinkModel) => { + if (!foundLinkModel) { + throw new ServiceError(ERRORS.DOCUMENT_LINK_ID_INVALID); + } +}; diff --git a/packages/server-nest/src/modules/Attachments/constants.ts b/packages/server-nest/src/modules/Attachments/constants.ts new file mode 100644 index 000000000..b046e9ca7 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/constants.ts @@ -0,0 +1,5 @@ +export enum ERRORS { + DOCUMENT_LINK_REF_INVALID = 'DOCUMENT_LINK_REF_INVALID', + DOCUMENT_LINK_ID_INVALID = 'DOCUMENT_LINK_ID_INVALID', + DOCUMENT_LINK_ALREADY_LINKED = 'DOCUMENT_LINK_ALREADY_LINKED' +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnBills.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnBills.ts new file mode 100644 index 000000000..6909626ec --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnBills.ts @@ -0,0 +1,151 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IBIllEventDeletedPayload, + IBillCreatedPayload, + IBillCreatingPayload, + IBillEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnBills { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.bill.onCreating, + this.validateAttachmentsOnBillCreate.bind(this) + ); + bus.subscribe( + events.bill.onCreated, + this.handleAttachmentsOnBillCreated.bind(this) + ); + bus.subscribe( + events.bill.onEdited, + this.handleUnlinkUnpresentedKeysOnBillEdited.bind(this) + ); + bus.subscribe( + events.bill.onEdited, + this.handleLinkPresentedKeysOnBillEdited.bind(this) + ); + bus.subscribe( + events.bill.onDeleting, + this.handleUnlinkAttachmentsOnBillDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating bill. + * @param {ISaleInvoiceCreatingPaylaod} + * @returns {Promise} + */ + private async validateAttachmentsOnBillCreate({ + billDTO, + tenantId, + }: IBillCreatingPayload): Promise { + if (isEmpty(billDTO.attachments)) { + return; + } + const documentKeys = billDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created bill. + * @param {ISaleInvoiceCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnBillCreated({ + tenantId, + bill, + billDTO, + trx, + }: IBillCreatedPayload): Promise { + if (isEmpty(billDTO.attachments)) return; + + const keys = billDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'Bill', + bill.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited bill. + * @param {IBillEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnBillEdited({ + tenantId, + billDTO, + bill, + trx + }: IBillEditedPayload) { + const keys = billDTO.attachments?.map((attachment) => attachment.key); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'Bill', + bill.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited bill. + * @param {ISaleInvoiceEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnBillEdited({ + tenantId, + billDTO, + oldBill, + trx, + }: IBillEditedPayload) { + if (isEmpty(billDTO.attachments)) return; + + const keys = billDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'Bill', + oldBill.id, + trx + ); + } + + /** + * Unlink all attachments once the bill deleted. + * @param {ISaleInvoiceDeletedPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnBillDeleted({ + tenantId, + oldBill, + trx, + }: IBIllEventDeletedPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'Bill', + oldBill.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnCreditNote.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnCreditNote.ts new file mode 100644 index 000000000..32547b036 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnCreditNote.ts @@ -0,0 +1,155 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + ICreditNoteCreatedPayload, + ICreditNoteCreatingPayload, + ICreditNoteDeletingPayload, + ICreditNoteEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnCreditNote { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.creditNote.onCreating, + this.validateAttachmentsOnCreditNoteCreate.bind(this) + ); + bus.subscribe( + events.creditNote.onCreated, + this.handleAttachmentsOnCreditNoteCreated.bind(this) + ); + bus.subscribe( + events.creditNote.onEdited, + this.handleUnlinkUnpresentedKeysOnCreditNoteEdited.bind(this) + ); + bus.subscribe( + events.creditNote.onEdited, + this.handleLinkPresentedKeysOnCreditNoteEdited.bind(this) + ); + bus.subscribe( + events.creditNote.onDeleting, + this.handleUnlinkAttachmentsOnCreditNoteDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating credit note. + * @param {ICreditNoteCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnCreditNoteCreate({ + creditNoteDTO, + tenantId, + }: ICreditNoteCreatingPayload): Promise { + if (isEmpty(creditNoteDTO.attachments)) { + return; + } + const documentKeys = creditNoteDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created credit note. + * @param {ICreditNoteCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnCreditNoteCreated({ + tenantId, + creditNote, + creditNoteDTO, + trx, + }: ICreditNoteCreatedPayload): Promise { + if (isEmpty(creditNoteDTO.attachments)) return; + + const keys = creditNoteDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'CreditNote', + creditNote.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited credit note. + * @param {ICreditNoteEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnCreditNoteEdited({ + tenantId, + creditNoteEditDTO, + oldCreditNote, + trx, + }: ICreditNoteEditedPayload) { + const keys = creditNoteEditDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'CreditNote', + oldCreditNote.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited credit note. + * @param {ICreditNoteEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnCreditNoteEdited({ + tenantId, + creditNoteEditDTO, + oldCreditNote, + trx, + }: ICreditNoteEditedPayload) { + if (isEmpty(creditNoteEditDTO.attachments)) return; + + const keys = creditNoteEditDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'CreditNote', + oldCreditNote.id, + trx + ); + } + + /** + * Unlink all attachments once the credit note deleted. + * @param {ICreditNoteDeletingPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnCreditNoteDeleted({ + tenantId, + oldCreditNote, + trx, + }: ICreditNoteDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'CreditNote', + oldCreditNote.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnExpenses.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnExpenses.ts new file mode 100644 index 000000000..77babe6c0 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnExpenses.ts @@ -0,0 +1,151 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IExpenseCreatedPayload, + IExpenseCreatingPayload, + IExpenseDeletingPayload, + IExpenseEventEditPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnExpenses { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.expenses.onCreating, + this.validateAttachmentsOnExpenseCreate.bind(this) + ); + bus.subscribe( + events.expenses.onCreated, + this.handleAttachmentsOnExpenseCreated.bind(this) + ); + bus.subscribe( + events.expenses.onEdited, + this.handleUnlinkUnpresentedKeysOnExpenseEdited.bind(this) + ); + bus.subscribe( + events.expenses.onEdited, + this.handleLinkPresentedKeysOnExpenseEdited.bind(this) + ); + bus.subscribe( + events.expenses.onDeleting, + this.handleUnlinkAttachmentsOnExpenseDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating expense. + * @param {ISaleInvoiceCreatingPaylaod} + * @returns {Promise} + */ + private async validateAttachmentsOnExpenseCreate({ + expenseDTO, + tenantId, + }: IExpenseCreatingPayload): Promise { + if (isEmpty(expenseDTO.attachments)) { + return; + } + const documentKeys = expenseDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created expense. + * @param {ISaleInvoiceCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnExpenseCreated({ + tenantId, + expenseDTO, + expense, + trx, + }: IExpenseCreatedPayload): Promise { + if (isEmpty(expenseDTO.attachments)) return; + + const keys = expenseDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'Expense', + expense.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited expense. + * @param {ISaleInvoiceEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnExpenseEdited({ + tenantId, + expenseDTO, + expense, + trx, + }: IExpenseEventEditPayload) { + const keys = expenseDTO.attachments?.map((attachment) => attachment.key); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'Expense', + expense.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited expense. + * @param {ISaleInvoiceEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnExpenseEdited({ + tenantId, + expenseDTO, + oldExpense, + trx, + }: IExpenseEventEditPayload) { + if (isEmpty(expenseDTO.attachments)) return; + + const keys = expenseDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'Expense', + oldExpense.id, + trx + ); + } + + /** + * Unlink all attachments once the expense deleted. + * @param {ISaleInvoiceDeletedPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnExpenseDeleted({ + tenantId, + oldExpense, + trx, + }: IExpenseDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'Expense', + oldExpense.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnManualJournals.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnManualJournals.ts new file mode 100644 index 000000000..b0a133d3a --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnManualJournals.ts @@ -0,0 +1,157 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IManualJournalCreatingPayload, + IManualJournalEventCreatedPayload, + IManualJournalEventDeletedPayload, + IManualJournalEventEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnManualJournals { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.manualJournals.onCreating, + this.validateAttachmentsOnManualJournalCreate.bind(this) + ); + bus.subscribe( + events.manualJournals.onCreated, + this.handleAttachmentsOnManualJournalCreated.bind(this) + ); + bus.subscribe( + events.manualJournals.onEdited, + this.handleUnlinkUnpresentedKeysOnManualJournalEdited.bind(this) + ); + bus.subscribe( + events.manualJournals.onEdited, + this.handleLinkPresentedKeysOnManualJournalEdited.bind(this) + ); + bus.subscribe( + events.manualJournals.onDeleting, + this.handleUnlinkAttachmentsOnManualJournalDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating manual journal. + * @param {IManualJournalCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnManualJournalCreate({ + manualJournalDTO, + tenantId, + }: IManualJournalCreatingPayload): Promise { + if (isEmpty(manualJournalDTO.attachments)) { + return; + } + const documentKeys = manualJournalDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created manual journal. + * @param {IManualJournalEventCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnManualJournalCreated({ + tenantId, + manualJournalDTO, + manualJournal, + trx, + }: IManualJournalEventCreatedPayload): Promise { + if (isEmpty(manualJournalDTO.attachments)) return; + + const keys = manualJournalDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'ManualJournal', + manualJournal.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited manual journal. + * @param {ISaleInvoiceEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnManualJournalEdited({ + tenantId, + manualJournalDTO, + manualJournal, + trx + }: IManualJournalEventEditedPayload) { + const keys = manualJournalDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'SaleInvoice', + manualJournal.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited manual journal. + * @param {ISaleInvoiceEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnManualJournalEdited({ + tenantId, + manualJournalDTO, + oldManualJournal, + trx, + }: IManualJournalEventEditedPayload) { + if (isEmpty(manualJournalDTO.attachments)) return; + + const keys = manualJournalDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'ManualJournal', + oldManualJournal.id, + trx + ); + } + + /** + * Unlink all attachments once the manual journal deleted. + * @param {ISaleInvoiceDeletedPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnManualJournalDeleted({ + tenantId, + oldManualJournal, + trx, + }: IManualJournalEventDeletedPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'SaleInvoice', + oldManualJournal.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsMade.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsMade.ts new file mode 100644 index 000000000..276e1abbb --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsMade.ts @@ -0,0 +1,157 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IBillPaymentCreatingPayload, + IBillPaymentDeletingPayload, + IBillPaymentEventCreatedPayload, + IBillPaymentEventEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnBillPayments { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.billPayment.onCreating, + this.validateAttachmentsOnBillPaymentCreate.bind(this) + ); + bus.subscribe( + events.billPayment.onCreated, + this.handleAttachmentsOnBillPaymentCreated.bind(this) + ); + bus.subscribe( + events.billPayment.onEdited, + this.handleUnlinkUnpresentedKeysOnBillPaymentEdited.bind(this) + ); + bus.subscribe( + events.billPayment.onEdited, + this.handleLinkPresentedKeysOnBillPaymentEdited.bind(this) + ); + bus.subscribe( + events.billPayment.onDeleting, + this.handleUnlinkAttachmentsOnBillPaymentDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating bill payment. + * @param {IBillPaymentCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnBillPaymentCreate({ + billPaymentDTO, + tenantId, + }: IBillPaymentCreatingPayload): Promise { + if (isEmpty(billPaymentDTO.attachments)) { + return; + } + const documentKeys = billPaymentDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created bill payment. + * @param {IBillPaymentEventCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnBillPaymentCreated({ + tenantId, + billPaymentDTO, + billPayment, + trx, + }: IBillPaymentEventCreatedPayload): Promise { + if (isEmpty(billPaymentDTO.attachments)) return; + + const keys = billPaymentDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'BillPayment', + billPayment.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited bill payment. + * @param {IBillPaymentEventEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnBillPaymentEdited({ + tenantId, + billPaymentDTO, + oldBillPayment, + trx, + }: IBillPaymentEventEditedPayload) { + const keys = billPaymentDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'BillPayment', + oldBillPayment.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited bill payment. + * @param {IBillPaymentEventEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnBillPaymentEdited({ + tenantId, + billPaymentDTO, + oldBillPayment, + trx, + }: IBillPaymentEventEditedPayload) { + if (isEmpty(billPaymentDTO.attachments)) return; + + const keys = billPaymentDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'BillPayment', + oldBillPayment.id, + trx + ); + } + + /** + * Unlink all attachments once the bill payment deleted. + * @param {IBillPaymentDeletingPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnBillPaymentDeleted({ + tenantId, + oldBillPayment, + trx, + }: IBillPaymentDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'BillPayment', + oldBillPayment.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsReceived.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsReceived.ts new file mode 100644 index 000000000..311fbb8ea --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnPaymentsReceived.ts @@ -0,0 +1,157 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IPaymentReceivedCreatedPayload, + IPaymentReceivedCreatingPayload, + IPaymentReceivedDeletingPayload, + IPaymentReceivedEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnPaymentsReceived { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.paymentReceive.onCreating, + this.validateAttachmentsOnPaymentCreate.bind(this) + ); + bus.subscribe( + events.paymentReceive.onCreated, + this.handleAttachmentsOnPaymentCreated.bind(this) + ); + bus.subscribe( + events.paymentReceive.onEdited, + this.handleUnlinkUnpresentedKeysOnPaymentEdited.bind(this) + ); + bus.subscribe( + events.paymentReceive.onEdited, + this.handleLinkPresentedKeysOnPaymentEdited.bind(this) + ); + bus.subscribe( + events.paymentReceive.onDeleting, + this.handleUnlinkAttachmentsOnPaymentDelete.bind(this) + ); + } + + /** + * Validates the attachment keys on creating payment. + * @param {IPaymentReceivedCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnPaymentCreate({ + paymentReceiveDTO, + tenantId, + }: IPaymentReceivedCreatingPayload): Promise { + if (isEmpty(paymentReceiveDTO.attachments)) { + return; + } + const documentKeys = paymentReceiveDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created payment. + * @param {IPaymentReceivedCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnPaymentCreated({ + tenantId, + paymentReceiveDTO, + paymentReceive, + trx, + }: IPaymentReceivedCreatedPayload): Promise { + if (isEmpty(paymentReceiveDTO.attachments)) return; + + const keys = paymentReceiveDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'PaymentReceive', + paymentReceive.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited payment. + * @param {IPaymentReceivedEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnPaymentEdited({ + tenantId, + paymentReceiveDTO, + oldPaymentReceive, + trx, + }: IPaymentReceivedEditedPayload) { + const keys = paymentReceiveDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'PaymentReceive', + oldPaymentReceive.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited payment. + * @param {IPaymentReceivedEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnPaymentEdited({ + tenantId, + paymentReceiveDTO, + oldPaymentReceive, + trx, + }: IPaymentReceivedEditedPayload) { + if (isEmpty(paymentReceiveDTO.attachments)) return; + + const keys = paymentReceiveDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'PaymentReceive', + oldPaymentReceive.id, + trx + ); + } + + /** + * Unlink all attachments once the payment deleted. + * @param {ISaleInvoiceDeletedPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnPaymentDelete({ + tenantId, + oldPaymentReceive, + trx, + }: IPaymentReceivedDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'PaymentReceive', + oldPaymentReceive.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleEstimates.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleEstimates.ts new file mode 100644 index 000000000..53eb483bb --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleEstimates.ts @@ -0,0 +1,154 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + ISaleEstimateCreatedPayload, + ISaleEstimateCreatingPayload, + ISaleEstimateDeletingPayload, + ISaleEstimateEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnSaleEstimates { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.saleEstimate.onCreating, + this.validateAttachmentsOnSaleEstimateCreated.bind(this) + ); + bus.subscribe( + events.saleEstimate.onCreated, + this.handleAttachmentsOnSaleEstimateCreated.bind(this) + ); + bus.subscribe( + events.saleEstimate.onEdited, + this.handleUnlinkUnpresentedKeysOnSaleEstimateEdited.bind(this) + ); + bus.subscribe( + events.saleEstimate.onEdited, + this.handleLinkPresentedKeysOnSaleEstimateEdited.bind(this) + ); + bus.subscribe( + events.saleEstimate.onDeleting, + this.handleUnlinkAttachmentsOnSaleEstimateDelete.bind(this) + ); + } + + /** + * Validates the attachment keys on creating sale estimate. + * @param {ISaleEstimateCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnSaleEstimateCreated({ + estimateDTO, + tenantId, + }: ISaleEstimateCreatingPayload): Promise { + if (isEmpty(estimateDTO.attachments)) { + return; + } + const documentKeys = estimateDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created sale estimate. + * @param {ISaleEstimateCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnSaleEstimateCreated({ + tenantId, + saleEstimateDTO, + saleEstimate, + trx, + }: ISaleEstimateCreatedPayload): Promise { + if (isEmpty(saleEstimateDTO.attachments)) return; + + const keys = saleEstimateDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleEstimate', + saleEstimate.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited sale estimate. + * @param {ISaleEstimateEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnSaleEstimateEdited({ + tenantId, + estimateDTO, + oldSaleEstimate, + trx + }: ISaleEstimateEditedPayload) { + const keys = estimateDTO.attachments?.map((attachment) => attachment.key); + + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'SaleEstimate', + oldSaleEstimate.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited sale estimate. + * @param {ISaleEstimateEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnSaleEstimateEdited({ + tenantId, + estimateDTO, + oldSaleEstimate, + trx, + }: ISaleEstimateEditedPayload) { + if (isEmpty(estimateDTO.attachments)) return; + + const keys = estimateDTO.attachments?.map((attachment) => attachment.key); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleEstimate', + oldSaleEstimate.id, + trx + ); + } + + /** + * Unlink all attachments once the estimate deleted. + * @param {ISaleEstimateDeletingPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnSaleEstimateDelete({ + tenantId, + oldSaleEstimate, + trx, + }: ISaleEstimateDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'SaleEstimate', + oldSaleEstimate.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleInvoice.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleInvoice.ts new file mode 100644 index 000000000..717d956dc --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleInvoice.ts @@ -0,0 +1,159 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + ISaleInvoiceCreatedPayload, + ISaleInvoiceCreatingPaylaod, + ISaleInvoiceDeletingPayload, + ISaleInvoiceEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnSaleInvoiceCreated { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.saleInvoice.onCreating, + this.validateAttachmentsOnSaleInvoiceCreate.bind(this) + ); + bus.subscribe( + events.saleInvoice.onCreated, + this.handleAttachmentsOnSaleInvoiceCreated.bind(this) + ); + bus.subscribe( + events.saleInvoice.onEdited, + this.handleUnlinkUnpresentedKeysOnInvoiceEdited.bind(this) + ); + bus.subscribe( + events.saleInvoice.onEdited, + this.handleLinkPresentedKeysOnInvoiceEdited.bind(this) + ); + bus.subscribe( + events.saleInvoice.onDeleting, + this.handleUnlinkAttachmentsOnInvoiceDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating sale invoice. + * @param {ISaleInvoiceCreatingPaylaod} + * @returns {Promise} + */ + private async validateAttachmentsOnSaleInvoiceCreate({ + saleInvoiceDTO, + tenantId, + }: ISaleInvoiceCreatingPaylaod): Promise { + if (isEmpty(saleInvoiceDTO.attachments)) { + return; + } + const documentKeys = saleInvoiceDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created sale invoice. + * @param {ISaleInvoiceCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnSaleInvoiceCreated({ + tenantId, + saleInvoiceDTO, + saleInvoice, + trx, + }: ISaleInvoiceCreatedPayload): Promise { + if (isEmpty(saleInvoiceDTO.attachments)) return; + + const keys = saleInvoiceDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleInvoice', + saleInvoice.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited sale invoice. + * @param {ISaleInvoiceEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnInvoiceEdited({ + tenantId, + saleInvoiceDTO, + saleInvoice, + trx, + }: ISaleInvoiceEditedPayload) { + // if (isEmpty(saleInvoiceDTO.attachments)) return; + + const keys = saleInvoiceDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'SaleInvoice', + saleInvoice.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited sale invoice. + * @param {ISaleInvoiceEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnInvoiceEdited({ + tenantId, + saleInvoiceDTO, + oldSaleInvoice, + trx, + }: ISaleInvoiceEditedPayload) { + if (isEmpty(saleInvoiceDTO.attachments)) return; + + const keys = saleInvoiceDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleInvoice', + oldSaleInvoice.id, + trx + ); + } + + /** + * Unlink all attachments once the invoice deleted. + * @param {ISaleInvoiceDeletedPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnInvoiceDeleted({ + tenantId, + oldSaleInvoice, + trx, + }: ISaleInvoiceDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'SaleInvoice', + oldSaleInvoice.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleReceipts.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleReceipts.ts new file mode 100644 index 000000000..01a8d9fc3 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnSaleReceipts.ts @@ -0,0 +1,157 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + ISaleReceiptCreatedPayload, + ISaleReceiptCreatingPayload, + ISaleReceiptDeletingPayload, + ISaleReceiptEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnSaleReceipt { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.saleReceipt.onCreating, + this.validateAttachmentsOnSaleInvoiceCreate.bind(this) + ); + bus.subscribe( + events.saleReceipt.onCreated, + this.handleAttachmentsOnSaleInvoiceCreated.bind(this) + ); + bus.subscribe( + events.saleReceipt.onEdited, + this.handleUnlinkUnpresentedKeysOnInvoiceEdited.bind(this) + ); + bus.subscribe( + events.saleReceipt.onEdited, + this.handleLinkPresentedKeysOnInvoiceEdited.bind(this) + ); + bus.subscribe( + events.saleReceipt.onDeleting, + this.handleUnlinkAttachmentsOnReceiptDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating sale receipt. + * @param {ISaleReceiptCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnSaleInvoiceCreate({ + saleReceiptDTO, + tenantId, + }: ISaleReceiptCreatingPayload): Promise { + if (isEmpty(saleReceiptDTO.attachments)) { + return; + } + const documentKeys = saleReceiptDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created sale receipt. + * @param {ISaleReceiptCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnSaleInvoiceCreated({ + tenantId, + saleReceiptDTO, + saleReceipt, + trx, + }: ISaleReceiptCreatedPayload): Promise { + if (isEmpty(saleReceiptDTO.attachments)) return; + + const keys = saleReceiptDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleReceipt', + saleReceipt.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited sale receipt. + * @param {ISaleReceiptEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnInvoiceEdited({ + tenantId, + saleReceiptDTO, + saleReceipt, + trx, + }: ISaleReceiptEditedPayload) { + const keys = saleReceiptDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'SaleReceipt', + saleReceipt.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited sale receipt. + * @param {ISaleReceiptEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnInvoiceEdited({ + tenantId, + saleReceiptDTO, + oldSaleReceipt, + trx, + }: ISaleReceiptEditedPayload) { + if (isEmpty(saleReceiptDTO.attachments)) return; + + const keys = saleReceiptDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'SaleReceipt', + oldSaleReceipt.id, + trx + ); + } + + /** + * Unlink all attachments once the receipt deleted. + * @param {ISaleReceiptDeletingPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnReceiptDeleted({ + tenantId, + oldSaleReceipt, + trx, + }: ISaleReceiptDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'SaleReceipt', + oldSaleReceipt.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/events/AttachmentsOnVendorCredits.ts b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnVendorCredits.ts new file mode 100644 index 000000000..30ce63ca5 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/events/AttachmentsOnVendorCredits.ts @@ -0,0 +1,157 @@ +import { Inject, Service } from 'typedi'; +import { isEmpty } from 'lodash'; +import { + IVendorCreditCreatedPayload, + IVendorCreditCreatingPayload, + IVendorCreditDeletingPayload, + IVendorCreditEditedPayload, +} from '@/interfaces'; +import events from '@/subscribers/events'; +import { LinkAttachment } from '../LinkAttachment'; +import { ValidateAttachments } from '../ValidateAttachments'; +import { UnlinkAttachment } from '../UnlinkAttachment'; + +@Service() +export class AttachmentsOnVendorCredits { + @Inject() + private linkAttachmentService: LinkAttachment; + + @Inject() + private unlinkAttachmentService: UnlinkAttachment; + + @Inject() + private validateDocuments: ValidateAttachments; + + /** + * Constructor method. + */ + public attach(bus) { + bus.subscribe( + events.vendorCredit.onCreating, + this.validateAttachmentsOnVendorCreditCreate.bind(this) + ); + bus.subscribe( + events.vendorCredit.onCreated, + this.handleAttachmentsOnVendorCreditCreated.bind(this) + ); + bus.subscribe( + events.vendorCredit.onEdited, + this.handleUnlinkUnpresentedKeysOnVendorCreditEdited.bind(this) + ); + bus.subscribe( + events.vendorCredit.onEdited, + this.handleLinkPresentedKeysOnVendorCreditEdited.bind(this) + ); + bus.subscribe( + events.vendorCredit.onDeleting, + this.handleUnlinkAttachmentsOnVendorCreditDeleted.bind(this) + ); + } + + /** + * Validates the attachment keys on creating vendor credit. + * @param {IVendorCreditCreatingPayload} + * @returns {Promise} + */ + private async validateAttachmentsOnVendorCreditCreate({ + vendorCreditCreateDTO, + tenantId, + }: IVendorCreditCreatingPayload): Promise { + if (isEmpty(vendorCreditCreateDTO.attachments)) { + return; + } + const documentKeys = vendorCreditCreateDTO?.attachments?.map((a) => a.key); + + await this.validateDocuments.validate(tenantId, documentKeys); + } + + /** + * Handles linking the attachments of the created vendor credit. + * @param {IVendorCreditCreatedPayload} + * @returns {Promise} + */ + private async handleAttachmentsOnVendorCreditCreated({ + tenantId, + vendorCreditCreateDTO, + vendorCredit, + trx, + }: IVendorCreditCreatedPayload): Promise { + if (isEmpty(vendorCreditCreateDTO.attachments)) return; + + const keys = vendorCreditCreateDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'VendorCredit', + vendorCredit.id, + trx + ); + } + + /** + * Handles unlinking all the unpresented keys of the edited vendor credit. + * @param {IVendorCreditEditedPayload} + */ + private async handleUnlinkUnpresentedKeysOnVendorCreditEdited({ + tenantId, + vendorCreditDTO, + oldVendorCredit, + trx, + }: IVendorCreditEditedPayload) { + const keys = vendorCreditDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.unlinkAttachmentService.unlinkUnpresentedKeys( + tenantId, + keys, + 'VendorCredit', + oldVendorCredit.id, + trx + ); + } + + /** + * Handles linking all the presented keys of the edited vendor credit. + * @param {IVendorCreditEditedPayload} + * @returns {Promise} + */ + private async handleLinkPresentedKeysOnVendorCreditEdited({ + tenantId, + vendorCreditDTO, + oldVendorCredit, + trx, + }: IVendorCreditEditedPayload) { + if (isEmpty(vendorCreditDTO.attachments)) return; + + const keys = vendorCreditDTO.attachments?.map( + (attachment) => attachment.key + ); + await this.linkAttachmentService.bulkLink( + tenantId, + keys, + 'VendorCredit', + oldVendorCredit.id, + trx + ); + } + + /** + * Unlink all attachments once the vendor credit deleted. + * @param {IVendorCreditDeletingPayload} + * @returns {Promise} + */ + private async handleUnlinkAttachmentsOnVendorCreditDeleted({ + tenantId, + oldVendorCredit, + trx, + }: IVendorCreditDeletingPayload) { + await this.unlinkAttachmentService.unlinkAllModelKeys( + tenantId, + 'VendorCredit', + oldVendorCredit.id, + trx + ); + } +} diff --git a/packages/server-nest/src/modules/Attachments/models/Document.model.ts b/packages/server-nest/src/modules/Attachments/models/Document.model.ts new file mode 100644 index 000000000..054b992ce --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/models/Document.model.ts @@ -0,0 +1,22 @@ +import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; + +export class DocumentModel extends TenantBaseModel{ + originName!: string; +size!: number; +mimeType!: string; +key!: string; + + /** + * Table name + */ + static get tableName() { + return 'documents'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } +} diff --git a/packages/server-nest/src/modules/Attachments/models/DocumentLink.model.ts b/packages/server-nest/src/modules/Attachments/models/DocumentLink.model.ts new file mode 100644 index 000000000..c218d1615 --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/models/DocumentLink.model.ts @@ -0,0 +1,39 @@ +import { TenantBaseModel } from '@/modules/System/models/TenantBaseModel'; +import { Model, mixin } from 'objection'; + +export class DocumentLinkModel extends TenantBaseModel { + /** + * Table name + */ + static get tableName() { + return 'document_links'; + } + + /** + * Model timestamps. + */ + get timestamps() { + return ['createdAt', 'updatedAt']; + } + + /** + * Relationship mapping. + */ + static get relationMappings() { + const { DocumentModel } = require('./Document'); + + return { + /** + * Sale invoice associated entries. + */ + document: { + relation: Model.HasOneRelation, + modelClass: DocumentModel, + join: { + from: 'document_links.documentId', + to: 'documents.id', + }, + }, + }; + } +} diff --git a/packages/server-nest/src/modules/Attachments/utils.ts b/packages/server-nest/src/modules/Attachments/utils.ts new file mode 100644 index 000000000..4f58fbc9c --- /dev/null +++ b/packages/server-nest/src/modules/Attachments/utils.ts @@ -0,0 +1,9 @@ +import path from 'path'; +import config from '@/config'; + +export const getUploadedObjectUri = (objectKey: string) => { + return new URL( + path.join(config.s3.bucket, objectKey), + config.s3.endpoint + ).toString(); +}; diff --git a/packages/server-nest/src/modules/S3/S3.module.ts b/packages/server-nest/src/modules/S3/S3.module.ts new file mode 100644 index 000000000..f62917107 --- /dev/null +++ b/packages/server-nest/src/modules/S3/S3.module.ts @@ -0,0 +1,29 @@ +import { Module } from '@nestjs/common'; +import { S3Client } from '@aws-sdk/client-s3'; +import { ConfigService } from '@nestjs/config'; + +export const S3_CLIENT = 'S3_CLIENT'; + +const services = [ + { + provide: S3_CLIENT, + useFactory: (configService: ConfigService) => { + const config = configService.get('s3'); + + return new S3Client({ + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + endpoint: config.endpoint, + forcePathStyle: config.forcePathStyle, + }); + }, + }, +]; +@Module({ + providers: [...services], + exports: [...services], +}) +export class S3Module {}