Add support of saving authors and authorship

This commit is contained in:
Wu Cheng-Han 2016-07-30 11:21:38 +08:00
parent 44fd0a617b
commit 2f117a22cd
6 changed files with 240 additions and 3 deletions

View file

@ -0,0 +1,28 @@
'use strict';
module.exports = {
up: function (queryInterface, Sequelize) {
queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT);
queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT);
queryInterface.createTable('Authors', {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
color: Sequelize.STRING,
noteId: Sequelize.UUID,
userId: Sequelize.UUID,
createdAt: Sequelize.DATE,
updatedAt: Sequelize.DATE
});
return;
},
down: function (queryInterface, Sequelize) {
queryInterface.dropTable('Authors');
queryInterface.removeColumn('Revisions', 'authorship');
queryInterface.removeColumn('Notes', 'authorship');
return;
}
};

43
lib/models/author.js Normal file
View file

@ -0,0 +1,43 @@
"use strict";
// external modules
var Sequelize = require("sequelize");
// core
var logger = require("../logger.js");
module.exports = function (sequelize, DataTypes) {
var Author = sequelize.define("Author", {
id: {
type: Sequelize.INTEGER,
primaryKey: true,
autoIncrement: true
},
color: {
type: DataTypes.STRING
}
}, {
indexes: [
{
unique: true,
fields: ['noteId', 'userId']
}
],
classMethods: {
associate: function (models) {
Author.belongsTo(models.Note, {
foreignKey: "noteId",
as: "note",
constraints: false
});
Author.belongsTo(models.User, {
foreignKey: "userId",
as: "user",
constraints: false
});
}
}
});
return Author;
};

View file

