Refactor out promisifyAll, use Mongoose's promises and promisify

This commit is contained in:
Scott Erickson 2016-02-22 15:08:58 -08:00
parent becad06750
commit 7749f9cc0a
10 changed files with 372 additions and 358 deletions

View file

@ -3,34 +3,201 @@ winston = require 'winston'
mongoose = require 'mongoose'
Grid = require 'gridfs-stream'
mongooseCache = require 'mongoose-cache'
errors = require '../commons/errors'
Promise = require 'bluebird'
_ = require 'lodash'
module.exports.connect = () ->
address = module.exports.generateMongoConnectionString()
winston.info "Connecting to Mongo with connection string #{address}"
module.exports =
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
connect: () ->
address = module.exports.generateMongoConnectionString()
winston.info "Connecting to Mongo with connection string #{address}"
mongoose.connect address
mongoose.connection.once 'open', -> Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
# Hack around Mongoose not exporting Aggregate so that we can patch its exec, too
# https://github.com/LearnBoost/mongoose/issues/1910
Level = require '../levels/Level'
Aggregate = Level.aggregate().constructor
maxAge = (Math.random() * 10 + 10) * 60 * 1000 # Randomize so that each server doesn't refresh cache from db at same times
mongooseCache.install(mongoose, {max: 1000, maxAge: maxAge, debug: false}, Aggregate)
mongoose.connect address
mongoose.connection.once 'open', -> Grid.gfs = Grid(mongoose.connection.db, mongoose.mongo)
generateMongoConnectionString: ->
if not global.testing and config.tokyo
address = config.mongo.mongoose_tokyo_replica_string
else if not global.testing and config.saoPaulo
address = config.mongo.mongoose_saoPaulo_replica_string
else if not global.testing and config.mongo.mongoose_replica_string
address = config.mongo.mongoose_replica_string
else
dbName = config.mongo.db
dbName += '_unittest' if global.testing
address = config.mongo.host + ':' + config.mongo.port
if config.mongo.username and config.mongo.password
address = config.mongo.username + ':' + config.mongo.password + '@' + address
address = "mongodb://#{address}/#{dbName}"
return address
# Hack around Mongoose not exporting Aggregate so that we can patch its exec, too
# https://github.com/LearnBoost/mongoose/issues/1910
Level = require '../levels/Level'
Aggregate = Level.aggregate().constructor
maxAge = (Math.random() * 10 + 10) * 60 * 1000 # Randomize so that each server doesn't refresh cache from db at same times
mongooseCache.install(mongoose, {max: 1000, maxAge: maxAge, debug: false}, Aggregate)
initDoc: (req, Model) ->
# TODO: Move to model superclass or plugins?
doc = new Model({})
module.exports.generateMongoConnectionString = ->
if not global.testing and config.tokyo
address = config.mongo.mongoose_tokyo_replica_string
else if not global.testing and config.saoPaulo
address = config.mongo.mongoose_saoPaulo_replica_string
else if not global.testing and config.mongo.mongoose_replica_string
address = config.mongo.mongoose_replica_string
else
dbName = config.mongo.db
dbName += '_unittest' if global.testing
address = config.mongo.host + ':' + config.mongo.port
if config.mongo.username and config.mongo.password
address = config.mongo.username + ':' + config.mongo.password + '@' + address
address = "mongodb://#{address}/#{dbName}"
if Model.schema.is_patchable
watchers = [req.user.get('_id')]
if req.user.isAdmin() # https://github.com/codecombat/codecombat/issues/1105
nick = mongoose.Types.ObjectId('512ef4805a67a8c507000001')
watchers.push nick unless _.find watchers, (id) -> id.equals nick
doc.set 'watchers', watchers
if Model.schema.uses_coco_versions
doc.set('original', doc._id)
doc.set('creator', req.user._id)
applyCustomSearchToDBQ: (req, dbq) ->
specialParameters = ['term', 'project', 'conditions']
return unless req.user?.isAdmin()
return unless req.query.filter or req.query.conditions
# admins can send any sort of query down the wire
# Example URL: http://localhost:3000/db/user?filter[anonymous]=true
filter = {}
if 'filter' of req.query
for own key, val of req.query.filter
if key not in specialParameters
try
filter[key] = JSON.parse(val)
catch SyntaxError
throw new errors.UnprocessableEntity("Could not parse filter for key '#{key}'.")
dbq.find(filter)
# Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated')
# Example URL: http://localhost:3000/db/user?conditions[limit]=20&conditions[sort]="-dateCreated"
for own key, val of req.query.conditions
if not dbq[key]
throw new errors.UnprocessableEntity("No query condition '#{key}'.")
try
val = JSON.parse(val)
dbq[key](val)
catch SyntaxError
throw new errors.UnprocessableEntity("Could not parse condition for key '#{key}'.")
viewSearch: Promise.promisify (dbq, req, done) ->
Model = dbq.model
# TODO: Make this function only alter dbq or returns a find. It should not also execute the query.
term = req.query.term
matchedObjects = []
filters = if Model.schema.uses_coco_versions or Model.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
if Model.schema.uses_coco_permissions and req.user
filters.push {filter: {index: req.user.get('id')}}
for filter in filters
callback = (err, results) ->
return done(new errors.InternalServerError('Error fetching search results.', {err: err})) if err
for r in results.results ? results
obj = r.obj ? r
continue if obj in matchedObjects # TODO: probably need a better equality check
matchedObjects.push obj
filters.pop() # doesn't matter which one
unless filters.length
done(null, matchedObjects)
if term
filter.filter.$text = $search: term
else if filters.length is 1 and filters[0].filter?.index is true
# All we are doing is an empty text search, but that doesn't hit the index,
# so we'll just look for the slug.
filter.filter = slug: {$exists: true}
# This try/catch is here to handle when a custom search tries to find by slug. TODO: Fix this more gracefully.
try
dbq.find filter.filter
catch
dbq.exec callback
assignBody: (req, doc, options={}) ->
if _.isEmpty(req.body)
throw new errors.UnprocessableEntity('No input')
props = doc.schema.editableProperties.slice()
if doc.isNew
props = props.concat doc.schema.postEditableProperties
if doc.schema.uses_coco_permissions and req.user
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'
if doc.isNew or isOwner or req.user?.isAdmin()
props.push 'permissions'
props.push 'commitMessage' if doc.schema.uses_coco_versions
props.push 'allowPatches' if doc.schema.is_patchable
for prop in props
if (val = req.body[prop])?
doc.set prop, val
else if options.unsetMissing and doc.get(prop)?
doc.set prop, undefined
validateDoc: (doc) ->
obj = doc.toObject()
# Hack to get saving of Users to work. Probably should replace these props with strings
# so that validation doesn't get hung up on Date objects in the documents.
delete obj.dateCreated
tv4 = require('tv4').tv4
result = tv4.validateMultiple(obj, doc.schema.jsonSchema)
if not result.valid
throw new errors.UnprocessableEntity('JSON-schema validation failed', { validationErrors: result.errors })
getDocFromHandle: Promise.promisify (req, Model, options, done) ->
if _.isFunction(options)
done = options
options = {}
dbq = Model.find()
handle = req.params.handle
if not handle
return done(new errors.UnprocessableEntity('No handle provided.'))
if @isID(handle)
dbq.findOne({ _id: handle })
else
dbq.findOne({ slug: handle })
dbq.exec(done)
hasAccessToDocument: (req, doc, method) ->
method = method or req.method
return true if req.user?.isAdmin()
if doc.schema.uses_coco_translation_coverage and method in ['post', 'put']
return true if @isJustFillingTranslations(req, doc)
if doc.schema.uses_coco_permissions
return doc.hasPermissionsForMethod?(req.user, method)
return true
isJustFillingTranslations: (req, doc) ->
deltasLib = require '../../app/core/deltas'
differ = deltasLib.makeJSONDiffer()
omissions = ['original'].concat(deltasLib.DOC_SKIP_PATHS)
delta = differ.diff(_.omit(doc.toObject(), omissions), _.omit(req.body, omissions))
flattened = deltasLib.flattenDelta(delta)
_.all flattened, (delta) ->
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
return false unless _.isArray(delta.o)
return true if 'i18nCoverage' in delta.dataPath
return false unless delta.o.length is 1
index = delta.deltaPath.indexOf('i18n')
return false if index is -1
return false if delta.deltaPath[index+1] in ['en', 'en-US', 'en-GB'] # English speakers are most likely just spamming, so always treat those as patches, not saves.
return true
return address

