├── test ├── unit │ ├── model.js │ └── collection.js ├── base │ ├── tests │ │ ├── eager.js │ │ ├── events.js │ │ ├── model.js │ │ ├── relation.js │ │ └── collection.js │ ├── README.md │ └── index.js ├── integration │ ├── helpers │ │ ├── config.js │ │ ├── migration.js │ │ ├── logger.js │ │ ├── objects.js │ │ └── inserts.js │ ├── plugins.js │ ├── collection.js │ ├── output │ │ └── Collection.js │ ├── relation.js │ └── relations.js ├── index.js └── integration.js ├── CNAME ├── .npmignore ├── .gitignore ├── gruntfile.js ├── docs ├── images │ └── bookshelf.png ├── assets │ └── ga.js ├── dialects │ ├── base │ │ ├── events.html │ │ ├── sync.html │ │ └── relation.html │ └── sql │ │ ├── helpers.html │ │ └── sync.html ├── plugins │ └── exec.html └── bookshelf.html ├── .jshintrc ├── .travis.yml ├── CONTRIBUTING.md ├── dialects ├── base │ ├── events.js │ ├── sync.js │ ├── relation.js │ ├── eager.js │ ├── collection.js │ └── model.js └── sql │ ├── helpers.js │ ├── sync.js │ ├── eager.js │ ├── collection.js │ ├── model.js │ └── relation.js ├── LICENSE ├── README.md ├── package.json ├── plugins └── exec.js └── bookshelf.js /test/unit/model.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | bookshelfjs.org -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | docs/ 2 | index.html -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | raw 2 | *.sw? 3 | .DS_Store 4 | node_modules -------------------------------------------------------------------------------- /test/base/tests/eager.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | 4 | 5 | }; -------------------------------------------------------------------------------- /test/base/tests/events.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | 4 | 5 | }; -------------------------------------------------------------------------------- /test/base/tests/model.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | 4 | 5 | }; -------------------------------------------------------------------------------- /test/base/tests/relation.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function() { 3 | 4 | 5 | }; -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | grunt.loadNpmTasks('grunt-release'); 3 | }; -------------------------------------------------------------------------------- /docs/images/bookshelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rstacruz/bookshelf/master/docs/images/bookshelf.png -------------------------------------------------------------------------------- /test/base/README.md: -------------------------------------------------------------------------------- 1 | tests-base 2 | ========== 3 | 4 | Tests the Base Objects in the Bookshelf DataMapper 5 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "browser": true, 4 | "expr": true, 5 | "eqnull": true, 6 | "latedef": true, 7 | "newcap": false, 8 | "node": true, 9 | "strict": false, 10 | "supernew": true, 11 | "sub": true, 12 | "trailing": true 13 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # .travis.yml 2 | language: node_js 3 | node_js: 4 | - 0.10 5 | - 0.8 6 | - 0.6 7 | 8 | before_script: 9 | - psql -c 'create database bookshelf_test;' -U postgres 10 | - mysql -e 'create database bookshelf_test;' 11 | 12 | notifications: 13 | email: false -------------------------------------------------------------------------------- /test/base/index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | 4 | Collection: require('./tests/collection'), 5 | 6 | Model: require('./tests/model'), 7 | 8 | Events: require('./tests/events'), 9 | 10 | Relation: require('./tests/relation'), 11 | 12 | Eager: require('./tests/eager') 13 | 14 | }; -------------------------------------------------------------------------------- /test/integration/helpers/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | mysql: { 4 | database: 'bookshelf_test', 5 | user: 'root', 6 | encoding: 'utf8' 7 | }, 8 | 9 | postgres: { 10 | database: 'bookshelf_test', 11 | user: 'postgres' 12 | }, 13 | 14 | sqlite3: { 15 | filename: ':memory:' 16 | } 17 | 18 | }; -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to Bookshelf.js 2 | 3 | * Before sending a pull request for a feature or bug fix, be sure to have 4 | [tests](https://github.com/tgriesser/bookshelf/tree/master/test). 5 | 6 | * Use the same coding style as the rest of the 7 | [codebase](https://github.com/tgriesser/bookshelf/blob/master/bookshelf.js). 8 | 9 | * All pull requests should be made to the `master` branch. -------------------------------------------------------------------------------- /docs/assets/ga.js: -------------------------------------------------------------------------------- 1 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 2 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 3 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 4 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 5 | ga('create', 'UA-39763513-1', 'bookshelfjs.org'); 6 | ga('send', 'pageview'); -------------------------------------------------------------------------------- /dialects/base/events.js: -------------------------------------------------------------------------------- 1 | // Events 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var when = require('when'); 10 | var Backbone = require('backbone'); 11 | var triggerThen = require('trigger-then'); 12 | 13 | // Mixin the `triggerThen` function into all relevant Backbone objects, 14 | // so we can have event driven async validations, functions, etc. 15 | triggerThen(Backbone, when); 16 | 17 | exports.Events = Backbone.Events; 18 | 19 | }); 20 | 21 | })( 22 | typeof define === 'function' && define.amd ? define : function(factory) { factory(require, exports); } 23 | ); -------------------------------------------------------------------------------- /test/integration/plugins.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | module.exports = function(Bookshelf) { 4 | 5 | describe('Plugins', function() { 6 | 7 | describe('exec', function() { 8 | 9 | it('adds `then` and `exec` to all sync methods', function() { 10 | 11 | Bookshelf.plugin(require(process.cwd() + '/plugins/exec')); 12 | 13 | var model = new Bookshelf.Model(); 14 | var collection = new Bookshelf.Collection(); 15 | 16 | _.each(['load', 'fetch', 'save', 'destroy'], function(method) { 17 | var fn = model[method](); 18 | if (!_.isFunction(fn.then) || !_.isFunction(fn.exec)) { 19 | throw new Error('then and exec are not both defined'); 20 | } 21 | }); 22 | 23 | _.each(['load', 'fetch'], function(method) { 24 | var fn = collection[method](); 25 | if (!_.isFunction(fn.then) || !_.isFunction(fn.exec)) { 26 | throw new Error('then and exec are not both defined'); 27 | } 28 | }); 29 | 30 | }); 31 | 32 | }); 33 | 34 | }); 35 | 36 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Tim Griesser 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ._. ._. 3 | | | ______ _ _ _ __ | | 4 | | | | ___ \ | | | | | |/ _| | | 5 | | | | |_/ / ___ ___ | | _____| |__ ___| | |_ | | 6 | | | | ___ \/ _ \ / _ \| |/ / __| '_ \ / _ \ | _| | | 7 | | | | |_/ / (_) | (_) | <\__ \ | | | __/ | | | | 8 | | | \____/ \___/ \___/|_|\_\___/_| |_|\___|_|_| | | 9 | |----------------------------------------------------------| 10 | \--------\ /----------------------------------\ /--------/ 11 | \/ \/ 12 | ``` 13 | 14 | Bookshelf is a node.js ORM with support for postgreSQL, mySQL, and SQLite3. 15 | It is built atop the Knex Query Builder, 16 | and is strongly influenced by the Model and Collection foundations of Backbone.js. 17 | It features transaction support, one-to-one, one-to-many, and many-to-many relations. 18 | 19 | For Docs, License, Tests, FAQ, and other information, see: http://bookshelfjs.org. 20 | 21 | To suggest a feature, report a bug, or general discussion: http://github.com/tgriesser/bookshelf/issues/ -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | require('when/monitor/console'); 3 | 4 | var Bookshelf = require('../bookshelf'); 5 | var base = require('./base'); 6 | var mocha = require('mocha'); 7 | 8 | require("mocha-as-promised")(mocha); 9 | 10 | global.sinon = require("sinon"); 11 | 12 | var chai = global.chai = require("chai"); 13 | 14 | chai.use(require("chai-as-promised")); 15 | chai.use(require("sinon-chai")); 16 | chai.should(); 17 | 18 | global.whenResolve = require('when').resolve; 19 | global.expect = chai.expect; 20 | global.AssertionError = chai.AssertionError; 21 | global.Assertion = chai.Assertion; 22 | global.assert = chai.assert; 23 | 24 | // Unit test all of the abstract base interfaces 25 | describe('Unit Tests', function () { 26 | 27 | base.Collection(); 28 | base.Model(); 29 | base.Events(); 30 | base.Relation(); 31 | base.Eager(); 32 | 33 | }); 34 | 35 | describe('Integration Tests', function () { 36 | 37 | var helper = require('./integration/helpers/logger'); 38 | 39 | before(function() { 40 | helper.setLib(this); 41 | }); 42 | 43 | require('./integration')(Bookshelf); 44 | 45 | after(function() { 46 | helper.writeResult(); 47 | }); 48 | 49 | }); 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookshelf", 3 | "version": "0.5.7", 4 | "description": "A lightweight ORM for PostgreSQL, MySQL, and SQLite3, influenced by Backbone.js", 5 | "main": "bookshelf.js", 6 | "scripts": { 7 | "test": "mocha -R spec test/index.js", 8 | "doc": "groc -o docs --verbose dialects/base/collection.js dialects/**/*.js plugins/*.js bookshelf.js" 9 | }, 10 | "homepage": "http://bookshelfjs.org", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/tgriesser/bookshelf.git" 14 | }, 15 | "keywords": [ 16 | "orm", 17 | "mysql", 18 | "postgresql", 19 | "sqlite", 20 | "datamapper", 21 | "active record" 22 | ], 23 | "dependencies": { 24 | "backbone": "~1.0.0", 25 | "inflection": "~1.2.x", 26 | "when": "~2.4.0", 27 | "trigger-then": "~0.1.1", 28 | "underscore": "~1.5.1", 29 | "knex": "~0.4.6" 30 | }, 31 | "devDependencies": { 32 | "mocha": "~1.13.0", 33 | "mysql": "~2.0.0-alpha7", 34 | "pg": "~2.6.2", 35 | "sqlite3": "~2.1.7", 36 | "objectdump": "~0.3.0", 37 | "underscore.string": "~2.3.1", 38 | "grunt": "~0.4.1", 39 | "grunt-release": "~0.5.1", 40 | "mocha-as-promised": "~1.4.0", 41 | "chai-as-promised": "~3.3.1", 42 | "chai": "~1.8.0", 43 | "sinon-chai": "~2.4.0", 44 | "sinon": "~1.7.3" 45 | }, 46 | "author": { 47 | "name": "Tim Griesser", 48 | "web": "https://github.com/tgriesser" 49 | }, 50 | "license": "MIT", 51 | "readmeFilename": "README.md" 52 | } 53 | -------------------------------------------------------------------------------- /dialects/base/sync.js: -------------------------------------------------------------------------------- 1 | // Base Sync 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | // An example "sync" object which is extended 8 | // by dialect-specific sync implementations, 9 | // making Bookshelf effectively a data store 10 | // agnostic "Data Mapper". 11 | define(function(require, exports) { 12 | 13 | var when = require('when'); 14 | var Backbone = require('backbone'); 15 | 16 | // Used as the base of the prototype chain, 17 | // a convenient object for any `instanceof` 18 | // checks you may need. 19 | var BaseSync = function() {}; 20 | 21 | BaseSync.prototype = { 22 | 23 | // May be used for any setup for the class. 24 | initialize: function() {}, 25 | 26 | // Return a single model object. 27 | first: function() { 28 | return when.resolve({}); 29 | }, 30 | 31 | // Select one or more models, returning an array 32 | // of data objects. 33 | select: function() { 34 | return when.resolve([]); 35 | }, 36 | 37 | // Insert a single row, returning an object 38 | // (typically containing an "insert id"). 39 | insert: function() { 40 | return when.resolve({}); 41 | }, 42 | 43 | // Update an object in the data store. 44 | update: function() { 45 | return when.resolve({}); 46 | }, 47 | 48 | // Delete a record from the data store. 49 | del: function() { 50 | return when.resolve({}); 51 | } 52 | 53 | }; 54 | 55 | BaseSync.extend = Backbone.Model.extend; 56 | 57 | exports.BaseSync = BaseSync; 58 | 59 | }); 60 | 61 | })( 62 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 63 | ); -------------------------------------------------------------------------------- /dialects/base/relation.js: -------------------------------------------------------------------------------- 1 | // Base Relation 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var Backbone = require('backbone'); 11 | 12 | var CollectionBase = require('./collection').CollectionBase; 13 | 14 | // Used internally, the `Relation` helps in simplifying the relationship building, 15 | // centralizing all logic dealing with type & option handling. 16 | var RelationBase = function(type, Target, options) { 17 | this.type = type; 18 | if (this.target = Target) { 19 | this.targetTableName = _.result(Target.prototype, 'tableName'); 20 | this.targetIdAttribute = _.result(Target.prototype, 'idAttribute'); 21 | } 22 | _.extend(this, options); 23 | }; 24 | 25 | RelationBase.prototype = { 26 | 27 | // Creates a new relation instance, used by the `Eager` relation in 28 | // dealing with `morphTo` cases, where the same relation is targeting multiple models. 29 | instance: function(type, Target, options) { 30 | return new this.constructor(type, Target, options); 31 | }, 32 | 33 | // Creates a new model, used internally in the eager fetch helper methods. 34 | createModel: function(data) { 35 | if (this.target.prototype instanceof CollectionBase) { 36 | return new this.target.prototype.model(data, {parse: true})._reset(); 37 | } 38 | return new this.target(data, {parse: true})._reset(); 39 | }, 40 | 41 | // Eager pair the models. 42 | eagerPair: function() {} 43 | 44 | }; 45 | 46 | RelationBase.extend = Backbone.Model.extend; 47 | 48 | exports.RelationBase = RelationBase; 49 | 50 | }); 51 | 52 | })( 53 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 54 | ); -------------------------------------------------------------------------------- /plugins/exec.js: -------------------------------------------------------------------------------- 1 | // Exec plugin 2 | // --------------- 3 | (function(define) { "use strict"; 4 | 5 | // The `exec` plugin is used to optionally add 6 | // support node-style callbacks, delegating to the promise 7 | // method under the hood: 8 | 9 | // `Bookshelf.plugin(require('bookshelf/plugins/exec'))` 10 | define(function(require, exports, module) { 11 | 12 | var _ = require('underscore'); 13 | 14 | // Accept the instance of `Bookshelf` we'd like to add `exec` support to. 15 | module.exports = function(Bookshelf) { 16 | 17 | // A method which is passed the `target` object and `method` we're 18 | // looking to extend with the `exec` interface. 19 | var wrapExec = function(target, method) { 20 | var targetMethod = target[method]; 21 | target[method] = function() { 22 | var result, args = arguments; 23 | var ctx = this; 24 | return { 25 | 26 | // The then method is essentially the same as it was before, 27 | // just is not automatically called. 28 | then: function(onFulfilled, onRejected) { 29 | result || (result = targetMethod.apply(ctx, args)); 30 | return result.then(onFulfilled, onRejected); 31 | }, 32 | 33 | // A facade for the `then` method, throwing any uncaught errors 34 | // rather than swallowing them. 35 | exec: function(callback) { 36 | result || (result = targetMethod.apply(ctx, args)); 37 | return result.then(function(resp) { 38 | callback(null, resp); 39 | }, function(err) { 40 | callback(err, null); 41 | }).then(null, function(err) { 42 | setTimeout(function() { throw err; }, 0); 43 | }); 44 | } 45 | }; 46 | }; 47 | }; 48 | 49 | // Wrap the appropriate methods on each object prototype, exposing the new API. 50 | _.each(['load', 'fetch', 'save', 'destroy'], function(method) { 51 | wrapExec(Bookshelf.Model.prototype, method); 52 | }); 53 | _.each(['load', 'fetch'], function(method) { 54 | wrapExec(Bookshelf.Collection.prototype, method); 55 | }); 56 | }; 57 | 58 | }); 59 | 60 | })( 61 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } 62 | ); 63 | 64 | -------------------------------------------------------------------------------- /dialects/sql/helpers.js: -------------------------------------------------------------------------------- 1 | // Helpers 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | 11 | exports.Helpers = { 12 | 13 | // Sets the constraints necessary during a `model.save` call. 14 | saveConstraints: function(model, relatedData) { 15 | var data = {}; 16 | if (relatedData && relatedData.type && relatedData.type !== 'belongsToMany') { 17 | data[relatedData.key('foreignKey')] = relatedData.parentFk; 18 | if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue'); 19 | } 20 | return model.set(data); 21 | }, 22 | 23 | // Finds the specific `morphTo` table we should be working with, or throws 24 | // an error if none is matched. 25 | morphCandidate: function(candidates, foreignTable) { 26 | var Target = _.find(candidates, function(Candidate) { 27 | return (_.result(Candidate.prototype, 'tableName') === foreignTable); 28 | }); 29 | if (!Target) { 30 | throw new Error('The target polymorphic model was not found'); 31 | } 32 | return Target; 33 | }, 34 | 35 | // If there are no arguments, return the current object's 36 | // query builder (or create and return a new one). If there are arguments, 37 | // call the query builder with the first argument, applying the rest. 38 | // If the first argument is an object, assume the keys are query builder 39 | // methods, and the values are the arguments for the query. 40 | query: function(obj, args) { 41 | obj._knex = obj._knex || obj._builder(_.result(obj, 'tableName')); 42 | if (args.length === 0) return obj._knex; 43 | var method = args[0]; 44 | if (_.isFunction(method)) { 45 | method.call(obj._knex, obj._knex); 46 | } else if (_.isObject(method)) { 47 | for (var key in method) { 48 | var target = _.isArray(method[key]) ? method[key] : [method[key]]; 49 | obj._knex[key].apply(obj._knex, target); 50 | } 51 | } else { 52 | obj._knex[method].apply(obj._knex, args.slice(1)); 53 | } 54 | return obj; 55 | } 56 | 57 | }; 58 | 59 | }); 60 | 61 | })( 62 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 63 | ); -------------------------------------------------------------------------------- /test/integration.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | module.exports = function(Bookshelf) { 4 | 5 | var Knex = require('knex'); 6 | var config = require(process.env.BOOKSHELF_TEST || './integration/helpers/config'); 7 | var nodefn = require('when/node/function'); 8 | 9 | var MySQL = Bookshelf.initialize({ 10 | client: 'mysql', 11 | connection: config.mysql, 12 | pool: { 13 | afterCreate: function(connection, callback) { 14 | return nodefn.call(connection.query.bind(connection), "SET sql_mode='TRADITIONAL';", []).then(function() { 15 | callback(null, connection); 16 | }); 17 | } 18 | } 19 | }); 20 | 21 | var PostgreSQL = Bookshelf.initialize({ 22 | client: 'postgres', 23 | connection: config.postgres, 24 | }); 25 | 26 | var SQLite3 = Bookshelf.initialize({ 27 | client: 'sqlite3', 28 | connection: config.sqlite3, 29 | }); 30 | 31 | var knexSqlite3 = Knex.initialize({ 32 | client: 'sqlite3', 33 | connection: config.sqlite3 34 | }); 35 | 36 | it('should allow creating a Bookshelf client from a Knex instance', function() { 37 | var bookshelf = Bookshelf.initialize(knexSqlite3); 38 | expect(bookshelf.knex).to.equal(knexSqlite3); 39 | }); 40 | 41 | it('should allow creating a new Bookshelf instance with "new"', function() { 42 | var bookshelf = new Bookshelf(knexSqlite3); 43 | expect(bookshelf.knex).to.equal(knexSqlite3); 44 | }); 45 | 46 | _.each([MySQL, PostgreSQL, SQLite3], function(bookshelf) { 47 | 48 | describe('Dialect: ' + bookshelf.knex.client.dialect, function() { 49 | 50 | before(function() { 51 | return require('./integration/helpers/migration')(bookshelf).then(function() { 52 | return require('./integration/helpers/inserts')(bookshelf); 53 | }); 54 | }); 55 | 56 | this.dialect = bookshelf.knex.client.dialect; 57 | 58 | // Only testing this against mysql for now, just so the toString is reliable... 59 | if (bookshelf.knex.client.dialect === 'mysql') { 60 | require('./integration/relation')(bookshelf); 61 | } 62 | 63 | require('./integration/model')(bookshelf); 64 | require('./integration/collection')(bookshelf); 65 | require('./integration/relations')(bookshelf); 66 | require('./integration/plugins')(bookshelf); 67 | }); 68 | 69 | }); 70 | 71 | }; 72 | -------------------------------------------------------------------------------- /test/unit/collection.js: -------------------------------------------------------------------------------- 1 | 2 | describe('extend/constructor/initialize', function() { 3 | 4 | var Users = Bookshelf.Collection.extend({ 5 | getData: function() { return 'test'; } 6 | }, { 7 | classMethod: function() { return 'test'; } 8 | }); 9 | 10 | var SubUsers = Users.extend({ 11 | otherMethod: function() { return this.getData(); } 12 | }, { 13 | classMethod2: function() { return 'test2'; } 14 | }); 15 | 16 | it('can be extended', function() { 17 | var users = new Users(); 18 | var subUsers = new SubUsers(); 19 | equal(users.getData(), 'test'); 20 | equal(subUsers.otherMethod(), 'test'); 21 | equal(Users.classMethod(), 'test'); 22 | equal(SubUsers.classMethod(), 'test'); 23 | equal(SubUsers.classMethod2(), 'test2'); 24 | }); 25 | 26 | it('accepts a custom `constructor` property', function() { 27 | var Users = Bookshelf.Collection.extend({ 28 | constructor: function() { 29 | this.item = 'test'; 30 | Bookshelf.Collection.apply(this, arguments); 31 | } 32 | }); 33 | equal(new Users().item, 'test'); 34 | }); 35 | 36 | }); 37 | 38 | describe('forge', function() { 39 | 40 | it('should create a new collection instance', function() { 41 | var User = Bookshelf.Model.extend({ 42 | tableName: 'users' 43 | }); 44 | var Users = Bookshelf.Collection.extend({ 45 | model: User 46 | }); 47 | var users = Users.forge(); 48 | equal(users.tableName(), 'users'); 49 | }); 50 | 51 | }); 52 | 53 | describe('tableName', function() { 54 | 55 | it('returns the `tableName` attribute off the `Collection#model` prototype', function() { 56 | var collection = new (Bookshelf.Collection.extend({ 57 | model: Bookshelf.Model.extend({ 58 | tableName: 'test' 59 | }) 60 | }))(); 61 | equal(collection.tableName(), 'test'); 62 | }); 63 | }); 64 | 65 | describe('idAttribute', function() { 66 | 67 | it('returns the `idAttribute` attribute from the `Collection#model` prototype', function() { 68 | var collection = new (Bookshelf.Collection.extend({ 69 | model: Bookshelf.Model.extend({ 70 | idAttribute: 'test' 71 | }) 72 | }))(); 73 | equal(collection.idAttribute(), 'test'); 74 | }); 75 | }); 76 | 77 | 78 | -------------------------------------------------------------------------------- /docs/dialects/base/events.html: -------------------------------------------------------------------------------- 1 | dialects/base/events
dialects/base/events.js

