2014-01-03 10:32:13 -08:00
mongoose = require ( ' mongoose ' )
textSearch = require ( ' mongoose-text-search ' )
2014-06-10 13:20:14 -07:00
module.exports.MigrationPlugin = (schema, migrations) ->
# Property name migrations made EZ
# This is for just when you want one property to be named differently
# 1. Change the schema and the client/server logic to use the new name
# 2. Add this plugin to the target models, passing in a dictionary of old/new names.
# 3. Check that tests still run, deploy to production.
2014-06-10 15:03:29 -07:00
# 4. Run db.<collection>.update({}, { $rename: {'<oldname>':'<newname>'} }, { multi: true }) on the server
2014-06-10 13:20:14 -07:00
# 5. Remove the names you added to the migrations dictionaries for the next deploy
schema . post ' init ' , ->
for oldKey in _ . keys migrations
val = @ get oldKey
@ set oldKey , undefined
continue if val is undefined
newKey = migrations [ oldKey ]
@ set newKey , val
2014-04-08 19:26:19 -07:00
module.exports.PatchablePlugin = (schema) ->
schema.is_patchable = true
schema . index ( { ' target.original ' : 1 , ' status ' : ' 1 ' , ' created ' : - 1 } )
2014-04-28 14:09:29 -07:00
2014-06-12 19:19:18 +02:00
RESERVED_NAMES = [ ' names ' ]
2014-04-08 19:26:19 -07:00
2014-01-03 10:32:13 -08:00
module.exports.NamedPlugin = (schema) ->
2014-04-28 14:09:29 -07:00
schema.uses_coco_names = true
2014-01-03 10:32:13 -08:00
schema . add ( { name: String , slug: String } )
schema . index ( { ' slug ' : 1 } , { unique: true , sparse: true , name: ' slug index ' } )
schema . pre ( ' save ' , (next) ->
if schema . uses_coco_versions
v = @ get ( ' version ' )
return next ( ) unless v . isLatestMajor and v . isLatestMinor
newSlug = _ . str . slugify ( @ get ( ' name ' ) )
2014-04-28 14:09:29 -07:00
if newSlug in RESERVED_NAMES
err = new Error ( ' Reserved name. ' )
err.response = { message : ' is a reserved name ' , property: ' name ' }
err.code = 422
return next ( err )
2014-01-03 10:32:13 -08:00
if newSlug isnt @ get ( ' slug ' )
@ set ( ' slug ' , newSlug )
@ checkSlugConflicts ( next )
else
next ( )
)
schema.methods.checkSlugConflicts = (done) ->
slug = @ get ( ' slug ' )
2014-04-11 20:38:34 -07:00
if slug . length is 24 and slug . match ( /[a-f0-9]/gi ) ? . length is 24
2014-01-03 10:32:13 -08:00
err = new Error ( ' Bad name. ' )
2014-04-11 20:38:34 -07:00
err.response = { message: ' cannot be like a MongoDB ID, Mr. Hacker. ' , property: ' name ' }
2014-01-03 10:32:13 -08:00
err.code = 422
done ( err )
query = { slug : slug }
if @ get ( ' original ' )
query.original = { ' $ne ' : @ original }
else if @ _id
query._id = { ' $ne ' : @ _id }
@ model ( @ constructor . modelName ) . count query , (err, count) ->
if count
err = new Error ( ' Slug conflict. ' )
err.response = { message : ' is already in use ' , property : ' name ' }
err.code = 409
done ( err )
done ( )
module.exports.PermissionsPlugin = (schema) ->
schema.uses_coco_permissions = true
PermissionSchema = new mongoose . Schema
target: mongoose . Schema . Types . Mixed
access: { type: String , ' enum ' : [ ' read ' , ' write ' , ' owner ' ] }
, { id: false , _id: false }
schema . add ( permissions: [ PermissionSchema ] )
schema . pre ' save ' , (next) ->
return next ( ) if @ getOwner ( )
err = new Error ( ' Permissions needs an owner. ' )
err.response = { message : ' needs an owner. ' , property : ' permissions ' }
err.code = 409
next ( err )
schema.methods.hasPermissionsForMethod = (actor, method) ->
method = method . toLowerCase ( )
# method is 'get', 'put', 'patch', 'post', or 'delete'
# actor is a User object
allowed =
get: [ ' read ' , ' write ' , ' owner ' ]
put: [ ' write ' , ' owner ' ]
patch: [ ' write ' , ' owner ' ]
post: [ ' write ' , ' owner ' ] # used to post new versions of something
delete : [ ] # nothing may go!
allowed = allowed [ method ] or [ ]
for permission in @ permissions
if permission . target is ' public ' or actor . _id . equals ( permission . target )
return true if permission . access in allowed
return false
schema.methods.getOwner = ->
for permission in @ permissions
if permission . access is ' owner '
return permission . target
schema.methods.getPublicAccess = ->
for permission in @ permissions
if permission . target is ' public '
return permission . access
schema.methods.getAccessForUserObjectId = (objectId) ->
public_access = null
for permission in @ permissions
if permission . target is ' public '
public_access = permission . access
continue
if objectId . equals ( permission . target )
return permission . access
return public_access
module.exports.VersionedPlugin = (schema) ->
schema.uses_coco_versions = true
schema . add (
version:
major: { type: Number , ' default ' : 0 }
minor: { type: Number , ' default ' : 0 }
isLatestMajor: { type: Boolean , ' default ' : true }
isLatestMinor: { type: Boolean , ' default ' : true }
original: { type: mongoose . Schema . ObjectId , ref: @ modelName }
parent: { type: mongoose . Schema . ObjectId , ref: @ modelName }
creator: { type: mongoose . Schema . ObjectId , ref: ' User ' }
created: { type: Date , ' default ' : Date . now }
commitMessage: { type: String }
)
# Prevent multiple documents with the same version
# Also used for looking up latest version, or specific versions.
schema . index ( { ' original ' : 1 , ' version.major ' : - 1 , ' version.minor ' : - 1 } , { unique: true , name: ' version index ' } )
schema.statics.getLatestMajorVersion = (original, options, done) ->
options = options or { }
query = @ findOne ( { original : original , ' version.isLatestMajor ' : true } )
query . select ( options . select ) if options . select
query . exec ( (err, latest) =>
return done ( err ) if err
return done ( null , latest ) if latest
# handle the case where no version is marked as the latest
q = @ find ( { original : original } )
q . sort ( { ' version.major ' : - 1 , ' version.minor ' : - 1 } )
q . select ( options . select ) if options . select
q . limit ( 1 )
q . exec ( (err, latest) =>
return done ( err ) if err
return done ( null , null ) if latest . length is 0
latest = latest [ 0 ]
# don't fix missing versions by default. In all likelihood, it's about to change anyway
if options . autofix
latest.version.isLatestMajor = true
latest.version.isLatestMinor = true
latestObject = latest . toObject ( )
@ update ( { _id: latest . _id } , { version: latestObject . version } )
done ( null , latest )
)
)
schema.statics.getLatestMinorVersion = (original, majorVersion, options, done) ->
options = options or { }
query = @ findOne ( { original : original , ' version.isLatestMinor ' : true , ' version.major ' : majorVersion } )
query . select ( options . select ) if options . select
query . exec ( (err, latest) =>
return done ( err ) if err
return done ( null , latest ) if latest
q = @ find ( { original : original , ' version.major ' : majorVersion } )
q . sort ( { ' version.minor ' : - 1 } )
q . select ( options . select ) if options . select
q . limit ( 1 )
q . exec ( (err, latest) ->
return done ( err ) if err
return done ( null , null ) if latest . length is 0
latest = latest [ 0 ]
if options . autofix
latestObject = latest . toObject ( )
latestObject.version.isLatestMajor = true
latestObject.version.isLatestMinor = true
@ update ( { _id: latest . _id } , { version: latestObject . version } )
done ( null , latest )
)
)
schema.methods.makeNewMajorVersion = (newObject, done) ->
Model = @ model ( @ constructor . modelName )
latest = Model . getLatestMajorVersion ( @ original , { select : ' version ' } , (err, latest) =>
return done ( err ) if err
updatedObject = _ . cloneDeep latestObject
# unmark the current latest major version in the database
latestObject = latest . toObject ( )
latestObject.version.isLatestMajor = false
Model . update ( { _id: latest . _id } , { version: latestObject . version , $unset: { index : 1 , slug: 1 } } , { } , (err) =>
return done ( err ) if err
newObject [ ' version ' ] = { major: latest . version . major + 1 }
newObject.index = true
newObject.parent = @ _id
delete newObject [ ' _id ' ]
delete newObject [ ' created ' ]
done ( null , new Model ( newObject ) )
)
)
schema.methods.makeNewMinorVersion = (newObject, majorVersion, done) ->
Model = @ model ( @ constructor . modelName )
latest = Model . getLatestMinorVersion ( @ original , majorVersion , { select : ' version ' } , (err, latest) =>
return done ( err ) if err
# unmark the current latest major version in the database
latestObject = latest . toObject ( )
wasLatestMajor = latestObject . version . isLatestMajor
latestObject.version.isLatestMajor = false
latestObject.version.isLatestMinor = false
Model . update ( { _id: latest . _id } , { version: latestObject . version , $unset: { index : 1 , slug: 1 } } , { } , (err) =>
return done ( err ) if err
newObject [ ' version ' ] =
major: latest . version . major
minor: latest . version . minor + 1
isLatestMajor: wasLatestMajor
if wasLatestMajor
newObject.index = true
else
delete newObject . index if newObject . index ?
delete newObject . slug if newObject . slug ?
newObject.parent = @ _id
delete newObject [ ' _id ' ]
delete newObject [ ' created ' ]
done ( null , new Model ( newObject ) )
)
)
module.exports.SearchablePlugin = (schema, options) ->
# this plugin must be added only after the others (specifically Versioned and Permissions)
# have been added, as how it builds the text search index depends on which of those are used.
searchable = options . searchable
unless searchable
throw Error ( ' SearchablePlugin options must include list of searchable properties. ' )
index = { }
schema.uses_coco_search = true
if schema . uses_coco_versions or schema . uses_coco_permissions
index [ ' index ' ] = 1
schema . add ( index: mongoose . Schema . Types . Mixed )
index [ prop ] = ' text ' for prop in searchable
# should now have something like {'index': 1, name:'text', body:'text'}
schema . plugin ( textSearch )
schema . index ( index , { sparse: true , name: ' search index ' , language_override: ' searchLanguage ' } )
schema . pre ' save ' , (next) ->
# never index old versions, index plugin handles un-indexing old versions
if schema . uses_coco_versions and ( ( not @ version . isLatestMajor ) or ( not @ version . isLatestMinor ) )
return next ( )
@index = true
if schema . uses_coco_permissions
access = @ getPublicAccess ( )
@index = @ getOwner ( ) unless access
next ( )