@ -51,6 +51,9 @@ module.exports = function (sequelize, DataTypes) {
content: { content: {
type: DataTypes.TEXT type: DataTypes.TEXT
}, },
authorship: {
type: DataTypes.TEXT
},
lastchangeAt: { lastchangeAt: {
type: DataTypes.DATE type: DataTypes.DATE
}, },
@ -74,6 +77,11 @@ module.exports = function (sequelize, DataTypes) {
foreignKey: "noteId", foreignKey: "noteId",
constraints: false constraints: false
}); });
Note.hasMany(models.Author, {
foreignKey: "noteId",
as: "authors",
constraints: false
});
}, },
checkFileExist: function (filePath) { checkFileExist: function (filePath) {
try { try {

View file

@ -30,6 +30,9 @@ module.exports = function (sequelize, DataTypes) {
}, },
length: { length: {
type: DataTypes.INTEGER type: DataTypes.INTEGER
},
authorship: {
type: DataTypes.TEXT
} }
}, { }, {
classMethods: { classMethods: {

View file

@ -10,7 +10,7 @@ var util = require('util');
var LZString = require('lz-string'); var LZString = require('lz-string');
var logger = require('../logger'); var logger = require('../logger');
function EditorSocketIOServer(document, operations, docId, mayWrite) { function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
EventEmitter.call(this); EventEmitter.call(this);
Server.call(this, document, operations); Server.call(this, document, operations);
this.users = {}; this.users = {};
@ -18,6 +18,7 @@ function EditorSocketIOServer(document, operations, docId, mayWrite) {
this.mayWrite = mayWrite || function (_, cb) { this.mayWrite = mayWrite || function (_, cb) {
cb(true); cb(true);
}; };
this.operationCallback = operationCallback;
} }
util.inherits(EditorSocketIOServer, Server); util.inherits(EditorSocketIOServer, Server);
@ -51,6 +52,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
} }
try { try {
self.onOperation(socket, revision, operation, selection); self.onOperation(socket, revision, operation, selection);
if (typeof self.operationCallback === 'function')
self.operationCallback(socket, operation);
} catch (err) { } catch (err) {
socket.disconnect(true); socket.disconnect(true);
} }

View file

@ -151,6 +151,7 @@ function finishUpdateNote(note, _note, callback) {
var values = { var values = {
title: title, title: title,
content: body, content: body,
authorship: LZString.compressToBase64(JSON.stringify(note.authorship)),
lastchangeuserId: note.lastchangeuser, lastchangeuserId: note.lastchangeuser,
lastchangeAt: Date.now() lastchangeAt: Date.now()
}; };
@ -404,6 +405,13 @@ function startConnection(socket) {
}, { }, {
model: models.User, model: models.User,
as: "lastchangeuser" as: "lastchangeuser"
}, {
model: models.Author,
as: "authors",
include: [{
model: models.User,
as: "user"
}]
}]; }];
models.Note.findOne({ models.Note.findOne({
@ -424,7 +432,19 @@ function startConnection(socket) {
var body = LZString.decompressFromBase64(note.content); var body = LZString.decompressFromBase64(note.content);
var createtime = note.createdAt; var createtime = note.createdAt;
var updatetime = note.lastchangeAt; var updatetime = note.lastchangeAt;
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit); var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
var authors = {};
for (var i = 0; i < note.authors.length; i++) {
var author = note.authors[i];
var profile = models.User.parseProfile(author.user.profile);
authors[author.userId] = {
userid: author.userId,
color: author.color,
photo: profile.photo,
name: profile.name
};
}
notes[noteId] = { notes[noteId] = {
id: noteId, id: noteId,
@ -437,7 +457,9 @@ function startConnection(socket) {
users: {}, users: {},
createtime: moment(createtime).valueOf(), createtime: moment(createtime).valueOf(),
updatetime: moment(updatetime).valueOf(), updatetime: moment(updatetime).valueOf(),
server: server server: server,
authors: authors,
authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []
}; };
return finishConnection(socket, notes[noteId], users[socket.id]); return finishConnection(socket, notes[noteId], users[socket.id]);
@ -581,6 +603,136 @@ function ifMayEdit(socket, callback) {
return callback(mayEdit); return callback(mayEdit);
} }
function operationCallback(socket, operation) {
var noteId = socket.noteId;
if (!noteId || !notes[noteId]) return;
var note = notes[noteId];
var userId = null;
// save authors
if (socket.request.user && socket.request.user.logged_in) {
var socketId = socket.id;
var user = users[socketId];
userId = socket.request.user.id;
if (!note.authors[userId]) {
models.Author.create({
noteId: noteId,
userId: userId,
color: users[socketId].color
}).then(function (author) {
note.authors[author.userId] = {
userid: author.userId,
color: author.color,
photo: user.photo,
name: user.name
};
}).catch(function (err) {
return logger.error('operation callback failed: ' + err);
});
}
}
// save authorship
var index = 0;
var authorships = note.authorship;
var timestamp = Date.now();
for (var i = 0; i < operation.length; i++) {
var op = operation[i];
if (ot.TextOperation.isRetain(op)) {
index += op;
} else if (ot.TextOperation.isInsert(op)) {
var opStart = index;
var opEnd = index + op.length;
var inserted = false;
// authorship format: [userId, startPos, endPos, createdAt, updatedAt]
if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
else {
for (var j = 0; j < authorships.length; j++) {
var authorship = authorships[j];
if (!inserted) {
var nextAuthorship = authorships[j + 1] || -1;
if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
if (authorship[1] < opStart && authorship[2] > opStart) {
// divide
var postLength = authorship[2] - opStart;
authorship[2] = opStart;
authorship[4] = timestamp;
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
j += 2;
inserted = true;
} else if (authorship[1] >= opStart) {
authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
j += 1;
inserted = true;
} else if (authorship[2] <= opStart) {
authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
j += 1;
inserted = true;
}
}
}
if (authorship[1] >= opStart) {
authorship[1] += op.length;
authorship[2] += op.length;
}
}
}
index += op.length;
} else if (ot.TextOperation.isDelete(op)) {
var opStart = index;
var opEnd = index - op;
if (operation.length == 1) {
authorships = [];
} else if (authorships.length > 0) {
for (var j = 0; j < authorships.length; j++) {
var authorship = authorships[j];
if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
authorships.splice(j, 1);
j -= 1;
} else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
authorship[2] += op;
authorship[4] = timestamp;
} else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
authorship[2] = opStart;
authorship[4] = timestamp;
} else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
authorship[1] = opEnd;
authorship[4] = timestamp;
}
if (authorship[1] >= opEnd) {
authorship[1] += op;
authorship[2] += op;
}
}
}
index += op;
}
}
// merge
for (var j = 0; j < authorships.length; j++) {
var authorship = authorships[j];
for (var k = j + 1; k < authorships.length; k++) {
var nextAuthorship = authorships[k];
if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
authorships.splice(k, 1);
j -= 1;
break;
}
}
}
// clear
for (var j = 0; j < authorships.length; j++) {
var authorship = authorships[j];
if (!authorship[0]) {
authorships.splice(j, 1);
j -= 1;
}
}
note.authorship = authorships;
}
function connection(socket) { function connection(socket) {
if (config.maintenance) return; if (config.maintenance) return;
parseNoteIdFromSocket(socket, function (err, noteId) { parseNoteIdFromSocket(socket, function (err, noteId) {