Use @Core.MediaType for binary attachments. Integrate with SAP Object Store for scalable file storage.
@Core.MediaTypeEvery business app needs file attachments — product images, invoice PDFs, contract documents. CAP's OData media entity pattern gives you a standardised upload/download API that Fiori Attachments works with out of the box.
// db/schema.cds
entity Attachments {
key ID : UUID;
order_ID : UUID;
filename : String;
mediaType : String @Core.IsMediaType; // stores MIME type
content : LargeBinary @Core.MediaType: mediaType; // the binary blob
size : Integer;
createdAt : Timestamp @cds.on.insert: $now;
}
// srv/orders-service.cds
service OrdersService {
entity Attachments as projection on my.Attachments;
}
// srv/orders-service.js
const fs = require('fs')
const path = require('path')
this.on('PUT', 'Attachments', async (req) => {
const { ID } = req.params[0] // key from URL: PUT /Attachments(uuid)/$value
const mediaType = req.headers['content-type']
const filename = req.headers['slug'] || 'upload'
// For small files: buffer and store in DB
const chunks = []
for await (const chunk of req.data) chunks.push(chunk)
const content = Buffer.concat(chunks)
await UPSERT.into('Attachments').entries({
ID,
order_ID: req.headers['x-order-id'],
filename,
mediaType,
content,
size: content.length
})
return { ID, filename, size: content.length }
})
this.on('READ', 'Attachments', async (req) => {
// When URL is /Attachments(uuid)/$value — streaming the binary
if (req.params?.[0] && req._.req?.url?.endsWith('/$value')) {
const { ID } = req.params[0]
const row = await SELECT.one.from('Attachments').where({ ID })
if (!row) return req.reject(404, 'Attachment not found')
req._.res.setHeader('Content-Type', row.mediaType)
req._.res.setHeader('Content-Length', row.size)
req._.res.setHeader('Content-Disposition', `inline; filename="${row.filename}"`)
req._.res.end(row.content)
return // prevent CAP from sending its own response
}
// Normal metadata read
return SELECT.from('Attachments').columns('ID','filename','mediaType','size','createdAt')
})
// For large files, stream to SAP Object Store instead of DB
const objectStore = await cds.connect.to('object-store')
this.on('PUT', 'Attachments', async (req) => {
const { ID } = req.params[0]
const key = `attachments/${ID}`
// Stream directly to Object Store
await objectStore.upload({ key, body: req.data, contentType: req.headers['content-type'] })
await UPSERT.into('Attachments').entries({
ID,
storageKey: key,
mediaType: req.headers['content-type'],
filename: req.headers['slug']
})
})
// UI5: upload via OData V4 model
const oListBinding = oModel.bindList('/Attachments')
const oContext = oListBinding.create({ ID: uuid() })
const oBinding = oModel.bindProperty(`${oContext.getPath()}/$value`)
await oBinding.setValue(file) // file is a File object from <input type="file">
### Create metadata first
POST http://localhost:4004/orders/Attachments
Content-Type: application/json
{ "ID": "att-001", "order_ID": "ord-001", "filename": "invoice.pdf" }
### Then upload content
PUT http://localhost:4004/orders/Attachments('att-001')/$value
Content-Type: application/pdf
Slug: invoice.pdf
< ./invoice.pdf
| Mistake | Symptom | Fix |
|---|---|---|
@Core.MediaType on wrong field |
Download returns wrong MIME type | Annotate the mediaType string field |
| Buffering large files in memory | OOM error | Use streaming or Object Store |
Missing $value segment in URL |
Gets metadata, not file | Append /$value to download binary |
| Returning from download handler | Double response error | Return undefined after res.end() |
Attachments entity to your bookshop schemacurl -X PUT .../Attachments('test')/$value --data-binary @file.pdf@Core.IsMediaType marks the MIME type field; @Core.MediaType: mimeField marks the binary/$value endpoint uploads the binary contentres.end() and return undefined to avoid double-response errors