├── .gitignore ├── .npmignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── Todo.md ├── examples └── blog │ └── models.js ├── index.js ├── lib ├── index.js ├── relationships.js └── utils.js ├── package.json └── specs ├── belongsTo.spec.js ├── hasAndBelongsToMany.spec.js ├── hasMany.spec.js ├── hasOne.spec.js ├── mongoose_array.spec.js ├── spec_helper.js └── support ├── categoryModel.js ├── postModel.js ├── tagModel.js ├── tweetModel.js └── userModel.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **.swp 2 | node_modules -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | - "0.8" 5 | services: 6 | - mongodb 7 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | 0.5.3 / 2014-05-03 2 | ================== 3 | 4 | * Migrated tests to Mocha. Thanks to @jonstorer 5 | * Added TravisCI 6 | 7 | 0.5.2 / 2014-04-18 8 | ================== 9 | 10 | * Lint code for readability 11 | 12 | 0.5.1 / 2014-04-18 13 | ================== 14 | 15 | * Edit README 16 | 17 | 0.5.0 / 2014-04-18 18 | ================== 19 | 20 | * Fix JS tests. Thanks to [jonstorer](https://github.com/jonstorer) #5 21 | * Fix TypeError on Query. Thanks to [Tolsi](https://github.com/Tolsi) #3 22 | 23 | 0.4.0 / 2011-09-01 24 | ================== 25 | 26 | * Added; `populate` method 27 | * Added: `build` method 28 | * Added: `append` method 29 | * Added: `concat` method 30 | 31 | 0.3.0 / 2011-08-31 32 | ================== 33 | 34 | * Added; `find` method to fetch children documents 35 | * Changed; tests from vows to expresso/should 36 | 37 | 0.2.0 / 2011-08-31 38 | ================== 39 | 40 | * Added; support for "has and belongs to many" relationships 41 | * Added; failing tests for "one to one" relationships 42 | * Fixed; a few tests that didn't make sense, some of the lexicon too 43 | 44 | 0.1.0 / 2011-08-30 45 | ================== 46 | 47 | * Added; support for "one to many" relationships 48 | * Added; `create` method for child documents 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mongoose Relationships [![Build Status](https://travis-ci.org/JamesS237/mongo-relation.svg?branch=master)](https://travis-ci.org/JamesS237/mongo-relation) 2 | ====================== 3 | _... because sometimes embedded documents aren't enough._ 4 | 5 | A plugin for [Mongoose](http://github.com/learnboost/mongoose) adding a simple syntax for model relationships and providing useful helpers to empower them. 6 | 7 | This is an early release with limited functionalities. I'm looking for feedback on the API and features (been exploring a few different solutions, nothing's impossible!). 8 | 9 | I'm inspiring from various libraries in Ruby I've used throughout the years. Might not be your cup of tea. 10 | 11 | Goals 12 | ----- 13 | 14 | * Be unobtrusive and compliant with the ways of Mongoose (coding style, testing, API). 15 | 16 | Usage 17 | ===== 18 | 19 | First, `npm install mongo-relation`. 20 | 21 | Add relationships to your schema through either `hasMany`, `belongsTo` or `habtm` (has and belongs to many). 22 | 23 | * {String} `ModelName` is the name of the Model. 24 | * {Object} `options` 25 | * {String} `through` if you want to specify what path to use for the relationship. Else the path will be created for you by pluralizing the `ModelName`. 26 | * {String} `dependent` takes either "delete" or "nullify" and indicated what to do when the element is removed from the parent's `through` array. 27 | 28 | ```javascript 29 | var mongoose = require('mongoose'); 30 | require('mongo-relation'); 31 | 32 | YourSchema.hasMany('ModelName', {through: 'PathName', dependent: 'delete|nullify'}); 33 | ``` 34 | 35 | It's good to take note that for "has and belongs to many" type relationships, the dependent option only deletes the reference, not the actual referenced document. 36 | 37 | Examples 38 | -------- 39 | 40 | One to Many 41 | ----------- 42 | 43 | ```javascript 44 | UserSchema.hasMany('Post', {dependent: 'delete'}); 45 | 46 | // uses the 'author' path for the relation 47 | PostSchema.belongsTo('User', {through: 'author'}); 48 | ``` 49 | 50 | Has and Belongs to Many 51 | ----------------------- 52 | 53 | ```javascript 54 | PostSchema.habtm('Category'); 55 | CategorySchema.habtm('Post'); 56 | ``` 57 | 58 | Methods 59 | ======= 60 | 61 | Every `Document` that has their `Schema` plugged with `mongo-relation` has access to the following methods. 62 | 63 | __Let's use this starting point:__ 64 | 65 | ```javascript 66 | var mongoose = require('mongoose'); 67 | require('mongo-relation'); 68 | 69 | // UserSchema stores an Array of ObjectIds for posts 70 | var UserSchema = new mongoose.Schema({ 71 | posts: [mongoose.Schema.ObjectId] 72 | }); 73 | 74 | // PostSchema stores an ObjectId for the author 75 | var PostSchema = new mongoose.Schema({ 76 | title : String 77 | , author : mongoose.Schema.ObjectId 78 | }); 79 | 80 | // Attach the plugin 81 | UserSchema.hasMany('Post'); 82 | PostSchema.belongsTo('User', {through: 'author'}); 83 | 84 | var User = mongoose.model('User', UserSchema) 85 | , Post = mongoose.model('Post', PostSchema); 86 | ``` 87 | 88 | create 89 | ------ 90 | 91 | Takes care of creating the child document and the links between it and the parent document. 92 | 93 | * {Object|Array} `objs` representation of the child document(s) to create 94 | * {Function} `callback` (optional) function returning an error if any, the new parent document and the created post(s) 95 | 96 | __Example:__ 97 | 98 | ```javascript 99 | var user = new User(); 100 | 101 | user.posts.create({ 102 | title: "Mongoose, now with added love through relationships!" 103 | }, function(err, user, post){ 104 | // user.posts.length === 1 105 | // post.title === "Mongoose, now with added love through relationships!" 106 | }); 107 | 108 | // Using an `Array` 109 | user.posts.create([ 110 | { title: "Not too imaginative post title" } 111 | , { title: "... a tad more imaginative post title" } 112 | ], function(err, user, posts){ 113 | // user.posts.length === 3 114 | // posts.length == 2 115 | // posts[0] instanceof Post 116 | }); 117 | ``` 118 | 119 | build 120 | ----- 121 | 122 | Instantiates a child document, appends its reference to the parent document and returns the child document. _Does not save anything._ 123 | 124 | * {Object} `obj` representation of the child document(s) to create 125 | 126 | __Example:__ 127 | 128 | ```javascript 129 | var post = user.posts.build({title: "Just instantiating me"}); 130 | // post.author === user._id 131 | ``` 132 | 133 | append 134 | ------ 135 | 136 | Allows pushing of an already existing document into the parent document. Creates all the right references. 137 | 138 | Works with either a saved or unsaved document. 139 | 140 | The parent document is not saved, you'll have to do that yourself. 141 | 142 | * {Document} `child` document to push. 143 | * {Function} `callback` called with an error if any and the child document w/ references. 144 | 145 | __Example:__ 146 | 147 | ```javascript 148 | var post = new Post(); 149 | 150 | user.posts.append(post, function(err, post){ 151 | // post.author === user._id 152 | // user.posts.id(post._id) === post._id 153 | }); 154 | ``` 155 | 156 | concat 157 | ------ 158 | 159 | Just like `Array.prototype.concat`, it appends an `Array` to another `Array` 160 | 161 | * {Document} `child` document to push. 162 | * {Function} `callback` called with an error if any and the child document w/ references. 163 | 164 | __Example:__ 165 | 166 | ```javascript 167 | var posts = [new Post(), new Post()]; 168 | 169 | user.posts.concat(posts, function(err, posts){ 170 | // `posts` is an `Array` of `Document` 171 | // each have its author set to `user._id` 172 | }); 173 | ``` 174 | 175 | find 176 | ---- 177 | 178 | It's the same as a `Mongoose.Query`. Only looks through the children documents. 179 | 180 | See [Mongoose.Query](http://mongoosejs.com/docs/finding-documents.html) for the params 181 | 182 | __Example:__ 183 | 184 | ```javascript 185 | user.posts.find({title: "Not too imaginative post title"}, function(err, posts){ 186 | // posts.length === 1 187 | // posts[0].author == user._id 188 | // posts[0].title == "Not too imaginative post title"; 189 | }); 190 | ``` 191 | 192 | populate 193 | -------- 194 | 195 | Some sugary syntax to populate the parent document's child documents. 196 | 197 | * {Array} `fields` (optional) you want to get back with each child document 198 | * {Function} `callback` called with an error and the populate document 199 | 200 | __Example:__ 201 | 202 | ```javascript 203 | user.posts.populate(function(err, user){ 204 | // user.posts.length === 2 205 | }); 206 | ``` 207 | 208 | remove 209 | ------ 210 | 211 | Depending on the `dependent` option, it'll either delete or nullify the 212 | 213 | * {ObjectId} `id` of the document to remove 214 | * {Function} `callback` (optional) called after the deed is done with an error if any and the new parent document. 215 | 216 | __Example:__ 217 | 218 | ```javascript 219 | user.posts.remove(user.posts[0]._id, function(err, user){ 220 | // The post will either be delete or have its `author` field nullified 221 | }); 222 | ``` 223 | 224 | Testing 225 | ======= 226 | 227 | Mongo-Relation uses [Mocha](http://github.com/visionmedia/mocha) with [Should](http://github.com/visionmedia/should.js). Tests are located in `./test` and should be ran with the `make test` command. 228 | 229 | Contribute 230 | ========== 231 | 232 | * Pick up any of the items above & send a pull request (w/ __passing__ tests please) 233 | * Discuss the API / features in the [Issues](http://github.com/JamesS237/mongo-relation/issues) 234 | * Use it and report bugs in the [Issues](http://github.com/JamesS237/mongo-relation/issues) (w/ __failing__ tests please) 235 | -------------------------------------------------------------------------------- /Todo.md: -------------------------------------------------------------------------------- 1 | HasMany should not set the id on the parent document 2 | 3 | User.hasMany('Tweet'); 4 | user = new User(); 5 | tweet = user.tweets.build(); 6 | user.tweets.should.eql([]); 7 | tweet.user.should.eql user._id; 8 | -------------------------------------------------------------------------------- /examples/blog/models.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose') 2 | , Schema = mongoose.Schema 3 | , ObjectId = Schema.ObjectId 4 | , relationships = require('../../index'); // require('mongoose-relationships'); 5 | 6 | /** 7 | * Blog Post Schema 8 | * "belongs to one author" 9 | */ 10 | var PostSchema = new Schema({ 11 | title : String 12 | , body : String 13 | , author : {type: ObjectId, ref: 'User'} 14 | }); 15 | 16 | /** 17 | * User Schema 18 | * "has many posts" 19 | */ 20 | var UserSchema = new Schema({ 21 | name : String 22 | , posts : [{type: ObjectId, ref: 'Post'}] 23 | }); 24 | 25 | /** 26 | * Attach the plugin to the schemas 27 | */ 28 | PostSchema.plugin(relationships, { 29 | belongsTo : "User" 30 | , through : "author" 31 | }); 32 | UserSchema.plugin(relationships, { 33 | hasMany : "Post" 34 | , through : "posts" 35 | }); 36 | 37 | /** 38 | * Register the models with Mongoose 39 | */ 40 | var Post = mongoose.model('Post', PostSchema) 41 | , User = mongoose.model('User', UserSchema); 42 | 43 | // Have fun here: 44 | var user = new User(); 45 | 46 | user.posts.create({ 47 | title: "Mongoose, now with added love through relationships!" 48 | }, function(err, user, post){ 49 | // user.posts.length === 1 50 | // post.title === "Mongoose, now with added love through relationships!" 51 | }); 52 | 53 | // Using an `Array` 54 | user.posts.create([ 55 | { title: "Not too imaginative post title" } 56 | , { title: "... a tad more imaginative post title" } 57 | ], function(err, user, posts){ 58 | // user.posts.length === 3 59 | // posts.length == 2 60 | // posts[0] instanceof Post 61 | }); 62 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/'); -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./relationships'); -------------------------------------------------------------------------------- /lib/relationships.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), Schema = mongoose.Schema, ObjectId = Schema.ObjectId, MongooseArray = mongoose.Types.Array, utils = require('./utils'), merge = utils.merge, pluralize = utils.pluralize; 2 | /** 3 | * Adds the relationship to the Schema's path 4 | * and creates the path if necessary 5 | * 6 | * @param {Object} relationship 7 | * @return {Schema} 8 | * @api private 9 | */ 10 | Schema.prototype._addRelationship = function (type, model, options) { 11 | if (!model) 12 | throw new Error('Model name needed'); 13 | var array = type === 'hasMany' || type === 'habtm', pathName, cast = array ? [{ 14 | type: ObjectId, 15 | index: true, 16 | ref: model 17 | }] : { 18 | type: ObjectId, 19 | index: true, 20 | ref: model 21 | }; 22 | if (options && options.through) { 23 | pathName = options.through; 24 | } else { 25 | pathName = array ? pluralize(model.toLowerCase()) : model.toLowerCase(); 26 | } 27 | if (!this.paths[pathName]) { 28 | var path = {}; 29 | path[pathName] = cast; 30 | this.add(path); 31 | } 32 | this.paths[pathName].options[type] = model; 33 | if (options && options.dependent) 34 | this.paths[pathName].options.dependent = options.dependent; 35 | return this; 36 | }; 37 | /** 38 | * Syntactic sugar to create the relationships 39 | * 40 | * @param {String} model [name of the model in the DB] 41 | * @param {Object} options [through, dependent] 42 | * @return {Schema} 43 | * @api public 44 | */ 45 | Schema.prototype.belongsTo = function (model, options) { 46 | this._addRelationship.call(this, 'belongsTo', model, options); 47 | }; 48 | Schema.prototype.hasOne = function (model, options) { 49 | this._addRelationship.call(this, 'hasOne', model, options); 50 | }; 51 | Schema.prototype.hasMany = function (model, options) { 52 | this._addRelationship.call(this, 'hasMany', model, options); 53 | }; 54 | Schema.prototype.habtm = function (model, options) { 55 | this._addRelationship.call(this, 'habtm', model, options); 56 | }; 57 | /** 58 | * Finds the path referencing supplied model name 59 | * 60 | * @param {String} modelName 61 | * @param {String} type (optional) 62 | * @return {Object} 63 | * @param {String} type 64 | * @param {String} name 65 | * @api private 66 | */ 67 | Schema.prototype._findPathReferencing = function (modelName, type) { 68 | for (var path in this.paths) { 69 | var options = this.paths[path].options; 70 | if (type) { 71 | if (options[type] && options[type] === modelName) { 72 | return { 73 | type: type, 74 | name: path 75 | }; 76 | break; 77 | } 78 | } else if (options.belongsTo === modelName || options.habtm === modelName) { 79 | var type = Array.isArray(options.type) ? 'habtm' : 'belongsTo'; 80 | return { 81 | type: type, 82 | name: path 83 | }; 84 | break; 85 | } 86 | } 87 | }; 88 | /** 89 | * Check for presence of relationship 90 | */ 91 | MongooseArray.prototype._hasRelationship = function () { 92 | return this._schema && (this._schema.options.hasMany || this._schema.options.habtm || this._schema.options.hasOne); 93 | }; 94 | /** 95 | * Figure out the relationship 96 | * 97 | * @return {Object} 98 | * @param {String} type 99 | * @param {String} ref 100 | * @param {Object} options 101 | * @api private 102 | */ 103 | MongooseArray.prototype._getRelationship = function () { 104 | var schemaOpts = this._schema.options, type, ref, options = {}; 105 | if (schemaOpts.hasMany) { 106 | type = 'hasMany'; 107 | ref = schemaOpts.hasMany; 108 | } 109 | if (schemaOpts.hasOne) { 110 | type = 'hasOne'; 111 | ref = schemaOpts.hasOne; 112 | } 113 | if (schemaOpts.habtm) { 114 | type = 'habtm'; 115 | ref = schemaOpts.habtm; 116 | } 117 | if (schemaOpts.dependent) 118 | options.dependent = schemaOpts.dependent; 119 | return { 120 | type: type, 121 | ref: ref, 122 | options: options 123 | }; 124 | }; 125 | /** 126 | * Builds the instance of the child element 127 | * 128 | * @param {Object|Array} objs 129 | * @return {Document|Array} 130 | * @api public 131 | */ 132 | MongooseArray.prototype.build = function (objs) { 133 | if (!this._hasRelationship()) 134 | throw new Error('Path doesn\'t contain a reference'); 135 | var self = this, parent = this._parent, childModelName = this._schema.options.hasMany || this._schema.options.habtm, childModel = parent.model(childModelName), childSchema = childModel.schema, parentModelName = parent.constructor.modelName, childPath = childSchema._findPathReferencing(parentModelName); 136 | var build = function (obj) { 137 | obj = new childModel(obj); 138 | // HABTM or belongsTo? 139 | if (childPath.type === 'habtm') 140 | obj[childPath.name].push(parent); 141 | else 142 | obj[childPath.name] = parent; 143 | parent[self._path].push(obj); 144 | return obj; 145 | }; 146 | if (Array.isArray(objs)) { 147 | return objs.map(function (obj) { 148 | return build(obj); 149 | }); 150 | } 151 | return build(objs); 152 | }; 153 | /** 154 | * Create a child document and add it to the parent `Array` 155 | * 156 | * @param {Object|Array} objs [object(s) to create] 157 | * @param {Functions} callback [passed: (err, parent, created children)] 158 | * @api public 159 | */ 160 | MongooseArray.prototype.create = function (objs, callback) { 161 | if (!this._hasRelationship()) 162 | return callback(new Error('Path doesn\'t contain a reference')); 163 | var self = this, parent = this._parent, childModelName = this._schema.options.hasMany || this._schema.options.habtm, childModel = parent.model(childModelName), childSchema = childModel.schema, parentModelName = parent.constructor.modelName, childPath = childSchema._findPathReferencing(parentModelName); 164 | // You *need* a reference in the child `Document` 165 | if (!childPath) 166 | throw new Error('Parent model not referenced anywhere in the Child Schema'); 167 | // If we're provided an `Array`, we need to iterate 168 | objs = this.build(objs); 169 | if (Array.isArray(objs)) { 170 | var created = [], total = objs.length; 171 | objs.forEach(function (obj, i) { 172 | obj.save(function (err, obj) { 173 | if (err) { 174 | // Empty the array and return the error, 175 | // effectively breaking the loop. 176 | objs.splice(i, objs.length - i); 177 | return callback(err); 178 | } 179 | // Store the created records; 180 | created.push(obj); 181 | --total || parent.save(function (err, parent) { 182 | if (err) 183 | return callback(err); 184 | return callback(null, parent, created); 185 | }); 186 | }); 187 | }); 188 | } else { 189 | // Only one object needs creation. 190 | // Going for it then! 191 | objs.save(function (err, obj) { 192 | if (err) 193 | return callback(err); 194 | parent.save(function (err, parent) { 195 | if (err) 196 | return callback(err); 197 | return callback(null, parent, obj); 198 | }); 199 | }); 200 | } 201 | }; 202 | /** 203 | * Find children documents 204 | * 205 | * *This is a copy of Model.find w/ added error throwing and such* 206 | */ 207 | MongooseArray.prototype.find = function (conditions, fields, options, callback) { 208 | if (!this._hasRelationship()) 209 | return callback(new Error('Path doesn\'t contain a reference')); 210 | // Copied from `Model.find` 211 | if ('function' == typeof conditions) { 212 | callback = conditions; 213 | conditions = {}; 214 | fields = null; 215 | options = null; 216 | } else if ('function' == typeof fields) { 217 | callback = fields; 218 | fields = null; 219 | options = null; 220 | } else if ('function' == typeof options) { 221 | callback = options; 222 | options = null; 223 | } else { 224 | conditions = {}; 225 | } 226 | var parent = this._parent, childModel = parent.model(this._schema.options.hasMany || this._schema.options.habtm), childPath = childModel.schema._findPathReferencing(parent.constructor.modelName); 227 | // You *need* a reference in the child `Document` 228 | if (!childPath) 229 | throw new Error('Parent model not referenced anywhere in the Child Schema'); 230 | var safeConditions = {}; 231 | safeConditions[childPath.name] = parent._id; 232 | merge(safeConditions, conditions); 233 | merge(safeConditions, { _id: { $in: parent[this._path] } }); 234 | //var query = new mongoose.Query(safeConditions, options).select(fields).bind(childModel, 'find'); 235 | var query = childModel.find(safeConditions, options).select(fields); 236 | if ('undefined' === typeof callback) 237 | return query; 238 | return query.find(callback); 239 | }; 240 | /** 241 | * Syntactic sugar to populate the array 242 | * 243 | * @param {Array} fields 244 | * @param {Function} callback 245 | * @return {Query} 246 | * @api public 247 | */ 248 | MongooseArray.prototype.populate = function (fields, callback) { 249 | if ('function' == typeof fields) { 250 | callback = fields; 251 | fields = null; 252 | } 253 | var parent = this._parent, model = parent.constructor, path = this._path, self = this; 254 | return model.findById(parent._id).populate(path, fields).exec(callback); 255 | }; 256 | /** 257 | * Overrides MongooseArray.remove 258 | * only for dependent:destroy relationships 259 | * 260 | * @param {ObjectId} id 261 | * @param {Function} callback 262 | * @return {ObjectId} 263 | * @api public 264 | */ 265 | var oldRemove = MongooseArray.prototype.remove; 266 | MongooseArray.prototype.remove = MongooseArray.prototype.delete = function (id, callback) { 267 | var args = arguments, relationship = this._getRelationship(); 268 | if (id._id) { 269 | var child = id; 270 | id = child._id; 271 | } 272 | if (arguments[1] && typeof arguments[1] === 'function') 273 | oldRemove.call(this, id); 274 | else { 275 | oldRemove.apply(this, arguments); 276 | } 277 | if (!callback || typeof arguments[1] !== 'function') 278 | callback = function (err) { 279 | if (err) 280 | throw err; 281 | }; 282 | var self = this, parent = this._parent, childModel = parent.model(relationship.ref); 283 | if (relationship.options.dependent) { 284 | if (relationship.type === 'habtm') { 285 | if (relationship.options.dependent === 'delete' || relationship.options.dependent === 'nullify') { 286 | if (child) { 287 | var childPath = child._findPathReferencing(parent.constructor.modelName, 'habtm'); 288 | child[childPath.name].remove(parent._id); 289 | child.save(function (err, child) { 290 | if (err) 291 | callback(err); 292 | callback(null, parent); 293 | }); 294 | } else { 295 | childModel.findById(id, function (err, child) { 296 | if (err) 297 | return callback(err); 298 | var childPath = child.schema._findPathReferencing(parent.constructor.modelName, 'habtm'); 299 | child[childPath.name].remove(parent._id); 300 | child.save(function (err, child) { 301 | if (err) 302 | callback(err); 303 | callback(null, parent); 304 | }); 305 | }); 306 | } 307 | } 308 | } else { 309 | if (relationship.options.dependent === 'delete') { 310 | childModel.remove({ _id: id }, function (err) { 311 | if (err) 312 | return callback(err); 313 | parent.save(callback); 314 | }); 315 | } else if (relationship.options.dependent === 'nullify') { 316 | if (child) { 317 | var childPath = child._findPathReferencing(parent.constructor.modelName); 318 | child.set(childPath.name, null); 319 | child.save(function (err, child) { 320 | if (err) 321 | callback(err); 322 | callback(null, parent); 323 | }); 324 | } else { 325 | childModel.findById(id, function (err, child) { 326 | if (err) 327 | return callback(err); 328 | var childPath = child.schema._findPathReferencing(parent.constructor.modelName); 329 | child.set(childPath.name, null); 330 | child.save(function (err, child) { 331 | if (err) 332 | callback(err); 333 | callback(null, parent); 334 | }); 335 | }); 336 | } 337 | } 338 | } 339 | } else { 340 | callback(null, parent); 341 | } 342 | }; 343 | /** 344 | * Append an already instantiated document 345 | * saves it in the process. 346 | * 347 | * @param {Document} child 348 | * @param {Function} callback 349 | * @api public 350 | */ 351 | MongooseArray.prototype.append = function (child, callback) { 352 | var throwErr = function (message) { 353 | if (callback) 354 | return callback(new Error(message)); 355 | else 356 | throw new Error(message); 357 | }; 358 | if (!this._hasRelationship()) 359 | return throwErr('Path doesn\'t contain a reference'); 360 | var relationship = this._getRelationship(); 361 | if (child.constructor.modelName !== relationship.ref) 362 | return throwErr('Wrong Model type'); 363 | var childPath = child.schema._findPathReferencing(this._parent.constructor.modelName); 364 | if (childPath.type === 'habtm') 365 | child[childPath.name].push(this._parent._id); 366 | else 367 | child[childPath.name] = this._parent._id; 368 | this._parent[this._path].push(child._id); 369 | if (!callback) 370 | return child; 371 | return child.save(callback); 372 | }; 373 | /** 374 | * Append many instantiated children documents 375 | * 376 | * @param {Array} children 377 | * @param {Function} callback 378 | * @api public 379 | */ 380 | var oldConcat = MongooseArray.prototype.concat; 381 | MongooseArray.prototype.concat = function (children, callback) { 382 | if (!Array.isArray(children)) 383 | return callback(new Error('First argument needs to be an Array')); 384 | var self = this, children = children.map(function (child) { 385 | return self.append(child); 386 | }), childrenIds = children.map(function (child) { 387 | return child._id; 388 | }); 389 | var total = children.length; 390 | children.forEach(function (child) { 391 | child.save(function (err, child) { 392 | if (err) { 393 | // Empty the array and return the error, 394 | // effectively breaking the loop. 395 | objs.splice(i, objs.length - i); 396 | return callback(err); 397 | } 398 | --total || function () { 399 | oldConcat.call(self, childrenIds); 400 | self._markModified(); 401 | callback(null, children); 402 | }(); 403 | }); 404 | }); 405 | }; 406 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Merges `from` into `to` without overwriting 3 | * existing properties of `to`. 4 | * 5 | * @param {Object} to 6 | * @param {Object} from 7 | */ 8 | exports.merge = function merge(to, from) { 9 | var keys = Object.keys(from), i = keys.length, key; 10 | while (i--) { 11 | key = keys[i]; 12 | if ('undefined' === typeof to[key]) { 13 | to[key] = from[key]; 14 | } else { 15 | merge(to[key], from[key]); 16 | } 17 | } 18 | }; 19 | /** 20 | * Generates a random string 21 | * 22 | * @api private 23 | */ 24 | exports.random = function () { 25 | return Math.random().toString().substr(3); 26 | }; 27 | /** 28 | * Pluralization rules. 29 | */ 30 | var rules = [ 31 | [ 32 | /(m)an$/gi, 33 | '$1en' 34 | ], 35 | [ 36 | /(pe)rson$/gi, 37 | '$1ople' 38 | ], 39 | [ 40 | /(child)$/gi, 41 | '$1ren' 42 | ], 43 | [ 44 | /^(ox)$/gi, 45 | '$1en' 46 | ], 47 | [ 48 | /(ax|test)is$/gi, 49 | '$1es' 50 | ], 51 | [ 52 | /(octop|vir)us$/gi, 53 | '$1i' 54 | ], 55 | [ 56 | /(alias|status)$/gi, 57 | '$1es' 58 | ], 59 | [ 60 | /(bu)s$/gi, 61 | '$1ses' 62 | ], 63 | [ 64 | /(buffal|tomat|potat)o$/gi, 65 | '$1oes' 66 | ], 67 | [ 68 | /([ti])um$/gi, 69 | '$1a' 70 | ], 71 | [ 72 | /sis$/gi, 73 | 'ses' 74 | ], 75 | [ 76 | /(?:([^f])fe|([lr])f)$/gi, 77 | '$1$2ves' 78 | ], 79 | [ 80 | /(hive)$/gi, 81 | '$1s' 82 | ], 83 | [ 84 | /([^aeiouy]|qu)y$/gi, 85 | '$1ies' 86 | ], 87 | [ 88 | /(x|ch|ss|sh)$/gi, 89 | '$1es' 90 | ], 91 | [ 92 | /(matr|vert|ind)ix|ex$/gi, 93 | '$1ices' 94 | ], 95 | [ 96 | /([m|l])ouse$/gi, 97 | '$1ice' 98 | ], 99 | [ 100 | /(quiz)$/gi, 101 | '$1zes' 102 | ], 103 | [ 104 | /s$/gi, 105 | 's' 106 | ], 107 | [ 108 | /$/gi, 109 | 's' 110 | ] 111 | ]; 112 | /** 113 | * Uncountable words. 114 | */ 115 | var uncountables = [ 116 | 'advice', 117 | 'energy', 118 | 'excretion', 119 | 'digestion', 120 | 'cooperation', 121 | 'health', 122 | 'justice', 123 | 'labour', 124 | 'machinery', 125 | 'equipment', 126 | 'information', 127 | 'pollution', 128 | 'sewage', 129 | 'paper', 130 | 'money', 131 | 'species', 132 | 'series', 133 | 'rain', 134 | 'rice', 135 | 'fish', 136 | 'sheep', 137 | 'moose', 138 | 'deer', 139 | 'news' 140 | ]; 141 | /** 142 | * Pluralize function. 143 | * 144 | * @author TJ Holowaychuk (extracted from _ext.js_) 145 | * @param {String} string to pluralize 146 | * @api private 147 | */ 148 | exports.pluralize = function (str) { 149 | var rule, found; 150 | if (!~uncountables.indexOf(str.toLowerCase())) { 151 | found = rules.filter(function (rule) { 152 | return str.match(rule[0]); 153 | }); 154 | if (found[0]) 155 | return str.replace(found[0][0], found[0][1]); 156 | } 157 | return str; 158 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mongo-relation", 3 | "description": "Model relationships plugin for Mongoose", 4 | "version": "0.5.4", 5 | "homepage": "https://github.com/JamesS237/mongo-relation/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/JamesS237/mongo-relation.git" 9 | }, 10 | "keywords": [ 11 | "mongodb", 12 | "mongoose", 13 | "orm", 14 | "relations", 15 | "relationships" 16 | ], 17 | "author": { 18 | "name": "Jerome Gravel-Niquet", 19 | "email": "jeromegn@gmail.com", 20 | "url": "http://jgn.me/" 21 | }, 22 | "scripts": { 23 | "test": "./node_modules/.bin/mocha -R spec specs/*.spec.js", 24 | "nyan": "./node_modules/.bin/mocha -R nyan specs/*.spec.js" 25 | }, 26 | "main": "./index.js", 27 | "engines": { 28 | "node": ">= 0.2.0" 29 | }, 30 | "dependencies": { 31 | "mongoose": ">=3.0.0" 32 | }, 33 | "devDependencies": { 34 | "mocha": "latest", 35 | "should": "latest" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /specs/belongsTo.spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | var mongoose = require('mongoose'), 4 | should = require('should'), 5 | User = require('./support/userModel'), 6 | Tweet = require('./support/tweetModel'), 7 | Tag = require('./support/tagModel'); 8 | 9 | describe('belongsTo', function() { 10 | it('child schema belongsTo path', function() { 11 | Tweet.schema.paths.author.options.belongsTo.should.equal('User'); 12 | }); 13 | 14 | it('sets the standard mongoose refs', function() { 15 | Tweet.schema.paths.author.instance.should.equal('ObjectID'); 16 | Tweet.schema.paths.author.options.ref.should.equal('User'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /specs/hasAndBelongsToMany.spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | var mongoose = require('mongoose'), 4 | should = require('should'), 5 | User = require('./support/userModel'), 6 | Post = require('./support/postModel'), 7 | Category = require('./support/categoryModel'); 8 | 9 | describe('hasManyBelongsToMany', function() { 10 | 11 | it('has hasMany on the path', function() { 12 | Category.schema.paths['posts'].options.habtm.should.equal('Post'); 13 | }); 14 | 15 | it('test child schema habtm path', function() { 16 | Post.schema.paths['categories'].options.habtm.should.equal('Category'); 17 | }); 18 | 19 | it('test presence of added methods to the MongooseArray', function() { 20 | var category = new Category(), 21 | post = new Post(); 22 | 23 | category.posts.create.should.be.a.Function; 24 | post.categories.create.should.be.a.Function; 25 | 26 | category.posts.find.should.be.a.Function; 27 | post.categories.find.should.be.a.Function; 28 | 29 | category.posts.populate.should.be.a.Function; 30 | post.categories.populate.should.be.a.Function; 31 | 32 | category.posts.remove.should.be.a.Function; 33 | post.categories.remove.should.be.a.Function; 34 | 35 | category.posts.append.should.be.a.Function; 36 | post.categories.append.should.be.a.Function; 37 | 38 | category.posts.concat.should.be.a.Function; 39 | post.categories.concat.should.be.a.Function; 40 | }); 41 | 42 | it('instantiates one child document', function(){ 43 | var category = new Category(), 44 | post = { title: 'Easy relationships with mongoose-relationships' }; 45 | 46 | var built = category.posts.build(post); 47 | 48 | built.should.be.an.instanceof(Post); 49 | built.categories.should.containEql(category._id); 50 | category.posts.should.containEql(built._id); 51 | 52 | category.posts.should.have.length(1); 53 | }); 54 | 55 | it('instantiates many children documents', function(done) { 56 | var category = new Category(), 57 | posts = [{}, {}]; 58 | 59 | var built = category.posts.build(posts); 60 | 61 | category.posts.should.have.length(2); 62 | 63 | var count = category.posts.length; 64 | built.forEach(function(post){ 65 | post.should.be.an.instanceof(Post); 66 | post.categories.should.containEql(category._id); 67 | category.posts.should.containEql(post._id); 68 | --count || done(); 69 | }); 70 | }); 71 | 72 | it('appends an instantiated child document', function(done) { 73 | var category = new Category(), 74 | post = new Post(); 75 | 76 | category.posts.append(post, function(err, post){ 77 | should.strictEqual(err, null); 78 | 79 | post.categories.should.containEql(category._id); 80 | category.posts.should.containEql(post._id); 81 | 82 | done(); 83 | }); 84 | }); 85 | 86 | it('concates many instantiated child documents', function(done) { 87 | var category = new Category(), 88 | posts = [new Post(), new Post()]; 89 | 90 | category.posts.concat(posts, function(err, posts){ 91 | should.strictEqual(err, null); 92 | 93 | var count = posts.length; 94 | posts.forEach(function(post){ 95 | post.categories.should.containEql(category._id); 96 | category.posts.should.containEql(post._id); 97 | --count || done(); 98 | }); 99 | }); 100 | }); 101 | 102 | it('creates one child document', function(done) { 103 | var category = new Category(), 104 | post = { title: 'Easy relationships with mongoose-relationships' }; 105 | 106 | category.posts.create(post, function(err, category, post){ 107 | should.strictEqual(err, null); 108 | 109 | category.should.be.an.instanceof(Category); 110 | category.posts.should.have.length(1); 111 | 112 | category.posts[0].should.equal(post._id); 113 | 114 | post.should.be.an.instanceof(Post); 115 | post.title.should.equal('Easy relationships with mongoose-relationships') 116 | post.categories.should.containEql(category._id); 117 | 118 | done(); 119 | }); 120 | }); 121 | 122 | it('creates many child documents', function(done){ 123 | var category = new Category(); 124 | posts = [ { title: 'Blog post #1' }, 125 | { title: 'Blog post #2' } ] 126 | 127 | category.posts.create(posts, function(err, category, posts){ 128 | should.strictEqual(err, null); 129 | 130 | category.posts.should.have.length(2); 131 | 132 | posts.should.have.length(2); 133 | 134 | var count = posts.length; 135 | posts.forEach(function(post){ 136 | category.posts.should.containEql(post._id) 137 | post.should.be.an.instanceof(Post); 138 | post.categories.should.containEql(category._id); 139 | --count || done(); 140 | }); 141 | }); 142 | }); 143 | 144 | it('finds children documents', function(done){ 145 | var category = new Category(), 146 | posts = [ { title: 'Blog post #1' }, 147 | { title: 'Blog post #2' } ] 148 | 149 | category.posts.create(posts, function(err, category, posts){ 150 | var find = category.posts.find({}, function(err, newPosts){ 151 | should.strictEqual(err, null); 152 | 153 | find.should.be.an.instanceof(mongoose.Query); 154 | find._conditions.should.have.property('_id'); 155 | find._conditions.should.have.property('categories'); 156 | find._conditions._id['$in'].should.be.an.instanceof(Array); 157 | 158 | var testFind = function(){ 159 | find.find({title: 'Blog post #1'}, function(err, otherPosts){ 160 | find._conditions.title.should.equal('Blog post #1'); 161 | find._conditions.should.have.property('_id'); 162 | 163 | otherPosts.should.have.length(1); 164 | otherPosts[0].title.should.equal('Blog post #1'); 165 | 166 | done(); 167 | }); 168 | }; 169 | 170 | var count = newPosts.length; 171 | newPosts.should.have.length(2); 172 | newPosts.forEach(function(post){ 173 | category.posts.should.containEql(post._id) 174 | post.should.be.an.instanceof(Post); 175 | post.categories.should.containEql(category._id); 176 | --count || testFind(); 177 | }); 178 | }); 179 | }); 180 | }); 181 | 182 | it('deletes dependent', function(done){ 183 | var category = new Category(), 184 | posts = [ { title: 'Blog post #1' }, 185 | { title: 'Blog post #2' } ] 186 | 187 | category.posts.create(posts, function(err, category, posts){ 188 | var post = posts[0]; 189 | 190 | category.posts.remove(post._id, function(err, category){ 191 | should.strictEqual(err, null); 192 | 193 | category.posts.should.not.containEql(post._id); 194 | category.posts.should.have.length(1); 195 | 196 | // Post, should still exist, this is HABTM 197 | Post.findById(post._id, function(err, post){ 198 | should.strictEqual(err, null); 199 | 200 | should.exist(post); 201 | 202 | post.categories.should.not.containEql(category._id); 203 | post.categories.should.have.length(0); 204 | 205 | post.categories.create({}, function(err, post, category){ 206 | 207 | post.categories.remove(category._id, function(err, post){ 208 | should.strictEqual(err, null); 209 | 210 | // Deletes the category reference in the post 211 | post.categories.should.not.containEql(category._id); 212 | post.categories.should.have.length(0); 213 | 214 | // ... but shouldn't have in the category's post (no dependent: delete); 215 | Category.findById(category._id, function(err, category){ 216 | should.strictEqual(err, null); 217 | 218 | category.posts.should.containEql(post._id); 219 | category.posts.should.have.length(1); 220 | 221 | done(); 222 | }); 223 | }); 224 | }); 225 | }); 226 | }); 227 | }); 228 | }); 229 | 230 | it('populations of path', function(done){ 231 | var category = new Category(), 232 | posts = [ { title: 'Blog post #1' }, 233 | { title: 'Blog post #2' } ]; 234 | 235 | category.posts.create(posts, function(err, category, posts){ 236 | category.save(function(err, category){ 237 | Category.findById(category._id).populate('posts').exec(function(err, populatedCategory){ 238 | should.strictEqual(err, null); 239 | 240 | // Syntactic sugar 241 | var testSugar = function(){ 242 | category.posts.populate(function(err, category){ 243 | should.strictEqual(err, null); 244 | 245 | var count = category.posts.length; 246 | category.posts.forEach(function(post){ 247 | post.should.be.an.instanceof(Post); 248 | --count || done(); 249 | }); 250 | }); 251 | }; 252 | 253 | var count = populatedCategory.posts.length; 254 | populatedCategory.posts.forEach(function(post){ 255 | post.should.be.an.instanceof(Post); 256 | --count || testSugar(); 257 | }); 258 | }); 259 | }); 260 | }); 261 | }); 262 | }); 263 | -------------------------------------------------------------------------------- /specs/hasMany.spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | var mongoose = require('mongoose'), 4 | should = require('should'), 5 | User = require('./support/userModel'), 6 | Tweet = require('./support/tweetModel'), 7 | Tag = require('./support/tagModel'); 8 | 9 | describe('hasMany', function() { 10 | it('has hasMany on the path', function() { 11 | User.schema.paths.tweets.options.hasMany.should.equal('Tweet'); 12 | }); 13 | 14 | it('instantiates one child document', function() { 15 | var user = new User({}), 16 | tweet = { title: 'Easy relationships with mongoose-relationships' }; 17 | 18 | var built = user.tweets.build(tweet); 19 | 20 | built.should.be.an.instanceof(Tweet); 21 | built.author.should.eql(user._id); 22 | built.title.should.equal('Easy relationships with mongoose-relationships') 23 | 24 | user.tweets.should.have.length(1); 25 | }); 26 | 27 | it('instantiates many children documents', function(done) { 28 | var user = new User(), 29 | tweets = [{}, {}]; 30 | 31 | var built = user.tweets.build(tweets); 32 | 33 | user.tweets.should.have.length(2); 34 | 35 | var count = built.length; 36 | built.forEach(function(tweet){ 37 | tweet.should.be.an.instanceof(Tweet); 38 | tweet.author.should.eql(user._id); 39 | --count || done(); 40 | }); 41 | }); 42 | 43 | it('appendes an instantiated child document', function(done) { 44 | var user = new User(), 45 | tweet = new Tweet(); 46 | 47 | user.tweets.append(tweet, function(err, tweet) { 48 | should.strictEqual(err, null); 49 | tweet.author.should.eql(user._id); 50 | user.tweets.should.containEql(tweet._id); 51 | done(); 52 | }); 53 | }); 54 | 55 | it('concates many instantiated child documents', function(done) { 56 | var user = new User(), 57 | tweets = [ new Tweet(), new Tweet() ]; 58 | 59 | user.tweets.concat(tweets, function(err, tweets) { 60 | should.strictEqual(err, null); 61 | 62 | var count = tweets.length; 63 | tweets.forEach(function(tweet){ 64 | tweet.author.should.eql(user._id); 65 | user.tweets.should.containEql(tweet._id); 66 | --count || done(); 67 | }); 68 | }); 69 | }); 70 | 71 | it('creates one child document', function(done) { 72 | var user = new User(), 73 | tweet = { title: 'Easy relationships with mongoose-relationships' }; 74 | 75 | user.tweets.create(tweet, function(err, user, tweet) { 76 | should.strictEqual(err, null); 77 | 78 | user.should.be.an.instanceof(User); 79 | user.tweets.should.have.length(1); 80 | user.tweets[0].should.equal(tweet._id); 81 | 82 | tweet.should.be.an.instanceof(Tweet); 83 | tweet.title.should.equal('Easy relationships with mongoose-relationships') 84 | tweet.author.should.equal(user._id); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('creates many children documents', function(done) { 90 | var user = new User(), 91 | tweets = [ { title: 'Blog tweet #1' }, 92 | { title: 'Blog tweet #2' } ]; 93 | 94 | user.tweets.create(tweets, function(err, user, tweets) { 95 | should.strictEqual(err, null); 96 | 97 | user.tweets.should.have.length(2); 98 | tweets.should.have.length(2); 99 | 100 | var count = tweets.length; 101 | tweets.forEach(function(tweet) { 102 | user.tweets.should.containEql(tweet._id) 103 | tweet.should.be.an.instanceof(Tweet); 104 | tweet.author.should.equal(user._id); 105 | --count || done() 106 | }); 107 | }); 108 | }); 109 | 110 | it('finds children documents', function(done) { 111 | var user = new User(), 112 | tweets = [ { title: 'Blog tweet #1' }, 113 | { title: 'Blog tweet #2' } ] 114 | 115 | user.tweets.create(tweets, function(err, user, tweets) { 116 | var find = user.tweets.find({}, function(err, newTweets) { 117 | should.strictEqual(err, null); 118 | 119 | find.should.be.an.instanceof(mongoose.Query); 120 | find._conditions.should.have.property('_id'); 121 | find._conditions.should.have.property('author'); 122 | find._conditions._id['$in'].should.be.an.instanceof(Array); 123 | 124 | var search = function() { 125 | find.find({ title: 'Blog tweet #1' }, function(err, otherTweets) { 126 | find._conditions.title.should.equal('Blog tweet #1'); 127 | find._conditions.should.have.property('_id'); 128 | 129 | otherTweets.should.have.length(1); 130 | otherTweets[0].title.should.equal('Blog tweet #1'); 131 | done(); 132 | }); 133 | }; 134 | 135 | newTweets.should.have.length(2); 136 | 137 | var count = newTweets.length; 138 | newTweets.forEach(function(tweet) { 139 | user.tweets.should.containEql(tweet._id) 140 | tweet.should.be.an.instanceof(Tweet); 141 | tweet.author.should.eql(user._id); 142 | --count || search(); 143 | }); 144 | }); 145 | }); 146 | }); 147 | 148 | it('deletes dependents', function(done) { 149 | var user = new User(), 150 | tweets = [ { title: 'Blog tweet #1' }, 151 | { title: 'Blog tweet #2' } ]; 152 | 153 | user.tweets.create(tweets, function(err, user, tweets){ 154 | var tweet = tweets[0]; 155 | user.tweets.remove(tweet._id, function(err, user){ 156 | should.strictEqual(err, null); 157 | 158 | user.tweets.should.not.containEql(tweet._id); 159 | user.tweets.should.have.length(1); 160 | 161 | // Tweet, be gone! 162 | Tweet.findById(tweet._id, function(err, found){ 163 | should.strictEqual(err, null); 164 | should.not.exist(found); 165 | done(); 166 | }); 167 | }); 168 | }); 169 | }); 170 | 171 | it('nullifies dependents', function(done){ 172 | var user = new User(), 173 | tags = [ { name: 'awesome' }, 174 | { name: 'omgbbq' } ]; 175 | 176 | user.tags.create(tags, function(err, user, tags){ 177 | var tag = tags[0]; 178 | user.tags.remove(tag._id, function(err, user){ 179 | should.strictEqual(err, null); 180 | 181 | user.tags.should.not.containEql(tag._id); 182 | user.tags.should.have.length(1); 183 | 184 | // Tweet, be nullified! 185 | Tag.findById(tag._id, function(err, tag){ 186 | should.strictEqual(err, null); 187 | should.not.exist(tag.user); 188 | done(); 189 | }); 190 | }); 191 | }); 192 | }); 193 | 194 | it('test population of path', function(done){ 195 | var user = new User(), 196 | tweets = [ { title: 'Blog tweet #1' }, 197 | { title: 'Blog tweet #2' } ]; 198 | 199 | user.tweets.create(tweets, function(err, user, tweets){ 200 | user.save(function(err, user){ 201 | User.findById(user._id).populate('tweets').exec(function(err, populatedUser){ 202 | should.strictEqual(err, null); 203 | 204 | var testSugar = function(){ 205 | // Syntactic sugar 206 | user.tweets.populate(function(err, user){ 207 | should.strictEqual(err, null); 208 | 209 | var count = user.tweets.length; 210 | user.tweets.forEach(function(tweet){ 211 | tweet.should.be.an.instanceof(Tweet); 212 | --count || done(); 213 | }); 214 | }); 215 | }; 216 | 217 | var count = populatedUser.tweets.length; 218 | populatedUser.tweets.forEach(function(tweet){ 219 | tweet.should.be.an.instanceof(Tweet); 220 | --count || testSugar(); 221 | }); 222 | }); 223 | }); 224 | }); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /specs/hasOne.spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | var mongoose = require('mongoose'), 4 | should = require('should'), 5 | User = require('./support/userModel'), 6 | Tweet = require('./support/tweetModel'), 7 | Tag = require('./support/tagModel'), 8 | Post = require('./support/postModel'); 9 | 10 | describe('hasOne', function() { 11 | it('adds the belongsTo path to the child schema', function() { 12 | Tweet.schema.paths.author.options.belongsTo.should.equal('User'); 13 | }); 14 | 15 | it('adds the belongsTo path to the child schema', function() { 16 | Post.schema.paths.editor.options.belongsTo.should.equal('User'); 17 | }); 18 | 19 | it('adds the hasOne path to the parent schema', function() { 20 | User.schema.paths.post.options.hasOne.should.equal('Post'); 21 | }); 22 | 23 | it.skip('has a create function on the association', function() { 24 | var user = new User(); 25 | user.post.create.should.be.a.Function; 26 | }); 27 | 28 | it.skip('creates a child document', function(done){ 29 | var user = new User(), 30 | post = { title: 'Deep thinking, by a mongoose.' }; 31 | 32 | user.post.create(post, function(err, user, post){ 33 | should.strictEqual(err, null); 34 | 35 | user.should.be.an.instanceof(user); 36 | post.should.be.an.instanceof(post); 37 | 38 | user.post.should.eql(post._id); 39 | post.editor.should.equal(user._id); 40 | 41 | post.title.should.equal('Deep thinking, by a mongoose.'); 42 | done(); 43 | }); 44 | }); 45 | 46 | it.skip('finds the child document', function(done){ 47 | var user = new User(), 48 | post = { title: 'Deep thinking, by a mongoose.'}; 49 | 50 | user.post.create(post, function(err, user, post){ 51 | var find = user.post.find(function(err, newPost){ 52 | should.strictEqual(err, null); 53 | 54 | find.should.be.an.instanceof(mongoose.Query); 55 | find._conditions.should.have.property('_id'); 56 | find._conditions.should.have.property('editor'); 57 | find.op.should.equal('findOne'); 58 | 59 | user.post.should.equal(newPost._id); 60 | 61 | newPost.should.be.an.instanceof(post); 62 | newPost.editor.should.eql(user._id); 63 | done(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /specs/mongoose_array.spec.js: -------------------------------------------------------------------------------- 1 | require('./spec_helper'); 2 | 3 | var mongoose = require('mongoose'), 4 | should = require('should'), 5 | User = require('./support/userModel'); 6 | 7 | describe('additions to mongoose array prototype', function(){ 8 | beforeEach(function(){ 9 | this.user = new User({}); 10 | }); 11 | 12 | it('adds a create methods to the association', function(){ 13 | this.user.tweets.create.should.be.a.Function; 14 | }); 15 | 16 | it('adds a find methods to the association', function(){ 17 | this.user.tweets.find.should.be.a.Function; 18 | }); 19 | 20 | it('adds a populate methods to the association', function(){ 21 | this.user.tweets.populate.should.be.a.Function; 22 | }); 23 | 24 | it('adds a remove methods to the association', function(){ 25 | this.user.tweets.remove.should.be.a.Function; 26 | this.user.tweets.delete.should.be.a.Function; 27 | this.user.tweets.remove.should.eql(this.user.tweets.delete); 28 | }); 29 | 30 | it('adds a append methods to the association', function(){ 31 | this.user.tweets.append.should.be.a.Function; 32 | }); 33 | 34 | it('adds a concat methods to the association', function(){ 35 | this.user.tweets.concat.should.be.a.Function; 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /specs/spec_helper.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'), 2 | relationships = require('../'); 3 | 4 | var resetDb = function(next){ 5 | mongoose.connection.db.dropDatabase(function(err){ 6 | if(err) 7 | throw(err); 8 | else 9 | next(); 10 | }); 11 | }; 12 | 13 | beforeEach(function(done){ 14 | if(mongoose.get('isConnected')){ 15 | resetDb(done); 16 | } else { 17 | mongoose.connection.on('open', function(){ 18 | resetDb(done); 19 | }); 20 | } 21 | }); 22 | 23 | var host = process.env.BOXEN_MONGODB_URL || process.env.MONGOOSE_TEST_URL || 'mongodb://localhost/'; 24 | var uri = host + 'mongo_relations_' + process.env.SEQ || '0'; 25 | 26 | mongoose.connect(uri, function(){ 27 | mongoose.set('isConnected', true); 28 | }); 29 | -------------------------------------------------------------------------------- /specs/support/categoryModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var CategorySchema = new mongoose.Schema({ 4 | title: String 5 | }); 6 | 7 | CategorySchema.belongsTo('User', { through: 'editor' }); 8 | 9 | // should only delete the reference 10 | CategorySchema.habtm('Post', { dependent: 'delete' }); 11 | 12 | module.exports = mongoose.model('Category', CategorySchema); 13 | -------------------------------------------------------------------------------- /specs/support/postModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var PostSchema = new mongoose.Schema({ 4 | title: String 5 | }); 6 | 7 | PostSchema.belongsTo('User', { through: 'editor' }); 8 | PostSchema.belongsTo('User', { through: 'author' }); 9 | 10 | // should not delete the reference 11 | PostSchema.habtm('Category'); 12 | 13 | module.exports = mongoose.model('Post', PostSchema); 14 | -------------------------------------------------------------------------------- /specs/support/tagModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var TagSchema = new mongoose.Schema({ 4 | name: String 5 | }); 6 | 7 | TagSchema.belongsTo('User'); 8 | 9 | module.exports = mongoose.model('Tag', TagSchema); 10 | -------------------------------------------------------------------------------- /specs/support/tweetModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var TweetSchema = new mongoose.Schema({ 4 | title: String, 5 | body: String 6 | }); 7 | 8 | TweetSchema.belongsTo('User', { through: 'author' }); 9 | 10 | module.exports = mongoose.model('Tweet', TweetSchema); 11 | -------------------------------------------------------------------------------- /specs/support/userModel.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | 3 | var UserSchema = new mongoose.Schema({ 4 | name: String, 5 | someArray: [ mongoose.Schema.Types.ObjectId ] 6 | }); 7 | 8 | UserSchema.hasMany('Tweet', { dependent: 'delete' }); 9 | UserSchema.hasMany('Tag', { dependent: 'nullify'}); 10 | UserSchema.hasOne( 'Post', { through: 'post' }); 11 | 12 | module.exports = mongoose.model('User', UserSchema); 13 | --------------------------------------------------------------------------------