2014-02-04 15:30:05 -05:00
config = require ' ../../server_config '
winston = require ' winston '
mongoose = require ' mongoose '
Grid = require ' gridfs-stream '
2015-02-26 20:20:27 -05:00
mongooseCache = require ' mongoose-cache '
2016-02-22 18:08:58 -05:00
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 ( )
2016-08-16 16:20:38 -04:00
winston . info " Connecting to Mongo with connection string #{ address } , readpref: #{ config . mongo . readpref } "
2016-02-22 18:08:58 -05:00
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
2016-04-06 13:56:06 -04:00
Level = require ' ../models/Level '
2016-02-22 18:08:58 -05:00
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: ->
2016-08-16 16:20:38 -04:00
if not global . testing and config . mongo . mongoose_replica_string
2016-02-22 18:08:58 -05:00
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 )
2016-03-09 19:59:25 -05:00
return doc
2016-02-22 18:08:58 -05:00
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 ' )
2016-03-09 19:59:25 -05:00
2016-08-24 18:46:35 -04:00
if not doc . schema . statics . editableProperties
console . warn ' No editableProperties set for ' , doc . constructor . modelName
props = ( doc . schema . statics . editableProperties or [ ] ) . slice ( )
2016-02-22 18:08:58 -05:00
if doc . isNew
2016-08-24 18:46:35 -04:00
props = props . concat ( doc . schema . statics . postEditableProperties or [ ] )
if not doc . schema . statics . postEditableProperties
console . warn ' No postEditableProperties set for ' , doc . constructor . modelName
2016-02-22 18:08:58 -05:00
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
2016-03-09 19:59:25 -05:00
result = tv4 . validateMultiple ( obj , doc . schema . statics . jsonSchema )
2016-02-22 18:08:58 -05:00
if not result . valid
2016-04-13 12:54:24 -04:00
prunedErrors = ( _ . omit ( error , ' stack ' ) for error in result . errors )
winston . debug ( ' Validation errors: ' , JSON . stringify ( prunedErrors , null , ' \t ' ) )
2016-02-22 18:08:58 -05:00
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 } )
2016-08-23 17:36:45 -04:00
if options . select
dbq . select ( options . select )
2016-02-22 18:08:58 -05:00
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
2014-02-04 15:30:05 -05:00