@tdukart/gatsby-source-cockpit
Version: 
Gatsby source plugin for the Cockpit Headless CMS.
574 lines (497 loc) • 15.5 kB
JavaScript
const mime = require('mime')
const request = require('request-promise')
const slugify = require('slugify')
const hash = require('string-hash')
const {
  METHODS,
  MARKDOWN_IMAGE_REGEXP,
  MARKDOWN_ASSET_REGEXP,
} = require('./constants')
const getFieldsOfTypes = require('./helpers.js').getFieldsOfTypes
module.exports = class CockpitService {
  constructor(
    baseUrl,
    token,
    locales,
    whiteListedCollectionNames = [],
    whiteListedSingletonNames = [],
    aliases = {}
  ) {
    this.baseUrl = baseUrl
    this.token = token
    this.locales = locales
    this.whiteListedCollectionNames = whiteListedCollectionNames
    this.whiteListedSingletonNames = whiteListedSingletonNames
    this.aliases = aliases
  }
  async fetch(endpoint, method, lang = null) {
    return request({
      uri: `${this.baseUrl}/api${endpoint}?token=${this.token}${
        lang ? `&lang=${lang}` : ''
      }`,
      method,
      json: true,
    })
  }
  async validateBaseUrl() {
    try {
      await this.fetch('', METHODS.GET)
    } catch (error) {
      throw new Error(
        'BaseUrl config parameter is invalid or there is no internet connection'
      )
    }
  }
  async validateToken() {
    try {
      await this.fetch('/collections/listCollections', METHODS.GET)
    } catch (error) {
      throw new Error('Token config parameter is invalid')
    }
  }
  async getCollectionNames() {
    return this.fetch('/collections/listCollections', METHODS.GET)
  }
  async getSingletonNames() {
    return this.fetch('/singletons/listSingletons', METHODS.GET)
  }
  async getCollection(name) {
    const {
      fields: collectionFields,
      entries: collectionEntries,
    } = await this.fetch(`/collections/get/${name}`, METHODS.GET)
    const collectionItems = collectionEntries.map(collectionEntry =>
      createCollectionItem(name, collectionFields, collectionEntry)
    )
    for (let index = 0; index < this.locales.length; index++) {
      const {
        fields: collectionFields,
        entries: collectionEntries,
      } = await this.fetch(
        `/collections/get/${name}`,
        METHODS.GET,
        this.locales[index]
      )
      collectionItems.push(
        ...collectionEntries.map(collectionEntry =>
          createCollectionItem(
            name,
            collectionFields,
            collectionEntry,
            this.locales[index]
          )
        )
      )
    }
    const officialName =
      (this.aliases['collection'] && this.aliases['collection'][name]) || name
    return { items: collectionItems, name: officialName }
  }
  async getSingleton(name) {
    const singletonEntry = await this.fetch(
      `/singletons/get/${name}`,
      METHODS.GET
    )
    const singletonDescriptor = await this.fetch(
      `/singletons/singleton/${name}`,
      METHODS.GET
    )
    const singletonItems = [
      createSingletonItem(singletonDescriptor, singletonEntry),
    ]
    for (let index = 0; index < this.locales.length; index++) {
      const singletonEntry = await this.fetch(
        `/singletons/get/${name}`,
        METHODS.GET,
        this.locales[index]
      )
      singletonItems.push(
        createSingletonItem(
          singletonDescriptor,
          singletonEntry,
          this.locales[index]
        )
      )
    }
    const officialName =
      (this.aliases['singleton'] && this.aliases['singleton'][name]) || name
    return { items: singletonItems, name: officialName }
  }
  async getCollections() {
    const names = await this.getCollectionNames()
    const whiteListedNames = this.whiteListedCollectionNames
    return Promise.all(
      names
        .filter(
          name =>
            whiteListedNames === null ||
            (Array.isArray(whiteListedNames) &&
              whiteListedNames.length === 0) ||
            whiteListedNames.includes(name)
        )
        .map(name => this.getCollection(name))
    )
  }
  async getSingletons() {
    const names = await this.getSingletonNames()
    const whiteListedNames = this.whiteListedSingletonNames
    return Promise.all(
      names
        .filter(
          name =>
            whiteListedNames === null ||
            (Array.isArray(whiteListedNames) &&
              whiteListedNames.length === 0) ||
            whiteListedNames.includes(name)
        )
        .map(name => this.getSingleton(name))
    )
  }
  normalizeResources(nodes) {
    const existingImages = {}
    const existingAssets = {}
    const existingMarkdowns = {}
    const existingLayouts = {}
    nodes.forEach(node => {
      node.items.forEach(item => {
        this.normalizeNodeItemImages(item, existingImages)
        this.normalizeNodeItemAssets(item, existingAssets)
        this.normalizeNodeItemMarkdowns(
          item,
          existingImages,
          existingAssets,
          existingMarkdowns
        )
        this.normalizeNodeItemLayouts(
          item,
          existingImages,
          existingAssets,
          existingMarkdowns,
          existingLayouts
        )
      })
    })
    return {
      images: existingImages,
      assets: existingAssets,
      markdowns: existingMarkdowns,
      layouts: existingLayouts,
    }
  }
  normalizeNodeItemImages(item, existingImages) {
    getFieldsOfTypes(item, ['image', 'gallery']).forEach(field => {
      if (!Array.isArray(field.value)) {
        const imageField = field
        let path = imageField.value.path
        if (path == null) {
          return
        }
        if (path.startsWith('/')) {
          path = `${this.baseUrl}${path}`
        } else if (!path.startsWith('http')) {
          path = `${this.baseUrl}/${path}`
        }
        imageField.value = path
        existingImages[path] = null
      } else {
        const galleryField = field
        galleryField.value.forEach(galleryImageField => {
          let path = galleryImageField.path
          if (path == null) {
            return
          }
          trimGalleryImageField(galleryImageField)
          if (path.startsWith('/')) {
            path = `${this.baseUrl}${path}`
          } else if (!path.startsWith('http')) {
            path = `${this.baseUrl}/${path}`
          }
          galleryImageField.value = path
          existingImages[path] = null
        })
      }
    })
    if (Array.isArray(item.children)) {
      item.children.forEach(child => {
        this.normalizeNodeItemImages(child, existingImages)
      })
    }
  }
  normalizeNodeItemAssets(item, existingAssets) {
    getFieldsOfTypes(item, ['asset']).forEach(assetField => {
      let path = assetField.value.path
      trimAssetField(assetField)
      path = `${this.baseUrl}/storage/uploads${path}`
      assetField.value = path
      existingAssets[path] = null
    })
    if (Array.isArray(item.children)) {
      item.children.forEach(child => {
        this.normalizeNodeItemAssets(child, existingAssets)
      })
    }
  }
  normalizeNodeItemMarkdowns(
    item,
    existingImages,
    existingAssets,
    existingMarkdowns
  ) {
    getFieldsOfTypes(item, ['markdown']).forEach(markdownField => {
      existingMarkdowns[markdownField.value] = null
      extractImagesFromMarkdown(markdownField.value, existingImages)
      extractAssetsFromMarkdown(markdownField.value, existingAssets)
    })
    if (Array.isArray(item.children)) {
      item.children.forEach(child => {
        this.normalizeNodeItemMarkdowns(
          child,
          existingImages,
          existingAssets,
          existingMarkdowns
        )
      })
    }
  }
  normalizeNodeItemLayouts(
    item,
    existingImages,
    existingAssets,
    existingMarkdowns,
    existingLayouts
  ) {
    getFieldsOfTypes(item, ['layout', 'layout-grid']).forEach(layoutField => {
      const stringifiedLayout = JSON.stringify(layoutField.value)
      const layoutHash = hash(stringifiedLayout)
      existingLayouts[layoutHash] = layoutField.value
      // TODO: this still needs to be implemented for layout fields
      // extractImagesFromMarkdown(markdownField.value, existingImages)
      // extractAssetsFromMarkdown(markdownField.value, existingAssets)
    })
    if (Array.isArray(item.children)) {
      item.children.forEach(child => {
        this.normalizeNodeItemLayouts(
          child,
          existingImages,
          existingAssets,
          existingMarkdowns,
          existingLayouts
        )
      })
    }
  }
}
const trimAssetField = assetField => {
  delete assetField.value._id
  delete assetField.value.path
  delete assetField.value.title
  delete assetField.value.mime
  delete assetField.value.size
  delete assetField.value.image
  delete assetField.value.video
  delete assetField.value.audio
  delete assetField.value.archive
  delete assetField.value.document
  delete assetField.value.code
  delete assetField.value.created
  delete assetField.value.modified
  delete assetField.value._by
  Object.keys(assetField.value).forEach(attribute => {
    assetField[attribute] = assetField.value[attribute]
    delete assetField.value[attribute]
  })
}
const trimGalleryImageField = galleryImageField => {
  galleryImageField.type = 'image'
  delete galleryImageField.meta.asset
  delete galleryImageField.path
}
const createCollectionItem = (
  collectionName,
  collectionFields,
  collectionEntry,
  locale = null,
  level = 1
) => {
  const item = {
    cockpitId: collectionEntry._id,
    cockpitCreated: new Date(collectionEntry._created * 1000),
    cockpitModified: new Date(collectionEntry._modified * 1000),
    // TODO: Replace with Users... once implemented (GitHub Issue #15)
    cockpitBy: collectionEntry._by,
    cockpitModifiedBy: collectionEntry._mby,
    lang: locale == null ? 'any' : locale,
    level: level,
  }
  Object.keys(collectionFields).reduce((accumulator, collectionFieldName) => {
    const collectionFieldValue = collectionEntry[collectionFieldName]
    const collectionFieldConfiguration = collectionFields[collectionFieldName]
    const collectionFieldSlug = collectionEntry[`${collectionFieldName}_slug`]
    const field = createNodeField(
      'collection',
      collectionName,
      collectionFieldValue,
      collectionFieldConfiguration,
      collectionFieldSlug
    )
    if (field !== null) {
      accumulator[collectionFieldName] = field
    }
    return accumulator
  }, item)
  if (collectionEntry.hasOwnProperty('children')) {
    item.children = collectionEntry.children.map(childEntry => {
      return createCollectionItem(
        collectionName,
        collectionFields,
        childEntry,
        locale,
        level + 1
      )
    })
  }
  return item
}
const createSingletonItem = (
  singletonDescriptor,
  singletonEntry,
  locale = null
) => {
  const item = {
    cockpitId: singletonDescriptor._id,
    cockpitCreated: new Date(singletonDescriptor._created * 1000),
    cockpitModified: new Date(singletonDescriptor._modified * 1000),
    // TODO: Replace with Users... once implemented (GitHub Issue #15)
    cockpitBy: singletonEntry._by,
    cockpitModifiedBy: singletonEntry._mby,
    lang: locale == null ? 'any' : locale,
  }
  singletonDescriptor.fields.reduce(
    (accumulator, singletonFieldConfiguration) => {
      const singletonFieldValue =
        singletonEntry[singletonFieldConfiguration.name]
      const singletonFieldSlug =
        singletonEntry[`${singletonFieldConfiguration.name}_slug`]
      const field = createNodeField(
        'singleton',
        singletonDescriptor.name,
        singletonFieldValue,
        singletonFieldConfiguration,
        singletonFieldSlug
      )
      if (field !== null) {
        accumulator[singletonFieldConfiguration.name] = field
      }
      return accumulator
    },
    item
  )
  return item
}
const createNodeField = (
  nodeType,
  nodeName,
  nodeFieldValue,
  nodeFieldConfiguration,
  nodeFieldSlug
) => {
  const nodeFieldType = nodeFieldConfiguration.type
  if (
    !(Array.isArray(nodeFieldValue) && nodeFieldValue.length === 0) &&
    nodeFieldValue != null &&
    nodeFieldValue !== ''
  ) {
    const itemField = {
      type: nodeFieldType,
    }
    if (nodeFieldType === 'repeater') {
      const repeaterFieldOptions = nodeFieldConfiguration.options || {}
      if (typeof repeaterFieldOptions.field !== 'undefined') {
        itemField.value = nodeFieldValue.map(repeaterEntry =>
          createNodeField(
            nodeType,
            nodeName,
            repeaterEntry.value,
            repeaterFieldOptions.field
          )
        )
      } else if (typeof repeaterFieldOptions.fields !== 'undefined') {
        itemField.value = nodeFieldValue.map(repeaterEntry =>
          repeaterFieldOptions.fields.reduce(
            (accumulator, currentFieldConfiguration) => {
              if (
                typeof currentFieldConfiguration.name === 'undefined' &&
                currentFieldConfiguration.label === repeaterEntry.field.label
              ) {
                const generatedNameProperty = slugify(
                  currentFieldConfiguration.label,
                  { lower: true }
                )
                console.warn(
                  `\nRepeater field without 'name' attribute used in ${nodeType} '${nodeName}'. ` +
                    `Using value '${generatedNameProperty}' for name (generated from the label).`
                )
                currentFieldConfiguration.name = generatedNameProperty
                repeaterEntry.field.name = generatedNameProperty
              }
              if (currentFieldConfiguration.name === repeaterEntry.field.name) {
                accumulator.valueType = currentFieldConfiguration.name
                accumulator.value[
                  currentFieldConfiguration.name
                ] = createNodeField(
                  nodeType,
                  nodeName,
                  repeaterEntry.value,
                  currentFieldConfiguration
                )
              }
              return accumulator
            },
            { type: 'set', value: {} }
          )
        )
      }
    } else if (nodeFieldType === 'set') {
      const setFieldOptions = nodeFieldConfiguration.options || {}
      itemField.value = setFieldOptions.fields.reduce(
        (accumulator, currentFieldConfiguration) => {
          const currentFieldName = currentFieldConfiguration.name
          accumulator[currentFieldName] = createNodeField(
            nodeType,
            nodeName,
            nodeFieldValue[currentFieldName],
            currentFieldConfiguration
          )
          return accumulator
        },
        {}
      )
    } else {
      itemField.value = nodeFieldValue
      if (nodeFieldSlug) {
        itemField.slug = nodeFieldSlug
      }
    }
    return itemField
  }
  return null
}
const extractImagesFromMarkdown = (markdown, existingImages) => {
  let unparsedMarkdown = markdown
  let match
  while ((match = MARKDOWN_IMAGE_REGEXP.exec(unparsedMarkdown))) {
    unparsedMarkdown = unparsedMarkdown.substring(match.index + match[0].length)
    existingImages[match[1]] = null
  }
}
const extractAssetsFromMarkdown = (markdown, existingAssets) => {
  let unparsedMarkdown = markdown
  let match
  while ((match = MARKDOWN_ASSET_REGEXP.exec(unparsedMarkdown))) {
    unparsedMarkdown = unparsedMarkdown.substring(match.index + match[0].length)
    const mediaType = mime.getType(match[1])
    if (mediaType && mediaType !== 'text/html') {
      existingAssets[match[1]] = null
    }
  }
}