text: const _ = require(`lodash`)
  const invariant = require(`invariant`)
  const { getDb, colls } = require(`./index`)
  import { createPageDependency } from "../../redux/actions/add-page-dependency"
  
  // ///////////////////////////////////////////////////////////////////
  // Node collection metadata
  // ///////////////////////////////////////////////////////////////////
  
  function makeTypeCollName(type) {
    return `gatsby:nodeType:${type}`
  }
  
  /**
   * Creates a collection that will contain nodes of a certain type. The
   * name of the collection for type `MyType` will be something like
   * `gatsby:nodeType:MyType` (see `makeTypeCollName`)
   */
  function createNodeTypeCollection(type) {
    const collName = makeTypeCollName(type)
    const nodeTypesColl = getDb().getCollection(colls.nodeTypes.name)
    invariant(nodeTypesColl, `Collection ${colls.nodeTypes.name} should exist`)
    nodeTypesColl.insert({ type, collName })
    // TODO what if `addCollection` fails? We will have inserted into
    // nodeTypesColl but no collection will exist. Need to make this
    // into a transaction
    const options = {
      unique: [`id`],
      indices: [`id`, `internal.counter`],
      disableMeta: true,
    }
    const coll = getDb().addCollection(collName, options)
    return coll
  }
  
  /**
   * Returns the name of the collection that contains nodes of the
   * specified type, where type is the node's `node.internal.type`
   */
  function getTypeCollName(type) {
    const nodeTypesColl = getDb().getCollection(colls.nodeTypes.name)
    invariant(nodeTypesColl, `Collection ${colls.nodeTypes.name} should exist`)
    const nodeTypeInfo = nodeTypesColl.by(`type`, type)
    return nodeTypeInfo ? nodeTypeInfo.collName : undefined
  }
  
  /**
   * Returns a reference to the collection that contains nodes of the
   * specified type, where type is the node's `node.internal.type`
   */
  function getNodeTypeCollection(type) {
    const typeCollName = getTypeCollName(type)
    let coll
    if (typeCollName) {
      coll = getDb().getCollection(typeCollName)
      invariant(
        coll,
        `Type [${type}] Collection doesn't exist for nodeTypeInfo: [${typeCollName}]`
      )
      return coll
    } else {
      return undefined
    }
  }
  
  /**
   * Deletes all empty node type collections, unless `force` is true, in
   * which case it deletes the collections even if they have nodes in
   * them
   */
  function deleteNodeTypeCollections(force = false) {
    const nodeTypesColl = getDb().getCollection(colls.nodeTypes.name)
    // find() returns all objects in collection
    const nodeTypes = nodeTypesColl.find()
    for (const nodeType of nodeTypes) {
      const coll = getDb().getCollection(nodeType.collName)
      if (coll.count() === 0 || force) {
        getDb().removeCollection(coll.name)
        nodeTypesColl.remove(nodeType)
      }
    }
  }
  
  /**
   * Deletes all nodes from all the node type collections, including the
   * id -> type metadata. There will be no nodes related data in loki
   * after this is called
   */
  function deleteAll() {
    const db = getDb()
    if (db) {
      deleteNodeTypeCollections(true)
      db.getCollection(colls.nodeMeta.name).clear()
    }
  }
  
  // ///////////////////////////////////////////////////////////////////
  // Queries
  // ///////////////////////////////////////////////////////////////////
  
  /**
   * Returns the node with `id` == id, or null if not found
   */
  function getNode(id) {
    if (!id) {
      return null
    }
    // First, find out which collection the node is in
    const nodeMetaColl = getDb().getCollection(colls.nodeMeta.name)
    invariant(nodeMetaColl, `nodeMeta collection should exist`)
    const nodeMeta = nodeMetaColl.by(`id`, id)
    if (nodeMeta) {
      // Now get the collection and query it by the `id` field, which
      // has an index on it
      const { typeCollName } = nodeMeta
      const typeColl = getDb().getCollection(typeCollName)
      invariant(
        typeColl,
        `type collection ${typeCollName} referenced by nodeMeta but doesn't exist`
      )
      return typeColl.by(`id`, id)
    } else {
      return undefined
    }
  }
  
  /**
   * Returns all nodes of a type (where `typeName ==
   * node.internal.type`). This is an O(1) operation since nodes are
   * already stored in separate collections by type
   */
  function getNodesByType(typeName) {
    invariant(typeName, `typeName is null`)
    const collName = getTypeCollName(typeName)
    const coll = getDb().getCollection(collName)
    if (!coll) return []
    return coll
      .chain()
      .simplesort(`internal.counter`)
      .data()
  }
  
  /**
   * Returns the collection of all nodes. This should be deprecated and
   * `getNodesByType` should be used instead. Or at least where possible
   */
  function getNodes() {
    const nodeTypes = getTypes()
    return _.flatMap(nodeTypes, nodeType => getNodesByType(nodeType))
  }
  
  /**
   * Returns the unique collection of all node types
   */
  function getTypes() {
    const nodeTypes = getDb().getCollection(colls.nodeTypes.name).data
    return nodeTypes.map(nodeType => nodeType.type)
  }
  
  /**
   * Looks up the node by id, records a dependency between the node and
   * the path, and then returns the node
   *
   * @param {string} id node id to lookup
   * @param {string} path the page path to record a node dependency
   * against
   * @returns {Object} node or undefined if not found
   */
  function getNodeAndSavePathDependency(id, path) {
    invariant(id, `id is null`)
    invariant(id, `path is null`)
    const node = getNode(id)
    createPageDependency({ path, nodeId: id })
    return node
  }
  
  /**
   * Determine if node has changed (by comparing its
   * `internal.contentDigest`
   *
   * @param {string} id
   * @param {string} digest
   * @returns {boolean}
   */
  function hasNodeChanged(id, digest) {
    const node = getNode(id)
    if (!node) {
      return true
    } else {
      return node.internal.contentDigest !== digest
    }
  }
  
  // /////////////////////////////////////////////////////////////////
  // Create/Update/Delete
  // ///////////////////////////////////////////////////////////////////
  
  /**
   * Creates a node in the DB. Will create a collection for the node
   * type if one hasn't been created yet
   *
   * @param {Object} node The node to add. Must have an `id` and
   * `internal.type`
   */
  function createNode(node, oldNode) {
    invariant(node.internal, `node has no "internal" field`)
    invariant(node.internal.type, `node has no "internal.type" field`)
    invariant(node.id, `node has no "id" field`)
  
    const type = node.internal.type
  
    // Loki doesn't provide "upsert", so if the node already exists, we
    // delete and then create it
    if (oldNode) {
      deleteNode(oldNode)
    }
  
    let nodeTypeColl = getNodeTypeCollection(type)
    if (!nodeTypeColl) {
      nodeTypeColl = createNodeTypeCollection(type)
    }
  
    const nodeMetaColl = getDb().getCollection(colls.nodeMeta.name)
    invariant(nodeMetaColl, `Collection ${colls.nodeMeta.name} should exist`)
    nodeMetaColl.insert({ id: node.id, typeCollName: nodeTypeColl.name })
    // TODO what if this insert fails? We will have inserted the id ->
    // collName mapping, but there won't be any nodes in the type
    // collection. Need to create a transaction around this
    return nodeTypeColl.insert(node)
  }
  
  /**
   * Updates a node in the DB. The contents of `node` will completely
   * overwrite value in the DB. Note, `node` must be a loki node. i.e it
   * has `$loki` and `meta` fields.
   *
   * @param {Object} node The new node information. This should be all
   * the node information. Not just changes
   */
  function updateNode(node) {
    invariant(node.internal, `node has no "internal" field`)
    invariant(node.internal.type, `node has no "internal.type" field`)
    invariant(node.id, `node has no "id" field`)
  
    const oldNode = getNode(node.id)
    return createNode(node, oldNode)
  }
  
  /**
   * Deletes a node from its type collection and removes its id ->
   * collName mapping. Function is idempotent. If the node has already
   * been deleted, this is a noop.
   *
   * @param {Object} the node to delete. Must have an `id` and
   * `internal.type`
   */
  function deleteNode(node) {
    invariant(node.internal, `node has no "internal" field`)
    invariant(node.internal.type, `node has no "internal.type" field`)
    invariant(node.id, `node has no "id" field`)
  
    const type = node.internal.type
  
    const nodeTypeColl = getNodeTypeCollection(type)
    if (!nodeTypeColl) {
      invariant(
        nodeTypeColl,
        `${type} collection doesn't exist. When trying to delete`
      )
    }
  
    const obj = nodeTypeColl.by(`id`, node.id)
    if (obj) {
      const nodeMetaColl = getDb().getCollection(colls.nodeMeta.name)
      invariant(nodeMetaColl, `Collection ${colls.nodeMeta.name} should exist`)
      nodeMetaColl.findAndRemove({ id: node.id })
      // TODO What if this `remove()` fails? We will have removed the id
      // -> collName mapping, but not the actual node in the
      // collection. Need to make this into a transaction
      nodeTypeColl.remove(obj)
    }
    // idempotent. Do nothing if node wasn't already in DB
  }
  
  /**
   * deprecated
   */
  function deleteNodes(nodes) {
    for (const node of nodes) {
      deleteNode(node)
    }
  }
  
  const saveResolvedNodes = async (nodeTypeNames, resolver) => {
    for (const typeName of nodeTypeNames) {
      const nodes = getNodesByType(typeName)
      const resolved = await Promise.all(
        nodes.map(async node => {
          node.__gatsby_resolved = await resolver(node)
          return node
        })
      )
      const nodeColl = getNodeTypeCollection(typeName)
      if (nodeColl) {
        nodeColl.update(resolved)
      }
    }
  }
  
  // Cleared on DELETE_CACHE
  let fieldUsages = null
  const FIELD_INDEX_THRESHOLD = 5
  
  // Every time we run a query, we increment a counter for each of its
  // fields, so that we can determine which fields are used the
  // most. Any time a field is seen more than `FIELD_INDEX_THRESHOLD`
  // times, we create a loki index so that future queries with that
  // field will execute faster.
  // times, we create a loki index so that future queries with that
  // field will execute faster.
  function ensureFieldIndexes(typeName, lokiArgs, sortArgs) {
    if (fieldUsages == null) {
      fieldUsages = {}
      const { emitter } = require(`../../redux`)
  
      emitter.on(`DELETE_CACHE`, () => {
        for (const field in fieldUsages) {
          delete fieldUsages[field]
        }
      })
    }
  
    const nodeColl = getNodeTypeCollection(typeName)
  
    if (!fieldUsages[typeName]) {
      fieldUsages[typeName] = {}
    }
  
    _.forEach(lokiArgs, (v, fieldName) => {
      // Increment the usages of the field
      _.update(fieldUsages[typeName], fieldName, n => (n ? n + 1 : 1))
      // If we have crossed the threshold, then create the index
      if (_.get(fieldUsages[typeName], fieldName) >= FIELD_INDEX_THRESHOLD) {
        // Loki ensures that this is a noop if index already exists. E.g
        // if it was previously added via a sort field
        nodeColl.ensureIndex(fieldName)
      }
    })
  }
  
  // ///////////////////////////////////////////////////////////////////
  // Reducer
  // ///////////////////////////////////////////////////////////////////
  
  function reducer(state = new Map(), action) {
    switch (action.type) {
      case `DELETE_CACHE`:
        deleteAll()
        return null
  
      case `CREATE_NODE`: {
        createNode(action.payload, action.oldNode)
        return null
      }
  
      case `ADD_FIELD_TO_NODE`:
      case `ADD_CHILD_NODE_TO_PARENT_NODE`:
        updateNode(action.payload)
        return null
  
      case `DELETE_NODE`: {
        if (action.payload) {
          deleteNode(action.payload)
        }
        return null
      }
  
      case `DELETE_NODES`: {
        deleteNodes(action.fullNodes)
        return null
      }
  
      default:
        return null
    }
  }
  
  // ///////////////////////////////////////////////////////////////////
  // Exports
  // ///////////////////////////////////////////////////////////////////
  
  module.exports = {
    getNodeTypeCollection,
  
    getNodes,
    getNode,
    getNodesByType,
    getTypes,
    hasNodeChanged,
    getNodeAndSavePathDependency,
  
    createNode,
    updateNode,
    deleteNode,
  
    deleteNodeTypeCollections,
    deleteAll,
  
    reducer,
  
    saveResolvedNodes,
    ensureFieldIndexes,
  }
  