View file

@ -0,0 +1,60 @@
errors = require '../commons/errors'
_ = require 'lodash'
Promise = require 'bluebird'
module.exports =
getLimitFromReq: (req, options) ->
options = _.extend({
max: 1000
default: 100
}, options)
limit = options.default
if req.query.limit
limit = parseInt(req.query.limit)
valid = tv4.validate(limit, {
type: 'integer'
maximum: options.max
minimum: 1
})
if not valid
throw new errors.UnprocessableEntity('Invalid limit parameter.')
return limit
getSkipFromReq: (req, options) ->
options = _.extend({
max: 1000000
default: 0
}, options)
skip = options.default
if req.query.skip
skip = parseInt(req.query.skip)
valid = tv4.validate(skip, {
type: 'integer'
maximum: options.max
minimum: 0
})
if not valid
throw new errors.UnprocessableEntity('Invalid sort parameter.')
return skip
getProjectFromReq: (req, options) ->
options = _.extend({}, options)
return null unless req.query.project
projection = {}
if req.query.project is 'true'
projection = {original: 1, name: 1, version: 1, description: 1, slug: 1, kind: 1, created: 1, permissions: 1}
else
for field in req.query.project.split(',')
projection[field] = 1
return projection

View file

@ -2,12 +2,10 @@ AnalyticsString = require '../analytics/AnalyticsString'
log = require 'winston'
mongoose = require 'mongoose'
config = require '../../server_config'
errors = require '../commons/errors'
_ = require 'lodash'
Promise = require 'bluebird'
deltasLib = require '../../app/core/deltas'
module.exports = utils =
# TODO: Remove, use commons/database.isID instead
isID: (id) -> _.isString(id) and id.length is 24 and id.match(/[a-f0-9]/gi)?.length is 24
getCodeCamel: (numWords=3) ->
@ -96,219 +94,3 @@ module.exports = utils =
@analyticsStringCache[str] = document._id
return callback @analyticsStringCache[str]
insertString()
getLimitFromReq: (req, options) ->
options = _.extend({
max: 1000
default: 100
}, options)
limit = options.default
if req.query.limit
limit = parseInt(req.query.limit)
valid = tv4.validate(limit, {
type: 'integer'
maximum: options.max
minimum: 1
})
if not valid
throw new errors.UnprocessableEntity('Invalid limit parameter.')
return limit
getSkipFromReq: (req, options) ->
options = _.extend({
max: 1000000
default: 0
}, options)
skip = options.default
if req.query.skip
skip = parseInt(req.query.skip)
valid = tv4.validate(skip, {
type: 'integer'
maximum: options.max
minimum: 0
})
if not valid
throw new errors.UnprocessableEntity('Invalid sort parameter.')
return skip
getProjectFromReq: (req, options) ->
options = _.extend({}, options)
return null unless req.query.project
projection = {}
if req.query.project is 'true'
projection = {original: 1, name: 1, version: 1, description: 1, slug: 1, kind: 1, created: 1, permissions: 1}
else
for field in req.query.project.split(',')
projection[field] = 1
return projection
applyCustomSearchToDBQ: (req, dbq) ->
specialParameters = ['term', 'project', 'conditions']
return unless req.user?.isAdmin()
return unless req.query.filter or req.query.conditions
# admins can send any sort of query down the wire
# Example URL: http://localhost:3000/db/user?filter[anonymous]=true
filter = {}
if 'filter' of req.query
for own key, val of req.query.filter
if key not in specialParameters
try
filter[key] = JSON.parse(val)
catch SyntaxError
throw new errors.UnprocessableEntity("Could not parse filter for key '#{key}'.")
dbq.find(filter)
# Conditions are chained query functions, for example: query.find().limit(20).sort('-dateCreated')
# Example URL: http://localhost:3000/db/user?conditions[limit]=20&conditions[sort]="-dateCreated"
for own key, val of req.query.conditions
if not dbq[key]
throw new errors.UnprocessableEntity("No query condition '#{key}'.")
try
val = JSON.parse(val)
dbq[key](val)
catch SyntaxError
throw new errors.UnprocessableEntity("Could not parse condition for key '#{key}'.")
viewSearch: (dbq, req, done) ->
Model = dbq.model
# TODO: Make this function only alter dbq or returns a find. It should not also execute the query.
term = req.query.term
matchedObjects = []
filters = if Model.schema.uses_coco_versions or Model.schema.uses_coco_permissions then [filter: {index: true}] else [filter: {}]
if Model.schema.uses_coco_permissions and req.user
filters.push {filter: {index: req.user.get('id')}}
for filter in filters
callback = (err, results) ->
return done(new errors.InternalServerError('Error fetching search results.', {err: err})) if err
for r in results.results ? results
obj = r.obj ? r
continue if obj in matchedObjects # TODO: probably need a better equality check
matchedObjects.push obj
filters.pop() # doesn't matter which one
unless filters.length
done(null, matchedObjects)
if term
filter.filter.$text = $search: term
else if filters.length is 1 and filters[0].filter?.index is true
# All we are doing is an empty text search, but that doesn't hit the index,
# so we'll just look for the slug.
filter.filter = slug: {$exists: true}
# This try/catch is here to handle when a custom search tries to find by slug. TODO: Fix this more gracefully.
try
dbq.find filter.filter
catch
dbq.exec callback
assignBody: (req, doc, options={}) ->
if _.isEmpty(req.body)
throw new errors.UnprocessableEntity('No input')
props = doc.schema.editableProperties.slice()
if doc.isNew
props = props.concat doc.schema.postEditableProperties
if doc.schema.uses_coco_permissions and req.user
isOwner = doc.getAccessForUserObjectId(req.user._id) is 'owner'
if doc.isNew or isOwner or req.user?.isAdmin()
props.push 'permissions'
props.push 'commitMessage' if doc.schema.uses_coco_versions
props.push 'allowPatches' if doc.schema.is_patchable
for prop in props
if (val = req.body[prop])?
doc.set prop, val
else if options.unsetMissing and doc.get(prop)?
doc.set prop, undefined
validateDoc: (doc) ->
obj = doc.toObject()
# Hack to get saving of Users to work. Probably should replace these props with strings
# so that validation doesn't get hung up on Date objects in the documents.
delete obj.dateCreated
tv4 = require('tv4').tv4
result = tv4.validateMultiple(obj, doc.schema.jsonSchema)
if not result.valid
throw new errors.UnprocessableEntity('JSON-schema validation failed', { validationErrors: result.errors })
getDocFromHandle: (req, Model, options, done) ->
if _.isFunction(options)
done = options
options = {}
dbq = Model.find()
handle = req.params.handle
if not handle
return done(new errors.UnprocessableEntity('No handle provided.'))
if utils.isID(handle)
dbq.findOne({ _id: handle })
else
dbq.findOne({ slug: handle })
dbq.exec(done)
initDoc: (req, Model) ->
# TODO: Move to model superclass or plugins?
doc = new Model({})
if Model.schema.is_patchable
watchers = [req.user.get('_id')]
if req.user.isAdmin() # https://github.com/codecombat/codecombat/issues/1105
nick = mongoose.Types.ObjectId('512ef4805a67a8c507000001')
watchers.push nick unless _.find watchers, (id) -> id.equals nick
doc.set 'watchers', watchers
if Model.schema.uses_coco_versions
doc.set('original', doc._id)
doc.set('creator', req.user._id)
hasAccessToDocument: (req, doc, method) ->
method = method or req.method
return true if req.user?.isAdmin()
if doc.schema.uses_coco_translation_coverage and method in ['post', 'put']
return true if @isJustFillingTranslations(req, doc)
if doc.schema.uses_coco_permissions
return doc.hasPermissionsForMethod?(req.user, method)
return true
isJustFillingTranslations: (req, doc) ->
differ = deltasLib.makeJSONDiffer()
omissions = ['original'].concat(deltasLib.DOC_SKIP_PATHS)
delta = differ.diff(_.omit(doc.toObject(), omissions), _.omit(req.body, omissions))
flattened = deltasLib.flattenDelta(delta)
_.all flattened, (delta) ->
# sometimes coverage gets moved around... allow other changes to happen to i18nCoverage
return false unless _.isArray(delta.o)
return true if 'i18nCoverage' in delta.dataPath
return false unless delta.o.length is 1
index = delta.deltaPath.indexOf('i18n')
return false if index is -1
return false if delta.deltaPath[index+1] in ['en', 'en-US', 'en-GB'] # English speakers are most likely just spamming, so always treat those as patches, not saves.
return true
Promise.promisifyAll(module.exports)

