https://github.com/cap-js/attachments
The @cap-js/attachments package is a CDS plugin that provides out-of-the box asset storage and handling by using an aspect Attachments. It also provides a CAP-level, easy to use integration of the SAP Object Store.
https://github.com/cap-js/attachments
btp object-store plugin
Last synced: 2 months ago
JSON representation
The @cap-js/attachments package is a CDS plugin that provides out-of-the box asset storage and handling by using an aspect Attachments. It also provides a CAP-level, easy to use integration of the SAP Object Store.
- Host: GitHub
- URL: https://github.com/cap-js/attachments
- Owner: cap-js
- License: apache-2.0
- Created: 2023-08-14T08:51:22.000Z (almost 3 years ago)
- Default Branch: main
- Last Pushed: 2026-04-17T14:49:08.000Z (2 months ago)
- Last Synced: 2026-04-17T16:39:52.486Z (2 months ago)
- Topics: btp, object-store, plugin
- Language: JavaScript
- Homepage:
- Size: 28.9 MB
- Stars: 32
- Watchers: 11
- Forks: 14
- Open Issues: 12
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Codeowners: .github/CODEOWNERS
Awesome Lists containing this project
README
[](https://api.reuse.software/info/github.com/cap-js/attachments)
# Attachments Plugin
The `@cap-js/attachments` package is a [CDS plugin](https://cap.cloud.sap/docs/node.js/cds-plugins#cds-plugin-packages) that provides out-of-the box asset storage and handling by using an [_aspect_](https://cap.cloud.sap/docs/cds/cdl#aspects) called `Attachments`. It also provides a CAP-level, easy-to-use integration of the [SAP Object Store](https://help.sap.com/docs/object-store/object-store-service-on-sap-btp/what-is-object-store).
### Table of Contents
- [Attachments Plugin](#attachments-plugin)
- [Table of Contents](#table-of-contents)
- [Usage](#usage)
- [Quick Start](#quick-start)
- [Local Walk-Through](#local-walk-through)
- [Changes in the CDS Models](#changes-in-the-cds-models)
- [Storage Targets](#storage-targets)
- [Malware Scanner](#malware-scanner)
- [Rate Limit Handling (Auto-Retry)](#rate-limit-handling-auto-retry)
- [Scan Concurrency Limiting](#scan-concurrency-limiting)
- [Automatic file rescanning](#automatic-file-rescanning)
- [Audit logging](#audit-logging)
- [Visibility Control for Attachments UI Facet Generation](#visibility-control-for-attachments-ui-facet-generation)
- [Example Usage](#example-usage)
- [Copying Attachments](#copying-attachments)
- [Examples](#examples)
- [Querying Attachments Programmatically](#querying-attachments-programmatically)
- [Non-Draft Upload](#non-draft-upload)
- [Specify the maximum file size](#specify-the-maximum-file-size)
- [Restrict allowed MIME types](#restrict-allowed-mime-types)
- [Minimum and Maximum Number of Attachments](#minimum-and-maximum-number-of-attachments)
- [Limit to a Maximum of 2 Attachments](#limit-to-a-maximum-of-2-attachments)
- [Require a Minimum of 2 Attachments](#require-a-minimum-of-2-attachments)
- [Allow Overwriting Attachment Content](#allow-overwriting-attachment-content)
- [Releases](#releases)
- [Minimum UI5 and CAP NodeJS Version](#minimum-ui5-and-cap-nodejs-version)
- [Architecture Overview](#architecture-overview)
- [Multitenancy](#multitenancy)
- [Separate object store instances](#separate-object-store-instances)
- [Shared Object Store Instance](#shared-object-store-instance)
- [Object Stores](#object-stores)
- [Deployment to Cloud Foundry](#deployment-to-cloud-foundry)
- [Tests](#tests)
- [Supported Storage Provider](#supported-storage-provider)
- [Model Texts](#model-texts)
- [Monitoring \& Logging](#monitoring--logging)
- [Support, Feedback, and Contributing](#support-feedback-and-contributing)
- [Code of Conduct](#code-of-conduct)
- [Licensing](#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:
```sh
npm add @cap-js/attachments
```
The attachments plugin needs to be referenced in the package.json of the consuming CAP NodeJS application:
```cds
"dependencies": {
"@cap-js/attachments": "",
// (...)
}
```
In addition, different profiles can be found in `package.json` as well, such as:
```json
"cds": {
"requires": {
// (...)
"[hybrid]": {
"attachments": {
"kind": "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](#changes-in-the-cds-models) for more details):
```cds
using { Attachments } from '@cap-js/attachments';
entity Incidents {
// (...)
attachments: Composition of many Attachments;
}
```
In this guide, we use the [Incidents Management reference sample app](https://github.com/cap-js/incidents-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](./tests/incidents-app) directory for local testing.
For productive use, a valid object store binding is required, see [Object Stores](#object-stores) and [Storage Targets](#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](https://cap.cloud.sap/docs/guides/databases-sqlite).
1. **Start the server**:
- _Default_ scenario (In memory database):
```sh
cds watch
```
2. **Navigate to the object page** of the incident `Solar panel broken`:
Go to object page for incident **Solar panel broken**
3. The `Attachments` type has generated an out-of-the-box Attachments table (see 1) at the bottom of the Object page:

4. **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_](./tests/integration/content/sample.pdf):

5. **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:
```cds
using { Attachments } from '@cap-js/attachments';
entity Incidents {
// ...
attachments: Composition of many Attachments;
}
```
The entity Incidents can also be extended in the `srv` module, as seen in the following example:
```cds
using { Attachments } from '@cap-js/attachments';
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](https://cap.cloud.sap/docs/advanced/fiori#enabling-draft-with-odata-draft-enabled) for the entity using `@odata.draft.enabled`. For example:
```cds
annotate service.Incidents with @odata.draft.enabled;
```
If you are not using SAP Fiori elements, draft enablement is not required. For more information, see [non-draft upload](#non-draft-upload) for an alternative upload flow.
### Storage Targets
When testing locally, the plugin operates without a dedicated storage target, storing attachments directly in the underlying database. In a hybrid setup, a dedicated storage target is preferred. You can bind it by using the `cds bind` command as described in the [CAP documentation for hybrid testing](https://cap.cloud.sap/docs/advanced/hybrid-testing#services-on-cloud-foundry).
Meanwhile, with a dedicated storage target the attachment is not stored in the underlying database; instead, it is saved on the specified storage target and only a reference to the file including metadata is kept in the database, as defined in the CDS model.
For using an Object Store in BTP, you must already have an SAP Object Store service instance on the appropriate landscape created. To bind it in a hybrid setup, follow this setup:
1. Log in to Cloud Foundry:
```sh
cf login -a -o -s --sso
```
2. To bind to the service, generate a new file \_.cdsrc-private.json in the project directory by running:
```sh
cds bind --to
```
Where `HybridObjectStoreName` can be any name given by the user here and `RemoteObjectStoreName` is the name of your object store instance in SAP BTP.
3. To run the application in hybrid mode, run the command:
```bash
cds watch --profile hybrid
```
See [Object Stores](#object-stores) for further information on SAP Object Store.
### Malware Scanner
The BTP malware scanning service is used in the `AttachmentService` to scan attachments for vulnerabilities.
For using [SAP Malware Scanning Service](https://discovery-center.cloud.sap/serviceCatalog/malware-scanning-service), you must already have a service instance which you can access. To bind it, run the following command:
```sh
cds bind --to
```
By default, malware scanning is enabled for all profiles if a storage provider has been specified. You can configure malware scanning by setting:
```json
{
"cds": {
// (...)
"attachments": {
"scan": true
}
}
}
```
If there is no malware scanner available and the scanner is not disabled, then the upload will fail.
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](#specify-the-maximum-file-size) it removes the file and sets the status of the attachment metadata to failed.
#### Rate Limit Handling (Auto-Retry)
The SAP Malware Scanning Service enforces a rate limit of 30 concurrent requests per subaccount. When this limit is exceeded, the service responds with HTTP `429 Too Many Requests`. By default, the plugin automatically retries scan requests that receive a 429 response using exponential backoff with jitter.
You can configure the retry behavior in `package.json` or `.cdsrc.json`:
```json
{
"cds": {
"requires": {
"malwareScanner": {
"retry": {
"maxAttempts": 5,
"initialDelay": 1000,
"maxDelay": 30000
}
}
}
}
}
```
| Option | Default | Description |
| -------------------- | ------- | ------------------------------------------------------ |
| `retry.maxAttempts` | `5` | Total number of attempts including the initial request |
| `retry.initialDelay` | `1000` | Base delay in milliseconds before the first retry |
| `retry.maxDelay` | `30000` | Maximum delay in milliseconds between retries |
When a 429 response includes a `Retry-After` header, the plugin respects that value (capped at `maxDelay`). Only 429 responses trigger retries — other errors fail immediately.
To disable retry and restore the previous behavior (immediate failure on 429), set `retry` to `false`.
#### Scan Concurrency Limiting
To reduce pressure on the shared rate limit, the plugin limits how many scan requests run concurrently within a single process. Excess scans are queued and processed as slots become available.
```json
{
"cds": {
"requires": {
"malwareScanner": {
"maxConcurrentScans": 10
}
}
}
}
```
| Option | Default | Description |
| -------------------- | ------- | ------------------------------------------------------------------------------------------------------ |
| `maxConcurrentScans` | `30` | Maximum number of concurrent scan requests per process. Set to `0` to disable (unbounded parallelism). |
A scan that is retrying due to a 429 response holds its concurrency slot during the backoff wait, preventing retry storms from competing with new scans.
#### Automatic file rescanning
According to the recommendation of the [Malware Scanning Service](http://help.sap.com/docs/malware-scanning-service/sap-malware-scanning-service/developing-applications-with-sap-malware-scanning-service), attachments should be rescanned automatically if the last scan is older than 3 days. This behavior can be configured in the attachments settings by specifying the `scanExpiryMs` property:
```json
{
"cds": {
"requires": {
"attachments": {
"scanExpiryMs": 259200000
}
}
}
}
```
By default, `scanExpiryMs` is set to `259200000` milliseconds (3 days). Downloading an attachment is not permitted unless its status is `Clean`.
### Audit logging
The attachment service emits the following three events:
- AttachmentDownloadRejected,
- AttachmentSizeExceeded,
- AttachmentUploadRejected
When `@cap-js/audit-logging` is a dependency of your app, the three events will be automatically logged as security events in the audit log service.
You can register custom handlers for the three events by writing:
```js
const attachments = await cds.connect.to("attachments")
attachments.on("AttachmentDownloadRejected", (msg) => {})
```
### 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
```cds
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.
```cds
entity Incidents {
// ...
status : Integer enum {
submitted = 1;
fulfilled = 2;
shipped = 3;
canceled = -1;
};
@UI.Hidden : (status = #canceled ? true : false)
attachments: Composition of many Attachments;
}
```
### Copying Attachments
The `AttachmentsService` exposes a programmatic `copy()` method that copies an attachment to a new record. On cloud storage backends (AWS S3, Azure Blob Storage, GCP Cloud Storage) this uses a backend-native server-side copy — no binary data is transferred through your application. On database storage it reads and inserts the content directly.
**Signature:**
```js
const AttachmentsSrv = await cds.connect.to("attachments")
await AttachmentsSrv.copy(
sourceAttachmentsEntity,
sourceKeys,
targetAttachmentsEntity,
(targetKeys = {}),
)
```
| Parameter | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sourceAttachmentsEntity` | CDS entity definition of the source attachment composition. |
| `sourceKeys` | Keys of the attachment (e.g. `{ ID: '...' }`) |
| `targetAttachmentsEntity` | CDS entity definition of the target attachment composition. |
| `targetKeys` | Parent FK fields for the new record (e.g. `{ up__ID: '...' }`). When `targetAttachmentsEntity` is a draft table, must also include `DraftAdministrativeData_DraftUUID`. |
The scan `status`, `lastScan`, and `hash` are inherited from the source — no re-scan is triggered since the binary content is identical. Copying an attachment when the status is not `Clean` is rejected with a `400` error.
> [!NOTE]
> Only copies within the same tenant are supported. Cross-tenant copies are not possible.
#### Examples
copy between two active records:
```js
const { Incidents } = ProcessorService.entities
await AttachmentsSrv.copy(
Incidents.attachments,
{ ID: sourceAttachmentID },
Incidents.attachments,
{ up__ID: targetIncidentID },
)
```
copy into a new draft record (e.g. creating an incident from a template)
```js
const { Incidents } = ProcessorService.entities
// Look up the draft session UUID for the target incident's open draft
const targetDraft = await SELECT.one
.from(Incidents.drafts, { ID: targetIncidentID })
.columns("DraftAdministrativeData_DraftUUID")
await AttachmentsSrv.copy(
Incidents.attachments,
{ ID: sourceAttachmentID },
Incidents.attachments.drafts,
{
up__ID: targetIncidentID,
DraftAdministrativeData_DraftUUID:
targetDraft.DraftAdministrativeData_DraftUUID,
},
)
```
### Querying Attachments Programmatically
Because `Attachments` is a standard CDS composition, the resulting attachment entity can be queried directly using [cds.ql](https://cap.cloud.sap/docs/node.js/cds-ql) in the same way as querying any other entity in a CAP service.
The entity is accessible by its fully-qualified name `".attachments"` via `service.entities`, for example:
```js
const Attachments = ProcessorService.entities["Incidents.attachments"]
```
Attachment metadata (ID, filename, mimeType, status, lastScan, note, createdAt, createdBy) for a given parent record can be fetched using `SELECT.from`. Note that the binary content field is excluded by default, making the operation lightweight:
```js
const attachmentsMeta = await SELECT.from(Attachments).where({
up__ID: incidentID,
})
```
The `up__ID` column is the auto-generated foreign key back to the parent record. The suffix after `up__` is the parent entity's key field name (e.g. `ID`).
### Non-Draft Upload
For scenarios where the entity is not draft-enabled, for example [`tests/non-draft-request.http`](./tests/non-draft-request.http), separate HTTP requests for metadata creation and asset uploading need to be performed manually.
The typical sequence includes:
1. **POST** -> create attachment metadata, returns ID
2. **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`
```cds
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.
```cds
entity Incidents {
...
attachments: Composition of many Attachments;
}
annotate Incidents.attachments with {
content @Core.AcceptableMediaTypes : ['image/jpeg', 'image/png', 'application/pdf'];
}
```
Wildcard patterns are supported:
```cds
annotate Incidents.attachments with {
content @Core.AcceptableMediaTypes : ['image/*', 'application/pdf'];
}
```
To allow all MIME types (default behavior), either omit the annotation or use:
```cds
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.
### Minimum and Maximum Number of Attachments
You can control the number of attachments allowed for an entity by using the `@Validation.MaxItems` and `@Validation.MinItems` annotations. These annotations define the maximum and minimum number of files that can be associated with an entity.
#### Limit to a Maximum of 2 Attachments
```cds
entity Incidents {
...
@Validation.MaxItems: 2
attachments: Composition of many Attachments;
}
```
#### Require a Minimum of 2 Attachments
```cds
entity Incidents {
...
@Validation.MinItems: 2
attachments: Composition of many Attachments;
}
```
### Allow Overwriting Attachment Content
By default, the `Attachments` aspect annotates the entity with `@Capabilities.UpdateRestrictions.NonUpdateableProperties: [content]`, which prevents overwriting the content of an existing attachment. Any attempt to upload new content to an attachment that already has content will be rejected with a `409 Conflict` error.
To allow overwriting attachment content, override the annotation with an empty array on the specific attachment composition:
```cds
using { Attachments } from '@cap-js/attachments';
entity Incidents {
...
attachments: Composition of many Attachments;
}
// Allow content to be overwritten
annotate Incidents.attachments with
@Capabilities.UpdateRestrictions.NonUpdateableProperties: [] {};
```
With this annotation in place, uploading new content via `PUT` to an attachment that already has content will overwrite the existing content instead of returning a `409` error.
> [!NOTE]
> This annotation is evaluated at runtime by all storage backends. When content overwrite is allowed, uploading to an existing attachment replaces the stored file.
## Releases
- The plugin is released to [NPM Registry](https://www.npmjs.com/package/@cap-js/attachments).
- See the [changelog](./CHANGELOG.md) or [GitHub Releases](https://github.com/cap-js/attachments/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-js/attachments` 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:
```json
"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
A valid object store service binding is required, typically one provisioned through SAP BTP. See [Storage Targets](#storage-targets) and [Deployment to Cloud Foundry](#deployment-to-cloud-foundry) on how to use this object store service binding.
#### Deployment to Cloud Foundry
The corresponding entry in the [mta-file](https://cap.cloud.sap/docs/guides/deployment/to-cf#add-mta-yaml) 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: standard
```
### Tests
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.
For testing locally with a Postgres database, create a Podman (or Docker) container and run the command `podman compose -f tests/pg.yml up -d`. This should be run every time the container is stopped. On the initial setup, the database must be deployed with `npm run deploy:postgres`. From then on, running the tests with Postgres is simply `npm run test:postgres`. For more information on Postgres setup, see the official [Capire documentation](https://cap.cloud.sap/docs/guides/databases/postgres).
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](#storage-targets).
### Supported Storage Provider
- **Standard** (`kind: "standard"`) | Depending on the bound object store credentials, uses AWS S3, Azure Blob Storage or GCP Cloud Storage. You can manually specify the implementation by adjusting the type to:
- **AWS S3** (`kind: "s3"`)
- **Azure Blob Storage** (`kind: "azure"`)
- **GCP Cloud Storage** (`kind: "gcp"`)
### Model Texts
In the model, several fields are annotated with the `@title` annotation. Default texts are provided in [2 languages](./_i18n). 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:
```json
{
"cds": {
"log": {
"levels": {
// (...)
"attachments": "debug"
}
}
}
}
...
```
## Support, Feedback, and Contributing
This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/attachments/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](CONTRIBUTING.md).
## 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](CODE_OF_CONDUCT.md) at all times.
## Licensing
Copyright 2024 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/attachments).