{"id":19755839,"url":"https://github.com/cap-js/attachments","last_synced_at":"2026-04-20T13:01:25.524Z","repository":{"id":230220210,"uuid":"678309412","full_name":"cap-js/attachments","owner":"cap-js","description":"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.","archived":false,"fork":false,"pushed_at":"2026-04-17T14:49:08.000Z","size":30354,"stargazers_count":32,"open_issues_count":12,"forks_count":14,"subscribers_count":11,"default_branch":"main","last_synced_at":"2026-04-17T16:39:52.486Z","etag":null,"topics":["btp","object-store","plugin"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cap-js.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-08-14T08:51:22.000Z","updated_at":"2026-04-17T08:17:32.000Z","dependencies_parsed_at":"2024-04-10T05:35:53.652Z","dependency_job_id":"fb92af89-1dfc-4ec4-9e4f-a1332601ecc1","html_url":"https://github.com/cap-js/attachments","commit_stats":{"total_commits":288,"total_committers":14,"mean_commits":"20.571428571428573","dds":0.6631944444444444,"last_synced_commit":"988123f2a4c92bfee1978585b90468676b212595"},"previous_names":["cap-js/attachments"],"tags_count":37,"template":false,"template_full_name":null,"purl":"pkg:github/cap-js/attachments","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cap-js%2Fattachments","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cap-js%2Fattachments/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cap-js%2Fattachments/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cap-js%2Fattachments/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cap-js","download_url":"https://codeload.github.com/cap-js/attachments/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cap-js%2Fattachments/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32048444,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-20T11:35:06.609Z","status":"ssl_error","status_checked_at":"2026-04-20T11:34:48.899Z","response_time":94,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["btp","object-store","plugin"],"created_at":"2024-11-12T03:13:38.801Z","updated_at":"2026-04-20T13:01:25.517Z","avatar_url":"https://github.com/cap-js.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"[![REUSE status](https://api.reuse.software/badge/github.com/cap-js/attachments)](https://api.reuse.software/info/github.com/cap-js/attachments)\n\n# Attachments Plugin\n\nThe `@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).\n\n### Table of Contents\n\n\u003c!-- TOC --\u003e\n\n- [Attachments Plugin](#attachments-plugin)\n  - [Table of Contents](#table-of-contents)\n  - [Usage](#usage)\n    - [Quick Start](#quick-start)\n    - [Local Walk-Through](#local-walk-through)\n    - [Changes in the CDS Models](#changes-in-the-cds-models)\n    - [Storage Targets](#storage-targets)\n    - [Malware Scanner](#malware-scanner)\n      - [Rate Limit Handling (Auto-Retry)](#rate-limit-handling-auto-retry)\n      - [Scan Concurrency Limiting](#scan-concurrency-limiting)\n      - [Automatic file rescanning](#automatic-file-rescanning)\n    - [Audit logging](#audit-logging)\n    - [Visibility Control for Attachments UI Facet Generation](#visibility-control-for-attachments-ui-facet-generation)\n      - [Example Usage](#example-usage)\n    - [Copying Attachments](#copying-attachments)\n      - [Examples](#examples)\n    - [Querying Attachments Programmatically](#querying-attachments-programmatically)\n    - [Non-Draft Upload](#non-draft-upload)\n    - [Specify the maximum file size](#specify-the-maximum-file-size)\n    - [Restrict allowed MIME types](#restrict-allowed-mime-types)\n    - [Minimum and Maximum Number of Attachments](#minimum-and-maximum-number-of-attachments)\n      - [Limit to a Maximum of 2 Attachments](#limit-to-a-maximum-of-2-attachments)\n      - [Require a Minimum of 2 Attachments](#require-a-minimum-of-2-attachments)\n    - [Allow Overwriting Attachment Content](#allow-overwriting-attachment-content)\n  - [Releases](#releases)\n  - [Minimum UI5 and CAP NodeJS Version](#minimum-ui5-and-cap-nodejs-version)\n  - [Architecture Overview](#architecture-overview)\n    - [Multitenancy](#multitenancy)\n      - [Separate object store instances](#separate-object-store-instances)\n      - [Shared Object Store Instance](#shared-object-store-instance)\n    - [Object Stores](#object-stores)\n      - [Deployment to Cloud Foundry](#deployment-to-cloud-foundry)\n    - [Tests](#tests)\n    - [Supported Storage Provider](#supported-storage-provider)\n    - [Model Texts](#model-texts)\n  - [Monitoring \\\u0026 Logging](#monitoring--logging)\n  - [Support, Feedback, and Contributing](#support-feedback-and-contributing)\n  - [Code of Conduct](#code-of-conduct)\n  - [Licensing](#licensing)\n\n## Usage\n\n### Quick Start\n\nFor a quick local development setup with in-memory storage:\n\n- The plugin is self-configuring as described, see the following details section. To enable attachments, simply add the plugin package to your project:\n\n  ```sh\n  npm add @cap-js/attachments\n  ```\n\n  \u003cdetails\u003e\n    The attachments plugin needs to be referenced in the package.json of the consuming CAP NodeJS application:\n\n  ```cds\n  \"dependencies\": {\n    \"@cap-js/attachments\": \"\u003clatest-version\u003e\",\n    // (...)\n  }\n  ```\n\n  In addition, different profiles can be found in `package.json` as well, such as:\n\n  ```json\n  \"cds\": {\n    \"requires\": {\n      // (...)\n      \"[hybrid]\": {\n        \"attachments\": {\n          \"kind\": \"standard\"\n          // (...)\n        }\n      }\n    }\n  }\n  ```\n\n  \u003c/details\u003e\n\n- 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):\n\n  ```cds\n  using { Attachments } from '@cap-js/attachments';\n\n  entity Incidents {\n      // (...)\n      attachments: Composition of many Attachments;\n  }\n  ```\n\nIn 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.\n\nFor productive use, a valid object store binding is required, see [Object Stores](#object-stores) and [Storage Targets](#storage-targets).\n\n### Local Walk-Through\n\nWith the steps above, we have successfully set up asset handling for our reference application. To test the application locally, use the following steps.\n\n\u003e [!NOTE]\n\u003e For local testing, the attachment objects are stored in a [local database](https://cap.cloud.sap/docs/guides/databases-sqlite).\n\n1. **Start the server**:\n\n- _Default_ scenario (In memory database):\n  ```sh\n  cds watch\n  ```\n\n2. **Navigate to the object page** of the incident `Solar panel broken`:\n   Go to object page for incident **Solar panel broken**\n\n3. The `Attachments` type has generated an out-of-the-box Attachments table (see 1) at the bottom of the Object page:\n   \u003cimg width=\"1300\" alt=\"Attachments Table\" style=\"border-radius:0.5rem;\" src=\"etc/facet.png\"\u003e\n\n4. **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):\n   \u003cimg width=\"1300\" alt=\"Upload an attachment\" style=\"border-radius:0.5rem;\" src=\"etc/upload.gif\"\u003e\n\n5. **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.).\n   \u003cimg width=\"1300\" alt=\"Delete an attachment\" style=\"border-radius:0.5rem;\" src=\"etc/delete.gif\"\u003e\n\n### Changes in the CDS Models\n\nTo 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:\n\n```cds\nusing { Attachments } from '@cap-js/attachments';\n\nentity Incidents {\n  // ...\n  attachments: Composition of many Attachments;\n}\n```\n\nThe entity Incidents can also be extended in the `srv` module, as seen in the following example:\n\n```cds\nusing { Attachments } from '@cap-js/attachments';\n\nextend my.Incidents with {\n  attachments: Composition of many Attachments;\n}\n\nservice ProcessorService {\n  entity Incidents as projection on my.Incidents\n}\n```\n\nBoth 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:\n\n```cds\nannotate service.Incidents with @odata.draft.enabled;\n```\n\nIf 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.\n\n### Storage Targets\n\nWhen 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).\n\nMeanwhile, 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.\n\nFor 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:\n\n1. Log in to Cloud Foundry:\n\n```sh\ncf login -a \u003cCF-API\u003e -o \u003cORG-NAME\u003e -s \u003cSPACE-NAME\u003e --sso\n```\n\n2.  To bind to the service, generate a new file \\_.cdsrc-private.json in the project directory by running:\n\n```sh\ncds bind \u003cHybridObjectStoreName\u003e --to \u003cRemoteObjectStoreName\u003e\n```\n\nWhere `HybridObjectStoreName` can be any name given by the user here and `RemoteObjectStoreName` is the name of your object store instance in SAP BTP.\n\n3.  To run the application in hybrid mode, run the command:\n\n```bash\ncds watch --profile hybrid\n```\n\nSee [Object Stores](#object-stores) for further information on SAP Object Store.\n\n### Malware Scanner\n\nThe BTP malware scanning service is used in the `AttachmentService` to scan attachments for vulnerabilities.\n\nFor 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:\n\n```sh\ncds bind \u003cHybridMalwareScannerName\u003e --to \u003cRemoteMalwareScannerName\u003e\n```\n\nBy default, malware scanning is enabled for all profiles if a storage provider has been specified. You can configure malware scanning by setting:\n\n```json\n{\n  \"cds\": {\n    // (...)\n    \"attachments\": {\n      \"scan\": true\n    }\n  }\n}\n```\n\nIf there is no malware scanner available and the scanner is not disabled, then the upload will fail.\n\nScan status codes:\n\n- `Unscanned`: Attachment is still unscanned.\n- `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.\n- `Clean`: Only attachments with the status Clean are accessible.\n- `Infected`: The attachment is infected.\n- `Failed`: Scanning failed.\n\n\u003e [!Note]\n\u003e The malware scanner supports mTLS authentication which requires an annual renewal of the certificate. Previously, basic authentication was used which has now been deprecated.\n\n\u003e [!Note]\n\u003e 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.\n\n#### Rate Limit Handling (Auto-Retry)\n\nThe 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.\n\nYou can configure the retry behavior in `package.json` or `.cdsrc.json`:\n\n```json\n{\n  \"cds\": {\n    \"requires\": {\n      \"malwareScanner\": {\n        \"retry\": {\n          \"maxAttempts\": 5,\n          \"initialDelay\": 1000,\n          \"maxDelay\": 30000\n        }\n      }\n    }\n  }\n}\n```\n\n| Option               | Default | Description                                            |\n| -------------------- | ------- | ------------------------------------------------------ |\n| `retry.maxAttempts`  | `5`     | Total number of attempts including the initial request |\n| `retry.initialDelay` | `1000`  | Base delay in milliseconds before the first retry      |\n| `retry.maxDelay`     | `30000` | Maximum delay in milliseconds between retries          |\n\nWhen 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.\n\nTo disable retry and restore the previous behavior (immediate failure on 429), set `retry` to `false`.\n\n#### Scan Concurrency Limiting\n\nTo 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.\n\n```json\n{\n  \"cds\": {\n    \"requires\": {\n      \"malwareScanner\": {\n        \"maxConcurrentScans\": 10\n      }\n    }\n  }\n}\n```\n\n| Option               | Default | Description                                                                                            |\n| -------------------- | ------- | ------------------------------------------------------------------------------------------------------ |\n| `maxConcurrentScans` | `30`    | Maximum number of concurrent scan requests per process. Set to `0` to disable (unbounded parallelism). |\n\nA 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.\n\n#### Automatic file rescanning\n\nAccording 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:\n\n```json\n{\n  \"cds\": {\n    \"requires\": {\n      \"attachments\": {\n        \"scanExpiryMs\": 259200000\n      }\n    }\n  }\n}\n```\n\nBy default, `scanExpiryMs` is set to `259200000` milliseconds (3 days). Downloading an attachment is not permitted unless its status is `Clean`.\n\n### Audit logging\n\nThe attachment service emits the following three events:\n\n- AttachmentDownloadRejected,\n- AttachmentSizeExceeded,\n- AttachmentUploadRejected\n\nWhen `@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.\n\nYou can register custom handlers for the three events by writing:\n\n```js\nconst attachments = await cds.connect.to(\"attachments\")\nattachments.on(\"AttachmentDownloadRejected\", (msg) =\u003e {})\n```\n\n### Visibility Control for Attachments UI Facet Generation\n\nBy 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.\n\n#### Example Usage\n\n```cds\nentity Incidents {\n  // ...\n  @UI.Hidden\n  attachments: Composition of many Attachments;\n}\n```\n\nIn 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.\n\n```cds\nentity Incidents {\n  // ...\n  status : Integer enum {\n    submitted =  1;\n    fulfilled =  2;\n    shipped   =  3;\n    canceled  = -1;\n  };\n  @UI.Hidden : (status = #canceled ? true : false)\n  attachments: Composition of many Attachments;\n}\n```\n\n### Copying Attachments\n\nThe `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.\n\n**Signature:**\n\n```js\nconst AttachmentsSrv = await cds.connect.to(\"attachments\")\nawait AttachmentsSrv.copy(\n  sourceAttachmentsEntity,\n  sourceKeys,\n  targetAttachmentsEntity,\n  (targetKeys = {}),\n)\n```\n\n| Parameter                 | Description                                                                                                                                                             |\n| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `sourceAttachmentsEntity` | CDS entity definition of the source attachment composition.                                                                                                             |\n| `sourceKeys`              | Keys of the attachment (e.g. `{ ID: '...' }`)                                                                                                                           |\n| `targetAttachmentsEntity` | CDS entity definition of the target attachment composition.                                                                                                             |\n| `targetKeys`              | Parent FK fields for the new record (e.g. `{ up__ID: '...' }`). When `targetAttachmentsEntity` is a draft table, must also include `DraftAdministrativeData_DraftUUID`. |\n\nThe 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.\n\n\u003e [!NOTE]\n\u003e Only copies within the same tenant are supported. Cross-tenant copies are not possible.\n\n#### Examples\n\n\u003cdetails\u003e\n\n\u003csummary\u003ecopy between two active records:\u003c/summary\u003e\n\n```js\nconst { Incidents } = ProcessorService.entities\n\nawait AttachmentsSrv.copy(\n  Incidents.attachments,\n  { ID: sourceAttachmentID },\n  Incidents.attachments,\n  { up__ID: targetIncidentID },\n)\n```\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\n\u003csummary\u003ecopy into a new draft record (e.g. creating an incident from a template)\u003c/summary\u003e\n\n```js\nconst { Incidents } = ProcessorService.entities\n\n// Look up the draft session UUID for the target incident's open draft\nconst targetDraft = await SELECT.one\n  .from(Incidents.drafts, { ID: targetIncidentID })\n  .columns(\"DraftAdministrativeData_DraftUUID\")\n\nawait AttachmentsSrv.copy(\n  Incidents.attachments,\n  { ID: sourceAttachmentID },\n  Incidents.attachments.drafts,\n  {\n    up__ID: targetIncidentID,\n    DraftAdministrativeData_DraftUUID:\n      targetDraft.DraftAdministrativeData_DraftUUID,\n  },\n)\n```\n\n\u003c/details\u003e\n\n### Querying Attachments Programmatically\n\nBecause `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.\n\nThe entity is accessible by its fully-qualified name `\"\u003cEntity\u003e.attachments\"` via `service.entities`, for example:\n\n```js\nconst Attachments = ProcessorService.entities[\"Incidents.attachments\"]\n```\n\nAttachment 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:\n\n```js\nconst attachmentsMeta = await SELECT.from(Attachments).where({\n  up__ID: incidentID,\n})\n```\n\nThe `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`).\n\n### Non-Draft Upload\n\nFor 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.\n\nThe typical sequence includes:\n\n1. **POST** -\u003e create attachment metadata, returns ID\n2. **PUT** -\u003e upload file content using the ID\n\n### Specify the maximum file size\n\nYou can specify the maximum file size by annotating the attachments content property with `@Validation.Maximum`\n\n```cds\nentity Incidents {\n  ...\n  attachments: Composition of many Attachments;\n}\n\nannotate Incidents.attachments with {\n  content @Validation.Maximum : '20MB';\n}\n```\n\nThe default is 400MB\n\n### Restrict allowed MIME types\n\nYou can restrict which MIME types are allowed for attachments by annotating the content property with `@Core.AcceptableMediaTypes`. This validation is performed during file upload.\n\n```cds\nentity Incidents {\n  ...\n  attachments: Composition of many Attachments;\n}\n\nannotate Incidents.attachments with {\n  content @Core.AcceptableMediaTypes : ['image/jpeg', 'image/png', 'application/pdf'];\n}\n```\n\nWildcard patterns are supported:\n\n```cds\nannotate Incidents.attachments with {\n  content @Core.AcceptableMediaTypes : ['image/*', 'application/pdf'];\n}\n```\n\nTo allow all MIME types (default behavior), either omit the annotation or use:\n\n```cds\nannotate Incidents.attachments with {\n  content @Core.AcceptableMediaTypes : ['*/*'];\n}\n```\n\nWhen a file with a disallowed MIME type is uploaded, the request will be rejected with a `400` error.\n\n### Minimum and Maximum Number of Attachments\n\nYou 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.\n\n#### Limit to a Maximum of 2 Attachments\n\n```cds\nentity Incidents {\n  ...\n  @Validation.MaxItems: 2\n  attachments: Composition of many Attachments;\n}\n```\n\n#### Require a Minimum of 2 Attachments\n\n```cds\nentity Incidents {\n  ...\n  @Validation.MinItems: 2\n  attachments: Composition of many Attachments;\n}\n```\n\n### Allow Overwriting Attachment Content\n\nBy 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.\n\nTo allow overwriting attachment content, override the annotation with an empty array on the specific attachment composition:\n\n```cds\nusing { Attachments } from '@cap-js/attachments';\n\nentity Incidents {\n  ...\n  attachments: Composition of many Attachments;\n}\n\n// Allow content to be overwritten\nannotate Incidents.attachments with\n  @Capabilities.UpdateRestrictions.NonUpdateableProperties: [] {};\n```\n\nWith 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.\n\n\u003e [!NOTE]\n\u003e This annotation is evaluated at runtime by all storage backends. When content overwrite is allowed, uploading to an existing attachment replaces the stored file.\n\n## Releases\n\n- The plugin is released to [NPM Registry](https://www.npmjs.com/package/@cap-js/attachments).\n- See the [changelog](./CHANGELOG.md) or [GitHub Releases](https://github.com/cap-js/attachments/releases) for the latest changes.\n\n## Minimum UI5 and CAP NodeJS Version\n\n| Component | Minimum Version |\n| --------- | --------------- |\n| CAP Node  | 8.0.0           |\n| UI5       | 1.136.0         |\n\n## Architecture Overview\n\n### Multitenancy\n\nThe plugin supports multi-tenancy scenarios, allowing both shared and tenant-specific object store instances.\n\n\u003e [!Note]\n\u003e Starting from version 2.1.0, **separate mode** for object store instances is the default setting for multi-tenancy.\n\nFor multi-tenant applications, `@cap-js/attachments` must be included in the dependencies of both the application-level and _mtx/sidecar/package.json_ files.\n\n#### Separate object store instances\n\nBy default the plugin creates for each tenant its own object store instance during the tenants subscription.\n\nWhen the tenant unsubscribes the object store instance is deleted.\n\n\u003e [!WARNING]\n\u003e When you remove the plugin from an application after separate object stores already have been created, the object stores are not automatically removed!\n\n#### Shared Object Store Instance\n\nTo configure a shared object store instance, modify both the package.json files as follows:\n\n```json\n\"cds\": {\n  \"requires\": {\n    \"attachments\": {\n      \"objectStore\": {\n        \"kind\": \"shared\"\n      }\n    }\n  }\n}\n```\n\nTo 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.\n\n### Object Stores\n\nA 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.\n\n#### Deployment to Cloud Foundry\n\nThe corresponding entry in the [mta-file](https://cap.cloud.sap/docs/guides/deployment/to-cf#add-mta-yaml) possibly looks like:\n\n```\n_schema-version: '0.1'\nID: consuming-app\nversion: 1.0.0\ndescription: \"App consuming the attachments plugin with an object store\"\nparameters:\n  ...\nmodules:\n  - name: consuming-app-srv\n# ------------------------------------------------------------\n    type: nodejs\n    path: srv\n    parameters:\n      ...\n    properties:\n      ...\n    build-parameters:\n      ...\n    requires:\n      - name: consuming-app-hdi-container\n      - name: consuming-app-uaa\n      - name: cf-logging\n      - name: **object-store-service**\n...\nresources:\n  ...\n  - name: **object-store-service**\n    type: org.cloudfoundry.managed-service\n    parameters:\n      service: objectstore\n      service-plan: standard\n```\n\n### Tests\n\nThe 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.\n\nFor 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).\n\nThe integration tests need a binding to a real object store. Run them with `npm run test`.\nTo set the binding, please see the section [Storage Targets](#storage-targets).\n\n### Supported Storage Provider\n\n- **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:\n  - **AWS S3** (`kind: \"s3\"`)\n  - **Azure Blob Storage** (`kind: \"azure\"`)\n  - **GCP Cloud Storage** (`kind: \"gcp\"`)\n\n### Model Texts\n\nIn 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.\n\nThe following table gives an overview of the fields and the i18n codes:\n\n| Field Name | i18n Code    |\n| ---------- | ------------ |\n| `mimeType` | `MediaType`  |\n| `fileName` | `FileName`   |\n| `status`   | `ScanStatus` |\n| `note`     | `note`       |\n\nIn addition to the field names, header information (`@UI.HeaderInfo`) are also annotated:\n\n| Header Info      | i18n Code     |\n| ---------------- | ------------- |\n| `TypeName`       | `Attachment`  |\n| `TypeNamePlural` | `Attachments` |\n\n## Monitoring \u0026 Logging\n\nTo configure logging for the attachments plugin, add the following configuration to the `package.json` of the consuming application:\n\n```json\n{\n  \"cds\": {\n    \"log\": {\n      \"levels\": {\n         // (...)\n         \"attachments\": \"debug\"\n      }\n    }\n  }\n}\n...\n```\n\n## Support, Feedback, and Contributing\n\nThis 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).\n\n## Code of Conduct\n\nWe 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.\n\n## Licensing\n\nCopyright 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).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcap-js%2Fattachments","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcap-js%2Fattachments","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcap-js%2Fattachments/lists"}