View file

@ -3,10 +3,11 @@ errors = require '../commons/errors'
wrap = require 'co-express'
Grid = require 'gridfs-stream'
Promise = require 'bluebird'
database = require '../commons/database'
module.exports =
files: (Model, options={}) -> wrap (req, res) ->
doc = yield utils.getDocFromHandleAsync(req, Model)
doc = yield database.getDocFromHandle(req, Model)
if not doc
throw new errors.NotFound('Document not found.')
module = options.module or req.path[4..].split('/')[0]

View file

@ -2,6 +2,7 @@ utils = require '../lib/utils'
errors = require '../commons/errors'
wrap = require 'co-express'
Promise = require 'bluebird'
database = require '../commons/database'
module.exports =
names: (Model, options={}) -> wrap (req, res) ->
@ -17,7 +18,7 @@ module.exports =
sort = if Model.schema.uses_coco_versions then {'version.major': -1, 'version.minor': -1} else {}
for id in ids
if not utils.isID(id)
if not database.isID(id)
throw new errors.UnprocessableEntity('Invalid MongoDB id given')
ids = (mongoose.Types.ObjectId(id) for id in ids)

View file

@ -4,16 +4,18 @@ wrap = require 'co-express'
Promise = require 'bluebird'
Patch = require '../models/Patch'
mongoose = require 'mongoose'
database = require '../commons/database'
parse = require '../commons/parse'
module.exports =
patches: (options={}) -> wrap (req, res) ->
dbq = Patch.find()
dbq.limit(utils.getLimitFromReq(req))
dbq.skip(utils.getSkipFromReq(req))
dbq.select(utils.getProjectFromReq(req))
dbq.limit(parse.getLimitFromReq(req))
dbq.skip(parse.getSkipFromReq(req))
dbq.select(parse.getProjectFromReq(req))
id = req.params.handle
if not utils.isID(id)
if not database.isID(id)
throw new errors.UnprocessableEntity('Invalid ID')
query =
@ -27,10 +29,10 @@ module.exports =
res.status(200).send(patches)
joinWatchers: (Model, options={}) -> wrap (req, res) ->
doc = yield utils.getDocFromHandleAsync(req, Model)
doc = yield database.getDocFromHandle(req, Model)
if not doc
throw new errors.NotFound('Document not found.')
if not utils.hasAccessToDocument(req, doc, 'get')
if not database.hasAccessToDocument(req, doc, 'get')
throw new errors.Forbidden()
updateResult = yield doc.update({ $addToSet: { watchers: req.user.get('_id') }})
if updateResult.nModified
@ -40,7 +42,7 @@ module.exports =
res.status(200).send(doc)
leaveWatchers: (Model, options={}) -> wrap (req, res) ->
doc = yield utils.getDocFromHandleAsync(req, Model)
doc = yield database.getDocFromHandle(req, Model)
if not doc
throw new errors.NotFound('Document not found.')
updateResult = yield doc.update({ $pull: { watchers: req.user.get('_id') }})

