├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── Gruntfile.js ├── README.md ├── api └── models │ └── User.js ├── index.js ├── lib ├── countAndFind.js ├── countAndSearch.js ├── first.js ├── last.js ├── search.js ├── softDelete.js └── utils │ ├── countAndFindDeferred.js │ └── lastDeferred.js ├── package.json ├── seeds └── test │ └── UserSeed.js └── test ├── bootstrap.spec.js └── specs ├── countAndFind.js ├── countAndSearch.js ├── first.js ├── last.js ├── search.js └── softDelete.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | #Temporary data 6 | .tmp 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | 13 | # Directory for instrumented libs generated by jscoverage/JSCover 14 | lib-cov 15 | 16 | # Coverage directory used by tools like istanbul 17 | coverage 18 | 19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 20 | .grunt 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # Deployed apps should consider commenting this line out: 27 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 28 | node_modules 29 | doc 30 | startup.sh -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "browser": true, 4 | "camelcase": true, 5 | "curly": true, 6 | "eqeqeq": true, 7 | "esnext": true, 8 | "immed": true, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "node": true, 13 | "mocha": true, 14 | "quotmark": "single", 15 | "strict": true, 16 | "undef": true, 17 | "unused": true, 18 | "expr": true, 19 | "ignore": true, 20 | "globals": { 21 | "User": true, 22 | "sails": true, 23 | "_": true, 24 | "async": true 25 | } 26 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | api 2 | config 3 | node_modules 4 | seeds 5 | ssl 6 | .DS_STORE 7 | *~ 8 | .idea 9 | nbproject 10 | test 11 | .git 12 | .gitignore 13 | .tmp 14 | *.swo 15 | *.swp 16 | *.swn 17 | *.swm 18 | .jshintrc 19 | .editorconfig 20 | doc.html 21 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | before_script: 5 | - npm install -g grunt-cli -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | // Add the grunt-mocha-test and jshint tasks. 6 | grunt.loadNpmTasks('grunt-mocha-test'); 7 | grunt.loadNpmTasks('grunt-contrib-jshint'); 8 | 9 | grunt.initConfig({ 10 | // Configure a mochaTest task 11 | mochaTest: { 12 | test: { 13 | options: { 14 | reporter: 'spec', 15 | timeout: 20000 16 | }, 17 | src: ['test/**/*.js'] 18 | } 19 | }, 20 | jshint: { 21 | options: { 22 | reporter: require('jshint-stylish'), 23 | jshintrc: '.jshintrc' 24 | }, 25 | all: [ 26 | 'Gruntfile.js', 27 | 'index.js', 28 | 'lib/**/*.js', 29 | 'test/**/*.js' 30 | ] 31 | } 32 | }); 33 | 34 | //custom tasks 35 | grunt.registerTask('default', ['jshint', 'mochaTest']); 36 | grunt.registerTask('test', ['jshint', 'mochaTest']); 37 | 38 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | sails-hook-model-extra 2 | ====================== 3 | 4 | [![Build Status](https://travis-ci.org/lykmapipo/sails-hook-model-extra.svg?branch=master)](https://travis-ci.org/lykmapipo/sails-hook-model-extra) 5 | 6 | [![Tips](https://img.shields.io/gratipay/lykmapipo.svg)](https://gratipay.com/lykmapipo/) 7 | 8 | [![Support via Gratipay](https://cdn.rawgit.com/gratipay/gratipay-badge/2.3.0/dist/gratipay.svg)](https://gratipay.com/lykmapipo/) 9 | 10 | Additional model methods for sails. They works with both `callback`, `deferred` and `promise` style `model API` provided with sails. 11 | 12 | *Note: This requires Sails v0.11.0+. If v0.11.0+ isn't published to NPM yet, you'll need to install it via Github.* 13 | 14 | ## Installation 15 | ```sh 16 | $ npm install --save sails-hook-model-extra 17 | ``` 18 | 19 | ## API 20 | The following methods will be added to all models once hook is installed. 21 | 22 | * [`countAndFind(criteria, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#countandfindcriteria-callback) 23 | * [`countAndSearch(searchTerm, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#countandsearchsearchterm-callback) 24 | * [`first(howMany, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#firsthowmany-callback) 25 | * [`last(howMany, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#lasthowmany-callback) 26 | * [`search(searchTerm, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#searchsearchterm-callback) 27 | * [`softDelete(criteria, callback)`](https://github.com/lykmapipo/sails-hook-model-extra#softdeletecriteria-callback) 28 | 29 | 30 | ### `countAndFind(criteria, callback)` 31 | 32 | Allow `count` and `find` to be executed as a compound(single) query. 33 | 34 | - `criteria`: A valid sails waterline query criteria. If not provided an empty object criteria `{}` will be used. This criteria will be applied to both `count()` and `find()` query to retain result consistence. 35 | 36 | - `callback`: A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 37 | 38 | *Note!: `countAndFind()` run count() and find() in parallel* 39 | 40 | #### Examples with no criteria 41 | 42 | ##### Using callback API 43 | ```js 44 | //callback style 45 | User 46 | .countAndFind(function(error, results) { 47 | if (error) { 48 | done(error); 49 | } else { 50 | expect(results.count).to.exist; 51 | expect(results.data).to.exist; 52 | 53 | expect(results.count).to.be.equal(10); 54 | expect(results.data.length).to.be.equal(10); 55 | 56 | done(); 57 | } 58 | }); 59 | ``` 60 | 61 | ##### Using deferred API 62 | ```js 63 | //deferred style 64 | User 65 | .countAndFind() 66 | .exec(function(error, results) { 67 | if (error) { 68 | done(error); 69 | } else { 70 | expect(results.count).to.exist; 71 | expect(results.data).to.exist; 72 | 73 | expect(results.count).to.be.equal(10); 74 | expect(results.data.length).to.be.equal(10); 75 | 76 | done(); 77 | } 78 | }); 79 | ``` 80 | 81 | ##### Using promis API 82 | ```js 83 | //promise style 84 | User 85 | .countAndFind() 86 | .then(function(results) { 87 | 88 | expect(results.count).to.exist; 89 | expect(results.data).to.exist; 90 | 91 | expect(results.count).to.be.equal(10); 92 | expect(results.data.length).to.be.equal(10); 93 | 94 | done(); 95 | }) 96 | .catch(function(error) { 97 | done(error); 98 | }); 99 | ``` 100 | 101 | #### Examples with criteria provided 102 | 103 | ##### Using callback API 104 | ```js 105 | //callback style 106 | User 107 | .countAndFind({ 108 | id: { 109 | '>': 2 110 | } 111 | }, function(error, results) { 112 | if (error) { 113 | done(error); 114 | } else { 115 | 116 | expect(results.count).to.exist; 117 | expect(results.data).to.exist; 118 | 119 | expect(results.count).to.be.equal(8); 120 | expect(results.data.length).to.be.equal(8); 121 | 122 | done(); 123 | } 124 | }); 125 | ``` 126 | 127 | ##### Using deferred API 128 | ```js 129 | //deferred style 130 | User 131 | .countAndFind({ 132 | id: { 133 | '>': 2 134 | } 135 | }) 136 | .exec(function(error, results) { 137 | if (error) { 138 | done(error); 139 | } else { 140 | 141 | expect(results.count).to.exist; 142 | expect(results.data).to.exist; 143 | 144 | expect(results.count).to.be.equal(8); 145 | expect(results.data.length).to.be.equal(8); 146 | 147 | done(); 148 | } 149 | }); 150 | ``` 151 | 152 | ##### Using promise API 153 | ```js 154 | //promise style 155 | User 156 | .countAndFind({ 157 | id: { 158 | '>': 2 159 | } 160 | }) 161 | .then(function(results) { 162 | 163 | expect(results.count).to.exist; 164 | expect(results.data).to.exist; 165 | 166 | expect(results.count).to.be.equal(8); 167 | expect(results.data.length).to.be.equal(8); 168 | 169 | done(); 170 | }) 171 | .catch(function(error) { 172 | done(error); 173 | }); 174 | ``` 175 | 176 | 177 | ### `countAndSearch(searchTerm, callback)` 178 | Count hits and perform to free search on model record(s). Currently `sails-hook-model-extra` will search model attributes of type `string, text, integer, float, json and email`, unless you explicit ovveride this default behaviour per model or globally on all models in `config/models.js` by providing array of searchable attributes types using `searchableTypes` static attribute. 179 | i.e 180 | ```js 181 | ... 182 | 183 | //tells which attributes types are searchable for only this model 184 | //you can also configure it global for all models in `config/models.js` 185 | searchableTypes: [ 186 | 'string', 'text', 'integer', 187 | 'float', 'json', 'email' 188 | ] 189 | ... 190 | ``` 191 | 192 | - `searchTerm`: A string to be used in searching records. If not provided an empty object criteria `{}` will be used. This criteria will be applied to both `count()` and `find()` query to retain result consistence. 193 | 194 | - `callback`: A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 195 | 196 | *Warning!: Using this type of search directly when dataset is few hundreds otherwise consider using search with pagination* 197 | 198 | *Note!: `countAndSearch()` run count() and find() in parallel* 199 | 200 | #### Examples 201 | ##### Example using model callback API 202 | ```js 203 | User 204 | .countAndSearch('gmail', function(error, results) { 205 | if (error) { 206 | done(error) 207 | } else { 208 | expect(results.count).to.be.equal(4); 209 | 210 | expect(_.map(results.data, 'username')) 211 | .to.include.members(['Trent Marvin', 'Malika Greenfelder']); 212 | 213 | done(); 214 | } 215 | }); 216 | ``` 217 | ##### Example using model deferred API 218 | ```js 219 | User 220 | .countAndSearch('vi') 221 | .exec(function(error, results) { 222 | if (error) { 223 | done(error); 224 | } else { 225 | expect(results.count).to.be.equal(4); 226 | 227 | expect(_.map(results.data, 'username')) 228 | .to 229 | .include 230 | .members(['Trent Marvin', 'Viva Gaylord', 'Victoria Steuber']); 231 | 232 | expect(_.map(results.data, 'email')) 233 | .to 234 | .include 235 | .members(['vicky2@gmail.com']); 236 | 237 | done(); 238 | } 239 | }); 240 | ``` 241 | ##### Example using model promise API 242 | ```js 243 | User 244 | .countAndSearch('Malika') 245 | .then(function(results) { 246 | expect(results.count).to.be.equal(1); 247 | 248 | expect(results.data[0].username).to.be.equal('Malika Greenfelder'); 249 | expect(results.data[0].email).to.be.equal('kory.dooley@gmail.com'); 250 | 251 | done(); 252 | }) 253 | .catch(function(error) { 254 | done(error); 255 | }); 256 | ``` 257 | 258 | 259 | 260 | ### `first(howMany, callback)` 261 | Allow to select top(first) `n records(models)` from the database. 262 | 263 | - `howMany` : Specify how many records required. If not provided only single record is returned. 264 | - `callback` : A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 265 | 266 | #### Examples with no additional criterias 267 | 268 | ##### Get only first record 269 | ```js 270 | User 271 | .first(function(error, users) { 272 | if (error) { 273 | done(error); 274 | } else { 275 | expect(users[0].id).to.be.equal(1); 276 | expect(users.length).to.be.equal(1); 277 | done(); 278 | } 279 | }); 280 | ``` 281 | 282 | ##### Get top five records 283 | ```js 284 | User 285 | .first(5, function(error, users) { 286 | if (error) { 287 | done(error); 288 | } else { 289 | expect(_.map(users, 'id')) 290 | .to.include.members([1, 2, 3, 4, 5]); 291 | expect(users.length).to.be.equal(5); 292 | done(); 293 | } 294 | }); 295 | ``` 296 | 297 | #### Examples with additional criterias 298 | 299 | ##### Get only first record where id > 2 300 | ```js 301 | User 302 | .first() 303 | .where({ 304 | id: { 305 | '>': 2 306 | } 307 | }) 308 | .exec(function(error, users) { 309 | if (error) { 310 | done(error); 311 | } else { 312 | expect(users[0].id).to.be.equal(3); 313 | expect(users.length).to.be.equal(1); 314 | done(); 315 | } 316 | }); 317 | ``` 318 | 319 | ##### Get top five records where id > 2 320 | ```js 321 | User 322 | .first(5) 323 | .where({ 324 | id: { 325 | '>': 2 326 | } 327 | }) 328 | .exec(function(error, users) { 329 | if (error) { 330 | done(error); 331 | } else { 332 | expect(_.map(users, 'id')) 333 | .to.include.members([3, 4, 5, 6, 7]); 334 | expect(users.length).to.be.equal(5); 335 | done(); 336 | } 337 | }); 338 | ``` 339 | 340 | 341 | ### `last(howMany, callback)` 342 | Allow to select bottom(last) `n records(models)` from the database. 343 | 344 | - `howMany` : Specify how many records required. If not provided only single record is returned. 345 | - `callback` : A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 346 | 347 | #### Examples with no additional criterias 348 | 349 | ##### Get only last record 350 | ```js 351 | User 352 | .last(function(error, users) { 353 | if (error) { 354 | done(error); 355 | } else { 356 | expect(users[0].id).to.be.equal(10); 357 | expect(users.length).to.be.equal(1); 358 | done(); 359 | } 360 | }); 361 | ``` 362 | 363 | ##### Get last five records 364 | ```js 365 | User 366 | .last(5, function(error, users) { 367 | if (error) { 368 | done(error); 369 | } else { 370 | expect(_.map(users, 'id')) 371 | .to.include.members([10, 9, 8, 7, 6]); 372 | expect(users.length).to.be.equal(5); 373 | done(); 374 | } 375 | }); 376 | ``` 377 | 378 | #### Examples with additional criterias 379 | 380 | ##### Get only last record where id < 8 381 | ```js 382 | User 383 | .last() 384 | .where({ 385 | id: { 386 | '<': 8 387 | } 388 | }) 389 | .exec(function(error, users) { 390 | if (error) { 391 | done(error); 392 | } else { 393 | expect(users[0].id).to.be.equal(7); 394 | expect(users.length).to.be.equal(1); 395 | done(); 396 | } 397 | }); 398 | ``` 399 | 400 | ##### Get las five records where id < 8 401 | ```js 402 | User 403 | .last(5) 404 | .where({ 405 | id: { 406 | '<': 8 407 | } 408 | }) 409 | .exec(function(error, users) { 410 | if (error) { 411 | done(error); 412 | } else { 413 | expect(_.map(users, 'id')) 414 | .to.include.members([7, 6, 5, 4, 3]); 415 | expect(users.length).to.be.equal(5); 416 | done(); 417 | } 418 | }); 419 | ``` 420 | 421 | 422 | ### `search(searchTerm, callback)` 423 | Allow to free search model record(s). Currently `sails-hook-model-extra` will search model attributes of type `string, text, integer, float, json and email`, unless you explicit ovveride this default behaviour per model or globally on all models in `config/models.js` by providing array of searchable attributes types using `searchableTypes` static attribute. 424 | i.e 425 | ```js 426 | ... 427 | 428 | //tells which attributes types are searchable for only this model 429 | //you can also configure it global for all models in `config/models.js` 430 | searchableTypes: [ 431 | 'string', 'text', 'integer', 432 | 'float', 'json', 'email' 433 | ] 434 | ... 435 | ``` 436 | 437 | - `searchTerm`: A string to be used in searching records. If not provided an empty object criteria `{}` will be used. 438 | 439 | - `callback`: A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 440 | 441 | *Warning!: Using this type of search directly when dataset is few hundreds otherwise consider using search with pagination* 442 | 443 | #### Examples 444 | ##### Example using model callback API 445 | ```js 446 | User 447 | .search('gmail', function(error, users) { 448 | if (error) { 449 | done(error) 450 | } else { 451 | expect(users.length).to.be.equal(4); 452 | 453 | expect(_.map(users, 'username')) 454 | .to.include.members(['Trent Marvin', 'Malika Greenfelder']); 455 | 456 | done(); 457 | } 458 | }); 459 | ``` 460 | ##### Example using model deferred API 461 | ```js 462 | User 463 | .search('vi') 464 | .exec(function(error, users) { 465 | if (error) { 466 | done(error); 467 | } else { 468 | expect(users.length).to.be.equal(4); 469 | 470 | expect(_.map(users, 'username')) 471 | .to 472 | .include 473 | .members(['Trent Marvin', 'Viva Gaylord', 'Victoria Steuber']); 474 | 475 | expect(_.map(users, 'email')) 476 | .to 477 | .include 478 | .members(['vicky2@gmail.com']); 479 | 480 | done(); 481 | } 482 | }); 483 | ``` 484 | ##### Example using model promise API 485 | ```js 486 | User 487 | .search('Malika') 488 | .then(function(users) { 489 | expect(users.length).to.be.equal(1); 490 | 491 | expect(users[0].username).to.be.equal('Malika Greenfelder'); 492 | expect(users[0].email).to.be.equal('kory.dooley@gmail.com'); 493 | 494 | done(); 495 | }) 496 | .catch(function(error) { 497 | done(error); 498 | }); 499 | ``` 500 | 501 | 502 | ### `softDelete(criteria, callback)` 503 | Allow to soft delete model(s) by set `deletedAt` attribute to current timestamp. Currently `sails-hook-model-extra` will extend loaded models with `deletedAt datetime` attribute unless explicit defined on the models. 504 | 505 | - `criteria`: A valid sails waterline query criteria. If not provided an empty object criteria `{}` will be used. 506 | 507 | - `callback`: A callback to invoke on results. If not specified a `Deferred object` is returned to allow futher criteria(s) to be chained. 508 | 509 | #### Examples 510 | ##### Example using model callback API 511 | ```js 512 | User 513 | .softDelete({ 514 | id: user.id 515 | }, function(error, deletedUsers) { 516 | if (error) { 517 | done(error); 518 | } else { 519 | expect(deletedUsers.length).to.be.equal(1); 520 | expect(deletedUsers[0].deletedAt).to.not.be.null; 521 | done(); 522 | } 523 | }); 524 | ``` 525 | 526 | ##### Example using model deferred API 527 | ```js 528 | User 529 | .softDelete({ 530 | id: user.id 531 | }) 532 | .exec(function(error, deletedUsers) { 533 | if (error) { 534 | done(error); 535 | } else { 536 | expect(deletedUsers.length).to.be.equal(1); 537 | expect(deletedUsers[0].deletedAt).to.not.be.null; 538 | done(); 539 | } 540 | }); 541 | ``` 542 | 543 | ##### Example using model promise API 544 | ```js 545 | User 546 | .softDelete({ 547 | id: user.id 548 | }) 549 | .then(function(deletedUsers) { 550 | expect(deletedUsers.length).to.be.equal(1); 551 | expect(deletedUsers[0].deletedAt).to.not.be.null; 552 | done(); 553 | }) 554 | .catch(function(error) { 555 | done(error); 556 | }); 557 | ``` 558 | 559 | 560 | ## Testing 561 | * Clone this repository 562 | 563 | * Install all development dependencies 564 | ```sh 565 | $ npm install 566 | ``` 567 | 568 | * Then run test 569 | ```sh 570 | $ npm test 571 | ``` 572 | 573 | ## Contribute 574 | 575 | Fork this repo and push in your ideas. Do not forget to add a bit of test(s) of what value you adding. 576 | 577 | ## Literature Reviewed 578 | 579 | - [Waterline](https://github.com/balderdashy/waterline) 580 | - [Sails Waterline(ORM)](http://sailsjs.org/#!/documentation/reference/waterline) 581 | - [Sail ORM](http://sailsjs.org/#!/documentation/concepts/ORM) 582 | - [Rails ActiveRecord FinderMethods](http://api.rubyonrails.org/classes/ActiveRecord/FinderMethods.html) 583 | - [Waterline aggregate queries](https://github.com/balderdashy/waterline/issues/61) 584 | 585 | 586 | ## Licence 587 | 588 | The MIT License (MIT) 589 | 590 | Copyright (c) 2015 lykmapipo 591 | 592 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 593 | 594 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 595 | 596 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /api/models/User.js: -------------------------------------------------------------------------------- 1 | /** 2 | * sample model 3 | * @type {Object} 4 | */ 5 | module.exports = { 6 | //tells which attributes types are searchable 7 | //you can also configure it global to all models in `config/models.js` 8 | searchableTypes: [ 9 | 'string', 'text', 'integer', 10 | 'float', 'json', 'email' 11 | ], 12 | 13 | attributes: { 14 | username: { 15 | type: 'string', 16 | required: true 17 | }, 18 | email: { 19 | type: 'email', 20 | required: true, 21 | unique: true 22 | } 23 | // deletedAt: { 24 | // type: 'datetime' 25 | // } 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var path = require('path'); 5 | var libPath = path.join(__dirname, 'lib'); 6 | 7 | //model extras 8 | var countAndFind = require(path.join(libPath, 'countAndFind')); 9 | var countAndSearch = require(path.join(libPath, 'countAndSearch')); 10 | var first = require(path.join(libPath, 'first')); 11 | var last = require(path.join(libPath, 'last')); 12 | var search = require(path.join(libPath, 'search')); 13 | var softDelete = require(path.join(libPath, 'softDelete')); 14 | 15 | /** 16 | * @function 17 | * @description additional methods for sails model 18 | * @param {Object} sails a sails application instance 19 | */ 20 | module.exports = function(sails) { 21 | //patch sails model 22 | //to add extra methods 23 | function patch() { 24 | _(sails.models) 25 | .forEach(function(model) { 26 | 27 | //bind model additional methods 28 | //on concrete models 29 | //and left derived model 30 | //build from associations 31 | if (model.globalId) { 32 | countAndFind(model); 33 | countAndSearch(model); 34 | first(model); 35 | last(model); 36 | search(model); 37 | softDelete(model); 38 | } 39 | }); 40 | } 41 | 42 | //extend model attributes 43 | //with deletedAt attribute 44 | function patchAttributes() { 45 | _(sails.models) 46 | .forEach(function(model) { 47 | //bind deleteAt attributes into 48 | //model attributes if not explicit defined 49 | if (!model.attributes.deletedAt) { 50 | _.extend(model.attributes, { 51 | deletedAt: { 52 | type: 'datetime', 53 | defaultsTo: null 54 | } 55 | }); 56 | } 57 | }); 58 | } 59 | 60 | return { 61 | initialize: function(done) { 62 | //first 63 | // 64 | //patching models to add additional required attributes 65 | sails 66 | .after(['hook:moduleloader:loaded'], function() { 67 | patchAttributes(); 68 | }); 69 | 70 | //later on wait for this events 71 | //to apply extra methods to models 72 | var eventsToWaitFor = []; 73 | 74 | //wait for orm 75 | //and pub sub hooks 76 | //to be loaded 77 | //for additional methods to 78 | //be attached to models 79 | if (sails.hooks.orm) { 80 | eventsToWaitFor.push('hook:orm:loaded'); 81 | } 82 | if (sails.hooks.pubsub) { 83 | eventsToWaitFor.push('hook:pubsub:loaded'); 84 | } 85 | 86 | sails 87 | .after(eventsToWaitFor, function() { 88 | 89 | //bind additional methods 90 | //to models 91 | //and let sails to continue 92 | patch(); 93 | 94 | done(); 95 | }); 96 | } 97 | }; 98 | }; -------------------------------------------------------------------------------- /lib/countAndFind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var path = require('path'); 5 | 6 | //import countAndFind deferred 7 | var Deferred = require(path.join(__dirname, 'utils', 'countAndFindDeferred')); 8 | 9 | /** 10 | * @function 11 | * @description count and find records based or criteria specified. 12 | * The returned result is in the form {count:...,data:[..]} 13 | * @param {Object} model a valid sails model 14 | */ 15 | module.exports = function(model) { 16 | 17 | /** 18 | * @function 19 | * @description Count and find records based on the criteria specified 20 | * @param {Object} criteria A valid sails waterline criteria. 21 | * If not provide empty `{}` criteria will be used. 22 | * @param {Function} callback A callback to be invoked on result 23 | */ 24 | function countAndFind(criteria, callback) { 25 | //check if criteria provided 26 | //default to `{}` if not explicit provided 27 | if (criteria && !_.isPlainObject(criteria)) { 28 | callback = criteria; 29 | criteria = {}; 30 | } 31 | 32 | //we are not sure if callback 33 | //was provided too 34 | else { 35 | criteria = criteria || {}; 36 | } 37 | 38 | //create model.countAndFind() deferrred object 39 | //with model.find() as query method 40 | //and use current criteria 41 | var deferred = new Deferred(model, model.find, criteria); 42 | 43 | //if callback provided 44 | //execute the query 45 | if (_.isFunction(callback)) { 46 | return deferred.exec(callback); 47 | } 48 | 49 | //otherwise return the deferred object 50 | else { 51 | return deferred; 52 | } 53 | 54 | } 55 | 56 | //attach countAndFind 57 | //to the model 58 | model.countAndFind = countAndFind; 59 | }; -------------------------------------------------------------------------------- /lib/countAndSearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | /** 5 | * @function 6 | * @requires countAndFind 7 | * @description Count hits and perform free search on record(s) using like 8 | * criteria modifier on model attributes 9 | * Warning!: Using this type of search directly when dataset is 10 | * few hundreds otherwise consider using search with pagination 11 | * @param {Object} model a valid sails model 12 | */ 13 | module.exports = function(model) { 14 | 15 | /** 16 | * @function 17 | * @description Count hits and perform free search on record(s) using like criteria modifier 18 | * @param {String} searchTerm A search term to match agaist record(s) 19 | * @param {Function} callback A callback to be invoked on result 20 | */ 21 | function countAndSearch(searchTerm, callback) { 22 | //check if search term provided 23 | //default to undefined if not explicit provided 24 | if (searchTerm && !_.isString(searchTerm)) { 25 | callback = searchTerm; 26 | searchTerm = undefined; 27 | } 28 | 29 | //using OR criteria to perfom search 30 | var criterias = { 31 | or: [] 32 | }; 33 | 34 | //prepare types of model attributes 35 | //where search can be performed on 36 | var searchableTypes = 37 | model.searchableTypes || [ 38 | 'string', 'text', 'integer', 39 | 'float', 'json', 'email' 40 | ]; 41 | 42 | //if search term is provided 43 | //build criterias 44 | if (searchTerm) { 45 | //using model attributes to build search query 46 | _(model.attributes) 47 | .forEach(function(attributeDefinition, attributeName) { 48 | // check if attribute type is within searchable types 49 | var isSearchable = 50 | _.indexOf(searchableTypes, attributeDefinition.type) >= 0; 51 | 52 | if (isSearchable) { 53 | //create attribute contains criteria 54 | var criteria = {}; 55 | criteria[attributeName] = { 56 | 'contains': searchTerm 57 | }; 58 | 59 | // push attribute criteria on criterias 60 | criterias.or.push(criteria); 61 | } 62 | }); 63 | } 64 | 65 | //otherwise no search term provided 66 | //then use empty criterias 67 | else { 68 | criterias = {}; 69 | } 70 | 71 | //build a countAndFind query using criterias 72 | var query = model.countAndFind(criterias); 73 | 74 | //if callback provided 75 | //execute the query 76 | if (_.isFunction(callback)) { 77 | return query.exec(callback); 78 | } 79 | 80 | //otherwise return the deferred object 81 | else { 82 | return query; 83 | } 84 | } 85 | 86 | //attach countAndSearch 87 | //to the model 88 | model.countAndSearch = countAndSearch; 89 | }; -------------------------------------------------------------------------------- /lib/first.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @function 5 | * @description Find the first record (or first N records if a parameter is supplied) 6 | * @param {Object} model a valid sails model 7 | */ 8 | module.exports = function(model) { 9 | 10 | /** 11 | * @function 12 | * @description Find the first record (or first N records if a parameter is supplied) 13 | * @param {Integer} howMany Number of records required. If not provided 14 | * default to 1. 15 | * @param {Function} callback A callback to be invoked on result 16 | */ 17 | function first(howMany, callback) { 18 | //check how many records are needed 19 | //default to 1 if not explicit provided 20 | if (howMany && !_.isNumber(howMany)) { 21 | callback = howMany; 22 | howMany = 1; 23 | } 24 | 25 | 26 | //we are not sure if callback 27 | //was provided too 28 | else { 29 | howMany = howMany || 1; 30 | } 31 | 32 | //initialize Deferred style 33 | //query builder 34 | var query = model.find(); 35 | 36 | //start from offset 0 37 | query.skip(0); 38 | 39 | //limit to how many required 40 | query.limit(howMany); 41 | 42 | //if callback provided 43 | //execute the query 44 | if (_.isFunction(callback)) { 45 | return query.exec(callback); 46 | } 47 | 48 | //otherwise return the deferred object 49 | else { 50 | return query; 51 | } 52 | 53 | } 54 | 55 | //attach first 56 | //to the model 57 | model.first = first; 58 | }; -------------------------------------------------------------------------------- /lib/last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | //dependencies 3 | var path = require('path'); 4 | 5 | //import last deferred 6 | var Deferred = require(path.join(__dirname, 'utils', 'lastDeferred')); 7 | 8 | /** 9 | * @function 10 | * @description Find the last record (or last N records if a parameter is supplied) 11 | * @param {Object} model a valid sails model 12 | */ 13 | module.exports = function(model) { 14 | 15 | /** 16 | * @function 17 | * @description Find the last record (or last N records if a parameter is supplied) 18 | * @param {Integer} howMany Number of records required. If not provided 19 | * default to 1. 20 | * @param {Function} callback A callback to be invoked on result 21 | */ 22 | function last(howMany, callback) { 23 | //check how many records are needed 24 | //default to 1 if not explicit provided 25 | if (howMany && !_.isNumber(howMany)) { 26 | callback = howMany; 27 | howMany = 1; 28 | } 29 | 30 | //we are not sure if callback 31 | //was provided too 32 | else { 33 | howMany = howMany || 1; 34 | } 35 | 36 | //create model.last() deferrred object 37 | //with model.find() as query method 38 | //and use empty criteria 39 | var deferred = new Deferred(model, model.find, {}); 40 | 41 | //set how many record needed 42 | deferred.howMany = howMany; 43 | 44 | //if callback provided 45 | //execute the query 46 | if (_.isFunction(callback)) { 47 | return deferred.exec(callback); 48 | } 49 | 50 | //otherwise return the deferred object 51 | else { 52 | return deferred; 53 | } 54 | 55 | } 56 | 57 | //attach last 58 | //to the model 59 | model.last = last; 60 | }; -------------------------------------------------------------------------------- /lib/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * @function 4 | * @description Perform free search on record(s) using like criteria modifier on 5 | * model attributes 6 | * Warning!: Using this type of search directly when dataset is 7 | * few hundreds otherwise consider using search with pagination 8 | * @param {Object} model a valid sails model 9 | */ 10 | module.exports = function(model) { 11 | 12 | /** 13 | * @function 14 | * @description Perform free search on record(s) using like criteria modifier 15 | * @param {String} searchTerm A search term to match agaist record(s) 16 | * @param {Function} callback A callback to be invoked on result 17 | */ 18 | function search(searchTerm, callback) { 19 | //check if search term provided 20 | //default to undefined if not explicit provided 21 | if (searchTerm && !_.isString(searchTerm)) { 22 | callback = searchTerm; 23 | searchTerm = undefined; 24 | } 25 | 26 | //using OR criteria to perfom search 27 | var criterias = { 28 | or: [] 29 | }; 30 | 31 | //prepare types of model attributes 32 | //where search can be performed on 33 | var searchableTypes = 34 | model.searchableTypes || [ 35 | 'string', 'text', 'integer', 36 | 'float', 'json', 'email' 37 | ]; 38 | 39 | //if search term is provided 40 | //build criterias 41 | if (searchTerm) { 42 | //using model attributes to build search query 43 | _(model.attributes) 44 | .forEach(function(attributeDefinition, attributeName) { 45 | // check if attribute type is within searchable types 46 | var isSearchable = 47 | _.indexOf(searchableTypes, attributeDefinition.type) >= 0; 48 | 49 | if (isSearchable) { 50 | //create attribute contains criteria 51 | var criteria = {}; 52 | criteria[attributeName] = { 53 | 'contains': searchTerm 54 | }; 55 | 56 | // push attribute criteria on criterias 57 | criterias.or.push(criteria); 58 | } 59 | }); 60 | } 61 | 62 | //otherwise no search term provided 63 | //then use empty criterias 64 | else { 65 | criterias = {}; 66 | } 67 | 68 | //build a find query using criterias 69 | var query = model.find(criterias); 70 | 71 | //if callback provided 72 | //execute the query 73 | if (_.isFunction(callback)) { 74 | return query.exec(callback); 75 | } 76 | 77 | //otherwise return the deferred object 78 | else { 79 | return query; 80 | } 81 | } 82 | 83 | //attach search 84 | //to the model 85 | model.search = search; 86 | }; -------------------------------------------------------------------------------- /lib/softDelete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @function 5 | * @description Allow to soft delete of a model by set deletedAt 6 | * to current timestamp 7 | * @param {Object} model a valid sails model 8 | */ 9 | module.exports = function(model) { 10 | /** 11 | * @description soft delete a model by set deletedAt to current timestamp 12 | * @param {Object} criteria A criteria to use on soft delete a model(s) 13 | * @param {Function} callback A callback to be invoked on result 14 | */ 15 | function softDelete(criteria, callback) { 16 | //check if criteria provided 17 | //default to `{}` if not explicit provided 18 | if (criteria && !_.isPlainObject(criteria)) { 19 | callback = criteria; 20 | criteria = {}; 21 | } 22 | 23 | //we are not sure if callback 24 | //was provided too 25 | else { 26 | criteria = criteria || {}; 27 | } 28 | 29 | //set deletedAt timestamp 30 | var deletedAt = { 31 | deletedAt: new Date() 32 | }; 33 | 34 | //if callback provided 35 | //execute the query 36 | if (_.isFunction(callback)) { 37 | return model.update(criteria, deletedAt).exec(callback); 38 | } 39 | 40 | //otherwise return the deferred object 41 | else { 42 | return model.update(criteria, deletedAt); 43 | } 44 | } 45 | 46 | //attach softDelete 47 | //to model 48 | model.softDelete = softDelete; 49 | }; -------------------------------------------------------------------------------- /lib/utils/countAndFindDeferred.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var util = require('util'); 5 | 6 | //import sails waterline Deferred 7 | var Deferred = require('sails/node_modules/waterline/lib/waterline/query/deferred'); 8 | 9 | //create a countAndFind deferred 10 | // which inherit from sails waterline deferred 11 | // to allow easy queries constructions 12 | var ExtraDeferred = function(context, method, criteria, values) { 13 | 14 | if (!context) { 15 | return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); 16 | } 17 | 18 | if (!method) { 19 | return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); 20 | } 21 | 22 | this._context = context; 23 | this._method = method; 24 | this._criteria = criteria; 25 | this._values = values || null; 26 | 27 | this._deferred = null; // deferred object for promises 28 | 29 | return this; 30 | 31 | }; 32 | 33 | //inherit sails deferred 34 | util.inherits(ExtraDeferred, Deferred); 35 | 36 | 37 | //override exec to allow 38 | //to run both count and find queries 39 | //using same criteria 40 | ExtraDeferred.prototype.exec = function(callback) { 41 | var me = this; 42 | 43 | if (!callback) { 44 | console.log(new Error('Error: No Callback supplied, you must define a callback.').message); 45 | return; 46 | } 47 | 48 | var normalize = require('sails/node_modules/waterline/lib/waterline/utils/normalize'); 49 | 50 | // Normalize callback/switchback 51 | callback = normalize.callback(callback); 52 | 53 | //run count and find 54 | //parallel 55 | async 56 | .parallel({ 57 | count: function(done) { 58 | //count records using criteria 59 | me._context 60 | .count(me._criteria).exec(done); 61 | }, 62 | data: function(done) { 63 | //find records using criteria 64 | me._method.call(me._context, me._criteria, done); 65 | } 66 | }, 67 | function(error, results) { 68 | //back off on error 69 | if (error) { 70 | callback(error); 71 | } 72 | 73 | //return result in form 74 | //{count:dataCount, data:[..found records...]} 75 | else { 76 | //TODO what should we do if data is undefined? 77 | callback(null, results); 78 | } 79 | }); 80 | }; 81 | 82 | //export extra deferred 83 | module.exports = ExtraDeferred; -------------------------------------------------------------------------------- /lib/utils/lastDeferred.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var util = require('util'); 5 | 6 | //import sails waterline Deferred 7 | var Deferred = require('sails/node_modules/waterline/lib/waterline/query/deferred'); 8 | 9 | //create a last deferred 10 | // which inherit from sails waterline deferred 11 | // to allow easy queries constructions 12 | var ExtraDeferred = function(context, method, criteria, values) { 13 | 14 | if (!context) { 15 | return new Error('Must supply a context to a new Deferred object. Usage: new Deferred(context, method, criteria)'); 16 | } 17 | 18 | if (!method) { 19 | return new Error('Must supply a method to a new Deferred object. Usage: new Deferred(context, method, criteria)'); 20 | } 21 | 22 | this._context = context; 23 | this._method = method; 24 | this._criteria = criteria; 25 | this._values = values || null; 26 | 27 | this._deferred = null; // deferred object for promises 28 | 29 | return this; 30 | 31 | }; 32 | 33 | //inherit sails deferred 34 | util.inherits(ExtraDeferred, Deferred); 35 | 36 | //keep reference of how many 37 | //records required before apply it 38 | //to criteria 39 | ExtraDeferred.prototype.howMany = 1; 40 | 41 | //override exec to allow 42 | //to run both count and find using 43 | //same criteria 44 | ExtraDeferred.prototype.exec = function(callback) { 45 | var me = this; 46 | 47 | if (!callback) { 48 | console.log(new Error('Error: No Callback supplied, you must define a callback.').message); 49 | return; 50 | } 51 | 52 | var normalize = require('sails/node_modules/waterline/lib/waterline/utils/normalize'); 53 | 54 | // Normalize callback/switchback 55 | callback = normalize.callback(callback); 56 | 57 | //count existing records 58 | //using criteria 59 | // 60 | //TODO wait fo waterline2 to be able 61 | //to use sub queries 62 | me._context 63 | .count(me._criteria, function(error, count) { 64 | //back off if count error(s) found 65 | if (error) { 66 | callback(error); 67 | } 68 | 69 | //count successfully 70 | //continue with selection 71 | else { 72 | //set criteria offset 73 | me._criteria.skip = count - me.howMany; 74 | 75 | //set criteria limit 76 | me._criteria.limit = me.howMany; 77 | 78 | // Pass control to the adapter with the appropriate arguments. 79 | me._method.call(me._context, me._criteria, callback); 80 | } 81 | }); 82 | }; 83 | 84 | 85 | 86 | //export extra deferred 87 | module.exports = ExtraDeferred; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sails-hook-model-extra", 3 | "version": "0.3.1", 4 | "description": "Additional model methods for sails", 5 | "main": "index.js", 6 | "sails": { 7 | "isHook": true 8 | }, 9 | "scripts": { 10 | "pretest": "rm -rf .tmp && npm link && npm link sails-hook-model-extra", 11 | "test": "grunt test", 12 | "posttest": "rm -rf .tmp" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/lykmapipo/sails-hook-model-extra.git" 17 | }, 18 | "keywords": [ 19 | "sails", 20 | "model", 21 | "extra", 22 | "hook", 23 | "additional", 24 | "filters", 25 | "projections", 26 | "queries", 27 | "helpers", 28 | "search", 29 | "waterline", 30 | "orm", 31 | "plugin", 32 | "module" 33 | ], 34 | "author": "Lally Elias", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/lykmapipo/sails-hook-model-extra/issues" 38 | }, 39 | "homepage": "https://github.com/lykmapipo/sails-hook-model-extra", 40 | "contributors": [{ 41 | "name": "lykmapipo", 42 | "github": "https://github.com/lykmapipo" 43 | }], 44 | "devDependencies": { 45 | "chai": "^2.1.2", 46 | "faker": "^2.1.2", 47 | "mocha": "^2.2.1", 48 | "sails": "^0.11.0", 49 | "sails-disk": "^0.10.8", 50 | "sails-hook-seed": "^0.1.1", 51 | "grunt": "^0.4.5", 52 | "grunt-contrib-jshint": "^0.11.2", 53 | "grunt-mocha-test": "^0.12.7", 54 | "jshint-stylish": "^1.0.1" 55 | } 56 | } -------------------------------------------------------------------------------- /seeds/test/UserSeed.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // var faker = require('faker'); 3 | // var fs = require('fs'); 4 | // var users = []; 5 | 6 | //fake users generator 7 | // for (var i = 0; i < 10; i++) { 8 | // users.push({ 9 | // email: faker.internet.email().toLowerCase(), 10 | // username: faker.name.firstName() + ' ' + faker.name.lastName() 11 | // }); 12 | // }; 13 | // fs.writeFileSync('users.json', JSON.stringify(users)); 14 | 15 | /** 16 | * exports user seeds 17 | */ 18 | module.exports = [{ 19 | email: 'aurelio_breitenberg@gmail.com', 20 | username: 'Trent Marvin' 21 | }, { 22 | email: 'hayden_durgan32@hotmail.com', 23 | username: 'Mabelle Moore' 24 | }, { 25 | email: 'velda_konopelski1@yahoo.com', 26 | username: 'Viva Gaylord' 27 | }, { 28 | email: 'kory.dooley@gmail.com', 29 | username: 'Malika Greenfelder' 30 | }, { 31 | email: 'kenyon_luettgen@gmail.com', 32 | username: 'Anne Howe' 33 | }, { 34 | email: 'dino_braun@yahoo.com', 35 | username: 'Victoria Steuber' 36 | }, { 37 | email: 'vicky2@gmail.com', 38 | username: 'Tillman Hermann' 39 | }, { 40 | email: 'kelton_pagac@hotmail.com', 41 | username: 'Raymundo Osinski' 42 | }, { 43 | email: 'maud.schmidt@hotmail.com', 44 | username: 'Richard Hudson' 45 | }, { 46 | email: 'arthur.kiehn3@yahoo.com', 47 | username: 'Adeline Wolff' 48 | }]; 49 | -------------------------------------------------------------------------------- /test/bootstrap.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * This file is useful when you want to execute some 5 | * code before and after running your tests 6 | * (e.g. lifting and lowering your sails application): 7 | */ 8 | var sails = require('sails'); 9 | 10 | /** 11 | * Lifting sails before all tests 12 | */ 13 | before(function(done) { 14 | sails 15 | .lift({ // configuration for testing purposes 16 | port: 7070, 17 | environment: 'test', 18 | log: { 19 | noShip: true 20 | }, 21 | models: { 22 | migrate: 'drop' 23 | }, 24 | hooks: { 25 | sockets: false, 26 | pubsub: false, 27 | views: false, 28 | http: false, 29 | grunt: false //we dont need grunt in test 30 | } 31 | }, function(error, sails) { 32 | if (error) { 33 | return done(error); 34 | } 35 | done(null, sails); 36 | }); 37 | }); 38 | 39 | 40 | /** 41 | * Lowering sails after done testing 42 | */ 43 | after(function(done) { 44 | User 45 | .destroy() 46 | .then(function( /*result*/ ) { 47 | sails.lower(done); 48 | }) 49 | .catch(function(error) { 50 | done(error); 51 | }); 52 | }); -------------------------------------------------------------------------------- /test/specs/countAndFind.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | 4 | describe('Model#countAndFind', function() { 5 | 6 | it('should attach countAndFind model static method', function(done) { 7 | expect(User).to.respondTo('countAndFind'); 8 | 9 | done(); 10 | }); 11 | 12 | 13 | it('should able to count and find all records with no criteria using callback model API', function(done) { 14 | 15 | User 16 | .countAndFind(function(error, results) { 17 | if (error) { 18 | done(error); 19 | } else { 20 | expect(results.count).to.exist; 21 | expect(results.data).to.exist; 22 | 23 | expect(results.count).to.be.equal(10); 24 | expect(results.data.length).to.be.equal(10); 25 | 26 | done(); 27 | } 28 | }); 29 | }); 30 | 31 | 32 | it('should able to count and find all records with criteria using callback model API', function(done) { 33 | 34 | User 35 | .countAndFind({ 36 | id: { 37 | '>': 2 38 | } 39 | }, function(error, results) { 40 | if (error) { 41 | done(error); 42 | } else { 43 | 44 | expect(results.count).to.exist; 45 | expect(results.data).to.exist; 46 | 47 | expect(results.count).to.be.equal(8); 48 | expect(results.data.length).to.be.equal(8); 49 | 50 | done(); 51 | } 52 | }); 53 | }); 54 | 55 | 56 | it('should able to count and find all records with no criteria using deferred model API', function(done) { 57 | 58 | User 59 | .countAndFind() 60 | .exec(function(error, results) { 61 | if (error) { 62 | done(error); 63 | } else { 64 | expect(results.count).to.exist; 65 | expect(results.data).to.exist; 66 | 67 | expect(results.count).to.be.equal(10); 68 | expect(results.data.length).to.be.equal(10); 69 | 70 | done(); 71 | } 72 | }); 73 | }); 74 | 75 | 76 | it('should able to count and find all records with criteria using deferred model API', function(done) { 77 | 78 | User 79 | .countAndFind({ 80 | id: { 81 | '>': 2 82 | } 83 | }) 84 | .exec(function(error, results) { 85 | if (error) { 86 | done(error); 87 | } else { 88 | 89 | expect(results.count).to.exist; 90 | expect(results.data).to.exist; 91 | 92 | expect(results.count).to.be.equal(8); 93 | expect(results.data.length).to.be.equal(8); 94 | 95 | done(); 96 | } 97 | }); 98 | }); 99 | 100 | 101 | 102 | it('should able to count and find all records with no criteria using promise model API', function(done) { 103 | 104 | User 105 | .countAndFind() 106 | .then(function(results) { 107 | 108 | expect(results.count).to.exist; 109 | expect(results.data).to.exist; 110 | 111 | expect(results.count).to.be.equal(10); 112 | expect(results.data.length).to.be.equal(10); 113 | 114 | done(); 115 | }) 116 | .catch(function(error) { 117 | done(error); 118 | }); 119 | }); 120 | 121 | 122 | it('should able to count and find all records with criteria using promise model API', function(done) { 123 | 124 | User 125 | .countAndFind({ 126 | id: { 127 | '>': 2 128 | } 129 | }) 130 | .then(function(results) { 131 | 132 | expect(results.count).to.exist; 133 | expect(results.data).to.exist; 134 | 135 | expect(results.count).to.be.equal(8); 136 | expect(results.data.length).to.be.equal(8); 137 | 138 | done(); 139 | }) 140 | .catch(function(error) { 141 | done(error); 142 | }); 143 | }); 144 | 145 | }); 146 | -------------------------------------------------------------------------------- /test/specs/countAndSearch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var expect = require('chai').expect; 5 | 6 | describe('Model#search', function() { 7 | 8 | it('should be able to attach countAndSearch as model method', function(done) { 9 | expect(User).to.respondTo('countAndSearch'); 10 | done(); 11 | }); 12 | 13 | it('should be able to run as countAndFind() when no searchTerm provided', function(done) { 14 | User 15 | .countAndSearch(function(error, results) { 16 | if (error) { 17 | done(error); 18 | } else { 19 | expect(results.count).to.be.equal(10); 20 | done(); 21 | } 22 | }); 23 | }); 24 | 25 | 26 | it('should be able to count hits and search for records using model callback API', function(done) { 27 | User 28 | .countAndSearch('gmail', function(error, results) { 29 | if (error) { 30 | done(error); 31 | } else { 32 | expect(results.count).to.be.equal(4); 33 | 34 | expect(_.map(results.data, 'username')) 35 | .to.include.members(['Trent Marvin', 'Malika Greenfelder']); 36 | 37 | done(); 38 | } 39 | }); 40 | }); 41 | 42 | 43 | it('should be able to count hits and search for records using model deferred API', function(done) { 44 | User 45 | .countAndSearch('vi') 46 | .exec(function(error, results) { 47 | if (error) { 48 | done(error); 49 | } else { 50 | expect(results.count).to.be.equal(4); 51 | 52 | expect(_.map(results.data, 'username')) 53 | .to 54 | .include 55 | .members(['Trent Marvin', 'Viva Gaylord', 'Victoria Steuber']); 56 | 57 | expect(_.map(results.data, 'email')) 58 | .to 59 | .include 60 | .members(['vicky2@gmail.com']); 61 | 62 | done(); 63 | } 64 | }); 65 | }); 66 | 67 | 68 | it('should be able to count hits and search for records using model promise API', function(done) { 69 | User 70 | .countAndSearch('Malika') 71 | .then(function(results) { 72 | expect(results.count).to.be.equal(1); 73 | 74 | expect(results.data[0].username).to.be.equal('Malika Greenfelder'); 75 | expect(results.data[0].email).to.be.equal('kory.dooley@gmail.com'); 76 | 77 | done(); 78 | }) 79 | .catch(function(error) { 80 | done(error); 81 | }); 82 | }); 83 | 84 | }); -------------------------------------------------------------------------------- /test/specs/first.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | 4 | describe('Model#first', function() { 5 | 6 | it('should be able to attach first as model method', function(done) { 7 | expect(User).to.respondTo('first'); 8 | done(); 9 | }); 10 | 11 | it('should be able to set only one record is wanted by default', function(done) { 12 | var first = User.first(); 13 | 14 | expect(first._criteria.skip).to.be.equal(0); 15 | expect(first._criteria.limit).to.be.equal(1); 16 | 17 | done(); 18 | }); 19 | 20 | it('should be able to set how many records are wanted', function(done) { 21 | var first = User.first(5); 22 | 23 | expect(first._criteria.skip).to.be.equal(0); 24 | expect(first._criteria.limit).to.be.equal(5); 25 | 26 | done(); 27 | }); 28 | 29 | 30 | 31 | it('should be able to get a first record using callback model API', function(done) { 32 | 33 | User 34 | .first(function(error, users) { 35 | if (error) { 36 | done(error); 37 | } else { 38 | expect(users[0].id).to.be.equal(1); 39 | expect(users.length).to.be.equal(1); 40 | done(); 41 | } 42 | }); 43 | 44 | }); 45 | 46 | 47 | it('should able to get first five records using callback model API', function(done) { 48 | 49 | User 50 | .first(5, function(error, users) { 51 | if (error) { 52 | done(error); 53 | } else { 54 | expect(_.map(users, 'id')) 55 | .to.include.members([1, 2, 3, 4, 5]); 56 | 57 | expect(users.length).to.be.equal(5); 58 | done(); 59 | } 60 | }); 61 | 62 | }); 63 | 64 | 65 | it('should be able to get a first record using deferred model API', function(done) { 66 | 67 | User 68 | .first() 69 | .exec(function(error, users) { 70 | if (error) { 71 | done(error); 72 | } else { 73 | expect(users[0].id).to.be.equal(1); 74 | expect(users.length).to.be.equal(1); 75 | done(); 76 | } 77 | }); 78 | 79 | }); 80 | 81 | 82 | it('should be able to get first five record using promise model API', function(done) { 83 | 84 | User 85 | .first(5) 86 | .then(function(users) { 87 | expect(_.map(users, 'id')) 88 | .to.include.members([1, 2, 3, 4, 5]); 89 | 90 | expect(users.length).to.be.equal(5); 91 | done(); 92 | }) 93 | .catch(function(error) { 94 | done(error); 95 | }); 96 | 97 | }); 98 | 99 | 100 | it('should be able to apply other criteria(s) on returned deferred to get a first record', 101 | function(done) { 102 | 103 | User 104 | .first() 105 | .where({ 106 | id: { 107 | '>': 2 108 | } 109 | }) 110 | .exec(function(error, users) { 111 | if (error) { 112 | done(error); 113 | } else { 114 | expect(users[0].id).to.be.equal(3); 115 | expect(users.length).to.be.equal(1); 116 | done(); 117 | } 118 | }); 119 | 120 | } 121 | ); 122 | 123 | it('should be able to apply other criteria(s) on returned deferred to get first five record(s)', 124 | function(done) { 125 | 126 | User 127 | .first(5) 128 | .where({ 129 | id: { 130 | '>': 2 131 | } 132 | }) 133 | .exec(function(error, users) { 134 | if (error) { 135 | done(error); 136 | } else { 137 | expect(_.map(users, 'id')) 138 | .to.include.members([3, 4, 5, 6, 7]); 139 | 140 | expect(users.length).to.be.equal(5); 141 | done(); 142 | } 143 | }); 144 | 145 | } 146 | ); 147 | 148 | }); 149 | -------------------------------------------------------------------------------- /test/specs/last.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | 4 | describe('Model#last', function() { 5 | 6 | it('should be able to attach last as model method', function(done) { 7 | expect(User).to.respondTo('last'); 8 | done(); 9 | }); 10 | 11 | it('should be able to set only one record is wanted by default', function(done) { 12 | var last = User.last(); 13 | expect(last.howMany).to.be.equal(1); 14 | done(); 15 | }); 16 | 17 | it('should be able to set how many records are wanted', function(done) { 18 | var last = User.last(5); 19 | expect(last.howMany).to.be.equal(5); 20 | done(); 21 | }); 22 | 23 | 24 | 25 | it('should be able to get a last record using callback model API', function(done) { 26 | 27 | User 28 | .last(function(error, users) { 29 | if (error) { 30 | done(error); 31 | } else { 32 | expect(users[0].id).to.be.equal(10); 33 | expect(users.length).to.be.equal(1); 34 | done(); 35 | } 36 | }); 37 | 38 | }); 39 | 40 | 41 | it('should able to get last five records using callback model API', function(done) { 42 | 43 | User 44 | .last(5, function(error, users) { 45 | if (error) { 46 | done(error); 47 | } else { 48 | expect(_.map(users, 'id')) 49 | .to.include.members([10, 9, 8, 7, 6]); 50 | 51 | expect(users.length).to.be.equal(5); 52 | done(); 53 | } 54 | }); 55 | 56 | }); 57 | 58 | 59 | it('should be able to get a last record using deferred model API', function(done) { 60 | 61 | User 62 | .last() 63 | .exec(function(error, users) { 64 | if (error) { 65 | done(error); 66 | } else { 67 | expect(users[0].id).to.be.equal(10); 68 | expect(users.length).to.be.equal(1); 69 | done(); 70 | } 71 | }); 72 | 73 | }); 74 | 75 | 76 | it('should be able to get last five record using promise model API', function(done) { 77 | 78 | User 79 | .last(5) 80 | .then(function(users) { 81 | expect(_.map(users, 'id')) 82 | .to.include.members([10, 9, 8, 7, 6]); 83 | 84 | expect(users.length).to.be.equal(5); 85 | done(); 86 | }) 87 | .catch(function(error) { 88 | done(error); 89 | }); 90 | 91 | }); 92 | 93 | 94 | it('should be able to apply other criteria(s) on returned deferred to get a last record', 95 | function(done) { 96 | 97 | User 98 | .last() 99 | .where({ 100 | id: { 101 | '<': 8 102 | } 103 | }) 104 | .exec(function(error, users) { 105 | if (error) { 106 | done(error); 107 | } else { 108 | expect(users[0].id).to.be.equal(7); 109 | expect(users.length).to.be.equal(1); 110 | done(); 111 | } 112 | }); 113 | 114 | } 115 | ); 116 | 117 | it('should be able to apply other criteria(s) on returned deferred to get last five record(s)', 118 | function(done) { 119 | 120 | User 121 | .last(5) 122 | .where({ 123 | id: { 124 | '<': 8 125 | } 126 | }) 127 | .exec(function(error, users) { 128 | if (error) { 129 | done(error); 130 | } else { 131 | expect(_.map(users, 'id')) 132 | .to.include.members([7, 6, 5, 4, 3]); 133 | 134 | expect(users.length).to.be.equal(5); 135 | 136 | done(); 137 | } 138 | }); 139 | 140 | } 141 | ); 142 | 143 | }); 144 | -------------------------------------------------------------------------------- /test/specs/search.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | //dependencies 4 | var expect = require('chai').expect; 5 | 6 | describe('Model#search', function() { 7 | 8 | it('should be able to attach search as model method', function(done) { 9 | expect(User).to.respondTo('search'); 10 | done(); 11 | }); 12 | 13 | it('should be able to run as find() when no searchTerm provided', function(done) { 14 | User 15 | .search(function(error, users) { 16 | if (error) { 17 | done(error); 18 | } else { 19 | expect(users.length).to.be.equal(10); 20 | done(); 21 | } 22 | }); 23 | }); 24 | 25 | 26 | it('should be able to search for records using model callback API', function(done) { 27 | User 28 | .search('gmail', function(error, users) { 29 | if (error) { 30 | done(error); 31 | } else { 32 | expect(users.length).to.be.equal(4); 33 | 34 | expect(_.map(users, 'username')) 35 | .to.include.members(['Trent Marvin', 'Malika Greenfelder']); 36 | 37 | done(); 38 | } 39 | }); 40 | }); 41 | 42 | 43 | it('should be able to search for records using model deferred API', function(done) { 44 | User 45 | .search('vi') 46 | .exec(function(error, users) { 47 | if (error) { 48 | done(error); 49 | } else { 50 | expect(users.length).to.be.equal(4); 51 | 52 | expect(_.map(users, 'username')) 53 | .to 54 | .include 55 | .members(['Trent Marvin', 'Viva Gaylord', 'Victoria Steuber']); 56 | 57 | expect(_.map(users, 'email')) 58 | .to 59 | .include 60 | .members(['vicky2@gmail.com']); 61 | 62 | done(); 63 | } 64 | }); 65 | }); 66 | 67 | 68 | it('should be able to search for records using model promise API', function(done) { 69 | User 70 | .search('Malika') 71 | .then(function(users) { 72 | expect(users.length).to.be.equal(1); 73 | 74 | expect(users[0].username).to.be.equal('Malika Greenfelder'); 75 | expect(users[0].email).to.be.equal('kory.dooley@gmail.com'); 76 | 77 | done(); 78 | }) 79 | .catch(function(error) { 80 | done(error); 81 | }); 82 | }); 83 | 84 | }); -------------------------------------------------------------------------------- /test/specs/softDelete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var expect = require('chai').expect; 3 | var faker = require('faker'); 4 | 5 | describe('Model#softDelete', function() { 6 | var user; 7 | 8 | //create user 9 | before(function(done) { 10 | User.create({ 11 | email: faker.internet.email(), 12 | username: faker.internet.email() 13 | }, function(error, response) { 14 | if (error) { 15 | done(error); 16 | } else { 17 | user = response; 18 | done(); 19 | } 20 | }); 21 | }); 22 | 23 | 24 | describe('Model#softDelete#static', function() { 25 | //clear user deletedAt before each spec 26 | beforeEach(function(done) { 27 | user.deletedAt = null; 28 | 29 | user 30 | .save(function(error, response) { 31 | if (error) { 32 | done(error); 33 | } else { 34 | user = response; 35 | done(); 36 | } 37 | }); 38 | }); 39 | 40 | it('should be able attach softDelete as model static method', function(done) { 41 | expect(User).to.respondTo('softDelete'); 42 | done(); 43 | }); 44 | 45 | it('should be able to soft delete model(s) using callback model API', function(done) { 46 | User 47 | .softDelete({ 48 | id: user.id 49 | }, function(error, deletedUsers) { 50 | if (error) { 51 | done(error); 52 | } else { 53 | 54 | expect(deletedUsers.length).to.be.equal(1); 55 | expect(deletedUsers[0].deletedAt).to.not.be.null; 56 | 57 | expect(deletedUsers[0].id).to.be.equal(user.id); 58 | expect(deletedUsers[0].email).to.be.equal(user.email); 59 | 60 | done(); 61 | } 62 | }); 63 | }); 64 | 65 | it('should be able to soft delete model(s) using deferred model API', function(done) { 66 | User 67 | .softDelete({ 68 | id: user.id 69 | }) 70 | .exec(function(error, deletedUsers) { 71 | if (error) { 72 | done(error); 73 | } else { 74 | 75 | expect(deletedUsers.length).to.be.equal(1); 76 | expect(deletedUsers[0].deletedAt).to.not.be.null; 77 | 78 | expect(deletedUsers[0].id).to.be.equal(user.id); 79 | expect(deletedUsers[0].email).to.be.equal(user.email); 80 | 81 | done(); 82 | } 83 | }); 84 | }); 85 | 86 | 87 | it('should be able to soft delete model(s) using promise model API', function(done) { 88 | User 89 | .softDelete({ 90 | id: user.id 91 | }) 92 | .then(function(deletedUsers) { 93 | expect(deletedUsers.length).to.be.equal(1); 94 | expect(deletedUsers[0].deletedAt).to.not.be.null; 95 | 96 | expect(deletedUsers[0].id).to.be.equal(user.id); 97 | expect(deletedUsers[0].email).to.be.equal(user.email); 98 | 99 | done(); 100 | }) 101 | .catch(function(error) { 102 | done(error); 103 | }); 104 | }); 105 | 106 | }); 107 | 108 | }); 109 | --------------------------------------------------------------------------------