2014-01-03 13:32:13 -05:00
|
|
|
mongoose = require('mongoose')
|
2014-06-27 15:30:31 -04:00
|
|
|
log = require 'winston'
|
2014-08-14 10:38:50 -04:00
|
|
|
utils = require '../lib/utils'
|
2014-01-03 13:32:13 -05:00
|
|
|
|
2014-06-10 16:20:14 -04: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-30 22:16:26 -04:00
|
|
|
# 4. Run db.<collection>.update({}, {$rename: {'<oldname>': '<newname>'}}, {multi: true}) on the server
|
2014-06-10 16:20:14 -04: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 22:26:19 -04:00
|
|
|
module.exports.PatchablePlugin = (schema) ->
|
|
|
|
schema.is_patchable = true
|
2014-06-30 22:16:26 -04:00
|
|
|
schema.index({'target.original': 1, 'status': '1', 'created': -1})
|
|
|
|
|
2014-06-12 13:19:18 -04:00
|
|
|
RESERVED_NAMES = ['names']
|
2014-04-08 22:26:19 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
module.exports.NamedPlugin = (schema) ->
|
2014-04-28 17:09:29 -04:00
|
|
|
schema.uses_coco_names = true
|
2014-01-03 13:32:13 -05:00
|
|
|
schema.add({name: String, slug: String})
|
|
|
|
schema.index({'slug': 1}, {unique: true, sparse: true, name: 'slug index'})
|
|
|
|
|
2014-08-14 10:38:50 -04:00
|
|
|
schema.statics.findBySlug = (slug, done) ->
|
2014-07-29 06:45:47 -04:00
|
|
|
@findOne {slug: slug}, done
|
2014-07-28 15:25:11 -04:00
|
|
|
|
2014-08-14 10:38:50 -04:00
|
|
|
schema.statics.findBySlugOrId = (slugOrID, done) ->
|
|
|
|
return @findById slugOrID, done if utils.isID slugOrID
|
|
|
|
@findOne {slug: slugOrID}, done
|
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
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 17:09:29 -04:00
|
|
|
if newSlug in RESERVED_NAMES
|
|
|
|
err = new Error('Reserved name.')
|
2014-06-30 22:16:26 -04:00
|
|
|
err.response = {message: ' is a reserved name', property: 'name'}
|
2014-04-28 17:09:29 -04:00
|
|
|
err.code = 422
|
|
|
|
return next(err)
|
2014-07-10 04:46:34 -04:00
|
|
|
if newSlug not in [@get('slug'), ''] and not @get 'anonymous'
|
2014-01-03 13:32:13 -05:00
|
|
|
@set('slug', newSlug)
|
|
|
|
@checkSlugConflicts(next)
|
2014-07-10 04:46:34 -04:00
|
|
|
else if newSlug is '' and @get 'slug'
|
|
|
|
@set 'slug', undefined
|
2014-07-10 12:24:02 -04:00
|
|
|
next()
|
2014-01-03 13:32:13 -05:00
|
|
|
else
|
|
|
|
next()
|
|
|
|
)
|
|
|
|
|
|
|
|
schema.methods.checkSlugConflicts = (done) ->
|
|
|
|
slug = @get('slug')
|
|
|
|
|
2014-04-11 23:38:34 -04:00
|
|
|
if slug.length is 24 and slug.match(/[a-f0-9]/gi)?.length is 24
|
2014-01-03 13:32:13 -05:00
|
|
|
err = new Error('Bad name.')
|
2014-04-11 23:38:34 -04:00
|
|
|
err.response = {message: 'cannot be like a MongoDB ID, Mr. Hacker.', property: 'name'}
|
2014-01-03 13:32:13 -05:00
|
|
|
err.code = 422
|
|
|
|
done(err)
|
|
|
|
|
2014-06-30 22:16:26 -04:00
|
|
|
query = {slug: slug}
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
if @get('original')
|
2014-06-30 22:16:26 -04:00
|
|
|
query.original = {'$ne': @original}
|
2014-01-03 13:32:13 -05:00
|
|
|
else if @_id
|
2014-06-30 22:16:26 -04:00
|
|
|
query._id = {'$ne': @_id}
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
@model(@constructor.modelName).count query, (err, count) ->
|
|
|
|
if count
|
|
|
|
err = new Error('Slug conflict.')
|
2014-06-30 22:16:26 -04:00
|
|
|
err.response = {message: 'is already in use', property: 'name'}
|
2014-01-03 13:32:13 -05:00
|
|
|
err.code = 409
|
|
|
|
done(err)
|
|
|
|
done()
|
|
|
|
|
|
|
|
module.exports.PermissionsPlugin = (schema) ->
|
|
|
|
schema.uses_coco_permissions = true
|
|
|
|
|
|
|
|
PermissionSchema = new mongoose.Schema
|
|
|
|
target: mongoose.Schema.Types.Mixed
|
2014-06-30 22:16:26 -04:00
|
|
|
access: {type: String, 'enum': ['read', 'write', 'owner']}
|
2014-01-03 13:32:13 -05:00
|
|
|
, {id: false, _id: false}
|
|
|
|
|
|
|
|
schema.add(permissions: [PermissionSchema])
|
|
|
|
|
|
|
|
schema.pre 'save', (next) ->
|
|
|
|
return next() if @getOwner()
|
|
|
|
err = new Error('Permissions needs an owner.')
|
2014-06-30 22:16:26 -04:00
|
|
|
err.response = {message: 'needs an owner.', property: 'permissions'}
|
2014-01-03 13:32:13 -05:00
|
|
|
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
|
2014-11-22 21:40:28 -05:00
|
|
|
if permission.target is 'public' or actor?._id.equals(permission.target)
|
2014-01-03 13:32:13 -05:00
|
|
|
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'}
|
2014-06-30 22:16:26 -04:00
|
|
|
created: {type: Date, 'default': Date.now}
|
2014-01-03 13:32:13 -05:00
|
|
|
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 {}
|
2014-06-30 22:16:26 -04:00
|
|
|
query = @findOne({original: original, 'version.isLatestMajor': true})
|
2014-01-03 13:32:13 -05:00
|
|
|
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
|
2014-06-30 22:16:26 -04:00
|
|
|
q = @find({original: original})
|
|
|
|
q.sort({'version.major': -1, 'version.minor': -1})
|
2014-01-03 13:32:13 -05:00
|
|
|
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
|
2015-12-16 20:09:22 -05:00
|
|
|
if options.autofix # not used
|
2014-01-03 13:32:13 -05:00
|
|
|
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 {}
|
2014-06-30 22:16:26 -04:00
|
|
|
query = @findOne({original: original, 'version.isLatestMinor': true, 'version.major': majorVersion})
|
2014-01-03 13:32:13 -05:00
|
|
|
query.select(options.select) if options.select
|
|
|
|
query.exec((err, latest) =>
|
|
|
|
return done(err) if err
|
|
|
|
return done(null, latest) if latest
|
2014-06-30 22:16:26 -04:00
|
|
|
q = @find({original: original, 'version.major': majorVersion})
|
|
|
|
q.sort({'version.minor': -1})
|
2014-01-03 13:32:13 -05:00
|
|
|
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]
|
|
|
|
|
2015-12-16 20:09:22 -05:00
|
|
|
if options.autofix # not used
|
2014-01-03 13:32:13 -05:00
|
|
|
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)
|
|
|
|
|
2014-06-30 22:16:26 -04:00
|
|
|
latest = Model.getLatestMajorVersion(@original, {select: 'version'}, (err, latest) =>
|
2014-01-03 13:32:13 -05:00
|
|
|
return done(err) if err
|
|
|
|
|
|
|
|
updatedObject = _.cloneDeep latestObject
|
|
|
|
# unmark the current latest major version in the database
|
|
|
|
latestObject = latest.toObject()
|
|
|
|
latestObject.version.isLatestMajor = false
|
2014-06-30 22:16:26 -04:00
|
|
|
Model.update({_id: latest._id}, {version: latestObject.version, $unset: {index: 1, slug: 1}}, {}, (err) =>
|
2014-01-03 13:32:13 -05:00
|
|
|
return done(err) if err
|
|
|
|
|
2014-06-30 22:16:26 -04:00
|
|
|
newObject['version'] = {major: latest.version.major + 1}
|
2014-01-03 13:32:13 -05:00
|
|
|
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)
|
|
|
|
|
2014-06-30 22:16:26 -04:00
|
|
|
latest = Model.getLatestMinorVersion(@original, majorVersion, {select: 'version'}, (err, latest) =>
|
2014-01-03 13:32:13 -05:00
|
|
|
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
|
2014-06-30 22:16:26 -04:00
|
|
|
Model.update({_id: latest._id}, {version: latestObject.version, $unset: {index: 1, slug: 1}}, {}, (err) =>
|
2014-01-03 13:32:13 -05:00
|
|
|
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))
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
2014-07-13 15:32:07 -04:00
|
|
|
# Assume every save is a new version, hence an edit
|
2014-06-25 14:04:39 -04:00
|
|
|
schema.pre 'save', (next) ->
|
2016-04-06 13:56:06 -04:00
|
|
|
User = require '../models/User' # Avoid mutual inclusion cycles
|
2014-06-27 15:30:31 -04:00
|
|
|
userID = @get('creator')?.toHexString()
|
|
|
|
return next() unless userID?
|
2014-06-25 14:04:39 -04:00
|
|
|
|
2014-06-27 15:30:31 -04:00
|
|
|
statName = User.statsMapping.edits[@constructor.modelName]
|
2014-07-13 15:32:07 -04:00
|
|
|
User.incrementStat userID, statName, next
|
2014-06-25 14:04:39 -04:00
|
|
|
|
2014-01-03 13:32:13 -05:00
|
|
|
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
|
2014-10-17 12:12:06 -04:00
|
|
|
throw new Error('SearchablePlugin options must include list of searchable properties.')
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2014-06-30 22:16:26 -04:00
|
|
|
# should now have something like {'index': 1, name: 'text', body: 'text'}
|
|
|
|
schema.index(index, {sparse: true, name: 'search index', language_override: 'searchLanguage'})
|
2014-01-03 13:32:13 -05:00
|
|
|
|
|
|
|
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()
|
2014-10-17 12:12:06 -04:00
|
|
|
|
|
|
|
module.exports.TranslationCoveragePlugin = (schema, options) ->
|
2014-12-28 16:25:20 -05:00
|
|
|
|
2014-10-17 12:12:06 -04:00
|
|
|
schema.uses_coco_translation_coverage = true
|
|
|
|
schema.set('autoIndex', true)
|
2014-12-28 16:25:20 -05:00
|
|
|
|
2014-10-17 12:12:06 -04:00
|
|
|
index = {}
|
2014-12-28 16:25:20 -05:00
|
|
|
|
2014-10-17 12:12:06 -04:00
|
|
|
if schema.uses_coco_versions
|
|
|
|
if not schema.uses_coco_names
|
|
|
|
throw Error('If using translation coverage and versioning, should also use names for indexing.')
|
|
|
|
index.slug = 1
|
2014-12-28 16:25:20 -05:00
|
|
|
|
2014-10-17 12:12:06 -04:00
|
|
|
index.i18nCoverage = 1
|
2014-12-28 16:25:20 -05:00
|
|
|
|
|
|
|
schema.index(index, {sparse: true, name: 'translation coverage index', background: true})
|