CI HUBCI HUB SDK
Building an Adapter

Folder Navigation

getFolder returns the contents of a folder — subfolders, assets, filters, and a pagination cursor. CI HUB calls it when the user clicks a folder in the plugin UI, starting with folderId = "root" at the top level.

Inputs

FieldDescription
requestData.query.folderIdFolder ID to open. "root" for top level
requestData.query.sizePage size (max assets per page)
requestData.query.morePagination cursor from previous response
requestData.query.filtersActive filter selections (same format as search)

Response

Same shape as search:

{
  id: string,           // echo back the folderId
  name: string,         // display name
  folders: Folder[],    // subfolders
  assets: Asset[],      // assets in this folder
  filters: Filter[],    // available filters for this folder
  more: number | string | undefined,  // pagination cursor for next page, undefined = last page
  capabilities: FolderCapabilities,   // what operations are allowed
  totalAssetsCount?: number
}

more applies to assets only. Always return all subfolders in a single response (see Subfolder Pagination).

Hardcoded Root Folders

Some adapters define virtual root folders that map to different API endpoints or content types.

Example - boards, collections, and license categories:

getFolder: async (locals, requestData) => {
  try {
    const folderNames = {
      'all': 'All content',
      'downloads': 'Downloaded content',
      'boards': 'Boards',
      'collections': 'Collections',
      'licensed': 'Licensed Assets',
      'videos:creative': 'Creative',
      'videos:editorial': 'Editorial',
      'images:creative': 'Creative',
      'images:editorial': 'Editorial'
    }

    const folderId = requestData.query.folderId || 'root'
    const page = Number.parseInt(requestData.query.more || 0, 10)
    const size = Math.min(Number.parseInt(requestData.query.size || 50, 10), config.get('maxQuerySize'))

    let result = {
      folders: [],
      assets: [],
      more: undefined,
      capabilities: defaultFolderCapabilities
    }

    if (folderId === 'root') {
      result.folders = Object.keys(folderNames)
        .filter(folder => !['videos:creative', 'videos:editorial', 'images:creative', 'images:editorial'].includes(folder))
        .map(folder => ({ id: folder, name: folderNames[folder], uploadUrl: '' }))
    }
    // ... handle each folder type
  }
}

Example - DAM with collections, assets, work-in-progress, projects, and entities:

getFolder: async (locals, requestData) => {
  try {
    let size = Math.min(parseInt(requestData.query.size || 50, 10), config.get('maxQuerySize'))
    const more = parseInt(requestData.query.more || 0, 10) || 0
    const facetQuery = getFilterQuery(requestData.query)
    const folderCapabilities = await getFolderCapabilities(locals)
    const { canAddAsset, canAddFolder, canDeleteFolder } = folderCapabilities

    const result = {
      id: `${requestData.query.folderId}`,
      name: `${requestData.query.folderId}`,
      more: undefined,
      assets: [],
      folders: [],
      filters: [],
      capabilities: { ...defaultFolderCapabilities, canAddAsset },
      totalAssetsCount: 0
    }

    if (requestData.query.folderId === 'root') {
      result.folders = [
        { id: 'collections', name: 'Collections' },
        { id: 'assets', name: 'Assets' },
        { id: 'wip', name: 'Work In Progress' },
        { id: 'projects', name: 'Projects' },
        { id: 'entities', name: 'Entities' }
      ]
    }
    // ... handle each folder type with different API calls and query configs
  }
}

Folder ID Prefixing

When a single adapter exposes multiple content types (libraries, collections, boards), use prefixed IDs to route to the correct API in subsequent getFolder calls.

Example - DAM with libraries and collections:

getFolder: async (locals, requestData) => {
  try {
    const folderId = (requestData.query.folderId as string).toString()
    const size = Math.min(parseInt((requestData.query.size as string) || '25', 10), config.get('maxQuerySize'))
    const more = parseInt((requestData.query.more as string) || '1', 10)
    const { brandId } = locals.adapterData
    const result: any = {
      id: folderId, assets: [], folders: [], filters: [],
      capabilities: { ...defaultFolderCapabilities, ...defaultAssetCapabilities },
      more: undefined, totalAssetsCount: 0
    }

    if (folderId === 'root') {
      const libraries = await callApi(locals, {
        query: queries.getGetBrandLibrariesQuery,
        variables: { brandId, page: more, limit: size }
      })
      // Prefix library IDs so getFolder can route back to the library API
      result.folders = libraries.data.brand.libraries.items.map(
        (library: any) => ({ id: `library:${library.id}`, name: library.name })
      )
      result.more = libraries.data.brand.libraries.hasNextPage ? more + 1 : undefined
    } else if (folderId.startsWith('library:')) {
      const libraryId = folderId.replace('library:', '')
      const assetsRes = await callApi(locals, {
        query: queries.getGetLibraryAssetsQuery,
        variables: { libraryId, page: more, limit: size }
      })
      const { library } = assetsRes.data
      const { browse, collections, currentUserPermissions } = library

      result.assets = extractAssetsData(locals, browse.assets.items)
      result.more = browse.assets.hasNextPage ? more + 1 : undefined
      result.folders = browse.folders.items?.map(
        (folder: any) => ({ id: `${folder.id}`, name: folder.name })
      )
      if (collections.total > 0) {
        result.folders.push({ id: `collections:${libraryId}`, name: 'Collections' })
      }
      result.capabilities.canAddFolder = true
      result.capabilities.canAddAsset = currentUserPermissions.canCreateAssets
    } else if (folderId.startsWith('collections:')) {
      const libraryId = folderId.replace('collections:', '')
      const collections = await callApi(locals, {
        query: queries.getGetLibraryCollectionsQuery,
        variables: { libraryId }
      })
      result.folders = collections.data.library.collections.items.map(
        (collection: any) => ({ id: `collection:${collection.id}`, name: collection.name })
      )
    } else if (folderId.startsWith('collection:')) {
      const collectionId = folderId.replace('collection:', '')
      const assets = await callApi(locals, {
        query: queries.getCollectionAssetsQuery,
        variables: { collectionId, page: more, limit: size }
      })
      const canDeleteAsset = assets.data.node.currentUserPermissions.canRemoveAssets || false
      result.assets = extractAssetsData(locals, assets.data.node.assets.items, { canDeleteAsset })
      result.more = assets.data.node.assets.hasNextPage ? more + 1 : undefined
    }
    // ...
    return send(result)
  }
}

