├── test ├── createdb.sql ├── tests │ ├── cleanUp.js │ └── setup.js ├── main.js ├── filterBuilder.js ├── postgres.sql └── mysql.sql ├── bin └── related.js ├── lib ├── Lock.js ├── clone.js ├── ModelDefinition.js ├── JoinStatement.js ├── ExtensionManager.js ├── FullTextQueryBuilder.js ├── Selector.js ├── AdvancedQueryBuilder.js ├── Migration.js ├── Query.js ├── FilterBuilder.js ├── Database.js ├── Set.js ├── StaticORM.js ├── ModelCloner.js ├── Entity.js ├── QueryCompiler.js ├── TransactionBuilder.js ├── ModelBuilder.js ├── QueryBuilderBuilder.js └── RelatingSet.js ├── config.js.dist ├── .gitignore ├── index.js ├── .travis.yml ├── .jshintrc ├── docs ├── Docs │ └── index.json ├── index.json ├── teaser.md └── website-example.md ├── LICENSE ├── package.json ├── memory-tests ├── list.js ├── list-raw.js ├── insert.js └── update.js └── README.md /test/createdb.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE test; -------------------------------------------------------------------------------- /bin/related.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | !function() { 3 | 4 | 5 | var log = require('ee-log'); 6 | 7 | log.warn('The related cli is currently under development .... sorry!'); 8 | }(); 9 | 10 | -------------------------------------------------------------------------------- /lib/Lock.js: -------------------------------------------------------------------------------- 1 | { 2 | 'use strict'; 3 | 4 | 5 | const EventEmitter = require('events'); 6 | 7 | 8 | module.exports = class Lock extends EventEmitter { 9 | 10 | free() { 11 | this.emit('end'); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /config.js.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | module.exports = { 4 | db: { 5 | eventbooster: { 6 | type: 'mysql' 7 | , hosts: [ 8 | { 9 | host : '10.0.100.1' 10 | , username : '' 11 | , password : '' 12 | , mode : 'readwrite' 13 | } 14 | ] 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | config.js 15 | config-test.js 16 | test-config.js 17 | 18 | npm-debug.log 19 | 20 | travis.js 21 | 22 | test.js 23 | 24 | node_modules 25 | config.js 26 | 27 | coverage -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | // process.argv.push('--related-hanging'); 3 | // const log = require('ee-log'); 4 | 5 | // // handle unhandleed rejections 6 | // process.on('unhandledRejection', (reason, p) => { 7 | // log(reason); 8 | // }); 9 | 10 | 11 | 12 | module.exports = require('./lib/ORM'); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js : 4 | - "v8" 5 | - "v9" 6 | 7 | addons: 8 | postgresql: "9.6" 9 | 10 | before_script: 11 | - psql -f test/createdb.sql -U postgres 12 | - psql -f test/postgres.sql -U postgres 13 | - mysql -e 'create database ee_orm_test_mysql;' 14 | 15 | 16 | sudo: false 17 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "es5" : true 3 | , "freeze" : true 4 | , "latedef" : true 5 | , "noarg" : true 6 | , "notypeof" : true 7 | , "undef" : true 8 | , "unused" : true 9 | , "esnext" : true 10 | , "laxcomma" : true 11 | , "node" : true 12 | } 13 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | var type = require('ee-types'); 6 | 7 | module.exports = function(input) { 8 | var returnValue; 9 | 10 | switch(type(input)) { 11 | case 'array': 12 | returnValue = []; 13 | input.forEach(function(item){ 14 | returnValue.push(cloneObjectAndArrays(item)); 15 | }); 16 | return returnValue; 17 | break; 18 | 19 | case 'object': 20 | returnValue = {}; 21 | Object.keys(input).forEach(function(key){ 22 | returnValue[key] = cloneObjectAndArrays(input[key]); 23 | }); 24 | return returnValue; 25 | break; 26 | 27 | default: 28 | return input; 29 | } 30 | }; 31 | })(); 32 | -------------------------------------------------------------------------------- /test/tests/cleanUp.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | 3 | var log = require('ee-log') 4 | , assert = require('assert') 5 | , ORM = require('../../'); 6 | 7 | 8 | module.exports = function(orm, config) { 9 | describe('Cleanup', function() { 10 | it('Closing all conenctions on the orm and destructing the api', function(done) { 11 | this.timeout(10000); 12 | orm.end(done); 13 | }); 14 | 15 | 16 | it('Dropping the schema «related_orm_test»', function(done) { 17 | orm.dropSchema(config[0], 'related_orm_test').then(function() { 18 | done(); 19 | }).catch(done); 20 | }); 21 | 22 | 23 | it('Dropping the database «related_orm_test»', function(done) { 24 | orm.dropDatabase(config[0], 'related_orm_test').then(function() { 25 | done(); 26 | }).catch(done); 27 | }); 28 | }); 29 | }; 30 | }(); 31 | -------------------------------------------------------------------------------- /docs/Docs/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "filter": true 3 | , "contents": { 4 | "description": { 5 | "type": "teaser" 6 | , "source": "teaser.md" 7 | } 8 | , "getting-started": { 9 | "type": "reference" 10 | , "title": "getting Started" 11 | , "source": "docs/getting-started" 12 | } 13 | , "docs": { 14 | "type": "project-documentation" 15 | , "title": "Docs" 16 | , "source": "/docs" 17 | } 18 | , "features": { 19 | "type": "text" 20 | , "title": "Features" 21 | , "source": "features.md" 22 | } 23 | , "support": { 24 | "type": "text" 25 | , "title": "Support" 26 | , "source": "features.md" 27 | } 28 | , "changelog": { 29 | "type": "gh-changelog" 30 | , "title": "changelog" 31 | , "source": "distributed-systems/related" 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2015, 2016, 2017, 2018 Michael van der Weg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Related ORM" 3 | , "description": "An easy to use ORM for node.js / io.js nad relational databases" 4 | , "keywords": ["Related", "ORM", "Postgres", "MySQL"] 5 | , "translations": ["en"] 6 | , "contents": { 7 | "description": { 8 | "type": "teaser" 9 | , "source": "teaser.md" 10 | } 11 | , "getting-started": { 12 | "type": "reference" 13 | , "title": "getting Started" 14 | , "source": "docs/getting-started" 15 | } 16 | , "docs": { 17 | "type": "project-documentation" 18 | , "title": "Docs" 19 | , "source": "/docs" 20 | } 21 | , "features": { 22 | "type": "text" 23 | , "title": "Features" 24 | , "source": "features.md" 25 | } 26 | , "support": { 27 | "type": "text" 28 | , "title": "Support" 29 | , "source": "features.md" 30 | } 31 | , "changelog": { 32 | "type": "gh-changelog" 33 | , "title": "changelog" 34 | , "source": "distributed-systems/related" 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/tests/setup.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | 3 | var log = require('ee-log') 4 | , assert = require('assert') 5 | , ORM = require('../../'); 6 | 7 | 8 | module.exports = function(orm, config) { 9 | describe('Database Setup', function() { 10 | 11 | it('Creating the database «related_orm_test»', function(done) { 12 | orm.createDatabase(config[0], 'related_orm_test').then(function() { 13 | done(); 14 | }).catch(done); 15 | }); 16 | 17 | 18 | it('Creating the schema «related_orm_test»', function(done) { 19 | orm.createSchema(config[0], 'related_orm_test').then(function() { 20 | done(); 21 | }).catch(done); 22 | }); 23 | 24 | 25 | it('Should accept a new config and load the «related_orm_test» schema', function(done) { 26 | orm.addConfig(config, done); 27 | }); 28 | }); 29 | 30 | 31 | 32 | describe('Creating Tables', function() { 33 | 34 | it('Creating the event table', function(done) { 35 | log(orm); 36 | done(); 37 | }); 38 | }); 39 | }; 40 | }(); 41 | -------------------------------------------------------------------------------- /docs/teaser.md: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Related ORM" 3 | , "description": "An easy to use ORM for node.js / io.js nad relational databases" 4 | , "keywords": ["Related", "ORM", "Postgres", "MySQL"] 5 | , "translations": ["en"] 6 | , "toc": { 7 | "Getting Started": "/docs/getting-started/" 8 | , "Docs": "/docs/" 9 | , "Features": "/Features.md" 10 | , "Support": "/Support.md" 11 | } 12 | , "contents": { 13 | "docs": { 14 | "type": "project-documentation" 15 | , "title": "Docs" 16 | , "basedir": "/docs" 17 | } 18 | , "getting-started": { 19 | "type": "reference" 20 | , "title": "getting Started" 21 | , "target": "docs/getting-started" 22 | } 23 | , "features": { 24 | "type": "text" 25 | , "title": "Features" 26 | , "target": "features.md" 27 | } 28 | , "support": { 29 | "type": "text" 30 | , "title": "Support" 31 | , "target": "features.md" 32 | } 33 | , "changelog": { 34 | "type": "gh-changelog" 35 | , "title": "changelog" 36 | , "source": "distributed-systems/related" 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /docs/website-example.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 1. install 4 | 5 | ``` 6 | npm i --save related 7 | ``` 8 | 9 | 10 | 2. Connect to any existing database 11 | ``` 12 | const Related = require('related'); 13 | 14 | const customerDatabase = new Related({ 15 | type : 'postgres' 16 | , database : 'customer' 17 | , hosts: [{ 18 | host : '10.1.33.7' 19 | , username : 'customer-db-user' 20 | , password : 'my-secure-pw' 21 | }] 22 | }); 23 | 24 | 25 | customerDatabase.load().then(() => { 26 | // ready to execute queries 27 | 28 | }).catch((err) => { 29 | // oh snap :( 30 | }); 31 | ``` 32 | 33 | 34 | 3. Execute Queries 35 | ``` 36 | // get a list of customers with emails ending in 37 | // @joinbox.com, start with record 10, return not 38 | // more than 100 records and also load the company 39 | // info for all customers 40 | customerDatabase.customers(['id', 'name', 'email'], { 41 | email: Related.like('%@joinbox.com') 42 | }).getCompany('*').offset(10).limit(100).order('name').find().then((data) => { 43 | 44 | console.log(data); 45 | // [{ 46 | // id: 23 47 | // , name: 'Michael van der Weg' 48 | // , email: 'michael@joinbox.com' 49 | // , id_company: 3 50 | // , company: { 51 | // id: 56 52 | // , name: 'Joinbox Ltd.' 53 | // , countryCode: 'ch' 54 | // } 55 | // }] 56 | }).catch(err => console.log(err)); 57 | ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "related", 3 | "description": "Unopinionated ORM for relational databases", 4 | "version": "2.16.5", 5 | "homepage": "https://github.com/linaGirl/related", 6 | "author": "Lina van der Weg ", 7 | "license": "MIT", 8 | "repository": { 9 | "url": "https://github.com/linaGirl/related.git", 10 | "type": "git" 11 | }, 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/linaGirl/related/issues" 17 | }, 18 | "dependencies": { 19 | "async-method": "^0.1.1", 20 | "ee-arguments": "^1.0.4", 21 | "ee-argv": "^0.1.4", 22 | "ee-class": "^1.4.0", 23 | "ee-event-emitter": "^0.3.1", 24 | "ee-log": "^3.0.5", 25 | "ee-types": "^2.1.4", 26 | "related-db-cluster": "^2.2.16", 27 | "related-query-context": "^2.1.1", 28 | "test-config": "^0.1.1" 29 | }, 30 | "devDependencies": { 31 | "mocha": "^5.2.0", 32 | "related-timestamps": "^2.0.0" 33 | }, 34 | "bin": { 35 | "related": "./bin/related.js" 36 | }, 37 | "keywords": [ 38 | "orm", 39 | "mysql", 40 | "postgres", 41 | "object relational mapper", 42 | "eager loading", 43 | "connection pooling", 44 | "cluster" 45 | ], 46 | "scripts": { 47 | "test": "./node_modules/mocha/bin/mocha --reporter spec --bail" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/ModelDefinition.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log'); 6 | 7 | 8 | // model definition 9 | module.exports = new Class({ 10 | 11 | init: function(definition) { 12 | this.name = definition.name; 13 | this.aliasName = definition.aliasName; 14 | this.isMapping = definition.isMapping; 15 | this.columns = definition.columns; 16 | this.primaryKeys = definition.primaryKeys; 17 | 18 | 19 | // extract info 20 | this.databaseName = definition.getDatabaseName(); 21 | this.databaseAliasName = definition.getDatabaseAliasName(); 22 | this.tableName = definition.getTableName(); 23 | } 24 | 25 | 26 | 27 | /* 28 | * chck if this model has a specifi column 29 | */ 30 | , hasColumn: function(column) { 31 | return !!this.columns[column]; 32 | } 33 | 34 | 35 | /* 36 | * return the name of the table this model belongs to 37 | */ 38 | , getTableName: function() { 39 | return this.tableName; 40 | } 41 | 42 | 43 | /* 44 | * return the name of the database this model belongs to 45 | */ 46 | , getDatabaseName: function() { 47 | return this.databaseName; 48 | } 49 | 50 | 51 | /* 52 | * return the name of the database this model belongs to 53 | */ 54 | , getDatabaseAliasName: function() { 55 | return this.databaseAliasName ||this.databaseName; 56 | } 57 | }); 58 | })(); 59 | -------------------------------------------------------------------------------- /memory-tests/list.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | 6 | var Class = require('ee-class') 7 | , log = require('ee-log') 8 | , assert = require('assert') 9 | , ORM = require('../') 10 | , v8 = require('v8') 11 | , fs = require('fs') 12 | , project = require('ee-project'); 13 | 14 | 15 | let orm = new ORM(require('../config.js').db); 16 | let calls = 0; 17 | let start; 18 | let last; 19 | let now; 20 | 21 | 22 | 23 | 24 | setInterval(() => { 25 | if (!start) start = v8.getHeapStatistics(); 26 | last = now || v8.getHeapStatistics(); 27 | now = v8.getHeapStatistics(); 28 | 29 | 30 | let out = [Date.now(), now.used_heap_size/1000, (now.used_heap_size-start.used_heap_size)/1000, (now.used_heap_size-last.used_heap_size)/1000, calls, ((now.used_heap_size-start.used_heap_size)/1000)/calls]; 31 | fs.appendFile(__dirname+'/list.csv', `${out.join(',')}\n`); 32 | 33 | 34 | log.warn(`--------- ${calls} calls ----------`); 35 | log.info(`used: ${now.used_heap_size/1000}`); 36 | log.info(`diff start: ${(now.used_heap_size-start.used_heap_size)/1000}`); 37 | log.info(`diff last: ${(now.used_heap_size-last.used_heap_size)/1000}`); 38 | //log(v8.getHeapStatistics()); 39 | 40 | if (calls > 1000000) process.exit(); 41 | }, 1000); 42 | 43 | 44 | 45 | 46 | 47 | orm.load().then((err) => { 48 | let db = orm.related_test_postgres; 49 | 50 | 51 | 52 | let list = () => { 53 | db.event('*').limit(50).getVenue('*').find().then((data) => { 54 | calls++; 55 | 56 | setTimeout(list, 20); 57 | }).catch(log); 58 | } 59 | 60 | 61 | list(); 62 | }).catch(log); 63 | })(); 64 | -------------------------------------------------------------------------------- /memory-tests/list-raw.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | 6 | var Class = require('ee-class') 7 | , log = require('ee-log') 8 | , assert = require('assert') 9 | , ORM = require('../') 10 | , v8 = require('v8') 11 | , fs = require('fs') 12 | , project = require('ee-project'); 13 | 14 | 15 | let orm = new ORM(require('../config.js').db); 16 | let calls = 0; 17 | let start; 18 | let last; 19 | let now; 20 | 21 | 22 | 23 | 24 | setInterval(() => { 25 | if (!start) start = v8.getHeapStatistics(); 26 | last = now || v8.getHeapStatistics(); 27 | now = v8.getHeapStatistics(); 28 | 29 | let out = [Date.now(), now.used_heap_size/1000, (now.used_heap_size-start.used_heap_size)/1000, (now.used_heap_size-last.used_heap_size)/1000, calls, ((now.used_heap_size-start.used_heap_size)/1000)/calls]; 30 | fs.appendFile(__dirname+'/list-raw.csv', `${out.join(',')}\n`); 31 | 32 | 33 | log.warn(`--------- ${calls} calls ----------`); 34 | log.info(`used: ${now.used_heap_size/1000}`); 35 | log.info(`diff start: ${(now.used_heap_size-start.used_heap_size)/1000}`); 36 | log.info(`diff last: ${(now.used_heap_size-last.used_heap_size)/1000}`); 37 | //log(v8.getHeapStatistics()); 38 | 39 | if (calls > 1000000) process.exit(); 40 | }, 1000); 41 | 42 | 43 | 44 | 45 | 46 | orm.load().then((err) => { 47 | let db = orm.related_test_postgres; 48 | 49 | 50 | 51 | let list = () => { 52 | db.event('*').limit(50).getVenue('*').raw().find().then((data) => { 53 | calls++; 54 | 55 | setTimeout(list, 20); 56 | }).catch(log); 57 | } 58 | 59 | 60 | list(); 61 | }).catch(log); 62 | })(); 63 | -------------------------------------------------------------------------------- /lib/JoinStatement.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | var Class = require('ee-class') 4 | , EventEmitter = require('ee-event-emitter') 5 | , log = require('ee-log'); 6 | 7 | 8 | 9 | 10 | module.exports = new Class({ 11 | 12 | unformatted: true 13 | 14 | , init: function(options) { 15 | this.type = options.type || 'inner'; 16 | this.source = options.source; 17 | this.target = options.target; 18 | } 19 | 20 | 21 | 22 | , reverseFormat: function(aliasSuffix, secondAliasSuffix, left) { 23 | return { 24 | type : left ? 'left' : this.type 25 | , source : { 26 | table : this.target.table + (secondAliasSuffix === undefined ? '' : secondAliasSuffix) 27 | , column : this.target.column 28 | } 29 | , target : this.source 30 | , alias : this.source.table + (aliasSuffix === undefined ? '' : aliasSuffix) 31 | }; 32 | } 33 | 34 | 35 | , format: function(aliasSuffix, secondAliasSuffix, left) { 36 | return { 37 | type : left ? 'left' : this.type 38 | , source : this.source 39 | , target : { 40 | table : this.target.table + (secondAliasSuffix === undefined ? '' : secondAliasSuffix) 41 | , column : this.target.column 42 | } 43 | , alias : this.target.table + (aliasSuffix === undefined ? '' : aliasSuffix) 44 | }; 45 | } 46 | 47 | 48 | , _addAlias: function(config, alias) { 49 | config.table = config.table +(alias === undefined ? '' : alias); 50 | return config; 51 | } 52 | }); 53 | })(); 54 | -------------------------------------------------------------------------------- /memory-tests/insert.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | 6 | var Class = require('ee-class') 7 | , log = require('ee-log') 8 | , assert = require('assert') 9 | , ORM = require('../') 10 | , v8 = require('v8') 11 | , fs = require('fs') 12 | , project = require('ee-project'); 13 | 14 | 15 | let orm = new ORM(require('../config.js').db); 16 | let calls = 0; 17 | let start; 18 | let last; 19 | let now; 20 | 21 | 22 | 23 | 24 | setInterval(() => { 25 | if (!start) start = v8.getHeapStatistics(); 26 | last = now || v8.getHeapStatistics(); 27 | now = v8.getHeapStatistics(); 28 | 29 | 30 | let out = [Date.now(), now.used_heap_size/1000, (now.used_heap_size-start.used_heap_size)/1000, (now.used_heap_size-last.used_heap_size)/1000, calls, ((now.used_heap_size-start.used_heap_size)/1000)/calls]; 31 | fs.appendFile(__dirname+'/insert.csv', `${out.join(',')}\n`); 32 | 33 | 34 | log.warn(`--------- ${calls} calls ----------`); 35 | log.info(`used: ${now.used_heap_size/1000}`); 36 | log.info(`diff start: ${(now.used_heap_size-start.used_heap_size)/1000}`); 37 | log.info(`diff last: ${(now.used_heap_size-last.used_heap_size)/1000}`); 38 | //log(v8.getHeapStatistics()); 39 | 40 | if (calls > 1000000) process.exit(); 41 | }, 1000); 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | orm.load().then((err) => { 51 | let db = orm.related_test_postgres; 52 | 53 | 54 | 55 | let insert = () => { 56 | new db.event({ 57 | title: `memtest-${Math.random()}` 58 | , startdate: new Date() 59 | , venue: db.venue().limit(1) 60 | }).save().then((data) => { 61 | calls++; 62 | 63 | setTimeout(insert, 20); 64 | }).catch(log); 65 | } 66 | 67 | 68 | insert(); 69 | }).catch(log); 70 | })(); 71 | -------------------------------------------------------------------------------- /memory-tests/update.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | 6 | var Class = require('ee-class') 7 | , log = require('ee-log') 8 | , assert = require('assert') 9 | , ORM = require('../') 10 | , v8 = require('v8') 11 | , fs = require('fs') 12 | , project = require('ee-project'); 13 | 14 | 15 | let orm = new ORM(require('../config.js').db); 16 | let calls = 0; 17 | let start; 18 | let last; 19 | let now; 20 | 21 | 22 | 23 | 24 | setInterval(() => { 25 | if (!start) start = v8.getHeapStatistics(); 26 | last = now || v8.getHeapStatistics(); 27 | now = v8.getHeapStatistics(); 28 | 29 | 30 | let out = [Date.now(), now.used_heap_size/1000, (now.used_heap_size-start.used_heap_size)/1000, (now.used_heap_size-last.used_heap_size)/1000, calls, ((now.used_heap_size-start.used_heap_size)/1000)/calls]; 31 | fs.appendFile(__dirname+'/update.csv', `${out.join(',')}\n`); 32 | 33 | 34 | log.warn(`--------- ${calls} calls ----------`); 35 | log.info(`used: ${now.used_heap_size/1000}`); 36 | log.info(`diff start: ${(now.used_heap_size-start.used_heap_size)/1000}`); 37 | log.info(`diff last: ${(now.used_heap_size-last.used_heap_size)/1000}`); 38 | //log(v8.getHeapStatistics()); 39 | 40 | if (calls > 1000000) process.exit(); 41 | }, 1000); 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | orm.load().then((err) => { 51 | let db = orm.related_test_postgres; 52 | 53 | 54 | 55 | let update = () => { 56 | db.event('*').limit(1).getVenue('*').findOne().then((data) => { 57 | 58 | data.startdate = new Date(); 59 | data.title = `updated-${Math.random()}`; 60 | return data.save(); 61 | }).then(() => { 62 | calls++; 63 | 64 | setTimeout(update, 20); 65 | }).catch(log); 66 | } 67 | 68 | 69 | update(); 70 | }).catch(log); 71 | })(); 72 | -------------------------------------------------------------------------------- /test/main.js: -------------------------------------------------------------------------------- 1 | !function() { 2 | return; 3 | var Class = require('ee-class') 4 | , log = require('ee-log') 5 | , assert = require('assert') 6 | , async = require('ee-async') 7 | , fs = require('fs') 8 | , Config = require('test-config') 9 | , ORM = require('../'); 10 | 11 | 12 | var expect = function(val, cb){ 13 | return function(err, result){ 14 | try { 15 | assert.equal(JSON.stringify(result), val); 16 | } catch (err) { 17 | return cb(err); 18 | } 19 | cb(); 20 | } 21 | }; 22 | 23 | 24 | 25 | ['postgres', 'mysql'].forEach(function(dbType) { 26 | var config 27 | , orm; 28 | 29 | 30 | 31 | config = new Config('config-test.js', {db:[{ 32 | schema : 'related_orm_test' 33 | , database : 'related_orm_test' 34 | , type : 'postgres' 35 | , hosts: [{ 36 | host : 'localhost' 37 | , username : 'postgres' 38 | , password : '' 39 | , port : 5432 40 | , mode : 'readwrite' 41 | , maxConnections: 50 42 | }] 43 | }, { 44 | schema : 'related_orm_test' 45 | , type : 'mysql' 46 | , hosts: [{ 47 | host : 'localhost' 48 | , username : 'root' 49 | , password : '' 50 | , port : 3306 51 | , mode : 'readwrite' 52 | , maxConnections: 20 53 | }] 54 | }]}).db.filter(function(config) {return config.type === dbType}); 55 | 56 | // need a global orm instancd that can be used by all tests 57 | orm = new ORM(); 58 | 59 | 60 | describe(dbType.toUpperCase(), function() { 61 | //require('./tests/cleanUp')(orm, config); 62 | require('./tests/setup')(orm, config); 63 | 64 | 65 | require('./tests/cleanUp')(orm, config); 66 | }); 67 | }); 68 | }(); 69 | -------------------------------------------------------------------------------- /lib/ExtensionManager.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log'); 6 | 7 | 8 | // extension manager 9 | module.exports = new Class({ 10 | 11 | init: function(ormInstance) { 12 | Class.define(this, '_extensions', Class([])); 13 | Class.define(this, '_orm', Class(ormInstance)); 14 | Class.define(this, '_map', Class({})); 15 | } 16 | 17 | 18 | /* 19 | * add a new extension 20 | */ 21 | , register: function(extension) { 22 | var name = extension.getName(); 23 | 24 | // cannot register an extension without a name 25 | if (!name) throw new Error('Cannot register an extension without a name!'); 26 | 27 | // add variables to extension 28 | extension.setVariables({ 29 | orm: this._orm 30 | }); 31 | 32 | 33 | 34 | extension.on('reloadEntity', (dbName, entityName) => { 35 | if (this._orm[dbName] && this._orm[dbName][entityName]) this._orm[dbName][entityName].reload(); 36 | else throw new Error(`Cannot reload entity ${entityName} on database ${dbName} for the extension ${name}: The entity does not exits!`); 37 | }); 38 | 39 | 40 | // add to map 41 | if (!this._map[name]) this._map[name] = []; 42 | this._map[name].push(extension); 43 | 44 | // store 45 | this._extensions.push(extension); 46 | } 47 | 48 | 49 | /* 50 | * return a specifi extension 51 | */ 52 | , get: function(name) { 53 | return this._map[name]; 54 | } 55 | 56 | 57 | /* 58 | * return all extension that whish to register 59 | * themself for the current model 60 | */ 61 | , getModelExtensions: function(defintion) { 62 | return this._extensions.filter(function(extension) { 63 | return extension.useOnModel(defintion); 64 | }.bind(this)); 65 | } 66 | 67 | 68 | // return the amount of extensions registered 69 | , count: { 70 | get: function() { 71 | return this._extensions.length; 72 | } 73 | } 74 | }); 75 | })(); 76 | -------------------------------------------------------------------------------- /lib/FullTextQueryBuilder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | 6 | const log = require('ee-log'); 7 | 8 | 9 | 10 | module.exports = class FullTextQueryBuilder { 11 | 12 | 13 | constructor(language, nodeType, parent) { 14 | if (!nodeType) { 15 | this.type = 'fulltext'; 16 | if (language) this.language = language; 17 | } 18 | else { 19 | this.type = nodeType; 20 | this.children = []; 21 | } 22 | 23 | 24 | Object.defineProperty(this, 'isFulltext', {value: true}); 25 | Object.defineProperty(this, 'parent', {value: parent}); 26 | } 27 | 28 | 29 | 30 | 31 | not() { 32 | this.lastValue.isNot = true; 33 | return this; 34 | } 35 | 36 | 37 | 38 | 39 | wildcardAfter() { 40 | this.lastValue.hasWildcardAfter = true; 41 | return this; 42 | } 43 | 44 | 45 | 46 | 47 | wildcardBefore() { 48 | this.lastValue.hasWildcardBefore = true; 49 | return this; 50 | } 51 | 52 | 53 | 54 | 55 | 56 | value(value) { 57 | const node = new FullTextQueryBuilder(null, 'value', this); 58 | 59 | node.value = value; 60 | 61 | if (this.type === 'fulltext') this.value = node; 62 | else this.children.push(node); 63 | 64 | 65 | Object.defineProperty(this, 'lastValue', {value: node, configurable: true}); 66 | 67 | return this; 68 | } 69 | 70 | 71 | 72 | and() { 73 | const node = new FullTextQueryBuilder(null, 'and', this); 74 | 75 | if (this.type === 'fulltext') this.value = node; 76 | else this.children.push(node); 77 | 78 | 79 | return node; 80 | } 81 | 82 | 83 | 84 | or() { 85 | const node = new FullTextQueryBuilder(null, 'or', this); 86 | 87 | if (this.type === 'fulltext') this.value = node; 88 | else this.children.push(node); 89 | 90 | 91 | return node; 92 | } 93 | 94 | 95 | 96 | up() { 97 | return this.parent; 98 | } 99 | 100 | 101 | 102 | 103 | getRoot() { 104 | return this.parent ? this.parent.getRoot() : this; 105 | } 106 | } 107 | })(); 108 | -------------------------------------------------------------------------------- /lib/Selector.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | var Class = require('ee-class') 6 | , log = require('ee-log') 7 | , type = require('ee-types'); 8 | 9 | 10 | /* 11 | * This class provides the select method on the static orm. 12 | * this can be used to add select functions to the query. 13 | * 14 | */ 15 | 16 | 17 | module.exports = new Class({ 18 | 19 | 20 | /** 21 | * class constructor 22 | */ 23 | init: function() { 24 | 25 | // storage for the extensions 26 | this.classDefinition = { 27 | extensions: {} 28 | , init: function(alias) { 29 | this.alias = alias; 30 | } 31 | }; 32 | 33 | // accessors 34 | this.classConstructor = new Class(this.classDefinition); 35 | } 36 | 37 | 38 | 39 | 40 | /** 41 | * returns a new object that exposes all select extensions 42 | * 43 | * @param alias 44 | * 45 | * @returns object exposing the select extensions 46 | */ 47 | , select: function(alias) { 48 | return new this.classConstructor(alias); 49 | } 50 | 51 | 52 | 53 | 54 | /** 55 | * register a new selector extension 56 | * 57 | * @param extension 58 | */ 59 | , registerExtension: function(extension) { 60 | var name = extension.extensionName; 61 | 62 | if (!extension.isRelatedSelector || !extension.isRelatedSelector()) throw new Error('Cannot register selector extension, expected a class extending from the RelatedSelector class!'); 63 | if (!type.string(name)) throw new Error('Cannot register extension without a name!'); 64 | if (this.classDefinition.extensions[name]) return;//throw new Error('Cannot register «'+name+'» extension, an extension with the same name was already registered before!'); 65 | 66 | // store extension 67 | this.classDefinition.extensions[name] = extension; 68 | 69 | 70 | // create accessor on the class 71 | this.classDefinition[name] = function() { 72 | var args = new Array(arguments.length); 73 | for(var i = 0; i < args.length; ++i) args[i] = arguments[i]; 74 | 75 | return new this.extensions[name]({ 76 | parameters : args 77 | , alias : this.alias 78 | }); 79 | }; 80 | 81 | // recerate Class 82 | this.classConstructor = new Class(this.classDefinition); 83 | } 84 | 85 | 86 | 87 | 88 | /** 89 | * appoly this model to the static orm object 90 | */ 91 | , applyTo: function(obj) { 92 | obj.select = this.select.bind(this); 93 | } 94 | }); 95 | })(); 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Related ORM 2 | 3 | ORM for relational databases. 4 | 5 | 6 | [![npm](https://img.shields.io/npm/dm/related.svg?style=flat-square)](https://www.npmjs.com/package/related) 7 | [![Travis](https://img.shields.io/travis/eventEmitter/related.svg?style=flat-square)](https://travis-ci.org/eventEmitter/related) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://raw.githubusercontent.com/eventEmitter/related/master/LICENSE) 9 | [![node](https://img.shields.io/node/v/related.svg?style=flat-square)](https://nodejs.org/) 10 | 11 | 12 | 13 | On the fly builds an extensive API representing your database and its tables including their relations and columns. No need to write javascript models. 14 | 15 | 16 | **Features** 17 | - Supports Postgres & MySQL 18 | - Simple loading and filtering of nested entities 19 | - Advanced query builder that rocks 20 | - Transactions 21 | - Table locks 22 | - Bulk operations 23 | - User extendable models (optional) 24 | - Connection pooling 25 | - Extensions for soft-deletes, nested sets, multilingual content, geo distance computation and reference counting 26 | - Complex DB Cluster support (includes read replicas and failover on AWS RDS) 27 | - No conventions for column names etc. 28 | - Commercial support avialable 29 | - And much more 30 | 31 | 32 | ```` 33 | var Related = require('related'); 34 | 35 | // The ORM looks at your database and builds on the fly models. 36 | // You can start working on your database immediatelly. 37 | new Related({ 38 | schema : 'mySchemaName' // optional 39 | , database : 'myDatabaseName' 40 | , type : 'postgres' // or mysql 41 | , hosts: [{ 42 | host : 'localhost' 43 | , username : 'postgres' 44 | , password : '' 45 | , maxConnections: 20 46 | , pools : ['master', 'read', 'write'] 47 | }] 48 | }).load().then(function(orm) { 49 | 50 | 51 | // get 10 events, their images, their tags, their categories, their venues, 52 | // the venues images, the venues types. the 'get' prefix change sscope to the other model 53 | // the 'fetch' prefix doenst change the scope and you can continue working on the current 54 | // model 55 | orm.event({id: Related.lt(2000)}, ['*']) 56 | .fetchImage(['url']) 57 | .fetchTag(['name']) 58 | .fetchCategory(['name']) 59 | .getVenue(['*']) 60 | .fetchImage(['url']) 61 | .fetchVenueType(['name']) 62 | .limit(10) 63 | .find().then(function(events) { 64 | 65 | log(events); 66 | }).catch(function(err) { 67 | 68 | log('something went wrong :('); 69 | }); 70 | }); 71 | ```` 72 | 73 | 74 | ## API 75 | 76 | We are currently working on an extensive documentation and a website. Until those are online please look at the [tests](https://github.com/eventEmitter/related/blob/master/test/orm.js) 77 | 78 | 79 | ### Extensions 80 | 81 | - ***[Timestamps](https://www.npmjs.com/package/related-timestamps):*** support for automatic timestamps and soft deletes 82 | - ***[GEO](https://www.npmjs.com/package/related-geo):*** Area and distance searches using longitutde and latitude 83 | - ***[Localization](https://www.npmjs.com/package/related-localization):*** support for multilanguage content in the relational model 84 | - ***[Nested Sets](https://www.npmjs.com/package/related-nested-set):*** support for [nested sets](https://en.wikipedia.org/wiki/Nested_set_model) 85 | - ***[Reference Counting](https://www.npmjs.com/package/related-reference-counter):*** Counts items that are referenced by the current entity 86 | -------------------------------------------------------------------------------- /lib/AdvancedQueryBuilder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log') 6 | , type = require('ee-types'); 7 | 8 | 9 | 10 | /* 11 | * Build Advanced Query 12 | */ 13 | module.exports = new Class({ 14 | 15 | 16 | init: function(query) { 17 | // the query we're working on 18 | this._query = query; 19 | 20 | // register myself for execution 21 | this._query.setQueryBuilder(this); 22 | } 23 | 24 | 25 | 26 | /* 27 | * add and'ed items to the query 28 | */ 29 | , and: function() { 30 | var items = Array.prototype.slice.call(arguments); 31 | items.mode = 'and'; 32 | this._rootItem = items; 33 | return items; 34 | } 35 | 36 | 37 | /* 38 | * add or'ed items to the query 39 | */ 40 | , or: function() { 41 | var items = Array.prototype.slice.call(arguments); 42 | items.mode = 'or'; 43 | this._rootItem = items; 44 | return items; 45 | } 46 | 47 | 48 | 49 | /* 50 | * apply my filters to the root resource 51 | */ 52 | , apply: function(filter, rootQuery) { 53 | var filters = this._applyTo(this._rootItem, rootQuery); 54 | filter._ = filters; 55 | } 56 | 57 | 58 | /* 59 | * buid filters 60 | */ 61 | , _applyTo: function(list, rootQuery) { 62 | var filters = []; 63 | 64 | // set the type of the filter combination 65 | filters.mode = list.mode; 66 | 67 | 68 | list.forEach(function(item) { 69 | if (type.array(item)) { 70 | // nested item 71 | filters.push(this._applyTo(item, rootQuery)); 72 | } 73 | else if (type.object(item)) { 74 | // filter set 75 | Object.keys(item).forEach(function(key) { 76 | var filter = {} 77 | , entityName = this._join(rootQuery, key); 78 | 79 | filter[entityName] = {}; 80 | filter[entityName][key.substr(key.lastIndexOf('.')+1)] = item[key]; 81 | 82 | filters.push(filter); 83 | }.bind(this)); 84 | } 85 | else throw new Error('Invalid filter definition, expexted array or object, got «'+type(item)+'»!'); 86 | }.bind(this)); 87 | 88 | return filters; 89 | } 90 | 91 | 92 | 93 | /* 94 | * force the orm to join the required tables, return the alias name of the 95 | * affetced table 96 | */ 97 | , _join: function(rootQuery, path) { 98 | var parts = path.split(/\./g) 99 | , targetEntity; 100 | 101 | if (parts.length === 1) { 102 | return rootQuery.getresource().getAliasName(); 103 | } 104 | else if (parts.length > 1) { 105 | targetEntity = this._joinPath(rootQuery, parts.slice(0, -1)); 106 | return targetEntity.getresource().getAliasName(); 107 | } 108 | else throw new Error('Failed to determine entity!'); 109 | } 110 | 111 | 112 | 113 | /* 114 | * force the orm to join a path 115 | */ 116 | , _joinPath: function(queryBuilder, parts) { 117 | if (parts.length === 0) { 118 | return queryBuilder; 119 | } 120 | else { 121 | return this._joinPath(queryBuilder.leftJoin(parts[0], true), parts.slice(1)); 122 | } 123 | } 124 | }); 125 | })(); 126 | -------------------------------------------------------------------------------- /lib/Migration.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , type = require('ee-types') 6 | , log = require('ee-log'); 7 | 8 | 9 | 10 | module.exports = new Class({ 11 | 12 | 13 | init: function(version) { 14 | if (!type.string(version)) throw new Error('Expecting a version string!'); 15 | 16 | this.databases = []; 17 | this.schemas = []; 18 | this.dependecies = {}; 19 | 20 | // remove .js from the filename 21 | this.version = version.replace('.js', ''); 22 | } 23 | 24 | 25 | 26 | /** 27 | * creates a serializable representation of this migration 28 | */ 29 | , serialize: function() { 30 | if (!type.string(this.version)) throw new Error('No migration version specified!'); 31 | if (!type.string(this.description)) throw new Error('No migration description specified!'); 32 | if (!type.function(this.up)) throw new Error('Missing the «up» migration method!'); 33 | if (!type.function(this.down)) throw new Error('Missing the «down» migration method!'); 34 | 35 | return { 36 | version: this.version 37 | , dependecies: this.dependecies 38 | , description: this.description 39 | , up: this.parseMethod('up') 40 | , down: this.parseMethod('down') 41 | , createDatababase: this.databases 42 | , createSchema: this.schemas 43 | }; 44 | } 45 | 46 | 47 | 48 | 49 | /** 50 | * retusn a proper serialized method ;) 51 | * 52 | * @param methodff name 53 | */ 54 | , parseMethod: function(name) { 55 | var method = /^\s*function\s*\(([^\)]*)\)\s*\{([^$]*)\}\s*$/gi.exec(this[name].toString()); 56 | 57 | return { 58 | arguments: method[1].split(',').map(function(arg) { return arg.trim();}).filter(function(arg) {return arg && arg.length;}) 59 | , body: method[2].trim().replace(/[\t]+/gi, ' ') 60 | .replace(/[\s]{2,}/gi, ' ') 61 | .replace(/[\n]{2,}/gi, '\n') 62 | .replace(/\s+,\s+/gi, ', ') 63 | .replace(/\(\s+/gi, '(') 64 | .replace(/\{\s+/gi, '{') 65 | .replace(/\[\s+/gi, '[') 66 | .replace(/\s+\)/gi, ')') 67 | .replace(/\s+\]/gi, ']') 68 | .replace(/\s+\}/gi, '}') 69 | .replace(/\s+:/gi, ':') 70 | } 71 | } 72 | 73 | 74 | /** 75 | * tells the migrator to do th eother migration first 76 | * 77 | * @param moduleName 78 | * @param semantic version 79 | */ 80 | , dependsOn: function(moduleName, semanticVersion) { 81 | this.dependecies[moduleName] = semanticVersion; 82 | } 83 | 84 | 85 | 86 | 87 | /** 88 | * add a description for this migration 89 | * 90 | * @param description 91 | */ 92 | , describe: function(description) { 93 | this.description = description; 94 | } 95 | 96 | 97 | 98 | /** 99 | * tell the migration to create a database 100 | * 101 | * @param name 102 | */ 103 | , createDatababase: function(name) { 104 | this.databases.push(name); 105 | } 106 | 107 | 108 | 109 | /** 110 | * tell the migration to create a schema 111 | * 112 | * @param name 113 | */ 114 | , createSchema: function(name) { 115 | this.schemas.push(name); 116 | } 117 | }); 118 | })(); 119 | -------------------------------------------------------------------------------- /lib/Query.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , type = require('ee-types') 6 | , EventEmitter = require('ee-event-emitter') 7 | , log = require('ee-log') 8 | , clone = require('./clone'); 9 | 10 | 11 | 12 | 13 | var Query = module.exports = new Class({ 14 | 15 | init: function(options) { 16 | options = options || {}; 17 | 18 | this.filter = options.filter || {}; 19 | this.select = []; 20 | this.from = options.from || 'undefined'; 21 | this.database = options.database || 'undefined'; 22 | this.join = options.join || []; 23 | this.group = options.group || []; 24 | this.order = options.order || []; 25 | this.having = options.having || []; 26 | 27 | this.addSeleted(options.select); 28 | } 29 | 30 | 31 | 32 | 33 | /** 34 | * add new selections, makes sure there are no dupes 35 | * its slow as fuck. sorry! 36 | */ 37 | , addSeleted: function(select) { 38 | if (select) { 39 | select.forEach((selection) => { 40 | if (!this.select.some((existing) => { 41 | if (type.string(selection) && type.string(existing)) { 42 | return selection === existing; 43 | } 44 | else if (type.object(selection) && type.object(existing)) { 45 | return selection.alias === existing.alias; 46 | } 47 | })) { 48 | this.select.push(selection); 49 | } 50 | }); 51 | } 52 | } 53 | 54 | 55 | 56 | 57 | /* 58 | * reset the order statement 59 | */ 60 | , resetOrder: function() { 61 | this.order = []; 62 | return this; 63 | } 64 | 65 | /* 66 | * configre the limit 67 | */ 68 | , setLimit: function(limit) { 69 | if (type.number(limit)) this.limit = limit; 70 | else if (limit === null && this.limit) delete this.limit; 71 | return this; 72 | } 73 | 74 | 75 | /* 76 | * returns the current offset 77 | */ 78 | , getOffset: function() { 79 | return this.offset || 0; 80 | } 81 | 82 | 83 | /* 84 | * configre the offset 85 | */ 86 | , setOffset: function(offset) { 87 | if (type.number(offset)) this.offset = offset; 88 | else if (offset === null && this.offset) delete this.offset; 89 | return this; 90 | } 91 | 92 | /* 93 | * return a new query instance with cloned objects / arrays 94 | */ 95 | , clone: function() { 96 | return new Query({ 97 | filter : clone(this.filter) 98 | , from : clone(this.from) 99 | , select : clone(this.select) 100 | , database : clone(this.database) 101 | , join : clone(this.join) 102 | , group : clone(this.group) 103 | , order : clone(this.order) 104 | , filter : clone(this.filter) 105 | , having : clone(this.having) 106 | }); 107 | } 108 | 109 | 110 | /* 111 | * Return a copy of the filter object 112 | */ 113 | , cloneFilter: function() { 114 | return clone(this.filter); 115 | } 116 | 117 | , formatJoins: function() { 118 | this.join = this.join.map(function(join){ 119 | return join.unformatted ? join.format() : join; 120 | }); 121 | } 122 | }); 123 | })(); 124 | -------------------------------------------------------------------------------- /lib/FilterBuilder.js: -------------------------------------------------------------------------------- 1 | { 2 | 'use strict'; 3 | 4 | 5 | 6 | const log = require('ee-log'); 7 | 8 | 9 | 10 | 11 | 12 | // comparator whitelist 13 | const comparators = new Map(); 14 | 15 | comparators.set('>', 'gt'); 16 | comparators.set('<', 'lt'); 17 | comparators.set('>=', 'gte'); 18 | comparators.set('<=', 'lte'); 19 | comparators.set('!=', 'notEqual'); 20 | comparators.set('=', 'equal'); 21 | 22 | comparators.set('like', 'like'); 23 | comparators.set('notLike', 'notLike'); 24 | comparators.set('in', 'in'); 25 | comparators.set('notIn', 'notIn'); 26 | comparators.set('notNull', 'notNull'); 27 | comparators.set('isNull', 'isNull'); 28 | comparators.set('equal', 'equal'); 29 | comparators.set('not', 'not'); 30 | comparators.set('is', 'is'); 31 | comparators.set('jsonValue', 'jsonValue'); 32 | 33 | 34 | 35 | 36 | 37 | 38 | module.exports = class FilterBuilder { 39 | 40 | 41 | 42 | constructor(queryBuilder) { 43 | this.queryBuilder = queryBuilder; 44 | this.Related = require('../'); 45 | } 46 | 47 | 48 | 49 | 50 | 51 | build(filter) { 52 | return this._build(filter, this.queryBuilder); 53 | } 54 | 55 | 56 | 57 | 58 | 59 | _build(filter, queryBuilder, container, property) { 60 | let processChildren = true; 61 | 62 | // if the children of a comparator are functions, 63 | // we can safely ignore the comparator and process 64 | // the filter instead 65 | if (filter.type === 'comparator' && 66 | filter.comparator === '=' && 67 | filter.children.length === 1 && 68 | filter.children[0].type === 'function') { 69 | filter = filter.children[0]; 70 | } 71 | 72 | 73 | 74 | switch (filter.type) { 75 | case 'root': 76 | if (!container) { 77 | container = []; 78 | container.mode = 'and'; 79 | } 80 | 81 | 82 | case 'and': 83 | case 'or': 84 | const newContainer = []; 85 | newContainer.mode = filter.type; 86 | container.push(newContainer); 87 | 88 | // overwrite pointer, so that it can be passed to 89 | // the children 90 | container = newContainer; 91 | break; 92 | 93 | 94 | case 'entity': 95 | 96 | // get the next level of queryBuilders 97 | queryBuilder = queryBuilder.leftJoin(filter.entityName, true); 98 | break; 99 | 100 | 101 | case 'property': 102 | 103 | // set the property so that it can be used later 104 | property = filter.propertyName; 105 | break; 106 | 107 | 108 | case 'comparator': 109 | case 'function': 110 | 111 | // build the filter object 112 | const alias = queryBuilder.getresource().getAliasName(); 113 | const filterObject = {}; 114 | filterObject[alias] = {}; 115 | filterObject[alias][property] = this.getFilterValues(filter.comparator || filter.functionName, filter.children); 116 | container.push(filterObject); 117 | processChildren = false; 118 | break; 119 | 120 | 121 | 122 | default: 123 | throw new Error(`Unexpected filter node '${filter.type}'. Did you specify the property & comparator to filter with?`); 124 | } 125 | 126 | 127 | 128 | if (processChildren && filter.children && filter.children.length) { 129 | for (const child of filter.children) { 130 | this._build(child, queryBuilder, container, property); 131 | } 132 | } 133 | 134 | 135 | return container; 136 | } 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | getFilterValues(comparator, children) { 145 | if (!comparators.has(comparator)) throw new Error(`The comparator or function '${comparator}' is not supported!`); 146 | else { 147 | if (children.length === 0) return null; 148 | else { 149 | const values = []; 150 | 151 | for (const node of children) { 152 | if (node.type === 'value') { 153 | values.push(node.nodeValue); 154 | } 155 | else throw new Error(`Expected a value filter node, got ${node.type} instead!`); 156 | } 157 | 158 | return this.Related[comparators.get(comparator)](...values); 159 | } 160 | } 161 | } 162 | } 163 | } -------------------------------------------------------------------------------- /lib/Database.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , EventEmitter = require('ee-event-emitter') 6 | , log = require('ee-log') 7 | , argv = require('ee-argv') 8 | , QueryContext = require('related-query-context') 9 | , Entity = require('./Entity') 10 | , buildTransaction = require('./TransactionBuilder') 11 | , ORM; 12 | 13 | 14 | 15 | var dev = argv.has('dev-orm'); 16 | 17 | 18 | 19 | module.exports = new Class({ 20 | inherits: EventEmitter 21 | 22 | , init: function(options) { 23 | if (dev) log.warn('initialize new db instance for «'+options.databaseName+'»...'); 24 | 25 | Class.define(this, '_orm', Class(options.orm)); 26 | Class.define(this, '_database', Class(options.database)); 27 | Class.define(this, '_queryBuilders', Class({})); 28 | Class.define(this, '_models', Class({})); 29 | Class.define(this, '_databaseName', Class(options.databaseName)); 30 | Class.define(this, '_extensions', Class(options.extensions)); 31 | Class.define(this, '_definition', Class(options.definition)); 32 | 33 | 34 | // initialize the orm 35 | this._initialize(options.definition); 36 | 37 | // build transaction class for this db 38 | Class.define(this, 'Transaction', Class(buildTransaction(this))); 39 | 40 | // emit load not before the next main loop execution 41 | process.nextTick(function(){ 42 | this.emit('load'); 43 | }.bind(this)); 44 | } 45 | 46 | 47 | , isTransaction: function(){ 48 | return false; 49 | } 50 | 51 | 52 | /** 53 | * sets up a new transaction 54 | * 55 | * @param pool {string} pool, options pool to create the transaction on 56 | */ 57 | , createTransaction: function(pool) { 58 | return new this.Transaction({pool: pool}); 59 | } 60 | 61 | 62 | 63 | /** 64 | * execute a query 65 | */ 66 | , executeQuery: function(context, values, pool) { 67 | 68 | // the user may also give us simple sql to execute 69 | if (typeof context === 'string') { 70 | context = new QueryContext({ 71 | sql: context 72 | , values: values 73 | , pool: pool || 'write' 74 | }); 75 | } 76 | 77 | return this._database.query(context); 78 | } 79 | 80 | 81 | 82 | 83 | 84 | /** 85 | * renders a query 86 | */ 87 | , renderQuery: function(connection, context) { 88 | return this._database.renderQuery(connection, context); 89 | } 90 | 91 | 92 | 93 | 94 | 95 | /* 96 | * returns the orm this database is attached to 97 | */ 98 | , getOrm: function() { 99 | return this._orm; 100 | } 101 | 102 | 103 | 104 | 105 | /* 106 | * return the ORM object used to create filters & more 107 | */ 108 | , getORM: function() { 109 | if (!ORM) ORM = require('./ORM'); 110 | return ORM; 111 | } 112 | 113 | 114 | 115 | /* 116 | * returns this, used for multiple components 117 | * which need to acces this via this method 118 | */ 119 | , _getDatabase: function(){ 120 | return this; 121 | } 122 | 123 | 124 | /* 125 | * returns the db name 126 | */ 127 | , getDatabaseName: function() { 128 | return this._databaseName 129 | } 130 | 131 | 132 | /** 133 | * return the db defintion object 134 | */ 135 | , getDefinition: function() { 136 | return this._definition; 137 | } 138 | 139 | 140 | /** 141 | * set up the entities 142 | */ 143 | , _initialize: function(definition){ 144 | Object.keys(definition).sort().forEach(function(tablename) { 145 | if (this[tablename]) next(new Error('Failed to load ORM for database «'+this._databaseName+'», the tablename «'+tablename+'» is reserved for the orm.').setName('ORMException')); 146 | if (dev) log.debug('['+this._databaseName+'] initializing new model «'+tablename+'» ...'); 147 | 148 | this[tablename] = new Entity({ 149 | orm : this._orm 150 | , definition : definition[tablename] 151 | , queryBuilders : this._queryBuilders 152 | , getDatabase : this._getDatabase.bind(this) 153 | , extensions : this._extensions 154 | }); 155 | 156 | //store this reference for the use in transactions 157 | this._models[tablename] = this[tablename]; 158 | }.bind(this)); 159 | } 160 | }); 161 | })(); 162 | -------------------------------------------------------------------------------- /lib/Set.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , type = require('ee-types') 6 | , log = require('ee-log'); 7 | 8 | 9 | 10 | 11 | module.exports = new Class({ 12 | inherits: Array 13 | 14 | /** 15 | * class constructor 16 | */ 17 | , init: function init(options){ 18 | Object.defineProperty(this, '_primaryKeys', {value: options.primaryKeys}); 19 | Object.defineProperty(this, '_name', {value: options.name}); 20 | Object.defineProperty(this, '_maps', {value: {_primary: {}}}); 21 | 22 | // the resource that led to the creation of this set, 23 | // it is used by the nested set functionality to apply 24 | // the correct filter to the nested set queries 25 | Object.defineProperty(this, '_resource', {value: options.resource}); 26 | 27 | Array.prototype.constructor.call(this); 28 | } 29 | 30 | 31 | /** 32 | * returns the resource which was used to load this set from the db 33 | */ 34 | , getResource: function() { 35 | return this._resource; 36 | } 37 | 38 | 39 | /** 40 | * add an row to the set, checks if an row is already in the 41 | * set, doesn't add it twice 42 | * 43 | * @param row 44 | */ 45 | , push: function(row) { 46 | // we need unique items by primary key 47 | var key = ''; 48 | 49 | this._primaryKeys.forEach(function(keyName){ 50 | key += row[keyName]; 51 | }.bind(this)); 52 | 53 | // if we got a primary key we should not add duplicates 54 | // if not, shit may happen, but at least the data is there! 55 | // views dont have pks 56 | if (key.length) { 57 | if (this._maps._primary[key]){ 58 | // this value was added before 59 | row._mappingIds.forEach(function(id){ 60 | this._maps._primary[key]._mappingIds.push(id); 61 | }.bind(this)); 62 | return this.length; 63 | } 64 | else { 65 | this._maps._primary[key] = row; 66 | return Array.prototype.push.call(this, row); 67 | } 68 | } 69 | else Array.prototype.push.call(this, row); 70 | } 71 | 72 | 73 | /** 74 | * returns the first row of the set 75 | */ 76 | , first: function(){ 77 | return this[0]; 78 | } 79 | 80 | 81 | /** 82 | * returns the last row of the set 83 | * 84 | * @retuns if there is at least on row or undefined 85 | */ 86 | , last: function() { 87 | return this.length ? this[this.length-1] : undefined; 88 | } 89 | 90 | 91 | /** 92 | * returns an array containing all the values of one column 93 | * 94 | * @param optional name of the column to return, defaults to «id» 95 | * 96 | * @retuns 97 | */ 98 | , getColumnValues: function(column) { 99 | column = column || 'id'; 100 | 101 | return this.map(function(row){ 102 | return row[column]; 103 | }); 104 | } 105 | 106 | 107 | /** 108 | * returns an array containing the all ids of the set 109 | * 110 | * @retuns 111 | */ 112 | , getIds: function() { 113 | return this.getColumnValues(); 114 | } 115 | 116 | 117 | 118 | 119 | /** 120 | * returns all rows that have a specoific value in a specifi column 121 | * 122 | * @param the column to filter 123 | * @param the value to filter for 124 | */ 125 | , getByColumnValue: function(column, value) { 126 | if (!this._maps[column]) this.createMap(column); 127 | return this._maps[column] ? this._maps[column][value] : undefined; 128 | } 129 | 130 | 131 | 132 | /** 133 | * creates a map of the values for a column, so that rows can be 134 | * accessed by the value of a specific column. if the column is 135 | * not unique mulltiple rows may be returned 136 | * 137 | * @param column name to create the map for 138 | */ 139 | , createMap: function(column) { 140 | if (!this._maps[column]) { 141 | this._maps[column] = {}; 142 | 143 | this.forEach(function(row) { 144 | if (!this._maps[column][row[column]]) this._maps[column][row[column]] = []; 145 | this._maps[column][row[column]].push(row); 146 | }.bind(this)); 147 | } 148 | } 149 | 150 | 151 | 152 | /** 153 | * log or return the rows as an array 154 | * 155 | * @param if true the values are not logged but returned 156 | */ 157 | , dir: function(returnResult) { 158 | var result = []; 159 | this.forEach(function(item){ 160 | result.push(item.dir(true)); 161 | }); 162 | 163 | if (returnResult) return result; 164 | else log(result); 165 | } 166 | 167 | 168 | /** 169 | * return the values as plain array, the contained rows are still orm 170 | * model objects, in opposite top the toJSON method 171 | * 172 | * @returns array 173 | */ 174 | , toArray: function() { 175 | return this.slice(); 176 | } 177 | 178 | 179 | 180 | /** 181 | * returns a plain js array containing plain js objects representing 182 | * the sets contents 183 | */ 184 | , toJSON: function() { 185 | return this.map(function(item){ 186 | return item.toJSON ? item.toJSON() : undefined; 187 | }.bind(this)); 188 | } 189 | }); 190 | })(); 191 | -------------------------------------------------------------------------------- /test/filterBuilder.js: -------------------------------------------------------------------------------- 1 | { 2 | 'use strict'; 3 | 4 | const log = require('ee-log'); 5 | const assert = require('assert'); 6 | const FilterBuilder = require('../lib/FilterBuilder'); 7 | 8 | 9 | 10 | 11 | class QueryBuilder { 12 | 13 | constructor(entity, id = 0) { 14 | this.id = id; 15 | this.entity = entity; 16 | } 17 | 18 | 19 | 20 | 21 | getORM() { 22 | 23 | } 24 | 25 | 26 | leftJoin(entity) { 27 | return new QueryBuilder(entity, ++this.id); 28 | } 29 | 30 | 31 | 32 | getresource() { 33 | const context = this; 34 | 35 | return { 36 | getAliasName() { 37 | return `${context.entity}-${context.id}`; 38 | } 39 | } 40 | } 41 | } 42 | 43 | 44 | 45 | 46 | 47 | 48 | const filter = { 49 | type: "root" 50 | , children:[ 51 | { 52 | type: "and" 53 | , children:[ 54 | { 55 | type: "entity" 56 | , children:[ 57 | { 58 | type: "and" 59 | , children:[ 60 | { 61 | type: "property" 62 | , children:[ 63 | { 64 | type: "comparator" 65 | , children:[ 66 | { 67 | type: "value" 68 | , children:[] 69 | , nodeValue: "2017-05-11T11:26:05.926Z" 70 | } 71 | ] 72 | , comparator: "<=" 73 | } 74 | ] 75 | , propertyName: "issueDate" 76 | } 77 | , { 78 | type: "property" 79 | , children:[ 80 | { 81 | type: "comparator" 82 | , children:[ 83 | { 84 | type: "value" 85 | , children:[] 86 | , nodeValue: false 87 | } 88 | ] 89 | , comparator: "=" 90 | } 91 | ] 92 | , propertyName: "hidden" 93 | } 94 | ] 95 | } 96 | ] 97 | , entityName: "issue" 98 | } 99 | , { 100 | type: "entity" 101 | , children:[ 102 | { 103 | type: "property" 104 | , children:[ 105 | { 106 | type: "comparator" 107 | , children:[ 108 | { 109 | type: "value" 110 | , children:[] 111 | , nodeValue: "moments" 112 | } 113 | ] 114 | , comparator: "=" 115 | } 116 | ] 117 | , propertyName: "identifier" 118 | } 119 | ] 120 | , entityName: "blog" 121 | } 122 | , { 123 | type: "property" 124 | , children:[ 125 | { 126 | type: "comparator" 127 | , children:[ 128 | { 129 | type: "value" 130 | , children:[] 131 | , nodeValue: false 132 | } 133 | ] 134 | , comparator: "=" 135 | } 136 | ] 137 | , propertyName: "hidden" 138 | } 139 | , { 140 | type: "property" 141 | , children:[ 142 | { 143 | type: "comparator" 144 | , children:[ 145 | { 146 | type: "value" 147 | , children:[] 148 | , nodeValue: false 149 | } 150 | ] 151 | , comparator: "=" 152 | } 153 | ] 154 | , propertyName: "private" 155 | } 156 | ] 157 | } 158 | ] 159 | } 160 | 161 | 162 | 163 | 164 | 165 | describe(`FilterBuilder`, () => { 166 | it('should not crash when instantiated', () => { 167 | new FilterBuilder(); 168 | }); 169 | 170 | 171 | it('should render filter objects correctly', () => { 172 | const builder = new FilterBuilder(new QueryBuilder('post')); 173 | const relatedFilter = builder.build(filter); 174 | assert.equal(JSON.stringify(relatedFilter), '[[[{"issue-1":{}},{"issue-1":{}}],{"blog-2":{}},{"post-2":{}},{"post-2":{}}]]'); 175 | }); 176 | }); 177 | } -------------------------------------------------------------------------------- /lib/StaticORM.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | const Class = require('ee-class'); 5 | const log = require('ee-log'); 6 | const type = require('ee-types'); 7 | const Model = require('./Model'); 8 | const AdvancedQueryBuilder = require('./AdvancedQueryBuilder'); 9 | const FullTextQueryBuilder = require('./FullTextQueryBuilder'); 10 | 11 | 12 | 13 | 14 | const Helpers = new Class({ 15 | fn: function(fn, values, alias, isAggregate) { 16 | const selection = function() { 17 | return { 18 | fn : fn 19 | , values : values 20 | , value : values 21 | , alias : alias 22 | , isAggregate: !!isAggregate 23 | }; 24 | }; 25 | 26 | selection.isAggregate = !!isAggregate; 27 | 28 | return selection; 29 | } 30 | 31 | , operator: function(operator, value) { 32 | return function() { 33 | return { 34 | operator : operator 35 | , value : value 36 | }; 37 | }; 38 | } 39 | }); 40 | const helpers = new Helpers(); 41 | 42 | 43 | 44 | 45 | module.exports = new Class({ 46 | 47 | // alias 48 | alias: function(){ 49 | var len = arguments.length 50 | , tableName = len === 3 ? arguments[1] : null 51 | , columnName = arguments[len === 3 ? 2 : 1] 52 | , alias = arguments[0]; 53 | 54 | 55 | return function() { 56 | return { 57 | table : tableName 58 | , column : columnName 59 | , alias : alias 60 | } 61 | }; 62 | } 63 | 64 | 65 | // logic 66 | , or: function(first){ 67 | var a = type.array(first) ? first : Array.prototype.slice.call(arguments); 68 | a.mode = 'or'; 69 | return a; 70 | } 71 | 72 | , and: function(first){ 73 | var a = type.array(first) ? first : Array.prototype.slice.call(arguments); 74 | a.mode = 'and'; 75 | return a; 76 | } 77 | 78 | 79 | // nil is used when filtering nullable columns on left joins. it 80 | // matches all rows that aren't set. null filters rows that are set 81 | // but have the actual value null (on nullable columns) 82 | , nil: Symbol('nil') 83 | 84 | 85 | // use a keyword in a select (non escaped string) 86 | , keyword: function(keyword) { 87 | return function() { 88 | return { 89 | keyword: keyword 90 | }; 91 | }; 92 | } 93 | 94 | //reference to other table, col 95 | , reference: function(entity, column) { 96 | return function() { 97 | return { 98 | fn: 'reference' 99 | , entity: entity 100 | , column: column 101 | }; 102 | }; 103 | } 104 | 105 | 106 | 107 | // let the user call functions 108 | , function: (functionName, args, alias) => { 109 | return () => ({ 110 | functionName : functionName 111 | , args : args 112 | , alias : alias 113 | }) 114 | } 115 | 116 | 117 | 118 | // aggregate functions 119 | , count: function(field, alias) { 120 | return helpers.fn('count', field, alias, true); 121 | } 122 | , max: function(field, alias) { 123 | return helpers.fn('max', field, alias, true); 124 | } 125 | , min: function(field, alias) { 126 | return helpers.fn('min', field, alias, true); 127 | } 128 | , avg: function(field, alias) { 129 | return helpers.fn('avg', field, alias, true); 130 | } 131 | , sum: function(field, alias) { 132 | return helpers.fn('sum', field, alias, true); 133 | } 134 | 135 | 136 | 137 | , len: function(field, alias) { 138 | return helpers.fn('length', field, alias); 139 | } 140 | 141 | 142 | // value updates 143 | , increaseBy: function(amount) { 144 | return helpers.fn('increaseBy', amount); 145 | } 146 | , decreaseBy: function(amount) { 147 | return helpers.fn('decreaseBy', amount); 148 | } 149 | 150 | 151 | // filters 152 | , like: function(value) { 153 | return helpers.fn('like', value); 154 | } 155 | , notLike: function(value) { 156 | return helpers.fn('notLike', value); 157 | } 158 | 159 | 160 | , jsonValue: function(path, value) { 161 | return function() { 162 | return { 163 | fn : 'jsonValue' 164 | , rightSide : true 165 | , path : path 166 | , value : value 167 | }; 168 | }; 169 | } 170 | 171 | 172 | , in: function(values) { 173 | return helpers.fn('in', type.array(values) ? values : Array.prototype.slice.call(arguments)); 174 | } 175 | , notIn: function(values) { 176 | return helpers.fn('notIn', type.array(values) ? values : Array.prototype.slice.call(arguments)); 177 | } 178 | , notNull: function() { 179 | return helpers.fn('notNull'); 180 | } 181 | , isNull: function() { 182 | return helpers.fn('null'); 183 | } 184 | 185 | 186 | , equal: function(value) { 187 | return helpers.operator('=', value); 188 | } 189 | , notEqual: function(value) { 190 | return helpers.operator('!=', value); 191 | } 192 | 193 | , gt: function(value) { 194 | return helpers.operator('>', value); 195 | } 196 | , gte: function(value) { 197 | return helpers.operator('>=', value); 198 | } 199 | 200 | , lt: function(value) { 201 | return helpers.operator('<', value); 202 | } 203 | , lte: function(value) { 204 | return helpers.operator('<=', value); 205 | } 206 | 207 | , not: function(value) { 208 | return helpers.operator('not', value); 209 | } 210 | , is: function(value) { 211 | return helpers.operator('is', value); 212 | } 213 | 214 | /* 215 | * return a new advanced querybuilder instance 216 | */ 217 | , createQueryBuilder: function(query) { 218 | return new AdvancedQueryBuilder(query); 219 | } 220 | 221 | /* 222 | * return a new advanced querybuilder instance 223 | */ 224 | , qb: function(query) { 225 | return new AdvancedQueryBuilder(query); 226 | } 227 | 228 | /* 229 | * return a new advanced querybuilder instance 230 | */ 231 | , queryBuilder: function(query) { 232 | return new AdvancedQueryBuilder(query); 233 | } 234 | 235 | 236 | 237 | , fulltext: function(language) { 238 | return new FullTextQueryBuilder(language); 239 | } 240 | 241 | 242 | // the model, needed for extending models 243 | , Model: Model 244 | }); 245 | })(); 246 | -------------------------------------------------------------------------------- /test/postgres.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | DROP SCHEMA IF EXISTS related_test_postgres CASCADE; 4 | CREATE SCHEMA related_test_postgres; 5 | 6 | 7 | 8 | CREATE TABLE related_test_postgres.language ( 9 | id serial NOT NULL 10 | , code character varying(2) 11 | , CONSTRAINT "pk_language" PRIMARY KEY (id) 12 | , CONSTRAINT "unique_language_code" UNIQUE (code) 13 | ); 14 | 15 | CREATE TABLE related_test_postgres.image ( 16 | id serial NOT NULL 17 | , url character varying(300) 18 | , CONSTRAINT "pk_image" PRIMARY KEY (id) 19 | ); 20 | 21 | 22 | 23 | 24 | CREATE TABLE related_test_postgres.country ( 25 | id serial NOT NULL 26 | , code character varying(2) 27 | , name character varying(200) 28 | , CONSTRAINT "pk_country" PRIMARY KEY (id) 29 | , CONSTRAINT "unique_country_code" UNIQUE(code) 30 | ); 31 | 32 | CREATE TABLE related_test_postgres.county ( 33 | id serial NOT NULL 34 | , id_country integer NOT NULL 35 | , code character varying(2) 36 | , name character varying(200) 37 | , CONSTRAINT "pk_county" PRIMARY KEY (id) 38 | , CONSTRAINT "unique_county_code" UNIQUE(code) 39 | , CONSTRAINT "fk_county_country" FOREIGN KEY (id_country) REFERENCES related_test_postgres.country(id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 40 | ); 41 | 42 | CREATE TABLE related_test_postgres.municipality ( 43 | id serial NOT NULL 44 | , id_county integer NOT NULL 45 | , name character varying(200) 46 | , CONSTRAINT "pk_municipality" PRIMARY KEY (id) 47 | , CONSTRAINT "fk_municipality_county" FOREIGN KEY (id_county) REFERENCES related_test_postgres.county(id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 48 | ); 49 | 50 | 51 | 52 | 53 | CREATE TABLE related_test_postgres.venue ( 54 | id serial NOT NULL 55 | , id_image integer NOT NULL 56 | , id_municipality integer NOT NULL 57 | , name character varying(200) 58 | , CONSTRAINT "pk_venue" PRIMARY KEY (id) 59 | , CONSTRAINT "fk_venue_image" FOREIGN KEY (id_image) REFERENCES related_test_postgres.image (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 60 | , CONSTRAINT "fk_venue_municipality" FOREIGN KEY (id_municipality) REFERENCES related_test_postgres.municipality (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 61 | ); 62 | 63 | CREATE TABLE related_test_postgres.venue_image ( 64 | id serial NOT NULL 65 | , id_venue integer NOT NULL 66 | , id_image integer NOT NULL 67 | , CONSTRAINT "pk_venue_image" PRIMARY KEY (id) 68 | , CONSTRAINT "unique_venue_image_venue_image" UNIQUE (id_venue, id_image) 69 | , CONSTRAINT "fk_venue_image_venue" FOREIGN KEY (id_venue) REFERENCES related_test_postgres.venue (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE 70 | , CONSTRAINT "fk_venue_image_image" FOREIGN KEY (id_image) REFERENCES related_test_postgres.image (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE 71 | ); 72 | 73 | 74 | 75 | 76 | CREATE TABLE related_test_postgres.event ( 77 | id serial NOT NULL 78 | , id_venue integer NOT NULL 79 | , title character varying(200) NOT NULL 80 | , startdate timestamp without time zone NOT NULL 81 | , enddate timestamp without time zone 82 | , canceled boolean 83 | , created timestamp without time zone 84 | , updated timestamp without time zone 85 | , deleted timestamp without time zone 86 | , CONSTRAINT "pk_event" PRIMARY KEY (id) 87 | , CONSTRAINT "fk_event_venue" FOREIGN KEY (id_venue) REFERENCES related_test_postgres.venue (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 88 | ); 89 | 90 | CREATE TABLE related_test_postgres."eventLocale" ( 91 | id_event integer NOT NULL 92 | , id_language integer NOT NULL 93 | , description text NOT NULL 94 | , CONSTRAINT "pk_eventLocale" PRIMARY KEY (id_event, id_language) 95 | , CONSTRAINT "fk_eventLocale_event" FOREIGN KEY (id_event) REFERENCES related_test_postgres.event (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE 96 | , CONSTRAINT "fk_eventLocale_language" FOREIGN KEY (id_language) REFERENCES related_test_postgres.language (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE RESTRICT 97 | ); 98 | 99 | CREATE TABLE related_test_postgres.event_image ( 100 | id_event integer NOT NULL 101 | , id_image integer NOT NULL 102 | , CONSTRAINT "pk_event_image" PRIMARY KEY (id_event, id_image) 103 | , CONSTRAINT "fk_event_image_event" FOREIGN KEY (id_event) REFERENCES related_test_postgres.event (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE 104 | , CONSTRAINT "fk_event_image_image" FOREIGN KEY (id_image) REFERENCES related_test_postgres.image (id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE 105 | ); 106 | 107 | CREATE TABLE related_test_postgres.tree ( 108 | id serial NOT NULL 109 | , name varchar(100) 110 | , "left" integer NOT NULL 111 | , "right" integer NOT NULL 112 | , CONSTRAINT "pk_tree" PRIMARY KEY (id) 113 | ); 114 | 115 | CREATE TABLE related_test_postgres."timeZoneTest" ( 116 | "id" serial NOT NULL 117 | , "timstampWithTimezone" timestamp with time zone 118 | , "timstampWithoutTimezone" timestamp without time zone 119 | , CONSTRAINT "pk_timeZoneTest" PRIMARY KEY (id) 120 | ); 121 | 122 | 123 | CREATE TABLE related_test_postgres."emptyTypes" ( 124 | "id" serial NOT NULL 125 | , "bool" boolean 126 | , "num" int 127 | , CONSTRAINT "pk_emptyTypes" PRIMARY KEY (id) 128 | ); 129 | 130 | CREATE TABLE related_test_postgres."jsonType" ( 131 | "id" serial NOT NULL 132 | , "data" json 133 | , CONSTRAINT "pk_jsonType" PRIMARY KEY (id) 134 | ); 135 | 136 | 137 | CREATE TABLE related_test_postgres."typeTest" ( 138 | "serial" serial 139 | , "bigserial" bigserial 140 | , "serial8" serial8 141 | , "bigint" bigint 142 | , "bigint_default" bigint DEFAULT 6 143 | , "int8" int8 144 | , "int8_default" int8 DEFAULT 6 145 | , "bit" bit 146 | , "bit_len" bit (69) 147 | , "bit_varying" bit varying 148 | , "bit_varying_len" bit varying (69) 149 | , "varbit" varbit 150 | , "boolean" boolean 151 | , "boolean_default" boolean DEFAULT TRUE 152 | , "bool" bool 153 | , "box" box 154 | , "bytea" bytea 155 | , "character" character 156 | , "character_len" character (69) 157 | , "character_varying" character varying 158 | , "character_varying_len" character varying (69) 159 | , "cidr" cidr 160 | , "circle" circle 161 | , "date" date 162 | , "double_precision" double precision 163 | , "float8" float8 164 | , "inet" inet 165 | , "integer" integer 166 | , "int" int 167 | , "int4" int4 168 | , "interval" interval 169 | , "json" json 170 | , "line" line 171 | , "lseg" lseg 172 | , "macaddr" macaddr 173 | , "money" money 174 | , "numeric" numeric 175 | , "numeric_len" numeric (10, 4) 176 | , "path" path 177 | , "point" point 178 | , "polygon" polygon 179 | , "real" real 180 | , "float4" float4 181 | , "smallint" smallint 182 | , "int2" int2 183 | , "smallserial" smallserial 184 | , "serial2" serial2 185 | , "text" text 186 | , "time" time 187 | , "timetz" timetz 188 | , "time_without_time_zone" time without time zone 189 | , "time_with_time_zone" time with time zone 190 | , "timestamp" timestamp 191 | , "timestamp_default" timestamp DEFAULT now() 192 | , "timestamptz" timestamptz 193 | , "timestamp_with_time_zone" timestamp with time zone 194 | , "timestamp_without_time_zone" timestamp without time zone 195 | , "tsquery" tsquery 196 | , "tsvector" tsvector 197 | , "txid_snapshot" txid_snapshot 198 | , "uuid" uuid 199 | , "xml" xml 200 | , CONSTRAINT "pf_typeTest" PRIMARY KEY ("serial") 201 | ); 202 | -------------------------------------------------------------------------------- /test/mysql.sql: -------------------------------------------------------------------------------- 1 | 2 | 3 | DROP DATABASE IF EXISTS related_test_mysql; 4 | CREATE DATABASE related_test_mysql; 5 | 6 | 7 | 8 | CREATE TABLE related_test_mysql.language ( 9 | id INTEGER NOT NULL AUTO_INCREMENT 10 | , code VARCHAR(2) 11 | , CONSTRAINT pk_language PRIMARY KEY (id) 12 | , CONSTRAINT unique_language_code UNIQUE (code) 13 | ); 14 | 15 | CREATE TABLE related_test_mysql.image ( 16 | id INTEGER NOT NULL AUTO_INCREMENT 17 | , url VARCHAR(300) 18 | , CONSTRAINT pk_image PRIMARY KEY (id) 19 | ); 20 | 21 | 22 | 23 | 24 | CREATE TABLE related_test_mysql.country ( 25 | id INTEGER NOT NULL AUTO_INCREMENT 26 | , code VARCHAR(2) 27 | , name VARCHAR(200) 28 | , CONSTRAINT pk_country PRIMARY KEY (id) 29 | , CONSTRAINT unique_country_code UNIQUE(code) 30 | ); 31 | 32 | CREATE TABLE related_test_mysql.county ( 33 | id INTEGER NOT NULL AUTO_INCREMENT 34 | , id_country integer NOT NULL 35 | , code VARCHAR(2) 36 | , name VARCHAR(200) 37 | , CONSTRAINT pk_county PRIMARY KEY (id) 38 | , CONSTRAINT unique_county_code UNIQUE(code) 39 | , CONSTRAINT fk_county_country FOREIGN KEY (id_country) REFERENCES related_test_mysql.country(id) ON UPDATE CASCADE ON DELETE RESTRICT 40 | ); 41 | 42 | CREATE TABLE related_test_mysql.municipality ( 43 | id INTEGER NOT NULL AUTO_INCREMENT 44 | , id_county integer NOT NULL 45 | , name VARCHAR(200) 46 | , CONSTRAINT pk_municipality PRIMARY KEY (id) 47 | , CONSTRAINT fk_municipality_county FOREIGN KEY (id_county) REFERENCES related_test_mysql.county(id) ON UPDATE CASCADE ON DELETE RESTRICT 48 | ); 49 | 50 | 51 | 52 | 53 | CREATE TABLE related_test_mysql.venue ( 54 | id INTEGER NOT NULL AUTO_INCREMENT 55 | , id_image integer NOT NULL 56 | , id_municipality integer NOT NULL 57 | , name VARCHAR(200) 58 | , CONSTRAINT pk_venue PRIMARY KEY (id) 59 | , CONSTRAINT fk_venue_image FOREIGN KEY (id_image) REFERENCES related_test_mysql.image (id) ON UPDATE CASCADE ON DELETE RESTRICT 60 | , CONSTRAINT fk_venue_municipality FOREIGN KEY (id_municipality) REFERENCES related_test_mysql.municipality (id) ON UPDATE CASCADE ON DELETE RESTRICT 61 | ); 62 | 63 | CREATE TABLE related_test_mysql.venue_image ( 64 | id INTEGER NOT NULL AUTO_INCREMENT 65 | , id_venue integer NOT NULL 66 | , id_image integer NOT NULL 67 | , CONSTRAINT pk_venue_image PRIMARY KEY (id) 68 | , CONSTRAINT unique_venue_image_venue_image UNIQUE (id_venue, id_image) 69 | , CONSTRAINT fk_venue_image_venue FOREIGN KEY (id_venue) REFERENCES related_test_mysql.venue (id) ON UPDATE CASCADE ON DELETE CASCADE 70 | , CONSTRAINT fk_venue_image_image FOREIGN KEY (id_image) REFERENCES related_test_mysql.image (id) ON UPDATE CASCADE ON DELETE CASCADE 71 | ); 72 | 73 | 74 | 75 | 76 | CREATE TABLE related_test_mysql.event ( 77 | id INTEGER NOT NULL AUTO_INCREMENT 78 | , id_venue integer NOT NULL 79 | , title VARCHAR(200) NOT NULL 80 | , startdate DATETIME NOT NULL 81 | , enddate DATETIME 82 | , canceled boolean 83 | , created DATETIME 84 | , updated DATETIME 85 | , deleted DATETIME 86 | , CONSTRAINT pk_event PRIMARY KEY (id) 87 | , CONSTRAINT fk_event_venue FOREIGN KEY (id_venue) REFERENCES related_test_mysql.venue (id) ON UPDATE CASCADE ON DELETE RESTRICT 88 | ); 89 | 90 | CREATE TABLE related_test_mysql.eventLocale ( 91 | id_event integer NOT NULL 92 | , id_language integer NOT NULL 93 | , description text NOT NULL 94 | , CONSTRAINT pk_eventLocale PRIMARY KEY (id_event, id_language) 95 | , CONSTRAINT fk_eventLocale_event FOREIGN KEY (id_event) REFERENCES related_test_mysql.event (id) ON UPDATE CASCADE ON DELETE CASCADE 96 | , CONSTRAINT fk_eventLocale_language FOREIGN KEY (id_language) REFERENCES related_test_mysql.language (id) ON UPDATE CASCADE ON DELETE RESTRICT 97 | ); 98 | 99 | CREATE TABLE related_test_mysql.event_image ( 100 | id_event integer NOT NULL 101 | , id_image integer NOT NULL 102 | , CONSTRAINT pk_event_image PRIMARY KEY (id_event, id_image) 103 | , CONSTRAINT fk_event_image_event FOREIGN KEY (id_event) REFERENCES related_test_mysql.event (id) ON UPDATE CASCADE ON DELETE CASCADE 104 | , CONSTRAINT fk_event_image_image FOREIGN KEY (id_image) REFERENCES related_test_mysql.image (id) ON UPDATE CASCADE ON DELETE CASCADE 105 | ); 106 | 107 | CREATE TABLE related_test_mysql.tree ( 108 | id INTEGER NOT NULL AUTO_INCREMENT 109 | , name varchar(100) 110 | , `left` integer NOT NULL 111 | ,` right` integer NOT NULL 112 | , CONSTRAINT pk_tree PRIMARY KEY (id) 113 | ); 114 | 115 | 116 | CREATE TABLE related_test_mysql.timeZoneTest ( 117 | id serial NOT NULL 118 | , timstampWithTimezone DATETIME 119 | , timstampWithoutTimezone TIMESTAMP 120 | , CONSTRAINT pk_timeZoneTest PRIMARY KEY (id) 121 | ); 122 | 123 | 124 | 125 | CREATE TABLE related_test_mysql.emptyTypes ( 126 | id serial NOT NULL 127 | , bool boolean 128 | , num int 129 | , CONSTRAINT pk_emptyTypes PRIMARY KEY (id) 130 | ); 131 | 132 | 133 | 134 | CREATE TABLE related_test_mysql.typeTest ( 135 | id int not null PRIMARY KEY AUTO_INCREMENT 136 | , t_bit bit 137 | , t_bit_len bit (55) 138 | , t_tinyint tinyint 139 | , t_tinyint_len tinyint (2) 140 | , t_tinyint_default tinyint default 6 141 | , t_bool bool 142 | , t_boolean boolean 143 | , t_bool_default bool default true 144 | , t_smallint smallint 145 | , t_smallint_len smallint (4) 146 | , t_mediumint mediumint 147 | , t_int int 148 | , t_int_len int (11 ) 149 | , t_int_default int default 69 150 | , t_integer integer 151 | , t_bigint bigint 152 | , t_decimal decimal 153 | , t_decimal_len decimal (10,4) 154 | , t_dec dec 155 | , t_float float 156 | , t_float_len float (10, 4) 157 | , t_double double 158 | , t_double_len double (10,4) 159 | , t_double_precision double 160 | , t_double_precision_len double (10,4) 161 | , t_float_alternative float (12) 162 | , t_date date 163 | , t_datetime datetime 164 | , t_timestamp timestamp 165 | , t_time time 166 | , t_year year 167 | , t_year_len year (4) 168 | , t_char char 169 | , t_char_len char (69) 170 | , t_varchar_len varchar (69) 171 | , t_binary binary 172 | , t_binary_len binary (69) 173 | , t_varbinary_len varbinary (34) 174 | , t_tinyblob tinyblob 175 | , t_tinytext tinytext 176 | , t_blob blob 177 | , t_blob_len blob (344) 178 | , t_text text 179 | , t_text_len text (333) 180 | , t_mediumblob mediumblob 181 | , t_mediumtext mediumtext 182 | , t_longblob longblob 183 | , t_longtext longtext 184 | ); 185 | -------------------------------------------------------------------------------- /lib/ModelCloner.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log') 6 | , asyncMethod = require('async-method') 7 | , type = require('ee-types'); 8 | 9 | 10 | 11 | module.exports = new Class({ 12 | 13 | 14 | 15 | /** 16 | * classs constructor 17 | * 18 | * @param model instance to be cloned 19 | */ 20 | init: function(sourceModelInstance) { 21 | 22 | // source of all required info for this class 23 | this.sourceModelInstance = sourceModelInstance; 24 | 25 | // config storage 26 | this._copy = {}; 27 | this._reassign = {}; 28 | } 29 | 30 | 31 | 32 | 33 | /** 34 | * copy the entites definedd here when cloning 35 | * 36 | * @param string entity (0-n) or array of strings 37 | * 38 | * @returns this 39 | */ 40 | , copy: function() { 41 | var addedEntities = [] 42 | , filter; 43 | 44 | for (var i = 0, l = arguments.length; i < l; i++) { 45 | if (type.array(arguments[i])) this.copy.apply(this, arguments[i]); 46 | else if (type.string(arguments[i])) { 47 | this._copy[arguments[i]] = true; 48 | addedEntities.push(arguments[i]); 49 | } 50 | else if (type.object(arguments[i])) filter = arguments[i]; 51 | else throw new Error('Expected array or string as argument to the copy method!'); 52 | } 53 | 54 | if (filter && addedEntities.length) { 55 | addedEntities.forEach(function(entity) { 56 | this._copy[entity] = filter; 57 | }.bind(this)); 58 | } 59 | 60 | return this; 61 | } 62 | 63 | 64 | 65 | 66 | 67 | /** 68 | * reassign entites pointing to the entity to be cloned 69 | * to the cloned copy 70 | * 71 | * @param string entity (0-n) or array of strings 72 | * 73 | * @returns this 74 | */ 75 | , reassign: function() { 76 | var addedEntities = [] 77 | , filter; 78 | 79 | for (var i = 0, l = arguments.length; i < l; i++) { 80 | if (type.array(arguments[i])) this.reassign.apply(this, arguments[i]); 81 | else if (type.string(arguments[i])) { 82 | this._reassign[arguments[i]] = true; 83 | addedEntities.push(arguments[i]); 84 | } 85 | else if (type.object(arguments[i])) filter = arguments[i]; 86 | else throw new Error('Expected array or string as argument to the reassign method!'); 87 | } 88 | 89 | 90 | if (filter && addedEntities.length) { 91 | addedEntities.forEach(function(entity) { 92 | this._reassign[entity] = filter; 93 | }.bind(this)); 94 | } 95 | 96 | return this; 97 | } 98 | 99 | 100 | 101 | 102 | /** 103 | * execute the cloning process 104 | */ 105 | , save: asyncMethod(function() { 106 | var isNewTransaction = false 107 | , callback 108 | , transaction 109 | , definition 110 | , query 111 | , err 112 | , filter; 113 | 114 | for (var i = 0, l = arguments.length; i < l; i++) { 115 | if (typeof arguments[i] === 'function') callback = arguments[i]; 116 | else if (typeof arguments[i] === 'Object') transaction = arguments[i]; 117 | else throw new Error('The save method expets a callback and or a transaction!'); 118 | } 119 | 120 | 121 | if (this.sourceModelInstance.isDirty()) callback(new Error('Cannot clone a dirty model! Please save your model before cloning it!')); 122 | else if (!this.sourceModelInstance.isFromDatabase()) callback(new Error('Cannot clone a model that was not saved before! Please save your model before cloning it!')); 123 | else { 124 | // we're good, check if we got instruction for each and every relation of the 125 | // source model 126 | definition = this.sourceModelInstance.getDefinition(); 127 | 128 | // need a proper transaction 129 | if (!transaction) { 130 | if (this.sourceModelInstance._getDatabase().isTransaction()) transaction = this.sourceModelInstance._getDatabase(); 131 | else { 132 | transaction = this.sourceModelInstance._getDatabase().createTransaction() 133 | isNewTransaction = true; 134 | } 135 | } 136 | 137 | // filter by pks 138 | filter = {}; 139 | 140 | // hopefully this works =) 141 | definition.primaryKeys.forEach(function(keyName) { 142 | filter[keyName] = this.sourceModelInstance[keyName]; 143 | }.bind(this)); 144 | 145 | 146 | // create a querybuilder in order to laod the entites 147 | query = transaction[definition.name]('*', filter); 148 | 149 | // belongs to items 150 | Object.keys(definition.belongsTo).some(function(name) { 151 | if (this._copy[name]) { 152 | 153 | // duplicate this record, thus load it 154 | // apply filters if provided 155 | if (this._copy[name] === true) query.get(name, '*'); 156 | else query.get(name, '*').filter(this._copy[name]); 157 | 158 | // we need to know our fk 159 | this._copy[name] = definition.belongsTo[name].targetColumn; 160 | } 161 | else if (this._reassign[name]) { 162 | 163 | // load this too, we need to update it 164 | // apply filters if provided 165 | if (this._reassign[name] === true) query.get(name, '*'); 166 | else query.get(name, '*').filter(this._reassign[name]); 167 | 168 | // we need to know our fk 169 | this._reassign[name] = definition.belongsTo[name].targetColumn; 170 | } 171 | }.bind(this)); 172 | 173 | 174 | // everything ok, proceed and load the stuff 175 | if (err) callback(err); 176 | else { 177 | query.findOne().then(function(entity) { 178 | if (!entity) callback(new Error('Failed to load the model to clone from the databse, cannot clone it!')); 179 | else { 180 | // we're ok 181 | 182 | 183 | // delete pks, set 184 | entity.isNotFromDatabase(); 185 | 186 | definition.primaryKeys.forEach(function(keyName) { 187 | entity.removeValue(keyName); 188 | }.bind(this)); 189 | 190 | 191 | // delete our fk on to be copied models 192 | Object.keys(this._copy).forEach(function(relationName) { 193 | entity[relationName].forEach(function(relatedEntity) { 194 | relatedEntity.isNotFromDatabase(); 195 | 196 | // delete our fk 197 | relatedEntity.removeValue(this._copy[relationName]); 198 | 199 | // check if the pk includes our fk, if not, remove it 200 | if (!relatedEntity.getDefinition().primaryKeys.some(function(keyName) { 201 | return this._copy[relationName] === keyName; 202 | }.bind(this))) { 203 | // delete pks 204 | relatedEntity.getDefinition().primaryKeys.forEach(relatedEntity.removeValue.bind(relatedEntity)); 205 | } 206 | }.bind(this)); 207 | }.bind(this)); 208 | 209 | 210 | // change reference on the models that need to be reassigned 211 | Object.keys(this._reassign).forEach(function(relationName) { 212 | entity[relationName].forEach(function(relatedEntity) { 213 | // delete our fk 214 | entity.removeValue(this._reassign[relationName]); 215 | }.bind(this)); 216 | }.bind(this)); 217 | 218 | // save everything 219 | entity.save(transaction, function(err, clonedEntity) { 220 | if (err) callback(err); 221 | else { 222 | if (isNewTransaction) { 223 | transaction.commit().then(() => { 224 | callback(null, clonedEntity); 225 | }).catch(callback); 226 | } 227 | else callback(null, clonedEntity); 228 | } 229 | }.bind(this)); 230 | } 231 | }.bind(this)).catch(function(err) { 232 | if (isNewTransaction) { 233 | transaction.rollback().then(() => { 234 | callback(err); 235 | }).catch(callback) 236 | } 237 | else callback(err); 238 | }.bind(this)); 239 | } 240 | } 241 | }) 242 | }); 243 | })(); -------------------------------------------------------------------------------- /lib/Entity.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | var Class = require('ee-class') 6 | , EventEmitter = require('ee-event-emitter') 7 | , argv = require('ee-argv') 8 | , QueryBuilder = require('./QueryBuilder') 9 | , Model = require('./Model') 10 | , ModelBuilder = require('./ModelBuilder') 11 | , QueryBuilderBuilder = require('./QueryBuilderBuilder') 12 | , log = require('ee-log'); 13 | 14 | 15 | 16 | 17 | var dev = argv.has('dev-orm'); 18 | 19 | 20 | 21 | // model initializer 22 | module.exports = new Class({ 23 | 24 | 25 | init: function(_options){ 26 | var thisContext = this 27 | , Constructor; 28 | 29 | this._definition = _options.definition; 30 | this._getDatabase = _options.getDatabase; 31 | this._orm = _options.orm; 32 | this._queryBuilders = _options.queryBuilders; 33 | this._extensions = _options.extensions; 34 | 35 | 36 | // storage for relations 37 | this._mappingMap = {}; 38 | this._belongsToMap = {}; 39 | this._referenceMap = {}; 40 | this._columns = {}; 41 | this._genericAccessors = {}; 42 | 43 | 44 | // the user may register events on models 45 | this._events = new Map(); 46 | 47 | 48 | // create Model Class 49 | this.createModel(); 50 | 51 | // create the querybuilder for this entity 52 | this.createQueryBuilder(); 53 | 54 | 55 | // constructor to expose 56 | Constructor = function(scope, options, relatingSets) { 57 | var isScoped = false; 58 | 59 | if (this instanceof Constructor) { 60 | // if we're running on a transaction object 61 | // we get the scope, else we have to remap the arguments 62 | // the scope is only passed if the function is called 63 | // as a constructor 64 | if (scope && scope.commit && scope.rollback && scope.LOCK_EXCLUSIVE) { 65 | isScoped = true; 66 | } 67 | else { 68 | options = scope; 69 | relatingSets = options; 70 | } 71 | 72 | 73 | // new model instance 74 | var instance = new thisContext.Model({ 75 | parameters : options 76 | , orm : thisContext._orm 77 | , definition : thisContext._definition 78 | , isFromDB : options && options._isFromDB 79 | , set : options && options._set 80 | , relatingSets : relatingSets 81 | , getDatabase : isScoped ? scope._getDatabase.bind(scope) : thisContext._getDatabase 82 | , events : thisContext._events 83 | }); 84 | 85 | return instance; 86 | } 87 | else { 88 | // return a querybuilder 89 | var qb = new thisContext.QueryBuilder({ 90 | parameters : Array.prototype.slice.call(arguments) 91 | , getDatabase : this && this._getDatabase ? this._getDatabase.bind(this) : thisContext._getDatabase 92 | }); 93 | 94 | return qb; 95 | } 96 | }; 97 | 98 | 99 | Object.defineProperty(Constructor, 'Model', Model); 100 | 101 | 102 | // the model definition must be accesible publicly 103 | Constructor.definition = _options.definition; 104 | 105 | // expose if its a mapping table 106 | if (this._definition.isMapping) Constructor.isMapping = true; 107 | 108 | // let the user define accessornames 109 | Constructor.setMappingAccessorName = this.setMappingAccessorName.bind(this); 110 | Constructor.setReferenceAccessorName = this.setReferenceAccessorName.bind(this); 111 | Constructor.setBelongsToAccessorName = this.setBelongsToAccessorName.bind(this); 112 | 113 | Constructor.getDefinition = this.getDefinition.bind(this); 114 | Constructor.extend = this.extend.bind(this); 115 | 116 | // entity events 117 | Constructor.on = this.addModelListener.bind(this); 118 | Constructor.off = this.removeModelListener.bind(this); 119 | 120 | Constructor.reload = this.reload.bind(this); 121 | 122 | return Constructor; 123 | } 124 | 125 | 126 | 127 | 128 | /** 129 | * the user may install model wide event listeners 130 | */ 131 | , addModelListener: function(eventName, listener){ 132 | if (!this._events.has(eventName)) this._events.set(eventName, []); 133 | this._events.get(eventName).push(listener); 134 | } 135 | 136 | 137 | /** 138 | * the user may remove model events again 139 | */ 140 | , removeModelListener: function(eventName, listener) { 141 | if (eventName) { 142 | if (this._events.has(eventName)) { 143 | if (listener) { 144 | const listeners = this._events.get(eventName); 145 | const index = listeners.indexOf(listener); 146 | 147 | if (index >= 0) listeners.splice(index, 1); 148 | } else this._events.set(eventName, []); 149 | } 150 | } else this._events = new Map(); 151 | } 152 | 153 | 154 | 155 | 156 | 157 | 158 | , extend: function(withModel) { 159 | this._extensionModel = withModel; 160 | this.createModel(); 161 | } 162 | 163 | 164 | , getDefinition: function() { 165 | return this._definition; 166 | } 167 | 168 | 169 | , createQueryBuilder: function() { 170 | this.QueryBuilder = new QueryBuilderBuilder({ 171 | orm : this._orm 172 | , queryBuilders : this._queryBuilders 173 | , definition : this._definition 174 | , mappingMap : this._mappingMap 175 | , belongsToMap : this._belongsToMap 176 | , referenceMap : this._referenceMap 177 | , columns : this._columns 178 | , extensions : this._extensions 179 | }); 180 | 181 | // store our instance of the querybuilders 182 | this._queryBuilders[this._definition.getTableName()] = this.QueryBuilder; 183 | } 184 | 185 | 186 | , createModel: function() { 187 | this.Model = new ModelBuilder({ 188 | baseModel : this._extensionModel || Model 189 | , definition : this._definition 190 | , mappingMap : this._mappingMap 191 | , belongsToMap : this._belongsToMap 192 | , referenceMap : this._referenceMap 193 | , genericAccessors : this._genericAccessors 194 | , columns : this._columns 195 | , orm : this._orm 196 | , extensions : this._extensions 197 | }); 198 | } 199 | 200 | 201 | 202 | 203 | , reload: function() { 204 | this.createModel(); 205 | this.createQueryBuilder(); 206 | } 207 | 208 | 209 | 210 | 211 | 212 | , setMappingAccessorName: function(mappingName, name) { 213 | if (!this.Model[name]) { 214 | if (!this._mappingMap[mappingName]) throw new Error('The mapping «'+mappingName+'» does not exists on the «'+this._definition.name+'» model!'); 215 | 216 | this._mappingMap[mappingName].definition.aliasName = name; 217 | this._mappingMap[mappingName].definition.useGenericAccessor = false; 218 | 219 | this.createModel(); 220 | this.createQueryBuilder(); 221 | } 222 | else throw new Error('The mapping accessor «'+name+'» on the model «'+this._model.name+'» is already in use!'); 223 | } 224 | 225 | , setReferenceAccessorName: function(referenceName, name) { 226 | if (!this.Model[name]) { 227 | if (!this._referenceMap[referenceName]) throw new Error('The reference «'+referenceName+'» does not exists on the «'+this._definition.name+'» model!'); 228 | 229 | this._referenceMap[referenceName].aliasName = name; 230 | this._referenceMap[referenceName].useGenericAccessor = false; 231 | 232 | this.getDefinition().references[name] = this._referenceMap[referenceName]; 233 | 234 | //this._genericAccessors[referenceName] 235 | 236 | this.createModel(); 237 | this.createQueryBuilder(); 238 | } 239 | else throw new Error('The reference accessor «'+name+'» on the model «'+this._model.name+'» is already in use!'); 240 | } 241 | 242 | 243 | /** 244 | * create an alias for a relation because the names were already used 245 | */ 246 | , setBelongsToAccessorName: function(targetModel, referenceName, name) { 247 | if (!this.Model[name]) { 248 | if (!this._belongsToMap[targetModel]) throw new Error('The belongs to «'+referenceName+'» does not exists on the «'+this._definition.name+'» model!'); 249 | 250 | this._belongsToMap[targetModel].column.belongsTo.some(function(ref) { 251 | if (ref.name === targetModel && ref.targetColumn === referenceName) { 252 | ref.aliasName = name; 253 | ref.useGenericAccessor = false; 254 | 255 | this.getDefinition().belongsTo[name] = ref; 256 | 257 | return true; 258 | } 259 | }.bind(this)); 260 | 261 | 262 | this.createModel(); 263 | this.createQueryBuilder(); 264 | } 265 | else throw new Error('The belongs to accessor «'+name+'» on the model «'+this._model.name+'» is already in use!'); 266 | } 267 | }); 268 | })(); 269 | -------------------------------------------------------------------------------- /lib/QueryCompiler.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log') 6 | , debug = require('ee-argv').has('dev-orm') 7 | , debugErrors = require('ee-argv').has('debug-orm-errors') 8 | , QueryContext = require('related-query-context') 9 | , Set = require('./Set'); 10 | 11 | 12 | 13 | 14 | module.exports = new Class({ 15 | 16 | init: function(options) { 17 | this._resource = options.resource; 18 | this._orm = options.orm; 19 | this.getDatabase = options.getDatabase; 20 | this.transaction = options.transaction; 21 | } 22 | 23 | 24 | 25 | , findOne: function(callback) { 26 | this._resource.query.limit = 1; 27 | 28 | this.find(function(err, results) { 29 | if (err) callback(err); 30 | else if (results && results.length) callback(null, results[0]); 31 | else callback(); 32 | }.bind(this)); 33 | } 34 | 35 | 36 | 37 | , find: function(callback) { 38 | var resource = this._resource; 39 | 40 | 41 | // rpepare the resources, apply filters & ordering to the 42 | // root resource, manage selects 43 | resource.setSelectMode(); 44 | resource.prepare(); 45 | 46 | // execute the base query 47 | this._executeQuery(resource.getQueryMode(), resource.query, resource, function(err, rows) { 48 | if (err) callback(err); 49 | else { 50 | // create set 51 | resource.set = this._makeSet(rows, resource); 52 | 53 | 54 | // collect subqueries, if there is data in the base result set 55 | if (resource.set.length) this._executeSubqueries(resource, this._collectQueries(resource, []), callback); 56 | else callback(null, resource.set); 57 | } 58 | }.bind(this)); 59 | } 60 | 61 | 62 | 63 | , delete: function(callback) { 64 | this._resource.setDeleteMode(); 65 | this._resource.prepare(true); 66 | 67 | this._executeQuery(this._resource.getQueryMode(), this._resource.query, this._resource, callback); 68 | } 69 | 70 | 71 | , update: function(callback) { 72 | this._resource.setUpdateMode(); 73 | this._resource.prepare(true); 74 | 75 | this._executeQuery(this._resource.getQueryMode(), this._resource.query, this._resource, callback); 76 | } 77 | 78 | 79 | , count: function(callback, column) { 80 | this._resource.setSelectMode(); 81 | this._resource.setCountingFlag(); 82 | this._resource.prepare(true); 83 | 84 | this._resource.prepareCounting(column); 85 | 86 | 87 | this._executeQuery(this._resource.getQueryMode(), this._resource.query, this._resource, function(err, result) { 88 | if (err) callback(err); 89 | else if (result && result.length) callback(null, parseInt(result[0].rowCount, 10)); 90 | }.bind(this)); 91 | } 92 | 93 | 94 | /* 95 | * prepare the query 96 | */ 97 | , prepare: function() { 98 | this._resource.prepare(true); 99 | return this; 100 | } 101 | 102 | 103 | , _executeSubqueries: function(rootResource, queries, callback) { 104 | Promise.all(queries.map((resource) => { 105 | 106 | // filter the resource by the ids of the root resource 107 | rootResource.applyFilter(resource); 108 | 109 | // rootResource.applyGroup(resource); 110 | if (resource.parentResource) resource.parentResource.applyGroup(resource); 111 | // log(resource); 112 | 113 | return new Promise((resolve, reject) => { 114 | this._executeQuery('query', resource.query, resource, function(err, rows) { 115 | if (err) reject(err); 116 | else { 117 | resource.set = this._makeSet(rows, resource); 118 | resolve(); 119 | } 120 | }.bind(this)); 121 | }); 122 | })).then(() => { 123 | this._buildRelations(this._resource); 124 | callback(null, this._resource.set); 125 | }).catch(callback); 126 | } 127 | 128 | 129 | 130 | 131 | , _buildRelations: function(resource) { 132 | if (resource.getRootResoure().raw) this.buildRawRelations(resource); 133 | else { 134 | if (resource.set && resource.hasChildren()) { 135 | resource.children.forEach(function(childResource) { 136 | if (childResource.set) { 137 | 138 | if (childResource.set.length) { 139 | childResource.set.forEach(function(record) { 140 | 141 | record._mappingIds.forEach(function(mappingId) { 142 | var parentRecords = resource.set.getByColumnValue(childResource.referencedParentColumn, mappingId); 143 | 144 | if (parentRecords && parentRecords.length) { 145 | parentRecords.forEach(function(parentRecord) { 146 | //log(childResource.loaderId); 147 | if (childResource.type === 'mapping') { 148 | parentRecord.getMapping(childResource.loaderId).addExisiting(record); 149 | //parentRecord[childResource.name].addExisiting(record); 150 | } 151 | else if (childResource.type === 'belongsTo') { 152 | parentRecord.getBelongsTo(childResource.loaderId).addExisiting(record); 153 | //parentRecord[childResource.name].addExisiting(record); 154 | } 155 | else { 156 | // reference 157 | //parentRecord.setReference(childResource.loaderId, record, true); 158 | parentRecord._references[childResource.aliasName] = record; 159 | //parentRecord[childResource.name] = record; 160 | } 161 | }.bind(this)); 162 | } 163 | }.bind(this)); 164 | }.bind(this)); 165 | } 166 | 167 | // tell the set 168 | 169 | this._buildRelations(childResource); 170 | } 171 | }.bind(this)); 172 | } 173 | } 174 | } 175 | 176 | 177 | 178 | 179 | /** 180 | * build relations without working with models 181 | */ 182 | , buildRawRelations: function(resource) { 183 | if (resource.set && resource.hasChildren()) { 184 | resource.children.forEach((childResource) => { 185 | if (childResource.set) { 186 | 187 | if (childResource.set.length) { 188 | 189 | // we need a map for correctly assigning records 190 | let map; 191 | 192 | childResource.set.forEach((record) => { 193 | 194 | if (typeof record.____id____ !== 'undefined') { 195 | 196 | // make sure we got a map of the the values of the 197 | // column 198 | if (!map) { 199 | map = {}; 200 | 201 | resource.set.forEach((row) => { 202 | if (!map[row[childResource.referencedParentColumn]]) map[row[childResource.referencedParentColumn]] = []; 203 | map[row[childResource.referencedParentColumn]].push(row); 204 | }); 205 | } 206 | 207 | 208 | if (map[record.____id____]) { 209 | map[record.____id____].forEach((parentRecord) => { 210 | 211 | // set the property to undefined 212 | delete record.____id____; 213 | 214 | if (childResource.type === 'mapping' || childResource.type === 'belongsTo') { 215 | let name = childResource.aliasName || childResource.name; 216 | 217 | if (!parentRecord[name]) parentRecord[name] = []; 218 | parentRecord[name].push(record); 219 | } 220 | else { 221 | // reference 222 | parentRecord[childResource.aliasName] = record; 223 | } 224 | }); 225 | } 226 | } 227 | }); 228 | } 229 | 230 | 231 | this.buildRawRelations(childResource); 232 | } 233 | }); 234 | } 235 | } 236 | 237 | 238 | 239 | 240 | 241 | // get all selected queries, add the correct filter to them 242 | , _collectQueries: function(resource, queries) { 243 | if (resource.hasChildren()) { 244 | resource.children.forEach(function(childResource){ 245 | if (childResource.isSelected()) queries.push(childResource); 246 | this._collectQueries(childResource, queries); 247 | }.bind(this)); 248 | } 249 | 250 | return queries; 251 | } 252 | 253 | 254 | 255 | , _executeQuery: function(mode, query, resource, callback) { 256 | query.mode = mode; 257 | 258 | let pool = resource.getRootResoure().pool; 259 | if (typeof pool !== 'string' || !pool.length) pool = (mode === 'select' ? 'read' : 'write'); 260 | 261 | (this.transaction || this.getDatabase()).executeQuery(new QueryContext({ 262 | query : query 263 | , pool : pool 264 | , debug : resource.getRootResoure().isInDebugMode() 265 | , wait : !!resource.getRootResoure().wait 266 | })).then((data) => { 267 | callback(null, data); 268 | }).catch((err) => { 269 | callback(err); 270 | }); 271 | } 272 | 273 | 274 | 275 | 276 | 277 | /** 278 | * creates a set if useful 279 | */ 280 | , _makeSet: function(rows, resource) { 281 | 282 | if (resource.getRootResoure().raw) { 283 | // we nedd to de-duplicate the rows by pk 284 | let existing = {}; 285 | 286 | return rows.filter((row) => { 287 | let id = resource.primaryKeys.map(key => row[key]).join('|'); 288 | 289 | if (!existing[id]) { 290 | existing[id] = true; 291 | return true; 292 | } 293 | else return false; 294 | }); 295 | } 296 | else { 297 | let records = new Set({ 298 | primaryKeys: resource.primaryKeys 299 | , name: resource.name 300 | , resource: resource 301 | }); 302 | 303 | 304 | if (rows && rows.length) { 305 | rows.forEach(function(row) { 306 | Object.defineProperty(row, '_isFromDB', {value:true}); 307 | Object.defineProperty(row, '_set', {value:records}); 308 | 309 | records.push(new resource.Model(row, resource.relatingSets)); 310 | }.bind(this)); 311 | } 312 | 313 | 314 | return records; 315 | } 316 | } 317 | }); 318 | })(); 319 | -------------------------------------------------------------------------------- /lib/TransactionBuilder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | const Class = require('ee-class'); 6 | const log = require('ee-log'); 7 | const type = require('ee-types'); 8 | 9 | 10 | 11 | module.exports = function(database) { 12 | 13 | 14 | var classDefinition = { 15 | inherits: database 16 | 17 | 18 | // differnet locks that can be obtained by the user on the transaction 19 | // the orm does its best to get a consistent behavior across the different 20 | // rdbms systems, which is sadly not possible with locks. 21 | // see the lock description inside the specific driver 22 | , LOCK_READ: 'LOCK_READ' 23 | , LOCK_WRITE: 'LOCK_WRITE' 24 | , LOCK_EXCLUSIVE: 'LOCK_EXCLUSIVE' 25 | 26 | 27 | // busy flag 28 | // false -> no transaction, need to get one 29 | // true -> busy getting a transaction 30 | // null -> got a transaction 31 | , _busy: null 32 | 33 | 34 | // the user may specify a pool he wants to execute a transaction on 35 | , pool: null 36 | 37 | 38 | // holds the pointer to the connection with the transaction 39 | // false -> not initialized 40 | // object -> active transaction 41 | // null -> transaction ended 42 | , _transaction: false 43 | 44 | 45 | /* 46 | * initialize class variable, make sure to interceopt all calls to the models 47 | * and to inject the transaction iinto them 48 | */ 49 | , init: function(options) { 50 | 51 | if (options) { 52 | if (options.pool) this.pool = options.pool; 53 | } 54 | 55 | // query queue (filled while waiting for the transaction 56 | // to be created) 57 | this._queue = []; 58 | 59 | // flags how many queries are currently beeing executedd 60 | this.runningQueries = 0; 61 | } 62 | 63 | 64 | 65 | /* 66 | * if anony need to know if we're a transaction 67 | */ 68 | , isTransaction: function() { 69 | return true; 70 | } 71 | 72 | 73 | /* 74 | * return myself 75 | */ 76 | , getTransaction: function() { 77 | return this; 78 | } 79 | 80 | 81 | 82 | 83 | 84 | /* 85 | * apply a table lock 86 | */ 87 | , lock: function(table, mode, callback) { 88 | if (this._transaction) this._transaction.lock(this._databaseName, table, mode, (callback || function(){})); 89 | else { 90 | // store the lock configuration, ti will be applied as soon as 91 | // the tranaction was created 92 | this._lockConfiguration = { 93 | table : table 94 | , mode : mode 95 | }; 96 | 97 | if (callback) callback(); 98 | } 99 | } 100 | 101 | 102 | 103 | 104 | 105 | 106 | /* 107 | * commit the changes made by the tranaction, end the connection 108 | * we're based on 109 | */ 110 | , commit: function(callback) { 111 | 112 | if (callback) { 113 | this._commit().then((data) => { 114 | callback(null, data); 115 | }).catch(callback); 116 | } 117 | else return this._commit(); 118 | } 119 | 120 | 121 | 122 | 123 | 124 | 125 | , _commit: function() { 126 | if (this._transaction) { 127 | 128 | return this._transaction.commit().then((data) => { 129 | this.emit('commit'); 130 | this._endTransaction(); 131 | return Promise.resolve(data); 132 | }).catch((err) => { 133 | this._endTransaction(); 134 | return Promise.reject(err); 135 | }); 136 | } 137 | else if (this._transaction === false) { 138 | this._endTransaction(); 139 | return Promise.resolve(); 140 | } 141 | else { 142 | this._endTransaction(); 143 | return Promise.reject(new Error('Cannot commit! The transaction has already ended.')); 144 | } 145 | } 146 | 147 | 148 | 149 | /* 150 | * commit the changes made by the tranaction, end the connection 151 | * we're based on 152 | */ 153 | , rollback: function(callback) { 154 | 155 | if (callback) { 156 | this._rollback().then((data) => { 157 | callback(null, data); 158 | }).catch(callback); 159 | } 160 | else return this._rollback(); 161 | } 162 | 163 | 164 | 165 | 166 | /* 167 | * rollback the changes made by the tranaction, end the connection 168 | * we're based on 169 | */ 170 | , _rollback: function() { 171 | if (this._transaction) { 172 | 173 | return this._transaction.rollback().then((data) => { 174 | this.emit('rollback'); 175 | this._endTransaction(); 176 | return Promise.resolve(data); 177 | }).catch((err) => { 178 | this._endTransaction(); 179 | return Promise.reject(err); 180 | }); 181 | } else if (this._transaction === false) { 182 | this._endTransaction(); 183 | return Promise.resolve(); 184 | } else { 185 | this._endTransaction(); 186 | return Promise.resolve(); 187 | } 188 | } 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | /* 197 | * execute a query, this overwrites the same method on the database object 198 | * we're inheritiung from 199 | */ 200 | , executeQuery: function(context) { 201 | 202 | // check for ended transactions 203 | if (this._transaction === null) return Promise.reject(new Error('Cannot execute query, the transaction has already ended!')); 204 | else { 205 | return new Promise((resolve, reject) => { 206 | 207 | // directly push the query to the queue 208 | // so that it can be executed in the 209 | // right order 210 | this._queue.push({ 211 | context: context 212 | , resolve: resolve 213 | , reject: reject 214 | }); 215 | 216 | 217 | // execute directly if the transaction exists already 218 | if (this._transaction) this.executeQueue(); 219 | else if (!this._busy) { 220 | 221 | // mark as busy, aka connection in progress 222 | // will not be reset because we cannot load multiple 223 | // connections on one transaction 224 | this._busy = true; 225 | 226 | 227 | // get a conenction and create the transaction 228 | return this._database.getConnection(this.pool || 'write').then((connection) => { 229 | this._transaction = connection; 230 | this._transaction.createTransaction(); 231 | this._transaction.on('end', this._endTransaction.bind(this)); 232 | 233 | // check if the table lock must be exexuted 234 | if (this._lockConfiguration) this.lock(this._lockConfiguration.table, this._lockConfiguration.mode, this._lockConfiguration.callback); 235 | 236 | // execute queue 237 | this.executeQueue(); 238 | }).catch((err) => { 239 | this._transaction = null; 240 | this.abortQueue(err); 241 | return Promise.reject(err); 242 | }); 243 | } 244 | }); 245 | } 246 | } 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | /** 255 | * executes all items in the queue in order 256 | */ 257 | , executeQueue: function() { 258 | if (this._queue && this._queue.length) { 259 | if (this._transaction) { 260 | 261 | const currentQuery = this._queue[0]; 262 | 263 | // check if we have to wait until all running queries 264 | // have been executed 265 | if (!currentQuery.context.transactionExecutionWait || this.runningQueries === 0) { 266 | this.runningQueries++; 267 | 268 | 269 | // remove the first item, the reference is already stored 270 | // on the currentQuery variable 271 | this._queue.shift(); 272 | 273 | // get the sql 274 | this.renderQuery(this._transaction, currentQuery.context).then(() => { 275 | 276 | 277 | // execute 278 | return this._transaction.query(currentQuery.context).then((result) => { 279 | 280 | this.runningQueries--; 281 | if (this.runningQueries === 0) this.executeQueue(); 282 | 283 | // return 284 | currentQuery.resolve(result); 285 | }); 286 | 287 | 288 | // execute next in line 289 | this.executeQueue(); 290 | }).catch((err) => { 291 | this.runningQueries--; 292 | 293 | currentQuery.reject(err); 294 | 295 | this.abortQueue(new Error(`The transaction has been rolled back because an earlier query failed: ${err.message}`)); 296 | this.rollback(); 297 | }); 298 | } 299 | } else this.abortQueue(new Error('Cannot execute query, the transaction has already ended!')); 300 | } 301 | } 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | /** 311 | * return an error to all items in the queue 312 | */ 313 | , abortQueue: function(err) { 314 | if (this._queue) { 315 | this._queue.forEach((item) => { 316 | item.reject(err); 317 | }); 318 | 319 | this._queue = null; 320 | } 321 | } 322 | 323 | 324 | 325 | 326 | 327 | 328 | /** 329 | * indicates if the trnsaction has endedd 330 | */ 331 | , hasEnded: function() { 332 | return this._transaction === null; 333 | } 334 | 335 | 336 | 337 | 338 | 339 | 340 | , _getDatabase: function() { 341 | return this; 342 | } 343 | 344 | 345 | 346 | 347 | , _endTransaction: function() { 348 | this._transaction = null; 349 | this.abortQueue(new Error(`Cannot execute query, the transaction has ended!`)); 350 | this.emit('transactionEnd'); 351 | } 352 | }; 353 | 354 | 355 | 356 | 357 | 358 | // we need to proxy calls on the 359 | // models to give them the correct scope ... 360 | Object.keys(database._models).forEach(function(modelName) { 361 | 362 | // dynamically bind the context 363 | classDefinition[modelName] = {get: function() { 364 | var transactionContext = this; 365 | 366 | var ModelConstructor = function(options, relatingSets) { 367 | if (this instanceof ModelConstructor) { 368 | return new database[modelName](transactionContext, options, relatingSets); 369 | } 370 | else { 371 | return database[modelName].apply(transactionContext, Array.prototype.slice.call(arguments)); 372 | } 373 | }; 374 | 375 | // apply stuff from the constructor to the new construcotr 376 | Object.keys(database[modelName]).forEach(function(propertyName) { 377 | ModelConstructor[propertyName] = database[modelName][propertyName]; 378 | }); 379 | 380 | return ModelConstructor; 381 | }}; 382 | }); 383 | 384 | return new Class(classDefinition); 385 | } 386 | })(); 387 | -------------------------------------------------------------------------------- /lib/ModelBuilder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , EventEmitter = require('ee-event-emitter') 6 | , type = require('ee-types') 7 | , log = require('ee-log') 8 | , argv = require('ee-argv') 9 | , RelatingSet = require('./RelatingSet') 10 | , debug = argv.has('dev-orm'); 11 | 12 | 13 | 14 | 15 | module.exports = new Class({ 16 | 17 | init: function(options) { 18 | // the model to inherit from 19 | this.baseModel = options.baseModel; 20 | 21 | // the orm 22 | this.orm = options.orm; 23 | 24 | // the model definition 25 | this.definition = options.definition; 26 | 27 | // access to the current database 28 | this.getDatabase = options.getDatabase; 29 | 30 | // the current db name 31 | this.databaseName = this.definition.getDatabaseName(); 32 | this.databaseAliasName = this.definition.getDatabaseAliasName(); 33 | 34 | // configurations 35 | // maps of mappings, references and belonsto 36 | this.mappingMap = options.mappingMap; 37 | this.belongsToMap = options.belongsToMap; 38 | this.referenceMap = options.referenceMap; 39 | 40 | // columns 41 | this.columns = options.columns; 42 | 43 | // generic accessors (double used names) 44 | this._genericAccessors = options.genericAccessors; 45 | 46 | // extensions registered with the orm 47 | this._extensions = options.extensions; 48 | 49 | // create the model 50 | this.Model = this.build(); 51 | 52 | return this.Model; 53 | } 54 | 55 | 56 | 57 | /* 58 | * the build method builds the classDefinition required for the model 59 | */ 60 | , build: function() { 61 | var classDefinition; 62 | 63 | 64 | classDefinition = { 65 | inherits: this.baseModel 66 | 67 | , _defintion: this.definition 68 | 69 | , init: function init(options) { 70 | if (init.super) init.super.call(this, options); 71 | } 72 | 73 | , _genericAccessors: Class(this._genericAccessors) 74 | 75 | , _extensionEventListeners: Class({}) 76 | 77 | , _jsonColumns: Class({}).Writable() 78 | , _jsonColumnsArray: Class([]) 79 | }; 80 | 81 | 82 | // define columns for the current class 83 | this.addColumnAccessors(classDefinition); 84 | 85 | 86 | // apply extensions 87 | this.applyExtensions(classDefinition); 88 | 89 | // return class 90 | return new Class(classDefinition); 91 | } 92 | 93 | 94 | /* 95 | * gets all extensions that should be used on this model and applies them 96 | * to this model 97 | * 98 | * @param class deinfition 99 | */ 100 | , applyExtensions: function(classDefinition) { 101 | var extensions = this._extensions.getModelExtensions(this.definition); 102 | 103 | if (debug) log.info('Found %s extension(s) for the «%s» model ..', extensions.length, this.definition.tableName); 104 | 105 | extensions.forEach(function(extension) { 106 | // collect mehotds 107 | if (type.function(extension.applyModelMethods)) extension.applyModelMethods(this.definition, classDefinition); 108 | 109 | // check for event listeners 110 | extension.getModelEventListeners().forEach(function(listener) { 111 | if (debug) log.debug('Registering «%s» extension event on the «%s» model ...', listener.event, this.definition.tableName); 112 | 113 | if (!classDefinition._extensionEventListeners[listener.event]) classDefinition._extensionEventListeners[listener.event] = []; 114 | classDefinition._extensionEventListeners[listener.event].push(listener.listener); 115 | }.bind(this)); 116 | }.bind(this)); 117 | } 118 | 119 | 120 | /* 121 | * the addColumnAccessors method adds the colum accessors to the class 122 | * 123 | * @param class deinfition 124 | */ 125 | , addColumnAccessors: function(classDefinition) { 126 | Object.keys(this.definition.columns).forEach(function(columnName){ 127 | var column = this.definition.columns[columnName]; 128 | 129 | // add mapping accesors 130 | this.addColumnMappings(classDefinition, column); 131 | 132 | // add belongsto accessors 133 | this.addColumnBelonging(classDefinition, column); 134 | 135 | // add column (reference) accessors 136 | this.addColumn(classDefinition, column); 137 | 138 | // th eclass needs to know what a column represents 139 | classDefinition._columns = Class(this.columns); 140 | }.bind(this)); 141 | 142 | // generic resource access 143 | this.addGenericAccessors(classDefinition); 144 | } 145 | 146 | 147 | 148 | 149 | 150 | , addGenericAccessors: function(classDefinition) { 151 | var thisContext = this; 152 | 153 | // generic accessor method for mappings 154 | classDefinition.getMapping = { 155 | value: function(mappingName) { 156 | if (thisContext.mappingMap[mappingName]) { 157 | var mappingId = thisContext.mappingMap[mappingName].name; 158 | 159 | // create referencing set only when used 160 | if (!this._mappings[mappingId]) { 161 | this._mappings[mappingId] = new RelatingSet({ 162 | orm: thisContext.orm 163 | , definition: thisContext.mappingMap[mappingName].definition 164 | , column: thisContext.mappingMap[mappingName].column 165 | , related: this 166 | , database: thisContext.databaseName 167 | , databaseAlias: thisContext.databaseAliasName 168 | , getDatabase: thisContext.getDatabase 169 | , isMapping: true 170 | }); 171 | 172 | // possible memory leak, somehow? and its not used since we always call save 173 | // on the collection 174 | //this._mappings[mappingId].on('change', this._setChanged.bind(this)); 175 | } 176 | 177 | return this._mappings[mappingId]; 178 | } 179 | else throw new Error('Mapping via «'+mappingName+'» on entity «'+thisContext.definition.name+'» doesn\'t exist!'); 180 | } 181 | }; 182 | 183 | // generic accessor method for belongTo 184 | classDefinition.getBelongsTo = { 185 | value: function(belongingName){ 186 | if (thisContext.belongsToMap[belongingName]) { 187 | var belongsToId = thisContext.belongsToMap[belongingName].name; 188 | 189 | // create referencing set only when used 190 | if (!this._belongsTo[belongsToId]) { 191 | this._belongsTo[belongsToId] = new RelatingSet({ 192 | orm: thisContext.orm 193 | , definition: thisContext.belongsToMap[belongingName].definition 194 | , column: thisContext.belongsToMap[belongingName].column 195 | , related: this 196 | , database: thisContext.databaseName 197 | , databaseAlias: thisContext.databaseAliasName 198 | , getDatabase: thisContext.getDatabase 199 | , isMapping: false 200 | }); 201 | 202 | // possible memory leak, somehow? and its not used since we always call save 203 | // on the collection 204 | //this._belongsTo[belongsToId].on('change', this._setChanged.bind(this)); 205 | } 206 | 207 | return this._belongsTo[belongsToId]; 208 | } 209 | else throw new Error('Belongs to «'+belongingName+'» on entity «'+thisContext.definition.name+'» doesn\'t exist!'); 210 | } 211 | }; 212 | 213 | 214 | // generic accessor method for reference 215 | classDefinition.getReference = { 216 | value: function(referenceName){ 217 | if (thisContext.referenceMap[referenceName]) { 218 | return this._references[referenceName]; 219 | } 220 | else throw new Error('Reference on «'+referenceName+'» on entity «'+thisContext.definition.name+'» doesn\'t exist!'); 221 | } 222 | }; 223 | 224 | classDefinition.setReference = { 225 | value: function(referenceName, newReferenceModel, existing){ 226 | if (thisContext.referenceMap[referenceName]) { 227 | if (!existing && this._references[referenceName] !== newReferenceModel) { 228 | this._changedReferences.push(referenceName); 229 | this._references[referenceName] = newReferenceModel; 230 | this._setChanged(); 231 | } 232 | } 233 | else throw new Error('Reference on «'+referenceName+'» on entity «'+thisContext.definition.name+'» doesn\'t exist!'); 234 | } 235 | }; 236 | } 237 | 238 | 239 | 240 | /* 241 | * the addColumn method adds the colum belongs to accessors to the class 242 | * 243 | * @param class definition 244 | * @param column definition 245 | */ 246 | , addColumn: function(classDefinition, column) { 247 | var definition = {} 248 | , columnName = column.name 249 | , referenceName 250 | , referenceDefinition; 251 | 252 | 253 | if (column.referencedModel) { 254 | this.referenceMap[columnName] = column; 255 | 256 | if (!column.useGenericAccessor) { 257 | referenceName = column.aliasName || column.referencedModel.name; 258 | 259 | this.columns[referenceName] = { 260 | type : 'reference' 261 | , column : column 262 | , definition : column.referencedModel 263 | }; 264 | 265 | referenceDefinition = {enumerable: true}; 266 | 267 | 268 | referenceDefinition.get = function(){ 269 | return this._references[referenceName]; 270 | }; 271 | 272 | referenceDefinition.set = function(newValue) { 273 | if (this._references[referenceName] !== newValue) { 274 | this._changedReferences.push(referenceName); 275 | this._changedValues.push(columnName); 276 | this._references[referenceName] = newValue; 277 | this._setChanged(); 278 | } 279 | }; 280 | 281 | classDefinition[referenceName] = referenceDefinition; 282 | } 283 | else if (column.useGenericAccessor) this.storeGenericAccessor(columnName, this.referenceMap[columnName]); 284 | } 285 | else { 286 | definition.enumerable = true; 287 | this.columns[columnName] = { 288 | type : 'scalar' 289 | , column : column 290 | }; 291 | } 292 | 293 | definition.get = function() { 294 | return this._values[columnName]; 295 | }; 296 | 297 | definition.set = function(value) { 298 | value = this._prepareTypedInput(columnName, value); 299 | 300 | // check if there was changed anything 301 | if (this._values[columnName] !== value) { 302 | this._changedValues.push(columnName); 303 | this._values[columnName] = value; 304 | this._setChanged(); 305 | } 306 | }; 307 | 308 | 309 | // we need this in order to be able to check if there were 310 | // change son json columns 311 | if (column.nativeType === 'json') { 312 | classDefinition._jsonColumns.value[columnName] = null; 313 | classDefinition._jsonColumnsArray.value.push(columnName); 314 | } 315 | 316 | 317 | classDefinition[column.name] = definition; 318 | } 319 | 320 | 321 | 322 | /* 323 | * the addColumnBelonging method adds the colum belongs to accessors to the class 324 | * 325 | * @param class definition 326 | * @param column definition 327 | */ 328 | , addColumnBelonging: function(classDefinition, column) { 329 | var thisContext = this; 330 | 331 | if (column.belongsTo){ 332 | column.belongsTo.filter(function(belongsTo){ 333 | 334 | // store information about all mappings 335 | this.belongsToMap[belongsTo.model.name] = { 336 | column : column 337 | , definition : belongsTo 338 | , name : belongsTo.aliasName || belongsTo.model.name 339 | }; 340 | 341 | // store info about generic accessors 342 | if (belongsTo.useGenericAccessor) this.storeGenericAccessor(belongsTo.name, this.belongsToMap[belongsTo.model.name]); 343 | 344 | return !belongsTo.useGenericAccessor; 345 | }.bind(this)).forEach(function(belongsTo) { 346 | var relationName = belongsTo.aliasName || belongsTo.model.name; 347 | 348 | this.columns[relationName] = { 349 | type : 'belongsTo' 350 | , column : column 351 | , definition : belongsTo 352 | }; 353 | 354 | // getter 355 | classDefinition[relationName] = { 356 | enumerable: true 357 | , get: function() { 358 | // create referencing set only when used 359 | if (!this._belongsTo[relationName]) { 360 | this._belongsTo[relationName] = new RelatingSet({ 361 | orm: thisContext.orm 362 | , definition: belongsTo 363 | , column: column 364 | , related: this 365 | , database: thisContext.databaseName 366 | , databaseAlias: thisContext.databaseAliasName 367 | , isMapping: false 368 | }); 369 | } 370 | 371 | return this._belongsTo[relationName]; 372 | } 373 | }; 374 | }.bind(this)); 375 | } 376 | } 377 | 378 | 379 | 380 | /* 381 | * the addColumnMappings method adds the colum mapping accessors to the class 382 | * 383 | * @param class definition 384 | * @param column definition 385 | */ 386 | , addColumnMappings: function(classDefinition, column) { 387 | var thisContext = this; 388 | 389 | if (column.mapsTo) { 390 | column.mapsTo.filter(function(mapping) { 391 | 392 | // store information about all mappings 393 | this.mappingMap[mapping.via.model.name] = { 394 | column : column 395 | , definition : mapping 396 | , name : mapping.aliasName || mapping.model.name 397 | }; 398 | 399 | // store info about generic accessors 400 | if (mapping.useGenericAccessor) this.storeGenericAccessor(mapping.name, this.mappingMap[mapping.via.model.name]); 401 | 402 | return !mapping.useGenericAccessor; 403 | }.bind(this)).forEach(function(mapping) { 404 | var mappingName = mapping.aliasName || mapping.model.name; 405 | 406 | this.columns[mappingName] = { 407 | type : 'mapping' 408 | , column : column 409 | , definition : mapping 410 | }; 411 | 412 | // getter 413 | classDefinition[mappingName] = { 414 | enumerable: true 415 | , get: function() { 416 | // create referencing set only when used 417 | if (!this._mappings[mappingName]) { 418 | this._mappings[mappingName] = new RelatingSet({ 419 | orm: thisContext.orm 420 | , definition: mapping 421 | , column: column 422 | , related: this 423 | , database: thisContext.databaseName 424 | , databaseAlias: thisContext.databaseAliasName 425 | , isMapping: true 426 | , getDatabase: thisContext.getDatabase 427 | }); 428 | } 429 | 430 | return this._mappings[mappingName]; 431 | } 432 | }; 433 | }.bind(this)); 434 | } 435 | } 436 | 437 | 438 | 439 | /* 440 | * the storeGenericAccessor method stores which columns are using a generic accessp 441 | * 442 | * @param colum name 443 | * @param a description of the relation of the column 444 | * 445 | */ 446 | , storeGenericAccessor: function(targetModelName, config){ 447 | if (!this._genericAccessors[targetModelName]) this._genericAccessors[targetModelName] = []; 448 | this._genericAccessors[targetModelName].push(config); 449 | } 450 | }); 451 | })(); 452 | -------------------------------------------------------------------------------- /lib/QueryBuilderBuilder.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var Class = require('ee-class') 5 | , log = require('ee-log') 6 | , argv = require('ee-argv') 7 | , type = require('ee-types') 8 | , QueryBuilder = require('./QueryBuilder') 9 | , Resource = require('./Resource') 10 | , debug = argv.has('dev-orm'); 11 | 12 | 13 | 14 | 15 | module.exports = new Class({ 16 | 17 | init: function(options){ 18 | this.queryBuilders = options.queryBuilders; 19 | this.definition = options.definition; 20 | this.orm = options.orm; 21 | this.getDatabase = options.getDatabase; 22 | this.mappingMap = options.mappingMap; 23 | this.referenceMap = options.referenceMap; 24 | this.belongsToMap = options.belongsToMap; 25 | this.columns = options.columns; 26 | this._extensions = options.extensions; 27 | 28 | this.accessorMap = {get:{}, fetch: {}, join: {}, leftJoin: {}}; 29 | this.description = {}; 30 | 31 | // create a custom querybuilder class 32 | this.QueryBuilder = this.build(); 33 | 34 | return this.QueryBuilder; 35 | } 36 | 37 | 38 | 39 | 40 | 41 | , build: function() { 42 | var classDefinition = {} 43 | , thisContext = this 44 | , CustomResource 45 | , QueryBuilderConstructor; 46 | 47 | // the class inherits from the querybuilder class 48 | classDefinition.inherits = QueryBuilder; 49 | 50 | // eventlisteners on the querybuilder, used by extensions 51 | classDefinition._extensionEventListeners = {}; 52 | 53 | // required objects on each querybuilder, shared between all of the instances 54 | classDefinition._queryBuilders = this.queryBuilders; 55 | classDefinition._definition = this.definition; 56 | classDefinition.definition = this.definition; 57 | classDefinition._orm = this.orm; 58 | //classDefinition._getDatabase = this.getDatabase; 59 | 60 | this.addMappingMethods(classDefinition); 61 | this.addBelongsToMethods(classDefinition); 62 | this.addReferenceMethods(classDefinition); 63 | 64 | this.addGenericMethods(classDefinition); 65 | 66 | classDefinition.describeMethods = function(){ 67 | log(thisContext.description); 68 | return this; 69 | }; 70 | 71 | // apply extensions 72 | this.applyQueryBuilderExtensions(classDefinition); 73 | 74 | // apply the custom resource 75 | CustomResource = this.applyResourceExtensions(classDefinition); 76 | classDefinition.Resource = CustomResource; 77 | 78 | QueryBuilderConstructor = new Class(classDefinition); 79 | QueryBuilderConstructor.Resource = CustomResource; 80 | QueryBuilderConstructor.describeMethods = classDefinition.describeMethods; 81 | 82 | return QueryBuilderConstructor; 83 | } 84 | 85 | 86 | 87 | /** 88 | * gets all extensions that should be used on this model and applies them 89 | * to this querybuilder 90 | * 91 | * @param class definition 92 | */ 93 | , applyQueryBuilderExtensions: function(classDefinition) { 94 | var extensions = this._extensions.getModelExtensions(this.definition); 95 | 96 | if (debug) log.info('Found %s extension(s) for the «%s» model ..', extensions.length, this.definition.tableName); 97 | 98 | extensions.forEach(function(extension) { 99 | // collect mehotds 100 | if (type.function(extension.applyQueryBuilderMethods)) extension.applyQueryBuilderMethods(this.definition, classDefinition); 101 | 102 | // check for event listeners 103 | extension.getQueryBuilderEventListeners().forEach(function(listener) { 104 | if (debug) log.debug('Registering «%s» extension event on the «%s» model ...', listener.event, this.definition.tableName); 105 | 106 | if (!classDefinition._extensionEventListeners[listener.event]) classDefinition._extensionEventListeners[listener.event] = []; 107 | classDefinition._extensionEventListeners[listener.event].push(listener.listener); 108 | }.bind(this)); 109 | }.bind(this)); 110 | } 111 | 112 | 113 | 114 | 115 | /** 116 | * gets all extensions that should be used on this model and applies them 117 | * to this resource 118 | * 119 | * @param class definition 120 | */ 121 | , applyResourceExtensions: function() { 122 | var extensions = this._extensions.getModelExtensions(this.definition) 123 | , CustomResource; 124 | 125 | // we're creating customized version of the resource 126 | CustomResource = { 127 | inherits: Resource 128 | 129 | , _extensionEventListeners: {} 130 | }; 131 | 132 | if (debug) log.info('Found %s extension(s) for the «%s» model ..', extensions.length, this.definition.tableName); 133 | 134 | extensions.forEach(function(extension) { 135 | // collect mehotds 136 | if (type.function(extension.applyResourceMethods)) extension.applyResourceMethods(this.definition, CustomResource); 137 | 138 | // check for event listeners 139 | extension.getResourceEventListeners().forEach(function(listener) { 140 | if (debug) log.debug('Registering «%s» extension event on the «%s» model ...', listener.event, this.definition.tableName); 141 | 142 | if (!CustomResource._extensionEventListeners[listener.event]) CustomResource._extensionEventListeners[listener.event] = []; 143 | CustomResource._extensionEventListeners[listener.event].push(listener.listener); 144 | }.bind(this)); 145 | }.bind(this)); 146 | 147 | // store on querybuilder 148 | return new Class(CustomResource); 149 | } 150 | 151 | 152 | 153 | 154 | 155 | , addMappingMethods: function(classDefinition) { 156 | Object.keys(this.columns).filter(function(id){ 157 | return this.columns[id].type === 'mapping'; 158 | }.bind(this)).forEach(function(id) { 159 | var target = this.columns[id].definition 160 | , column = this.columns[id].column 161 | , getName = this.getAccessorName(id) 162 | , fetchName = this.getAccessorName(id, 1) 163 | , joinName = this.getAccessorName(id, 2) 164 | , leftJoinName = this.getAccessorName(id, 3); 165 | 166 | this.accessorMap.get[id] = function() { 167 | return this._handleMapping(column, target, Array.prototype.slice.call(arguments), true); 168 | }; 169 | this.accessorMap.fetch[id] = function() { 170 | return this._handleMapping(column, target, Array.prototype.slice.call(arguments)); 171 | }; 172 | this.accessorMap.join[id] = function(returnTarget) { 173 | return this._handleMapping(column, target, null, returnTarget, true); 174 | }; 175 | this.accessorMap.leftJoin[id] = function(returnTarget) { 176 | return this._handleMapping(column, target, null, returnTarget, true, true); 177 | }; 178 | 179 | this.description[getName] = 'Mapping accessor for the «'+id+'» Model'; 180 | this.description[fetchName] = 'Mapping accessor for the «'+id+'» Model'; 181 | this.description[joinName] = 'Mapping accessor for the «'+id+'» Model'; 182 | this.description[leftJoinName] = 'Mapping accessor for the «'+id+'» Model'; 183 | 184 | classDefinition[getName] = Class(this.accessorMap.get[id]).Enumerable(); 185 | classDefinition[fetchName] = Class(this.accessorMap.fetch[id]).Enumerable(); 186 | classDefinition[joinName] = Class(this.accessorMap.join[id]).Enumerable(); 187 | classDefinition[leftJoinName] = Class(this.accessorMap.leftJoin[id]).Enumerable(); 188 | }.bind(this)); 189 | } 190 | 191 | 192 | 193 | , addBelongsToMethods: function(classDefinition) { 194 | Object.keys(this.columns).filter(function(id){ 195 | return this.columns[id].type === 'belongsTo'; 196 | }.bind(this)).forEach(function(id) { 197 | var target = this.columns[id].definition 198 | , column = this.columns[id].column 199 | , getName = this.getAccessorName(id) 200 | , fetchName = this.getAccessorName(id, 1) 201 | , joinName = this.getAccessorName(id, 2) 202 | , leftJoinName = this.getAccessorName(id, 3); 203 | 204 | this.accessorMap.get[id] = function() { 205 | return this._handleBelongsTo(column, target, Array.prototype.slice.call(arguments), true); 206 | }; 207 | this.accessorMap.fetch[id] = function() { 208 | return this._handleBelongsTo(column, target, Array.prototype.slice.call(arguments)); 209 | }; 210 | this.accessorMap.join[id] = function(returnTarget) { 211 | return this._handleBelongsTo(column, target, null, returnTarget, true); 212 | }; 213 | this.accessorMap.leftJoin[id] = function(returnTarget) { 214 | return this._handleBelongsTo(column, target, null, returnTarget, true, true); 215 | }; 216 | 217 | 218 | if (target.aliasName) { 219 | this.accessorMap.get[target.aliasName] = this.accessorMap.get[id]; 220 | this.accessorMap.fetch[target.aliasName] = this.accessorMap.fetch[id]; 221 | this.accessorMap.join[target.aliasName] = this.accessorMap.join[id]; 222 | this.accessorMap.leftJoin[target.aliasName] = this.accessorMap.leftJoin[id]; 223 | } 224 | 225 | 226 | this.description[getName] = 'Belongs to accessor for the «'+id+'» Model'; 227 | this.description[fetchName] = 'Belongs to accessor for the «'+id+'» Model'; 228 | this.description[joinName] = 'Belongs to accessor for the «'+id+'» Model'; 229 | this.description[leftJoinName] = 'Belongs to accessor for the «'+id+'» Model'; 230 | 231 | classDefinition[getName] = Class(this.accessorMap.get[id]).Enumerable(); 232 | classDefinition[fetchName] = Class(this.accessorMap.fetch[id]).Enumerable(); 233 | classDefinition[joinName] = Class(this.accessorMap.join[id]).Enumerable(); 234 | classDefinition[leftJoinName] = Class(this.accessorMap.leftJoin[id]).Enumerable(); 235 | }.bind(this)); 236 | } 237 | 238 | 239 | 240 | , addReferenceMethods: function(classDefinition) { 241 | Object.keys(this.columns).filter(function(id){ 242 | return this.columns[id].type === 'reference'; 243 | }.bind(this)).forEach(function(id) { 244 | var target = this.columns[id].definition 245 | , column = this.columns[id].column 246 | , getName = this.getAccessorName(id) 247 | , fetchName = this.getAccessorName(id, 1) 248 | , joinName = this.getAccessorName(id, 2) 249 | , leftJoinName = this.getAccessorName(id, 3); 250 | 251 | 252 | this.accessorMap.get[id] = function() { 253 | return this._handleReference(column, target, Array.prototype.slice.call(arguments), true); 254 | }; 255 | this.accessorMap.fetch[id] = function() { 256 | return this._handleReference(column, target, Array.prototype.slice.call(arguments)); 257 | }; 258 | this.accessorMap.join[id] = function(returnTarget) { 259 | return this._handleReference(column, target, null, returnTarget, true); 260 | }; 261 | this.accessorMap.leftJoin[id] = function(returnTarget) { 262 | return this._handleReference(column, target, null, returnTarget, true, true); 263 | }; 264 | 265 | 266 | if (column.aliasName) { 267 | this.accessorMap.get[column.aliasName] = this.accessorMap.get[id]; 268 | this.accessorMap.fetch[column.aliasName] = this.accessorMap.fetch[id]; 269 | this.accessorMap.join[column.aliasName] = this.accessorMap.join[id]; 270 | this.accessorMap.leftJoin[column.aliasName] = this.accessorMap.leftJoin[id]; 271 | } 272 | 273 | this.description[getName] = 'Reference accessor for the «'+id+'» Model'; 274 | this.description[fetchName] = 'Reference accessor for the «'+id+'» Model'; 275 | this.description[joinName] = 'Reference accessor for the «'+id+'» Model'; 276 | this.description[leftJoinName] = 'Reference accessor for the «'+id+'» Model'; 277 | 278 | classDefinition[getName] = Class(this.accessorMap.get[id]).Enumerable(); 279 | classDefinition[fetchName] = Class(this.accessorMap.fetch[id]).Enumerable(); 280 | classDefinition[joinName] = Class(this.accessorMap.join[id]).Enumerable(); 281 | classDefinition[leftJoinName] = Class(this.accessorMap.leftJoin[id]).Enumerable(); 282 | }.bind(this)); 283 | } 284 | 285 | 286 | 287 | , addGenericMethods: function(classDefinition) { 288 | var accessorMap = this.accessorMap 289 | , mappingMap = this.mappingMap 290 | , referenceMap = this.referenceMap 291 | , belongsToMap = this.belongsToMap 292 | , definition = this.definition; 293 | 294 | 295 | // generic methods for gettin a target entity 296 | classDefinition.get = Class(function(targetName) { 297 | if (Object.hasOwnProperty.call(accessorMap.get, targetName)) { 298 | return accessorMap.get[targetName].apply(this, Array.prototype.slice.call(arguments, 1)); 299 | } else throw new Error('The QueryBuilder has no property «'+targetName+'»!'); 300 | }).Enumerable(); 301 | 302 | classDefinition.fetch = Class(function(targetName) { 303 | if (Object.hasOwnProperty.call(accessorMap.fetch, targetName)) { 304 | return accessorMap.fetch[targetName].apply(this, Array.prototype.slice.call(arguments, 1)); 305 | } else throw new Error('The QueryBuilder has no property «'+targetName+'»!'); 306 | }).Enumerable(); 307 | 308 | classDefinition.join = Class(function(targetName, returnTarget) { 309 | if (Object.hasOwnProperty.call(accessorMap.join, targetName)) { 310 | return accessorMap.join[targetName].apply(this, Array.prototype.slice.call(arguments, 1), !!returnTarget); 311 | } else throw new Error('The QueryBuilder has no property «'+targetName+'»!'); 312 | }).Enumerable(); 313 | 314 | classDefinition.leftJoin = Class(function(targetName, returnTarget) { 315 | if (Object.hasOwnProperty.call(accessorMap.leftJoin, targetName)) { 316 | return accessorMap.leftJoin[targetName].apply(this, Array.prototype.slice.call(arguments, 1), !!returnTarget); 317 | } else throw new Error('The QueryBuilder has no property «'+targetName+'»!'); 318 | }).Enumerable(); 319 | 320 | 321 | 322 | // generic method for checking if a given entity exists 323 | classDefinition.has = Class(function(targetName) { 324 | return Object.hasOwnProperty.call(accessorMap.get, targetName); 325 | }).Enumerable(); 326 | 327 | 328 | 329 | // generic method for mappings 330 | classDefinition.getMapping = Class(function(mappingTableName) { 331 | var info = mappingMap[mappingTableName]; 332 | if (info) return this._handleMapping(info.column, info.target, Array.prototype.slice.call(arguments, 1), true); 333 | else throw new Error('Unknown mapping «'+mappingTableName+'» on entity «'+definition.name+'»!'); 334 | }).Enumerable(); 335 | 336 | classDefinition.fetchMapping = Class(function(mappingTableName) { 337 | var info = mappingMap[mappingTableName]; 338 | if (info) return this._handleMapping(info.column, info.target, Array.prototype.slice.call(arguments, 1)); 339 | else throw new Error('Unknown mapping «'+mappingTableName+'» on entity «'+definition.name+'»!'); 340 | }).Enumerable(); 341 | 342 | 343 | // generic methods for references 344 | classDefinition.getReference = Class(function(referencedTable) { 345 | var info = referenceMap[referencedTable]; 346 | if (info) return this._handleReference(info.column, info.target, Array.prototype.slice.call(arguments, 1), true); 347 | else throw new Error('Unknown reference «'+referencedTable+'» on entity «'+definition.name+'»!'); 348 | }).Enumerable(); 349 | 350 | classDefinition.fetchReference = Class(function(referencedTable) { 351 | var info = referenceMap[referencedTable]; 352 | if (info) return this._handleReference(info.column, info.target, Array.prototype.slice.call(arguments, 1)); 353 | else throw new Error('Unknown reference «'+referencedTable+'» on entity «'+definition.name+'»!'); 354 | }).Enumerable(); 355 | 356 | 357 | // generic methods for belongs to 358 | classDefinition.getBelongsTo = Class(function(belongingTableName) { 359 | var info = belongsToMap[belongingTableName]; 360 | if (info) return this._handleBelongsTo(info.column, info.target, Array.prototype.slice.call(arguments, 1), true); 361 | else throw new Error('Unknown belongstTo «'+mappingTableName+'» on entity «'+definition.name+'»!'); 362 | }).Enumerable(); 363 | 364 | classDefinition.fetchBelongsTo = Class(function(belongingTableName) { 365 | var info = belongsToMap[belongingTableName]; 366 | if (info) return this._handleBelongsTo(info.column, info.target, Array.prototype.slice.call(arguments, 1)); 367 | else throw new Error('Unknown belongstTo «'+mappingTableName+'» on entity «'+definition.name+'»!'); 368 | }).Enumerable(); 369 | } 370 | 371 | 372 | 373 | , getAccessorName: function(id, method) { 374 | if (method === 1) return 'fetch' + id[0].toUpperCase()+id.slice(1); 375 | else if (method === 2) return 'join' + id[0].toUpperCase()+id.slice(1); 376 | else if (method === 3) return 'leftJoin' + id[0].toUpperCase()+id.slice(1); 377 | else return 'get' + id[0].toUpperCase()+id.slice(1); 378 | } 379 | }); 380 | })(); 381 | -------------------------------------------------------------------------------- /lib/RelatingSet.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | 5 | const Class = require('ee-class'); 6 | const type = require('ee-types'); 7 | const debug = require('ee-argv').has('dev-orm'); 8 | const Arguments = require('ee-arguments'); 9 | const log = require('ee-log'); 10 | const Lock = require('./Lock'); 11 | 12 | 13 | 14 | const proto = Array.prototype; 15 | 16 | 17 | 18 | 19 | module.exports = new Class({ 20 | inherits: Array 21 | 22 | // should this quer be debugged? 23 | , _debugMode: false 24 | 25 | 26 | 27 | , init: function(options) { 28 | Class.define(this, '_orm' , Class(options.orm)); 29 | Class.define(this, '_definition' , Class(options.definition)); 30 | Class.define(this, '_relatesTo' , Class(options.related)); 31 | Class.define(this, '_column' , Class(options.column)); 32 | Class.define(this, '_database' , Class(options.database)); 33 | Class.define(this, '_databaseAlias' , Class(options.databaseAlias)); 34 | Class.define(this, '_workerCount' , Class(0).Writable()); 35 | Class.define(this, '_originalRecords' , Class([]).Writable()); 36 | 37 | Class.define(this, 'isMapping' , Class(!!options.isMapping)); 38 | Class.define(this, '_queries' , Class([])); 39 | Class.define(this, '_pool' , Class(null).Writable().Configurable()); 40 | 41 | this.Model = this._orm[this._databaseAlias][this._definition.name]; 42 | } 43 | 44 | 45 | 46 | /** 47 | * enable the bug mode 48 | * 49 | * @param optional mode 50 | */ 51 | , setDebugMode: function(status) { 52 | this._debugMode = type.undefined(status) ? true : !!status; 53 | return this; 54 | } 55 | 56 | 57 | 58 | 59 | /** 60 | * sets the pool for all transactions executed on this model 61 | */ 62 | , setPool: function(pool) { 63 | this._pool = pool; 64 | } 65 | 66 | 67 | 68 | 69 | 70 | // save changes on the 71 | , save: {value: function(...args) { 72 | let callback, transaction, stack, lock; 73 | let dontReload = false; 74 | 75 | 76 | args.forEach((arg) => { 77 | if (type.function(arg)) callback = arg; 78 | else if (arg instanceof Lock) lock = arg; 79 | else if (type.boolean(arg)) dontReload = arg; 80 | else if (type.array(arg)) stack = arg; 81 | else if (type.object(arg)) transaction = arg; 82 | }); 83 | 84 | 85 | 86 | const promise = Promise.resolve().then(() => { 87 | if (stack) stack.push({entity: this._definition.name, frame: 'save: promise start'}); 88 | 89 | if (transaction) return this._save({transaction, dontReload, stack}); 90 | else { 91 | if (stack) stack.push({entity: this._definition.name, frame: 'save: create transaction'}); 92 | transaction = this.getDatabase().createTransaction(this._pool); 93 | 94 | return this._save({transaction, dontReload, stack, lock}).then(() => { 95 | return transaction.commit(); 96 | }).catch((err) => { 97 | return transaction.rollback().then(() => { 98 | return Promise.reject(err); 99 | }).catch(() => { 100 | return Promise.reject(err); 101 | }); 102 | }); 103 | } 104 | }); 105 | 106 | 107 | if (type.function(callback)) promise.then(() => callback(null, this)).catch(callback); 108 | else return promise.then(() => Promise.resolve(this)); 109 | }} 110 | 111 | 112 | 113 | 114 | 115 | // save changes on the 116 | , _save: {value: function({transaction, dontReload, stack, lock}) { 117 | if (stack) stack.push({entity: this._definition.name, frame: '_save: before _addQueryItems'}); 118 | 119 | // get items stored as query 120 | return this._addQueryItems(transaction).then(() => { 121 | if (stack) stack.push({entity: this._definition.name, frame: '_save: after _addQueryItems'}); 122 | 123 | return Promise.all(this.map((item) => { 124 | if (!item.isFromDatabase() || !item.isSaved()) { 125 | 126 | // we have to save this item before we can proceed 127 | // if we're a belongs to set we need to set our id on the item 128 | if (!this.isMapping) { 129 | item[this._definition.targetColumn] = this._relatesTo[this._column.name]; 130 | } 131 | 132 | item.setDebugMode(this._debugMode); 133 | item.setPool(this._pool); 134 | 135 | return item.save(transaction, dontReload, stack, lock); 136 | } else return Promise.resolve(); 137 | })).then(() => { 138 | if (stack) stack.push({entity: this._definition.name, frame: '_save: all saved'}); 139 | 140 | // get diffs 141 | const {addedRecords, removedRecords} = this._getChangedRecords(); 142 | 143 | // create / remove relational records 144 | return Promise.all([ 145 | this._deleteRelationRecords({removedRecords, transaction, dontReload, stack, lock}), 146 | this._createRelationRecords({addedRecords, transaction, dontReload, stack, lock}) 147 | ]); 148 | }); 149 | }); 150 | }} 151 | 152 | 153 | 154 | 155 | /* 156 | * execute queries, add the items 157 | */ 158 | , _addQueryItems: function(transaction) { 159 | return Promise.all(this._queries.map((config) => { 160 | config.query.setDebugMode(this._debugMode); 161 | config.query.pool(this._pool || 'read'); 162 | 163 | return config.query.find(transaction).then((set) => { 164 | if(!set || !set.length) return Promise.resolve(); 165 | else { 166 | try { 167 | if (config.mode === 'splice') this.splice.apply(this, [config.position, 0].concat(proto.slice.call(set, 0))); 168 | else { 169 | set.forEach(function(item) { 170 | this[config.mode](item); 171 | }.bind(this)); 172 | } 173 | } catch (err) { 174 | if (err) return Promise.reject(err); 175 | } 176 | 177 | Promise.resolve(); 178 | } 179 | }); 180 | })); 181 | } 182 | 183 | 184 | 185 | , _getChangedRecords: {value: function() { 186 | const removedRecords= []; 187 | const addedRecords = []; 188 | const originalMap = this._createMap(this._originalRecords); 189 | const currentMap = this._createMap(this); 190 | 191 | // adde items 192 | Object.keys(currentMap).forEach((newItemKey) => { 193 | if (!originalMap[newItemKey]) { 194 | // new item 195 | addedRecords.push(currentMap[newItemKey]); 196 | } 197 | }); 198 | 199 | // removed items 200 | Object.keys(originalMap).forEach((oldItemKey) => { 201 | if (!currentMap[oldItemKey]) { 202 | // new item 203 | removedRecords.push(originalMap[oldItemKey]); 204 | } 205 | }); 206 | 207 | return {addedRecords, removedRecords}; 208 | }} 209 | 210 | 211 | 212 | 213 | 214 | , _createRelationRecords: { value: function({addedRecords, transaction, dontReload, stack, lock}) { 215 | if (stack) stack.push({entity: this._definition.name, frame: '_createRelationRecords: start'}); 216 | 217 | return Promise.all(addedRecords.map((record) => { 218 | if (this.isMapping) { 219 | const values = {}; 220 | 221 | values[this._definition.via.fk] = this._relatesTo[this._column.name]; 222 | values[this._definition.via.otherFk] = record[this._definition.column.name]; 223 | 224 | // add to original records 225 | this._originalRecordsPush(record); 226 | 227 | 228 | // remove them if saving failed! 229 | transaction.once('rollback', function() { 230 | var index = this._originalRecords.indexOf(record); 231 | if (index >= 0) this._originalRecords.splice(index, 1); 232 | }.bind(this)); 233 | 234 | if (stack) stack.push({entity: this._definition.name, frame: '_createRelationRecords: before new Model().save()', values: values}); 235 | return new this._orm[this._definition.model.getDatabaseName()][this._definition.via.model.name](values).setDebugMode(this._debugMode).save(transaction, stack, dontReload, lock).then(() => { 236 | if (stack) stack.push({entity: this._definition.name, frame: '_createRelationRecords: after new Model().save()', values: values}); 237 | return Promise.resolve(); 238 | }); 239 | } else return Promise.resolve(); 240 | })); 241 | }} 242 | 243 | 244 | 245 | , _deleteRelationRecords: {value: function({removedRecords, transaction, dontReload, stack}) { 246 | if (stack) stack.push({entity: this._definition.name, frame: '_deleteRelationRecords: start'}); 247 | 248 | return Promise.all(removedRecords.map((record) => { 249 | if (this.isMapping) { 250 | // mapping, notexplicitly selected 251 | const values = {}; 252 | 253 | values[this._definition.via.fk] = this._relatesTo[this._column.name]; 254 | values[this._definition.via.otherFk] = record[this._definition.column.name]; 255 | 256 | 257 | // remove records when deletion was successfull 258 | transaction.once('commit', function(){ 259 | var index = this._originalRecords.indexOf(record); 260 | if (index >= 0) this._originalRecords.splice(index, 1); 261 | 262 | index = this.indexOf(record); 263 | if (index >= 0) proto.splice.call(this, index, 1); 264 | }.bind(this)); 265 | 266 | return transaction[this._definition.via.model.name](values).setDebugMode(this._debugMode).delete(); 267 | } else if (this._definition.model.isMapping) { 268 | // mapping, explicitly selected 269 | const values = {}; 270 | 271 | // get mapping columns 272 | this._definition.model.mappingColumns.forEach(function(columnName) { 273 | values[columnName] = record[columnName]; 274 | }.bind(this)); 275 | 276 | // records were removed before 277 | return transaction[this._definition.model.name](values).limit(1).setDebugMode(this._debugMode).delete(); 278 | } else return Promise.resolve(); 279 | })).then(() => { 280 | if (stack) stack.push({entity: this._definition.name, frame: '_deleteRelationRecords: done'}); 281 | return Promise.resolve(); 282 | }); 283 | }} 284 | 285 | 286 | 287 | 288 | 289 | , _createMap: {value: function(items){ 290 | var map = {} 291 | , primaryKeys = this._relatesTo.getDefinition().primaryKeys; 292 | 293 | items.forEach(function(item){ 294 | var compositeKey = ''; 295 | 296 | primaryKeys.forEach(function(key){ 297 | compositeKey += '|'+item[key]; 298 | }.bind(this), ''); 299 | 300 | map[compositeKey] = item; 301 | }); 302 | 303 | return map; 304 | }} 305 | 306 | 307 | 308 | 309 | // reload all records 310 | , reload: {value: function(...args) { 311 | let callback, transaction; 312 | 313 | 314 | args.forEach((arg) => { 315 | if (type.function(arg)) callback = arg; 316 | else if (type.object(arg)) transaction = arg; 317 | }); 318 | 319 | if (!transaction) transaction = this._relatesTo._getDatabase(); 320 | 321 | 322 | const promise = this._reload({transaction}); 323 | 324 | 325 | // if the user passed a callback, use it. 326 | // otherwise return the promise 327 | if (typeof callback !== 'function') return promise.then(() => Promise.resolve(this)); 328 | else promise.then(() => callback(null, this)).catch(callback); 329 | }} 330 | 331 | 332 | 333 | , _reload: {value: function({transaction}) { 334 | // not implemented :/ 335 | 336 | return Promise.resolve(); 337 | }} 338 | 339 | 340 | 341 | /* 342 | * the push() method accepts eiteher a quer yor a saved or not saved 343 | * model of the target entity. 344 | * 345 | * @param item: query or model 346 | * @param callback 347 | */ 348 | , push: { value: function push (item) { 349 | //this.emit('change'); 350 | 351 | if (type.object(item)) { 352 | // check if the item is a model or a query 353 | if (type.function(item.isQuery) && item.isQuery()) this._queries.push({mode:'push', query: item}); 354 | else { 355 | if (this._isCorrectType(item)) this._protoPush(item); 356 | else throw new Error('Cannot add models of type «'+(item && item.getEntityName ? item.getEntityName() : 'unknown')+'» to a relatingSet of type «'+this._definition.name+'»!'); 357 | } 358 | } 359 | else throw new Error('Cannot add item of type «'+type(item)+'» to a relatingSet of type «'+this._definition.name+'», expected model or query!'); 360 | 361 | return this.length; 362 | }} 363 | 364 | 365 | 366 | 367 | , splice: {value:function(index, howMany) { 368 | var removedItems = proto.splice.call(this, index, howMany) 369 | , item; 370 | 371 | //this.emit('change'); 372 | 373 | if (arguments.length > 2) { 374 | for (var i = 2, l = arguments.length; i< l; i++ ) { 375 | item = arguments[i]; 376 | 377 | if (type.object(item)) { 378 | // check if the item is a model or a query 379 | if (type.function(item.isQuery) && item.isQuery()) this._queries.push({mode:'splice', position: index, query: item}); 380 | else { 381 | if (this._isCorrectType(item)) proto.splice.call(this, index, 0, item); 382 | else throw new Error('Cannot add models of type «'+item.getEntityName()+'» to a relatingSet of type «'+this._definition.name+'»!'); 383 | } 384 | } 385 | else throw new Error('Cannot add item of type «'+type(item)+'» to a relatingSet of type «'+this._definition.name+'», expected model or query!'); 386 | } 387 | } 388 | 389 | return removedItems; 390 | }} 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | , clear: {value:function() { 400 | if (this.length) return proto.splice.call(this, 0, this.length); 401 | }} 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | , unshift: {value: function(item, callback){ 410 | //this.emit('change'); 411 | 412 | if (type.object(item)) { 413 | // check if the item is a model or a query 414 | if (type.function(item.isQuery) && item.isQuery()) this._queries.push({mode:'unshift', query: item}); 415 | else { 416 | if (this._isCorrectType(item)) proto.splice.unshift(this, item); 417 | else throw new Error('Cannot add models of type «'+item.getName()+'» to a relatingSet of type «'+this._definition.name+'»!'); 418 | } 419 | } 420 | else throw new Error('Cannot add item of type «'+type(item)+'» to a relatingSet of type «'+this._definition.name+'», expected model or query!'); 421 | 422 | return this.length; 423 | }} 424 | 425 | 426 | 427 | // internal only method for adding records which were already mappend to the collection 428 | , addExisiting: { value: function(item){ 429 | this._originalRecordsPush(item); 430 | this._protoPush(item); 431 | }} 432 | 433 | 434 | // add to original records 435 | , _originalRecordsPush: {value: function(item) { 436 | item.on('delete', function(){ 437 | var idx = this._originalRecords.indexOf(item); 438 | if (idx >= 0) this._originalRecords.splice(idx, 1); 439 | }.bind(this)); 440 | 441 | this._originalRecords.push(item); 442 | }} 443 | 444 | 445 | , _protoPush: { value: function(item){ 446 | item.on('delete', function(){ 447 | var idx = this.indexOf(item); 448 | if (idx >= 0) this.splice(idx, 1); 449 | }.bind(this)); 450 | 451 | proto.push.call(this, item); 452 | }} 453 | 454 | 455 | // check if a model is typeof this 456 | , _isCorrectType: { value: function(model){ 457 | return model && type.function(model.getEntityName) && model.getEntityName() === this._definition.model.name; 458 | }} 459 | 460 | 461 | 462 | 463 | , pop: { value: function pop () { 464 | pop.parent(); 465 | }} 466 | 467 | 468 | 469 | , shift: { value: function shift () { 470 | shift.parent(); 471 | }} 472 | 473 | 474 | 475 | // inheriting from the array type, have to implement event by myself 476 | , _events: {value: {}, writable: true} 477 | 478 | // on 479 | , on: {value: function(evt, listener){ 480 | if (!this._events[evt]) this._events[evt] = []; 481 | this._events[evt].push({fn: listener}); 482 | }} 483 | 484 | // once 485 | , once: {value: function(evt, listener){ 486 | if (!this._events[evt]) this._events[evt] = []; 487 | this._events[evt].push({fn: listener, once: true}); 488 | }} 489 | 490 | // emit 491 | , emit: {value: function(evt){ 492 | var rm = []; 493 | 494 | if (this._events[evt]) { 495 | this._events[evt].forEach(function(listener){ 496 | listener.fn.apply(null, Array.prototype.slice.call(arguments, 1)); 497 | if (listener.once) rm.push(listener); 498 | }); 499 | 500 | rm.forEach(function(listener){ 501 | this.off(evt, listener); 502 | }.bind(this)); 503 | } 504 | }} 505 | 506 | // off 507 | , off: {value: function(evt, listener){ 508 | var index; 509 | 510 | if (evt === undefined) this._events = {}; 511 | else if (evt) { 512 | if (listener === undefined) delete this._events[evt]; 513 | else if(this._events[evt]) { 514 | index = this._events[evt].indexOf(listener); 515 | if (index >= 0) this._events[evt].splice(index, 1); 516 | } 517 | } 518 | }} 519 | 520 | , dir: {value: function(returnResult) { 521 | var result = []; 522 | this.forEach(function(item){ 523 | result.push(item.dir(true)); 524 | }); 525 | 526 | if (returnResult) return result; 527 | else log(result); 528 | }} 529 | 530 | , toJSON: {value: function() { 531 | return this.map(function(item){ 532 | return item.toJSON ? item.toJSON() : undefined; 533 | }.bind(this)); 534 | }} 535 | }); 536 | })(); 537 | --------------------------------------------------------------------------------