diff --git a/CHANGELOG.md b/CHANGELOG.md index 53ffd872..eebe0270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/lib/mtx/server.js b/lib/mtx/server.js index 6eca2bfc..bf562eb5 100644 --- a/lib/mtx/server.js +++ b/lib/mtx/server.js @@ -619,11 +619,22 @@ cds.on("listening", async () => { * @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( + '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", + { cause: e }, + ) + throw e + } const client = new S3Client({ region: creds.region, credentials: { @@ -682,7 +693,18 @@ const _cleanupAWSS3Objects = async (creds, tenant) => { * @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( + 'Cleanup of Azure Blob objects requires "@azure/storage-blob" to be installed.\n' + + "Please run: npm install @azure/storage-blob", + { cause: e }, + ) + throw e + } const blobServiceClient = new BlobServiceClient( `${creds.container_uri}?${creds.sas_token}`, ) @@ -721,7 +743,18 @@ const _cleanupAzureBlobObjects = async (creds, tenant) => { * @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( + 'Cleanup of Google Cloud Storage objects requires "@google-cloud/storage" to be installed.\n' + + "Please run: npm install @google-cloud/storage", + { cause: e }, + ) + throw e + } const storageClient = new Storage({ projectId: creds.project_id, credentials: creds.service_account, diff --git a/package.json b/package.json index e8072dfd..4a6916b0 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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" }, diff --git a/srv/attachments/aws-s3.js b/srv/attachments/aws-s3.js index 8db9c00f..4150554c 100644 --- a/srv/attachments/aws-s3.js +++ b/srv/attachments/aws-s3.js @@ -1,11 +1,27 @@ -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( + '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", + { cause: e }, + ) + throw e +} const cds = require("@sap/cds") const LOG = cds.log("attachments") const utils = require("../../lib/helper") diff --git a/srv/attachments/azure-blob-storage.js b/srv/attachments/azure-blob-storage.js index f43a8c99..a52854c3 100644 --- a/srv/attachments/azure-blob-storage.js +++ b/srv/attachments/azure-blob-storage.js @@ -1,4 +1,15 @@ -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( + 'The Azure Blob Storage provider requires "@azure/storage-blob" to be installed.\n' + + "Please run: npm install @azure/storage-blob", + { cause: e }, + ) + throw e +} const { AbortController } = require("abort-controller") const cds = require("@sap/cds") const LOG = cds.log("attachments") diff --git a/srv/attachments/gcp.js b/srv/attachments/gcp.js index caa42459..6b33a9ec 100644 --- a/srv/attachments/gcp.js +++ b/srv/attachments/gcp.js @@ -1,4 +1,15 @@ -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( + 'The Google Cloud Platform storage provider requires "@google-cloud/storage" to be installed.\n' + + "Please run: npm install @google-cloud/storage", + { cause: e }, + ) + throw e +} const cds = require("@sap/cds") const LOG = cds.log("attachments") const utils = require("../../lib/helper") diff --git a/tests/unit/optionalDeps.test.js b/tests/unit/optionalDeps.test.js new file mode 100644 index 00000000..4319890c --- /dev/null +++ b/tests/unit/optionalDeps.test.js @@ -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", + ) + }) + }) + + 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("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("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() + }) + }) +})