create-contentful-api.js

/**
 * Contentful Delivery API Client. Contains methods which allow access to the
 * different kinds of entities present in Contentful (Entries, Assets, etc).
 * @namespace ContentfulClientAPI
 * @see Entities
 */

/**
 * The different kinds of top level entities you can find in Contentful
 * @namespace Entities
 */

/**
 * System metadata. See <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/common-resource-attributes">Common Resource Attributes</a> for more details.
 * @memberof Entities
 * @typedef Sys
 * @prop {string} type
 * @prop {string} id
 * @prop {Entities.Link} space
 * @prop {string} createdAt
 * @prop {string} updatedAt
 * @prop {number} revision
 */

/**
 * Link to another entity. See <a href="https://www.contentful.com/developers/docs/concepts/links/">Links</a> for more details.
 * @memberof Entities
 * @typedef Link
 * @prop {string} type - type of this entity. Always link.
 * @prop {string} id
 * @prop {string} linkType - type of this link. If defined, either Entry or Asset
 */

/**
 * @memberof ContentfulClientAPI
 * @typedef {Object} ClientAPI
 * @prop {function} getSpace
 * @prop {function} getContentType
 * @prop {function} getTag
 * @prop {function} getTags
 * @prop {function} getContentTypes
 * @prop {function} getEntry
 * @prop {function} getEntries
 * @prop {function} getAsset
 * @prop {function} getAssets
 * @prop {function} createAssetKey
 * @prop {function} parseEntries
 * @prop {function} sync
 */

import { createRequestConfig, errorHandler } from 'contentful-sdk-core'
import entities from './entities'
import pagedSync from './paged-sync'
import normalizeSelect from './utils/normalize-select'
import validateTimestamp from './utils/validate-timestamp'

const ASSET_KEY_MAX_LIFETIME = 48 * 60 * 60

/**
 * Creates API object with methods to access functionality from Contentful's
 * Delivery API
 * @private
 * @param {Object} params - API initialization params
 * @prop {Object} http - HTTP client instance
 * @prop {Object} entities - Object with wrapper methods for each kind of entity
 * @prop {Function} getGlobalOptions - Link resolver preconfigured with global setting
 * @return {ClientAPI}
 */
