├── .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 |
--------------------------------------------------------------------------------