View file

@ -1,40 +1,42 @@
utils = require '../lib/utils'
errors = require '../commons/errors'
wrap = require 'co-express'
database = require '../commons/database'
parse = require '../commons/parse'
module.exports =
get: (Model, options={}) -> wrap (req, res) ->
dbq = Model.find()
dbq.limit(utils.getLimitFromReq(req))
dbq.skip(utils.getSkipFromReq(req))
dbq.select(utils.getProjectFromReq(req))
utils.applyCustomSearchToDBQ(req, dbq)
dbq.limit(parse.getLimitFromReq(req))
dbq.skip(parse.getSkipFromReq(req))
dbq.select(parse.getProjectFromReq(req))
database.applyCustomSearchToDBQ(req, dbq)
if Model.schema.uses_coco_translation_coverage and req.query.view is 'i18n-coverage'
dbq.find({ slug: {$exists: true}, i18nCoverage: {$exists: true} })
results = yield utils.viewSearchAsync(dbq, req)
results = yield database.viewSearch(dbq, req)
res.send(results)
post: (Model, options={}) -> wrap (req, res) ->
doc = utils.initDoc(req, Model)
utils.assignBody(req, doc)
utils.validateDoc(doc)
doc = database.initDoc(req, Model)
database.assignBody(req, doc)
database.validateDoc(doc)
doc = yield doc.save()
res.status(201).send(doc.toObject())
getByHandle: (Model, options={}) -> wrap (req, res) ->
doc = yield utils.getDocFromHandleAsync(req, Model)
doc = yield database.getDocFromHandle(req, Model)
if not doc
throw new errors.NotFound('Document not found.')
res.status(200).send(doc.toObject())
put: (Model, options={}) -> wrap (req, res) ->
doc = yield utils.getDocFromHandleAsync(req, Model)
doc = yield database.getDocFromHandle(req, Model)
if not doc
throw new errors.NotFound('Document not found.')
utils.assignBody(req, doc)
utils.validateDoc(doc)
database.assignBody(req, doc)
database.validateDoc(doc)
doc = yield doc.save()
res.status(200).send(doc.toObject())

