Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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 BREAKING-CHANGE t.b.d

### Changed

- 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.

## Version 3.12.2

### Fixed
Expand Down
44 changes: 37 additions & 7 deletions lib/mtx/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,21 @@
* @param {string} tenant - Tenant ID
*/
const _cleanupAWSS3Objects = async (creds, tenant) => {
const {
S3Client,
paginateListObjectsV2,
DeleteObjectsCommand,
} = require("@aws-sdk/client-s3")
let S3Client, paginateListObjectsV2, DeleteObjectsCommand
try {
;({
S3Client,
paginateListObjectsV2,
DeleteObjectsCommand,
} = require("@aws-sdk/client-s3"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 631 in lib/mtx/server.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'Cleanup of AWS S3 objects requires "@aws-sdk/client-s3" to be installed.\n' +
"Please run: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
throw e
}
const client = new S3Client({
region: creds.region,
credentials: {
Expand Down Expand Up @@ -682,7 +692,17 @@
* @param {string} tenant - Tenant ID
*/
const _cleanupAzureBlobObjects = async (creds, tenant) => {
const { BlobServiceClient } = require("@azure/storage-blob")
let BlobServiceClient
try {
;({ BlobServiceClient } = require("@azure/storage-blob"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 700 in lib/mtx/server.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'Cleanup of Azure Blob objects requires "@azure/storage-blob" to be installed.\n' +
"Please run: npm install @azure/storage-blob",
)
throw e
}
const blobServiceClient = new BlobServiceClient(
`${creds.container_uri}?${creds.sas_token}`,
)
Expand Down Expand Up @@ -721,7 +741,17 @@
* @param {string} tenant - Tenant ID
*/
const _cleanupGoogleCloudObjects = async (creds, tenant) => {
const { Storage } = require("@google-cloud/storage")
let Storage
try {
;({ Storage } = require("@google-cloud/storage"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 749 in lib/mtx/server.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'Cleanup of Google Cloud Storage objects requires "@google-cloud/storage" to be installed.\n' +
"Please run: npm install @google-cloud/storage",
)
throw e
}
const storageClient = new Storage({
projectId: creds.project_id,
credentials: creds.service_account,
Expand Down
24 changes: 21 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@
"prepare": "husky"
},
"dependencies": {
"axios": "^1.13.5"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.993.0",
"@aws-sdk/lib-storage": "^3.993.0",
"@azure/storage-blob": "^12.31.0",
"@google-cloud/storage": "^7.19.0",
"axios": "^1.13.5"
},
"devDependencies": {
"@cap-js/cds-test": ">=0",
"@cap-js/cds-types": "^0.16.0",
"@cap-js/hana": "^2.7.0",
Expand All @@ -39,8 +39,26 @@
"husky": "^9.1.7"
},
"peerDependencies": {
"@aws-sdk/client-s3": "^3",
"@aws-sdk/lib-storage": "^3",
"@azure/storage-blob": "^12",
"@google-cloud/storage": "^7",
"@sap/cds": ">=8"
},
"peerDependenciesMeta": {
"@aws-sdk/client-s3": {
"optional": true
},
"@aws-sdk/lib-storage": {
"optional": true
},
"@azure/storage-blob": {
"optional": true
},
"@google-cloud/storage": {
"optional": true
}
},
"engines": {
"node": ">=18.0.0"
},
Expand Down
23 changes: 19 additions & 4 deletions srv/attachments/aws-s3.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
const {
S3Client,
let S3Client,
GetObjectCommand,
DeleteObjectCommand,
HeadObjectCommand,
CopyObjectCommand,
} = require("@aws-sdk/client-s3")
const { Upload } = require("@aws-sdk/lib-storage")
Upload
try {
;({
S3Client,
GetObjectCommand,
DeleteObjectCommand,
HeadObjectCommand,
CopyObjectCommand,
} = require("@aws-sdk/client-s3"))
;({ Upload } = require("@aws-sdk/lib-storage"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 18 in srv/attachments/aws-s3.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'The AWS S3 storage provider requires "@aws-sdk/client-s3" and "@aws-sdk/lib-storage" to be installed.\n' +
"Please run: npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
throw e
}
const cds = require("@sap/cds")
const LOG = cds.log("attachments")
const utils = require("../../lib/helper")
Expand Down
12 changes: 11 additions & 1 deletion srv/attachments/azure-blob-storage.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
const { BlobServiceClient } = require("@azure/storage-blob")
let BlobServiceClient
try {
;({ BlobServiceClient } = require("@azure/storage-blob"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 6 in srv/attachments/azure-blob-storage.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'The Azure Blob Storage provider requires "@azure/storage-blob" to be installed.\n' +
"Please run: npm install @azure/storage-blob",
)
throw e
}
const { AbortController } = require("abort-controller")
const cds = require("@sap/cds")
const LOG = cds.log("attachments")
Expand Down
12 changes: 11 additions & 1 deletion srv/attachments/gcp.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
const { Storage } = require("@google-cloud/storage")
let Storage
try {
;({ Storage } = require("@google-cloud/storage"))
} catch (e) {
if (e.code === "MODULE_NOT_FOUND")
throw new Error(

Check failure on line 6 in srv/attachments/gcp.js

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
'The Google Cloud Platform storage provider requires "@google-cloud/storage" to be installed.\n' +
"Please run: npm install @google-cloud/storage",
)
throw e
}
const cds = require("@sap/cds")
const LOG = cds.log("attachments")
const utils = require("../../lib/helper")
Expand Down
75 changes: 75 additions & 0 deletions tests/unit/optionalDeps.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
describe("optional peer dependency errors", () => {
const makeNotFound = (pkg) => {
const err = new Error(`Cannot find module '${pkg}'`)
err.code = "MODULE_NOT_FOUND"
return err
}

test("aws-s3 throws helpful error when @aws-sdk/client-s3 is missing", () => {
jest.isolateModules(() => {
jest.doMock("@aws-sdk/client-s3", () => {
throw makeNotFound("@aws-sdk/client-s3")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
Comment on lines +8 to +16
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: expect assertions inside jest.isolateModules() callbacks are silently swallowed on failure

jest.isolateModules() executes the callback synchronously and propagates thrown errors, but the callback's return value is discarded. If an expect(...).toThrow() assertion fails (i.e., no error is thrown), expect itself throws internally — and that will propagate, so failure detection works in that direction. However, for expect(...).not.toThrow() (lines 56, 65, 72), if the module does throw, the error is thrown inside the callback but Jest may not always reliably associate it with the enclosing test, especially when modules load side-effects or when an unhandled rejection occurs.

More critically, these tests that assert successful loading (.not.toThrow()) load real modules that have real side-effects (e.g., aws-s3.js calls cds.log(...), requires ../../lib/helper, etc.). If those side-effect requires fail, the error propagates, but the isolation is not guaranteed to be clean across tests because jest.dontMock inside jest.isolateModules doesn't reset the mock registry for modules that were previously mocked in earlier tests.

The safest pattern is to return the jest.isolateModules call from the test callback, ensuring the module registry is properly scoped:

Suggested change
test("aws-s3 throws helpful error when @aws-sdk/client-s3 is missing", () => {
jest.isolateModules(() => {
jest.doMock("@aws-sdk/client-s3", () => {
throw makeNotFound("@aws-sdk/client-s3")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
test("aws-s3 throws helpful error when @aws-sdk/client-s3 is missing", () => {
return jest.isolateModules(() => {
jest.doMock("@aws-sdk/client-s3", () => {
throw makeNotFound("@aws-sdk/client-s3")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
})

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful

})

test("aws-s3 throws helpful error when @aws-sdk/lib-storage is missing", () => {
jest.isolateModules(() => {
jest.doMock("@aws-sdk/lib-storage", () => {
throw makeNotFound("@aws-sdk/lib-storage")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
})
Comment on lines +19 to +28
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: expect assertions inside jest.isolateModules() callbacks are silently swallowed on failure

Same issue as the first test — jest.isolateModules() return value is discarded. Return it to ensure the assertion result is propagated to Jest properly.

Suggested change
test("aws-s3 throws helpful error when @aws-sdk/lib-storage is missing", () => {
jest.isolateModules(() => {
jest.doMock("@aws-sdk/lib-storage", () => {
throw makeNotFound("@aws-sdk/lib-storage")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
})
test("aws-s3 throws helpful error when @aws-sdk/lib-storage is missing", () => {
return jest.isolateModules(() => {
jest.doMock("@aws-sdk/lib-storage", () => {
throw makeNotFound("@aws-sdk/lib-storage")
})
expect(() => require("../../srv/attachments/aws-s3")).toThrow(
"npm install @aws-sdk/client-s3 @aws-sdk/lib-storage",
)
})
})

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


test("azure-blob-storage throws helpful error when @azure/storage-blob is missing", () => {
jest.isolateModules(() => {
jest.doMock("@azure/storage-blob", () => {
throw makeNotFound("@azure/storage-blob")
})
expect(() => require("../../srv/attachments/azure-blob-storage")).toThrow(
"npm install @azure/storage-blob",
)
})
})
Comment on lines +30 to +39
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: jest.isolateModules() return value discarded — assertion failures may not propagate reliably

All remaining jest.isolateModules() calls discard the return value. Return it from the test callback so Jest can properly handle any thrown assertion errors.

Suggested change
test("azure-blob-storage throws helpful error when @azure/storage-blob is missing", () => {
jest.isolateModules(() => {
jest.doMock("@azure/storage-blob", () => {
throw makeNotFound("@azure/storage-blob")
})
expect(() => require("../../srv/attachments/azure-blob-storage")).toThrow(
"npm install @azure/storage-blob",
)
})
})
test("azure-blob-storage throws helpful error when @azure/storage-blob is missing", () => {
return jest.isolateModules(() => {
jest.doMock("@azure/storage-blob", () => {
throw makeNotFound("@azure/storage-blob")
})
expect(() => require("../../srv/attachments/azure-blob-storage")).toThrow(
"npm install @azure/storage-blob",
)
})
})

Double-check suggestion before committing. Edit this comment for amendments.


Please provide feedback on the review comment by checking the appropriate box:

  • 🌟 Awesome comment, a human might have missed that.
  • ✅ Helpful comment
  • 🤷 Neutral
  • ❌ This comment is not helpful


test("gcp throws helpful error when @google-cloud/storage is missing", () => {
jest.isolateModules(() => {
jest.doMock("@google-cloud/storage", () => {
throw makeNotFound("@google-cloud/storage")
})
expect(() => require("../../srv/attachments/gcp")).toThrow(
"npm install @google-cloud/storage",
)
})
})

test("aws-s3 loads successfully when SDKs are present", () => {
jest.isolateModules(() => {
jest.dontMock("@aws-sdk/client-s3")
jest.dontMock("@aws-sdk/lib-storage")
expect(() => require("../../srv/attachments/aws-s3")).not.toThrow()
})
})

test("azure-blob-storage loads successfully when SDK is present", () => {
jest.isolateModules(() => {
jest.dontMock("@azure/storage-blob")
expect(() =>
require("../../srv/attachments/azure-blob-storage"),
).not.toThrow()
})
})

test("gcp loads successfully when SDK is present", () => {
jest.isolateModules(() => {
jest.dontMock("@google-cloud/storage")
expect(() => require("../../srv/attachments/gcp")).not.toThrow()
})
})
})
Loading