├── .gitignore ├── test ├── .gitignore ├── server.js ├── package.json ├── index.html └── test.js ├── .project ├── lib ├── plugins │ └── marilyn.watch.js ├── marilyn-min.js └── marilyn.js ├── bower.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | Icon? 7 | ehthumbs.db 8 | Thumbs.db 9 | bower_components/ 10 | node_modules/ -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | Icon? 7 | ehthumbs.db 8 | Thumbs.db 9 | bower_components/ 10 | node_modules/ -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var io = require('socket.io').listen(3002); 2 | 3 | io.sockets.on('connection', function(socket) { 4 | 5 | socket.emit('response', { 6 | 'working': true 7 | }); 8 | 9 | socket.on('request', function(data) { 10 | console.log(data); 11 | 12 | socket.emit('response', { 13 | 'working': true 14 | }); 15 | 16 | }); 17 | 18 | }); -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marilyn-test", 3 | "authors": [ 4 | { 5 | "name": "Alan James", 6 | "email": "alanjames1987@gmail.com" 7 | } 8 | ], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "dependencies": { 18 | "socket.io": "0.9.16" 19 | } 20 | } -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | Marilyn 4 | 5 | 6 | 7 | 8 | 9 | com.aptana.ide.core.unifiedBuilder 10 | 11 | 12 | 13 | 14 | 15 | com.aptana.projects.webnature 16 | com.aptana.ruby.core.rubynature 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /lib/plugins/marilyn.watch.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | 3 | // define marilyn using AMD 4 | if (typeof define === 'function' && define.amd) { 5 | define(factory); 6 | } 7 | 8 | // define marilyn by adding it to the window object 9 | else { 10 | window.Marilyn.watch = window.marilyn.watch = factory(); 11 | } 12 | 13 | })(function() { 14 | 15 | // Marilyn Watch is a plugin for keeping Marilyn collections in sync with Mongo documents 16 | // this is not included as a core Marilyn feature because it requires Node.js integration 17 | function plugin() { 18 | 19 | this.watch = function(queries, callback) { 20 | 21 | var modelName = this._name; 22 | 23 | // setup a watch callback 24 | this.on('__watch.' + modelName, callback); 25 | 26 | // sent a watch emit 27 | this.emit('__watch', modelName, queries); 28 | 29 | }; 30 | 31 | } 32 | 33 | return plugin; 34 | 35 | }); -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marilyn", 3 | "version": "0.17.1", 4 | "homepage": "https://github.com/alanjames1987/marilyn", 5 | "authors": [{ 6 | "name": "Alan James", 7 | "email": "alanjames1987@gmail.com" 8 | }], 9 | "license": "MIT", 10 | "ignore": [ 11 | "**/.*", 12 | "node_modules", 13 | "bower_components", 14 | "test", 15 | "tests" 16 | ], 17 | "description": "A client side, Socket.IO driven, Pub/Sub, model layer with a query system similar to Mongoose.", 18 | "main": "/lib/marilyn.js", 19 | "keywords": [ 20 | "model", 21 | "browser", 22 | "socket.io", 23 | "socket", 24 | "websocket", 25 | "real time", 26 | "real-time", 27 | "realtime", 28 | "crud", 29 | "pub/sub", 30 | "pubsub", 31 | "publish–subscribe" 32 | ], 33 | "dependencies": { 34 | "underscore": ">=1.5.0" 35 | }, 36 | "devDependencies": { 37 | "node-uuid": "1.4.1", 38 | "qunit": "1.15.0" 39 | } 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Alan James 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/marilyn-min.js: -------------------------------------------------------------------------------- 1 | !function(e){"function"==typeof define&&define.amd?define(["underscore"],e):window.Marilyn=window.marilyn=e(_)}(function(e){var n,t={},i=[],o={},r=function(e){return o[e]},c=function(r,c){var a=function(n){n=n&&e.isFunction(n)?n:function(){};var t,i=function(e,i){"create"===t?u.inform("create",i):"update"===t&&u.inform("update",[i]),u.inform("save",i),n.call(u,e,i)},o=function(e,n){u._afters.hasOwnProperty("save")?u._afters.save.call(u,n,function(){i(e,n)}):i(e,n)},r=function(e){u.updateSilent({__id:e.__id},e,function(e,n){var t=n[0];u._afters.hasOwnProperty("update")?u._afters.update.call(u,n,function(){o(e,t)}):o(e,t)})},c=function(e){u.createSilent(e,function(n,t){u._afters.hasOwnProperty("create")?u._afters.create.call(u,e,function(){o(n,t)}):o(n,t)})},a=function(e){e.hasOwnProperty("__id")?(t="update",u._befores.hasOwnProperty("update")?u._befores.update.call(u,{__id:e.__id},e,function(){r(e)}):r(e)):(t="create",u._befores.hasOwnProperty("create")?u._befores.create.call(u,e,function(){c(e)}):c(e))},l=this||{};u._befores.hasOwnProperty("save")?u._befores.save.call(u,l,function(){a(l)}):a(l)},l=function(n){n=n&&e.isFunction(n)?n:function(){},u.del(this,n)},f=function(n){n=n&&e.isFunction(n)?n:function(){},u.delSilent(this,n)},u=o[r]=function(e){return e=e||{},e.save=a,e};return u._collection=[],u._id=0,u._befores={},u._afters={},u._receivers={},u._name=r,u._nextId=function(){return u._id++,u._id},u.on=function(e,i){n?n.on(e,function(e){i.call(u,e)}):(t.hasOwnProperty(u._name)||(t[u._name]={}),t[u._name][e]=i)},u.emit=function(e,t,o){n?n.emit(e,t,o):i.push([e,t,o])},u.use=function(e){e.call(u)},u.before=function(n,t){e.isString(n)?u._befores[n]=t:e.isArray(n)&&e.each(n,function(e){u._befores[e]=t})},u.beforeRemove=function(n){e.isString(n)?u._befores[n]=function(){}:e.isArray(n)&&e.each(n,function(e){u._befores[e]=function(){}})},u.after=function(n,t){e.isString(n)?u._afters[n]=t:e.isArray(n)&&e.each(n,function(e){u._afters[e]=t})},u.afterRemove=function(n){e.isString(n)?u._afters[n]=function(){}:e.isArray(n)&&e.each(n,function(e){u._afters[e]=function(){}})},u.inform=function(e,n){u._receivers[e]&&u._receivers[e].call(u,n)},u.receive=function(n,t){e.isString(n)?u._receivers[n]=t:e.isArray(n)&&e.each(n,function(e){u._receivers[e]=t})},u.receiveRemove=function(n){e.isEmpty(n)?u._receivers={}:e.isString(n)?u._receivers[n]=function(){}:e.isArray(n)&&e.each(n,function(e){u._receivers[e]=function(){}})},u.collection=function(n){if(e.isArray(n)){for(;u._collection.length>0;)u._collection.pop();for(var t=0,i=n.length;i>t;t++)n[t].__id=u._nextId(),u._collection.push(n[t])}},u.create=function(n,t){var i=e.clone(n);t=t&&e.isFunction(t)?t:function(){};var o=function(e,n){u.inform("create",n),t.call(u,e,n)},r=function(){u.createSilent(n,function(e,n){return u._afters.hasOwnProperty("create")?3==u._afters.create.length?void u._afters.create.call(u,i,n,function(){o(e,n)}):void u._afters.create.call(u,n,function(){o(e,n)}):void o(e,n)})};return u._befores.hasOwnProperty("create")?void u._befores.create.call(u,n,function(){r()}):void r()},u.createSilent=function(n,t){t=t&&e.isFunction(t)?t:function(){};var i=null;n.__id=u._nextId(),n.save=a,n.del=function(e){l.call(n,e)},n.delSilent=function(e){f.call(n,e)},n["delete"]=n.del,n.deleteSilent=n.delSilent,u._collection.push(n),t.call(u,i,n)},u.read=function(n,t){var i=e.clone(n);t=t&&e.isFunction(t)?t:function(){};var o=function(e,n){u.inform("read",n),t.call(u,e,n)},r=function(){u.readSilent(n,function(e,n){return u._afters.hasOwnProperty("read")?3==u._afters.read.length?void u._afters.read.call(u,i,n,function(){o(e,n)}):void u._afters.read.call(u,n,function(){o(e,n)}):void o(e,n)})};return u._befores.hasOwnProperty("read")?void u._befores.read.call(u,n,function(){r()}):void r()},u.readSilent=function(n,t){t=t&&e.isFunction(t)?t:function(){};var i=null,o=!1;"function"==typeof n?(t=n,o=!0):e.isEmpty(n)&&(o=!0);var r=[];r=o?u._collection:e.where(u._collection,n),e.each(r,function(e){e.save=a,e.del=function(n){l.call(e,n)},e.delSilent=function(n){f.call(e,n)},e["delete"]=e.del,e.deleteSilent=e.delSilent}),t.call(u,i,r)},u.readOne=function(n,t){var i=e.clone(n);t=t&&e.isFunction(t)?t:function(){};var o=function(e,n){u.inform("readOne",n),t.call(u,e,n)},r=function(){u.readOneSilent(n,function(e,n){return u._afters.hasOwnProperty("readOne")?3==u._afters.readOne.length?void u._afters.readOne.call(u,i,n,function(){o(e,n)}):void u._afters.readOne.call(u,n,function(){o(e,n)}):void o(e,n)})};u._befores.hasOwnProperty("readOne")?u._befores.readOne.call(u,n,function(){r()}):r()},u.readOneSilent=function(n,t){t=t&&e.isFunction(t)?t:function(){};var i=null,o=e.where(u._collection,n),r=null;o[0]&&(r=o[0],r.save=a,r.del=function(e){l.call(r,e)},r.delSilent=function(e){f.call(r,e)},r["delete"]=r.del,r.deleteSilent=r.delSilent),t.call(u,i,r)},u.update=function(n,t,i){var o=e.clone(n),r=e.clone(t);i=i&&e.isFunction(i)?i:function(){};var c=function(e,n){u.inform("update",n),i.call(u,e,n)},a=function(){u.updateSilent(n,t,function(e,n){return u._afters.hasOwnProperty("update")?4==u._afters.update.length?void u._afters.update.call(u,o,r,n,function(){c(e,n)}):void u._afters.update.call(u,n,function(){c(e,n)}):void c(e,n)})};return u._befores.hasOwnProperty("update")?void u._befores.update.call(u,n,t,function(){a()}):void a()},u.updateSilent=function(n,t,i){i=i&&e.isFunction(i)?i:function(){};var o=null,r=e.where(u._collection,n);r.length>0&&e.each(r,function(n){e.each(t,function(e,t){n[t]=e})}),i.call(u,o,r)},u.del=function(n,t){var i=e.clone(n);t=t&&e.isFunction(t)?t:function(){};var o=function(e,n){u.inform("delete",n),t.call(u,e,n)},r=function(){u.deleteSilent(n,function(e,n){return u._afters.hasOwnProperty("delete")?3==u._afters["delete"].length?void u._afters["delete"].call(u,i,n,function(){o(e,n)}):void u._afters["delete"].call(u,n,function(){o(e,n)}):void o(e,n)})};return u._befores.hasOwnProperty("delete")?void u._befores["delete"].call(u,n,function(){r()}):void r()},u.delSilent=function(n,t){t=t&&e.isFunction(t)?t:function(){};var i=null,o=e.where(u._collection,n);o.length>0&&e.each(o,function(n){var t=e.indexOf(u._collection,n);u._collection.splice(t,1)}),t.call(u,i,o)},u.find=u.read,u.findSilent=u.readSilent,u.findOne=u.readOne,u.findOneSilent=u.readOneSilent,u["delete"]=u.del,u.deleteSilent=u.delSilent,c=c&&e.isFunction(c)?c:function(){},c.call(u),u},a={};return a.VERSION="0.17.1",a.config=function(e){n=e;for(var r in t)for(var c in t[r])!function(e,t,i){n.on(t,function(n){i.call(e,n)})}(o[r],c,t[r][c]);for(var a=0,l=i.length;l>a;a++)n.emit(i[a][0],i[a][1],i[a][2]);t={}},a.model=function(e,n){return o[e]&&!n?r(e):c(e,n)},a.modelRemove=function(e){o[e]=null},a.receiveRemove=function(){for(var e in o)o[e].receiveRemove()},a}); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Marilyn 2 | --- 3 | 4 | [![Bower version](https://badge.fury.io/bo/marilyn.svg)](http://badge.fury.io/bo/marilyn) 5 | 6 | 7 | 8 | Marilyn is a client side, WebSocket driven, Pub/Sub, model layer with a query system similar to Mongoose. 9 | 10 | Angular, Backbone, Ember, and many other libraries provide model layers which are AJAX driven. While variations on these models exist they are usually only useful if you're using the entire framework they are built to work with. 11 | 12 | Marilyn can work with any framework, or by itself if you just need more data abstraction. 13 | 14 | ## Installation 15 | 16 | Install the module with bower: 17 | 18 | ```bash 19 | $ bower install marilyn 20 | ``` 21 | 22 | Or [download it from GitHub](https://github.com/alanjames1987/marilyn/archive/master.zip), extract it, and copy the files into your application. 23 | 24 | ### Intalling with Script Tags 25 | 26 | Include a script for the `marilyn.js` or `marilyn-min.js` file after a script for it's dependency, `underscore.js`. 27 | 28 | Upon including the `marilyn.js` file a global `marilyn` object will be available. 29 | 30 | ### Intalling with AMD 31 | 32 | Marilyn can also be loaded using any AMD compliant module loader such as [RequireJS](http://www.requirejs.org/). 33 | 34 | Marilyn's only dependency is `underscore`. 35 | 36 | When using Marilyn with AMD you MUST load the models before using them inside controllers. This can be done as follows. 37 | 38 | ```js 39 | 40 | require(['model/myModel'], function(myModel) { 41 | 42 | // configure Marilyn and other core libraries 43 | 44 | require(['controller/myController'], function(myController) { 45 | 46 | // you can now use the model in your controller 47 | 48 | }); 49 | 50 | }); 51 | 52 | ``` 53 | 54 | ## Usage 55 | 56 | ### Configure Socket.IO 57 | 58 | Before using Marilyn with Socket.IO you have to configure Marilyn to use Socket.IO's socket connection. Lets use the client side example connection from the Socket.IO website to demonstrate this. 59 | 60 | ```html 61 | 62 | 65 | ``` 66 | 67 | This creates a global variable called `socket`. This variable should be passed to Marilyn so Marilyn can use the Socket.IO `on` and `emit` methods. 68 | 69 | ```html 70 | 73 | ``` 74 | 75 | After this Marilyn has `on` and `emit` methods that we should call instead of the Socket.IO methods. This allows us to centralize all data querying and data fetching methods to Marilyn. 76 | 77 | ### Creating Model 78 | 79 | Like Mongoose, Marilyn creates models using the `model` method. 80 | 81 | ```js 82 | var MyModel = marilyn.model('someModelName'); 83 | ``` 84 | 85 | `MyModel` is now a Marilyn model, containing query and event methods. 86 | 87 | You can also create a model by passing the `marilyn.model` methods a second parameter, a callback function. Within this callback function `this` represents the model that has been created. 88 | 89 | ```js 90 | var MyModel = marilyn.model('someModelName', function(){ 91 | 92 | // "this" is the same as MyModel 93 | 94 | }); 95 | ``` 96 | 97 | Like Mongoose, the Marilyn model created, called `someModelName`, can now be accessed from the global `marilyn` object. 98 | 99 | This allows you to use self executing functions to create a model and not pollute the global scope. 100 | 101 | ```js 102 | // myModel.js 103 | 104 | (function(){ 105 | 106 | var NonPollutingModel = marilyn.model('someModelName', function(){ 107 | 108 | this.on('someSocketEvent', function(data){ 109 | // do something with data in model 110 | this.inform('someBrowserEvent', data); 111 | }); 112 | 113 | }); 114 | 115 | NonPollutingModel.on('someOtherSocketEvent', function(data){ 116 | // do something else 117 | }); 118 | 119 | })(); 120 | ``` 121 | 122 | ```js 123 | // myController.js 124 | 125 | (function(){ 126 | 127 | var MyModel = marilyn.model('someModelName'); 128 | 129 | MyModel.receive('someBrowserEvent', function(){ 130 | // do something with data in controller 131 | }); 132 | 133 | })(); 134 | ``` 135 | 136 | ### Adding Data to Models 137 | 138 | All models have a `_collection` variable. 139 | 140 | **Setting the `_collection` variable directly without the CRUD methods or the collection setter will not create this `__id` property and Marilyn will not function properly.** 141 | 142 | This variable is an array of all the objects you have stored in your frontend model. 143 | 144 | To populate this variable you can use the built in CRUD methods listed below, or the `collection` setter. 145 | 146 | If you use the CRUD methods various built in callbacks will be run. If you use the `collection` setter these callback functions won't be called. 147 | 148 | ```js 149 | marilyn.model('someModelName', function(){ 150 | 151 | this.on('someSocketEvent', function(data){ 152 | 153 | // sets the _collection array 154 | // this won't trigger any callbacks 155 | this.collection(data); 156 | 157 | }); 158 | 159 | this.on('someOtherServerEvent', function(data){ 160 | 161 | // pushes a new object into the _collection array and performs many other tasks 162 | // this will trigger all the "create" callbacks 163 | this.create(data); 164 | 165 | }); 166 | 167 | }); 168 | ``` 169 | 170 | When new objects are added to the `_collection` variable a property of `__id` is added to them so Marilyn can internally track them. 171 | 172 | ### Event Handlers 173 | 174 | Marilyn has four types of event handlers, socket events, browser events, befores, and afters. 175 | 176 | Socket events are for communicating from your model to a socket server, or from a socket server to your model. 177 | 178 | Browser events are for communicating between your model and controller, or client side logic layer. 179 | 180 | Befores run before a query method is executed. 181 | 182 | Afters run after a query method is executed. 183 | 184 | Socket events and browser events have two methods, an event listener and an event dispatcher. 185 | 186 | #### Socket Events 187 | 188 | The socket event methods behave the same events as Socket.IO. 189 | 190 | They are `on` and `emit`. 191 | 192 | Refere to [Socket.IO documentation](http://socket.io/) to understand how these work. 193 | 194 | #### Browser Events 195 | 196 | Browser event methods are `receive` and `inform`. They act very similarly to Socket.IO's `on` and `emit`. 197 | 198 | They can send data and receive data with callback functions. 199 | 200 | ```js 201 | // myModel.js 202 | 203 | marilyn.model('someModelName', function(){ 204 | 205 | this.inform('modelReady', { 206 | 'someKey':'someValue' 207 | }); 208 | 209 | }); 210 | ``` 211 | 212 | ```js 213 | // myController.js 214 | 215 | var MyModel = marilyn.model('someModelName'); 216 | 217 | MyModel.receive('modelReady', function(data){ 218 | // do something here 219 | // data is the object passed from the inform method 220 | }); 221 | ``` 222 | 223 | All query events inform receivers after completion. This is best shown in the next example. 224 | 225 | #### Befores and Afters 226 | 227 | Befores and afters are similar to Mongoose's `pre` and `post` events. Befores are triggered before all querys, and afters are after the query. 228 | 229 | Befores and afters are triggered from `save`, `create`, `read`, `readOne`, `update`, and `delete`. 230 | 231 | Sometimes multiple befores and afters can be triggered by one CRUD method being invoked, for example the `save` method can trigger befores and afters for `create` and `update` in addition to befores and afters for `save` events. 232 | 233 | To create a before or after you use the `before` or `after` method passing it two parameters 234 | 235 | The first parameter is either a string of a single event name (listed above) or an array of strings of event names. 236 | 237 | The second paramter is a callback function. 238 | 239 | All callbacks of befores and afters are passed data that they can manipulate and a next method, which must be called in order to progress the flow control. Callbacks are passed different sets of data depending on the event being listened for, which are listed below. 240 | 241 | ##### Save / Create 242 | 243 | **before callback** 244 | ```js 245 | data:Object, next:Function 246 | ``` 247 | `data` is the object being created. It is useful for validating data or adding default fields to objects. 248 | 249 | **after callback** 250 | ```js 251 | data:Object, next:Function 252 | ``` 253 | `data` is the object that has been created. It is useful for altering data before it's returned to the callback or syncing the data with a server. 254 | 255 | ##### Read 256 | 257 | **before callback** 258 | ```js 259 | query:Object, next:Function 260 | ``` 261 | `query` is what is being used to search for records. It is useful for restricting access to records. 262 | 263 | **after callback** 264 | ```js 265 | data:Array, next:Function 266 | ``` 267 | `data` is an array of objects found from the query. It is useful for adding custom fields or methods. 268 | 269 | ##### Read One 270 | 271 | **before callback** 272 | ```js 273 | query:Object, next:Function 274 | ``` 275 | `query` is the query being used to read the one object. It is useful for restricting access to records. 276 | 277 | **after callback** 278 | ```js 279 | data:Object, next:Function 280 | ``` 281 | `data` is an the single object found from the query. It is useful for adding custom fields and methods. 282 | 283 | ##### Update 284 | 285 | **before callback** 286 | ```js 287 | searchQuery:Object, updateQuery:Object, next:Function 288 | ``` 289 | `searchQuery` is the query being used to read the objects. It is useful for restricting access to records. 290 | `updateQuery` is the changes that will be made to all objects found. It is useful for restricting access to certain field updates. 291 | 292 | **after callback** 293 | ```js 294 | data:Array, next:Function 295 | ``` 296 | `data` is an array of objects that have been updated. It is useful for adding custom fields and methods or syncing with a server. 297 | 298 | ##### Delete 299 | 300 | **before callback** 301 | ```js 302 | searchQuery:Object, next:Function 303 | ``` 304 | `searchQuery` is the query being used to read the objects. It is useful for restricting access to records. 305 | 306 | **after callback** 307 | ```js 308 | data:Array, next:Function 309 | ``` 310 | `data` is an array of objects that have been deleted. It is useful for syncing with a server. 311 | 312 | ### Querying Data with CRUD Methods 313 | 314 | Each Marilyn model has a private variable called `_collection`, which can be populated with an array of data. All query methods query this variable. 315 | 316 | There are ten query methods, `create`, `createSilent`, `read`, `readSilent`, `readOne`, `readOneSilent`, `update`, `updateSilent`, `del`, and `delSilent`. 317 | 318 | All silent query methods don't trigger befores or afters. 319 | 320 | #### Create 321 | 322 | ```js 323 | var myModel = new MyModel(); 324 | myModel.title = 'Star Wars'; 325 | myModel.director = 'George Lucas'; 326 | 327 | // calling the save method will trigger create befores and afters as well as save befores and afters 328 | myModel.save(function(err, result){ 329 | // result is the object created 330 | }); 331 | 332 | // OR 333 | 334 | var myModel = new MyModel({ 335 | 'title':'Star Wars', 336 | 'director':'George Lucas' 337 | }); 338 | 339 | // calling the save method will trigger create befores and afters as well as save befores and afters 340 | myModel.save(function(err, result){ 341 | // result is the object created 342 | }); 343 | 344 | // OR 345 | 346 | // calling the create method will trigger create befores and afters, but not saves 347 | MyModel.create({ 348 | 'title':'Star Wars', 349 | 'director':'George Lucas' 350 | }, function(err, result){ 351 | // result is the object created 352 | }); 353 | ``` 354 | 355 | #### Read 356 | 357 | ```js 358 | // calling the read method will trigger read befores and afters 359 | // calling read without passing a query will read all 360 | MyModel.read(function(err, results){ 361 | // results is an array of all the objects in the collection 362 | }); 363 | 364 | // calling the read method will trigger read befores and afters 365 | MyModel.read({ 366 | 'director':'George Lucas' 367 | }, function(err, results){ 368 | // results is an array of all the objects found 369 | }); 370 | 371 | // calling this method will trigger read befores and afters 372 | MyModel.read({ 373 | 'director':'George Lucas' 374 | }, function(err, results){ 375 | // results is an array of all the objects found 376 | }); 377 | 378 | // calling this method will trigger readOne befores and afters 379 | MyModel.readOne({ 380 | 'id':1138 381 | }, function(err, result){ 382 | // result is the single object found 383 | }); 384 | ``` 385 | 386 | #### Update 387 | 388 | ```js 389 | // updates using readOne 390 | MyModel.readOne({ 391 | 'id':1138 392 | }, function(err, result){ 393 | 394 | result.director = 'George Lucas'; 395 | 396 | // calling the save method will trigger update befores and afters as well as save befores and afters 397 | result.save(function(err, result){ 398 | // result is the updated object 399 | }); 400 | 401 | }); 402 | 403 | // calling this method will trigger update befores and afters 404 | MyModel.update({ 405 | 'id':1138 406 | }, { 407 | 'propertyToUpdate':'someValue' 408 | }, function(err, results){ 409 | // results is an array of all the objects updated 410 | }); 411 | ``` 412 | 413 | #### Delete 414 | 415 | ```js 416 | // calling this method will trigger delete befores and afters 417 | MyModel.del({ 418 | 'id':1138 419 | }, function(err results){ 420 | // results is an array of all the objects deleted 421 | }); 422 | ``` 423 | 424 | `err` is always populated if nothing matches the query. 425 | 426 | Query methods don't directly call the server, you must call the server manually with `emit` either before or after query methods are invoked. This makes `before` and `after` very useful for server integration. 427 | 428 | Dependencies 429 | --- 430 | 431 | Marilyn requires Underscore >= 1.5.0. Get it from: [http://underscorejs.org/](http://underscorejs.org/) 432 | 433 | Author 434 | --- 435 | 436 | Alan James: [alanjames1987@gmail.com](mailto:alanjames1987@gmail.com) 437 | 438 | License 439 | --- 440 | 441 | Licensed under [MIT](https://github.com/alanjames1987/marilyn/blob/master/LICENSE). 442 | -------------------------------------------------------------------------------- /lib/marilyn.js: -------------------------------------------------------------------------------- 1 | (function(factory) { 2 | 3 | // define marilyn using AMD 4 | if (typeof define === 'function' && define.amd) { 5 | define(['underscore'], factory); 6 | } 7 | 8 | // define marilyn by adding it to the window object 9 | else { 10 | window.Marilyn = window.marilyn = factory(_); 11 | } 12 | 13 | })(function(_) { 14 | 15 | // _socketConnection should be shared between all the models 16 | // it's a single connection to the server 17 | var _socketConnection; 18 | 19 | // the socket.io on function isn't avalible until Marilyn gets the socketConnection 20 | // after it gets this connection all the on events need to get attached to socket.io 21 | var _onEventBuffer = {}; 22 | 23 | // the socket.io emit function isn't avalible until Marilyn gets the socketConnection 24 | // after it gets this connection all the emit events need to get sent to socket.io 25 | var _emitEventBuffer = []; 26 | 27 | // the is where all the models will be stored so getters can be used to retrieve them 28 | var _models = {}; 29 | 30 | // get a Model from the global Marilyn object 31 | var _modelGet = function(modelName) { 32 | return _models[modelName]; 33 | }; 34 | 35 | // create a Model and assign it to the global Marilyn object 36 | var _modelSet = function(modelName, init) { 37 | 38 | // this is for saving changes to the result generated by read and readOne 39 | var _resultSave = function(callback) { 40 | 41 | // check if the callback is a valid function 42 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 43 | 44 | // saveType is used to determine which receiver to fire 45 | var saveType; 46 | 47 | var runCallback = function(err, result) { 48 | 49 | // this is needed to send an object 50 | if (saveType === 'create') { 51 | Model.inform('create', result); 52 | } 53 | 54 | // this is needed to send an array 55 | else if (saveType === 'update') { 56 | Model.inform('update', [result]); 57 | } 58 | 59 | Model.inform('save', result); 60 | 61 | // call the callback that was passed into the save 62 | callback.call(Model, err, result); 63 | 64 | }; 65 | 66 | var runSaves = function(err, result) { 67 | 68 | // check if there are any afters assigned to the save 69 | if (Model._afters.hasOwnProperty('save')) { 70 | 71 | // call the after save callback 72 | Model._afters.save.call(Model, result, function() { 73 | runCallback(err, result); 74 | }); 75 | 76 | } else { 77 | runCallback(err, result); 78 | } 79 | 80 | }; 81 | 82 | var runUpdateComplete = function(object) { 83 | 84 | Model.updateSilent({ 85 | '__id': object.__id 86 | }, object, function(err, results) { 87 | 88 | var result = results[0]; 89 | 90 | // check if there are any afters assigned to the update 91 | if (Model._afters.hasOwnProperty('update')) { 92 | 93 | // call the adfter update callback 94 | Model._afters.update.call(Model, results, function() { 95 | runSaves(err, result); 96 | }); 97 | 98 | } else { 99 | runSaves(err, result); 100 | } 101 | 102 | }); 103 | 104 | }; 105 | 106 | var runCreateComplete = function(object) { 107 | 108 | Model.createSilent(object, function(err, result) { 109 | 110 | // check if there are any afters assigned to the create 111 | if (Model._afters.hasOwnProperty('create')) { 112 | 113 | // call the after create callback 114 | Model._afters.create.call(Model, object, function() { 115 | runSaves(err, result); 116 | }); 117 | 118 | } else { 119 | runSaves(err, result); 120 | } 121 | 122 | }); 123 | 124 | }; 125 | 126 | // run the correct CRUD method 127 | var runCRUD = function(object) { 128 | 129 | // CREATE 130 | // this object is being newly created 131 | if (!object.hasOwnProperty('__id')) { 132 | 133 | saveType = 'create'; 134 | 135 | // check if there are any befores assigned to the create 136 | if (Model._befores.hasOwnProperty('create')) { 137 | 138 | // call the before create callback 139 | Model._befores.create.call(Model, object, function() { 140 | runCreateComplete(object); 141 | }); 142 | 143 | } else { 144 | runCreateComplete(object); 145 | } 146 | 147 | } 148 | 149 | // UPDATE 150 | // check if the object has come from a read and needs to be updated 151 | else { 152 | 153 | saveType = 'update'; 154 | 155 | // check if there are any befores assigned to the update 156 | if (Model._befores.hasOwnProperty('update')) { 157 | 158 | // call the before update callback 159 | Model._befores.update.call(Model, { 160 | '__id': object.__id 161 | }, object, function() { 162 | runUpdateComplete(object); 163 | }); 164 | 165 | } else { 166 | runUpdateComplete(object); 167 | } 168 | 169 | } 170 | 171 | }; 172 | 173 | // "this" is the result or a new object being created 174 | var _this = this || {}; 175 | 176 | // the save can save a new or existing object using the create or update methods 177 | // this should trigger the correct befores and afters 178 | // it should always trigger all the before saves and after saves regardless of it being a create or update 179 | 180 | // check if there are any befores assigned to the save 181 | if (Model._befores.hasOwnProperty('save')) { 182 | 183 | // call the before save callback 184 | Model._befores.save.call(Model, _this, function() { 185 | runCRUD(_this); 186 | }); 187 | 188 | } else { 189 | runCRUD(_this); 190 | } 191 | 192 | }; 193 | 194 | var _resultDelete = function(callback) { 195 | 196 | // check if the callback is a valid function 197 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 198 | 199 | // "this" will always reference the item being deleted 200 | Model.del(this, callback); 201 | 202 | }; 203 | 204 | var _resultDeleteSilent = function(callback) { 205 | 206 | // check if the callback is a valid function 207 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 208 | 209 | // "this" will always reference the item 210 | Model.delSilent(this, callback); 211 | 212 | }; 213 | 214 | // this Model function will be what contains all the CRUD functions 215 | // it is an function so we can use the "new" keyword to create one 216 | var Model = _models[modelName] = function(object) { 217 | 218 | object = object || {}; 219 | 220 | // add additional methods to the object 221 | object.save = _resultSave; 222 | 223 | // we don't need to add the delete methods 224 | // they get added when the save method is called 225 | 226 | return object; 227 | 228 | }; 229 | 230 | // PRIVATE PROPERTIES 231 | 232 | // setup the private variables of the Model 233 | // collection will store all the data in the Model 234 | Model._collection = []; 235 | Model._id = 0; 236 | 237 | Model._befores = {}; 238 | Model._afters = {}; 239 | 240 | Model._receivers = {}; 241 | 242 | Model._name = modelName; 243 | 244 | // PRIVATE METHODS 245 | 246 | Model._nextId = function() { 247 | Model._id++; 248 | return Model._id; 249 | }; 250 | 251 | // PUBLIC METHODS 252 | 253 | // Socket.IO events 254 | 255 | Model.on = function(eventType, callback) { 256 | 257 | // if the socketConnection is not setup the ons need to get buffered and applied later 258 | if (!_socketConnection) { 259 | 260 | // set the Model name property if not already set 261 | if (!_onEventBuffer.hasOwnProperty(Model._name)) { 262 | _onEventBuffer[Model._name] = {}; 263 | } 264 | 265 | // set the callback into the onBuffer object 266 | _onEventBuffer[Model._name][eventType] = callback; 267 | 268 | } else { 269 | 270 | _socketConnection.on(eventType, function(data) { 271 | callback.call(Model, data); 272 | }); 273 | 274 | } 275 | 276 | }; 277 | 278 | Model.emit = function(eventType, data, data2) { 279 | 280 | // if the socketConnection is not setup the emits need to get buffered and sent later 281 | if (!_socketConnection) { 282 | 283 | // push the event and data into the emitBuffer object 284 | _emitEventBuffer.push([eventType, data, data2]); 285 | 286 | } else { 287 | 288 | _socketConnection.emit(eventType, data, data2); 289 | 290 | } 291 | 292 | }; 293 | 294 | // internal events 295 | 296 | Model.use = function(pluginCallback) { 297 | // call the plugin callback 298 | pluginCallback.call(Model); 299 | }; 300 | 301 | // listen for a before event 302 | Model.before = function(eventType, callback) { 303 | 304 | // the event was a single event 305 | // no need to setup multiple 306 | if (_.isString(eventType)) { 307 | Model._befores[eventType] = callback; 308 | } 309 | 310 | // multiple events were passed in 311 | else if (_.isArray(eventType)) { 312 | _.each(eventType, function(key) { 313 | Model._befores[key] = callback; 314 | }); 315 | } 316 | 317 | }; 318 | 319 | // remove a before event listener 320 | Model.beforeRemove = function(eventType) { 321 | 322 | // the event was a single event 323 | // no need to remove multiple 324 | if (_.isString(eventType)) { 325 | Model._befores[eventType] = function() {}; 326 | } 327 | 328 | // multiple events were passed in 329 | else if (_.isArray(eventType)) { 330 | _.each(eventType, function(key) { 331 | Model._befores[key] = function() {}; 332 | }); 333 | } 334 | 335 | }; 336 | 337 | // listen for an after event 338 | Model.after = function(eventType, callback) { 339 | 340 | // the event was a single event 341 | // no need to setup multiple 342 | if (_.isString(eventType)) { 343 | Model._afters[eventType] = callback; 344 | } 345 | 346 | // multiple events were passed in 347 | else if (_.isArray(eventType)) { 348 | _.each(eventType, function(key) { 349 | Model._afters[key] = callback; 350 | }); 351 | } 352 | 353 | }; 354 | 355 | // remove an after event listener 356 | Model.afterRemove = function(eventType) { 357 | 358 | // the event was a single event 359 | // no need to remove multiple 360 | if (_.isString(eventType)) { 361 | Model._afters[eventType] = function() {}; 362 | } 363 | 364 | // multiple events were passed in 365 | else if (_.isArray(eventType)) { 366 | _.each(eventType, function(key) { 367 | Model._afters[key] = function() {}; 368 | }); 369 | } 370 | 371 | }; 372 | 373 | // trigger a receive 374 | Model.inform = function(eventType, data) { 375 | if (Model._receivers[eventType]) { 376 | // call the receiver callback 377 | Model._receivers[eventType].call(Model, data); 378 | } 379 | }; 380 | 381 | // listen for an inform 382 | Model.receive = function(eventType, callback) { 383 | 384 | // the event was a single event 385 | // no need to setup multiple 386 | if (_.isString(eventType)) { 387 | Model._receivers[eventType] = callback; 388 | } 389 | 390 | // multiple events were passed in 391 | else if (_.isArray(eventType)) { 392 | _.each(eventType, function(key) { 393 | Model._receivers[key] = callback; 394 | }); 395 | } 396 | 397 | }; 398 | 399 | // remove a receive event listener 400 | Model.receiveRemove = function(eventType) { 401 | 402 | // remove all receivers if the event type is not specified 403 | if (_.isEmpty(eventType)) { 404 | 405 | Model._receivers = {}; 406 | 407 | } 408 | 409 | // remove specific receivers 410 | else { 411 | 412 | // the event was a single event 413 | // no need to remove multiple 414 | if (_.isString(eventType)) { 415 | Model._receivers[eventType] = function() {}; 416 | } 417 | 418 | // multiple events were passed in 419 | else if (_.isArray(eventType)) { 420 | _.each(eventType, function(key) { 421 | Model._receivers[key] = function() {}; 422 | }); 423 | } 424 | 425 | } 426 | 427 | }; 428 | 429 | // query methods 430 | 431 | // replaces the internal collection with a new array of objects 432 | // this will add an __id property 433 | // it will also change the collection in a data binding friendly way 434 | Model.collection = function(collection) { 435 | 436 | // check if the collection is an array 437 | if (!_.isArray(collection)) { 438 | // TODO throw error 439 | return; 440 | } 441 | 442 | // replace the collection like this for data binding 443 | while (Model._collection.length > 0) { 444 | Model._collection.pop(); 445 | } 446 | 447 | for (var i = 0, j = collection.length; i < j; i++) { 448 | collection[i].__id = Model._nextId(); 449 | Model._collection.push(collection[i]); 450 | } 451 | 452 | }; 453 | 454 | // CREATE 455 | 456 | Model.create = function(element, callback) { 457 | 458 | // store a clone of the query for later reference 459 | var originalQuery = _.clone(element); 460 | 461 | // check if the callback is a valid function 462 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 463 | 464 | var runCallback = function(err, result) { 465 | 466 | Model.inform('create', result); 467 | 468 | // call the callback that was passed into the create 469 | callback.call(Model, err, result); 470 | 471 | }; 472 | 473 | var runComplete = function() { 474 | 475 | Model.createSilent(element, function(err, result) { 476 | 477 | // check if there are any afters assigned to the create 478 | if (!Model._afters.hasOwnProperty('create')) { 479 | runCallback(err, result); 480 | return; 481 | } 482 | 483 | // if the function is expecting 3 arguments pass the query, result, and callback 484 | if (Model._afters.create.length == 3) { 485 | Model._afters.create.call(Model, originalQuery, result, function() { 486 | runCallback(err, result); 487 | }); 488 | return; 489 | } 490 | 491 | // if the function is expecting 2 arguments only pass the result and callback 492 | Model._afters.create.call(Model, result, function() { 493 | runCallback(err, result); 494 | }); 495 | 496 | }); 497 | 498 | }; 499 | 500 | // check if there are any befores assigned to the create 501 | if (!Model._befores.hasOwnProperty('create')) { 502 | runComplete(); 503 | return; 504 | } 505 | 506 | // call the before create callback 507 | Model._befores.create.call(Model, element, function() { 508 | runComplete(); 509 | }); 510 | 511 | }; 512 | 513 | Model.createSilent = function(element, callback) { 514 | 515 | // check if the callback is a valid function 516 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 517 | 518 | var err = null; 519 | 520 | // set the internal id using the Model's next id 521 | element.__id = Model._nextId(); 522 | 523 | // allow the returned object to be saved in the future 524 | element.save = _resultSave; 525 | 526 | element.del = function(callback) { 527 | _resultDelete.call(element, callback); 528 | }; 529 | 530 | element.delSilent = function(callback) { 531 | _resultDeleteSilent.call(element, callback); 532 | }; 533 | 534 | element['delete'] = element.del; 535 | element.deleteSilent = element.delSilent; 536 | 537 | Model._collection.push(element); 538 | 539 | // call the callback that was passed into the createSilent 540 | callback.call(Model, err, element); 541 | 542 | }; 543 | 544 | // READ 545 | 546 | Model.read = function(query, callback) { 547 | 548 | // store a clone of the query for later reference 549 | var originalQuery = _.clone(query); 550 | 551 | // check if the callback is a valid function 552 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 553 | 554 | var runCallback = function(err, results) { 555 | 556 | Model.inform('read', results); 557 | 558 | // call the callback that was passed into the read 559 | callback.call(Model, err, results); 560 | 561 | }; 562 | 563 | var runComplete = function() { 564 | 565 | Model.readSilent(query, function(err, results) { 566 | 567 | // check if there are any afters assigned to the read 568 | if (!Model._afters.hasOwnProperty('read')) { 569 | runCallback(err, results); 570 | return; 571 | } 572 | 573 | // if the function is expecting 3 arguments pass the query, results, and callback 574 | if (Model._afters.read.length == 3) { 575 | Model._afters.read.call(Model, originalQuery, results, function() { 576 | runCallback(err, results); 577 | }); 578 | return; 579 | } 580 | 581 | // if the after function is expecting anything else send back the expected result and a callback 582 | Model._afters.read.call(Model, results, function() { 583 | runCallback(err, results); 584 | }); 585 | 586 | }); 587 | 588 | }; 589 | 590 | // check if there are any befores assigned to the read 591 | if (!Model._befores.hasOwnProperty('read')) { 592 | runComplete(); 593 | return; 594 | } 595 | 596 | // call the before read callback 597 | Model._befores.read.call(Model, query, function() { 598 | runComplete(); 599 | }); 600 | 601 | }; 602 | 603 | Model.readSilent = function(query, callback) { 604 | 605 | // check if the callback is a valid function 606 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 607 | 608 | var err = null; 609 | 610 | var readAll = false; 611 | 612 | // if no query was passed 613 | if (typeof query === 'function') { 614 | callback = query; 615 | readAll = true; 616 | } 617 | 618 | // or if the query object was empty 619 | else if (_.isEmpty(query)) { 620 | readAll = true; 621 | } 622 | 623 | var results = []; 624 | 625 | if (readAll) { 626 | results = Model._collection; 627 | } else { 628 | results = _.where(Model._collection, query); 629 | } 630 | 631 | // add additional methods to the results 632 | _.each(results, function(result) { 633 | 634 | result.save = _resultSave; 635 | 636 | result.del = function(callback) { 637 | _resultDelete.call(result, callback); 638 | }; 639 | 640 | result.delSilent = function(callback) { 641 | _resultDeleteSilent.call(result, callback); 642 | }; 643 | 644 | result['delete'] = result.del; 645 | result.deleteSilent = result.delSilent; 646 | 647 | }); 648 | 649 | // call the callback that was passed into the readSilent 650 | callback.call(Model, err, results); 651 | 652 | }; 653 | 654 | Model.readOne = function(query, callback) { 655 | 656 | // store a clone of the query for later reference 657 | var originalQuery = _.clone(query); 658 | 659 | // check if the callback is a valid function 660 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 661 | 662 | var runCallback = function(err, result) { 663 | 664 | Model.inform('readOne', result); 665 | 666 | // call the callback that was passed into the readOne 667 | callback.call(Model, err, result); 668 | 669 | }; 670 | 671 | var runComplete = function() { 672 | 673 | Model.readOneSilent(query, function(err, result) { 674 | 675 | // check if there are any afters assigned to the readOne 676 | if (!Model._afters.hasOwnProperty('readOne')) { 677 | runCallback(err, result); 678 | return; 679 | } 680 | 681 | // if the function is expecting 3 arguments pass the query, result, and callback 682 | if (Model._afters.readOne.length == 3) { 683 | Model._afters.readOne.call(Model, originalQuery, result, function() { 684 | runCallback(err, result); 685 | }); 686 | return; 687 | } 688 | 689 | Model._afters.readOne.call(Model, result, function() { 690 | runCallback(err, result); 691 | }); 692 | 693 | }); 694 | 695 | }; 696 | 697 | // check if there are any befores assigned to the readOne 698 | if (Model._befores.hasOwnProperty('readOne')) { 699 | 700 | // call the before readOne callback 701 | Model._befores.readOne.call(Model, query, function() { 702 | runComplete(); 703 | }); 704 | 705 | } else { 706 | runComplete(); 707 | } 708 | 709 | }; 710 | 711 | Model.readOneSilent = function(query, callback) { 712 | 713 | // check if the callback is a valid function 714 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 715 | 716 | var err = null; 717 | 718 | var results = _.where(Model._collection, query); 719 | 720 | var result = null; 721 | 722 | if (results[0]) { 723 | 724 | result = results[0]; 725 | 726 | // add additonal methods to the result 727 | result.save = _resultSave; 728 | 729 | result.del = function(callback) { 730 | _resultDelete.call(result, callback); 731 | }; 732 | 733 | result.delSilent = function(callback) { 734 | _resultDeleteSilent.call(result, callback); 735 | }; 736 | 737 | result['delete'] = result.del; 738 | result.deleteSilent = result.delSilent; 739 | 740 | } 741 | 742 | // call the callback that was passed into the readOneSilent 743 | callback.call(Model, err, result); 744 | 745 | }; 746 | 747 | // UPDATE 748 | 749 | Model.update = function(query, changes, callback) { 750 | 751 | // store a clone of the query for later reference 752 | var originalQuery = _.clone(query); 753 | var originalChanges = _.clone(changes); 754 | 755 | // check if the callback is a valid function 756 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 757 | 758 | var runCallback = function(err, results) { 759 | 760 | Model.inform('update', results); 761 | 762 | // call the callback that was passed into the update 763 | callback.call(Model, err, results); 764 | 765 | }; 766 | 767 | var runComplete = function() { 768 | 769 | Model.updateSilent(query, changes, function(err, results) { 770 | 771 | // check if there are any afters assigned to the update 772 | if (!Model._afters.hasOwnProperty('update')) { 773 | runCallback(err, results); 774 | return; 775 | } 776 | 777 | // if the function is expecting 4 arguments pass the err, query, results, and callback 778 | if (Model._afters.update.length == 4) { 779 | Model._afters.update.call(Model, originalQuery, originalChanges, results, function() { 780 | runCallback(err, results); 781 | }); 782 | return; 783 | } 784 | 785 | // if the after function is expecting anything else send back the expected result and a callback 786 | Model._afters.update.call(Model, results, function() { 787 | runCallback(err, results); 788 | }); 789 | 790 | }); 791 | 792 | }; 793 | 794 | // check if there are any befores assigned to the update 795 | if (!Model._befores.hasOwnProperty('update')) { 796 | runComplete(); 797 | return; 798 | } 799 | 800 | // call the before update callback 801 | Model._befores.update.call(Model, query, changes, function() { 802 | runComplete(); 803 | }); 804 | 805 | }; 806 | 807 | Model.updateSilent = function(query, changes, callback) { 808 | 809 | // check if the callback is a valid function 810 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 811 | 812 | var err = null; 813 | 814 | var results = _.where(Model._collection, query); 815 | 816 | if (results.length > 0) { 817 | 818 | _.each(results, function(element) { 819 | 820 | _.each(changes, function(value, key) { 821 | element[key] = value; 822 | }); 823 | 824 | }); 825 | 826 | } 827 | 828 | // call the callback that was passed into the updateSilent 829 | callback.call(Model, err, results); 830 | 831 | }; 832 | 833 | // DELETE 834 | 835 | Model.del = function(query, callback) { 836 | 837 | // store a clone of the query for later reference 838 | var originalQuery = _.clone(query); 839 | 840 | // check if the callback is a valid function 841 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 842 | 843 | var runCallback = function(err, results) { 844 | 845 | Model.inform('delete', results); 846 | 847 | // call the callback that was passed into the del 848 | callback.call(Model, err, results); 849 | 850 | }; 851 | 852 | var runComplete = function() { 853 | 854 | Model.deleteSilent(query, function(err, results) { 855 | 856 | // check if there are any afters assigned to the delete 857 | if (!Model._afters.hasOwnProperty('delete')) { 858 | runCallback(err, results); 859 | return; 860 | } 861 | 862 | // if the function is expecting 3 arguments pass the query, results, and callback 863 | if (Model._afters['delete'].length == 3) { 864 | Model._afters['delete'].call(Model, originalQuery, results, function() { 865 | runCallback(err, results); 866 | }); 867 | return; 868 | } 869 | 870 | Model._afters['delete'].call(Model, results, function() { 871 | runCallback(err, results); 872 | }); 873 | 874 | 875 | }); 876 | 877 | }; 878 | 879 | // check if there are any befores assigned to the delete 880 | if (!Model._befores.hasOwnProperty('delete')) { 881 | runComplete(); 882 | return; 883 | } 884 | 885 | // call the before delete callback 886 | Model._befores['delete'].call(Model, query, function() { 887 | runComplete(); 888 | }); 889 | 890 | }; 891 | 892 | Model.delSilent = function(query, callback) { 893 | 894 | // check if the callback is a valid function 895 | callback = (callback && _.isFunction(callback)) ? callback : function() {}; 896 | 897 | var err = null; 898 | 899 | var results = _.where(Model._collection, query); 900 | 901 | if (results.length > 0) { 902 | 903 | _.each(results, function(element) { 904 | 905 | var index = _.indexOf(Model._collection, element); 906 | Model._collection.splice(index, 1); 907 | 908 | }); 909 | 910 | } 911 | 912 | // call the callback that was passed into the delSilent 913 | callback.call(Model, err, results); 914 | 915 | }; 916 | 917 | // aliases 918 | Model.find = Model.read; 919 | Model.findSilent = Model.readSilent; 920 | Model.findOne = Model.readOne; 921 | Model.findOneSilent = Model.readOneSilent; 922 | 923 | Model['delete'] = Model.del; 924 | Model.deleteSilent = Model.delSilent; 925 | 926 | // run the callback init function if it was passed passing the Model as the "this" 927 | init = (init && _.isFunction(init)) ? init : function() {}; 928 | init.call(Model); 929 | 930 | return Model; 931 | 932 | }; 933 | 934 | // create the marilyn object 935 | var marilyn = {}; 936 | 937 | marilyn.VERSION = '0.17.1'; 938 | 939 | marilyn.config = function(socketConnection) { 940 | 941 | _socketConnection = socketConnection; 942 | 943 | // setup the ons 944 | for (var model in _onEventBuffer) { 945 | 946 | for (var eventType in _onEventBuffer[model]) { 947 | 948 | // this has to be a self executing function to retain scope through dependency injection 949 | (function(model, eventType, callback) { 950 | 951 | _socketConnection.on(eventType, function(data) { 952 | callback.call(model, data); 953 | }); 954 | 955 | })(_models[model], eventType, _onEventBuffer[model][eventType]); 956 | 957 | } 958 | 959 | } 960 | 961 | // send the emits 962 | for (var i = 0, j = _emitEventBuffer.length; i < j; i++) { 963 | _socketConnection.emit(_emitEventBuffer[i][0], _emitEventBuffer[i][1], _emitEventBuffer[i][2]); 964 | } 965 | 966 | _onEventBuffer = {}; 967 | 968 | }; 969 | 970 | marilyn.model = function(modelName, init) { 971 | 972 | if (_models[modelName] && !init) { 973 | return _modelGet(modelName); 974 | } 975 | 976 | return _modelSet(modelName, init); 977 | 978 | }; 979 | 980 | marilyn.modelRemove = function(modelName) { 981 | _models[modelName] = null; 982 | }; 983 | 984 | marilyn.receiveRemove = function(modelName) { 985 | for (var model in _models) { 986 | _models[model].receiveRemove(); 987 | } 988 | }; 989 | 990 | return marilyn; 991 | 992 | }); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /** CREATE */ 2 | 3 | QUnit.test('CREATE - before, after, receive, and callback events are given the correct context', function(assert) { 4 | 5 | var context = {}; 6 | context.before = false; 7 | context.after = false; 8 | context.receive = false; 9 | context.callback = false; 10 | 11 | var name = uuid.v4(); 12 | 13 | var contextCheck = uuid.v4(); 14 | 15 | var Model = marilyn.model(name, function() { 16 | 17 | this.contextCheck = contextCheck; 18 | 19 | this.before('create', function(data, next) { 20 | context.before = (this.contextCheck === contextCheck); 21 | next(); 22 | }); 23 | 24 | this.after('create', function(data, next) { 25 | context.after = (this.contextCheck === contextCheck); 26 | next(); 27 | }); 28 | 29 | this.receive('create', function(data) { 30 | context.receive = (this.contextCheck === contextCheck); 31 | }); 32 | 33 | }); 34 | 35 | Model.create({ 36 | 'contextCheck': contextCheck, 37 | }, function(err, data) { 38 | context.callback = (this.contextCheck === contextCheck); 39 | }); 40 | 41 | assert.ok(context.before, 'create before context'); 42 | assert.ok(context.after, 'create after context'); 43 | assert.ok(context.receive, 'create receive context'); 44 | assert.ok(context.callback, 'create callback context'); 45 | 46 | }); 47 | 48 | QUnit.test('CREATE - before, after, receive, and callback events are given the correct context when saving a new instance', function(assert) { 49 | 50 | var context = {}; 51 | context.createBefore = false; 52 | context.createAfter = false; 53 | context.createReceive = false; 54 | context.saveBefore = false; 55 | context.saveAfter = false; 56 | context.saveReceive = false; 57 | context.callback = false; 58 | 59 | var name = uuid.v4(); 60 | 61 | var contextCheck = uuid.v4(); 62 | 63 | var Model = marilyn.model(name, function() { 64 | 65 | this.contextCheck = contextCheck; 66 | 67 | this.before('create', function(data, next) { 68 | context.createBefore = (this.contextCheck === contextCheck); 69 | next(); 70 | }); 71 | 72 | this.after('create', function(data, next) { 73 | context.createAfter = (this.contextCheck === contextCheck); 74 | next(); 75 | }); 76 | 77 | this.receive('create', function(data) { 78 | context.createReceive = (this.contextCheck === contextCheck); 79 | }); 80 | 81 | this.before('save', function(data, next) { 82 | context.saveBefore = (this.contextCheck === contextCheck); 83 | next(); 84 | }); 85 | 86 | this.after('save', function(data, next) { 87 | context.saveAfter = (this.contextCheck === contextCheck); 88 | next(); 89 | }); 90 | 91 | this.receive('save', function(data) { 92 | context.saveReceive = (this.contextCheck === contextCheck); 93 | }); 94 | 95 | }); 96 | 97 | var item = new Model(); 98 | item.someProperty = 'someValue'; 99 | 100 | item.save(function(err, data) { 101 | context.callback = (this.contextCheck === contextCheck); 102 | }); 103 | 104 | assert.ok(context.createBefore, 'new create before context'); 105 | assert.ok(context.createAfter, 'new create after context'); 106 | assert.ok(context.createReceive, 'new create receive context'); 107 | assert.ok(context.saveBefore, 'new create before context'); 108 | assert.ok(context.saveAfter, 'new create after context'); 109 | assert.ok(context.saveReceive, 'new create receive context'); 110 | assert.ok(context.callback, 'new create callback context'); 111 | 112 | }); 113 | 114 | QUnit.test('CREATE - before, after, receive, and callback events run when creating and have valid data types', function(assert) { 115 | 116 | var ran = {}; 117 | ran.before = 0; 118 | ran.after = 0; 119 | ran.receive = 0; 120 | ran.callback = 0; 121 | 122 | var typeValid = {}; 123 | typeValid.before = false; 124 | typeValid.after = false; 125 | typeValid.receive = false; 126 | typeValid.callback = false; 127 | 128 | var name = uuid.v4(); 129 | 130 | var Model = marilyn.model(name, function() { 131 | 132 | this.before('create', function(data, next) { 133 | ran.before = 1; 134 | typeValid.before = _.isObject(data); 135 | next(); 136 | }); 137 | 138 | this.after('create', function(data, next) { 139 | ran.after = 2; 140 | typeValid.after = _.isObject(data); 141 | next(); 142 | }); 143 | 144 | this.receive('create', function(data) { 145 | ran.receive = 3; 146 | typeValid.receive = _.isObject(data); 147 | }); 148 | 149 | }); 150 | 151 | Model.create({ 152 | 'someProperty': 'someValue', 153 | }, function(err, data) { 154 | ran.callback = 4; 155 | typeValid.callback = _.isObject(data); 156 | }); 157 | 158 | assert.equal(ran.before, 1, 'create before ran'); 159 | assert.equal(ran.after, 2, 'create after ran'); 160 | assert.equal(ran.receive, 3, 'create receive ran'); 161 | assert.equal(ran.callback, 4, 'create callback ran'); 162 | 163 | assert.ok(typeValid.before, 'create before has valid data type'); 164 | assert.ok(typeValid.after, 'create after has valid data type'); 165 | assert.ok(typeValid.receive, 'create receive has valid data type'); 166 | 167 | }); 168 | 169 | QUnit.test('CREATE - before, after, receive, and callback events run when saving a new instance and have valid data types', function(assert) { 170 | 171 | var ran = {}; 172 | ran.createBefore = 0; 173 | ran.createAfter = 0; 174 | ran.createReceive = 0; 175 | ran.saveBefore = 0; 176 | ran.saveAfter = 0; 177 | ran.saveReceive = 0; 178 | ran.callback = 0; 179 | 180 | var typeValid = {}; 181 | typeValid.createBefore = false; 182 | typeValid.createAfter = false; 183 | typeValid.createReceive = false; 184 | typeValid.saveBefore = false; 185 | typeValid.saveAfter = false; 186 | typeValid.saveReceive = false; 187 | typeValid.callback = false; 188 | 189 | var name = uuid.v4(); 190 | 191 | var Model = marilyn.model(name, function() { 192 | 193 | this.before('save', function(data, next) { 194 | ran.saveBefore = 1; 195 | typeValid.saveBefore = _.isObject(data); 196 | next(); 197 | }); 198 | 199 | this.before('create', function(data, next) { 200 | ran.createBefore = 2; 201 | typeValid.createBefore = _.isObject(data); 202 | next(); 203 | }); 204 | 205 | this.after('create', function(data, next) { 206 | ran.createAfter = 3; 207 | typeValid.createAfter = _.isObject(data); 208 | next(); 209 | }); 210 | 211 | this.after('save', function(data, next) { 212 | ran.saveAfter = 4; 213 | typeValid.saveAfter = _.isObject(data); 214 | next(); 215 | }); 216 | 217 | this.receive('create', function(data) { 218 | ran.createReceive = 5; 219 | typeValid.createReceive = _.isObject(data); 220 | }); 221 | 222 | this.receive('save', function(data) { 223 | ran.saveReceive = 6; 224 | typeValid.saveReceive = _.isObject(data); 225 | }); 226 | 227 | }); 228 | 229 | var item = new Model(); 230 | item.someProperty = 'someValue'; 231 | 232 | item.save(function(err, data) { 233 | ran.callback = 7; 234 | typeValid.callback = _.isObject(data); 235 | }); 236 | 237 | assert.equal(ran.createBefore, 2, 'new create before ran'); 238 | assert.equal(ran.createAfter, 3, 'new create after ran'); 239 | assert.equal(ran.createReceive, 5, 'new create receive ran'); 240 | assert.equal(ran.saveBefore, 1, 'new create before save ran'); 241 | assert.equal(ran.saveAfter, 4, 'new create after save ran'); 242 | assert.equal(ran.saveReceive, 6, 'new create receive save ran'); 243 | assert.equal(ran.callback, 7, 'new create callback ran'); 244 | 245 | assert.ok(typeValid.createBefore, 'new create before has valid data type'); 246 | assert.ok(typeValid.createAfter, 'new create after has valid data type'); 247 | assert.ok(typeValid.createReceive, 'new create receive has valid data type'); 248 | assert.ok(typeValid.saveBefore, 'new create before save has valid data type'); 249 | assert.ok(typeValid.saveAfter, 'new create after save has valid data type'); 250 | assert.ok(typeValid.saveReceive, 'new create receive save has valid data type'); 251 | assert.ok(typeValid.callback, 'new create calback has valid data type'); 252 | 253 | }); 254 | 255 | /** READ */ 256 | 257 | QUnit.test('READ - before, after, receive, and callback events are given the correct context', function(assert) { 258 | 259 | var context = {}; 260 | context.before = false; 261 | context.after = false; 262 | context.receive = false; 263 | context.callback = false; 264 | 265 | var name = uuid.v4(); 266 | 267 | var contextCheck = uuid.v4(); 268 | 269 | var Model = marilyn.model(name, function() { 270 | 271 | this.contextCheck = contextCheck; 272 | 273 | this.before('read', function(data, next) { 274 | context.before = (this.contextCheck === contextCheck); 275 | next(); 276 | }); 277 | 278 | this.after('read', function(data, next) { 279 | context.after = (this.contextCheck === contextCheck); 280 | next(); 281 | }); 282 | 283 | this.receive('read', function(data) { 284 | context.receive = (this.contextCheck === contextCheck); 285 | }); 286 | 287 | }); 288 | 289 | Model.read({ 290 | 'someProperty': 'someValue', 291 | }, function(err, data) { 292 | context.callback = (this.contextCheck === contextCheck); 293 | }); 294 | 295 | assert.ok(context.before, 'read before context'); 296 | assert.ok(context.after, 'read after context'); 297 | assert.ok(context.receive, 'read receive context'); 298 | assert.ok(context.callback, 'read callback context'); 299 | 300 | }); 301 | 302 | QUnit.test('READ - before receive, after, and callback events run when reading all and have valid data types', function(assert) { 303 | 304 | var ran = {}; 305 | ran.before = 0; 306 | ran.receive = 0; 307 | ran.after = 0; 308 | ran.callback = 0; 309 | 310 | var typeValid = {}; 311 | typeValid.before = false; 312 | typeValid.after = false; 313 | typeValid.receive = false; 314 | typeValid.callback = false; 315 | 316 | var name = uuid.v4(); 317 | 318 | var Model = marilyn.model(name, function() { 319 | 320 | this.before('read', function(query, next) { 321 | ran.before = 1; 322 | typeValid.before = _.isObject(query); 323 | next(); 324 | }); 325 | 326 | this.after('read', function(data, next) { 327 | ran.after = 2; 328 | typeValid.after = _.isArray(data); 329 | next(); 330 | }); 331 | 332 | this.receive('read', function(data) { 333 | ran.receive = 3; 334 | typeValid.receive = _.isArray(data); 335 | }); 336 | 337 | }); 338 | 339 | Model.read({ 340 | 'someProperty': 'someValue', 341 | }, function(err, data) { 342 | ran.callback = 4; 343 | typeValid.callback = _.isArray(data); 344 | }); 345 | 346 | assert.equal(ran.before, 1, 'read before ran'); 347 | assert.equal(ran.after, 2, 'read after ran'); 348 | assert.equal(ran.receive, 3, 'read receive ran'); 349 | assert.equal(ran.callback, 4, 'read callback ran'); 350 | 351 | assert.ok(typeValid.before, 'read before has valid data type'); 352 | assert.ok(typeValid.after, 'read after has valid data type'); 353 | assert.ok(typeValid.receive, 'read receive has valid data type'); 354 | assert.ok(typeValid.callback, 'read callback has valid data type'); 355 | 356 | }); 357 | 358 | QUnit.test('READONE - before, after, receive, and callback events are given the correct context', function(assert) { 359 | 360 | var context = {}; 361 | context.before = false; 362 | context.after = false; 363 | context.receive = false; 364 | context.callback = false; 365 | 366 | var name = uuid.v4(); 367 | 368 | var contextCheck = uuid.v4(); 369 | 370 | var Model = marilyn.model(name, function() { 371 | 372 | this.contextCheck = contextCheck; 373 | 374 | this.before('readOne', function(data, next) { 375 | context.before = (this.contextCheck === contextCheck); 376 | next(); 377 | }); 378 | 379 | this.after('readOne', function(data, next) { 380 | context.after = (this.contextCheck === contextCheck); 381 | next(); 382 | }); 383 | 384 | this.receive('readOne', function(data) { 385 | context.receive = (this.contextCheck === contextCheck); 386 | }); 387 | 388 | }); 389 | 390 | Model.create({ 391 | 'id': 1, 392 | 'someProperty': 'someValue', 393 | }, function(err, data) { 394 | 395 | Model.readOne({ 396 | 'id': 1 397 | }, function(err, data) { 398 | context.callback = (this.contextCheck === contextCheck); 399 | }); 400 | 401 | }); 402 | 403 | assert.ok(context.before, 'readOne before context'); 404 | assert.ok(context.after, 'readOne after context'); 405 | assert.ok(context.receive, 'readOne receive context'); 406 | assert.ok(context.callback, 'readOne callback context'); 407 | 408 | }); 409 | 410 | QUnit.test('READONE - before receive, after, and callback events run when reading all and have valid data types', function(assert) { 411 | 412 | var ran = {}; 413 | ran.before = 0; 414 | ran.receive = 0; 415 | ran.after = 0; 416 | ran.callback = 0; 417 | 418 | var typeValid = {}; 419 | typeValid.before = false; 420 | typeValid.after = false; 421 | typeValid.receive = false; 422 | typeValid.callback = false; 423 | 424 | var name = uuid.v4(); 425 | 426 | var Model = marilyn.model(name, function() { 427 | 428 | this.before('readOne', function(query, next) { 429 | ran.before = 1; 430 | typeValid.before = _.isObject(query); 431 | next(); 432 | }); 433 | 434 | this.after('readOne', function(data, next) { 435 | ran.after = 2; 436 | typeValid.after = _.isObject(data); 437 | next(); 438 | }); 439 | 440 | this.receive('readOne', function(data) { 441 | ran.receive = 3; 442 | typeValid.receive = _.isObject(data); 443 | }); 444 | 445 | }); 446 | 447 | Model.create({ 448 | 'id': 1, 449 | 'someProperty': 'someValue', 450 | }, function(err, data) { 451 | 452 | Model.readOne({ 453 | 'id': 1 454 | }, function(err, data) { 455 | ran.callback = 4; 456 | typeValid.callback = _.isObject(data); 457 | }); 458 | 459 | }); 460 | 461 | assert.equal(ran.before, 1, 'readOne before ran'); 462 | assert.equal(ran.after, 2, 'readOne after ran'); 463 | assert.equal(ran.receive, 3, 'readOne receive ran'); 464 | assert.equal(ran.callback, 4, 'readOne callback ran'); 465 | 466 | assert.ok(typeValid.before, 'readOne before has valid data type'); 467 | assert.ok(typeValid.after, 'readOne after has valid data type'); 468 | assert.ok(typeValid.receive, 'readOne receive has valid data type'); 469 | assert.ok(typeValid.callback, 'readOne callback has valid data type'); 470 | 471 | }); 472 | 473 | /** UPDATE */ 474 | 475 | QUnit.test('UPDATE - before, after, receive, and callback events are given the correct context', function(assert) { 476 | 477 | var context = {}; 478 | context.before = false; 479 | context.after = false; 480 | context.receive = false; 481 | context.callback = false; 482 | 483 | var name = uuid.v4(); 484 | 485 | var contextCheck = uuid.v4(); 486 | 487 | var Model = marilyn.model(name, function() { 488 | 489 | this.contextCheck = contextCheck; 490 | 491 | this.before('update', function(searchQuery, updateQuery, next) { 492 | context.before = (this.contextCheck === contextCheck); 493 | next(); 494 | }); 495 | 496 | this.after('update', function(data, next) { 497 | context.after = (this.contextCheck === contextCheck); 498 | next(); 499 | }); 500 | 501 | this.receive('update', function(data) { 502 | context.receive = (this.contextCheck === contextCheck); 503 | }); 504 | 505 | }); 506 | 507 | Model.create({ 508 | 'id': 1, 509 | 'someProperty': 'someValue', 510 | }, function(err, data) { 511 | 512 | Model.update({ 513 | 'id': 1, 514 | }, { 515 | 'title': 'Something', 516 | }, function(err, data) { 517 | context.callback = (this.contextCheck === contextCheck); 518 | }); 519 | 520 | }); 521 | 522 | assert.ok(context.before, 'update before context'); 523 | assert.ok(context.after, 'update after context'); 524 | assert.ok(context.receive, 'update receive context'); 525 | assert.ok(context.callback, 'update callback context'); 526 | 527 | }); 528 | 529 | QUnit.test('CREATE - before, after, receive, and callback events are given the correct context', function(assert) { 530 | 531 | var context = {}; 532 | context.before = false; 533 | context.after = false; 534 | context.receive = false; 535 | context.callback = false; 536 | 537 | var name = uuid.v4(); 538 | 539 | var contextCheck = uuid.v4(); 540 | 541 | var Model = marilyn.model(name, function() { 542 | 543 | this.contextCheck = contextCheck; 544 | 545 | this.before('create', function(data, next) { 546 | context.before = (this.contextCheck === contextCheck); 547 | next(); 548 | }); 549 | 550 | this.after('create', function(data, next) { 551 | context.after = (this.contextCheck === contextCheck); 552 | next(); 553 | }); 554 | 555 | this.receive('create', function(data) { 556 | context.receive = (this.contextCheck === contextCheck); 557 | }); 558 | 559 | }); 560 | 561 | Model.create({ 562 | 'contextCheck': contextCheck, 563 | }, function(err, data) { 564 | context.callback = (this.contextCheck === contextCheck); 565 | }); 566 | 567 | assert.ok(context.before, 'create before context'); 568 | assert.ok(context.after, 'create after context'); 569 | assert.ok(context.receive, 'create receive context'); 570 | assert.ok(context.callback, 'create callback context'); 571 | 572 | }); 573 | 574 | QUnit.test('UPDATE - before, after, receive, and callback events are given the correct context when saving a new instance', function(assert) { 575 | 576 | var context = {}; 577 | context.updateBefore = false; 578 | context.updateAfter = false; 579 | context.updateReceive = false; 580 | context.saveBefore = false; 581 | context.saveAfter = false; 582 | context.saveReceive = false; 583 | context.callback = false; 584 | 585 | var name = uuid.v4(); 586 | 587 | var contextCheck = uuid.v4(); 588 | 589 | var Model = marilyn.model(name, function() { 590 | 591 | this.contextCheck = contextCheck; 592 | 593 | this.before('update', function(searchQuery, updateQuery, next) { 594 | context.updateBefore = (this.contextCheck === contextCheck); 595 | next(); 596 | }); 597 | 598 | this.after('update', function(data, next) { 599 | context.updateAfter = (this.contextCheck === contextCheck); 600 | next(); 601 | }); 602 | 603 | this.receive('update', function(data) { 604 | context.updateReceive = (this.contextCheck === contextCheck); 605 | }); 606 | 607 | this.before('save', function(data, next) { 608 | context.saveBefore = (this.contextCheck === contextCheck); 609 | next(); 610 | }); 611 | 612 | this.after('save', function(data, next) { 613 | context.saveAfter = (this.contextCheck === contextCheck); 614 | next(); 615 | }); 616 | 617 | this.receive('save', function(data) { 618 | context.saveReceive = (this.contextCheck === contextCheck); 619 | }); 620 | 621 | }); 622 | 623 | Model.create({ 624 | 'id': 1, 625 | 'someProperty': 'someValue', 626 | }, function(err, data) { 627 | 628 | data.title = 'Something'; 629 | data.someProperty = 'someValue'; 630 | 631 | data.save(function(err, data) { 632 | context.callback = (this.contextCheck === contextCheck); 633 | }); 634 | 635 | }); 636 | 637 | assert.ok(context.updateBefore, 'new update before context'); 638 | assert.ok(context.updateAfter, 'new update after context'); 639 | assert.ok(context.updateReceive, 'new update receive context'); 640 | assert.ok(context.saveBefore, 'new update before context'); 641 | assert.ok(context.saveAfter, 'new update after context'); 642 | assert.ok(context.saveReceive, 'new update receive context'); 643 | assert.ok(context.callback, 'new update callback context'); 644 | 645 | }); 646 | 647 | QUnit.test('UPDATE - before, after, receive, and callback events run when updating and have valid data types', function(assert) { 648 | 649 | var ran = {}; 650 | ran.before = 0; 651 | ran.receive = 0; 652 | ran.after = 0; 653 | ran.callback = 0; 654 | 655 | var typeValid = {}; 656 | typeValid.before = false; 657 | typeValid.after = false; 658 | typeValid.receive = false; 659 | typeValid.callback = false; 660 | 661 | var name = uuid.v4(); 662 | 663 | var Model = marilyn.model(name, function() { 664 | 665 | this.before('update', function(searchQuery, updateQuery, next) { 666 | ran.before = 1; 667 | typeValid.before = (_.isObject(searchQuery) && _.isObject(updateQuery)); 668 | next(); 669 | }); 670 | 671 | this.after('update', function(data, next) { 672 | ran.after = 2; 673 | typeValid.after = _.isArray(data); 674 | next(); 675 | }); 676 | 677 | this.receive('update', function(data) { 678 | ran.receive = 3; 679 | typeValid.receive = _.isArray(data); 680 | }); 681 | 682 | }); 683 | 684 | Model.create({ 685 | 'id': 1, 686 | 'someProperty': 'someValue', 687 | }, function(err, data) { 688 | 689 | Model.update({ 690 | 'id': 1, 691 | }, { 692 | 'title': 'Something', 693 | }, function(err, data) { 694 | ran.callback = 4; 695 | typeValid.callback = _.isArray(data); 696 | }); 697 | 698 | }); 699 | 700 | assert.equal(ran.before, 1, 'update before ran'); 701 | assert.equal(ran.after, 2, 'update after ran'); 702 | assert.equal(ran.receive, 3, 'update receive ran'); 703 | assert.equal(ran.callback, 4, 'update callback ran'); 704 | 705 | assert.ok(typeValid.before, 'update before has valid data type'); 706 | assert.ok(typeValid.after, 'update after has valid data type'); 707 | assert.ok(typeValid.receive, 'update receive has valid data type'); 708 | assert.ok(typeValid.callback, 'update callback has valid data type'); 709 | 710 | }); 711 | 712 | QUnit.test('UPDATE - before, after, receive, and callback events run when saving an existing instance and have valid data types', function(assert) { 713 | 714 | var ran = {}; 715 | ran.updateBefore = 0; 716 | ran.updateReceive = 0; 717 | ran.updateAfter = 0; 718 | ran.saveBefore = 0; 719 | ran.saveReceive = 0; 720 | ran.saveAfter = 0; 721 | ran.callback = 0; 722 | 723 | var typeValid = {}; 724 | typeValid.updateBefore = false; 725 | typeValid.updateAfter = false; 726 | typeValid.updateReceive = false; 727 | typeValid.saveBefore = false; 728 | typeValid.saveAfter = false; 729 | typeValid.saveReceive = false; 730 | typeValid.callback = false; 731 | 732 | var name = uuid.v4(); 733 | 734 | var Model = marilyn.model(name, function() { 735 | 736 | this.before('save', function(data, next) { 737 | ran.saveBefore = 1; 738 | typeValid.saveBefore = _.isObject(data); 739 | next(); 740 | }); 741 | 742 | this.before('update', function(searchQuery, updateQuery, next) { 743 | ran.updateBefore = 2; 744 | typeValid.updateBefore = (_.isObject(searchQuery) && _.isObject(updateQuery)); 745 | next(); 746 | }); 747 | 748 | this.after('update', function(data, next) { 749 | ran.updateAfter = 3; 750 | typeValid.updateAfter = _.isArray(data); 751 | next(); 752 | }); 753 | 754 | this.after('save', function(data, next) { 755 | ran.saveAfter = 4; 756 | typeValid.saveAfter = _.isObject(data); 757 | next(); 758 | }); 759 | 760 | this.receive('update', function(data) { 761 | ran.updateReceive = 5; 762 | typeValid.updateReceive = _.isArray(data); 763 | }); 764 | 765 | this.receive('save', function(data) { 766 | ran.saveReceive = 6; 767 | typeValid.saveReceive = _.isObject(data); 768 | }); 769 | 770 | }); 771 | 772 | Model.create({ 773 | 'id': 1, 774 | 'someProperty': 'someValue', 775 | }, function(err, data) { 776 | 777 | data.title = 'Something'; 778 | data.someProperty = 'someValue'; 779 | 780 | data.save(function(err, data) { 781 | ran.callback = 7; 782 | typeValid.callback = _.isObject(data); 783 | }); 784 | 785 | }); 786 | 787 | assert.equal(ran.updateBefore, 2, 'update new instance before ran'); 788 | assert.equal(ran.updateAfter, 3, 'update new instance after ran'); 789 | assert.equal(ran.updateReceive, 5, 'update new instance receive ran'); 790 | assert.equal(ran.saveBefore, 1, 'update new instance before ran'); 791 | assert.equal(ran.saveAfter, 4, 'update new instance after ran'); 792 | assert.equal(ran.saveReceive, 6, 'update new instance receive ran'); 793 | assert.equal(ran.callback, 7, 'update new instance callback ran'); 794 | 795 | assert.ok(typeValid.updateBefore, 'update new instance before has valid data type'); 796 | assert.ok(typeValid.updateAfter, 'update new instance after has valid data type'); 797 | assert.ok(typeValid.updateReceive, 'update new instance receive has valid data type'); 798 | assert.ok(typeValid.saveBefore, 'update new instance before has valid data type'); 799 | assert.ok(typeValid.saveAfter, 'update new instance after has valid data type'); 800 | assert.ok(typeValid.saveReceive, 'update new instance receive has valid data type'); 801 | assert.ok(typeValid.callback, 'update new instance callback has valid data type'); 802 | 803 | }); 804 | 805 | /** DELETE */ 806 | 807 | QUnit.test('DELETE - before, after, receive, and callback events are given the correct context', function(assert) { 808 | 809 | var context = {}; 810 | context.before = false; 811 | context.after = false; 812 | context.receive = false; 813 | context.callback = false; 814 | 815 | var name = uuid.v4(); 816 | 817 | var contextCheck = uuid.v4(); 818 | 819 | var Model = marilyn.model(name, function() { 820 | 821 | this.contextCheck = contextCheck; 822 | 823 | this.before('delete', function(data, next) { 824 | context.before = (this.contextCheck === contextCheck); 825 | next(); 826 | }); 827 | 828 | this.after('delete', function(data, next) { 829 | context.after = (this.contextCheck === contextCheck); 830 | next(); 831 | }); 832 | 833 | this.receive('delete', function(data) { 834 | context.receive = (this.contextCheck === contextCheck); 835 | }); 836 | 837 | }); 838 | 839 | Model.create({ 840 | 'id': 1, 841 | 'someProperty': 'someValue', 842 | }, function(err, data) { 843 | 844 | Model.del({ 845 | 'id': 1 846 | }, function(err, results) { 847 | context.callback = (this.contextCheck === contextCheck); 848 | }); 849 | 850 | }); 851 | 852 | assert.ok(context.before, 'delete before context'); 853 | assert.ok(context.after, 'delete after context'); 854 | assert.ok(context.receive, 'delete receive context'); 855 | assert.ok(context.callback, 'delete callback context'); 856 | 857 | }); 858 | 859 | QUnit.test('DELETE - before, after, receive, and callback events run when deleting and have valid data types', function(assert) { 860 | 861 | var ran = {}; 862 | ran.before = 0; 863 | ran.receive = 0; 864 | ran.after = 0; 865 | ran.callback = 0; 866 | 867 | var typeValid = {}; 868 | typeValid.before = false; 869 | typeValid.after = false; 870 | typeValid.receive = false; 871 | typeValid.callback = false; 872 | 873 | var name = uuid.v4(); 874 | 875 | var Model = marilyn.model(name, function() { 876 | 877 | this.before('delete', function(query, next) { 878 | ran.before = 1; 879 | typeValid.before = _.isObject(query); 880 | next(); 881 | }); 882 | 883 | this.after('delete', function(data, next) { 884 | ran.after = 2; 885 | typeValid.after = _.isArray(data); 886 | next(); 887 | }); 888 | 889 | this.receive('delete', function(data) { 890 | ran.receive = 3; 891 | typeValid.receive = _.isArray(data); 892 | }); 893 | 894 | }); 895 | 896 | Model.create({ 897 | 'id': 1, 898 | 'someProperty': 'someValue', 899 | }, function(err, data) { 900 | 901 | Model.del({ 902 | 'id': 1 903 | }, function(err, results) { 904 | ran.callback = 4; 905 | typeValid.callback = _.isArray(results); 906 | }); 907 | 908 | }); 909 | 910 | assert.equal(ran.before, 1, 'delete before ran'); 911 | assert.equal(ran.after, 2, 'delete after ran'); 912 | assert.equal(ran.receive, 3, 'delete receive ran'); 913 | assert.equal(ran.callback, 4, 'delete callback ran'); 914 | 915 | assert.ok(typeValid.before, 'delete before has valid data type'); 916 | assert.ok(typeValid.after, 'delete after has valid data type'); 917 | assert.ok(typeValid.receive, 'delete receive has valid data type'); 918 | assert.ok(typeValid.callback, 'delete callback has valid data type'); 919 | 920 | }); 921 | 922 | QUnit.test('DELETE - before, after, and receive events run when calling instance delete method and have valid data types', function(assert) { 923 | 924 | var ran = {}; 925 | ran.before = 0; 926 | ran.receive = 0; 927 | ran.after = 0; 928 | ran.callback = 0; 929 | 930 | var typeValid = {}; 931 | typeValid.before = false; 932 | typeValid.after = false; 933 | typeValid.receive = false; 934 | typeValid.callback = false; 935 | 936 | var name = uuid.v4(); 937 | 938 | var Model = marilyn.model(name, function() { 939 | 940 | this.before('delete', function(query, next) { 941 | ran.before = 1; 942 | typeValid.before = _.isObject(query); 943 | next(); 944 | }); 945 | 946 | this.after('delete', function(data, next) { 947 | ran.after = 2; 948 | typeValid.after = _.isArray(data); 949 | next(); 950 | }); 951 | 952 | this.receive('delete', function(data) { 953 | ran.receive = 3; 954 | typeValid.receive = _.isArray(data); 955 | }); 956 | 957 | }); 958 | 959 | Model.create({ 960 | 'id': 1, 961 | 'someProperty': 'someValue', 962 | }, function(err, data) { 963 | 964 | data.delete(function(err, results) { 965 | ran.callback = 4; 966 | typeValid.callback = _.isArray(results); 967 | }); 968 | 969 | }); 970 | 971 | assert.equal(ran.before, 1, 'delete before ran'); 972 | assert.equal(ran.after, 2, 'delete after ran'); 973 | assert.equal(ran.receive, 3, 'delete receive ran'); 974 | assert.equal(ran.callback, 4, 'delete callback ran'); 975 | 976 | assert.ok(typeValid.before, 'delete before has valid data type'); 977 | assert.ok(typeValid.after, 'delete after has valid data type'); 978 | assert.ok(typeValid.receive, 'delete receive has valid data type'); 979 | assert.ok(typeValid.callback, 'delete callback has valid data type'); 980 | 981 | }); 982 | 983 | /** MULTIPLE EVENTS */ 984 | 985 | QUnit.test('MULTIPLE - before, after, and receive, events run when passed for multiple event types and have valid data types', function(assert) { 986 | 987 | var ran = {}; 988 | ran.before = 0; 989 | ran.receive = 0; 990 | ran.after = 0; 991 | 992 | var name = uuid.v4(); 993 | 994 | var Model = marilyn.model(name, function() { 995 | 996 | // updates can't be tested for because it's callback has a different signature 997 | 998 | this.before(['create', 'read', 'readOne', 'delete'], function(data, next) { 999 | ran.before++; 1000 | next(); 1001 | }); 1002 | 1003 | this.after(['create', 'read', 'readOne', 'delete'], function(data, next) { 1004 | ran.after++; 1005 | next(); 1006 | }); 1007 | 1008 | this.receive(['create', 'read', 'readOne', 'delete'], function(data) { 1009 | ran.receive++; 1010 | }); 1011 | 1012 | }); 1013 | 1014 | Model.create({ 1015 | 'id': 1, 1016 | 'someProperty': 'someValue', 1017 | }, function(err, data) { 1018 | 1019 | Model.read({}); 1020 | 1021 | Model.readOne({ 1022 | 'id': 1 1023 | }); 1024 | 1025 | Model.del({ 1026 | 'id': 1 1027 | }); 1028 | 1029 | }); 1030 | 1031 | assert.equal(ran.before, 4, 'multiple before ran'); 1032 | assert.equal(ran.after, 4, 'multiple after ran'); 1033 | assert.equal(ran.receive, 4, 'multiple receive ran'); 1034 | 1035 | }); 1036 | 1037 | /** PLUGINS */ 1038 | 1039 | QUnit.test('PLUGINS - plugin functions are called', function(assert) { 1040 | 1041 | var called = false; 1042 | 1043 | var name = uuid.v4(); 1044 | 1045 | var Model = marilyn.model(name); 1046 | 1047 | Model.use(function() { 1048 | called = true; 1049 | }); 1050 | 1051 | assert.ok(called, 'plugin function was called'); 1052 | 1053 | }); 1054 | 1055 | QUnit.test('PLUGINS - plugin functions have the correct context', function(assert) { 1056 | 1057 | var called = false; 1058 | 1059 | var name = uuid.v4(); 1060 | var randomTestingValue = uuid.v4(); 1061 | 1062 | var Model = marilyn.model(name, function() { 1063 | this.testingPropery = randomTestingValue; 1064 | }); 1065 | 1066 | Model.use(function() { 1067 | if (randomTestingValue === this.testingPropery) { 1068 | called = true; 1069 | } 1070 | }); 1071 | 1072 | assert.ok(called, 'plugin function was called'); 1073 | 1074 | }); --------------------------------------------------------------------------------