├── .gitignore ├── tests ├── functions.js └── tests.js ├── package.js ├── document-methods.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .versions 2 | -------------------------------------------------------------------------------- /tests/functions.js: -------------------------------------------------------------------------------- 1 | insert = function (collection) { 2 | collection.insert({ 3 | title: 'Buy groceries', 4 | createdAt: new Date('1/1/2014'), 5 | assignedTo: [{ 6 | user: 'lindsey', 7 | assignedOn: new Date('1/1/2014') 8 | }], 9 | some: { 10 | weirdly: 'nested object', 11 | that: 'I uncreatively', 12 | came: 'up with because', 13 | iwas: 'running out of creativity' 14 | }, 15 | tags: ['critical', 'yum'], 16 | done: false 17 | }); 18 | } 19 | 20 | inst = function (collection) { 21 | return collection.findOne(); 22 | }; -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'lai:document-methods', 3 | version: '0.1.13', 4 | // Brief, one-line summary of the package. 5 | summary: 'Extend your documents with helpful methods.', 6 | // URL to the Git repository containing the source code for this package. 7 | git: 'https://github.com/rclai/meteor-document-methods.git', 8 | // By default, Meteor will default to using README.md for documentation. 9 | // To avoid submitting documentation, set this field to null. 10 | documentation: 'README.md' 11 | }); 12 | 13 | Package.onUse(function(api) { 14 | 15 | api.versionsFrom('0.9.3'); 16 | 17 | api.use([ 18 | // Hopefully, MDG decides to switch to Lodash.. THEY BETTER! 19 | 'underscore', 20 | 'mongo', 21 | 'lai:collection-extensions@0.1.0', 22 | 'dburles:collection-helpers@1.0.3' 23 | ]); 24 | 25 | api.addFiles([ 26 | 'document-methods.js' 27 | ]); 28 | }); 29 | 30 | Package.onTest(function(api) { 31 | api.use([ 32 | 'accounts-base', 33 | 'tinytest', 34 | 'tracker', 35 | 'mongo', 36 | 'underscore', 37 | 'matb33:collection-hooks@0.7.9', 38 | 'aldeed:collection2@2.3.2', 39 | 'ongoworks:security@1.0.1', 40 | 'cfs:standard-packages@0.5.3', 41 | 'dburles:mongo-collection-instances@0.3.1', 42 | 'cfs:gridfs@0.0.27' 43 | ]); 44 | api.use('lai:document-methods'); 45 | api.addFiles([ 46 | 'tests/functions.js', 47 | 'tests/tests.js' 48 | ]); 49 | }); 50 | -------------------------------------------------------------------------------- /document-methods.js: -------------------------------------------------------------------------------- 1 | Meteor.addCollectionExtension(function () { 2 | var inst = this; 3 | if ((!_.isFunction(inst._transform) || inst._hasCollectionHelpers || inst._helpers) && _.isFunction(inst.helpers)) { 4 | inst.helpers({ 5 | $save: function () { 6 | var doc = {}; 7 | _.each(this, function (value, key) { 8 | doc[key] = value; 9 | }); 10 | delete doc._id; 11 | // I would _.clone or _.omit, but for some reason 12 | // freaking underscore.js copies the prototypes 13 | return inst.update.apply(inst, _.flatten([this._id, { $set: doc }, _.toArray(arguments)])); 14 | }, 15 | $duplicate: function () { 16 | var doc = {}; 17 | _.each(this, function (value, key) { 18 | doc[key] = value; 19 | }); 20 | delete doc._id; 21 | return inst.insert.apply(inst, _.flatten([doc, _.toArray(arguments)])); 22 | }, 23 | $update: function (modifier) { 24 | return inst.update.apply(inst, _.flatten([this._id, modifier, _.rest(_.toArray(arguments))])); 25 | }, 26 | $remove: function () { 27 | return inst.remove.apply(inst, _.flatten([this._id, _.toArray(arguments)])); 28 | }, 29 | $set: function (toSet) { 30 | return inst.update.apply(inst, _.flatten([this._id, { $set: toSet }, _.rest(_.toArray(arguments))])); 31 | }, 32 | $unset: function (toSet) { 33 | return inst.update.apply(inst, _.flatten([this._id, { $unset: toSet }, _.rest(_.toArray(arguments))])); 34 | }, 35 | $push: function (toPush) { 36 | return inst.update.apply(inst, _.flatten([this._id, { $push: toPush }, _.rest(_.toArray(arguments))])); 37 | }, 38 | $pushAll: function (toPush) { 39 | return inst.update.apply(inst, _.flatten([this._id, { $pushAll: toPush }, _.rest(_.toArray(arguments))])); 40 | }, 41 | $pull: function (toPull) { 42 | return inst.update.apply(inst, _.flatten([this._id, { $pull: toPull }, _.rest(_.toArray(arguments))])); 43 | }, 44 | $pullAll: function (toPull) { 45 | return inst.update.apply(inst, _.flatten([this._id, { $pullAll: toPull }, _.rest(_.toArray(arguments))])); 46 | }, 47 | $pop: function (toPop) { 48 | return inst.update.apply(inst, _.flatten([this._id, { $pop: toPop }, _.rest(_.toArray(arguments))])); 49 | }, 50 | $addToSet: function (toAdd) { 51 | return inst.update.apply(inst, _.flatten([this._id, { $addToSet: toAdd }, _.rest(_.toArray(arguments))])); 52 | } 53 | }); 54 | } 55 | }); -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | Tinytest.add("does not override your collection helpers", function (test) { 2 | Todos = new Mongo.Collection('todos' + test.id); 3 | 4 | Todos.helpers({ 5 | sayHello: function () { 6 | return 'hello'; 7 | } 8 | }); 9 | 10 | insert(Todos); 11 | 12 | var todo = inst(Todos); 13 | todo.$update({ 14 | $set: { 15 | title: 'Pick up more stuff' 16 | } 17 | }); 18 | 19 | todo = inst(Todos); 20 | test.equal(todo.title, 'Pick up more stuff'); 21 | test.equal(todo.sayHello(), 'hello'); 22 | }); 23 | 24 | Tinytest.add("works alongside dburles:mongo-collection-instances", function (test) { 25 | Todos = new Mongo.Collection('todos' + test.id); 26 | 27 | var todosInstance = Mongo.Collection.get('todos' + test.id); 28 | 29 | test.instanceOf(todosInstance, Mongo.Collection); 30 | 31 | insert(Todos); 32 | 33 | var todo = inst(Todos); 34 | todo.$update({ 35 | $set: { 36 | title: 'Pick up more stuff' 37 | } 38 | }); 39 | 40 | todo = inst(Todos); 41 | test.equal(todo.title, 'Pick up more stuff'); 42 | }); 43 | 44 | Tinytest.add("works alongside ongoworks:security", function (test) { 45 | Todos = new Mongo.Collection('todos' + test.id); 46 | if (Meteor.isServer) { 47 | Todos.permit(['insert', 'update', 'remove']).apply(); 48 | 49 | insert(Todos); 50 | 51 | var todo = inst(Todos); 52 | todo.$update({ 53 | $set: { 54 | title: 'Pick up more stuff' 55 | } 56 | }); 57 | 58 | todo = inst(Todos); 59 | test.equal(todo.title, 'Pick up more stuff'); 60 | } else { 61 | test.equal(Todos.permit, undefined); 62 | } 63 | }); 64 | 65 | Tinytest.add("works alongside aldeed:collection2", function (test) { 66 | Todos = new Mongo.Collection('todos' + test.id); 67 | 68 | Todos.attachSchema(new SimpleSchema({ 69 | title: { 70 | type: String 71 | } 72 | })); 73 | 74 | insert(Todos); 75 | 76 | var todo = inst(Todos); 77 | todo.$update({ 78 | $set: { 79 | title: 'Pick up more stuff' 80 | } 81 | }); 82 | 83 | todo = inst(Todos); 84 | test.equal(todo.title, 'Pick up more stuff'); 85 | }); 86 | 87 | Tinytest.add("works alongside matb33:collection-hooks", function (test) { 88 | Todos = new Mongo.Collection('todos' + test.id); 89 | 90 | Todos.after.update(function () { 91 | test.equal(true, true); 92 | }); 93 | 94 | insert(Todos); 95 | 96 | var todo = inst(Todos); 97 | todo.$update({ 98 | $set: { 99 | title: 'Pick up more stuff' 100 | } 101 | }); 102 | 103 | todo = inst(Todos); 104 | test.equal(todo.title, 'Pick up more stuff'); 105 | }); 106 | 107 | Tinytest.add("works alongside cfs:standard-packages + cfs:gridfs", function (test) { 108 | Todos = new Mongo.Collection('todos' + test.id); 109 | 110 | Images = new FS.Collection("images" + test.id, { 111 | stores: [new FS.Store.GridFS("images" + test.id)] 112 | }); 113 | 114 | insert(Todos); 115 | 116 | var todo = inst(Todos); 117 | todo.$update({ 118 | $set: { 119 | title: 'Pick up more stuff' 120 | } 121 | }); 122 | 123 | todo = inst(Todos); 124 | test.equal(todo.title, 'Pick up more stuff'); 125 | }); 126 | 127 | Tinytest.add("all methods", function (test) { 128 | Todos = new Mongo.Collection('todos' + test.id); 129 | 130 | insert(Todos); 131 | 132 | var todo = inst(Todos); 133 | 134 | // Test $save 135 | todo.done = true; 136 | todo.title = 'Buy cereal'; 137 | todo.createdAt = new Date('1/2/2014'); 138 | todo.assignedTo = [{ 139 | user: 'bob', 140 | assignedOn: new Date('1/2/2014') 141 | }, { 142 | user: 'lindsey', 143 | assignedOn: new Date('1/1/2014') 144 | }]; 145 | todo.tags = ['critical', 'yum', 'awesome']; 146 | todo.some.weirdly = 'odd object', 147 | todo.some.that = { 148 | now: 'has another', 149 | nested: 'object', 150 | whoa: 'yo' 151 | }; 152 | todo.$save(); 153 | 154 | todo = inst(Todos); 155 | test.equal(todo.done, true); 156 | test.equal(todo.title, 'Buy cereal'); 157 | test.equal(todo.createdAt, new Date('1/2/2014')); 158 | test.equal(todo.assignedTo, [{ 159 | user: 'bob', 160 | assignedOn: new Date('1/2/2014') 161 | }, { 162 | user: 'lindsey', 163 | assignedOn: new Date('1/1/2014') 164 | }]); 165 | test.equal(todo.tags, ['critical', 'yum', 'awesome']); 166 | test.equal(todo.some.weirdly, 'odd object'); 167 | test.equal(todo.some.that.now, 'has another'); 168 | test.equal(todo.some.that.nested, 'object'); 169 | test.equal(todo.some.that.whoa, 'yo'); 170 | test.equal(todo.some.came, 'up with because'); 171 | test.equal(todo.some.iwas, 'running out of creativity'); 172 | 173 | // Test $update 174 | todo.$update({ 175 | $set: { 176 | done: false, 177 | title: 'Buy groceries', 178 | createdAt: new Date('1/1/2014'), 179 | assignedTo: [{ 180 | user: 'lindsey', 181 | assignedOn: new Date('1/1/2014') 182 | }], 183 | tags: ['critical', 'yum'] 184 | }, 185 | $push: { 186 | blah: 1 187 | } 188 | }); 189 | 190 | todo = inst(Todos); 191 | test.equal(todo.done, false); 192 | test.equal(todo.title, 'Buy groceries'); 193 | test.equal(todo.createdAt, new Date('1/1/2014')); 194 | test.equal(todo.assignedTo, [{ 195 | user: 'lindsey', 196 | assignedOn: new Date('1/1/2014') 197 | }]); 198 | test.equal(todo.tags, ['critical', 'yum']); 199 | test.equal(todo.blah, [1]); 200 | 201 | // Test $update with $addToSet 202 | todo.$update({ 203 | $addToSet: { 204 | blah: 2 205 | } 206 | }); 207 | 208 | todo = inst(Todos); 209 | 210 | test.equal(todo.blah, [1, 2]); 211 | 212 | // Test $pop 213 | todo.$pop({ 214 | blah: 1 215 | }); 216 | 217 | todo = inst(Todos); 218 | 219 | test.equal(todo.blah, [1]); 220 | 221 | // Test $push 222 | todo.$push({ 223 | tags: 'what the' 224 | }); 225 | 226 | todo = inst(Todos); 227 | 228 | test.equal(todo.tags, ['critical', 'yum', 'what the']); 229 | 230 | // Test $addToSet 231 | todo.$addToSet({ 232 | tags: { 233 | some: 'weird', 234 | tag: '!' 235 | } 236 | }); 237 | 238 | todo = inst(Todos); 239 | 240 | test.equal(todo.tags, ['critical', 'yum', 'what the', { 241 | some: 'weird', 242 | tag: '!' 243 | }]); 244 | 245 | // Test $unset 246 | todo.$unset({ 247 | blah: '1' 248 | }); 249 | 250 | todo = inst(Todos); 251 | 252 | test.equal(todo.blah, undefined); 253 | 254 | // Test $pushAll 255 | todo.$pushAll({ 256 | blah: [1, 2, 3, 4] 257 | }); 258 | 259 | todo = inst(Todos); 260 | 261 | test.equal(todo.blah, [1, 2, 3, 4]); 262 | 263 | // Test $pullAll 264 | todo.$pullAll({ 265 | tags: ['critical', 'yum', 'what the'] 266 | }); 267 | 268 | todo = inst(Todos); 269 | 270 | test.equal(todo.tags, [{ 271 | some: 'weird', 272 | tag: '!' 273 | }]); 274 | 275 | // Test $remove 276 | todo.$remove(); 277 | test.equal(inst(Todos), undefined); 278 | 279 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Mongo Document Methods 2 | 3 | Adds helpful methods to each of your documents to easily update/duplicate/remove them. 4 | 5 | Instead of writing: 6 | 7 | ``` 8 | MyCollection.update(this._id, { $set: { a: 1 } }); 9 | ``` 10 | 11 | Now you can write: 12 | 13 | ``` 14 | this.$set({ a: 1 }) 15 | ``` 16 | 17 | Where ```this``` is a document that was ```findOne```'d. 18 | 19 | There's more stuff you can do, see below. 20 | 21 | ## Installation 22 | 23 | ``` 24 | meteor add lai:document-methods 25 | ``` 26 | 27 | ## Note 28 | 29 | If you have certain collection definitions with transforms applied to them by other means other than collection helpers (i.e CollectionFS), then those collections won't have these methods. 30 | 31 | This uses ```dburles:collection-helpers``` to get the job done. All of these methods are injected to all of your collections as collection helpers. Thanks __@dburles__ for your packages, without them this would not be possible, or rather, it would've made this super hard to make! 32 | 33 | ## Examples 34 | 35 | ```js 36 | Stuff = new Mongo.Collection('stuff'); 37 | 38 | var thing = Stuff.findOne(); 39 | 40 | // Set individual properties 41 | thing.$set({ foo: 'bar' }); 42 | 43 | // Or multiple properties 44 | thing.$set({ foo: 'bar', bar: 'baz' }); 45 | thing.$set({ foo: 'bar', bar: 'baz', testArray: [1, 2, 3] }); 46 | 47 | // You can also modify them by setting new properties 48 | // Note: setting properties/modifying them does not trigger reactivity) 49 | thing.newProperty = 'that means absolutely nothing'; 50 | thing.anotherOne = 'yea!'; 51 | // Then saving it (this will update and trigger reactivity) 52 | thing.$save(); 53 | 54 | // The following methods mimic the Mongo functions of $push, $pushAll, 55 | // $unset, $addToSet, $pull, $pullAll, and $pop 56 | // (Note: they will apply the update immediately) 57 | thing.$push({ myArrayProperty: 1 }); 58 | thing.$pushAll({ myArrayProperty: [2, 3, 4, 5] }); 59 | thing.$addToSet({ myPropertySet: { a: 1, b: 2, c: 3} }); 60 | thing.$pull({ myArrayProperty: 2 }); 61 | thing.$pullAll({ myArrayProperty: [1, 3, 4] }); 62 | thing.$pop({ myArrayProperty: 1 }); 63 | thing.$unset({ myArrayProperty: '' }); 64 | 65 | // If you think this is too limiting, you can have full control of the modifier, 66 | // but only on the modifier and not the criteria because remember, 67 | // these are methods that correspond to one specific document 68 | thing.$update({ 69 | $set: { 70 | a: '1', 71 | b: '2' 72 | }, 73 | $addToSet: { 74 | myPropertySet: { 75 | foo: 'bar', 76 | bar: 'baz' 77 | } 78 | }, 79 | ... 80 | }); 81 | 82 | // If you wanted to create a duplicate of this document 83 | var newThing, newThingId = thing.$duplicate(); 84 | if (newThingId) { 85 | newThing = Stuff.findOne(newThingId); 86 | // do new things with this newThing 87 | } 88 | 89 | // And once you've realized that you created a 90 | // completely meaningless document, you can remove it 91 | thing.$remove(); 92 | newThing.$remove(); 93 | ``` 94 | 95 | ## A more practical example 96 | 97 | ```js 98 | // app.js 99 | Todos = new Mongo.Collection('todos'); 100 | 101 | if (Meteor.isClient) { 102 | Template.todos.helpers({ 103 | todos: function () { 104 | return Todos.find(); 105 | }, 106 | checked: function () { 107 | return this.done ? 'checked' : ''; 108 | } 109 | }); 110 | Template.todos.events({ 111 | 'change .todo-chk': function (event, template) { 112 | this.$set({ done: !this.done }); 113 | }, 114 | 'submit .add-tag': function (event, template) { 115 | event.preventDefault(); 116 | var tag = $.trim($(event.target).find('.tag-input').val()); 117 | tag && this.$addToSet({ tags: tag }); 118 | }, 119 | 'click .duplicate': function (event, template) { 120 | // You can change the properties before duplicating it too! 121 | this.title = this.title.concat(' - new title'); 122 | this.$duplicate(); 123 | }, 124 | 'click .delete': function (event, template) { 125 | this.$remove(); 126 | } 127 | }); 128 | } 129 | ``` 130 | 131 | ```html 132 | 151 | ``` 152 | 153 | ## API 154 | 155 | Where ```document``` is a document that was ```findOne```'d. 156 | 157 | For all update-related callbacks, If present, called with an error object as the first argument and, if no error, the number of affected documents as the second. 158 | 159 | For the duplicate callback, if present, called with an error object as the first argument and, if no error, the _id as the second. 160 | 161 | For the remove callback, if present, called with an error object as its argument. 162 | 163 | If no callbacks were provided, they should function exactly as an insert, update, remove as specified in the Meteor documentation (insert will return the _id, update will return 1, remove will remove nothing, etc). 164 | 165 | #### document.$update(modifier [, callback]) 166 | 167 | Mimics ```MyCollection.update(this._id, modifier)```. 168 | 169 | #### document.$save([callback]) 170 | 171 | Updates the current document with its current properties. 172 | 173 | #### document.$duplicate([callback]) 174 | 175 | Does an insert of the document, returns _id if no callback was provided, otherwise, it's returned in the second argument of the callback. 176 | 177 | #### document.$remove([callback]) 178 | 179 | Mimics ```MyCollection.remove(this._id)``` 180 | 181 | #### document.$set(propertiesToSet [, callback]) 182 | 183 | Mimics ```MyCollection.update(this._id, { $set: { prop1: 1, prop2: 2 } })``` 184 | 185 | #### document.$unset(propertiesToUnset [, callback]) 186 | 187 | Mimics ```MyCollection.update(this._id, { $unset: { prop1: '' } })``` 188 | 189 | #### document.$addToSet(setToAdd [, callback]) 190 | 191 | Mimics ```MyCollection.update(this._id, { $addToSet: { mySetOfThings: { a: 1, b: 2 } } })``` 192 | 193 | #### document.$push(thingsToPush [, callback]) 194 | 195 | Mimics ```MyCollection.update(this._id, { $push: { myList: 1 } })``` 196 | 197 | #### document.$pushAll(allThingsToPush [, callback]) 198 | 199 | Mimics ```MyCollection.update(this._id, { $pushAll: { myList: [1, 2, 3, 4] } })``` 200 | 201 | #### document.$pull(thingToPull [, callback]) 202 | 203 | Mimics ```MyCollection.update(this._id, { $pull: { myList: 1 } })``` 204 | 205 | #### document.$pullAll(allThingsToPull [, callback]) 206 | 207 | Mimics ```MyCollection.update(this._id, { $pullAll: { myList: [1, 2, 3, 4] } })``` 208 | 209 | #### document.$pop(thingToPop [, callback]) 210 | 211 | Mimics ```MyCollection.update(this._id, { $pop: { myList: 1 } })``` 212 | 213 | ## FAQs 214 | 215 | #### Won't this override my collection helpers? 216 | 217 | No it doesn't, these helpers will be added alongside your current collection helpers, just make sure don't have collection helpers that have the same name as these methods. 218 | 219 | ## Now What? 220 | 221 | I wrote some tests but I would love your feedback and for you guys to test it out as well. 222 | 223 | Also, when I created this, I had intially named them without the ```$``` prefix, but then I figured that you might run into name conflicts and that's why I decided to add the ```$``` prefix. Let me know your thoughts. 224 | 225 | ## Thoughts 226 | 227 | I guess it is obvious that you shouldn't use this in Blaze even though you can, and perhaps you're thinking that collection helpers are supposed to be only used for Blaze! 228 | 229 | But this brings up a discussion of: 230 | 231 | Shouldn't documents have a nice API to manipulate them, at least at the individual document level? 232 | 233 | ## License 234 | 235 | MIT 236 | --------------------------------------------------------------------------------