Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ed57d0f
chore: remove jest from dependencies
viniciuslora Sep 30, 2025
9cfacf2
Switch tenantID from req to cds.context
eric-pSAP Oct 1, 2025
335e28b
Added race condition check
KoblerS Oct 6, 2025
27faa77
Enhance malware scanning with error handling and remove race conditio…
KoblerS Oct 6, 2025
27ecb3b
Enhance malware scanning by removing hardcoded tenant ID and adding e…
KoblerS Oct 7, 2025
d144df9
Refactor malware scan handling to improve error management and clarif…
KoblerS Oct 7, 2025
8c131a9
fix: delete attachments after deleting parente when request coming fr…
viniciuslora Oct 13, 2025
faa30cc
Merge branch 'main' into fix/delete-attachments-when-delete-parent
KoblerS Oct 14, 2025
9d2b5a9
Merge branch 'main' into fix/delete-attachments-when-delete-parent
KoblerS Oct 16, 2025
80ded3a
Proper fix for deleting attachment when deleting child entities
schiwekM Oct 16, 2025
2f80fb3
Update lib/plugin.js
schiwekM Oct 17, 2025
b1f6d04
Update lib/plugin.js
schiwekM Oct 17, 2025
e07e549
Update lib/plugin.js
schiwekM Oct 17, 2025
386ea39
Update lib/aws-s3.js
schiwekM Oct 17, 2025
a0be638
Merge branch 'main' into fix/delete-attachments-when-delete-parent
schiwekM Oct 17, 2025
f28bf63
Update lib/aws-s3.js
schiwekM Oct 17, 2025
47470f9
Update lib/aws-s3.js
KoblerS Oct 17, 2025
0328dc6
Update lib/plugin.js
KoblerS Oct 17, 2025
8313658
Update lib/plugin.js
KoblerS Oct 17, 2025
11d2a10
Update lib/plugin.js
KoblerS Oct 17, 2025
ea18bf6
Move generic handlers into basic + outbox
schiwekM Oct 17, 2025
5826462
Update basic.js
schiwekM Oct 17, 2025
0dab512
Added test case to ensure delete event is put into outbox
schiwekM Oct 19, 2025
9de2259
Merge branch 'main' into fix/delete-attachments-when-delete-parent
schiwekM Oct 20, 2025
4569bd3
Update plugin.js
schiwekM Oct 20, 2025
6de417f
Update lib/basic.js
schiwekM Oct 20, 2025
2277ae0
Update lib/basic.js
schiwekM Oct 20, 2025
9caf5cb
Update lib/basic.js
schiwekM Oct 20, 2025
ca3f99e
Removed unused parameter
schiwekM Oct 20, 2025
e3ddeb2
Merge branch 'fix/delete-attachments-when-delete-parent' of https://g…
schiwekM Oct 20, 2025
d5b8811
Update lib/basic.js
schiwekM Oct 20, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
node_modules/
gen/
.vscode/
package-lock.json
package-lock.json
200 changes: 38 additions & 162 deletions lib/aws-s3.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
} else {
logConfig.info('Separate object store mode enabled - clients will be created per tenant')
}

this.on('DeleteAttachment', async msg => {
await this.delete(msg.url);
});
}

/**
Expand Down Expand Up @@ -375,54 +379,6 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
}
}

/**
* Deletes a file from S3 based on the provided key
* @param {string} key - The key of the file to delete
* @returns {Promise} - Promise resolving when deletion is complete
*/
async deleteAttachment(key) {
if (!key) return
return await this.delete(key)
}

/**
* Registers attachment handlers for the given service and entity
* @param {*} records - The records to process
* @param {import('@sap/cds').Request} req - The request object
*/
async deleteAttachmentsWithKeys(records, req) {
if (req?.attachmentsToDelete?.length > 0) {
req.attachmentsToDelete.forEach((attachment) => {
this.deleteAttachment(attachment.url)
})
}
}

/**
* Registers attachment handlers for the given service and entity
* @param {import('@sap/cds').Request} req - The request object
*/
async attachDeletionData(req) {
const attachments = cds.model.definitions[req?.target?.name + ".attachments"]
if (attachments) {
const diffData = await req.diff()
let deletedAttachments = []
diffData.attachments?.filter((object) => {
return object._op === "delete"
})
.map((attachment) => {
deletedAttachments.push(attachment.ID)
})

if (deletedAttachments.length > 0) {
let attachmentsToDelete = await SELECT.from(attachments).columns("url").where({ ID: { in: [...deletedAttachments] } })
if (attachmentsToDelete.length > 0) {
req.attachmentsToDelete = attachmentsToDelete
}
}
}
}

