CI HUBCI HUB SDK
Building an Adapter

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

FieldDescription
requestData.query.assetIdID of the just-uploaded asset
requestData.query.nameFilename 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

FieldDescription
requestData.body.assetIdParent asset ID
requestData.body.qualityRendition quality identifier
requestData.body.nameRendition 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

FieldDescription
requestData.query.typeFile type: 'indd', 'ai', or 'psd'
requestData.query.assetIdParent asset ID
requestData.body.document or requestData.query.documentDocument 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

FieldDescription
requestData.query.transformationIdTransformation ID (from getAvailableTransformations)
requestData.body.dataBuffer or requestData.body.dataBase64Source file data
requestData.body.nameSource 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.

Create Asset Schema

On this page