The prefix → API routing pattern: library:abc → Libraries API, collection:xyz → Collections API, board:123 → Boards API.

Example - uses the same approach for boards and collections:

} else if (folderId === 'boards') {
  const boards = await getBoards(system, locals)
  for (const board of boards.boards) {
    result.folders.push({
      id: `board:${board.id}`,
      name: board.name,
      uploadUrl: ''
    })
  }
} else if (folderId === 'collections') {
  const collections = await getCollections(system, locals)
  collections.collections.forEach(collection => {
    result.folders.push({
      id: `collection:${collection.code};${collection.asset_family}`,
      name: collection.name,
      uploadUrl: ''
    })
  })
} else if (folderId.startsWith('board:')) {
  const id = folderId.substring(6)
  const board = await getBoard(system, locals, id)
  if (board.ids.length > 0) {
    result = await getAssets(system, locals, board.ids, undefined, undefined, undefined, `/Boards/${board.name}`, page, size)
  }
} else if (folderId.startsWith('collection:')) {
  const id = folderId.substring(11)
  result = await getAssets(system, locals, undefined, id, undefined, undefined, `/Collections/${id.split(';')[0]}`, page, size)
}

Subfolder Pagination

more in the response only applies to assets. Always fetch all subfolders — use a while-loop if the platform paginates folder listings:

let allFolders = []
let folderPage = 1
let hasMore = true

while (hasMore) {
  const res = await callApi(locals, {
    query: queries.getSubfolders,
    variables: { folderId, page: folderPage, limit: 100 }
  })
  allFolders = allFolders.concat(res.data.folders.items)
  hasMore = res.data.folders.hasNextPage
  folderPage++
}

result.folders = allFolders.map(f => ({ id: f.id, name: f.name }))
result.more = assetsResponse.hasNextPage ? nextAssetCursor : undefined

If the API requests to get all subfolders are costly and slow, you can return all subfolders only in the first getFolder call. in the first getFolder call you will have more = undefined.

example:

getFolder: async (locals, requestData) => {
  try {
    const more = requestData.query.more
    if (!more) {
      // get all subfolders
    }
    return send(result)
  }
}

This works because the CI HUB plugin will merge the results of the first getFolder call with the results of the subsequent getFolder calls.

Per-Folder Capabilities

Spread defaultFolderCapabilities and override per folder type. The defaults are all false:

import { defaultFolderCapabilities, defaultAssetCapabilities } from '@ci-hub/integration-sdk'
// defaultFolderCapabilities = { canDeleteFolder: false, canAddFolder: false, canAddAsset: false }
// defaultAssetCapabilities  = { canDeleteAsset: false, canUpdateAsset: false, canLockAsset: false, canUnlockAsset: false }

Override capabilities based on the folder type and user permissions:

// Read-only folder (boards listing)
result.capabilities = defaultFolderCapabilities

// Writable folder with upload
result.capabilities = {
  ...defaultFolderCapabilities,
  canAddFolder: true,
  canAddAsset: currentUserPermissions.canCreateAssets
}

// Full permissions subfolder
result.capabilities = {
  ...defaultFolderCapabilities,
  canAddFolder: true,
  canAddAsset: true,
  canDeleteFolder: true,
  canDeleteAsset: true,
  canUpdateAsset: true
}

UI Behavior

  • folderId = "root" → CI HUB shows the top-level folder list after login
  • Clicking a folder → calls getFolder with that folder's id
  • folders array → rendered as clickable folder items
  • assets array → rendered as asset thumbnails/list items
  • more defined → "Load More" button appears, clicks send more value back
  • capabilities.canAddAsset = true → upload button appears in folder
  • capabilities.canAddFolder = true → "New Folder" option appears

See Get Folder Schema | Create Folder Schema

On this page