UNPKG

@cap-js/sdm

Version:

CAP plugin for effortless integration of CAP applications with SAP Document Management Service.

1,067 lines (959 loc) 36 kB
const cds = require("@sap/cds/lib"); const NodeCache = require("node-cache"); const cache = new NodeCache(); const { getRepositoryInfo, getFolderIdByPath, getFolderIdByIDAsPath, createFolder, createAttachment, editLink, deleteAttachmentsOfFolder, deleteFolderWithAttachments, getAttachment, readAttachment, updateAttachment } = require("./handler/index"); const { fetchAccessToken, isRepositoryVersioned, getConfigurations, getClientCredentialsToken, isRestrictedCharactersInName, getStatusCondition, getPropertyTitles, getSecondaryPropertiesWithInvalidDefinition, getSecondaryTypeProperties, getUpdatedSecondaryProperties } = require("./util/index"); const { getDraftAttachments, getDraftAttachmentsForUpID, getFileNameForAttachmentID, getPropertiesForID, getURLsToDeleteFromAttachments, getURLsToDeleteFromDraftAttachments, getURLToDeleteFromDraftAttachments, getURLFromAttachments, getMetadataForOpenAttachment, getDraftAttachmentsMetadataForLinkCreation, getFolderIdForEntity, updateAttachmentInDraft, updateLinkInDraft, setRepositoryId, getDraftAdministrativeData_DraftUUIDForUpId, getAttachmentById, editLinkInDraft } = require("../lib/persistence"); const { duplicateDraftFileErr, renameFileErr, virusFileErr, duplicateFileErr, versionedRepositoryErr, otherFileErr, userDoesNotHaveRequiredScope, userNotAuthorisedError, renameOtherFilesErr, nameConstrainErr, linkNameConstraintMessage, sdmRolesErrorMessage, unsupportedProperties, noSDMRolesErrorMessage, unsupportedPropertiesErrorMessage, badRequestErrorMessage, emptyFileNameErr, userNotAuthorisedErrorLink, userNotAuthorisedErrorEditLink, attachmentIDRegex, editLinkNotFoundErr, userNotAuthorisedOpenLink, userNotAuthorisedReadError, attachmentNotFound, errorMessage } = require("./util/messageConsts"); module.exports = class SDMAttachmentsService extends ( require("@cap-js/attachments/lib/basic") ) { init() { this.creds = this.options.credentials; // Temporary storage for original URLs during draft editing this.originalUrlMap = new Map(); return super.init(); } getSDMCredentials() { return this.creds; } async checkRepositoryType(req) { const { repositoryId } = getConfigurations(); let subdomain = cds.context.user?.tokenInfo?.getPayload()?.ext_attr?.zdn; //check if repository is versionable let repotype = cache.get(repositoryId+"_"+subdomain); let isVersioned; if (repotype == undefined) { const token = await getClientCredentialsToken(this.creds); const repoInfo = await getRepositoryInfo(req, this.creds, token); isVersioned = isRepositoryVersioned(repoInfo, repositoryId); } else { isVersioned = repotype == "versioned"; } if (isVersioned) { req.reject(400, versionedRepositoryErr); } } async get(attachments, keys, req) { const response = await getURLFromAttachments(keys, attachments); const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); const Key = response?.url; const content = await readAttachment(Key, token, this.creds); if(content && typeof content === 'object' && content.status && content.status == 200){ return content.data; } else{ if(content == "Forbidden"){ req.reject(403, userNotAuthorisedReadError); } else if(content =="Not Found"){ req.reject(404, attachmentNotFound); } else req.reject(500, errorMessage); } } async renameHandler(req) { const { repositoryId } = getConfigurations(); const attachmentsEntity = cds.model.definitions[req.target.name + ".attachments"]; const attachment_val = await getDraftAttachments(attachmentsEntity, req, repositoryId); if (attachment_val.length > 0) { const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); await this.isFileNameDuplicateInDrafts(attachment_val, req); const propertyTitles = getPropertyTitles(attachmentsEntity, attachment_val[0]); const secondaryPropertiesWithInvalidDefinitions = getSecondaryPropertiesWithInvalidDefinition( attachmentsEntity, attachment_val[0]); const secondaryTypeProperties = getSecondaryTypeProperties(attachmentsEntity, attachment_val[0]); let attachment_val_rename = []; let draft_attachments = []; draft_attachments = attachment_val.filter(attachment => attachment.HasActiveEntity === false); attachment_val_rename = attachment_val.filter(attachment => attachment.HasActiveEntity === true); let allErrors = []; // Updating draft attachments for (const attachment of draft_attachments) { const errorResponse = await this.updateDraftAttachments(req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties); allErrors = allErrors.concat(errorResponse); } // Updating non-draft attachments for (const attachment of attachment_val_rename) { const errorResponse = await this.updateNonDraftAttachments(req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties); allErrors = allErrors.concat(errorResponse); } this.clearSecondaryPropertiesCache(repositoryId); const errorMessage = this.handleWarning(allErrors, propertyTitles); if(errorMessage.length != 0){ req.warn(500, errorMessage); } } } async updateDraftAttachments(req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties) { const attachmentData = await this.getAttachementDataInSDM(this.creds.uri, token, attachment.url); const filenameInSDM = attachmentData.filename; return this._updateAttachments( req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties, filenameInSDM ); } async updateNonDraftAttachments(req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties) { const fileNameInDB = await getFileNameForAttachmentID(attachmentsEntity, attachment.ID); return this._updateAttachments( req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties, fileNameInDB ); } async _updateAttachments(req, token, attachment, attachmentsEntity, secondaryPropertiesWithInvalidDefinitions, secondaryTypeProperties, filenameInSDM) { let failedReq = []; const propertiesInDB = await getPropertiesForID(attachmentsEntity, attachment.ID, secondaryTypeProperties); const updatedSecondaryProperties = getUpdatedSecondaryProperties(attachment, secondaryTypeProperties, propertiesInDB); const filenameInRequest = attachment.filename; if (isRestrictedCharactersInName(filenameInRequest)) { failedReq.push({ typeOfError: 'restricted characters', name: filenameInRequest }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); return failedReq; } if (filenameInRequest == null || filenameInRequest.trim().length === 0) { failedReq.push({ typeOfError: 'empty name', name: filenameInRequest }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); return failedReq; } else if (filenameInSDM !== filenameInRequest) { updatedSecondaryProperties["cmis:name"] = filenameInRequest; } if (Object.keys(updatedSecondaryProperties).length > 0) { try { const responseCode = await updateAttachment(req, attachment, this.creds, token, updatedSecondaryProperties, secondaryPropertiesWithInvalidDefinitions); switch (responseCode) { case 403: failedReq.push({ typeOfError: 'no sdm roles', name: filenameInRequest }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); break; case 409: failedReq.push({ typeOfError: 'duplicate', name: filenameInRequest }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); break; case 404: failedReq.push({ typeOfError: 'not found', name: filenameInRequest }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); break; case 200: case 201: // Success cases, do nothing break; default: throw new Error(sdmRolesErrorMessage); } } catch (e) { if (e.message.startsWith(unsupportedProperties)) { const unsupportedDetails = e.message.substring(unsupportedProperties.length).trim(); failedReq.push({ typeOfError: 'unsupported properties', details: unsupportedDetails }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); } else { failedReq.push({ typeOfError: 'bad request', name: filenameInRequest, message: e.message }); this.replacePropertiesInAttachment(req, attachment.ID, filenameInSDM, propertiesInDB, secondaryTypeProperties); } } } return failedReq; } replacePropertiesInAttachment(req, id, fileName, propertiesInDB, secondaryTypeProperties) { const attachment = req.data.attachments.find(element => element.ID === id); if (!attachment) { return; } if (propertiesInDB) { for (const [dbKey, dbValue] of Object.entries(propertiesInDB)) { const secondaryKey = [...secondaryTypeProperties.entries()] .find(([, value]) => value === dbKey)?.[0]; if (secondaryKey) { attachment[secondaryKey] = dbValue; } } } // Replace the file name in the attachment attachment.filename = fileName; } clearSecondaryPropertiesCache(repositoryId) { const cacheKey = `validSecondaryProperties_${repositoryId}`; // Check if the cache exists and remove the key if (cache.has(cacheKey)) { cache.del(cacheKey); // Emptying cache after attachments are updated in loop } } handleWarning(allErrors, propertyTitles) { const restrictedCharacters = allErrors.filter(error => error.typeOfError === 'restricted characters' ); const duplicate = allErrors.filter(error => error.typeOfError === 'duplicate' ); const duplicateNames = duplicate.map(attachment => attachment.name); const notFound = allErrors.filter(error => error.typeOfError === 'not found' ); const notFoundNames = notFound.map(attachment => attachment.name); const noSDMRoles = allErrors.filter(error => error.typeOfError === 'no sdm roles' ); const noSDMRolesNames = noSDMRoles.map(attachment => attachment.name); const unsupportedProperties = allErrors.filter(error => error.typeOfError === 'unsupported properties' ); const unsupportedPropertiesDetails = unsupportedProperties.map(attachment => attachment.details); const badRequest = allErrors.filter(error => error.typeOfError === 'bad request' ); const emptyFileNames = allErrors.filter(error => error.typeOfError === 'empty name' ); const otherErrors = allErrors.filter(error => error.typeOfError !== 'duplicate' && error.typeOfError !== 'not found' && error.typeOfError !== 'restricted characters' && error.typeOfError !== 'no sdm roles' && error.typeOfError !== 'unsupported properties' && error.typeOfError !== 'bad request' && error.typeOfError !== 'empty name' ); const otherNames = otherErrors.map(attachment => attachment.name); const otherMessages = otherErrors.map(attachment => attachment.typeOfError); let errorResponse = ""; if (restrictedCharacters.length > 0) { errorResponse += nameConstrainErr(restrictedCharacters.map(attachment => attachment.name), "Update"); } if (duplicateNames.length > 0) { errorResponse += renameFileErr(duplicateNames, getStatusCondition(409)); } if (notFoundNames.length > 0) { errorResponse += renameFileErr(notFoundNames, getStatusCondition(404)); } if (noSDMRolesNames.length > 0) { errorResponse += noSDMRolesErrorMessage(noSDMRolesNames, "update"); } if (unsupportedPropertiesDetails.length > 0) { const invalidPropertyNames = []; const uniqueValues = new Set(); // Extract unique values from filesWithUnsupportedProperties unsupportedPropertiesDetails.forEach((str) => { const values = str.split(","); values.forEach((value) => { uniqueValues.add(value.trim()); }); }); // Convert the Set to an array and map property titles const propertiesList = Array.from(uniqueValues); propertiesList.forEach((file) => { invalidPropertyNames.push(propertyTitles[file]); }); // Warn if invalid property names exist if (invalidPropertyNames.length > 0) { errorResponse += unsupportedPropertiesErrorMessage(invalidPropertyNames); } } if (badRequest.length > 0) { errorResponse += badRequestErrorMessage(badRequest); } if (emptyFileNames.length > 0) { errorResponse += emptyFileNameErr; } if (otherNames.length > 0) { errorResponse += renameOtherFilesErr(otherNames, otherMessages); } return errorResponse; } async getAttachementDataInSDM(uri, token, objectId) { const response = await getAttachment(uri, token, objectId); const responseData = { filename: response?.data?.succinctProperties["cmis:name"], folderId: response?.data?.succinctProperties["sap:parentIds"][0] }; return responseData; } async draftSaveHandler(req) { if (req?.data?.content) { const { repositoryId } = getConfigurations(); await this.checkRepositoryType(req); const draftAttachments = req.target; const attachment_val = await getDraftAttachmentsForUpID(draftAttachments, req, repositoryId); if (attachment_val.length > 0) { const attachmentID = req.req.url.match(attachmentIDRegex)[1]; const attachmentToUpload = attachment_val.find(attachment => attachment.ID === attachmentID); const filename = attachmentToUpload ? attachmentToUpload.filename : null; if (filename) { const nameConstraint = isRestrictedCharactersInName(filename); if (nameConstraint) { req.reject(409, nameConstrainErr([filename], "Upload")); } } await this.isFileNameDuplicateInDrafts(attachment_val, req); const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); let attachment_val_create = []; if (req.data.content) { attachment_val_create = attachment_val.filter(attachment => attachment.HasActiveEntity === false && attachment.ID === attachmentID); } if(attachment_val_create.length>0){ attachment_val_create[0].content = req.data.content; await this.create(attachment_val_create, draftAttachments, req, token) } } req.data.content = null; } } async create(attachment_val_create, attachments, req, token){ let parentId = await this.getParentId(attachments, req, token) await this.onCreate( attachment_val_create, this.creds, token, req, parentId ); } async getParentId(attachments, req, token, upId){ const { repositoryId } = getConfigurations(); const folderIds = await getFolderIdForEntity(attachments, req, repositoryId, upId); let parentId = null; for (const folder of folderIds) { if (folder.folderId !== null) { parentId = folder.folderId; break; } } if (!parentId) { const folderId = await getFolderIdByPath( req, this.creds, token, attachments, upId ); if (folderId) { parentId = folderId; } else { const response = await createFolder( req, this.creds, token, attachments, upId ); if (response.status == 403 && response.response.data == userDoesNotHaveRequiredScope) { req.reject(403, userNotAuthorisedError); } parentId = response.data.succinctProperties["cmis:objectId"]; } } return parentId; } async isFileNameDuplicateInDrafts(data, req) { let fileNames = []; for (let index in data) { fileNames.push(data[index].filename); } const duplicates = this.filterDuplicates(fileNames); if (duplicates.length != 0) { req.reject(409, duplicateDraftFileErr(duplicates.join(", "))); } } async validateLinkName(data, linkNameInRequest, req) { const nameConstraint = isRestrictedCharactersInName(linkNameInRequest); if (nameConstraint) { req.reject(409, linkNameConstraintMessage([linkNameInRequest], "created")); } let fileNames = []; for (let index in data) { fileNames.push(data[index].filename); } fileNames.push(linkNameInRequest); const duplicates = this.filterDuplicates(fileNames); if (duplicates.length != 0) { req.reject(409, duplicateDraftFileErr(duplicates.join(", "))); } } filterDuplicates(fileNames) { return [ ...new Set( fileNames.filter((value, index, self) => { return self.indexOf(value) !== index; }) ), ]; } async filterAttachments(req) { const { repositoryId } = getConfigurations(); if (!req.query.SELECT.where) { req.query.SELECT.where = []; } if (req.query.SELECT.where.length > 0) { req.query.SELECT.where.push('and'); } req.query.SELECT.where.push( { ref: ['repositoryId'] }, '=', { val: repositoryId } ); } async setRepository(req) { const attachments = cds.model.definitions[req.target.name]; const { repositoryId } = getConfigurations(); // Fetch repositoryId from configurations await setRepositoryId(attachments, repositoryId) } async attachDeletionData(req) { const attachments = cds.model.definitions[req.target.name + ".attachments"]; if (attachments) { const diffData = await req.diff(); let deletedAttachments = []; if (diffData?.attachments?.length > 0) { diffData.attachments .filter((object) => { return object._op === "delete"; }) .map((attachment) => { deletedAttachments.push(attachment.ID); }); if (deletedAttachments.length > 0) { const attachmentsToDelete = await getURLsToDeleteFromAttachments( deletedAttachments, attachments ); if (attachmentsToDelete.length > 0) { req.attachmentsToDelete = attachmentsToDelete; } } if (req.event == "DELETE") { const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); const folderId = await getFolderIdByIDAsPath( req, this.creds, token, attachments ); if (folderId) { req.parentId = folderId; } } } } } async attachDraftDeletionData(req) { let draftAttachments = cds.model.definitions[req.target.name.replace(/\.drafts$/, ".attachments.drafts")]; if(draftAttachments) { const attachmentsToDeleteFromDraft = await getURLsToDeleteFromDraftAttachments(req.data.ID, draftAttachments); if (attachmentsToDeleteFromDraft?.length > 0) { req.attachmentsToDelete = attachmentsToDeleteFromDraft; } const diffData = await req.diff(); if (req.event == "DELETE" && diffData.attachments?.length == req.attachmentsToDelete?.length) { const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); const folderId = await getFolderIdByIDAsPath( req, this.creds, token, draftAttachments ); if (folderId) { req.parentId = folderId; } } } } async attachURLsToDeleteFromAttachmentsDraft(req) { let draftAttachments = cds.model.definitions[req.target.name]; if(draftAttachments) { const attachmentsToDeleteFromDraft = await getURLToDeleteFromDraftAttachments(req.data.ID, draftAttachments); if (attachmentsToDeleteFromDraft?.length > 0) { req.attachmentsToDelete = attachmentsToDeleteFromDraft; } if (req?.attachmentsToDelete?.length > 0) { await this.deleteAttachmentsWithKeys(req.attachmentsToDelete, req); } } } async deleteAttachmentsWithKeys(records, req) { let failedReq = [], Ids = []; if (req?.attachmentsToDelete?.length > 0) { const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); if (req?.parentId) { await deleteFolderWithAttachments(this.creds, token, req.parentId); } else { const deletePromises = req.attachmentsToDelete.map( async (attachment) => { const deleteAttachmentResponse = await deleteAttachmentsOfFolder( this.creds, token, attachment.url ); const delData = await this.handleRequest( deleteAttachmentResponse, attachment.url ); if (delData && Object.keys(delData).length > 0) { failedReq.push(delData.message); Ids.push(delData.ID); } } ); // Execute all promises await Promise.all(deletePromises); let removeCondition = (obj) => Ids.includes(obj.ID); req.attachmentsToDelete = req.attachmentsToDelete.filter( (obj) => !removeCondition(obj) ); let errorResponse = ""; failedReq.forEach((attachment) => { errorResponse = errorResponse + "\n" + attachment; }); if (errorResponse != "") req.info(200, errorResponse); } } else { if (req?.parentId) { const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); await deleteFolderWithAttachments(this.creds, token, req.parentId); } } } async onCreate(data, credentials, token, req, parentId) { let fileNames = []; const { repositoryId } = getConfigurations(); await Promise.all( data.map(async (d) => { const response = await createAttachment( d, credentials, token, parentId ); if (response.status == 201) { d.folderId = parentId; d.url = response.data?.succinctProperties["cmis:objectId"]; d.repositoryId = repositoryId; d.content = null; d.type = 'sap-icon://document'; await updateAttachmentInDraft(req, d); } else { console.log("Response "+response.response.data.message); fileNames.push(d.filename); if(response.response.data.message == 'Malware Service Exception: Virus found in the file!'){ req.reject(403, virusFileErr(fileNames)); } else if(response.response.data.exception == "nameConstraintViolation"){ req.reject(409, duplicateFileErr(fileNames)); } else if(response.status == 403){ req.reject(403, userNotAuthorisedError); } else{ req.reject(otherFileErr(fileNames)); } } }) ); } async openAttachment(req) { let attachments = cds.model.definitions[req.target.name]; let attachmentId = { ID: req.req.url.match(attachmentIDRegex)[1] } let response = await getMetadataForOpenAttachment(attachmentId, attachments); let objectId = response?.url; if (response?.filename == null) { attachments = cds.model.definitions[req.target.name.replace(/\.drafts$/, "")]; response = await getMetadataForOpenAttachment(attachmentId, attachments); } if (response?.mimeType.toLowerCase() == "application/internet-shortcut") { //Fetch token see if roles are present const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); const authresponse = await getAttachment(this.creds.uri, token, objectId); if(authresponse == "Forbidden" || (authresponse && typeof authresponse === 'object' && authresponse.status === 403)){ req.reject(403,userNotAuthorisedOpenLink); } return { value: response.linkUrl }; } else { return { value: "None" }; } } async handleCreateLinkAction(req) { const { repositoryId } = getConfigurations(); let key = req.req.url.match(attachmentIDRegex)[1]; const linkNameInRequest = req.data.name; console.info(`[createLink] action called`, { repositoryId, entity: req.target?.name, key, linkName: linkNameInRequest, user: req.user?.id }); await this.checkRepositoryType(req); const attachment = cds.model.definitions[req.target.name]; const draftAttachments = await getDraftAttachmentsMetadataForLinkCreation(key, attachment, repositoryId); await this.validateLinkName(draftAttachments, linkNameInRequest, req); const linkToCreateInSDM = { filename: linkNameInRequest, mimeType: "application/internet-shortcut", repositoryId: repositoryId, linkUrl: req.data.url }; const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); await this.processLinkCreation(linkToCreateInSDM, attachment, req, token); } async processLinkCreation(linkToCreateInSDM, attachment, req, token) { const upIdKey = attachment.keys.up_.keys[0].$generatedFieldName; const upId = req.req.url.match(attachmentIDRegex)[1]; console.info(`[processLinkCreation] called`, { upIdKey, upId, linkToCreateInSDM }); let parentId = await this.getParentId(attachment, req, token, upId); console.info(`[processLinkCreation] parentId resolved`, { parentId }); await this.createLink( linkToCreateInSDM, this.creds, token, req, parentId, upIdKey ); console.info(`[processLinkCreation] createLink completed`, { parentId, upIdKey, upId }); } async createLink(linkToCreateInSDM, credentials, token, req, parentId, upIdKey) { const { repositoryId } = getConfigurations(); const upId = req.req.url.match(attachmentIDRegex)[1]; console.info(`[createLink] called`, { linkToCreateInSDM, parentId, upIdKey, upId }); // Process single link object const response = await createAttachment( linkToCreateInSDM, credentials, token, parentId ); console.info(`[createLink] createAttachment response`, { status: response.status }); if (response.status == 201) { const draftUUID = await getDraftAdministrativeData_DraftUUIDForUpId(req, upIdKey, upId); console.info(`[createLink] draftUUID fetched`, { draftUUID: draftUUID[0]?.DraftAdministrativeData_DraftUUID }); // Update the link in draft const updatedFields = { url: response.data?.succinctProperties['cmis:objectId'], repositoryId: repositoryId, folderId: parentId, status: "Clean", type: "sap-icon://internet-browser", [upIdKey]: upId, mimeType: response.data?.succinctProperties['cmis:contentStreamMimeType'], filename: req.data?.name, HasDraftEntity: false, HasActiveEntity: false, linkUrl: req.data?.url, DraftAdministrativeData_DraftUUID: draftUUID[0].DraftAdministrativeData_DraftUUID, }; console.info(`[createLink] updating link in draft`, { updatedFields }); await updateLinkInDraft(req, updatedFields); } else { const fileName = req.data?.name; if (response.response.data.exception == "nameConstraintViolation") { console.warn(`[createLink] nameConstraintViolation`, { fileName, response: response.response.data }); req.reject(409, duplicateFileErr([fileName])); } else if (response.status == 403) { console.warn(`[createLink] user not authorised`, { user: req.user?.id, response: response.response.data }); req.reject(403, userNotAuthorisedErrorLink); } else { console.error(`[createLink] other error`, { message: response?.response?.data?.message, response: response.response.data }); req.reject(response?.response?.data?.message); } } } async handleEditLinkAction(req) { const attachmentId = req.req.url.match(attachmentIDRegex)[1]; const attachmentsEntity = cds.model.definitions[req.target.name]; const existingAttachment = await getAttachmentById(attachmentId, attachmentsEntity); if (!existingAttachment || !existingAttachment.url) { req.reject(404, editLinkNotFoundErr); return; } const newLinkUrl = req.data.url; const token = await fetchAccessToken( this.creds, req.user.tokenInfo.getTokenValue() ); const filenameToUpdate = existingAttachment.filename.replace(/\.url$/, ''); const objectIdToUpdate = existingAttachment.url; const response = await editLink(objectIdToUpdate, filenameToUpdate, newLinkUrl, this.creds, token); const status = response?.status || response?.code; if (status === 200 || status === 201) { let baselineUrl = existingAttachment.linkUrl; const attachmentKey = `${attachmentId}`; if (this.originalUrlMap.has(attachmentKey)) { baselineUrl = this.originalUrlMap.get(attachmentKey); } else { this.originalUrlMap.set(attachmentKey, baselineUrl); } const updatedFields = { ID: attachmentId, linkUrl: newLinkUrl, note: `__BASELINE_URL__:${baselineUrl}` }; await editLinkInDraft(req, updatedFields); return { success: true, message: "Link edited successfully" }; } else if (status === 403) { console.warn(`[editLink] user not authorised`, { user: req.user?.id, response: response?.response?.data }); req.reject(400, userNotAuthorisedErrorEditLink); } else { console.error(`[editLink] other error`, { message: response?.response?.data?.message, response: response.response.data }); req.reject(response?.response?.data?.message); } } async handleDraftSaveForLinks(req) { if (!req.target?.name) { const entityPatterns = [ 'ProcessorService.Incidents.attachments', 'ProcessorService.Incidents.attachments.drafts' ]; for (const entityName of entityPatterns) { const attachmentsEntity = cds.model.definitions[entityName]; if (attachmentsEntity) { await this.updateBaselinesForEntity(entityName); } } } } async updateBaselinesForEntity(attachmentsEntityName) { for (const [attachmentKey] of this.originalUrlMap.entries()) { const attachment = await global.SELECT.one.from(attachmentsEntityName) .where({ ID: attachmentKey, mimeType: "application/internet-shortcut", note: { like: "__BASELINE_URL__:%" } }); if (attachment) { this.originalUrlMap.set(attachmentKey, attachment.linkUrl); await global.UPDATE(attachmentsEntityName) .set({ note: null }) .where({ ID: attachmentKey }); } } } async handleDraftDiscardForLinks(req) { let parentId = req.data.ID; const attachmentsEntityName = req.target.name.replace('.drafts', '.attachments.drafts'); const attachmentsEntity = cds.model.definitions[attachmentsEntityName]; const upKey = attachmentsEntity.keys?.up_?.keys?.[0]?.$generatedFieldName || 'up__ID'; const draftAttachments = await global.SELECT.from(attachmentsEntityName) .where({ [upKey]: parentId, mimeType: "application/internet-shortcut", note: { like: "__BASELINE_URL__:%" } }); const token = await fetchAccessToken( this.creds, req.user.authInfo?.token?.getTokenValue() || req.user.tokenInfo.getTokenValue() ); for (const attachment of draftAttachments) { if (attachment.note?.startsWith("__BASELINE_URL__:")) { const baselineUrl = attachment.note.substring("__BASELINE_URL__:".length); if (baselineUrl && attachment.linkUrl !== baselineUrl) { await this.revertLinkInSDM(attachment, baselineUrl, token); const attachmentKey = `${attachment.ID}`; this.originalUrlMap.delete(attachmentKey); } } } } async revertLinkInSDM(draftAttachment, originalLinkUrl, token) { try { const filenameToUpdate = draftAttachment.filename.replace(/\.url$/, ''); const objectIdToUpdate = draftAttachment.url; await editLink( objectIdToUpdate, filenameToUpdate, originalLinkUrl, this.creds, token ); } catch (error) { console.error(`[revertLinkInSDM] error reverting link for attachment ${draftAttachment.ID}:`, error.message); throw error; } } async handleRequest(response, objectId) { let responseData = {}, status = ""; if (response.status != undefined) { status = response.status; } else status = response.response.status; switch (status) { case 404: case 200: break; default: responseData["ID"] = objectId; responseData["message"] = response.message; return responseData; } } async getStatus() { return "Clean"; } registerUpdateHandlers(srv, entity, target) { srv.before( ["DELETE","UPDATE"], entity, this.attachDeletionData.bind(this) ); srv.before( ["DELETE"], entity.drafts, this.attachDraftDeletionData.bind(this) ); srv.before(["DELETE"], [ target.drafts], this.attachURLsToDeleteFromAttachmentsDraft.bind(this)); srv.before("DELETE", entity.drafts, this.handleDraftDiscardForLinks.bind(this)); srv.after("SAVE", entity, this.handleDraftSaveForLinks.bind(this)); srv.before("READ", [target, target.drafts], this.setRepository.bind(this)) srv.before("READ", [target, target.drafts], this.filterAttachments.bind(this)) srv.before("SAVE", entity, this.renameHandler.bind(this)); if (target.drafts) { srv.before( "PUT", target.drafts, this.draftSaveHandler.bind(this) ); } srv.after( ["DELETE","UPDATE"], [entity, entity.drafts], this.deleteAttachmentsWithKeys.bind(this) ); // Handler for custom action 'openAttachment' srv.on('openAttachment', async (req) => { return this.openAttachment(req); }); // Handler for custom action 'createLink' srv.on('createLink', async (req) => { return this.handleCreateLinkAction(req); }); // Handler for custom action 'editLink' srv.on('editLink', async (req) => { return this.handleEditLinkAction(req); }); } };