mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 08:08:15 -05:00
206 lines
7.9 KiB
CoffeeScript
206 lines
7.9 KiB
CoffeeScript
config = require '../../server_config'
|
|
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 =
|
|
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 '../models/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)
|
|
|
|
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
|
|
|
|
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)
|
|
|
|
return doc
|
|
|
|
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.statics.editableProperties.slice()
|
|
|
|
if doc.isNew
|
|
props = props.concat doc.schema.statics.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.statics.jsonSchema)
|
|
if not result.valid
|
|
prunedErrors = (_.omit(error, 'stack') for error in result.errors)
|
|
winston.debug('Validation errors: ', JSON.stringify(prunedErrors, null, '\t'))
|
|
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
|
|
|