Events

(function(define) { 2 | 3 | "use strict"; 4 | 5 | define(function(require, exports) { 6 | 7 | var when = require('when'); 8 | var Backbone = require('backbone'); 9 | var triggerThen = require('trigger-then');

Mixin the triggerThen function into all relevant Backbone objects, 10 | so we can have event driven async validations, functions, etc.

triggerThen(Backbone, when); 11 | 12 | exports.Events = Backbone.Events; 13 | 14 | }); 15 | 16 | })( 17 | typeof define === 'function' && define.amd ? define : function(factory) { factory(require, exports); } 18 | );
-------------------------------------------------------------------------------- /test/base/tests/collection.js: -------------------------------------------------------------------------------- 1 | var when = require('when'); 2 | var assert = require('assert'); 3 | var equal = assert.equal; 4 | var _ = require('underscore'); 5 | 6 | module.exports = function() { 7 | 8 | // This module is included into the `bookshelf` repository, 9 | // and run from the root of the directory. 10 | var path = require('path'); 11 | var basePath = process.cwd(); 12 | 13 | var CollectionBase = require(path.resolve(basePath + '/dialects/base/collection')).CollectionBase; 14 | var ModelBase = require(path.resolve(basePath + '/dialects/base/model')).ModelBase; 15 | 16 | describe('Collection', function() { 17 | 18 | var collection; 19 | var Collection = CollectionBase.extend({ 20 | model: ModelBase.extend({ 21 | tableName: 'test_table', 22 | idAttribute: 'some_id', 23 | invokedMethod: function() { 24 | return when(this.id); 25 | } 26 | }) 27 | }); 28 | 29 | beforeEach(function() { 30 | collection = new Collection([{some_id: 1, name: 'Test'}, {id: 2, name: 'No Id'}]); 31 | }); 32 | 33 | it('should have a tableName method, returning the tableName of the model', function () { 34 | 35 | equal(collection.tableName(), 'test_table'); 36 | 37 | }); 38 | 39 | it('should have an idAttribute method, returning the idAttribute of the model', function() { 40 | 41 | equal(collection.idAttribute(), 'some_id'); 42 | 43 | }); 44 | 45 | it('should initialize the items passed to the constructor', function() { 46 | 47 | equal(collection.length, 2); 48 | 49 | equal(collection.at(0).id, 1); 50 | 51 | equal(collection.at(1).id, undefined); 52 | 53 | }); 54 | 55 | it('should use the `set` method to update the collections, similar to Backbone', function() { 56 | 57 | collection.set([{some_id: 2, name: 'Test'}, {some_id: 3, name: 'Item'}]); 58 | 59 | equal(collection.length, 2); 60 | 61 | collection.set([{some_id: 2, name: 'Item'}], {remove: false}); 62 | 63 | equal(collection.length, 2); 64 | 65 | }); 66 | 67 | it('should use the `reset` method, to reset the collection', function() { 68 | 69 | collection.reset([]); 70 | 71 | equal(collection.length, 0); 72 | 73 | }); 74 | 75 | it('should use _prepareModel to prep model instances', function() { 76 | 77 | var model = new ModelBase({id: 1}); 78 | 79 | expect(model).to.equal(collection._prepareModel(model)); 80 | 81 | var newModel = collection._prepareModel({some_id: 1}); 82 | 83 | assert.ok((newModel instanceof collection.model)); 84 | 85 | }); 86 | 87 | it('contains a mapThen method, which calls map on the models, and returns a when.all promise', function() { 88 | 89 | var spyIterator = sinon.spy(function(model) { 90 | return model.id; 91 | }); 92 | 93 | return collection.mapThen(spyIterator).then(function(resp) { 94 | spyIterator.should.have.been.calledTwice; 95 | expect(_.compact(resp)).to.eql([1]); 96 | }); 97 | 98 | }); 99 | 100 | it('contains an invokeThen method, which does an invoke on the models, and returns a when.all promise', function() { 101 | 102 | var spyIterator = sinon.spy(function(model) { 103 | return model.id; 104 | }); 105 | 106 | return collection.invokeThen('invokedMethod').then(function(resp) { 107 | expect(_.compact(resp)).to.eql([1]); 108 | }); 109 | 110 | }); 111 | 112 | }); 113 | 114 | 115 | }; -------------------------------------------------------------------------------- /dialects/sql/sync.js: -------------------------------------------------------------------------------- 1 | // Sync 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var when = require('when'); 11 | 12 | // Sync is the dispatcher for any database queries, 13 | // taking the "syncing" `model` or `collection` being queried, along with 14 | // a hash of options that are used in the various query methods. 15 | // If the `transacting` option is set, the query is assumed to be 16 | // part of a transaction, and this information is passed along to `Knex`. 17 | var Sync = function(syncing, options) { 18 | options || (options = {}); 19 | this.query = syncing.query(); 20 | this.syncing = syncing.resetQuery(); 21 | this.options = options; 22 | this._init(syncing, options); 23 | this.initialize(syncing, options); 24 | }; 25 | 26 | _.extend(Sync.prototype, { 27 | 28 | initialize: function() {}, 29 | 30 | // Select the first item from the database - only used by models. 31 | first: function() { 32 | var syncing = this.syncing; 33 | this.query.where(syncing.format(_.extend(Object.create(null), syncing.attributes))).limit(1); 34 | return this.select(); 35 | }, 36 | 37 | // Runs a `select` query on the database, adding any necessary relational 38 | // constraints, resetting the query when complete. If there are results and 39 | // eager loaded relations, those are fetched and returned on the model before 40 | // the promise is resolved. Any `success` handler passed in the 41 | // options will be called - used by both models & collections. 42 | select: function() { 43 | var columns, sync = this, syncing = this.syncing, 44 | options = this.options, relatedData = syncing.relatedData; 45 | 46 | // Inject all appropriate select costraints dealing with the relation 47 | // into the `knex` query builder for the current instance. 48 | if (relatedData) { 49 | relatedData.selectConstraints(this.query, options); 50 | } else { 51 | columns = options.columns; 52 | if (!_.isArray(columns)) columns = columns ? [columns] : [_.result(syncing, 'tableName') + '.*']; 53 | } 54 | 55 | // Set the query builder on the options, in-case we need to 56 | // access in the `fetching` event handlers. 57 | options.query = this.query; 58 | 59 | // Trigger a `fetching` event on the model, and then select the appropriate columns. 60 | return syncing.triggerThen('fetching', syncing, columns, options).then(function() { 61 | return sync.query.select(columns); 62 | }); 63 | }, 64 | 65 | // Issues an `insert` command on the query - only used by models. 66 | insert: function() { 67 | var syncing = this.syncing; 68 | return this.query 69 | .insert(syncing.format(_.extend(Object.create(null), syncing.attributes)), syncing.idAttribute); 70 | }, 71 | 72 | // Issues an `update` command on the query - only used by models. 73 | update: function(attrs) { 74 | var syncing = this.syncing, query = this.query; 75 | if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); 76 | if (query.wheres.length === 0) { 77 | return when.reject(new Error('A model cannot be updated without a "where" clause or an idAttribute.')); 78 | } 79 | return query.update(syncing.format(_.extend(Object.create(null), attrs))); 80 | }, 81 | 82 | // Issues a `delete` command on the query. 83 | del: function() { 84 | var query = this.query, syncing = this.syncing; 85 | if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); 86 | if (query.wheres.length === 0) { 87 | return when.reject(new Error('A model cannot be destroyed without a "where" clause or an idAttribute.')); 88 | } 89 | return this.query.del(); 90 | }, 91 | 92 | _init: function(syncing, options) { 93 | if (options.transacting) this.query.transacting(options.transacting); 94 | } 95 | 96 | }); 97 | 98 | exports.Sync = Sync; 99 | 100 | }); 101 | 102 | })( 103 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 104 | ); -------------------------------------------------------------------------------- /dialects/sql/eager.js: -------------------------------------------------------------------------------- 1 | // EagerRelation 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var when = require('when'); 11 | 12 | var Helpers = require('./helpers').Helpers; 13 | var EagerBase = require('../base/eager').EagerBase; 14 | 15 | // An `EagerRelation` object temporarily stores the models from an eager load, 16 | // and handles matching eager loaded objects with their parent(s). The `tempModel` 17 | // is only used to retrieve the value of the relation method, to know the constrains 18 | // for the eager query. 19 | var EagerRelation = exports.EagerRelation = EagerBase.extend({ 20 | 21 | // Handles an eager loaded fetch, passing the name of the item we're fetching for, 22 | // and any options needed for the current fetch. 23 | eagerFetch: function(relationName, handled, options) { 24 | var relatedData = handled.relatedData; 25 | 26 | if (relatedData.type === 'morphTo') return this.morphToFetch(relationName, relatedData, options); 27 | 28 | // Call the function, if one exists, to constrain the eager loaded query. 29 | options.beforeFn.call(handled, handled.query()); 30 | 31 | return handled 32 | .sync(_.extend({}, options, {parentResponse: this.parentResponse})) 33 | .select() 34 | .tap(eagerLoadHelper(this, relationName, handled, options)); 35 | }, 36 | 37 | // Special handler for the eager loaded morph-to relations, this handles 38 | // the fact that there are several potential models that we need to be fetching against. 39 | // pairing them up onto a single response for the eager loading. 40 | morphToFetch: function(relationName, relatedData, options) { 41 | var pending = []; 42 | var groups = _.groupBy(this.parent, function(m) { 43 | return m.get(relationName + '_type'); 44 | }); 45 | for (var group in groups) { 46 | var Target = Helpers.morphCandidate(relatedData.candidates, group); 47 | var target = new Target(); 48 | pending.push(target 49 | .query('whereIn', 50 | _.result(target, 'idAttribute'), _.uniq(_.invoke(groups[group], 'get', relationName + '_id')) 51 | ) 52 | .sync(options) 53 | .select() 54 | .tap(eagerLoadHelper(this, relationName, { 55 | relatedData: relatedData.instance('morphTo', Target, {morphName: relationName}) 56 | }, options))); 57 | } 58 | return when.all(pending).then(function(resps) { 59 | return _.flatten(resps); 60 | }); 61 | } 62 | 63 | }); 64 | 65 | // Handles the eager load for both the `morphTo` and regular cases. 66 | var eagerLoadHelper = function(relation, relationName, handled, options) { 67 | return function(resp) { 68 | var relatedModels = relation.pushModels(relationName, handled, resp); 69 | var relatedData = handled.relatedData; 70 | 71 | // If there is a response, fetch additional nested eager relations, if any. 72 | if (resp.length > 0 && options.withRelated) { 73 | var relatedModel = relatedData.createModel(); 74 | 75 | // If this is a `morphTo` relation, we need to do additional processing 76 | // to ensure we don't try to load any relations that don't look to exist. 77 | if (relatedData.type === 'morphTo') { 78 | var withRelated = filterRelated(relatedModel, options); 79 | if (withRelated.length === 0) return; 80 | options = _.extend({}, options, {withRelated: withRelated}); 81 | } 82 | return new EagerRelation(relatedModels, resp, relatedModel).fetch(options).yield(resp); 83 | } 84 | }; 85 | }; 86 | 87 | // Filters the `withRelated` on a `morphTo` relation, to ensure that only valid 88 | // relations are attempted for loading. 89 | var filterRelated = function(relatedModel, options) { 90 | // By this point, all withRelated should be turned into a hash, so it should 91 | // be fairly simple to process by splitting on the dots. 92 | return _.reduce(options.withRelated, function(memo, val) { 93 | for (var key in val) { 94 | var seg = key.split('.')[0]; 95 | if (_.isFunction(relatedModel[seg])) memo.push(val); 96 | } 97 | return memo; 98 | }, []); 99 | }; 100 | 101 | 102 | }); 103 | 104 | })( 105 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 106 | ); -------------------------------------------------------------------------------- /dialects/base/eager.js: -------------------------------------------------------------------------------- 1 | // Eager Base 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | // The EagerBase provides a scaffold for handling with eager relation 8 | // pairing, by queueing the appropriate related method calls with 9 | // a database specific `eagerFetch` method, which then may utilize 10 | // `pushModels` for pairing the models depending on the database need. 11 | define(function(require, exports) { 12 | 13 | var _ = require('underscore'); 14 | var when = require('when'); 15 | var Backbone = require('backbone'); 16 | 17 | var EagerBase = function(parent, parentResponse, target) { 18 | this.parent = parent; 19 | this.target = target; 20 | this.parentResponse = parentResponse; 21 | _.bindAll(this, 'pushModels', 'eagerFetch'); 22 | }; 23 | 24 | EagerBase.prototype = { 25 | 26 | // This helper function is used internally to determine which relations 27 | // are necessary for fetching based on the `model.load` or `withRelated` option. 28 | fetch: function(options) { 29 | var relationName, related, relation; 30 | var target = this.target; 31 | var handled = this.handled = {}; 32 | var withRelated = this.prepWithRelated(options.withRelated); 33 | var subRelated = {}; 34 | 35 | // Internal flag to determine whether to set the ctor(s) on the `Relation` object. 36 | target._isEager = true; 37 | 38 | // Eager load each of the `withRelated` relation item, splitting on '.' 39 | // which indicates a nested eager load. 40 | for (var key in withRelated) { 41 | 42 | related = key.split('.'); 43 | relationName = related[0]; 44 | 45 | // Add additional eager items to an array, to load at the next level in the query. 46 | if (related.length > 1) { 47 | var relatedObj = {}; 48 | subRelated[relationName] || (subRelated[relationName] = []); 49 | relatedObj[related.slice(1).join('.')] = withRelated[key]; 50 | subRelated[relationName].push(relatedObj); 51 | } 52 | 53 | // Only allow one of a certain nested type per-level. 54 | if (handled[relationName]) continue; 55 | 56 | relation = target[relationName](); 57 | 58 | if (!relation) throw new Error(relationName + ' is not defined on the model.'); 59 | 60 | handled[relationName] = relation; 61 | } 62 | 63 | // Delete the internal flag from the model. 64 | delete target._isEager; 65 | 66 | // Fetch all eager loaded models, loading them onto 67 | // an array of pending deferred objects, which will handle 68 | // all necessary pairing with parent objects, etc. 69 | var pendingDeferred = []; 70 | for (relationName in handled) { 71 | pendingDeferred.push(this.eagerFetch(relationName, handled[relationName], _.extend({}, options, { 72 | isEager: true, 73 | withRelated: subRelated[relationName], 74 | beforeFn: withRelated[relationName] || noop 75 | }))); 76 | } 77 | 78 | // Return a deferred handler for all of the nested object sync 79 | // returning the original response when these syncs & pairings are complete. 80 | return when.all(pendingDeferred).yield(this.parentResponse); 81 | }, 82 | 83 | // Prep the `withRelated` object, to normalize into an object where each 84 | // has a function that is called when running the query. 85 | prepWithRelated: function(withRelated) { 86 | if (!_.isArray(withRelated)) withRelated = [withRelated]; 87 | return _.reduce(withRelated, function(memo, item) { 88 | _.isString(item) ? memo[item] = noop : _.extend(memo, item); 89 | return memo; 90 | }, {}); 91 | }, 92 | 93 | // Pushes each of the incoming models onto a new `related` array, 94 | // which is used to correcly pair additional nested relations. 95 | pushModels: function(relationName, handled, resp) { 96 | var models = this.parent; 97 | var relatedData = handled.relatedData; 98 | var related = []; 99 | for (var i = 0, l = resp.length; i < l; i++) { 100 | related.push(relatedData.createModel(resp[i])); 101 | } 102 | return relatedData.eagerPair(relationName, related, models); 103 | } 104 | 105 | }; 106 | 107 | var noop = function() {}; 108 | 109 | EagerBase.extend = Backbone.Model.extend; 110 | 111 | exports.EagerBase = EagerBase; 112 | 113 | }); 114 | 115 | })( 116 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 117 | ); -------------------------------------------------------------------------------- /test/integration/helpers/migration.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | var when = require('when'); 3 | 4 | var drops = [ 5 | 'sites', 'sitesmeta', 'admins', 6 | 'admins_sites', 'authors', 'authors_posts', 7 | 'blogs', 'posts', 'tags', 'posts_tags', 'comments', 8 | 'users', 'roles', 'photos', 'users_roles', 'info', 9 | 'Customer', 'Settings', 'hostnames', 'instances' 10 | ]; 11 | 12 | module.exports = function(Bookshelf) { 13 | 14 | var schema = Bookshelf.knex.schema; 15 | 16 | return when.all(_.map(drops, function(val) { 17 | return schema.dropTableIfExists(val); 18 | })) 19 | .then(function() { 20 | 21 | return when.all([ 22 | schema.createTable('sites', function(table) { 23 | table.increments('id'); 24 | table.string('name'); 25 | }), 26 | 27 | schema.createTable('sitesmeta', function(table) { 28 | table.increments('id'); 29 | table.integer('site_id'); 30 | table.text('description'); 31 | }), 32 | 33 | schema.createTable('info', function(table) { 34 | table.increments('id'); 35 | table.integer('meta_id'); 36 | table.text('other_description'); 37 | }), 38 | 39 | schema.createTable('admins', function(table) { 40 | table.increments('id'); 41 | table.string('username'); 42 | table.string('password'); 43 | table.timestamps(); 44 | }), 45 | 46 | schema.createTable('admins_sites', function(table) { 47 | table.increments('id'); 48 | table.integer('admin_id'); 49 | table.integer('site_id'); 50 | table.string('item').defaultTo('test'); 51 | }), 52 | 53 | schema.createTable('blogs', function(table) { 54 | table.increments('id'); 55 | table.integer('site_id'); 56 | table.string('name'); 57 | }), 58 | 59 | schema.createTable('authors', function(table) { 60 | table.increments('id'); 61 | table.integer('site_id'); 62 | table.string('first_name'); 63 | table.string('last_name'); 64 | }), 65 | 66 | schema.createTable('posts', function(table) { 67 | table.increments('id'); 68 | table.integer('owner_id'); 69 | table.integer('blog_id'); 70 | table.string('name'); 71 | table.text('content'); 72 | }), 73 | 74 | schema.createTable('authors_posts', function(table) { 75 | table.increments('id'); 76 | table.integer('author_id'); 77 | table.integer('post_id'); 78 | }), 79 | 80 | schema.createTable('tags', function(table) { 81 | table.increments('id'); 82 | table.string('name'); 83 | }), 84 | 85 | schema.createTable('posts_tags', function(table) { 86 | table.increments('id'); 87 | table.integer('post_id'); 88 | table.integer('tag_id'); 89 | }), 90 | 91 | schema.createTable('comments', function(table) { 92 | table.increments('id'); 93 | table.integer('post_id'); 94 | table.string('name'); 95 | table.string('email'); 96 | table.text('comment'); 97 | }), 98 | 99 | schema.createTable('users', function(table) { 100 | table.increments('uid'); 101 | table.string('username'); 102 | }), 103 | 104 | schema.createTable('roles', function(table) { 105 | table.increments('rid'); 106 | table.string('name'); 107 | }), 108 | 109 | schema.createTable('users_roles', function(table) { 110 | table.integer('rid'); 111 | table.integer('uid'); 112 | }), 113 | 114 | schema.createTable('photos', function(table) { 115 | table.increments('id'); 116 | table.string('url'); 117 | table.string('caption'); 118 | table.integer('imageable_id'); 119 | table.string('imageable_type'); 120 | }), 121 | 122 | schema.createTable('Customer', function(table) { 123 | table.increments('id'); 124 | table.string('name'); 125 | }), 126 | 127 | schema.createTable('Settings', function(table) { 128 | table.increments('id'); 129 | table.integer('Customer_id'); 130 | table.string('data', 64); 131 | }), 132 | 133 | schema.createTable('hostnames', function(table){ 134 | table.string('hostname'); 135 | table.integer('instance_id'); 136 | table.enu('route', ['annotate','submit']); 137 | }), 138 | 139 | schema.createTable('instances', function(table){ 140 | table.bigIncrements('id'); 141 | table.string('name'); 142 | }) 143 | 144 | ]); 145 | 146 | }); 147 | 148 | }; -------------------------------------------------------------------------------- /bookshelf.js: -------------------------------------------------------------------------------- 1 | // Bookshelf.js 0.5.7 2 | // --------------- 3 | 4 | // (c) 2013 Tim Griesser 5 | // Bookshelf may be freely distributed under the MIT license. 6 | // For all details and documentation: 7 | // http://bookshelfjs.org 8 | (function(define) { 9 | 10 | "use strict"; 11 | 12 | define(function(require, exports, module) { 13 | 14 | // All external libraries needed in this scope. 15 | var _ = require('underscore'); 16 | var Knex = require('knex'); 17 | 18 | // All local dependencies... These are the main objects that 19 | // need to be augmented in the constructor to work properly. 20 | var SqlModel = require('./dialects/sql/model').Model; 21 | var SqlCollection = require('./dialects/sql/collection').Collection; 22 | var SqlRelation = require('./dialects/sql/relation').Relation; 23 | 24 | // Finally, the `Events`, which we've supplemented with a `triggerThen` 25 | // method to allow for asynchronous event handling via promises. We also 26 | // mix this into the prototypes of the main objects in the library. 27 | var Events = require('./dialects/base/events').Events; 28 | 29 | // Constructor for a new `Bookshelf` object, it accepts 30 | // an active `knex` instance and initializes the appropriate 31 | // `Model` and `Collection` constructors for use in the current instance. 32 | var Bookshelf = function(knex) { 33 | 34 | // Allows you to construct the library with either `Bookshelf(opts)` 35 | // or `new Bookshelf(opts)`. 36 | if (!(this instanceof Bookshelf)) { 37 | return new Bookshelf(knex); 38 | } 39 | 40 | // If the knex isn't a `Knex` instance, we'll assume it's 41 | // a compatible config object and pass it through to create a new instance. 42 | if (!knex.client || !(knex.client instanceof Knex.ClientBase)) { 43 | knex = new Knex(knex); 44 | } 45 | 46 | // The `Model` constructor is referenced as a property on the `Bookshelf` instance, 47 | // mixing in the correct `builder` method, as well as the `relation` method, 48 | // passing in the correct `Model` & `Collection` constructors for later reference. 49 | var ModelCtor = this.Model = SqlModel.extend({ 50 | _builder: function(tableName) { 51 | return knex(tableName); 52 | }, 53 | _relation: function(type, Target, options) { 54 | return new Relation(type, Target, options); 55 | } 56 | }); 57 | 58 | // The collection also references the correct `Model`, specified above, for creating 59 | // new `Model` instances in the collection. We also extend with the correct builder / 60 | // `knex` combo. 61 | var CollectionCtor = this.Collection = SqlCollection.extend({ 62 | model: ModelCtor, 63 | _builder: function(tableName) { 64 | return knex(tableName); 65 | } 66 | }); 67 | 68 | // Used internally, the `Relation` helps in simplifying the relationship building, 69 | // centralizing all logic dealing with type & option handling. 70 | var Relation = Bookshelf.Relation = SqlRelation.extend({ 71 | Model: ModelCtor, 72 | Collection: CollectionCtor 73 | }); 74 | 75 | // Grab a reference to the `knex` instance passed (or created) in this constructor, 76 | // for convenience. 77 | this.knex = knex; 78 | }; 79 | 80 | // A `Bookshelf` instance may be used as a top-level pub-sub bus, as it mixes in the 81 | // `Events` object. It also contains the version number, and a `Transaction` method 82 | // referencing the correct version of `knex` passed into the object. 83 | _.extend(Bookshelf.prototype, Events, { 84 | 85 | // Keep in sync with `package.json`. 86 | VERSION: '0.5.7', 87 | 88 | // Helper method to wrap a series of Bookshelf actions in a `knex` transaction block; 89 | transaction: function() { 90 | return this.knex.transaction.apply(this, arguments); 91 | }, 92 | 93 | // Provides a nice, tested, standardized way of adding plugins to a `Bookshelf` instance, 94 | // injecting the current instance into the plugin, which should be a module.exports. 95 | plugin: function(plugin) { 96 | plugin(this); 97 | return this; 98 | } 99 | 100 | }); 101 | 102 | // Alias to `new Bookshelf(opts)`. 103 | Bookshelf.initialize = function(knex) { 104 | return new this(knex); 105 | }; 106 | 107 | // The `forge` function properly instantiates a new Model or Collection 108 | // without needing the `new` operator... to make object creation cleaner 109 | // and more chainable. 110 | SqlModel.forge = SqlCollection.forge = function() { 111 | var inst = Object.create(this.prototype); 112 | var obj = this.apply(inst, arguments); 113 | return (Object(obj) === obj ? obj : inst); 114 | }; 115 | 116 | // Finally, export `Bookshelf` to the world. 117 | module.exports = Bookshelf; 118 | 119 | }); 120 | 121 | })( 122 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } 123 | ); -------------------------------------------------------------------------------- /test/integration/collection.js: -------------------------------------------------------------------------------- 1 | var when = require('when'); 2 | 3 | module.exports = function(Bookshelf) { 4 | 5 | describe('Collection', function() { 6 | 7 | var Backbone = require('backbone'); 8 | 9 | var Models = require('./helpers/objects')(Bookshelf).Models; 10 | var Collections = require('./helpers/objects')(Bookshelf).Collections; 11 | 12 | // Models 13 | var Site = Models.Site; 14 | var SiteMeta = Models.SiteMeta; 15 | var Admin = Models.Admin; 16 | var Author = Models.Author; 17 | var Blog = Models.Blog; 18 | var Post = Models.Post; 19 | var Comment = Models.Comment; 20 | var Tag = Models.Tag; 21 | var User = Models.User; 22 | var Role = Models.Role; 23 | var Photo = Models.Photo; 24 | 25 | // Collections 26 | var Sites = Collections.Sites; 27 | var Admins = Collections.Admins; 28 | var Blogs = Collections.Blogs; 29 | var Posts = Collections.Posts; 30 | var Comments = Collections.Comment; 31 | var Photos = Collections.Photos; 32 | 33 | describe('fetch', function() { 34 | 35 | it ('fetches the models in a collection', function() { 36 | return Bookshelf.Collection.extend({tableName: 'posts'}) 37 | .forge() 38 | .logMe() 39 | .fetch(); 40 | }); 41 | 42 | }); 43 | 44 | describe('fetchOne', function() { 45 | 46 | it ('fetches a single model from the collection', function() { 47 | return new Site({id:1}) 48 | .authors() 49 | .fetchOne() 50 | .then(function(model) { 51 | return model.get('site_id', 1); 52 | }); 53 | }); 54 | 55 | it ('maintains a clone of the query builder from the current collection', function() { 56 | 57 | return new Site({id:1}) 58 | .authors() 59 | .query({where: {id: 40}}) 60 | .fetchOne() 61 | .then(function(model) { 62 | expect(model).to.be.null; 63 | }); 64 | 65 | }); 66 | 67 | it ('follows the typical model options, like require: true', function() { 68 | 69 | return expect(new Site({id:1}) 70 | .authors() 71 | .query({where: {id: 40}}) 72 | .fetchOne({require: true})).to.be.rejected; 73 | 74 | }); 75 | 76 | }); 77 | 78 | describe('sync', function() { 79 | 80 | it('creates a new instance of Sync', function(){ 81 | var model = new Bookshelf.Model(); 82 | expect(model.sync(model)).to.be.an.instanceOf(require('../../dialects/sql/sync').Sync); 83 | }); 84 | 85 | }); 86 | 87 | describe('create', function() { 88 | 89 | it('creates and saves a new model instance, saving it to the collection', function () { 90 | 91 | return new Sites().create({name: 'google.com'}).then(function(model) { 92 | expect(model.get('name')).to.equal('google.com'); 93 | return model.destroy(); 94 | }); 95 | 96 | }); 97 | 98 | it('should populate a `hasMany` or `morphMany` with the proper keys', function() { 99 | 100 | return new Site({id: 10}) 101 | .authors() 102 | .create({first_name: 'test', last_name: 'tester'}) 103 | .then(function(author) { 104 | expect(author.get('first_name')).to.equal('test'); 105 | expect(author.get('last_name')).to.equal('tester'); 106 | expect(author.get('site_id')).to.equal(10); 107 | return author.destroy(); 108 | }) 109 | .then(function() { 110 | return new Site({id: 10}) 111 | .photos() 112 | .create({ 113 | url: 'http://image.dev', 114 | caption: 'this is a test image' 115 | }) 116 | .then(function(photo) { 117 | expect(photo.get('imageable_id')).to.equal(10); 118 | expect(photo.get('imageable_type')).to.equal('sites'); 119 | expect(photo.get('url')).to.equal('http://image.dev'); 120 | }); 121 | }); 122 | 123 | }); 124 | 125 | it('should automatically create a join model when joining a belongsToMany', function() { 126 | 127 | return new Site({id: 1}) 128 | .admins() 129 | .create({username: 'test', password: 'test'}) 130 | .then(function(admin) { 131 | expect(admin.get('username')).to.equal('test'); 132 | }); 133 | 134 | }); 135 | 136 | it('should maintain the correct constraints when creating a model from a relation', function() { 137 | var authors = new Site({id: 1}).authors(); 138 | var query = authors.query(); 139 | 140 | query.then = function(onFufilled, onRejected) { 141 | expect(this.values[0]).to.eql([['first_name', 'Test'], ['last_name', 'User'], ['site_id', 1]]); 142 | return when.resolve(this.toString()).then(onFufilled, onRejected); 143 | }; 144 | 145 | return authors.create({first_name: 'Test', last_name: 'User'}); 146 | }); 147 | 148 | 149 | }); 150 | 151 | }); 152 | 153 | }; -------------------------------------------------------------------------------- /test/integration/helpers/logger.js: -------------------------------------------------------------------------------- 1 | var cwd = process.cwd(); 2 | var isDev = parseInt(process.env.BOOKSHELF_DEV, 10); 3 | 4 | var _ = require('underscore'); 5 | 6 | var Ctors = {}; 7 | 8 | var Common = require('knex/lib/common').Common; 9 | 10 | Ctors.Raw = require('knex/lib/raw').Raw; 11 | Ctors.Builder = require('knex/lib/builder').Builder; 12 | Ctors.SchemaBuilder = require('knex/lib/schemabuilder').SchemaBuilder; 13 | 14 | Ctors.Model = require('../../../dialects/sql/model').Model; 15 | Ctors.Collection = require('../../../dialects/sql/collection').Collection; 16 | 17 | var fs = require('fs'); 18 | var objectdump = require('objectdump'); 19 | 20 | // This is where all of the info from the query calls goes... 21 | var output = {}; 22 | var comparable = {}; 23 | var counters = {}; 24 | 25 | exports.setLib = function(context) { 26 | 27 | var logMe = function(logWhat) { 28 | this.isLogging = logWhat || 'result'; 29 | return this; 30 | }; 31 | 32 | var then = function(onFufilled, onRejected) { 33 | 34 | this._promise || (this._promise = this.client.query(this)); 35 | 36 | var then = this; 37 | 38 | if (this.isLogging) { 39 | 40 | var title = context.test.title; 41 | var parent = generateTitle(context.test).pop(); 42 | var dialect = this.client.dialect; 43 | 44 | if (!isDev && !comparable[parent]) { 45 | comparable[parent] = require(__dirname + '/../output/' + parent); 46 | } 47 | 48 | // If we're not only logging the result for this query... 49 | if (this.isLogging !== 'result') { 50 | var bindings = this.cleanBindings(); 51 | checkIt('sql', title, parent, dialect, {sql: this.toSql(), bindings: this.cleanBindings()}); 52 | } 53 | } 54 | 55 | return this._promise.tap(function(resp) { 56 | 57 | // If we're not only logging the sql for this query... 58 | if (then.isLogging && then.isLogging !== 'sql') { 59 | checkIt('result', title, parent, dialect, {result: resp}); 60 | } 61 | 62 | }).then(onFufilled, onRejected); 63 | 64 | }; 65 | 66 | var checkIt = function(type, title, parent, dialect, data) { 67 | output[parent] = output[parent] || {}; 68 | output[parent][title] = output[parent][title] || {}; 69 | var toCheck, target = output[parent][title][dialect] = output[parent][title][dialect] || {}; 70 | 71 | try { 72 | toCheck = comparable[parent][title][dialect]; 73 | } catch (e) { 74 | if (!isDev) throw e; 75 | } 76 | 77 | var items = type === 'sql' ? ['bindings', 'sql'] : ['result']; 78 | 79 | if (!isDev) { 80 | 81 | _.each(items, function(item) { 82 | 83 | var localData = toCheck[item]; 84 | var newData = data[item]; 85 | 86 | // Mutate the bindings arrays to not check dates. 87 | if (item === 'bindings') { 88 | parseBindingDates(newData, localData); 89 | } if (item === 'result') { 90 | parseResultDates(newData, localData); 91 | } 92 | 93 | expect(localData).to.eql(newData); 94 | }); 95 | 96 | 97 | } else { 98 | 99 | _.each(items, function(item) { 100 | 101 | var newData = data[item]; 102 | 103 | if (_.isObject(newData)) newData = JSON.parse(JSON.stringify(newData)); 104 | 105 | target[item] = newData; 106 | 107 | }); 108 | 109 | } 110 | 111 | }; 112 | 113 | _.each(['Raw', 'SchemaBuilder', 'Builder'], function(Item) { 114 | Ctors[Item].prototype.logMe = logMe; 115 | Ctors[Item].prototype.then = then; 116 | }); 117 | 118 | _.each(['Model', 'Collection'], function(Item) { 119 | var origFetch = Ctors[Item].prototype.fetch; 120 | Ctors[Item].prototype.fetch = function(options) { 121 | options = options || {}; 122 | var dialect = this.query().client.dialect; 123 | return origFetch.apply(this, arguments).then(function(obj) { 124 | if (options.log) { 125 | if (options.debugging) debugger; 126 | var title = context.test.title; 127 | var parent = generateTitle(context.test).pop(); 128 | if (!isDev && !comparable[parent]) { 129 | comparable[parent] = require(__dirname + '/../output/' + parent); 130 | } 131 | checkIt('result', title, parent, dialect, {result: JSON.parse(JSON.stringify(obj))}); 132 | } 133 | return obj; 134 | }); 135 | }; 136 | 137 | Ctors[Item].prototype.logMe = function(val) { 138 | this.query('logMe', val); 139 | return this; 140 | }; 141 | }); 142 | 143 | var parseResultDates = function(newData, localData) { 144 | _.each([newData, localData], function(item) { 145 | _.each(item, function(row, i) { 146 | item[i] = _.omit(row, 'created_at', 'updated_at'); 147 | }); 148 | }); 149 | }; 150 | 151 | var parseBindingDates = function(newData, localData) { 152 | _.each(localData, function(item, index) { 153 | if (_.isDate(item)) { 154 | localData[index] = '_date_'; 155 | newData[index] = '_date_'; 156 | } 157 | }); 158 | }; 159 | 160 | var generateTitle = function(context, stack) { 161 | stack = stack || []; 162 | if (context.parent && context.parent.title.indexOf('Dialect') !== 0) { 163 | stack.push(context.parent.title); 164 | return generateTitle(context.parent, stack); 165 | } 166 | return stack; 167 | }; 168 | 169 | }; 170 | 171 | exports.writeResult = function() { 172 | if (!isDev) return; 173 | _.each(output, function(val, key) { 174 | fs.writeFileSync(__dirname + '/../output/' + key + '.js', 'module.exports = ' + objectdump(val) + ';'); 175 | }); 176 | }; 177 | 178 | -------------------------------------------------------------------------------- /dialects/sql/collection.js: -------------------------------------------------------------------------------- 1 | // Collection 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var when = require('when'); 11 | 12 | var Sync = require('./sync').Sync; 13 | var Helpers = require('./helpers').Helpers; 14 | var EagerRelation = require('./eager').EagerRelation; 15 | 16 | var CollectionBase = require('../base/collection').CollectionBase; 17 | 18 | exports.Collection = CollectionBase.extend({ 19 | 20 | // Used to define passthrough relationships - `hasOne`, `hasMany`, 21 | // `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection. 22 | through: function(Interim, foreignKey, otherKey) { 23 | return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey}); 24 | }, 25 | 26 | // Fetch the models for this collection, resetting the models 27 | // for the query when they arrive. 28 | fetch: function(options) { 29 | options = options || {}; 30 | var collection = this, relatedData = this.relatedData; 31 | var sync = this.sync(options) 32 | .select() 33 | .tap(function(response) { 34 | if (!response || response.length === 0) { 35 | if (options.require) throw new Error('EmptyResponse'); 36 | return when.reject(null); 37 | } 38 | }) 39 | 40 | // Now, load all of the data onto the collection as necessary. 41 | .tap(this._handleResponse); 42 | 43 | // If the "withRelated" is specified, we also need to eager load all of the 44 | // data on the collection, as a side-effect, before we ultimately jump into the 45 | // next step of the collection. Since the `columns` are only relevant to the current 46 | // level, ensure those are omitted from the options. 47 | if (options.withRelated) { 48 | sync = sync.tap(this._handleEager(_.omit(options, 'columns'))); 49 | } 50 | 51 | return sync.tap(function(response) { 52 | return collection.triggerThen('fetched', collection, response, options); 53 | }) 54 | .otherwise(function(err) { 55 | if (err !== null) throw err; 56 | collection.reset([], {silent: true}); 57 | }) 58 | .yield(this); 59 | }, 60 | 61 | // Fetches a single model from the collection, useful on related collections. 62 | fetchOne: function(options) { 63 | var model = new this.model; 64 | model._knex = this.query().clone(); 65 | if (this.relatedData) model.relatedData = this.relatedData; 66 | return model.fetch(options); 67 | }, 68 | 69 | // Eager loads relationships onto an already populated `Collection` instance. 70 | load: function(relations, options) { 71 | var collection = this; 72 | _.isArray(relations) || (relations = [relations]); 73 | options = _.extend({}, options, {shallow: true, withRelated: relations}); 74 | return new EagerRelation(this.models, this.toJSON(options), new this.model()) 75 | .fetch(options) 76 | .yield(this); 77 | }, 78 | 79 | // Shortcut for creating a new model, saving, and adding to the collection. 80 | // Returns a promise which will resolve with the model added to the collection. 81 | // If the model is a relation, put the `foreignKey` and `fkValue` from the `relatedData` 82 | // hash into the inserted model. Also, if the model is a `manyToMany` relation, 83 | // automatically create the joining model upon insertion. 84 | create: function(model, options) { 85 | options = options || {}; 86 | 87 | var collection = this; 88 | var relatedData = this.relatedData; 89 | 90 | model = this._prepareModel(model, options); 91 | 92 | // If we've already added things on the query chain, 93 | // these are likely intended for the model. 94 | if (this._knex) { 95 | model._knex = this._knex; 96 | this.resetQuery(); 97 | } 98 | 99 | return Helpers 100 | .saveConstraints(model, relatedData) 101 | .save(null, options) 102 | .then(function() { 103 | if (relatedData && (relatedData.type === 'belongsToMany' || relatedData.isThrough())) { 104 | return collection.attach(model, options); 105 | } 106 | }) 107 | .then(function() { 108 | collection.add(model, options); 109 | return model; 110 | }); 111 | }, 112 | 113 | // Reset the query builder, called internally 114 | // each time a query is run. 115 | resetQuery: function() { 116 | this._knex = null; 117 | return this; 118 | }, 119 | 120 | // Returns an instance of the query builder. 121 | query: function() { 122 | return Helpers.query(this, _.toArray(arguments)); 123 | }, 124 | 125 | // Creates and returns a new `Bookshelf.Sync` instance. 126 | sync: function(options) { 127 | return new Sync(this, options); 128 | }, 129 | 130 | // Handles the response data for the collection, returning from the collection's fetch call. 131 | _handleResponse: function(response) { 132 | var relatedData = this.relatedData; 133 | this.set(response, {silent: true, parse: true}).invoke('_reset'); 134 | if (relatedData && relatedData.isJoined()) { 135 | relatedData.parsePivot(this.models); 136 | } 137 | }, 138 | 139 | // Handle the related data loading on the collection. 140 | _handleEager: function(options) { 141 | var collection = this; 142 | return function(response) { 143 | return new EagerRelation(collection.models, response, new collection.model()).fetch(options); 144 | }; 145 | } 146 | 147 | }); 148 | 149 | }); 150 | 151 | })( 152 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 153 | ); -------------------------------------------------------------------------------- /test/integration/helpers/objects.js: -------------------------------------------------------------------------------- 1 | 2 | // All Models & Collections Used in the Tests 3 | // (sort of mimics a simple multi-site blogging engine) 4 | module.exports = function(Bookshelf) { 5 | 6 | var Info = Bookshelf.Model.extend({ 7 | tableName: 'info' 8 | }); 9 | 10 | var SiteMeta = Bookshelf.Model.extend({ 11 | tableName: 'sitesmeta', 12 | site: function() { 13 | return this.belongsTo(Site); 14 | }, 15 | info: function() { 16 | return this.hasOne(Info); 17 | } 18 | }); 19 | 20 | var Site = Bookshelf.Model.extend({ 21 | tableName: 'sites', 22 | defaults: { 23 | name: 'Your Cool Site' 24 | }, 25 | authors: function() { 26 | return this.hasMany(Author); 27 | }, 28 | photos: function() { 29 | return this.morphMany(Photo, 'imageable'); 30 | }, 31 | blogs: function() { 32 | return this.hasMany(Blog); 33 | }, 34 | meta: function() { 35 | return this.hasOne(SiteMeta); 36 | }, 37 | info: function() { 38 | return this.hasOne(Info).through(SiteMeta, 'meta_id'); 39 | }, 40 | admins: function() { 41 | return this.belongsToMany(Admin).withPivot('item'); 42 | } 43 | }); 44 | 45 | var Sites = Bookshelf.Collection.extend({ 46 | model: Site 47 | }); 48 | 49 | var Admin = Bookshelf.Model.extend({ 50 | tableName: 'admins', 51 | hasTimestamps: true 52 | }); 53 | 54 | // All admins for a site. 55 | var Admins = Bookshelf.Collection.extend({ 56 | model: Admin 57 | }); 58 | 59 | // Author of a blog post. 60 | var Author = Bookshelf.Model.extend({ 61 | tableName: 'authors', 62 | site: function() { 63 | return this.belongsTo(Site); 64 | }, 65 | photo: function() { 66 | return this.morphOne(Photo, 'imageable'); 67 | }, 68 | posts: function() { 69 | return this.belongsToMany(Post); 70 | }, 71 | ownPosts: function() { 72 | return this.hasMany(Post, 'owner_id'); 73 | }, 74 | blogs: function() { 75 | return this.belongsToMany(Blog).through(Post, 'owner_id'); 76 | } 77 | }); 78 | 79 | var Authors = Bookshelf.Collection.extend({ 80 | model: Author 81 | }); 82 | 83 | // A blog for a site. 84 | var Blog = Bookshelf.Model.extend({ 85 | tableName: 'blogs', 86 | defaults: { 87 | title: '' 88 | }, 89 | site: function() { 90 | return this.belongsTo(Site); 91 | }, 92 | posts: function() { 93 | return this.hasMany(Post); 94 | }, 95 | validate: function(attrs) { 96 | if (!attrs.title) return 'A title is required.'; 97 | }, 98 | comments: function() { 99 | return this.hasMany(Comments).through(Post); 100 | } 101 | }); 102 | 103 | var Blogs = Bookshelf.Collection.extend({ 104 | model: Blog 105 | }); 106 | 107 | // An individual post on a blog. 108 | var Post = Bookshelf.Model.extend({ 109 | tableName: 'posts', 110 | defaults: { 111 | author: '', 112 | title: '', 113 | body: '', 114 | published: false 115 | }, 116 | hasTimestamps: true, 117 | blog: function() { 118 | return this.belongsTo(Blog); 119 | }, 120 | authors: function() { 121 | return this.belongsToMany(Author); 122 | }, 123 | tags: function() { 124 | return this.belongsToMany(Tag); 125 | }, 126 | comments: function() { 127 | return this.hasMany(Comment); 128 | } 129 | }); 130 | 131 | var Posts = Bookshelf.Collection.extend({ 132 | model: Post 133 | }); 134 | 135 | var Comment = Bookshelf.Model.extend({ 136 | tableName: 'comments', 137 | defaults: { 138 | email: '', 139 | post: '' 140 | }, 141 | posts: function() { 142 | return this.belongsTo(Post); 143 | } 144 | }); 145 | 146 | var Comments = Bookshelf.Collection.extend({ 147 | model: Comment 148 | }); 149 | 150 | var Tag = Bookshelf.Model.extend({ 151 | tableName: 'tags', 152 | posts: function() { 153 | return this.belongsToMany(Post); 154 | } 155 | }); 156 | 157 | var User = Bookshelf.Model.extend({ 158 | tableName: 'users', 159 | idAttribute: 'uid', 160 | roles: function() { 161 | return this.belongsToMany(Role, 'users_roles', 'uid', 'rid'); 162 | } 163 | }); 164 | 165 | var Role = Bookshelf.Model.extend({ 166 | tableName: 'roles', 167 | idAttribute: 'rid', 168 | users: function(){ 169 | return this.belongsToMany(User, 'users_roles', 'rid', 'uid'); 170 | } 171 | }); 172 | 173 | var Photo = Bookshelf.Model.extend({ 174 | tableName: 'photos', 175 | imageable: function() { 176 | return this.morphTo('imageable', Site, Author); 177 | } 178 | }); 179 | 180 | var Photos = Bookshelf.Collection.extend({ 181 | model: Photo 182 | }); 183 | 184 | var Settings = Bookshelf.Model.extend({ tableName: 'Settings' }); 185 | 186 | var Customer = Bookshelf.Model.extend({ 187 | tableName: 'Customer', 188 | settings: function () { 189 | return this.hasOne(Settings); 190 | } 191 | }); 192 | 193 | var Hostname = Bookshelf.Model.extend({ 194 | tableName: 'hostnames', 195 | idAttribute: 'hostname', 196 | instance: function() { 197 | return this.belongsTo(Instance); 198 | } 199 | }); 200 | 201 | var Instance = Bookshelf.Model.extend({ 202 | tableName: 'instances', 203 | hostnames: function() { 204 | return this.hasMany(Hostname); 205 | } 206 | }); 207 | 208 | return { 209 | Models: { 210 | Site: Site, 211 | SiteMeta: SiteMeta, 212 | Admin: Admin, 213 | Author: Author, 214 | Blog: Blog, 215 | Post: Post, 216 | Comment: Comment, 217 | Tag: Tag, 218 | User: User, 219 | Role: Role, 220 | Photo: Photo, 221 | Info: Info, 222 | Customer: Customer, 223 | Settings: Settings, 224 | Instance: Instance, 225 | Hostname: Hostname 226 | }, 227 | Collections: { 228 | Sites: Sites, 229 | Admins: Admins, 230 | Posts: Posts, 231 | Blogs: Blogs, 232 | Comments: Comments, 233 | Photos: Photos, 234 | Authors: Authors 235 | } 236 | }; 237 | 238 | }; -------------------------------------------------------------------------------- /test/integration/output/Collection.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'fetches the models in a collection': { 3 | mysql: { 4 | result: [{ 5 | id: 1, 6 | owner_id: 1, 7 | blog_id: 1, 8 | name: 'This is a new Title!', 9 | content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.' 10 | },{ 11 | id: 2, 12 | owner_id: 2, 13 | blog_id: 2, 14 | name: 'This is a new Title 2!', 15 | content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.' 16 | },{ 17 | id: 3, 18 | owner_id: 2, 19 | blog_id: 1, 20 | name: 'This is a new Title 3!', 21 | content: 'Lorem ipsum Reprehenderit esse esse consectetur aliquip magna.' 22 | },{ 23 | id: 4, 24 | owner_id: 3, 25 | blog_id: 3, 26 | name: 'This is a new Title 4!', 27 | content: 'Lorem ipsum Anim sed eu sint aute.' 28 | },{ 29 | id: 5, 30 | owner_id: 4, 31 | blog_id: 4, 32 | name: 'This is a new Title 5!', 33 | content: 'Lorem ipsum Commodo consectetur eu ea amet laborum nulla eiusmod minim veniam ullamco nostrud sed mollit consectetur veniam mollit Excepteur quis cupidatat.' 34 | }] 35 | }, 36 | postgresql: { 37 | result: [{ 38 | id: 1, 39 | owner_id: 1, 40 | blog_id: 1, 41 | name: 'This is a new Title!', 42 | content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.' 43 | },{ 44 | id: 2, 45 | owner_id: 2, 46 | blog_id: 2, 47 | name: 'This is a new Title 2!', 48 | content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.' 49 | },{ 50 | id: 3, 51 | owner_id: 2, 52 | blog_id: 1, 53 | name: 'This is a new Title 3!', 54 | content: 'Lorem ipsum Reprehenderit esse esse consectetur aliquip magna.' 55 | },{ 56 | id: 4, 57 | owner_id: 3, 58 | blog_id: 3, 59 | name: 'This is a new Title 4!', 60 | content: 'Lorem ipsum Anim sed eu sint aute.' 61 | },{ 62 | id: 5, 63 | owner_id: 4, 64 | blog_id: 4, 65 | name: 'This is a new Title 5!', 66 | content: 'Lorem ipsum Commodo consectetur eu ea amet laborum nulla eiusmod minim veniam ullamco nostrud sed mollit consectetur veniam mollit Excepteur quis cupidatat.' 67 | }] 68 | }, 69 | sqlite3: { 70 | result: [{ 71 | id: 1, 72 | owner_id: 1, 73 | blog_id: 1, 74 | name: 'This is a new Title!', 75 | content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.' 76 | },{ 77 | id: 2, 78 | owner_id: 2, 79 | blog_id: 2, 80 | name: 'This is a new Title 2!', 81 | content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.' 82 | },{ 83 | id: 3, 84 | owner_id: 2, 85 | blog_id: 1, 86 | name: 'This is a new Title 3!', 87 | content: 'Lorem ipsum Reprehenderit esse esse consectetur aliquip magna.' 88 | },{ 89 | id: 4, 90 | owner_id: 3, 91 | blog_id: 3, 92 | name: 'This is a new Title 4!', 93 | content: 'Lorem ipsum Anim sed eu sint aute.' 94 | },{ 95 | id: 5, 96 | owner_id: 4, 97 | blog_id: 4, 98 | name: 'This is a new Title 5!', 99 | content: 'Lorem ipsum Commodo consectetur eu ea amet laborum nulla eiusmod minim veniam ullamco nostrud sed mollit consectetur veniam mollit Excepteur quis cupidatat.' 100 | }] 101 | } 102 | } 103 | }; -------------------------------------------------------------------------------- /docs/dialects/base/sync.html: -------------------------------------------------------------------------------- 1 | dialects/base/sync
dialects/base/sync.js

Base Sync

(function(define) { 2 | 3 | "use strict";

An example "sync" object which is extended 4 | by dialect-specific sync implementations, 5 | making Bookshelf effectively a data store 6 | agnostic "Data Mapper".

define(function(require, exports) { 7 | 8 | var when = require('when'); 9 | var Backbone = require('backbone');

Used as the base of the prototype chain, 10 | a convenient object for any instanceof 11 | checks you may need.

var BaseSync = function() {}; 12 | 13 | BaseSync.prototype = {

May be used for any setup for the class.

initialize: function() {},

Return a single model object.

first: function() { 14 | return when.resolve({}); 15 | },

Select one or more models, returning an array 16 | of data objects.

select: function() { 17 | return when.resolve([]); 18 | },

Insert a single row, returning an object 19 | (typically containing an "insert id").

insert: function() { 20 | return when.resolve({}); 21 | },

Update an object in the data store.

update: function() { 22 | return when.resolve({}); 23 | },

Delete a record from the data store.

del: function() { 24 | return when.resolve({}); 25 | } 26 | 27 | }; 28 | 29 | BaseSync.extend = Backbone.Model.extend; 30 | 31 | exports.BaseSync = BaseSync; 32 | 33 | }); 34 | 35 | })( 36 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 37 | );
-------------------------------------------------------------------------------- /dialects/base/collection.js: -------------------------------------------------------------------------------- 1 | // Base Collection 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | // The `CollectionBase` is an object that takes 8 | define(function(require, exports) { 9 | 10 | // All exernal dependencies required in this scope. 11 | var _ = require('underscore'); 12 | var when = require('when'); 13 | var Backbone = require('backbone'); 14 | 15 | // All components that need to be referenced in this scope. 16 | var Events = require('./events').Events; 17 | var ModelBase = require('./model').ModelBase; 18 | 19 | var array = []; 20 | var push = array.push; 21 | var splice = array.splice; 22 | 23 | var CollectionBase = function(models, options) { 24 | if (options) _.extend(this, _.pick(options, collectionProps)); 25 | this._reset(); 26 | this.initialize.apply(this, arguments); 27 | if (models) this.reset(models, _.extend({silent: true}, options)); 28 | _.bindAll(this, '_handleResponse', '_handleEager'); 29 | }; 30 | 31 | // List of attributes attached directly from the constructor's options object. 32 | var collectionProps = ['model', 'comparator']; 33 | 34 | // A list of properties that are omitted from the `Backbone.Model.prototype`, to create 35 | // a generic collection base. 36 | var collectionOmitted = ['model', 'fetch', 'url', 'sync', 'create']; 37 | 38 | // Copied over from Backbone. 39 | var setOptions = {add: true, remove: true, merge: true}; 40 | 41 | _.extend(CollectionBase.prototype, _.omit(Backbone.Collection.prototype, collectionOmitted), Events, { 42 | 43 | // The `tableName` on the associated Model, used in relation building. 44 | tableName: function() { 45 | return _.result(this.model.prototype, 'tableName'); 46 | }, 47 | 48 | // The `idAttribute` on the associated Model, used in relation building. 49 | idAttribute: function() { 50 | return this.model.prototype.idAttribute; 51 | }, 52 | 53 | // A simplified version of Backbone's `Collection#set` method, 54 | // removing the comparator, and getting rid of the temporary model creation, 55 | // since there's *no way* we'll be getting the data in an inconsistent 56 | // form from the database. 57 | set: function(models, options) { 58 | options = _.defaults({}, options, setOptions); 59 | if (options.parse) models = this.parse(models, options); 60 | if (!_.isArray(models)) models = models ? [models] : []; 61 | var i, l, id, model, attrs, existing; 62 | var at = options.at; 63 | var targetModel = this.model; 64 | var toAdd = [], toRemove = [], modelMap = {}; 65 | var add = options.add, merge = options.merge, remove = options.remove; 66 | var order = add && remove ? [] : false; 67 | 68 | // Turn bare objects into model references, and prevent invalid models 69 | // from being added. 70 | for (i = 0, l = models.length; i < l; i++) { 71 | attrs = models[i]; 72 | if (attrs instanceof ModelBase) { 73 | id = model = attrs; 74 | } else { 75 | id = attrs[targetModel.prototype.idAttribute]; 76 | } 77 | 78 | // If a duplicate is found, prevent it from being added and 79 | // optionally merge it into the existing model. 80 | if (existing = this.get(id)) { 81 | if (remove) { 82 | modelMap[existing.cid] = true; 83 | continue; 84 | } 85 | if (merge) { 86 | attrs = attrs === model ? model.attributes : attrs; 87 | if (options.parse) attrs = existing.parse(attrs, options); 88 | existing.set(attrs, options); 89 | } 90 | 91 | // This is a new model, push it to the `toAdd` list. 92 | } else if (add) { 93 | if (!(model = this._prepareModel(attrs, options))) continue; 94 | toAdd.push(model); 95 | 96 | // Listen to added models' events, and index models for lookup by 97 | // `id` and by `cid`. 98 | model.on('all', this._onModelEvent, this); 99 | this._byId[model.cid] = model; 100 | if (model.id != null) this._byId[model.id] = model; 101 | } 102 | if (order) order.push(existing || model); 103 | } 104 | 105 | // Remove nonexistent models if appropriate. 106 | if (remove) { 107 | for (i = 0, l = this.length; i < l; ++i) { 108 | if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 109 | } 110 | if (toRemove.length) this.remove(toRemove, options); 111 | } 112 | 113 | // See if sorting is needed, update `length` and splice in new models. 114 | if (toAdd.length || (order && order.length)) { 115 | this.length += toAdd.length; 116 | if (at != null) { 117 | splice.apply(this.models, [at, 0].concat(toAdd)); 118 | } else { 119 | if (order) this.models.length = 0; 120 | push.apply(this.models, order || toAdd); 121 | } 122 | } 123 | 124 | if (options.silent) return this; 125 | 126 | // Trigger `add` events. 127 | for (i = 0, l = toAdd.length; i < l; i++) { 128 | (model = toAdd[i]).trigger('add', model, this, options); 129 | } 130 | return this; 131 | }, 132 | 133 | // Prepare a model or hash of attributes to be added to this collection. 134 | _prepareModel: function(attrs, options) { 135 | if (attrs instanceof ModelBase) return attrs; 136 | return new this.model(attrs, options); 137 | }, 138 | 139 | // Convenience method for map, returning a `when.all` promise. 140 | mapThen: function(iterator, context) { 141 | return when.all(this.map(iterator, context)); 142 | }, 143 | 144 | // Convenience method for invoke, returning a `when.all` promise. 145 | invokeThen: function() { 146 | return when.all(this.invoke.apply(this, arguments)); 147 | }, 148 | 149 | fetch: function() {}, 150 | 151 | _handleResponse: function() {}, 152 | 153 | _handleEager: function() {} 154 | 155 | }); 156 | 157 | // List of attributes attached directly from the `options` passed to the constructor. 158 | var modelProps = ['tableName', 'hasTimestamps']; 159 | 160 | CollectionBase.extend = Backbone.Collection.extend; 161 | 162 | // Helper to mixin one or more additional items to the current prototype. 163 | CollectionBase.include = function() { 164 | _.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); 165 | return this; 166 | }; 167 | 168 | exports.CollectionBase = CollectionBase; 169 | 170 | }); 171 | 172 | })( 173 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 174 | ); -------------------------------------------------------------------------------- /dialects/base/model.js: -------------------------------------------------------------------------------- 1 | // Base Model 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var Backbone = require('backbone'); 11 | 12 | var Events = require('./events').Events; 13 | 14 | // A list of properties that are omitted from the `Backbone.Model.prototype`, to create 15 | // a generic model base. 16 | var modelOmitted = [ 17 | 'changedAttributes', 'isValid', 'validationError', 18 | 'save', 'sync', 'fetch', 'destroy', 'url', 19 | 'urlRoot', '_validate' 20 | ]; 21 | 22 | // The "ModelBase" is similar to the 'Active Model' in Rails, 23 | // it defines a standard interface from which other objects may 24 | // inherit. 25 | var ModelBase = function(attributes, options) { 26 | var attrs = attributes || {}; 27 | options || (options = {}); 28 | this.attributes = Object.create(null); 29 | this._reset(); 30 | this.relations = {}; 31 | this.cid = _.uniqueId('c'); 32 | if (options) { 33 | _.extend(this, _.pick(options, modelProps)); 34 | if (options.parse) attrs = this.parse(attrs, options) || {}; 35 | } 36 | this.set(attrs, options); 37 | this.initialize.apply(this, arguments); 38 | _.bindAll(this, '_handleResponse', '_handleEager'); 39 | }; 40 | 41 | _.extend(ModelBase.prototype, _.omit(Backbone.Model.prototype), Events, { 42 | 43 | // Similar to the standard `Backbone` set method, but without individual 44 | // change events, and adding different meaning to `changed` and `previousAttributes` 45 | // defined as the last "sync"'ed state of the model. 46 | set: function(key, val, options) { 47 | if (key == null) return this; 48 | var attrs; 49 | 50 | // Handle both `"key", value` and `{key: value}` -style arguments. 51 | if (typeof key === 'object') { 52 | attrs = key; 53 | options = val; 54 | } else { 55 | (attrs = {})[key] = val; 56 | } 57 | options || (options = {}); 58 | 59 | // Extract attributes and options. 60 | var hasChanged = false; 61 | var unset = options.unset; 62 | var current = this.attributes; 63 | var prev = this._previousAttributes; 64 | 65 | // Check for changes of `id`. 66 | if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 67 | 68 | // For each `set` attribute, update or delete the current value. 69 | for (var attr in attrs) { 70 | val = attrs[attr]; 71 | if (!_.isEqual(prev[attr], val)) { 72 | this.changed[attr] = val; 73 | if (!_.isEqual(current[attr], val)) hasChanged = true; 74 | } else { 75 | delete this.changed[attr]; 76 | } 77 | unset ? delete current[attr] : current[attr] = val; 78 | } 79 | 80 | if (hasChanged && !options.silent) this.trigger('change', this, options); 81 | return this; 82 | }, 83 | 84 | // Returns an object containing a shallow copy of the model attributes, 85 | // along with the `toJSON` value of any relations, 86 | // unless `{shallow: true}` is passed in the `options`. 87 | toJSON: function(options) { 88 | var attrs = _.extend({}, this.attributes); 89 | if (options && options.shallow) return attrs; 90 | var relations = this.relations; 91 | for (var key in relations) { 92 | var relation = relations[key]; 93 | attrs[key] = relation.toJSON ? relation.toJSON() : relation; 94 | } 95 | if (this.pivot) { 96 | var pivot = this.pivot.attributes; 97 | for (key in pivot) { 98 | attrs['_pivot_' + key] = pivot[key]; 99 | } 100 | } 101 | return attrs; 102 | }, 103 | 104 | // **parse** converts a response into the hash of attributes to be `set` on 105 | // the model. The default implementation is just to pass the response along. 106 | parse: function(resp, options) { 107 | return resp; 108 | }, 109 | 110 | // **format** converts a model into the values that should be saved into 111 | // the database table. The default implementation is just to pass the data along. 112 | format: function(attrs, options) { 113 | return attrs; 114 | }, 115 | 116 | // Returns the related item, or creates a new 117 | // related item by creating a new model or collection. 118 | related: function(name) { 119 | return this.relations[name] || (this[name] ? this.relations[name] = this[name]() : void 0); 120 | }, 121 | 122 | // Create a new model with identical attributes to this one, 123 | // including any relations on the current model. 124 | clone: function() { 125 | var model = new this.constructor(this.attributes); 126 | var relations = this.relations; 127 | for (var key in relations) { 128 | model.relations[key] = relations[key].clone(); 129 | } 130 | model._previousAttributes = _.clone(this._previousAttributes); 131 | model.changed = _.clone(this.changed); 132 | return model; 133 | }, 134 | 135 | // Sets the timestamps before saving the model. 136 | timestamp: function(options) { 137 | var d = new Date(); 138 | var keys = (_.isArray(this.hasTimestamps) ? this.hasTimestamps : ['created_at', 'updated_at']); 139 | var vals = {}; 140 | vals[keys[1]] = d; 141 | if (this.isNew(options) && (!options || options.method !== 'update')) vals[keys[0]] = d; 142 | return vals; 143 | }, 144 | 145 | // Called after a `sync` action (save, fetch, delete) - 146 | // resets the `_previousAttributes` and `changed` hash for the model. 147 | _reset: function() { 148 | this._previousAttributes = _.extend(Object.create(null), this.attributes); 149 | this.changed = Object.create(null); 150 | return this; 151 | }, 152 | 153 | fetch: function() {}, 154 | 155 | save: function() {}, 156 | 157 | // Destroy a model, calling a "delete" based on its `idAttribute`. 158 | // A "destroying" and "destroyed" are triggered on the model before 159 | // and after the model is destroyed, respectively. If an error is thrown 160 | // during the "destroying" event, the model will not be destroyed. 161 | destroy: function(options) { 162 | var model = this; 163 | options = options || {}; 164 | return model.triggerThen('destroying', model, options) 165 | .then(function() { return model.sync(options).del(); }) 166 | .then(function(resp) { 167 | model.clear(); 168 | return model.triggerThen('destroyed', model, resp, options); 169 | }) 170 | .then(function() { return model._reset(); }); 171 | }, 172 | 173 | _handleResponse: function() {}, 174 | 175 | _handleEager: function() {} 176 | 177 | }); 178 | 179 | // List of attributes attached directly from the `options` passed to the constructor. 180 | var modelProps = ['tableName', 'hasTimestamps']; 181 | 182 | ModelBase.extend = Backbone.Model.extend; 183 | 184 | // Helper to mixin one or more additional items to the current prototype. 185 | ModelBase.include = function() { 186 | _.extend.apply(_, [this.prototype].concat(_.toArray(arguments))); 187 | return this; 188 | }; 189 | 190 | exports.ModelBase = ModelBase; 191 | 192 | }); 193 | 194 | })( 195 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 196 | ); -------------------------------------------------------------------------------- /test/integration/helpers/inserts.js: -------------------------------------------------------------------------------- 1 | var when = require('when'); 2 | var _ = require('underscore'); 3 | 4 | module.exports = function(bookshelf) { 5 | 6 | var knex = bookshelf.knex; 7 | 8 | return when.all([ 9 | 10 | knex('sites').insert([{ 11 | name: 'knexjs.org' 12 | }, { 13 | name: 'bookshelfjs.org' 14 | }, { 15 | name: 'backbonejs.org' 16 | }]), 17 | 18 | knex('sitesmeta').insert([{ 19 | site_id: 1, 20 | description: 'This is a description for the Knexjs Site' 21 | }, { 22 | site_id: 2, 23 | description: 'This is a description for the Bookshelfjs Site' 24 | }]), 25 | 26 | knex('info').insert([{ 27 | meta_id: 1, 28 | other_description: 'This is an info block for hasOne -> through test' 29 | }, { 30 | meta_id: 2, 31 | other_description: 'This is an info block for an eager hasOne -> through test' 32 | }]), 33 | 34 | knex('admins').insert([{ 35 | username: 'test1', 36 | password: 'testpwd1', 37 | created_at: new Date(), 38 | updated_at: new Date() 39 | }, { 40 | username: 'test2', 41 | password: 'testpwd2', 42 | created_at: new Date(), 43 | updated_at: new Date() 44 | }]), 45 | 46 | knex('blogs').insert([{ 47 | site_id: 1, 48 | name: 'Main Site Blog' 49 | },{ 50 | site_id: 1, 51 | name: 'Alternate Site Blog' 52 | },{ 53 | site_id: 2, 54 | name: 'Main Site Blog' 55 | },{ 56 | site_id: 2, 57 | name: 'Alternate Site Blog' 58 | }]), 59 | 60 | knex('authors').insert([{ 61 | site_id: 1, 62 | first_name: 'Tim', 63 | last_name: 'Griesser' 64 | },{ 65 | site_id: 1, 66 | first_name: 'Bazooka', 67 | last_name: 'Joe' 68 | },{ 69 | site_id: 2, 70 | first_name: 'Charlie', 71 | last_name: 'Brown' 72 | },{ 73 | site_id: 2, 74 | first_name: 'Ron', 75 | last_name: 'Burgundy' 76 | }]), 77 | 78 | knex('posts').insert([{ 79 | owner_id: 1, 80 | blog_id: 1, 81 | name: 'This is a new Title!', 82 | content: 'Lorem ipsum Labore eu sed sed Excepteur enim laboris deserunt adipisicing dolore culpa aliqua cupidatat proident ea et commodo labore est adipisicing ex amet exercitation est.' 83 | },{ 84 | owner_id: 2, 85 | blog_id: 2, 86 | name: 'This is a new Title 2!', 87 | content: 'Lorem ipsum Veniam ex amet occaecat dolore in pariatur minim est exercitation deserunt Excepteur enim officia occaecat in exercitation aute et ad esse ex in in dolore amet consequat quis sed mollit et id incididunt sint dolore velit officia dolor dolore laboris dolor Duis ea ex quis deserunt anim nisi qui culpa laboris nostrud Duis anim deserunt esse laboris nulla qui in dolor voluptate aute reprehenderit amet ut et non voluptate elit irure mollit dolor consectetur nisi adipisicing commodo et mollit dolore incididunt cupidatat nulla ut irure deserunt non officia laboris fugiat ut pariatur ut non aliqua eiusmod dolor et nostrud minim elit occaecat commodo consectetur cillum elit laboris mollit dolore amet id qui eiusmod nulla elit eiusmod est ad aliqua aute enim ut aliquip ex in Ut nisi sint exercitation est mollit veniam cupidatat adipisicing occaecat dolor irure in aute aliqua ullamco.' 88 | },{ 89 | owner_id: 2, 90 | blog_id: 1, 91 | name: 'This is a new Title 3!', 92 | content: 'Lorem ipsum Reprehenderit esse esse consectetur aliquip magna.' 93 | },{ 94 | owner_id: 3, 95 | blog_id: 3, 96 | name: 'This is a new Title 4!', 97 | content: 'Lorem ipsum Anim sed eu sint aute.' 98 | },{ 99 | owner_id: 4, 100 | blog_id: 4, 101 | name: 'This is a new Title 5!', 102 | content: 'Lorem ipsum Commodo consectetur eu ea amet laborum nulla eiusmod minim veniam ullamco nostrud sed mollit consectetur veniam mollit Excepteur quis cupidatat.' 103 | }]), 104 | 105 | knex('authors_posts').insert([{ 106 | author_id: 1, 107 | post_id: 1 108 | },{ 109 | author_id: 2, 110 | post_id: 2 111 | },{ 112 | author_id: 3, 113 | post_id: 4 114 | },{ 115 | author_id: 2, 116 | post_id: 1 117 | }]), 118 | 119 | knex('tags').insert([{ 120 | name: 'cool' 121 | },{ 122 | name: 'boring' 123 | },{ 124 | name: 'exciting' 125 | },{ 126 | name: 'amazing' 127 | }]), 128 | 129 | knex('posts_tags').insert([{ 130 | post_id: 1, 131 | tag_id: 1 132 | },{ 133 | post_id: 1, 134 | tag_id: 2 135 | },{ 136 | post_id: 1, 137 | tag_id: 3 138 | },{ 139 | post_id: 4, 140 | tag_id: 1 141 | }]), 142 | 143 | knex('comments').insert([{ 144 | post_id: 1, 145 | name: '(blank)', 146 | email: 'test@example.com', 147 | comment: 'this is neat.' 148 | }]), 149 | 150 | knex('photos').insert([{ 151 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 152 | url: 'https://www.google.com/images/srpr/logo4w.png', 153 | imageable_id: 1, 154 | imageable_type: 'authors' 155 | }, { 156 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 157 | url: 'https://www.google.com/images/srpr/logo4w.png', 158 | imageable_id: 2, 159 | imageable_type: 'authors' 160 | }, { 161 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 162 | url: 'https://www.google.com/images/srpr/logo4w.png', 163 | imageable_id: 1, 164 | imageable_type: 'sites' 165 | }, { 166 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 167 | url: 'https://www.google.com/images/srpr/logo4w.png', 168 | imageable_id: 1, 169 | imageable_type: 'sites' 170 | }, { 171 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 172 | url: 'https://www.google.com/images/srpr/logo4w.png', 173 | imageable_id: 2, 174 | imageable_type: 'sites' 175 | }, { 176 | caption: 'Lorem ipsum Quis Ut eu nostrud ea sint aute non aliqua ut ullamco cupidatat exercitation nisi nisi.', 177 | url: 'https://www.google.com/images/srpr/logo4w.png', 178 | imageable_id: 2, 179 | imageable_type: 'sites' 180 | }]), 181 | 182 | knex('users').insert({uid: 1, username: 'root'}), 183 | 184 | knex('roles').insert({rid: 4, name: 'admin'}), 185 | 186 | knex('users_roles').insert({uid: 1, rid: 4}), 187 | 188 | knex('Customer').insert([ 189 | {id: 1, name: 'Customer1'}, 190 | {id: 2, name: 'Customer2'}, 191 | {id: 3, name: 'Customer3'}, 192 | {id: 4, name: 'Customer4'}]), 193 | 194 | knex('Settings').insert([ 195 | {id: 1, Customer_id: 1, data: 'Europe/Paris'}, 196 | {id: 2, Customer_id: 4, data: 'UTC'} 197 | ]), 198 | 199 | knex('hostnames').insert([ 200 | {hostname: 'google.com', instance_id: 3}, 201 | {hostname: 'apple.com', instance_id: 10}, 202 | ]), 203 | 204 | knex('instances').insert([ 205 | {id: 3, name: 'search engine'}, 206 | {id: 4, name: 'not used'}, 207 | {id: 10, name: 'computers'}, 208 | ]) 209 | 210 | ]).then(null, function(e) { 211 | console.log(e.stack); 212 | }); 213 | 214 | }; -------------------------------------------------------------------------------- /docs/dialects/base/relation.html: -------------------------------------------------------------------------------- 1 | dialects/base/relation
dialects/base/relation.js

Base Relation

(function(define) { 2 | 3 | "use strict"; 4 | 5 | define(function(require, exports) { 6 | 7 | var _ = require('underscore'); 8 | var Backbone = require('backbone'); 9 | 10 | var CollectionBase = require('./collection').CollectionBase;

Used internally, the Relation helps in simplifying the relationship building, 11 | centralizing all logic dealing with type & option handling.

var RelationBase = function(type, Target, options) { 12 | this.type = type; 13 | if (this.target = Target) { 14 | this.targetTableName = _.result(Target.prototype, 'tableName'); 15 | this.targetIdAttribute = _.result(Target.prototype, 'idAttribute'); 16 | } 17 | _.extend(this, options); 18 | }; 19 | 20 | RelationBase.prototype = {

Creates a new relation instance, used by the Eager relation in 21 | dealing with morphTo cases, where the same relation is targeting multiple models.

instance: function(type, Target, options) { 22 | return new this.constructor(type, Target, options); 23 | },

Creates a new model, used internally in the eager fetch helper methods.

createModel: function(data) { 24 | if (this.target.prototype instanceof CollectionBase) { 25 | return new this.target.prototype.model(data, {parse: true})._reset(); 26 | } 27 | return new this.target(data, {parse: true})._reset(); 28 | },

Eager pair the models.

eagerPair: function() {} 29 | 30 | }; 31 | 32 | RelationBase.extend = Backbone.Model.extend; 33 | 34 | exports.RelationBase = RelationBase; 35 | 36 | }); 37 | 38 | })( 39 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 40 | );
-------------------------------------------------------------------------------- /docs/plugins/exec.html: -------------------------------------------------------------------------------- 1 | plugins/exec
plugins/exec.js

Exec plugin

(function(define) { "use strict";

The exec plugin is used to optionally add 2 | support node-style callbacks, delegating to the promise 3 | method under the hood:

Bookshelf.plugin(require('bookshelf/plugins/exec'))

define(function(require, exports, module) { 4 | 5 | var _ = require('underscore');

Accept the instance of Bookshelf we'd like to add exec support to.

module.exports = function(Bookshelf) {

A method which is passed the target object and method we're 6 | looking to extend with the exec interface.

var wrapExec = function(target, method) { 7 | var targetMethod = target[method]; 8 | target[method] = function() { 9 | var result, args = arguments; 10 | var ctx = this; 11 | return {

The then method is essentially the same as it was before, 12 | just is not automatically called.

then: function(onFulfilled, onRejected) { 13 | result || (result = targetMethod.apply(ctx, args)); 14 | return result.then(onFulfilled, onRejected); 15 | },

A facade for the then method, throwing any uncaught errors 16 | rather than swallowing them.

exec: function(callback) { 17 | result || (result = targetMethod.apply(ctx, args)); 18 | return result.then(function(resp) { 19 | callback(null, resp); 20 | }, function(err) { 21 | callback(err, null); 22 | }).then(null, function(err) { 23 | setTimeout(function() { throw err; }, 0); 24 | }); 25 | } 26 | }; 27 | }; 28 | };

Wrap the appropriate methods on each object prototype, exposing the new API.

_.each(['load', 'fetch', 'save', 'destroy'], function(method) { 29 | wrapExec(Bookshelf.Model.prototype, method); 30 | }); 31 | _.each(['load', 'fetch'], function(method) { 32 | wrapExec(Bookshelf.Collection.prototype, method); 33 | }); 34 | }; 35 | 36 | }); 37 | 38 | })( 39 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } 40 | );
-------------------------------------------------------------------------------- /test/integration/relation.js: -------------------------------------------------------------------------------- 1 | var equal = require('assert').equal; 2 | 3 | module.exports = function(Bookshelf) { 4 | 5 | describe('Relation', function() { 6 | 7 | var Relation = require('../../dialects/sql/relation').Relation; 8 | 9 | var Photo = Bookshelf.Model.extend({ 10 | tableName: 'photos', 11 | imageable: function() { 12 | this.morphTo('imageable', Doctor, Patient); 13 | } 14 | }); 15 | 16 | var Doctor = Bookshelf.Model.extend({ 17 | tableName: 'doctors', 18 | photos: function() { 19 | return this.morphMany(Photo, 'imageable'); 20 | }, 21 | patients: function() { 22 | return this.belongsToMany(Patient).through(Appointment); 23 | }, 24 | patientsStd: function() { 25 | return this.belongsToMany(Patient); 26 | }, 27 | meta: function() { 28 | return this.hasOne(DoctorMeta, 'doctoring_id'); 29 | } 30 | }); 31 | 32 | var DoctorMeta = Bookshelf.Model.extend({ 33 | idAttribute: 'customId', 34 | tableName: 'doctormeta', 35 | doctor: function() { 36 | return this.belongsTo(Doctor, 'doctoring_id'); 37 | } 38 | }); 39 | 40 | var Patient = Bookshelf.Model.extend({ 41 | tableName: 'patients', 42 | doctors: function() { 43 | return this.belongsToMany(Doctor).through(Appointment); 44 | }, 45 | photos: function() { 46 | return this.morphMany(Photo, 'imageable'); 47 | } 48 | }); 49 | 50 | var Appointment = Bookshelf.Model.extend({ 51 | tableName: 'appointments', 52 | patient: function() { 53 | return this.belongsTo(Patient); 54 | }, 55 | doctor: function() { 56 | return this.belongsTo(Doctor); 57 | } 58 | }); 59 | 60 | var Supplier = Bookshelf.Model.extend({ 61 | tableName: 'suppliers', 62 | accountHistory: function() { 63 | return this.hasOne(AccountHistory).through(Account); 64 | } 65 | }); 66 | 67 | var Account = Bookshelf.Model.extend({ 68 | tableName: 'accounts' 69 | }); 70 | 71 | var AccountHistory = Bookshelf.Model.extend({ 72 | tableName: 'account_histories', 73 | supplier: function() { 74 | return this.belongsTo(Supplier).through(Account); 75 | } 76 | }); 77 | 78 | describe('Bookshelf.Relation', function() { 79 | 80 | it('should not error if the type / target are not specified', function() { 81 | 82 | var relation = new Relation(); 83 | equal(relation.type, undefined); 84 | 85 | }); 86 | 87 | it('should handle a hasOne relation', function() { 88 | 89 | var base = new Doctor({id: 1}); 90 | var relation = base.meta(); 91 | var _knex = relation.query(); 92 | var relatedData = relation.relatedData; 93 | 94 | // Base 95 | equal(relatedData.type, 'hasOne'); 96 | equal(relatedData.target, DoctorMeta); 97 | equal(relatedData.targetTableName, 'doctormeta'); 98 | equal(relatedData.targetIdAttribute, 'customId'); 99 | equal(relatedData.foreignKey, 'doctoring_id'); 100 | 101 | // Init 102 | equal(relatedData.parentId, 1); 103 | equal(relatedData.parentTableName, 'doctors'); 104 | equal(relatedData.parentIdAttribute, 'id'); 105 | equal(relatedData.parentFk, 1); 106 | 107 | // init the select constraints 108 | relatedData.selectConstraints(_knex, {}); 109 | 110 | equal(_knex.toString(), 'select `doctormeta`.* from `doctormeta` where `doctoring_id` = 1 limit 1'); 111 | }); 112 | 113 | it('should handle a hasOne -> through relation', function() { 114 | var base = new Supplier({id: 1}); 115 | var relation = base.accountHistory(); 116 | var _knex = relation.query(); 117 | var relatedData = relation.relatedData; 118 | 119 | // Base 120 | equal(relatedData.type, 'hasOne'); 121 | equal(relatedData.target, AccountHistory); 122 | equal(relatedData.targetTableName, 'account_histories'); 123 | equal(relatedData.targetIdAttribute, 'id'); 124 | equal(relatedData.foreignKey, undefined); 125 | 126 | // Init 127 | equal(relatedData.parentId, 1); 128 | equal(relatedData.parentTableName, 'suppliers'); 129 | equal(relatedData.parentIdAttribute, 'id'); 130 | equal(relatedData.parentFk, 1); 131 | 132 | // Through 133 | equal(relatedData.throughTarget, Account); 134 | equal(relatedData.throughTableName, 'accounts'); 135 | equal(relatedData.throughIdAttribute, 'id'); 136 | 137 | // init the select constraints 138 | relatedData.selectConstraints(_knex, {}); 139 | 140 | var sql = 'select `account_histories`.*, `accounts`.`id` as `_pivot_id`, `accounts`.`supplier_id` as `_pivot_supplier_id` from `account_histories` inner join `accounts` on `accounts`.`id` = `account_histories`.`account_id` where `accounts`.`supplier_id` = 1 limit 1'; 141 | 142 | equal(_knex.toString(), sql); 143 | }); 144 | 145 | it('should handle a belongsTo -> through relation', function() { 146 | var base = new AccountHistory({id: 1}); 147 | var relation = base.supplier(); 148 | var _knex = relation.query(); 149 | var relatedData = relation.relatedData; 150 | 151 | // Base 152 | equal(relatedData.type, 'belongsTo'); 153 | equal(relatedData.target, Supplier); 154 | equal(relatedData.targetTableName, 'suppliers'); 155 | equal(relatedData.targetIdAttribute, 'id'); 156 | equal(relatedData.foreignKey, 'supplier_id'); 157 | 158 | // Init 159 | equal(relatedData.parentId, 1); 160 | equal(relatedData.parentTableName, 'account_histories'); 161 | equal(relatedData.parentIdAttribute, 'id'); 162 | equal(relatedData.parentFk, 1); 163 | 164 | // Through 165 | equal(relatedData.throughTarget, Account); 166 | equal(relatedData.throughTableName, 'accounts'); 167 | equal(relatedData.throughIdAttribute, 'id'); 168 | 169 | // init the select constraints 170 | relatedData.selectConstraints(_knex, {}); 171 | 172 | var sql = 'select `suppliers`.*, `accounts`.`id` as `_pivot_id`, `accounts`.`supplier_id` as `_pivot_supplier_id` from `suppliers` inner join `accounts` on `accounts`.`supplier_id` = `suppliers`.`id` inner join `account_histories` on `accounts`.`id` = `account_histories`.`account_id` where `account_histories`.`id` = 1 limit 1'; 173 | 174 | equal(_knex.toString(), sql); 175 | }); 176 | 177 | it('should handle a belongsToMany -> through relation', function() { 178 | var base = new Doctor({id: 1}); 179 | var relation = base.patients(); 180 | var _knex = relation.query(); 181 | var relatedData = relation.relatedData; 182 | 183 | // Base 184 | equal(relatedData.type, 'belongsToMany'); 185 | equal(relatedData.target, Patient); 186 | equal(relatedData.targetTableName, 'patients'); 187 | equal(relatedData.targetIdAttribute, 'id'); 188 | equal(relatedData.foreignKey, undefined); 189 | 190 | // Init 191 | equal(relatedData.parentId, 1); 192 | equal(relatedData.parentTableName, 'doctors'); 193 | equal(relatedData.parentIdAttribute, 'id'); 194 | equal(relatedData.parentFk, 1); 195 | 196 | // Through 197 | equal(relatedData.throughTarget, Appointment); 198 | equal(relatedData.throughTableName, 'appointments'); 199 | equal(relatedData.throughIdAttribute, 'id'); 200 | 201 | // init the select constraints 202 | relatedData.selectConstraints(_knex, {}); 203 | 204 | var sql = 'select `patients`.*, `appointments`.`id` as `_pivot_id`, `appointments`.`doctor_id` as `_pivot_doctor_id`, `appointments`.`patient_id` as `_pivot_patient_id` from `patients` inner join `appointments` on `appointments`.`patient_id` = `patients`.`id` where `appointments`.`doctor_id` = 1'; 205 | 206 | equal(_knex.toString(), sql); 207 | }); 208 | 209 | it('should handle a standard belongsToMany relation', function() { 210 | var base = new Doctor({id: 1}); 211 | var relation = base.patientsStd(); 212 | var _knex = relation.query(); 213 | var relatedData = relation.relatedData; 214 | 215 | // Base 216 | equal(relatedData.type, 'belongsToMany'); 217 | equal(relatedData.target, Patient); 218 | equal(relatedData.targetTableName, 'patients'); 219 | equal(relatedData.targetIdAttribute, 'id'); 220 | equal(relatedData.foreignKey, undefined); 221 | 222 | // Init 223 | equal(relatedData.parentId, 1); 224 | equal(relatedData.parentTableName, 'doctors'); 225 | equal(relatedData.parentIdAttribute, 'id'); 226 | equal(relatedData.parentFk, 1); 227 | 228 | // init the select constraints 229 | relatedData.selectConstraints(_knex, {}); 230 | 231 | var sql = 'select `patients`.*, `doctors_patients`.`doctor_id` as `_pivot_doctor_id`, `doctors_patients`.`patient_id` as `_pivot_patient_id` from `patients` inner join `doctors_patients` on `doctors_patients`.`patient_id` = `patients`.`id` where `doctors_patients`.`doctor_id` = 1'; 232 | 233 | equal(_knex.toString(), sql); 234 | }); 235 | 236 | it('should handle polymorphic relations', function() { 237 | var base = new Doctor({id: 1}); 238 | var relation = base.photos(); 239 | var _knex = relation.query(); 240 | var relatedData = relation.relatedData; 241 | 242 | // Base 243 | equal(relatedData.type, 'morphMany'); 244 | equal(relatedData.target, Photo); 245 | equal(relatedData.targetTableName, 'photos'); 246 | equal(relatedData.targetIdAttribute, 'id'); 247 | equal(relatedData.foreignKey, undefined); 248 | 249 | // Init 250 | equal(relatedData.parentId, 1); 251 | equal(relatedData.parentTableName, 'doctors'); 252 | equal(relatedData.parentIdAttribute, 'id'); 253 | equal(relatedData.parentFk, 1); 254 | 255 | // init the select constraints 256 | relatedData.selectConstraints(_knex, {}); 257 | 258 | var sql = "select `photos`.* from `photos` where `imageable_id` = 1 and `imageable_type` = 'doctors'"; 259 | 260 | equal(_knex.toString(), sql); 261 | }); 262 | 263 | }); 264 | 265 | }); 266 | 267 | }; -------------------------------------------------------------------------------- /dialects/sql/model.js: -------------------------------------------------------------------------------- 1 | // Model 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var when = require('when'); 11 | 12 | var Sync = require('./sync').Sync; 13 | var Helpers = require('./helpers').Helpers; 14 | var EagerRelation = require('./eager').EagerRelation; 15 | 16 | var ModelBase = require('../base/model').ModelBase; 17 | 18 | exports.Model = ModelBase.extend({ 19 | 20 | // The `hasOne` relation specifies that this table has exactly one of another type of object, 21 | // specified by a foreign key in the other table. The foreign key is assumed to be the singular of this 22 | // object's `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified. 23 | hasOne: function(Target, foreignKey) { 24 | return this._relation('hasOne', Target, {foreignKey: foreignKey}).init(this); 25 | }, 26 | 27 | // The `hasMany` relation specifies that this object has one or more rows in another table which 28 | // match on this object's primary key. The foreign key is assumed to be the singular of this object's 29 | // `tableName` with an `_id` suffix, but a custom `foreignKey` attribute may also be specified. 30 | hasMany: function(Target, foreignKey) { 31 | return this._relation('hasMany', Target, {foreignKey: foreignKey}).init(this); 32 | }, 33 | 34 | // A reverse `hasOne` relation, the `belongsTo`, where the specified key in this table 35 | // matches the primary `idAttribute` of another table. 36 | belongsTo: function(Target, foreignKey) { 37 | return this._relation('belongsTo', Target, {foreignKey: foreignKey}).init(this); 38 | }, 39 | 40 | // A `belongsToMany` relation is when there are many-to-many relation 41 | // between two models, with a joining table. 42 | belongsToMany: function(Target, joinTableName, foreignKey, otherKey) { 43 | return this._relation('belongsToMany', Target, { 44 | joinTableName: joinTableName, foreignKey: foreignKey, otherKey: otherKey 45 | }).init(this); 46 | }, 47 | 48 | // A `morphOne` relation is a one-to-one polymorphic association from this model 49 | // to another model. 50 | morphOne: function(Target, name, morphValue) { 51 | return this._morphOneOrMany(Target, name, morphValue, 'morphOne'); 52 | }, 53 | 54 | // A `morphMany` relation is a polymorphic many-to-one relation from this model 55 | // to many another models. 56 | morphMany: function(Target, name, morphValue) { 57 | return this._morphOneOrMany(Target, name, morphValue, 'morphMany'); 58 | }, 59 | 60 | // Defines the opposite end of a `morphOne` or `morphMany` relationship, where 61 | // the alternate end of the polymorphic model is defined. 62 | morphTo: function(morphName) { 63 | if (!_.isString(morphName)) throw new Error('The `morphTo` name must be specified.'); 64 | return this._relation('morphTo', null, {morphName: morphName, candidates: _.rest(arguments)}).init(this); 65 | }, 66 | 67 | // Used to define passthrough relationships - `hasOne`, `hasMany`, 68 | // `belongsTo` or `belongsToMany`, "through" a `Interim` model or collection. 69 | through: function(Interim, foreignKey, otherKey) { 70 | return this.relatedData.through(this, Interim, {throughForeignKey: foreignKey, otherKey: otherKey}); 71 | }, 72 | 73 | // Fetch a model based on the currently set attributes, 74 | // returning a model to the callback, along with any options. 75 | // Returns a deferred promise through the `Bookshelf.Sync`. 76 | // If `{require: true}` is set as an option, the fetch is considered 77 | // a failure if the model comes up blank. 78 | fetch: function(options) { 79 | options = options || {}; 80 | var model = this; 81 | 82 | // Run the `first` call on the `sync` object to fetch a single model. 83 | var sync = this.sync(options).first() 84 | 85 | // Jump the rest of the chain if the response doesn't exist... 86 | .tap(function(response) { 87 | if (!response || response.length === 0) { 88 | if (options.require) throw new Error('EmptyResponse'); 89 | return when.reject(null); 90 | } 91 | }) 92 | 93 | // Now, load all of the data into the model as necessary. 94 | .tap(this._handleResponse); 95 | 96 | // If the "withRelated" is specified, we also need to eager load all of the 97 | // data on the model, as a side-effect, before we ultimately jump into the 98 | // next step of the model. Since the `columns` are only relevant to the current 99 | // level, ensure those are omitted from the options. 100 | if (options.withRelated) { 101 | sync = sync.tap(this._handleEager(_.omit(options, 'columns'))); 102 | } 103 | 104 | return sync.tap(function(response) { 105 | return model.triggerThen('fetched', model, response, options); 106 | }) 107 | .yield(model) 108 | .otherwise(function(err) { 109 | if (err === null) return err; 110 | throw err; 111 | }); 112 | }, 113 | 114 | // Eager loads relationships onto an already populated `Model` instance. 115 | load: function(relations, options) { 116 | _.isArray(relations) || (relations = [relations]); 117 | var handler = this._handleEager(_.extend({}, options, {shallow: true, withRelated: relations})); 118 | return handler([this.toJSON({shallow: true})]).yield(this); 119 | }, 120 | 121 | // Sets and saves the hash of model attributes, triggering 122 | // a "creating" or "updating" event on the model, as well as a "saving" event, 123 | // to bind listeners for any necessary validation, logging, etc. 124 | // If an error is thrown during these events, the model will not be saved. 125 | save: function(key, val, options) { 126 | var attrs; 127 | 128 | // Handle both `"key", value` and `{key: value}` -style arguments. 129 | if (key == null || typeof key === "object") { 130 | attrs = key || {}; 131 | options = val || {}; 132 | } else { 133 | options || (options = {}); 134 | (attrs = {})[key] = val; 135 | } 136 | 137 | // If the model has timestamp columns, 138 | // set them as attributes on the model, even 139 | // if the "patch" option is specified. 140 | if (this.hasTimestamps) _.extend(attrs, this.timestamp(options)); 141 | 142 | // Determine whether the model is new, based on whether the model has an `idAttribute` or not. 143 | var method = options.method || (options.method = this.isNew(options) ? 'insert' : 'update'); 144 | var vals = attrs; 145 | 146 | // If the object is being created, we merge any defaults here 147 | // rather than during object creation. 148 | if (method === 'insert' || options.defaults) { 149 | var defaults = _.result(this, 'defaults'); 150 | if (defaults) { 151 | vals = _.extend({}, defaults, this.attributes, vals); 152 | } 153 | } 154 | 155 | // Set the attributes on the model, and maintain a reference to use below. 156 | var model = this.set(vals, {silent: true}); 157 | 158 | // If there are any save constraints, set them on the model. 159 | if (this.relatedData) this.relatedData.saveConstraints(this); 160 | 161 | var sync = this.sync(options); 162 | 163 | // Gives access to the `query` object in the `options`, in case we need it. 164 | options.query = sync.query; 165 | 166 | return when.all([ 167 | model.triggerThen((method === 'insert' ? 'creating' : 'updating'), model, attrs, options), 168 | model.triggerThen('saving', model, attrs, options) 169 | ]) 170 | .then(function() { 171 | return sync[options.method](method === 'update' && options.patch ? attrs : model.attributes); 172 | }) 173 | .then(function(resp) { 174 | 175 | // After a successful database save, the id is updated if the model was created 176 | if (method === 'insert' && resp) { 177 | model.attributes[model.idAttribute] = model[model.idAttribute] = resp[0]; 178 | } 179 | 180 | // In case we need to reference the `previousAttributes` for the model 181 | // in the following event handlers. 182 | options.previousAttributes = model._previousAttributes; 183 | 184 | model._reset(); 185 | 186 | return when.all([ 187 | model.triggerThen((method === 'insert' ? 'created' : 'updated'), model, resp, options), 188 | model.triggerThen('saved', model, resp, options) 189 | ]); 190 | 191 | }).yield(this); 192 | }, 193 | 194 | // Reset the query builder, called internally 195 | // each time a query is run. 196 | resetQuery: function() { 197 | this._knex = null; 198 | return this; 199 | }, 200 | 201 | // Returns an instance of the query builder. 202 | query: function() { 203 | return Helpers.query(this, _.toArray(arguments)); 204 | }, 205 | 206 | // Creates and returns a new `Sync` instance. 207 | sync: function(options) { 208 | return new Sync(this, options); 209 | }, 210 | 211 | // Helper for setting up the `morphOne` or `morphMany` relations. 212 | _morphOneOrMany: function(Target, morphName, morphValue, type) { 213 | if (!morphName || !Target) throw new Error('The polymorphic `name` and `Target` are required.'); 214 | return this._relation(type, Target, {morphName: morphName, morphValue: morphValue}).init(this); 215 | }, 216 | 217 | // Handles the response data for the model, returning from the model's fetch call. 218 | // Todo: {silent: true, parse: true}, for parity with collection#set 219 | // need to check on Backbone's status there, ticket #2636 220 | _handleResponse: function(response) { 221 | var relatedData = this.relatedData; 222 | this.set(this.parse(response[0]), {silent: true})._reset(); 223 | if (relatedData && relatedData.isJoined()) { 224 | relatedData.parsePivot([this]); 225 | } 226 | }, 227 | 228 | // Handle the related data loading on the model. 229 | _handleEager: function(options) { 230 | var model = this; 231 | return function(response) { 232 | return new EagerRelation([model], response, model).fetch(options); 233 | }; 234 | } 235 | 236 | }); 237 | 238 | }); 239 | 240 | })( 241 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 242 | ); -------------------------------------------------------------------------------- /docs/dialects/sql/helpers.html: -------------------------------------------------------------------------------- 1 | dialects/sql/helpers
dialects/sql/helpers.js

Helpers

(function(define) { 2 | 3 | "use strict"; 4 | 5 | define(function(require, exports) { 6 | 7 | var _ = require('underscore'); 8 | 9 | exports.Helpers = {

Sets the constraints necessary during a model.save call.

saveConstraints: function(model, relatedData) { 10 | var data = {}; 11 | if (relatedData && relatedData.type && relatedData.type !== 'belongsToMany') { 12 | data[relatedData.key('foreignKey')] = relatedData.parentFk; 13 | if (relatedData.isMorph()) data[relatedData.key('morphKey')] = relatedData.key('morphValue'); 14 | } 15 | return model.set(data); 16 | },

Finds the specific morphTo table we should be working with, or throws 17 | an error if none is matched.

morphCandidate: function(candidates, foreignTable) { 18 | var Target = _.find(candidates, function(Candidate) { 19 | return (_.result(Candidate.prototype, 'tableName') === foreignTable); 20 | }); 21 | if (!Target) { 22 | throw new Error('The target polymorphic model was not found'); 23 | } 24 | return Target; 25 | },

If there are no arguments, return the current object's 26 | query builder (or create and return a new one). If there are arguments, 27 | call the query builder with the first argument, applying the rest. 28 | If the first argument is an object, assume the keys are query builder 29 | methods, and the values are the arguments for the query.

query: function(obj, args) { 30 | obj._knex = obj._knex || obj._builder(_.result(obj, 'tableName')); 31 | if (args.length === 0) return obj._knex; 32 | var method = args[0]; 33 | if (_.isFunction(method)) { 34 | method.call(obj._knex, obj._knex); 35 | } else if (_.isObject(method)) { 36 | for (var key in method) { 37 | var target = _.isArray(method[key]) ? method[key] : [method[key]]; 38 | obj._knex[key].apply(obj._knex, target); 39 | } 40 | } else { 41 | obj._knex[method].apply(obj._knex, args.slice(1)); 42 | } 43 | return obj; 44 | } 45 | 46 | }; 47 | 48 | }); 49 | 50 | })( 51 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 52 | );
-------------------------------------------------------------------------------- /test/integration/relations.js: -------------------------------------------------------------------------------- 1 | var when = require('when'); 2 | var equal = require('assert').equal; 3 | 4 | module.exports = function(Bookshelf) { 5 | 6 | describe('Relations', function() { 7 | 8 | var objs = require('./helpers/objects')(Bookshelf); 9 | var Relation = objs.Relation; 10 | var Models = objs.Models; 11 | var Collections = objs.Collections; 12 | 13 | // Models 14 | var Site = Models.Site; 15 | var SiteMeta = Models.SiteMeta; 16 | var Admin = Models.Admin; 17 | var Author = Models.Author; 18 | var Blog = Models.Blog; 19 | var Post = Models.Post; 20 | var Comment = Models.Comment; 21 | var Tag = Models.Tag; 22 | var User = Models.User; 23 | var Role = Models.Role; 24 | var Photo = Models.Photo; 25 | var Customer = Models.Customer; 26 | var Instance = Models.Instance; 27 | var Hostname = Models.Hostname; 28 | 29 | // Collections 30 | var Sites = Collections.Sites; 31 | var Admins = Collections.Admins; 32 | var Blogs = Collections.Blogs; 33 | var Posts = Collections.Posts; 34 | var Comments = Collections.Comment; 35 | var Photos = Collections.Photos; 36 | var Authors = Collections.Authors; 37 | 38 | describe('Bookshelf Relations', function() { 39 | 40 | describe('Standard Relations - Models', function() { 41 | 42 | it('handles belongsTo (blog, site)', function() { 43 | return new Blog({id: 4}) 44 | .fetch() 45 | .then(function(model) { 46 | return model.site().fetch({log: true}); 47 | }); 48 | }); 49 | 50 | it('handles hasMany (posts)', function() { 51 | return new Blog({id: 1}) 52 | .fetch() 53 | .then(function(model) { 54 | return model.posts().fetch({log: true}); 55 | }); 56 | }); 57 | 58 | it('handles hasOne (meta)', function() { 59 | return new Site({id: 1}) 60 | .meta() 61 | .fetch({log: true}); 62 | }); 63 | 64 | it('handles belongsToMany (posts)', function() { 65 | return new Author({id: 1}) 66 | .posts() 67 | .fetch({log: true}); 68 | }); 69 | 70 | }); 71 | 72 | describe('Eager Loading - Models', function() { 73 | 74 | it('eager loads "hasOne" relationships correctly (site -> meta)', function() { 75 | return new Site({id: 1}).fetch({ 76 | log: true, 77 | withRelated: ['meta'] 78 | }); 79 | }); 80 | 81 | it('eager loads "hasMany" relationships correctly (site -> authors, blogs)', function() { 82 | return new Site({id: 1}).fetch({ 83 | log: true, 84 | withRelated: ['authors', 'blogs'] 85 | }); 86 | }); 87 | 88 | it('eager loads "belongsTo" relationships correctly (blog -> site)', function() { 89 | return new Blog({id: 3}).fetch({ 90 | log: true, 91 | withRelated: ['site'] 92 | }); 93 | }); 94 | 95 | // it('Throws an error if you try to fetch a related object without the necessary key', function() { 96 | // return new Blog({id: 1}).site().fetch().should.be.rejected; 97 | // }); 98 | 99 | it('eager loads "belongsToMany" models correctly (post -> tags)', function() { 100 | return new Post({id: 1}).fetch({ 101 | log: true, 102 | withRelated: ['tags'] 103 | }); 104 | }); 105 | 106 | it('Attaches an empty related model or collection if the `EagerRelation` comes back blank', function() { 107 | return new Site({id: 3}).fetch({ 108 | log: true, 109 | withRelated: ['meta', 'blogs', 'authors.posts'] 110 | }); 111 | }); 112 | 113 | }); 114 | 115 | describe('Eager Loading - Collections', function() { 116 | 117 | it('eager loads "hasOne" models correctly (sites -> meta)', function() { 118 | return new Sites().fetch({ 119 | log: true, 120 | withRelated: ['meta'] 121 | }); 122 | }); 123 | 124 | it('eager loads "belongsTo" models correctly (blogs -> site)', function() { 125 | return new Blogs().fetch({ 126 | log: true, 127 | withRelated: ['site'] 128 | }); 129 | }); 130 | 131 | it('eager loads "hasMany" models correctly (site -> blogs)', function() { 132 | return new Site({id: 1}).fetch({ 133 | log: true, 134 | withRelated: ['blogs'] 135 | }); 136 | }); 137 | 138 | it('eager loads "belongsToMany" models correctly (posts -> tags)', function() { 139 | return new Posts() 140 | .query('where', 'blog_id', '=', 1) 141 | .fetch({ 142 | log: true, 143 | withRelated: ['tags'] 144 | }); 145 | }); 146 | 147 | }); 148 | 149 | describe('Nested Eager Loading - Models', function() { 150 | 151 | it('eager loads "hasMany" -> "hasMany" (site -> authors.ownPosts)', function() { 152 | return new Site({id: 1}).fetch({ 153 | log: true, 154 | withRelated: ['authors.ownPosts'] 155 | }); 156 | }); 157 | 158 | it('eager loads "hasMany" -> "belongsToMany" (site -> authors.posts)', function() { 159 | return new Site({id: 1}).fetch({ 160 | log: true, 161 | withRelated: ['authors.posts'] 162 | }); 163 | }); 164 | 165 | it('does multi deep eager loads (site -> authors.ownPosts, authors.site, blogs.posts)', function() { 166 | return new Site({id: 1}).fetch({ 167 | log: true, 168 | withRelated: ['authors.ownPosts', 'authors.site', 'blogs.posts'] 169 | }); 170 | }); 171 | 172 | }); 173 | 174 | describe('Nested Eager Loading - Collections', function() { 175 | 176 | it('eager loads "hasMany" -> "hasMany" (sites -> authors.ownPosts)', function() { 177 | return new Sites().fetch({ 178 | log: true, 179 | withRelated: ['authors.ownPosts'] 180 | }); 181 | }); 182 | 183 | }); 184 | 185 | describe('Model & Collection - load', function() { 186 | 187 | it('eager loads relations on a populated model (site -> blogs, authors.site)', function() { 188 | return new Site({id: 1}).fetch({log: true}).then(function(m) { 189 | return m.load(['blogs', 'authors.site']); 190 | }); 191 | }); 192 | 193 | it('eager loads attributes on a collection (sites -> blogs, authors.site)', function() { 194 | return new Sites().fetch({log: true}).then(function(c) { 195 | return c.load(['blogs', 'authors.site']); 196 | }); 197 | }); 198 | }); 199 | 200 | describe('Pivot Tables', function() { 201 | 202 | before(function() { 203 | return when.all([ 204 | new Site({id: 1}).admins().detach(), 205 | new Site({id: 2}).admins().detach() 206 | ]); 207 | }); 208 | 209 | it('provides "attach" for creating or attaching records', function() { 210 | 211 | var site1 = new Site({id: 1}); 212 | var site2 = new Site({id: 2}); 213 | var admin1 = new Admin({username: 'syncable', password: 'test'}); 214 | var admin2 = new Admin({username: 'syncable', password: 'test'}); 215 | var admin1_id; 216 | 217 | return when.all([admin1.save(), admin2.save()]) 218 | .then(function() { 219 | admin1_id = admin1.id; 220 | return when.all([ 221 | site1.related('admins').attach([admin1, admin2]), 222 | site2.related('admins').attach(admin2) 223 | ]); 224 | }) 225 | .then(function(resp) { 226 | expect(site1.related('admins')).to.have.length(2); 227 | expect(site2.related('admins')).to.have.length(1); 228 | }).then(function() { 229 | return when.all([ 230 | new Site({id: 1}).related('admins').fetch().then(function(c) { 231 | c.each(function(m) { 232 | equal(m.hasChanged(), false); 233 | }); 234 | equal(c.at(0).pivot.get('item'), 'test'); 235 | equal(c.length, 2); 236 | }), 237 | new Site({id: 2}).related('admins').fetch().then(function(c) { 238 | equal(c.length, 1); 239 | }) 240 | ]); 241 | }) 242 | .then(function(resp) { 243 | return when.all([ 244 | new Site({id: 1}).related('admins').fetch(), 245 | new Site({id: 2}).related('admins').fetch() 246 | ]); 247 | }) 248 | .spread(function(admins1, admins2) { 249 | return when.all([ 250 | admins1.detach(admin1_id).then(function(c) { 251 | expect(admins1).to.have.length(1); 252 | return c.fetch(); 253 | }).then(function(c) { 254 | equal(c.length, 1); 255 | }), 256 | admins2.detach().then(function(c) { 257 | expect(admins2).to.have.length(0); 258 | return c.fetch(); 259 | }).then(function(c) { 260 | equal(c.length, 0); 261 | }) 262 | ]); 263 | }); 264 | }); 265 | 266 | }); 267 | 268 | describe('Custom foreignKey & otherKey', function() { 269 | 270 | it('works with many-to-many (user -> roles)', function() { 271 | return new User({uid: 1}) 272 | .roles() 273 | .fetch({log: true}); 274 | }); 275 | 276 | it('works with eager loaded many-to-many (user -> roles)', function() { 277 | return new User({uid: 1}) 278 | .fetch({log: true, withRelated: ['roles']}); 279 | }); 280 | 281 | }); 282 | 283 | describe('Polymorphic associations', function() { 284 | 285 | it('handles morphOne (photo)', function() { 286 | return new Author({id: 1}) 287 | .photo() 288 | .fetch({log: true}); 289 | }); 290 | 291 | it('handles morphMany (photo)', function() { 292 | return new Site({id: 1}) 293 | .photos() 294 | .fetch({log: true}); 295 | }); 296 | 297 | it('handles morphTo (imageable "authors")', function() { 298 | return new Photo({imageable_id: 1, imageable_type: 'authors'}) 299 | .imageable() 300 | .fetch({log: true}); 301 | }); 302 | 303 | it('handles morphTo (imageable "sites")', function() { 304 | return new Photo({imageable_id: 1, imageable_type: 'sites'}) 305 | .imageable() 306 | .fetch({log: true}); 307 | }); 308 | 309 | it('eager loads morphMany (sites -> photos)', function() { 310 | return new Sites().fetch({log: true, withRelated: ['photos']}); 311 | }); 312 | 313 | it('eager loads morphTo (photos -> imageable)', function() { 314 | return new Photos().fetch({log: true, withRelated: ['imageable']}); 315 | }); 316 | 317 | it('eager loads beyond the morphTo, where possible', function() { 318 | return new Photos().fetch({log: true, withRelated: ['imageable.authors']}); 319 | }); 320 | 321 | }); 322 | 323 | describe('`through` relations', function() { 324 | 325 | it('handles hasMany `through`', function() { 326 | return new Blog({id: 1}).comments().fetch({log: true}); 327 | }); 328 | 329 | it('eager loads hasMany `through`', function() { 330 | return new Blogs().query({where: {site_id: 1}}).fetch({ 331 | log: true, 332 | withRelated: 'comments' 333 | }); 334 | }); 335 | 336 | it('handles hasOne `through`', function() { 337 | return new Site({id: 1}).info().fetch({log: true}); 338 | }); 339 | 340 | it('eager loads hasOne `through`', function() { 341 | return new Sites().query('where', 'id', '<', 3).fetch({ 342 | log: true, 343 | withRelated: 'info' 344 | }); 345 | }); 346 | 347 | it('eager loads belongsToMany `through`', function() { 348 | return new Authors().fetch({log: true, withRelated: 'blogs'}); 349 | }); 350 | 351 | }); 352 | 353 | }); 354 | 355 | describe('Issue #63 - hasOne relations', function() { 356 | 357 | it('should return Customer (id=1) with settings', function () { 358 | 359 | var expected = { 360 | id : 1, 361 | name : 'Customer1', 362 | settings: { 363 | id : 1, 364 | Customer_id : 1, 365 | data : 'Europe/Paris' 366 | } 367 | }; 368 | 369 | return new Customer({ id: 1 }) 370 | .fetch({ withRelated: 'settings' }) 371 | .then(function (model) { 372 | var cust = model.toJSON(); 373 | expect(cust).to.eql(expected); 374 | }); 375 | }); 376 | 377 | it('should return Customer (id=4) with settings', function () { 378 | 379 | var expected = { 380 | id : 4, 381 | name : 'Customer4', 382 | settings: { 383 | id : 2, 384 | Customer_id : 4, 385 | data : 'UTC' 386 | } 387 | }; 388 | 389 | return new Customer({ id: 4 }) 390 | .fetch({ withRelated: 'settings' }) 391 | .then(function (model) { 392 | var cust = model.toJSON(); 393 | expect(cust).to.eql(expected); 394 | }); 395 | }); 396 | }); 397 | 398 | describe('Issue #65, custom idAttribute with eager loaded belongsTo', function() { 399 | 400 | it('#65 - should eager load correctly for models', function() { 401 | 402 | return new Hostname({hostname: 'google.com'}).fetch({log: true, withRelated: 'instance'}); 403 | 404 | }); 405 | 406 | it('#65 - should eager load correctly for collections', function() { 407 | 408 | return new Bookshelf.Collection([], {model: Hostname}).fetch({log: true, withRelated: 'instance'}); 409 | 410 | }); 411 | 412 | }); 413 | 414 | describe('Issue #70 - fetching specific columns, and relations', function() { 415 | 416 | it('doesnt pass the columns along to sub-queries', function() { 417 | 418 | return new Author({id: 2}) 419 | .fetch({ 420 | withRelated: 'posts', 421 | columns: ['id', 'last_name'] 422 | }) 423 | .then(function(author) { 424 | 425 | expect(author.attributes.first_name).to.be.undefined; 426 | 427 | expect(author.related('posts').length).to.equal(2); 428 | 429 | }); 430 | 431 | }); 432 | 433 | }); 434 | 435 | describe('Issue #77 - Using collection.create() on relations', function() { 436 | 437 | it('maintains the correct parent model references when using related()', function() { 438 | 439 | return new Site().fetch({withRelated: 'authors'}).then(function(site) { 440 | 441 | return site.related('authors').create({first_name: 'Dummy', last_name: 'Account'}).then(function(model) { 442 | 443 | expect(model.attributes).to.eql({first_name: 'Dummy', last_name: 'Account', site_id: site.id, id: model.id}); 444 | 445 | expect(site.related('authors')).to.have.length(3); 446 | 447 | }); 448 | 449 | }); 450 | 451 | }); 452 | 453 | }); 454 | 455 | 456 | }); 457 | 458 | }; -------------------------------------------------------------------------------- /docs/bookshelf.html: -------------------------------------------------------------------------------- 1 | bookshelf
bookshelf.js

Bookshelf.js 0.5.7

(c) 2013 Tim Griesser
 2 | Bookshelf may be freely distributed under the MIT license.
 3 | For all details and documentation:
 4 | http://bookshelfjs.org
 5 | 
(function(define) { 6 | 7 | "use strict"; 8 | 9 | define(function(require, exports, module) {

All external libraries needed in this scope.

var _ = require('underscore'); 10 | var Knex = require('knex');

All local dependencies... These are the main objects that 11 | need to be augmented in the constructor to work properly.

var SqlModel = require('./dialects/sql/model').Model; 12 | var SqlCollection = require('./dialects/sql/collection').Collection; 13 | var SqlRelation = require('./dialects/sql/relation').Relation;

Finally, the Events, which we've supplemented with a triggerThen 14 | method to allow for asynchronous event handling via promises. We also 15 | mix this into the prototypes of the main objects in the library.

var Events = require('./dialects/base/events').Events;

Constructor for a new Bookshelf object, it accepts 16 | an active knex instance and initializes the appropriate 17 | Model and Collection constructors for use in the current instance.

var Bookshelf = function(knex) {

Allows you to construct the library with either Bookshelf(opts) 18 | or new Bookshelf(opts).

if (!(this instanceof Bookshelf)) { 19 | return new Bookshelf(knex); 20 | }

If the knex isn't a Knex instance, we'll assume it's 21 | a compatible config object and pass it through to create a new instance.

if (!knex.client || !(knex.client instanceof Knex.ClientBase)) { 22 | knex = new Knex(knex); 23 | }

The Model constructor is referenced as a property on the Bookshelf instance, 24 | mixing in the correct builder method, as well as the relation method, 25 | passing in the correct Model & Collection constructors for later reference.

var ModelCtor = this.Model = SqlModel.extend({ 26 | _builder: function(tableName) { 27 | return knex(tableName); 28 | }, 29 | _relation: function(type, Target, options) { 30 | return new Relation(type, Target, options); 31 | } 32 | });

The collection also references the correct Model, specified above, for creating 33 | new Model instances in the collection. We also extend with the correct builder / 34 | knex combo.

var CollectionCtor = this.Collection = SqlCollection.extend({ 35 | model: ModelCtor, 36 | _builder: function(tableName) { 37 | return knex(tableName); 38 | } 39 | });

Used internally, the Relation helps in simplifying the relationship building, 40 | centralizing all logic dealing with type & option handling.

var Relation = Bookshelf.Relation = SqlRelation.extend({ 41 | Model: ModelCtor, 42 | Collection: CollectionCtor 43 | });

Grab a reference to the knex instance passed (or created) in this constructor, 44 | for convenience.

this.knex = knex; 45 | };

A Bookshelf instance may be used as a top-level pub-sub bus, as it mixes in the 46 | Events object. It also contains the version number, and a Transaction method 47 | referencing the correct version of knex passed into the object.

_.extend(Bookshelf.prototype, Events, {

Keep in sync with package.json.

VERSION: '0.5.7',

Helper method to wrap a series of Bookshelf actions in a knex transaction block;

transaction: function() { 48 | return this.knex.transaction.apply(this, arguments); 49 | },

Provides a nice, tested, standardized way of adding plugins to a Bookshelf instance, 50 | injecting the current instance into the plugin, which should be a module.exports.

plugin: function(plugin) { 51 | plugin(this); 52 | return this; 53 | } 54 | 55 | });

Alias to new Bookshelf(opts).

Bookshelf.initialize = function(knex) { 56 | return new this(knex); 57 | };

The forge function properly instantiates a new Model or Collection 58 | without needing the new operator... to make object creation cleaner 59 | and more chainable.

SqlModel.forge = SqlCollection.forge = function() { 60 | var inst = Object.create(this.prototype); 61 | var obj = this.apply(inst, arguments); 62 | return (Object(obj) === obj ? obj : inst); 63 | };

Finally, export Bookshelf to the world.

module.exports = Bookshelf; 64 | 65 | }); 66 | 67 | })( 68 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports, module); } 69 | );
-------------------------------------------------------------------------------- /dialects/sql/relation.js: -------------------------------------------------------------------------------- 1 | // Relation 2 | // --------------- 3 | (function(define) { 4 | 5 | "use strict"; 6 | 7 | define(function(require, exports) { 8 | 9 | var _ = require('underscore'); 10 | var when = require('when'); 11 | var inflection = require('inflection'); 12 | 13 | var Helpers = require('./helpers').Helpers; 14 | 15 | var ModelBase = require('../base/model').ModelBase; 16 | var RelationBase = require('../base/relation').RelationBase; 17 | 18 | var push = [].push; 19 | 20 | exports.Relation = RelationBase.extend({ 21 | 22 | // Assembles the new model or collection we're creating an instance of, 23 | // gathering any relevant primitives from the parent object, 24 | // without keeping any hard references. 25 | init: function(parent) { 26 | this.parentId = parent.id; 27 | this.parentTableName = _.result(parent, 'tableName'); 28 | this.parentIdAttribute = _.result(parent, 'idAttribute'); 29 | 30 | if (this.isInverse()) { 31 | // If the parent object is eager loading, and it's a polymorphic `morphTo` relation, 32 | // we can't know what the target will be until the models are sorted and matched. 33 | if (this.type === 'morphTo' && !parent._isEager) { 34 | this.target = Helpers.morphCandidate(this.candidates, parent.get(this.key('morphKey'))); 35 | this.targetTableName = _.result(this.target.prototype, 'tableName'); 36 | this.targetIdAttribute = _.result(this.target.prototype, 'idAttribute'); 37 | } 38 | this.parentFk = parent.get(this.key('foreignKey')); 39 | } else { 40 | this.parentFk = parent.id; 41 | } 42 | 43 | var target = this.target ? this.relatedInstance() : {}; 44 | target.relatedData = this; 45 | 46 | if (this.type === 'belongsToMany') { 47 | _.extend(target, pivotHelpers); 48 | } 49 | 50 | return target; 51 | }, 52 | 53 | // Initializes a `through` relation, setting the `Target` model and `options`, 54 | // which includes any additional keys for the relation. 55 | through: function(source, Target, options) { 56 | var type = this.type; 57 | if (type !== 'hasOne' && type !== 'hasMany' && type !== 'belongsToMany' && type !== 'belongsTo') { 58 | throw new Error('`through` is only chainable from `hasOne`, `belongsTo`, `hasMany`, or `belongsToMany`'); 59 | } 60 | 61 | this.throughTarget = Target; 62 | this.throughTableName = _.result(Target.prototype, 'tableName'); 63 | this.throughIdAttribute = _.result(Target.prototype, 'idAttribute'); 64 | 65 | // Set the parentFk as appropriate now. 66 | if (this.type === 'belongsTo') { 67 | this.parentFk = this.parentId; 68 | } 69 | 70 | _.extend(this, options); 71 | _.extend(source, pivotHelpers); 72 | 73 | // Set the appropriate foreign key if we're doing a belongsToMany, for convenience. 74 | if (this.type === 'belongsToMany') { 75 | this.foreignKey = this.throughForeignKey; 76 | } 77 | 78 | return source; 79 | }, 80 | 81 | // Generates and returns a specified key, for convenience... one of 82 | // `foreignKey`, `otherKey`, `throughForeignKey`. 83 | key: function(keyName) { 84 | if (this[keyName]) return this[keyName]; 85 | if (keyName === 'otherKey') { 86 | return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; 87 | } 88 | if (keyName === 'throughForeignKey') { 89 | return this[keyName] = singularMemo(this.joinTable()) + '_' + this.throughIdAttribute; 90 | } 91 | if (keyName === 'foreignKey') { 92 | if (this.type === 'morphTo') return this[keyName] = this.morphName + '_id'; 93 | if (this.type === 'belongsTo') return this[keyName] = singularMemo(this.targetTableName) + '_' + this.targetIdAttribute; 94 | if (this.isMorph()) return this[keyName] = this.morphName + '_id'; 95 | return this[keyName] = singularMemo(this.parentTableName) + '_' + this.parentIdAttribute; 96 | } 97 | if (keyName === 'morphKey') return this[keyName] = this.morphName + '_type'; 98 | if (keyName === 'morphValue') return this[keyName] = this.parentTableName || this.targetTableName; 99 | }, 100 | 101 | // Injects the necessary `select` constraints into a `knex` query builder. 102 | selectConstraints: function(knex, options) { 103 | var resp = options.parentResponse; 104 | 105 | // The base select column 106 | if (knex.columns.length === 0 && (!options.columns || options.columns.length === 0)) { 107 | knex.columns.push(this.targetTableName + '.*'); 108 | } else if (_.isArray(options.columns) && options.columns.length > 0) { 109 | push.apply(knex.columns, options.columns); 110 | } 111 | 112 | // The `belongsToMany` and `through` relations have joins & pivot columns. 113 | if (this.isJoined()) { 114 | this.joinClauses(knex); 115 | this.joinColumns(knex); 116 | } 117 | 118 | // If this is a single relation and we're not eager loading, 119 | // limit the query to a single item. 120 | if (this.isSingle() && !resp) knex.limit(1); 121 | 122 | // Finally, add (and validate) the where conditions, necessary for constraining the relation. 123 | this.whereClauses(knex, resp); 124 | }, 125 | 126 | // Inject & validates necessary `through` constraints for the current model. 127 | joinColumns: function(knex) { 128 | var columns = []; 129 | var joinTable = this.joinTable(); 130 | if (this.isThrough()) columns.push(this.throughIdAttribute); 131 | columns.push(this.key('foreignKey')); 132 | if (this.type === 'belongsToMany') columns.push(this.key('otherKey')); 133 | push.apply(columns, this.pivotColumns); 134 | push.apply(knex.columns, _.map(columns, function(col) { 135 | return joinTable + '.' + col + ' as _pivot_' + col; 136 | })); 137 | }, 138 | 139 | // Generates the join clauses necessary for the current relation. 140 | joinClauses: function(knex) { 141 | var joinTable = this.joinTable(); 142 | 143 | if (this.type === 'belongsTo' || this.type === 'belongsToMany') { 144 | 145 | var targetKey = (this.type === 'belongsTo' ? this.key('foreignKey') : this.key('otherKey')); 146 | 147 | knex.join( 148 | joinTable, 149 | joinTable + '.' + targetKey, '=', 150 | this.targetTableName + '.' + this.targetIdAttribute 151 | ); 152 | 153 | // A `belongsTo` -> `through` is currently the only relation with two joins. 154 | if (this.type === 'belongsTo') { 155 | knex.join( 156 | this.parentTableName, 157 | joinTable + '.' + this.throughIdAttribute, '=', 158 | this.parentTableName + '.' + this.key('throughForeignKey') 159 | ); 160 | } 161 | 162 | } else { 163 | knex.join( 164 | joinTable, 165 | joinTable + '.' + this.throughIdAttribute, '=', 166 | this.targetTableName + '.' + this.key('throughForeignKey') 167 | ); 168 | } 169 | }, 170 | 171 | // Check that there isn't an incorrect foreign key set, vs. the one 172 | // passed in when the relation was formed. 173 | whereClauses: function(knex, resp) { 174 | var key; 175 | 176 | if (this.isJoined()) { 177 | var targetTable = this.type === 'belongsTo' ? this.parentTableName : this.joinTable(); 178 | key = targetTable + '.' + (this.type === 'belongsTo' ? this.parentIdAttribute : this.key('foreignKey')); 179 | } else { 180 | key = this.isInverse() ? this.targetIdAttribute : this.key('foreignKey'); 181 | } 182 | 183 | knex[resp ? 'whereIn' : 'where'](key, resp ? this.eagerKeys(resp) : this.parentFk); 184 | 185 | if (this.isMorph()) { 186 | knex.where(this.key('morphKey'), this.key('morphValue')); 187 | } 188 | }, 189 | 190 | // Fetches all `eagerKeys` from the current relation. 191 | eagerKeys: function(resp) { 192 | return _.uniq(_.pluck(resp, this.isInverse() ? this.key('foreignKey') : this.parentIdAttribute)); 193 | }, 194 | 195 | // Generates the appropriate standard join table. 196 | joinTable: function() { 197 | if (this.isThrough()) return this.throughTableName; 198 | return this.joinTableName || [ 199 | this.parentTableName, 200 | this.targetTableName 201 | ].sort().join('_'); 202 | }, 203 | 204 | // Creates a new model or collection instance, depending on 205 | // the `relatedData` settings and the models passed in. 206 | relatedInstance: function(models) { 207 | models || (models = []); 208 | 209 | var Target = this.target; 210 | 211 | // If it's a single model, check whether there's already a model 212 | // we can pick from... otherwise create a new instance. 213 | if (this.isSingle()) { 214 | if (!Target.prototype instanceof ModelBase) { 215 | throw new Error('The `'+this.type+'` related object must be a Bookshelf.Model'); 216 | } 217 | return models[0] || new Target(); 218 | } 219 | 220 | // Allows us to just use a model, but create a temporary 221 | // collection for a "*-many" relation. 222 | if (Target.prototype instanceof ModelBase) { 223 | Target = this.Collection.extend({ 224 | model: Target, 225 | _builder: Target.prototype._builder 226 | }); 227 | } 228 | return new Target(models, {parse: true}); 229 | }, 230 | 231 | // Groups the related response according to the type of relationship 232 | // we're handling, for easy attachment to the parent models. 233 | eagerPair: function(relationName, related, parentModels) { 234 | 235 | // If this is a morphTo, we only want to pair on the morphValue for the current relation. 236 | if (this.type === 'morphTo') { 237 | parentModels = _.filter(parentModels, function(model) { return model.get(this.key('morphKey')) === this.key('morphValue'); }, this); 238 | } 239 | 240 | // If this is a `through` or `belongsToMany` relation, we need to cleanup & setup the `interim` model. 241 | if (this.isJoined()) related = this.parsePivot(related); 242 | 243 | // Group all of the related models for easier association with their parent models. 244 | var grouped = _.groupBy(related, function(model) { 245 | return model.pivot ? model.pivot.get(this.key('foreignKey')) : 246 | this.isInverse() ? model.id : model.get(this.key('foreignKey')); 247 | }, this); 248 | 249 | // Loop over the `parentModels` and attach the appropriated grouped sub-models, 250 | // keeping the appropriated `relatedData` on the new related instance. 251 | for (var i = 0, l = parentModels.length; i < l; i++) { 252 | var model = parentModels[i]; 253 | var groupedKey = this.isInverse() ? model.get(this.key('foreignKey')) : model.id; 254 | var relation = model.relations[relationName] = this.relatedInstance(grouped[groupedKey]); 255 | relation.relatedData = this; 256 | } 257 | return related; 258 | }, 259 | 260 | // The `models` is an array of models returned from the fetch, 261 | // after they're `set`... parsing out any of the `_pivot_` items from the 262 | // join table and assigning them on the pivot model or object as appropriate. 263 | parsePivot: function(models) { 264 | var Through = this.throughTarget; 265 | return _.map(models, function(model) { 266 | var data = {}, keep = {}, attrs = model.attributes, through; 267 | if (Through) through = new Through(); 268 | for (var key in attrs) { 269 | if (key.indexOf('_pivot_') === 0) { 270 | data[key.slice(7)] = attrs[key]; 271 | } else { 272 | keep[key] = attrs[key]; 273 | } 274 | } 275 | model.attributes = keep; 276 | if (!_.isEmpty(data)) { 277 | model.pivot = through ? through.set(data, {silent: true}) : new this.Model(data, { 278 | tableName: this.joinTable() 279 | }); 280 | } 281 | return model; 282 | }, this); 283 | }, 284 | 285 | // A few predicates to help clarify some of the logic above. 286 | isThrough: function() { 287 | return (this.throughTarget != null); 288 | }, 289 | isJoined: function() { 290 | return (this.type === 'belongsToMany' || this.isThrough()); 291 | }, 292 | isMorph: function() { 293 | return (this.type === 'morphOne' || this.type === 'morphMany'); 294 | }, 295 | isSingle: function() { 296 | var type = this.type; 297 | return (type === 'hasOne' || type === 'belongsTo' || type === 'morphOne' || type === 'morphTo'); 298 | }, 299 | isInverse: function() { 300 | return (this.type === 'belongsTo' || this.type === 'morphTo'); 301 | }, 302 | 303 | // Sets the `pivotColumns` to be retrieved along with the current model. 304 | withPivot: function(columns) { 305 | if (!_.isArray(columns)) columns = [columns]; 306 | this.pivotColumns || (this.pivotColumns = []); 307 | push.apply(this.pivotColumns, columns); 308 | } 309 | 310 | }); 311 | 312 | // Simple memoization of the singularize call. 313 | var singularMemo = (function() { 314 | var cache = Object.create(null); 315 | return function(arg) { 316 | if (arg in cache) { 317 | return cache[arg]; 318 | } else { 319 | return cache[arg] = inflection.singularize(arg); 320 | } 321 | }; 322 | }()); 323 | 324 | // Specific to many-to-many relationships, these methods are mixed 325 | // into the `belongsToMany` relationships when they are created, 326 | // providing helpers for attaching and detaching related models. 327 | var pivotHelpers = { 328 | 329 | // Attach one or more "ids" from a foreign 330 | // table to the current. Creates & saves a new model 331 | // and attaches the model with a join table entry. 332 | attach: function(ids, options) { 333 | return this._handler('insert', ids, options); 334 | }, 335 | 336 | // Detach related object from their pivot tables. 337 | // If a model or id is passed, it attempts to remove the 338 | // pivot table based on that foreign key. If a hash is passed, 339 | // it attempts to remove the item based on a where clause with 340 | // these parameters. If no parameters are specified, we assume we will 341 | // detach all related associations. 342 | detach: function(ids, options) { 343 | return this._handler('delete', ids, options); 344 | }, 345 | 346 | // Selects any additional columns on the pivot table, 347 | // taking a hash of columns which specifies the pivot 348 | // column name, and the value the column should take on the 349 | // output to the model attributes. 350 | withPivot: function(columns) { 351 | this.relatedData.withPivot(columns); 352 | return this; 353 | }, 354 | 355 | // Helper for handling either the `attach` or `detach` call on 356 | // the `belongsToMany` or `hasOne` / `hasMany` :through relationship. 357 | _handler: function(method, ids, options) { 358 | var pending = []; 359 | if (ids == void 0) { 360 | if (method === 'insert') return when.resolve(this); 361 | if (method === 'delete') pending.push(this._processPivot(method, null, options)); 362 | } 363 | if (!_.isArray(ids)) ids = ids ? [ids] : []; 364 | for (var i = 0, l = ids.length; i < l; i++) { 365 | pending.push(this._processPivot(method, ids[i], options)); 366 | } 367 | return when.all(pending).yield(this); 368 | }, 369 | 370 | // Handles setting the appropriate constraints and shelling out 371 | // to either the `insert` or `delete` call for the current model, 372 | // returning a promise. 373 | _processPivot: function(method, item, options) { 374 | var data = {}; 375 | var relatedData = this.relatedData; 376 | data[relatedData.key('foreignKey')] = relatedData.parentFk; 377 | 378 | // If the item is an object, it's either a model 379 | // that we're looking to attach to this model, or 380 | // a hash of attributes to set in the relation. 381 | if (_.isObject(item)) { 382 | if (item instanceof ModelBase) { 383 | data[relatedData.key('otherKey')] = item.id; 384 | } else { 385 | _.extend(data, item); 386 | } 387 | } else if (item) { 388 | data[relatedData.key('otherKey')] = item; 389 | } 390 | var builder = this._builder(relatedData.joinTable()); 391 | if (options && options.transacting) { 392 | builder.transacting(options.transacting); 393 | } 394 | var collection = this; 395 | if (method === 'delete') { 396 | return builder.where(data).del().then(function() { 397 | var model; 398 | if (!item) return collection.reset(); 399 | if (model = collection.get(data[relatedData.key('otherKey')])) { 400 | collection.remove(model); 401 | } 402 | }); 403 | } 404 | return builder.insert(data).then(function() { 405 | collection.add(item); 406 | }); 407 | } 408 | 409 | }; 410 | 411 | 412 | }); 413 | 414 | })( 415 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 416 | ); -------------------------------------------------------------------------------- /docs/dialects/sql/sync.html: -------------------------------------------------------------------------------- 1 | dialects/sql/sync
dialects/sql/sync.js

Sync

(function(define) { 2 | 3 | "use strict"; 4 | 5 | define(function(require, exports) { 6 | 7 | var _ = require('underscore'); 8 | var when = require('when');

Sync is the dispatcher for any database queries, 9 | taking the "syncing" model or collection being queried, along with 10 | a hash of options that are used in the various query methods. 11 | If the transacting option is set, the query is assumed to be 12 | part of a transaction, and this information is passed along to Knex.

var Sync = function(syncing, options) { 13 | options || (options = {}); 14 | this.query = syncing.query(); 15 | this.syncing = syncing.resetQuery(); 16 | this.options = options; 17 | this._init(syncing, options); 18 | this.initialize(syncing, options); 19 | }; 20 | 21 | _.extend(Sync.prototype, { 22 | 23 | initialize: function() {},

Select the first item from the database - only used by models.

first: function() { 24 | var syncing = this.syncing; 25 | this.query.where(syncing.format(_.extend(Object.create(null), syncing.attributes))).limit(1); 26 | return this.select(); 27 | },

Runs a select query on the database, adding any necessary relational 28 | constraints, resetting the query when complete. If there are results and 29 | eager loaded relations, those are fetched and returned on the model before 30 | the promise is resolved. Any success handler passed in the 31 | options will be called - used by both models & collections.

select: function() { 32 | var columns, sync = this, syncing = this.syncing, 33 | options = this.options, relatedData = syncing.relatedData;

Inject all appropriate select costraints dealing with the relation 34 | into the knex query builder for the current instance.

if (relatedData) { 35 | relatedData.selectConstraints(this.query, options); 36 | } else { 37 | columns = options.columns; 38 | if (!_.isArray(columns)) columns = columns ? [columns] : [_.result(syncing, 'tableName') + '.*']; 39 | }

Set the query builder on the options, in-case we need to 40 | access in the fetching event handlers.

options.query = this.query;

Trigger a fetching event on the model, and then select the appropriate columns.

return syncing.triggerThen('fetching', syncing, columns, options).then(function() { 41 | return sync.query.select(columns); 42 | }); 43 | },

Issues an insert command on the query - only used by models.

insert: function() { 44 | var syncing = this.syncing; 45 | return this.query 46 | .insert(syncing.format(_.extend(Object.create(null), syncing.attributes)), syncing.idAttribute); 47 | },

Issues an update command on the query - only used by models.

update: function(attrs) { 48 | var syncing = this.syncing, query = this.query; 49 | if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); 50 | if (query.wheres.length === 0) { 51 | return when.reject(new Error('A model cannot be updated without a "where" clause or an idAttribute.')); 52 | } 53 | return query.update(syncing.format(_.extend(Object.create(null), attrs))); 54 | },

Issues a delete command on the query.

del: function() { 55 | var query = this.query, syncing = this.syncing; 56 | if (syncing.id != null) query.where(syncing.idAttribute, syncing.id); 57 | if (query.wheres.length === 0) { 58 | return when.reject(new Error('A model cannot be destroyed without a "where" clause or an idAttribute.')); 59 | } 60 | return this.query.del(); 61 | }, 62 | 63 | _init: function(syncing, options) { 64 | if (options.transacting) this.query.transacting(options.transacting); 65 | } 66 | 67 | }); 68 | 69 | exports.Sync = Sync; 70 | 71 | }); 72 | 73 | })( 74 | typeof define === 'function' && define.amd ? define : function (factory) { factory(require, exports); } 75 | );
--------------------------------------------------------------------------------