View file

@ -6,10 +6,12 @@ hipchat = require '../hipchat'
_ = require 'lodash'
wrap = require 'co-express'
mongoose = require 'mongoose'
database = require '../commons/database'
parse = require '../commons/parse'
module.exports =
postNewVersion: (Model, options={}) -> wrap (req, res) ->
parent = yield utils.getDocFromHandleAsync(req, Model)
parent = yield database.getDocFromHandle(req, Model)
if not parent
throw new errors.NotFound('Parent not found.')
@ -19,14 +21,14 @@ module.exports =
permissions = [permissions] if _.isString(permissions)
permissions = ['admin'] if not _.isArray(permissions)
hasPermission = _.any(req.user?.hasPermission(permission) for permission in permissions)
if not (hasPermission or utils.isJustFillingTranslations(req, parent))
if not (hasPermission or database.isJustFillingTranslations(req, parent))
throw new errors.Forbidden()
doc = utils.initDoc(req, Model)
doc = database.initDoc(req, Model)
ATTRIBUTES_NOT_INHERITED = ['_id', 'version', 'created', 'creator']
doc.set(_.omit(parent.toObject(), ATTRIBUTES_NOT_INHERITED))
utils.assignBody(req, doc, { unsetMissing: true })
database.assignBody(req, doc, { unsetMissing: true })
# Get latest version
major = req.body.version?.major
@ -124,7 +126,7 @@ module.exports =
# can get latest overall version, latest of a major version, or a specific version
original = req.params.handle
version = req.params.version
if not utils.isID(original)
if not database.isID(original)
throw new errors.UnprocessableEntity('Invalid MongoDB id: '+original)
query = { 'original': mongoose.Types.ObjectId(original) }
@ -139,7 +141,7 @@ module.exports =
dbq.sort({ 'version.major': -1, 'version.minor': -1 })
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
projection = utils.getProjectFromReq(req)
projection = parse.getProjectFromReq(req)
if projection
extraProjectionProps = []
extraProjectionProps.push 'permissions' unless projection.permissions
@ -150,7 +152,7 @@ module.exports =
doc = yield dbq.exec()
throw new errors.NotFound() if not doc
throw new errors.Forbidden() unless utils.hasAccessToDocument(req, doc)
throw new errors.Forbidden() unless database.hasAccessToDocument(req, doc)
doc = _.omit doc, extraProjectionProps if extraProjectionProps?
res.status(200).send(doc.toObject())
@ -160,9 +162,9 @@ module.exports =
original = req.params.handle
dbq = Model.find({'original': mongoose.Types.ObjectId(original)})
dbq.sort({'created': -1})
dbq.limit(utils.getLimitFromReq(req))
dbq.skip(utils.getSkipFromReq(req))
dbq.select(utils.getProjectFromReq(req) or 'slug name version commitMessage created creator permissions')
dbq.limit(parse.getLimitFromReq(req))
dbq.skip(parse.getSkipFromReq(req))
dbq.select(parse.getProjectFromReq(req) or 'slug name version commitMessage created creator permissions')
results = yield dbq.exec()
res.status(200).send(results)

