cap-attachments-plus
v1.0.29
Published
CAP cds-plugin providing image and attachment storing out-of-the-box.
Readme
Attachments Plugin
The cap-attachments-plus package is a CDS plugin that provides out-of-the box asset storage and handling by using an aspect called Attachments.
Why use this package
Any CAP application that lets users upload files (documents, images, PDFs, etc.) needs to solve the same set of problems repeatedly: where to store the binary content, how to keep a database reference in sync with it, how to render an upload/download UI facet, how to scan uploads for malware before they're accessible, and how to enforce size/MIME-type limits. Building all of this by hand for every application is repetitive and error-prone.
This plugin solves that once, and gives you:
- A single CDS aspect (
Attachments) that, when composed into an entity, automatically generates the OData actions, DB entity, and Fiori Elements UI facet for uploading, listing, downloading and deleting files — no handler code required. - Pluggable storage backends — store attachment content directly in the database (for local development), or offload it to AWS S3, Azure Blob Storage, or Google Cloud Storage, all configured directly against your own cloud account via a BTP destination. You choose the target per environment; the application code and CDS model never change.
- Built-in malware scanning via the SAP Malware Scanning Service (or a mocked scanner for local development), with per-attachment scan status tracking so infected files are automatically removed and never made downloadable.
- File size and MIME type validation out of the box, configurable per entity via standard CDS annotations (
@Validation.Maximum,@Core.AcceptableMediaTypes). - Multitenancy support, including tenant-scoped storage isolation.
- Consistent, actionable error handling and logging throughout, so failures during upload/download/scan surface a clear reason and remediation hint rather than an opaque stack trace.
In short: add the plugin, add one line to your CDS model, and you have production-ready attachment handling — you only configure where the files should live.
Table of Contents
- Why use this package
- Usage
- Configuration Reference
- Error Reference
- Releases
- Minimum UI5 and CAP NodeJS Version
- Architecture Overview
- Monitoring & Logging
- Support, Feedback, Contributing
- Code of Conduct
- Licensing
Usage
Quick Start
For a quick local development setup with in-memory storage:
The plugin is self-configuring as described, see the following details section. To enable attachments, simply add the plugin package to your project:
npm add cap-attachments-plus"devDependencies": { "cap-attachments-plus": "<latest-version>", // (...) }In addition, different profiles can be found in
package.jsonas well, such as:"cds": { "requires": { // (...) "[hybrid]": { "attachments": { "kind": "attachments-standard" // (...) } } } }To use Attachments, extend a CDS model by adding an element that refers to the pre-defined Attachments type (see Changes in the CDS Models for more details):
using { Attachments } from 'cap-attachments-plus'; entity Incidents { // (...) attachments: Composition of many Attachments; }
In this guide, we use the Incidents Management reference sample app as the base application to provide a demonstration how to use this plugin. A miniature version of this app can be found within the tests directory for local testing.
For productive use, a valid object store binding is required, see Object Stores and Storage Targets.
Local Walk-Through
With the steps above, we have successfully set up asset handling for our reference application. To test the application locally, use the following steps.
[!NOTE] For local testing, the attachment objects are stored in a local database.
- Start the server:
- Default scenario (In memory database):
cds watch
Navigate to the object page of the incident
Solar panel broken: Go to object page for incident Solar panel brokenThe
Attachmentstype has generated an out-of-the-box Attachments table (see 1) at the bottom of the Object page:
- Upload a file by going into Edit mode and either using the Upload button on the Attachments table or by drag/drop. Then click the Save button to have that file stored that file in the dedicated resource (database, S3 bucket, etc.). We demonstrate this by uploading the PDF file from tests/integration/content/sample.pdf:
- Delete a file by going into Edit mode, selecting the file, and pressing the Delete button above the Attachments table. Clicking the Save button will then delete that file from the resource (database, S3 bucket, etc.).
Changes in the CDS Models
To use the aspect Attachments on an existing entity, the corresponding entity needs to either include attachments as an element in the model definition or be extended in a CDS file in the srv module. In the quick start, the former was done, adding an element to the model definition:
using { Attachments } from 'cap-attachments-plus';
entity Incidents {
// ...
attachments: Composition of many Attachments;
} The entity Incidents can also be extended in the srv module, as seen in the following example:
using { Attachments } from 'cap-attachments-plus';
extend my.Incidents with {
attachments: Composition of many Attachments;
}
service ProcessorService {
entity Incidents as projection on my.Incidents
}Both methods directly add the respective UI Facet. To use the plugin with an SAP Fiori elements UI, be sure that draft is enabled for the entity using @odata.draft.enabled. For example:
annotate service.Incidents with @odata.draft.enabled;Storage Targets
The plugin supports two categories of storage target, selected via kind:
- Local database (
kind: "attachments-db", thedevelopmentprofile default) — attachments are stored directly in the application's own database. No cloud account or binding of any kind is required. - Cloud storage (
kind: "attachments-s3"/"attachments-azure"/"attachments-gcp"/"attachments-standard") — the attachment content is saved in your own AWS S3 bucket, Azure Blob Storage container, or GCP Cloud Storage bucket, and only a reference plus metadata is kept in the database, as defined in the CDS model. Credentials come from a BTP destination you create yourself (see step 4) — there is no SAP Object Store service instance to provision for this.
To test a cloud storage target in a hybrid setup, bind the Destination service instance (which the plugin uses to look up your BTP destination at runtime) using cds bind, as described in the CAP documentation for hybrid testing:
- Log in to Cloud Foundry:
cf login -a <CF-API> -o <ORG-NAME> -s <SPACE-NAME> --sso- Bind the Destination service instance, which generates/updates a
.cdsrc-private.jsonfile in the project directory:
cds bind <HybridDestinationServiceName> --to <RemoteDestinationServiceName>Where HybridDestinationServiceName can be any name you choose, and RemoteDestinationServiceName is the name of your Destination service instance in SAP BTP.
- Run the application in hybrid mode:
cds watch --profile hybridSee Object Stores for background on when an SAP Object Store binding is still relevant (multitenancy provisioning only, not attachment storage).
Malware Scanner
The BTP malware scanning service is used in the AttachmentService to scan attachments for vulnerabilities.
For using SAP Malware Scanning Service, you must already have a service instance which you can access. To bind it, run the following command:
cds bind <HybridMalwareScannerName> --to <RemoteMalwareScannerName>By default, malware scanning is enabled for all profiles if a storage provider has been specified. You can configure malware scanning by setting:
{
"cds": {
// (...)
"attachments": {
"scan": true
}
}
} If there is no malware scanner available and the scanner is not disabled, then the upload will fail.
Disabling malware scanning
If you don't need scanning at all, set scan: false in your application's own package.json:
{
"cds": {
"requires": {
"attachments": {
"scan": false
}
}
}
}With scanning disabled, no Malware Scanning Service binding is required, and uploaded files become downloadable immediately — the scan step (and the status gating in validateAttachment, which otherwise blocks download until status is Clean) is skipped entirely.
This can also be scoped to a single environment profile, e.g. to disable scanning only in development:
{
"cds": {
"requires": {
"[development]": {
"attachments": { "scan": false }
}
}
}
}[!Note] The plugin's own
[development]profile default already wires up a mocked scanner (kind: "malwareScanner-mocked") that always reportsCleanwithout calling any real service. So for local development you typically don't needscan: falseor real malware scanning credentials at all —scan: falseis for skipping the scan step altogether, including in production.
Scan status codes:
Unscanned: Attachment is still unscanned.Scanning: Immediately after upload, the attachment is marked as Scanning. Depending on processing speed, it may already appear as Clean when the page is reloaded.Clean: Only attachments with the status Clean are accessible.Infected: The attachment is infected.Failed: Scanning failed.
[!Note] The malware scanner supports mTLS authentication which requires an annual renewal of the certificate. Previously, basic authentication was used which has now been deprecated.
[!Note] If the malware scanner reports a file size larger than the limit specified via @Validation.Maximum it removes the file and sets the status of the attachment metadata to failed.
Visibility Control for Attachments UI Facet Generation
By setting the @UI.Hidden property to true, developers can hide the visibility of the plugin in the UI. This feature is particularly useful in scenarios where the visibility of the plugin needs to be dynamically controlled based on certain conditions.
Example Usage
entity Incidents {
// ...
@UI.Hidden
attachments: Composition of many Attachments;
}In this example, the @UI.Hidden is set to true, which means the plugin will be hidden by default. You can also use dynamic expressions which are then added to the facet.
entity Incidents {
// ...
status : Integer enum {
submitted = 1;
fulfilled = 2;
shipped = 3;
canceled = -1;
};
@UI.Hidden : (status = #canceled ? true : false)
attachments: Composition of many Attachments;
}Non-Draft Upload
For scenarios where the entity is not draft-enabled, for example tests/non-draft-request.http, separate HTTP requests for metadata creation and asset uploading need to be performed manually.
The typical sequence includes:
- POST -> create attachment metadata, returns ID
- PUT -> upload file content using the ID
Specify the maximum file size
You can specify the maximum file size by annotating the attachments content property with @Validation.Maximum
entity Incidents {
...
attachments: Composition of many Attachments;
}
annotate Incidents.attachments with {
content @Validation.Maximum : '20MB';
}The default is 400MB
Restrict allowed MIME types
You can restrict which MIME types are allowed for attachments by annotating the content property with @Core.AcceptableMediaTypes. This validation is performed during file upload.
entity Incidents {
...
attachments: Composition of many Attachments;
}
annotate Incidents.attachments with {
content @Core.AcceptableMediaTypes : ['image/jpeg', 'image/png', 'application/pdf'];
}Wildcard patterns are supported:
annotate Incidents.attachments with {
content @Core.AcceptableMediaTypes : ['image/*', 'application/pdf'];
}To allow all MIME types (default behavior), either omit the annotation or use:
annotate Incidents.attachments with {
content @Core.AcceptableMediaTypes : ['*/*'];
}When a file with a disallowed MIME type is uploaded, the request will be rejected with a 400 error.
Configuration Reference
This section lists every configuration step, in order, needed to go from "no attachments" to a fully working, production-ready setup.
1. Install the plugin
npm add cap-attachments-plus2. Reference it in your CDS model
Add the Attachments composition to the entity that should support file uploads (see Changes in the CDS Models):
using { Attachments } from 'cap-attachments-plus';
entity Incidents {
// ...
attachments: Composition of many Attachments;
}If the entity is exposed via a Fiori Elements UI, ensure draft is enabled:
annotate service.Incidents with @odata.draft.enabled;3. Choose where files are stored (kind)
The plugin acts as a middleware in front of whichever storage backend you choose — your CDS model and application code stay identical no matter which one is active. You select the backend entirely through configuration, in your own application's package.json, using the standard CAP cds.requires.<service>.kind convention (the same mechanism CAP itself uses to pick a database kind like "sqlite" or "hana"). Nothing Azure/AWS/GCP-specific needs to be written in your code or CDS model — just this one string:
"cds": {
"requires": {
"attachments": {
"kind": "attachments-azure"
}
}
}| kind value | Storage backend | What it needs |
| --- | --- | --- |
| attachments-db | The application's own database (SQLite/HANA) | Nothing — this is the default for the development profile |
| attachments-standard | Auto-detects AWS S3, Azure Blob Storage or GCP Cloud Storage from the shape of the bound destination's configuration | A BTP destination (any provider's fields) |
| attachments-s3 | AWS S3 | A BTP destination with AWS credentials |
| attachments-azure | Azure Blob Storage | A BTP destination with Azure credentials |
| attachments-gcp | GCP Cloud Storage | A BTP destination with GCP credentials |
The kind you set is the only thing that tells the plugin which client code to run (srv/aws-s3.js, srv/azure-blob-storage.js, srv/gcp.js or srv/basic.js, mapped once via cds.requires.kinds inside this plugin — consuming apps never need to know these file paths). Which cloud account those credentials actually point at is entirely up to what you put in the destination in step 4 below — the plugin itself is cloud-agnostic.
For local development, kind defaults to attachments-db and files are stored directly in the application's own database — no cloud account, destination, or further setup required (see Local Walk-Through).
4. For a cloud backend: create and bind a BTP destination with your storage credentials
Each cloud provider (attachments-s3/attachments-azure/attachments-gcp/attachments-standard) reads its credentials from a BTP destination that you create in BTP Cockpit, pointing at your own AWS, Azure, or GCP account. This means attachments are stored directly in your own cloud infrastructure — no SAP Object Store service instance is required.
- In BTP Cockpit, create a destination (default expected name:
attachments) withAuthentication: BasicAuthentication, and put the credential inUser/Passwordand the non-secret identifiers as additional properties, per the table below. - Ensure a Destination service instance is bound to your application (required to look up the destination at runtime).
- If you want to use a destination name other than
attachments, set it explicitly.destinationcan be overridden globally, or per environment profile (e.g."[production]").
[!IMPORTANT] Use
Authentication: BasicAuthenticationand put the actual secret in the destination'sUser/Passwordfields — not as a plain additional property. The Destination service encrypts and access-controlsUser/Password(and masksPasswordin BTP Cockpit); a custom additional property does not get that protection. Non-secret identifiers (bucket name, region, container URI, project ID, etc.) can stay as plain additional properties.
Destination configuration per provider — User/Password hold the actual secret, everything else is a plain additional property:
| Provider | User | Password | Additional properties (non-secret) |
| --- | --- | --- | --- |
| AWS S3 | access_key_id | secret_access_key | bucket, region (optionally endpoint for S3-compatible storage) |
| Azure Blob Storage | container_name | sas_token | container_uri |
| GCP Cloud Storage | projectId | base64EncodedPrivateKeyData (the full service account key, base64-encoded) | bucket |
For backwards compatibility, the plugin also still accepts these values as plain additional properties named access_key_id/secret_access_key, container_name/sas_token, and projectId/base64EncodedPrivateKeyData if User/Password aren't set — but that path is not recommended, since those properties aren't encrypted/masked the way User/Password are.
Full, ready-to-use examples
Azure Blob Storage
"cds": {
"requires": {
"attachments": {
"kind": "attachments-azure",
"destination": "myAzureDestination"
}
}
}AWS S3
"cds": {
"requires": {
"attachments": {
"kind": "attachments-s3",
"destination": "myAwsDestination"
}
}
}GCP Cloud Storage
"cds": {
"requires": {
"attachments": {
"kind": "attachments-gcp",
"destination": "myGcpDestination"
}
}
}Auto-detect (let the destination's contents decide the provider)
"cds": {
"requires": {
"attachments": {
"kind": "attachments-standard",
"destination": "myStorageDestination"
}
}
}Only one of these blocks needs to be in your package.json at a time — kind and destination are the complete, end-to-end configuration surface needed to target a specific provider. Nothing else in your application (model, handlers, UI) changes between providers.
5. Configure malware scanning
Malware scanning is enabled by default whenever a storage provider is configured. See Malware Scanner for how to bind the SAP Malware Scanning Service, or disable it explicitly:
"cds": {
"requires": {
"attachments": {
"scan": false
}
}
}[!WARNING] If scanning is enabled but no malware scanner is bound, uploads will fail. If disabled, uploaded files are made available for download immediately without being scanned.
6. (Optional) Restrict file size and MIME types
Add @Validation.Maximum and/or @Core.AcceptableMediaTypes annotations on the content property — see Specify the maximum file size and Restrict allowed MIME types.
7. (Optional) Configure multitenancy
See Multitenancy for shared vs. tenant-scoped storage.
8. (Optional) Configure logging
See Monitoring & Logging to raise the log level for troubleshooting.
Configuration checklist
| Step | Config key | Required? |
| --- | --- | --- |
| Storage backend | cds.requires.attachments.kind | Optional (defaults to attachments-db in development, attachments-standard otherwise) |
| Destination name | cds.requires.attachments.destination | Optional (defaults to "attachments") |
| Destination service binding | (VCAP binding) | Required for any storage backend other than the local database |
| Malware scanning toggle | cds.requires.attachments.scan | Optional (defaults to true) |
| Malware scanner binding | (VCAP binding) | Required if scanning is enabled |
| Max file size | @Validation.Maximum on content | Optional (defaults to 400 MB) |
| Allowed MIME types | @Core.AcceptableMediaTypes on content | Optional (defaults to all types) |
| Multitenancy object store mode | cds.requires.attachments.objectStore.kind | Optional (defaults to separate) |
| Log level | cds.log.levels.attachments | Optional (defaults to CAP's default log level) |
Error Reference
The plugin surfaces two categories of errors: OData request errors, returned to the client with an HTTP status and a message code (translatable via _i18n/messages.properties), and configuration errors, thrown during service initialization or client creation and written to the attachments log with a suggested remediation.
OData request errors
| HTTP Status | Message code | Meaning | Remediation |
| --- | --- | --- | --- |
| 400 | ContentLengthHeaderMissing | The Content-Length header is missing or invalid on an upload request. | Ensure the HTTP client sends a valid Content-Length header. |
| 400 | InvalidContentLengthHeader | The Content-Length header value could not be parsed as a number. | Check the client is not sending a malformed header. |
| 413 | AttachmentSizeExceeded | The uploaded file exceeds the configured (or default 400MB) size limit. | Increase @Validation.Maximum on the content property, or reduce the file size. |
| 400 | AttachmentMimeTypeDisallowed | The uploaded file's MIME type isn't in the entity's @Core.AcceptableMediaTypes allow-list. | Adjust the allow-list annotation, or upload a supported file type. |
| 403 | UnableToDownloadAttachmentScanStatusNotClean | A download was attempted before the malware scan completed (or the file was flagged). | Wait for the scan to complete; check the attachment's status field. |
| 404 | (none) | The attachment, or its parent record, could not be found. | Verify the entity/parent record exists before uploading or downloading. |
| 501 | MultiUpdateNotSupported | A PUT/UPDATE targeted more than one attachment record at once. | Update attachments one at a time. |
Configuration / connectivity errors
These are thrown (and logged with a suggestion) when the plugin cannot reach or validate a storage or scanning backend. They typically surface on the first upload/download after a deployment or configuration change.
| Error | Cause | Remediation |
| --- | --- | --- |
| Destination service Instance Binding is needed | No Destination service instance is bound to the application. | Bind a Destination service instance. |
| Destination "<name>" not found in BTP Cockpit | The configured/default destination name doesn't exist. | Create the destination, or set cds.requires.attachments.destination to the correct name. |
| Missing AWS S3 configuration in destination "<name>": ... | The bound destination is missing one or more required S3 properties (User/Password or bucket/region). | Add the missing properties (see step 4). |
| Missing Azure Blob Storage configuration in destination "<name>": ... | The bound destination is missing one or more required Azure properties (User/Password or container_uri). | Add the missing properties. |
| Missing Google Cloud Platform configuration in destination "<name>": ... | The bound destination is missing one or more required GCP properties (User/Password or bucket). | Add the missing properties. |
| <Provider> configuration found where <OtherProvider> configuration expected | kind doesn't match the credentials shape in the bound destination. | Fix cds.requires.attachments.kind, or point destination at the correct BTP destination. |
| Unable to determine storage provider from the destination configuration | Using kind: "attachments-standard" but the destination's fields don't match any known provider shape. | Select a provider explicitly via kind. |
| cds.env.requires.malwareScanner.credentials is empty! | Malware scanning is enabled but no Malware Scanning Service is bound. | Bind the service, or set cds.requires.attachments.scan to false. |
| Missing Malware Scanner credentials: mTLS [...], Basic Auth [...] | The malware scanner binding is missing required credential fields. | Check the service binding/service key. |
| Malware scanner certificate expired | The mTLS certificate used for the malware scanner has expired. | Renew the certificate (annual renewal required). |
| SAP Object Store instance credentials not found (multitenancy shared mode) | No shared Object Store instance bound to the mtx module. | Bind a shared Object Store instance to the mtx/sidecar application module. |
| Service Manager Instance is not bound (multitenancy separate mode) | No Service Manager instance bound, needed to provision per-tenant object stores. | Bind a Service Manager instance. |
Releases
- The plugin is released to NPM Registry.
- See the changelog or GitHub Releases for the latest changes.
Minimum UI5 and CAP NodeJS Version
| Component | Minimum Version | |-----------|-----------------| | CAP Node | 8.0.0 | | UI5 | 1.136.0 |
Architecture Overview
Multitenancy
The plugin supports multi-tenancy scenarios, allowing both shared and tenant-specific object store instances.
[!Note] Starting from version 2.1.0, separate mode for object store instances is the default setting for multi-tenancy.
For multi-tenant applications, cap-attachments-plus must be included in the dependencies of both the application-level and mtx/sidecar/package.json files.
Separate object store instances
By default the plugin creates for each tenant its own object store instance during the tenants subscription.
When the tenant unsubscribes the object store instance is deleted.
[!WARNING] When you remove the plugin from an application after separate object stores already have been created, the object stores are not automatically removed!
Shared Object Store Instance
To configure a shared object store instance, modify both the package.json files as follows:
"cds": {
"requires": {
"attachments": {
"objectStore": {
"kind": "shared"
}
}
}
}To ensure tenant identification when using a shared object store instance, the plugin prefixes attachment URLs with the tenant ID. Be sure the shared object store instance is bound to the mtx application module before deployment.
Object Stores
[!Note] The attachment content (AWS S3 / Azure Blob Storage / GCP Cloud Storage) is configured via a BTP destination pointing directly at your own cloud account — see Storage Targets and step 4 of the Configuration Reference. The SAP Object Store service binding described below is only relevant for multi-tenant deployments that use the plugin's automatic per-tenant Object Store provisioning (see Multitenancy); it is not required to store attachment files themselves.
A valid object store service binding is required, typically one provisioned through SAP BTP. See Storage Targets and Deployment to Cloud Foundry on how to use this object store service binding.
Deployment to Cloud Foundry
The corresponding entry in the mta-file possibly looks like:
_schema-version: '0.1'
ID: consuming-app
version: 1.0.0
description: "App consuming the attachments plugin with an object store"
parameters:
...
modules:
- name: consuming-app-srv
# ------------------------------------------------------------
type: nodejs
path: srv
parameters:
...
properties:
...
build-parameters:
...
requires:
- name: consuming-app-hdi-container
- name: consuming-app-uaa
- name: cf-logging
- name: **object-store-service**
...
resources:
...
- name: **object-store-service**
type: org.cloudfoundry.managed-service
parameters:
service: objectstore
service-plan: standardTests
The unit tests in this module do not need a binding to the respective object stores, run them with npm install. To achieve a clean install, the command rm -rf node_modules should be used before installation.
The integration tests need a binding to a real object store. Run them with npm run test.
To set the binding, please see the section Storage Targets.
Supported Storage Provider
Each cloud provider reads its configuration directly from a BTP destination, pointing at your own AWS, Azure or GCP account rather than an SAP Object Store service instance. attachments-db stores content in the application's own database instead, with no cloud dependency at all.
See step 3: Choose where files are stored and step 4: Create and bind a BTP destination in the Configuration Reference for the full list of kind values, required destination properties per provider, and ready-to-use package.json examples.
Model Texts
In the model, several fields are annotated with the @title annotation. Default texts are provided in 2 languages. If these defaults are not sufficient for an application, they can be overwritten by applications with custom texts or translations.
The following table gives an overview of the fields and the i18n codes:
| Field Name | i18n Code |
|------------|--------------|
| mimeType | MediaType |
| fileName | FileName |
| status | ScanStatus |
| note | note |
In addition to the field names, header information (@UI.HeaderInfo) are also annotated:
| Header Info | i18n Code |
|------------------|---------------|
| TypeName | Attachment |
| TypeNamePlural | Attachments |
Monitoring & Logging
To configure logging for the attachments plugin, add the following configuration to the package.json of the consuming application:
{
"cds": {
"log": {
"levels": {
// (...)
"attachments": "debug"
}
}
}
}
...Support, Feedback, and Contributing
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, the local development setup, as well as additional contribution information, see our Contribution Guidelines.
Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Licensing
Copyright 2024 SAP SE or an SAP affiliate company and contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.
