wikilab/wikilab-api

View on GitHub
api/models/docs.js

Summary

Maintainability
F
3 days
Test Coverage
var natural = require('natural');
var Instance = require('../../node_modules/sequelize/lib/instance.js');

module.exports = function(DataTypes) {
  return [{
    UUID: {
      type: DataTypes.UUID,
      allowNull: false,
      defaultValue: DataTypes.UUIDV4,
      validate: {
        isUUID: 4
      }
    },
    title: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        notEmpty: true
      }
    },
    content: {
      type: DataTypes.LONGTEXT,
      allowNull: false
    },
    version: {
      type: DataTypes.INTEGER,
      defaultValue: 1
    },
    current: {
      type: DataTypes.BOOLEAN,
      defaultValue: true
    },
    distance: {
      type: DataTypes.INTEGER,
      defaultValue: 0
    },
    parentUUID: {
      type: DataTypes.UUID,
      validate: {
        isUUID: 4
      }
    },
    order: {
      type: DataTypes.INTEGER
    }
  }, {
    timestamps: true,
    updatedAt: false,
    hooks: {
      beforeUpdate: function *(doc, options) {
        if (options.fields.indexOf('parentUUID') !== -1 && !options.transaction) {
          throw new Error('Updating the parentUUID should within a transaction');
        }
        var intrinsicProperties = ['title', 'content'];
        if (_.intersection(options.fields, intrinsicProperties).length) {
          throw new Error('Can\'t update intrinsic properties');
        }
      }
    },
    classMethods: {
      createWithTransaction: function *(doc) {
        if (!doc.CollectionId) {
          throw new Error('CollectionId is not specified');
        }
        var t = yield sequelize.transaction();
        try {
          if (doc.parentUUID) {
            yield checkParent(doc.parentUUID, doc.CollectionId, t);
          }
          var prevDoc = yield Doc.find({
            where: { UUID: doc.UUID, current: true },
            attributes: ['id', 'content', 'current', 'version', 'title']
          }, {
            transaction: t,
            lock: t.LOCK.UPDATE
          });
          if (prevDoc) {
            doc.distance = natural.LevenshteinDistance(doc.content, prevDoc.content) +
                           natural.LevenshteinDistance(doc.title, prevDoc.title);
            doc.version = prevDoc.version + 1;
            yield prevDoc.updateAttributes({ current: false }, { fields: ['current'], silent: true, transaction: t });
          }
          var createdDoc = yield this.create(doc, { transaction: t });
          t.commit();
          return createdDoc ;
        } catch (err) {
          t.rollback();
          throw err;
        }
      }
    },
    instanceMethods: {
      toJSON: function() {
        var ret = Instance.prototype.toJSON.call(this);
        delete ret.id;
        return ret;
      },
      setParent: function *(parentUUID) {
        var t = yield sequelize.transaction();
        try {
          yield checkParent(parentUUID, this.CollectionId, t);
          yield this.updateAttributes({ parentUUID: parentUUID }, { transaction: t, silent: true });
          t.commit();
        } catch (err) {
          t.rollback();
          throw err;
        }
      },
      setOrder: function *(order) {
        var t = yield sequelize.transaction();
        try {
          var docs = yield Doc.findAll({
            where: { parentUUID: this.parentUUID },
            attributes: ['id', 'order', 'createdAt']
          }, {
            transaction: t,
            lock: t.LOCK.UPDATE
          });
          docs.sort(function(a, b) {
            if (typeof a.order === 'number' && typeof b.order === 'number') {
              return a.order - b.order;
            } else {
              return a.createdAt - b.createdAt;
            }
          });
          var thisInstance, i;
          for (i = 0; i < docs.length; ++i) {
            if (docs[i].id === this.id) {
              thisInstance = docs[i];
              docs.splice(i, 1);
              break;
            }
          }
          docs.splice(order, 0, thisInstance);
          for (i = 0; i < docs.length; ++i) {
            if (docs[i].order !== i) {
              yield docs[i].updateAttributes({ order: i }, { transaction: t, silent: true });
            }
          }
          t.commit();
        } catch (err) {
          t.rollback();
          throw err;
        }
      }
    }
  }];
};

function checkParent(parentUUID, collectionId, t) {
  return Doc.find({
    where: { UUID: parentUUID },
    attributes: ['UUID', 'CollectionId']
  }, {
    transaction: t,
    lock: t.LOCK.UPDATE
  }).then(function(parentDoc) {
    if (!parentDoc) {
      throw new Error('Parent document not exists');
    }
    if (parentDoc.CollectionId !== collectionId) {
      throw new Error('Parent document should belong to the same collection');
    }
  });
}