diff --git a/CHANGELOG.md b/CHANGELOG.md index 70889d14..aa2786a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,14 @@ 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 - TBD +## Version 4.0.0 - 2026-08-03 **BREAKING CHANGE: The attachments plugin comes now without hyperscaler dependencies, please make sure to install them accordingly!** +**BREAKING CHANGE: Projects that explicitly set `attachments.outbox: true` in their own CDS configuration must rename the key to `outboxed`.** ### Changed +- The `outbox` configuration key under `cds.requires.attachments` has been renamed to `outboxed`. A deprecation warning is logged at startup when the old key is detected. - Cloud storage SDKs (`@aws-sdk/client-s3`, `@aws-sdk/lib-storage`, `@azure/storage-blob`, `@google-cloud/storage`) are now optional peer dependencies. Install only the SDK(s) for the provider you use (e.g. `npm install @aws-sdk/client-s3 @aws-sdk/lib-storage` for AWS S3). A clear error message with the exact install command is shown if a required SDK is missing at runtime. ### Fixed diff --git a/lib/generic-handlers.js b/lib/generic-handlers.js index 3665542e..05728034 100644 --- a/lib/generic-handlers.js +++ b/lib/generic-handlers.js @@ -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") /** @@ -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) diff --git a/lib/helper.js b/lib/helper.js index 6d4d1c5f..58c96af8 100644 --- a/lib/helper.js +++ b/lib/helper.js @@ -484,40 +484,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} 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 @@ -952,7 +918,6 @@ module.exports = { traverseEntity, buildBackAssocChain, MAX_FILE_SIZE, - inferTargetCAP8, getAttachmentKind, handleDuplicates, createSizeCheckHandler, diff --git a/lib/plugin.js b/lib/plugin.js index 37e5231e..02052334 100644 --- a/lib/plugin.js +++ b/lib/plugin.js @@ -7,18 +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.split(".")[0] >= 8 ? "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 @@ -27,6 +20,11 @@ cds.on( // 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 `outboxed` instead.", + ) + } const { db } = cds.services db.prepend(() => { @@ -853,8 +851,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)}`, @@ -935,8 +932,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)}`, diff --git a/package.json b/package.json index 2371596d..6a9dbc15 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "@aws-sdk/lib-storage": "^3", "@azure/storage-blob": "^12", "@google-cloud/storage": "^7", - "@sap/cds": ">=8" + "@sap/cds": ">=9" }, "peerDependenciesMeta": { "@aws-sdk/client-s3": { @@ -101,7 +101,7 @@ } }, "attachments": { - "outbox": true, + "outboxed": true, "scan": true, "deduplicateFileNames": true, "scanExpiryMs": 259200000, diff --git a/srv/attachments/gcp.js b/srv/attachments/gcp.js index 4d48cc12..9454a724 100644 --- a/srv/attachments/gcp.js +++ b/srv/attachments/gcp.js @@ -414,17 +414,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, diff --git a/tests/integration/attachments-non-draft.test.js b/tests/integration/attachments-non-draft.test.js index ac87c452..478df2f2 100644 --- a/tests/integration/attachments-non-draft.test.js +++ b/tests/integration/attachments-non-draft.test.js @@ -1,5 +1,4 @@ const cds = require("@sap/cds") -const { test } = cds.test() const { waitForScanStatus, newIncident, @@ -10,10 +9,7 @@ const { const path = require("path") const app = path.resolve(__dirname, "../incidents-app") -const { GET, POST, PATCH, DELETE, PUT } = withUser( - "alice", - require("@cap-js/cds-test")(app), -) +const { GET, POST, PATCH, DELETE, PUT } = withUser("alice", cds.test(app)) const { join } = cds.utils.path const { createReadStream, readFileSync, statSync } = cds.utils.fs @@ -32,7 +28,7 @@ describe("Tests for uploading/deleting and fetching attachments through API call originalDeduplicateFileNames }) - let log = test.log() + let log = cds.test.log() const { createAttachmentMetadata, uploadAttachmentContent } = createHelpers() // Allow background operations (malware scan status updates) to complete before teardown @@ -1059,7 +1055,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", @@ -1081,7 +1077,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",