/**
* Registers attachment handlers for the given service and entity
* @param {import('@sap/cds').Request} req - The request object
Expand Down Expand Up @@ -481,128 +437,48 @@ module.exports = class AWSAttachmentsService extends require("./basic") {
}
}

/**
* Registers attachment handlers for the given service and entity
* @param {{draftEntity: string, activeEntity:cds.Entity, id:string}} param0 - The service and entities
* @returns
*/
async getAttachmentsToDelete({ draftEntity, activeEntity, whereXpr }) {
const [draftAttachments, activeAttachments] = await Promise.all([
SELECT.from(draftEntity).columns("url").where(whereXpr),
SELECT.from(activeEntity).columns("url").where(whereXpr)
])

const activeUrls = new Set(activeAttachments.map(a => a.url))
return draftAttachments
.filter(({ url }) => !activeUrls.has(url))
.map(({ url }) => ({ url }))
}

/**
* Add draft attachment deletion data to the request
* @param {import('@sap/cds').Request} req - The request object
*/
async attachDraftDeletionData(req) {
const draftEntity = cds.model.definitions[req?.target?.name]
const name = req?.target?.name
const activeEntity = name ? cds.model.definitions?.[name.split(".").slice(0, -1).join(".")] : undefined

if (!draftEntity || !activeEntity) return

const diff = await req.diff()
if (diff._op !== "delete" || !diff.ID) return

const attachmentsToDelete = await this.getAttachmentsToDelete({
draftEntity,
activeEntity,
whereXpr: { ID: diff.ID }
})

if (attachmentsToDelete.length) {
req.attachmentsToDelete = attachmentsToDelete
}
}

/**
* Add draft discard deletion data to the request
* @param {import('@sap/cds').Request} req - The request object
*/
async attachDraftDiscardDeletionData(req) {
const parentEntity = req.target.name.split('.').slice(0, -1).join('.')
const draftEntity = cds.model.definitions[`${parentEntity}.attachments.drafts`]
const activeEntity = cds.model.definitions[`${parentEntity}.attachments`]
if (!draftEntity || !activeEntity) return

const whereXpr = []
for (const foreignKey of activeEntity.keys['up_']._foreignKeys) {
if (whereXpr.length) {
whereXpr.push('and')
}
whereXpr.push(
{ref: [foreignKey.parentElement.name]},
'=',
{val: req.data[foreignKey.childElement.name]}
)
}
const attachmentsToDelete = await this.getAttachmentsToDelete({
draftEntity,
activeEntity,
whereXpr
})

if (attachmentsToDelete.length) {
req.attachmentsToDelete = attachmentsToDelete
}
}

/**
* @inheritdoc
*/
registerUpdateHandlers(srv, entity, mediaElement) {
srv.before(["DELETE", "UPDATE"], entity, this.attachDeletionData.bind(this))
srv.after(["DELETE", "UPDATE"], entity, this.deleteAttachmentsWithKeys.bind(this))

srv.prepend(() => {
srv.on(
"PUT",
mediaElement,
this.updateContentHandler.bind(this)
)
})
}

/**
* @inheritdoc
*/
registerDraftUpdateHandlers(srv, entity, mediaElement) {
srv.before(["DELETE", "UPDATE"], entity, this.attachDeletionData.bind(this))
srv.after(["DELETE", "UPDATE"], entity, this.deleteAttachmentsWithKeys.bind(this))

// case: attachments uploaded in draft and draft is discarded
srv.before("CANCEL", entity.drafts, this.attachDraftDiscardDeletionData.bind(this))
srv.after("CANCEL", entity.drafts, this.deleteAttachmentsWithKeys.bind(this))

srv.prepend(() => {
if (mediaElement.drafts) {
registerUpdateHandlers(srv, mediaElements) {
for (const mediaElement of mediaElements) {
srv.prepend(() => {
srv.on(
"PUT",
mediaElement.drafts,
mediaElement,
this.updateContentHandler.bind(this)
)
})
}
}

// case: attachments uploaded in draft and deleted before saving
srv.before(
"DELETE",
mediaElement.drafts,
this.attachDraftDeletionData.bind(this)
)
srv.after(
"DELETE",
mediaElement.drafts,
this.deleteAttachmentsWithKeys.bind(this)
)
}
})
/**
* @inheritdoc
*/
registerDraftUpdateHandlers(srv, entity, mediaElements) {
for (const mediaElement of mediaElements) {
srv.prepend(() => {
if (mediaElement.drafts) {
srv.on(
"PUT",
mediaElement.drafts,
this.updateContentHandler.bind(this)
)

// case: attachments uploaded in draft and deleted before saving
srv.before(
"DELETE",
mediaElement.drafts,
this.attachDraftDeletionData.bind(this)
)
srv.after(
"DELETE",
mediaElement.drafts,
this.deleteAttachmentsWithKeys.bind(this)
)
}
})
}
}

/**
Expand Down
Loading