diff --git a/package.json b/package.json index c52f9fb8..5f49436e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build:webpack": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --progress", "build:analyze": "cross-env NODE_ENV=production webpack --config webpack.config.prod.js --json | webpack-bundle-size-analyzer", "build": "npm run clean && npm run build:webpack", - "start": "cross-env NODE_ENV=development DEBUG=cache,presenters ./node_modules/.bin/nodemon --watch server index.js", + "start": "cross-env NODE_ENV=development DEBUG=sql,cache,presenters ./node_modules/.bin/nodemon --watch server index.js", "lint": "eslint frontend", "deploy": "git push heroku master", "heroku-postbuild": "npm run build && npm run sequelize db:migrate", @@ -102,6 +102,8 @@ "react-keydown": "^1.6.1", "react-router": "2.5.1", "rebass": "0.2.6", + "redis": "^2.6.2", + "redis-lock": "^0.1.0", "reflexbox": "^2.0.0", "safestart": "0.8.0", "sass-loader": "4.0.0", @@ -109,6 +111,7 @@ "sequelize-cli": "2.4.0", "sequelize-encrypted": "0.1.0", "slug": "0.9.1", + "string-hash": "^1.1.0", "style-loader": "0.13.0", "truncate-html": "0.0.6", "url-loader": "0.5.7", diff --git a/server/api/documents.js b/server/api/documents.js index b3ed3c0e..6c9f3f03 100644 --- a/server/api/documents.js +++ b/server/api/documents.js @@ -3,6 +3,8 @@ import httpErrors from 'http-errors'; import { sequelize, } from '../sequelize'; +import { lock } from '../redis'; +import isUUID from 'validator/lib/isUUID'; const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/; @@ -14,26 +16,28 @@ import { Document, Atlas } from '../models'; const router = new Router(); const getDocumentForId = async (id) => { - let document; - if (id.match(URL_REGEX)) { - document = await Document.findOne({ - where: { - urlId: id.match(URL_REGEX)[1], - }, - }); - } else { - try { + try { + let document; + if (isUUID(id)) { document = await Document.findOne({ where: { id, }, }); - } catch (e) { - // Invalid UUID + } else if (id.match(URL_REGEX)) { + document = await Document.findOne({ + where: { + urlId: id.match(URL_REGEX)[1], + }, + }); + } else { throw httpErrors.NotFound(); } + return document; + } catch (e) { + // Invalid UUID + throw httpErrors.NotFound(); } - return document; }; // FIXME: This really needs specs :/ @@ -130,32 +134,41 @@ router.post('documents.create', auth(), async (ctx) => { if (!ownerCollection) throw httpErrors.BadRequest(); - let parentDocumentObj = {}; - if (parentDocument && ownerCollection.type === 'atlas') { - parentDocumentObj = await Document.findOne({ - where: { - id: parentDocument, - atlasId: ownerCollection.id, - }, + const document = await (() => { + return new Promise(resolve => { + lock(ownerCollection.id, 10000, async (done) => { + let parentDocumentObj = {}; + if (parentDocument && ownerCollection.type === 'atlas') { + parentDocumentObj = await Document.findOne({ + where: { + id: parentDocument, + atlasId: ownerCollection.id, + }, + }); + } + + const document = await Document.create({ + parentDocumentId: parentDocumentObj.id, + atlasId: ownerCollection.id, + teamId: user.teamId, + userId: user.id, + lastModifiedById: user.id, + createdById: user.id, + title, + text, + }); + + // TODO: Move to afterSave hook if possible with imports + if (parentDocument && ownerCollection.type === 'atlas') { + await ownerCollection.reload(); + ownerCollection.addNodeToNavigationTree(document); + await ownerCollection.save(); + } + + done(resolve(document)); + }); }); - } - - const document = await Document.create({ - parentDocumentId: parentDocumentObj.id, - atlasId: ownerCollection.id, - teamId: user.teamId, - userId: user.id, - lastModifiedById: user.id, - createdById: user.id, - title, - text, - }); - - // TODO: Move to afterSave hook if possible with imports - if (parentDocument && ownerCollection.type === 'atlas') { - ownerCollection.addNodeToNavigationTree(document); - await ownerCollection.save(); - } + })(); ctx.body = { data: await presentDocument(ctx, document, { @@ -187,6 +200,7 @@ router.post('documents.update', auth(), async (ctx) => { await document.save(); // Update + // TODO: Add locking const collection = await Atlas.findById(document.atlasId); if (collection.type === 'atlas') { await collection.updateNavigationTree(); @@ -212,6 +226,7 @@ router.post('documents.delete', auth(), async (ctx) => { if (!document || document.teamId !== user.teamId) throw httpErrors.BadRequest(); + // TODO: Add locking if (collection.type === 'atlas') { // Don't allow deletion of root docs if (!document.parentDocumentId) { diff --git a/server/models/Atlas.js b/server/models/Atlas.js index b8ca7029..6d13330d 100644 --- a/server/models/Atlas.js +++ b/server/models/Atlas.js @@ -142,7 +142,7 @@ const Atlas = sequelize.define('atlas', { return newTree; }, - addNodeToNavigationTree(document) { + async addNodeToNavigationTree(document) { const newNode = { id: document.id, title: document.title, diff --git a/server/redis.js b/server/redis.js new file mode 100644 index 00000000..fcbabbcd --- /dev/null +++ b/server/redis.js @@ -0,0 +1,10 @@ +import redis from 'redis'; +import redisLock from 'redis-lock'; + +const client = redis.createClient(process.env.REDIS_URL); +const lock = redisLock(client); + +export { + client, + lock, +};