export default function createContentfulApi ({ http, getGlobalOptions }) {
  const { wrapSpace } = entities.space
  const { wrapContentType, wrapContentTypeCollection } = entities.contentType
  const { wrapEntry, wrapEntryCollection } = entities.entry
  const { wrapAsset, wrapAssetCollection } = entities.asset
  const { wrapTag, wrapTagCollection } = entities.tag
  const { wrapAssetKey } = entities.assetKey
  const { wrapLocaleCollection } = entities.locale
  const notFoundError = (id) => {
    const error = new Error('The resource could not be found.')
    error.sys = {
      type: 'Error',
      id: 'NotFound'
    }
    error.details = {
      type: 'Entry',
      id: id,
      environment: getGlobalOptions().environment,
      space: getGlobalOptions().space
    }
    return error
  }

  /**
   * Gets the Space which the client is currently configured to use
   * @memberof ContentfulClientAPI
   * @return {Promise<Entities.Space>} Promise for a Space
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   * // returns the space object with the above <space-id>
   * const space = await client.getSpace()
   * console.log(space)
   */
  async function getSpace () {
    switchToSpace(http)
    try {
      const response = await http.get('/')
      return wrapSpace(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a Content Type
   * @memberof ContentfulClientAPI
   * @param  {string} id
   * @return {Promise<Entities.ContentType>} Promise for a Content Type
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const contentType = await client.getContentType('<content_type_id>')
   * console.log(contentType)
   */
  async function getContentType (id) {
    switchToEnvironment(http)

    try {
      const response = await http.get(`content_types/${id}`)
      return wrapContentType(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a collection of Content Types
   * @memberof ContentfulClientAPI
   * @param  {Object=} query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
   * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.getContentTypes()
   * console.log(response.items)
   */
  async function getContentTypes (query = {}) {
    switchToEnvironment(http)
    try {
      const response = await http.get('content_types', createRequestConfig({ query: query }))
      return wrapContentTypeCollection(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets an Entry
   * @memberof ContentfulClientAPI
   * @param  {string} id
   * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
   * @return {Promise<Entities.Entry>} Promise for an Entry
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const entry = await client.getEntry('<entry_id>')
   * console.log(entry)
   */
  async function getEntry (id, query = {}) {
    if (!id) {
      throw notFoundError(id)
    }

    try {
      const response = await this.getEntries({ 'sys.id': id, ...query })
      if (response.items.length > 0) {
        return wrapEntry(response.items[0])
      } else {
        throw notFoundError(id)
      }
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a collection of Entries
   * @memberof ContentfulClientAPI
   * @param  {Object=} query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
   * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.getEntries()
   * console.log(response.items)
   */
  async function getEntries (query = {}) {
    switchToEnvironment(http)
    const { resolveLinks, removeUnresolved } = getGlobalOptions(query)
    query = normalizeSelect(query)

    try {
      const response = await http.get('entries', createRequestConfig({ query: query }))
      return wrapEntryCollection(response.data, { resolveLinks, removeUnresolved })
    } catch (error) {
      errorHandler(error)
    }
  }
  /**
   * Gets an Asset
   * @memberof ContentfulClientAPI
   * @param  {string} id
   * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
   * @return {Promise<Entities.Asset>} Promise for an Asset
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const asset = await client.getAsset('<asset_id>')
   * console.log(asset)
   */
  async function getAsset (id, query = {}) {
    switchToEnvironment(http)
    query = normalizeSelect(query)

    try {
      const response = await http.get(`assets/${id}`, createRequestConfig({ query: query }))
      return wrapAsset(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a collection of Assets
   * @memberof ContentfulClientAPI
   * @param  {Object=} query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
   * @return {Promise<Entities.AssetCollection>} Promise for a collection of Assets
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.getAssets()
   * console.log(response.items)
   */
  async function getAssets (query = {}) {
    switchToEnvironment(http)
    query = normalizeSelect(query)

    try {
      const response = await http.get('assets', createRequestConfig({ query: query }))
      return wrapAssetCollection(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a Tag
   * @memberof ContentfulClientAPI
   * @param  {string} id
   * @return {Promise<Entities.Tag>} Promise for a Tag
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const tag = await client.getTag('<asset_id>')
   * console.log(tag)
   */
  async function getTag (id) {
    switchToEnvironment(http)

    try {
      const response = await http.get(`tags/${id}`)
      return wrapTag(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a collection of Tags
   * @memberof ContentfulClientAPI
   * @param  {Object=} query - Object with search parameters.
   * @return {Promise<Entities.TagCollection>} Promise for a collection of Tags
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.getTags()
   * console.log(response.items)
   */
  async function getTags (query = {}) {
    switchToEnvironment(http)
    query = normalizeSelect(query)

    try {
      const response = await http.get('tags', createRequestConfig({ query: query }))
      return wrapTagCollection(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Creates an asset key for signing asset URLs (Embargoed Assets)
   * @memberof ContentfulClientAPI
   * @param {number} expiresAt - UNIX timestamp in the future, maximum of 48h from now
   * @return {Promise<Entities.AssetKey>} Promise for an AssetKey
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const assetKey = await client.getAssetKey(<UNIX timestamp>)
   * console.log(assetKey)
   */
  async function createAssetKey (expiresAt) {
    switchToEnvironment(http)

    try {
      const now = Math.floor(Date.now() / 1000)
      const currentMaxLifetime = now + ASSET_KEY_MAX_LIFETIME
      validateTimestamp('expiresAt', expiresAt, { maximum: currentMaxLifetime, now })

      const params = { expiresAt }
      const response = await http.post('asset_keys', params)
      return wrapAssetKey(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Gets a collection of Locale
   * @memberof ContentfulClientAPI
   * @param  {Object=} query - Object with search parameters. Check the <a href="https://www.contentful.com/developers/docs/javascript/tutorials/using-js-cda-sdk/#retrieving-entries-with-search-parameters">JS SDK tutorial</a> and the <a href="https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters">REST API reference</a> for more details.
   * @return {Promise<Entities.LocaleCollection>} Promise for a collection of Locale
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.getLocales()
   * console.log(response.items)
   */
  async function getLocales (query = {}) {
    switchToEnvironment(http)

    try {
      const response = await http.get('locales', createRequestConfig({ query: query }))
      return wrapLocaleCollection(response.data)
    } catch (error) {
      errorHandler(error)
    }
  }

  /**
   * Synchronizes either all the content or only new content since last sync
   * See <a href="https://www.contentful.com/developers/docs/concepts/sync/">Synchronization</a> for more information.
   * <strong> Important note: </strong> The the sync api endpoint does not support include or link resolution.
   * However contentful.js is doing link resolution client side if you only make an initial sync.
   * For the delta sync (using nextSyncToken) it is not possible since the sdk wont have access to all the data to make such an operation.
   * @memberof ContentfulClientAPI
   * @param  {Object} query - Query object for the sync call. One of initial or nextSyncToken always needs to be specified, but not both.
   * @param  {boolean?} query.initial - Indicates if this is the first sync. Use it if you don't have a sync token.
   * @param  {string?} query.nextSyncToken - The token you got the last time you used this method. Ensures you only get changed content.
   * @param  {string=} query.type - Filter by this type (all (default), Entry, Asset, Deletion, DeletedAsset or DeletedEntry)
   * @param  {string=} query.content_type - Filter by this content type id
   * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
   * @param  {Object} options
   * @param  {boolean=} [options.paginate = true] - Set to false to disable pagination
   * @return {Promise<Sync.SyncCollection>} Promise for the collection resulting of a sync operation
   * @example
   * const contentful = require('contentful')
   *
   * const client = contentful.createClient({
   *   space: '<space_id>',
   *   accessToken: '<content_delivery_api_key>'
   * })
   *
   * const response = await client.sync({
   *   initial: true
   * })
   * console.log({
   *   entries: response.entries,
   *   assets: response.assets,
   *   nextSyncToken: response.nextSyncToken
   * })
   */
  async function sync (query = {}, options = { paginate: true }) {
    const { resolveLinks, removeUnresolved } = getGlobalOptions(query)
    switchToEnvironment(http)
    return pagedSync(http, query, { resolveLinks, removeUnresolved, ...options })
  }

  /**
  * Parse raw json data into collection of entry objects.Links will be resolved also
  * @memberof ContentfulClientAPI
  * @param {Object} raw json data
  * @example
  * let data = {items: [
  *    {
  *    sys: {type: 'Entry', locale: 'en-US'},
  *    fields: {
  *      animal: {sys: {type: 'Link', linkType: 'Animal', id: 'oink'}},
  *      anotheranimal: {sys: {type: 'Link', linkType: 'Animal', id: 'middle-parrot'}}
  *    }
  *  }
  * ],
  * includes: {
  *  Animal: [
  *    {
  *      sys: {type: 'Animal', id: 'oink', locale: 'en-US'},
  *      fields: {
  *        name: 'Pig',
  *        friend: {sys: {type: 'Link', linkType: 'Animal', id: 'groundhog'}}
  *      }
  *    }
  *   ]
  *  }
  * }
  * console.log( data.items[0].fields.foo ); // undefined
  * let parsedData = client.parseEntries(data);
  * console.log( parsedData.items[0].fields.foo ); // foo
  */
  function parseEntries (data) {
    const { resolveLinks, removeUnresolved } = getGlobalOptions({})
    return wrapEntryCollection(data, { resolveLinks, removeUnresolved })
  }

  /*
   * Switches BaseURL to use /environments path
   * */
  function switchToEnvironment (http) {
    http.defaults.baseURL = getGlobalOptions().environmentBaseUrl
  }

  /*
   * Switches BaseURL to use /spaces path
   * */
  function switchToSpace (http) {
    http.defaults.baseURL = getGlobalOptions().spaceBaseUrl
  }

  return {
    getSpace,
    getContentType,
    getContentTypes,
    getEntry,
    getEntries,
    getAsset,
    getAssets,
    getTag,
    getTags,
    createAssetKey,
    getLocales,
    parseEntries,
    sync
  }
}