├── .gitmodules ├── .babelrc ├── .eslintrc ├── .travis.yml ├── docs ├── adapter.md ├── validations.md ├── roadmap.md ├── ga.html ├── index.txt ├── footer.html ├── changelog.md ├── hooks.md ├── jugglingdb.md ├── schema.md └── model.md ├── test ├── common.batch.js ├── init.js ├── jugglingdb.test.js ├── defaults.test.js ├── spec_helper.js ├── json.test.js ├── i18n.test.js ├── datatype.test.js ├── performance.coffee ├── scope.test.js ├── model.test.js ├── schema.test.js ├── include.test.js ├── manipulation.test.js ├── relations.test.js └── hooks.test.js ├── .npmignore ├── .gitignore ├── main.js ├── lib ├── utils.js ├── jutil.js ├── hooks.js ├── adapters │ ├── riak.js.1 │ ├── http.js.1 │ ├── memory.js │ ├── cradle.js.1 │ └── neo4j.js.1 ├── sql.js ├── scope.js ├── include.js ├── railway.js └── relations.js ├── scripts └── doc.sh ├── circle.yml ├── index.js ├── legacy-compound-schema-loader.js ├── legacy-compound-init.js ├── package.json └── Makefile /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { "presets": ["es2015"] } 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": "1602" } 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.10 4 | - 4.4.5 5 | - 6 6 | -------------------------------------------------------------------------------- /docs/adapter.md: -------------------------------------------------------------------------------- 1 | jugglingdb-adapter(3) - Adapter API explained 2 | ==================== 3 | -------------------------------------------------------------------------------- /test/common.batch.js: -------------------------------------------------------------------------------- 1 | require('./datatype.test.js'); 2 | require('./basic-querying.test.js'); 3 | require('./hooks.test.js'); 4 | require('./relations.test.js'); 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | doc 3 | coverage.html 4 | coverage 5 | v8.log 6 | 7 | .DS_Store 8 | benchmark.js 9 | analyse.r 10 | docs/html 11 | npm-debug.log 12 | -------------------------------------------------------------------------------- /docs/validations.md: -------------------------------------------------------------------------------- 1 | jugglingdb-validations(3) - Built-in validators, creating custom validations, syncronous and asyncronous object validation. 2 | ======================== 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | doc 3 | coverage.html 4 | coverage 5 | v8.log 6 | 7 | .DS_Store 8 | benchmark.js 9 | analyse.r 10 | docs/html 11 | docs/man 12 | npm-debug.log 13 | .idea 14 | .coveralls.yml 15 | build 16 | .eslintcache 17 | -------------------------------------------------------------------------------- /test/init.js: -------------------------------------------------------------------------------- 1 | module.exports = require('should'); 2 | 3 | const Schema = require('../').Schema; 4 | 5 | if (!('getSchema' in global)) { 6 | global.getSchema = function() { 7 | return new Schema('memory'); 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | var isNodeSix = process.versions.node >= '6'; 2 | 3 | if (!isNodeSix) { 4 | global.Promise = require('when').Promise; 5 | } 6 | 7 | module.exports = isNodeSix 8 | ? require('./index') 9 | : require('./build/index'); 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/jugglingdb.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | const jugglingdb = require('../'); 5 | 6 | describe('jugglingdb', function() { 7 | it('should expose version', function() { 8 | jugglingdb.version.should.equal(require('../package.json').version); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | exports.safeRequire = safeRequire; 2 | 3 | function safeRequire(module) { 4 | try { 5 | return require(module); 6 | } catch (e) { 7 | console.log('Run "npm install jugglingdb ' + module + '" command to use jugglingdb using ' + module + ' database engine'); 8 | process.exit(1); 9 | } 10 | } 11 | 12 | -------------------------------------------------------------------------------- /lib/jutil.js: -------------------------------------------------------------------------------- 1 | exports.inherits = function(newClass, baseClass) { 2 | Object.keys(baseClass).forEach(classMethod => { 3 | newClass[classMethod] = baseClass[classMethod]; 4 | }); 5 | 6 | Object.keys(baseClass.prototype).forEach(instanceMethod => { 7 | newClass.prototype[instanceMethod] = baseClass.prototype[instanceMethod]; 8 | }); 9 | }; 10 | 11 | -------------------------------------------------------------------------------- /docs/roadmap.md: -------------------------------------------------------------------------------- 1 | jugglingdb-roadmap - The Future of JugglingDB 2 | ============================================= 3 | 4 | ## DOCUMENTATION 5 | 6 | ### SECTIONS 7 | 8 | * hooks 9 | * validations 10 | * schema 11 | * model 12 | * adapters 13 | * testing 14 | 15 | ## MODEL CORE 16 | 17 | * immutable models 18 | * common transaction support 19 | * object presentation modes 20 | 21 | -------------------------------------------------------------------------------- /docs/ga.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /scripts/doc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | src=$1 4 | dest=$2 5 | 6 | if ! [ `which ronn` ]; then 7 | echo 'ronn rubygem is not installed, run "gem install ronn"' 8 | exit 0 9 | fi 10 | 11 | mkdir -p $(dirname $dest) 12 | 13 | # VERSION=$(grep version package.json | perl -pi -e 's/[^-\d\.]//g') 14 | 15 | case $dest in 16 | *.[13]) 17 | ronn --roff $1 --pipe --organization=1602\ Software --manual=JugglingDB > $2 18 | exit $? 19 | ;; 20 | 21 | *.html) 22 | (ronn -5 $1 --pipe\ 23 | --style='print toc'\ 24 | --organization=1602\ 25 | --manual=JugglingDB &&\ 26 | cat docs/ga.html &&\ 27 | cat docs/footer.html) > $2 28 | exit $? 29 | ;; 30 | esac 31 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.3.0 4 | 5 | general: 6 | branches: 7 | ignore: 8 | - gh-pages 9 | 10 | test: 11 | pre: 12 | - nvm install 0.12.9 13 | - nvm install 4.3.0 14 | - nvm install 5.7.0 15 | - nvm install 6.1.0 16 | - make build 17 | override: 18 | - eslint lib/ 19 | - nvm use 0.12.9 && npm run test-ci-babel 20 | - nvm use 4.3.0 && npm run test-ci-babel 21 | - nvm use 5.7.0 && npm run test-ci-babel 22 | - nvm use 6.1.0 && npm run test-ci-native 23 | - nvm use 6.3.0 && npm run test-ci-native 24 | post: 25 | - npm i coveralls && export COVERALLS_GIT_COMMIT=`git rev-parse HEAD` && cat ./coverage/lcov.info | coveralls 26 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { Schema } = require('./lib/schema'); 4 | const AbstractClass = require('./lib/model.js'); 5 | 6 | module.exports = { 7 | 8 | Schema, 9 | 10 | AbstractClass, 11 | 12 | // deprecated api 13 | loadSchema: function(filename, settings, compound) { 14 | return require('./legacy-compound-schema-loader')(Schema, filename, settings, compound); 15 | }, 16 | 17 | init: function init(compound) { 18 | return require('./legacy-compound-init')(compound, Schema, AbstractClass); 19 | }, 20 | 21 | get BaseSQL() { 22 | return require('./lib/sql'); 23 | }, 24 | 25 | get version() { 26 | return require( 27 | process.versions.node >= '6' 28 | ? './package.json' 29 | : '../package.json' 30 | ).version; 31 | }, 32 | 33 | get test() { 34 | return require('./test/common_test'); 35 | } 36 | 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /test/defaults.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | const db = getSchema(); 5 | 6 | describe('defaults', function() { 7 | let Server; 8 | 9 | before(function() { 10 | Server = db.define('Server', { 11 | host: String, 12 | port: { type: Number, default: 80 } 13 | }); 14 | }); 15 | 16 | it('should apply defaults on new', function() { 17 | const s = new Server; 18 | s.port.should.equal(80); 19 | }); 20 | 21 | it('should apply defaults on create', function(done) { 22 | Server.create(function(err, s) { 23 | s.port.should.equal(80); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should apply defaults on read', function(done) { 29 | db.defineProperty('Server', 'host', { 30 | type: String, 31 | default: 'localhost' 32 | }); 33 | Server.all(function(err, servers) { 34 | (new String('localhost')).should.equal(servers[0].host); 35 | done(); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/spec_helper.js: -------------------------------------------------------------------------------- 1 | try { 2 | global.sinon = require('sinon'); 3 | } catch (e) { 4 | // ignore 5 | } 6 | 7 | let group_name = false, EXT_EXP; 8 | function it(should, test_case) { 9 | check_external_exports(); 10 | if (group_name) { 11 | EXT_EXP[group_name][should] = test_case; 12 | } else { 13 | EXT_EXP[should] = test_case; 14 | } 15 | } 16 | 17 | global.it = it; 18 | 19 | function context(name, tests) { 20 | check_external_exports(); 21 | EXT_EXP[name] = {}; 22 | group_name = name; 23 | tests({ 24 | before(f) { 25 | it('setUp', f); 26 | }, 27 | after(f) { 28 | it('tearDown', f); 29 | } 30 | }); 31 | group_name = false; 32 | } 33 | 34 | global.context = context; 35 | 36 | exports.init = function init(external_exports) { 37 | EXT_EXP = external_exports; 38 | if (external_exports.done) { 39 | external_exports.done(); 40 | } 41 | }; 42 | 43 | function check_external_exports() { 44 | if (!EXT_EXP) throw new Error( 45 | 'Before run this, please ensure that ' + 46 | 'require("spec_helper").init(exports); called'); 47 | } 48 | 49 | -------------------------------------------------------------------------------- /legacy-compound-schema-loader.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function(Schema, filename, settings, compound) { 3 | var schema = []; 4 | var definitions = require(filename); 5 | Object.keys(definitions).forEach(function(k) { 6 | var conf = settings[k]; 7 | if (!conf) { 8 | console.log('No config found for ' + k + ' schema, using in-memory schema'); 9 | conf = {driver: 'memory'}; 10 | } 11 | schema[k] = new Schema(conf.driver, conf); 12 | schema[k].on('define', function(m, name, prop, sett) { 13 | compound.models[name] = m; 14 | if (conf.backyard) { 15 | schema[k].backyard.define(name, prop, sett); 16 | } 17 | }); 18 | schema[k].name = k; 19 | schema.push(schema[k]); 20 | if (conf.backyard) { 21 | schema[k].backyard = new Schema(conf.backyard.driver, conf.backyard); 22 | } 23 | if ('function' === typeof definitions[k]) { 24 | define(schema[k], definitions[k]); 25 | if (conf.backyard) { 26 | define(schema[k].backyard, definitions[k]); 27 | } 28 | } 29 | }); 30 | 31 | return schema; 32 | 33 | function define(db, def) { 34 | def(db, compound); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /test/json.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | const Schema = require('../').Schema; 5 | 6 | describe('JSON property', function() { 7 | let schema, Model; 8 | 9 | it('should be defined', function() { 10 | schema = getSchema(); 11 | Model = schema.define('Model', { propertyName: Schema.JSON }); 12 | const m = new Model; 13 | (new Boolean('propertyName' in m)).should.eql(true); 14 | should.not.exist(m.propertyName); 15 | }); 16 | 17 | it('should accept JSON in constructor and return object', function() { 18 | const m = new Model({ 19 | propertyName: '{"foo": "bar"}' 20 | }); 21 | m.propertyName.should.be.an.Object; 22 | m.propertyName.foo.should.equal('bar'); 23 | }); 24 | 25 | it('should accept object in setter and return object', function() { 26 | const m = new Model; 27 | m.propertyName = { 'foo': 'bar' }; 28 | m.propertyName.should.be.an.Object; 29 | m.propertyName.foo.should.equal('bar'); 30 | }); 31 | 32 | it('should accept string in setter and return string', function() { 33 | const m = new Model; 34 | m.propertyName = '{"foo": "bar"}'; 35 | m.propertyName.should.be.a.String; 36 | m.propertyName.should.equal('{"foo": "bar"}'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /legacy-compound-init.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var loadSchema = require('./legacy-compound-schema-loader'); 4 | 5 | module.exports = function init(compound, Schema, AbstractClass) { 6 | 7 | if (global.railway) { 8 | global.railway.orm = exports; 9 | } else { 10 | compound.orm = { 11 | Schema, 12 | AbstractClass 13 | }; 14 | if (compound.app.enabled('noeval schema')) { 15 | compound.orm.schema = loadSchema( 16 | Schema, 17 | compound.root + '/db/schema', 18 | compound.app.get('database'), 19 | compound 20 | ); 21 | if (compound.app.enabled('autoupdate')) { 22 | compound.on('ready', function() { 23 | compound.orm.schema.forEach(function(s) { 24 | s.autoupdate(); 25 | if (s.backyard) { 26 | s.backyard.autoupdate(); 27 | s.backyard.log = s.log; 28 | } 29 | }); 30 | }); 31 | } 32 | return; 33 | } 34 | } 35 | 36 | // legacy stuff 37 | 38 | if (compound.version > '1.1.5-15') { 39 | compound.on('after routes', initialize); 40 | } else { 41 | initialize(); 42 | } 43 | 44 | function initialize() { 45 | var railway = './lib/railway', init; 46 | try { 47 | init = require(railway); 48 | } catch (e) { 49 | console.log(e.stack); 50 | } 51 | if (init) { 52 | init(compound); 53 | } 54 | } 55 | 56 | }; 57 | -------------------------------------------------------------------------------- /docs/index.txt: -------------------------------------------------------------------------------- 1 | # man pages 2 | jugglingdb(3) index 3 | jugglingdb-changelog(3) changelog.3 4 | jugglingdb-roadmap(3) roadmap.3 5 | jugglingdb-validations(3) validations.3 6 | jugglingdb-hooks(3) hooks.3 7 | jugglingdb-schema(3) schema.3 8 | jugglingdb-model(3) model.3 9 | jugglingdb-adapter(3) adapter.3 10 | 11 | # adapters github 12 | jugglingdb-nano https://github.com/jugglingdb/nano-adapter 13 | jugglingdb-mysql https://github.com/jugglingdb/mysql-adapter 14 | jugglingdb-firebird https://github.com/jugglingdb/firebird-adapter 15 | jugglingdb-sqlite3 https://github.com/jugglingdb/sqlite3-adapter 16 | jugglingdb-postgres https://github.com/jugglingdb/postgres-adapter 17 | jugglingdb-redis https://github.com/jugglingdb/redis-adapter 18 | jugglingdb-mongodb https://github.com/jugglingdb/mongodb-adapter 19 | 20 | # adapters npm 21 | nano-adapter-npm https://npmjs.org/package/jugglingdb-nano 22 | mysql-adapter-npm https://npmjs.org/package/jugglingdb-mysql 23 | firebird-adapter-npm https://npmjs.org/package/jugglingdb-firebird 24 | sqlite3-adapter-npm https://npmjs.org/package/jugglingdb-sqlite3 25 | postgres-adapter-npm https://npmjs.org/package/jugglingdb-postgres 26 | redis-adapter-npm https://npmjs.org/package/jugglingdb-redis 27 | mongodb-adapter-npm https://npmjs.org/package/jugglingdb-mongodb 28 | 29 | # external resources 30 | github.com/jugglingdb https://github.com/jugglingdb 31 | github.com/1602/jugglingdb https://github.com/1602/jugglingdb 32 | issues https://github.com/1602/jugglingdb/issues?state=open 33 | jsdoc.info/1602/jugglingdb http://jsdoc.info/1602/jugglingdb 34 | -------------------------------------------------------------------------------- /test/i18n.test.js: -------------------------------------------------------------------------------- 1 | let should = require('./init.js'), db, User; 2 | 3 | describe('i18n', function() { 4 | db = getSchema(); 5 | 6 | before(function() { 7 | 8 | User = db.define('User', { 9 | email: { type: String, index: true, limit: 100 }, 10 | name: String 11 | }); 12 | 13 | User.i18n = { 14 | en: { 15 | validation: { 16 | name: { 17 | presence: 'User name is not present' 18 | }, 19 | email: { 20 | presence: 'Email required', 21 | uniqueness: 'Email already taken' 22 | } 23 | } 24 | }, 25 | ru: { 26 | validation: { 27 | name: { 28 | }, 29 | email: { 30 | presence: 'Электропочта надо', 31 | uniqueness: 'Электропочта уже взят' 32 | } 33 | } 34 | } 35 | }; 36 | 37 | User.validatesUniquenessOf('email'); 38 | User.validatesPresenceOf('name', 'email'); 39 | }); 40 | 41 | it('should hook up localized string', function(done) { 42 | User.create({ email: 'John.Doe@example.com', name: 'John Doe' }, function(err, user) { 43 | User.create({ email: 'John.Doe@example.com' }, function(err, user) { 44 | const errors = user.errors.__localize('ru'); 45 | errors.name[0].should.equal('can\'t be blank'); 46 | errors.email[0].should.equal('Электропочта уже взят'); 47 | done(); 48 | }); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /docs/footer.html: -------------------------------------------------------------------------------- 1 | 34 | 67 | -------------------------------------------------------------------------------- /test/datatype.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let db, Model; 5 | 6 | describe('datatypes', function() { 7 | 8 | before(function(done) { 9 | db = getSchema(); 10 | Model = db.define('Model', { 11 | str: String, 12 | date: Date, 13 | num: Number, 14 | bool: Boolean 15 | }); 16 | db.automigrate(function() { 17 | Model.destroyAll(done); 18 | }); 19 | }); 20 | 21 | it('should keep types when get read data from db', function() { 22 | let d = new Date, id; 23 | 24 | d.setMilliseconds(0); 25 | 26 | return Model.create({ 27 | str: 'hello', 28 | date: d, 29 | num: '3', 30 | bool: 1 31 | }) 32 | .then(function(m) { 33 | should.exist(m && m.id); 34 | m.str.should.be.a.String; 35 | m.num.should.be.a.Number; 36 | m.bool.should.be.a.Boolean; 37 | id = m.id; 38 | return Model.find(id); 39 | }) 40 | .then(function(m) { 41 | should.exist(m); 42 | m.str.should.be.a.String; 43 | m.num.should.be.a.Number; 44 | m.bool.should.be.a.Boolean; 45 | m.date.should.be.an.instanceOf(Date); 46 | m.date.toString().should.equal(d.toString(), 'Time must match'); 47 | return Model.findOne(); 48 | }) 49 | .then(function(m) { 50 | should.exist(m); 51 | m.str.should.be.a.String; 52 | m.num.should.be.a.Number; 53 | m.bool.should.be.a.Boolean; 54 | m.date.should.be.an.instanceOf(Date); 55 | m.date.toString().should.equal(d.toString(), 'Time must match'); 56 | }); 57 | }); 58 | 59 | it('should convert "false" to false for boolean', function() { 60 | const m = new Model({ bool: 'false' }); 61 | m.bool.should.equal(false); 62 | }); 63 | 64 | }); 65 | -------------------------------------------------------------------------------- /lib/hooks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Hooks mixins for ./model.js 3 | */ 4 | const Hookable = require('./model.js'); 5 | 6 | /** 7 | * Module exports 8 | */ 9 | exports.Hookable = Hookable; 10 | 11 | /** 12 | * List of hooks available 13 | */ 14 | Hookable.afterInitialize = null; 15 | Hookable.beforeValidate = null; 16 | Hookable.afterValidate = null; 17 | Hookable.beforeSave = null; 18 | Hookable.afterSave = null; 19 | Hookable.beforeCreate = null; 20 | Hookable.afterCreate = null; 21 | Hookable.beforeUpdate = null; 22 | Hookable.afterUpdate = null; 23 | Hookable.beforeDestroy = null; 24 | Hookable.afterDestroy = null; 25 | 26 | Hookable.prototype.trigger = function trigger(actionName, work, data, quit) { 27 | const capitalizedName = capitalize(actionName); 28 | let beforeHook = this.constructor['before' + capitalizedName]; 29 | let afterHook = this.constructor['after' + capitalizedName]; 30 | if (actionName === 'validate') { 31 | beforeHook = beforeHook || this.constructor.beforeValidation; 32 | afterHook = afterHook || this.constructor.afterValidation; 33 | } 34 | const inst = this; 35 | 36 | // we only call "before" hook when we have actual action (work) to perform 37 | if (work) { 38 | if (beforeHook) { 39 | // before hook should be called on instance with one param: callback 40 | beforeHook.call(inst, err => { 41 | if (err) { 42 | if (quit) { 43 | quit.call(inst, err); 44 | } 45 | return; 46 | } 47 | // actual action also have one param: callback 48 | work.call(inst, next); 49 | }, data); 50 | } else { 51 | work.call(inst, next); 52 | } 53 | } else { 54 | next(); 55 | } 56 | 57 | function next(done) { 58 | if (afterHook) { 59 | afterHook.call(inst, done); 60 | } else if (done) { 61 | done.call(this); 62 | } 63 | } 64 | }; 65 | 66 | function capitalize(string) { 67 | return string.charAt(0).toUpperCase() + string.slice(1); 68 | } 69 | -------------------------------------------------------------------------------- /test/performance.coffee: -------------------------------------------------------------------------------- 1 | Schema = require('../index').Schema 2 | Text = Schema.Text 3 | 4 | require('./spec_helper').init exports 5 | 6 | schemas = 7 | neo4j: 8 | url: 'http://localhost:7474/' 9 | mongoose: 10 | url: 'mongodb://localhost/test' 11 | redis: {} 12 | memory: {} 13 | cradle: {} 14 | nano: 15 | url: 'http://localhost:5984/nano-test' 16 | 17 | testOrm = (schema) -> 18 | 19 | User = Post = 'unknown' 20 | maxUsers = 100 21 | maxPosts = 50000 22 | users = [] 23 | 24 | it 'should define simple', (test) -> 25 | 26 | User = schema.define 'User', { 27 | name: String, 28 | bio: Text, 29 | approved: Boolean, 30 | joinedAt: Date, 31 | age: Number 32 | } 33 | 34 | Post = schema.define 'Post', 35 | title: { type: String, length: 255, index: true } 36 | content: { type: Text } 37 | date: { type: Date, detault: Date.now } 38 | published: { type: Boolean, default: false } 39 | 40 | User.hasMany(Post, {as: 'posts', foreignKey: 'userId'}) 41 | Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}) 42 | 43 | test.done() 44 | 45 | it 'should create users', (test) -> 46 | wait = maxUsers 47 | done = (e, u) -> 48 | users.push(u) 49 | test.done() if --wait == 0 50 | User.create(done) for i in [1..maxUsers] 51 | 52 | it 'should create bunch of data', (test) -> 53 | wait = maxPosts 54 | done = -> test.done() if --wait == 0 55 | rnd = (title) -> 56 | { 57 | userId: users[Math.floor(Math.random() * maxUsers)].id 58 | title: 'Post number ' + (title % 5) 59 | } 60 | Post.create(rnd(num), done) for num in [1..maxPosts] 61 | 62 | it 'do some queries using foreign keys', (test) -> 63 | wait = 4 64 | done = -> test.done() if --wait == 0 65 | ts = Date.now() 66 | query = (num) -> 67 | users[num].posts { title: 'Post number 3' }, (err, collection) -> 68 | console.log('User ' + num + ':', collection.length, 'posts in', Date.now() - ts,'ms') 69 | done() 70 | query num for num in [0..4] 71 | 72 | return 73 | 74 | it 'should destroy all data', (test) -> 75 | Post.destroyAll -> 76 | User.destroyAll(test.done) 77 | 78 | Object.keys(schemas).forEach (schemaName) -> 79 | return if process.env.ONLY && process.env.ONLY != schemaName 80 | context schemaName, -> 81 | schema = new Schema schemaName, schemas[schemaName] 82 | testOrm(schema) 83 | 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jugglingdb", 3 | "description": "Node.js ORM for every database: redis, mysql, mongodb, postgres, sqlite, ...", 4 | "version": "2.0.1", 5 | "author": "Anatoliy Chakkaev ", 6 | "contributors": [ 7 | { 8 | "name": "Anatoliy Chakkaev", 9 | "email": "rpm1602@gmail.com" 10 | }, 11 | { 12 | "name": "Julien Guimont", 13 | "email": "julien.guimont@gmail.com" 14 | }, 15 | { 16 | "name": "Joseph Junker", 17 | "email": "joseph.jnk@gmail.com" 18 | }, 19 | { 20 | "name": "Henri Bergius", 21 | "email": "henri.bergius@iki.fi" 22 | }, 23 | { 24 | "name": "redvulps", 25 | "email": "fabopereira@gmail.com" 26 | }, 27 | { 28 | "name": "Felipe Sateler", 29 | "email": "fsateler@gmail.com" 30 | }, 31 | { 32 | "name": "Amir M. Mahmoudi", 33 | "email": "a@geeknux.com" 34 | }, 35 | { 36 | "name": "Justinas Stankevičius", 37 | "email": "justinas@justinas.me" 38 | }, 39 | { 40 | "name": "Rick O'Toole", 41 | "email": "patrick.n.otoole@gmail.com" 42 | }, 43 | { 44 | "name": "Nicholas Westlake", 45 | "email": "nicholasredlin@gmail.com" 46 | } 47 | ], 48 | "repository": { 49 | "type": "git", 50 | "url": "https://github.com/1602/jugglingdb" 51 | }, 52 | "main": "main.js", 53 | "scripts": { 54 | "test": "mocha --bail --reporter spec --check-leaks test/", 55 | "test-babel": "mocha --compilers js:babel-register --bail --reporter spec --check-leaks test/", 56 | "prepublish": "make build", 57 | "test-coverage": "istanbul cover node_modules/.bin/_mocha -- --reporter landing --no-exit --check-leaks test/", 58 | "test-ci-babel": "node_modules/mocha/bin/_mocha --compilers js:babel-register --reporter spec --check-leaks test/", 59 | "test-ci-native": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --reporter spec --check-leaks test/" 60 | }, 61 | "man": [ 62 | "./docs/man/jugglingdb.3", 63 | "./docs/man/schema.3", 64 | "./docs/man/model.3", 65 | "./docs/man/hooks.3", 66 | "./docs/man/validations.3", 67 | "./docs/man/roadmap.3", 68 | "./docs/man/changelog.3" 69 | ], 70 | "engines": [ 71 | "node >= 0.6" 72 | ], 73 | "devDependencies": { 74 | "babel-cli": "^6.11.4", 75 | "babel-preset-es2015": "^6.9.0", 76 | "babel-register": "^6.11.5", 77 | "eslint": "^3.2.0", 78 | "eslint-config-1602": "^1.2.0", 79 | "expect": "^1.20.2", 80 | "istanbul": "^0.4.4", 81 | "jshint": "2.9.2", 82 | "mocha": "2.5.3", 83 | "should": "9.0.2" 84 | }, 85 | "dependencies": { 86 | "inflection": "=1.2.7", 87 | "when": "3.7.3" 88 | }, 89 | "license": "MIT", 90 | "jshintConfig": { 91 | "proto": true 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/scope.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let db, Railway, Station; 5 | 6 | describe('sc0pe', function() { 7 | 8 | before(function() { 9 | db = getSchema(); 10 | Railway = db.define('Railway', { 11 | URID: { type: String, index: true } 12 | }); 13 | Station = db.define('Station', { 14 | USID: { type: String, index: true }, 15 | capacity: { type: Number, index: true }, 16 | thoughput: { type: Number, index: true }, 17 | isActive: { type: Boolean, index: true }, 18 | isUndeground: { type: Boolean, index: true } 19 | }); 20 | }); 21 | 22 | beforeEach(function(done) { 23 | Railway.destroyAll(function() { 24 | Station.destroyAll(done); 25 | }); 26 | }); 27 | 28 | it('should define scope with query', function(done) { 29 | Station.scope('active', { where: { isActive: true } }); 30 | Station.active.create(function(err, station) { 31 | should.not.exist(err); 32 | should.exist(station); 33 | should.exist(station.isActive); 34 | station.isActive.should.be.true; 35 | done(); 36 | }); 37 | }); 38 | 39 | it('should allow scope chaining', function(done) { 40 | Station.scope('active', { where: { isActive: true } }); 41 | Station.scope('subway', { where: { isUndeground: true } }); 42 | Station.active.subway.create(function(err, station) { 43 | should.not.exist(err); 44 | should.exist(station); 45 | station.isActive.should.be.true; 46 | station.isUndeground.should.be.true; 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should query all', function(done) { 52 | Station.scope('active', { where: { isActive: true } }); 53 | Station.scope('inactive', { where: { isActive: false } }); 54 | Station.scope('ground', { where: { isUndeground: true } }); 55 | Station.active.ground.create(function() { 56 | Station.inactive.ground.create(function() { 57 | Station.ground.inactive(function(err, ss) { 58 | ss.should.have.lengthOf(1); 59 | done(); 60 | }); 61 | }); 62 | }); 63 | }); 64 | 65 | it('should destroy all', function(done) { 66 | Station.inactive.ground.create(function() { 67 | Station.inactive(function(err, ss) { 68 | ss.should.have.lengthOf(1); 69 | Station.inactive.destroyAll(function() { 70 | Station.inactive(true, function(err, ss) { 71 | ss.should.have.lengthOf(0); 72 | done(); 73 | }); 74 | }); 75 | }); 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | jugglingdb-changelog(3) - The History of JugglingDB 2 | =================================================== 3 | 4 | ## HISTORY 5 | 6 | ### 2.1.0 7 | 8 | * ES6 9 | * Promises 10 | * Support promises in adapters 11 | * Deprecated List type (a.k.a. `type: []`) 12 | * Bugfixes in validations 13 | * Deprecated Model.update({update, where}) (use Model.bulkUpdate({update, where})) 14 | 15 | ### 0.3.0 16 | 17 | * Documentation: 18 | Available in [web](http://1602.github.io/jugglingdb/) and man [jugglingdb(3)] 19 | 20 | * **Hooks**: 21 | Changed format of update and save hooks. Hook accept data as second argument. 22 | This data could be modified and it will be saved to database after hook done. 23 | **NOTE**: this change could break some code. 24 | 25 | * **Datatypes**: 26 | Now object casts type of member on assignment. It may cause issues if 27 | mongodb's ObjectID was manually used as type for property. Solution: not use 28 | it as type directly, and specify wrapper instead. 29 | 30 | * **Model.iterate(opts, iterator, callback)**: 31 | Async iterator for large datasets. 32 | 33 | ### 0.2.1 34 | 35 | * Introduced `include` method 36 | * Use semver 37 | * Added WebService adapter for client-side compound 38 | * Added array methods to List 39 | * Code cleanup and documenation amends 40 | * Custom type registration 41 | * Browserify-friendly core 42 | 43 | ### 0.2.0 44 | 45 | * Namespace adapter packages (should start with "jugglingdb-") 46 | * Added [nano][jugglingdb-nano] adapter 47 | * Adapters removed from core to separate packages 48 | 49 | ### 0.1.27 50 | 51 | * `autoupdate` fixes for MySQL 52 | * Added `schema.isActual` to check whether migration necessary 53 | * Redis adapter refactored and optimized (fully rewritten) 54 | * Introduce sort-only indexes in redis 55 | * Introduce List API (type: []) 56 | * Update to MySQL 2.0 57 | 58 | ### 0.1.13 59 | 60 | * Validations: sync/async, custom, bugfixes 61 | * MySQL adapter enhancementsenhancements 62 | * DB sync: autoupdate/automigrate 63 | * Ability to overwrite getters/setters 64 | * Resig-style model constructors 65 | * Added [postgres][jugglingdb-postgres] adapter 66 | * Added [sqlite3][jugglingdb-postgres] adapter 67 | * Added [mongodb][jugglingdb-mongodb] adapter 68 | * Redis adapter filter/sort rewriting 69 | * Added `findOne` method 70 | * Custom table names in sqlite, mysql and postgres 71 | * Sequelize adapter removed 72 | * Support `upsert` method 73 | * Delayed db calls (wait for `.on('connected')`) 74 | 75 | ### 0.0.6 76 | 77 | * Tests 78 | * Logging in MySQL and Redis 79 | 80 | ### 0.0.4 81 | 82 | * MySQL adapter 83 | * Indexes in redis 84 | * Neo4j cypher query support 85 | 86 | ### 0.0.2 (16 Oct 2011) 87 | 88 | * Built-in adapters: [redis][jugglingdb-redis], mongoose, sequelize, neo4j 89 | * Scopes 90 | * Conditional validations, null checks everywhere 91 | * Defaults applied on create 92 | 93 | ### 0.0.1 94 | 95 | Package extracted from [RailwayJS MVC](http://railwayjs.com) 96 | 97 | ## SEE ALSO 98 | 99 | jugglingdb-roadmap(3) 100 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## TESTS 2 | 3 | TESTER = ./node_modules/.bin/mocha 4 | TESTS = test/*.test.js 5 | JSHINT = ./node_modules/.bin/jshint 6 | 7 | JS_FILES = $(shell find . -type f -name "*.js" \ 8 | -not -path "./legacy-compound-init.js" -and \ 9 | -not -path "./node_modules/*" -and \ 10 | -not -path "./coverage/*" -and \ 11 | -not -path "./support/*" -and \ 12 | -not -path "./lib/adapters/neo4j.js" -and \ 13 | -not -path "./test/*") 14 | 15 | check: 16 | @$(JSHINT) $(JS_FILES) 17 | test: 18 | $(TESTER) $(OPTS) $(TESTS) 19 | test-verbose: 20 | $(TESTER) $(OPTS) --reporter spec $(TESTS) 21 | testing: 22 | $(TESTER) $(OPTS) --watch $(TESTS) 23 | 24 | about-testing: 25 | @echo "\n## TESTING\n" 26 | @echo " make test # Run all tests in silent mode" 27 | @echo " make test-verbose # Run all tests in verbose mode" 28 | @echo " make testing # Run tests continuously" 29 | 30 | ## DOCS 31 | 32 | MAN_DOCS = $(shell find docs -name '*.md' \ 33 | |sed 's|.md|.3|g' \ 34 | |sed 's|docs/|docs/man/|g' ) 35 | 36 | HTML_DOCS = $(shell find docs -name '*.md' \ 37 | |sed 's|.md|.3.html|g' \ 38 | |sed 's|docs/|docs/html/|g' ) \ 39 | docs/html/index.html 40 | 41 | docs/man/%.3: docs/%.md scripts/doc.sh 42 | scripts/doc.sh $< $@ 43 | 44 | docs/html/%.3.html: docs/%.md scripts/doc.sh docs/footer.html 45 | scripts/doc.sh $< $@ 46 | 47 | docs/html/index.html: docs/jugglingdb.md scripts/doc.sh docs/footer.html 48 | scripts/doc.sh $< $@ 49 | 50 | man: $(MAN_DOCS) 51 | html: $(HTML_DOCS) 52 | 53 | build: man 54 | babel -d build/ $(shell find ./lib/ -type f -name "*.js") *.js 55 | 56 | web: html 57 | cp ./docs/html/* ../docs 58 | 59 | about-docs: 60 | @echo "\n## DOCS\n" 61 | @echo " make man # Create docs for man" 62 | @echo " make html # Create docs in html" 63 | @echo " make web # Publish docs to jugglingdb.co" 64 | 65 | ## WORKFLOW 66 | 67 | GITBRANCH = $(shell git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/') 68 | 69 | REPO = marcusgreenwood/hatchjs 70 | TARGET = origin 71 | FROM = $(GITBRANCH) 72 | TO = $(GITBRANCH) 73 | 74 | pull: 75 | git pull $(TARGET) $(FROM) 76 | 77 | safe-pull: 78 | git pull $(TARGET) $(FROM) --no-commit 79 | 80 | push: test 81 | git push $(TARGET) $(TO) 82 | 83 | feature: 84 | git checkout -b feature-$(filter-out $@,$(MAKECMDGOALS)) 85 | git push -u $(TARGET) feature-$(filter-out $@,$(MAKECMDGOALS)) 86 | %: 87 | @: 88 | 89 | version-build: 90 | @echo "Increasing version build, publishing package, then push, hit Ctrl+C to skip before 'three'" 91 | @sleep 1 && echo 'one...' 92 | @sleep 1 && echo 'two...' 93 | @sleep 1 && echo 'three!' 94 | @sleep 1 95 | npm version build && npm publish && git push 96 | 97 | about-workflow: 98 | @echo "\n## WORKFLOW\n" 99 | @echo " make pull # Pull changes from current branch" 100 | @echo " make push # Push changes to current branch" 101 | @echo " make feature {name} # Create feature branch 'feature-name'" 102 | @echo " make pr # Make pull request" 103 | @echo " make version-build # Create new build version" 104 | 105 | ## HELP 106 | 107 | help: about-testing about-docs about-workflow 108 | 109 | .PHONY: test docs 110 | -------------------------------------------------------------------------------- /lib/adapters/riak.js.1: -------------------------------------------------------------------------------- 1 | var safeRequire = require('../utils').safeRequire; 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | var uuid = require('node-uuid'); 7 | var riak = safeRequire('riak-js'); 8 | 9 | exports.initialize = function initializeSchema(schema) { 10 | schema.client = riak.getClient({ 11 | host: schema.settings.host || '127.0.0.1', 12 | port: schema.settings.port || 8091 13 | }); 14 | schema.adapter = new Riak(schema.client); 15 | }; 16 | 17 | function Riak(client) { 18 | this._models = {}; 19 | this.client = client; 20 | } 21 | 22 | Riak.prototype.define = function(descr) { 23 | this._models[descr.model.modelName] = descr; 24 | }; 25 | 26 | Riak.prototype.save = function(model, data, callback) { 27 | this.client.save(this.table(model), data.id, data, callback); 28 | }; 29 | 30 | Riak.prototype.create = function(model, data, callback) { 31 | data.id = uuid(); 32 | this.save(model, data, function(err) { 33 | if (callback) { 34 | callback(err, data.id); 35 | } 36 | }); 37 | }; 38 | 39 | Riak.prototype.exists = function(model, id, callback) { 40 | this.client.exists(this.table(model), id, function(err, exists) { 41 | if (callback) { 42 | callback(err, exists); 43 | } 44 | }); 45 | }; 46 | 47 | Riak.prototype.find = function find(model, id, callback) { 48 | this.client.get(this.table(model), id, function(err, data) { 49 | if (data && data.id) { 50 | data.id = id; 51 | } else { 52 | data = null; 53 | } 54 | if (typeof callback === 'function') callback(err, data); 55 | }); 56 | }; 57 | 58 | Riak.prototype.destroy = function destroy(model, id, callback) { 59 | this.client.remove(this.table(model), id, function(err) { 60 | callback(err); 61 | }); 62 | }; 63 | 64 | Riak.prototype.all = function all(model, filter, callback) { 65 | var opts = {}; 66 | if (filter && filter.where) opts.where = filter.where; 67 | this.client.getAll(this.table(model), function(err, result) { 68 | if (err) return callback(err, []); 69 | /// return callback(err, result.map(function(x) { return {id: x}; })); 70 | result = (result || []).map(function(row) { 71 | var record = row.data; 72 | record.id = row.meta.key; 73 | console.log(record); 74 | return record; 75 | }); 76 | 77 | return callback(err, result); 78 | }.bind(this)); 79 | }; 80 | 81 | Riak.prototype.destroyAll = function destroyAll(model, callback) { 82 | var self = this; 83 | this.all(this.table(model), {}, function(err, recs) { 84 | if (err) callback(err); 85 | 86 | removeOne(); 87 | 88 | function removeOne(error) { 89 | err = err || error; 90 | var rec = recs.pop(); 91 | if (!rec) return callback(err && String(err.statusCode) !== '404' ? err : null); 92 | console.log(rec.id); 93 | self.client.remove(this.table(model), rec.id, removeOne); 94 | } 95 | 96 | }); 97 | 98 | }; 99 | 100 | Riak.prototype.count = function count(model, callback) { 101 | this.client.keys(this.table(model) + ':*', function(err, keys) { 102 | callback(err, err ? null : keys.length); 103 | }); 104 | }; 105 | 106 | Riak.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { 107 | data.id = id; 108 | this.save(model, data, cb); 109 | }; 110 | 111 | Riak.prototype.table = function(model) { 112 | return this._models[model].model.tableName; 113 | }; 114 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | jugglingdb-hooks(3) - Hooks and object lifecycle. 2 | =================== 3 | 4 | ## DESCRIPTION 5 | 6 | Hook is a class method called on object when some event happens. List of events: 7 | 8 | * `initialize`: 9 | Called after `new Model` called. 10 | 11 | * `create`: 12 | Called before and after create. 13 | 14 | * `update`: 15 | Called before and after save (except create). 16 | 17 | * `save`: 18 | Called before and after save (including both create and update). 19 | 20 | * `validate`: 21 | Called before and after validations. 22 | 23 | * `destroy`: 24 | Called before and after destroy on instance. 25 | 26 | 27 | Each hook except `initialize` accepts callback as the first argument. This callback 28 | should be called when hook is done. All hooks are called on object instance, but it's 29 | not recommended to use `this` for updating all hooks where data argument is 30 | available (second argument for all data-related before-hooks: save, update, 31 | create). 32 | 33 | ## INITIALIZE 34 | 35 | Initialize hook called when new object created after all setters and defaults 36 | being applied. 37 | 38 | Model.afterInitialize = function() { 39 | this.property = 'some value; 40 | console.log('afterInitialize called'); 41 | }; 42 | new Model; // afterInitialize called 43 | 44 | ## CREATE 45 | 46 | Create hooks is being called when object is created. 47 | The `beforeCreate` hook accepts `data` as a second argument. 48 | 49 | Model.beforeCreate = function(next, data) { 50 | // use data argument to update object 51 | data.createdAt = new Date(); 52 | console.log('before'); 53 | next(); 54 | }; 55 | 56 | Model.afterCreate = function(next) { 57 | this.notifySocialNetworks(); 58 | this.sendEmailNotifications(); 59 | console.log('after'); 60 | next(); 61 | }; 62 | 63 | Model.create({foo: 'bar'}, function(err, model) { 64 | console.log('callback'); 65 | }); 66 | 67 | Example output will be: 68 | 69 | before 70 | after 71 | callback 72 | 73 | ## UPDATE 74 | 75 | Update hooks called on each save except create. 76 | The `beforeUpdate` hook accepts data as second argument. 77 | The data argument contains only actual data for update, not full object data. 78 | 79 | Model.beforeUpdate = function(next, data) { 80 | // use data argument to update object 81 | // in update hook data argumen only contains data for update (not 82 | // full object) 83 | data.updatedAt = new Date(); 84 | console.log('before'); 85 | next(); 86 | }; 87 | 88 | Model.afterUpdate = function(next) { 89 | this.scheduleFulltextIndexUpdate(); 90 | console.log('after'); 91 | next(); 92 | }; 93 | 94 | model.updateAttributes({foo: 'bar'}, function(err, model) { 95 | console.log('callback'); 96 | }); 97 | 98 | Example output will be: 99 | 100 | before 101 | after 102 | callback 103 | 104 | ## SAVE 105 | 106 | Save hooks called on each save, both during update and create. 107 | The `beforeSave` hook accepts `data` as a second argument. For this cook hook `data` argument is the same as `this` when model.save() is called. When model.updateAttributes() called data argument contains only actual changes. 108 | 109 | Model.beforeSave = function(next, data) { 110 | if ('string' !== typeof data.tags) { 111 | data.tags = JSON.stringify(data.tags); 112 | } 113 | next(); 114 | }; 115 | 116 | Model.afterSave = function(next) { 117 | next(); 118 | }; 119 | 120 | ## DESTROY 121 | 122 | Hook is called once `model.destroy()` is called. Please note that 123 | `destroyAll` method doesn't call destroy hooks. 124 | 125 | Model.beforeDestroy = function(next, data) { 126 | next(); 127 | }; 128 | 129 | Model.afterDestroy = function(next) { 130 | next(); 131 | }; 132 | 133 | ## VALIDATE 134 | 135 | Validate hooks called before and after validation and should be used for data 136 | modification and not for validation. Use custom validation described in 137 | jugglingdb-validations(3) man section. 138 | 139 | ## SEE ALSO 140 | 141 | jugglingdb-model(3) 142 | jugglingdb-validations(3) 143 | -------------------------------------------------------------------------------- /docs/jugglingdb.md: -------------------------------------------------------------------------------- 1 | JugglingDB(3) - cross-database ORM for nodejs and the browser 2 | ========================================================= 3 | 4 | ## DESCRIPTION 5 | 6 | JugglingDB is a cross-db ORM for nodejs, that provides a **common interface** to access 7 | many popular database engines. JugglingDB Current supports: mysql, sqlite3, 8 | postgres, couchdb, mongodb, redis, neo4j and in-memory-storage. 9 | 10 | JugglingDB also works client-side (using WebService and Memory adapters), 11 | which allows rich client-side apps to talk to application servers using a JSON API. 12 | 13 | 14 | ## INSTALLATION 15 | 16 | Use npm to install the core package: 17 | 18 | npm install jugglingdb --save 19 | 20 | Alternatively you can install the jugglingdb core from github: 21 | 22 | npm install 1602/jugglingdb 23 | 24 | Then install an adapter for specific database, for example for redis `jugglingdb-redis`: 25 | 26 | npm install jugglingdb-redis 27 | 28 | See [ADAPTERS][] for a list of available adapters. 29 | 30 | ## DOCUMENTATION 31 | 32 | Autogenerated documentation available at [jsdoc.info/1602/jugglingdb]. 33 | Hand written manual sections are: 34 | 35 | * jugglingdb-schema(3): 36 | Everything about the schema, data types and model definition. 37 | 38 | * jugglingdb-model(3): 39 | Model methods, features and internals. 40 | 41 | * jugglingdb-validations(3): 42 | Built-in validators, creating custom validations, synchronous and asynchronous 43 | object validation. 44 | 45 | * jugglingdb-hooks(3): 46 | Hooks and object lifecycle. 47 | 48 | * jugglingdb-adapter(3): 49 | Adapter API explained. 50 | 51 | ## ADAPTERS 52 | 53 | All adapters are available as separate packages at 54 | [github.com/jugglingdb] and published in npm. 55 | 56 | * MySQL: [github](jugglingdb-mysql) [npm](mysql-adapter-npm) 57 | * SQLite3: [github](jugglingdb-sqlite3) [npm](sqlite3-adapter-npm) 58 | * Postgres: [github](jugglingdb-postgres) [npm](postgres-adapter-npm) 59 | * Redis: [github](jugglingdb-redis) [npm](redis-adapter-npm) 60 | * MongoDB: [github](jugglingdb-mongodb) [npm](mongodb-adapter-npm) 61 | * CouchDB/nano: [github](jugglingdb-nano) [npm](nano-adapter-npm) 62 | * Firebird: [github](jugglingdb-firebird) [npm](firebird-adapter-npm) 63 | 64 | ## CONTRIBUTION 65 | 66 | You can take part in improving the jugglingdb codebase and documents. Please make your proposed changes by forking our documentation's repository on github, and then create a new pull request. 67 | Please remember that your contribution is highly encouraged. Be sure to follow the discussion in google group and see [github issues][issues] before you make changes. It's preferable that you post new comment in google group or in github before you make changes and create pull request. 68 | 69 | ## FUTURE 70 | 71 | See jugglingdb-roadmap(3) and [github issues][issues] to catch up current 72 | development and see how you can help jugglingdb to grow up. 73 | 74 | ## BUGS 75 | 76 | When you find issues, please report them: 77 | 78 | * github/core: 79 | 80 | * github/adapters: 81 | 82 | * email: 83 | 84 | 85 | Provide a test case for reproducing your error. When reporting issues to the core, mention which 86 | adapter you are using and where problem could be reproduced. 87 | 88 | ## HISTORY 89 | 90 | See jugglingdb-changelog(3) 91 | 92 | ## COPYRIGHT 93 | 94 | JugglingDB is Copyright (C) 2011 Anatoliy Chakkaev http://anatoliy.in 95 | 96 | ## AUTHOR 97 | 98 | * [blog](http://anatoliy.in/) 99 | * [github/1602](https://github.com/1602/) 100 | * [github/anatoliychakkaev](https://github.com/anatoliychakkaev/) 101 | * [twitter@1602](http://twitter.com/1602) 102 | * 103 | 104 | ## CONTRIBUTORS 105 | 106 | ### core contributors (more than 1%) 107 | 410 Anatoliy Chakkaev 73.9% 108 | 31 Sebastien Drouyer 5.6% 109 | 25 1602 4.5% 110 | 9 Muneeb Samuels 1.6% 111 | 6 Henri Bergius 1.1% 112 | 113 | ### adapters maintainers 114 | 115 | * [jugglingdb-nano] - [Nicholas Westlake](https://github.com/nrw) 116 | * [jugglingdb-mysql] - [dgsan](https://github.com/dgsan) 117 | * [jugglingdb-firebird] - [Henri Gourvest](https://github.com/hgourvest) 118 | 119 | *NOTE:* Other adapters waits for their maintainers, drop a line to 120 | if you want to maintain some adapter on regular basis. 121 | 122 | ## SEE ALSO 123 | 124 | jugglingdb-schema(3) 125 | -------------------------------------------------------------------------------- /docs/schema.md: -------------------------------------------------------------------------------- 1 | jugglingdb-schema(3) - Everything about schema, data types and model definition. 2 | ==================== 3 | 4 | ## DESCRIPTION 5 | 6 | Schema is a factory for classes. Schema connected with specific database using 7 | adapter. 8 | 9 | All classes within single schema shares same adapter type and one database 10 | connection. But it's possible to use more than one schema to connect with 11 | different databases. 12 | 13 | ## EVENTS 14 | 15 | Instances of Schema are event emitters, events supported by default: 16 | 17 | * `.on('connected', function() {})`: 18 | Fired when db connection established. Params: none. 19 | * `.on('log', function(msg, duration) {})`: 20 | Fired when adapter logged line. Params: String message, Number duration 21 | 22 | ## USAGE 23 | 24 | ### Creating schema 25 | 26 | `Schema` constructor available on `jugglingdb` module: 27 | 28 | var Schema = require('jugglingdb').Schema; 29 | 30 | Schema constructor accepts two arguments. First argument is adapter. It could be 31 | adapter name or adapter package: 32 | 33 | var schemaByAdapterName = new Schema('memory'); 34 | var schemaByAdapterPackage = new Schema(require('redis')); 35 | 36 | ### Settings 37 | 38 | Second argument is optional settings. Settings object format and defaults 39 | depends on specific adapter, but common fields are: 40 | 41 | * `host`: 42 | Database host 43 | * `port`: 44 | Database port 45 | * `username`: 46 | Username to connect to database 47 | * `password`: 48 | Password to connect to database 49 | * `database`: 50 | Database name 51 | * `debug`: 52 | Turn on verbose mode to debug db queries and lifecycle 53 | 54 | For adapter-specific settings refer to adapter's readme file. 55 | 56 | ### Connecting to database 57 | 58 | Schema connecting to database automatically. Once connection established schema 59 | object emit 'connected' event, and set `connected` flag to true, but it is not 60 | necessary to wait for 'connected' event because all queries cached and executed 61 | when schema emit 'connected' event. 62 | 63 | To disconnect from database server call `schema.disconnect` method. This call 64 | forwarded to adapter if adapter have ability to connect/disconnect. 65 | 66 | ### Model definition 67 | 68 | To define model schema have single method `schema.define`. It accepts three 69 | arguments: 70 | 71 | * **model name**: 72 | String name in camel-case with first upper-case letter. This name will be used 73 | later to access model. 74 | * **properties**: 75 | Object with property type definitions. Key is property name, value is type 76 | definition. Type definition can be function representing type of property 77 | (String, Number, Date, Boolean), or object with {type: String|Number|..., 78 | index: true|false} format. 79 | * **settings**: 80 | Object with model-wide settings such as `table` or so. 81 | 82 | Examples of model definition: 83 | 84 | var User = schema.define('User', { 85 | email: String, 86 | password: String, 87 | birthDate: Date, 88 | activated: Boolean 89 | }); 90 | 91 | var User = schema.define('User', { 92 | email: { type: String, limit: 150, index: true }, 93 | password: { type: String, limit: 50 }, 94 | birthDate: Date, 95 | registrationDate: { 96 | type: Date, 97 | default: function () { return new Date } 98 | }, 99 | activated: { type: Boolean, default: false } 100 | }, { 101 | table: 'users' 102 | }); 103 | 104 | #### Custom database column/field names 105 | 106 | You can store the data using a different field/column name by specifying the 107 | `name` property in the field definition. 108 | 109 | For example, to store `firstName` as `first_name`: 110 | 111 | var User = schema.define('User', { 112 | firstName: { type: String, name: 'first_name' }, 113 | lastName: { type: String, name: 'last_name' } 114 | }); 115 | 116 | ### DB structure synchronization 117 | 118 | Schema instance have two methods for updating db structure: automigrate and 119 | autoupdate. 120 | 121 | The `automigrate` method drop table (if exists) and create it again, 122 | `autoupdate` method generates ALTER TABLE query. Both method accepts callback 123 | called when migration/update done. 124 | 125 | To check if any db changes required use `isActual` method. It accepts single 126 | `callback` argument, which receive boolean value depending on db state: false if 127 | db structure outdated, true when schema and db is in sync, if it returns undefined that means adapter doesn't implements schema checking (most likely schema-less db 128 | or just buggy adapter). 129 | 130 | schema.isActual(function(err, actual) { 131 | if (actual === false) { 132 | schema.autoupdate(); 133 | } 134 | }); 135 | 136 | ## SEE ALSO 137 | 138 | jugglingdb-model(3) 139 | jugglingdb-adapter(3) 140 | -------------------------------------------------------------------------------- /lib/sql.js: -------------------------------------------------------------------------------- 1 | module.exports = BaseSQL; 2 | 3 | /** 4 | * Base SQL class 5 | */ 6 | function BaseSQL() { 7 | } 8 | 9 | BaseSQL.prototype.query = function() { 10 | throw new Error('query method should be declared in adapter'); 11 | }; 12 | 13 | BaseSQL.prototype.command = function(sql, callback) { 14 | return this.query(sql, callback); 15 | }; 16 | 17 | BaseSQL.prototype.queryOne = function(sql, callback) { 18 | return this.query(sql, (err, data) => { 19 | if (err) { 20 | return callback(err); 21 | } 22 | callback(err, data[0]); 23 | }); 24 | }; 25 | 26 | BaseSQL.prototype.table = function(model) { 27 | return this._models[model].model.tableName; 28 | }; 29 | 30 | BaseSQL.prototype.escapeName = function() { 31 | throw new Error('escapeName method should be declared in adapter'); 32 | }; 33 | 34 | BaseSQL.prototype.tableEscaped = function(model) { 35 | return this.escapeName(this.table(model)); 36 | }; 37 | 38 | BaseSQL.prototype.define = function(descr) { 39 | if (!descr.settings) { 40 | descr.settings = {}; 41 | } 42 | this._models[descr.model.modelName] = descr; 43 | }; 44 | 45 | BaseSQL.prototype.defineProperty = function(model, prop, params) { 46 | this._models[model].properties[prop] = params; 47 | }; 48 | 49 | BaseSQL.prototype.escapeId = function(id) { 50 | if (this.schema.settings.slave) { 51 | if (id === null) { 52 | return 'NULL'; 53 | } 54 | return '"' + (typeof id === 'undefined' ? '' : id.toString().replace(/["\n]/g, '')) + '"'; 55 | } 56 | 57 | const idNumber = Number(id); 58 | if (isNaN(idNumber)) { 59 | return '\'' + String(id).replace(/'/g, '') + '\''; 60 | } 61 | 62 | return idNumber; 63 | }; 64 | 65 | BaseSQL.prototype.save = function(model, data, callback) { 66 | const sql = 'UPDATE ' + this.tableEscaped(model) + ' SET ' + this.toFields(model, data) + ' WHERE ' + this.escapeName('id') + ' = ' + this.escapeId(data.id); 67 | 68 | this.query(sql, err => callback(err)); 69 | }; 70 | 71 | 72 | BaseSQL.prototype.exists = function(model, id, callback) { 73 | const sql = 'SELECT 1 FROM ' + 74 | this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ' + this.escapeId(id) + ' LIMIT 1'; 75 | 76 | this.query(sql, (err, data) => { 77 | if (err) { 78 | return callback(err); 79 | } 80 | callback(null, data.length === 1); 81 | }); 82 | }; 83 | 84 | BaseSQL.prototype.find = function find(model, id, callback) { 85 | const idNumber = this.escapeId(id); 86 | const sql = 'SELECT * FROM ' + 87 | this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ' + idNumber + ' LIMIT 1'; 88 | 89 | this.query(sql, (err, data) => { 90 | if (data && data.length === 1) { 91 | data[0].id = id; 92 | } else { 93 | data = [null]; 94 | } 95 | callback(err, this.fromDatabase(model, data[0])); 96 | }); 97 | }; 98 | 99 | BaseSQL.prototype.destroy = function destroy(model, id, callback) { 100 | const sql = 'DELETE FROM ' + 101 | this.tableEscaped(model) + ' WHERE ' + this.escapeName('id') + ' = ' + this.escapeId(id); 102 | 103 | this.command(sql, err => callback(err)); 104 | }; 105 | 106 | BaseSQL.prototype.destroyAll = function destroyAll(model, callback) { 107 | this.command('DELETE FROM ' + this.tableEscaped(model), err => { 108 | if (err) { 109 | return callback(err, []); 110 | } 111 | callback(err); 112 | }); 113 | }; 114 | 115 | BaseSQL.prototype.count = function count(model, callback, where) { 116 | const self = this; 117 | const props = this._models[model].properties; 118 | 119 | this.queryOne( 120 | 'SELECT count(*) as cnt FROM ' + 121 | this.tableEscaped(model) + ' ' + buildWhere(where), 122 | (err, res) => { 123 | if (err) { 124 | return callback(err); 125 | } 126 | callback(err, res && res.cnt); 127 | }); 128 | 129 | function buildWhere(conds) { 130 | const cs = []; 131 | Object.keys(conds || {}).forEach(key => { 132 | const keyEscaped = self.escapeName(key); 133 | if (conds[key] === null) { 134 | cs.push(keyEscaped + ' IS NULL'); 135 | } else { 136 | cs.push(keyEscaped + ' = ' + self.toDatabase(props[key], conds[key])); 137 | } 138 | }); 139 | return cs.length ? ' WHERE ' + cs.join(' AND ') : ''; 140 | } 141 | }; 142 | 143 | BaseSQL.prototype.updateAttributes = function updateAttrs(model, id, data, cb) { 144 | data.id = id; 145 | this.save(model, data, cb); 146 | }; 147 | 148 | BaseSQL.prototype.disconnect = function disconnect(cb) { 149 | this.client.end(cb); 150 | }; 151 | 152 | BaseSQL.prototype.automigrate = function(cb) { 153 | const self = this; 154 | let wait = 0; 155 | Object.keys(this._models).forEach(model => { 156 | wait += 1; 157 | self.dropTable(model, () => { 158 | // console.log('drop', model); 159 | self.createTable(model, err => { 160 | // console.log('create', model); 161 | if (err) { 162 | console.log(err); 163 | } 164 | done(); 165 | }); 166 | }); 167 | }); 168 | 169 | if (wait === 0) { 170 | cb(); 171 | } 172 | 173 | function done() { 174 | wait += -1; 175 | if (wait === 0 && cb) { 176 | cb(); 177 | } 178 | } 179 | }; 180 | 181 | BaseSQL.prototype.dropTable = function(model, cb) { 182 | this.command('DROP TABLE IF EXISTS ' + this.tableEscaped(model), cb); 183 | }; 184 | 185 | BaseSQL.prototype.createTable = function(model, cb) { 186 | this.command('CREATE TABLE ' + this.tableEscaped(model) + 187 | ' (\n ' + this.propertiesSQL(model) + '\n)', cb); 188 | }; 189 | 190 | -------------------------------------------------------------------------------- /lib/scope.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module exports 3 | */ 4 | exports.defineScope = defineScope; 5 | 6 | /** 7 | * Scope mixin for ./model.js 8 | */ 9 | const Model = require('./model.js'); 10 | const when = require('when'); 11 | 12 | /** 13 | * Define scope 14 | * TODO: describe behavior and usage examples 15 | */ 16 | Model.scope = function(name, params) { 17 | defineScope(this, this, name, params); 18 | }; 19 | 20 | function defineScope(cls, targetClass, name, params, methods) { 21 | 22 | // collect meta info about scope 23 | if (!cls._scopeMeta) { 24 | cls._scopeMeta = {}; 25 | } 26 | 27 | // only makes sence to add scope in meta if base and target classes 28 | // are same 29 | if (cls === targetClass) { 30 | cls._scopeMeta[name] = params; 31 | } else if (!targetClass._scopeMeta) { 32 | targetClass._scopeMeta = {}; 33 | } 34 | 35 | Object.defineProperty(cls, name, { 36 | enumerable: false, 37 | configurable: true, 38 | get() { 39 | const f = function caller(condOrRefresh, cb) { 40 | let actualCond; 41 | if (typeof condOrRefresh === 'function') { 42 | cb = condOrRefresh; 43 | actualCond = {}; 44 | condOrRefresh = null; 45 | } else if (typeof condOrRefresh === 'object') { 46 | actualCond = condOrRefresh; 47 | } else { 48 | actualCond = {}; 49 | } 50 | 51 | const cached = this.__cachedRelations && this.__cachedRelations[name]; 52 | if (cached && !condOrRefresh) { 53 | if (typeof cb === 'function') { 54 | return cb(null, cached); 55 | } 56 | return when.resolve(cached); 57 | } 58 | 59 | const self = this; 60 | const params = mergeParams(actualCond, caller._scope); 61 | return targetClass.all(params) 62 | .then(data => { 63 | if (!self.__cachedRelations) { 64 | self.__cachedRelations = {}; 65 | } 66 | self.__cachedRelations[name] = data; 67 | if (typeof cb === 'function') { 68 | cb(null, data); 69 | } else { 70 | return data; 71 | } 72 | }) 73 | .catch(err => { 74 | if (typeof cb === 'function') { 75 | cb(err); 76 | } else { 77 | throw err; 78 | } 79 | }); 80 | }; 81 | f._scope = typeof params === 'function' ? params.call(this) : params; 82 | f.build = build; 83 | f.create = create; 84 | f.destroyAll = destroyAll; 85 | const inst = this; 86 | if (methods) { 87 | Object.keys(methods).forEach(key => { 88 | f[key] = methods[key].bind(inst); 89 | }); 90 | } 91 | 92 | // define sub-scopes 93 | Object.keys(targetClass._scopeMeta).forEach(name => { 94 | Object.defineProperty(f, name, { 95 | enumerable: false, 96 | get() { 97 | mergeParams(f._scope, targetClass._scopeMeta[name]); 98 | return f; 99 | } 100 | }); 101 | }); 102 | return f; 103 | } 104 | }); 105 | 106 | // and it should have create/build methods with binded thisModelNameId param 107 | function build(data) { 108 | return new targetClass(mergeParams(this._scope, { where:data || {} }).where); 109 | } 110 | 111 | function create(data, cb) { 112 | if (typeof data === 'function') { 113 | cb = data; 114 | data = {}; 115 | } 116 | return this.build(data).save(cb); 117 | } 118 | 119 | /* 120 | Callback 121 | - The callback will be called after all elements are destroyed 122 | - For every destroy call which results in an error 123 | - If fetching the Elements on which destroyAll is called results in an error 124 | */ 125 | function destroyAll(cb) { 126 | targetClass.all(this._scope, (err, data) => { 127 | if (err) { 128 | cb(err); 129 | } else { 130 | (function loopOfDestruction(data) { 131 | if (data.length > 0) { 132 | data.shift().destroy(err => { 133 | if (err && cb) { 134 | cb(err); 135 | } 136 | loopOfDestruction(data); 137 | }); 138 | } else if (cb) { 139 | cb(); 140 | } 141 | }(data)); 142 | } 143 | }); 144 | } 145 | 146 | function mergeParams(base, update) { 147 | if (update.where) { 148 | base.where = merge(base.where, update.where); 149 | } 150 | if (update.include) { 151 | base.include = update.include; 152 | } 153 | if (update.collect) { 154 | base.collect = update.collect; 155 | } 156 | 157 | // overwrite order 158 | if (update.order) { 159 | base.order = update.order; 160 | } 161 | 162 | return base; 163 | 164 | } 165 | } 166 | 167 | /** 168 | * Merge `base` and `update` params 169 | * @param {Object} base - base object (updating this object) 170 | * @param {Object} update - object with new data to update base 171 | * @returns {Object} `base` 172 | */ 173 | function merge(base, update) { 174 | base = base || {}; 175 | 176 | if (update) { 177 | Object.keys(update).forEach(key => base[key] = update[key]); 178 | } 179 | 180 | return base; 181 | } 182 | 183 | -------------------------------------------------------------------------------- /lib/adapters/http.js.1: -------------------------------------------------------------------------------- 1 | /* global $ */ 2 | 3 | exports.initialize = function initializeSchema(schema, callback) { 4 | schema.adapter = new WebService(); 5 | process.nextTick(callback); 6 | }; 7 | 8 | function WebService() { 9 | this._models = {}; 10 | this.cache = {}; 11 | this.ids = {}; 12 | } 13 | 14 | WebService.prototype.installPostProcessor = function installPostProcessor(descr) { 15 | var dates = []; 16 | Object.keys(descr.properties).forEach(function(column) { 17 | if (descr.properties[column].type.name === 'Date') { 18 | dates.push(column); 19 | } 20 | }); 21 | 22 | var postProcessor = function(model) { 23 | var max = dates.length; 24 | for (var i = 0; i < max; i++) { 25 | var column = dates[i]; 26 | if (model[column]) { 27 | model[column] = new Date(model[column]); 28 | } 29 | } 30 | }; 31 | 32 | descr.postProcessor = postProcessor; 33 | }; 34 | 35 | WebService.prototype.preProcess = function preProcess(data) { 36 | var result = {}; 37 | Object.keys(data).forEach(function(key) { 38 | if (data[key] !== null) { 39 | result[key] = data[key]; 40 | } 41 | }); 42 | return result; 43 | }; 44 | 45 | WebService.prototype.postProcess = function postProcess(model, data) { 46 | var postProcessor = this._models[model].postProcessor; 47 | if (postProcessor && data) { 48 | postProcessor(data); 49 | } 50 | }; 51 | 52 | WebService.prototype.postProcessMultiple = function postProcessMultiple(model, data) { 53 | var postProcessor = this._models[model].postProcessor; 54 | if (postProcessor) { 55 | var max = data.length; 56 | for (var i = 0; i < max; i++) { 57 | if (data[i]) { 58 | postProcessor(data[i]); 59 | } 60 | } 61 | } 62 | }; 63 | 64 | WebService.prototype.define = function defineModel(descr) { 65 | var m = descr.model.modelName; 66 | this.installPostProcessor(descr); 67 | this._models[m] = descr; 68 | }; 69 | 70 | WebService.prototype.getResourceUrl = function getResourceUrl(model) { 71 | var url = this._models[model].settings.restPath; 72 | if (!url) throw new Error('Resource url (restPath) for ' + model + ' is not defined'); 73 | return url; 74 | }; 75 | 76 | WebService.prototype.getBlankReq = function() { 77 | if (!this.csrfToken) { 78 | this.csrfToken = $('meta[name=csrf-token]').attr('content'); 79 | this.csrfParam = $('meta[name=csrf-param]').attr('content'); 80 | } 81 | var req = {}; 82 | req[this.csrfParam] = this.csrfToken; 83 | return req; 84 | }; 85 | 86 | WebService.prototype.create = function create(model, data, callback) { 87 | var req = this.getBlankReq(); 88 | req[model] = this.preProcess(data); 89 | $.post(this.getResourceUrl(model) + '.json', req, function(res) { 90 | if (res.code === 200) { 91 | callback(null, res.data.id); 92 | } else { 93 | callback(res.error); 94 | } 95 | }, 'json'); 96 | // this.cache[model][id] = data; 97 | }; 98 | 99 | WebService.prototype.updateOrCreate = function(model, data, callback) { 100 | var mem = this; 101 | this.exists(model, data.id, function(err, exists) { 102 | if (exists) { 103 | mem.save(model, data, callback); 104 | } else { 105 | mem.create(model, data, function(err, id) { 106 | data.id = id; 107 | callback(err, data); 108 | }); 109 | } 110 | }); 111 | }; 112 | 113 | WebService.prototype.save = function save(model, data, callback) { 114 | var _this = this; 115 | var req = this.getBlankReq(); 116 | req._method = 'PUT'; 117 | req[model] = this.preProcess(data); 118 | $.post(this.getResourceUrl(model) + '/' + data.id + '.json', req, function(res) { 119 | if (res.code === 200) { 120 | _this.postProcess(model, res.data); 121 | callback(null, res.data); 122 | } else { 123 | callback(res.error); 124 | } 125 | }, 'json'); 126 | }; 127 | 128 | WebService.prototype.exists = function exists(model, id, callback) { 129 | $.getJSON(this.getResourceUrl(model) + '/' + id + '.json', function(res) { 130 | if (res.code === 200) { 131 | callback(null, true); 132 | } else if (res.code === 404) { 133 | callback(null, false); 134 | } else { 135 | callback(res.error); 136 | } 137 | }); 138 | }; 139 | 140 | WebService.prototype.find = function find(model, id, callback) { 141 | var _this = this; 142 | $.getJSON(this.getResourceUrl(model) + '/' + id + '.json', function(res) { 143 | if (res.code === 200) { 144 | _this.postProcess(model, res.data); 145 | callback(null, res.data); 146 | } else { 147 | callback(res.error); 148 | } 149 | }); 150 | }; 151 | 152 | WebService.prototype.destroy = function destroy(model, id, callback) { 153 | var req = this.getBlankReq(); 154 | req._method = 'DELETE'; 155 | $.post(this.getResourceUrl(model) + '/' + id + '.json', req, function(res) { 156 | if (res.code === 200) { 157 | //delete _this.cache[model][id]; 158 | callback(null, res.data); 159 | } else { 160 | callback(res.error); 161 | } 162 | }, 'json'); 163 | }; 164 | 165 | WebService.prototype.all = function all(model, filter, callback) { 166 | var _this = this; 167 | $.getJSON(this.getResourceUrl(model) + '.json?query=' + encodeURIComponent(JSON.stringify(filter)), function(res) { 168 | if (res.code === 200) { 169 | _this.postProcessMultiple(model, res.data); 170 | callback(null, res.data); 171 | } else { 172 | callback(res.error); 173 | } 174 | }); 175 | }; 176 | 177 | WebService.prototype.destroyAll = function destroyAll() { 178 | throw new Error('Not supported'); 179 | }; 180 | 181 | WebService.prototype.count = function count() { 182 | throw new Error('Not supported'); 183 | }; 184 | 185 | WebService.prototype.updateAttributes = function(model, id, data, callback) { 186 | data.id = id; 187 | this.save(model, data, callback); 188 | }; 189 | 190 | -------------------------------------------------------------------------------- /test/model.test.js: -------------------------------------------------------------------------------- 1 | const should = require('./init.js'); 2 | const expect = require('expect'); 3 | 4 | let db, Model; 5 | 6 | /* global getSchema */ 7 | 8 | describe('Model', function() { 9 | 10 | before(function() { 11 | db = getSchema(); 12 | Model = db.define('Model', function(m) { 13 | m.property('field', String, { index: true }); 14 | }); 15 | }); 16 | 17 | it('should reset prev data on save', function(done) { 18 | const inst = new Model({ field: 'hello' }); 19 | inst.field = 'world'; 20 | inst.save().then(function(s) { 21 | s.field.should.equal('world'); 22 | s.propertyChanged('field').should.be.false; 23 | done(); 24 | }).catch(done); 25 | }); 26 | 27 | describe('#toString', function() { 28 | 29 | it('should add model name to stringified representation', function() { 30 | Model.toString().should.equal('[Model Model]'); 31 | }); 32 | 33 | }); 34 | 35 | describe('fetch', function() { 36 | 37 | it('should find record by id', function() { 38 | const randomNumber = Math.random(); 39 | return Model.create({ field: 'test' + randomNumber }) 40 | .then(function(inst) { 41 | return Model.fetch(inst.id); 42 | }) 43 | .then(function(inst) { 44 | expect(inst).toExist(); 45 | expect(inst.field).toBe('test' + randomNumber); 46 | }); 47 | }); 48 | 49 | it('should result in error when not found', function() { 50 | return Model.destroyAll() 51 | .then(function() { return Model.fetch(1); }) 52 | .then(function() { 53 | throw new Error('Unexpected success'); 54 | }) 55 | .catch(function(err) { 56 | expect(err).toExist(); 57 | expect(err.code).toBe('not_found'); 58 | expect(err.details).toExist(); 59 | expect(err.details.id).toBe(1); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('reload', function() { 65 | 66 | it('should reload model from db', function() { 67 | let cached; 68 | return Model.create({ field: 'hello' }) 69 | .then(function(inst) { 70 | cached = inst; 71 | return Model.bulkUpdate({ 72 | where: { id: inst.id }, 73 | update: { field: 'data' } 74 | }); 75 | }) 76 | .then(function() { 77 | return cached.reload(); 78 | }) 79 | .then(function(inst) { 80 | inst.field.should.equal('data'); 81 | }); 82 | }); 83 | 84 | }); 85 | 86 | describe('upsert', function() { 87 | 88 | it('should create record when no id provided', function() { 89 | return Model.upsert({ field: 'value' }) 90 | .then(function(inst) { 91 | should.exist(inst); 92 | should.exist(inst.id); 93 | }); 94 | }); 95 | 96 | context('adapter does not support upsert', function() { 97 | let updateOrCreate, find, save, updateAttributes; 98 | 99 | beforeEach(function() { 100 | updateOrCreate = Model.schema.adapter.updateOrCreate; 101 | updateAttributes = Model.prototype.updateAttributes; 102 | find = Model.schema.adapter.find; 103 | save = Model.schema.adapter.save; 104 | Model.schema.adapter.updateOrCreate = null; 105 | }); 106 | 107 | afterEach(function() { 108 | Model.schema.adapter.updateOrCreate = updateOrCreate; 109 | Model.prototype.updateAttributes = updateAttributes; 110 | Model.prototype.save = save; 111 | Model.schema.adapter.find = find; 112 | }); 113 | 114 | it('should find and update when found', function() { 115 | 116 | Model.schema.adapter.find = function(modelName, id, cb) { 117 | cb(null, { id: 1602, field: 'hello there' }); 118 | }; 119 | 120 | Model.prototype.updateAttributes = function(data, cb) { 121 | this.__data.field = data.field; 122 | cb(null, this); 123 | }; 124 | 125 | return Model.upsert({ id: 1602, field: 'value' }, function(err, inst) { 126 | should.not.exist(err); 127 | should.exist(inst); 128 | inst.id.should.equal(1602); 129 | inst.field.should.equal('value'); 130 | }); 131 | 132 | }); 133 | 134 | it('should try to find and create when not found', function() { 135 | 136 | Model.schema.adapter.find = function(modelName, id, cb) { 137 | cb(null, null); 138 | }; 139 | 140 | Model.prototype.save = function(data, cb) { 141 | this.__data.field = data.field; 142 | cb(null, this); 143 | }; 144 | 145 | return Model.upsert({ 146 | id: 1602, 147 | field: 'value' 148 | }, function(err, inst) { 149 | should.not.exist(err); 150 | should.exist(inst); 151 | inst.id.should.equal(1602); 152 | inst.field.should.equal('value'); 153 | }); 154 | 155 | }); 156 | 157 | it('should throw if find returns error', function() { 158 | 159 | Model.schema.adapter.find = function(modelName, id, cb) { 160 | cb(new Error('Uh-oh')); 161 | }; 162 | 163 | return Model.upsert({ id: 1602, field: 'value' }, function(err, inst) { 164 | should.exist(err); 165 | err.message.should.equal('Uh-oh'); 166 | }); 167 | 168 | }); 169 | 170 | }); 171 | 172 | }); 173 | 174 | describe('findOrCreate', function() { 175 | 176 | it('should find and create if not found', function() { 177 | 178 | return Model.findOrCreate() 179 | .then(function(inst) { 180 | should.exist(inst); 181 | }); 182 | }); 183 | 184 | }); 185 | 186 | describe('exists', function() { 187 | 188 | it('should return error when falsy id provided', function() { 189 | Model.exists(null, function(err) { 190 | should.exist(err); 191 | }); 192 | }); 193 | 194 | }); 195 | 196 | describe('fromObject', function() { 197 | 198 | it('should mutate existing object', function() { 199 | const inst = new Model(); 200 | inst.fromObject({ field: 'haha' }); 201 | inst.field.should.equal('haha'); 202 | }); 203 | 204 | }); 205 | 206 | }); 207 | 208 | -------------------------------------------------------------------------------- /test/schema.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | const Schema = require ('../').Schema; 4 | 5 | let db = getSchema(), slave = getSchema(), Model, SlaveModel; 6 | 7 | describe('schema', function() { 8 | 9 | it('should define Model', function() { 10 | Model = db.define('Model'); 11 | Model.schema.should.eql(db); 12 | const m = new Model; 13 | m.schema.should.eql(db); 14 | }); 15 | 16 | it('should clone existing model', function() { 17 | SlaveModel = slave.copyModel(Model); 18 | SlaveModel.schema.should.eql(slave); 19 | slave.should.not.eql(db); 20 | const sm = new SlaveModel; 21 | sm.should.be.instanceOf(Model); 22 | sm.schema.should.not.eql(db); 23 | sm.schema.should.eql(slave); 24 | }); 25 | 26 | it('should automigrate', function(done) { 27 | db.automigrate(done); 28 | }); 29 | 30 | it('should create transaction', function(done) { 31 | const tr = db.transaction(); 32 | tr.connected.should.be.false; 33 | tr.connecting.should.be.false; 34 | let called = false; 35 | tr.models.Model.should.not.equal(db.models.Model); 36 | tr.models.Model.create([{},{}, {}], function() { 37 | called = true; 38 | }); 39 | tr.connected.should.be.false; 40 | tr.connecting.should.be.true; 41 | 42 | db.models.Model.count(function(err, c) { 43 | should.not.exist(err); 44 | should.exist(c); 45 | c.should.equal(0); 46 | called.should.be.false; 47 | tr.exec(function() { 48 | setTimeout(function() { 49 | called.should.be.true; 50 | db.models.Model.count(function(err, c) { 51 | c.should.equal(3); 52 | done(); 53 | }); 54 | }, 100); 55 | }); 56 | }); 57 | }); 58 | 59 | describe('isActual', function() { 60 | 61 | it('should delegate schema check to adapter', function(done) { 62 | const db = new Schema('memory'); 63 | db.adapter.isActual = function(cb) { 64 | return cb(null, true); 65 | }; 66 | 67 | db.isActual(function(err, result) { 68 | result.should.be.true(); 69 | done(); 70 | }); 71 | }); 72 | 73 | it('should return undefined when adapter is schema-less', function(done) { 74 | const db = new Schema('memory'); 75 | delete db.adapter.isActual; 76 | 77 | db.isActual(function(err, result) { 78 | (typeof result).should.equal('undefined'); 79 | done(); 80 | }); 81 | }); 82 | 83 | }); 84 | 85 | describe('autoupdate', function() { 86 | 87 | it('should delegate autoupdate to adapter', function(done) { 88 | const db = new Schema('memory'); 89 | db.adapter = { 90 | autoupdate: done 91 | }; 92 | db.autoupdate(); 93 | }); 94 | 95 | }); 96 | 97 | describe('automigrate', function() { 98 | 99 | it('should delegate automigrate to adapter', function() { 100 | const db = new Schema('memory'); 101 | let called = false; 102 | db.adapter.automigrate = function(cb) { 103 | process.nextTick(function() { 104 | called = true; 105 | cb(null); 106 | }); 107 | }; 108 | 109 | return db.automigrate() 110 | .then(function() { 111 | return called.should.be.true(); 112 | }); 113 | }); 114 | 115 | it('should reject in case of error', function() { 116 | const db = new Schema('memory'); 117 | const called = false; 118 | db.adapter.automigrate = function(cb) { 119 | throw new Error('Oopsie'); 120 | }; 121 | 122 | return db.automigrate() 123 | .then(function() { 124 | throw new Error('Unexpected success'); 125 | }) 126 | .catch(function(err) { 127 | err.message.should.equal('Oopsie'); 128 | }); 129 | }); 130 | 131 | }); 132 | 133 | describe('defineForeignKey', function() { 134 | 135 | it('should allow adapter to define foreign key', function(done) { 136 | const db = new Schema('memory'); 137 | db.define('User', { something: Number }); 138 | db.adapter = { 139 | defineForeignKey(model, prop, cb) { 140 | cb(null, Number); 141 | done(); 142 | } 143 | }; 144 | db.defineForeignKey('User', 'appId'); 145 | }); 146 | 147 | }); 148 | 149 | describe('connect', function() { 150 | 151 | it('should delegate connect to adapter', function(done) { 152 | const db = new Schema({ 153 | initialize(schema, cb) { 154 | schema.adapter = { 155 | connect(cb) { 156 | cb(); 157 | } 158 | }; 159 | } 160 | }); 161 | db.once('connected', done); 162 | db.connect(); 163 | }); 164 | 165 | it('should support adapters without connections', function() { 166 | const db = new Schema({ 167 | initialize(schema, cb) { 168 | schema.adapter = {}; 169 | } 170 | }); 171 | return db.connect() 172 | .then(function(schema) { 173 | schema.connecting.should.be.false(); 174 | }); 175 | }); 176 | 177 | it('should catch connection errors', function() { 178 | const db = new Schema({ 179 | initialize(schema, cb) { 180 | schema.adapter = { 181 | connect(cb) { 182 | cb(new Error('Connection error')); 183 | } 184 | }; 185 | } 186 | }); 187 | 188 | return db.connect() 189 | .then(function() { 190 | throw new Error('Unexpected success'); 191 | }) 192 | .catch(function(err) { 193 | err.message.should.equal('Connection error'); 194 | }); 195 | }); 196 | 197 | }); 198 | 199 | describe('disconnect', function() { 200 | 201 | it('should delegate disconnection to adapter', function(done) { 202 | const db = new Schema('memory'); 203 | db.adapter = { 204 | disconnect: done 205 | }; 206 | db.disconnect(); 207 | }); 208 | 209 | it('should call callback with "disconnect" is not handled by adapter', function(done) { 210 | const db = new Schema('memory'); 211 | delete db.adapter.disconnect; 212 | db.disconnect(done); 213 | }); 214 | 215 | }); 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /docs/model.md: -------------------------------------------------------------------------------- 1 | jugglingdb-model(3) - Model methods, features and internals 2 | =================== 3 | 4 | ## DESCRIPTION 5 | 6 | This section describes common methods of models managed by jugglingdb and 7 | explains some model internals, such as data representation, setters, getters and 8 | virtual attributes. 9 | 10 | ## DB WRITE METHODS 11 | 12 | Database write methods performs hooks and validations. See jugglingdb-hooks(3) 13 | and jugglingdb-validations(3) to learn how hooks and validations works. 14 | 15 | ### Model.create([data[, callback]]); 16 | 17 | Create instance of Model with given data and save to database. 18 | Invoke callback when ready. Callback accepts two arguments: error and model 19 | instance. 20 | 21 | User.create({name: 'Jared Hanson'}, function(err, user) { 22 | console.log(user instanceof User); 23 | }); 24 | 25 | When called with array of objects as first argument `Model.create` creates bunch 26 | of records. Both `err` and `model instance` arguments passed to callback will be 27 | arrays then. When no errors happened `err` argument will be null. 28 | 29 | The value returned from `Model.create` depends on second argument too. In case 30 | of Array it will return an array of instances, otherwise single instance. But be 31 | away, this instance(s) aren't save to database yet and you have to wait until 32 | callback called to be able to do id-sensitive stuff. 33 | 34 | ### Model.prototype.save([options[, callback]]); 35 | 36 | Save instance to database, options is an object {validate: true, throws: false}, 37 | it allows to turn off validation (turned on by default) and throw error on 38 | validation error (doesn't throws by default). 39 | 40 | user.email = 'incorrect email'; 41 | user.save({throws: true}, callback); // will throw ValidationError 42 | user.save({validate: false}, callback); // will save incorrect data 43 | user.save(function(err, user) { 44 | console.log(err); // ValidationError 45 | console.log(user.errors); // some errors 46 | }); 47 | 48 | ### Model.prototype.updateAttributes(data[, callback]); 49 | 50 | Save specified attributes database. 51 | Invoke callback when ready. Callback accepts two arguments: error and model 52 | instance. 53 | 54 | user.updateAttributes({ 55 | email: 'new-email@example.com', 56 | name: 'New Name' 57 | }, callback); 58 | 59 | ### Model.prototype.updateAttribute(key, value[, callback]); 60 | 61 | Shortcut for updateAttributes, but for one field, works in the save way as 62 | updateAttributes. 63 | 64 | user.updateAttribute('email', 'new-email@example.com', callback); 65 | 66 | ### Model.upsert(data, callback) 67 | 68 | Update when record with id=data.id found, insert otherwise. Be aware: no 69 | setters, validations or hooks applied when use upsert. This is seed-friendly 70 | method. 71 | 72 | ### Model.prototype.destroy([callback]); 73 | 74 | Delete database record. 75 | Invoke callback when ready. Callback accepts two arguments: error and model 76 | instance. 77 | 78 | model.destroy(function(err) { 79 | // model instance destroyed 80 | }); 81 | 82 | ### Model.destroyAll(callback) 83 | 84 | Delete all Model instances from database. Be aware: `destroyAll` method doesn't 85 | perform destroy hooks. 86 | 87 | ### Model.iterate(options, iterator, callback) 88 | 89 | Iterate through dataset and perform async method iterator. This method designed 90 | to work with large datasets loading data by batches. First argument (options) is 91 | optional and have same signature as for Model.all, it has additional member 92 | `batchSize` which allows to specify size of batch loaded into memory from the 93 | database. 94 | 95 | Iterator argument is a function that accepts three arguments: item, callback and 96 | index in collection. 97 | 98 | Model.iterate({batchSize: 100}, function(obj, next, i) { 99 | doSomethingAsync(obj, next); 100 | }, function(err) { 101 | // all done 102 | }); 103 | 104 | ## DB READ METHODS 105 | 106 | ### Model.find(id, callback); 107 | 108 | Find instance by id. 109 | Invoke callback when ready. Callback accepts two arguments: error and model 110 | instance. 111 | 112 | ### Model.all([params, ]callback); 113 | 114 | Find all instances of Model, matched by query. Fields used for filter and sort 115 | should be declared with `{index: true}` in model definition. 116 | 117 | * `param`: 118 | * where: Object `{ key: val, key2: {gt: 'val2'}}` 119 | * include: String, Object or Array. See AbstractClass.include documentation. 120 | * order: String 121 | * limit: Number 122 | * skip: Number 123 | 124 | * `callback`: 125 | Accepts two arguments: 126 | * err (null or Error) 127 | * Array of instances 128 | 129 | ### Model.count([query, ]callback); 130 | 131 | Query count of instances stored in database. Optional `query` param allows to 132 | count filtered set of records. Callback called with error and count arguments. 133 | 134 | User.count({approved: true}, function(err, count) { 135 | console.log(count); // count of approved users stored in database 136 | }); 137 | 138 | ## RELATIONS 139 | 140 | ### hasMany 141 | 142 | Define all necessary stuff for "one to many" relation: 143 | 144 | * foreign key in "many" model 145 | * named scope in "one" model 146 | 147 | Example: 148 | 149 | var Book = db.define('Book'); 150 | var Chapter = db.define('Chapters'); 151 | 152 | // syntax 1 (old): 153 | Book.hasMany(Chapter); 154 | // syntax 2 (new): 155 | Book.hasMany('chapters'); 156 | 157 | Syntax 1 and 2 does same things in different ways: adds `chapters` method to 158 | `Book.prototype` and add `bookId` property to `Chapter` model. Foreign key name 159 | (`bookId`) could be specified manually using second param: 160 | 161 | Book.hasMany('chapters', {foreignKey: `chapter_id`}); 162 | 163 | When using syntax 2 jugglingdb looking for model with singularized name: 164 | 165 | 'chapters' => 'chapter' => 'Chapter' 166 | 167 | But it's possible to specify model manually using second param: 168 | 169 | Book.hasMany('stories', {model: Chapter}); 170 | 171 | Syntax 1 allows to override scope name using `as` property of second param: 172 | 173 | Book.hasMany(Chapter, {as: 'stories'}); 174 | 175 | **Scope methods** created on BaseClass by hasMany allows to build, create and 176 | query instances of other class. For example: 177 | 178 | Book.create(function(err, book) { 179 | // using 'chapters' scope for build: 180 | var c = book.chapters.build({name: 'Chapter 1'}); 181 | // same as: 182 | c = new Chapter({name: 'Chapter 1', bookId: book.id}); 183 | // using 'chapters' scope for create: 184 | book.chapters.create(); 185 | // same as: 186 | Chapter.create({bookId: book.id}); 187 | 188 | // using scope for querying: 189 | book.chapters(function() {/* all chapters with bookId = book.id */ }); 190 | book.chapters({where: {name: 'test'}, function(err, chapters) { 191 | // all chapters with bookId = book.id and name = 'test' 192 | }); 193 | }); 194 | 195 | ### belongsTo 196 | 197 | TODO: document 198 | 199 | ### hasAndBelongsToMany 200 | 201 | TODO: document 202 | 203 | ## SEE ALSO 204 | 205 | jugglingdb-schema(3) 206 | jugglingdb-validations(3) 207 | jugglingdb-hooks(3) 208 | jugglingdb-adapter(3) 209 | -------------------------------------------------------------------------------- /lib/include.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | const i8n = require('inflection'); 5 | 6 | /** 7 | * Include mixin for ./model.js 8 | */ 9 | const AbstractClass = require('./model.js'); 10 | 11 | /** 12 | * Allows you to load relations of several objects and optimize numbers of requests. 13 | * 14 | * @param {Array} objects - array of instances 15 | * @param {String}, {Object} or {Array} include - which relations you want to load. 16 | * @param {Function} cb - Callback called when relations are loaded 17 | * 18 | * Examples: 19 | * 20 | * - User.include(users, 'posts', function() {}); will load all users posts with only one additional request. 21 | * - User.include(users, ['posts'], function() {}); // same 22 | * - User.include(users, ['posts', 'passports'], function() {}); // will load all users posts and passports with two 23 | * additional requests. 24 | * - Passport.include(passports, {owner: 'posts'}, function() {}); // will load all passports owner (users), and all 25 | * posts of each owner loaded 26 | * - Passport.include(passports, {owner: ['posts', 'passports']}); // ... 27 | * - Passport.include(passports, {owner: [{posts: 'images'}, 'passports']}); // ... 28 | * 29 | */ 30 | 31 | /*jshint sub: true */ 32 | 33 | AbstractClass.include = function(objects, include, cb) { 34 | 35 | if ((include.constructor.name === 'Array' && include.length === 0) || (include.constructor.name === 'Object' && Object.keys(include).length === 0)) { 36 | cb(null, objects); 37 | return; 38 | } 39 | 40 | include = processIncludeJoin(include); 41 | 42 | const keyVals = {}; 43 | const objsByKeys = {}; 44 | 45 | let nbCallbacks = 0; 46 | let totalCallbacks = 0; 47 | 48 | let callback; 49 | 50 | for (let i = 0; i < include.length; i += 1) { 51 | callback = processIncludeItem(this, objects, include[i], keyVals, objsByKeys); 52 | if (callback !== null) { 53 | totalCallbacks += 1; 54 | if (callback instanceof Error) { 55 | cb(callback); 56 | } else { 57 | includeItemCallback(callback); 58 | } 59 | } 60 | } 61 | 62 | if (totalCallbacks === 0) { 63 | cb(null, objects); 64 | } 65 | 66 | function includeItemCallback(itemCb) { 67 | nbCallbacks += 1; 68 | itemCb(() => { 69 | nbCallbacks += -1; 70 | if (nbCallbacks === 0) { 71 | cb(null, objects); 72 | } 73 | }); 74 | } 75 | 76 | function processIncludeJoin(ij) { 77 | if (typeof ij === 'string') { 78 | ij = [ij]; 79 | } 80 | if (ij.constructor.name === 'Object') { 81 | const newIj = []; 82 | Object.keys(ij).forEach(key => { 83 | const obj = {}; 84 | obj[key] = ij[key]; 85 | newIj.push(obj); 86 | }); 87 | return newIj; 88 | } 89 | return ij; 90 | } 91 | }; 92 | 93 | function processIncludeItem(cls, objs, include, keyVals, objsByKeys) { 94 | const relations = cls.relations; 95 | let relationName, subInclude; 96 | 97 | if (include.constructor.name === 'Object') { 98 | relationName = Object.keys(include)[0]; 99 | subInclude = include[relationName]; 100 | } else { 101 | relationName = include; 102 | subInclude = []; 103 | } 104 | 105 | const relation = relations[relationName]; 106 | 107 | if (!relation) { 108 | return new Error('Relation "' + relationName + '" is not defined for ' + cls.modelName + ' model'); 109 | } 110 | 111 | const req = { 112 | 'where': {} 113 | }; 114 | 115 | if (!keyVals[relation.keyFrom]) { 116 | objsByKeys[relation.keyFrom] = {}; 117 | objs.filter(Boolean).forEach(obj => { 118 | if (!objsByKeys[relation.keyFrom][obj[relation.keyFrom]]) { 119 | objsByKeys[relation.keyFrom][obj[relation.keyFrom]] = []; 120 | } 121 | objsByKeys[relation.keyFrom][obj[relation.keyFrom]].push(obj); 122 | }); 123 | keyVals[relation.keyFrom] = Object.keys(objsByKeys[relation.keyFrom]); 124 | } 125 | 126 | // deep clone is necessary since inq seems to change the processed array 127 | const keysToBeProcessed = {}; 128 | const inValues = []; 129 | for (let j = 0; j < keyVals[relation.keyFrom].length; j += 1) { 130 | keysToBeProcessed[keyVals[relation.keyFrom][j]] = true; 131 | if (keyVals[relation.keyFrom][j] !== 'null' && keyVals[relation.keyFrom][j] !== 'undefined') { 132 | inValues.push(keyVals[relation.keyFrom][j]); 133 | } 134 | } 135 | 136 | let _model, _through; 137 | 138 | function done(err, objsIncluded, cb) { 139 | let objectsFrom; 140 | 141 | for (let i = 0; i < objsIncluded.length; i += 1) { 142 | delete keysToBeProcessed[objsIncluded[i][relation.keyTo]]; 143 | objectsFrom = objsByKeys[relation.keyFrom][objsIncluded[i][relation.keyTo]]; 144 | 145 | for (let j = 0; j < objectsFrom.length; j += 1) { 146 | if (!objectsFrom[j].__cachedRelations) { 147 | objectsFrom[j].__cachedRelations = {}; 148 | } 149 | 150 | if (relation.multiple) { 151 | if (!objectsFrom[j].__cachedRelations[relationName]) { 152 | objectsFrom[j].__cachedRelations[relationName] = []; 153 | } 154 | 155 | if (_through) { 156 | objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i].__cachedRelations[_through]); 157 | } else { 158 | objectsFrom[j].__cachedRelations[relationName].push(objsIncluded[i]); 159 | } 160 | 161 | } else if (_through) { 162 | objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i].__cachedRelations[_through]; 163 | } else { 164 | objectsFrom[j].__cachedRelations[relationName] = objsIncluded[i]; 165 | } 166 | } 167 | } 168 | 169 | // No relation have been found for these keys 170 | Object.keys(keysToBeProcessed).forEach(key => { 171 | objectsFrom = objsByKeys[relation.keyFrom][key]; 172 | for (let k = 0; k < objectsFrom.length; k += 1) { 173 | if (!objectsFrom[k].__cachedRelations) { 174 | objectsFrom[k].__cachedRelations = {}; 175 | } 176 | objectsFrom[k].__cachedRelations[relationName] = relation.multiple ? [] : null; 177 | } 178 | }); 179 | 180 | cb(err, objsIncluded); 181 | } 182 | 183 | if (keyVals[relation.keyFrom].length > 0) { 184 | 185 | if (relation.modelThrough) { 186 | req['where'][relation.keyTo] = { inq: inValues }; 187 | 188 | _model = cls.schema.models[relation.modelThrough.modelName]; 189 | _through = i8n.camelize(relation.modelTo.modelName, true); 190 | 191 | } else { 192 | req['where'][relation.keyTo] = { inq: inValues }; 193 | req['include'] = subInclude; 194 | } 195 | 196 | return function(cb) { 197 | 198 | if (_through) { 199 | 200 | relation.modelThrough.all(req, (err, objsIncluded) => { 201 | 202 | _model.include(objsIncluded, _through, (err, throughIncludes) => { 203 | 204 | done(err, throughIncludes, cb); 205 | }); 206 | }); 207 | 208 | } else { 209 | 210 | relation.modelTo.all(req, (err, objsIncluded) => { 211 | 212 | done(err, objsIncluded, cb); 213 | }); 214 | } 215 | }; 216 | } 217 | 218 | return null; 219 | } 220 | -------------------------------------------------------------------------------- /lib/railway.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const Schema = require('./schema').Schema; 4 | 5 | const existsSync = fs.existsSync || path.existsSync; 6 | 7 | /* global railway */ 8 | 9 | if (global.railway) { 10 | railway.orm._schemas = []; 11 | } 12 | 13 | module.exports = function init(root) { 14 | let railway, app, models; 15 | 16 | if (typeof root !== 'object' || (root.constructor.name !== 'Compound' && root.constructor.name !== 'CompoundServer')) { 17 | railway = global.railway; 18 | app = global.app; 19 | models = app.models; 20 | } else { 21 | railway = root; 22 | app = railway.app; 23 | root = railway.root; 24 | models = railway.models; 25 | } 26 | 27 | railway.orm._schemas = []; 28 | 29 | const confFile = (root || app.root) + '/config/database'; 30 | const appConf = app.get('database'); 31 | let config = railway.orm.config = appConf || {}; 32 | const env = app.set('env'); 33 | let schema; 34 | 35 | if (!railway.parent) { 36 | if (!appConf) { 37 | try { 38 | let cf = require(confFile); 39 | if (cf instanceof Array) { 40 | cf = cf[0]; 41 | } 42 | if (typeof cf === 'function') { 43 | config = cf(railway); 44 | } else { 45 | config = cf[env]; 46 | } 47 | } catch (e) { 48 | console.log('Could not load config/database.{js|json|yml}'); 49 | throw e; 50 | } 51 | } 52 | 53 | if (!config) { 54 | console.log('No environment ' + env + ' found in config/database.{js|json|yml}'); 55 | throw new Error('No environment ' + env + ' found in config/database.{js|json|yml}'); 56 | } 57 | 58 | // when driver name started with point - look for driver in app root (relative path) 59 | if (config.driver && config.driver.match(/^\./)) { 60 | config.driver = path.join(app.root, config.driver); 61 | } 62 | 63 | schema = new Schema(config && config.driver || 'memory', config); 64 | schema.log = log; 65 | if (!schema.adapter) { 66 | throw new Error('Adapter is not defined'); 67 | } 68 | 69 | } else { 70 | schema = railway.parent.orm._schemas[0]; 71 | } 72 | 73 | if (schema.waitForConnect) { 74 | schema.on('connected', () => loadSchema(schema, railway, app, models)); 75 | } else { 76 | loadSchema(schema, railway, app, models); 77 | } 78 | 79 | // check validations and display warning 80 | 81 | function loadSchema(schema, railway, app, models) { 82 | railway.orm._schemas.push(schema); 83 | 84 | const context = prepareContext(models, railway, app, schema); 85 | 86 | // run schema first 87 | let schemaFile = (root || app.root) + '/db/schema.'; 88 | if (existsSync(schemaFile + 'js')) { 89 | schemaFile += 'js'; 90 | } else if (existsSync(schemaFile + 'coffee')) { 91 | schemaFile += 'coffee'; 92 | } else { 93 | schemaFile = false; 94 | } 95 | 96 | if (schemaFile) { 97 | let code = fs.readFileSync(schemaFile).toString(); 98 | if (schemaFile.match(/\.coffee$/)) { 99 | code = require('coffee-script').compile(code); 100 | } 101 | /*jshint evil: true */ 102 | const fn = new Function('context', 'require', 'with(context){(function(){' + code + '})()}'); 103 | fn(context, require); 104 | } 105 | 106 | // autoupdate if set app.enable('autoupdate') or freeze schemas by default 107 | railway.orm._schemas.forEach(schema => { 108 | if (app.enabled('autoupdate')) { 109 | schema.autoupdate(); 110 | } else { 111 | schema.freeze(); 112 | } 113 | }); 114 | } 115 | 116 | function log(str, startTime) { 117 | const $ = railway.utils.stylize.$; 118 | const m = Date.now() - startTime; 119 | railway.utils.debug(str + $(' [' + (m < 10 ? m : $(m).red) + ' ms]').bold); 120 | app.emit('app-event', { 121 | type: 'query', 122 | param: str, 123 | time: m 124 | }); 125 | } 126 | 127 | function prepareContext(models, railway, app, defSchema, done) { 128 | const ctx = { app }, 129 | _models = {}, 130 | settings = {}; 131 | let cname, 132 | schema, 133 | connected = 0, 134 | wait = 0, 135 | nonJugglingSchema = false; 136 | 137 | done = done || function() {}; 138 | 139 | /** 140 | * Multiple schemas support 141 | * example: 142 | * schema('redis', {url:'...'}, function() { 143 | * describe models using redis connection 144 | * ... 145 | * }); 146 | * schema(function() { 147 | * describe models stored in memory 148 | * ... 149 | * }); 150 | */ 151 | ctx.schema = function() { 152 | const name = argument('string'); 153 | const opts = argument('object') || {}; 154 | const def = argument('function') || function() {}; 155 | schema = new Schema(name || opts.driver || 'memory', opts); 156 | railway.orm._schemas.push(schema); 157 | wait += 1; 158 | ctx.gotSchema = true; 159 | schema.on('log', log); 160 | schema.on('connected', () => { 161 | connected += 1; 162 | if (wait === connected) { 163 | done(); 164 | } 165 | }); 166 | def(); 167 | schema = false; 168 | }; 169 | 170 | /** 171 | * Use custom schema driver 172 | */ 173 | ctx.customSchema = function() { 174 | const def = argument('function') || function() {}; 175 | nonJugglingSchema = true; 176 | def(); 177 | Object.keys(ctx.exports).forEach(m => { 178 | ctx.define(m, ctx.exports[m]); 179 | }); 180 | nonJugglingSchema = false; 181 | }; 182 | ctx.exports = {}; 183 | ctx.module = { exports: ctx.exports }; 184 | 185 | /** 186 | * Define a class in current schema 187 | */ 188 | ctx.describe = ctx.define = function(className, callback) { 189 | let m; 190 | cname = className; 191 | _models[cname] = {}; 192 | settings[cname] = {}; 193 | if (nonJugglingSchema) { 194 | m = callback; 195 | } else { 196 | if (typeof callback === 'function') { 197 | callback(); 198 | } 199 | m = (schema || defSchema).define(className, _models[cname], settings[cname]); 200 | } 201 | if (global.railway) { 202 | global[cname] = m; 203 | } 204 | models[cname] = ctx[cname] = m; 205 | return m; 206 | }; 207 | 208 | /** 209 | * Define a property in current class 210 | */ 211 | ctx.property = function(name, type, params) { 212 | if (!params) { 213 | params = {}; 214 | } 215 | 216 | if (typeof type !== 'function' && typeof type === 'object' && !(type instanceof Array)) { 217 | params = type; 218 | type = String; 219 | } 220 | 221 | params.type = type || String; 222 | _models[cname][name] = params; 223 | }; 224 | 225 | /** 226 | * Set custom table name for current class 227 | * @param name - name of table 228 | */ 229 | ctx.setTableName = function(name) { 230 | if (cname) { 231 | settings[cname].table = name; 232 | } 233 | }; 234 | 235 | /** 236 | * Set configuration param 237 | * 238 | * @param name - name of param. 239 | * @param value - value. 240 | */ 241 | ctx.set = function(name, value) { 242 | if (cname) { 243 | settings[cname][name] = value; 244 | } 245 | }; 246 | 247 | ctx.pathTo = railway.map && railway.map.pathTo || {}; 248 | 249 | /** 250 | * If the Schema has additional types, add them to the context 251 | * e.g. MySQL has an additional Point type 252 | */ 253 | if (Schema.types && Object.keys(Schema.types).length) { 254 | Object.keys(Schema.types).forEach(typeName => { 255 | ctx[typeName] = Schema.types[typeName]; 256 | }); 257 | } 258 | 259 | return ctx; 260 | 261 | function argument(type) { 262 | let r; 263 | [].forEach.call(arguments.callee.caller.arguments, a => { 264 | if (!r && typeof a === type) { 265 | r = a; 266 | } 267 | }); 268 | return r; 269 | } 270 | } 271 | 272 | }; 273 | -------------------------------------------------------------------------------- /lib/adapters/memory.js: -------------------------------------------------------------------------------- 1 | exports.initialize = function initializeSchema(schema, callback) { 2 | schema.adapter = new Memory(); 3 | schema.adapter.connect(callback); 4 | }; 5 | 6 | function Memory(m) { 7 | if (m) { 8 | this.isTransaction = true; 9 | this.cache = m.cache; 10 | this.ids = m.ids; 11 | this._models = m._models; 12 | } else { 13 | this.isTransaction = false; 14 | this.cache = {}; 15 | this.ids = {}; 16 | this._models = {}; 17 | } 18 | } 19 | 20 | Memory.prototype.connect = function(callback) { 21 | if (this.isTransaction) { 22 | this.onTransactionExec = callback; 23 | } else { 24 | process.nextTick(callback); 25 | } 26 | }; 27 | 28 | Memory.prototype.define = function defineModel(descr) { 29 | const m = descr.model.modelName; 30 | this._models[m] = descr; 31 | this.cache[this.table(m)] = {}; 32 | this.ids[m] = 0; 33 | }; 34 | 35 | Memory.prototype.create = function create(model, data, callback) { 36 | const id = data.id ? data.id : this.ids[model] += 1; 37 | data.id = id; 38 | this.cache[this.table(model)][id] = JSON.stringify(data); 39 | process.nextTick(() => { 40 | callback(null, id, 1); 41 | }); 42 | }; 43 | 44 | /** 45 | * Updates the respective record 46 | * 47 | * @param {Object} params - { where:{uid:'10'}, update:{ Name:'New name' } } 48 | * @param callback(err, obj) 49 | */ 50 | Memory.prototype.update = function(model, params, callback) { 51 | const mem = this; 52 | 53 | this.all(model, { where: params.where, limit: params.limit, skip: params.skip }, (err, records) => { 54 | let wait = records.length; 55 | 56 | records.forEach(record => { 57 | mem.updateAttributes(model, record.id, params.update, done); 58 | }); 59 | 60 | if (wait === 0) { 61 | callback(); 62 | } 63 | 64 | function done() { 65 | wait += -1; 66 | if (wait === 0) { 67 | callback(); 68 | } 69 | } 70 | }); 71 | }; 72 | 73 | Memory.prototype.updateOrCreate = function(model, data, callback) { 74 | const mem = this; 75 | this.find(model, data.id, (err, exists) => { 76 | if (exists) { 77 | mem.save(model, merge(exists, data), callback); 78 | } else { 79 | mem.create(model, data, (err, id) => { 80 | data.id = id; 81 | callback(err, data); 82 | }); 83 | } 84 | }); 85 | }; 86 | 87 | Memory.prototype.save = function save(model, data, callback) { 88 | this.cache[this.table(model)][data.id] = JSON.stringify(data); 89 | process.nextTick(() => callback(null, data)); 90 | }; 91 | 92 | Memory.prototype.exists = function exists(model, id, callback) { 93 | const table = this.table(model); 94 | process.nextTick(() => { 95 | callback(null, this.cache[table].hasOwnProperty(id)); 96 | }); 97 | }; 98 | 99 | Memory.prototype.find = function find(model, id, callback) { 100 | const table = this.table(model); 101 | process.nextTick(() => { 102 | callback(null, id in this.cache[table] && this.fromDb(model, this.cache[table][id])); 103 | }); 104 | }; 105 | 106 | Memory.prototype.destroy = function destroy(model, id, callback) { 107 | delete this.cache[this.table(model)][id]; 108 | process.nextTick(callback); 109 | }; 110 | 111 | Memory.prototype.fromDb = function(model, data) { 112 | if (!data) { 113 | return null; 114 | } 115 | data = JSON.parse(data); 116 | const props = this._models[model].properties; 117 | Object.keys(data).forEach(key => { 118 | let val = data[key]; 119 | if (typeof val === 'undefined' || val === null) { 120 | return; 121 | } 122 | if (props[key]) { 123 | switch (props[key].type.name) { 124 | case 'Date': 125 | val = new Date(val.toString().replace(/GMT.*$/, 'GMT')); 126 | break; 127 | case 'Boolean': 128 | val = Boolean(val); 129 | break; 130 | } 131 | } 132 | data[key] = val; 133 | }); 134 | return data; 135 | }; 136 | 137 | Memory.prototype.all = function all(model, filter, callback) { 138 | const table = this.table(model); 139 | let nodes = Object.keys(this.cache[table]).map(key => { 140 | return this.fromDb(model, this.cache[table][key]); 141 | }); 142 | 143 | if (filter) { 144 | 145 | // do we need some sorting? 146 | if (filter.order) { 147 | let orders = filter.order; 148 | if (typeof filter.order === 'string') { 149 | orders = [filter.order]; 150 | } 151 | orders.forEach((key, i) => { 152 | let reverse = 1; 153 | const m = key.match(/\s+(A|DE)SC$/i); 154 | if (m) { 155 | key = key.replace(/\s+(A|DE)SC/i, ''); 156 | if (m[1].toLowerCase() === 'de') { 157 | reverse = -1; 158 | } 159 | } 160 | orders[i] = { key, reverse }; 161 | }); 162 | nodes = nodes.sort(sorting.bind(orders)); 163 | } 164 | 165 | // do we need some filtration? 166 | if (filter.where) { 167 | nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes; 168 | } 169 | 170 | // limit/skip 171 | filter.skip = filter.skip || 0; 172 | filter.limit = filter.limit || nodes.length; 173 | const countBeforeLimit = nodes.length; 174 | nodes = nodes.slice(filter.skip, filter.skip + filter.limit); 175 | nodes.countBeforeLimit = countBeforeLimit; 176 | 177 | } 178 | 179 | process.nextTick(() => { 180 | if (filter && filter.include) { 181 | this._models[model].model.include(nodes, filter.include, callback); 182 | } else { 183 | callback(null, nodes); 184 | } 185 | }); 186 | 187 | function sorting(a, b) { 188 | for (let i = 0, l = this.length; i < l; i += 1) { 189 | if (a[this[i].key] > b[this[i].key]) { 190 | return 1 * this[i].reverse; 191 | } else if (a[this[i].key] < b[this[i].key]) { 192 | return -1 * this[i].reverse; 193 | } 194 | } 195 | return 0; 196 | } 197 | }; 198 | 199 | function applyFilter(filter) { 200 | if (typeof filter.where === 'function') { 201 | return filter.where; 202 | } 203 | const keys = Object.keys(filter.where); 204 | return function(obj) { 205 | let pass = true; 206 | keys.forEach(key => { 207 | if (!test(filter.where[key], obj[key])) { 208 | pass = false; 209 | } 210 | }); 211 | return pass; 212 | }; 213 | 214 | function test(example, value) { 215 | if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { 216 | return value.match(example); 217 | } 218 | if (typeof example === 'undefined') { 219 | return undefined; 220 | } 221 | if (typeof value === 'undefined') { 222 | return undefined; 223 | } 224 | if (typeof example === 'object') { 225 | if (example === null) { 226 | return value === null; 227 | } 228 | if (example.inq) { 229 | if (!value) { 230 | return false; 231 | } 232 | for (let i = 0; i < example.inq.length; i += 1) { 233 | if (String(example.inq[i]) === String(value)) { 234 | return true; 235 | } 236 | } 237 | return false; 238 | } 239 | } 240 | // not strict equality 241 | return String(example !== null ? example.toString() : example) === 242 | String(value !== null ? value.toString() : value); 243 | } 244 | } 245 | 246 | Memory.prototype.destroyAll = function destroyAll(model, callback) { 247 | const table = this.table(model); 248 | Object.keys(this.cache[table]).forEach(id => { 249 | delete this.cache[table][id]; 250 | }); 251 | this.cache[table] = {}; 252 | process.nextTick(callback); 253 | }; 254 | 255 | Memory.prototype.count = function count(model, where, callback) { 256 | const cache = this.cache[this.table(model)]; 257 | let data = Object.keys(cache); 258 | if (where) { 259 | data = data.filter(id => { 260 | let ok = true; 261 | Object.keys(where).forEach(key => { 262 | if (String(JSON.parse(cache[id])[key]) !== String(where[key])) { 263 | ok = false; 264 | } 265 | }); 266 | return ok; 267 | }); 268 | } 269 | process.nextTick(() => callback(null, data.length)); 270 | }; 271 | 272 | Memory.prototype.updateAttributes = function updateAttributes(model, id, data, cb) { 273 | data.id = id; 274 | const base = JSON.parse(this.cache[this.table(model)][id]); 275 | this.save(model, merge(base, data), cb); 276 | }; 277 | 278 | Memory.prototype.transaction = function() { 279 | return new Memory(this); 280 | }; 281 | 282 | Memory.prototype.exec = function(callback) { 283 | this.onTransactionExec(); 284 | setTimeout(callback, 50); 285 | }; 286 | 287 | Memory.prototype.table = function(model) { 288 | return this._models[model].model.tableName; 289 | }; 290 | 291 | function merge(base, update) { 292 | if (!base) { 293 | return update; 294 | } 295 | 296 | Object.keys(update).forEach(key => base[key] = update[key]); 297 | 298 | return base; 299 | } 300 | 301 | -------------------------------------------------------------------------------- /lib/adapters/cradle.js.1: -------------------------------------------------------------------------------- 1 | const safeRequire = require('../utils').safeRequire; 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | const cradle = safeRequire('cradle'); 7 | 8 | /** 9 | * Private functions for internal use 10 | */ 11 | function CradleAdapter(client) { 12 | this._models = {}; 13 | this.client = client; 14 | } 15 | 16 | function createdbif(client, callback) { 17 | client.exists(function(err, exists) { 18 | if (err) { 19 | return callback(err); 20 | } 21 | if (!exists) { 22 | client.create(function() { callback(); }); 23 | } else { 24 | callback(); 25 | } 26 | }); 27 | } 28 | 29 | function naturalize(data, table) { 30 | data.nature = table; 31 | //TODO: maybe this is not a really good idea 32 | if (data.date) { 33 | data.date = data.date.toString(); 34 | } 35 | return data; 36 | } 37 | function idealize(data) { 38 | data.id = data._id; 39 | return data; 40 | } 41 | function stringify(data) { 42 | return data ? data.toString() : data; 43 | } 44 | 45 | function errorHandler(callback, func) { 46 | return function(err, res) { 47 | if (err) { 48 | console.log('cradle', err); 49 | callback(err); 50 | } else if (func) { 51 | func(res, function(res) { 52 | callback(null, res); 53 | }); 54 | } else { 55 | callback(null, res); 56 | } 57 | }; 58 | } 59 | 60 | function synchronize(functions, args, callback) { 61 | if (functions.length === 0) { 62 | callback(); 63 | } 64 | if (functions.length > 0 && args.length === functions.length) { 65 | functions[0](args[0][0], args[0][1], function(err) { 66 | if (err) { 67 | callback(err); 68 | } 69 | functions.splice(0, 1); 70 | args.splice(0, 1); 71 | synchronize(functions, args, callback); 72 | }); 73 | } 74 | } 75 | 76 | function applyFilter(filter) { 77 | if (typeof filter.where === 'function') { 78 | return filter.where; 79 | } 80 | const keys = Object.keys(filter.where); 81 | return function(obj) { 82 | let pass = true; 83 | keys.forEach(function(key) { 84 | if (!test(filter.where[key], obj[key])) { 85 | pass = false; 86 | } 87 | }); 88 | return pass; 89 | }; 90 | 91 | function test(example, value) { 92 | if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { 93 | return value.match(example); 94 | } 95 | // not strict equality 96 | return example === value; 97 | } 98 | } 99 | 100 | function numerically(a, b) { 101 | return a[this[0]] - b[this[0]]; 102 | } 103 | 104 | function literally(a, b) { 105 | return a[this[0]] > b[this[0]]; 106 | } 107 | 108 | function filtering(res, model, filter, instance) { 109 | if (model) { 110 | if (filter === null) { 111 | filter = {}; 112 | } 113 | if (filter.where === null) { 114 | filter.where = {}; 115 | } 116 | // use table() function on fake instance 117 | filter.where.nature = CradleAdapter.prototype.table.call({_models: instance}, model); 118 | } 119 | // do we need some filtration? 120 | if (filter.where) { 121 | res = res ? res.filter(applyFilter(filter)) : res; 122 | } 123 | 124 | // do we need some sorting? 125 | if (filter.order) { 126 | var props = instance[model].properties; 127 | var allNumeric = true; 128 | var orders = filter.order; 129 | var reverse = false; 130 | if (typeof filter.order === 'string') { 131 | orders = [filter.order]; 132 | } 133 | 134 | orders.forEach(function(key, i) { 135 | var m = key.match(/\s+(A|DE)SC$/i); 136 | if (m) { 137 | key = key.replace(/\s+(A|DE)SC/i, ''); 138 | if (m[1] === 'DE') reverse = true; 139 | } 140 | orders[i] = key; 141 | if (props[key].type.name !== 'Number') { 142 | allNumeric = false; 143 | } 144 | }); 145 | if (allNumeric) { 146 | res = res.sort(numerically.bind(orders)); 147 | } else { 148 | res = res.sort(literally.bind(orders)); 149 | } 150 | if (reverse) res = res.reverse(); 151 | } 152 | return res; 153 | } 154 | 155 | /** 156 | * Connection/Disconnection 157 | */ 158 | exports.initialize = function(schema, callback) { 159 | if (!cradle) return; 160 | 161 | // when using cradle if we dont wait for the schema to be connected, the models fails to load correctly. 162 | schema.waitForConnect = true; 163 | if (!schema.settings.url) { 164 | var host = schema.settings.host || 'localhost'; 165 | var port = schema.settings.port || '5984'; 166 | var options = schema.settings.options || { 167 | cache: true, 168 | raw: false 169 | }; 170 | if (schema.settings.username) { 171 | options.auth = {}; 172 | options.auth.username = schema.settings.username; 173 | if (schema.settings.password) { 174 | options.auth.password = schema.settings.password; 175 | } 176 | } 177 | var database = schema.settings.database || 'jugglingdb'; 178 | 179 | schema.settings.host = host; 180 | schema.settings.port = port; 181 | schema.settings.database = database; 182 | schema.settings.options = options; 183 | } 184 | schema.client = new(cradle.Connection)(schema.settings.host, schema.settings.port,schema.settings.options).database(schema.settings.database); 185 | 186 | createdbif( 187 | schema.client, 188 | errorHandler(callback, function() { 189 | schema.adapter = new CradleAdapter(schema.client); 190 | process.nextTick(callback); 191 | })); 192 | }; 193 | 194 | CradleAdapter.prototype.disconnect = function() { 195 | }; 196 | 197 | /** 198 | * Write methods 199 | */ 200 | CradleAdapter.prototype.define = function(descr) { 201 | this._models[descr.model.modelName] = descr; 202 | }; 203 | 204 | CradleAdapter.prototype.create = function(model, data, callback) { 205 | this.client.save( 206 | stringify(data.id), 207 | naturalize(data, this.table(model)), 208 | errorHandler(callback, function(res, cb) { 209 | cb(res.id); 210 | }) 211 | ); 212 | }; 213 | 214 | CradleAdapter.prototype.save = function(model, data, callback) { 215 | this.client.save( 216 | stringify(data.id), 217 | naturalize(data, this.table(model)), 218 | errorHandler(callback) 219 | ); 220 | }; 221 | 222 | CradleAdapter.prototype.updateAttributes = function(model, id, data, callback) { 223 | this.client.merge( 224 | stringify(id), 225 | data, 226 | errorHandler(callback, function(doc, cb) { 227 | cb(idealize(doc)); 228 | }) 229 | ); 230 | }; 231 | 232 | CradleAdapter.prototype.updateOrCreate = function(model, data, callback) { 233 | this.client.get( 234 | stringify(data.id), 235 | function(err) { 236 | if (err) { 237 | this.create(model, data, callback); 238 | } else { 239 | this.updateAttributes(model, data.id, data, callback); 240 | } 241 | }.bind(this) 242 | ); 243 | }; 244 | 245 | /** 246 | * Read methods 247 | */ 248 | CradleAdapter.prototype.exists = function(model, id, callback) { 249 | this.client.get( 250 | stringify(id), 251 | errorHandler(callback, function(doc, cb) { 252 | cb(!!doc); 253 | }) 254 | ); 255 | }; 256 | 257 | CradleAdapter.prototype.find = function(model, id, callback) { 258 | this.client.get( 259 | stringify(id), 260 | errorHandler(callback, function(doc, cb) { 261 | cb(idealize(doc)); 262 | }) 263 | ); 264 | }; 265 | 266 | CradleAdapter.prototype.count = function(model, callback, where) { 267 | this.models( 268 | model, 269 | {where: where}, 270 | callback, 271 | function(docs, cb) { 272 | cb(docs.length); 273 | } 274 | ); 275 | }; 276 | 277 | CradleAdapter.prototype.models = function(model, filter, callback, func) { 278 | var limit = 200; 279 | var skip = 0; 280 | if (filter !== null) { 281 | limit = filter.limit || limit; 282 | skip = filter.skip ||skip; 283 | } 284 | 285 | var self = this; 286 | var table = this.table(model); 287 | 288 | self.client.save('_design/'+table, { 289 | views : { 290 | all : { 291 | map : 'function(doc) { if (doc.nature == "'+table+'") { emit(doc._id, doc); } }' 292 | } 293 | } 294 | }, function() { 295 | self.client.view(table+'/all', {'include_docs': true, limit:limit, skip:skip}, errorHandler(callback, function(res, cb) { 296 | var docs = res.map(function(doc) { 297 | return idealize(doc); 298 | }); 299 | var filtered = filtering(docs, model, filter, this._models); 300 | 301 | if ('function' === typeof func) { 302 | func(filtered, cb); 303 | } else { 304 | cb(filtered); 305 | } 306 | }.bind(self))); 307 | }); 308 | }; 309 | 310 | CradleAdapter.prototype.all = function(model, filter, callback) { 311 | this.models( 312 | model, 313 | filter, 314 | callback 315 | ); 316 | }; 317 | 318 | /** 319 | * Detroy methods 320 | */ 321 | CradleAdapter.prototype.destroy = function(model, id, callback) { 322 | this.client.remove( 323 | stringify(id), 324 | function(err) { 325 | callback(err); 326 | } 327 | ); 328 | }; 329 | 330 | CradleAdapter.prototype.destroyAll = function(model, callback) { 331 | this.models( 332 | model, 333 | null, 334 | callback, 335 | function(docs, cb) { 336 | var docIds = docs.map(function(doc) { 337 | return doc.id; 338 | }); 339 | this.client.get(docIds, function(err, res) { 340 | if(err) cb(err); 341 | 342 | var funcs = res.map(function() { 343 | return this.client.remove.bind(this.client); 344 | }.bind(this)); 345 | 346 | var args = res.map(function(doc) { 347 | return [doc._id, doc._rev]; 348 | }); 349 | 350 | synchronize(funcs, args, cb); 351 | }.bind(this)); 352 | }.bind(this) 353 | ); 354 | }; 355 | 356 | CradleAdapter.prototype.table = function(model) { 357 | return this._models[model].model.tableName; 358 | }; 359 | -------------------------------------------------------------------------------- /test/include.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let db, User, Post, Passport, City, Street, Building, Asset; 5 | const nbSchemaRequests = 0; 6 | 7 | let createdUsers = []; 8 | 9 | describe('include', function() { 10 | 11 | before(setup); 12 | 13 | it('should fetch belongsTo relation', function(done) { 14 | Passport.all({ include: 'owner' }, function(err, passports) { 15 | passports.length.should.be.ok; 16 | passports.forEach(function(p) { 17 | p.__cachedRelations.should.have.property('owner'); 18 | const owner = p.__cachedRelations.owner; 19 | if (!p.ownerId) { 20 | should.not.exist(owner); 21 | } else { 22 | should.exist(owner); 23 | owner.id.should.equal(p.ownerId); 24 | } 25 | }); 26 | done(); 27 | }); 28 | }); 29 | 30 | it('should fetch hasMany relation', function(done) { 31 | User.all({ include: 'posts' }, function(err, users) { 32 | should.not.exist(err); 33 | should.exist(users); 34 | users.length.should.be.ok; 35 | users.forEach(function(u) { 36 | u.__cachedRelations.should.have.property('posts'); 37 | u.__cachedRelations.posts.forEach(function(p) { 38 | p.userId.should.equal(u.id); 39 | }); 40 | }); 41 | done(); 42 | }); 43 | }); 44 | 45 | it('should fetch hasAndBelongsToMany relation', function(done) { 46 | User.all({ include: ['assets'] }, function(err, users) { 47 | should.not.exist(err); 48 | should.exist(users); 49 | users.length.should.be.ok; 50 | users.forEach(function(user) { 51 | user.__cachedRelations.should.have.property('assets'); 52 | if (user.id === createdUsers[0].id) { 53 | user.__cachedRelations.assets.should.have.length(3); 54 | } 55 | if (user.id === createdUsers[1].id) { 56 | user.__cachedRelations.assets.should.have.length(1); 57 | } 58 | user.__cachedRelations.assets.forEach(function(a) { 59 | a.url.should.startWith('http://placekitten.com'); 60 | }); 61 | }); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('should fetch Passport - Owner - Posts', function(done) { 67 | Passport.all({ include: { owner: 'posts' } }, function(err, passports) { 68 | should.not.exist(err); 69 | should.exist(passports); 70 | passports.length.should.be.ok; 71 | passports.forEach(function(p) { 72 | p.__cachedRelations.should.have.property('owner'); 73 | const user = p.__cachedRelations.owner; 74 | if (!p.ownerId) { 75 | should.not.exist(user); 76 | } else { 77 | should.exist(user); 78 | user.id.should.equal(p.ownerId); 79 | user.__cachedRelations.should.have.property('posts'); 80 | user.__cachedRelations.posts.forEach(function(pp) { 81 | pp.userId.should.equal(user.id); 82 | }); 83 | } 84 | }); 85 | done(); 86 | }); 87 | }); 88 | 89 | it('should fetch Passports - User - Posts - User', function(done) { 90 | Passport.all({ 91 | include: { owner: { posts: 'author' } } 92 | }, function(err, passports) { 93 | should.not.exist(err); 94 | should.exist(passports); 95 | passports.length.should.be.ok; 96 | passports.forEach(function(p) { 97 | p.__cachedRelations.should.have.property('owner'); 98 | const user = p.__cachedRelations.owner; 99 | if (!p.ownerId) { 100 | should.not.exist(user); 101 | } else { 102 | should.exist(user); 103 | user.id.should.equal(p.ownerId); 104 | user.__cachedRelations.should.have.property('posts'); 105 | user.__cachedRelations.posts.forEach(function(pp) { 106 | pp.userId.should.equal(user.id); 107 | pp.__cachedRelations.should.have.property('author'); 108 | const author = pp.__cachedRelations.author; 109 | author.id.should.equal(user.id); 110 | }); 111 | } 112 | }); 113 | done(); 114 | }); 115 | }); 116 | 117 | it('should fetch User - Posts AND Passports', function(done) { 118 | User.all({ include: ['posts', 'passports'] }, function(err, users) { 119 | should.not.exist(err); 120 | should.exist(users); 121 | users.length.should.be.ok; 122 | users.forEach(function(user) { 123 | user.__cachedRelations.should.have.property('posts'); 124 | user.__cachedRelations.should.have.property('passports'); 125 | user.__cachedRelations.posts.forEach(function(p) { 126 | p.userId.should.equal(user.id); 127 | }); 128 | user.__cachedRelations.passports.forEach(function(pp) { 129 | pp.ownerId.should.equal(user.id); 130 | }); 131 | }); 132 | done(); 133 | }); 134 | }); 135 | }); 136 | 137 | function setup(done) { 138 | db = getSchema(); 139 | City = db.define('City'); 140 | Street = db.define('Street'); 141 | Building = db.define('Building'); 142 | User = db.define('User', { 143 | name: String, 144 | age: Number 145 | }); 146 | Passport = db.define('Passport', { 147 | number: String 148 | }); 149 | Post = db.define('Post', { 150 | title: String 151 | }); 152 | Asset = db.define('Asset', { 153 | url: String 154 | }); 155 | 156 | Passport.belongsTo('owner', { model: User }); 157 | User.hasMany('passports', { foreignKey: 'ownerId' }); 158 | User.hasMany('posts', { foreignKey: 'userId' }); 159 | Post.belongsTo('author', { model: User, foreignKey: 'userId' }); 160 | User.hasAndBelongsToMany('assets'); 161 | 162 | db.automigrate(function() { 163 | let createdPassports = []; 164 | let createdPosts = []; 165 | let createdAssets = []; 166 | createUsers(); 167 | function createUsers() { 168 | clearAndCreate( 169 | User, 170 | [ 171 | { name: 'User A', age: 21 }, 172 | { name: 'User B', age: 22 }, 173 | { name: 'User C', age: 23 }, 174 | { name: 'User D', age: 24 }, 175 | { name: 'User E', age: 25 } 176 | ], 177 | function(items) { 178 | createdUsers = items; 179 | createPassports(); 180 | } 181 | ); 182 | } 183 | 184 | function createPassports() { 185 | clearAndCreate( 186 | Passport, 187 | [ 188 | { number: '1', ownerId: createdUsers[0].id }, 189 | { number: '2', ownerId: createdUsers[1].id }, 190 | { number: '3' } 191 | ], 192 | function(items) { 193 | createdPassports = items; 194 | createPosts(); 195 | } 196 | ); 197 | } 198 | 199 | function createPosts() { 200 | clearAndCreate( 201 | Post, 202 | [ 203 | { title: 'Post A', userId: createdUsers[0].id }, 204 | { title: 'Post B', userId: createdUsers[0].id }, 205 | { title: 'Post C', userId: createdUsers[0].id }, 206 | { title: 'Post D', userId: createdUsers[1].id }, 207 | { title: 'Post E' } 208 | ], 209 | function(items) { 210 | createdPosts = items; 211 | createAssets(); 212 | } 213 | ); 214 | } 215 | 216 | function createAssets() { 217 | clearAndCreateScoped( 218 | 'assets', 219 | [ 220 | { url: 'http://placekitten.com/200/200' }, 221 | { url: 'http://placekitten.com/300/300' }, 222 | { url: 'http://placekitten.com/400/400' }, 223 | { url: 'http://placekitten.com/500/500' } 224 | ], 225 | [ 226 | createdUsers[0], 227 | createdUsers[0], 228 | createdUsers[0], 229 | createdUsers[1] 230 | ], 231 | function(items) { 232 | createdAssets = items; 233 | done(); 234 | } 235 | ); 236 | } 237 | 238 | }); 239 | } 240 | 241 | function clearAndCreate(model, data, callback) { 242 | const createdItems = []; 243 | 244 | model.destroyAll(function() { 245 | nextItem(null, null); 246 | }); 247 | 248 | let itemIndex = 0; 249 | function nextItem(err, lastItem) { 250 | if (lastItem !== null) { 251 | createdItems.push(lastItem); 252 | } 253 | if (itemIndex >= data.length) { 254 | callback(createdItems); 255 | return; 256 | } 257 | model.create(data[itemIndex], nextItem); 258 | itemIndex++; 259 | } 260 | } 261 | 262 | function clearAndCreateScoped(modelName, data, scope, callback) { 263 | const createdItems = []; 264 | 265 | let clearedItemIndex = 0; 266 | if (scope && scope.length) { 267 | 268 | scope.forEach(function(instance) { 269 | instance[modelName].destroyAll(function(err) { 270 | clearedItemIndex++; 271 | if (clearedItemIndex >= scope.length) { 272 | createItems(); 273 | } 274 | }); 275 | }); 276 | 277 | } else { 278 | 279 | callback(createdItems); 280 | } 281 | 282 | let itemIndex = 0; 283 | function nextItem(err, lastItem) { 284 | itemIndex++; 285 | 286 | if (lastItem !== null) { 287 | createdItems.push(lastItem); 288 | } 289 | if (itemIndex >= data.length) { 290 | callback(createdItems); 291 | return; 292 | } 293 | } 294 | 295 | function createItems() { 296 | scope.forEach(function(instance, instanceIndex) { 297 | instance[modelName].create(data[instanceIndex], nextItem); 298 | }); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /test/manipulation.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let db, Person; 5 | 6 | describe('manipulation', function() { 7 | 8 | before(function(done) { 9 | db = getSchema(); 10 | 11 | Person = db.define('Person', { 12 | name: { type: String, name: 'full_name' }, 13 | gender: String, 14 | married: Boolean, 15 | age: { type: Number, index: true }, 16 | dob: Date, 17 | createdAt: { type: Number, default: Date.now, name: 'created_at' } 18 | }); 19 | 20 | db.automigrate(done); 21 | 22 | }); 23 | 24 | describe('create', function() { 25 | 26 | before(function(done) { 27 | Person.destroyAll(done); 28 | }); 29 | 30 | it('should create instance', function(done) { 31 | Person.create({ name: 'Anatoliy' }).then(function(p) { 32 | p.name.should.equal('Anatoliy'); 33 | should.exist(p); 34 | Person.find(p.id, function(err, person) { 35 | person.id.should.equal(p.id); 36 | person.name.should.equal('Anatoliy'); 37 | done(); 38 | }); 39 | }).catch(done); 40 | }); 41 | 42 | it('should return instance of object', function(done) { 43 | Person.create().then(function(person) { 44 | should.exist(person); 45 | person.should.be.an.instanceOf(Person); 46 | done(); 47 | }).catch(done); 48 | }); 49 | 50 | it('should work when called without callback', function(done) { 51 | Person.afterCreate = function(next) { 52 | this.should.be.an.instanceOf(Person); 53 | this.name.should.equal('Nickolay'); 54 | should.exist(this.id); 55 | Person.afterCreate = null; 56 | next(); 57 | setTimeout(done, 10); 58 | }; 59 | Person.create({ name: 'Nickolay' }); 60 | }); 61 | 62 | it('should create instance with blank data', function(done) { 63 | Person.create(function(err, p) { 64 | should.not.exist(err); 65 | should.exist(p); 66 | should.not.exists(p.name); 67 | Person.find(p.id, function(err, person) { 68 | person.id.should.equal(p.id); 69 | should.not.exists(person.name); 70 | done(); 71 | }); 72 | }); 73 | }); 74 | 75 | it('should work when called with no data and callback', function(done) { 76 | Person.afterCreate = function(next) { 77 | this.should.be.an.instanceOf(Person); 78 | should.not.exist(this.name); 79 | should.exist(this.id); 80 | Person.afterCreate = null; 81 | next(); 82 | setTimeout(done, 30); 83 | }; 84 | Person.create(); 85 | }); 86 | 87 | it('should create batch of objects', function(done) { 88 | const batch = [{ name: 'Shaltay' }, { name: 'Boltay' }, {}]; 89 | Person.create(batch, function(e, ps) { 90 | should.not.exist(e); 91 | should.exist(ps); 92 | ps.should.be.instanceOf(Array); 93 | ps.should.have.lengthOf(batch.length); 94 | 95 | Person.validatesPresenceOf('name'); 96 | Person.create(batch, function(errors, persons) { 97 | delete Person._validations; 98 | should.exist(errors); 99 | errors.should.have.lengthOf(batch.length); 100 | should.not.exist(errors[0]); 101 | should.not.exist(errors[1]); 102 | should.exist(errors[2]); 103 | 104 | should.exist(persons); 105 | persons.should.have.lengthOf(batch.length); 106 | should.not.exist(persons[0].errors); 107 | should.exist(persons[2].errors); 108 | done(); 109 | }); 110 | }); 111 | }); 112 | }); 113 | 114 | describe('save', function() { 115 | 116 | it('should save new object', function(done) { 117 | const p = new Person; 118 | p.save(function(err) { 119 | should.not.exist(err); 120 | should.exist(p.id); 121 | done(); 122 | }); 123 | }); 124 | 125 | it('should save existing object', function(done) { 126 | Person.findOne(function(err, p) { 127 | should.not.exist(err); 128 | p.name = 'Hans'; 129 | p.propertyChanged('name').should.be.true; 130 | p.save(function(err) { 131 | should.not.exist(err); 132 | p.propertyChanged('name').should.be.false; 133 | Person.findOne(function(err, p) { 134 | should.not.exist(err); 135 | p.name.should.equal('Hans'); 136 | p.propertyChanged('name').should.be.false; 137 | done(); 138 | }); 139 | }); 140 | }); 141 | }); 142 | 143 | it('should save invalid object (skipping validation)', function(done) { 144 | Person.findOne(function(err, p) { 145 | should.not.exist(err); 146 | p.isValid = function(done) { 147 | process.nextTick(done); 148 | return false; 149 | }; 150 | p.name = 'Nana'; 151 | p.save(function(err) { 152 | should.exist(err); 153 | p.propertyChanged('name').should.be.true; 154 | p.save({ validate: false }, function(err) { 155 | should.not.exist(err); 156 | p.propertyChanged('name').should.be.false; 157 | done(); 158 | }); 159 | }); 160 | }); 161 | }); 162 | 163 | it('should save invalid new object (skipping validation)', function(done) { 164 | const p = new Person(); 165 | p.isNewRecord().should.be.true; 166 | 167 | p.isValid = function(done) { 168 | if (done) { 169 | process.nextTick(done); 170 | } 171 | return false; 172 | }; 173 | p.isValid().should.be.false; 174 | 175 | p.save({ validate: false }, function(err) { 176 | should.not.exist(err); 177 | p.isNewRecord().should.be.false; 178 | p.isValid().should.be.false; 179 | done(); 180 | }); 181 | }); 182 | 183 | it('should save throw error on validation', function() { 184 | Person.findOne(function(err, p) { 185 | should.not.exist(err); 186 | p.isValid = function(cb) { 187 | cb(false); 188 | return false; 189 | }; 190 | p.save({ 191 | 'throws': true 192 | }).catch(function(err) { 193 | should.exist(err); 194 | }); 195 | }); 196 | }); 197 | 198 | it.skip('should save with custom fields', function() { 199 | return Person.create({ name: 'Anatoliy' }, function(err, p) { 200 | should.exist(p.id); 201 | should.exist(p.name); 202 | should.not.exist(p['full_name']); 203 | const storedObj = JSON.parse(db.adapter.cache.Person[p.id]); 204 | should.exist(storedObj['full_name']); 205 | }); 206 | }); 207 | 208 | }); 209 | 210 | describe('updateAttributes', function() { 211 | let person; 212 | 213 | before(function(done) { 214 | Person.destroyAll(function() { 215 | Person.create().then(function(pers) { 216 | person = pers; 217 | done(); 218 | }); 219 | }); 220 | }); 221 | 222 | it('should update one attribute', function(done) { 223 | person.updateAttribute('name', 'Paul Graham', function(err, p) { 224 | should.not.exist(err); 225 | Person.all(function(e, ps) { 226 | should.not.exist(err); 227 | ps.should.have.lengthOf(1); 228 | ps.pop().name.should.equal('Paul Graham'); 229 | done(); 230 | }); 231 | }); 232 | }); 233 | }); 234 | 235 | describe('destroy', function() { 236 | 237 | it('should destroy record', function(done) { 238 | Person.create(function(err, p) { 239 | p.destroy(function(err) { 240 | should.not.exist(err); 241 | Person.exists(p.id, function(err, ex) { 242 | ex.should.not.be.ok; 243 | done(); 244 | }); 245 | }); 246 | }); 247 | }); 248 | 249 | it('should destroy all records', function(done) { 250 | Person.destroyAll(function(err) { 251 | should.not.exist(err); 252 | Person.all(function(err, posts) { 253 | posts.should.have.lengthOf(0); 254 | Person.count(function(err, count) { 255 | count.should.eql(0); 256 | done(); 257 | }); 258 | }); 259 | }); 260 | }); 261 | 262 | // TODO: implement destroy with filtered set 263 | it('should destroy filtered set of records'); 264 | }); 265 | 266 | describe('iterate', function() { 267 | 268 | before(function(next) { 269 | Person.destroyAll().then(function() { 270 | const ps = []; 271 | for (let i = 0; i < 507; i += 1) { 272 | ps.push({ name: 'Person ' + i }); 273 | } 274 | Person.create(ps).then(function(x) { 275 | next(); 276 | }); 277 | }); 278 | }); 279 | 280 | it('should iterate through the batch of objects', function(done) { 281 | let num = 0; 282 | Person.iterate({ batchSize: 100, limit: 507 }, function(person, next, i) { 283 | num += 1; 284 | next(); 285 | }, function(err) { 286 | num.should.equal(507); 287 | done(); 288 | }); 289 | }); 290 | 291 | it('should take limit into account', function(done) { 292 | let num = 0; 293 | Person.iterate({ batchSize: 20, limit: 21 }, function(person, next, i) { 294 | num += 1; 295 | next(); 296 | }, function(err) { 297 | num.should.equal(21); 298 | done(); 299 | }); 300 | }); 301 | 302 | it('should process in concurrent mode', function(done) { 303 | let num = 0, time = Date.now(); 304 | Person.iterate({ batchSize: 10, limit: 21, concurrent: true }, function(person, next, i) { 305 | num += 1; 306 | setTimeout(next, 20); 307 | }, function(err) { 308 | num.should.equal(21); 309 | should.ok(Date.now() - time < 300, 'should work in less than 300ms'); 310 | done(); 311 | }); 312 | }); 313 | }); 314 | 315 | describe('initialize', function() { 316 | it('should initialize object properly', function() { 317 | let hw = 'Hello word', 318 | now = Date.now(), 319 | person = new Person({ name: hw }); 320 | 321 | person.name.should.equal(hw); 322 | person.propertyChanged('name').should.be.false; 323 | person.name = 'Goodbye, Lenin'; 324 | person.name_was.should.equal(hw); 325 | person.propertyChanged('name').should.be.true; 326 | (person.createdAt >= now).should.be.true; 327 | person.isNewRecord().should.be.true; 328 | }); 329 | 330 | it('should work when constructor called as function', function() { 331 | const p = Person({ name: 'John Resig' }); 332 | p.should.be.an.instanceOf(Person); 333 | p.name.should.equal('John Resig'); 334 | }); 335 | }); 336 | }); 337 | -------------------------------------------------------------------------------- /lib/adapters/neo4j.js.1: -------------------------------------------------------------------------------- 1 | var safeRequire = require('../utils').safeRequire; 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | var neo4j = safeRequire('neo4j'); 7 | 8 | exports.initialize = function initializeSchema(schema, callback) { 9 | schema.client = new neo4j.GraphDatabase(schema.settings.url); 10 | schema.adapter = new Neo4j(schema.client); 11 | process.nextTick(callback); 12 | }; 13 | 14 | function Neo4j(client) { 15 | this._models = {}; 16 | this.client = client; 17 | this.cache = {}; 18 | } 19 | 20 | Neo4j.prototype.define = function defineModel(descr) { 21 | this.mixClassMethods(descr.model, descr.properties); 22 | this.mixInstanceMethods(descr.model.prototype, descr.properties); 23 | this._models[descr.model.modelName] = descr; 24 | }; 25 | 26 | Neo4j.prototype.createIndexHelper = function(cls, indexName) { 27 | var db = this.client; 28 | var method = 'findBy' + indexName[0].toUpperCase() + indexName.substr(1); 29 | cls[method] = function(value, cb) { 30 | db.getIndexedNode(cls.tableName, indexName, value, function(err, node) { 31 | if (err) return cb(err); 32 | if (node) { 33 | node.data.id = node.id; 34 | cb(null, new cls(node.data)); 35 | } else { 36 | cb(null, null); 37 | } 38 | }); 39 | }; 40 | }; 41 | 42 | Neo4j.prototype.mixClassMethods = function mixClassMethods(cls, properties) { 43 | var neo = this; 44 | 45 | Object.keys(properties).forEach(function(name) { 46 | if (properties[name].index) { 47 | neo.createIndexHelper(cls, name); 48 | } 49 | }); 50 | 51 | cls.setupCypherQuery = function(name, queryStr, rowHandler) { 52 | cls[name] = function cypherQuery(params, cb) { 53 | if (typeof params === 'function') { 54 | cb = params; 55 | params = []; 56 | } else if (params.constructor.name !== 'Array') { 57 | params = [params]; 58 | } 59 | 60 | var i = 0; 61 | var q = queryStr.replace(/\?/g, function() { 62 | return params[i++]; 63 | }); 64 | 65 | neo.client.query(function(err, result) { 66 | if (err) return cb(err, []); 67 | cb(null, result.map(rowHandler)); 68 | }, q); 69 | }; 70 | }; 71 | 72 | /** 73 | * @param from - id of object to check relation from 74 | * @param to - id of object to check relation to 75 | * @param type - type of relation 76 | * @param direction - all | incoming | outgoing 77 | * @param cb - callback (err, rel || false) 78 | */ 79 | cls.relationshipExists = function relationshipExists(from, to, type, direction, cb) { 80 | neo.node(from, function(err, node) { 81 | if (err) return cb(err); 82 | node._getRelationships(direction, type, function(err, rels) { 83 | if (err && cb) return cb(err); 84 | if (err && !cb) throw err; 85 | var found = false; 86 | if (rels && rels.forEach) { 87 | rels.forEach(function(r) { 88 | if (r.start.id === from && r.end.id === to) { 89 | found = true; 90 | } 91 | }); 92 | } 93 | cb && cb(err, found); 94 | }); 95 | }); 96 | }; 97 | 98 | cls.createRelationshipTo = function createRelationshipTo(id1, id2, type, data, cb) { 99 | var fromNode, toNode; 100 | neo.node(id1, function(err, node) { 101 | if (err && cb) return cb(err); 102 | if (err && !cb) throw err; 103 | fromNode = node; 104 | ok(); 105 | }); 106 | neo.node(id2, function(err, node) { 107 | if (err && cb) return cb(err); 108 | if (err && !cb) throw err; 109 | toNode = node; 110 | ok(); 111 | }); 112 | function ok() { 113 | if (fromNode && toNode) { 114 | fromNode.createRelationshipTo(toNode, type, cleanup(data), cb); 115 | } 116 | } 117 | }; 118 | 119 | cls.createRelationshipFrom = function createRelationshipFrom(id1, id2, type, data, cb) { 120 | cls.createRelationshipTo(id2, id1, type, data, cb); 121 | }; 122 | 123 | // only create relationship if it is not exists 124 | cls.ensureRelationshipTo = function(id1, id2, type, data, cb) { 125 | cls.relationshipExists(id1, id2, type, 'outgoing', function(err, exists) { 126 | if (err && cb) return cb(err); 127 | if (err && !cb) throw err; 128 | if (exists) return cb && cb(null); 129 | cls.createRelationshipTo(id1, id2, type, data, cb); 130 | }); 131 | }; 132 | }; 133 | 134 | Neo4j.prototype.mixInstanceMethods = function mixInstanceMethods(proto) { 135 | /** 136 | * @param obj - Object or id of object to check relation with 137 | * @param type - type of relation 138 | * @param cb - callback (err, rel || false) 139 | */ 140 | proto.isInRelationWith = function isInRelationWith(obj, type, direction, cb) { 141 | this.constructor.relationshipExists(this.id, obj.id || obj, type, 'all', cb); 142 | }; 143 | }; 144 | 145 | Neo4j.prototype.node = function find(id, callback) { 146 | if (this.cache[id]) { 147 | callback(null, this.cache[id]); 148 | } else { 149 | this.client.getNodeById(id, function(err, node) { 150 | if (node) { 151 | this.cache[id] = node; 152 | } 153 | callback(err, node); 154 | }.bind(this)); 155 | } 156 | }; 157 | 158 | Neo4j.prototype.create = function create(model, data, callback) { 159 | var table = this.table(model); 160 | data.nodeType = table; 161 | var node = this.client.createNode(); 162 | node.data = cleanup(data); 163 | node.data.nodeType = table; 164 | node.save(function(err) { 165 | if (err) { 166 | return callback(err); 167 | } 168 | this.cache[node.id] = node; 169 | node.index(table, 'id', node.id, function(err) { 170 | if (err) return callback(err); 171 | this.updateIndexes(model, node, function(err) { 172 | if (err) return callback(err); 173 | callback(null, node.id); 174 | }); 175 | }.bind(this)); 176 | }.bind(this)); 177 | }; 178 | 179 | Neo4j.prototype.updateIndexes = function updateIndexes(model, node, cb) { 180 | var props = this._models[model].properties; 181 | var wait = 1; 182 | var table = this.table(model); 183 | Object.keys(props).forEach(function(key) { 184 | if (props[key].index && node.data[key]) { 185 | wait += 1; 186 | node.index(table, key, node.data[key], done); 187 | } 188 | }); 189 | 190 | done(); 191 | 192 | var error = false; 193 | function done(err) { 194 | error = error || err; 195 | if (--wait === 0) { 196 | cb(error); 197 | } 198 | } 199 | }; 200 | 201 | Neo4j.prototype.save = function save(model, data, callback) { 202 | var self = this; 203 | this.node(data.id, function(err, node) { 204 | //delete id property since that's redundant and we use the node.id 205 | delete data.id; 206 | if (err) return callback(err); 207 | node.data = cleanup(data); 208 | node.save(function(err) { 209 | if (err) return callback(err); 210 | self.updateIndexes(model, node, function(err) { 211 | if (err) return console.log(err); 212 | //map node id to the id property being sent back 213 | node.data.id = node.id; 214 | callback(null, node.data); 215 | }); 216 | }); 217 | }); 218 | }; 219 | 220 | Neo4j.prototype.exists = function exists(model, id, callback) { 221 | delete this.cache[id]; 222 | this.node(id, callback); 223 | }; 224 | 225 | Neo4j.prototype.find = function find(model, id, callback) { 226 | delete this.cache[id]; 227 | this.node(id, function(err, node) { 228 | if (node && node.data) { 229 | node.data.id = id; 230 | } 231 | callback(err, this.readFromDb(model, node && node.data)); 232 | }.bind(this)); 233 | }; 234 | 235 | Neo4j.prototype.readFromDb = function readFromDb(model, data) { 236 | if (!data) return data; 237 | var res = {}; 238 | var props = this._models[model].properties; 239 | Object.keys(data).forEach(function(key) { 240 | if (props[key] && props[key].type.name === 'Date') { 241 | res[key] = new Date(data[key]); 242 | } else { 243 | res[key] = data[key]; 244 | } 245 | }); 246 | return res; 247 | }; 248 | 249 | Neo4j.prototype.destroy = function destroy(model, id, callback) { 250 | var force = true; 251 | this.node(id, function(err, node) { 252 | if (err) return callback(err); 253 | node.delete(function(err) { 254 | if (err) return callback(err); 255 | delete this.cache[id]; 256 | }.bind(this), force); 257 | }); 258 | }; 259 | 260 | Neo4j.prototype.all = function all(model, filter, callback) { 261 | this.client.queryNodeIndex(this.table(model), 'id:*', function(err, nodes) { 262 | if (nodes) { 263 | nodes = nodes.map(function(obj) { 264 | obj.data.id = obj.id; 265 | return this.readFromDb(model, obj.data); 266 | }.bind(this)); 267 | } 268 | if (filter) { 269 | nodes = nodes ? nodes.filter(applyFilter(filter)) : nodes; 270 | if (filter.order) { 271 | var key = filter.order.split(' ')[0]; 272 | var dir = filter.order.split(' ')[1]; 273 | nodes = nodes.sort(function(a, b) { 274 | return a[key] > b[key]; 275 | }); 276 | if (dir === 'DESC') nodes = nodes.reverse(); 277 | } 278 | } 279 | callback(err, nodes); 280 | }.bind(this)); 281 | }; 282 | 283 | Neo4j.prototype.allNodes = function all(model, callback) { 284 | this.client.queryNodeIndex(this.table(model), 'id:*', function(err, nodes) { 285 | callback(err, nodes); 286 | }); 287 | }; 288 | 289 | function applyFilter(filter) { 290 | if (typeof filter.where === 'function') { 291 | return filter.where; 292 | } 293 | var keys = Object.keys(filter.where || {}); 294 | return function(obj) { 295 | var pass = true; 296 | keys.forEach(function(key) { 297 | if (!test(filter.where[key], obj[key])) { 298 | pass = false; 299 | } 300 | }); 301 | return pass; 302 | }; 303 | 304 | function test(example, value) { 305 | if (typeof value === 'string' && example && example.constructor.name === 'RegExp') { 306 | return value.match(example); 307 | } 308 | 309 | if (typeof value === 'object' && value.constructor.name === 'Date' && typeof example === 'object' && example.constructor.name === 'Date') { 310 | return example.toString() === value.toString(); 311 | } 312 | 313 | // not strict equality 314 | return String(example) === String(value); 315 | } 316 | } 317 | 318 | Neo4j.prototype.destroyAll = function destroyAll(model, callback) { 319 | var wait, error = null; 320 | this.allNodes(model, function(err, collection) { 321 | if (err) return callback(err); 322 | wait = collection.length; 323 | collection && collection.forEach && collection.forEach(function(node) { 324 | node.delete(done, true); 325 | }); 326 | }); 327 | 328 | function done(err) { 329 | error = error || err; 330 | if (--wait === 0) { 331 | callback(error); 332 | } 333 | } 334 | }; 335 | 336 | Neo4j.prototype.count = function count(model, callback, conds) { 337 | this.all(model, {where: conds}, function(err, collection) { 338 | callback(err, collection ? collection.length : 0); 339 | }); 340 | }; 341 | 342 | Neo4j.prototype.updateAttributes = function updateAttributes(model, id, data, cb) { 343 | data.id = id; 344 | this.node(id, function(err, node) { 345 | this.save(model, merge(node.data, data), cb); 346 | }.bind(this)); 347 | }; 348 | 349 | Neo4j.prototype.table = function(model) { 350 | return this._models[model].model.tableName; 351 | }; 352 | 353 | function cleanup(data) { 354 | if (!data) return null; 355 | 356 | var res = {}; 357 | Object.keys(data).forEach(function(key) { 358 | var v = data[key]; 359 | if (v === null) { 360 | // skip 361 | // console.log('skip null', key); 362 | } else if (v && v.constructor.name === 'Array' && v.length === 0) { 363 | // skip 364 | // console.log('skip blank array', key); 365 | } else if (typeof v !== 'undefined') { 366 | res[key] = v; 367 | } 368 | }); 369 | return res; 370 | } 371 | 372 | function merge(base, update) { 373 | Object.keys(update).forEach(function(key) { 374 | base[key] = update[key]; 375 | }); 376 | return base; 377 | } 378 | -------------------------------------------------------------------------------- /lib/relations.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Dependencies 3 | */ 4 | const i8n = require('inflection'); 5 | const defineScope = require('./scope.js').defineScope; 6 | 7 | /** 8 | * Relations mixins for ./model.js 9 | */ 10 | const Model = require('./model.js'); 11 | 12 | Model.relationNameFor = function relationNameFor(foreignKey) { 13 | let found; 14 | Object.keys(this.relations).forEach(rel => { 15 | if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) { 16 | found = rel; 17 | } 18 | }); 19 | return found; 20 | }; 21 | 22 | /** 23 | * Declare hasMany relation 24 | * 25 | * @param {Model} anotherClass - class to has many 26 | * @param {Object} params - configuration {as:, foreignKey:} 27 | * @example `User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});` 28 | */ 29 | Model.hasMany = function hasMany(anotherClass, params) { 30 | const thisClassName = this.modelName; 31 | params = params || {}; 32 | if (typeof anotherClass === 'string') { 33 | params.as = anotherClass; 34 | if (params.model) { 35 | anotherClass = params.model; 36 | } else { 37 | const anotherClassName = i8n.singularize(anotherClass).toLowerCase(); 38 | Object.keys(this.schema.models).forEach(name => { 39 | if (name.toLowerCase() === anotherClassName) { 40 | anotherClass = this.schema.models[name]; 41 | } 42 | }); 43 | } 44 | } 45 | const methodName = params.as || 46 | i8n.camelize(i8n.pluralize(anotherClass.modelName), true); 47 | const fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true); 48 | 49 | this.relations[methodName] = { 50 | type: 'hasMany', 51 | keyFrom: 'id', 52 | keyTo: fk, 53 | modelTo: anotherClass, 54 | multiple: true 55 | }; 56 | // each instance of this class should have method named 57 | // pluralize(anotherClass.modelName) 58 | // which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb); 59 | const scopeMethods = { 60 | find, 61 | destroy 62 | }; 63 | 64 | let fk2; 65 | 66 | if (params.through) { 67 | 68 | // Append through relation, like modelTo 69 | this.relations[methodName].modelThrough = params.through; 70 | 71 | fk2 = i8n.camelize(anotherClass.modelName + '_id', true); 72 | scopeMethods.create = function create(data, done) { 73 | if (typeof data !== 'object') { 74 | done = data; 75 | data = {}; 76 | } 77 | const self = this; 78 | return anotherClass.create(data) 79 | .then(ac => { 80 | const d = {}; 81 | d[params.through.relationNameFor(fk)] = self; 82 | d[params.through.relationNameFor(fk2)] = ac; 83 | return params.through.create(d) 84 | .then(() => ac) 85 | .catch(e => { 86 | return ac.destroy() 87 | .then(() => { 88 | throw e; 89 | }); 90 | }); 91 | }) 92 | .then(ac => { 93 | if (typeof done === 'function') { 94 | done(null, ac); 95 | } else { 96 | return ac; 97 | } 98 | }) 99 | .catch(err => { 100 | if (typeof done === 'function') { 101 | done(err); 102 | } else { 103 | throw err; 104 | } 105 | }); 106 | }; 107 | scopeMethods.add = function(acInst, data, done) { 108 | if (typeof data === 'function') { 109 | done = data; 110 | data = {}; 111 | } 112 | if (typeof data === 'undefined') { 113 | data = {}; 114 | } 115 | const query = {}; 116 | query[fk] = this.id; 117 | data[params.through.relationNameFor(fk)] = this; 118 | query[fk2] = acInst.id || acInst; 119 | data[params.through.relationNameFor(fk2)] = acInst; 120 | return params.through.findOrCreate({ where: query }, data) 121 | .then(through => { 122 | if (typeof done === 'function') { 123 | done(null, through); 124 | } else { 125 | return through; 126 | } 127 | }) 128 | .catch(err => { 129 | if (typeof done === 'function') { 130 | done(err); 131 | } else { 132 | throw err; 133 | } 134 | }); 135 | }; 136 | scopeMethods.remove = function(acInst, done) { 137 | const q = {}; 138 | q[fk] = this.id; 139 | q[fk2] = acInst.id || acInst; 140 | return params.through.findOne({ where: q }) 141 | .then(d => { 142 | if (d) { 143 | return d.destroy(); 144 | } 145 | }) 146 | .then(() => { 147 | if (typeof done === 'function') { 148 | done(); 149 | } 150 | }) 151 | .catch(e => { 152 | if (typeof done === 'function') { 153 | done(e); 154 | } else { 155 | throw e; 156 | } 157 | }); 158 | }; 159 | delete scopeMethods.destroy; 160 | } 161 | 162 | defineScope(this.prototype, params.through || anotherClass, methodName, function() { 163 | const filter = {}; 164 | filter.where = {}; 165 | filter.where[fk] = this.id; 166 | if (params.through) { 167 | filter.collect = i8n.camelize(anotherClass.modelName, true); 168 | filter.include = filter.collect; 169 | } 170 | return filter; 171 | }, scopeMethods); 172 | 173 | if (!params.through) { 174 | // obviously, anotherClass should have attribute called `fk` 175 | anotherClass.schema.defineForeignKey(anotherClass.modelName, fk, this.modelName); 176 | } 177 | 178 | function find(id, cb) { 179 | const id_ = this.id; 180 | return anotherClass.find(id) 181 | .then(inst => { 182 | if (!inst) { 183 | throw new Error('Not found'); 184 | } 185 | if (inst[fk] && inst[fk].toString() === id_.toString()) { 186 | if (typeof cb === 'function') { 187 | cb(null, inst); 188 | } else { 189 | return inst; 190 | } 191 | } else { 192 | throw new Error('Permission denied'); 193 | } 194 | }) 195 | .catch(err => { 196 | if (typeof cb === 'function') { 197 | cb(err); 198 | } else { 199 | throw err; 200 | } 201 | }); 202 | } 203 | 204 | function destroy(id, cb) { 205 | const id_ = this.id; 206 | return anotherClass.find(id) 207 | .then(inst => { 208 | if (!inst) { 209 | throw new Error('Not found'); 210 | } 211 | if (inst[fk] && inst[fk].toString() === id_.toString()) { 212 | return inst.destroy() 213 | .then(() => { 214 | if (typeof cb === 'function') { 215 | cb(); 216 | } 217 | }); 218 | } 219 | throw new Error('Permission denied'); 220 | }) 221 | .catch(err => { 222 | if (typeof cb === 'function') { 223 | cb(err); 224 | } else { 225 | throw err; 226 | } 227 | }); 228 | } 229 | 230 | }; 231 | 232 | /** 233 | * Declare belongsTo relation 234 | * 235 | * @param {Class} anotherClass - class to belong 236 | * @param {Object} params - configuration {as: 'propertyName', foreignKey: 'keyName'} 237 | * 238 | * **Usage examples** 239 | * Suppose model Post have a *belongsTo* relationship with User (the author of the post). You could declare it this way: 240 | * Post.belongsTo(User, {as: 'author', foreignKey: 'userId'}); 241 | * 242 | * When a post is loaded, you can load the related author with: 243 | * post.author(function(err, user) { 244 | * // the user variable is your user object 245 | * }); 246 | * 247 | * The related object is cached, so if later you try to get again the author, no additional request will be made. 248 | * But there is an optional boolean parameter in first position that set whether or not you want to reload the cache: 249 | * post.author(true, function(err, user) { 250 | * // The user is reloaded, even if it was already cached. 251 | * }); 252 | * 253 | * This optional parameter default value is false, so the related object will be loaded from cache if available. 254 | */ 255 | Model.belongsTo = function(anotherClass, params) { 256 | params = params || {}; 257 | if (typeof anotherClass === 'string') { 258 | params.as = anotherClass; 259 | if (params.model) { 260 | anotherClass = params.model; 261 | } else { 262 | const anotherClassName = anotherClass.toLowerCase(); 263 | Object.keys(this.schema.models).forEach(name => { 264 | if (name.toLowerCase() === anotherClassName) { 265 | anotherClass = this.schema.models[name]; 266 | } 267 | }); 268 | } 269 | } 270 | const methodName = params.as || i8n.camelize(anotherClass.modelName, true); 271 | const fk = params.foreignKey || methodName + 'Id'; 272 | 273 | this.relations[methodName] = { 274 | type: 'belongsTo', 275 | keyFrom: fk, 276 | keyTo: 'id', 277 | modelTo: anotherClass, 278 | multiple: false 279 | }; 280 | 281 | this.schema.defineForeignKey(this.modelName, fk, anotherClass.modelName); 282 | this.prototype.__finders__ = this.prototype.__finders__ || {}; 283 | 284 | this.prototype.__finders__[methodName] = function(id) { 285 | if (id === null || !this[fk]) { 286 | return Promise.resolve(null); 287 | } 288 | const fk_ = this[fk].toString(); 289 | return anotherClass.find(id) 290 | .then(inst => { 291 | if (!inst) { 292 | return null; 293 | } 294 | 295 | if (inst.id.toString() === fk_) { 296 | return inst; 297 | } 298 | 299 | throw new Error('Permission denied'); 300 | }); 301 | }; 302 | 303 | this.prototype[methodName] = function(p) { 304 | const self = this; 305 | const cachedValue = this.__cachedRelations && this.__cachedRelations[methodName]; 306 | // acts as setter 307 | if (p instanceof Model) { 308 | this.__cachedRelations[methodName] = p; 309 | return this.updateAttribute(fk, p.id); 310 | } 311 | // acts as async getter 312 | if (typeof cachedValue === 'undefined') { 313 | return this.__finders__[methodName].call(self, this[fk]) 314 | .then(inst => { 315 | self.__cachedRelations[methodName] = inst; 316 | if (typeof p === 'function') { 317 | p(null, inst); 318 | } else { 319 | return inst; 320 | } 321 | }); 322 | } 323 | // return cached relation 324 | if (typeof p === 'function') { 325 | p(null, cachedValue); 326 | } else { 327 | return Promise.resolve(cachedValue); 328 | } 329 | }; 330 | 331 | }; 332 | 333 | /** 334 | * Many-to-many relation 335 | * 336 | * Post.hasAndBelongsToMany('tags'); creates connection model 'PostTag' 337 | */ 338 | Model.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) { 339 | params = params || {}; 340 | const models = this.schema.models; 341 | 342 | if (typeof anotherClass === 'string') { 343 | params.as = anotherClass; 344 | if (params.model) { 345 | anotherClass = params.model; 346 | } else { 347 | anotherClass = lookupModel(i8n.singularize(anotherClass)) || 348 | anotherClass; 349 | } 350 | if (typeof anotherClass === 'string') { 351 | throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName); 352 | } 353 | } 354 | 355 | if (!params.through) { 356 | const name1 = this.modelName + anotherClass.modelName; 357 | const name2 = anotherClass.modelName + this.modelName; 358 | params.through = lookupModel(name1) || lookupModel(name2) || 359 | this.schema.define(name1); 360 | } 361 | params.through.belongsTo(this); 362 | params.through.belongsTo(anotherClass); 363 | 364 | this.hasMany(anotherClass, { as: params.as, through: params.through }); 365 | 366 | function lookupModel(modelName) { 367 | const lookupClassName = modelName.toLowerCase(); 368 | let found; 369 | Object.keys(models).forEach(name => { 370 | if (name.toLowerCase() === lookupClassName) { 371 | found = models[name]; 372 | } 373 | }); 374 | return found; 375 | } 376 | 377 | }; 378 | -------------------------------------------------------------------------------- /test/relations.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let db, Book, Chapter, Author, Reader; 5 | 6 | describe('relations', function() { 7 | before(function(done) { 8 | db = getSchema(); 9 | Book = db.define('Book', { name: String }); 10 | Chapter = db.define('Chapter', { name: { type: String, index: true, limit: 20 } }); 11 | Author = db.define('Author', { name: String }); 12 | Reader = db.define('Reader', { name: String }); 13 | 14 | db.automigrate(function() { 15 | Book.destroyAll().then(function() { 16 | return Chapter.destroyAll(); 17 | }).then(function() { 18 | return Author.destroyAll(); 19 | }).then(function() { 20 | return Reader.destroyAll(); 21 | }).then(done); 22 | }); 23 | }); 24 | 25 | after(function() { 26 | db.disconnect(); 27 | }); 28 | 29 | describe('hasMany', function() { 30 | it('can be declared in different ways', function(done) { 31 | Book.hasMany(Chapter); 32 | Book.hasMany(Reader, { as: 'users' }); 33 | Book.hasMany(Author, { foreignKey: 'projectId' }); 34 | const b = new Book(); 35 | b.chapters.should.be.an.instanceOf(Function); 36 | b.users.should.be.an.instanceOf(Function); 37 | b.authors.should.be.an.instanceOf(Function); 38 | (new Chapter()).toObject().should.have.property('bookId'); 39 | (new Author()).toObject().should.have.property('projectId'); 40 | 41 | db.automigrate(done); 42 | }); 43 | 44 | it('can be declared in short form', function(done) { 45 | Author.hasMany('readers'); 46 | (new Author()).readers.should.be.an.instanceOf(Function); 47 | (new Reader()).toObject().should.have.property('authorId'); 48 | 49 | db.autoupdate(done); 50 | }); 51 | 52 | it('should build record on scope', function(done) { 53 | Book.create(function(err, book) { 54 | const c = book.chapters.build(); 55 | c.bookId.should.equal(book.id); 56 | c.save(done); 57 | }); 58 | }); 59 | 60 | it('should create record on scope', function() { 61 | let book; 62 | return Book.create() 63 | .then(function(book_) { 64 | book = book_; 65 | return book.chapters.create(); 66 | }).then(function(c) { 67 | should.exist(c); 68 | c.bookId.should.equal(book.id); 69 | }); 70 | }); 71 | 72 | it.skip('should fetch all scoped instances', function(done) { 73 | Book.create(function(err, book) { 74 | book.chapters.create({ name: 'a' }, function() { 75 | book.chapters.create({ name: 'z' }, function() { 76 | book.chapters.create({ name: 'c' }, function() { 77 | fetch(book); 78 | }); 79 | }); 80 | }); 81 | }); 82 | function fetch(book) { 83 | book.chapters(function(err, ch) { 84 | should.not.exist(err); 85 | should.exist(ch); 86 | ch.should.have.lengthOf(3); 87 | 88 | book.chapters({ order: 'name DESC' }, function(e, c) { 89 | should.not.exist(e); 90 | should.exist(c); 91 | c.shift().name.should.equal('z'); 92 | c.pop().name.should.equal('a'); 93 | done(); 94 | }); 95 | }); 96 | } 97 | }); 98 | 99 | it('should find scoped record', function(done) { 100 | let id; 101 | Book.create(function(err, book) { 102 | book.chapters.create({ name: 'a' }, function(err, ch) { 103 | id = ch.id; 104 | book.chapters.create({ name: 'z' }, function() { 105 | book.chapters.create({ name: 'c' }, function() { 106 | fetch(book); 107 | }); 108 | }); 109 | }); 110 | }); 111 | 112 | function fetch(book) { 113 | book.chapters.find(id, function(err, ch) { 114 | should.not.exist(err); 115 | should.exist(ch); 116 | ch.id.should.equal(id); 117 | done(); 118 | }); 119 | } 120 | }); 121 | 122 | it('should destroy scoped record', function(done) { 123 | Book.create(function(err, book) { 124 | book.chapters.create({ name: 'a' }, function(err, ch) { 125 | book.chapters.destroy(ch.id, function(err) { 126 | should.not.exist(err); 127 | book.chapters.find(ch.id, function(err, ch) { 128 | should.exist(err); 129 | err.message.should.equal('Not found'); 130 | should.not.exist(ch); 131 | done(); 132 | }); 133 | }); 134 | }); 135 | }); 136 | }); 137 | 138 | it('should not allow destroy not scoped records', function(done) { 139 | Book.create(function(err, book1) { 140 | book1.chapters.create({ name: 'a' }, function(err, ch) { 141 | const id = ch.id; 142 | Book.create(function(err, book2) { 143 | book2.chapters.destroy(ch.id, function(err) { 144 | should.exist(err); 145 | err.message.should.equal('Permission denied'); 146 | book1.chapters.find(ch.id, function(err, ch) { 147 | should.not.exist(err); 148 | should.exist(ch); 149 | ch.id.should.equal(id); 150 | done(); 151 | }); 152 | }); 153 | }); 154 | }); 155 | }); 156 | }); 157 | }); 158 | 159 | describe('belongsTo', function() { 160 | let List, Item, Fear, Mind; 161 | 162 | it('can be declared in different ways', function() { 163 | List = db.define('List', { name: String }); 164 | Item = db.define('Item', { name: String }); 165 | Fear = db.define('Fear'); 166 | Mind = db.define('Mind'); 167 | 168 | // syntax 1 (old) 169 | Item.belongsTo(List); 170 | (new Item()).toObject().should.have.property('listId'); 171 | (new Item()).list.should.be.an.instanceOf(Function); 172 | 173 | // syntax 2 (new) 174 | Fear.belongsTo('mind'); 175 | (new Fear()).toObject().should.have.property('mindId'); 176 | (new Fear()).mind.should.be.an.instanceOf(Function); 177 | // (new Fear).mind.build().should.be.an.instanceOf(Mind); 178 | }); 179 | 180 | it('can be used to query data', function(done) { 181 | List.hasMany('todos', { model: Item }); 182 | db.automigrate(function() { 183 | List.create(function(e, list) { 184 | should.not.exist(e); 185 | should.exist(list); 186 | list.todos.create(function(err, todo) { 187 | todo.list(function(e, l) { 188 | should.not.exist(e); 189 | should.exist(l); 190 | l.should.be.an.instanceOf(List); 191 | done(); 192 | }); 193 | }); 194 | }); 195 | }); 196 | }); 197 | 198 | it('can be used to query data as promise', function() { 199 | List.hasMany('todos', { model: Item }); 200 | return db.automigrate() 201 | .then(function() { 202 | return List.create(); 203 | }) 204 | .then(function(list) { 205 | should.exist(list); 206 | return list.todos.create(); 207 | }) 208 | .then(function(todo) { 209 | return todo.list(); 210 | }) 211 | .then(function(l) { 212 | should.exist(l); 213 | l.should.be.an.instanceOf(List); 214 | }); 215 | }); 216 | 217 | it('could accept objects when creating on scope', function(done) { 218 | List.create(function(e, list) { 219 | should.not.exist(e); 220 | should.exist(list); 221 | Item.create({ list }, function(err, item) { 222 | should.not.exist(err); 223 | should.exist(item); 224 | should.exist(item.listId); 225 | item.listId.should.equal(list.id); 226 | item.__cachedRelations.list.should.equal(list); 227 | done(); 228 | }); 229 | }); 230 | }); 231 | 232 | }); 233 | 234 | describe('hasAndBelongsToMany', function() { 235 | let Article, Tag, ArticleTag; 236 | 237 | before(function(done) { 238 | Article = db.define('Article', { title: String }); 239 | Tag = db.define('Tag', { name: String }); 240 | Article.hasAndBelongsToMany('tags'); 241 | ArticleTag = db.models.ArticleTag; 242 | db.automigrate(function() { 243 | Article.destroyAll(function() { 244 | Tag.destroyAll(function() { 245 | ArticleTag.destroyAll(done); 246 | }); 247 | }); 248 | }); 249 | }); 250 | 251 | it('should allow to create instances on scope', function(done) { 252 | Article.create(function(e, article) { 253 | article.tags.create({ name: 'popular' }, function(e, t) { 254 | t.should.be.an.instanceOf(Tag); 255 | ArticleTag.findOne(function(e, at) { 256 | should.exist(at); 257 | at.tagId.toString().should.equal(t.id.toString()); 258 | at.articleId.toString().should.equal(article.id.toString()); 259 | done(); 260 | }); 261 | }); 262 | }); 263 | }); 264 | 265 | it('should allow to fetch scoped instances', function(done) { 266 | Article.findOne(function(e, article) { 267 | article.tags(function(e, tags) { 268 | should.not.exist(e); 269 | should.exist(tags); 270 | done(); 271 | }); 272 | }); 273 | }); 274 | 275 | it('should allow to add connection with instance', function(done) { 276 | Article.findOne(function(e, article) { 277 | Tag.create({ name: 'awesome' }, function(e, tag) { 278 | article.tags.add(tag, function(e, at) { 279 | should.not.exist(e); 280 | should.exist(at); 281 | at.should.be.an.instanceOf(ArticleTag); 282 | at.tagId.should.equal(tag.id); 283 | at.articleId.should.equal(article.id); 284 | done(); 285 | }); 286 | }); 287 | }); 288 | }); 289 | 290 | it('should allow to remove connection with instance', function(done) { 291 | Article.findOne(function(e, article) { 292 | article.tags(function(e, tags) { 293 | const len = tags.length; 294 | tags.should.not.be.empty; 295 | should.exist(tags[0]); 296 | article.tags.remove(tags[0], function(e) { 297 | if (e) { 298 | console.log(e.stack); 299 | } 300 | should.not.exist(e); 301 | article.tags(true, function(e, tags) { 302 | tags.should.have.lengthOf(len - 1); 303 | done(); 304 | }); 305 | }); 306 | }); 307 | }); 308 | }); 309 | 310 | it('should remove the correct connection', function() { 311 | let article1, article2, tag; 312 | return Article.create({ title: 'Article 1' }) 313 | .then(function(article1_) { 314 | article1 = article1_; 315 | return Article.create({ title: 'Article 2' }); 316 | }) 317 | .then(function(article2_) { 318 | article2 = article2_; 319 | return Tag.create({ name: 'correct' }); 320 | }) 321 | .then(function(tag_) { 322 | tag = tag_; 323 | return article1.tags.add(tag); 324 | }) 325 | .then(function() { 326 | return article2.tags.add(tag); 327 | }) 328 | .then(function() { 329 | return article2.tags(); 330 | }) 331 | .then(function(tags) { 332 | tags.should.have.lengthOf(1); 333 | return article2.tags.remove(tag); 334 | }) 335 | .then(function() { 336 | delete article2.__cachedRelations.tags; 337 | return article2.tags(); 338 | }) 339 | .then(function(tags) { 340 | tags.should.have.lengthOf(0); 341 | return article1.tags(); 342 | }) 343 | .then(function(tags) { 344 | tags.should.have.lengthOf(1); 345 | }); 346 | }); 347 | 348 | }); 349 | 350 | }); 351 | -------------------------------------------------------------------------------- /test/hooks.test.js: -------------------------------------------------------------------------------- 1 | // This test written in mocha+should.js 2 | const should = require('./init.js'); 3 | 4 | let j = require('../'), 5 | Schema = j.Schema, 6 | AbstractClass = j.AbstractClass, 7 | Hookable = j.Hookable, 8 | 9 | db, User; 10 | 11 | describe('hooks', function() { 12 | 13 | before(function(done) { 14 | db = getSchema(); 15 | 16 | User = db.define('User', { 17 | email: { type: String, index: true, limit: 100 }, 18 | name: String, 19 | password: String, 20 | state: String 21 | }); 22 | 23 | db.automigrate(done); 24 | }); 25 | 26 | describe('behavior', function() { 27 | 28 | it('should allow to break flow in case of error', function(done) { 29 | 30 | const Model = db.define('Model'); 31 | Model.beforeCreate = function(next, data) { 32 | next(new Error('Fail')); 33 | }; 34 | 35 | Model.create(function(err, model) { 36 | should.not.exist(model); 37 | should.exist(err); 38 | done(); 39 | }); 40 | }); 41 | }); 42 | 43 | describe('initialize', function() { 44 | 45 | afterEach(function() { 46 | User.afterInitialize = null; 47 | }); 48 | 49 | it('should be triggered on new', function(done) { 50 | User.afterInitialize = function() { 51 | done(); 52 | }; 53 | new User; 54 | }); 55 | 56 | it('should be triggered on create', function(done) { 57 | let user; 58 | User.afterInitialize = function() { 59 | if (this.name === 'Nickolay') { 60 | this.name += ' Rozental'; 61 | } 62 | }; 63 | User.create({ name: 'Nickolay' }, function(err, u) { 64 | u.id.should.be.ok; 65 | u.name.should.equal('Nickolay Rozental'); 66 | done(); 67 | }); 68 | }); 69 | 70 | }); 71 | 72 | describe('create', function() { 73 | 74 | afterEach(removeHooks('Create')); 75 | 76 | it('should be triggered on create', function(done) { 77 | addHooks('Create', done); 78 | User.create(); 79 | }); 80 | 81 | it('should not be triggered on new', function() { 82 | User.beforeCreate = function(next) { 83 | should.fail('This should not be called'); 84 | next(); 85 | }; 86 | const u = new User; 87 | }); 88 | 89 | it('should be triggered on new+save', function(done) { 90 | addHooks('Create', done); 91 | (new User).save(); 92 | }); 93 | 94 | it('should be triggered on upsert', function(done) { 95 | addHooks('Create', done); 96 | User.upsert({ name: 'Anatoliy' }); 97 | }); 98 | 99 | it('afterCreate should not be triggered on failed create', function(done) { 100 | const old = User.schema.adapter.create; 101 | User.schema.adapter.create = function(modelName, id, cb) { 102 | cb(new Error('error')); 103 | }; 104 | 105 | User.afterCreate = function() { 106 | throw new Error('shouldn\'t be called'); 107 | }; 108 | User.create(function(err, user) { 109 | User.schema.adapter.create = old; 110 | done(); 111 | }); 112 | }); 113 | }); 114 | 115 | describe('save', function() { 116 | afterEach(removeHooks('Save')); 117 | 118 | it('should be triggered on create', function(done) { 119 | addHooks('Save', done); 120 | User.create(); 121 | }); 122 | 123 | it('should be triggered on new+save', function(done) { 124 | addHooks('Save', done); 125 | (new User).save(); 126 | }); 127 | 128 | it('should be triggered on updateAttributes', function(done) { 129 | User.create(function(err, user) { 130 | addHooks('Save', done); 131 | user.updateAttributes({ name: 'Anatoliy' }); 132 | }); 133 | }); 134 | 135 | it('should be triggered on upsert', function(done) { 136 | addHooks('Save', done); 137 | User.upsert(); 138 | }); 139 | 140 | it('should be triggered on save', function(done) { 141 | User.create(function(err, user) { 142 | addHooks('Save', done); 143 | user.name = 'Hamburger'; 144 | user.save(); 145 | }); 146 | }); 147 | 148 | it('should save full object', function(done) { 149 | User.create(function(err, user) { 150 | User.beforeSave = function(next, data) { 151 | data.should.have.keys('id', 'name', 'email', 152 | 'password', 'state'); 153 | done(); 154 | }; 155 | user.save(); 156 | }); 157 | }); 158 | 159 | it('should save actual modifications to database', function(done) { 160 | User.beforeSave = function(next, data) { 161 | data.password = 'hash'; 162 | next(); 163 | }; 164 | User.destroyAll(function() { 165 | User.create({ 166 | email: 'james.bond@example.com', 167 | password: '53cr3t' 168 | }, function() { 169 | User.findOne({ 170 | where: { email: 'james.bond@example.com' } 171 | }, function(err, jb) { 172 | jb.password.should.equal('hash'); 173 | done(); 174 | }); 175 | }); 176 | }); 177 | }); 178 | 179 | it('should save actual modifications on updateAttributes', function(done) { 180 | User.beforeSave = function(next, data) { 181 | data.password = 'hash'; 182 | next(); 183 | }; 184 | User.destroyAll(function() { 185 | User.create({ 186 | email: 'james.bond@example.com' 187 | }, function(err, u) { 188 | u.updateAttribute('password', 'new password', function(e, u) { 189 | should.not.exist(e); 190 | should.exist(u); 191 | u.password.should.equal('hash'); 192 | User.findOne({ 193 | where: { email: 'james.bond@example.com' } 194 | }, function(err, jb) { 195 | jb.password.should.equal('hash'); 196 | done(); 197 | }); 198 | }); 199 | }); 200 | }); 201 | }); 202 | 203 | }); 204 | 205 | describe('update', function() { 206 | afterEach(removeHooks('Update')); 207 | 208 | it('should not be triggered on create', function() { 209 | User.beforeUpdate = function(next) { 210 | should.fail('This should not be called'); 211 | next(); 212 | }; 213 | User.create(); 214 | }); 215 | 216 | it('should not be triggered on new+save', function() { 217 | User.beforeUpdate = function(next) { 218 | should.fail('This should not be called'); 219 | next(); 220 | }; 221 | (new User).save(); 222 | }); 223 | 224 | it('should be triggered on updateAttributes', function(done) { 225 | User.create(function(err, user) { 226 | addHooks('Update', done); 227 | user.updateAttributes({ name: 'Anatoliy' }); 228 | }); 229 | }); 230 | 231 | it('should be triggered on upsert', function(done) { 232 | addHooks('Update', done); 233 | User.upsert({ id: 1 }); 234 | }); 235 | 236 | it('should be triggered on save', function(done) { 237 | User.create(function(err, user) { 238 | addHooks('Update', done); 239 | user.name = 'Hamburger'; 240 | user.save(); 241 | }); 242 | }); 243 | 244 | it('should update limited set of fields', function(done) { 245 | User.create(function(err, user) { 246 | User.beforeUpdate = function(next, data) { 247 | data.should.have.keys('name', 'email'); 248 | done(); 249 | }; 250 | user.updateAttributes({ name: 1, email: 2 }); 251 | }); 252 | }); 253 | 254 | it('should not trigger after-hook on failed save', function(done) { 255 | User.afterUpdate = function() { 256 | should.fail('afterUpdate shouldn\'t be called'); 257 | }; 258 | User.create(function(err, user) { 259 | const save = User.schema.adapter.save; 260 | User.schema.adapter.save = function(modelName, id, cb) { 261 | User.schema.adapter.save = save; 262 | cb(new Error('Error')); 263 | }; 264 | 265 | user.save(function(err) { 266 | done(); 267 | }); 268 | }); 269 | }); 270 | }); 271 | 272 | describe('destroy', function() { 273 | 274 | afterEach(removeHooks('Destroy')); 275 | 276 | it('should be triggered on destroy', function(done) { 277 | let hook = 'not called'; 278 | User.beforeDestroy = function(next) { 279 | hook = 'called'; 280 | next(); 281 | }; 282 | User.afterDestroy = function(next) { 283 | hook.should.eql('called'); 284 | next(); 285 | }; 286 | User.create(function(err, user) { 287 | user.destroy(done); 288 | }); 289 | }); 290 | 291 | it('should not trigger after-hook on failed destroy', function(done) { 292 | const destroy = User.schema.adapter.destroy; 293 | User.schema.adapter.destroy = function(modelName, id, cb) { 294 | cb(new Error('error')); 295 | }; 296 | User.afterDestroy = function() { 297 | should.fail('afterDestroy shouldn\'t be called'); 298 | }; 299 | User.create(function(err, user) { 300 | user.destroy(function(err) { 301 | User.schema.adapter.destroy = destroy; 302 | done(); 303 | }); 304 | }); 305 | }); 306 | 307 | }); 308 | 309 | describe('lifecycle', function() { 310 | let life = [], user; 311 | before(function(done) { 312 | User.beforeSave = function(d) {life.push('beforeSave'); d();}; 313 | User.beforeCreate = function(d) {life.push('beforeCreate'); d();}; 314 | User.beforeUpdate = function(d) {life.push('beforeUpdate'); d();}; 315 | User.beforeDestroy = function(d) {life.push('beforeDestroy'); d();}; 316 | User.beforeValidate = function(d) {life.push('beforeValidate'); d();}; 317 | User.afterInitialize = function() {life.push('afterInitialize'); }; 318 | User.afterSave = function(d) {life.push('afterSave'); d();}; 319 | User.afterCreate = function(d) {life.push('afterCreate'); d();}; 320 | User.afterUpdate = function(d) {life.push('afterUpdate'); d();}; 321 | User.afterDestroy = function(d) {life.push('afterDestroy'); d();}; 322 | User.afterValidate = function(d) {life.push('afterValidate'); d();}; 323 | User.create(function(e, u) { 324 | user = u; 325 | life = []; 326 | done(); 327 | }); 328 | }); 329 | beforeEach(function() { 330 | life = []; 331 | }); 332 | 333 | it('should describe create sequence', function(done) { 334 | User.create(function() { 335 | life.should.eql([ 336 | 'afterInitialize', 337 | 'beforeValidate', 338 | 'afterValidate', 339 | 'beforeCreate', 340 | 'beforeSave', 341 | 'afterSave', 342 | 'afterCreate' 343 | ]); 344 | done(); 345 | }); 346 | }); 347 | 348 | it('should describe new+save sequence', function(done) { 349 | const u = new User; 350 | u.save(function() { 351 | life.should.eql([ 352 | 'afterInitialize', 353 | 'beforeValidate', 354 | 'afterValidate', 355 | 'beforeCreate', 356 | 'beforeSave', 357 | 'afterSave', 358 | 'afterCreate' 359 | ]); 360 | done(); 361 | }); 362 | }); 363 | 364 | it('should describe updateAttributes sequence', function(done) { 365 | user.updateAttributes({ name: 'Antony' }, function() { 366 | life.should.eql([ 367 | 'beforeValidate', 368 | 'afterValidate', 369 | 'beforeSave', 370 | 'beforeUpdate', 371 | 'afterUpdate', 372 | 'afterSave', 373 | ]); 374 | done(); 375 | }); 376 | }); 377 | 378 | it('should describe isValid sequence', function(done) { 379 | should.not.exist( 380 | user.constructor._validations, 381 | 'Expected user to have no validations, but she have'); 382 | user.isValid(function(valid) { 383 | valid.should.be.true; 384 | life.should.eql([ 385 | 'beforeValidate', 386 | 'afterValidate' 387 | ]); 388 | done(); 389 | }); 390 | }); 391 | 392 | it('should describe destroy sequence', function(done) { 393 | user.destroy(function() { 394 | life.should.eql([ 395 | 'beforeDestroy', 396 | 'afterDestroy' 397 | ]); 398 | done(); 399 | }); 400 | }); 401 | 402 | }); 403 | }); 404 | 405 | function addHooks(name, done) { 406 | let called = false, random = String(Math.floor(Math.random() * 1000)); 407 | User['before' + name] = function(next, data) { 408 | called = true; 409 | data.email = random; 410 | next(); 411 | }; 412 | User['after' + name] = function(next) { 413 | (new Boolean(called)).should.equal(true); 414 | this.email.should.equal(random); 415 | done(); 416 | }; 417 | } 418 | 419 | function removeHooks(name) { 420 | return function() { 421 | User['after' + name] = null; 422 | User['before' + name] = null; 423 | }; 424 | } 425 | --------------------------------------------------------------------------------