import { ConfService } from "@libs/library-app/conf/conf.service"; import { EntityCRUDTypeDefinition, EtcCRUDTypeDefinition, FileRelocationDtoCRUDTypeDefinition, FileRelocationInputArtefactCRUDTypeDefinition, FileRelocationOutputArtefactCRUDTypeDefinition, SnapshotListArtefactCRUDTypeDefinition } from "../crud.type"; import { RootCrudEngine } from "./root.engine"; import { LogService } from "@libs/library-app/log/log.service"; import { DataValidationPipe } from "@libs/library-app/data-validation/data.validation.pipe"; import { LibraryAppService } from "@libs/library-app/library.app.service"; import { SelfGraphqlMicroserviceService } from "@libs/dynamic-app"; import { BadRequestException, Type } from "@nestjs/common"; import { DataSource, getMetadataArgsStorage, In, Repository } from "typeorm"; import { GraphQLResolveInfo } from "graphql"; import { getEntityUploadFieldMetadata, UploadFieldMeta } from "@libs/library-app/upload/upload.decorator"; import { FileRelocationTypeEnum } from "../crud.enum"; import { UploadCrudEngine } from "./upload.engine"; import path from "path"; import fs from 'fs-extra'; import { String } from "lodash"; export class FileRelocationCrudEngine < // entity (1) ENTITY_TYPE extends EntityCRUDTypeDefinition, // file_relocation (3) FILE_RELOCATION_DTO_TYPE extends FileRelocationDtoCRUDTypeDefinition, FILE_RELOCATION_INPUT_TYPE extends FileRelocationInputArtefactCRUDTypeDefinition, FILE_RELOCATION_OUTPUT_TYPE extends FileRelocationOutputArtefactCRUDTypeDefinition >extends RootCrudEngine < ENTITY_TYPE > { constructor( // data source protected readonly dataSource: DataSource, protected readonly repository: Repository, // dependecy services protected readonly confService: ConfService, protected readonly logService: LogService, protected readonly validationPipe: DataValidationPipe, protected readonly libraryAppService: LibraryAppService, protected readonly selfGraphqlMicroserviceService: SelfGraphqlMicroserviceService, // entity (1) protected readonly ENTITY: Type & EntityCRUDTypeDefinition, //file_relocation (3) protected readonly FILE_RELOCATION_DTO: Type & FileRelocationDtoCRUDTypeDefinition, protected readonly FILE_RELOCATION_INPUT_DTO: Type & FileRelocationInputArtefactCRUDTypeDefinition, protected readonly FILE_RELOCATION_OUTPUT_DTO: Type & FileRelocationOutputArtefactCRUDTypeDefinition ) { super( // data source dataSource, repository, // dependecy services confService, logService, validationPipe, libraryAppService, selfGraphqlMicroserviceService, // entity (1) ENTITY ); } /** * ████████████████████████████████████████████████████████████████████████████████████ * ████ RESOLVER METHODES █████████████████████████████████████████████████████████████ * ████████████████████████████████████████████████████████████████████████████████████ **/ public async fileRelocation(input: FILE_RELOCATION_INPUT_TYPE | FILE_RELOCATION_INPUT_TYPE[], selection: FILE_RELOCATION_OUTPUT_TYPE, info: GraphQLResolveInfo, ctx: any, etc?: EtcCRUDTypeDefinition): Promise { try{ return await this._fileRelocation(input, selection, info, ctx); }catch (err: any) { this.logService.error(err, err.message); throw new BadRequestException(`${this.FILE_RELOCATION_DTO?.metaname}: ${this.logService.redactSensitive(err.message)}`); } } /** * ████████████████████████████████████████████████████████████████████████████████████ * ████ SERVICE METHODES ██████████████████████████████████████████████████████████████ * ████████████████████████████████████████████████████████████████████████████████████ **/ public async _fileRelocation(input: FILE_RELOCATION_INPUT_TYPE | FILE_RELOCATION_INPUT_TYPE[], selection: FILE_RELOCATION_OUTPUT_TYPE, info: GraphQLResolveInfo, ctx: any, etc?: EtcCRUDTypeDefinition): Promise { try{ // if single record passed convert to array if(!Array.isArray(input)){ input = [input]; } // set the array for output const respArr: FILE_RELOCATION_OUTPUT_TYPE[] = []; // set the flag if multiple recrods are processed let mutiRelocate:boolean = false; // get primary key field of entity const pk_field = await this._getEntityPrimaryKeyFieldName(this.ENTITY) as keyof ENTITY_TYPE as string; // loop through all input for(let i=0; i < input.length; i++){ const inputItem = input[i]; const resp = new this.FILE_RELOCATION_OUTPUT_DTO(); const snapshot: SnapshotListDto = new SnapshotListDto(); resp.affected = 0; resp.file_field = inputItem.file_field; resp.id = inputItem.destination_id; resp.ref_id = inputItem.destination_ref_id; // get upload field of entity const entityUploadFieldsMeta = await getEntityUploadFieldMetadata(this.ENTITY.prototype); // get the current upload requested field meta const curUploadFieldMeta = entityUploadFieldsMeta.find(item => item.propertyKey === inputItem.file_field); // make sure filed has upload feature enabled in entity if(curUploadFieldMeta && curUploadFieldMeta.data){ // get entity field meta for upload related settings const fieldMeta = curUploadFieldMeta as UploadFieldMeta; // make sure source and destination both records are availabe in database if(fieldMeta.data.req_max_count == 1 && inputItem.source_id && inputItem.destination_id && (inputItem.source_ref_id == null || inputItem.source_ref_id == undefined) && (inputItem.destination_ref_id == null || inputItem.destination_ref_id == undefined)){ // PROCESS SINGLE FILE // USE CASE // source_id = 1 // source_ref_id = N/A // destination_id = 2 // destination_ref_id = N/A // get the record from the database const findWhere: any = {}; findWhere[pk_field] = In([inputItem.source_id, inputItem.destination_id]); const currRecordArr = await this.repository.find({ where: findWhere }); // Create a Map with the primary key as the key //new Map(currRecordArr.map(record => [record.id, record])); const recordMap = new Map(currRecordArr.map(record => [record.id, record])); const sourceArr = recordMap.get(Number(inputItem.source_id)); const destinationArr = recordMap.get(Number(inputItem.destination_id)); if(inputItem.relocation_type && currRecordArr.length == 2 && sourceArr && destinationArr){ // get destination path and source path const destinationPath = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_id, inputItem.file_field, this.ENTITY.uploaddir); const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_id, inputItem.file_field, this.ENTITY.uploaddir); const sourceFileName = sourceArr[fieldMeta.propertyKey]; const destinationFileName = destinationArr[fieldMeta.propertyKey] try{ const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath); if(relocation == FileRelocationTypeEnum.MOVE){ // PROCESS DESTINATION // update file name as new file in destination await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName); // PROCESS SOURCE // update source file as null in database await this._updateRecordFileName(pk_field, inputItem.source_id, inputItem.file_field, null); // remove destination file await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName)); await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName))) } else if(relocation == FileRelocationTypeEnum.DUPLICATE) { //TODO: update destination file field name as new file in database await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName); // TODO: remove destination file from destination path await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName)); await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName))) } // get new destination record const nwhere: any = {}; nwhere[pk_field] = inputItem.destination_id; const newDestRecordArr = await this.repository.findOne({where: nwhere}); resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newDestRecordArr, this.ENTITY.uploaddir); resp.affected = 1 ; resp.relocation_type = relocation; snapshot.success.push(`File relocated successfully.`); } catch (err: any) { resp.relocation_type = inputItem.relocation_type; snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`); } } else{ snapshot.error.push(`Source id ${inputItem.source_id} or ${inputItem.destination_id} not found. Please enter proper source id and destination id to complete the file relocation process`); } } else if(fieldMeta.data.req_max_count > 1 && inputItem.source_id && inputItem.source_ref_id && inputItem.destination_ref_id){ // PROCESS MULTIPLE FILE BUT ONLY ONE AS PER REQUEST //TODO:Function MultiRecordSingleRelocation if(inputItem.destination_id){ // USE CASE // source_id = 1 // source_ref_id = 10 // destination_id = 2 // destination_ref_id = 20 // get the source record from the database const sourceFindWhere: any = {}; sourceFindWhere[pk_field] = inputItem.source_id; sourceFindWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id; const sourceRecordArr = await this.repository.findOne({ where: sourceFindWhere }); // get the destination record from the database const destFindWhere: any = {}; destFindWhere[pk_field] = inputItem.destination_id; destFindWhere[fieldMeta.data.ref_id_field] = inputItem.destination_ref_id; const destRecordArr = await this.repository.findOne({ where: destFindWhere }); if(sourceRecordArr != null && destRecordArr != null && inputItem.relocation_type){ // get destination path and source path const destinationPath = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourceFileName = sourceRecordArr[fieldMeta.propertyKey]; const destinationFileName = destRecordArr[fieldMeta.propertyKey]; try{ const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath); if(relocation == FileRelocationTypeEnum.MOVE){ // TODO: update file field in destination id await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName); // TODO: remove current destination file from the folder. await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName)) await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName))) // ToDo: delete source record await this._deleteRecord(inputItem.source_id); } else if(relocation == FileRelocationTypeEnum.DUPLICATE) { // TODO: update file field in destination id await this._updateRecordFileName(pk_field, inputItem.destination_id, inputItem.file_field, sourceFileName); // TODO: remove current destination file from the folder. await fs.remove(path.join(this.confService.cwd, destinationPath, destinationFileName)); await fs.remove(path.join(this.confService.cwd, destinationPath, this._getThumbFilename(destinationFileName))) } // get new destination record const nwhere:any = {}; nwhere[pk_field] = inputItem.destination_id; const newDestRecordArr = await this.repository.findOne({where: nwhere}); resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newDestRecordArr, this.ENTITY.uploaddir); resp.affected = 1; resp.relocation_type = relocation; snapshot.success.push(`File relocated successfully.`); } catch (err: any) { resp.relocation_type = inputItem.relocation_type; snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`); } }else{ snapshot.error.push(`Provided source and destination record not found. Please provide proper details to complete the file relocation process`); } } else { // PROCESS FOR ONE FILE TO CREATE AS NEW RECORD // USE CASE // source_id = 1 // source_ref_id = 10 // destination_id = N/A // destination_ref_id = 20 // get the record from the database const findWhere: any = {}; findWhere[pk_field] = inputItem.source_id; findWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id; const currRecord = await this.repository.findOne({ where: findWhere }); if(currRecord != null && inputItem.relocation_type){ // get destination path and source path const destinationPath = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourceFileName = currRecord[fieldMeta.propertyKey]; try{ const relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath); let newRecordId: any; if(relocation === FileRelocationTypeEnum.MOVE){ //TODO: update ref id field in database const affected = await this._updateRecordRefIdById(pk_field, inputItem.source_id, fieldMeta.data.ref_id_field, inputItem.destination_ref_id); newRecordId = inputItem.source_id; }else if(relocation == FileRelocationTypeEnum.DUPLICATE) { // TODO: create new record based on ref id and file field // create record in database newRecordId = await this._createRecord(fieldMeta.data.ref_id_field, inputItem.destination_ref_id, inputItem.file_field, sourceFileName); } if(newRecordId){ // get new insterted record const nWhere: any = {}; nWhere[pk_field] = newRecordId; const newRecord = await this.repository.findOne({ where: nWhere}); resp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newRecord, this.ENTITY.uploaddir); } resp.id = newRecordId; resp.affected = 1; resp.relocation_type = relocation; snapshot.success.push(`File relocated successfully.`); } catch (err: any) { resp.relocation_type = inputItem.relocation_type; snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`); } } } } else if(inputItem.source_ref_id && (inputItem.destination_ref_id || inputItem.destination_id)){ // USE CASE // source_id = N/A // source_ref_id = 10 // destination_id = N/A // destination_ref_id = 20 // PROCESS FOR MULTIPLE FILE AS PER REF ID // if destination ref id is not provided then destination id will be used if(!inputItem.destination_ref_id){ // USE CASE // source_id = N/A // source_ref_id = 9 // destination_id = 10 // destination_ref_id = N/A // considering that id is passed const nWhere: any = {}; nWhere[pk_field] = inputItem.destination_id; const destRecordArr = await this.repository.findOne({where: nWhere}); if(destRecordArr) inputItem.destination_ref_id = String(destRecordArr.destination_ref_id); else snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`); } if(inputItem.destination_ref_id){ // get the record from the database const findWhere: any = {}; findWhere[fieldMeta.data.ref_id_field] = inputItem.source_ref_id; let currRecordArr = await this.repository.find({ where: findWhere }); if(currRecordArr.length > 0 && inputItem.relocation_type){ mutiRelocate = true; let relocation: FileRelocationTypeEnum | undefined; // as need to process multiple records loop through all for(let i=0; i < currRecordArr.length; i++){ const multiResp = new this.FILE_RELOCATION_OUTPUT_DTO(); // get destination path and source path const destinationPath = await this._uploadFileDestinationPath(fieldMeta, inputItem.destination_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourcePath = await this._uploadFileDestinationPath(fieldMeta, inputItem.source_ref_id, inputItem.file_field, this.ENTITY.uploaddir); const sourceFileName = currRecordArr[i][fieldMeta.propertyKey]; try{ relocation = await this._relocateFile(inputItem.relocation_type, sourceFileName, sourcePath, destinationPath); let newRecordId: any; if(relocation === FileRelocationTypeEnum.MOVE){ // update source ref id await this._updateRecordRefIdById(pk_field, currRecordArr[i][pk_field],fieldMeta.data.ref_id_field, inputItem.destination_ref_id); // set new record id as current source id becuase data is only updated newRecordId = currRecordArr[i][pk_field]; }else if(relocation == FileRelocationTypeEnum.DUPLICATE){ // TODO: create new record based on ref id and file field newRecordId = await this._createRecord(fieldMeta.data.ref_id_field, inputItem.destination_ref_id, inputItem.file_field, sourceFileName); } // get new insterted record const nWhere: any = {}; nWhere[pk_field] = newRecordId; const newRecord = await this.repository.findOne({ where: nWhere }); multiResp.access_url = await this._getUploadFileAccessUrl(fieldMeta, inputItem.file_field, newRecord, this.ENTITY.uploaddir); multiResp.file_field = inputItem.file_field; multiResp.id = newRecordId; multiResp.ref_id = inputItem.destination_ref_id; multiResp.affected = 1; multiResp.relocation_type = relocation; snapshot.success.push(`File relocated successfully.`); multiResp.snapshot = snapshot } catch (err: any) { multiResp.relocation_type = inputItem.relocation_type; snapshot.error.push(`Error relocating file ${sourceFileName} from ${sourcePath} to ${destinationPath}. ${err.message}`); } multiResp.snapshot = snapshot; respArr.push(multiResp); } } else{ snapshot.error.push(`ref_id ${inputItem.source_ref_id} not found. Please enter proper ref id to complete the file relocation process`); } } else{ snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`); } } else{ // USE CASE // source_id = 1 // source_ref_id = N/A // destination_id = N/A // destination_ref_id = N/A // USE CASE // source_id = N/A // source_ref_id = N/A // destination_id = N/A // destination_ref_id = N/A // USE CASE // source_id = N/A // source_ref_id = N/A // destination_id = N/A // destination_ref_id = 20 // USE CASE // source_id = N/A // source_ref_id = N/A // destination_id = 2 // destination_ref_id = 20 // USE CASE // source_id = 1 // source_ref_id = 10 // destination_id = N/A // destination_ref_id = N/A snapshot.error.push(`source_id and destination_id OR source_ref_id and destination_ref_id is required. Please provide valid combination input to process file relocation`); } } else{ snapshot.error.push(`There is no upload feature available as per file_field, so you can not perform file relocation`); } if(mutiRelocate == false){ resp.snapshot = snapshot; respArr.push(resp); } } return respArr; } catch(err: any){ this.logService.error(err, `Update failed: ${err.message}`); throw new BadRequestException(err, err.message); } } private async _createRecord(refField: string, refId: string, fileField: string, fileName: string): Promise { try{ const req: any = {}; req[refField] = refId; req[fileField] = fileName; const create = this.repository.create(req); const insert = await this.repository.insert(create); if(insert.identifiers.length === 1) { return insert.identifiers[0].id; } } catch(err: any){ throw new BadRequestException(err, err.message); } } private async _updateRecordRefIdById(pkField: string, pkValue: string, refField: string, refId: string): Promise { try{ const uSets: any = {}; const uWhere: any = {}; uSets[refField] = refId; uWhere[pkField] = pkValue; getMetadataArgsStorage().columns.find(column => column.propertyName === refField && column.target === this.ENTITY )!.options.update = true; const affectedRow = await this.repository.update(uWhere, uSets); getMetadataArgsStorage().columns.find(column => column.propertyName === refField && column.target === this.ENTITY )!.options.update = false; return affectedRow.affected as number; } catch(err: any){ throw new BadRequestException(err, err.message); } } private async _updateRecordFileName(pkField: string, pkValue: string, fileField: string, fileName: string | null): Promise { try{ const uSets: any = {}; const uWhere: any = {}; uSets[fileField] = fileName; uWhere[pkField] = pkValue; const affectedRow = await this.repository.update(uWhere, uSets); return affectedRow.affected as number; } catch(err: any){ throw new BadRequestException(err, err.message); } } private async _deleteRecord(pk_value: string | number): Promise{ try{ const resp = await this.repository.delete(pk_value as number); return resp; } catch(err: any){ throw new BadRequestException(err, err.message); } } private async _SingleRecordRelocation(){ } private async MultiRecordSingleRelocation(){ } }