Asset Operations
Handlers for modifying assets after they exist: delete, version history, lock/unlock, rename, and move. Each requires a capability flag to appear in the plugin UI.
Capability Flags
Enable each operation in your adapter's capabilities object:
capabilities: {
canDeleteAsset: true,
canLockAsset: true,
canRenameAsset: true,
canMoveAsset: true
}getAssetVersions has no capability flag — it's always available when implemented.
deleteAsset
Permanently removes an asset from the platform. CI HUB calls this when the user clicks Delete on an asset in the plugin UI.
Inputs
| Field | Description |
|---|---|
requestData.params.assetId | Asset ID to delete |
requestData.query.parentId or requestData.body.parentId | Parent folder ID (some platforms need this to distinguish collections from folders) |
Response
return sendResult(null) // or return sendStatus(200)Patterns
Example - REST API, strips version suffix from compound IDs:
deleteAsset: async (locals, requestData) => {
const assetId = requestData.params.assetId.split('::')[0]
try {
await callApi(locals, 'DELETE', `/api/v4/media/${assetId}/`, {})
return sendResult(null)
} catch (err) {
return sendError(`Could not delete asset ${assetId} (${err})`, 403)
}
},Example - DAM that handles collection vs folder context:
deleteAsset: async (locals, requestData) => {
try {
const { assetId } = requestData.params
const parentFolderId = requestData.query.parentId || requestData.body.parentId || ''
if (parentFolderId.startsWith('collection:')) {
const collectionId = parentFolderId.replace('collection:', '')
await callApi(locals, { query: queries.removeCollectionAssetsMutation, variables: { input: { collectionId, assetIds: [ assetId ] } } })
} else {
await callApi(locals, { query: queries.deleteAssetMutation, variables: { input: { id: assetId } } })
}
return sendStatus(200)
} catch (error) {
return sendError(error, 400)
}
},UI Behavior
On success, the asset disappears from the current folder/search results. On error, the plugin UI shows the error message.
getAssetVersions
Returns version history for an asset. CI HUB calls this when the user opens the version panel for an asset.
Inputs
| Field | Description |
|---|---|
requestData.query.assetId | Single asset ID |
requestData.query.assetIds or requestData.body.assetIds | Array of asset IDs (batch mode) |
requestData.query.withMaster | "true" to include the current/master version |
Response
Return an object with id, name, and versions array. Each version uses the same shape as extractAssetsData output plus masterId:
return send({
id: assetId,
name: 'filename.jpg',
versions: [
{
...assetData, // standard asset fields (thumbnailUrl, fileSize, etc.)
masterId: assetId, // always the root asset ID
id: 'asset123::2', // version-specific ID
version: 2,
}
]
})Patterns
Example - DAM that iterates mediaItems for version history:
const getAssetVersions = async (locals, requestData, assetId) => {
const withMaster = requestData.query.withMaster === 'true'
try {
const asset = await callApi(locals, 'GET', `/api/v4/media/${assetId}`, {
versions: true
})
const assetInfo = extractAssetData(locals, asset)
const result = {
id: assetId,
name: '',
versions: []
}
result.versions = asset.mediaItems.
filter(item => item.type === 'original').
sort((itemA, itemB) => itemB.version - itemA.version).
map((item, idx) => {
const versionId = `${assetId}::${item.version}`
const version = Object.assign({}, assetInfo, {
masterId: assetId,
id: versionId,
version: item.version,
fileSize: item.size,
xSizePx: item.width,
ySizePx: item.height,
thumbnailUrl: item.thumbnails.thul && `${item.thumbnails.thul}$NO_AUTH$`,
downloadUrl: getDownloadUrl(locals, versionId),
lowresUrl: getDownloadUrl(locals, versionId, '?lowres=1')
})
if (idx) {
version.created = new Date(item.dateCreated).getTime()
version.modified = new Date(item.dateCreated).getTime()
version.downloadHashFileAttributes = JSON.stringify({ fileSize: version.fileSize, modified: version.modified })
}
return version
})
if (!withMaster) {
result.versions.shift()
}
return result
} catch (err) {
throw new Error(`Could not get asset versions for ${assetId} (${err})`)
}
}Example - DAM that returns single version when platform doesn't support version history:
getAssetVersions: async (locals, requestData) => {
const { assetId } = requestData.query
const withMaster = requestData.query.withMaster === 'true'
const result = { id: assetId, name: undefined, versions: [] }
if (!withMaster) {
return sendError('DAM doesn\'t support versioning', 400)
}
try {
const asset = await callApi(locals, { query: queries.getAssetByIdQuery, variables: { assetId } })
result.versions = asset.data.asset && extractAssetsData(locals, [ asset.data.asset ]).map(assetData => ({ ...assetData, masterId: assetId }))
result.name = result.versions[0]?.name
return send(result)
} catch (error) {
return sendError(error, 400)
}
},UI Behavior
The version panel lists all returned versions with thumbnails, dates, and file sizes. Users can download or place any version. The masterId links versions back to the root asset.
lockAsset
Locks (checks out) or unlocks (checks in) an asset. Prevents concurrent edits on platforms that support exclusive checkout.
Inputs
| Field | Description |
|---|---|
requestData.params.assetId | Asset ID |
requestData.body.locked | true to lock, false to unlock |
Response
return send({ id: assetId, lockedBy: 'username' }) // locked
return send({ id: assetId, lockedBy: null }) // unlockedPatterns
Example - DAM with checkout/checkin endpoint, polls for lock confirmation:
lockAsset: async (locals, requestData) => {
const assetId = requestData.params.assetId
const locked = requestData.body.locked
const action = locked ? 'checkout' : 'checkin'
try {
await callApi(locals, 'POST', `content/dam/${assetId}.checkout.json`, { action })
let retries = maxRetries
let lockedBy
if (locked) {
while (!lockedBy && retries) {
if (retries !== maxRetries) {
await promisify(setTimeout)(1000)
}
lockedBy = (await callApi(locals, 'GET', `/platform/content/dam/${assetId};resource=metadata`))?.['aem:checkedOutBy']
retries -= 1
}
if (retries === 0) {
throw new Error(`${locked ? 'Lock (checkout)' : 'Unlock (checkin)'} operation failed after a ${maxRetries} second(s) waiting period.`)
}
}
return send({ id: assetId, lockedBy })
} catch (err) {
return sendResult(err)
}
},Example - DAM that supports separate lock/unlock endpoints:
lockAsset: async (locals, requestData) => {
const assetId = requestData.params.assetId
const locked = requestData.body.locked
const result = { id: null, lockedBy: null }
try {
const currentUser = (await callApi(locals, 'GET', '/users/current')).payload
if (locked) {
const lockResponse = await callApi(locals, 'POST', `/assets/${assetId}/checkOut`)
if (lockResponse.errors && lockResponse.errors.length) {
return sendError(lockResponse.errors.join(', '), 400)
}
result.id = assetId
result.lockedBy = `You (${currentUser.userName})`
} else {
const unlockResponse = await callApi(locals, 'PATCH', `/assets/${assetId}`, null, [{ op: 'remove', path: '/checkedOutBy' }])
if (unlockResponse.errors && unlockResponse.errors.length) {
return sendError(unlockResponse.errors.join(', '), 400)
}
result.id = assetId
}
} catch (err) {
return sendError(err, 400)
}
return send(result)
},UI Behavior
When locked, the plugin UI shows a lock icon on the asset and displays lockedBy. Only the lock owner (or an admin, depending on platform) can unlock. The lock prevents other users from uploading new versions.
renameAsset
Changes an asset's display name or filename.
Inputs
| Field | Description |
|---|---|
requestData.params.assetId | Asset ID |
requestData.body.name | New name |
Response
return send({ id: assetId, name: newName })Patterns
Example - DAM that distinguishes filename vs title based on extension:
renameAsset: async (locals, requestData) => {
try {
const { name } = requestData.body
const { assetId } = requestData.params
const hasExtension = Boolean(mime.lookup(name))
const asset = await callApi(locals, {
query: queries.updateAssetMutation,
variables: { input: { id: assetId, data: hasExtension ? { filename: name } : { title: name } } }
})
const updatedAsset = asset.data.updateAsset.asset
return send({ id: updatedAsset.id.toString(), name: hasExtension ? updatedAsset.filename : updatedAsset.title })
} catch (error) {
return sendError(error, 400)
}
},Example — REST API, straightforward PUT:
renameAsset: async (locals, requestData) => {
try {
const { name } = requestData.body
const { assetId } = requestData.params
const { id } = getAssetIdInfo(assetId)
await callApi(locals, 'PUT', `/files/${id}`, null, { name })
return send({ id: assetId, name })
} catch (error) {
return sendError(error, 400)
}
},UI Behavior
On success, the asset's name updates immediately in the plugin UI (folder view and detail panel).
moveAsset
Moves an asset from one folder to another.
Inputs
| Field | Description |
|---|---|
requestData.body.assetId | Asset ID |
requestData.body.targetParentId | Destination folder ID |
requestData.body.currentParentId | Source folder ID |
Response
return send({ id: assetId, folderId: targetParentId })Patterns
Example - DAM that replaces old category with new one:
moveAsset: async (locals, requestData) => {
try {
const { assetId, targetParentId: folderId, currentParentId } = requestData.body
if (typeof folderId !== 'string' || !folderId || folderId.startsWith('lightbox') || [ 'root', 'categories', 'lightboxes', 'home', 'mostViewed', 'recentlyUploaded' ].includes(folderId)) {
return sendError('Invalid folder ID', 400)
}
const assetCategories = ((await callApi(locals, 'GET', `/assets/${assetId}/versions/basic`))?.payload?.[0]?.categoryIds || []).filter(category => category !== currentParentId?.toString()).concat(folderId.toString())
await callApi(locals, 'PUT', `/assets/${assetId}/categories`, null, assetCategories)
return send({ id: assetId, folderId })
} catch (err) {
return sendError(err, 400)
}
},Validate targetParentId before making API calls. Some virtual folder IDs (root, lightbox:*) are not valid move targets.
UI Behavior
On success, the asset disappears from the current folder view and appears in the target folder.