DemocracyOS/democracyos

View on GitHub
migrations/1487112733708-update-topic-clauses.js

Summary

Maintainability
C
1 day
Test Coverage
const jsdom = require('jsdom').jsdom

require('lib/models')()

const Topic = require('lib/models').Topic
const Comment = require('lib/models').Comment
const dbReady = require('lib/models').ready

const mapPromises = (fn) => (array) => Promise.all(array.map(fn))

function guessVersion (topic) {
  const migratedClauses = topic.clauses.filter(function (clause) {
    return !!clause.markup
  })

  // Migrated and unmigrated clauses may coexist. Better catch it early.
  if (migratedClauses.length) {
    return 3
  }

  // Handle the case when a v1 topic has summary but no clauses
  if (topic._doc && topic._doc.summary &&
    topic._doc.summary.toLowerCase().substring(0, 4) !== '<div') {
    return 1
  }

  if (topic.clauses[0] && topic.clauses[0]._doc && topic.clauses[0]._doc.clauseName) {
    // Topic %s is v1 (very old stuff with clauses)
    return 1
  } else if (topic.clauses[0] && topic.clauses[0]._doc && topic._doc.clauses[0]._doc.markup) {
    // Topic %s is v3 (wrote with a rich text editor in DemocracyOS 1.0) or already migrated
    return 3
  } else if (topic._doc && topic._doc.summary) {
    // Topic %s is v2 (wrote with a rich text editor)
    return 2
  } else {
    // Can't guess topic version
    return 3
  }
}

/**
 * Performs a topic migration to v2 supposing it has v1 structure.
 * @param topic The topic mongoose document
 * @param cb A callback function with two params: err and topic, that represents the migrated topic
 * @api private
 */

function migrateV1 (topic) {
  const data = {}

  data.clauses = topic.clauses.map((clause) => ({
    id: clause._id,
    markup: `<div>${clause._doc.text}</div>`,
    position: clause._doc.order,
    empty: false
  }))

  data.clauses.push({
    markup: `<div>${topic._doc && topic._doc.summary}</div>`,
    position: -1,
    empty: false
  })

  return Promise.all([
    Topic.collection.findOneAndUpdate({ _id: topic._id }, {
      $set: {
        clauses: data.clauses
      }
    }),
    mapPromises(function (clause) {
      const context = (clause.position === -1) ? 'summary' : 'clause'

      let reference
      if (context === 'summary') {
        reference = topic._id.toString() + '-0'
      } else {
        reference = clause._id.toString()
      }

      const query = {
        context: context,
        reference: reference
      }

      const data = {
        reference: clause._id,
        context: 'paragraph',
        topicId: topic._id
      }

      return new Promise(function (resolve, reject) {
        Comment.update(query, data, { multi: true }, function (err) {
          if (err) {
            console.log('Error saving comment: ' + err.toString())
          }

          resolve()
        })
      })
    })(topic.clauses)
  ])
}

function getDOM (str) {
  if (!str) return undefined
  var dom = jsdom(str)
  return dom.documentElement
}

/**
 * Performs a topic migration supposing it has v1 structure.
 * @param topic The topic mongoose document
 * @param cb A callback function with two params: err and topic, that represents the migrated topic
 * @api private
 */

function migrateV2 (topic) {
  const html = topic._doc && topic._doc.summary
  const document = getDOM(html)
  if (!document) throw Error('Bad topic _doc')
  const divs = document.getElementsByTagName('div')
  const commentsUpdates = []
  let clauses = topic.clauses

  for (var i in divs) {
    if (divs.hasOwnProperty(i)) {
      const markup = divs[i].outerHTML
      const doc = {
        markup: markup,
        position: i,
        empty: false
      }

      clauses.push(doc)

      // The newly created clause ID
      const clauseId = clauses[clauses.length - 1]._id.toString()

      // Now update its side-comments
      const reference = `${topic._id}-${parseInt(i)}`

      const query = {
        context: 'summary',
        reference: reference
      }

      const data = {
        reference: clauseId,
        context: 'paragraph',
        topicId: topic._id
      }

      commentsUpdates.push(new Promise(function (resolve, reject) {
        Comment.update(query, data, { multi: true }, function (err) {
          if (err) {
            console.log('Error saving comment: ' + err.toString())
          }

          resolve()
        })
      }))
    }
  }

  return Promise.all(commentsUpdates)
    .then(function () {
      return Topic.collection.findOneAndUpdate({ _id: topic._id }, {
        $set: {
          clauses: clauses
        }
      })
    })
}

exports.up = function (done) {
  const clausesVersionsAcc = { 1: 0, 2: 0, 3: 0 }

  dbReady()
    .then(() => Topic.collection
      .find({})
      .toArray()
      .then(mapPromises(function (topic) {
        var versionTopic = guessVersion(topic)
        clausesVersionsAcc[versionTopic]++

        if (versionTopic === 1) {
          return migrateV1(topic)
        } else if (versionTopic === 2) {
          return migrateV2(topic)
        } else {
          return Promise.resolve(0)
        }
      }))
      .then(function (results) {
        console.log('v1: ' + clausesVersionsAcc[1])
        console.log('v2: ' + clausesVersionsAcc[2])
        console.log('v3: ' + clausesVersionsAcc[3])
        console.log('update clauses from ' + results.filter((v) => v).length + ' topics succeded')
        done()
      })
    )
    .catch(function (err) {
      console.log('update topics clauses failed at ', err)
      throw new Error('update topics clauses failed')
    })
}

exports.down = function (done) {
  console.log('update topic clauses down migration is not implemented')
  done()
}