diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3e2de84 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +**/node_modules/** +node_modules/** +**/vendors/** +coverage +test/*.test.js +test/util.js +test/data.js \ No newline at end of file diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b62a31a --- /dev/null +++ b/.eslintrc @@ -0,0 +1,154 @@ +{ + "parserOptions": { + "ecmaVersion": 6 + }, + "env": { + "browser": true, + "node": true, + "mocha": true + }, + "rules": { + "arrow-spacing": [2, { "before": true, "after": true }], + "constructor-super":2, + "generator-star-spacing": [2, {"before": false, "after": true}], + "no-class-assign": 2, + "no-const-assign": 2, + "no-dupe-class-members": 2, + "no-this-before-super": 2, + "no-useless-constructor": 2, + "object-shorthand": 2, + "prefer-arrow-callback": 2, + "prefer-const": 2, + "prefer-rest-params": 2, + "prefer-spread": 2, + "prefer-template": 2, + "require-yield": 2, + "template-curly-spacing": [2, "never"], + + "array-bracket-spacing": [2, "never"], + "block-spacing": [2, "always"], + "brace-style": [2, "1tbs"], + "comma-spacing": [2, {"before": false, "after": true}], + "comma-style": [2, "last"], + "computed-property-spacing": [2, "never"], + "consistent-this": [2, "self"], + "eol-last": 2, + "indent": [2, 2, {"SwitchCase": 1}], + "key-spacing": [2, {"beforeColon": false, "afterColon": true}], + "keyword-spacing": [2, {"before": true, "after": true}], + "linebreak-style": [2, "unix"], + "lines-around-comment": [2, { "beforeBlockComment": true, "beforeLineComment": false }], + "max-depth": [2, 4], + "max-nested-callbacks": [2, 2], + "max-params": [2, 4], + "new-cap": 2, + "new-parens": 2, + "no-array-constructor": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [2, {"max": 1}], + "no-nested-ternary": 2, + "no-new-object": 2, + "no-whitespace-before-property": 2, + "no-spaced-func": 2, + "no-trailing-spaces": 2, + "no-underscore-dangle": [2, { "allowAfterThis": true, "allow": ["_id", "_created"] }], + "no-unneeded-ternary": 2, + "object-curly-spacing": [2, "never"], + "one-var": [2, { + "uninitialized": "always", + "initialized": "never" + }], + "operator-assignment": [2, "always"], + "operator-linebreak": [2, "after"], + "quote-props": [2, "as-needed"], + "quotes": [2, "single"], + "require-jsdoc": 2, + "semi-spacing": [2, {"before": false, "after": true}], + "semi": 2, + "space-before-blocks": 2, + "space-before-function-paren": [2, "never"], + "space-in-parens": [2, "never"], + "space-infix-ops": 2, + "space-unary-ops": [2, { "words": true, "nonwords": false }], + "spaced-comment": [2, "always"], + + "handle-callback-err": 2, + "no-mixed-requires": [1, true], + "no-new-require": 2, + "no-path-concat": 2, + "no-sync": 2, + + "strict": [2, "global"], + + "complexity": [2, 10], + "consistent-return": 2, + "curly": [2, "all"], + "dot-location": [2, "property"], + "dot-notation": [2, {"allowKeywords": true}], + "eqeqeq": 2, + "guard-for-in": 2, + "no-alert": 2, + "no-caller": 2, + "no-else-return": 2, + "no-eq-null": 2, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-implicit-coercion": 2, + "no-implicit-globals": 2, + "no-implied-eval": 2, + "no-invalid-this": 2, + "no-iterator": 2, + "no-labels": 2, + "no-lone-blocks": 2, + "no-loop-func": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-native-reassign": 2, + "no-new-func": 2, + "no-new-wrappers": 2, + "no-new": 2, + "no-process-env": 2, + "no-proto": 2, + "no-redeclare": 2, + "no-return-assign": 2, + "no-self-assign": 2, + "no-self-compare": 2, + "no-sequences": 2, + "no-throw-literal": 2, + "no-unmodified-loop-condition": 2, + "no-unused-expressions": 2, + "no-useless-call": 2, + "no-useless-concat": 2, + "no-void": 2, + "no-with": 2, + "radix": [2, "always"], + "wrap-iife": [2, "outside"], + "yoda": [2, "never"], + + "comma-dangle": [2, "never"], + "no-cond-assign": 2, + "no-console": 2, + "no-constant-condition": 2, + "no-debugger": 2, + "no-dupe-args": 2, + "no-dupe-keys": 2, + "no-duplicate-case": 2, + "no-empty-character-class": 2, + "no-empty": 2, + "no-ex-assign": 2, + "no-extra-boolean-cast": 2, + "no-extra-semi": 2, + "no-func-assign": 2, + "no-inner-declarations": 2, + "no-invalid-regexp": 2, + "no-irregular-whitespace": 2, + "no-negated-in-lhs": 2, + "no-obj-calls": 2, + "no-sparse-arrays": 2, + "no-unexpected-multiline": 2, + "no-unreachable": 2, + "use-isnan": 2, + "valid-typeof": 2 + } +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index a721a0d..d84975e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ notes.txt node_modules/ npm-debug.log .jshintrc +.idea/ diff --git a/lib/base-document.js b/lib/base-document.js index 93fd526..c7be1ea 100644 --- a/lib/base-document.js +++ b/lib/base-document.js @@ -1,541 +1,614 @@ -"use strict"; - -var _ = require('lodash'); -var deprecate = require('depd')('camo'); -var DB = require('./clients').getClient; -var isSupportedType = require('./validate').isSupportedType; -var isValidType = require('./validate').isValidType; -var isEmptyValue = require('./validate').isEmptyValue; -var isInChoices = require('./validate').isInChoices; -var isArray = require('./validate').isArray; -var isDocument = require('./validate').isDocument; -var isEmbeddedDocument = require('./validate').isEmbeddedDocument; -var isString = require('./validate').isString; -var isNumber = require('./validate').isNumber; -var isDate = require('./validate').isDate; -var ValidationError = require('./errors').ValidationError; - -var normalizeType = function(property) { - // TODO: Only copy over stuff we support - - var typeDeclaration = {}; - if (property.type) { - typeDeclaration = property; - } else if (isSupportedType(property)) { - typeDeclaration.type = property; - } else { - throw new Error('Unsupported type or bad variable. ' + - 'Remember, non-persisted objects must start with an underscore (_). Got:', property); - } - - return typeDeclaration; +'use strict'; + +const _ = require('lodash'); +const deprecate = require('depd')('camo'); +const db = require('./clients').getClient; +const isSupportedType = require('./validate').isSupportedType; +const isValidType = require('./validate').isValidType; +const isEmptyValue = require('./validate').isEmptyValue; +const isInChoices = require('./validate').isInChoices; +const isArray = require('./validate').isArray; +const isDocument = require('./validate').isDocument; +const isEmbeddedDocument = require('./validate').isEmbeddedDocument; +const isString = require('./validate').isString; +const isNumber = require('./validate').isNumber; +const isDate = require('./validate').isDate; +const ValidationError = require('./errors').ValidationError; + +const normalizeType = function(property) { + // TODO: Only copy over stuff we support + + let typeDeclaration = {}; + if (property.type) { + typeDeclaration = property; + } else if (isSupportedType(property)) { + typeDeclaration.type = property; + } else { + throw new Error(`Unsupported type or bad constiable. + Remember, non-persisted objects must start with an underscore (_). Got: ${property}`); + } + + return typeDeclaration; }; class BaseDocument { - constructor() { - this._schema = { // Defines document structure/properties - _id: { type: DB().nativeIdType() }, // Native ID to backend database - }; - - this._id = null; + constructor() { + this._schema = { // Defines document structure/properties + _id: {type: db().nativeIdType()} // Native ID to backend database + }; + + this._id = null; + } + + // TODO: Is there a way to tell if a class is + // a subclass of something? Until I find out + // how, we'll be lazy use this. + static documentClass() { + throw new TypeError('You must override documentClass (static).'); + } + + documentClass() { + throw new TypeError('You must override documentClass.'); + } + + collectionName() { + // DEPRECATED + // Getting ready to remove this functionality + if (this._meta) { + return this._meta.collection; } - // TODO: Is there a way to tell if a class is - // a subclass of something? Until I find out - // how, we'll be lazy use this. - static documentClass() { - throw new TypeError('You must override documentClass (static).'); + return this.constructor.collectionName(); + } + + /** + * Get current collection name + * + * @returns {String} + */ + static collectionName() { + // DEPRECATED + // Getting ready to remove this functionality + const instance = new this(); + if (instance._meta) { + return instance._meta.collection; } - documentClass() { - throw new TypeError('You must override documentClass.'); + return `${this.name.toLowerCase()}s`; + } + + get id() { + deprecate('Document.id - use Document._id instead'); + return this._id; + } + + set id(id) { + deprecate('Document.id - use Document._id instead'); + this._id = id; + } + + /** + * set schema + * @param {Object} extension + */ + schema(extension) { + if (!extension) { + return; } - collectionName() { - // DEPRECATED - // Getting ready to remove this functionality - if (this._meta) { - return this._meta.collection; + Object.keys(extension).forEach(k => { + this[k] = extension[k]; + }); + } + + /* + * Pre/post Hooks + * + * To add a hook, the extending class just needs + * to override the appropriate hook method below. + */ + + preValidate() { + } + + postValidate() { + } + + preSave() { + } + + postSave() { + } + + preDelete() { + } + + postDelete() { + } + + /** + * Generate this._schema from fields + * + * TODO : EMBEDDED + * Need to share this with embedded + */ + generateSchema() { + Object.keys(this).forEach(k => { + // Ignore private constiables + if (k.startsWith('_')) { + return; + } + + // Normalize the type format + this._schema[k] = normalizeType(this[k]); + + // Assign a default if needed + if (isArray(this._schema[k].type)) { + this[k] = this.getDefault(k) || []; + } else { + this[k] = this.getDefault(k); + } + }); + } + + /** + * Validate current document + * + * The method throw errors if document has invalid value + * + * TODO: This is not the right approach. The method needs to collect all errors in array and return them. + */ + validate() { + + /* eslint complexity: 0 */ + Object.keys(this._schema).forEach(key => { + const value = this[key]; + + // TODO: This should probably be in Document, not BaseDocument + if (value !== null && value !== undefined) { + if (isEmbeddedDocument(value)) { + value.validate(); + return; + } else if (isArray(value) && value.length > 0 && isEmbeddedDocument(value[0])) { + value.forEach(v => { + if (v.validate) { + v.validate(); + } + }); + return; } + } - return this.constructor.collectionName(); - } + if (!isValidType(value, this._schema[key].type)) { + // TODO: Formatting should probably be done somewhere else + let typeName = null; + let valueName = null; - static collectionName() { - // DEPRECATED - // Getting ready to remove this functionality - var instance = new this(); - if (instance._meta) { - return instance._meta.collection; + if (Array.isArray(this._schema[key].type) && this._schema[key].type.length > 0) { + typeName = `[${this._schema[key].type[0].name}]`; + } else if (Array.isArray(this._schema[key].type) && this._schema[key].type.length === 0) { + typeName = '[]'; + } else { + typeName = this._schema[key].type.name; } - return this.name.toLowerCase() + 's'; - } - - get id() { - deprecate('Document.id - use Document._id instead'); - return this._id; - } + if (Array.isArray(value)) { + // TODO: Not descriptive enough! Strings can look like numbers + valueName = `[${value.toString()}]`; + } else { + valueName = typeof value; + } - set id(id) { - deprecate('Document.id - use Document._id instead'); - this._id = id; + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} + should be ${typeName}, got ${valueName}`); + } + + if (this._schema[key].required && isEmptyValue(value)) { + throw new ValidationError(`Key ${this.collectionName()}.${key} is required, but got ${value}`); + } + + if (this._schema[key].match && isString(value) && !this._schema[key].match.test(value)) { + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} does not match the + regex/string ${this._schema[key].match.toString()}. Value was ${value}`); + } + + if (!isInChoices(this._schema[key].choices, value)) { + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} should be + in choices [${this._schema[key].choices.join(', ')}], got ${value}`); + } + + if (isNumber(this._schema[key].min) && value < this._schema[key].min) { + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} is less + than min, ${this._schema[key].min}, got ${value}`); + } + + if (isNumber(this._schema[key].max) && value > this._schema[key].max) { + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} is less + than max, ${this._schema[key].max}, got ${value}`); + } + + if (typeof this._schema[key].validate === 'function' && !this._schema[key].validate(value)) { + throw new ValidationError(`Value assigned to ${this.collectionName()}.${key} failed custom validator. + Value was ${value}`); + } + }); + } + + /* + * Right now this only canonicalizes dates (integer timestamps + * get converted to Date objects), but maybe we should do the + * same for strings (UTF, Unicode, ASCII, etc)? + */ + canonicalize() { + Object.keys(this._schema).forEach(key => { + const value = this[key]; + + if (this._schema[key].type === Date && isDate(value)) { + this[key] = new Date(value); + } else if (value !== null && value !== undefined && + value.documentClass && value.documentClass() === 'embedded') { + // TODO: This should probably be in Document, not BaseDocument + value.canonicalize(); + return; + } + }); + } + + /** + * Create new document from data + * + * @param {Object} data + * @returns {Document} + */ + static create(data) { + this.createIndexes(); + + if (typeof data !== 'undefined') { + return this._fromData(data); } - schema(extension) { - var that = this; - - if (!extension) return; - _.keys(extension).forEach(function(k) { - that[k] = extension[k]; - }); + return this._instantiate(); + } + + static createIndexes() { + } + + /** + * Create new document from self + * + * @returns {BaseDocument} + * @private + */ + static _instantiate() { + const instance = new this(); + + instance.generateSchema(); + + return instance; + } + + // TODO: Should probably move some of this to + // Embedded and Document classes since Base shouldn't + // need to know about child classes + /* eslint max-nested-callbacks: [2,5] */ // TODO: To reduce the number of nested callbacks + static _fromData(datas) { + if (!isArray(datas)) { + datas = [datas]; } - /* - * Pre/post Hooks - * - * To add a hook, the extending class just needs - * to override the appropriate hook method below. - */ - - preValidate() { } - - postValidate() { } - - preSave() { } - - postSave() { } - - preDelete() { } - - postDelete() { } + const documents = []; + datas.forEach(data => { + const instance = this._instantiate(); - // TODO : EMBEDDED - // Need to share this with embedded - generateSchema() { - var that = this; + Object.keys(data).forEach(key => { + let value = null; - _.keys(this).forEach(function(k) { - // Ignore private variables - if (_.startsWith(k, '_')) { - return; - } - - // Normalize the type format - that._schema[k] = normalizeType(that[k]); - - // Assign a default if needed - if (isArray(that._schema[k].type)) { - that[k] = that.getDefault(k) || []; - } else { - that[k] = that.getDefault(k); - } - }); - } + if (data[key] === null) { + value = instance.getDefault(key); + } else { + value = data[key]; + } - validate() { - var that = this; - - _.keys(that._schema).forEach(function(key) { - var value = that[key]; - - // TODO: This should probably be in Document, not BaseDocument - if (value !== null && value !== undefined) { - if (isEmbeddedDocument(value)) { - value.validate(); - return; - } else if (isArray(value) && value.length > 0 && isEmbeddedDocument(value[0])) { - value.forEach(function(v) { - if (v.validate) { - v.validate(); - } - }); - return; - } - } + // If its not in the schema, we don't care about it... right? + if (key in instance._schema) { + const type = instance._schema[key].type; - if (!isValidType(value, that._schema[key].type)) { - // TODO: Formatting should probably be done somewhere else - var typeName = null; - var valueName = null; - if (Array.isArray(that._schema[key].type) && that._schema[key].type.length > 0) { - typeName = '[' + that._schema[key].type[0].name + ']'; - } else if (Array.isArray(that._schema[key].type) && that._schema[key].type.length === 0) { - typeName = '[]'; - } else { - typeName = that._schema[key].type.name; - } + if (type.documentClass && type.documentClass() === 'embedded') { - if (Array.isArray(value)) { - // TODO: Not descriptive enough! Strings can look like numbers - valueName = '[' + value.toString() + ']'; - } else { - valueName = typeof(value); - } - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' should be ' + typeName + ', got ' + valueName); - } + // Initialize EmbeddedDocument + instance[key] = type._fromData(value); + } else if (isArray(type) && type.length > 0 && + type[0].documentClass && type[0].documentClass() === 'embedded') { - if (that._schema[key].required && isEmptyValue(value)) { - throw new ValidationError('Key ' + that.collectionName() + '.' + key + - ' is required' + ', but got ' + value); - } - - if (that._schema[key].match && isString(value) && !that._schema[key].match.test(value)) { - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' does not match the regex/string ' + that._schema[key].match.toString() + '. Value was ' + value); - } + // Initialize array of EmbeddedDocuments + instance[key] = []; + value.forEach((v, i) => { + instance[key][i] = type[0]._fromData(v); + }); + } else { - if (!isInChoices(that._schema[key].choices, value)) { - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' should be in choices [' + that._schema[key].choices.join(', ') + '], got ' + value); - } + // Initialize primitive or array of primitives + instance[key] = value; + } + } else if (key in instance) { - if (isNumber(that._schema[key].min) && value < that._schema[key].min) { - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' is less than min, ' + that._schema[key].min + ', got ' + value); - } - - if (isNumber(that._schema[key].max) && value > that._schema[key].max) { - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' is less than max, ' + that._schema[key].max + ', got ' + value); - } + // Handles virtual setters + instance[key] = value; + } + }); - if (typeof(that._schema[key].validate) === 'function' && !that._schema[key].validate(value)) { - throw new ValidationError('Value assigned to ' + that.collectionName() + '.' + key + - ' failed custom validator. Value was ' + value); - } - }); - } + documents.push(instance); + }); - /* - * Right now this only canonicalizes dates (integer timestamps - * get converted to Date objects), but maybe we should do the - * same for strings (UTF, Unicode, ASCII, etc)? - */ - canonicalize() { - var that = this; - - _.keys(that._schema).forEach(function(key) { - var value = that[key]; - - if (that._schema[key].type === Date && isDate(value)) { - that[key] = new Date(value); - } else if (value !== null && value !== undefined && - value.documentClass && value.documentClass() === 'embedded') { - // TODO: This should probably be in Document, not BaseDocument - value.canonicalize(); - return; - } - }); + if (documents.length === 1) { + return documents[0]; } - - static create(data) { - this.createIndexes(); - - if (typeof(data) !== 'undefined') { - return this._fromData(data); - } - - return this._instantiate(); + return documents; + } + + populate() { + return BaseDocument.populate(this); + } + + /** + * Populates document references + * + * TODO : EMBEDDED + * @param {Array|Document} docs + * @param {Array} fields + * @returns {Promise} + */ + static populate(docs, fields) { + if (!docs) { + return Promise.resolve([]); } - static createIndexes() { } + let documents = null; - static _instantiate() { - var instance = new this(); - instance.generateSchema(); - return instance; + if (!isArray(docs)) { + documents = [docs]; + } else if (docs.length < 1) { + return Promise.all(docs); + } else { + documents = docs; } - // TODO: Should probably move some of this to - // Embedded and Document classes since Base shouldn't - // need to know about child classes - static _fromData(datas) { - var that = this; - - if (!isArray(datas)) { - datas = [datas]; + // Load all 1-level-deep references + // First, find all unique keys needed to be loaded... + const keys = []; + + // TODO: Bad assumption: Not all documents in the database will have the same schema... + // Hmm, if this is true, thats an error on the user. Right? + const anInstance = documents[0]; + + Object.keys(anInstance._schema).forEach(key => { + // Only populate specified fields + if (isArray(fields) && fields.indexOf(key) < 0) { + return; + } + + // Handle array of references (ex: { type: [MyObject] }) + if (isArray(anInstance._schema[key].type) && + anInstance._schema[key].type.length > 0 && + isDocument(anInstance._schema[key].type[0])) { + keys.push(key); + + // Handle anInstance[key] being a string id, a native id, or a Document instance + } else if ((isString(anInstance[key]) || db().isNativeId(anInstance[key])) && + isDocument(anInstance._schema[key].type)) { + keys.push(key); + } + }); + + // ...then get all ids for each type of reference to be loaded... + // ids = { + // houses: { + // 'abc123': ['ak23lj', '2kajlc', 'ckajl32'], + // 'l2jo99': ['28dsa0'] + // }, + // friends: { + // '1039da': ['lj0adf', 'k2jha'] + // } + // } + const ids = {}; + keys.forEach(key => { + ids[key] = {}; + documents.forEach(document => { + ids[key][db().toCanonicalId(document._id)] = [].concat(document[key]); // Handles values and arrays + + // Also, initialize document member arrays + // to assign to later if needed + if (isArray(document[key])) { + document[key] = []; } - - var documents = []; - var embeddedPromises = []; - datas.forEach(function(d) { - var instance = that._instantiate(); - _.keys(d).forEach(function(key) { - var value = null; - if (d[key] === null) { - value = instance.getDefault(key); - } else { - value = d[key]; - } - - // If its not in the schema, we don't care about it... right? - if (key in instance._schema) { - - var type = instance._schema[key].type; - - if (type.documentClass && type.documentClass() === 'embedded') { - // Initialize EmbeddedDocument - instance[key] = type._fromData(value); - } else if (isArray(type) && type.length > 0 && - type[0].documentClass && type[0].documentClass() === 'embedded') { - // Initialize array of EmbeddedDocuments - instance[key] = []; - value.forEach(function(v, i) { - instance[key][i] = type[0]._fromData(v); - }); - } else { - // Initialize primitive or array of primitives - instance[key] = value; - } - } else if (key in instance) { - // Handles virtual setters - instance[key] = value; - } + }); + }); + + // TODO: Is this really the most efficient + // way to do this? Maybe make a master list + // of all objects that need to be loaded (separated + // by type), load those, and then search through + // ids to see where dereferenced objects should + // go? + + // ...then for each array of ids, load them all... + const loadPromises = []; + Object.keys(ids).forEach(key => { + + let keyIds = []; + Object.keys(ids[key]).forEach(k => { + // Before adding to list, we convert id to the + // backend database's native ID format. + keyIds = keyIds.concat(ids[key][k]); + }); + + // Only want to load each reference once + keyIds = _.unique(keyIds); + + // Handle array of references (like [MyObject]) + let type = null; + if (isArray(anInstance._schema[key].type)) { + type = anInstance._schema[key].type[0]; + } else { + type = anInstance._schema[key].type; + } + + // Bulk load dereferences + const promise = type.find({_id: {$in: keyIds}}, {populate: false}) + .then(dereferences => { + // Assign each dereferenced object to parent + + Object.keys(ids[key]).forEach(k => { + // TODO: Replace with documents.find when able + // Find the document to assign the derefs to + let doc = null; + documents.forEach(document => { + if (db().toCanonicalId(document._id) === k) { + doc = document; + } }); - documents.push(instance); - }); - - if (documents.length === 1) { - return documents[0]; - } - return documents; - } - - populate() { - return BaseDocument.populate(this); - } - - // TODO : EMBEDDED - // - static populate(docs, fields) { - if (!docs) return Promise.all([]); - - var documents = null; - - if (!isArray(docs)) { - documents = [docs]; - } else if (docs.length < 1) { - return Promise.all(docs); - } else { - documents = docs; - } - - // Load all 1-level-deep references - // First, find all unique keys needed to be loaded... - var keys = []; - - // TODO: Bad assumption: Not all documents in the database will have the same schema... - // Hmm, if this is true, thats an error on the user. Right? - var anInstance = documents[0]; - - _.keys(anInstance._schema).forEach(function(key) { - // Only populate specified fields - if (isArray(fields) && fields.indexOf(key) < 0) { - return; - } - - // Handle array of references (ex: { type: [MyObject] }) - if (isArray(anInstance._schema[key].type) && - anInstance._schema[key].type.length > 0 && - isDocument(anInstance._schema[key].type[0])) { - keys.push(key); - } - // Handle anInstance[key] being a string id, a native id, or a Document instance - else if ((isString(anInstance[key]) || DB().isNativeId(anInstance[key])) && - isDocument(anInstance._schema[key].type)) { - keys.push(key); - } - }); - - // ...then get all ids for each type of reference to be loaded... - // ids = { - // houses: { - // 'abc123': ['ak23lj', '2kajlc', 'ckajl32'], - // 'l2jo99': ['28dsa0'] - // }, - // friends: { - // '1039da': ['lj0adf', 'k2jha'] - // } - //} - var ids = {}; - keys.forEach(function(k) { - ids[k] = {}; - documents.forEach(function(d) { - ids[k][DB().toCanonicalId(d._id)] = [].concat(d[k]); // Handles values and arrays - - // Also, initialize document member arrays - // to assign to later if needed - if (isArray(d[k])) { - d[k] = []; + // For all ids to be dereferenced, find the + // deref and assign or push it + ids[key][k].forEach(id => { + // TODO: Replace with dereferences.find when able + // Find the right dereference + let deref = null; + dereferences.forEach(dereference => { + if (db().toCanonicalId(dereference._id) === db().toCanonicalId(id)) { + deref = dereference; } + }); + + /* eslint no-underscore-dangle: 0 */ + if (isArray(anInstance._schema[key].type)) { + doc[key].push(deref); + } else { + doc[key] = deref; + } }); + }); }); - // TODO: Is this really the most efficient - // way to do this? Maybe make a master list - // of all objects that need to be loaded (separated - // by type), load those, and then search through - // ids to see where dereferenced objects should - // go? - - // ...then for each array of ids, load them all... - var loadPromises = []; - _.keys(ids).forEach(function(key) { - var keyIds = []; - _.keys(ids[key]).forEach(function(k) { - // Before adding to list, we convert id to the - // backend database's native ID format. - keyIds = keyIds.concat(ids[key][k]); - }); - - // Only want to load each reference once - keyIds = _.unique(keyIds); - - // Handle array of references (like [MyObject]) - var type = null; - if (isArray(anInstance._schema[key].type)) { - type = anInstance._schema[key].type[0]; - } else { - type = anInstance._schema[key].type; - } - - // Bulk load dereferences - var p = type.find({ '_id': { $in: keyIds } }, { populate: false }) - .then(function(dereferences) { - // Assign each dereferenced object to parent - - _.keys(ids[key]).forEach(function(k) { - // TODO: Replace with documents.find when able - // Find the document to assign the derefs to - var doc; - documents.forEach(function(d) { - if (DB().toCanonicalId(d._id) === k) doc = d; - }); - - // For all ids to be dereferenced, find the - // deref and assign or push it - ids[key][k].forEach(function(id) { - // TODO: Replace with dereferences.find when able - // Find the right dereference - var deref; - dereferences.forEach(function(d) { - if (DB().toCanonicalId(d._id) === DB().toCanonicalId(id)) deref = d; - }); - - if (isArray(anInstance._schema[key].type)) { - doc[key].push(deref); - } else { - doc[key] = deref; - } - }); - }); - }); - - loadPromises.push(p); - }); + loadPromises.push(promise); + }); + + // ...and finally execute all promises and return our + // fully loaded documents. + return Promise + .all(loadPromises) + .then(() => docs); + } + + /** + * Get default value + * + * @param {String} schemaProp Key of current schema + * @returns {*} + */ + getDefault(schemaProp) { + if (schemaProp in this._schema && 'default' in this._schema[schemaProp]) { + const def = this._schema[schemaProp].default; + const defValue = typeof def === 'function' ? def() : def; + this[schemaProp] = defValue; // TODO: Wait... should we be assigning it here? + + return defValue; + } else if (schemaProp === '_id') { + return null; + } - // ...and finally execute all promises and return our - // fully loaded documents. - return Promise.all(loadPromises).then(function() { - return docs; + return undefined; + } + + /** + * For JSON.Stringify + * + * @returns {*} + */ + toJSON() { + const values = this._toData({_id: true}); + const schema = this._schema; + Object.keys(schema).forEach(key => { + if (schema[key].private) { + delete values[key]; + + } else if (values[key] && values[key].toJSON) { + values[key] = values[key].toJSON(); + + } else if (isArray(values[key])) { + const newArray = []; + values[key].forEach(value => { + if (value && value.toJSON) { + newArray.push(value.toJSON()); + } else { + newArray.push(value); + } }); + values[key] = newArray; + } + }); + + return values; + } + + /** + * + * @param keep + * @returns {{}} + * @private + */ + _toData(keep) { + if (keep === undefined || keep === null) { + keep = {}; + } else if (keep._id === undefined) { + keep._id = true; } - getDefault(schemaProp) { - if (schemaProp in this._schema && 'default' in this._schema[schemaProp]) { - var def = this._schema[schemaProp].default; - var defVal = typeof(def) === 'function' ? def() : def; - this[schemaProp] = defVal; // TODO: Wait... should we be assigning it here? - return defVal; - } else if (schemaProp === '_id') { - return null; + const values = {}; + Object.keys(this).forEach(key => { + if (key.startsWith('_')) { + if (key !== '_id' || !keep._id) { + return; } - return undefined; - } + values[key] = this[key]; - toJSON() { - var values = this._toData({_id: true}); - var schema = this._schema; - for (var key in schema) { - if (schema.hasOwnProperty(key)) { - if (schema[key].private){ - delete values[key]; - } else if (values[key] && values[key].toJSON) { - values[key] = values[key].toJSON(); - } else if (isArray(values[key])) { - var newArray = []; - values[key].forEach(function(i) { - if (i && i.toJSON) { - newArray.push(i.toJSON()); - } else { - newArray.push(i); - } - }); - values[key] = newArray; - } - } - } - - return values; - } + } else if (isEmbeddedDocument(this[key])) { + values[key] = this[key]._toData(); - _toData(keep) { - var that = this; + } else if (isArray(this[key]) && this[key].length > 0 && isEmbeddedDocument(this[key][0])) { + values[key] = []; + this[key].forEach(value => values[key].push(value._toData())); - if (keep === undefined || keep === null) { - keep = {}; - } else if (keep._id === undefined) { - keep._id = true; - } + } else { + values[key] = this[key]; + } + }); - var values = {}; - _.keys(this).forEach(function(k) { - if (_.startsWith(k, '_')) { - if (k !== '_id' || !keep._id) { - return; - } else { - values[k] = that[k]; - } - } else if (isEmbeddedDocument(that[k])) { - values[k] = that[k]._toData(); - } else if (isArray(that[k]) && that[k].length > 0 && isEmbeddedDocument(that[k][0])) { - values[k] = []; - that[k].forEach(function(v) { - values[k].push(v._toData()); - }); - } else { - values[k] = that[k]; - } - }); + return values; + } - return values; - } + _getEmbeddeds() { + let embeddeds = []; - _getEmbeddeds() { - var that = this; + Object.keys(this._schema).forEach(key => { + if (isEmbeddedDocument(this._schema[key].type) || + (isArray(this._schema[key].type) && isEmbeddedDocument(this._schema[key].type[0]))) { + embeddeds = embeddeds.concat(this[key]); + } + }); - var embeddeds = []; - _.keys(this._schema).forEach(function(v) { - if (isEmbeddedDocument(that._schema[v].type) || - (isArray(that._schema[v].type) && isEmbeddedDocument(that._schema[v].type[0]))) { - embeddeds = embeddeds.concat(that[v]); - } - }); - return embeddeds; - } + return embeddeds; + } - _getHookPromises(hookName) { - var embeddeds = this._getEmbeddeds(); + _getHookPromises(hookName) { + const embeddeds = this._getEmbeddeds(); - var hookPromises = []; - hookPromises = hookPromises.concat(_.invoke(embeddeds, hookName)); - hookPromises.push(this[hookName]()); - return hookPromises; - } + let hookPromises = []; + hookPromises = hookPromises.concat(_.invoke(embeddeds, hookName)); + hookPromises.push(this[hookName]()); + return hookPromises; + } } -module.exports = BaseDocument; \ No newline at end of file +module.exports = BaseDocument; diff --git a/lib/clients/client.js b/lib/clients/client.js index 357dc50..846e410 100644 --- a/lib/clients/client.js +++ b/lib/clients/client.js @@ -1,87 +1,81 @@ -"use strict"; - -var deprecate = require('depd')('camo'); +'use strict'; class DatabaseClient { - constructor(url) { - this._url = url; - } - - save(collection, query, values) { - throw new TypeError('You must override save.'); - } + save(collection, query, values) { + throw new TypeError('You must override save.'); + } - delete(collection) { - throw new TypeError('You must override delete.'); - } + delete(collection) { + throw new TypeError('You must override delete.'); + } - deleteOne(collection, query) { - throw new TypeError('You must override deleteOne.'); - } + deleteOne(collection, query) { + throw new TypeError('You must override deleteOne.'); + } - deleteMany(collection, query) { - throw new TypeError('You must override deleteMany.'); - } + deleteMany(collection, query) { + throw new TypeError('You must override deleteMany.'); + } - findOne(collection, query) { - throw new TypeError('You must override findOne.'); - } + findOne(collection, query) { + throw new TypeError('You must override findOne.'); + } - findOneAndUpdate(collection, query, values, options) { - throw new TypeError('You must override findOneAndUpdate.'); - } + findOneAndUpdate(collection, query, values, options) { + throw new TypeError('You must override findOneAndUpdate.'); + } - findOneAndDelete(collection, query, options) { - throw new TypeError('You must override findOneAndDelete.'); - } + findOneAndDelete(collection, query, options) { + throw new TypeError('You must override findOneAndDelete.'); + } - find(collection, query, options) { - throw new TypeError('You must override findMany.'); - } + find(collection, query, options) { + throw new TypeError('You must override findMany.'); + } - count(collection, query) { - throw new TypeError('You must override count.'); - } + count(collection, query) { + throw new TypeError('You must override count.'); + } - createIndex(collection, field, options) { - throw new TypeError('You must override createIndex.'); - } + createIndex(collection, field, options) { + throw new TypeError('You must override createIndex.'); + } - static connect(url, options) { - throw new TypeError('You must override connect (static).'); - } + static connect(url, options) { + throw new TypeError('You must override connect (static).'); + } - close() { - throw new TypeError('You must override close.'); - } + close() { + throw new TypeError('You must override close.'); + } - clearCollection(collection) { - throw new TypeError('You must override clearCollection.'); - } + clearCollection(collection) { + throw new TypeError('You must override clearCollection.'); + } - dropDatabase() { - throw new TypeError('You must override dropDatabase.'); - } + dropDatabase() { + throw new TypeError('You must override dropDatabase.'); + } - toCanonicalId(id) { - throw new TypeError('You must override toCanonicalId.'); - } + toCanonicalId(id) { + throw new TypeError('You must override toCanonicalId.'); + } - isNativeId(value) { - throw new TypeError('You must override isNativeId.'); - } + isNativeId(value) { + throw new TypeError('You must override isNativeId.'); + } - toNativeId(id) { - return this.nativeIdType()(id); - } + toNativeId(id) { + return this.nativeIdType()(id); + } - nativeIdType() { - throw new TypeError('You must override nativeIdType.'); - } + nativeIdType() { + throw new TypeError('You must override nativeIdType.'); + } - driver() { - throw new TypeError('You must override driver.'); - } + driver() { + throw new TypeError('You must override driver.'); + } } -module.exports = DatabaseClient; \ No newline at end of file +module.exports = DatabaseClient; diff --git a/lib/clients/index.js b/lib/clients/index.js index d75a7a4..499c14f 100644 --- a/lib/clients/index.js +++ b/lib/clients/index.js @@ -1,11 +1,13 @@ -var assertConnected = function(db) { - if (db === null || db === undefined) { - throw new Error('You must first call \'connect\' before loading/saving documents.'); - } +'use strict'; + +const assertConnected = db => { + if (db === null || db === undefined) { + throw new Error('You must first call \'connect\' before loading/saving documents.'); + } }; -exports.getClient = function() { - var client = global.CLIENT; - assertConnected(client); - return client; -}; \ No newline at end of file +exports.getClient = () => { + const client = global.CLIENT; + assertConnected(client); + return client; +}; diff --git a/lib/clients/mongoclient.js b/lib/clients/mongoclient.js index 24e0764..169d97d 100644 --- a/lib/clients/mongoclient.js +++ b/lib/clients/mongoclient.js @@ -1,293 +1,355 @@ -"use strict"; +'use strict'; -var _ = require('lodash'); -var path = require('path'); -var fs = require('fs'); -var MDBClient = require('mongodb').MongoClient; -var ObjectId = require('mongodb').ObjectId; -var DatabaseClient = require('./client'); -var isObject = require('../validate').isObject; -var deepTraverse = require('../util').deepTraverse; +const _ = require('lodash'); +const path = require('path'); +const fs = require('fs'); +const MDBClient = require('mongodb').MongoClient; +const ObjectId = require('mongodb').ObjectId; +const DatabaseClient = require('./client'); +const isObject = require('../validate').isObject; +const deepTraverse = require('../util').deepTraverse; class MongoClient extends DatabaseClient { - constructor(url, mongo) { - super(url); - - this._mongo = mongo; - } - - save(collection, id, values) { - var that = this; - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - - // TODO: I'd like to just use update with upsert:true, but I'm - // note sure how the query will work if id == null. Seemed to - // have some problems before with passing null ids. - if (id === null) { - db.insertOne(values, function(error, result) { - if (error) return reject(error); - if (!result.hasOwnProperty('insertedId') || result.insertedId === null) { - return reject(new Error('Save failed to generate ID for object.')); - } - - return resolve(result.insertedId); - }); - } else { - db.updateOne({ _id: id }, { $set: values }, { upsert: true }, function(error, result) { - if (error) return reject(error); - return resolve(); - }); - } + constructor(url, mongo) { + super(url); + + this._mongo = mongo; + } + + /** + * Save (upsert) document + * + * @param {String} collection Collection's name + * @param {ObjectId?} id Document's id + * @param {Object} values Data for save + * @returns {Promise} Promise with result insert or update query + */ + save(collection, id, values) { + const db = this._mongo.collection(collection); + + // TODO: I'd like to just use update with upsert:true, but I'm + // note sure how the query will work if id == null. Seemed to + // have some problems before with passing null ids. + if (id === null) { + return db + .insertOne(values) + .then(result => { + if (!result.hasOwnProperty('insertedId') || result.insertedId === null) { + return Promise.reject(new Error('Save failed to generate ID for object.')); + } + + return result.insertedId; }); } - delete(collection, id) { - var that = this; - return new Promise(function(resolve, reject) { - if (id === null) resolve(0); - - var db = that._mongo.collection(collection); - db.deleteOne({ _id: id }, {w:1}, function (error, result) { - if (error) return reject(error); - return resolve(result.deletedCount); - }); - }); + return db.updateOne({_id: id}, {$set: values}, {upsert: true}); + } + + /** + * Delete document + * + * @param {String} collection Collection's name + * @param {ObjectId} id Document's id + * @returns {Promise} + */ + delete(collection, id) { + if (!id) { + return Promise.resolve(0); } - deleteOne(collection, query) { - var that = this; - query = castQueryIds(query); - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - db.deleteOne(query, {w:1}, function (error, result) { - if (error) return reject(error); - return resolve(result.deletedCount); - }); - }); + const db = this._mongo.collection(collection); + return db + .deleteOne({_id: id}, {w: 1}) + .then(result => result.deletedCount); + } + + /** + * Delete one document by query + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + deleteOne(collection, query) { + const db = this._mongo.collection(collection); + + query = castQueryIds(query); + + return db + .deleteOne(query, {w: 1}) + .then(result => result.deletedCount); + } + + /** + * Delete many documents by query + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + deleteMany(collection, query) { + const db = this._mongo.collection(collection); + + query = castQueryIds(query); + + return db + .deleteMany(query, {w: 1}) + .then(result => result.deletedCount); + } + + /** + * Find one document + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + findOne(collection, query) { + const db = this._mongo.collection(collection); + + query = castQueryIds(query); + + return db.findOne(query); + } + + /** + * Find one document and update it + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} values + * @param {Object} options + * @returns {Promise} + */ + findOneAndUpdate(collection, query, values, options) { + const db = that._mongo.collection(collection); + + query = castQueryIds(query); + options = options || {}; + // Always return the updated object + options.returnOriginal = false; + + let update = values; + + if (options.upsert) { + update = {$setOnInsert: update}; + } else { + update = {$set: update}; } - deleteMany(collection, query) { - var that = this; - query = castQueryIds(query); - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - db.deleteMany(query, {w:1}, function (error, result) { - if (error) return reject(error); - return resolve(result.deletedCount); - }); - }); - } - - findOne(collection, query) { - var that = this; - query = castQueryIds(query); - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - db.findOne(query, function (error, doc) { - if (error) return reject(error); - return resolve(doc); - }); - }); - } - - findOneAndUpdate(collection, query, values, options) { - var that = this; - query = castQueryIds(query); - if (!options) { - options = {}; + return db + .findOneAndUpdate(query, update, options) + .then(result => result.value); + } + + /** + * Find one document and delete it + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + findOneAndDelete(collection, query, options) { + const db = this._mongo.collection(collection); + query = castQueryIds(query); + options = options || {}; + + return db + .findOneAndDelete(query, options) + .then(result => result.value === null ? 0 : 1); + } + + /** + * Find documents + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + find(collection, query, options) { + query = castQueryIds(query); + options = options || {}; + + const db = this._mongo.collection(collection); + let cursor = db.find(query); + + if (options.sort && (_.isArray(options.sort) || _.isString(options.sort))) { + const sortOptions = {}; + + if (!_.isArray(options.sort)) { + options.sort = [options.sort]; + } + + options.sort.forEach(s => { + if (!_.isString(s)) { + return; } - // Always return the updated object - options.returnOriginal = false; - - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - - var update = values; - if (options.upsert) { - update = { $setOnInsert: update }; - } else { - update = { $set: update }; - } - - db.findOneAndUpdate(query, update, options, function(error, result) { - if (error) return reject(error); - resolve(result.value); - }); - }); - } - - findOneAndDelete(collection, query, options) { - var that = this; - query = castQueryIds(query); - if (!options) { - options = {}; + let sortOrder = 1; + if (s[0] === '-') { + sortOrder = -1; + s = s.substring(1); } - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); + sortOptions[s] = sortOrder; + }); - db.findOneAndDelete(query, options, function (error, result) { - if (error) return reject(error); - return resolve(result.value === null ? 0 : 1); - }); - }); + cursor = cursor.sort(sortOptions); } - find(collection, query, options) { - var that = this; - query = castQueryIds(query); - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - var cursor = db.find(query); - if (options.sort && (_.isArray(options.sort) || _.isString(options.sort))) { - var sortOptions = {}; - if (!_.isArray(options.sort)) { - options.sort = [options.sort]; - } - - options.sort.forEach(function(s) { - if (!_.isString(s)) return; - - var sortOrder = 1; - if (s[0] === '-') { - sortOrder = -1; - s = s.substring(1); - } - sortOptions[s] = sortOrder; - }); - - cursor = cursor.sort(sortOptions); - } - if (typeof options.skip === 'number') { - cursor = cursor.skip(options.skip); - } - if (typeof options.limit === 'number') { - cursor = cursor.limit(options.limit); - } - cursor.toArray(function(error, docs) { - if (error) return reject(error); - return resolve(docs); - }); - }); + if (typeof options.skip === 'number') { + cursor = cursor.skip(options.skip); } - count(collection, query) { - var that = this; - query = castQueryIds(query); - return new Promise(function(resolve, reject) { - var db = that._mongo.collection(collection); - db.count(query, function (error, count) { - if (error) return reject(error); - return resolve(count); - }); - }); + if (typeof options.limit === 'number') { + cursor = cursor.limit(options.limit); } - createIndex(collection, field, options) { - options = options || {}; - options.unique = options.unique || false; - options.sparse = options.sparse || false; - - var db = this._mongo.collection(collection); - - var keys = {}; - keys[field] = 1; - db.createIndex(keys, {unique: options.unique, sparse: options.sparse}); - } - - static connect(url, options) { - if (typeof(options) === 'undefined') { - options = { }; - } - return new Promise(function(resolve, reject) { - MDBClient.connect(url, options, function(error, client) { - if (error) return reject(error); - return resolve(new MongoClient(url, client)); - }); - }); - } - - close() { - var that = this; - return new Promise(function(resolve, reject) { - that._mongo.close(function(error) { - if (error) return reject(error); - return resolve(); - }); - }); - } - - clearCollection(collection) { - var that = this; - return new Promise(function(resolve, reject) { - that._mongo.dropCollection(collection, function(error, result) { - if (error) return reject(error); - return resolve(); - }); - }); - } - - dropDatabase() { - var that = this; - return new Promise(function(resolve, reject) { - that._mongo.dropDatabase(function(error, result) { - if (error) return reject(error); - return resolve(); - }); - }); - } - - toCanonicalId(id) { - return id.toString(); - } - - isNativeId(value) { - return value instanceof ObjectId || String(value).match(/^[a-fA-F0-9]{24}$/) !== null; - } - - nativeIdType() { - return ObjectId; - } - - driver() { - return this._mongo; - } + return cursor.toArray(); + } + + /** + * Count number of matching documents in the db to a query. + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + count(collection, query) { + const db = this._mongo.collection(collection); + query = castQueryIds(query); + + return db.count(query); + } + + /** + * Create index + * + * @param {String} collection Collection's name + * @param {String} field Field name + * @param {Object} options Options + * @returns {Promise} + */ + createIndex(collection, field, options) { + options = options || {}; + options.unique = options.unique || false; + options.sparse = options.sparse || false; + + const db = this._mongo.collection(collection); + const keys = {}; + + keys[field] = 1; + + return db.createIndex(keys, {unique: options.unique, sparse: options.sparse}); + } + + /** + * Connect to database + * + * @param {String} url + * @param {Object} options + * @returns {Promise} + */ + static connect(url, options) { + options = options || {}; + + return MDBClient + .connect(url, options) + .then(client => new MongoClient(url, client)); + } + + /** + * Close current connection + * + * @returns {Promise} + */ + close() { + return this._mongo.close(); + } + + /** + * Drop collection + * + * @param {String} collection + * @returns {Promise} + */ + clearCollection(collection) { + return this._mongo.dropCollection(collection); + } + + /** + * Drop current database + * + * @returns {Promise} + */ + dropDatabase() { + return this._mongo.dropDatabase(); + } + + /** + * Convert ObjectId to canonical form + * + * @param {ObjectId} id + * @returns {*|string|String} + */ + toCanonicalId(id) { + return id.toString(); + } + + /** + * Is Native ID + * + * @param {*} value + * @returns {boolean} + */ + isNativeId(value) { + return value instanceof ObjectId || String(value).match(/^[a-fA-F0-9]{24}$/) !== null; + } + + nativeIdType() { + return ObjectId; + } + + driver() { + return this._mongo; + } } -var castId = function(val) { - return new ObjectId(val); -}; +const castId = val => new ObjectId(val); -var castIdArray = function(vals) { - return vals.map(function(v) { - return castId(v); - }); -}; +const castIdArray = vals => vals.map(castId); -/* +/** * Traverses query and converts all IDs to MongoID * * TODO: Should we check for $not operator? + * + * @param {Object} query + * @returns {Object} */ -var castQueryIds = function(query) { - deepTraverse(query, function(key, val, parent) { - if (key === '_id') { - if (String(parent[key]).match(/^[a-fA-F0-9]{24}$/)) { - parent[key] = castId(parent[key]); - } else if (isObject(parent[key]) && _.has(parent[key], '$in')) { - // { _id: { '$in': [ 'K1cbMk7T8A0OU83IAT4dFa91', 'Y1cbak7T8A1OU83IBT6aPq11' ] } } - parent[key].$in = castIdArray(parent[key].$in); - } else if (isObject(parent[key]) && _.has(parent[key], '$nin')) { - // { _id: { '$nin': [ 'K1cbMk7T8A0OU83IAT4dFa91', 'Y1cbak7T8A1OU83IBT6aPq11' ] } } - parent[key].$nin = castIdArray(parent[key].$nin); - } - } - }); - +const castQueryIds = function(query) { + if (!_.isObject(query)) { return query; + } + + deepTraverse(query, (key, val, parent) => { + if (key === '_id') { + if (String(parent[key]).match(/^[a-fA-F0-9]{24}$/)) { + parent[key] = castId(parent[key]); + } else if (isObject(parent[key]) && _.has(parent[key], '$in')) { + // { _id: { '$in': [ 'K1cbMk7T8A0OU83IAT4dFa91', 'Y1cbak7T8A1OU83IBT6aPq11' ] } } + parent[key].$in = castIdArray(parent[key].$in); + } else if (isObject(parent[key]) && _.has(parent[key], '$nin')) { + // { _id: { '$nin': [ 'K1cbMk7T8A0OU83IAT4dFa91', 'Y1cbak7T8A1OU83IBT6aPq11' ] } } + parent[key].$nin = castIdArray(parent[key].$nin); + } + } + }); + + return query; }; -module.exports = MongoClient; \ No newline at end of file +module.exports = MongoClient; diff --git a/lib/clients/nedbclient.js b/lib/clients/nedbclient.js index b5c3756..a90c59d 100644 --- a/lib/clients/nedbclient.js +++ b/lib/clients/nedbclient.js @@ -1,332 +1,424 @@ -"use strict"; - -var _ = require('lodash'); -var path = require('path'); -var fs = require('fs'); -var Datastore = require('nedb'); -var DatabaseClient = require('./client'); - -var urlToPath = function(url) { - if (url.indexOf('nedb://') > -1) { - return url.slice(7, url.length); - } - return url; +'use strict'; + +const _ = require('lodash'); +const path = require('path'); +const fs = require('fs'); +const Datastore = require('nedb'); +const DatabaseClient = require('./client'); + +const urlToPath = function(url) { + if (url.indexOf('nedb://') > -1) { + return url.slice(7, url.length); + } + + return url; }; -var getCollectionPath = function(dbLocation, collection) { - if (dbLocation === 'memory') { - return dbLocation; - } - return path.join(dbLocation, collection) + '.db'; +const getCollectionPath = function(dbLocation, collection) { + if (dbLocation === 'memory') { + return dbLocation; + } + + return path.join(dbLocation, collection) + '.db'; }; -var createCollection = function(collectionName, url) { - if (url === 'memory') { - return new Datastore({inMemoryOnly: true}); - } - var collectionPath = getCollectionPath(url, collectionName); - return new Datastore({filename: collectionPath, autoload: true}); +const createCollection = function(collectionName, url) { + if (url === 'memory') { + return new Datastore({inMemoryOnly: true}); + } + + const collectionPath = getCollectionPath(url, collectionName); + return new Datastore({filename: collectionPath, autoload: true}); }; -var getCollection = function(name, collections, path) { - if (!(name in collections)) { - var collection = createCollection(name, path); - collections[name] = collection; - return collection; - } - - return collections[name]; +const getCollection = function(name, collections, path) { + if (!(name in collections)) { + const collection = createCollection(name, path); + collections[name] = collection; + return collection; + } + + return collections[name]; }; class NeDbClient extends DatabaseClient { - constructor(url, collections) { - super(url); - this._path = urlToPath(url); - - if (collections) { - this._collections = collections; - } else { - this._collections = {}; - } - } - - save(collection, id, values) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - - // TODO: I'd like to just use update with upsert:true, but I'm - // note sure how the query will work if id == null. Seemed to - // have some problems before with passing null ids. - if (id === null) { - db.insert(values, function(error, result) { - if (error) return reject(error); - return resolve(result._id); - }); - } else { - db.update({ _id: id }, { $set: values }, { upsert: true }, function(error, result) { - if (error) return reject(error); - return resolve(result); - }); - } - }); - } - - delete(collection, id) { - var that = this; - return new Promise(function(resolve, reject) { - if (id === null) resolve(0); - - var db = getCollection(collection, that._collections, that._path); - db.remove({ _id: id }, function (error, numRemoved) { - if (error) return reject(error); - return resolve(numRemoved); - }); - }); - } - - deleteOne(collection, query) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - db.remove(query, function (error, numRemoved) { - if (error) return reject(error); - return resolve(numRemoved); - }); - }); - } - - deleteMany(collection, query) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - db.remove(query, { multi: true }, function (error, numRemoved) { - if (error) return reject(error); - return resolve(numRemoved); - }); - }); - } - - findOne(collection, query) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - db.findOne(query, function (error, result) { - if (error) return reject(error); - return resolve(result); - }); - }); - } - - findOneAndUpdate(collection, query, values, options) { - var that = this; - - if (!options) { - options = {}; - } - // Since this is 'findOne...' we'll only allow user to update - // one document at a time - options.multi = false; - - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - - // TODO: Would like to just use 'Collection.update' here, but - // it doesn't return objects on update (but will on insert)... - /*db.update(query, values, options, function(error, numReplaced, newDoc) { - if (error) return reject(error); - resolve(newDoc); - });*/ - - that.findOne(collection, query).then(function(data) { - if (!data) { - if (options.upsert) { - return db.insert(values, function(error, result) { - if (error) return reject(error); - return resolve(result); - }); - } else { - return resolve(null); - } - } else { - return db.update(query, { $set: values }, function(error, result) { - if (error) return reject(error); - - // Fixes issue #55. Remove when NeDB is updated to v1.8+ - db.findOne({_id: data._id}, function(error, doc) { - if (error) return reject(error); - resolve(doc); - }); - }); - } - }); + constructor(url, collections) { + super(url); + this._path = urlToPath(url); + this._collections = collections || {}; + } + + /** + * Save (upsert) document + * + * @param {String} collection Collection's name + * @param {ObjectId?} id Document's id + * @param {Object} values Data for save + * @returns {Promise} Promise with result insert or update query + */ + save(collection, id, values) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + const cb = (error, result) => error ? reject(error) : resolve(result._id); + + // TODO: I'd like to just use update with upsert:true, but I'm + // note sure how the query will work if id == null. Seemed to + // have some problems before with passing null ids. + if (id === null) { + db.insert(values, cb); + } else { + db.update({_id: id}, {$set: values}, {upsert: true}, cb); + } + }); + } + + /** + * Delete document + * + * @param {String} collection Collection's name + * @param {ObjectId} id Document's id + * @returns {Promise} + */ + delete(collection, id) { + return new Promise((resolve, reject) => { + if (id === null) { + return resolve(0); + } + + const db = getCollection(collection, this._collections, this._path); + return db.remove({_id: id}, (error, numRemoved) => error ? reject(error) : resolve(numRemoved)); + }); + } + + /** + * Delete one document by query + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + deleteOne(collection, query) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + + db.remove(query, (error, numRemoved) => error ? reject(error) : resolve(numRemoved)); + }); + } + + /** + * Delete many documents by query + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + deleteMany(collection, query) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + db.remove(query, {multi: true}, (error, numRemoved) => error ? reject(error) : resolve(numRemoved)); + }); + } + + /** + * Find one document + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + findOne(collection, query) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + db.findOne(query, (error, result) => error ? reject(error) : resolve(result)); + }); + } + + /** + * Find one document and update it + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} values + * @param {Object} options + * @returns {Promise} + */ + findOneAndUpdate(collection, query, values, options) { + options = options || {}; + + // Since this is 'findOne...' we'll only allow user to update + // one document at a time + options.multi = false; + + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + + // TODO: Would like to just use 'Collection.update' here, but + // it doesn't return objects on update (but will on insert)... + /* db.update(query, values, options, function(error, numReplaced, newDoc) { + if (error) return reject(error); + resolve(newDoc); + }); */ + + this.findOne(collection, query) + .then(data => { + if (!data) { + if (options.upsert) { + return db.insert(values, (error, result) => error ? reject(error) : resolve(result)); + } + + return resolve(null); + } + + return db.update(query, {$set: values}, error => { + if (error) { + return reject(error); + } + + // Fixes issue #55. Remove when NeDB is updated to v1.8+ + return db.findOne({_id: data._id}, (error, result) => error ? reject(error) : resolve(result)); + }); }); - } + }); + } + + /** + * Find one document and delete it + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + findOneAndDelete(collection, query, options) { + options = options || {}; + + // Since this is 'findOne...' we'll only allow user to update + // one document at a time + options.multi = false; + + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + db.remove(query, options, (error, numRemoved) => error ? reject(error) : resolve(numRemoved)); + }); + } + + /** + * Find documents + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + find(collection, query, options) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + let cursor = db.find(query); + + if (options.sort && (_.isArray(options.sort) || _.isString(options.sort))) { + const sortOptions = {}; + if (!_.isArray(options.sort)) { + options.sort = [options.sort]; + } + + options.sort.forEach(function(s) { + if (!_.isString(s)) return; - findOneAndDelete(collection, query, options) { - var that = this; + let sortOrder = 1; + if (s[0] === '-') { + sortOrder = -1; + s = s.substring(1); + } + sortOptions[s] = sortOrder; + }); - if (!options) { - options = {}; + cursor = cursor.sort(sortOptions); + } + + if (typeof options.skip === 'number') { + cursor = cursor.skip(options.skip); + } + + if (typeof options.limit === 'number') { + cursor = cursor.limit(options.limit); + } + + cursor.exec((error, result) => error ? reject(error) : resolve(result)); + }); + } + + /** + * Get count of collection by query + * + * @param {String} collection Collection's name + * @param {Object} query Query + * @returns {Promise} + */ + count(collection, query) { + return new Promise((resolve, reject) => { + const db = getCollection(collection, this._collections, this._path); + db.count(query, (error, result) => error ? reject(error) : resolve(result)); + }); + } + + /** + * Create index + * + * @param {String} collection Collection's name + * @param {String} field Field name + * @param {Object} options Options + * @returns {Promise} + */ + createIndex(collection, field, options) { + options = options || {}; + options.unique = options.unique || false; + options.sparse = options.sparse || false; + + const db = getCollection(collection, this._collections, this._path); + + return new Promise((resolve, reject) => { + db.ensureIndex( + {fieldName: field, unique: options.unique, sparse: options.sparse}, + (error, result) => error ? reject(error) : resolve(result) + ); + }); + } + + /** + * Connect to database + * + * @param {String} url + * @param {Object} options + * @returns {Promise} + */ + static connect(url, options) { + // Could be directory path or 'memory' + const dbLocation = urlToPath(url); + + return new Promise((resolve, reject) => { + const collections = {}; + + // TODO: Load all data upfront or on-demand? + // Maybe give user the option to load upfront. + // But which should we do by default? + /*fs.readdir(dbLocation, function(error, files) { + files.forEach(function(file) { + const extname = path.extname(file); + const filename = file.split('.')[0]; + if (extname === '.db' && filename.length > 0) { + const collectionName = filename; + collections[collectionName] = createCollection(collectionName, dbLocation); + } + }); + global.CLIENT = new NeDbClient(dbLocation, collections); + resolve(global.CLIENT); + });*/ + //global.CLIENT = new NeDbClient(dbLocation, collections); + resolve(new NeDbClient(dbLocation, collections)); + }); + } + + /** + * Close current connection + * + * @returns {Promise} + */ + close() { + return Promise.resolve(); + } + + /** + * Drop collection + * + * @param {String} collection + * @returns {Promise} + */ + clearCollection(collection) { + return this.deleteMany(collection, {}); + } + + /** + * Drop current database + * + * @returns {Promise} + */ + dropDatabase() { + const clearPromises = []; + + Object.keys(this._collections) + .forEach(collectionName => clearPromises.push(this._removeCollection(collectionName))); + + return Promise.all(clearPromises); + } + + /** + * Remove collection from _collection and from memory or FS + * + * @param {String} collectionName + * @returns {Promise} + * @private + */ + _removeCollection(collectionName) { + return new Promise(resolve => { + const dbLocation = getCollectionPath(this._path, collectionName); + + if (dbLocation === 'memory') { + // Only exists in memory, so just delete the 'Datastore' + delete this._collections[collectionName]; + resolve(); + } else { + resolve(this._removeFileCollection(collectionName, dbLocation)); + } + }); + } + + /** + * Remove collection from FS + * + * @param {String} collectionName + * @param {String} dbLocation + * @private + */ + _removeFileCollection(collectionName, dbLocation) { + const exist = new Promise(resolve => fs.stat(dbLocation, err => err ? resolve(false) : resolve(true))); + + exist + .then(isExists => { + if (!isExists) { + return Promise.resolve(); } - // Since this is 'findOne...' we'll only allow user to update - // one document at a time - options.multi = false; - - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - db.remove(query, options, function (error, numRemoved) { - if (error) return reject(error); - return resolve(numRemoved); - }); - }); - } - - find(collection, query, options) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - var cursor = db.find(query); - - if (options.sort && (_.isArray(options.sort) || _.isString(options.sort))) { - var sortOptions = {}; - if (!_.isArray(options.sort)) { - options.sort = [options.sort]; - } - - options.sort.forEach(function(s) { - if (!_.isString(s)) return; - - var sortOrder = 1; - if (s[0] === '-') { - sortOrder = -1; - s = s.substring(1); - } - sortOptions[s] = sortOrder; - }); - - cursor = cursor.sort(sortOptions); - } - if (typeof options.skip === 'number') { - cursor = cursor.skip(options.skip); - } - if (typeof options.limit === 'number') { - cursor = cursor.limit(options.limit); + return new Promise((resolve, reject) => { + fs.unlink(dbLocation, unlink_error => { + if (unlink_error) { + return reject(unlink_error); } - cursor.exec(function(error, result) { - if (error) return reject(error); - return resolve(result); - }); + + delete this._collections[collectionName]; + return resolve(); + }); }); - } - - count(collection, query) { - var that = this; - return new Promise(function(resolve, reject) { - var db = getCollection(collection, that._collections, that._path); - db.count(query, function (error, count) { - if (error) return reject(error); - return resolve(count); - }); - }); - } - - createIndex(collection, field, options) { - options = options || {}; - options.unique = options.unique || false; - options.sparse = options.sparse || false; - - var db = getCollection(collection, this._collections, this._path); - db.ensureIndex({fieldName: field, unique: options.unique, sparse: options.sparse}); - } - - static connect(url, options) { - // Could be directory path or 'memory' - var dbLocation = urlToPath(url); - - return new Promise(function(resolve, reject) { - var collections = {}; - - // TODO: Load all data upfront or on-demand? - // Maybe give user the option to load upfront. - // But which should we do by default? - /*fs.readdir(dbLocation, function(error, files) { - files.forEach(function(file) { - var extname = path.extname(file); - var filename = file.split('.')[0]; - if (extname === '.db' && filename.length > 0) { - var collectionName = filename; - collections[collectionName] = createCollection(collectionName, dbLocation); - } - }); - global.CLIENT = new NeDbClient(dbLocation, collections); - resolve(global.CLIENT); - });*/ - //global.CLIENT = new NeDbClient(dbLocation, collections); - resolve(new NeDbClient(dbLocation, collections)); - }); - } - - close() { - // Nothing to do for NeDB - } - - clearCollection(collection) { - return this.deleteMany(collection, {}); - } - - dropDatabase() { - var that = this; - - var clearPromises = []; - _.keys(this._collections).forEach(function(key) { - var p = new Promise(function(resolve, reject) { - var dbLocation = getCollectionPath(that._path, key); - - if (dbLocation === 'memory') { - // Only exists in memory, so just delete the 'Datastore' - delete that._collections[key]; - resolve(); - } else { - // Delete the file, but only if it exists - fs.stat(dbLocation, function(err, stat) { - if (err === null) { - fs.unlink(dbLocation, function(err) { - if (err) reject(err); - delete that._collections[key]; - resolve(); - }); - } else { - resolve(); - } - }); - } - }); - clearPromises.push(p); - }); - - return Promise.all(clearPromises); - } - - toCanonicalId(id) { - return id; - } - - // Native ids are the same as NeDB ids - isNativeId(value) { - return String(value).match(/^[a-zA-Z0-9]{16}$/) !== null; - } - - nativeIdType() { - return String; - } - - driver() { - return this._collections; - } + }); + } + + toCanonicalId(id) { + return id; + } + + // Native ids are the same as NeDB ids + isNativeId(value) { + return String(value).match(/^[a-zA-Z0-9]{16}$/) !== null; + } + + nativeIdType() { + return String; + } + + driver() { + return this._collections; + } } -module.exports = NeDbClient; \ No newline at end of file +module.exports = NeDbClient; diff --git a/lib/db.js b/lib/db.js index 6a9deb6..44450a4 100644 --- a/lib/db.js +++ b/lib/db.js @@ -1,20 +1,30 @@ -var NeDbClient = require('./clients/nedbclient'); -var MongoClient = require('./clients/mongoclient'); +'use strict'; +const NeDbClient = require('./clients/nedbclient'); +const MongoClient = require('./clients/mongoclient'); + +/** + * Connect to current database + * + * @param {String} url + * @param {Object} options + * @returns {Promise} + */ exports.connect = function(url, options) { - if (url.indexOf('nedb://') > -1) { - // url example: nedb://path/to/file/folder - return NeDbClient.connect(url, options).then(function(db) { - global.CLIENT = db; - return db; - }); - } else if(url.indexOf('mongodb://') > -1) { - // url example: 'mongodb://localhost:27017/myproject' - return MongoClient.connect(url, options).then(function(db) { - global.CLIENT = db; - return db; - }); - } else { - return Promise.reject(new Error('Unrecognized DB connection url.')); - } -}; \ No newline at end of file + let client = null; + + if (url.indexOf('nedb://') > -1) { + client = NeDbClient; + } else if (url.indexOf('mongodb://') > -1) { + client = MongoClient; + } else { + return Promise.reject(new Error('Unrecognized DB connection url.')); + } + + return client + .connect(url, options) + .then(db => { + global.CLIENT = db; + return db; + }); +}; diff --git a/lib/document.js b/lib/document.js index 7c1fae7..bc23b09 100644 --- a/lib/document.js +++ b/lib/document.js @@ -1,357 +1,382 @@ -"use strict"; - -var _ = require('lodash'); -var deprecate = require('depd')('camo'); -var DB = require('./clients').getClient; -var BaseDocument = require('./base-document'); -var isSupportedType = require('./validate').isSupportedType; -var isArray = require('./validate').isArray; -var isReferenceable = require('./validate').isReferenceable; -var isEmbeddedDocument = require('./validate').isEmbeddedDocument; -var isString = require('./validate').isString; +'use strict'; -class Document extends BaseDocument { - constructor(name) { - super(); - - if (name !== undefined && name !== null) { - deprecate('Document.constructor(name) - override Document.collectionName() instead'); - this._meta = { - collection: name - }; - } - } - - // TODO: Is there a way to tell if a class is - // a subclass of something? Until I find out - // how, we'll be lazy use this. - static documentClass() { - return 'document'; - } - - documentClass() { - return 'document'; - } - - get meta() { - return this._meta; - } +const _ = require('lodash'); +const deprecate = require('depd')('camo'); +const db = require('./clients').getClient; +const BaseDocument = require('./base-document'); +const isArray = require('./validate').isArray; +const isReferenceable = require('./validate').isReferenceable; +const isEmbeddedDocument = require('./validate').isEmbeddedDocument; - set meta(meta) { - this._meta = meta; +class Document extends BaseDocument { + constructor(name) { + super(); + + if (name !== undefined && name !== null) { + deprecate('Document.constructor(name) - override Document.collectionName() instead'); + this._meta = { + collection: name + }; } + } + + // TODO: Is there a way to tell if a class is + // a subclass of something? Until I find out + // how, we'll be lazy use this. + static documentClass() { + return 'document'; + } + + documentClass() { + return 'document'; + } + + get meta() { + return this._meta; + } + + set meta(meta) { + this._meta = meta; + } + + /** + * Save (upsert) current document + * + * TODO: The method is too long and complex, it is necessary to divide... + * @returns {Promise} + */ + save() { + const preValidatePromises = this._getHookPromises('preValidate'); + + /* eslint max-nested-callbacks: [2,5] */ // TODO: To reduce the number of nested callbacks + return Promise + .all(preValidatePromises) + .then(() => { + // Ensure we at least have defaults set + + // TODO: We already do this on .create(), so + // should it really be done again? + Object.keys(this._schema).forEach(key => { + if (!(key in this._schema)) { + this[key] = this.getDefault(key); + } + }); - save() { - var that = this; - - var preValidatePromises = this._getHookPromises('preValidate'); - - return Promise.all(preValidatePromises).then(function() { + // Validate the assigned type, choices, and min/max + this.validate(); - // Ensure we at least have defaults set + // Ensure all data types are saved in the same encodings + this.canonicalize(); - // TODO: We already do this on .create(), so - // should it really be done again? - _.keys(that._schema).forEach(function(key) { - if (!(key in that._schema)) { - that[key] = that.getDefault(key); - } - }); - - // Validate the assigned type, choices, and min/max - that.validate(); - - // Ensure all data types are saved in the same encodings - that.canonicalize(); - - // TODO: We should instead track what has changed and - // only update those values. Maybe make that._changed - // object to do this. - // Also, this might be really slow for objects with - // lots of references. Figure out a better way. - var toUpdate = that._toData({_id: false}); - - // Reference our objects - _.keys(that._schema).forEach(function(key) { - // Never care about _id - if (key === '_id') return; - - if (isReferenceable(that[key]) || // isReferenceable OR - (isArray(that[key]) && // isArray AND contains value AND value isReferenceable - that[key].length > 0 && - isReferenceable(that[key][0]))) { - - // Handle array of references (ex: { type: [MyObject] }) - if (isArray(that[key])) { - toUpdate[key] = []; - that[key].forEach(function(v) { - if (DB().isNativeId(v)) { - toUpdate[key].push(v); - } else { - toUpdate[key].push(v._id); - } - }); - } else { - if (DB().isNativeId(that[key])) { - toUpdate[key] = that[key]; - } else { - toUpdate[key] = that[key]._id; - } - } - - } - }); - - // Replace EmbeddedDocument references with just their data - _.keys(that._schema).forEach(function(key) { - if (isEmbeddedDocument(that[key]) || // isEmbeddedDocument OR - (isArray(that[key]) && // isArray AND contains value AND value isEmbeddedDocument - that[key].length > 0 && - isEmbeddedDocument(that[key][0]))) { - - // Handle array of references (ex: { type: [MyObject] }) - if (isArray(that[key])) { - toUpdate[key] = []; - that[key].forEach(function(v) { - toUpdate[key].push(v._toData()); - }); - } else { - toUpdate[key] = that[key]._toData(); - } + // TODO: We should instead track what has changed and + // only update those values. Maybe make this._changed + // object to do this. + // Also, this might be really slow for objects with + // lots of references. Figure out a better way. + const toUpdate = this._toData({_id: false}); + // Reference our objects + Object.keys(this._schema).forEach(key => { + // Never care about _id + if (key === '_id') { + return; + } + + if (isReferenceable(this[key]) || // isReferenceable OR + (isArray(this[key]) && // isArray AND contains value AND value isReferenceable + this[key].length > 0 && + isReferenceable(this[key][0]))) { + + // Handle array of references (ex: { type: [MyObject] }) + if (isArray(this[key])) { + toUpdate[key] = []; + this[key].forEach(value => { + if (db().isNativeId(value)) { + toUpdate[key].push(value); + } else { + toUpdate[key].push(value._id); } - }); - - return toUpdate; - }).then(function(data) { - // TODO: hack? - var postValidatePromises = [data].concat(that._getHookPromises('postValidate')); - return Promise.all(postValidatePromises); - }).then(function(prevData) { - var data = prevData[0]; - // TODO: hack? - var preSavePromises = [data].concat(that._getHookPromises('preSave')); - return Promise.all(preSavePromises); - }).then(function(prevData) { - var data = prevData[0]; - return DB().save(that.collectionName(), that._id, data); - }).then(function(id) { - if (that._id === null) { - that._id = id; + }); + } else { + if (db().isNativeId(this[key])) { + toUpdate[key] = this[key]; + } else { + toUpdate[key] = this[key]._id; + } } - }).then(function() { - // TODO: hack? - var postSavePromises = that._getHookPromises('postSave'); - return Promise.all(postSavePromises); - }).then(function() { - return that; - }).catch(function(error) { - return Promise.reject(error); - }); - } - delete() { - var that = this; - - var preDeletePromises = that._getHookPromises('preDelete'); - - return Promise.all(preDeletePromises).then(function() { - return DB().delete(that.collectionName(), that._id); - }).then(function(deleteReturn) { - // TODO: hack? - var postDeletePromises = [deleteReturn].concat(that._getHookPromises('postDelete')); - return Promise.all(postDeletePromises); - }).then(function(prevData) { - var deleteReturn = prevData[0]; - return deleteReturn; + } }); - } - - static deleteOne(query) { - return DB().deleteOne(this.collectionName(), query); - } - static deleteMany(query) { - if (query === undefined || query === null) { - query = {}; - } - - return DB().deleteMany(this.collectionName(), query); - } - - static loadOne(query, options) { - deprecate('loadOne - use findOne instead'); - return this.findOne(query, options); - } - - // TODO: Need options to specify whether references should be loaded - static findOne(query, options) { - var that = this; - - var populate = true; - if (options && options.hasOwnProperty('populate')) { - populate = options.populate; - } - - return DB().findOne(this.collectionName(), query) - .then(function(data) { - if (!data) { - return null; - } - - var doc = that._fromData(data); - if (populate === true || (isArray(populate) && populate.length > 0)) { - return that.populate(doc, populate); + // Replace EmbeddedDocument references with just their data + /* eslint no-underscore-dangle: 0 */ + Object.keys(this._schema).forEach(key => { + if (isEmbeddedDocument(this[key]) || // isEmbeddedDocument OR + (isArray(this[key]) && // isArray AND contains value AND value isEmbeddedDocument + this[key].length > 0 && + isEmbeddedDocument(this[key][0]))) { + + // Handle array of references (ex: { type: [MyObject] }) + if (isArray(this[key])) { + toUpdate[key] = []; + this[key].forEach(value => toUpdate[key].push(value._toData())); + } else { + toUpdate[key] = this[key]._toData(); } - return doc; - }).then(function(docs) { - if (docs) { - return docs; - } - return null; + } }); - } - static loadOneAndUpdate(query, values, options) { - deprecate('loadOneAndUpdate - use findOneAndUpdate instead'); - return this.findOneAndUpdate(query, values, options); + return toUpdate; + }) + .then(data => { + // TODO: hack? + const postValidatePromises = [data].concat(this._getHookPromises('postValidate')); + return Promise.all(postValidatePromises); + }) + .then(prevData => { + const data = prevData[0]; + // TODO: hack? + const preSavePromises = [data].concat(this._getHookPromises('preSave')); + return Promise.all(preSavePromises); + }) + .then(prevData => { + const data = prevData[0]; + return db().save(this.collectionName(), this._id, data); + }) + .then(id => { + if (this._id === null) { + this._id = id; + } + }) + .then(() => { + // TODO: hack? + const postSavePromises = this._getHookPromises('postSave'); + return Promise.all(postSavePromises); + }) + .then(() => this); + } + + /** + * Delete current document + * + * @returns {Promise} + */ + delete() { + const preDeletePromises = this._getHookPromises('preDelete'); + + return Promise + .all(preDeletePromises) + .then(() => db().delete(this.collectionName(), this._id)) + .then(deleteReturn => { + // TODO: hack? + const postDeletePromises = [deleteReturn].concat(this._getHookPromises('postDelete')); + return Promise.all(postDeletePromises); + }) + .then(prevData => { + const deleteReturn = prevData[0]; + return deleteReturn; + }); + } + + /** + * Delete one document in current collection + * + * @param {Object} query Query + * @returns {Promise} + */ + static deleteOne(query) { + return db().deleteOne(this.collectionName(), query); + } + + /** + * Delete many documents in current collection + * + * @param {Object} query Query + * @returns {Promise} + */ + static deleteMany(query) { + query = query || {}; + + return db().deleteMany(this.collectionName(), query); + } + + /** + * Find one document in current collection + * + * TODO: Need options to specify whether references should be loaded + * + * @param {Object} query Query + * @returns {Promise} + */ + static findOne(query, options) { + let populate = true; + if (options && options.hasOwnProperty('populate')) { + populate = options.populate; } - static findOneAndUpdate(query, values, options) { - var that = this; - - if (arguments.length < 2) { - throw new Error('findOneAndUpdate requires at least 2 arguments. Got ' + arguments.length + '.'); + return db() + .findOne(this.collectionName(), query) + .then(data => { + if (!data) { + return null; } - if (!options) { - options = {}; + const doc = this._fromData(data); + if (populate === true || (isArray(populate) && populate.length > 0)) { + return this.populate(doc, populate); } - var populate = true; - if (options.hasOwnProperty('populate')) { - populate = options.populate; + return doc; + }) + .then(docs => { + if (docs) { + return docs; } - - return DB().findOneAndUpdate(this.collectionName(), query, values, options) - .then(function(data) { - if (!data) { - return null; - } - - var doc = that._fromData(data); - if (populate) { - return that.populate(doc); - } - - return doc; - }).then(function(doc) { - if (doc) { - return doc; - } - return null; - }); - } - - static loadOneAndDelete(query, options) { - deprecate('loadOneAndDelete - use findOneAndDelete instead'); - return this.findOneAndDelete(query, options); + return null; + }); + } + + /** + * Find one document and update it in current collection + * + * @param {Object} query Query + * @param {Object} values + * @param {Object} options + * @returns {Promise} + */ + static findOneAndUpdate(query, values, options) { + if (values === undefined) { + throw new Error('findOneAndUpdate requires at least 2 arguments.'); } - static findOneAndDelete(query, options) { - var that = this; - - if (arguments.length < 1) { - throw new Error('findOneAndDelete requires at least 1 argument. Got ' + arguments.length + '.'); - } - - if (!options) { - options = {}; - } - - return DB().findOneAndDelete(this.collectionName(), query, options); - } + options = options || {}; - static loadMany(query, options) { - deprecate('loadMany - use find instead'); - return this.find(query, options); + let populate = true; + if (options.hasOwnProperty('populate')) { + populate = options.populate; } - // TODO: Need options to specify whether references should be loaded - static find(query, options) { - var that = this; - - if (query === undefined || query === null) { - query = {}; + return db().findOneAndUpdate(this.collectionName(), query, values, options) + .then(data => { + if (!data) { + return null; } - if (options === undefined || options === null) { - // Populate by default - options = {populate: true}; + const doc = this._fromData(data); + if (populate) { + return this.populate(doc); } - return DB().find(this.collectionName(), query, options) - .then(function(datas) { - var docs = that._fromData(datas); - - if (options.populate === true || - (isArray(options.populate) && options.populate.length > 0)) { - return that.populate(docs, options.populate); - } - - return docs; - }).then(function(docs) { - // Ensure we always return an array - return [].concat(docs); - }); - } - - static count(query) { - var that = this; - return DB().count(this.collectionName(), query); + return doc; + }) + .then(doc => doc || null); + } + + /** + * Find one document and delete it in current collection + * + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + static findOneAndDelete(query, options) { + if (query === undefined) { + throw new Error('findOneAndDelete requires at least 1 argument.'); } - static createIndexes() { - if (this._indexesCreated) { - return; + options = options || {}; + + return db().findOneAndDelete(this.collectionName(), query, options); + } + + /** + * Find documents + * + * TODO: Need options to specify whether references should be loaded + * + * @param {Object} query Query + * @param {Object} options + * @returns {Promise} + */ + static find(query, options) { + query = query || {}; + options = options || {populate: true}; // TODO: WHAT? WHY? + + return db() + .find(this.collectionName(), query, options) + .then(datas => { + const docs = this._fromData(datas); + + if (options.populate === true || + (isArray(options.populate) && options.populate.length > 0)) { + return this.populate(docs, options.populate); } - var that = this; - var instance = this._instantiate(); - - _.keys(instance._schema).forEach(function(k) { - if (instance._schema[k].unique) { - DB().createIndex(that.collectionName(), k, {unique: true}); - } - }); - - this._indexesCreated = true; + return docs; + }) + .then(docs => [].concat(docs)); // Ensure we always return an array + } + + /** + * Get count documents in current collection by query + * + * @param {Object} query Query + * @returns {Promise} + */ + static count(query) { + return db().count(this.collectionName(), query); + } + + /** + * Create indexes + * + * @returns {Promise} + */ + static createIndexes() { + if (this._indexesCreated) { + return; } - static _fromData(datas) { - var instances = super._fromData(datas); - // This way we preserve the original structure of the data. Data - // that was passed as an array is returned as an array, and data - // passes as a single object is returned as single object - var datasArray = [].concat(datas); - var instancesArray = [].concat(instances); - - /*for (var i = 0; i < instancesArray.length; i++) { - if (datasArray[i].hasOwnProperty('_id')) { - instancesArray[i]._id = datasArray[i]._id; - } else { - instancesArray[i]._id = null; - } - }*/ - - return instances; - } + const instance = this._instantiate(); + + Object.keys(instance._schema).forEach(key => { + if (instance._schema[key].unique) { + db().createIndex(this.collectionName(), key, {unique: true}); + } + }); + + this._indexesCreated = true; + } + + static _fromData(datas) { + const instances = super._fromData(datas); + // This way we preserve the original structure of the data. Data + // that was passed as an array is returned as an array, and data + // passes as a single object is returned as single object + const datasArray = [].concat(datas); + const instancesArray = [].concat(instances); + + /* for (const i = 0; i < instancesArray.length; i++) { + if (datasArray[i].hasOwnProperty('_id')) { + instancesArray[i]._id = datasArray[i]._id; + } else { + instancesArray[i]._id = null; + } + } */ + + return instances; + } + + /** + * Clear current collection + * + * @returns {Promise} + */ + static clearCollection() { + return db().clearCollection(this.collectionName()); + } - static clearCollection() { - return DB().clearCollection(this.collectionName()); - } - } -module.exports = Document; \ No newline at end of file +module.exports = Document; diff --git a/lib/embedded-document.js b/lib/embedded-document.js index 5b3a751..9c1a721 100644 --- a/lib/embedded-document.js +++ b/lib/embedded-document.js @@ -1,31 +1,31 @@ -"use strict"; +'use strict'; var BaseDocument = require('./base-document'); class EmbeddedDocument extends BaseDocument { - constructor() { - super(); + constructor() { + super(); - // TODO: Move _id logic out of BaseDocument. - // A better fix to this issue is to remove - // _schema._id and _id from BaseDocument. But - // since quite a bit of _id logic is still - // in BD, we'll have to use this fix until - // it is removed - delete this._schema._id; - delete this._id; - } + // TODO: Move _id logic out of BaseDocument. + // A better fix to this issue is to remove + // _schema._id and _id from BaseDocument. But + // since quite a bit of _id logic is still + // in BD, we'll have to use this fix until + // it is removed + delete this._schema._id; + delete this._id; + } - // TODO: Is there a way to tell if a class is - // a subclass of something? Until I find out - // how, we'll be lazy use this. - static documentClass() { - return 'embedded'; - } + // TODO: Is there a way to tell if a class is + // a subclass of something? Until I find out + // how, we'll be lazy use this. + static documentClass() { + return 'embedded'; + } - documentClass() { - return 'embedded'; - } + documentClass() { + return 'embedded'; + } } -module.exports = EmbeddedDocument; \ No newline at end of file +module.exports = EmbeddedDocument; diff --git a/lib/errors.js b/lib/errors.js index 3f651e9..7d1b64d 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -2,44 +2,40 @@ /* * Base Camo error. - * + * * Adapted from es6-error package. */ class CamoError extends Error { - constructor(message) { - super(message); + constructor(message) { + super(message); - // Extending Error is weird and does not propagate `message` - Object.defineProperty(this, 'message', { - enumerable : false, - value : message - }); + // Extending Error is weird and does not propagate `message` + Object.defineProperty(this, 'message', { + enumerable: false, + value: message + }); - Object.defineProperty(this, 'name', { - enumerable : false, - value : this.constructor.name, - }); + Object.defineProperty(this, 'name', { + enumerable: false, + value: this.constructor.name + }); - if (Error.hasOwnProperty('captureStackTrace')) { - Error.captureStackTrace(this, this.constructor); - return; - } - - Object.defineProperty(this, 'stack', { - enumerable : false, - value : (new Error(message)).stack, - }); + if (Error.hasOwnProperty('captureStackTrace')) { + Error.captureStackTrace(this, this.constructor); + return; } + + Object.defineProperty(this, 'stack', { + enumerable: false, + value: (new Error(message)).stack + }); + } } /* * Error indicating document didn't pass validation. */ -class ValidationError extends CamoError { - constructor(message) { - super(message); - } -} +class ValidationError extends CamoError {} exports.CamoError = CamoError; -exports.ValidationError = ValidationError; \ No newline at end of file +exports.ValidationError = ValidationError; diff --git a/lib/util.js b/lib/util.js index 22cead7..77c9438 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,10 +1,16 @@ -var deepTraverse = function(obj, func) { - for (var i in obj) { - func.apply(this, [i, obj[i], obj]); - if (obj[i] !== null && typeof(obj[i]) == 'object') { - deepTraverse(obj[i], func); - } - } +'use strict'; + +const deepTraverse = function(obj, func) { + Object.keys(obj) + .forEach(key => { + + /* eslint no-invalid-this: 0 */ + func.apply(this, [key, obj[key], obj]); + + if (obj[key] !== null && typeof obj[key] === 'object') { + deepTraverse(obj[key], func); + } + }); }; -exports.deepTraverse = deepTraverse; \ No newline at end of file +exports.deepTraverse = deepTraverse; diff --git a/lib/validate.js b/lib/validate.js index 55a9f8c..c1c57b3 100644 --- a/lib/validate.js +++ b/lib/validate.js @@ -1,134 +1,143 @@ -var _ = require('lodash'); -var DB = require('./clients').getClient; +'use strict'; -var isString = function(s) { - return _.isString(s); +const _ = require('lodash'); +const db = require('./clients').getClient; + +const isString = function(s) { + return _.isString(s); }; -var isNumber = function(n) { - return _.isNumber(n) && _.isFinite(n) && !isString(n); +const isNumber = function(n) { + return _.isNumber(n) && _.isFinite(n) && !isString(n); }; -var isBoolean = function(b) { - return _.isBoolean(b); +const isBoolean = function(b) { + return _.isBoolean(b); }; -var isDate = function(d) { - return isNumber(d) || _.isDate(d) || isNumber(Date.parse(d)); +const isDate = function(d) { + return isNumber(d) || _.isDate(d) || isNumber(Date.parse(d)); }; -var isBuffer = function(b) { - return typeof b === 'object' || b instanceof Buffer; +const isBuffer = function(b) { + return typeof b === 'object' || b instanceof Buffer; }; -var isObject = function(o) { - return _.isObject(o); +const isObject = function(o) { + return _.isObject(o); }; -var isArray = function(a) { - return _.isArray(a); +const isArray = function(a) { + return _.isArray(a); }; -var isDocument = function(m) { - return m && m.documentClass && m.documentClass() === 'document'; +const isDocument = function(m) { + return m && m.documentClass && m.documentClass() === 'document'; }; -var isEmbeddedDocument = function(e) { - return e && e.documentClass && e.documentClass() === 'embedded'; +const isEmbeddedDocument = function(e) { + return e && e.documentClass && e.documentClass() === 'embedded'; }; -var isReferenceable = function(r) { - return isDocument(r) || isNativeId(r); +const isReferenceable = function(r) { + return isDocument(r) || isNativeId(r); }; -var isNativeId = function(n) { - return DB().isNativeId(n); +const isNativeId = function(n) { + return db().isNativeId(n); }; -var isSupportedType = function(t) { - return (t === String || t === Number || t === Boolean || - t === Buffer || t === Date || t === Array || - isArray(t) || t === Object || t instanceof Object || - typeof(t.documentClass) === 'function'); +const isSupportedType = function(t) { + return (t === String || t === Number || t === Boolean || + t === Buffer || t === Date || t === Array || + isArray(t) || t === Object || t instanceof Object || + typeof t.documentClass === 'function'); }; -var isType = function(value, type) { - if (type === String) { - return isString(value); - } else if (type === Number) { - return isNumber(value); - } else if (type === Boolean) { - return isBoolean(value); - } else if (type === Buffer) { - return isBuffer(value); - } else if (type === Date) { - return isDate(value); - } else if (type === Array || isArray(type)) { - return isArray(value); - } else if (type === Object) { - return isObject(value); - } else if (type.documentClass && type.documentClass() === 'document') { - return isDocument(value) || DB().isNativeId(value); - } else if (type.documentClass && type.documentClass() === 'embedded') { - return isEmbeddedDocument(value); - } else if (type === DB().nativeIdType()) { - return isNativeId(value); - } else { - throw new Error('Unsupported type: ' + type.name); - } +const isType = function(value, type) { + + /* eslint complexity: 0 */ + if (type === String) { + return isString(value); + } else if (type === Number) { + return isNumber(value); + } else if (type === Boolean) { + return isBoolean(value); + } else if (type === Buffer) { + return isBuffer(value); + } else if (type === Date) { + return isDate(value); + } else if (type === Array || isArray(type)) { + return isArray(value); + } else if (type === Object) { + return isObject(value); + } else if (type.documentClass && type.documentClass() === 'document') { + return isDocument(value) || db().isNativeId(value); + } else if (type.documentClass && type.documentClass() === 'embedded') { + return isEmbeddedDocument(value); + } else if (type === db().nativeIdType()) { + return isNativeId(value); + } + + throw new Error(`Unsupported type: ${type.name}`); }; -var isValidType = function(value, type) { - // NOTE - // Maybe look at this: - // https://github.com/Automattic/mongoose/tree/master/lib/types +const isValidType = function(value, type) { + // NOTE + // Maybe look at this: + // https://github.com/Automattic/mongoose/tree/master/lib/types - // TODO: For now, null is okay for all types. May - // want to specify in schema using 'nullable'? - if (value === null) return true; + // TODO: For now, null is okay for all types. May + // want to specify in schema using 'nullable'? + if (value === null) { + return true; + } - // Issue #9: To avoid all model members being stored - // in DB, allow undefined to be assigned. If you want - // unassigned members in DB, use null. - if (value === undefined) return true; + // Issue #9: To avoid all model members being stored + // in DB, allow undefined to be assigned. If you want + // unassigned members in DB, use null. + if (value === undefined) { + return true; + } - // Arrays take a bit more work - if (type === Array || isArray(type)) { - // Validation for types of the form [String], [Number], etc - if (isArray(type) && type.length > 1) { - throw new Error('Unsupported type. Only one type can be specified in arrays, but multiple found:', + type); - } + // Arrays take a bit more work + if (type === Array || isArray(type)) { + // Validation for types of the form [String], [Number], etc + if (isArray(type) && type.length > 1) { + throw new Error(`Unsupported type. Only one type can be specified in arrays, but multiple found: ${Number(type)}`); + } - if (isArray(type) && type.length === 1 && isArray(value)) { - var arrayType = type[0]; - for (var i = 0; i < value.length; i++) { - var v = value[i]; - if (!isType(v, arrayType)) { - return false; - } - } - } else if (isArray(type) && type.length === 0 && !isArray(value)) { - return false; - } else if (type === Array && !isArray(value)) { - return false; + if (isArray(type) && type.length === 1 && isArray(value)) { + const arrayType = type[0]; + for (let i = 0; i < value.length; i++) { + const v = value[i]; + if (!isType(v, arrayType)) { + return false; } - - return true; + } + } else if (isArray(type) && type.length === 0 && !isArray(value)) { + return false; + } else if (type === Array && !isArray(value)) { + return false; } - return isType(value, type); + return true; + } + + return isType(value, type); }; -var isInChoices = function(choices, choice) { - if (!choices) { - return true; - } - return choices.indexOf(choice) > -1; +const isInChoices = function(choices, choice) { + if (!choices) { + return true; + } + return choices.indexOf(choice) > -1; }; -var isEmptyValue = function(value) { - return typeof value === 'undefined' || (!(typeof value === 'number' || value instanceof Date || typeof value === 'boolean') - && (0 === Object.keys(value).length)); +const isEmptyValue = function(value) { + return typeof value === 'undefined' || + (!(typeof value === 'number' || value instanceof Date || typeof value === 'boolean') && + Object.keys(value).length === 0); }; exports.isString = isString; @@ -146,4 +155,4 @@ exports.isSupportedType = isSupportedType; exports.isType = isType; exports.isValidType = isValidType; exports.isInChoices = isInChoices; -exports.isEmptyValue = isEmptyValue; \ No newline at end of file +exports.isEmptyValue = isEmptyValue; diff --git a/package.json b/package.json index a5e5783..c9b4680 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,8 @@ }, "license": "MIT", "scripts": { - "test": "mocha" + "test": "./node_modules/.bin/mocha", + "lint": "./node_modules/.bin/eslint ./**/*.js" }, "engines": { "iojs": "~2.2.1", @@ -40,11 +41,12 @@ "lodash": "3.9.3" }, "optionalDependencies": { - "mongodb": "2.0.42", + "mongodb": "2.2.4", "nedb": "1.8.0" }, "devDependencies": { "chai": "3.0.0", - "mocha": "2.2.5" + "mocha": "2.2.5", + "eslint": "3.1.1" } } diff --git a/test/client.test.js b/test/client.test.js index 05aceaa..568fab3 100644 --- a/test/client.test.js +++ b/test/client.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var expect = require('chai').expect; @@ -14,620 +14,621 @@ var isNativeId = require('../lib/validate').isNativeId; describe('Client', function() { - var url = 'nedb://memory'; - //var url = 'mongodb://localhost/camo_test'; - var database = null; + var url = 'nedb://memory'; + var database = null; - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function() { - return done(); - }); + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); + }); - beforeEach(function(done) { - done(); - }); + beforeEach(function(done) { + done(); + }); - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); - after(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); + after(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); - describe('#save()', function() { - it('should persist the object and its members to the database', function(done) { + describe('#save()', function() { + it('should persist the object and its members to the database', function(done) { - var data = getData1(); + var data = getData1(); - data.save().then(function() { - validateId(data); - validateData1(data); - }).then(done, done); - }); + data.save().then(function() { + validateId(data); + validateData1(data); + }).then(done, done); }); + }); - class Address extends Document { - constructor() { - super(); + class Address extends Document { + constructor() { + super(); - this.street = String; - this.city = String; - this.zipCode = Number; - } + this.street = String; + this.city = String; + this.zipCode = Number; + } - static collectionName() { - return 'addresses'; - } + static collectionName() { + return 'addresses'; } + } - class Pet extends Document { - constructor() { - super(); + class Pet extends Document { + constructor() { + super(); - this.schema({ - type: String, - name: String, - }); - } + this.schema({ + type: String, + name: String, + }); } - - class User extends Document { - constructor() { - super(); - - this.schema({ - firstName: String, - lastName: String, - pet: Pet, - address: Address - }); - } + } + + class User extends Document { + constructor() { + super(); + + this.schema({ + firstName: String, + lastName: String, + pet: Pet, + address: Address + }); } + } - describe('#findOne()', function() { - it('should load a single object from the collection', function(done) { + describe('#findOne()', function() { + it('should load a single object from the collection', function(done) { - var data = getData1(); + var data = getData1(); - data.save().then(function() { - validateId(data); - return Data.findOne({item:99}); - }).then(function(d) { - validateId(d); - validateData1(d); - }).then(done, done); - }); - - it('should populate all fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return user.save(); - }).then(function() { - validateId(user); - return User.findOne({_id: user._id}, {populate: true}); - }).then(function(u) { - expect(u.pet).to.be.an.instanceof(Pet); - expect(u.address).to.be.an.instanceof(Address); - }).then(done, done); - }); + data.save().then(function() { + validateId(data); + return Data.findOne({item: 99}); + }).then(function(d) { + validateId(d); + validateData1(d); + }).then(done, done); + }); - it('should not populate any fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return user.save(); - }).then(function() { - validateId(user); - return User.findOne({_id: user._id}, {populate: false}); - }).then(function(u) { - expect(isNativeId(u.pet)).to.be.true; - expect(isNativeId(u.address)).to.be.true; - }).then(done, done); - }); + it('should populate all fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return user.save(); + }).then(function() { + validateId(user); + return User.findOne({_id: user._id}, {populate: true}); + }).then(function(u) { + expect(u.pet).to.be.an.instanceof(Pet); + expect(u.address).to.be.an.instanceof(Address); + }).then(done, done); + }); - it('should populate specified fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return user.save(); - }).then(function() { - validateId(user); - return User.findOne({_id: user._id}, {populate: ['pet']}); - }).then(function(u) { - expect(u.pet).to.be.an.instanceof(Pet); - expect(isNativeId(u.address)).to.be.true; - }).then(done, done); - }); + it('should not populate any fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return user.save(); + }).then(function() { + validateId(user); + return User.findOne({_id: user._id}, {populate: false}); + }).then(function(u) { + expect(isNativeId(u.pet)).to.be.true; + expect(isNativeId(u.address)).to.be.true; + }).then(done, done); }); - describe('#findOneAndUpdate()', function() { - it('should load and update a single object from the collection', function(done) { + it('should populate specified fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return user.save(); + }).then(function() { + validateId(user); + return User.findOne({_id: user._id}, {populate: ['pet']}); + }).then(function(u) { + expect(u.pet).to.be.an.instanceof(Pet); + expect(isNativeId(u.address)).to.be.true; + }).then(done, done); + }); + }); - var data = getData1(); + describe('#findOneAndUpdate()', function() { + it('should load and update a single object from the collection', function(done) { - data.save().then(function() { - validateId(data); - return Data.findOneAndUpdate({number: 1}, {source: 'wired'}); - }).then(function(d) { - validateId(d); - expect(d.number).to.equal(1); - expect(d.source).to.equal('wired'); - }).then(done, done); - }); + var data = getData1(); - it('should insert a single object to the collection', function(done) { - Data.findOne({number: 1}).then(function(d) { - expect(d).to.be.null; - return Data.findOneAndUpdate({number: 1}, {number: 1}, {upsert: true}); - }).then(function(data) { - validateId(data); - expect(data.number).to.equal(1); - return Data.findOne({number: 1}); - }).then(function(d) { - validateId(d); - expect(d.number).to.equal(1); - }).then(done, done); - }); + data.save().then(function() { + validateId(data); + return Data.findOneAndUpdate({number: 1}, {source: 'wired'}); + }).then(function(d) { + validateId(d); + expect(d.number).to.equal(1); + expect(d.source).to.equal('wired'); + }).then(done, done); }); - describe('#findOneAndDelete()', function() { - it('should load and delete a single object from the collection', function(done) { - - var data = getData1(); - - data.save().then(function() { - validateId(data); - return Data.count({ number: 1 }); - }).then(function(count) { - expect(count).to.be.equal(1); - return Data.findOneAndDelete({number: 1}); - }).then(function(numDeleted) { - expect(numDeleted).to.equal(1); - return Data.count({ number: 1 }); - }).then(function(count) { - expect(count).to.equal(0); - }).then(done, done); - }); + it('should insert a single object to the collection', function(done) { + Data.findOne({number: 1}).then(function(d) { + expect(d).to.be.null; + return Data.findOneAndUpdate({number: 1}, {number: 1}, {upsert: true}); + }).then(function(data) { + validateId(data); + expect(data.number).to.equal(1); + return Data.findOne({number: 1}); + }).then(function(d) { + validateId(d); + expect(d.number).to.equal(1); + }).then(done, done); }); + }); + + describe('#findOneAndDelete()', function() { + it('should load and delete a single object from the collection', function(done) { + + var data = getData1(); + + data.save().then(function() { + validateId(data); + return Data.count({number: 1}); + }).then(function(count) { + expect(count).to.be.equal(1); + return Data.findOneAndDelete({number: 1}); + }).then(function(numDeleted) { + expect(numDeleted).to.equal(1); + return Data.count({number: 1}); + }).then(function(count) { + expect(count).to.equal(0); + }).then(done, done); + }); + }); - describe('#find()', function() { - class City extends Document { - constructor() { - super(); - - this.name = String; - this.population = Number; - } - - static collectionName() { - return 'cities'; - } - } - - var Springfield, SouthPark, Quahog; - - beforeEach(function(done) { - Springfield = City.create({ - name: 'Springfield', - population: 30720 - }); - - SouthPark = City.create({ - name: 'South Park', - population: 4388 - }); - - Quahog = City.create({ - name: 'Quahog', - population: 800 - }); - - Promise.all([Springfield.save(), SouthPark.save(), Quahog.save()]) - .then(function() { - validateId(Springfield); - validateId(SouthPark); - validateId(Quahog); - done(); - }); - }); - - it('should load multiple objects from the collection', function(done) { - City.find({}).then(function(cities) { - expect(cities).to.have.length(3); - validateId(cities[0]); - validateId(cities[1]); - validateId(cities[2]); - }).then(done, done); - }); - - it('should load all objects when query is not provided', function(done) { - City.find().then(function(cities) { - expect(cities).to.have.length(3); - validateId(cities[0]); - validateId(cities[1]); - validateId(cities[2]); - }).then(done, done); - }); + describe('#find()', function() { + class City extends Document { + constructor() { + super(); - it('should sort results in ascending order', function(done) { - City.find({}, {sort: 'population'}).then(function(cities) { - expect(cities).to.have.length(3); - validateId(cities[0]); - validateId(cities[1]); - validateId(cities[2]); - expect(cities[0].population).to.be.equal(800); - expect(cities[1].population).to.be.equal(4388); - expect(cities[2].population).to.be.equal(30720); - }).then(done, done); - }); + this.name = String; + this.population = Number; + } - it('should sort results in descending order', function(done) { - City.find({}, {sort: '-population'}).then(function(cities) { - expect(cities).to.have.length(3); - validateId(cities[0]); - validateId(cities[1]); - validateId(cities[2]); - expect(cities[0].population).to.be.equal(30720); - expect(cities[1].population).to.be.equal(4388); - expect(cities[2].population).to.be.equal(800); - }).then(done, done); - }); + static collectionName() { + return 'cities'; + } + } - it('should sort results using multiple keys', function(done) { - var AlphaVille = City.create({ - name: 'Alphaville', - population: 4388 - }); - - var BetaTown = City.create({ - name: 'Beta Town', - population: 4388 - }); - - Promise.all([AlphaVille.save(), BetaTown.save()]).then(function() { - return City.find({}, {sort: ['population', '-name']}); - }).then(function(cities) { - expect(cities).to.have.length(5); - validateId(cities[0]); - validateId(cities[1]); - validateId(cities[2]); - validateId(cities[3]); - validateId(cities[4]); - expect(cities[0].population).to.be.equal(800); - expect(cities[0].name).to.be.equal('Quahog'); - expect(cities[1].population).to.be.equal(4388); - expect(cities[1].name).to.be.equal('South Park'); - expect(cities[2].population).to.be.equal(4388); - expect(cities[2].name).to.be.equal('Beta Town'); - expect(cities[3].population).to.be.equal(4388); - expect(cities[3].name).to.be.equal('Alphaville'); - expect(cities[4].population).to.be.equal(30720); - expect(cities[4].name).to.be.equal('Springfield'); - }).then(done, done); - }); + var Springfield, SouthPark, Quahog; - it('should limit number of results returned', function(done) { - City.find({}, {limit: 2}).then(function(cities) { - expect(cities).to.have.length(2); - validateId(cities[0]); - validateId(cities[1]); - }).then(done, done); + beforeEach(function(done) { + Springfield = City.create({ + name: 'Springfield', + population: 30720 + }); + + SouthPark = City.create({ + name: 'South Park', + population: 4388 + }); + + Quahog = City.create({ + name: 'Quahog', + population: 800 + }); + + Promise.all([Springfield.save(), SouthPark.save(), Quahog.save()]) + .then(function() { + validateId(Springfield); + validateId(SouthPark); + validateId(Quahog); + done(); }); + }); - it('should skip given number of results', function(done) { - City.find({}, {sort: 'population', skip: 1}).then(function(cities) { - expect(cities).to.have.length(2); - validateId(cities[0]); - validateId(cities[1]); - expect(cities[0].population).to.be.equal(4388); - expect(cities[1].population).to.be.equal(30720); - }).then(done, done); - }); + it('should load multiple objects from the collection', function(done) { + City.find({}).then(function(cities) { + expect(cities).to.have.length(3); + validateId(cities[0]); + validateId(cities[1]); + validateId(cities[2]); + }).then(done, done); + }); - it('should populate all fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user1 = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - var user2 = User.create({ - firstName: 'Sally', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return Promise.all([user1.save(), user2.save()]); - }).then(function() { - validateId(user1); - validateId(user2); - return User.find({}, {populate: true}); - }).then(function(users) { - expect(users[0].pet).to.be.an.instanceof(Pet); - expect(users[0].address).to.be.an.instanceof(Address); - expect(users[1].pet).to.be.an.instanceof(Pet); - expect(users[1].address).to.be.an.instanceof(Address); - }).then(done, done); - }); + it('should load all objects when query is not provided', function(done) { + City.find().then(function(cities) { + expect(cities).to.have.length(3); + validateId(cities[0]); + validateId(cities[1]); + validateId(cities[2]); + }).then(done, done); + }); - it('should not populate any fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user1 = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - var user2 = User.create({ - firstName: 'Sally', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return Promise.all([user1.save(), user2.save()]); - }).then(function() { - validateId(user1); - validateId(user2); - return User.find({}, {populate: false}); - }).then(function(users) { - expect(isNativeId(users[0].pet)).to.be.true; - expect(isNativeId(users[0].address)).to.be.true; - expect(isNativeId(users[1].pet)).to.be.true; - expect(isNativeId(users[1].address)).to.be.true; - }).then(done, done); - }); + it('should sort results in ascending order', function(done) { + City.find({}, {sort: 'population'}).then(function(cities) { + expect(cities).to.have.length(3); + validateId(cities[0]); + validateId(cities[1]); + validateId(cities[2]); + expect(cities[0].population).to.be.equal(800); + expect(cities[1].population).to.be.equal(4388); + expect(cities[2].population).to.be.equal(30720); + }).then(done, done); + }); - it('should populate specified fields', function(done) { - var address = Address.create({ - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345 - }); - - var dog = Pet.create({ - type: 'dog', - name: 'Fido', - }); - - var user1 = User.create({ - firstName: 'Billy', - lastName: 'Bob', - pet: dog, - address: address - }); - - var user2 = User.create({ - firstName: 'Sally', - lastName: 'Bob', - pet: dog, - address: address - }); - - Promise.all([address.save(), dog.save()]).then(function() { - validateId(address); - validateId(dog); - return Promise.all([user1.save(), user2.save()]); - }).then(function() { - validateId(user1); - validateId(user2); - return User.find({}, {populate: ['pet']}); - }).then(function(users) { - expect(users[0].pet).to.be.an.instanceof(Pet); - expect(isNativeId(users[0].address)).to.be.true; - expect(users[1].pet).to.be.an.instanceof(Pet); - expect(isNativeId(users[1].address)).to.be.true; - }).then(done, done); - }); + it('should sort results in descending order', function(done) { + City.find({}, {sort: '-population'}).then(function(cities) { + expect(cities).to.have.length(3); + validateId(cities[0]); + validateId(cities[1]); + validateId(cities[2]); + expect(cities[0].population).to.be.equal(30720); + expect(cities[1].population).to.be.equal(4388); + expect(cities[2].population).to.be.equal(800); + }).then(done, done); }); - describe('#count()', function() { - it('should return 0 objects from the collection', function(done) { + it('should sort results using multiple keys', function(done) { + var AlphaVille = City.create({ + name: 'Alphaville', + population: 4388 + }); + + var BetaTown = City.create({ + name: 'Beta Town', + population: 4388 + }); + + Promise.all([AlphaVille.save(), BetaTown.save()]).then(function() { + return City.find({}, {sort: ['population', '-name']}); + }).then(function(cities) { + expect(cities).to.have.length(5); + validateId(cities[0]); + validateId(cities[1]); + validateId(cities[2]); + validateId(cities[3]); + validateId(cities[4]); + expect(cities[0].population).to.be.equal(800); + expect(cities[0].name).to.be.equal('Quahog'); + expect(cities[1].population).to.be.equal(4388); + expect(cities[1].name).to.be.equal('South Park'); + expect(cities[2].population).to.be.equal(4388); + expect(cities[2].name).to.be.equal('Beta Town'); + expect(cities[3].population).to.be.equal(4388); + expect(cities[3].name).to.be.equal('Alphaville'); + expect(cities[4].population).to.be.equal(30720); + expect(cities[4].name).to.be.equal('Springfield'); + }).then(done, done); + }); - var data1 = getData1(); - var data2 = getData2(); + it('should limit number of results returned', function(done) { + City.find({}, {limit: 2}).then(function(cities) { + expect(cities).to.have.length(2); + validateId(cities[0]); + validateId(cities[1]); + }).then(done, done); + }); - Promise.all([data1.save(), data2.save()]).then(function() { - validateId(data1); - validateId(data2); - return Data.count({ number: 3 }); - }).then(function(count) { - expect(count).to.be.equal(0); - }).then(done, done); - }); + it('should skip given number of results', function(done) { + City.find({}, {sort: 'population', skip: 1}).then(function(cities) { + expect(cities).to.have.length(2); + validateId(cities[0]); + validateId(cities[1]); + expect(cities[0].population).to.be.equal(4388); + expect(cities[1].population).to.be.equal(30720); + }).then(done, done); + }); - it('should return 2 matching objects from the collection', function(done) { + it('should populate all fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user1 = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + var user2 = User.create({ + firstName: 'Sally', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return Promise.all([user1.save(), user2.save()]); + }).then(function() { + validateId(user1); + validateId(user2); + return User.find({}, {populate: true}); + }).then(function(users) { + expect(users[0].pet).to.be.an.instanceof(Pet); + expect(users[0].address).to.be.an.instanceof(Address); + expect(users[1].pet).to.be.an.instanceof(Pet); + expect(users[1].address).to.be.an.instanceof(Address); + }).then(done, done); + }); - var data1 = getData1(); - var data2 = getData2(); + it('should not populate any fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user1 = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + var user2 = User.create({ + firstName: 'Sally', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return Promise.all([user1.save(), user2.save()]); + }).then(function() { + validateId(user1); + validateId(user2); + return User.find({}, {populate: false}); + }).then(function(users) { + expect(isNativeId(users[0].pet)).to.be.true; + expect(isNativeId(users[0].address)).to.be.true; + expect(isNativeId(users[1].pet)).to.be.true; + expect(isNativeId(users[1].address)).to.be.true; + }).then(done, done); + }); - Promise.all([data1.save(), data2.save()]).then(function() { - validateId(data1); - validateId(data2); - return Data.count({}); - }).then(function(count) { - expect(count).to.be.equal(2); - }).then(done, done); - }); + it('should populate specified fields', function(done) { + var address = Address.create({ + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345 + }); + + var dog = Pet.create({ + type: 'dog', + name: 'Fido', + }); + + var user1 = User.create({ + firstName: 'Billy', + lastName: 'Bob', + pet: dog, + address: address + }); + + var user2 = User.create({ + firstName: 'Sally', + lastName: 'Bob', + pet: dog, + address: address + }); + + Promise.all([address.save(), dog.save()]).then(function() { + validateId(address); + validateId(dog); + return Promise.all([user1.save(), user2.save()]); + }).then(function() { + validateId(user1); + validateId(user2); + return User.find({}, {populate: ['pet']}); + }).then(function(users) { + expect(users[0].pet).to.be.an.instanceof(Pet); + expect(isNativeId(users[0].address)).to.be.true; + expect(users[1].pet).to.be.an.instanceof(Pet); + expect(isNativeId(users[1].address)).to.be.true; + }).then(done, done); }); + }); - describe('#delete()', function() { - it('should remove instance from the collection', function(done) { + describe('#count()', function() { + it('should return 0 objects from the collection', function(done) { - var data = getData1(); + var data1 = getData1(); + var data2 = getData2(); - data.save().then(function() { - validateId(data); - return data.delete(); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(1); - return Data.findOne({item:99}); - }).then(function(d) { - expect(d).to.be.null; - }).then(done, done); - }); + Promise.all([data1.save(), data2.save()]).then(function() { + validateId(data1); + validateId(data2); + return Data.count({number: 3}); + }).then(function(count) { + expect(count).to.be.equal(0); + }).then(done, done); }); - describe('#deleteOne()', function() { - it('should remove the object from the collection', function(done) { + it('should return 2 matching objects from the collection', function(done) { - var data = getData1(); + var data1 = getData1(); + var data2 = getData2(); - data.save().then(function() { - validateId(data); - return Data.deleteOne({number: 1}); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(1); - return Data.findOne({number: 1}); - }).then(function(d) { - expect(d).to.be.null; - }).then(done, done); - }); + Promise.all([data1.save(), data2.save()]).then(function() { + validateId(data1); + validateId(data2); + return Data.count({}); + }).then(function(count) { + expect(count).to.be.equal(2); + }).then(done, done); }); - - describe('#deleteMany()', function() { - it('should remove multiple objects from the collection', function(done) { - - var data1 = getData1(); - var data2 = getData2(); - - Promise.all([data1.save(), data2.save()]).then(function() { - validateId(data1); - validateId(data2); - return Data.deleteMany({}); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(2); - return Data.find({}); - }).then(function(datas) { - expect(datas).to.have.length(0); - }).then(done, done); - }); - - it('should remove all objects when query is not provided', function(done) { - - var data1 = getData1(); - var data2 = getData2(); - - Promise.all([data1.save(), data2.save()]).then(function() { - validateId(data1); - validateId(data2); - return Data.deleteMany(); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(2); - return Data.find({}); - }).then(function(datas) { - expect(datas).to.have.length(0); - }).then(done, done); - }); + }); + + describe('#delete()', function() { + it('should remove instance from the collection', function(done) { + + var data = getData1(); + + data.save().then(function() { + validateId(data); + return data.delete(); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(1); + return Data.findOne({item: 99}); + }).then(function(d) { + expect(d).to.be.null; + }).then(done, done); + }); + }); + + describe('#deleteOne()', function() { + it('should remove the object from the collection', function(done) { + + var data = getData1(); + + data.save().then(function() { + validateId(data); + return Data.deleteOne({number: 1}); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(1); + return Data.findOne({number: 1}); + }).then(function(d) { + expect(d).to.be.null; + }).then(done, done); + }); + }); + + describe('#deleteMany()', function() { + it('should remove multiple objects from the collection', function(done) { + + var data1 = getData1(); + var data2 = getData2(); + + Promise.all([data1.save(), data2.save()]).then(function() { + validateId(data1); + validateId(data2); + return Data.deleteMany({}); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(2); + return Data.find({}); + }).then(function(datas) { + expect(datas).to.have.length(0); + }).then(done, done); }); - describe('#clearCollection()', function() { - it('should remove all objects from the collection', function(done) { - - var data1 = getData1(); - var data2 = getData2(); - - Promise.all([data1.save(), data2.save()]).then(function() { - validateId(data1); - validateId(data2); - return Data.clearCollection(); - }).then(function() { - return Data.find(); - }).then(function(datas) { - expect(datas).to.have.length(0); - }).then(done, done); - }); + it('should remove all objects when query is not provided', function(done) { + + var data1 = getData1(); + var data2 = getData2(); + + Promise.all([data1.save(), data2.save()]).then(function() { + validateId(data1); + validateId(data2); + return Data.deleteMany(); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(2); + return Data.find({}); + }).then(function(datas) { + expect(datas).to.have.length(0); + }).then(done, done); + }); + }); + + describe('#clearCollection()', function() { + it('should remove all objects from the collection', function(done) { + + var data1 = getData1(); + var data2 = getData2(); + + Promise.all([data1.save(), data2.save()]).then(function() { + validateId(data1); + validateId(data2); + return Data.clearCollection(); + }).then(function() { + return Data.find(); + }).then(function(datas) { + expect(datas).to.have.length(0); + }).then(done, done); }); + }); }); \ No newline at end of file diff --git a/test/cyclic.test.js b/test/cyclic.test.js index 8eeaddf..6999a53 100644 --- a/test/cyclic.test.js +++ b/test/cyclic.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var fs = require('fs'); @@ -10,60 +10,62 @@ var Bar = require('./cyclic/bar'); describe('Cyclic', function() { - // TODO: Should probably use mock database client... - var url = 'nedb://memory'; - //var url = 'mongodb://localhost/camo_test'; - var database = null; + // TODO: Should probably use mock database client... + var url = 'nedb://memory'; + //var url = 'mongodb://localhost/camo_test'; + var database = null; - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function() { - return done(); - }); + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); + }); - beforeEach(function(done) { - done(); - }); + beforeEach(function(done) { + done(); + }); - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); - after(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); + after(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + describe('schema', function() { + it('should allow cyclic dependencies', function(done) { + var f = Foo.create(); + f.num = 26; + var b = Bar.create(); + b.num = 99; - describe('schema', function() { - it('should allow cyclic dependencies', function(done) { - var f = Foo.create(); - f.num = 26; - var b = Bar.create(); - b.num = 99; + f.save().then(function(foo) { + b.foo = foo; + return b.save(); + }).then(function(bar) { + f.bar = b; + return f.save(); + }).then(function(foo) { + return Foo.findOne({num: 26}); + }).then(function(foo) { + validateId(foo); + validateId(foo.bar); + expect(foo.num).to.be.equal(26); + expect(foo.bar.num).to.be.equal(99); + return Bar.findOne({num: 99}); + }).then(function(bar) { + validateId(bar); + validateId(bar.foo); + expect(bar.num).to.be.equal(99); + expect(bar.foo.num).to.be.equal(26); + }).then(done, done); - f.save().then(function(foo) { - b.foo = foo; - return b.save(); - }).then(function(bar) { - f.bar = b; - return f.save(); - }).then(function(foo) { - return Foo.findOne({ num: 26 }); - }).then(function(foo) { - validateId(foo); - validateId(foo.bar); - expect(foo.num).to.be.equal(26); - expect(foo.bar.num).to.be.equal(99); - return Bar.findOne({ num: 99 }); - }).then(function(bar) { - validateId(bar); - validateId(bar.foo); - expect(bar.num).to.be.equal(99); - expect(bar.foo.num).to.be.equal(26); - }).then(done, done); - - }); }); + }); }); \ No newline at end of file diff --git a/test/cyclic/bar.js b/test/cyclic/bar.js index cafd92d..e60e109 100644 --- a/test/cyclic/bar.js +++ b/test/cyclic/bar.js @@ -1,15 +1,14 @@ -"use strict"; +'use strict'; var Document = require('../../index').Document; -//var Foo = require('./foo'); class Bar extends Document { - constructor() { - super(); + constructor() { + super(); - this.foo = require('./foo'); - this.num = Number; - } + this.foo = require('./foo'); + this.num = Number; + } } -module.exports = Bar; \ No newline at end of file +module.exports = Bar; diff --git a/test/cyclic/foo.js b/test/cyclic/foo.js index 23c83d9..f0f28ac 100644 --- a/test/cyclic/foo.js +++ b/test/cyclic/foo.js @@ -1,15 +1,15 @@ -"use strict"; +'use strict'; var Document = require('../../index').Document; var Bar = require('./bar'); class Foo extends Document { - constructor() { - super(); + constructor() { + super(); - this.bar = Bar; - this.num = Number; - } + this.bar = Bar; + this.num = Number; + } } -module.exports = Foo; \ No newline at end of file +module.exports = Foo; diff --git a/test/data.js b/test/data.js index c8a0261..e1e8de6 100644 --- a/test/data.js +++ b/test/data.js @@ -1,34 +1,34 @@ -"use strict"; +'use strict'; var Document = require('../index').Document; class Data extends Document { - constructor() { - super(); + constructor() { + super(); - this.schema({ - number: { - type: Number - }, - source: { - type: String, - choices: ['reddit', 'hacker-news', 'wired', 'arstechnica'], - default: 'reddit' - }, - item: { - type: Number, - min: 0, - max: 100 - }, - values: { - type: [Number] - }, - date: { - type: Date, - default: Date.now - } - }); - } + this.schema({ + number: { + type: Number + }, + source: { + type: String, + choices: ['reddit', 'hacker-news', 'wired', 'arstechnica'], + default: 'reddit' + }, + item: { + type: Number, + min: 0, + max: 100 + }, + values: { + type: [Number] + }, + date: { + type: Date, + default: Date.now + } + }); + } } module.exports = Data; \ No newline at end of file diff --git a/test/document.test.js b/test/document.test.js index 34cf69a..2214557 100644 --- a/test/document.test.js +++ b/test/document.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var fs = require('fs'); @@ -16,1664 +16,1666 @@ var expectError = require('./util').expectError; describe('Document', function() { - // TODO: Should probably use mock database client... - var url = 'nedb://memory'; - //var url = 'mongodb://localhost/camo_test'; - var database = null; - - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function() { - return done(); - }); + // TODO: Should probably use mock database client... + var url = 'nedb://memory'; + //var url = 'mongodb://localhost/camo_test'; + var database = null; + + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); - - beforeEach(function(done) { - done(); - }); - - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); - - after(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + }); + + beforeEach(function(done) { + done(); + }); + + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + after(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + describe('instantiation', function() { + it('should allow creation of instance', function(done) { + + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } + } + + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; + + user.save().then(function() { + validateId(user); + }).then(done, done); }); - describe('instantiation', function() { - it('should allow creation of instance', function(done) { + it('should allow schema declaration via method', function(done) { - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - } - } + class User extends Document { + constructor() { + super(); - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; + this.schema({ + firstName: String, + lastName: String + }); + } + } - user.save().then(function() { - validateId(user); - }).then(done, done); - }); + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; - it('should allow schema declaration via method', function(done) { + user.save().then(function() { + validateId(user); + }).then(done, done); + }); - class User extends Document { - constructor() { - super(); + it('should allow creation of instance with data', function(done) { + + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + this.nicknames = [String]; + } + } + + var user = User.create({ + firstName: 'Billy', + lastName: 'Bob', + nicknames: ['Bill', 'William', 'Will'] + }); + + expect(user.firstName).to.be.equal('Billy'); + expect(user.lastName).to.be.equal('Bob'); + expect(user.nicknames).to.have.length(3); + expect(user.nicknames).to.include('Bill'); + expect(user.nicknames).to.include('William'); + expect(user.nicknames).to.include('Will'); + + done(); + }); - this.schema({ - firstName: String, - lastName: String - }); - } - } + it('should allow creation of instance with references', function(done) { + + class Coffee extends Document { + constructor() { + super(); + this.temp = Number; + } + } + + class User extends Document { + constructor() { + super(); + this.drinks = [Coffee]; + } + } + + var coffee = Coffee.create(); + coffee.temp = 105; + + coffee.save().then(function() { + var user = User.create({drinks: [coffee]}); + expect(user.drinks).to.have.length(1); + }).then(done, done); + }); + }); + + describe('class', function() { + it('should allow use of member variables in getters', function(done) { + + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } + + get fullName() { + return this.firstName + ' ' + this.lastName; + } + } + + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; + + user.save().then(function() { + validateId(user); + expect(user.fullName).to.be.equal('Billy Bob'); + }).then(done, done); + }); - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; + it('should allow use of member variables in setters', function(done) { + + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } + + get fullName() { + return this.firstName + ' ' + this.lastName; + } + + set fullName(name) { + var nameArr = name.split(' '); + this.firstName = nameArr[0]; + this.lastName = nameArr[1]; + } + } + + var user = User.create(); + user.fullName = 'Billy Bob'; + + user.save().then(function() { + validateId(user); + expect(user.firstName).to.be.equal('Billy'); + expect(user.lastName).to.be.equal('Bob'); + }).then(done, done); + }); - user.save().then(function() { - validateId(user); - }).then(done, done); - }); + it('should allow use of member variables in methods', function(done) { - it('should allow creation of instance with data', function(done) { + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - this.nicknames = [String]; - } - } + fullName() { + return this.firstName + ' ' + this.lastName; + } + } - var user = User.create({ - firstName: 'Billy', - lastName: 'Bob', - nicknames: ['Bill', 'William', 'Will'] - }); - - expect(user.firstName).to.be.equal('Billy'); - expect(user.lastName).to.be.equal('Bob'); - expect(user.nicknames).to.have.length(3); - expect(user.nicknames).to.include('Bill'); - expect(user.nicknames).to.include('William'); - expect(user.nicknames).to.include('Will'); - - done(); - }); - - it('should allow creation of instance with references', function(done) { - - class Coffee extends Document { - constructor() { - super(); - this.temp = Number; - } - } + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; - class User extends Document { - constructor() { - super(); - this.drinks = [Coffee]; - } - } + user.save().then(function() { + validateId(user); + expect(user.fullName()).to.be.equal('Billy Bob'); + }).then(done, done); + }); - var coffee = Coffee.create(); - coffee.temp = 105; + it('should allow schemas to be extended', function(done) { + + class User extends Document { + constructor(collection) { + super(collection); + this.firstName = String; + this.lastName = String; + } + } + + class ProUser extends User { + constructor() { + super(); + this.paymentMethod = String; + } + } + + var user = ProUser.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; + user.paymentMethod = 'cash'; + + user.save().then(function() { + validateId(user); + expect(user.firstName).to.be.equal('Billy'); + expect(user.lastName).to.be.equal('Bob'); + expect(user.paymentMethod).to.be.equal('cash'); + }).then(done, done); + }); - coffee.save().then(function() { - var user = User.create({ drinks: [coffee] }); - expect(user.drinks).to.have.length(1); - }).then(done, done); - }); + it('should allow schemas to be overridden', function(done) { + + class Vehicle extends Document { + constructor(collection) { + super(collection); + this.numWheels = { + type: Number, + default: 4 + }; + } + } + + class Motorcycle extends Vehicle { + constructor() { + super(); + this.numWheels = { + type: Number, + default: 2 + }; + } + } + + var bike = Motorcycle.create(); + + bike.save().then(function() { + validateId(bike); + expect(bike.numWheels).to.be.equal(2); + }).then(done, done); }); - describe('class', function() { - it('should allow use of member variables in getters', function(done) { + it('should provide default collection name based on class name', function(done) { - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - } + class User extends Document { + constructor() { + super(); + } + } - get fullName() { - return this.firstName + ' ' + this.lastName; - } - } + var user = User.create(); - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; - - user.save().then(function() { - validateId(user); - expect(user.fullName).to.be.equal('Billy Bob'); - }).then(done, done); - }); - - it('should allow use of member variables in setters', function(done) { - - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - } - - get fullName() { - return this.firstName + ' ' + this.lastName; - } - - set fullName(name) { - var nameArr = name.split(' '); - this.firstName = nameArr[0]; - this.lastName = nameArr[1]; - } - } + expect(user.collectionName()).to.be.equal('users'); + expect(User.collectionName()).to.be.equal('users'); - var user = User.create(); - user.fullName = 'Billy Bob'; + done(); + }); - user.save().then(function() { - validateId(user); - expect(user.firstName).to.be.equal('Billy'); - expect(user.lastName).to.be.equal('Bob'); - }).then(done, done); - }); + it('should provide default collection name based on subclass name', function(done) { - it('should allow use of member variables in methods', function(done) { + class User extends Document { + constructor() { + super(); + } + } - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - } + class ProUser extends User { + constructor() { + super(); + } + } - fullName() { - return this.firstName + ' ' + this.lastName; - } - } + var pro = ProUser.create(); - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; + expect(pro.collectionName()).to.be.equal('prousers'); + expect(ProUser.collectionName()).to.be.equal('prousers'); - user.save().then(function() { - validateId(user); - expect(user.fullName()).to.be.equal('Billy Bob'); - }).then(done, done); - }); + done(); + }); - it('should allow schemas to be extended', function(done) { + it('should allow custom collection name', function(done) { - class User extends Document { - constructor(collection) { - super(collection); - this.firstName = String; - this.lastName = String; - } - } + class User extends Document { + constructor() { + super(); + } - class ProUser extends User { - constructor() { - super(); - this.paymentMethod = String; - } - } + static collectionName() { + return 'sheeple'; + } + } - var user = ProUser.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; - user.paymentMethod = 'cash'; - - user.save().then(function() { - validateId(user); - expect(user.firstName).to.be.equal('Billy'); - expect(user.lastName).to.be.equal('Bob'); - expect(user.paymentMethod).to.be.equal('cash'); - }).then(done, done); - }); - - it('should allow schemas to be overridden', function(done) { - - class Vehicle extends Document { - constructor(collection) { - super(collection); - this.numWheels = { - type: Number, - default: 4 - }; - } - } + var user = User.create(); - class Motorcycle extends Vehicle { - constructor() { - super(); - this.numWheels = { - type: Number, - default: 2 - }; - } - } + expect(user.collectionName()).to.be.equal('sheeple'); + expect(User.collectionName()).to.be.equal('sheeple'); - var bike = Motorcycle.create(); + done(); + }); + }); + + describe('types', function() { + it('should allow reference types', function(done) { + + class ReferenceeModel extends Document { + constructor() { + super(); + this.str = String; + } + + static collectionName() { + return 'referencee1'; + } + } + + class ReferencerModel extends Document { + constructor() { + super(); + this.ref = ReferenceeModel; + this.num = {type: Number}; + } + + static collectionName() { + return 'referencer1'; + } + } + + var data = ReferencerModel.create(); + data.ref = ReferenceeModel.create(); + data.ref.str = 'some data'; + data.num = 1; + + data.ref.save().then(function() { + validateId(data.ref); + return data.save(); + }).then(function() { + validateId(data); + return ReferencerModel.findOne({num: 1}); + }).then(function(d) { + validateId(d); + validateId(d.ref); + expect(d.ref).to.be.an.instanceof(ReferenceeModel); + expect(d.ref.str).to.be.equal('some data'); + }).then(done, done); + }); - bike.save().then(function() { - validateId(bike); - expect(bike.numWheels).to.be.equal(2); - }).then(done, done); - }); + it('should allow array of references', function(done) { + + class ReferenceeModel extends Document { + constructor() { + super(); + this.schema({str: {type: String}}); + } + + static collectionName() { + return 'referencee2'; + } + } + + class ReferencerModel extends Document { + constructor() { + super(); + this.refs = [ReferenceeModel]; + this.num = Number; + } + + static collectionName() { + return 'referencer2'; + } + } + + var data = ReferencerModel.create(); + data.refs.push(ReferenceeModel.create()); + data.refs.push(ReferenceeModel.create()); + data.refs[0].str = 'string1'; + data.refs[1].str = 'string2'; + data.num = 1; + + data.refs[0].save().then(function() { + validateId(data.refs[0]); + return data.refs[1].save(); + }).then(function() { + validateId(data.refs[1]); + return data.save(); + }).then(function() { + validateId(data); + return ReferencerModel.findOne({num: 1}); + }).then(function(d) { + validateId(d); + validateId(d.refs[0]); + validateId(d.refs[1]); + expect(d.refs[0]).to.be.an.instanceof(ReferenceeModel); + expect(d.refs[1]).to.be.an.instanceof(ReferenceeModel); + expect(d.refs[0].str).to.be.equal('string1'); + expect(d.refs[1].str).to.be.equal('string2'); + }).then(done, done); + }); - it('should provide default collection name based on class name', function(done) { + it('should allow references to be saved using the object or its id', function(done) { + class ReferenceeModel extends Document { + constructor() { + super(); + this.str = String; + } + + static collectionName() { + return 'referencee3'; + } + } + + class ReferencerModel extends Document { + constructor() { + super(); + this.ref1 = ReferenceeModel; + this.ref2 = ReferenceeModel; + this.num = {type: Number}; + } + + static collectionName() { + return 'referencer3'; + } + } + + var data = ReferencerModel.create(); + data.ref1 = ReferenceeModel.create(); + var ref2 = ReferenceeModel.create(); + data.ref1.str = 'string1'; + ref2.str = 'string2'; + data.num = 1; + + data.ref1.save().then(function() { + validateId(data.ref1); + return data.save(); + }).then(function() { + validateId(data); + return ref2.save(); + }).then(function() { + validateId(ref2); + data.ref2 = ref2._id; + return data.save(); + }).then(function() { + return ReferencerModel.findOne({num: 1}); + }).then(function(d) { + validateId(d.ref1); + validateId(d.ref2); + expect(d.ref1.str).to.be.equal('string1'); + expect(d.ref2.str).to.be.equal('string2'); + }).then(done, done); + }); - class User extends Document { - constructor() { - super(); - } - } + it('should allow array of references to be saved using the object or its id', function(done) { + class ReferenceeModel extends Document { + constructor() { + super(); + this.schema({str: {type: String}}); + } + + static collectionName() { + return 'referencee4'; + } + } + + class ReferencerModel extends Document { + constructor() { + super(); + this.refs = [ReferenceeModel]; + this.num = Number; + } + + static collectionName() { + return 'referencer4'; + } + } + + var data = ReferencerModel.create(); + data.refs.push(ReferenceeModel.create()); + var ref2 = ReferenceeModel.create(); + data.refs[0].str = 'string1'; + ref2.str = 'string2'; + data.num = 1; + + data.refs[0].save().then(function() { + validateId(data.refs[0]); + return data.save(); + }).then(function() { + validateId(data); + return ref2.save(); + }).then(function() { + validateId(ref2); + data.refs.push(ref2._id); + return data.save(); + }).then(function() { + return ReferencerModel.findOne({num: 1}); + }).then(function(d) { + validateId(d.refs[0]); + validateId(d.refs[1]); + expect(d.refs[1].str).to.be.equal('string2'); + }).then(done, done); + }); - var user = User.create(); + it('should allow circular references', function(done) { + + class Employee extends Document { + constructor() { + super(); + this.name = String; + this.boss = Boss; + } + } + + class Boss extends Document { + constructor() { + super(); + this.salary = Number; + this.employees = [Employee]; + } + + static collectionName() { + return 'bosses'; + } + } + + var employee = Employee.create(); + employee.name = 'Scott'; + + var boss = Boss.create(); + boss.salary = 10000000; + + employee.boss = boss; + + boss.save().then(function() { + validateId(boss); + + return employee.save(); + }).then(function() { + validateId(employee); + validateId(employee.boss); + + boss.employees.push(employee); + + return boss.save(); + }).then(function() { + validateId(boss); + validateId(boss.employees[0]); + validateId(boss.employees[0].boss); + + return Boss.findOne({salary: 10000000}); + }).then(function(b) { + // If we had an issue with an infinite loop + // of loading circular dependencies then the + // test probably would have crashed by now, + // so we're good. + + validateId(b); + + // Validate that boss employee ref was loaded + validateId(b.employees[0]); + + // .findOne should have only loaded 1 level + // of references, so the boss's reference + // to the employee is still the ID. + expect(b.employees[0].boss).to.not.be.null; + expect(!isDocument(b.employees[0].boss)).to.be.true; + }).then(done, done); + }); - expect(user.collectionName()).to.be.equal('users'); - expect(User.collectionName()).to.be.equal('users'); + it('should allow string types', function(done) { - done(); - }); + class StringModel extends Document { + constructor() { + super(); + this.schema({str: {type: String}}); + } + } - it('should provide default collection name based on subclass name', function(done) { + var data = StringModel.create(); + data.str = 'hello'; - class User extends Document { - constructor() { - super(); - } - } + data.save().then(function() { + validateId(data); + expect(data.str).to.be.equal('hello'); + }).then(done, done); + }); - class ProUser extends User { - constructor() { - super(); - } - } + it('should allow number types', function(done) { - var pro = ProUser.create(); + class NumberModel extends Document { + constructor() { + super(); + this.schema({num: {type: Number}}); + } - expect(pro.collectionName()).to.be.equal('prousers'); - expect(ProUser.collectionName()).to.be.equal('prousers'); + static collectionName() { + return 'numbers1'; + } + } - done(); - }); + var data = NumberModel.create(); + data.num = 26; - it('should allow custom collection name', function(done) { + data.save().then(function() { + validateId(data); + expect(data.num).to.be.equal(26); + }).then(done, done); + }); - class User extends Document { - constructor() { - super(); - } + it('should allow boolean types', function(done) { - static collectionName() { - return 'sheeple'; - } - } + class BooleanModel extends Document { + constructor() { + super(); + this.schema({bool: {type: Boolean}}); + } + } - var user = User.create(); + var data = BooleanModel.create(); + data.bool = true; - expect(user.collectionName()).to.be.equal('sheeple'); - expect(User.collectionName()).to.be.equal('sheeple'); - - done(); - }); + data.save().then(function() { + validateId(data); + expect(data.bool).to.be.equal(true); + }).then(done, done); }); - describe('types', function() { - it('should allow reference types', function(done) { - - class ReferenceeModel extends Document { - constructor() { - super(); - this.str = String; - } + it('should allow date types', function(done) { - static collectionName() { - return 'referencee1'; - } - } + class DateModel extends Document { + constructor() { + super(); + this.schema({date: {type: Date}}); + } + } - class ReferencerModel extends Document { - constructor() { - super(); - this.ref = ReferenceeModel; - this.num = { type: Number }; - } - - static collectionName() { - return 'referencer1'; - } - } + var data = DateModel.create(); + var date = new Date(); + data.date = date; - var data = ReferencerModel.create(); - data.ref = ReferenceeModel.create(); - data.ref.str = 'some data'; - data.num = 1; - - data.ref.save().then(function() { - validateId(data.ref); - return data.save(); - }).then(function() { - validateId(data); - return ReferencerModel.findOne({ num: 1 }); - }).then(function(d) { - validateId(d); - validateId(d.ref); - expect(d.ref).to.be.an.instanceof(ReferenceeModel); - expect(d.ref.str).to.be.equal('some data'); - }).then(done, done); - }); - - it('should allow array of references', function(done) { - - class ReferenceeModel extends Document { - constructor() { - super(); - this.schema({ str: { type: String } }); - } - - static collectionName() { - return 'referencee2'; - } - } + data.save().then(function() { + validateId(data); + expect(data.date.valueOf()).to.be.equal(date.valueOf()); + }).then(done, done); + }); - class ReferencerModel extends Document { - constructor() { - super(); - this.refs = [ReferenceeModel]; - this.num = Number; - } + it('should allow object types', function(done) { - static collectionName() { - return 'referencer2'; - } - } + class ObjectModel extends Document { + constructor() { + super(); + this.schema({obj: {type: Object}}); + } + } - var data = ReferencerModel.create(); - data.refs.push(ReferenceeModel.create()); - data.refs.push(ReferenceeModel.create()); - data.refs[0].str = 'string1'; - data.refs[1].str = 'string2'; - data.num = 1; - - data.refs[0].save().then(function() { - validateId(data.refs[0]); - return data.refs[1].save(); - }).then(function() { - validateId(data.refs[1]); - return data.save(); - }).then(function() { - validateId(data); - return ReferencerModel.findOne({ num: 1 }); - }).then(function(d) { - validateId(d); - validateId(d.refs[0]); - validateId(d.refs[1]); - expect(d.refs[0]).to.be.an.instanceof(ReferenceeModel); - expect(d.refs[1]).to.be.an.instanceof(ReferenceeModel); - expect(d.refs[0].str).to.be.equal('string1'); - expect(d.refs[1].str).to.be.equal('string2'); - }).then(done, done); - }); - - it('should allow references to be saved using the object or its id', function(done) { - class ReferenceeModel extends Document { - constructor() { - super(); - this.str = String; - } - - static collectionName() { - return 'referencee3'; - } - } + var data = ObjectModel.create(); + data.obj = {hi: 'bye'}; - class ReferencerModel extends Document { - constructor() { - super(); - this.ref1 = ReferenceeModel; - this.ref2 = ReferenceeModel; - this.num = { type: Number }; - } - - static collectionName() { - return 'referencer3'; - } - } + data.save().then(function() { + validateId(data); + expect(data.obj.hi).to.not.be.null; + expect(data.obj.hi).to.be.equal('bye'); + }).then(done, done); + }); - var data = ReferencerModel.create(); - data.ref1 = ReferenceeModel.create(); - var ref2 = ReferenceeModel.create(); - data.ref1.str = 'string1'; - ref2.str = 'string2'; - data.num = 1; - - data.ref1.save().then(function() { - validateId(data.ref1); - return data.save(); - }).then(function() { - validateId(data); - return ref2.save(); - }).then(function() { - validateId(ref2); - data.ref2 = ref2._id; - return data.save(); - }).then(function() { - return ReferencerModel.findOne({num: 1}); - }).then(function(d) { - validateId(d.ref1); - validateId(d.ref2); - expect(d.ref1.str).to.be.equal('string1'); - expect(d.ref2.str).to.be.equal('string2'); - }).then(done, done); - }); - - it('should allow array of references to be saved using the object or its id', function(done) { - class ReferenceeModel extends Document { - constructor() { - super(); - this.schema({ str: { type: String } }); - } - - static collectionName() { - return 'referencee4'; - } - } + it('should allow buffer types', function(done) { - class ReferencerModel extends Document { - constructor() { - super(); - this.refs = [ReferenceeModel]; - this.num = Number; - } + class BufferModel extends Document { + constructor() { + super(); + this.schema({buf: {type: Buffer}}); + } + } - static collectionName() { - return 'referencer4'; - } - } + var data = BufferModel.create(); + data.buf = new Buffer('hello'); - var data = ReferencerModel.create(); - data.refs.push(ReferenceeModel.create()); - var ref2 = ReferenceeModel.create(); - data.refs[0].str = 'string1'; - ref2.str = 'string2'; - data.num = 1; - - data.refs[0].save().then(function() { - validateId(data.refs[0]); - return data.save(); - }).then(function() { - validateId(data); - return ref2.save(); - }).then(function() { - validateId(ref2); - data.refs.push(ref2._id); - return data.save(); - }).then(function() { - return ReferencerModel.findOne({num: 1}); - }).then(function(d) { - validateId(d.refs[0]); - validateId(d.refs[1]); - expect(d.refs[1].str).to.be.equal('string2'); - }).then(done, done); - }); - - it('should allow circular references', function(done) { - - class Employee extends Document { - constructor() { - super(); - this.name = String; - this.boss = Boss; - } - } + data.save().then(function() { + validateId(data); + expect(data.buf.toString('ascii')).to.be.equal('hello'); + }).then(done, done); + }); - class Boss extends Document { - constructor() { - super(); - this.salary = Number; - this.employees = [Employee]; - } + it('should allow array types', function(done) { + + class ArrayModel extends Document { + constructor() { + super(); + this.schema({arr: {type: Array}}); + } + } + + var data = ArrayModel.create(); + data.arr = [1, 'number', true]; + + data.save().then(function() { + validateId(data); + expect(data.arr).to.have.length(3); + expect(data.arr).to.include(1); + expect(data.arr).to.include('number'); + expect(data.arr).to.include(true); + }).then(done, done); + }); - static collectionName() { - return 'bosses'; - } - } + it('should allow typed-array types', function(done) { + + class ArrayModel extends Document { + constructor() { + super(); + this.schema({arr: {type: [String]}}); + } + } + + var data = ArrayModel.create(); + data.arr = ['1', '2', '3']; + + data.save().then(function() { + validateId(data); + expect(data.arr).to.have.length(3); + expect(data.arr).to.include('1'); + expect(data.arr).to.include('2'); + expect(data.arr).to.include('3'); + }).then(done, done); + }); - var employee = Employee.create(); - employee.name = 'Scott'; + it('should reject objects containing values with different types', function(done) { - var boss = Boss.create(); - boss.salary = 10000000; + class NumberModel extends Document { + constructor() { + super(); + this.schema({num: {type: Number}}); + } - employee.boss = boss; + static collectionName() { + return 'numbers2'; + } + } - boss.save().then(function() { - validateId(boss); + var data = NumberModel.create(); + data.num = '1'; - return employee.save(); - }).then(function() { - validateId(employee); - validateId(employee.boss); + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); + }); - boss.employees.push(employee); + it('should reject typed-arrays containing different types', function(done) { - return boss.save(); - }).then(function() { - validateId(boss); - validateId(boss.employees[0]); - validateId(boss.employees[0].boss); + class ArrayModel extends Document { + constructor() { + super(); + this.schema({arr: {type: [String]}}); + } + } - return Boss.findOne({ salary: 10000000 }); - }).then(function(b) { - // If we had an issue with an infinite loop - // of loading circular dependencies then the - // test probably would have crashed by now, - // so we're good. + var data = ArrayModel.create(); + data.arr = [1, 2, 3]; - validateId(b); + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); + }); + }); - // Validate that boss employee ref was loaded - validateId(b.employees[0]); + describe('defaults', function() { + it('should assign default value if unassigned', function(done) { - // .findOne should have only loaded 1 level - // of references, so the boss's reference - // to the employee is still the ID. - expect(b.employees[0].boss).to.not.be.null; - expect(!isDocument(b.employees[0].boss)).to.be.true; - }).then(done, done); - }); + var data = Data.create(); - it('should allow string types', function(done) { + data.save().then(function() { + validateId(data); + expect(data.source).to.be.equal('reddit'); + }).then(done, done); + }); - class StringModel extends Document { - constructor() { - super(); - this.schema({ str: { type: String } }); - } - } + it('should assign default value via function if unassigned', function(done) { - var data = StringModel.create(); - data.str = 'hello'; + var data = Data.create(); - data.save().then(function() { - validateId(data); - expect(data.str).to.be.equal('hello'); - }).then(done, done); - }); + data.save().then(function() { + validateId(data); + expect(data.date).to.be.lessThan(Date.now()); + }).then(done, done); + }); - it('should allow number types', function(done) { + it('should be undefined if unassigned and no default is given', function(done) { + + class Person extends Document { + constructor() { + super(); + this.name = String; + this.age = Number; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Scott' + }); + + person.save().then(function() { + validateId(person); + return Person.findOne({name: 'Scott'}); + }).then(function(p) { + validateId(p); + expect(p.name).to.be.equal('Scott'); + expect(p.age).to.be.undefined; + }).then(done, done); + }); + }); - class NumberModel extends Document { - constructor() { - super(); - this.schema({ num: { type: Number } }); - } + describe('choices', function() { + it('should accept value specified in choices', function(done) { - static collectionName() { - return 'numbers1'; - } - } + var data = Data.create(); + data.source = 'wired'; - var data = NumberModel.create(); - data.num = 26; + data.save().then(function() { + validateId(data); + expect(data.source).to.be.equal('wired'); + }).then(done, done); + }); - data.save().then(function() { - validateId(data); - expect(data.num).to.be.equal(26); - }).then(done, done); - }); + it('should reject values not specified in choices', function(done) { - it('should allow boolean types', function(done) { + var data = Data.create(); + data.source = 'google'; - class BooleanModel extends Document { - constructor() { - super(); - this.schema({ bool: { type: Boolean } }); - } - } + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); + }); + }); - var data = BooleanModel.create(); - data.bool = true; + describe('min', function() { + it('should accept value > min', function(done) { - data.save().then(function() { - validateId(data); - expect(data.bool).to.be.equal(true); - }).then(done, done); - }); + var data = Data.create(); + data.item = 1; - it('should allow date types', function(done) { + data.save().then(function() { + validateId(data); + expect(data.item).to.be.equal(1); + }).then(done, done); + }); - class DateModel extends Document { - constructor() { - super(); - this.schema({ date: { type: Date } }); - } - } + it('should accept value == min', function(done) { - var data = DateModel.create(); - var date = new Date(); - data.date = date; + var data = Data.create(); + data.item = 0; - data.save().then(function() { - validateId(data); - expect(data.date.valueOf()).to.be.equal(date.valueOf()); - }).then(done, done); - }); + data.save().then(function() { + validateId(data); + expect(data.item).to.be.equal(0); + }).then(done, done); + }); - it('should allow object types', function(done) { + it('should reject value < min', function(done) { - class ObjectModel extends Document { - constructor() { - super(); - this.schema({ obj: { type: Object } }); - } - } + var data = Data.create(); + data.item = -1; - var data = ObjectModel.create(); - data.obj = { hi: 'bye'}; + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); + }); + }); - data.save().then(function() { - validateId(data); - expect(data.obj.hi).to.not.be.null; - expect(data.obj.hi).to.be.equal('bye'); - }).then(done, done); - }); + describe('max', function() { + it('should accept value < max', function(done) { - it('should allow buffer types', function(done) { + var data = Data.create(); + data.item = 99; - class BufferModel extends Document { - constructor() { - super(); - this.schema({ buf: { type: Buffer } }); - } - } + data.save().then(function() { + validateId(data); + expect(data.item).to.be.equal(99); + }).then(done, done); + }); - var data = BufferModel.create(); - data.buf = new Buffer('hello'); + it('should accept value == max', function(done) { - data.save().then(function() { - validateId(data); - expect(data.buf.toString('ascii')).to.be.equal('hello'); - }).then(done, done); - }); + var data = Data.create(); + data.item = 100; - it('should allow array types', function(done) { + data.save().then(function() { + validateId(data); + expect(data.item).to.be.equal(100); + }).then(done, done); + }); - class ArrayModel extends Document { - constructor() { - super(); - this.schema({ arr: { type: Array } }); - } - } + it('should reject value > max', function(done) { - var data = ArrayModel.create(); - data.arr = [1, 'number', true]; - - data.save().then(function() { - validateId(data); - expect(data.arr).to.have.length(3); - expect(data.arr).to.include(1); - expect(data.arr).to.include('number'); - expect(data.arr).to.include(true); - }).then(done, done); - }); - - it('should allow typed-array types', function(done) { - - class ArrayModel extends Document { - constructor() { - super(); - this.schema({ arr: { type: [String] } }); - } - } + var data = Data.create(); + data.item = 101; - var data = ArrayModel.create(); - data.arr = ['1', '2', '3']; - - data.save().then(function() { - validateId(data); - expect(data.arr).to.have.length(3); - expect(data.arr).to.include('1'); - expect(data.arr).to.include('2'); - expect(data.arr).to.include('3'); - }).then(done, done); - }); - - it('should reject objects containing values with different types', function(done) { - - class NumberModel extends Document { - constructor() { - super(); - this.schema({ num: { type: Number } }); - } - - static collectionName() { - return 'numbers2'; - } - } + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); + }); + }); + + describe('match', function() { + it('should accept value matching regex', function(done) { + + class Product extends Document { + constructor() { + super(); + this.name = String; + this.cost = { + type: String, + match: /^\$?[\d,]+(\.\d*)?$/ + }; + } + } + + var product = Product.create(); + product.name = 'Dark Roast Coffee'; + product.cost = '$1.39'; + + product.save().then(function() { + validateId(product); + expect(product.name).to.be.equal('Dark Roast Coffee'); + expect(product.cost).to.be.equal('$1.39'); + }).then(done, done); + }); - var data = NumberModel.create(); - data.num = '1'; + it('should reject value not matching regex', function(done) { + + class Product extends Document { + constructor() { + super(); + this.name = String; + this.cost = { + type: String, + match: /^\$?[\d,]+(\.\d*)?$/ + }; + } + } + + var product = Product.create(); + product.name = 'Light Roast Coffee'; + product.cost = '$1..39'; + + product.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); + }); - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); + describe('validate', function() { + it('should accept value that passes custom validator', function(done) { - it('should reject typed-arrays containing different types', function(done) { + class Person extends Document { + constructor() { + super(); - class ArrayModel extends Document { - constructor() { - super(); - this.schema({ arr: { type: [String] } }); - } + this.name = { + type: String, + validate: function(value) { + return value.length > 4; } - - var data = ArrayModel.create(); - data.arr = [1, 2, 3]; - - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Scott' + }); + + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal('Scott'); + }).then(done, done); }); - describe('defaults', function() { - it('should assign default value if unassigned', function(done) { - - var data = Data.create(); - - data.save().then(function() { - validateId(data); - expect(data.source).to.be.equal('reddit'); - }).then(done, done); - }); - - it('should assign default value via function if unassigned', function(done) { + it('should reject value that fails custom validator', function(done) { - var data = Data.create(); + class Person extends Document { + constructor() { + super(); - data.save().then(function() { - validateId(data); - expect(data.date).to.be.lessThan(Date.now()); - }).then(done, done); - }); - - it('should be undefined if unassigned and no default is given', function(done) { - - class Person extends Document { - constructor() { - super(); - this.name = String; - this.age = Number; - } - - static collectionName() { - return 'people'; - } + this.name = { + type: String, + validate: function(value) { + return value.length > 4; } - - var person = Person.create({ - name: 'Scott' - }); - - person.save().then(function() { - validateId(person); - return Person.findOne({name: 'Scott'}); - }).then(function(p) { - validateId(p); - expect(p.name).to.be.equal('Scott'); - expect(p.age).to.be.undefined; - }).then(done, done); - }); + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Matt' + }); + + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); }); + }); - describe('choices', function() { - it('should accept value specified in choices', function(done) { + describe('canonicalize', function() { + it('should ensure timestamp dates are converted to Date objects', function(done) { - var data = Data.create(); - data.source = 'wired'; + class Person extends Document { + constructor() { + super(); - data.save().then(function() { - validateId(data); - expect(data.source).to.be.equal('wired'); - }).then(done, done); - }); + this.birthday = Date; + } - it('should reject values not specified in choices', function(done) { + static collectionName() { + return 'people'; + } + } - var data = Data.create(); - data.source = 'google'; + var now = new Date(); - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); - }); + var person = Person.create({ + birthday: now + }); - describe('min', function() { - it('should accept value > min', function(done) { - - var data = Data.create(); - data.item = 1; + person.save().then(function() { + validateId(person); + expect(person.birthday.valueOf()).to.be.equal(now.valueOf()); + }).then(done, done); + }); - data.save().then(function() { - validateId(data); - expect(data.item).to.be.equal(1); - }).then(done, done); - }); + it('should ensure date strings are converted to Date objects', function(done) { + + class Person extends Document { + constructor() { + super(); + this.birthday = Date; + this.graduationDate = Date; + this.weddingDate = Date; + } + + static collectionName() { + return 'people'; + } + } + + var birthday = new Date(Date.UTC(2016, 1, 17, 5, 6, 8, 0)); + var graduationDate = new Date(2016, 1, 17, 0, 0, 0, 0); + var weddingDate = new Date(2016, 1, 17, 0, 0, 0, 0); + + var person = Person.create({ + birthday: '2016-02-17T05:06:08+00:00', + graduationDate: 'February 17, 2016', + weddingDate: '2016/02/17' + }); + + person.save().then(function() { + validateId(person); + expect(person.birthday.valueOf()).to.be.equal(birthday.valueOf()); + expect(person.graduationDate.valueOf()).to.be.equal(graduationDate.valueOf()); + expect(person.weddingDate.valueOf()).to.be.equal(weddingDate.valueOf()); + }).then(done, done); + }); + }); + + describe('required', function() { + it('should accept empty value that is not reqired', function(done) { + + class Person extends Document { + constructor() { + super(); + + this.name = { + type: String, + required: false + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: '' + }); + + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal(''); + }).then(done, done); + }); - it('should accept value == min', function(done) { + it('should accept value that is not undefined', function(done) { - var data = Data.create(); - data.item = 0; + class Person extends Document { + constructor() { + super(); - data.save().then(function() { - validateId(data); - expect(data.item).to.be.equal(0); - }).then(done, done); - }); + this.name = { + type: String, + required: true + }; + } - it('should reject value < min', function(done) { + static collectionName() { + return 'people'; + } + } - var data = Data.create(); - data.item = -1; + var person = Person.create({ + name: 'Scott' + }); - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal('Scott'); + }).then(done, done); }); - describe('max', function() { - it('should accept value < max', function(done) { + it('should accept an empty value if default is specified', function(done) { - var data = Data.create(); - data.item = 99; + class Person extends Document { + constructor() { + super(); - data.save().then(function() { - validateId(data); - expect(data.item).to.be.equal(99); - }).then(done, done); - }); + this.name = { + type: String, + required: true, + default: 'Scott' + }; + } - it('should accept value == max', function(done) { + static collectionName() { + return 'people'; + } + } - var data = Data.create(); - data.item = 100; + var person = Person.create(); - data.save().then(function() { - validateId(data); - expect(data.item).to.be.equal(100); - }).then(done, done); - }); - - it('should reject value > max', function(done) { - - var data = Data.create(); - data.item = 101; - - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal('Scott'); + }).then(done, done); }); - describe('match', function() { - it('should accept value matching regex', function(done) { - - class Product extends Document { - constructor() { - super(); - this.name = String; - this.cost = { - type: String, - match: /^\$?[\d,]+(\.\d*)?$/ - }; - } - } - - var product = Product.create(); - product.name = 'Dark Roast Coffee'; - product.cost = '$1.39'; - - product.save().then(function() { - validateId(product); - expect(product.name).to.be.equal('Dark Roast Coffee'); - expect(product.cost).to.be.equal('$1.39'); - }).then(done, done); - }); - - it('should reject value not matching regex', function(done) { - - class Product extends Document { - constructor() { - super(); - this.name = String; - this.cost = { - type: String, - match: /^\$?[\d,]+(\.\d*)?$/ - }; - } - } - - var product = Product.create(); - product.name = 'Light Roast Coffee'; - product.cost = '$1..39'; - - product.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + it('should accept boolean value', function(done) { + + class Person extends Document { + constructor() { + super(); + + this.isSingle = { + type: Boolean, + required: true + }; + this.isMerried = { + type: Boolean, + required: true + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + isMerried: true, + isSingle: false + }); + + person.save().then(function() { + validateId(person); + expect(person.isMerried).to.be.true; + expect(person.isSingle).to.be.false; + }).then(done, done); }); - describe('validate', function() { - it('should accept value that passes custom validator', function(done) { + it('should accept date value', function(done) { - class Person extends Document { - constructor() { - super(); + class Person extends Document { + constructor() { + super(); - this.name = { - type: String, - validate: function(value) { - return value.length > 4; - } - }; - } + this.birthDate = { + type: Date, + required: true + }; + } - static collectionName() { - return 'people'; - } - } + static collectionName() { + return 'people'; + } + } - var person = Person.create({ - name: 'Scott' - }); - - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal('Scott'); - }).then(done, done); - }); - - it('should reject value that fails custom validator', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.name = { - type: String, - validate: function(value) { - return value.length > 4; - } - }; - } - - static collectionName() { - return 'people'; - } - } + var myBirthDate = new Date(); - var person = Person.create({ - name: 'Matt' - }); + var person = Person.create({ + birthDate: myBirthDate + }); - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + person.save().then(function(savedPerson) { + validateId(person); + expect(savedPerson.birthDate.valueOf()).to.equal(myBirthDate.valueOf()); + }).then(done, done); }); - describe('canonicalize', function() { - it('should ensure timestamp dates are converted to Date objects', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.birthday = Date; - } - - static collectionName() { - return 'people'; - } - } - - var now = new Date(); + it('should accept any number value', function(done) { + + class Person extends Document { + constructor() { + super(); + + this.age = { + type: Number, + required: true + }; + this.level = { + type: Number, + required: true + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + age: 21, + level: 0 + }); + + person.save().then(function(savedPerson) { + validateId(person); + expect(savedPerson.age).to.equal(21); + expect(savedPerson.level).to.equal(0); + }).then(done, done); + }); - var person = Person.create({ - birthday: now - }); + it('should reject value that is undefined', function(done) { - person.save().then(function() { - validateId(person); - expect(person.birthday.valueOf()).to.be.equal(now.valueOf()); - }).then(done, done); - }); + class Person extends Document { + constructor() { + super(); - it('should ensure date strings are converted to Date objects', function(done) { + this.name = { + type: String, + required: true + }; + } - class Person extends Document { - constructor() { - super(); - this.birthday = Date; - this.graduationDate = Date; - this.weddingDate = Date; - } + static collectionName() { + return 'people'; + } + } - static collectionName() { - return 'people'; - } - } + var person = Person.create(); - var birthday = new Date(Date.UTC(2016, 1, 17, 5, 6, 8, 0)); - var graduationDate = new Date(2016, 1, 17, 0, 0, 0, 0); - var weddingDate = new Date(2016, 1, 17, 0, 0, 0, 0); - - var person = Person.create({ - birthday: '2016-02-17T05:06:08+00:00', - graduationDate: 'February 17, 2016', - weddingDate: '2016/02/17' - }); - - person.save().then(function() { - validateId(person); - expect(person.birthday.valueOf()).to.be.equal(birthday.valueOf()); - expect(person.graduationDate.valueOf()).to.be.equal(graduationDate.valueOf()); - expect(person.weddingDate.valueOf()).to.be.equal(weddingDate.valueOf()); - }).then(done, done); - }); + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); }); - describe('required', function() { - it('should accept empty value that is not reqired', function(done) { + it('should reject value if specified default empty value', function(done) { - class Person extends Document { - constructor() { - super(); + class Person extends Document { + constructor() { + super(); - this.name = { - type: String, - required: false - }; - } - - static collectionName() { - return 'people'; - } - } + this.name = { + type: String, + required: true, + default: '' + }; + } - var person = Person.create({ - name: '' - }); + static collectionName() { + return 'people'; + } + } - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal(''); - }).then(done, done); - }); + var person = Person.create(); - it('should accept value that is not undefined', function(done) { - - class Person extends Document { - constructor() { - super(); + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); - this.name = { - type: String, - required: true - }; - } + it('should reject value that is null', function(done) { - static collectionName() { - return 'people'; - } - } + class Person extends Document { + constructor() { + super(); - var person = Person.create({ - name: 'Scott' - }); + this.name = { + type: Object, + required: true + }; + } - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal('Scott'); - }).then(done, done); - }); + static collectionName() { + return 'people'; + } + } - it('should accept an empty value if default is specified', function(done) { + var person = Person.create({ + name: null + }); - class Person extends Document { - constructor() { - super(); - - this.name = { - type: String, - required: true, - default: 'Scott' - }; - } + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); - static collectionName() { - return 'people'; - } - } + it('should reject value that is an empty array', function(done) { - var person = Person.create(); - - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal('Scott'); - }).then(done, done); - }); - - it('should accept boolean value', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.isSingle = { - type: Boolean, - required: true - }; - this.isMerried = { - type: Boolean, - required: true - }; - } - - static collectionName() { - return 'people'; - } - } + class Person extends Document { + constructor() { + super(); - var person = Person.create({ - isMerried: true, - isSingle: false - }); - - person.save().then(function() { - validateId(person); - expect(person.isMerried).to.be.true; - expect(person.isSingle).to.be.false; - }).then(done, done); - }); - - it('should accept date value', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.birthDate = { - type: Date, - required: true - }; - } - - static collectionName() { - return 'people'; - } - } + this.names = { + type: Array, + required: true + }; + } - var myBirthDate = new Date(); - - var person = Person.create({ - birthDate: myBirthDate - }); - - person.save().then(function(savedPerson) { - validateId(person); - expect(savedPerson.birthDate.valueOf()).to.equal(myBirthDate.valueOf()); - }).then(done, done); - }); - - it('should accept any number value', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.age = { - type: Number, - required: true - }; - this.level = { - type: Number, - required: true - }; - } - - static collectionName() { - return 'people'; - } - } + static collectionName() { + return 'people'; + } + } - var person = Person.create({ - age: 21, - level: 0 - }); - - person.save().then(function(savedPerson) { - validateId(person); - expect(savedPerson.age).to.equal(21); - expect(savedPerson.level).to.equal(0); - }).then(done, done); - }); - - it('should reject value that is undefined', function(done) { - - class Person extends Document { - constructor() { - super(); - - this.name = { - type: String, - required: true - }; - } - - static collectionName() { - return 'people'; - } - } + var person = Person.create({ + names: [] + }); - var person = Person.create(); - - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); - it('should reject value if specified default empty value', function(done) { + it('should reject value that is an empty string', function(done) { - class Person extends Document { - constructor() { - super(); + class Person extends Document { + constructor() { + super(); - this.name = { - type: String, - required: true, - default: '' - }; - } + this.name = { + type: String, + required: true + }; + } - static collectionName() { - return 'people'; - } - } + static collectionName() { + return 'people'; + } + } - var person = Person.create(); + var person = Person.create({ + name: '' + }); - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); - it('should reject value that is null', function(done) { + it('should reject value that is an empty object', function(done) { - class Person extends Document { - constructor() { - super(); + class Person extends Document { + constructor() { + super(); - this.name = { - type: Object, - required: true - }; - } + this.names = { + type: Object, + required: true + }; + } - static collectionName() { - return 'people'; - } - } + static collectionName() { + return 'people'; + } + } - var person = Person.create({ - name: null - }); + var person = Person.create({ + names: {} + }); - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + person.save().then(function() { + fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expectError(error); + }).then(done, done); + }); + }); - it('should reject value that is an empty array', function(done) { + describe('hooks', function() { + it('should call all pre and post functions', function(done) { - class Person extends Document { - constructor() { - super(); + var preValidateCalled = false; + var preSaveCalled = false; + var preDeleteCalled = false; - this.names = { - type: Array, - required: true - }; - } + var postValidateCalled = false; + var postSaveCalled = false; + var postDeleteCalled = false; - static collectionName() { - return 'people'; - } - } + class Person extends Document { + constructor() { + super(); + } - var person = Person.create({ - names: [] - }); + static collectionName() { + return 'people'; + } - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + preValidate() { + preValidateCalled = true; + } - it('should reject value that is an empty string', function(done) { + postValidate() { + postValidateCalled = true; + } - class Person extends Document { - constructor() { - super(); + preSave() { + preSaveCalled = true; + } - this.name = { - type: String, - required: true - }; - } + postSave() { + postSaveCalled = true; + } - static collectionName() { - return 'people'; - } - } + preDelete() { + preDeleteCalled = true; + } - var person = Person.create({ - name: '' - }); + postDelete() { + postDeleteCalled = true; + } + } - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + var person = Person.create(); - it('should reject value that is an empty object', function(done) { + person.save().then(function() { + validateId(person); - class Person extends Document { - constructor() { - super(); + // Pre/post save and validate should be called + expect(preValidateCalled).to.be.equal(true); + expect(preSaveCalled).to.be.equal(true); + expect(postValidateCalled).to.be.equal(true); + expect(postSaveCalled).to.be.equal(true); - this.names = { - type: Object, - required: true - }; - } + // Pre/post delete should not have been called yet + expect(preDeleteCalled).to.be.equal(false); + expect(postDeleteCalled).to.be.equal(false); - static collectionName() { - return 'people'; - } - } + return person.delete(); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(1); - var person = Person.create({ - names: {} - }); - - person.save().then(function() { - fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expectError(error); - }).then(done, done); - }); + expect(preDeleteCalled).to.be.equal(true); + expect(postDeleteCalled).to.be.equal(true); + }).then(done, done); + }); + }); + + describe('serialize', function() { + it('should serialize data to JSON', function(done) { + class Person extends Document { + constructor() { + super(); + + this.name = String; + this.age = Number; + this.isAlive = Boolean; + this.children = [String]; + this.spouse = { + type: String, + default: null + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Scott', + age: 28, + isAlive: true, + children: ['Billy', 'Timmy'], + spouse: null + }); + + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal('Scott'); + expect(person.age).to.be.equal(28); + expect(person.isAlive).to.be.equal(true); + expect(person.children).to.have.length(2); + expect(person.spouse).to.be.null; + + var json = person.toJSON(); + + expect(json.name).to.be.equal('Scott'); + expect(json.age).to.be.equal(28); + expect(json.isAlive).to.be.equal(true); + expect(json.children).to.have.length(2); + expect(json.spouse).to.be.null; + expect(json._id).to.be.equal(person._id.toString()); + }).then(done, done); }); - describe('hooks', function() { - it('should call all pre and post functions', function(done) { - - var preValidateCalled = false; - var preSaveCalled = false; - var preDeleteCalled = false; - - var postValidateCalled = false; - var postSaveCalled = false; - var postDeleteCalled = false; - - class Person extends Document { - constructor() { - super(); - } - - static collectionName() { - return 'people'; - } - - preValidate() { - preValidateCalled = true; - } - - postValidate() { - postValidateCalled = true; - } - - preSave() { - preSaveCalled = true; - } - - postSave() { - postSaveCalled = true; - } - - preDelete() { - preDeleteCalled = true; - } - - postDelete() { - postDeleteCalled = true; - } - } - - var person = Person.create(); - - person.save().then(function() { - validateId(person); - - // Pre/post save and validate should be called - expect(preValidateCalled).to.be.equal(true); - expect(preSaveCalled).to.be.equal(true); - expect(postValidateCalled).to.be.equal(true); - expect(postSaveCalled).to.be.equal(true); - - // Pre/post delete should not have been called yet - expect(preDeleteCalled).to.be.equal(false); - expect(postDeleteCalled).to.be.equal(false); - - return person.delete(); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(1); - - expect(preDeleteCalled).to.be.equal(true); - expect(postDeleteCalled).to.be.equal(true); - }).then(done, done); - }); + it('should serialize data to JSON', function(done) { + class Person extends Document { + constructor() { + super(); + + this.name = String; + this.children = [Person]; + this.spouse = { + type: Person, + default: null + }; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Scott' + }); + + var spouse = Person.create({ + name: 'Jane' + }); + + var kid1 = Person.create({ + name: 'Billy' + }); + + var kid2 = Person.create({ + name: 'Timmy' + }); + + spouse.save().then(function() { + return kid1.save(); + }).then(function() { + return kid2.save(); + }).then(function() { + person.spouse = spouse; + person.children.push(kid1); + person.children.push(kid2); + + return person.save(); + }).then(function() { + validateId(person); + validateId(spouse); + validateId(kid1); + validateId(kid2); + + expect(person.name).to.be.equal('Scott'); + expect(person.children).to.have.length(2); + expect(person.spouse.name).to.be.equal('Jane'); + expect(person.children[0].name).to.be.equal('Billy'); + expect(person.children[1].name).to.be.equal('Timmy'); + expect(person.spouse).to.be.an.instanceof(Person); + expect(person.children[0]).to.be.an.instanceof(Person); + expect(person.children[1]).to.be.an.instanceof(Person); + + var json = person.toJSON(); + + expect(json.name).to.be.equal('Scott'); + expect(json.children).to.have.length(2); + expect(json.spouse.name).to.be.equal('Jane'); + expect(json.children[0].name).to.be.equal('Billy'); + expect(json.children[1].name).to.be.equal('Timmy'); + expect(json.spouse).to.not.be.an.instanceof(Person); + expect(json.children[0]).to.not.be.an.instanceof(Person); + expect(json.children[1]).to.not.be.an.instanceof(Person); + }).then(done, done); }); - describe('serialize', function() { - it('should serialize data to JSON', function(done) { - class Person extends Document { - constructor() { - super(); - - this.name = String; - this.age = Number; - this.isAlive = Boolean; - this.children = [String]; - this.spouse = { - type: String, - default: null - }; - } - - static collectionName() { - return 'people'; - } - } + it('should serialize data to JSON and ignore methods', function(done) { + class Person extends Document { + constructor() { + super(); - var person = Person.create({ - name: 'Scott', - age: 28, - isAlive: true, - children: ['Billy', 'Timmy'], - spouse: null - }); - - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal('Scott'); - expect(person.age).to.be.equal(28); - expect(person.isAlive).to.be.equal(true); - expect(person.children).to.have.length(2); - expect(person.spouse).to.be.null; - - var json = person.toJSON(); - - expect(json.name).to.be.equal('Scott'); - expect(json.age).to.be.equal(28); - expect(json.isAlive).to.be.equal(true); - expect(json.children).to.have.length(2); - expect(json.spouse).to.be.null; - expect(json._id).to.be.equal(person._id.toString()); - }).then(done, done); - }); - - it('should serialize data to JSON', function(done) { - class Person extends Document { - constructor() { - super(); - - this.name = String; - this.children = [Person]; - this.spouse = { - type: Person, - default: null - }; - } - - static collectionName() { - return 'people'; - } - } + this.name = String; + } - var person = Person.create({ - name: 'Scott' - }); - - var spouse = Person.create({ - name: 'Jane' - }); - - var kid1 = Person.create({ - name: 'Billy' - }); - - var kid2 = Person.create({ - name: 'Timmy' - }); - - spouse.save().then(function() { - return kid1.save(); - }).then(function() { - return kid2.save(); - }).then(function() { - person.spouse = spouse; - person.children.push(kid1); - person.children.push(kid2); - - return person.save(); - }).then(function() { - validateId(person); - validateId(spouse); - validateId(kid1); - validateId(kid2); - - expect(person.name).to.be.equal('Scott'); - expect(person.children).to.have.length(2); - expect(person.spouse.name).to.be.equal('Jane'); - expect(person.children[0].name).to.be.equal('Billy'); - expect(person.children[1].name).to.be.equal('Timmy'); - expect(person.spouse).to.be.an.instanceof(Person); - expect(person.children[0]).to.be.an.instanceof(Person); - expect(person.children[1]).to.be.an.instanceof(Person); - - var json = person.toJSON(); - - expect(json.name).to.be.equal('Scott'); - expect(json.children).to.have.length(2); - expect(json.spouse.name).to.be.equal('Jane'); - expect(json.children[0].name).to.be.equal('Billy'); - expect(json.children[1].name).to.be.equal('Timmy'); - expect(json.spouse).to.not.be.an.instanceof(Person); - expect(json.children[0]).to.not.be.an.instanceof(Person); - expect(json.children[1]).to.not.be.an.instanceof(Person); - }).then(done, done); - }); - - it('should serialize data to JSON and ignore methods', function(done) { - class Person extends Document { - constructor() { - super(); - - this.name = String; - } - - static collectionName() { - return 'people'; - } - - getFoo() { - return 'foo'; - } - } + static collectionName() { + return 'people'; + } + + getFoo() { + return 'foo'; + } + } - var person = Person.create({ - name: 'Scott' - }); + var person = Person.create({ + name: 'Scott' + }); - var json = person.toJSON(); - expect(json).to.have.keys(['_id', 'name']); - done(); - }); + var json = person.toJSON(); + expect(json).to.have.keys(['_id', 'name']); + done(); }); + }); }); \ No newline at end of file diff --git a/test/embedded.test.js b/test/embedded.test.js index 22d9190..b9cc9ec 100644 --- a/test/embedded.test.js +++ b/test/embedded.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var fs = require('fs'); @@ -15,702 +15,704 @@ var validateId = require('./util').validateId; describe('Embedded', function() { - // TODO: Should probably use mock database client... - var url = 'nedb://memory'; - //var url = 'mongodb://localhost/camo_test'; - var database = null; - - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function() { - return done(); - }); + // TODO: Should probably use mock database client... + var url = 'nedb://memory'; + //var url = 'mongodb://localhost/camo_test'; + var database = null; + + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); - - beforeEach(function(done) { - done(); + }); + + beforeEach(function(done) { + done(); + }); + + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + after(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + describe('general', function() { + it('should not have an _id', function(done) { + + class EmbeddedModel extends EmbeddedDocument { + constructor() { + super(); + this.str = String; + } + } + + class DocumentModel extends Document { + constructor() { + super(); + this.mod = EmbeddedModel; + this.num = {type: Number}; + } + } + + var data = DocumentModel.create(); + data.mod = EmbeddedModel.create(); + data.mod.str = 'some data'; + data.num = 1; + + data.save().then(function() { + expect(data.mod._id).to.be.undefined; + return DocumentModel.findOne({num: 1}); + }).then(function(d) { + expect(d.mod._id).to.be.undefined; + }).then(done, done); + }); + }); + + describe('types', function() { + it('should allow embedded types', function(done) { + + class EmbeddedModel extends EmbeddedDocument { + constructor() { + super(); + this.str = String; + } + } + + class DocumentModel extends Document { + constructor() { + super(); + this.mod = EmbeddedModel; + this.num = {type: Number}; + } + } + + var data = DocumentModel.create(); + data.mod = EmbeddedModel.create(); + data.mod.str = 'some data'; + data.num = 1; + + data.save().then(function() { + validateId(data); + return DocumentModel.findOne({num: 1}); + }).then(function(d) { + validateId(d); + expect(d.num).to.be.equal(1); + expect(d.mod).to.be.a('object'); + expect(d.mod).to.be.an.instanceof(EmbeddedModel); + expect(d.mod.str).to.be.equal('some data'); + }).then(done, done); }); - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + it('should allow array of embedded types', function(done) { + + class Limb extends EmbeddedDocument { + constructor() { + super(); + this.type = String; + } + } + + class Person extends Document { + constructor() { + super(); + this.limbs = [Limb]; + this.name = String; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create(); + person.name = 'Scott'; + person.limbs.push(Limb.create()); + person.limbs[0].type = 'left arm'; + person.limbs.push(Limb.create()); + person.limbs[1].type = 'right arm'; + person.limbs.push(Limb.create()); + person.limbs[2].type = 'left leg'; + person.limbs.push(Limb.create()); + person.limbs[3].type = 'right leg'; + + person.save().then(function() { + validateId(person); + expect(person.limbs).to.have.length(4); + return Person.findOne({name: 'Scott'}); + }).then(function(p) { + validateId(p); + expect(p.name).to.be.equal('Scott'); + expect(p.limbs).to.be.a('array'); + expect(p.limbs).to.have.length(4); + expect(p.limbs[0].type).to.be.equal('left arm'); + expect(p.limbs[1].type).to.be.equal('right arm'); + expect(p.limbs[2].type).to.be.equal('left leg'); + expect(p.limbs[3].type).to.be.equal('right leg'); + }).then(done, done); }); - after(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + it('should save nested array of embeddeds', function(done) { + class Point extends EmbeddedDocument { + constructor() { + super(); + this.x = Number; + this.y = Number; + } + } + + class Polygon extends EmbeddedDocument { + constructor() { + super(); + this.points = [Point]; + } + } + + class WorldMap extends Document { + constructor() { + super(); + this.polygons = [Polygon]; + } + } + + var map = WorldMap.create(); + var polygon1 = Polygon.create(); + var polygon2 = Polygon.create(); + var point1 = Point.create({x: 123.45, y: 678.90}); + var point2 = Point.create({x: 543.21, y: 987.60}); + + map.polygons.push(polygon1); + map.polygons.push(polygon2); + polygon2.points.push(point1); + polygon2.points.push(point2); + + map.save().then(function() { + return WorldMap.findOne(); + }).then(function(m) { + expect(m.polygons).to.have.length(2); + expect(m.polygons[0]).to.be.instanceof(Polygon); + expect(m.polygons[1]).to.be.instanceof(Polygon); + expect(m.polygons[1].points).to.have.length(2); + expect(m.polygons[1].points[0]).to.be.instanceof(Point); + expect(m.polygons[1].points[1]).to.be.instanceof(Point); + }).then(done, done); }); - describe('general', function() { - it('should not have an _id', function(done) { - - class EmbeddedModel extends EmbeddedDocument { - constructor() { - super(); - this.str = String; - } - } - - class DocumentModel extends Document { - constructor() { - super(); - this.mod = EmbeddedModel; - this.num = { type: Number }; - } - } - - var data = DocumentModel.create(); - data.mod = EmbeddedModel.create(); - data.mod.str = 'some data'; - data.num = 1; - - data.save().then(function() { - expect(data.mod._id).to.be.undefined; - return DocumentModel.findOne({ num: 1 }); - }).then(function(d) { - expect(d.mod._id).to.be.undefined; - }).then(done, done); - }); + it('should allow nested initialization of embedded types', function(done) { + + class Discount extends EmbeddedDocument { + constructor() { + super(); + this.authorized = Boolean; + this.amount = Number; + } + } + + class Product extends Document { + constructor() { + super(); + this.name = String; + this.discount = Discount; + } + } + + var product = Product.create({ + name: 'bike', + discount: { + authorized: true, + amount: 9.99 + } + }); + + product.save().then(function() { + validateId(product); + expect(product.name).to.be.equal('bike'); + expect(product.discount).to.be.a('object'); + expect(product.discount instanceof Discount).to.be.true; + expect(product.discount.authorized).to.be.equal(true); + expect(product.discount.amount).to.be.equal(9.99); + }).then(done, done); }); - describe('types', function() { - it('should allow embedded types', function(done) { - - class EmbeddedModel extends EmbeddedDocument { - constructor() { - super(); - this.str = String; - } - } - - class DocumentModel extends Document { - constructor() { - super(); - this.mod = EmbeddedModel; - this.num = { type: Number }; - } - } - - var data = DocumentModel.create(); - data.mod = EmbeddedModel.create(); - data.mod.str = 'some data'; - data.num = 1; - - data.save().then(function() { - validateId(data); - return DocumentModel.findOne({ num: 1 }); - }).then(function(d) { - validateId(d); - expect(d.num).to.be.equal(1); - expect(d.mod).to.be.a('object'); - expect(d.mod).to.be.an.instanceof(EmbeddedModel); - expect(d.mod.str).to.be.equal('some data'); - }).then(done, done); - }); + it('should allow initialization of array of embedded documents', function(done) { + + class Discount extends EmbeddedDocument { + constructor() { + super(); + this.authorized = Boolean; + this.amount = Number; + } + } + + class Product extends Document { + constructor() { + super(); + this.name = String; + this.discounts = [Discount]; + } + } + + var product = Product.create({ + name: 'bike', + discounts: [{ + authorized: true, + amount: 9.99 + }, + { + authorized: false, + amount: 187.44 + }] + }); + + product.save().then(function() { + validateId(product); + expect(product.name).to.be.equal('bike'); + expect(product.discounts).to.have.length(2); + expect(product.discounts[0] instanceof Discount).to.be.true; + expect(product.discounts[1] instanceof Discount).to.be.true; + expect(product.discounts[0].authorized).to.be.equal(true); + expect(product.discounts[0].amount).to.be.equal(9.99); + expect(product.discounts[1].authorized).to.be.equal(false); + expect(product.discounts[1].amount).to.be.equal(187.44); + }).then(done, done); + }); + }); + + describe('defaults', function() { + it('should assign defaults to embedded types', function(done) { + + class EmbeddedModel extends EmbeddedDocument { + constructor() { + super(); + this.str = {type: String, default: 'hello'}; + } + } + + class DocumentModel extends Document { + constructor() { + super(); + this.emb = EmbeddedModel; + this.num = {type: Number}; + } + } + + var data = DocumentModel.create(); + data.emb = EmbeddedModel.create(); + data.num = 1; + + data.save().then(function() { + validateId(data); + return DocumentModel.findOne({num: 1}); + }).then(function(d) { + validateId(d); + expect(d.emb.str).to.be.equal('hello'); + }).then(done, done); + }); - it('should allow array of embedded types', function(done) { + it('should assign defaults to array of embedded types', function(done) { + + class Money extends EmbeddedDocument { + constructor() { + super(); + this.value = {type: Number, default: 100}; + } + } + + class Wallet extends Document { + constructor() { + super(); + this.contents = [Money]; + this.owner = String; + } + } + + var wallet = Wallet.create(); + wallet.owner = 'Scott'; + wallet.contents.push(Money.create()); + wallet.contents.push(Money.create()); + wallet.contents.push(Money.create()); + + wallet.save().then(function() { + validateId(wallet); + return Wallet.findOne({owner: 'Scott'}); + }).then(function(w) { + validateId(w); + expect(w.owner).to.be.equal('Scott'); + expect(w.contents[0].value).to.be.equal(100); + expect(w.contents[1].value).to.be.equal(100); + expect(w.contents[2].value).to.be.equal(100); + }).then(done, done); + }); + }); + + describe('validate', function() { + + it('should validate embedded values', function(done) { + + class EmbeddedModel extends EmbeddedDocument { + constructor() { + super(); + this.num = {type: Number, max: 10}; + } + } + + class DocumentModel extends Document { + constructor() { + super(); + this.emb = EmbeddedModel; + } + } + + var data = DocumentModel.create(); + data.emb = EmbeddedModel.create(); + data.emb.num = 26; + + data.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + expect(error.message).to.contain('max'); + }).then(done, done); + }); - class Limb extends EmbeddedDocument { - constructor() { - super(); - this.type = String; - } - } - - class Person extends Document { - constructor() { - super(); - this.limbs = [Limb]; - this.name = String; - } - - static collectionName() { - return 'people'; - } - } - - var person = Person.create(); - person.name = 'Scott'; - person.limbs.push(Limb.create()); - person.limbs[0].type = 'left arm'; - person.limbs.push(Limb.create()); - person.limbs[1].type = 'right arm'; - person.limbs.push(Limb.create()); - person.limbs[2].type = 'left leg'; - person.limbs.push(Limb.create()); - person.limbs[3].type = 'right leg'; - - person.save().then(function() { - validateId(person); - expect(person.limbs).to.have.length(4); - return Person.findOne({ name: 'Scott' }); - }).then(function(p) { - validateId(p); - expect(p.name).to.be.equal('Scott'); - expect(p.limbs).to.be.a('array'); - expect(p.limbs).to.have.length(4); - expect(p.limbs[0].type).to.be.equal('left arm'); - expect(p.limbs[1].type).to.be.equal('right arm'); - expect(p.limbs[2].type).to.be.equal('left leg'); - expect(p.limbs[3].type).to.be.equal('right leg'); - }).then(done, done); - }); - - it('should save nested array of embeddeds', function(done) { - class Point extends EmbeddedDocument { - constructor() { - super(); - this.x = Number; - this.y = Number; - } - } - - class Polygon extends EmbeddedDocument { - constructor() { - super(); - this.points = [Point]; - } - } - - class WorldMap extends Document { - constructor() { - super(); - this.polygons = [Polygon]; - } - } - - var map = WorldMap.create(); - var polygon1 = Polygon.create(); - var polygon2 = Polygon.create(); - var point1 = Point.create({ x: 123.45, y: 678.90 }); - var point2 = Point.create({ x: 543.21, y: 987.60 }); - - map.polygons.push(polygon1); - map.polygons.push(polygon2); - polygon2.points.push(point1); - polygon2.points.push(point2); - - map.save().then(function() { - return WorldMap.findOne(); - }).then(function(m) { - expect(m.polygons).to.have.length(2); - expect(m.polygons[0]).to.be.instanceof(Polygon); - expect(m.polygons[1]).to.be.instanceof(Polygon); - expect(m.polygons[1].points).to.have.length(2); - expect(m.polygons[1].points[0]).to.be.instanceof(Point); - expect(m.polygons[1].points[1]).to.be.instanceof(Point); - }).then(done, done); - }); - - it('should allow nested initialization of embedded types', function(done) { - - class Discount extends EmbeddedDocument { - constructor() { - super(); - this.authorized = Boolean; - this.amount = Number; - } - } - - class Product extends Document { - constructor() { - super(); - this.name = String; - this.discount = Discount; - } - } - - var product = Product.create({ - name: 'bike', - discount: { - authorized: true, - amount: 9.99 - } - }); - - product.save().then(function() { - validateId(product); - expect(product.name).to.be.equal('bike'); - expect(product.discount).to.be.a('object'); - expect(product.discount instanceof Discount).to.be.true; - expect(product.discount.authorized).to.be.equal(true); - expect(product.discount.amount).to.be.equal(9.99); - }).then(done, done); - }); - - it('should allow initialization of array of embedded documents', function(done) { - - class Discount extends EmbeddedDocument { - constructor() { - super(); - this.authorized = Boolean; - this.amount = Number; - } - } - - class Product extends Document { - constructor() { - super(); - this.name = String; - this.discounts = [Discount]; - } - } - - var product = Product.create({ - name: 'bike', - discounts: [{ - authorized: true, - amount: 9.99 - }, - { - authorized: false, - amount: 187.44 - }] - }); - - product.save().then(function() { - validateId(product); - expect(product.name).to.be.equal('bike'); - expect(product.discounts).to.have.length(2); - expect(product.discounts[0] instanceof Discount).to.be.true; - expect(product.discounts[1] instanceof Discount).to.be.true; - expect(product.discounts[0].authorized).to.be.equal(true); - expect(product.discounts[0].amount).to.be.equal(9.99); - expect(product.discounts[1].authorized).to.be.equal(false); - expect(product.discounts[1].amount).to.be.equal(187.44); - }).then(done, done); - }); + it('should validate array of embedded values', function(done) { + + class Money extends EmbeddedDocument { + constructor() { + super(); + this.value = {type: Number, choices: [1, 5, 10, 20, 50, 100]}; + } + } + + class Wallet extends Document { + constructor() { + super(); + this.contents = [Money]; + } + } + + var wallet = Wallet.create(); + wallet.contents.push(Money.create()); + wallet.contents[0].value = 5; + wallet.contents.push(Money.create()); + wallet.contents[1].value = 26; + + wallet.save().then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + expect(error.message).to.contain('choices'); + }).then(done, done); }); - describe('defaults', function() { - it('should assign defaults to embedded types', function(done) { - - class EmbeddedModel extends EmbeddedDocument { - constructor() { - super(); - this.str = { type: String, default: 'hello' }; - } - } - - class DocumentModel extends Document { - constructor() { - super(); - this.emb = EmbeddedModel; - this.num = { type: Number }; - } - } - - var data = DocumentModel.create(); - data.emb = EmbeddedModel.create(); - data.num = 1; - - data.save().then(function() { - validateId(data); - return DocumentModel.findOne({ num: 1 }); - }).then(function(d) { - validateId(d); - expect(d.emb.str).to.be.equal('hello'); - }).then(done, done); - }); - - it('should assign defaults to array of embedded types', function(done) { - - class Money extends EmbeddedDocument { - constructor() { - super(); - this.value = { type: Number, default: 100 }; - } - } - - class Wallet extends Document { - constructor() { - super(); - this.contents = [Money]; - this.owner = String; - } - } - - var wallet = Wallet.create(); - wallet.owner = 'Scott'; - wallet.contents.push(Money.create()); - wallet.contents.push(Money.create()); - wallet.contents.push(Money.create()); - - wallet.save().then(function() { - validateId(wallet); - return Wallet.findOne({ owner: 'Scott' }); - }).then(function(w) { - validateId(w); - expect(w.owner).to.be.equal('Scott'); - expect(w.contents[0].value).to.be.equal(100); - expect(w.contents[1].value).to.be.equal(100); - expect(w.contents[2].value).to.be.equal(100); - }).then(done, done); - }); + }); + + describe('canonicalize', function() { + it('should ensure timestamp dates are converted to Date objects', function(done) { + class Education extends EmbeddedDocument { + constructor() { + super(); + + this.school = String; + this.major = String; + this.dateGraduated = Date; + } + + static collectionName() { + return 'people'; + } + } + + class Person extends Document { + constructor() { + super(); + + this.gradSchool = Education; + } + + static collectionName() { + return 'people'; + } + } + + var now = new Date(); + + var person = Person.create({ + gradSchool: { + school: 'CMU', + major: 'ECE', + dateGraduated: now + } + }); + + person.save().then(function() { + validateId(person); + expect(person.gradSchool.school).to.be.equal('CMU'); + expect(person.gradSchool.dateGraduated.getFullYear()).to.be.equal(now.getFullYear()); + expect(person.gradSchool.dateGraduated.getHours()).to.be.equal(now.getHours()); + expect(person.gradSchool.dateGraduated.getMinutes()).to.be.equal(now.getMinutes()); + expect(person.gradSchool.dateGraduated.getMonth()).to.be.equal(now.getMonth()); + expect(person.gradSchool.dateGraduated.getSeconds()).to.be.equal(now.getSeconds()); + }).then(done, done); }); + }); - describe('validate', function() { - - it('should validate embedded values', function(done) { - - class EmbeddedModel extends EmbeddedDocument { - constructor() { - super(); - this.num = { type: Number, max: 10 }; - } - } - - class DocumentModel extends Document { - constructor() { - super(); - this.emb = EmbeddedModel; - } - } - - var data = DocumentModel.create(); - data.emb = EmbeddedModel.create(); - data.emb.num = 26; - - data.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - expect(error.message).to.contain('max'); - }).then(done, done); - }); - - it('should validate array of embedded values', function(done) { - - class Money extends EmbeddedDocument { - constructor() { - super(); - this.value = { type: Number, choices: [1, 5, 10, 20, 50, 100] }; - } - } - - class Wallet extends Document { - constructor() { - super(); - this.contents = [Money]; - } - } - - var wallet = Wallet.create(); - wallet.contents.push(Money.create()); - wallet.contents[0].value = 5; - wallet.contents.push(Money.create()); - wallet.contents[1].value = 26; - - wallet.save().then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - expect(error.message).to.contain('choices'); - }).then(done, done); - }); + describe('hooks', function() { - }); + it('should call all pre and post functions on embedded models', function(done) { + + var preValidateCalled = false; + var preSaveCalled = false; + var preDeleteCalled = false; + + var postValidateCalled = false; + var postSaveCalled = false; + var postDeleteCalled = false; + + class Coffee extends EmbeddedDocument { + constructor() { + super(); + } + + preValidate() { + preValidateCalled = true; + } + + postValidate() { + postValidateCalled = true; + } + + preSave() { + preSaveCalled = true; + } + + postSave() { + postSaveCalled = true; + } + + preDelete() { + preDeleteCalled = true; + } - describe('canonicalize', function() { - it('should ensure timestamp dates are converted to Date objects', function(done) { - class Education extends EmbeddedDocument { - constructor() { - super(); - - this.school = String; - this.major = String; - this.dateGraduated = Date; - } - - static collectionName() { - return 'people'; - } - } - - class Person extends Document { - constructor() { - super(); - - this.gradSchool = Education; - } - - static collectionName() { - return 'people'; - } - } - - var now = new Date(); - - var person = Person.create({ - gradSchool: { - school: 'CMU', - major: 'ECE', - dateGraduated: now - } - }); - - person.save().then(function() { - validateId(person); - expect(person.gradSchool.school).to.be.equal('CMU'); - expect(person.gradSchool.dateGraduated.getFullYear()).to.be.equal(now.getFullYear()); - expect(person.gradSchool.dateGraduated.getHours()).to.be.equal(now.getHours()); - expect(person.gradSchool.dateGraduated.getMinutes()).to.be.equal(now.getMinutes()); - expect(person.gradSchool.dateGraduated.getMonth()).to.be.equal(now.getMonth()); - expect(person.gradSchool.dateGraduated.getSeconds()).to.be.equal(now.getSeconds()); - }).then(done, done); - }); + postDelete() { + postDeleteCalled = true; + } + } + + class Cup extends Document { + constructor() { + super(); + + this.contents = Coffee; + } + } + + var cup = Cup.create(); + cup.contents = Coffee.create(); + + cup.save().then(function() { + validateId(cup); + + // Pre/post save and validate should be called + expect(preValidateCalled).to.be.equal(true); + expect(preSaveCalled).to.be.equal(true); + expect(postValidateCalled).to.be.equal(true); + expect(postSaveCalled).to.be.equal(true); + + // Pre/post delete should not have been called yet + expect(preDeleteCalled).to.be.equal(false); + expect(postDeleteCalled).to.be.equal(false); + + return cup.delete(); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(1); + + expect(preDeleteCalled).to.be.equal(true); + expect(postDeleteCalled).to.be.equal(true); + }).then(done, done); }); - describe('hooks', function() { + it('should call all pre and post functions on array of embedded models', function(done) { + + var preValidateCalled = false; + var preSaveCalled = false; + var preDeleteCalled = false; - it('should call all pre and post functions on embedded models', function(done) { + var postValidateCalled = false; + var postSaveCalled = false; + var postDeleteCalled = false; - var preValidateCalled = false; - var preSaveCalled = false; - var preDeleteCalled = false; + class Money extends EmbeddedDocument { + constructor() { + super(); + } - var postValidateCalled = false; - var postSaveCalled = false; - var postDeleteCalled = false; + preValidate() { + preValidateCalled = true; + } - class Coffee extends EmbeddedDocument { - constructor() { - super(); - } + postValidate() { + postValidateCalled = true; + } - preValidate() { - preValidateCalled = true; - } + preSave() { + preSaveCalled = true; + } - postValidate() { - postValidateCalled = true; - } + postSave() { + postSaveCalled = true; + } - preSave() { - preSaveCalled = true; - } + preDelete() { + preDeleteCalled = true; + } - postSave() { - postSaveCalled = true; - } + postDelete() { + postDeleteCalled = true; + } + } - preDelete() { - preDeleteCalled = true; - } - - postDelete() { - postDeleteCalled = true; - } - } - - class Cup extends Document { - constructor() { - super(); - - this.contents = Coffee; - } - } - - var cup = Cup.create(); - cup.contents = Coffee.create(); - - cup.save().then(function() { - validateId(cup); - - // Pre/post save and validate should be called - expect(preValidateCalled).to.be.equal(true); - expect(preSaveCalled).to.be.equal(true); - expect(postValidateCalled).to.be.equal(true); - expect(postSaveCalled).to.be.equal(true); - - // Pre/post delete should not have been called yet - expect(preDeleteCalled).to.be.equal(false); - expect(postDeleteCalled).to.be.equal(false); - - return cup.delete(); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(1); - - expect(preDeleteCalled).to.be.equal(true); - expect(postDeleteCalled).to.be.equal(true); - }).then(done, done); - }); - - it('should call all pre and post functions on array of embedded models', function(done) { - - var preValidateCalled = false; - var preSaveCalled = false; - var preDeleteCalled = false; - - var postValidateCalled = false; - var postSaveCalled = false; - var postDeleteCalled = false; - - class Money extends EmbeddedDocument { - constructor() { - super(); - } - - preValidate() { - preValidateCalled = true; - } - - postValidate() { - postValidateCalled = true; - } - - preSave() { - preSaveCalled = true; - } - - postSave() { - postSaveCalled = true; - } - - preDelete() { - preDeleteCalled = true; - } - - postDelete() { - postDeleteCalled = true; - } - } - - class Wallet extends Document { - constructor() { - super(); - - this.contents = [Money]; - } - } - - var wallet = Wallet.create(); - wallet.contents.push(Money.create()); - wallet.contents.push(Money.create()); - - wallet.save().then(function() { - validateId(wallet); - - // Pre/post save and validate should be called - expect(preValidateCalled).to.be.equal(true); - expect(postValidateCalled).to.be.equal(true); - expect(preSaveCalled).to.be.equal(true); - expect(postSaveCalled).to.be.equal(true); - - // Pre/post delete should not have been called yet - expect(preDeleteCalled).to.be.equal(false); - expect(postDeleteCalled).to.be.equal(false); - - return wallet.delete(); - }).then(function(numDeleted) { - expect(numDeleted).to.be.equal(1); - - expect(preDeleteCalled).to.be.equal(true); - expect(postDeleteCalled).to.be.equal(true); - }).then(done, done); - }); + class Wallet extends Document { + constructor() { + super(); + + this.contents = [Money]; + } + } + + var wallet = Wallet.create(); + wallet.contents.push(Money.create()); + wallet.contents.push(Money.create()); + + wallet.save().then(function() { + validateId(wallet); + + // Pre/post save and validate should be called + expect(preValidateCalled).to.be.equal(true); + expect(postValidateCalled).to.be.equal(true); + expect(preSaveCalled).to.be.equal(true); + expect(postSaveCalled).to.be.equal(true); + + // Pre/post delete should not have been called yet + expect(preDeleteCalled).to.be.equal(false); + expect(postDeleteCalled).to.be.equal(false); + + return wallet.delete(); + }).then(function(numDeleted) { + expect(numDeleted).to.be.equal(1); + + expect(preDeleteCalled).to.be.equal(true); + expect(postDeleteCalled).to.be.equal(true); + }).then(done, done); + }); + }); + + describe('serialize', function() { + it('should serialize data to JSON', function(done) { + class Address extends EmbeddedDocument { + constructor() { + super(); + + this.street = String; + this.city = String; + this.zipCode = Number; + this.isPoBox = Boolean; + } + } + + class Person extends Document { + constructor() { + super(); + + this.name = String; + this.age = Number; + this.isAlive = Boolean; + this.children = [String]; + this.address = Address; + } + + static collectionName() { + return 'people'; + } + } + + var person = Person.create({ + name: 'Scott', + address: { + street: '123 Fake St.', + city: 'Cityville', + zipCode: 12345, + isPoBox: false + } + }); + + person.save().then(function() { + validateId(person); + expect(person.name).to.be.equal('Scott'); + expect(person.address).to.be.an.instanceof(Address); + expect(person.address.street).to.be.equal('123 Fake St.'); + expect(person.address.city).to.be.equal('Cityville'); + expect(person.address.zipCode).to.be.equal(12345); + expect(person.address.isPoBox).to.be.equal(false); + + var json = person.toJSON(); + + expect(json.name).to.be.equal('Scott'); + expect(json.address).to.not.be.an.instanceof(Address); + expect(json.address.street).to.be.equal('123 Fake St.'); + expect(json.address.city).to.be.equal('Cityville'); + expect(json.address.zipCode).to.be.equal(12345); + expect(json.address.isPoBox).to.be.equal(false); + }).then(done, done); }); - describe('serialize', function() { - it('should serialize data to JSON', function(done) { - class Address extends EmbeddedDocument { - constructor() { - super(); - - this.street = String; - this.city = String; - this.zipCode = Number; - this.isPoBox = Boolean; - } - } - - class Person extends Document { - constructor() { - super(); - - this.name = String; - this.age = Number; - this.isAlive = Boolean; - this.children = [String]; - this.address = Address; - } - - static collectionName() { - return 'people'; - } - } - - var person = Person.create({ - name: 'Scott', - address: { - street: '123 Fake St.', - city: 'Cityville', - zipCode: 12345, - isPoBox: false - } - }); - - person.save().then(function() { - validateId(person); - expect(person.name).to.be.equal('Scott'); - expect(person.address).to.be.an.instanceof(Address); - expect(person.address.street).to.be.equal('123 Fake St.'); - expect(person.address.city).to.be.equal('Cityville'); - expect(person.address.zipCode).to.be.equal(12345); - expect(person.address.isPoBox).to.be.equal(false); - - var json = person.toJSON(); - - expect(json.name).to.be.equal('Scott'); - expect(json.address).to.not.be.an.instanceof(Address); - expect(json.address.street).to.be.equal('123 Fake St.'); - expect(json.address.city).to.be.equal('Cityville'); - expect(json.address.zipCode).to.be.equal(12345); - expect(json.address.isPoBox).to.be.equal(false); - }).then(done, done); - }); - - it('should serialize data to JSON and ignore methods', function(done) { - class Address extends EmbeddedDocument { - constructor() { - super(); - this.street = String; - } - - getBar() { - return 'bar'; - } - } - - class Person extends Document { - constructor() { - super(); - - this.name = String; - this.address = Address; - } - - static collectionName() { - return 'people'; - } - - getFoo() { - return 'foo'; - } - } - - var person = Person.create({ - name: 'Scott', - address : { - street : 'Bar street' - } - }); - - var json = person.toJSON(); - expect(json).to.have.keys(['_id', 'name', 'address']); - expect(json.address).to.have.keys(['street']); - - done(); - }); + it('should serialize data to JSON and ignore methods', function(done) { + class Address extends EmbeddedDocument { + constructor() { + super(); + this.street = String; + } + + getBar() { + return 'bar'; + } + } + + class Person extends Document { + constructor() { + super(); + + this.name = String; + this.address = Address; + } + + static collectionName() { + return 'people'; + } + + getFoo() { + return 'foo'; + } + } + + var person = Person.create({ + name: 'Scott', + address: { + street: 'Bar street' + } + }); + + var json = person.toJSON(); + expect(json).to.have.keys(['_id', 'name', 'address']); + expect(json.address).to.have.keys(['street']); + + done(); }); + }); }); \ No newline at end of file diff --git a/test/issues.test.js b/test/issues.test.js index 9a9f73e..2bcac6f 100644 --- a/test/issues.test.js +++ b/test/issues.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var expect = require('chai').expect; var connect = require('../index').connect; @@ -9,365 +9,369 @@ var validateId = require('./util').validateId; describe('Issues', function() { - // TODO: Should probably use mock database client... - var url = 'nedb://memory'; - //var url = 'mongodb://localhost/camo_test'; - var database = null; - - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function() { - return done(); - }); - }); - - beforeEach(function(done) { - done(); + // TODO: Should probably use mock database client... + var url = 'nedb://memory'; + //var url = 'mongodb://localhost/camo_test'; + var database = null; + + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); - - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); - }); - - after(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + }); + + beforeEach(function(done) { + done(); + }); + + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + after(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + describe('#4', function() { + it('should not load duplicate references in array when only one reference is present', function(done) { + /* + * This issue happens when there are multiple objects in the database, + * each object has an array of references, and at least two of the + * object's arrays contain the same reference. + + * In this case, both user1 and user2 have a reference to eye1. So + * when we call `.find()`, both user1 and user2 will have a + * duplicate reference to eye1, which is not correct. + */ + + class Eye extends Document { + constructor() { + super(); + this.color = String; + } + } + + class User extends Document { + constructor() { + super(); + this.eyes = [Eye]; + } + } + + var user1 = User.create(); + var user2 = User.create(); + var eye1 = Eye.create({color: 'blue'}); + var eye2 = Eye.create({color: 'brown'}); + + var id; + + eye1.save().then(function(e) { + validateId(e); + return eye2.save(); + }).then(function(e) { + validateId(e); + user1.eyes.push(eye1, eye2); + return user1.save(); + }).then(function(u) { + validateId(u); + user2.eyes.push(eye1); + return user2.save(); + }).then(function(u) { + validateId(u); + return User.find({}); + }).then(function(users) { + expect(users).to.have.length(2); + + // Get user1 + var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; + + // Ensure we have correct number of eyes... + expect(u1.eyes).to.have.length(2); + + var e1 = String(u1.eyes[0]._id) === String(eye1._id) ? u1.eyes[0] : u1.eyes[1]; + var e2 = String(u1.eyes[1]._id) === String(eye2._id) ? u1.eyes[1] : u1.eyes[0]; + + // ...and that we have the correct eyes + expect(String(e1._id)).to.be.equal(String(eye1._id)); + expect(String(e2._id)).to.be.equal(String(eye2._id)); + }).then(done, done); }); - - describe('#4', function() { - it('should not load duplicate references in array when only one reference is present', function(done) { - /* - * This issue happens when there are multiple objects in the database, - * each object has an array of references, and at least two of the - * object's arrays contain the same reference. - - * In this case, both user1 and user2 have a reference to eye1. So - * when we call `.find()`, both user1 and user2 will have a - * duplicate reference to eye1, which is not correct. - */ - - class Eye extends Document { - constructor() { - super(); - this.color = String; - } - } - - class User extends Document { - constructor() { - super(); - this.eyes = [Eye]; - } - } - - var user1 = User.create(); - var user2 = User.create(); - var eye1 = Eye.create({color: 'blue'}); - var eye2 = Eye.create({color: 'brown'}); - - var id; - - eye1.save().then(function(e) { - validateId(e); - return eye2.save(); - }).then(function(e) { - validateId(e); - user1.eyes.push(eye1, eye2); - return user1.save(); - }).then(function(u) { - validateId(u); - user2.eyes.push(eye1); - return user2.save(); - }).then(function(u) { - validateId(u); - return User.find({}); - }).then(function(users) { - expect(users).to.have.length(2); - - // Get user1 - var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; - - // Ensure we have correct number of eyes... - expect(u1.eyes).to.have.length(2); - - var e1 = String(u1.eyes[0]._id) === String(eye1._id) ? u1.eyes[0] : u1.eyes[1]; - var e2 = String(u1.eyes[1]._id) === String(eye2._id) ? u1.eyes[1] : u1.eyes[0]; - - // ...and that we have the correct eyes - expect(String(e1._id)).to.be.equal(String(eye1._id)); - expect(String(e2._id)).to.be.equal(String(eye2._id)); - }).then(done, done); + }); + + describe('#5', function() { + it('should allow multiple references to the same object in same array', function(done) { + /* + * This issue happens when an object has an array of + * references and there are multiple references to the + * same object in the array. + * + * In the code below, we give the user two references + * to the same Eye, but when we load the user there is + * only one reference there. + */ + + class Eye extends Document { + constructor() { + super(); + this.color = String; + } + } + + class User extends Document { + constructor() { + super(); + this.eyes = [Eye]; + } + } + + var user = User.create(); + var eye = Eye.create({color: 'blue'}); + + eye.save().then(function(e) { + validateId(e); + user.eyes.push(eye, eye); + return user.save(); + }).then(function(u) { + validateId(u); + return User.find({}); + }).then(function(users) { + expect(users).to.have.length(1); + expect(users[0].eyes).to.have.length(2); + + var eyeRefs = users[0].eyes.map(function(e) { + return e._id; }); - }); - describe('#5', function() { - it('should allow multiple references to the same object in same array', function(done) { - /* - * This issue happens when an object has an array of - * references and there are multiple references to the - * same object in the array. - * - * In the code below, we give the user two references - * to the same Eye, but when we load the user there is - * only one reference there. - */ - - class Eye extends Document { - constructor() { - super(); - this.color = String; - } - } - - class User extends Document { - constructor() { - super(); - this.eyes = [Eye]; - } - } - - var user = User.create(); - var eye = Eye.create({color: 'blue'}); - - eye.save().then(function(e) { - validateId(e); - user.eyes.push(eye, eye); - return user.save(); - }).then(function(u) { - validateId(u); - return User.find({}); - }).then(function(users) { - expect(users).to.have.length(1); - expect(users[0].eyes).to.have.length(2); - - var eyeRefs = users[0].eyes.map(function(e) {return e._id;}); - - expect(eyeRefs).to.include(eye._id); - }).then(done, done); - }); + expect(eyeRefs).to.include(eye._id); + }).then(done, done); }); - - describe('#8', function() { - it('should use virtuals when initializing instance with data', function(done) { - /* - * This issue happens when a model has virtual setters - * and the caller tries to use those setters during - * initialization via `create()`. The setters are - * never called, but they should be. - */ - - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; - } - - set fullName(name) { - var split = name.split(' '); - this.firstName = split[0]; - this.lastName = split[1]; - } - - get fullName() { - return this.firstName + ' ' + this.lastName; - } - } - - var user = User.create({ - fullName: 'Billy Bob' - }); - - expect(user.firstName).to.be.equal('Billy'); - expect(user.lastName).to.be.equal('Bob'); - - done(); - }); + }); + + describe('#8', function() { + it('should use virtuals when initializing instance with data', function(done) { + /* + * This issue happens when a model has virtual setters + * and the caller tries to use those setters during + * initialization via `create()`. The setters are + * never called, but they should be. + */ + + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } + + set fullName(name) { + var split = name.split(' '); + this.firstName = split[0]; + this.lastName = split[1]; + } + + get fullName() { + return this.firstName + ' ' + this.lastName; + } + } + + var user = User.create({ + fullName: 'Billy Bob' + }); + + expect(user.firstName).to.be.equal('Billy'); + expect(user.lastName).to.be.equal('Bob'); + + done(); }); - - describe('#20', function() { - it('should not alias _id to id in queries and returned documents', function(done) { - /* - * Camo inconsistently aliases the '_id' field to 'id'. When - * querying, we must use '_id', but documents are returned - * with '_id' AND 'id'. 'id' alias should be removed. - * - * TODO: Uncomment lines below once '_id' is fully - * deprecated and removed. - */ - - class User extends Document { - constructor() { - super(); - this.name = String; - } - } - - var user = User.create({ - name: 'Billy Bob' - }); - - user.save().then(function() { - validateId(user); - - //expect(user.id).to.not.exist; - expect(user._id).to.exist; - - // Should NOT be able to use 'id' to query - return User.findOne({ id: user._id }); - }).then(function(u) { - expect(u).to.not.exist; - - // SHOULD be able to use '_id' to query - return User.findOne({ _id: user._id }); - }).then(function(u) { - //expect(u.id).to.not.exist; - expect(u).to.exist; - validateId(user); - }).then(done, done); - }); + }); + + describe('#20', function() { + it('should not alias _id to id in queries and returned documents', function(done) { + /* + * Camo inconsistently aliases the '_id' field to 'id'. When + * querying, we must use '_id', but documents are returned + * with '_id' AND 'id'. 'id' alias should be removed. + * + * TODO: Uncomment lines below once '_id' is fully + * deprecated and removed. + */ + + class User extends Document { + constructor() { + super(); + this.name = String; + } + } + + var user = User.create({ + name: 'Billy Bob' + }); + + user.save().then(function() { + validateId(user); + + //expect(user.id).to.not.exist; + expect(user._id).to.exist; + + // Should NOT be able to use 'id' to query + return User.findOne({id: user._id}); + }).then(function(u) { + expect(u).to.not.exist; + + // SHOULD be able to use '_id' to query + return User.findOne({_id: user._id}); + }).then(function(u) { + //expect(u.id).to.not.exist; + expect(u).to.exist; + validateId(user); + }).then(done, done); + }); + }); + + describe('#53', function() { + /* + * Camo should validate that all properties conform to + * the type they were given in the schema. However, + * array types are not properly validated due to not + * properly checking for 'type === Array' and + * 'type === []' in validator code. + */ + + it('should validate Array types properly', function(done) { + class Foo extends Document { + constructor() { + super(); + + this.bar = Array; + } + } + + var foo = Foo.create({bar: [1, 2, 3]}); + + foo.save().then(function(f) { + expect(f.bar).to.have.length(3); + expect(f.bar).to.include(1); + expect(f.bar).to.include(2); + expect(f.bar).to.include(3); + + foo.bar = 1; + return foo.save(); + }).then(function(f) { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); }); - describe('#53', function() { - /* - * Camo should validate that all properties conform to - * the type they were given in the schema. However, - * array types are not properly validated due to not - * properly checking for 'type === Array' and - * 'type === []' in validator code. - */ - - it('should validate Array types properly', function(done) { - class Foo extends Document { - constructor() { - super(); - - this.bar = Array; - } - } - - var foo = Foo.create({bar: [1, 2, 3]}); - - foo.save().then(function(f) { - expect(f.bar).to.have.length(3); - expect(f.bar).to.include(1); - expect(f.bar).to.include(2); - expect(f.bar).to.include(3); - - foo.bar = 1; - return foo.save(); - }).then(function(f){ - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); - - it('should validate [] types properly', function(done) { + it('should validate [] types properly', function(done) { - class Foo extends Document { - constructor() { - super(); + class Foo extends Document { + constructor() { + super(); - this.bar = []; - } - } + this.bar = []; + } + } - var foo = Foo.create({bar: [1, 2, 3]}); + var foo = Foo.create({bar: [1, 2, 3]}); - foo.save().then(function(f) { - expect(f.bar).to.have.length(3); - expect(f.bar).to.include(1); - expect(f.bar).to.include(2); - expect(f.bar).to.include(3); + foo.save().then(function(f) { + expect(f.bar).to.have.length(3); + expect(f.bar).to.include(1); + expect(f.bar).to.include(2); + expect(f.bar).to.include(3); - foo.bar = 2; - return foo.save(); - }).then(function(f){ - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.instanceof(ValidationError); - }).then(done, done); - }); + foo.bar = 2; + return foo.save(); + }).then(function(f) { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.instanceof(ValidationError); + }).then(done, done); }); - - describe('#55', function() { - it('should return updated data on findOneAndUpdate when updating nested data', function(done) { - /* - * When updating nested data with findOneAndUpdate, - * the document returned to you should contain - * all of the updated data. But due to lack of - * support in NeDB versions < 1.8, I had to use - * a hack (_.assign) to update the document. This - * doesn't properly update nested data. - * - * Temporary fix is to just reload the document - * with findOne. - */ - - class Contact extends EmbeddedDocument { - constructor() { - super(); - - this.email = String; - this.phone = String; - } - } - - class Person extends Document { - constructor() { - super(); - this.name = String; - this.contact = Contact; - } - } - - var person = Person.create({ - name: 'John Doe', - contact: { - email: 'john@doe.info', - phone: 'NA' - } - }); - - person.save().then(function(person) { - return Person.findOneAndUpdate({_id: person._id}, {name: 'John Derp', 'contact.phone': '0123456789'}); - }).then(function(person) { - expect(person.name).to.be.equal('John Derp'); - expect(person.contact.email).to.be.equal('john@doe.info'); - expect(person.contact.phone).to.be.equal('0123456789'); - }).then(done, done); - }); + }); + + describe('#55', function() { + it('should return updated data on findOneAndUpdate when updating nested data', function(done) { + /* + * When updating nested data with findOneAndUpdate, + * the document returned to you should contain + * all of the updated data. But due to lack of + * support in NeDB versions < 1.8, I had to use + * a hack (_.assign) to update the document. This + * doesn't properly update nested data. + * + * Temporary fix is to just reload the document + * with findOne. + */ + + class Contact extends EmbeddedDocument { + constructor() { + super(); + + this.email = String; + this.phone = String; + } + } + + class Person extends Document { + constructor() { + super(); + this.name = String; + this.contact = Contact; + } + } + + var person = Person.create({ + name: 'John Doe', + contact: { + email: 'john@doe.info', + phone: 'NA' + } + }); + + person.save().then(function(person) { + return Person.findOneAndUpdate({_id: person._id}, {name: 'John Derp', 'contact.phone': '0123456789'}); + }).then(function(person) { + expect(person.name).to.be.equal('John Derp'); + expect(person.contact.email).to.be.equal('john@doe.info'); + expect(person.contact.phone).to.be.equal('0123456789'); + }).then(done, done); }); - - describe('#57', function() { - it('should not save due to Promise.reject in hook', function(done) { - /* - * Rejecting a Promise inside of a pre-save hook should - * cause the save to be aborted, and the .caught() method - * should be invoked on the Promise chain. This wasn't - * happening due to how the hooks were being collected - * and executed. - */ - - class Foo extends Document { - constructor() { - super(); - - this.bar = String; - } - - preValidate() { - return Promise.reject('DO NOT SAVE'); - } - } - - Foo.create({bar: 'bar'}).save().then(function(foo) { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error).to.be.equal('DO NOT SAVE'); - }).then(done, done); - }); + }); + + describe('#57', function() { + it('should not save due to Promise.reject in hook', function(done) { + /* + * Rejecting a Promise inside of a pre-save hook should + * cause the save to be aborted, and the .caught() method + * should be invoked on the Promise chain. This wasn't + * happening due to how the hooks were being collected + * and executed. + */ + + class Foo extends Document { + constructor() { + super(); + + this.bar = String; + } + + preValidate() { + return Promise.reject('DO NOT SAVE'); + } + } + + Foo.create({bar: 'bar'}).save().then(function(foo) { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error).to.be.equal('DO NOT SAVE'); + }).then(done, done); }); + }); }); \ No newline at end of file diff --git a/test/mongoclient.test.js b/test/mongoclient.test.js index a8a9bc5..0de8714 100644 --- a/test/mongoclient.test.js +++ b/test/mongoclient.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var expect = require('chai').expect; @@ -9,229 +9,218 @@ var validateId = require('./util').validateId; describe('MongoClient', function() { - var url = 'mongodb://localhost/camo_test'; - var database = null; + var url = 'mongodb://localhost/camo_test'; + var database = null; - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function(){ - return done(); - }); - }); + before(() => { + return connect(url) + .then(function(db) { + database = db; + return database.dropDatabase(); + }); + }); + + afterEach(() => database.dropDatabase()); - beforeEach(function(done) { - done(); + describe('id', function() { + it('should allow custom _id values', function(done) { + class School extends Document { + constructor() { + super(); + + this.name = String; + } + } + + var school = School.create(); + school._id = new ObjectId('1234567890abcdef12345678'); + school.name = 'Springfield Elementary'; + + school.save().then(function() { + validateId(school); + expect(school._id.toString()).to.be.equal('1234567890abcdef12345678'); + return School.findOne(); + }).then(function(s) { + validateId(s); + expect(s._id.toString()).to.be.equal('1234567890abcdef12345678'); + }).then(done, done); + }); + }); + + describe('query', function() { + class User extends Document { + constructor() { + super(); + this.firstName = String; + this.lastName = String; + } + } + + /* + * The MongoClient should cast all IDs to ObjectIDs. If the objects + * requested aren't properly returned, then the IDs were not + * successfully cast. + */ + it('should automatically cast string ID in query to ObjectID', function(done) { + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; + + user.save().then(function() { + validateId(user); + + var id = String(user._id); + return User.findOne({_id: id}); + }).then(function(u) { + validateId(u); + }).then(done, done); }); - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + /* + * Sanity check to make sure we didn't screw up the case + * where user actually passes an ObjectId + */ + it('should automatically cast string ID in query to ObjectID', function(done) { + var user = User.create(); + user.firstName = 'Billy'; + user.lastName = 'Bob'; + + user.save().then(function() { + validateId(user); + + return User.findOne({_id: user._id}); + }).then(function(u) { + validateId(u); + }).then(done, done); }); - after(function(done) { - done(); + /* + * Same as above, but we're testing out more complicated + * queries. In this case we try it with '$in'. + */ + it('should automatically cast string IDs in \'$in\' operator to ObjectIDs', function(done) { + var user1 = User.create(); + user1.firstName = 'Billy'; + user1.lastName = 'Bob'; + + var user2 = User.create(); + user2.firstName = 'Jenny'; + user2.lastName = 'Jane'; + + var user3 = User.create(); + user3.firstName = 'Danny'; + user3.lastName = 'David'; + + Promise.all([user1.save(), user2.save(), user3.save()]).then(function() { + validateId(user1); + validateId(user2); + + var id1 = String(user1._id); + var id3 = String(user3._id); + return User.find({_id: {'$in': [id1, id3]}}); + }).then(function(users) { + expect(users).to.have.length(2); + + var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; + var u3 = String(users[1]._id) === String(user3._id) ? users[1] : users[0]; + + expect(String(u1._id)).to.be.equal(String(user1._id)); + expect(String(u3._id)).to.be.equal(String(user3._id)); + }).then(done, done); }); - describe('id', function() { - it('should allow custom _id values', function(done) { - class School extends Document { - constructor() { - super(); + it('should automatically cast string IDs in deep query objects', function(done) { + var user1 = User.create(); + user1.firstName = 'Billy'; + user1.lastName = 'Bob'; - this.name = String; - } - } + var user2 = User.create(); + user2.firstName = 'Jenny'; + user2.lastName = 'Jane'; - var school = School.create(); - school._id = new ObjectId('1234567890abcdef12345678'); - school.name = 'Springfield Elementary'; - - school.save().then(function() { - validateId(school); - expect(school._id.toString()).to.be.equal('1234567890abcdef12345678'); - return School.findOne(); - }).then(function(s) { - validateId(s); - expect(s._id.toString()).to.be.equal('1234567890abcdef12345678'); - }).then(done, done); - }); - }); + var user3 = User.create(); + user3.firstName = 'Danny'; + user3.lastName = 'David'; + + Promise.all([user1.save(), user2.save(), user3.save()]).then(function() { + validateId(user1); + validateId(user2); - describe('query', function() { - class User extends Document { - constructor() { - super(); - this.firstName = String; - this.lastName = String; + var id1 = String(user1._id); + var id3 = String(user3._id); + return User.find({$or: [{_id: id1}, {_id: id3}]}); + }).then(function(users) { + expect(users).to.have.length(2); + + var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; + var u3 = String(users[1]._id) === String(user3._id) ? users[1] : users[0]; + + expect(String(u1._id)).to.be.equal(String(user1._id)); + expect(String(u3._id)).to.be.equal(String(user3._id)); + }).then(done, done); + }); + }); + + describe('indexes', function() { + it('should reject documents with duplicate values in unique-indexed fields', function(done) { + class User extends Document { + constructor() { + super(); + + this.schema({ + name: String, + email: { + type: String, + unique: true } + }); } + } + + var user1 = User.create(); + user1.name = 'Bill'; + user1.email = 'billy@example.com'; - /* - * The MongoClient should cast all IDs to ObjectIDs. If the objects - * requested aren't properly returned, then the IDs were not - * successfully cast. - */ - it('should automatically cast string ID in query to ObjectID', function(done) { - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; - - user.save().then(function() { - validateId(user); - - var id = String(user._id); - return User.findOne({_id: id}); - }).then(function(u) { - validateId(u); - }).then(done, done); - }); - - /* - * Sanity check to make sure we didn't screw up the case - * where user actually passes an ObjectId - */ - it('should automatically cast string ID in query to ObjectID', function(done) { - var user = User.create(); - user.firstName = 'Billy'; - user.lastName = 'Bob'; - - user.save().then(function() { - validateId(user); - - return User.findOne({_id: user._id}); - }).then(function(u) { - validateId(u); - }).then(done, done); - }); - - /* - * Same as above, but we're testing out more complicated - * queries. In this case we try it with '$in'. - */ - it('should automatically cast string IDs in \'$in\' operator to ObjectIDs', function(done) { - var user1 = User.create(); - user1.firstName = 'Billy'; - user1.lastName = 'Bob'; - - var user2 = User.create(); - user2.firstName = 'Jenny'; - user2.lastName = 'Jane'; - - var user3 = User.create(); - user3.firstName = 'Danny'; - user3.lastName = 'David'; - - Promise.all([user1.save(), user2.save(), user3.save()]).then(function() { - validateId(user1); - validateId(user2); - - var id1 = String(user1._id); - var id3 = String(user3._id); - return User.find({ _id: { '$in': [ id1, id3 ] } }); - }).then(function(users) { - expect(users).to.have.length(2); - - var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; - var u3 = String(users[1]._id) === String(user3._id) ? users[1] : users[0]; - - expect(String(u1._id)).to.be.equal(String(user1._id)); - expect(String(u3._id)).to.be.equal(String(user3._id)); - }).then(done, done); - }); - - it('should automatically cast string IDs in deep query objects', function(done) { - var user1 = User.create(); - user1.firstName = 'Billy'; - user1.lastName = 'Bob'; - - var user2 = User.create(); - user2.firstName = 'Jenny'; - user2.lastName = 'Jane'; - - var user3 = User.create(); - user3.firstName = 'Danny'; - user3.lastName = 'David'; - - Promise.all([user1.save(), user2.save(), user3.save()]).then(function() { - validateId(user1); - validateId(user2); - - var id1 = String(user1._id); - var id3 = String(user3._id); - return User.find({ $or: [ {_id: id1 }, {_id: id3 } ] }); - }).then(function(users) { - expect(users).to.have.length(2); - - var u1 = String(users[0]._id) === String(user1._id) ? users[0] : users[1]; - var u3 = String(users[1]._id) === String(user3._id) ? users[1] : users[0]; - - expect(String(u1._id)).to.be.equal(String(user1._id)); - expect(String(u3._id)).to.be.equal(String(user3._id)); - }).then(done, done); - }); + var user2 = User.create(); + user1.name = 'Billy'; + user2.email = 'billy@example.com'; + + Promise.all([user1.save(), user2.save()]).then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error instanceof Error).to.be.true; + }).then(done, done); }); - describe('indexes', function() { - it('should reject documents with duplicate values in unique-indexed fields', function(done) { - class User extends Document { - constructor() { - super(); - - this.schema({ - name: String, - email: { - type: String, - unique: true - } - }); - } - } + it('should accept documents with duplicate values in non-unique-indexed fields', function(done) { + class User extends Document { + constructor() { + super(); - var user1 = User.create(); - user1.name = 'Bill'; - user1.email = 'billy@example.com'; - - var user2 = User.create(); - user1.name = 'Billy'; - user2.email = 'billy@example.com'; - - Promise.all([user1.save(), user2.save()]).then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error instanceof Error).to.be.true; - }).then(done, done); - }); - - it('should accept documents with duplicate values in non-unique-indexed fields', function(done) { - class User extends Document { - constructor() { - super(); - - this.schema({ - name: String, - email: { - type: String, - unique: false - } - }); - } + this.schema({ + name: String, + email: { + type: String, + unique: false } - - var user1 = User.create(); - user1.name = 'Bill'; - user1.email = 'billy@example.com'; - - var user2 = User.create(); - user1.name = 'Billy'; - user2.email = 'billy@example.com'; - - Promise.all([user1.save(), user2.save()]).then(function() { - validateId(user1); - validateId(user2); - expect(user1.email).to.be.equal('billy@example.com'); - expect(user2.email).to.be.equal('billy@example.com'); - }).then(done, done); - }); + }); + } + } + + var user1 = User.create(); + user1.name = 'Bill'; + user1.email = 'billy@example.com'; + + var user2 = User.create(); + user1.name = 'Billy'; + user2.email = 'billy@example.com'; + + Promise.all([user1.save(), user2.save()]).then(function() { + validateId(user1); + validateId(user2); + expect(user1.email).to.be.equal('billy@example.com'); + expect(user2.email).to.be.equal('billy@example.com'); + }).then(done, done); }); + }); }); \ No newline at end of file diff --git a/test/nedbclient.test.js b/test/nedbclient.test.js index d488d7b..081659d 100644 --- a/test/nedbclient.test.js +++ b/test/nedbclient.test.js @@ -1,4 +1,4 @@ -"use strict"; +'use strict'; var _ = require('lodash'); var fs = require('fs'); @@ -9,173 +9,174 @@ var validateId = require('./util').validateId; describe('NeDbClient', function() { - var url = 'nedb://memory'; - var database = null; - - // TODO: This is acting weird. Randomly passes/fails. Seems to - // be caused by document.test.js. When that one doesn't run, - // this one always passes. Maybe some leftover files are still - // floating around due to document.test.js? - before(function(done) { - connect(url).then(function(db) { - database = db; - return database.dropDatabase(); - }).then(function(){ - return done(); - }); + var url = 'nedb://memory'; + var database = null; + + // TODO: This is acting weird. Randomly passes/fails. Seems to + // be caused by document.test.js. When that one doesn't run, + // this one always passes. Maybe some leftover files are still + // floating around due to document.test.js? + before(function(done) { + connect(url).then(function(db) { + database = db; + return database.dropDatabase(); + }).then(function() { + return done(); }); - - beforeEach(function(done) { - done(); - }); - - afterEach(function(done) { - database.dropDatabase().then(function() {}).then(done, done); + }); + + beforeEach(function(done) { + done(); + }); + + afterEach(function(done) { + database.dropDatabase().then(function() { + }).then(done, done); + }); + + after(function(done) { + done(); + }); + + /*describe('#dropDatabase()', function() { + it('should drop the database and delete all its data', function(done) { + + console.log('here-2'); + + var data1 = getData1(); + var data2 = getData2(); + + console.log('here-22'); + + data1.save().then(function(d) { + console.log('here-1'); + validateId(d); + return data2.save(); + }).then(function(d) { + console.log('here00'); + validateId(d); + }).then(function() { + console.log('here0'); + // Validate the client CREATED the necessary file(s) + expect(_.isEmpty(database.driver())).to.not.be.true; + return new Promise(function(resolve, reject) { + console.log('here1'); + fs.readdir(database._path, function(error, files) { + var dbFiles = []; + files.forEach(function(f) { + if (_.endsWith(f, '.db')) dbFiles.push(f); + }); + expect(dbFiles).to.have.length(1); + resolve(); + }); + }); + }).then(function() { + console.log('here2'); + return database.dropDatabase(); + }).then(function() { + console.log('here3'); + // Validate the client DELETED the necessary file(s) + expect(_.isEmpty(database.driver())).to.be.true; + return new Promise(function(resolve, reject) { + console.log('here4'); + fs.readdir(database._path, function(error, files) { + var dbFiles = []; + files.forEach(function(f) { + if (_.endsWith(f, '.db')) dbFiles.push(f); + }); + expect(dbFiles).to.have.length(0); + resolve(); + }); + }); + }).then(done, done); + }); + });*/ + + describe('id', function() { + it('should allow custom _id values', function(done) { + class School extends Document { + constructor() { + super(); + + this.name = String; + } + } + + var school = School.create(); + school._id = '1234567890abcdef'; + school.name = 'South Park Elementary'; + + school.save().then(function() { + validateId(school); + expect(school._id).to.be.equal('1234567890abcdef'); + return School.findOne(); + }).then(function(s) { + validateId(s); + expect(s._id).to.be.equal('1234567890abcdef'); + }).then(done, done); }); - - after(function(done) { - done(); - }); - - /*describe('#dropDatabase()', function() { - it('should drop the database and delete all its data', function(done) { - - console.log('here-2'); - - var data1 = getData1(); - var data2 = getData2(); - - console.log('here-22'); - - data1.save().then(function(d) { - console.log('here-1'); - validateId(d); - return data2.save(); - }).then(function(d) { - console.log('here00'); - validateId(d); - }).then(function() { - console.log('here0'); - // Validate the client CREATED the necessary file(s) - expect(_.isEmpty(database.driver())).to.not.be.true; - return new Promise(function(resolve, reject) { - console.log('here1'); - fs.readdir(database._path, function(error, files) { - var dbFiles = []; - files.forEach(function(f) { - if (_.endsWith(f, '.db')) dbFiles.push(f); - }); - expect(dbFiles).to.have.length(1); - resolve(); - }); - }); - }).then(function() { - console.log('here2'); - return database.dropDatabase(); - }).then(function() { - console.log('here3'); - // Validate the client DELETED the necessary file(s) - expect(_.isEmpty(database.driver())).to.be.true; - return new Promise(function(resolve, reject) { - console.log('here4'); - fs.readdir(database._path, function(error, files) { - var dbFiles = []; - files.forEach(function(f) { - if (_.endsWith(f, '.db')) dbFiles.push(f); - }); - expect(dbFiles).to.have.length(0); - resolve(); - }); - }); - }).then(done, done); - }); - });*/ - - describe('id', function() { - it('should allow custom _id values', function(done) { - class School extends Document { - constructor() { - super(); - - this.name = String; - } + }); + + describe('indexes', function() { + it('should reject documents with duplicate values in unique-indexed fields', function(done) { + class User extends Document { + constructor() { + super(); + + this.schema({ + name: String, + email: { + type: String, + unique: true } - - var school = School.create(); - school._id = '1234567890abcdef'; - school.name = 'South Park Elementary'; - - school.save().then(function() { - validateId(school); - expect(school._id).to.be.equal('1234567890abcdef'); - return School.findOne(); - }).then(function(s) { - validateId(s); - expect(s._id).to.be.equal('1234567890abcdef'); - }).then(done, done); - }); + }); + } + } + + var user1 = User.create(); + user1.name = 'Bill'; + user1.email = 'billy@example.com'; + + var user2 = User.create(); + user1.name = 'Billy'; + user2.email = 'billy@example.com'; + + Promise.all([user1.save(), user2.save()]).then(function() { + expect.fail(null, Error, 'Expected error, but got none.'); + }).catch(function(error) { + expect(error.errorType).to.be.equal('uniqueViolated'); + }).then(done, done); }); - describe('indexes', function() { - it('should reject documents with duplicate values in unique-indexed fields', function(done) { - class User extends Document { - constructor() { - super(); - - this.schema({ - name: String, - email: { - type: String, - unique: true - } - }); - } - } + it('should accept documents with duplicate values in non-unique-indexed fields', function(done) { + class User extends Document { + constructor() { + super(); - var user1 = User.create(); - user1.name = 'Bill'; - user1.email = 'billy@example.com'; - - var user2 = User.create(); - user1.name = 'Billy'; - user2.email = 'billy@example.com'; - - Promise.all([user1.save(), user2.save()]).then(function() { - expect.fail(null, Error, 'Expected error, but got none.'); - }).catch(function(error) { - expect(error.errorType).to.be.equal('uniqueViolated'); - }).then(done, done); - }); - - it('should accept documents with duplicate values in non-unique-indexed fields', function(done) { - class User extends Document { - constructor() { - super(); - - this.schema({ - name: String, - email: { - type: String, - unique: false - } - }); - } + this.schema({ + name: String, + email: { + type: String, + unique: false } - - var user1 = User.create(); - user1.name = 'Bill'; - user1.email = 'billy@example.com'; - - var user2 = User.create(); - user1.name = 'Billy'; - user2.email = 'billy@example.com'; - - Promise.all([user1.save(), user2.save()]).then(function() { - validateId(user1); - validateId(user2); - expect(user1.email).to.be.equal('billy@example.com'); - expect(user2.email).to.be.equal('billy@example.com'); - }).then(done, done); - }); + }); + } + } + + var user1 = User.create(); + user1.name = 'Bill'; + user1.email = 'billy@example.com'; + + var user2 = User.create(); + user1.name = 'Billy'; + user2.email = 'billy@example.com'; + + Promise.all([user1.save(), user2.save()]).then(function() { + validateId(user1); + validateId(user2); + expect(user1.email).to.be.equal('billy@example.com'); + expect(user2.email).to.be.equal('billy@example.com'); + }).then(done, done); }); + }); }); \ No newline at end of file diff --git a/test/util.js b/test/util.js index 6aa207f..35fdf26 100644 --- a/test/util.js +++ b/test/util.js @@ -1,48 +1,50 @@ +'use strict'; + var expect = require('chai').expect; var inherits = require('util').inherits; var Data = require('./data'); exports.validateId = function(obj) { - expect(obj).to.not.be.null; - expect(obj).to.be.a('object'); - expect(obj._id.toString()).to.be.a('string'); - expect(obj._id.toString()).to.have.length.of.at.least(1); + expect(obj).to.not.be.null; + expect(obj).to.be.a('object'); + expect(obj._id.toString()).to.be.a('string'); + expect(obj._id.toString()).to.have.length.of.at.least(1); }; exports.data1 = function() { - var data = Data.create(); - data.number = 1; - data.source = 'arstechnica'; - data.item = 99; - data.values = [33, 101, -1]; - data.date = 1434304033241; - return data; + var data = Data.create(); + data.number = 1; + data.source = 'arstechnica'; + data.item = 99; + data.values = [33, 101, -1]; + data.date = 1434304033241; + return data; }; exports.validateData1 = function(d) { - expect(d.number).to.be.equal(1); - expect(d.source).to.be.equal('arstechnica'); - expect(d.item).to.be.equal(99); - expect(d).to.have.property('values').with.length(3); - expect(d.date.valueOf()).to.be.equal(1434304033241); + expect(d.number).to.be.equal(1); + expect(d.source).to.be.equal('arstechnica'); + expect(d.item).to.be.equal(99); + expect(d).to.have.property('values').with.length(3); + expect(d.date.valueOf()).to.be.equal(1434304033241); }; exports.data2 = function() { - var data = Data.create(); - data.number = 2; - data.source = 'reddit'; - data.item = 26; - data.values = [1, 2, 3, 4]; - data.date = 1434304039234; - return data; + var data = Data.create(); + data.number = 2; + data.source = 'reddit'; + data.item = 26; + data.values = [1, 2, 3, 4]; + data.date = 1434304039234; + return data; }; exports.validateData2 = function(d) { - expect(d.number).to.be.equal(2); - expect(d.source).to.be.equal('reddit'); - expect(d.item).to.be.equal(26); - expect(d).to.have.property('values').with.length(4); - expect(d.date.valueOf()).to.be.equal(1434304039234); + expect(d.number).to.be.equal(2); + expect(d.source).to.be.equal('reddit'); + expect(d.item).to.be.equal(26); + expect(d).to.have.property('values').with.length(4); + expect(d.date.valueOf()).to.be.equal(1434304039234); }; // If we expect an error (and check for it in 'catch'), then @@ -62,13 +64,13 @@ var FailError = function(expected, actual, message) { inherits(FailError, Error); exports.fail = function(expected, actual, message) { - throw new FailError(expected, actual, message); + throw new FailError(expected, actual, message); }; exports.expectError = function(error) { - if (error instanceof FailError) { - expect.fail(error.expected, error.actual, error.message); - return; - } - expect(error instanceof Error).to.be.true; + if (error instanceof FailError) { + expect.fail(error.expected, error.actual, error.message); + return; + } + expect(error instanceof Error).to.be.true; }; \ No newline at end of file diff --git a/test/util.test.js b/test/util.test.js index 379a2e8..626ca86 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -1,35 +1,35 @@ -"use strict"; +'use strict'; var expect = require('chai').expect; var deepTraverse = require('../lib/util').deepTraverse; describe('Util', function() { - describe('deepTraverse()', function() { - it('should iterate over all keys nested in an object', function(done) { - var object = { 'a': [{ 'b': { 'c': 3 } }] }; + describe('deepTraverse()', function() { + it('should iterate over all keys nested in an object', function(done) { + var object = {'a': [{'b': {'c': 3}}]}; - var keysSeen = []; - var valsSeen = []; - var parentsSeen = []; + var keysSeen = []; + var valsSeen = []; + var parentsSeen = []; - deepTraverse(object, function(key, value, parent) { - keysSeen.push(key); - valsSeen.push(value); - parentsSeen.push(parent); - }); + deepTraverse(object, function(key, value, parent) { + keysSeen.push(key); + valsSeen.push(value); + parentsSeen.push(parent); + }); - expect(keysSeen).to.have.length(4); - expect(keysSeen).to.include('a'); - expect(keysSeen).to.include('0'); - expect(keysSeen).to.include('b'); - expect(keysSeen).to.include('c'); - expect(valsSeen).to.have.length(4); - expect(parentsSeen).to.have.length(4); - expect(keysSeen[0]).to.be.equal('a'); - expect(parentsSeen[1]).to.be.equal(object.a); + expect(keysSeen).to.have.length(4); + expect(keysSeen).to.include('a'); + expect(keysSeen).to.include('0'); + expect(keysSeen).to.include('b'); + expect(keysSeen).to.include('c'); + expect(valsSeen).to.have.length(4); + expect(parentsSeen).to.have.length(4); + expect(keysSeen[0]).to.be.equal('a'); + expect(parentsSeen[1]).to.be.equal(object.a); - done(); - }); + done(); }); + }); }); \ No newline at end of file