View file

@ -9,12 +9,12 @@ describe 'GET /db/article', ->
articleData2 = { name: 'Article 2', body: 'Article 2 body moo' }
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin()
yield utils.loginUser(@admin)
yield request.postAsync(getURL('/db/article'), { json: articleData1 })
yield request.postAsync(getURL('/db/article'), { json: articleData2 })
yield utils.logoutAsync()
yield utils.logout()
done()
@ -68,22 +68,22 @@ describe 'GET /db/article', ->
it 'accepts custom filter parameters', utils.wrap (done) ->
yield utils.loginUserAsync(@admin)
yield utils.loginUser(@admin)
[res, body] = yield request.getAsync {uri: getURL('/db/article?filter[slug]="article-1"'), json: true}
expect(body.length).toBe(1)
done()
it 'ignores custom filter parameters for non-admins', utils.wrap (done) ->
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
user = yield utils.initUser()
yield utils.loginUser(user)
[res, body] = yield request.getAsync {uri: getURL('/db/article?filter[slug]="article-1"'), json: true}
expect(body.length).toBe(2)
done()
it 'accepts custom condition parameters', utils.wrap (done) ->
yield utils.loginUserAsync(@admin)
yield utils.loginUser(@admin)
[res, body] = yield request.getAsync {uri: getURL('/db/article?conditions[select]="slug body"'), json: true}
expect(body.length).toBe(2)
for doc in body
@ -92,8 +92,8 @@ describe 'GET /db/article', ->
it 'ignores custom condition parameters for non-admins', utils.wrap (done) ->
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
user = yield utils.initUser()
yield utils.loginUser(user)
[res, body] = yield request.getAsync {uri: getURL('/db/article?conditions[select]="slug body"'), json: true}
expect(body.length).toBe(2)
for doc in body
@ -120,9 +120,9 @@ describe 'POST /db/article', ->
articleData = { name: 'Article', body: 'Article', otherProp: 'not getting set' }
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin({})
yield utils.loginUser(@admin)
[@res, @body] = yield request.postAsync {
uri: getURL('/db/article'), json: articleData
}
@ -172,26 +172,26 @@ describe 'POST /db/article', ->
it 'allows artisans to create Articles', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
artisan = yield utils.initArtisanAsync({})
yield utils.loginUserAsync(artisan)
yield utils.clearModels([Article])
artisan = yield utils.initArtisan({})
yield utils.loginUser(artisan)
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
expect(res.statusCode).toBe(201)
done()
it 'does not allow normal users to create Articles', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
user = yield utils.initUserAsync({})
yield utils.loginUserAsync(user)
yield utils.clearModels([Article])
user = yield utils.initUser({})
yield utils.loginUser(user)
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
expect(res.statusCode).toBe(403)
done()
it 'does not allow anonymous users to create Articles', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
yield utils.logoutAsync()
yield utils.clearModels([Article])
yield utils.logout()
[res, body] = yield request.postAsync({uri: getURL('/db/article'), json: articleData })
expect(res.statusCode).toBe(401)
done()
@ -214,9 +214,9 @@ describe 'GET /db/article/:handle', ->
articleData = { name: 'Some Name', body: 'Article' }
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin({})
yield utils.loginUser(@admin)
[@res, @body] = yield request.postAsync {
uri: getURL('/db/article'), json: articleData
}
@ -247,9 +247,9 @@ putTests = (method='PUT') ->
articleData = { name: 'Some Name', body: 'Article' }
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin({})
yield utils.loginUser(@admin)
[@res, @body] = yield request.postAsync {
uri: getURL('/db/article'), json: articleData
}
@ -270,8 +270,8 @@ putTests = (method='PUT') ->
it 'does not allow normal artisan, non-admins to make changes', utils.wrap (done) ->
artisan = yield utils.initArtisanAsync({})
yield utils.loginUserAsync(artisan)
artisan = yield utils.initArtisan({})
yield utils.loginUser(artisan)
[res, body] = yield requestAsync {method: method, uri: getURL("/db/article/#{@body._id}"), json: { name: 'Another name' }}
expect(res.statusCode).toBe(403)
done()
@ -286,9 +286,9 @@ describe 'POST /db/article/:handle/new-version', ->
articleID = null
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin({})
yield utils.loginUser(@admin)
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
articleID = body._id
@ -418,9 +418,9 @@ describe 'POST /db/article/:handle/new-version', ->
it 'works for artisans', utils.wrap (done) ->
yield utils.logoutAsync()
artisan = yield utils.initArtisanAsync()
yield utils.loginUserAsync(artisan)
yield utils.logout()
artisan = yield utils.initArtisan()
yield utils.loginUser(artisan)
yield postNewVersion({ name: 'Article name', body: 'New body' })
articles = yield Article.find()
expect(articles.length).toBe(2)
@ -428,9 +428,9 @@ describe 'POST /db/article/:handle/new-version', ->
it 'works for normal users submitting translations', utils.wrap (done) ->
yield utils.logoutAsync()
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
yield utils.logout()
user = yield utils.initUser()
yield utils.loginUser(user)
yield postNewVersion({ name: 'Article name', body: 'Article body', i18n: { fr: { name: 'Le Article' }}}, 201)
articles = yield Article.find()
expect(articles.length).toBe(2)
@ -438,9 +438,9 @@ describe 'POST /db/article/:handle/new-version', ->
it 'does not work for normal users', utils.wrap (done) ->
yield utils.logoutAsync()
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
yield utils.logout()
user = yield utils.initUser()
yield utils.loginUser(user)
yield postNewVersion({ name: 'Article name', body: 'New body' }, 403)
articles = yield Article.find()
expect(articles.length).toBe(1)
@ -448,7 +448,7 @@ describe 'POST /db/article/:handle/new-version', ->
it 'does not work for anonymous users', utils.wrap (done) ->
yield utils.logoutAsync()
yield utils.logout()
yield postNewVersion({ name: 'Article name', body: 'New body' }, 401)
articles = yield Article.find()
expect(articles.length).toBe(1)
@ -490,9 +490,9 @@ describe 'version fetching endpoints', ->
beforeEach utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
@admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(@admin)
yield utils.clearModels([Article])
@admin = yield utils.initAdmin({})
yield utils.loginUser(@admin)
[res, body] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
articleOriginal = body._id
@ -548,10 +548,10 @@ describe 'version fetching endpoints', ->
describe 'GET /db/article/:handle/files', ->
it 'returns an array of file metadata for the given original article', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
yield utils.clearModels([Article])
articleData = { name: 'Article', body: 'Article' }
admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(admin)
admin = yield utils.initAdmin({})
yield utils.loginUser(admin)
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
[res, body] = yield request.postAsync(getURL('/file'), { json: {
@ -572,12 +572,12 @@ describe 'GET and POST /db/article/:handle/names', ->
articleData2 = { name: 'Article 2', body: 'Article 2 body' }
it 'returns an object mapping ids to names', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(admin)
yield utils.clearModels([Article])
admin = yield utils.initAdmin({})
yield utils.loginUser(admin)
[res, article1] = yield request.postAsync(getURL('/db/article'), { json: articleData1 })
[res, article2] = yield request.postAsync(getURL('/db/article'), { json: articleData2 })
yield utils.logoutAsync()
yield utils.logout()
[res, body] = yield request.getAsync { uri: getURL('/db/article/names?ids='+[article1._id, article2._id].join(',')), json: true }
expect(body.length).toBe(2)
expect(body[0].name).toBe('Article 1')
@ -590,10 +590,10 @@ describe 'GET and POST /db/article/:handle/names', ->
describe 'GET /db/article/:handle/patches', ->
it 'returns pending patches for the given original article', utils.wrap (done) ->
yield utils.clearModelsAsync([Article])
yield utils.clearModels([Article])
articleData = { name: 'Article', body: 'Article' }
admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(admin)
admin = yield utils.initAdmin({})
yield utils.loginUser(admin)
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
[res, patch] = yield request.postAsync { uri: getURL('/db/patch'), json: {
@ -620,17 +620,17 @@ describe 'POST /db/article/:handle/watchers', ->
it 'adds self to the list of watchers, and is idempotent', utils.wrap (done) ->
# create article
yield utils.clearModelsAsync([Article])
yield utils.clearModels([Article])
articleData = { name: 'Article', body: 'Article' }
admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(admin)
admin = yield utils.initAdmin({})
yield utils.loginUser(admin)
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
# add new user as watcher
yield utils.logoutAsync()
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
yield utils.logout()
user = yield utils.initUser()
yield utils.loginUser(user)
[res, article] = yield request.postAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
expect(res.statusCode).toBe(200)
expect(_.contains(article.watchers, user.id)).toBe(true)
@ -649,17 +649,17 @@ describe 'DELETE /db/article/:handle/watchers', ->
it 'removes self from the list of watchers, and is idempotent', utils.wrap (done) ->
# create article
yield utils.clearModelsAsync([Article])
yield utils.clearModels([Article])
articleData = { name: 'Article', body: 'Article' }
admin = yield utils.initAdminAsync({})
yield utils.loginUserAsync(admin)
admin = yield utils.initAdmin({})
yield utils.loginUser(admin)
[res, article] = yield request.postAsync { uri: getURL('/db/article'), json: articleData }
expect(res.statusCode).toBe(201)
# add new user as watcher
yield utils.logoutAsync()
user = yield utils.initUserAsync()
yield utils.loginUserAsync(user)
yield utils.logout()
user = yield utils.initUser()
yield utils.loginUser(user)
[res, article] = yield request.postAsync { uri: getURL("/db/article/#{article._id}/watchers"), json: true }
expect(_.contains(article.watchers, user.id)).toBe(true)

View file

@ -1,11 +1,12 @@
async = require 'async'
utils = require '../../server/lib/utils'
co = require 'co'
Promise = require 'bluebird'
module.exports = mw =
getURL: (path) -> 'http://localhost:3001' + path
clearModels: (models, done) ->
clearModels: Promise.promisify (models, done) ->
funcs = []
for model in models
wrapped = (m) ->
@ -25,11 +26,11 @@ module.exports = mw =
password: 'password'
permissions: options.permissions
}
new User(doc).save (err, user) ->
expect(err).toBe(null)
done(err, user)
user = new User(doc)
promise = user.save()
return promise
loginUser: (user, done) ->
loginUser: Promise.promisify (user, done) ->
form = {
username: user.get('email')
password: 'password'
@ -39,21 +40,21 @@ module.exports = mw =
expect(res.statusCode).toBe(200)
done(err, user)
initAdmin: (options, done) ->
initAdmin: (options) ->
if _.isFunction(options)
done = options
options = {}
options = _.extend({permissions: ['admin']}, options)
return @initUser(options, done)
return @initUser(options)
initArtisan: (options, done) ->
initArtisan: (options) ->
if _.isFunction(options)
done = options
options = {}
options = _.extend({permissions: ['artisan']}, options)
return @initUser(options, done)
return @initUser(options)
logout: (done) ->
logout: Promise.promisify (done) ->
request.post mw.getURL('/auth/logout'), done
wrap: (gen) ->
@ -61,7 +62,3 @@ module.exports = mw =
return (done) ->
fn.apply(@, [done]).catch (err) -> done.fail(err)
Promise = require 'bluebird'
Promise.promisifyAll(module.exports)