From 49b51e478fa75b8d5254662de3265edcf8906004 Mon Sep 17 00:00:00 2001 From: Cheng-Han, Wu Date: Wed, 20 Apr 2016 18:03:55 +0800 Subject: Refactor server with Sequelize ORM, refactor server configs, now will show note status (created or updated) and support docs (note alias) --- lib/models/index.js | 37 ++++++++++ lib/models/note.js | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/models/temp.js | 19 +++++ lib/models/user.js | 77 +++++++++++++++++++ 4 files changed, 341 insertions(+) create mode 100644 lib/models/index.js create mode 100644 lib/models/note.js create mode 100644 lib/models/temp.js create mode 100644 lib/models/user.js (limited to 'lib/models') diff --git a/lib/models/index.js b/lib/models/index.js new file mode 100644 index 00000000..3b49d459 --- /dev/null +++ b/lib/models/index.js @@ -0,0 +1,37 @@ +"use strict"; + +// external modules +var fs = require("fs"); +var path = require("path"); +var Sequelize = require("sequelize"); + +// core +var config = require('../config.js'); +var logger = require("../logger.js"); + +var dbconfig = config.db; +dbconfig.logging = config.debug ? logger.info : false; +var sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig); + +var db = {}; + +fs + .readdirSync(__dirname) + .filter(function (file) { + return (file.indexOf(".") !== 0) && (file !== "index.js"); + }) + .forEach(function (file) { + var model = sequelize.import(path.join(__dirname, file)); + db[model.name] = model; + }); + +Object.keys(db).forEach(function (modelName) { + if ("associate" in db[modelName]) { + db[modelName].associate(db); + } +}); + +db.sequelize = sequelize; +db.Sequelize = Sequelize; + +module.exports = db; \ No newline at end of file diff --git a/lib/models/note.js b/lib/models/note.js new file mode 100644 index 00000000..96043b75 --- /dev/null +++ b/lib/models/note.js @@ -0,0 +1,208 @@ +"use strict"; + +// external modules +var fs = require('fs'); +var path = require('path'); +var LZString = require('lz-string'); +var marked = require('marked'); +var cheerio = require('cheerio'); +var shortId = require('shortid'); +var Sequelize = require("sequelize"); +var async = require('async'); + +// core +var config = require("../config.js"); +var logger = require("../logger.js"); + +// permission types +var permissionTypes = ["freely", "editable", "locked", "private"]; + +module.exports = function (sequelize, DataTypes) { + var Note = sequelize.define("Note", { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + shortid: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + defaultValue: shortId.generate + }, + alias: { + type: DataTypes.STRING, + unique: true + }, + permission: { + type: DataTypes.ENUM, + values: permissionTypes + }, + viewcount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + title: { + type: DataTypes.TEXT + }, + content: { + type: DataTypes.TEXT + }, + lastchangeAt: { + type: DataTypes.DATE + } + }, { + classMethods: { + associate: function (models) { + Note.belongsTo(models.User, { + foreignKey: "ownerId", + as: "owner", + constraints: false + }); + Note.belongsTo(models.User, { + foreignKey: "lastchangeuserId", + as: "lastchangeuser", + constraints: false + }); + }, + checkFileExist: function (filePath) { + try { + return fs.statSync(filePath).isFile(); + } catch (err) { + return false; + } + }, + checkNoteIdValid: function (id) { + var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + var result = id.match(uuidRegex); + if (result && result.length == 1) + return true; + else + return false; + }, + parseNoteId: function (noteId, callback) { + async.series({ + parseNoteIdByAlias: function (_callback) { + // try to parse note id by alias (e.g. doc) + Note.findOne({ + where: { + alias: noteId + } + }).then(function (note) { + if (note) { + return callback(null, note.id); + } else { + var filePath = path.join(config.docspath, noteId + '.md'); + if (Note.checkFileExist(filePath)) { + Note.create({ + alias: noteId, + owner: null, + permission: 'locked' + }).then(function (note) { + return callback(null, note.id); + }).catch(function (err) { + return _callback(err, null); + }); + } else { + return _callback(null, null); + } + } + }).catch(function (err) { + return _callback(err, null); + }); + }, + parseNoteIdByLZString: function (_callback) { + // try to parse note id by LZString Base64 + try { + var id = LZString.decompressFromBase64(noteId); + if (id && Note.checkNoteIdValid(id)) + return callback(null, id); + else + return _callback(null, null); + } catch (err) { + return _callback(err, null); + } + }, + parseNoteIdByShortId: function (_callback) { + // try to parse note id by shortId + try { + if (shortId.isValid(noteId)) { + Note.findOne({ + where: { + shortid: noteId + } + }).then(function (note) { + if (!note) return _callback(null, null); + return callback(null, note.id); + }).catch(function (err) { + return _callback(err, null); + }); + } else { + return _callback(null, null); + } + } catch (err) { + return _callback(err, null); + } + } + }, function (err, result) { + if (err) { + logger.error(err); + return callback(err, null); + } + return callback(null, null); + }); + }, + parseNoteTitle: function (body) { + var $ = cheerio.load(marked(body)); + var h1s = $("h1"); + var title = ""; + if (h1s.length > 0 && h1s.first().text().split('\n').length == 1) + title = h1s.first().text(); + else + title = "Untitled"; + return title; + }, + decodeTitle: function (title) { + var decodedTitle = LZString.decompressFromBase64(title); + if (decodedTitle) title = decodedTitle; + else title = 'Untitled'; + return title; + }, + generateWebTitle: function (title) { + title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD"; + return title; + } + }, + hooks: { + beforeCreate: function (note, options, callback) { + // if no content specified then use default note + if (!note.content) { + var body = null; + var filePath = null; + if (!note.alias) { + filePath = config.defaultnotepath; + } else { + filePath = path.join(config.docspath, note.alias + '.md'); + } + if (Note.checkFileExist(filePath)) { + body = fs.readFileSync(filePath, 'utf8'); + note.title = LZString.compressToBase64(Note.parseNoteTitle(body)); + note.content = LZString.compressToBase64(body); + } + } + // if no permission specified and have owner then give editable permission, else default permission is freely + if (!note.permission) { + if (note.ownerId) { + note.permission = "editable"; + } else { + note.permission = "freely"; + } + } + return callback(null, note); + } + } + }); + + return Note; +}; \ No newline at end of file diff --git a/lib/models/temp.js b/lib/models/temp.js new file mode 100644 index 00000000..6eeff153 --- /dev/null +++ b/lib/models/temp.js @@ -0,0 +1,19 @@ +"use strict"; + +//external modules +var shortId = require('shortid'); + +module.exports = function (sequelize, DataTypes) { + var Temp = sequelize.define("Temp", { + id: { + type: DataTypes.STRING, + primaryKey: true, + defaultValue: shortId.generate + }, + data: { + type: DataTypes.TEXT + } + }); + + return Temp; +}; \ No newline at end of file diff --git a/lib/models/user.js b/lib/models/user.js new file mode 100644 index 00000000..e1a373d6 --- /dev/null +++ b/lib/models/user.js @@ -0,0 +1,77 @@ +"use strict"; + +// external modules +var md5 = require("blueimp-md5"); +var Sequelize = require("sequelize"); + +// core +var logger = require("../logger.js"); + +module.exports = function (sequelize, DataTypes) { + var User = sequelize.define("User", { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + profileid: { + type: DataTypes.STRING, + unique: true + }, + profile: { + type: DataTypes.TEXT + }, + history: { + type: DataTypes.TEXT + } + }, { + classMethods: { + associate: function (models) { + User.hasMany(models.Note, { + foreignKey: "ownerId", + constraints: false + }); + User.hasMany(models.Note, { + foreignKey: "lastchangeuserId", + constraints: false + }); + }, + parseProfile: function (profile) { + try { + var profile = JSON.parse(profile); + } catch (err) { + logger.error(err); + profile = null; + } + if (profile) { + profile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile) + } + } + return profile; + }, + parsePhotoByProfile: function (profile) { + var photo = null; + switch (profile.provider) { + case "facebook": + photo = 'https://graph.facebook.com/' + profile.id + '/picture'; + break; + case "twitter": + photo = profile.photos[0].value; + break; + case "github": + photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=48'; + break; + case "dropbox": + //no image api provided, use gravatar + photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value); + break; + } + return photo; + } + } + }); + + return User; +}; \ No newline at end of file -- cgit v1.2.3