Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 4.0.0

### Changed

- **Breaking:** The `outbox` configuration key under `cds.requires.attachments` has been renamed to `queue`. Projects that explicitly set `attachments.outbox: true` in their own CDS configuration must rename the key to `queue`. A deprecation warning is logged at startup when the old key is detected.

## Version 3.13.1

### Fixed
Expand Down
15 changes: 2 additions & 13 deletions lib/generic-handlers.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
const cds = require("@sap/cds")
const LOG = cds.log("attachments")
const { extname } = require("path")
const {
MAX_FILE_SIZE,
sizeInBytes,
checkMimeTypeMatch,
inferTargetCAP8,
} = require("./helper")
const { MAX_FILE_SIZE, sizeInBytes, checkMimeTypeMatch } = require("./helper")
const { getMime } = require("./mime")

/**
Expand Down Expand Up @@ -40,13 +35,7 @@ async function finalizePrepareAttachment(data, req, prefix) {
// Only try to populate parent keys if there is a parent reference
if (parentRef && parentRef.length > 0) {
let target
if (cds.infer?.target) {
// CAP 9+: Use cds.infer.target
target = cds.infer.target({ SELECT: { from: { ref: parentRef } } })
} else {
// CAP 8 fallback: Use inferTargetCAP8 helper
target = inferTargetCAP8(req, parentRef)
}
target = cds.infer.target({ SELECT: { from: { ref: parentRef } } })

if (target?.keys) {
LOG.info(`Populating parent keys for attachment upload`, target)
Expand Down
35 changes: 0 additions & 35 deletions lib/helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -439,40 +439,6 @@ async function computeHash(input) {
return hash.digest("hex")
}

/**
* Resolves the target entity definition from a subject ref in CAP 8 (fallback for CAP 9+ cds.infer).
* Walks the ref path through the model and returns the draft entity if draft-enabled.
* @param {import('@sap/cds').Request} req - The current request (unused, kept for API symmetry with cds.infer)
* @param {Array<string|{id: string}>} ref - The subject ref array from req.subject
* @returns {import('@sap/cds').entity|null} The resolved entity definition, or null if not found
*/
function inferTargetCAP8(req, ref) {
const model = cds.context?.model || cds.model

// Extract entity/navigation names from ref array
// Handle both simple strings and objects with .id property
const names = ref
.map((part) => {
if (typeof part === "string") return part
if (typeof part === "object" && part.id) return part.id
return null
})
.filter(Boolean)

const name = names.join(".")

let target = model.definitions[name]
if (!target) return null

// draft fallback
if (target["@odata.draft.enabled"]) {
const draft = model.definitions[`${name}.drafts`]
if (draft) target = draft
}

return target
}

