/**
* 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
}
}