Advanced Handlers
Handlers for rendition management, asset relationship tracking, and file transformations. These extend the upload pipeline and enable platform-specific features like linked-asset detection and format conversion.
Upload Pipeline with Renditions
When an asset is uploaded, CI HUB can trigger additional processing steps. The flow:
getAdditionalRenditionsNeeded
Called after an asset upload to determine if additional files need to be uploaded alongside the main asset. Typically used for InDesign, Illustrator, and Photoshop files that have linked assets.
Inputs
| Field | Description |
|---|---|
requestData.query.assetId | ID of the just-uploaded asset |
requestData.query.name | Filename of the uploaded asset |
Response
Return an array of rendition descriptors. Each has a type and a url that CI HUB will POST the rendition data to:
return send([
{
type: 'DocumentSummary',
url: `${locals.endpointUrl}/saveAssetRelations?type=${fileExtension}&assetId=${encodeURIComponent(assetId)}`
}
])Return undefined or an empty array if no additional renditions are needed.
Patterns
Example - Triggers for InDesign, Illustrator, and Photoshop files:
const createAdditionalRenditions = (locals, name, assetId) => {
const renditions = []
const fileExtension = path.extname(name).toLowerCase().
replace('.', '')
if ([ 'ai', 'indd', 'psd' ].includes(fileExtension)) {
renditions.push({
type: 'DocumentSummary',
url: `${locals.endpointUrl}/saveAssetRelations?type=${fileExtension}&assetId=${encodeURIComponent(assetId)}`
})
return renditions
}
}
getAdditionalRenditionsNeeded: async (locals, requestData) => {
try {
const { assetId, name } = requestData.query
return send(createAdditionalRenditions(locals, name, assetId))
} catch (error) {
return sendError(error, 400)
}
},Example - Always returns a DocumentSummary rendition for every file type:
const createAdditionalRenditions = (locals, name, assetId) => {
const result = []
const fileExtension = path.extname(name).toLowerCase().
replace('.', '')
result.push({
type: 'DocumentSummary',
url: `${locals.endpointUrl}/saveAssetRelations?type=${fileExtension}&assetId=${encodeURIComponent(assetId)}`
})
return result
}
getAdditionalRenditionsNeeded: async (locals, requestData) => {
try {
const { assetId, name } = requestData.query
return send(createAdditionalRenditions(locals, name, assetId))
} catch (error) {
return sendError(error, 400)
}
},additionalRendition
Receives and uploads one additional rendition file (e.g., a preview image, a linked asset). CI HUB calls this for each rendition returned by getAdditionalRenditionsNeeded.
Inputs
| Field | Description |
|---|---|
requestData.body.assetId | Parent asset ID |
requestData.body.quality | Rendition quality identifier |
requestData.body.name | Rendition filename |
requestData.getFileAsBuffer() | Binary file data |
Response
return sendResult()Patterns
Example - Uploads rendition as multipart form data:
additionalRendition: async (locals, requestData) => {
const quality = requestData.body.quality
const asset_id = requestData.body.assetId
const name = requestData.body.name
const dataBuffer = requestData.getFileAsBuffer()
const formData = new FormDataNode()
try {
formData.append('file', dataBuffer, name || '')
await callApi(locals, 'POST', '/cihub/updateAssetMedia', {
asset_id,
quality
}, formData, formData.getHeaders())
return sendResult()
} catch (err) {
return sendError(err, 400)
}
},saveAssetRelations
Called after CI HUB extracts the document structure from InDesign, Illustrator, or Photoshop files. Receives the document model with linked assets and creates relationship records in the DAM.
Inputs
| Field | Description |
|---|---|
requestData.query.type | File type: 'indd', 'ai', or 'psd' |
requestData.query.assetId | Parent asset ID |
requestData.body.document or requestData.query.document | Document structure with spreads, artboards, and links |
The document object structure varies by type:
- InDesign (
indd):document.spreads[].pages[].links[]with master spread inheritance - Illustrator (
ai):document.artBoards[].links[] - Photoshop (
psd):document.links[]
Each link contains linkData with providerId, remoteSystemPrefix, and id identifying the linked asset.
Response
return sendStatus(200)Patterns
Example - Extracts links from all document types and creates asset relations via REST API:
saveAssetRelations: async (locals, requestData) => {
const { type, assetId, name } = requestData.query
const document = requestData.body.document || requestData.query.document
try {
if (type && !name && [ 'indd', 'ai', 'psd' ].includes(type)) {
const systemPrefix = remoteSystemPrefix(locals)
let links = []
if (type === 'indd') {
const recursiveGetLinksFromMaster = (masterSpreadId, side) => {
const masterSpread = masterSpreadId && (document?.masterSpreads || []).find(entry => entry.id === masterSpreadId)
const page = masterSpread?.pages?.length && masterSpread.pages[masterSpread.pages.length > 1 && side === 'RIGHT_HAND' ? 1 : 0]
return (page?.links && [ ...page.links, ...recursiveGetLinksFromMaster(page.masterSpreadId, side) ]) || []
}
(document?.spreads || []).forEach(spread =>
(spread?.pages || []).forEach(page => {
links = links.concat(page.links, recursiveGetLinksFromMaster(page.masterSpreadId, page.side))
}))
} else if (type === 'ai') {
(document?.artBoards || []).forEach(artBoard => {
links = links.concat(artBoard.links || [])
})
} else if (type === 'psd') {
links = document?.links || []
}
const children = links.map(link => {
const linkData = link && link.linkData
if (linkData && linkData.providerId === 'exampleAdapter1' && linkData.remoteSystemPrefix === systemPrefix) {
return { href: new URL(`api/entities/${linkData.id}`, locals.adapterData.systemUrl).toString() }
}
return undefined
}).filter(item => item)
if (children.length) {
await callApi(locals, 'PUT', `/api/entities/${assetId}/relations/AssetToLinkedAsset`, null, { children, self: { href: new URL(`api/entities/${assetId}/relations/AssetToLinkedAsset`, locals.adapterData.systemUrl) } })
}
}
return sendStatus(200)
} catch (err) {
return sendError(`saveAssetRelations failed: ${err}`, 400)
}
},Example - Extracts links and creates bidirectional asset associations:
saveAssetRelations: async (locals, requestData) => {
const { type, assetId } = requestData.query
const document = requestData.body.document || requestData.query.document
try {
if (type && [ 'indd', 'ai', 'psd' ].includes(type)) {
let links = []
if (type === 'indd') {
const recursiveGetLinksFromMaster = (masterSpreadId, side) => {
const masterSpread = masterSpreadId && (document?.masterSpreads || []).find(entry => entry.id === masterSpreadId)
const page = masterSpread?.pages?.length && masterSpread.pages[masterSpread.pages.length > 1 && side === 'RIGHT_HAND' ? 1 : 0]
return (page?.links && [ ...page.links, ...recursiveGetLinksFromMaster(page.masterSpreadId, side) ]) || []
}
(document?.spreads || []).forEach(spread =>
(spread?.pages || []).forEach(page => {
links = links.concat(page.links, recursiveGetLinksFromMaster(page.masterSpreadId, page.side))
}))
} else if (type === 'ai') {
(document?.artBoards || []).forEach(artBoard => {
links = links.concat(artBoard.links || [])
})
} else if (type === 'psd') {
links = document?.links || []
}
let relations = []
links.forEach(link => {
const linkData = link && link.linkData
if (linkData && linkData.id && !relations.includes(linkData.id)) {
relations.push(linkData.id)
}
})
if (relations.length) {
relations = relations.map(relation => [ assetId, relation ])
relations.forEach(async relation => {
await callApi(locals, 'POST', '/assets/associate', null, { assetIds: relation })
})
}
}
return sendStatus(200)
} catch (err) {
return sendError(`saveAssetRelations failed: ${err}`, 400)
}
},The InDesign link extraction is recursive — master spreads can reference other master spreads, so recursiveGetLinksFromMaster traverses the chain.
getAvailableTransformations
Returns the list of transformations the user can apply to assets (e.g., named presets, format conversions). CI HUB displays these as options in the transformation UI.
Response
Return an array of transformation descriptors:
return send([
{ id: 'named:my-preset', name: 'my-preset', details: { ... } },
{ id: 'format:png', name: 'PNG', extension: 'png' },
{ id: 'format:jpg', name: 'JPEG', extension: 'jpg' }
])Patterns
Example - Fetches named transformations from the platform API and adds format conversions:
getAvailableTransformations: async (locals, requestData) => {
const client = getClient(locals)
const result = []
try {
const transformationList = await client.api.transformations({ named: 'true' })
for (const transformation of transformationList.transformations) {
const transformationId = `${transformation.name.startsWith('t_') ? transformation.name.substring(2) : transformation.name}`
const transformationDetails = await client.api.transformation(transformationId)
const listItem = {
id: `named:${transformationId}`,
name: transformationId,
details: transformationDetails.info
}
result.push(listItem)
}
result.push(...formatConversions)
return send(result)
} catch (err) {
console.error(err)
return sendError(err, 400)
}
},doTransformation
Applies a transformation to an asset and returns the transformed file as a stream. CI HUB calls this when the user selects a transformation and confirms.
Inputs
| Field | Description |
|---|---|
requestData.query.transformationId | Transformation ID (from getAvailableTransformations) |
requestData.body.dataBuffer or requestData.body.dataBase64 | Source file data |
requestData.body.name | Source filename |
Response
Return the transformed file as a stream:
const { data } = await axios.get(downloadUrl, { responseType: 'stream', headers: { Accept: 'application/octet-stream' } })
return send(data)Patterns
Example - Uploads source file, applies named transformation or format conversion, pipes result:
doTransformation: async (locals, requestData) => {
const { transformationId } = requestData.query
const { dataBuffer, dataBase64, name } = requestData.body
const client = getClient(locals)
const [ transformationType, transformationData ] = transformationId.split(':')
try {
const nativeAssetId = await uploadAsset(locals, dataBuffer, dataBase64, name, undefined, undefined, false, true)
const { publicId, version, resourceType, uploadType } = splitAssetId(nativeAssetId)
let transformationParams
if (transformationType === 'format') {
transformationParams = { format: transformationData }
} else {
transformationParams = { transformation: transformationData }
}
const downloadUrl = getTransformedImageUrl(client, publicId, version, resourceType, uploadType, transformationParams)
const { data } = await axios.get(downloadUrl, { responseType: 'stream', headers: { Accept: 'application/octet-stream' } })
return send(data)
} catch (err) {
if (err && err.error && err.error.message) {
return sendError(err.error.message, 400)
} else {
return sendError(err, 400)
}
}
},The transformationId format uses a prefix to distinguish types: named:preset-name for named transformations, format:png for format conversions. Split on : to extract the type and value.