├── test ├── db.js ├── knexfile.js ├── migrations │ └── 20141216121111_test-setup.js └── index.js ├── .travis.yml ├── .gitignore ├── package.json ├── LICENSE ├── lib └── index.js └── README.md /test/db.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var knexFile = require(path.resolve(__dirname, 'knexfile.js')) 3 | var knex = require('knex')(knexFile['development']) 4 | 5 | module.exports = knex 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | - '8' 5 | after_script: 6 | - npm install -g codeclimate-test-reporter 7 | - CODECLIMATE_REPO_TOKEN=be81a850806bfaf0ea98cbd38daef658481d1bbcf04886de925b8c043eb0c20e codeclimate < ./coverage/lcov.info 8 | -------------------------------------------------------------------------------- /test/knexfile.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | // Update with your config settings. 3 | 4 | module.exports = { 5 | development: { 6 | client: 'sqlite3', 7 | connection: ':memory:', 8 | pool: { 9 | max: 1, 10 | min: 1 11 | }, 12 | useNullAsDefault: true, 13 | migrations: { 14 | directory: path.resolve(__dirname, 'migrations') 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/migrations/20141216121111_test-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | exports.up = function (knex, Promise) { 4 | return knex.schema.createTable('test_table', function (table) { 5 | table.increments('id') 6 | table.string('first_name').notNullable() 7 | table.string('last_name') 8 | table.timestamps() 9 | }) 10 | } 11 | 12 | exports.down = function (knex, Promise) { 13 | return knex.schema.dropTable('test_table') 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # Development DB 31 | test/dev.sqlite3 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookshelf-modelbase", 3 | "version": "2.11.0", 4 | "description": "Extensible ModelBase for bookshelf-based model layers", 5 | "main": "./lib", 6 | "scripts": { 7 | "test": "standard && istanbul cover _mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/bsiddiqui/bookshelf-modelbase.git" 12 | }, 13 | "author": "Basil Siddiqui , Grayson Chao ", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/bsiddiqui/bookshelf-modelbase/issues" 17 | }, 18 | "homepage": "https://github.com/bsiddiqui/bookshelf-modelbase", 19 | "dependencies": { 20 | "joi": "^14.3.1", 21 | "lodash.difference": "^4.5.0" 22 | }, 23 | "devDependencies": { 24 | "bookshelf": "^1.1.0", 25 | "chai": "^4.2.0", 26 | "istanbul": "^0.4.4", 27 | "knex": "^0.20.10", 28 | "mocha": "^5.2.0", 29 | "sinon": "^6.3.4", 30 | "sqlite3": "^4.0.2", 31 | "standard": "^12.0.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Basil Siddiqui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Joi = require('joi') 2 | var difference = require('lodash.difference') 3 | 4 | module.exports = function modelBase (bookshelf, params) { 5 | if (!bookshelf) { 6 | throw new Error('Must pass an initialized bookshelf instance') 7 | } 8 | 9 | var bookshelfModel = bookshelf.Model 10 | 11 | var model = bookshelf.Model.extend({ 12 | constructor: function () { 13 | bookshelfModel.apply(this, arguments) 14 | 15 | if (this.validate) { 16 | var baseValidation = { 17 | // id might be number or string, for optimization 18 | id: Joi.any().optional(), 19 | created_at: Joi.date().optional(), 20 | updated_at: Joi.date().optional() 21 | } 22 | 23 | this.validate = this.validate.isJoi 24 | ? this.validate.keys(baseValidation) 25 | : Joi.object(this.validate).keys(baseValidation) 26 | 27 | this.on('saving', this.validateSave) 28 | } 29 | }, 30 | 31 | hasTimestamps: ['created_at', 'updated_at'], 32 | 33 | validateSave: function (model, attrs, options) { 34 | var validation 35 | // model is not new or update method explicitly set 36 | if ((model && !model.isNew()) || (options && (options.method === 'update' || options.patch === true))) { 37 | var schemaKeys = this.validate._inner.children.map(function (child) { 38 | return child.key 39 | }) 40 | var presentKeys = Object.keys(attrs) 41 | var optionalKeys = difference(schemaKeys, presentKeys) 42 | // only validate the keys that are being updated 43 | validation = Joi.validate( 44 | attrs, 45 | optionalKeys.length 46 | // optionalKeys() doesn't like empty arrays 47 | ? this.validate.optionalKeys(optionalKeys) 48 | : this.validate 49 | ) 50 | } else { 51 | validation = Joi.validate(this.attributes, this.validate) 52 | } 53 | 54 | if (validation.error) { 55 | validation.error.tableName = this.tableName 56 | 57 | throw validation.error 58 | } else { 59 | this.set(validation.value) 60 | return validation.value 61 | } 62 | } 63 | 64 | }, { 65 | 66 | /** 67 | * Select a collection based on a query 68 | * @param {Object} [filter] 69 | * @param {Object} [options] Options used of model.fetchAll 70 | * @return {Promise(bookshelf.Collection)} Bookshelf Collection of Models 71 | */ 72 | findAll: function (filter, options) { 73 | return this.forge().where(filter || {}).fetchAll(options) 74 | }, 75 | 76 | /** 77 | * Find a model based on it's ID 78 | * @param {String} id The model's ID 79 | * @param {Object} [options] Options used of model.fetch 80 | * @return {Promise(bookshelf.Model)} 81 | */ 82 | findById: function (id, options) { 83 | return this.findOne({ [this.prototype.idAttribute]: id }, options) 84 | }, 85 | 86 | /** 87 | * Select a model based on a query 88 | * @param {Object} [query] 89 | * @param {Object} [options] Options for model.fetch 90 | * @param {Boolean} [options.require=false] 91 | * @return {Promise(bookshelf.Model)} 92 | */ 93 | findOne: function (query, options = {}) { 94 | options = Object.assign({ require: true }, options) 95 | return this.forge(query).fetch(options) 96 | }, 97 | 98 | /** 99 | * Insert a model based on data 100 | * @param {Object} data 101 | * @param {Object} [options] Options for model.save 102 | * @return {Promise(bookshelf.Model)} 103 | */ 104 | create: function (data, options) { 105 | return this.forge(data) 106 | .save(null, options) 107 | }, 108 | 109 | /** 110 | * Update a model based on data 111 | * @param {Object} data 112 | * @param {Object} options Options for model.fetch and model.save 113 | * @param {String|Integer} options.id The id of the model to update 114 | * @param {Boolean} [options.patch=true] 115 | * @param {Boolean} [options.require=true] 116 | * @return {Promise(bookshelf.Model)} 117 | */ 118 | update: function (data, options = {}) { 119 | options = Object.assign({ patch: true, require: true }, options) 120 | return this.forge({ [this.prototype.idAttribute]: options.id }).fetch(options) 121 | .then(function (model) { 122 | return model ? model.save(data, options) : undefined 123 | }) 124 | }, 125 | 126 | /** 127 | * Destroy a model by id 128 | * @param {Object} options 129 | * @param {String|Integer} options.id The id of the model to destroy 130 | * @param {Boolean} [options.require=true] 131 | * @return {Promise(bookshelf.Model)} empty model 132 | */ 133 | destroy: function (options = {}) { 134 | options = Object.assign({ require: true }, options) 135 | return this.forge({ [this.prototype.idAttribute]: options.id }) 136 | .destroy(options) 137 | }, 138 | 139 | /** 140 | * Select a model based on data and insert if not found 141 | * @param {Object} data 142 | * @param {Object} [options] Options for model.fetch and model.save 143 | * @param {Object} [options.defaults] Defaults to apply to a create 144 | * @return {Promise(bookshelf.Model)} single Model 145 | */ 146 | findOrCreate: function (data, options = {}) { 147 | return this.findOne(data, Object.assign({}, options, { require: false })) 148 | .bind(this) 149 | .then(function (model) { 150 | var defaults = (options && options.defaults) || {} 151 | return model || this.create(Object.assign(defaults, data), options) 152 | }) 153 | }, 154 | 155 | /** 156 | * Select a model based on data and update if found, insert if not found 157 | * @param {Object} selectData Data for select 158 | * @param {Object} updateData Data for update 159 | * @param {Object} [options] Options for model.save 160 | */ 161 | upsert: function (selectData, updateData, options = {}) { 162 | return this.findOne(selectData, Object.assign({}, options, { require: false })) 163 | .bind(this) 164 | .then(function (model) { 165 | return model 166 | ? model.save( 167 | updateData, 168 | Object.assign({ patch: true, method: 'update' }, options) 169 | ) 170 | : this.create( 171 | Object.assign({}, selectData, updateData), 172 | Object.assign({}, options, { method: 'insert' }) 173 | ) 174 | }) 175 | } 176 | }) 177 | 178 | return model 179 | } 180 | 181 | module.exports.pluggable = function (bookshelf, params) { 182 | bookshelf.Model = module.exports.apply(null, arguments) 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bookshelf-modelbase 2 | [![Build Status](https://travis-ci.org/bsiddiqui/bookshelf-modelbase.svg?branch=master)](https://travis-ci.org/bsiddiqui/bookshelf-modelbase) [![Code Climate](https://codeclimate.com/github/bsiddiqui/bookshelf-modelbase/badges/gpa.svg)](https://codeclimate.com/github/bsiddiqui/bookshelf-modelbase) [![Test Coverage](https://codeclimate.com/github/bsiddiqui/bookshelf-modelbase/badges/coverage.svg)](https://codeclimate.com/github/bsiddiqui/bookshelf-modelbase) [![Version](https://badge.fury.io/js/bookshelf-modelbase.svg)](http://badge.fury.io/js/bookshelf-modelbase) [![Downloads](http://img.shields.io/npm/dm/bookshelf-modelbase.svg)](https://www.npmjs.com/package/bookshelf-modelbase) 3 | 4 | ## Why 5 | [Bookshelf.js](https://github.com/tgriesser/bookshelf) is awesome. However, 6 | we found ourselves extending `bookshelf.Model` for the same reasons over and 7 | over - parsing and formatting (to and from DB) niceties, adding timestamps, and 8 | validating data on save, for example. Since these are problems you'll likely 9 | have to solve for most use cases of Bookshelf, it made sense to provide a 10 | convenient set of core model features. 11 | 12 | ### Please note 13 | * `bookshelf-modelbase` will not force you to use it for all your models. 14 | If you want to use it for some and not others, nothing bad will happen. 15 | 16 | * `bookshelf-modelbase` requires you to pass in an initialized instance 17 | of bookshelf, meaning that you can configure bookshelf however you please. 18 | Outside of overriding `bookshelf.Model`, there is nothing you can do to 19 | your bookshelf instance that will break `bookshelf-modelbase`. 20 | 21 | ### Features 22 | * Adds timestamps (`created_at` and `updated_at`) 23 | 24 | * Validate own attributes on save using [Joi](https://github.com/hapijs/joi). 25 | You can pass in a validation object as a class attribute when you extend 26 | `bookshelf-modelbase` - see below for usage. 27 | 28 | * Naive CRUD methods - `findAll`, `findOne`, `findOrCreate`, `create`, `update`, and `destroy` 29 | 30 | ## Usage 31 | ```javascript 32 | var db = require(knex)(require('./knexfile')); 33 | var bookshelf = require('bookshelf')(db); 34 | var Joi = require('joi'); 35 | // Pass an initialized bookshelf instance 36 | var ModelBase = require('bookshelf-modelbase')(bookshelf); 37 | // Or initialize as a bookshelf plugin 38 | bookshelf.plugin(require('bookshelf-modelbase').pluggable); 39 | 40 | var User = ModelBase.extend({ 41 | tableName: 'users', 42 | 43 | // validation is passed to Joi.object(), so use a raw object 44 | validate: { 45 | firstName: Joi.string() 46 | } 47 | }); 48 | 49 | User.create({ firstName: 'Grayson' }) 50 | .then(function () { 51 | return User.findOne({ firstName: 'Grayson' }, { require: true }); 52 | }) 53 | .then(function (grayson) { 54 | // passes patch: true to .save() by default 55 | return User.update({ firstName: 'Basil' }, { id: grayson.id }); 56 | }) 57 | .then(function (basil) { 58 | return User.destroy({ id: basil.id }); 59 | }) 60 | .then(function () { 61 | return User.findAll(); 62 | }) 63 | .then(function (collection) { 64 | console.log(collection.models.length); // => 0 65 | }) 66 | 67 | ``` 68 | 69 | ### API 70 | 71 | #### model.create 72 | 73 | ```js 74 | /** 75 | * Insert a model based on data 76 | * @param {Object} data 77 | * @param {Object} [options] Options for model.save 78 | * @return {Promise(bookshelf.Model)} 79 | */ 80 | create: function (data, options) { 81 | return this.forge(data).save(null, options); 82 | } 83 | ``` 84 | 85 | #### model.destroy 86 | 87 | ```js 88 | /** 89 | * Destroy a model by id 90 | * @param {Object} options 91 | * @param {String|Integer} options.id The id of the model to destroy 92 | * @param {Boolean} [options.require=false] 93 | * @return {Promise(bookshelf.Model)} empty model 94 | */ 95 | destroy: function (options) { 96 | options = extend({ require: true }, options); 97 | return this.forge({ [this.prototype.idAttribute]: options.id }) 98 | .destroy(options); 99 | } 100 | ``` 101 | 102 | #### model.findAll 103 | 104 | ```javascript 105 | /** 106 | * Select a collection based on a query 107 | * @param {Object} [query] 108 | * @param {Object} [options] Options used of model.fetchAll 109 | * @return {Promise(bookshelf.Collection)} Bookshelf Collection of Models 110 | */ 111 | findAll: function (filter, options) { 112 | return this.forge().where(extend({}, filter)).fetchAll(options); 113 | } 114 | ``` 115 | 116 | #### model.findById 117 | 118 | ```javascript 119 | /** 120 | * Find a model based on it's ID 121 | * @param {String} id The model's ID 122 | * @param {Object} [options] Options used of model.fetch 123 | * @return {Promise(bookshelf.Model)} 124 | */ 125 | findById: function (id, options) { 126 | return this.findOne({ [this.prototype.idAttribute]: id }, options); 127 | } 128 | ``` 129 | 130 | #### model.findOne 131 | 132 | ```js 133 | /** 134 | * Select a model based on a query 135 | * @param {Object} [query] 136 | * @param {Object} [options] Options for model.fetch 137 | * @param {Boolean} [options.require=false] 138 | * @return {Promise(bookshelf.Model)} 139 | */ 140 | findOne: function (query, options) { 141 | options = extend({ require: true }, options); 142 | return this.forge(query).fetch(options); 143 | } 144 | ``` 145 | 146 | #### model.findOrCreate 147 | ```js 148 | /** 149 | * Select a model based on data and insert if not found 150 | * @param {Object} data 151 | * @param {Object} [options] Options for model.fetch and model.save 152 | * @param {Object} [options.defaults] Defaults to apply to a create 153 | * @return {Promise(bookshelf.Model)} single Model 154 | */ 155 | findOrCreate: function (data, options) { 156 | return this.findOne(data, extend(options, { require: false })) 157 | .bind(this) 158 | .then(function (model) { 159 | var defaults = options && options.defaults; 160 | return model || this.create(extend(defaults, data), options); 161 | }); 162 | } 163 | ``` 164 | 165 | #### model.update 166 | 167 | ```js 168 | /** 169 | * Update a model based on data 170 | * @param {Object} data 171 | * @param {Object} options Options for model.fetch and model.save 172 | * @param {String|Integer} options.id The id of the model to update 173 | * @param {Boolean} [options.patch=true] 174 | * @param {Boolean} [options.require=true] 175 | * @return {Promise(bookshelf.Model)} 176 | */ 177 | update: function (data, options) { 178 | options = extend({ patch: true, require: true }, options); 179 | return this.forge({ [this.prototype.idAttribute]: options.id }).fetch(options) 180 | .then(function (model) { 181 | return model ? model.save(data, options) : undefined; 182 | }); 183 | } 184 | ``` 185 | 186 | #### model.upsert 187 | ```js 188 | /** 189 | * Select a model based on data and update if found, insert if not found 190 | * @param {Object} selectData Data for select 191 | * @param {Object} updateData Data for update 192 | * @param {Object} [options] Options for model.save 193 | */ 194 | upsert: function (selectData, updateData, options) { 195 | return this.findOne(selectData, extend(options, { require: false })) 196 | .bind(this) 197 | .then(function (model) { 198 | return model 199 | ? model.save( 200 | updateData, 201 | extend({ patch: true, method: 'update' }, options) 202 | ) 203 | : this.create( 204 | extend(selectData, updateData), 205 | extend(options, { method: 'insert' }) 206 | ) 207 | }); 208 | } 209 | ``` 210 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /* global describe, before, after, beforeEach, it */ 2 | 3 | var Joi = require('joi') 4 | var chai = require('chai') 5 | var expect = chai.expect 6 | var db = require('./db') 7 | var bookshelf = require('bookshelf')(db) 8 | var ModelBase = require('../lib/index')(bookshelf) 9 | 10 | describe('modelBase', function () { 11 | var specimen 12 | var SpecimenClass 13 | 14 | before(function () { 15 | return db.migrate.latest() 16 | }) 17 | 18 | beforeEach(function () { 19 | SpecimenClass = ModelBase.extend({ 20 | tableName: 'test_table', 21 | validate: { 22 | first_name: Joi.string().valid('hello', 'goodbye', 'yo').required(), 23 | last_name: Joi.string().allow(null) 24 | } 25 | }) 26 | 27 | specimen = new SpecimenClass({ 28 | first_name: 'hello' 29 | }) 30 | 31 | return specimen.save() 32 | }) 33 | 34 | after(function () { 35 | return db.destroy() 36 | }) 37 | 38 | describe('initialize', function () { 39 | var origModelBase 40 | 41 | before(function () { 42 | origModelBase = bookshelf.Model 43 | }) 44 | 45 | after(function () { 46 | bookshelf.Model = origModelBase 47 | }) 48 | 49 | it('should error if not passed bookshelf object', function () { 50 | expect(function () { 51 | require('../lib/index')() 52 | }).to.throw(/Must pass an initialized bookshelf instance/) 53 | }) 54 | it('should be separately applyable', function () { 55 | var Model = require('../lib/index')(bookshelf) 56 | expect(Model.findOne).to.be.a('function') 57 | expect(bookshelf.Model.findOne).to.be.an('undefined') 58 | }) 59 | it('should be usable as a bookshelf plugin', function () { 60 | expect(bookshelf.Model.findOne).to.be.an('undefined') 61 | bookshelf.plugin(function () { 62 | require('../lib/index').pluggable.apply(null, arguments) 63 | }) 64 | expect(bookshelf.Model.findOne).to.be.a('function') 65 | }) 66 | }) 67 | 68 | describe('validateSave', function () { 69 | it('should allow extended Joi object', function () { 70 | SpecimenClass = ModelBase.extend({ 71 | tableName: 'test_table', 72 | validate: Joi.object().keys({ 73 | first_name: Joi.string().valid('hello', 'goodbye'), 74 | last_name: Joi.string().default('world') 75 | }) 76 | }) 77 | 78 | specimen = new SpecimenClass({ 79 | first_name: 'hello' 80 | }) 81 | 82 | return specimen.save() 83 | .then(function (model) { 84 | expect(model).to.not.be.an('undefined') 85 | expect(model.get('last_name')).to.be.equal('world') 86 | }) 87 | }) 88 | 89 | it('should validate own attributes', function () { 90 | return expect(specimen.validateSave()).to.contain({ 91 | first_name: 'hello' 92 | }) 93 | }) 94 | 95 | it('should error on invalid attributes', function () { 96 | var error 97 | 98 | specimen.set('first_name', 1) 99 | try { 100 | specimen.validateSave() 101 | } catch (err) { 102 | error = err 103 | } 104 | 105 | expect(error.tableName).to.equal('test_table') 106 | }) 107 | 108 | it('should work with updates method specified', function () { 109 | return SpecimenClass 110 | .where({ first_name: 'hello' }) 111 | .save({ last_name: 'world' }, { patch: true, method: 'update', require: false }) 112 | .then(function (model) { 113 | return expect(model.get('last_name')).to.equal('world') 114 | }) 115 | }) 116 | 117 | it('should work with model id specified', function () { 118 | return SpecimenClass.forge({ id: 1 }) 119 | .save({ last_name: 'world' }, { patch: true, require: false }) 120 | .then(function (model) { 121 | return expect(model.get('last_name')).to.equal('world') 122 | }) 123 | }) 124 | 125 | it('should not validate when Model.validate is not present', function () { 126 | var Model = ModelBase.extend({ tableName: 'test_table' }) 127 | return Model.forge({ id: 1 }) 128 | .save('first_name', 'notYoName') 129 | .then(function (model) { 130 | return expect(model.get('first_name')).to.equal('notYoName') 131 | }) 132 | }) 133 | }) 134 | 135 | describe('constructor', function () { 136 | it('should itself be extensible', function () { 137 | return expect(ModelBase.extend({ tablefirst_name: 'test' })) 138 | .to.itself.respondTo('extend') 139 | }) 140 | }) 141 | 142 | describe('findAll', function () { 143 | it('should return a collection', function () { 144 | return SpecimenClass.findAll() 145 | .then(function (collection) { 146 | return expect(collection).to.be.instanceof(bookshelf.Collection) 147 | }) 148 | }) 149 | }) 150 | 151 | describe('findById', function () { 152 | it('should find a model by it\'s id', function () { 153 | var created 154 | return SpecimenClass.create({ first_name: 'yo' }) 155 | .then(function (model) { 156 | created = model 157 | return SpecimenClass.findById(model.id) 158 | }) 159 | .then(function (model) { 160 | expect(model.id).to.deep.equal(created.id) 161 | }) 162 | }) 163 | }) 164 | 165 | describe('findOne', function () { 166 | it('should return a model', function () { 167 | return SpecimenClass.findOne() 168 | .then(function (model) { 169 | expect(model).to.be.instanceof(SpecimenClass) 170 | }) 171 | }) 172 | }) 173 | 174 | describe('create', function () { 175 | it('should return a model', function () { 176 | return SpecimenClass.create({ 177 | first_name: 'hello' 178 | }) 179 | .then(function (model) { 180 | expect(model.id).to.not.eql(specimen.id) 181 | }) 182 | }) 183 | }) 184 | 185 | describe('update', function () { 186 | it('should return a model', function () { 187 | expect(specimen.get('first_name')).to.not.eql('goodbye') 188 | return SpecimenClass.update({ 189 | first_name: 'goodbye' 190 | }, { 191 | id: specimen.get('id') 192 | }) 193 | .then(function (model) { 194 | expect(model.get('id')).to.eql(specimen.get('id')) 195 | expect(model.get('first_name')).to.eql('goodbye') 196 | }) 197 | }) 198 | 199 | it('should return if require:false and not found', function () { 200 | return SpecimenClass.update({ 201 | first_name: 'goodbye' 202 | }, { 203 | id: -1, 204 | require: false 205 | }) 206 | .then(function (model) { 207 | expect(model).to.eql(undefined) 208 | }) 209 | }) 210 | }) 211 | 212 | describe('destroy', function () { 213 | it('should destroy the model', function () { 214 | return SpecimenClass.forge({ first_name: 'hello' }) 215 | .save() 216 | .bind({}) 217 | .then(function (model) { 218 | this.modelId = model.id 219 | return SpecimenClass.destroy({ id: this.modelId }) 220 | }) 221 | .then(function (model) { 222 | return SpecimenClass.findOne({ id: this.modelId }) 223 | }) 224 | .catch(function (err) { 225 | expect(err.message).to.eql('EmptyResponse') 226 | }) 227 | }) 228 | }) 229 | 230 | describe('findOrCreate', function () { 231 | it('should find an existing model', function () { 232 | return SpecimenClass.findOrCreate({ id: specimen.id }) 233 | .then(function (model) { 234 | expect(model.id).to.eql(specimen.id) 235 | expect(model.get('first_name')).to.equal('hello') 236 | }) 237 | }) 238 | 239 | it('should find with options', function () { 240 | return SpecimenClass.findOrCreate({ id: specimen.id }, { columns: 'id' }) 241 | .then(function (model) { 242 | expect(model.id).to.eql(specimen.id) 243 | expect(model.get('first_name')).to.equal(undefined) 244 | }) 245 | }) 246 | 247 | it('should not apply defaults when model found', function () { 248 | return SpecimenClass.findOrCreate({ id: specimen.id }, { defaults: { last_name: 'world' } }) 249 | .then(function (model) { 250 | expect(model.id).to.eql(specimen.id) 251 | expect(model.get('first_name')).to.equal('hello') 252 | expect(model.get('last_name')).to.be.a('null') 253 | }) 254 | }) 255 | 256 | it('should create when model not found', function () { 257 | return SpecimenClass.findOrCreate({ 258 | first_name: 'hello', 259 | last_name: '' + new Date() 260 | }) 261 | .then(function (model) { 262 | expect(model.id).to.not.eql(specimen.id) 263 | }) 264 | }) 265 | 266 | it('should apply defaults if creating', function () { 267 | var date = '' + new Date() 268 | 269 | return SpecimenClass.findOrCreate({ 270 | last_name: date 271 | }, { 272 | defaults: { first_name: 'hello' } 273 | }) 274 | .then(function (model) { 275 | expect(model.id).to.not.eql(specimen.id) 276 | expect(model.get('first_name')).to.equal('hello') 277 | expect(model.get('last_name')).to.equal(date) 278 | }) 279 | }) 280 | 281 | it('should work with defaults and options', function () { 282 | return SpecimenClass.findOrCreate({ 283 | id: specimen.id 284 | }, { 285 | defaults: { last_name: 'hello' }, 286 | columns: ['id', 'last_name'] 287 | }) 288 | .then(function (model) { 289 | expect(model.get('id')).to.equal(specimen.id) 290 | expect(model.get('first_name')).to.be.an('undefined') 291 | expect(model.get('last_name')).to.be.a('null') 292 | }) 293 | }) 294 | }) 295 | 296 | describe('upsert', function () { 297 | it('should update if existing model found', function () { 298 | return SpecimenClass.create({ 299 | first_name: 'hello', 300 | last_name: 'upsert' 301 | }) 302 | .bind({}) 303 | .then(function (model) { 304 | this.createdModelId = model.id 305 | return SpecimenClass.upsert({ 306 | last_name: 'upsert' 307 | }, { 308 | last_name: 'success' 309 | }) 310 | }) 311 | .then(function (model) { 312 | expect(model.get('first_name')).to.equal('hello') 313 | expect(model.get('last_name')).to.equal('success') 314 | expect(model.id).to.equal(this.createdModelId) 315 | }) 316 | }) 317 | 318 | it('should create if existing model not found', function () { 319 | return SpecimenClass.upsert({ 320 | first_name: 'goodbye', 321 | last_name: 'update' 322 | }, { 323 | last_name: 'updated' 324 | }) 325 | .then(function (model) { 326 | expect(model.get('first_name')).to.equal('goodbye') 327 | expect(model.get('last_name')).to.equal('updated') 328 | }) 329 | }) 330 | 331 | it('should create even with application assigned id', function () { 332 | return SpecimenClass.upsert({ 333 | id: 0, 334 | first_name: 'goodbye', 335 | last_name: 'update' 336 | }, { 337 | last_name: 'updated' 338 | }) 339 | .then(function (model) { 340 | expect(model.id).to.equal(0) 341 | expect(model.get('first_name')).to.equal('goodbye') 342 | expect(model.get('last_name')).to.equal('updated') 343 | }) 344 | }) 345 | }) 346 | }) 347 | --------------------------------------------------------------------------------