Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Mongoose 7 #47

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 55 additions & 66 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
## mongoose-id-validator

Provides a mongoose plugin that can be used to verify that a document which references
Provides a mongoose plugin that can be used to verify that a document which references
other documents by their ID is referring to documents that actually exist.

This plugin works by performing a count query for documents in the relevant collection with the
This plugin works by performing a count query for documents in the relevant collection with the
ID specified in the document being validated. Note that the validation method is only executed if the ID
value has been updated. This helps prevent unnecessary queries when saving a document if the ID has not been changed.

However, please be aware that this will NOT catch any IDs that were valid at the time they were saved but the referenced
document has subsequently been removed. You should have validation logic in your delete/remove code to handle this.
However, please be aware that this will NOT catch any IDs that were valid at the time they were saved but the referenced
document has subsequently been removed. You should have validation logic in your delete/remove code to handle this.

# Usage

Expand All @@ -27,43 +27,38 @@ var ManufacturerSchema = new Schema({
var Manufacturer = mongoose.model('Manufacturer', ManufacturerSchema);

var CarSchema = new Schema({
name : String,
manufacturer : {
type: Schema.Types.ObjectId,
ref: 'Manufacturer',
required: true
}
name: String,
manufacturer: {
type: Schema.Types.ObjectId,
ref: 'Manufacturer',
required: true
}
});
CarSchema.plugin(idvalidator);
var Car = mongoose.model('Car', CarSchema);

var ford = new ManufacturerSchema({ name : 'Ford' });

ford.save(function() {
var focus = new Car({ name : 'Focus' });
focus.manufacturer = "50136e40c78c4b9403000001";

focus.validate(function(err) {
//err.errors would contain a validation error for manufacturer with default message

focus.manufacturer = ford;
focus.validate(function(err) {
//err will now be null as validation will pass
});
});
});
await ford.save()
var focus = new Car({ name : 'Focus' });
focus.manufacturer = "50136e40c78c4b9403000001";

await focus.validate() // err.errors would contain a validation error for manufacturer with default message

focus.manufacturer = ford;
await focus.validate() // err will now be null as validation will pass
```

You may also use declare a optional refConditions method in your schema. For example:
```javascript
var OtherSchema = new Schema({
referencedId : {
type: Schema.Types.ObjectId,
ref: 'RefSchema',
refConditions: {
field1: 123
}
}
referencedId : {
type: Schema.Types.ObjectId,
ref: 'RefSchema',
refConditions: {
field1: 123
}
}
});
```

Expand All @@ -74,18 +69,18 @@ These functions have access to the context of the schema instance. An example:
var OtherSchema = new Schema({
refFieldMustBeTrue: true,
referencedId : {
type: Schema.Types.ObjectId,
ref: 'RefSchema',
refConditions: {
field1: function () {
return this.refFieldMustBeTrue
}
}
}
type: Schema.Types.ObjectId,
ref: 'RefSchema',
refConditions: {
field1: function () {
return this.refFieldMustBeTrue
}
}
}
});
```

The referenceId value in the code above would only pass validation if the object with this ID exists AND had a property
The referenceId value in the code above would only pass validation if the object with this ID exists AND had a property
'field1' that has the value 123. If any conditional property does not match then it would not pass validation.

You can also use this plugin to validate an array of ID references. Please note as above that the implementation
Expand All @@ -102,41 +97,35 @@ var ColourSchema = new Schema({
var Colour = mongoose.model('Colour', ColourSchema);

var CarSchema = new Schema({
name : String,
colours : [{
type: Schema.Types.ObjectId,
ref: 'Colour'
}]
name: String,
colours: [{
type: Schema.Types.ObjectId,
ref: 'Colour'
}]
});
CarSchema.plugin(idvalidator);
var Car = mongoose.model('Car', CarSchema);

var red = new Colour({ name : 'Red' });
var blue = new Colour({ name : 'Blue' });

red.save(function() {
blue.save(function() {
var focus = new Car({ name : 'Focus' });
focus.colours = [red, "50136e40c78c4b9403000001"];

focus.validate(function(err) {
//err.errors would contain a validation error for colours with default message

focus.colours = [red, blue]
focus.validate(function(err) {
//err will now be null as validation will pass
});
});
});
});
await red.save()
await blue.save()
var focus = new Car({ name : 'Focus' });
focus.colours = [red, "50136e40c78c4b9403000001"];

await focus.validate() // err.errors would contain a validation error for colours with default message

focus.colours = [red, blue]
await focus.validate() // err will now be null as validation will pass
```

## Options

```javascript
Model.plugin(id-validator, {
/* Custom validation message with {PATH} being replaced
* with the relevant schema path that contains an invalid
/* Custom validation message with {PATH} being replaced
* with the relevant schema path that contains an invalid
* document ID.
*/
message : 'Bad ID value for {PATH}',
Expand All @@ -147,7 +136,7 @@ Model.plugin(id-validator, {
* Defaults to built-in mongoose connection if not specified.
*/
connection: myConnection

/* Applies to validation of arrays of ID references only. Set
* to true if you sometimes have the same object ID reference
* repeated in an array. If set, the validator will use the
Expand Down Expand Up @@ -182,16 +171,16 @@ validator.enable();
To run the tests you need a local MongoDB instance available. Run with:

npm test

# Issues

Please use the GitHub issue tracker to raise any problems or feature requests.

If you would like to submit a pull request with any changes you make, please feel free!

# Legal

Code is Copyright (C) Campbell Software Solutions 2014 - 2017.

This module is available under terms of the LGPL V3 license. Hence you can use it in other proprietary projects
but any changes to the library should be made available.
This module is available under terms of the LGPL V3 license. Hence you can use it in other proprietary projects
but any changes to the library should be made available.
110 changes: 46 additions & 64 deletions lib/id-validator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
var mongoose = require('mongoose')
var traverse = require('traverse')
var clone = require('clone')
var Schema = mongoose.Schema

function IdValidator () {
this.enabled = true
Expand Down Expand Up @@ -40,24 +39,23 @@ IdValidator.prototype.validateSchema = function (
}

var validateFunction = null
var refModelName = null
var refModelPath = null
var ref = null
var refPath = null
var conditions = {}
var optionsSource = null

if (schemaType.options && schemaType.options.ref) {
refModelName = schemaType.options.ref
ref = schemaType.options.ref
if (schemaType.options.refConditions) {
conditions = schemaType.options.refConditions
}
} else if (schemaType.options && schemaType.options.refPath) {
refModelPath = schemaType.options.refPath
refPath = schemaType.options.refPath
if (schemaType.options.refConditions) {
conditions = schemaType.options.refConditions
}
} else if (schemaType.caster && schemaType.caster.instance &&
schemaType.caster.options && schemaType.caster.options.ref) {
refModelName = schemaType.caster.options.ref
ref = schemaType.caster.options.ref
if (schemaType.caster.options.refConditions) {
conditions = schemaType.caster.options.refConditions
}
Expand All @@ -69,80 +67,66 @@ IdValidator.prototype.validateSchema = function (
(schemaType['$isMongooseArray'] === true)
validateFunction = isArraySchemaType ? validateIdArray : validateId

if (refModelName || refModelPath) {
if (ref || refPath) {
schema.path(path).validate({
validator: function (value) {
return new Promise(function (resolve, reject) {
var conditionsCopy = conditions
//A query may not implement an isModified function.
if (this && !!this.isModified && !this.isModified(path)) {
resolve(true)
return
var conditionsCopy = conditions
//A query may not implement an isModified function.
if (this && !!this.isModified && !this.isModified(path)) {
return true
}
if (!(self instanceof IdValidator) || self.enabled) {
if (Object.keys(conditionsCopy).length > 0) {
var instance = this

conditionsCopy = clone(conditions)
traverse(conditionsCopy).forEach(function (value) {
if (typeof value === 'function') {
this.update(value.call(instance))
}
})
}
if (!(self instanceof IdValidator) || self.enabled) {
if (Object.keys(conditionsCopy).length > 0) {
var instance = this

conditionsCopy = clone(conditions)
traverse(conditionsCopy).forEach(function (value) {
if (typeof value === 'function') {
this.update(value.call(instance))
}
})
}
var localRefModelName = refModelName
if (refModelPath) {
localRefModelName = this[refModelPath]
}

return validateFunction(this, connection, localRefModelName,
value, conditionsCopy, resolve, reject, allowDuplicates)
var localRefModelName =
typeof ref === 'function' ? ref.call(this, value) : ref
if (refPath) {
const refModelPath =
typeof refPath === 'function' ? refPath.call(this, value) : refPath
localRefModelName = this[refModelPath]
}
resolve(true)
return
}.bind(this));

return validateFunction(this, connection, localRefModelName,
value, conditionsCopy, allowDuplicates)
}
return true
},
message: message
})
}
})
}

function executeQuery (query, conditions, validateValue, resolve, reject) {
async function executeQuery (query, conditions, validateValue) {
for (var fieldName in conditions) {
query.where(fieldName, conditions[fieldName])
}
query.exec(function (err, count) {
if (err) {
reject(err)
return
}
return count === validateValue ? resolve(true) : resolve(false)
})
query.setOptions({ readPreference: 'primary' })
const count = await query.exec()
return count === validateValue
}

function validateId (
doc, connection, refModelName, value, conditions, resolve, reject) {
if (value == null) {
resolve(true)
return
}
function validateId (doc, connection, refModelName, value, conditions) {
if (value == null) return true

var refModel = connection.model(refModelName)
var query = refModel.countDocuments({_id: value})
var session = doc.$session && doc.$session()
if (session) {
query.session(session)
}
executeQuery(query, conditions, 1, resolve, reject)
if (session) query.session(session)

return executeQuery(query, conditions, 1)
}

function validateIdArray (
doc, connection, refModelName, values, conditions, resolve, reject,
allowDuplicates) {
if (values == null || values.length == 0) {
resolve(true)
return
}
function validateIdArray (doc, connection, refModelName, values, conditions, allowDuplicates) {
if (values == null || values.length == 0) return true

var checkValues = values
if (allowDuplicates) {
Expand All @@ -155,11 +139,9 @@ function validateIdArray (
var refModel = connection.model(refModelName)
var query = refModel.countDocuments().where('_id')['in'](checkValues)
var session = doc.$session && doc.$session()
if (session) {
query.session(session)
}
if (session) query.session(session)

executeQuery(query, conditions, checkValues.length, resolve, reject)
return executeQuery(query, conditions, checkValues.length)
}

module.exports = IdValidator.prototype.validate
Expand Down
Loading