├── HISTORY.md ├── package.js ├── README.md ├── find_and_modify.js └── find_and_modify_tests.js /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## v.1.0.0 2 | 3 | * Fix for compatability with Meteor 1.4.x. Supports writeConcern. 4 | Thanks to @tcastelli. 5 | 6 | ## v.0.2.1 7 | 8 | * Update to use the `rawCollection` method introduced in Meteor 1.0.4. 9 | 10 | ## v.0.2.0 11 | 12 | * upserts now use a string _id of 17 random characters for consistency with 13 | Meteor's default behavior. #4 14 | -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | Package.describe({ 2 | name: 'fongandrew:find-and-modify', 3 | summary: 'findAndModify implementation for Meteor collection', 4 | version: '1.0.0', 5 | git: 'https://github.com/fongandrew/meteor-find-and-modify.git' 6 | }); 7 | 8 | Package.onUse(function(api) { 9 | api.versionsFrom('1.4.1.1'); 10 | api.use('ddp', ['client', 'server']); 11 | api.use('mongo', ['client', 'server']); 12 | api.use('random', ['client', 'server']); 13 | api.imply('mongo', ['client', 'server']) 14 | api.addFiles('find_and_modify.js', ['client', 'server']); 15 | }); 16 | 17 | Package.onTest(function(api) { 18 | api.use('tinytest'); 19 | api.use('fongandrew:find-and-modify'); 20 | api.mainModule('find_and_modify_tests.js'); 21 | }); 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FindAndModify 2 | ============= 3 | 4 | **NB: This package isn't actively maintained at the moment.** I'll review PRs 5 | as I can, but I'm currently not working on any Meteor projects, 6 | so my incentive to bug-fix is pretty low. 7 | 8 | This Meteor package adds [findAndModify](https://docs.mongodb.com/manual/reference/command/findAndModify/) support to Meteor's MongoDB Collections. It should work on both the server and client. It adapts 9 | and cleans up some code found on https://github.com/meteor/meteor/issues/1070. 10 | 11 | 12 | Installation 13 | ------------ 14 | 15 | meteor add fongandrew:find-and-modify 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | TestCollection.findAndModify({ 22 | query: {name: "Batman"}, 23 | update: {$set: {name: "Bruce Wayne"}}, 24 | sort: {age: 1}, 25 | upsert: true, 26 | new: true, 27 | fields: {favoriteColor: 0} 28 | }); 29 | 30 | TestCollection.findAndModify({ 31 | query: {name: "Robin"}, 32 | sort: {age: -1}, 33 | skip: 2, // available only client side 34 | remove: true 35 | }); 36 | 37 | See mongo documentation for arguments details: 38 | http://docs.mongodb.org/manual/reference/method/db.collection.findAndModify/ 39 | 40 | 41 | Known Issues 42 | ------------ 43 | 44 | The client-side code (and the tests for the client-side code) works on the 45 | assumption that is running in a Meteor method simulation. If run outside of a 46 | simulation, the client-side code will use an `_id` selector in lieu of your 47 | original query to get around Meteor's restriction on `_id` only updates 48 | for untrusted (client) code. This may result in unusual errors or behavior if 49 | your modifier is dependent on your query selector (e.g. as with positional 50 | operators). 51 | 52 | For consistency with Meteor's default behavior, upserts using findAndModify 53 | use _ids consisting of a 17 character string rather than an ObjectId. This 54 | behavior has only been tested with Mongo 2.6+ and may result in issues with 55 | Mongo 2.4.x. 56 | 57 | 58 | License 59 | ------- 60 | 61 | Permission is hereby granted, free of charge, to any person obtaining a copy 62 | of this software and associated documentation files (the "Software"), to deal 63 | in the Software without restriction, including without limitation the rights 64 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 65 | copies of the Software, and to permit persons to whom the Software is 66 | furnished to do so, subject to the following conditions: 67 | 68 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 69 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 70 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 71 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 72 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 73 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 74 | THE SOFTWARE. 75 | -------------------------------------------------------------------------------- /find_and_modify.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | // Code adapted from https://github.com/meteor/meteor/issues/1070 4 | 5 | // Helper func to run shared validation code 6 | function validate(collection, args) { 7 | if(!collection._name) 8 | throw new Meteor.Error(405, 9 | "findAndModify: Must have collection name."); 10 | 11 | if(!args) 12 | throw new Meteor.Error(405, "findAndModify: Must have args."); 13 | 14 | if(!args.query) 15 | throw new Meteor.Error(405, "findAndModify: Must have query."); 16 | 17 | if(!args.update && !args.remove) 18 | throw new Meteor.Error(405, 19 | "findAndModify: Must have update or remove."); 20 | }; 21 | 22 | if (Meteor.isServer) { 23 | Mongo.Collection.prototype.findAndModify = function(args,rawResult){ 24 | validate(this, args); 25 | 26 | var q = {}; 27 | q.query = args.query || {}; 28 | q.sort = args.sort || []; 29 | if (args.update) 30 | q.update = args.update; 31 | 32 | q.options = {}; 33 | if (args.new !== undefined) 34 | q.options.new = args.new; 35 | if (args.remove !== undefined) 36 | q.options.remove = args.remove; 37 | if (args.upsert !== undefined) 38 | q.options.upsert = args.upsert; 39 | if (args.fields !== undefined) 40 | q.options.fields = args.fields; 41 | if (args.writeConcern !== undefined) 42 | q.options.w = args.writeConcern; 43 | if (args.maxTimeMS !== undefined) 44 | q.options.wtimeout = args.maxTimeMS; 45 | if (args.bypassDocumentValidation != undefined) 46 | q.options.bypassDocumentValidation = args.bypassDocumentValidation; 47 | 48 | // If upsert, assign a string Id to $setOnInsert unless otherwise provided 49 | if (q.options.upsert) { 50 | q.update = q.update || {}; 51 | q.update.$setOnInsert = q.update.$setOnInsert || {}; 52 | q.update.$setOnInsert._id = q.update.$setOnInsert._id || Random.id(17); 53 | } 54 | 55 | // Use rawCollection object introduced in Meteor 1.0.4. 56 | var collectionObj = this.rawCollection(); 57 | 58 | var wrappedFunc = Meteor.wrapAsync(collectionObj.findAndModify, 59 | collectionObj); 60 | var result = wrappedFunc( 61 | q.query, 62 | q.sort, 63 | q.update, 64 | q.options 65 | ); 66 | return rawResult? result : result.value; 67 | }; 68 | } 69 | 70 | if (Meteor.isClient) { 71 | Mongo.Collection.prototype.findAndModify = function(args) { 72 | validate(this, args); 73 | 74 | var findOptions = {}; 75 | if (args.sort !== undefined) 76 | findOptions.sort = args.sort; 77 | if (args.fields !== undefined) 78 | findOptions.fields = args.fields; 79 | if (args.skip !== undefined) 80 | findOptions.skip = args.skip; 81 | 82 | var ret = this.findOne(args.query, findOptions); 83 | if (args.remove) { 84 | if (ret) this.remove({_id: ret._id}); 85 | } 86 | 87 | else { 88 | if (args.upsert && !ret) { 89 | var writeResult = this.upsert(args.query, args.update); 90 | if (writeResult.insertedId && args.new) 91 | return this.findOne({_id: writeResult.insertedId}, findOptions); 92 | else if (findOptions.sort) 93 | return {}; 94 | return null; 95 | } 96 | 97 | else if (ret) { 98 | 99 | // If we're in a simulation, it's safe to call update with normal 100 | // selectors (which is needed, e.g., for modifiers with positional 101 | // operators). Otherwise, we'll have to do an _id only update to 102 | // get around the restriction that lets untrusted (e.g. client) 103 | // code update collections by _id only. 104 | var enclosing = DDP._CurrentInvocation.get(); 105 | var alreadyInSimulation = enclosing && enclosing.isSimulation; 106 | if (alreadyInSimulation) { 107 | // Add _id to query because Meteor's update doesn't include certain 108 | // options that the full findAndModify does (like sort). Create 109 | // shallow copy before update so as not to mess with user's 110 | // original query object 111 | var updatedQuery = {}; 112 | for (var prop in args.query) { 113 | updatedQuery[prop] = args.query[prop]; 114 | } 115 | updatedQuery._id = ret._id; 116 | this.update(updatedQuery, args.update); 117 | } 118 | 119 | else { 120 | this.update({_id: ret._id}, args.update); 121 | } 122 | 123 | if (args.new) 124 | return this.findOne({_id: ret._id}, findOptions); 125 | } 126 | } 127 | 128 | return ret; 129 | }; 130 | } 131 | 132 | })(); 133 | 134 | 135 | -------------------------------------------------------------------------------- /find_and_modify_tests.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var TestCollection = new Mongo.Collection("findAndModifyTestCol"); 5 | if (Meteor.isServer) { 6 | TestCollection.remove({}); 7 | TestCollection.allow({ 8 | insert: function() {return true;}, 9 | remove: function() {return true;}, 10 | update: function() {return true;} 11 | }); 12 | } 13 | 14 | /** Helper function to create test methods that can run async or sync 15 | * depending on context 16 | * @param {Function} name - Name for both method and test 17 | * @param {Function} method - Something that returns a result and can be 18 | * wrapped in a Meteor method. 19 | * @param {Function} assertions - A callback that should accept the "test" 20 | * object for tinytest and the result from the Meteor method. 21 | */ 22 | var addTest = function(name, method, assertions) { 23 | var methodObj = {}; 24 | var returnVal; 25 | methodObj[name] = function() { 26 | if (this.isSimulation) { 27 | returnVal = method(); // Capture outside scope so we can examine the 28 | // actual client code indepently of server 29 | // response 30 | return returnVal; 31 | } 32 | return method(); // Server runs normally 33 | }; 34 | Meteor.methods(methodObj); 35 | 36 | if (Meteor.isServer) { 37 | Tinytest.add(name, function(test) { 38 | var result = method(); 39 | assertions(test, result); 40 | }); 41 | } 42 | 43 | else { 44 | Tinytest.addAsync(name, function(test, done) { 45 | Meteor.call(name, function(error) { 46 | test.isFalse(!!error, error); 47 | try { 48 | if (returnVal) { 49 | assertions(test, returnVal); 50 | } else { 51 | test.isTrue(false, "Missing return val"); 52 | } 53 | } finally { 54 | done(); 55 | } 56 | }); 57 | }); 58 | } 59 | } 60 | 61 | 62 | addTest("findAndModify - find + set", 63 | function() { 64 | TestCollection.remove({}); 65 | var batmanId = TestCollection.insert({ name: "Batman", 66 | favoriteColor: "black" }); 67 | var supermanId = TestCollection.insert({ name: "Superman", 68 | favoriteColor: "blue" }); 69 | return TestCollection.findAndModify({ 70 | query: {name: "Batman"}, 71 | update: {$set: {name: "Bruce Wayne"}} 72 | }); 73 | }, 74 | function(test, result) { 75 | test.equal(result.name, "Batman"); 76 | test.equal(result.favoriteColor, "black"); 77 | }); 78 | 79 | 80 | addTest("findAndModify - find + set + new", 81 | function() { 82 | TestCollection.remove({}); 83 | var batmanId = TestCollection.insert({ name: "Batman", 84 | favoriteColor: "black" }); 85 | var supermanId = TestCollection.insert({ name: "Superman", 86 | favoriteColor: "blue" }); 87 | return TestCollection.findAndModify({ 88 | query: {name: "Batman"}, 89 | update: {$set: {name: "Bruce Wayne"}}, 90 | new: true 91 | }); 92 | }, 93 | function(test, result) { 94 | test.equal(result.name, "Bruce Wayne"); 95 | test.equal(result.favoriteColor, "black"); 96 | }); 97 | 98 | 99 | addTest("findAndModify - upsert when doesn't exist", 100 | function() { 101 | TestCollection.remove({}); 102 | return TestCollection.findAndModify({ 103 | query: {name: "Batman"}, 104 | update: {$set: {name: "Bruce Wayne"}}, 105 | new: true, 106 | upsert: true 107 | }); 108 | }, 109 | function(test, result) { 110 | test.equal(result.name, "Bruce Wayne"); 111 | test.equal(result.favoriteColor, undefined); 112 | }); 113 | 114 | 115 | addTest("findAndModify - upsert when doesn't exist (force str ID)", 116 | function() { 117 | TestCollection.remove({}); 118 | return TestCollection.findAndModify({ 119 | query: {name: "Batman"}, 120 | update: {$set: {name: "Bruce Wayne"}}, 121 | new: true, 122 | upsert: true 123 | }); 124 | }, 125 | function(test, result) { 126 | test.equal(result.name, "Bruce Wayne"); 127 | test.equal(result.favoriteColor, undefined); 128 | test.equal(typeof(result._id), "string"); 129 | }); 130 | 131 | 132 | addTest("findAndModify - upsert when already exists", 133 | function() { 134 | TestCollection.remove({}); 135 | var batmanId = TestCollection.insert({ name: "Batman", 136 | favoriteColor: "black" }); 137 | var supermanId = TestCollection.insert({ name: "Superman", 138 | favoriteColor: "blue" }); 139 | 140 | return TestCollection.findAndModify({ 141 | query: {name: "Batman"}, 142 | update: {$set: {name: "Bruce Wayne"}}, 143 | new: true, 144 | upsert: true 145 | }); 146 | }, 147 | function(test, result) { 148 | test.equal(result.name, "Bruce Wayne"); 149 | test.equal(result.favoriteColor, "black"); 150 | }); 151 | 152 | 153 | addTest("findAndModify - sort", 154 | function() { 155 | TestCollection.remove({}); 156 | TestCollection.insert({ name: "Batman", 157 | favoriteColor: "black" }); 158 | TestCollection.insert({ name: "Superman", 159 | favoriteColor: "blue" }); 160 | TestCollection.insert({ name: "Flash", 161 | favoriteColor: "red" }); 162 | return TestCollection.findAndModify({ 163 | query: {name: {$exists: true}}, 164 | sort: {name: -1}, 165 | update: {$set: {name: "Clark Kent"}}, 166 | new: true 167 | }); 168 | }, 169 | function(test, result) { 170 | test.equal(result.name, "Clark Kent"); 171 | test.equal(result.favoriteColor, "blue"); 172 | }); 173 | 174 | 175 | addTest("findAndModify - remove", 176 | function() { 177 | TestCollection.remove({}); 178 | TestCollection.insert({ name: "Batman", 179 | favoriteColor: "black" }); 180 | TestCollection.insert({ name: "Superman", 181 | favoriteColor: "blue" }); 182 | var findAndModifyResponse = TestCollection.findAndModify({ 183 | query: {name: "Batman"}, 184 | remove: true 185 | }); 186 | var count = TestCollection.find({}).count(); 187 | var batman = TestCollection.findOne({name: "Batman"}); 188 | return { 189 | findAndModifyResponse: findAndModifyResponse, 190 | count: count, 191 | batman: batman 192 | }; 193 | }, 194 | function(test, result) { 195 | test.equal(result.findAndModifyResponse.name, "Batman"); 196 | test.equal(result.findAndModifyResponse.favoriteColor, "black"); 197 | test.equal(result.count, 1); 198 | test.equal(result.batman, undefined); 199 | }); 200 | 201 | 202 | addTest("findAndModify - fields", 203 | function() { 204 | TestCollection.remove({}); 205 | var batmanId = TestCollection.insert({ name: "Batman", 206 | favoriteColor: "black" }); 207 | var supermanId = TestCollection.insert({ name: "Superman", 208 | favoriteColor: "blue" }); 209 | 210 | return TestCollection.findAndModify({ 211 | query: {name: "Batman"}, 212 | update: {$set: {name: "Bruce Wayne"}}, 213 | fields: { 214 | favoriteColor: 0 215 | } 216 | }); 217 | }, 218 | function(test, result) { 219 | test.equal(result.name, "Batman"); 220 | test.equal(result.favoriteColor, undefined); 221 | }); 222 | 223 | 224 | addTest("findAndModify - positional operators", 225 | function() { 226 | TestCollection.remove({}); 227 | var justiceLeague = TestCollection.insert({ 228 | heroes: [ 229 | { name: "Batman", favoriteColor: "black"}, 230 | { name: "Superman", favoriteColor: "blue"}, 231 | { name: "Wonder Woman", favoriteColor: "red"}, 232 | ] 233 | }); 234 | var avengers = TestCollection.insert({ 235 | heroes: [ 236 | { name: "Iron Man", favoriteColor: "gold"}, 237 | { name: "Hulk", favoriteColor: "green"}, 238 | { name: "Captain America", favoriteColor: "blue"} 239 | ] 240 | }); 241 | 242 | return TestCollection.findAndModify({ 243 | query: {"heroes.name": "Batman"}, 244 | update: {$set: {"heroes.$.favoriteColor": "darkness"}}, 245 | new: true 246 | }); 247 | }, 248 | function(test, result) { 249 | test.equal(result.heroes[0].name, "Batman"); 250 | test.equal(result.heroes[0].favoriteColor, "darkness"); 251 | }); 252 | 253 | })(); 254 | --------------------------------------------------------------------------------