const multipliers = {}
multipliers.B = 1
multipliers.KB = multipliers.B * 1024
Expand Down Expand Up @@ -907,7 +873,6 @@ module.exports = {
traverseEntity,
buildBackAssocChain,
MAX_FILE_SIZE,
inferTargetCAP8,
getAttachmentKind,
handleDuplicates,
createSizeCheckHandler,
Expand Down
19 changes: 9 additions & 10 deletions lib/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,11 @@ const {
validateAttachmentMimeType,
validateAndInsertAttachmentFromDBHandler,
} = require("./generic-handlers")
const {
inferTargetCAP8,
getAttachmentKind,
handleDuplicates,
} = require("./helper")
const { getAttachmentKind, handleDuplicates } = require("./helper")
require("./csn-runtime-extension")
const LOG = cds.log("attachments")

cds.on(cds.version >= "8.6.0" ? "compile.to.edmx" : "loaded", unfoldModel)
cds.on("compile.to.edmx", unfoldModel)

// Register the db handler ONCE (not per-service) to intercept attachment INSERT
// and handle it through the attachments service instead of native DB insert
Expand All @@ -24,6 +20,11 @@ cds.on(cds.version >= "8.6.0" ? "compile.to.edmx" : "loaded", unfoldModel)
// NOTE: Must use db.prepend() to ensure handlers run before existing ones
cds.once("served", () => {
if (!cds.env.requires.attachments) return
if (cds.env.requires.attachments?.outbox !== undefined) {
LOG.warn(
"`cds.requires.attachments.outbox` is deprecated; use `queue` instead.",
)
}
const { db } = cds.services

db.prepend(() => {
Expand Down Expand Up @@ -838,8 +839,7 @@ cds.ApplicationService.handle_attachments = cds.service.impl(async function () {
if (req.query?.INSERT?.into.ref.length > 1) {
const ref = req.query.INSERT.into.ref.slice(0, -1)
const parentQuery = { SELECT: { from: { ref: ref }, one: true } }
const parent =
cds.infer?.target?.(parentQuery) || inferTargetCAP8(req, ref)
const parent = cds.infer.target(parentQuery)
if (!parent) {
LOG.warn(
`Could not determine parent target. Ref: ${JSON.stringify(ref)}`,
Expand Down Expand Up @@ -920,8 +920,7 @@ cds.ApplicationService.handle_attachments = cds.service.impl(async function () {
if (req.query?.DELETE?.from?.ref.length > 1) {
const ref = req.query.DELETE.from.ref.slice(0, -1)
const parentQuery = { SELECT: { from: { ref: ref }, one: true } }
const parent =
cds.infer?.target?.(parentQuery) || inferTargetCAP8(req, ref)
const parent = cds.infer.target(parentQuery)
if (!parent) {
LOG.warn(
`Could not determine parent target. Ref: ${JSON.stringify(ref)}`,
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@cap-js/attachments",
"description": "CAP cds-plugin providing image and attachment storing out-of-the-box.",
"version": "3.13.1",
"version": "4.0.0",
"repository": "cap-js/attachments",
"author": "SAP SE (https://www.sap.com)",
"homepage": "https://cap.cloud.sap/",
Expand Down Expand Up @@ -39,7 +39,7 @@
"husky": "^9.1.7"
},
"peerDependencies": {
"@sap/cds": ">=8"
"@sap/cds": ">=9"
Comment thread
eric-pSAP marked this conversation as resolved.
},
"engines": {
"node": ">=18.0.0"
Expand Down Expand Up @@ -94,7 +94,7 @@
}
},
"attachments": {
"outbox": true,
"outboxed": true,
"scan": true,
"deduplicateFileNames": true,
"scanExpiryMs": 259200000,
Expand Down
13 changes: 2 additions & 11 deletions srv/attachments/gcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -403,17 +403,8 @@ module.exports = class GoogleAttachmentsService extends (
)

const file = bucket.file(blobName)
let response
try {
response = await file.delete()
} catch (error) {
if (error.statusCode === 404) {
response = error
} else {
throw error
}
}
if (response?.[0]?.statusCode !== 204) {
const [response] = await file.delete({ ignoreNotFound: true })
if (response?.statusCode !== 204) {
LOG.warn("File has not been deleted from Google Cloud Storage", {
blobName,
bucketName: bucket.name,
Expand Down
2 changes: 1 addition & 1 deletion tests/incidents-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"dependencies": {
"@cap-js/attachments": "file:../../.",
"@cap-js/audit-logging": "^1.2.0",
"@cap-js/hana": "2.6.0",
"@cap-js/hana": ">=2.6.0",
"@cap-js/postgres": "^2.2.0"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions tests/integration/attachments-non-draft.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -872,7 +872,7 @@ describe("Testing max and min amounts of attachments", () => {
})
})

it("custom error message can be specified targeting composition property", async () => {
it("Custom error message can be specified targeting composition property", async () => {
await POST(`odata/v4/validation-test-non-draft/Incidents`, {
customer_ID: "1004155",
title: "ABC",
Expand All @@ -894,7 +894,7 @@ describe("Testing max and min amounts of attachments", () => {
})
})

it("custom error message can be specified for entity", async () => {
it("Custom error message can be specified for entity", async () => {
await POST(`odata/v4/validation-test-non-draft/Incidents`, {
customer_ID: "1004155",
title: "ABC",
Expand Down
Loading