├── test ├── .gitignore ├── migrator │ ├── custom.stub │ ├── cli.js │ ├── custom.migration │ ├── default.migration │ ├── main.js │ ├── testWithCustomStub.js │ └── testWithDefaultStub.js ├── orm │ ├── testRelationJoints.js │ ├── testManyToManyHelpers.js │ ├── testMap.js │ ├── testReduce.js │ ├── testUpdate.js │ ├── testCache.js │ ├── testDelete.js │ ├── testScopesAndJoints.js │ ├── testShape.js │ ├── testAutoIncrementIdTables.js │ ├── testInsert.js │ ├── main.js │ ├── testTableDefinitions.js │ └── testEagerLoads.js ├── kredis │ ├── testQueue.js │ ├── testHash.js │ ├── main.js │ └── testClient.js ├── config.sample.js └── collisions │ └── main.js ├── .gitignore ├── .npmignore ├── .babelrc ├── src ├── kredis │ ├── index.js │ ├── Queue.js │ ├── Hash.js │ └── Client.js ├── migration.stub ├── Scope.js ├── migrate.js ├── Track.js ├── relations │ ├── Relation.js │ ├── HasOne.js │ ├── HasMany.js │ ├── BelongsTo.js │ ├── MorphOne.js │ ├── MorphMany.js │ ├── HasManyThrough.js │ ├── MorphTo.js │ └── ManyToMany.js ├── Scoper.js ├── Shape.js ├── migrator.js └── Orm.js ├── lib ├── migration.stub ├── migrate.js ├── Scope.js ├── Track.js ├── migrator.js ├── Scoper.js ├── Shape.js └── relations │ ├── Relation.js │ ├── HasMany.js │ ├── HasOne.js │ ├── BelongsTo.js │ ├── MorphMany.js │ ├── MorphOne.js │ └── HasManyThrough.js ├── v3notes ├── readme.md ├── LICENSE └── package.json /test/.gitignore: -------------------------------------------------------------------------------- 1 | config.js 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | lib -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .babelrc 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /src/kredis/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./Client'); 2 | -------------------------------------------------------------------------------- /test/migrator/custom.stub: -------------------------------------------------------------------------------- 1 | exports.up = (knex) => { 2 | 3 | }; 4 | 5 | exports.down = (knex) => { 6 | 7 | }; 8 | -------------------------------------------------------------------------------- /lib/migration.stub: -------------------------------------------------------------------------------- 1 | function up(knex) { 2 | 3 | } 4 | 5 | function down(knex) { 6 | 7 | } 8 | 9 | module.exports = {up, down}; 10 | -------------------------------------------------------------------------------- /src/migration.stub: -------------------------------------------------------------------------------- 1 | function up(knex) { 2 | 3 | } 4 | 5 | function down(knex) { 6 | 7 | } 8 | 9 | module.exports = {up, down}; 10 | -------------------------------------------------------------------------------- /test/migrator/cli.js: -------------------------------------------------------------------------------- 1 | const config = require('../config'); 2 | 3 | const migrate = require('../../src/migrate'); 4 | 5 | migrate(config); 6 | -------------------------------------------------------------------------------- /test/orm/testRelationJoints.js: -------------------------------------------------------------------------------- 1 | function testRelationJoints(test, orm) { 2 | return Promise.resolve(); 3 | } 4 | 5 | module.exports = testRelationJoints; 6 | -------------------------------------------------------------------------------- /test/orm/testManyToManyHelpers.js: -------------------------------------------------------------------------------- 1 | function testManyToManyHelpers(test, orm) { 2 | return Promise.resolve(); 3 | } 4 | 5 | module.exports = testManyToManyHelpers; 6 | -------------------------------------------------------------------------------- /v3notes: -------------------------------------------------------------------------------- 1 | 1. using rdisdsl instead of kredis 2 | 2. `autoId` table definition flag changed to `uuid` 3 | 3. Added mysql support with `uuid` flag `true` 4 | 4. Added mysql support with `increments` flag true -------------------------------------------------------------------------------- /test/migrator/custom.migration: -------------------------------------------------------------------------------- 1 | function up(knex) { 2 | return knex.schema.createTable('test_custom', (t) => { 3 | t.uuid('id').primary(); 4 | }); 5 | } 6 | 7 | function down(knex) { 8 | return knex.schema.dropTable('test_custom'); 9 | } 10 | 11 | module.exports = {up, down}; 12 | -------------------------------------------------------------------------------- /test/migrator/default.migration: -------------------------------------------------------------------------------- 1 | function up(knex) { 2 | return knex.schema.createTable('test_default', (t) => { 3 | t.uuid('id').primary(); 4 | }); 5 | } 6 | 7 | function down(knex) { 8 | return knex.schema.dropTable('test_default'); 9 | } 10 | 11 | module.exports = {up, down}; 12 | -------------------------------------------------------------------------------- /src/Scope.js: -------------------------------------------------------------------------------- 1 | class Scope { 2 | constructor(closure, label, isJoint=false) { 3 | this.closure = closure; 4 | this.label = label; 5 | this.isJoint = !!isJoint; 6 | } 7 | 8 | apply(q) { 9 | this.closure(q); 10 | return q; 11 | } 12 | } 13 | 14 | module.exports = Scope; 15 | -------------------------------------------------------------------------------- /test/orm/testMap.js: -------------------------------------------------------------------------------- 1 | const {isString} = require('lodash'); 2 | 3 | function testMap(assert, orm) { 4 | console.log('testing map'); 5 | 6 | const {table} = orm.exports; 7 | 8 | return table('posts').map((p) => p.title).all().then((titles) => { 9 | console.log(titles); 10 | assert.ok(titles.reduce((areValid, t) => areValid && isString(t), true)); 11 | }); 12 | } 13 | 14 | module.exports = testMap; 15 | -------------------------------------------------------------------------------- /test/orm/testReduce.js: -------------------------------------------------------------------------------- 1 | const {isString} = require('lodash'); 2 | 3 | function testReduce(assert, orm) { 4 | console.log('testing reduce'); 5 | 6 | const {table} = orm.exports; 7 | 8 | return table('posts').reduce((str, p) => { 9 | return `${p.title}-${str}`; 10 | }, '').then((str) => { 11 | console.log(str); 12 | assert.ok(isString(str)); 13 | }); 14 | } 15 | 16 | module.exports = testReduce; 17 | -------------------------------------------------------------------------------- /test/kredis/testQueue.js: -------------------------------------------------------------------------------- 1 | const run = async (assert, client) => { 2 | console.log('testing queue'); 3 | 4 | const q = client.queue('q'); 5 | 6 | await q.nq({x: 1}, {x: 2}); 7 | await q.nq([{x: 3}, {x: 4}]); 8 | 9 | const items = await q.range(); 10 | 11 | items.forEach(({x}) => assert.ok([1, 2, 3, 4].indexOf(x) > -1)); 12 | 13 | assert.deepEqual((await q.dq()).x, 1); 14 | assert.deepEqual((await q.dq()).x, 2); 15 | assert.deepEqual((await q.dq()).x, 3); 16 | assert.deepEqual((await q.dq()).x, 4); 17 | assert.deepEqual(await q.dq(), null); 18 | }; 19 | 20 | module.exports = run; 21 | -------------------------------------------------------------------------------- /src/migrate.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const Orm = require('./Orm'); 4 | 5 | const defaultMigrationsDir = path.join(process.cwd(), 'migrations'); 6 | const defaultMigratorConfig = { 7 | devDir: defaultMigrationsDir, 8 | distDir: defaultMigrationsDir, 9 | args: process.argv.slice(2) 10 | }; 11 | 12 | function run(ormConfig, migratorConfig={}) { 13 | migratorConfig = { 14 | ...defaultMigratorConfig, 15 | ...migratorConfig 16 | }; 17 | 18 | const orm = new Orm(ormConfig); 19 | const {migrator} = orm.exports; 20 | 21 | migrator.mount(migratorConfig).then(() => orm.close()); 22 | } 23 | 24 | module.exports = run; 25 | -------------------------------------------------------------------------------- /src/kredis/Queue.js: -------------------------------------------------------------------------------- 1 | const {isArray} = require('lodash'); 2 | 3 | class Queue { 4 | constructor(client, name) { 5 | this.client = client; 6 | this.name = name; 7 | } 8 | 9 | dq() { 10 | return this.client.dq(this.name); 11 | } 12 | 13 | nq(...args) { 14 | if (isArray(args[0])) { 15 | return this.client.nq(this.name, args[0]); 16 | } else { 17 | return this.client.nq(this.name, args); 18 | } 19 | } 20 | 21 | range(startI=0, endI=-1) { 22 | return this.client.range(this.name, startI, endI); 23 | } 24 | 25 | clear() { 26 | return this.client.clear(this.name); 27 | } 28 | } 29 | 30 | module.exports = Queue; 31 | -------------------------------------------------------------------------------- /test/orm/testUpdate.js: -------------------------------------------------------------------------------- 1 | function testUpdate(assert, orm) { 2 | console.log('testing update'); 3 | 4 | const {table} = orm.exports; 5 | 6 | return table('posts').all() 7 | .then((allPosts) => { 8 | const post0 = allPosts[0]; 9 | const newTitle = post0.title.slice(0, Math.floor(post0.title.length / 2)); 10 | 11 | return table('posts').update(post0.id, {title: newTitle}) 12 | .then((post) => { 13 | assert.deepEqual(post.id, post0.id); 14 | assert.deepEqual(post.title, newTitle); 15 | console.log('update test passed'); 16 | }) 17 | ; 18 | }) 19 | ; 20 | } 21 | 22 | 23 | module.exports = testUpdate; 24 | -------------------------------------------------------------------------------- /test/kredis/testHash.js: -------------------------------------------------------------------------------- 1 | const run = async (assert, client) => { 2 | console.log('testing hash'); 3 | 4 | const testHash = client.hash('test'); 5 | 6 | await testHash.set('foo', 'bar'); 7 | await testHash.set('fizz', {x: 1}); 8 | 9 | assert.deepEqual(await testHash.get('foo'), 'bar'); 10 | assert.deepEqual((await testHash.get('fizz')).x, 1); 11 | 12 | await testHash.del(['foo', 'fizz']); 13 | 14 | await testHash.set('foo', 'bar', 500); 15 | 16 | assert.deepEqual(await testHash.has('foo'), true); 17 | 18 | await new Promise((resolve) => setTimeout(async () => { 19 | assert.deepEqual(await testHash.has('foo'), false); 20 | resolve(); 21 | }, 1000)); 22 | }; 23 | 24 | module.exports = run; 25 | -------------------------------------------------------------------------------- /test/orm/testCache.js: -------------------------------------------------------------------------------- 1 | module.exports = async (assert, orm) => { 2 | console.log('testing cache story'); 3 | 4 | const {table} = orm.exports; 5 | 6 | const commentsTable = table('comments').eagerLoad('post').where('is_flagged', false); 7 | 8 | const comments = await commentsTable.remember(2000).all(); 9 | 10 | await table('comments').delete(comments[0].id); 11 | 12 | const cachedComments = await commentsTable.all(); 13 | 14 | await commentsTable.forget(); 15 | const actualComments = await commentsTable.all(); 16 | 17 | assert.ok(comments.length === cachedComments.length); 18 | assert.ok(comments.length === actualComments.length+1); 19 | 20 | await table('comments').insert(comments[0]); 21 | }; 22 | -------------------------------------------------------------------------------- /test/kredis/main.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | 3 | const Client = require('../../src/kredis'); 4 | const config = require('../config'); 5 | 6 | const testClient = require('./testClient'); 7 | const testHash = require('./testHash'); 8 | const testQueue = require('./testQueue'); 9 | 10 | const run = async () => { 11 | const client = new Client(config.redis); 12 | 13 | await client.clear(); 14 | 15 | await testClient(assert, client); 16 | await testHash(assert, client); 17 | await testQueue(assert, client); 18 | 19 | await client.disconnect(); 20 | }; 21 | 22 | if (require.main === module) { 23 | // handle promise errors 24 | process.on('unhandledRejection', (err) => { throw err; }); 25 | 26 | run(...process.argv.slice(2)); 27 | } 28 | -------------------------------------------------------------------------------- /test/config.sample.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // pg 3 | db: { 4 | client: 'postgresql', 5 | connection: { 6 | database: 'tabel_test', 7 | host: 'localhost', 8 | port: 5432, 9 | user: 'dev', 10 | password: 'dev' 11 | }, 12 | migrations: 'knex_migrations' 13 | }, 14 | redis: { 15 | host: 'localhost', 16 | port: '6379', 17 | keyPrefix: 'test.tabel.' 18 | } 19 | 20 | // mysql 21 | // db: { 22 | // client: 'mysql', 23 | // connection: { 24 | // database: 'tabel_test', 25 | // host: 'localhost', 26 | // port: 3306, 27 | // user: 'root', 28 | // password: 'root' 29 | // }, 30 | // migrations: 'knex_migrations' 31 | // }, 32 | // redis: { 33 | // host: 'localhost', 34 | // port: '6379', 35 | // keyPrefix: 'test.tabel.' 36 | // } 37 | }; 38 | -------------------------------------------------------------------------------- /test/migrator/main.js: -------------------------------------------------------------------------------- 1 | const Tabel = require('../../src/Orm'); 2 | const config = require('../config'); 3 | 4 | const testWithDefaultStub = require('./testWithDefaultStub'); 5 | const testWithCustomStub = require('./testWithCustomStub'); 6 | 7 | // handle promise errors 8 | process.on('unhandledRejection', err => { throw err; }); 9 | 10 | function run(mode) { 11 | if (['custom', 'default'].indexOf(mode) === -1) { 12 | console.log('Usage: `npm run test.migrate default|custom`'); 13 | return Promise.resolve(); 14 | } 15 | 16 | const orm = new Tabel(config); 17 | const {migrator} = orm.exports; 18 | 19 | return (() => ( 20 | mode === 'custom' ? testWithCustomStub(orm, migrator) : 21 | mode === 'default' ? testWithDefaultStub(orm, migrator) : 22 | Promise.reject(new Error('invalid mode')) 23 | ))().then(() => orm.close()); 24 | } 25 | 26 | run(...process.argv.slice(2)); 27 | -------------------------------------------------------------------------------- /lib/migrate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var path = require('path'); 6 | 7 | var Orm = require('./Orm'); 8 | 9 | var defaultMigrationsDir = path.join(process.cwd(), 'migrations'); 10 | var defaultMigratorConfig = { 11 | devDir: defaultMigrationsDir, 12 | distDir: defaultMigrationsDir, 13 | args: process.argv.slice(2) 14 | }; 15 | 16 | function run(ormConfig) { 17 | var migratorConfig = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 18 | 19 | migratorConfig = _extends({}, defaultMigratorConfig, migratorConfig); 20 | 21 | var orm = new Orm(ormConfig); 22 | var migrator = orm.exports.migrator; 23 | 24 | 25 | migrator.mount(migratorConfig).then(function () { 26 | return orm.close(); 27 | }); 28 | } 29 | 30 | module.exports = run; -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Tabel - node.js ORM for PostgreSQL 2 | 3 | ## A simple orm for PostgreSQL, built over [knex.js](http://knexjs.org/) which works with simple javascript objects and arrays. 4 | 5 | #### MIT License 6 | 7 | `npm install --save tabel` this installs v3 8 | 9 | If you are an existing tabel user, use: 10 | `npm install --save tabel@2` 11 | 12 | ## Docs only for v2 13 | #### [Read the docs](https://github.com/fractaltech/tabel/wiki). 14 | 15 | Following tests are available: 16 | 1. `npm run test.orm` 17 | 2. `npm run test.collisions` 18 | 3. `npm run test.migrator` 19 | 4. `npm run test.migrate.cli` 20 | 21 | Before running tests, copy `test/config.sample.js` to `test/config.js`. 22 | 23 | ### V3 notes 24 | 25 | 1. Added `scoperAsync`. `const {scoperAsync} = require('app/orm');`. 26 | 2. Working on dynamic-query-chain-classes. 27 | 3. using rdisdsl instead of kredis 28 | 4. `autoId` table definition flag changed to `uuid`. Breaks BC 29 | 5. Added mysql support with `uuid` flag `true` 30 | 6. Added mysql support with `increments` flag `true` -------------------------------------------------------------------------------- /src/kredis/Hash.js: -------------------------------------------------------------------------------- 1 | const {isArray} = require('lodash'); 2 | 3 | class Hash { 4 | constructor(client, name) { 5 | this.client = client; 6 | this.name = name; 7 | } 8 | 9 | fullKey(key) { 10 | if (isArray(key)) { 11 | return key.map((k) => this.fullKey(k)); 12 | } 13 | 14 | return `${this.name}.${key}`; 15 | } 16 | 17 | assign(values, lifetime) { 18 | return Promise.all( 19 | Object.keys(values).map((key) => this.set(key, values[key], lifetime)) 20 | ); 21 | } 22 | 23 | has(key) { 24 | return this.client.exists(this.fullKey(key)); 25 | } 26 | 27 | get(key, defaultVal=null) { 28 | return this.client.get(this.fullKey(key), defaultVal); 29 | } 30 | 31 | set(key, val, lifetime) { 32 | return this.client.set(this.fullKey(key), val, lifetime); 33 | } 34 | 35 | del(key) { 36 | return this.client.del(this.fullKey(key)); 37 | } 38 | 39 | delete(key) { 40 | return this.delete(key); 41 | } 42 | 43 | clear() { 44 | return this.client.clear(`${this.name}.`); 45 | } 46 | } 47 | 48 | module.exports = Hash; 49 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kapil Verma 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /test/orm/testDelete.js: -------------------------------------------------------------------------------- 1 | function testDelete(assert, orm) { 2 | const {table} = orm.exports; 3 | console.log('testing delete'); 4 | 5 | return table('photos').all().then((allPhotos) => { 6 | return table('photos').del(allPhotos[0].id) 7 | .then(() => table('photos').find(allPhotos[0].id)) 8 | .then((existing) => assert.ok(!existing, 'del(key) works')) 9 | .then(() => table('photos').insert(allPhotos[0])) 10 | ; 11 | }).then(() => table('photos').all()).then((allPhotos) => { 12 | return table('photos').whereKey(allPhotos[0].id).del() 13 | .then(() => table('photos').find(allPhotos[0].id)) 14 | .then((existing) => assert.ok(!existing, '.where(...).del() works')) 15 | .then(() => table('photos').insert(allPhotos[0])) 16 | ; 17 | }).then(() => table('photos').all().then((allPhotos) => { 18 | return table('photos').del('id', allPhotos[0].id) 19 | .then(() => table('photos').find(allPhotos[0].id)) 20 | .then((existing) => assert.ok(!existing, '.where(...).del() works')) 21 | .then(() => table('photos').insert(allPhotos[0])) 22 | ; 23 | })); 24 | } 25 | 26 | module.exports = testDelete; 27 | -------------------------------------------------------------------------------- /test/orm/testScopesAndJoints.js: -------------------------------------------------------------------------------- 1 | function testScopesAndJoints(assert, orm) { 2 | const {table} = orm.exports; 3 | 4 | return (() => { 5 | console.log('test scopes'); 6 | return Promise.resolve(); 7 | })().then(() => table('comments').all()).then((comments) => Promise.all(comments.map(({id}, i) => { 8 | return i % 2 === 0 ? table('comments').update(id, {is_flagged: true}) : table('comments').find(id); 9 | }))).then((comments) => Promise.all([ 10 | comments, 11 | table('comments').whereFlagged().all(), 12 | table('comments').whereNotFlagged().all() 13 | ])).then(([comments, flaggedComments, unflaggedComments]) => { 14 | assert.deepEqual(comments.filter((_, i) => i % 2 === 0).length, flaggedComments.length); 15 | 16 | comments.filter((_, i) => i % 2 === 0).forEach((comment) => { 17 | assert.ok(flaggedComments.map(({id}) => id).indexOf(comment.id) > -1); 18 | }); 19 | 20 | comments.filter((_, i) => i % 2 !== 0).forEach((comment) => { 21 | assert.ok(unflaggedComments.map(({id}) => id).indexOf(comment.id) > -1); 22 | }); 23 | }).then(() => { 24 | console.log('test joints'); 25 | }); 26 | } 27 | 28 | module.exports = testScopesAndJoints; 29 | -------------------------------------------------------------------------------- /test/orm/testShape.js: -------------------------------------------------------------------------------- 1 | module.exports = async (assert, orm) => { 2 | console.log('testing shape'); 3 | 4 | const {shape} = orm.exports; 5 | 6 | const validData = { 7 | x: 1, 8 | y: 2 9 | }; 10 | 11 | const dataWithExtraKey = { 12 | x: 1, 13 | y: 2, 14 | z: 3 15 | }; 16 | 17 | const invalidData = { 18 | x: 1, 19 | y: 1 20 | }; 21 | 22 | const validation = shape({ 23 | x: (x) => x === 1 ? null : 'invalid', 24 | y: (y) => new Promise((resolve) => { 25 | setTimeout(() => resolve(2), 300); 26 | }).then((val) => y === val ? null : 'invalid') 27 | }); 28 | 29 | console.log('testing valid data'); 30 | const noErr = await validation.errors(validData); 31 | assert.ok(noErr === null); 32 | 33 | console.log('testing data with extra key'); 34 | const extraKeyErr = await validation.errors(dataWithExtraKey); 35 | assert.ok(extraKeyErr.x === null); 36 | assert.ok(extraKeyErr.y === null); 37 | assert.ok(extraKeyErr.z === 'invalid key'); 38 | 39 | console.log('testing invalid data'); 40 | const invalidDataErr = await validation.errors(invalidData); 41 | assert.ok(invalidDataErr.x === null); 42 | assert.ok(invalidDataErr.y === 'invalid'); 43 | }; 44 | -------------------------------------------------------------------------------- /lib/Scope.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Scope = function () { 8 | function Scope(closure, label) { 9 | var isJoint = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; 10 | 11 | _classCallCheck(this, Scope); 12 | 13 | this.closure = closure; 14 | this.label = label; 15 | this.isJoint = !!isJoint; 16 | } 17 | 18 | _createClass(Scope, [{ 19 | key: "apply", 20 | value: function apply(q) { 21 | this.closure(q); 22 | return q; 23 | } 24 | }]); 25 | 26 | return Scope; 27 | }(); 28 | 29 | module.exports = Scope; -------------------------------------------------------------------------------- /src/Track.js: -------------------------------------------------------------------------------- 1 | class Track { 2 | constructor(scopes=[]) { 3 | this.scopes = scopes; 4 | } 5 | 6 | apply(q) { 7 | return this.scopes.reduce((q, scope) => scope.apply(q), q); 8 | } 9 | 10 | hasJoint(label) { 11 | const i = this.scopes.map((scope) => scope.label).indexOf(label); 12 | return i > -1 && this.scopes[i].isJoint; 13 | } 14 | 15 | hasScope(label) { 16 | return this.scopes.map((scope) => scope.label).indexOf(label) > -1; 17 | } 18 | 19 | push(scope) { 20 | if (scope.isJoint && this.hasJoint(scope.label)) { 21 | return this; 22 | } else { 23 | this.scopes.push(scope); 24 | return this; 25 | } 26 | } 27 | 28 | fork() { 29 | return new Track(this.scopes.slice(0)); 30 | } 31 | 32 | rewind() { 33 | this.scopes.pop(); 34 | return this; 35 | } 36 | 37 | merge(track) { 38 | track.scopes.forEach((scope) => { 39 | this.scopes.push(scope); 40 | }); 41 | 42 | return this; 43 | } 44 | 45 | relabelLastScope(name) { 46 | if (this.scopes.length === 0) { 47 | return this; 48 | } 49 | 50 | this.scopes.slice(-1)[0].label = name; 51 | return this; 52 | } 53 | 54 | convertLastScopeToJoint() { 55 | if (this.scopes.length === 0) { 56 | return this; 57 | } 58 | 59 | this.scopes.slice(-1)(0).isJoint = true; 60 | return this; 61 | } 62 | } 63 | 64 | module.exports = Track; 65 | -------------------------------------------------------------------------------- /test/collisions/main.js: -------------------------------------------------------------------------------- 1 | // this is the test for collison of uuids 2 | 3 | const {range, isFinite} = require('lodash'); 4 | const faker = require('faker'); 5 | 6 | const Tabel = require('../../src/Orm'); 7 | 8 | const config = require('../config'); 9 | 10 | // handle promise errors 11 | process.on('unhandledRejection', err => { throw err; }); 12 | 13 | runTests(...process.argv.slice(2)); 14 | 15 | function runTests(numTestCases, chunk) { 16 | if (!isFinite(parseInt(numTestCases, 10) || !isFinite(parseInt(chunk, 10)))) { 17 | console.log('Usage: `npm run test.collisions [numTestCases] [chunk]`'); 18 | console.log('Please provide the appropriate config too in `test/config.js`'); 19 | return Promise.resolve(); 20 | } 21 | 22 | const {knex, table, orm} = new Tabel(config).exports; 23 | 24 | return knex.schema.dropTableIfExists('collisions') 25 | .then(() => knex.schema.createTable('collisions', (t) => { 26 | t.uuid('id').primary(); 27 | t.text('title'); 28 | })) 29 | .then(() => orm.defineTable({ 30 | name: 'collisions', 31 | props: {uuid: true} 32 | })) 33 | .then(() => insert(table, parseInt(numTestCases, 10), parseInt(chunk, 10))) 34 | .then(() => console.log(`${parseInt(numTestCases, 10)} cases tested`)) 35 | .then(() => knex.schema.dropTableIfExists('collisions')) 36 | .then(() => orm.close()) 37 | ; 38 | } 39 | 40 | 41 | function insert(table, testCases, chunk, cur=0) { 42 | if (cur < testCases) { 43 | console.log(`${cur} to ${cur+chunk}`); 44 | return Promise.all(range(chunk).map(() => table('collisions').insert({title: faker.lorem.sentence()}))) 45 | .then(() => insert(table, testCases, chunk, cur+chunk)) 46 | ; 47 | } else { 48 | return Promise.resolve(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tabel", 3 | "version": "3.0.1", 4 | "repository": "https://github.com/fractaltech/tabel", 5 | "description": "A simple orm for PostgreSQL which works with simple javascript objects and arrays", 6 | "main": "lib/Orm.js", 7 | "homepage": "https://github.com/fractaltech/tabel", 8 | "scripts": { 9 | "prepublishOnly": "npm run build", 10 | "clean": "rm -rf lib/", 11 | "build": "npm run clean && babel src --out-dir lib/ && cp src/migration.stub lib/migration.stub", 12 | "test": "npm run test.kredis && npm run test.orm && npm run test.collisions 1000 100 && npm run test.migrator default && npm run test.migrator custom && npm run test.migrate.cli", 13 | "test.kredis": "babel-node test/kredis/main.js", 14 | "test.orm": "babel-node test/orm/main.js", 15 | "test.collisions": "babel-node test/collisions/main.js", 16 | "test.migrator": "babel-node test/migrator/main.js", 17 | "test.migrate.cli": "babel-node test/migrator/cli.js" 18 | }, 19 | "author": "Kapil Verma ", 20 | "keywords": [ 21 | "object-relational-mapper", 22 | "orm", 23 | "postgresql", 24 | "postgres", 25 | "redis", 26 | "mysql" 27 | ], 28 | "license": "ISC", 29 | "dependencies": { 30 | "file-exists": "^2.0.0", 31 | "isusableobject": "^0.1.2", 32 | "knex": "^0.21.1", 33 | "lodash": "^4.17.11", 34 | "md5": "^2.2.1", 35 | "redisdsl": "^1.0.3", 36 | "uuid": "^2.0.3" 37 | }, 38 | "devDependencies": { 39 | "babel-cli": "^6.26.0", 40 | "babel-eslint": "^6.1.2", 41 | "babel-preset-es2015": "^6.24.1", 42 | "babel-preset-stage-0": "^6.5.0", 43 | "eslint": "^2.13.1", 44 | "faker": "^3.1.0", 45 | "mysql": "^2.15.0", 46 | "pg": "^8.0.3", 47 | "rimraf": "^2.5.4" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/migrator/testWithCustomStub.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const rimraf = require('rimraf'); 3 | const path = require('path'); 4 | const assert = require('assert'); 5 | 6 | function run(orm, migrator) { 7 | return cleanup(orm) 8 | .then(() => testMake(orm, migrator)) 9 | .then(() => cleanup(orm)) 10 | ; 11 | } 12 | 13 | function testMake(orm, migrator) { 14 | console.log('testing make'); 15 | 16 | const migrationsDir = path.join(process.cwd(), 'migrations'); 17 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 18 | 19 | return migrator.mount({devDir, distDir, args: ['make', 'Foo'], stub: path.join(__dirname, 'custom.stub')}) 20 | .then(() => new Promise((resolve, reject) => fs.readdir(migrationsDir, (err, files) => { 21 | if (err) { 22 | reject(err); 23 | } else { 24 | assert.ok( 25 | files.filter((file) => file.indexOf('Foo.js') > -1, 'migrate make creates correct file') 26 | ); 27 | resolve(files[0]); 28 | } 29 | }))) 30 | .then((migrationFile) => Promise.all([ 31 | new Promise((resolve, reject) => fs.readFile(path.join(migrationsDir, migrationFile), 'utf8', (err, text) => { 32 | if (err) { 33 | reject(err); 34 | } else { 35 | resolve(text); 36 | } 37 | })), 38 | new Promise((resolve, reject) => fs.readFile(path.join(__dirname, 'custom.stub'), 'utf8', (err, text) => { 39 | if (err) { 40 | reject(err); 41 | } else { 42 | resolve(text); 43 | } 44 | })) 45 | ])) 46 | .then(([migrationFileText, stubText]) => { 47 | assert.ok(migrationFileText === stubText, 'project stub is used properly'); 48 | }) 49 | ; 50 | } 51 | 52 | function cleanup(orm) { 53 | console.log('cleaning up'); 54 | 55 | const {knex} = orm.exports; 56 | 57 | return Promise.all([ 58 | knex.schema.dropTableIfExists('knex_migrations'), 59 | knex.schema.dropTableIfExists('knex_migrations_lock'), 60 | knex.schema.dropTableIfExists('test_custom'), 61 | new Promise((resolve) => ( 62 | rimraf(path.join(process.cwd(), 'migrations'), () => resolve()) 63 | )) 64 | ]); 65 | } 66 | 67 | module.exports = run; 68 | -------------------------------------------------------------------------------- /src/relations/Relation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Already used methods: 3 | * - setName 4 | * - forModel 5 | * - constrain 6 | * - eagerLoad 7 | * - load 8 | */ 9 | 10 | const {isString} = require('lodash'); 11 | 12 | const Scope = require('../Scope'); 13 | const Track = require('../Track'); 14 | 15 | class Relation { 16 | constructor(ownerTable) { 17 | this.ownerTable = ownerTable; 18 | this.constraints = new Track(); 19 | this.activeModel = null; 20 | this.relationName = null; 21 | } 22 | 23 | setName(relationName) { 24 | this.relationName = relationName; 25 | return this; 26 | } 27 | 28 | forModel(model) { 29 | this.activeModel = model; 30 | return this; 31 | } 32 | 33 | constrain(constraint, label='constraint') { 34 | this.constraints.push(new Scope(constraint, label)); 35 | return this; 36 | } 37 | 38 | eagerLoad(...args) { 39 | return this.constrain((t) => t.eagerLoad(...args), 'eagerLoad'); 40 | } 41 | 42 | load(fromModels=[]) { 43 | if (fromModels.length === 0) { 44 | return Promise.resolve(fromModels); 45 | } 46 | 47 | return this.getRelated(fromModels).then((relatedModels) => { 48 | return this.matchModels(this.initRelation(fromModels), relatedModels); 49 | }); 50 | } 51 | 52 | initRelation(fromModels=[]) { 53 | throw new Error('not implemented'); 54 | } 55 | 56 | getRelated(fromModels=[]) { 57 | throw new Error('not implemented'); 58 | } 59 | 60 | matchModels(fromModels=[], relatedModels=[]) { 61 | throw new Error('not implemented'); 62 | } 63 | 64 | jointLabel(label, {isLeftJoin=false}) { 65 | return `${ 66 | isLeftJoin ? 'leftJoin' : 'join' 67 | }.${this.constructor.name}.${this.relationName}${ 68 | isString(label) ? `.${label}` : '' 69 | }`; 70 | } 71 | 72 | pivotJointLabel(label, {isLeftJoin=false}) { 73 | return `${this.jointLabel(label, {isLeftJoin})}.pivot${ 74 | isString(label) ? `.${label}` : '' 75 | }`; 76 | } 77 | 78 | throughJointLabel(label, {isLeftJoin=false}) { 79 | return `${this.jointLabel(label, {isLeftJoin})}.through${ 80 | isString(label) ? `.${label}` : '' 81 | }`; 82 | } 83 | 84 | join() { 85 | throw new Error('not implemented'); 86 | } 87 | 88 | joinPivot() { 89 | throw new Error('not imeplemented'); 90 | } 91 | 92 | joinThrough() { 93 | throw new Error('not imeplemented'); 94 | } 95 | } 96 | 97 | module.exports = Relation; 98 | -------------------------------------------------------------------------------- /src/Scoper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Usage: 3 | * 4 | * 'eagerLoader' 5 | * new Scoper([ 6 | * {key: 'posts', scope(t) { t.eagerLoad(['posts']); }} 7 | * {key: 'posts.tags', scope(t) { t.eagerLoad['posts.tags']; }} 8 | * {key: 'posts.comments', scope(t) { t.eagerLoad['posts.comments']; }} 9 | * {key: 'posts.tags.posts', scope(t) { t.eagerLoad['posts.tags.posts']; }} 10 | * ]); 11 | * 12 | * 'filterer' 13 | * new Scoper([ 14 | * {key: 'posts.ids', scope(t, ids=[]) { 15 | * if (ids.length > 0) { 16 | * t.posts().join(t).whereIn('posts.id', ids); 17 | * } 18 | * }}, 19 | * 20 | * {key: 'name', scope(t, val) { t.where('name', like, val); }} 21 | * {key: 'posts.count.gte', scope(t, val) { 22 | * val = parseInt(val, 10); 23 | * if (isFinite(val)) { 24 | * t.posts().join(t).groupBy(t.keyCol()).having(t.raw('count(posts.id)'), '>=', val) 25 | * } 26 | * }} 27 | * ]); 28 | */ 29 | 30 | const {isArray, isObject} = require('lodash'); 31 | 32 | class Scoper { 33 | constructor(scopes=[]) { 34 | this.scopes = new Map(); 35 | 36 | this.addScopes(scopes); 37 | } 38 | 39 | addScopes(scopes=[]) { 40 | if (isObject(scopes) && !isArray(scopes)) { 41 | scopes = Object.keys(scopes).map((k) => ({key: k, scope: scopes[k]})); 42 | } 43 | 44 | scopes.forEach(({key, scope}) => { 45 | this.scopes.set(key, scope); 46 | }); 47 | 48 | return this; 49 | } 50 | 51 | addScope({key, scope}) { 52 | return this.addScopes([{key, scope}]); 53 | } 54 | 55 | merge(scoper) { 56 | Array.from(scoper.scopes.keys()).forEach((k) => { 57 | this.scopes.set(k, scoper.scopes.get(k)); 58 | }); 59 | 60 | return this; 61 | } 62 | 63 | apply(table, params) { 64 | const actionableParams = this.actionableParams(params); 65 | 66 | return Promise.all(actionableParams 67 | .filter(({key}) => this.scopes.has(key)) 68 | .map(({key, val}) => { 69 | return this.scopes.get(key).bind(this)(table, val, key); 70 | }, table)).then(() => table) 71 | ; 72 | } 73 | 74 | actionableParams(params={}) { 75 | if (isArray(params)) { 76 | return params.reduce((actionableParams, param) => { 77 | return actionableParams.concat([{ 78 | key: param, val: null 79 | }]); 80 | }, []); 81 | } else if (isObject(params)) { 82 | return Object.keys(params).reduce((actionableParams, param) => { 83 | return actionableParams.concat([{ 84 | key: param, val: params[param] 85 | }]); 86 | }, []); 87 | } else { 88 | throw new Error('invalid params'); 89 | } 90 | } 91 | } 92 | 93 | module.exports = Scoper; 94 | -------------------------------------------------------------------------------- /src/Shape.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Promise based validation of input 3 | * 4 | 5 | const Shape = require('shape-errors'); 6 | const s = new Shape({ 7 | user_id: (userId) => userExists(userId).then((exists) => exists ? null : 'invalid'), 8 | name: (name, {user_id}) => findUserByName(name).then( 9 | (user) => user.id === user_id ? null : 'invalid' 10 | ) 11 | }) 12 | 13 | s.errors(data).then(({result, errors}) => {}) 14 | */ 15 | 16 | const {assign, toPlainObject} = require('lodash'); 17 | 18 | const isUsableObject = require('isusableobject'); 19 | 20 | class Shape { 21 | constructor(validations=[]) { 22 | this.validations = new Map(); 23 | 24 | this.addValidations(validations); 25 | } 26 | 27 | addValidations(validations=[]) { 28 | if (isUsableObject(validations)) { 29 | validations = toPlainObject(validations); 30 | validations = Object.keys(validations).map((k) => ({key: k, validation: validations[k]})); 31 | } 32 | 33 | validations.forEach(({key, validation}) => { 34 | this.validations.set(key, validation); 35 | }); 36 | 37 | return this; 38 | } 39 | 40 | addValidation({key, validation}) { 41 | this.validations.set(key, validation); 42 | return this; 43 | } 44 | 45 | merge(validator) { 46 | Array.from(validator.validation.keys()).forEach((k) => { 47 | this.validations.set(k, validator.validations.get(k)); 48 | }); 49 | 50 | return this; 51 | } 52 | 53 | errors(input={}) { 54 | const invalidInputKeysErr = Object.keys(input) 55 | .filter((k) => { 56 | return Array.from(this.validations.keys()).indexOf(k) === -1; 57 | }) 58 | .reduce((err, k) => ({ 59 | ...err, 60 | [k]: 'invalid key' 61 | }), {}) 62 | ; 63 | 64 | return Promise.all( 65 | Array.from(this.validations.keys()).map((key) => { 66 | const err = this.validations.get(key)(input[key], input, key); 67 | 68 | if (err instanceof Promise) { 69 | return err.then((err) => { 70 | return {key, err}; 71 | }); 72 | } else if (err instanceof Shape) { 73 | return err.errors(input[key]).then((err) => { 74 | return {key, err}; 75 | }); 76 | } else { 77 | return {key, err}; 78 | } 79 | }) 80 | ) 81 | .then((checks) => { 82 | if (checks.filter(({err}) => !!err).length === 0 && Object.keys(invalidInputKeysErr).length === 0) { 83 | return null; 84 | } else { 85 | return checks.reduce((all, {key, err}) => { 86 | return assign(all, {[key]: err}); 87 | }, invalidInputKeysErr); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | module.exports = Shape; 94 | -------------------------------------------------------------------------------- /test/orm/testAutoIncrementIdTables.js: -------------------------------------------------------------------------------- 1 | const {isArray} = require('lodash'); 2 | 3 | module.exports = async (assert, orm) => { 4 | const {ok, deepEqual} = assert; 5 | const {table} = orm.exports; 6 | 7 | orm.defineTable({ 8 | name: 'products', 9 | 10 | props: { 11 | increments: true, 12 | timestamps: true 13 | }, 14 | 15 | relations: { 16 | category() { 17 | return this.belongsTo('categories', 'category_id'); 18 | }, 19 | 20 | sellers() { 21 | return this.manyToMany('sellers', 'product_seller', 'product_id', 'seller_id'); 22 | } 23 | } 24 | }); 25 | 26 | orm.defineTable({ 27 | name: 'categories', 28 | 29 | props: { 30 | increments: true, 31 | timestamps: true 32 | }, 33 | 34 | relations: { 35 | products() { 36 | return this.hasMany('products', 'category_id'); 37 | } 38 | } 39 | }); 40 | 41 | orm.defineTable({ 42 | name: 'sellers', 43 | 44 | props: { 45 | increments: true, 46 | timestamps: true 47 | }, 48 | 49 | relations: { 50 | products() { 51 | return this.manyToMany('products', 'product_seller', 'seller_id', 'product_id'); 52 | } 53 | } 54 | }); 55 | 56 | orm.defineTable({ 57 | name: 'product_seller' 58 | }); 59 | 60 | console.log('testing insert in autoincrement table'); 61 | 62 | const c1 = await table('categories').insert({name: 'c foo'}); 63 | const c2 = await table('categories').insert({name: 'c bar'}); 64 | 65 | deepEqual(c1.id, 1, 'auto insert id 1'); 66 | deepEqual(c2.id, 2, 'auto insert id 2'); 67 | 68 | const [p1, p2, p3] = await table('products').insert([ 69 | {name: 'p foo', category_id: c1.id}, 70 | {name: 'p bar', category_id: c1.id}, 71 | {name: 'p baz', category_id: c2.id} 72 | ]); 73 | 74 | console.log('testing hasMany eagerloads with autoincrement'); 75 | const c1WithProducts = await table('categories').eagerLoad('products').find(c1.id) 76 | ok(isArray(c1WithProducts.products), 'category.products eagerload works fine'); 77 | 78 | const [s1, s2] = await table('sellers').insert([ 79 | {name: 's foo'}, 80 | {name: 's bar'} 81 | ]); 82 | 83 | await table('product_seller').insert([ 84 | {product_id: p1.id, seller_id: s1.id}, 85 | {product_id: p2.id, seller_id: s1.id}, 86 | {product_id: p2.id, seller_id: s2.id}, 87 | {product_id: p3.id, seller_id: s2.id} 88 | ]); 89 | 90 | console.log('testing multiple col whereIn'); 91 | 92 | const pivots = await table('product_seller') 93 | .whereIn(['product_id', 'seller_id'], [ 94 | {product_id: p1.id, seller_id: s1.id}, 95 | {product_id: p2.id, seller_id: s2.id} 96 | ]) 97 | .all() 98 | ; 99 | 100 | ok(isArray(pivots), 'pivots fetched'); 101 | deepEqual(pivots.length, 2, 'correct no. of pivots fetched'); 102 | }; -------------------------------------------------------------------------------- /src/migrator.js: -------------------------------------------------------------------------------- 1 | const {isString} = require('lodash'); 2 | const path = require('path'); 3 | 4 | function migrator(orm) { 5 | return { 6 | mount({devDir, distDir, args=[], stub=path.join(__dirname, 'migration.stub')}) { 7 | const knex = orm.knex; 8 | 9 | if (args.length === 0 || Object.keys(commands).indexOf(args[0]) === -1) { 10 | console.log('Available Commands:'); 11 | console.log(Object.keys(commands).join('\n')); 12 | 13 | return Promise.resolve(); 14 | } else { 15 | return ((cmd, ...args) => commands[cmd](knex, {devDir, distDir, stub}, ...args))(...args); 16 | } 17 | } 18 | }; 19 | } 20 | 21 | const commands = { 22 | make(knex, {devDir, stub}, migration) { 23 | if (!isString(migration) || migration.length === 0) { 24 | console.log('Usage: npm run task:migrate make MigrationName'); 25 | return Promise.resolve({}); 26 | } 27 | 28 | console.log(`Making migration ${migration}`); 29 | return knex.migrate.make(migration, { 30 | stub, 31 | directory: devDir 32 | }); 33 | }, 34 | 35 | latest(knex, {distDir}) { 36 | console.log('Migrating...'); 37 | 38 | return knex.migrate.latest({directory: distDir}).then((batch) => { 39 | if (batch[0] === 0) { 40 | return; 41 | } else { 42 | console.log(`Batch: ${batch[0]}`); 43 | batch[1].forEach((file) => { 44 | console.log(file); 45 | }); 46 | return; 47 | } 48 | }); 49 | }, 50 | 51 | rollback(knex, {distDir}) { 52 | console.log('Rolling Back...'); 53 | 54 | return knex.migrate.rollback({directory: distDir}).then((batch) => { 55 | if (batch[0] === 0) { 56 | return; 57 | } else { 58 | console.log(`Batch: ${batch[0]}`); 59 | batch[1].forEach((file) => { 60 | console.log(file); 61 | }); 62 | } 63 | }); 64 | }, 65 | 66 | version(knex, {distDir}) { 67 | return knex.migrate.currentVersion({directory: distDir}).then((version) => { 68 | console.log(`Current Version: ${version}`); 69 | }); 70 | }, 71 | 72 | reset(knex, {distDir}) { 73 | console.log('Resetting...'); 74 | 75 | function rollbackToBeginning() { 76 | return knex.migrate.rollback({directory: distDir}).then((batch) => { 77 | if (batch[0] === 0) { 78 | return Promise.resolve(null); 79 | } else { 80 | console.log(`Batch: ${batch[0]}`); 81 | batch[1].forEach((file) => { 82 | console.log(file); 83 | }); 84 | 85 | return rollbackToBeginning(); 86 | } 87 | }); 88 | } 89 | 90 | return rollbackToBeginning(); 91 | }, 92 | 93 | refresh(knex, {distDir}) { 94 | return this.reset(knex, {distDir}).then(() => { 95 | return this.latest(knex, {distDir}); 96 | }); 97 | } 98 | }; 99 | 100 | module.exports = migrator; 101 | -------------------------------------------------------------------------------- /lib/Track.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | var Track = function () { 8 | function Track() { 9 | var scopes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 10 | 11 | _classCallCheck(this, Track); 12 | 13 | this.scopes = scopes; 14 | } 15 | 16 | _createClass(Track, [{ 17 | key: "apply", 18 | value: function apply(q) { 19 | return this.scopes.reduce(function (q, scope) { 20 | return scope.apply(q); 21 | }, q); 22 | } 23 | }, { 24 | key: "hasJoint", 25 | value: function hasJoint(label) { 26 | var i = this.scopes.map(function (scope) { 27 | return scope.label; 28 | }).indexOf(label); 29 | return i > -1 && this.scopes[i].isJoint; 30 | } 31 | }, { 32 | key: "hasScope", 33 | value: function hasScope(label) { 34 | return this.scopes.map(function (scope) { 35 | return scope.label; 36 | }).indexOf(label) > -1; 37 | } 38 | }, { 39 | key: "push", 40 | value: function push(scope) { 41 | if (scope.isJoint && this.hasJoint(scope.label)) { 42 | return this; 43 | } else { 44 | this.scopes.push(scope); 45 | return this; 46 | } 47 | } 48 | }, { 49 | key: "fork", 50 | value: function fork() { 51 | return new Track(this.scopes.slice(0)); 52 | } 53 | }, { 54 | key: "rewind", 55 | value: function rewind() { 56 | this.scopes.pop(); 57 | return this; 58 | } 59 | }, { 60 | key: "merge", 61 | value: function merge(track) { 62 | var _this = this; 63 | 64 | track.scopes.forEach(function (scope) { 65 | _this.scopes.push(scope); 66 | }); 67 | 68 | return this; 69 | } 70 | }, { 71 | key: "relabelLastScope", 72 | value: function relabelLastScope(name) { 73 | if (this.scopes.length === 0) { 74 | return this; 75 | } 76 | 77 | this.scopes.slice(-1)[0].label = name; 78 | return this; 79 | } 80 | }, { 81 | key: "convertLastScopeToJoint", 82 | value: function convertLastScopeToJoint() { 83 | if (this.scopes.length === 0) { 84 | return this; 85 | } 86 | 87 | this.scopes.slice(-1)(0).isJoint = true; 88 | return this; 89 | } 90 | }]); 91 | 92 | return Track; 93 | }(); 94 | 95 | module.exports = Track; -------------------------------------------------------------------------------- /test/kredis/testClient.js: -------------------------------------------------------------------------------- 1 | module.exports = async (assert, client) => { 2 | console.log('testing client'); 3 | 4 | await (async () => { 5 | console.log('testing get'); 6 | 7 | const val = await client.get('o'); 8 | const count = await client.get('count', 0); 9 | 10 | assert.deepEqual(val, null); 11 | assert.deepEqual(count, 0); 12 | })(); 13 | 14 | await (async () => { 15 | console.log('testing set with lifetime'); 16 | 17 | const o = {x: 1, y: 2}; 18 | 19 | await client.set('o', o, 500); 20 | const retrievedO = await client.get('o'); 21 | 22 | assert.deepEqual(o.x, retrievedO.x, 'correct prop x'); 23 | assert.deepEqual(o.y, retrievedO.y, 'correct prop y'); 24 | 25 | await new Promise((resolve) => setTimeout(async () => { 26 | const retrievedO = await client.get('o'); 27 | assert.deepEqual(retrievedO, null); 28 | resolve(); 29 | }, 1000)); 30 | })(); 31 | 32 | await (async () => { 33 | console.log('testing exists'); 34 | 35 | assert.deepEqual(await client.exists('foo'), false); 36 | 37 | await client.set('foo', 'bar', 500); 38 | assert.deepEqual(await client.exists('foo'), true); 39 | 40 | await new Promise((resolve) => setTimeout(async () => { 41 | assert.deepEqual(await client.exists('foo'), false); 42 | resolve(); 43 | }, 1000)); 44 | })(); 45 | 46 | await (async () => { 47 | console.log('testing set without lifetime, and get'); 48 | 49 | await client.set('foo', 'bar'); 50 | assert.deepEqual(await client.get('foo'), 'bar'); 51 | await client.del('foo'); 52 | assert.deepEqual(await client.exists('foo'), false); 53 | })(); 54 | 55 | await (async () => { 56 | console.log('testing clear'); 57 | 58 | await Promise.all(['foo', 'bar', 'baz'].map((str) => client.set(str, str))); 59 | 60 | assert.deepEqual(await client.get('foo'), 'foo'); 61 | assert.deepEqual(await client.get('bar'), 'bar'); 62 | assert.deepEqual(await client.get('baz'), 'baz'); 63 | 64 | await client.clear(); 65 | 66 | assert.deepEqual(await client.exists('foo'), false); 67 | assert.deepEqual(await client.exists('bar'), false); 68 | assert.deepEqual(await client.exists('baz'), false); 69 | })(); 70 | 71 | await (async () => { 72 | console.log('testing clear with prefix'); 73 | 74 | await Promise.all(['foo.fizz', 'foo.buzz', 'baz'].map((str) => client.set(str, str))); 75 | 76 | assert.deepEqual(await client.get('foo.fizz'), 'foo.fizz'); 77 | assert.deepEqual(await client.get('foo.buzz'), 'foo.buzz'); 78 | assert.deepEqual(await client.get('baz'), 'baz'); 79 | 80 | await client.clear('foo'); 81 | 82 | assert.deepEqual(await client.exists('foo.fizz'), false); 83 | assert.deepEqual(await client.exists('foo.buzz'), false); 84 | assert.deepEqual(await client.exists('baz'), true); 85 | 86 | await client.del('baz'); 87 | })(); 88 | 89 | await (async () => { 90 | console.log('testing nq & dq'); 91 | 92 | await client.nq('q', {x: 1}); 93 | await client.nq('q', [{x: 2}, {x: 3}]); 94 | 95 | const o1 = await client.dq('q'); 96 | assert.deepEqual(o1.x, 1); 97 | 98 | const o2 = await client.dq('q'); 99 | assert.deepEqual(o2.x, 2); 100 | 101 | const o3 = await client.dq('q'); 102 | assert.deepEqual(o3.x, 3); 103 | 104 | const o4 = await client.dq('q'); 105 | assert.deepEqual(o4, null); 106 | })(); 107 | }; 108 | -------------------------------------------------------------------------------- /src/relations/HasOne.js: -------------------------------------------------------------------------------- 1 | const {assign} = require('lodash'); 2 | const isUsableObject = require('isusableobject'); 3 | 4 | const Relation = require('./Relation'); 5 | 6 | class HasOne extends Relation { 7 | constructor(ownerTable, toTable, foreignKey, key) { 8 | super(ownerTable); 9 | assign(this, {fromTable: ownerTable.fork(), toTable, foreignKey, key}); 10 | } 11 | 12 | initRelation(fromModels) { 13 | return fromModels.map((m) => assign(m, {[this.relationName]: null})); 14 | } 15 | 16 | getRelated(...args) { 17 | if (args.length === 0) { 18 | if (this.activeModel !== null) { 19 | return this.getRelated([this.activeModel]).then(([relatedModel]) => relatedModel); 20 | } else { 21 | return Promise.resolve(null); 22 | } 23 | } 24 | 25 | const [fromModels] = args; 26 | 27 | const {toTable, foreignKey, key} = this; 28 | 29 | return this.constraints.apply(toTable.fork()) 30 | .whereIn(foreignKey, fromModels.map((m) => m[key])) 31 | .all() 32 | ; 33 | } 34 | 35 | matchModels(fromModels=[], relatedModels=[]) { 36 | const {relationName, foreignKey, key} = this; 37 | 38 | const keyDict = relatedModels.reduce((dict, m) => { 39 | return assign(dict, {[m[foreignKey]]: m}); 40 | }, {}); 41 | 42 | return fromModels.map((m) => assign(m, { 43 | [relationName]: isUsableObject(keyDict[m[key]]) ? keyDict[m[key]] : null 44 | })); 45 | } 46 | 47 | insert(...args) { 48 | if (args.length === 0) { 49 | throw new Error('bad method call'); 50 | } 51 | 52 | if (args.length === 1) { 53 | return this.insert(this.activeModel, ...args); 54 | } 55 | 56 | const [fromModel, values] = args; 57 | 58 | return this.toTable.insert(assign(values, {[this.foreignKey]: fromModel[this.key]})); 59 | } 60 | 61 | update(...args) { 62 | if (args.length === 0) { 63 | throw new Error('bad method call'); 64 | } 65 | 66 | if (args.length === 1) { 67 | return this.update(this.activeModel, ...args); 68 | } 69 | 70 | const [fromModel, values] = args; 71 | 72 | return this.constraints.apply(this.toTable.fork()) 73 | .where(this.foreignKey, fromModel[this.key]) 74 | .update(assign(values, {[this.foreignKey]: fromModel[this.key]})) 75 | ; 76 | } 77 | 78 | del(...args) { 79 | if (args.length === 0) { 80 | return this.del(this.activeModel); 81 | } 82 | 83 | const [fromModel] = args; 84 | 85 | return this.constraints.apply(this.toTable.fork()) 86 | .where(this.foreignKey, fromModel) 87 | .del() 88 | ; 89 | } 90 | 91 | join(joiner=(() => {}), label=null) { 92 | label = this.jointLabel(label, {}); 93 | const {fromTable, toTable, foreignKey, key} = this; 94 | 95 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 96 | return this.ownerTable; 97 | } else { 98 | return this.ownerTable.joint((q) => { 99 | q.join(toTable.tableName(), (j) => { 100 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 101 | joiner(j); 102 | }); 103 | }, label); 104 | } 105 | } 106 | 107 | leftJoin(joiner=(() => {}), label=null) { 108 | label = this.jointLabel(label, {isLeftJoin: true}); 109 | const {fromTable, toTable, foreignKey, key} = this; 110 | 111 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 112 | return this.ownerTable; 113 | } else { 114 | return this.ownerTable.joint((q) => { 115 | q.leftJoin(toTable.tableName(), (j) => { 116 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 117 | joiner(j); 118 | }); 119 | }, label); 120 | } 121 | } 122 | } 123 | 124 | module.exports = HasOne; 125 | -------------------------------------------------------------------------------- /src/kredis/Client.js: -------------------------------------------------------------------------------- 1 | const redisdsl = require('redisdsl'); 2 | const {isString, isNumber, isArray} = require('lodash'); 3 | const Queue = require('./Queue'); 4 | const Hash = require('./Hash'); 5 | 6 | class Connection { 7 | constructor(config) { 8 | this.config = { 9 | ...config, 10 | ...('keyPrefix' in config ? {prefix: config.keyPrefix} : {}) 11 | }; 12 | 13 | const { 14 | quit, 15 | psetex, 16 | set, 17 | get, 18 | exists, 19 | del, 20 | lrange, 21 | rpush, 22 | lpop, 23 | eval: rEval 24 | } = redisdsl(this.config); 25 | 26 | Object.assign(this, { 27 | cmd: { 28 | quit, 29 | psetex, 30 | set, 31 | get, 32 | exists, 33 | del, 34 | lrange, 35 | rpush, 36 | lpop, 37 | rEval, 38 | clear: (prefix) => { 39 | const pattern = isString(prefix) ? `${prefix}*` : `*`; 40 | 41 | const luaScript = [ 42 | `local keys = redis.call("keys", "${this.config.prefix}${pattern}")`, 43 | `for i=1,#keys,5000 do`, 44 | ` redis.call("del", unpack(keys, i, math.min(i+4999, #keys)))`, 45 | `end`, 46 | `return keys` 47 | ].join('\r\n'); 48 | 49 | return this.cmd.rEval(luaScript, 0); 50 | } 51 | } 52 | }); 53 | 54 | // console.log(this.cmd.clear); 55 | } 56 | } 57 | 58 | class Client { 59 | constructor(config) { 60 | this.connection = new Connection(config); 61 | } 62 | 63 | disconnect() { 64 | return this.connection.cmd.quit(); 65 | } 66 | 67 | set(key, val, lifetime) { 68 | if (isNumber(lifetime)) { 69 | return this.connection.cmd.psetex(key, lifetime, JSON.stringify(val)); 70 | } else { 71 | return this.connection.cmd.set(key, JSON.stringify(val)); 72 | } 73 | } 74 | 75 | put(values, lifetime) { 76 | return Promise.all( 77 | Object.keys(values).map((key) => this.connection.cmd.set(key, values[key], lifetime)) 78 | ); 79 | } 80 | 81 | get(key, defaultVal=null) { 82 | if (isArray(key)) { 83 | return Promise.all(key.map((k) => this.connection.cmd.get(k, defaultVal))); 84 | } 85 | 86 | return this.connection.cmd.get(key).then((val) => { 87 | if (val === null) { 88 | return defaultVal; 89 | } else { 90 | return JSON.parse(val); 91 | } 92 | }); 93 | } 94 | 95 | exists(key) { 96 | if (isArray(key)) { 97 | return Promise.all(key.map((k) => this.exists(k))); 98 | } 99 | 100 | return this.connection.cmd.exists(key).then((result) => { 101 | return result !== 0; 102 | }); 103 | } 104 | 105 | del(key) { 106 | if (isArray(key)) { 107 | return Promise.all(key.map((k) => this.del(k))); 108 | } 109 | 110 | return this.connection.cmd.del(key); 111 | } 112 | 113 | delete(key) { 114 | return this.del(key); 115 | } 116 | 117 | clear(prefix) { 118 | return this.connection.cmd.clear(prefix); 119 | } 120 | 121 | range(queue, startI=0, endI=-1) { 122 | return this.connection.cmd.lrange(queue, startI, endI) 123 | .then((items) => items.map((item) => JSON.parse(item))) 124 | ; 125 | } 126 | 127 | nq(queue, vals) { 128 | if (isArray(vals)) { 129 | vals = vals.map((v) => JSON.stringify(v)); 130 | } else { 131 | vals = JSON.stringify(vals); 132 | } 133 | 134 | return this.connection.cmd.rpush(queue, vals); 135 | } 136 | 137 | dq(queue) { 138 | return this.connection.cmd.lpop(queue).then((val) => { 139 | return JSON.parse(val); 140 | }); 141 | } 142 | 143 | queue(name) { 144 | return new Queue(this, name); 145 | } 146 | 147 | hash(name) { 148 | return new Hash(this, name); 149 | } 150 | } 151 | 152 | module.exports = Client; 153 | -------------------------------------------------------------------------------- /src/relations/HasMany.js: -------------------------------------------------------------------------------- 1 | const {assign, isArray} = require('lodash'); 2 | 3 | const Relation = require('./Relation'); 4 | 5 | class HasMany extends Relation { 6 | constructor(ownerTable, toTable, foreignKey, key) { 7 | super(ownerTable); 8 | assign(this, {fromTable: ownerTable.fork(), toTable, foreignKey, key}); 9 | } 10 | 11 | initRelation(fromModels) { 12 | return fromModels.map((m) => assign(m, {[this.relationName]: []})); 13 | } 14 | 15 | getRelated(...args) { 16 | if (args.length === 0) { 17 | if (this.activeModel !== null) { 18 | return this.getRelated([this.activeModel]); 19 | } else { 20 | return Promise.resolve([]); 21 | } 22 | } 23 | 24 | const [fromModels] = args; 25 | 26 | const {toTable, foreignKey, key} = this; 27 | 28 | return this.constraints.apply(toTable.fork()) 29 | .whereIn(foreignKey, fromModels.map((m) => m[key])) 30 | .all() 31 | ; 32 | } 33 | 34 | matchModels(fromModels=[], relatedModels=[]) { 35 | const {relationName, foreignKey, key} = this; 36 | 37 | const keyDict = relatedModels.reduce((dict, m) => { 38 | const key = m[foreignKey]; 39 | 40 | if (!isArray(dict[key])) { 41 | return assign(dict, {[key]: [m]}); 42 | } else { 43 | return assign(dict, {[key]: dict[key].concat(m)}); 44 | } 45 | }, {}); 46 | 47 | return fromModels.map((m) => assign(m, { 48 | [relationName]: isArray(keyDict[m[key]]) ? keyDict[m[key]] : [] 49 | })); 50 | } 51 | 52 | insert(...args) { 53 | if (args.length === 0) { 54 | throw new Error('bad method call'); 55 | } 56 | 57 | if (args.length === 1) { 58 | return this.insert(this.activeModel, ...args); 59 | } 60 | 61 | const [fromModel, values] = args; 62 | 63 | return this.toTable.insert((() => { 64 | if (isArray(values)) { 65 | return values.map((v) => assign(v, {[this.foreignKey]: fromModel[this.key]})); 66 | } else { 67 | return assign(values, {[this.foreignKey]: fromModel[this.key]}); 68 | } 69 | })()); 70 | } 71 | 72 | update(...args) { 73 | if (args.length === 0) { 74 | throw new Error('bad method call'); 75 | } 76 | 77 | if (args.length === 1) { 78 | return this.update(this.activeModel, ...args); 79 | } 80 | 81 | const [fromModel, values] = args; 82 | 83 | return this.constraints.apply(this.toTable.fork()) 84 | .where(this.foreignKey, fromModel[this.key]) 85 | .update(assign(values, { 86 | [this.foreignKey]: fromModel[this.key] 87 | })) 88 | ; 89 | } 90 | 91 | del(...args) { 92 | if (args.length === 0) { 93 | return this.del(this.activeModel); 94 | } 95 | 96 | const [fromModel] = args; 97 | 98 | return this.constraints.apply(this.toTable.fork()) 99 | .where(this.foreignKey, fromModel[this.key]) 100 | .del() 101 | ; 102 | } 103 | 104 | join(joiner=(() => {}), label=null) { 105 | label = this.jointLabel(label, {}); 106 | const {fromTable, toTable, foreignKey, key} = this; 107 | 108 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 109 | return this.ownerTable; 110 | } else { 111 | return this.ownerTable.joint((q) => { 112 | q.join(toTable.tableName(), (j) => { 113 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 114 | joiner(j); 115 | }); 116 | }, label); 117 | } 118 | } 119 | 120 | leftJoin(joiner=(() => {}), label=null) { 121 | label = this.jointLabel(label, {isLeftJoin: true}); 122 | const {fromTable, toTable, foreignKey, key} = this; 123 | 124 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 125 | return this.ownerTable; 126 | } else { 127 | return this.ownerTable.joint((q) => { 128 | q.leftJoin(toTable.tableName(), (j) => { 129 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 130 | joiner(j); 131 | }); 132 | }, label); 133 | } 134 | } 135 | } 136 | 137 | module.exports = HasMany; 138 | -------------------------------------------------------------------------------- /src/relations/BelongsTo.js: -------------------------------------------------------------------------------- 1 | const {assign} = require('lodash'); 2 | const isUsableObject = require('isusableobject'); 3 | 4 | const Relation = require('./Relation'); 5 | 6 | class BelongsTo extends Relation { 7 | constructor(ownerTable, toTable, foreignKey, otherKey) { 8 | super(ownerTable); 9 | assign(this, {fromTable: ownerTable.fork(), toTable, foreignKey, otherKey}); 10 | } 11 | 12 | initRelation(fromModels=[]) { 13 | return fromModels.map((model) => assign(model, {[this.relationName]: null})); 14 | } 15 | 16 | getRelated(...args) { 17 | if (args.length === 0) { 18 | if (this.activeModel !== null) { 19 | return this.getRelated([this.activeModel]).then(([relatedModel]) => relatedModel); 20 | } else { 21 | return Promise.resolve(null); 22 | } 23 | } 24 | 25 | const [fromModels] = args; 26 | 27 | if (fromModels.length === 0) { 28 | return Promise.resolve([]); 29 | } else { 30 | const foreignKeys = fromModels.filter((m) => !!m).map((m) => m[this.foreignKey]); 31 | 32 | return this.constraints.apply(this.toTable.fork()) 33 | .whereIn(this.otherKey, foreignKeys) 34 | .all() 35 | ; 36 | } 37 | } 38 | 39 | matchModels(fromModels=[], relatedModels=[]) { 40 | const keyDict = relatedModels.reduce( 41 | (dict, m) => assign(dict, {[m[this.otherKey]]: m}), 42 | {} 43 | ); 44 | 45 | return fromModels.map((m) => assign(m, { 46 | [this.relationName]: isUsableObject(keyDict[m[this.foreignKey]]) ? keyDict[m[this.foreignKey]] : null 47 | })); 48 | } 49 | 50 | associate(...args) { 51 | if (args.length === 0) { 52 | throw new Error('bad method call'); 53 | } 54 | 55 | if (args.length === 1) { 56 | return this.associate(this.activeModel, ...args); 57 | } 58 | 59 | const [fromModel, toModel] = args; 60 | 61 | return this.fromTable.whereKey(fromModel).update({ 62 | [this.foreignKey]: toModel[this.otherKey] 63 | }); 64 | } 65 | 66 | dissociate(...args) { 67 | if (args.length === 0) { 68 | return this.dissociate(this.activeModel); 69 | } 70 | 71 | const [fromModel] = args; 72 | 73 | return this.fromTable.whereKey(fromModel).update({ 74 | [this.foreignKey]: null 75 | }); 76 | } 77 | 78 | update(...args) { 79 | if (args.length === 0) { 80 | throw new Error('bad method call'); 81 | } 82 | 83 | if (args.length === 1) { 84 | return this.update(this.activeModel, ...args); 85 | } 86 | 87 | const [fromModel, values] = args; 88 | 89 | return this.constraints.apply(this.toTable.fork()) 90 | .where(this.otherKey, fromModel[this.foreignKey]) 91 | .update(values); 92 | } 93 | 94 | del(...args) { 95 | if (args.length === 0) { 96 | return this.del(this.activeModel); 97 | } 98 | 99 | const [fromModel] = args; 100 | 101 | return this.constraints.apply(this.toTable.fork()) 102 | .where(this.otherKey, fromModel[this.foreignKey]) 103 | .del(); 104 | } 105 | 106 | join(joiner=(() => {}), label=null) { 107 | label = this.jointLabel(label, {}); 108 | const {fromTable, toTable, foreignKey, otherKey} = this; 109 | 110 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 111 | return this.ownerTable; 112 | } else { 113 | return this.ownerTable.joint((q) => { 114 | q.join(toTable.tableName(), (j) => { 115 | j.on(fromTable.c(foreignKey), '=', toTable.c(otherKey)); 116 | joiner(j); 117 | }); 118 | }, label); 119 | } 120 | } 121 | 122 | leftJoin(joiner=(() => {}), label=null) { 123 | label = this.jointLabel(label, {isLeftJoin: true}); 124 | const {fromTable, toTable, foreignKey, otherKey} = this; 125 | 126 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 127 | return this.ownerTable; 128 | } else { 129 | return this.ownerTable.joint((q) => { 130 | q.leftJoin(toTable.tableName(), (j) => { 131 | j.on(fromTable.c(foreignKey), '=', toTable.c(otherKey)); 132 | joiner(j); 133 | }); 134 | }, label); 135 | } 136 | } 137 | } 138 | 139 | module.exports = BelongsTo; 140 | -------------------------------------------------------------------------------- /lib/migrator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 4 | 5 | var _require = require('lodash'), 6 | isString = _require.isString; 7 | 8 | var path = require('path'); 9 | 10 | function migrator(orm) { 11 | return { 12 | mount: function mount(_ref) { 13 | var devDir = _ref.devDir, 14 | distDir = _ref.distDir, 15 | _ref$args = _ref.args, 16 | args = _ref$args === undefined ? [] : _ref$args, 17 | _ref$stub = _ref.stub, 18 | stub = _ref$stub === undefined ? path.join(__dirname, 'migration.stub') : _ref$stub; 19 | 20 | var knex = orm.knex; 21 | 22 | if (args.length === 0 || Object.keys(commands).indexOf(args[0]) === -1) { 23 | console.log('Available Commands:'); 24 | console.log(Object.keys(commands).join('\n')); 25 | 26 | return Promise.resolve(); 27 | } else { 28 | return function (cmd) { 29 | for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 30 | args[_key - 1] = arguments[_key]; 31 | } 32 | 33 | return commands[cmd].apply(commands, [knex, { devDir: devDir, distDir: distDir, stub: stub }].concat(args)); 34 | }.apply(undefined, _toConsumableArray(args)); 35 | } 36 | } 37 | }; 38 | } 39 | 40 | var commands = { 41 | make: function make(knex, _ref2, migration) { 42 | var devDir = _ref2.devDir, 43 | stub = _ref2.stub; 44 | 45 | if (!isString(migration) || migration.length === 0) { 46 | console.log('Usage: npm run task:migrate make MigrationName'); 47 | return Promise.resolve({}); 48 | } 49 | 50 | console.log('Making migration ' + migration); 51 | return knex.migrate.make(migration, { 52 | stub: stub, 53 | directory: devDir 54 | }); 55 | }, 56 | latest: function latest(knex, _ref3) { 57 | var distDir = _ref3.distDir; 58 | 59 | console.log('Migrating...'); 60 | 61 | return knex.migrate.latest({ directory: distDir }).then(function (batch) { 62 | if (batch[0] === 0) { 63 | return; 64 | } else { 65 | console.log('Batch: ' + batch[0]); 66 | batch[1].forEach(function (file) { 67 | console.log(file); 68 | }); 69 | return; 70 | } 71 | }); 72 | }, 73 | rollback: function rollback(knex, _ref4) { 74 | var distDir = _ref4.distDir; 75 | 76 | console.log('Rolling Back...'); 77 | 78 | return knex.migrate.rollback({ directory: distDir }).then(function (batch) { 79 | if (batch[0] === 0) { 80 | return; 81 | } else { 82 | console.log('Batch: ' + batch[0]); 83 | batch[1].forEach(function (file) { 84 | console.log(file); 85 | }); 86 | } 87 | }); 88 | }, 89 | version: function version(knex, _ref5) { 90 | var distDir = _ref5.distDir; 91 | 92 | return knex.migrate.currentVersion({ directory: distDir }).then(function (version) { 93 | console.log('Current Version: ' + version); 94 | }); 95 | }, 96 | reset: function reset(knex, _ref6) { 97 | var distDir = _ref6.distDir; 98 | 99 | console.log('Resetting...'); 100 | 101 | function rollbackToBeginning() { 102 | return knex.migrate.rollback({ directory: distDir }).then(function (batch) { 103 | if (batch[0] === 0) { 104 | return Promise.resolve(null); 105 | } else { 106 | console.log('Batch: ' + batch[0]); 107 | batch[1].forEach(function (file) { 108 | console.log(file); 109 | }); 110 | 111 | return rollbackToBeginning(); 112 | } 113 | }); 114 | } 115 | 116 | return rollbackToBeginning(); 117 | }, 118 | refresh: function refresh(knex, _ref7) { 119 | var _this = this; 120 | 121 | var distDir = _ref7.distDir; 122 | 123 | return this.reset(knex, { distDir: distDir }).then(function () { 124 | return _this.latest(knex, { distDir: distDir }); 125 | }); 126 | } 127 | }; 128 | 129 | module.exports = migrator; -------------------------------------------------------------------------------- /lib/Scoper.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | /** 8 | * Usage: 9 | * 10 | * 'eagerLoader' 11 | * new Scoper([ 12 | * {key: 'posts', scope(t) { t.eagerLoad(['posts']); }} 13 | * {key: 'posts.tags', scope(t) { t.eagerLoad['posts.tags']; }} 14 | * {key: 'posts.comments', scope(t) { t.eagerLoad['posts.comments']; }} 15 | * {key: 'posts.tags.posts', scope(t) { t.eagerLoad['posts.tags.posts']; }} 16 | * ]); 17 | * 18 | * 'filterer' 19 | * new Scoper([ 20 | * {key: 'posts.ids', scope(t, ids=[]) { 21 | * if (ids.length > 0) { 22 | * t.posts().join(t).whereIn('posts.id', ids); 23 | * } 24 | * }}, 25 | * 26 | * {key: 'name', scope(t, val) { t.where('name', like, val); }} 27 | * {key: 'posts.count.gte', scope(t, val) { 28 | * val = parseInt(val, 10); 29 | * if (isFinite(val)) { 30 | * t.posts().join(t).groupBy(t.keyCol()).having(t.raw('count(posts.id)'), '>=', val) 31 | * } 32 | * }} 33 | * ]); 34 | */ 35 | 36 | var _require = require('lodash'), 37 | isArray = _require.isArray, 38 | isObject = _require.isObject; 39 | 40 | var Scoper = function () { 41 | function Scoper() { 42 | var scopes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 43 | 44 | _classCallCheck(this, Scoper); 45 | 46 | this.scopes = new Map(); 47 | 48 | this.addScopes(scopes); 49 | } 50 | 51 | _createClass(Scoper, [{ 52 | key: 'addScopes', 53 | value: function addScopes() { 54 | var _this = this; 55 | 56 | var scopes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 57 | 58 | if (isObject(scopes) && !isArray(scopes)) { 59 | scopes = Object.keys(scopes).map(function (k) { 60 | return { key: k, scope: scopes[k] }; 61 | }); 62 | } 63 | 64 | scopes.forEach(function (_ref) { 65 | var key = _ref.key, 66 | scope = _ref.scope; 67 | 68 | _this.scopes.set(key, scope); 69 | }); 70 | 71 | return this; 72 | } 73 | }, { 74 | key: 'addScope', 75 | value: function addScope(_ref2) { 76 | var key = _ref2.key, 77 | scope = _ref2.scope; 78 | 79 | return this.addScopes([{ key: key, scope: scope }]); 80 | } 81 | }, { 82 | key: 'merge', 83 | value: function merge(scoper) { 84 | var _this2 = this; 85 | 86 | Array.from(scoper.scopes.keys()).forEach(function (k) { 87 | _this2.scopes.set(k, scoper.scopes.get(k)); 88 | }); 89 | 90 | return this; 91 | } 92 | }, { 93 | key: 'apply', 94 | value: function apply(table, params) { 95 | var _this3 = this; 96 | 97 | var actionableParams = this.actionableParams(params); 98 | 99 | return Promise.all(actionableParams.filter(function (_ref3) { 100 | var key = _ref3.key; 101 | return _this3.scopes.has(key); 102 | }).map(function (_ref4) { 103 | var key = _ref4.key, 104 | val = _ref4.val; 105 | 106 | return _this3.scopes.get(key).bind(_this3)(table, val, key); 107 | }, table)).then(function () { 108 | return table; 109 | }); 110 | } 111 | }, { 112 | key: 'actionableParams', 113 | value: function actionableParams() { 114 | var params = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 115 | 116 | if (isArray(params)) { 117 | return params.reduce(function (actionableParams, param) { 118 | return actionableParams.concat([{ 119 | key: param, val: null 120 | }]); 121 | }, []); 122 | } else if (isObject(params)) { 123 | return Object.keys(params).reduce(function (actionableParams, param) { 124 | return actionableParams.concat([{ 125 | key: param, val: params[param] 126 | }]); 127 | }, []); 128 | } else { 129 | throw new Error('invalid params'); 130 | } 131 | } 132 | }]); 133 | 134 | return Scoper; 135 | }(); 136 | 137 | module.exports = Scoper; -------------------------------------------------------------------------------- /src/relations/MorphOne.js: -------------------------------------------------------------------------------- 1 | const {assign} = require('lodash'); 2 | const isUsableObject = require('isusableobject'); 3 | 4 | const Relation = require('./Relation'); 5 | const MorphTo = require('./Relation'); 6 | 7 | class MorphOne extends Relation { 8 | constructor(ownerTable, toTable, inverse) { 9 | if (!(inverse instanceof MorphTo)) { 10 | throw new Error('inverse should be a MorphTo relation'); 11 | } 12 | 13 | super(ownerTable); 14 | assign(this, {fromTable: ownerTable.fork(), toTable, inverse}); 15 | } 16 | 17 | initRelation(fromModels) { 18 | return fromModels.map((m) => assign(m, {[this.relationName]: []})); 19 | } 20 | 21 | getRelated(...args) { 22 | if (args.length === 0) { 23 | if (this.activeModel !== null) { 24 | return this.getRelated([this.activeModel]).then(([relatedModel]) => relatedModel); 25 | } else { 26 | return Promise.resolve(null); 27 | } 28 | } 29 | 30 | const [fromModels] = args; 31 | const {fromTable, inverse} = this; 32 | const toTable = this.constraints.apply(this.toTable.fork()); 33 | 34 | const {foreignKey, typeField} = inverse; 35 | const typeValue = fromTable.tableName(); 36 | 37 | return toTable.where({[typeField]: typeValue}) 38 | .whereIn(foreignKey, fromModels.map((m) => m[fromTable.key()])) 39 | .all() 40 | ; 41 | } 42 | 43 | matchModels(fromModels=[], relatedModels=[]) { 44 | const {relationName, fromTable, inverse} = this; 45 | const {foreignKey} = inverse; 46 | 47 | const keyDict = relatedModels.reduce((dict, m) => { 48 | return assign(dict, {[m[foreignKey]]: m}); 49 | }, {}); 50 | 51 | return fromModels.map((m) => assign(m, { 52 | [relationName]: isUsableObject(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : null 53 | })); 54 | } 55 | 56 | insert(...args) { 57 | if (args.length === 0) { 58 | throw new Error('bad method call'); 59 | } 60 | 61 | if (args.length === 1) { 62 | return this.insert(this.activeModel, ...args); 63 | } 64 | 65 | const [fromModel, values] = args; 66 | const {fromTable, toTable, inverse} = this; 67 | const {foreignKey, typeField} = inverse; 68 | 69 | return toTable.insert(assign(values, { 70 | [foreignKey]: fromModel[this.key], 71 | [typeField]: fromTable.tableName() 72 | })); 73 | } 74 | 75 | update(...args) { 76 | if (args.length === 0) { 77 | throw new Error('bad method call'); 78 | } 79 | 80 | if (args.length === 1) { 81 | return this.update(this.activeModel, ...args); 82 | } 83 | 84 | const [fromModel, values] = args; 85 | const {fromTable, toTable, inverse} = this; 86 | const {foreignKey, typeField} = inverse; 87 | 88 | return this.constraints.apply(toTable.fork()) 89 | .where(typeField, fromTable.tableName()) 90 | .where(foreignKey, fromModel[fromTable.key()]) 91 | .update(assign(values, { 92 | [foreignKey]: fromModel[fromTable.key()], 93 | [typeField]: fromTable.tableName() 94 | })); 95 | } 96 | 97 | del(...args) { 98 | if (args.length === 0) { 99 | return this.del(this.activeModel); 100 | } 101 | 102 | const [fromModel] = args; 103 | const {fromTable, toTable, inverse} = this; 104 | const {foreignKey, typeField} = inverse; 105 | 106 | return this.constraints.apply(toTable.fork()) 107 | .where(typeField, fromTable.tableName()) 108 | .where(foreignKey, fromModel[fromTable.key()]) 109 | .del() 110 | ; 111 | } 112 | 113 | join(joiner=(() => {}), label=null) { 114 | label = this.jointLabel(label, {}); 115 | const {fromTable, toTable, inverse} = this; 116 | const {foreignKey, typeField} = inverse; 117 | 118 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 119 | return this.ownerTable; 120 | } else { 121 | return this.ownerTable.joint((q) => { 122 | q.join(toTable.tableName(), (j) => { 123 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])) 124 | .on(toTable.c(foreignKey), '=', fromTable.keyCol()); 125 | 126 | joiner(j); 127 | }); 128 | }, label); 129 | } 130 | } 131 | 132 | leftJoin(joiner=(() => {}), label=null) { 133 | label = this.jointLabel(label, {isLeftJoin: true}); 134 | const {fromTable, toTable, inverse} = this; 135 | const {foreignKey, typeField} = inverse; 136 | 137 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 138 | return this.ownerTable; 139 | } else { 140 | return this.ownerTable.joint((q) => { 141 | q.leftJoin(toTable.tableName(), (j) => { 142 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])) 143 | .on(toTable.c(foreignKey), '=', fromTable.keyCol()); 144 | 145 | joiner(j); 146 | }); 147 | }, label); 148 | } 149 | } 150 | } 151 | 152 | module.exports = MorphOne; 153 | -------------------------------------------------------------------------------- /lib/Shape.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 4 | 5 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 6 | 7 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | /** 12 | * Promise based validation of input 13 | * 14 | 15 | const Shape = require('shape-errors'); 16 | const s = new Shape({ 17 | user_id: (userId) => userExists(userId).then((exists) => exists ? null : 'invalid'), 18 | name: (name, {user_id}) => findUserByName(name).then( 19 | (user) => user.id === user_id ? null : 'invalid' 20 | ) 21 | }) 22 | 23 | s.errors(data).then(({result, errors}) => {}) 24 | */ 25 | 26 | var _require = require('lodash'), 27 | assign = _require.assign, 28 | toPlainObject = _require.toPlainObject; 29 | 30 | var isUsableObject = require('isusableobject'); 31 | 32 | var Shape = function () { 33 | function Shape() { 34 | var validations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 35 | 36 | _classCallCheck(this, Shape); 37 | 38 | this.validations = new Map(); 39 | 40 | this.addValidations(validations); 41 | } 42 | 43 | _createClass(Shape, [{ 44 | key: 'addValidations', 45 | value: function addValidations() { 46 | var _this = this; 47 | 48 | var validations = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 49 | 50 | if (isUsableObject(validations)) { 51 | validations = toPlainObject(validations); 52 | validations = Object.keys(validations).map(function (k) { 53 | return { key: k, validation: validations[k] }; 54 | }); 55 | } 56 | 57 | validations.forEach(function (_ref) { 58 | var key = _ref.key, 59 | validation = _ref.validation; 60 | 61 | _this.validations.set(key, validation); 62 | }); 63 | 64 | return this; 65 | } 66 | }, { 67 | key: 'addValidation', 68 | value: function addValidation(_ref2) { 69 | var key = _ref2.key, 70 | validation = _ref2.validation; 71 | 72 | this.validations.set(key, validation); 73 | return this; 74 | } 75 | }, { 76 | key: 'merge', 77 | value: function merge(validator) { 78 | var _this2 = this; 79 | 80 | Array.from(validator.validation.keys()).forEach(function (k) { 81 | _this2.validations.set(k, validator.validations.get(k)); 82 | }); 83 | 84 | return this; 85 | } 86 | }, { 87 | key: 'errors', 88 | value: function errors() { 89 | var _this3 = this; 90 | 91 | var input = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 92 | 93 | var invalidInputKeysErr = Object.keys(input).filter(function (k) { 94 | return Array.from(_this3.validations.keys()).indexOf(k) === -1; 95 | }).reduce(function (err, k) { 96 | return _extends({}, err, _defineProperty({}, k, 'invalid key')); 97 | }, {}); 98 | 99 | return Promise.all(Array.from(this.validations.keys()).map(function (key) { 100 | var err = _this3.validations.get(key)(input[key], input, key); 101 | 102 | if (err instanceof Promise) { 103 | return err.then(function (err) { 104 | return { key: key, err: err }; 105 | }); 106 | } else if (err instanceof Shape) { 107 | return err.errors(input[key]).then(function (err) { 108 | return { key: key, err: err }; 109 | }); 110 | } else { 111 | return { key: key, err: err }; 112 | } 113 | })).then(function (checks) { 114 | if (checks.filter(function (_ref3) { 115 | var err = _ref3.err; 116 | return !!err; 117 | }).length === 0 && Object.keys(invalidInputKeysErr).length === 0) { 118 | return null; 119 | } else { 120 | return checks.reduce(function (all, _ref4) { 121 | var key = _ref4.key, 122 | err = _ref4.err; 123 | 124 | return assign(all, _defineProperty({}, key, err)); 125 | }, invalidInputKeysErr); 126 | } 127 | }); 128 | } 129 | }]); 130 | 131 | return Shape; 132 | }(); 133 | 134 | module.exports = Shape; -------------------------------------------------------------------------------- /test/migrator/testWithDefaultStub.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const rimraf = require('rimraf'); 4 | const assert = require('assert'); 5 | 6 | function run(orm, migrator) { 7 | return cleanup(orm) 8 | .then(() => testMount(orm, migrator)) 9 | .then(() => testMake(orm, migrator)) 10 | .then(() => testLatest(orm, migrator)) 11 | .then(() => testVersion(orm, migrator)) 12 | .then(() => testRollback(orm, migrator)) 13 | .then(() => testRefresh(orm, migrator)) 14 | .then(() => testReset(orm, migrator)) 15 | .then(() => cleanup(orm)) 16 | ; 17 | } 18 | 19 | function testMount(orm, migrator) { 20 | console.log('testing mount'); 21 | 22 | return migrator.mount({ 23 | devDir: path.join(process.cwd(), 'migrations'), 24 | distDir: path.join(process.cwd(), 'migrations'), 25 | args: [] 26 | }); 27 | } 28 | 29 | function testMake(orm, migrator) { 30 | console.log('testing make'); 31 | 32 | const migrationsDir = path.join(process.cwd(), 'migrations'); 33 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 34 | 35 | return migrator.mount({devDir, distDir, args: ['make', 'Foo']}) 36 | .then(() => new Promise((resolve, reject) => fs.readdir(migrationsDir, (err, files) => { 37 | if (err) { 38 | reject(err); 39 | } else { 40 | assert.ok( 41 | files.filter((file) => file.indexOf('Foo.js') > -1, 'migrate make creates correct file') 42 | ); 43 | resolve(); 44 | } 45 | }))); 46 | } 47 | 48 | function testLatest(orm, migrator) { 49 | console.log('testing latest'); 50 | 51 | const {knex} = orm.exports; 52 | const migrationsDir = path.join(process.cwd(), 'migrations'); 53 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 54 | 55 | return new Promise((resolve, reject) => fs.readdir(migrationsDir, (err, files) => { 56 | if (err) { 57 | reject(err); 58 | } else { 59 | resolve(path.join(migrationsDir, files[0])); 60 | } 61 | })).then((migrationFilePath) => { 62 | return new Promise((resolve, reject) => fs.readFile(path.join(__dirname, 'default.migration'), (err, data) => { 63 | if (err) { 64 | reject(err); 65 | } else { 66 | resolve({data, migrationFilePath}); 67 | } 68 | })); 69 | }).then(({data, migrationFilePath}) => { 70 | return new Promise((resolve, reject) => fs.writeFile(migrationFilePath, data, (err) => { 71 | if (err) { 72 | reject(err); 73 | } else { 74 | resolve(); 75 | } 76 | })); 77 | }).then(() => migrator.mount({devDir, distDir, args: ['latest']})) 78 | .then(() => knex.schema.hasTable('test_default')) 79 | .then((result) => { 80 | assert.ok(result, 'migrate latest works'); 81 | }) 82 | ; 83 | } 84 | 85 | function testVersion(orm, migrator) { 86 | console.log('testing version'); 87 | 88 | const migrationsDir = path.join(process.cwd(), 'migrations'); 89 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 90 | 91 | return migrator.mount({devDir, distDir, args: ['version']}); 92 | } 93 | 94 | function testRollback(orm, migrator) { 95 | console.log('testing rollback'); 96 | 97 | const migrationsDir = path.join(process.cwd(), 'migrations'); 98 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 99 | const {knex} = orm.exports; 100 | 101 | return migrator.mount({devDir, distDir, args: ['rollback']}) 102 | .then(() => knex.schema.hasTable('test_default')) 103 | .then((result) => { 104 | assert.ok(!result, 'migrate rollback works'); 105 | }) 106 | ; 107 | } 108 | 109 | function testRefresh(orm, migrator) { 110 | console.log('testing refresh'); 111 | 112 | const migrationsDir = path.join(process.cwd(), 'migrations'); 113 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 114 | const {knex} = orm.exports; 115 | 116 | return migrator.mount({devDir, distDir, args: ['latest']}) 117 | .then(() => migrator.mount({devDir, distDir, args: ['refresh']})) 118 | .then(() => knex.schema.hasTable('test_default')) 119 | .then((result) => { 120 | assert.ok(result, 'migrate reset works'); 121 | }) 122 | ; 123 | } 124 | 125 | function testReset(orm, migrator) { 126 | console.log('testing reset'); 127 | 128 | const migrationsDir = path.join(process.cwd(), 'migrations'); 129 | const [devDir, distDir] = [migrationsDir, migrationsDir]; 130 | const {knex} = orm.exports; 131 | 132 | return migrator.mount({devDir, distDir, args: ['latest']}) 133 | .then(() => migrator.mount({devDir, distDir, args: ['reset']})) 134 | .then(() => knex.schema.hasTable('test_default')) 135 | .then((result) => { 136 | assert.ok(!result, 'migrate reset works'); 137 | }) 138 | ; 139 | } 140 | 141 | function cleanup(orm) { 142 | console.log('cleaning up'); 143 | 144 | const {knex} = orm.exports; 145 | 146 | return Promise.all([ 147 | knex.schema.dropTableIfExists('knex_migrations'), 148 | knex.schema.dropTableIfExists('knex_migrations_lock'), 149 | knex.schema.dropTableIfExists('test_default'), 150 | new Promise((resolve) => ( 151 | rimraf(path.join(process.cwd(), 'migrations'), () => resolve()) 152 | )) 153 | ]); 154 | } 155 | 156 | module.exports = run; 157 | -------------------------------------------------------------------------------- /lib/relations/Relation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 6 | 7 | /** 8 | * Already used methods: 9 | * - setName 10 | * - forModel 11 | * - constrain 12 | * - eagerLoad 13 | * - load 14 | */ 15 | 16 | var _require = require('lodash'), 17 | isString = _require.isString; 18 | 19 | var Scope = require('../Scope'); 20 | var Track = require('../Track'); 21 | 22 | var Relation = function () { 23 | function Relation(ownerTable) { 24 | _classCallCheck(this, Relation); 25 | 26 | this.ownerTable = ownerTable; 27 | this.constraints = new Track(); 28 | this.activeModel = null; 29 | this.relationName = null; 30 | } 31 | 32 | _createClass(Relation, [{ 33 | key: 'setName', 34 | value: function setName(relationName) { 35 | this.relationName = relationName; 36 | return this; 37 | } 38 | }, { 39 | key: 'forModel', 40 | value: function forModel(model) { 41 | this.activeModel = model; 42 | return this; 43 | } 44 | }, { 45 | key: 'constrain', 46 | value: function constrain(constraint) { 47 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'constraint'; 48 | 49 | this.constraints.push(new Scope(constraint, label)); 50 | return this; 51 | } 52 | }, { 53 | key: 'eagerLoad', 54 | value: function eagerLoad() { 55 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 56 | args[_key] = arguments[_key]; 57 | } 58 | 59 | return this.constrain(function (t) { 60 | return t.eagerLoad.apply(t, args); 61 | }, 'eagerLoad'); 62 | } 63 | }, { 64 | key: 'load', 65 | value: function load() { 66 | var _this = this; 67 | 68 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 69 | 70 | if (fromModels.length === 0) { 71 | return Promise.resolve(fromModels); 72 | } 73 | 74 | return this.getRelated(fromModels).then(function (relatedModels) { 75 | return _this.matchModels(_this.initRelation(fromModels), relatedModels); 76 | }); 77 | } 78 | }, { 79 | key: 'initRelation', 80 | value: function initRelation() { 81 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 82 | 83 | throw new Error('not implemented'); 84 | } 85 | }, { 86 | key: 'getRelated', 87 | value: function getRelated() { 88 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 89 | 90 | throw new Error('not implemented'); 91 | } 92 | }, { 93 | key: 'matchModels', 94 | value: function matchModels() { 95 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 96 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 97 | 98 | throw new Error('not implemented'); 99 | } 100 | }, { 101 | key: 'jointLabel', 102 | value: function jointLabel(label, _ref) { 103 | var _ref$isLeftJoin = _ref.isLeftJoin, 104 | isLeftJoin = _ref$isLeftJoin === undefined ? false : _ref$isLeftJoin; 105 | 106 | return (isLeftJoin ? 'leftJoin' : 'join') + '.' + this.constructor.name + '.' + this.relationName + (isString(label) ? '.' + label : ''); 107 | } 108 | }, { 109 | key: 'pivotJointLabel', 110 | value: function pivotJointLabel(label, _ref2) { 111 | var _ref2$isLeftJoin = _ref2.isLeftJoin, 112 | isLeftJoin = _ref2$isLeftJoin === undefined ? false : _ref2$isLeftJoin; 113 | 114 | return this.jointLabel(label, { isLeftJoin: isLeftJoin }) + '.pivot' + (isString(label) ? '.' + label : ''); 115 | } 116 | }, { 117 | key: 'throughJointLabel', 118 | value: function throughJointLabel(label, _ref3) { 119 | var _ref3$isLeftJoin = _ref3.isLeftJoin, 120 | isLeftJoin = _ref3$isLeftJoin === undefined ? false : _ref3$isLeftJoin; 121 | 122 | return this.jointLabel(label, { isLeftJoin: isLeftJoin }) + '.through' + (isString(label) ? '.' + label : ''); 123 | } 124 | }, { 125 | key: 'join', 126 | value: function join() { 127 | throw new Error('not implemented'); 128 | } 129 | }, { 130 | key: 'joinPivot', 131 | value: function joinPivot() { 132 | throw new Error('not imeplemented'); 133 | } 134 | }, { 135 | key: 'joinThrough', 136 | value: function joinThrough() { 137 | throw new Error('not imeplemented'); 138 | } 139 | }]); 140 | 141 | return Relation; 142 | }(); 143 | 144 | module.exports = Relation; -------------------------------------------------------------------------------- /src/relations/MorphMany.js: -------------------------------------------------------------------------------- 1 | const {assign, isArray} = require('lodash'); 2 | 3 | const Relation = require('./Relation'); 4 | const MorphTo = require('./MorphTo'); 5 | 6 | class MorphMany extends Relation { 7 | constructor(ownerTable, toTable, inverse) { 8 | if (!(inverse instanceof MorphTo)) { 9 | throw new Error('inverse should be a MorphTo relation'); 10 | } 11 | 12 | super(ownerTable); 13 | assign(this, {fromTable: ownerTable.fork(), toTable, inverse}); 14 | } 15 | 16 | initRelation(fromModels) { 17 | return fromModels.map((m) => assign(m, {[this.relationName]: []})); 18 | } 19 | 20 | getRelated(...args) { 21 | if (args.length === 0) { 22 | if (this.activeModel !== null) { 23 | return this.getRelated([this.activeModel]); 24 | } else { 25 | return Promise.resolve([]); 26 | } 27 | } 28 | 29 | const [fromModels] = args; 30 | const {fromTable, inverse} = this; 31 | const toTable = this.constraints.apply(this.toTable.fork()); 32 | 33 | const {foreignKey, typeField} = inverse; 34 | const typeValue = fromTable.tableName(); 35 | 36 | return toTable.where({[typeField]: typeValue}) 37 | .whereIn(foreignKey, fromModels.map((m) => m[fromTable.key()])) 38 | .all() 39 | ; 40 | } 41 | 42 | matchModels(fromModels=[], relatedModels=[]) { 43 | const {relationName, fromTable, inverse} = this; 44 | const {foreignKey} = inverse; 45 | 46 | const keyDict = relatedModels.reduce((dict, m) => { 47 | const key = m[foreignKey]; 48 | 49 | if (!isArray(dict[key])) { 50 | return assign(dict, {[key]: [m]}); 51 | } else { 52 | return assign(dict, {[key]: dict[key].concat(m)}); 53 | } 54 | }, {}); 55 | 56 | return fromModels.map((m) => assign(m, { 57 | [relationName]: isArray(keyDict[m[fromTable.key()]]) ? 58 | keyDict[m[fromTable.key()]] : [] 59 | })); 60 | } 61 | 62 | insert(...args) { 63 | if (args.length === 0) { 64 | throw new Error('bad method call'); 65 | } 66 | 67 | if (args.length === 1) { 68 | return this.insert(this.activeModel, ...args); 69 | } 70 | 71 | const [fromModel, values] = args; 72 | const {fromTable, toTable, inverse} = this; 73 | const {foreignKey, typeField} = inverse; 74 | 75 | return toTable.insert((() => { 76 | if (isArray(values)) { 77 | return values.map((v) => assign(v, { 78 | [foreignKey]: fromModel[fromTable.key()], 79 | [typeField]: fromTable.tableName() 80 | })); 81 | } else { 82 | return assign(values, { 83 | [foreignKey]: fromModel[this.key], 84 | [typeField]: fromTable.tableName() 85 | }); 86 | } 87 | })()); 88 | } 89 | 90 | update(...args) { 91 | if (args.length === 0) { 92 | throw new Error('bad method call'); 93 | } 94 | 95 | if (args.length === 1) { 96 | return this.update(this.activeModel, ...args); 97 | } 98 | 99 | const [fromModel, values] = args; 100 | const {fromTable, toTable, inverse} = this; 101 | const {foreignKey, typeField} = inverse; 102 | 103 | return this.constraints.apply(toTable.fork()) 104 | .where(typeField, fromTable.tableName()) 105 | .where(foreignKey, fromModel[fromTable.key()]) 106 | .update(assign(values, { 107 | [foreignKey]: fromModel[fromTable.key()], 108 | [typeField]: fromTable.tableName() 109 | })); 110 | } 111 | 112 | del(...args) { 113 | if (args.length === 0) { 114 | return this.del(this.activeModel); 115 | } 116 | 117 | const [fromModel] = args; 118 | const {fromTable, toTable, inverse} = this; 119 | const {foreignKey, typeField} = inverse; 120 | 121 | return this.constraints.apply(toTable.fork()) 122 | .where(typeField, fromTable.tableName()) 123 | .where(foreignKey, fromModel[fromTable.key()]) 124 | .del() 125 | ; 126 | } 127 | 128 | join(joiner=(() => {}), label=null) { 129 | label = this.jointLabel(label, {}); 130 | const {fromTable, toTable, inverse} = this; 131 | const {foreignKey, typeField} = inverse; 132 | 133 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 134 | return this.ownerTable; 135 | } else { 136 | return this.ownerTable.joint((q) => { 137 | q.join(toTable.tableName(), (j) => { 138 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])) 139 | .on(toTable.c(foreignKey), '=', fromTable.keyCol()); 140 | 141 | joiner(j); 142 | }); 143 | }, label); 144 | } 145 | } 146 | 147 | leftJoin(joiner=(() => {}), label=null) { 148 | label = this.jointLabel(label, {isLeftJoin: true}); 149 | const {fromTable, toTable, inverse} = this; 150 | const {foreignKey, typeField} = inverse; 151 | 152 | if (this.ownerTable.scopeTrackhasJoint(label)) { 153 | return this.ownerTable; 154 | } else { 155 | return this.ownerTable.joint((q) => { 156 | q.leftJoin(toTable.tableName(), (j) => { 157 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])) 158 | .on(toTable.c(foreignKey), '=', fromTable.keyCol()); 159 | 160 | joiner(j); 161 | }); 162 | }, label); 163 | } 164 | } 165 | } 166 | 167 | module.exports = MorphMany; 168 | -------------------------------------------------------------------------------- /src/relations/HasManyThrough.js: -------------------------------------------------------------------------------- 1 | const {assign, isArray} = require('lodash'); 2 | 3 | const Relation = require('./Relation'); 4 | 5 | class HasManyThrough extends Relation { 6 | constructor(ownerTable, toTable, throughTable, firstKey, secondKey, joiner=(() => {})) { 7 | super(ownerTable); 8 | assign(this, {fromTable: ownerTable.fork(), toTable, throughTable, firstKey, secondKey}); 9 | 10 | this.throughFields = [throughTable.key(), firstKey]; 11 | 12 | this.constrain((t) => { 13 | t.scope((q) => { 14 | q.join( 15 | this.throughTable.tableName(), (j) => { 16 | j.on(this.throughTable.keyCol(), '=', this.toTable.c(secondKey)); 17 | joiner(j); 18 | } 19 | ); 20 | }); 21 | }); 22 | } 23 | 24 | withThrough(...throughFields) { 25 | this.throughFields = throughFields.concat([this.throughTable.key(), this.firstKey]); 26 | return this; 27 | } 28 | 29 | initRelation(fromModels) { 30 | return fromModels.map((m) => assign(m, {[this.relationName]: []})); 31 | } 32 | 33 | getRelated(...args) { 34 | if (args.length === 0) { 35 | if (this.activeModel !== null) { 36 | return this.getRelated([this.activeModel]); 37 | } else { 38 | return Promise.resolve([]); 39 | } 40 | } 41 | 42 | const [fromModels] = args; 43 | 44 | const {fromTable, throughTable, firstKey} = this; 45 | const toTable = this.constraints.apply(this.toTable.fork()); 46 | 47 | const fromKeys = fromModels.map((m) => m[fromTable.key()]); 48 | 49 | const cols = ['*'].concat(this.throughFields.map((field) => { 50 | return `${throughTable.c(field)} as ${throughTable.tableName()}__${field}`; 51 | })); 52 | 53 | return toTable.whereIn(throughTable.c(firstKey), fromKeys) 54 | .select(...cols).all().then((relatedModels) => { 55 | return relatedModels.map((model) => { 56 | const through = Object.keys(model) 57 | .filter((field) => field.indexOf(`${throughTable.tableName()}__`) > -1) 58 | .reduce((throughModel, field) => { 59 | const strippedField = field.slice(`${throughTable.tableName()}__`.length); 60 | return assign({}, throughModel, {[strippedField]: model[field]}); 61 | }, {}) 62 | ; 63 | 64 | return assign( 65 | Object.keys(model) 66 | .filter((field) => field.indexOf(`${throughTable.tableName()}__`) === -1) 67 | .reduce((modelWithoutThroughs, field) => { 68 | return assign({}, modelWithoutThroughs, {[field]: model[field]}); 69 | }, {}), 70 | {through} 71 | ); 72 | }); 73 | }); 74 | } 75 | 76 | matchModels(fromModels=[], relatedModels=[]) { 77 | const {relationName, firstKey, fromTable} = this; 78 | 79 | const keyDict = relatedModels.reduce((dict, m) => { 80 | const key = m.through[firstKey]; 81 | 82 | if (!isArray(dict[key])) { 83 | return assign(dict, {[key]: [m]}); 84 | } else { 85 | return assign(dict, {[key]: dict[key].concat([m])}); 86 | } 87 | }, {}); 88 | 89 | return fromModels.map((m) => assign(m, { 90 | [relationName]: isArray(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : [] 91 | })); 92 | } 93 | 94 | join(joiner=(() => {}), label=null) { 95 | label = this.jointLabel(label, {}); 96 | const {throughTable, toTable, secondKey} = this; 97 | 98 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 99 | return this.ownerTable; 100 | } else { 101 | return this.joinThrough().joint((q) => { 102 | q.join(toTable.tableName(), (j) => { 103 | j.on(throughTable.keyCol(), '=', toTable.c(secondKey)); 104 | joiner(j); 105 | }); 106 | }, label); 107 | } 108 | } 109 | 110 | joinThrough(joiner=(() => {}), label=null) { 111 | label = this.throughJointLabel(label, {}); 112 | const {fromTable, throughTable, firstKey} = this; 113 | 114 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 115 | return this.ownerTable; 116 | } else { 117 | return this.ownerTable.joint((q) => { 118 | q.join(throughTable.tableName(), (j) => { 119 | j.on(fromTable.keyCol(), '=', throughTable.c(firstKey)); 120 | joiner(j); 121 | }); 122 | }, label); 123 | } 124 | } 125 | 126 | leftJoin(joiner=(() => {}), label=null) { 127 | label = this.jointLabel(label, {isLeftJoin: true}); 128 | const {throughTable, toTable, secondKey} = this; 129 | 130 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 131 | return this.ownerTable; 132 | } else { 133 | return this.leftJoinThrough().joint((q) => { 134 | q.leftJoin(toTable.tableName(), (j) => { 135 | j.on(throughTable.keyCol(), '=', toTable.c(secondKey)); 136 | joiner(j); 137 | }); 138 | }, label); 139 | } 140 | } 141 | 142 | leftJoinThrough(joiner=(() => {}), label=null) { 143 | label = this.throughJointLabel(label, {isLeftJoin: true}); 144 | const {fromTable, throughTable, firstKey} = this; 145 | 146 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 147 | return this.ownerTable; 148 | } else { 149 | return this.ownerTable.joint((q) => { 150 | q.leftJoin(throughTable.tableName(), (j) => { 151 | j.on(fromTable.keyCol(), '=', throughTable.c(firstKey)); 152 | joiner(j); 153 | }); 154 | }, label); 155 | } 156 | } 157 | } 158 | 159 | module.exports = HasManyThrough; 160 | -------------------------------------------------------------------------------- /test/orm/testInsert.js: -------------------------------------------------------------------------------- 1 | const {isString, range} = require('lodash'); 2 | const faker = require('faker'); 3 | 4 | function testInsert(assert, orm) { 5 | const {knex, table} = orm.exports; 6 | 7 | return (() => { 8 | console.log('testing insert one'); 9 | return Promise.resolve(); 10 | })().then(() => { 11 | return table('users').insert({ 12 | username: faker.internet.userName(), 13 | password: table('users').hashPassword(faker.internet.password()) 14 | }).then((user) => knex('users').where('id', user.id).first().then((knexUser) => ({user, knexUser}))) 15 | .then(({user, knexUser}) => { 16 | assert.ok(user.hasOwnProperty('id'), 'insertion appends id automatically in tables with uuid true'); 17 | assert.deepEqual(knexUser.id, user.id, 'the inserted record exists in db'); 18 | assert.deepEqual(knexUser.username, user.username, 'with same fields'); 19 | assert.deepEqual(knexUser.password, user.password, '...checking another field'); 20 | assert.ok(user.created_at instanceof Date, 'timestamps work for created_at'); 21 | assert.ok(user.updated_at instanceof Date, 'timestamps work for updated_at'); 22 | assert.ok(isString(user.id)); 23 | }) 24 | ; 25 | }).then(() => { 26 | console.log('testing insert many'); 27 | return Promise.resolve(); 28 | }).then(() => { 29 | return knex('users').first() 30 | .then((user) => table('posts').insert(range(4).map(() => ({ 31 | user_id: user.id, 32 | title: faker.lorem.sentence(), 33 | body: faker.lorem.paragraphs(3) 34 | })))) 35 | .then((posts) => Promise.all(posts.map((post) => { 36 | return knex('posts').where('id', post.id).first().then((knexPost) => ({post, knexPost})) 37 | .then(({post, knexPost}) => { 38 | assert.ok('id' in post, 'insertion appends id automatically in tables with uuid true'); 39 | assert.deepEqual(knexPost.id, post.id, 'the inserted record exists in db'); 40 | assert.deepEqual(knexPost.title, post.title, 'with same fields'); 41 | assert.deepEqual(knexPost.body, post.body, '...checking another field'); 42 | assert.ok(post.created_at instanceof Date, 'timestamps work for created_at'); 43 | assert.ok(post.updated_at instanceof Date, 'timestamps work for updated_at'); 44 | assert.ok(isString(post.id)); 45 | }) 46 | ; 47 | }), Promise.resolve({}))) 48 | ; 49 | }).then(() => { 50 | console.log(''); 51 | console.log('now we are just going to insert things in order to fill up the db'); 52 | console.log('and hope that shit works'); 53 | console.log('if it doesn\'t, then we follow the stacktrace and fix broken shit'); 54 | console.log(''); 55 | }).then(() => { 56 | return table('roles').insert(range(3).map(() => ({ 57 | name: faker.name.jobTitle() 58 | }))).then((roles) => knex('users').first().then((user) => ({user, roles}))).then(({user, roles}) => { 59 | return table('user_role').insert(roles.map((role) => ({ 60 | role_id: role.id, user_id: user.id 61 | }))).then((userRolePivots) => ({user, roles, userRolePivots})); 62 | }).then(({user, roles, userRolePivots}) => { 63 | userRolePivots.forEach((pivot) => { 64 | assert.deepEqual(pivot.user_id, user.id, 'pivot has proper user_id'); 65 | assert.ok(roles.map(({id}) => id).indexOf(pivot.role_id) > -1, 'pivot has proper role_id'); 66 | assert.ok(pivot.created_at instanceof Date, 'pivot timestamps work for created_at'); 67 | assert.ok(pivot.updated_at instanceof Date, 'pivot timestamps work for updated_at'); 68 | }); 69 | }).then(() => Promise.all([knex('users').first(), knex('posts').select('*')])).then(([user, posts]) => { 70 | return Promise.all(posts.map((post) => { 71 | return table('comments').insert(range(3).map(() => ({ 72 | user_id: user.id, 73 | post_id: post.id, 74 | text: faker.lorem.paragraphs(1), 75 | is_flagged: false 76 | }))); 77 | })); 78 | }).then(() => Promise.all([knex('users').first(), knex('posts').select('*')])).then(([user, posts]) => { 79 | return table('photos').insert({ 80 | doc_type: 'users', 81 | doc_id: user.id, 82 | url: faker.image.imageUrl() 83 | }).then((photo) => table('photo_details').insert({ 84 | photo_id: photo.id, 85 | title: faker.name.title(), 86 | about: faker.lorem.paragraphs(1) 87 | })).then(() => Promise.all(posts.map((post) => { 88 | return table('photos').insert(range(2).map(() => ({ 89 | doc_type: 'posts', 90 | doc_id: post.id, 91 | url: faker.image.imageUrl() 92 | }))).then((photos) => table('photo_details').insert( 93 | photos.map((photo) => ({ 94 | photo_id: photo.id, 95 | title: faker.name.title(), 96 | about: faker.lorem.paragraphs(1) 97 | }) 98 | ))); 99 | }))); 100 | }).then(() => { 101 | return table('tags').insert(range(4).map(() => ({ 102 | name: faker.name.title() 103 | }))).then((tags) => { 104 | return Promise.all([ 105 | knex('posts').select('*'), 106 | knex('photos').select('*') 107 | ]).then(([posts, photos]) => ({tags, posts, photos})); 108 | }).then(({tags, posts, photos}) => { 109 | return Promise.all(tags.slice(2).map((tag) => { 110 | return table('tagable_tag').insert(posts.map((post) => ({ 111 | tagable_type: 'posts', 112 | tagable_id: post.id, 113 | tag_id: tag.id 114 | }))); 115 | })).then(() => Promise.all(tags.slice(0, 2).map((tag) => { 116 | return table('tagable_tag').insert(photos.map((photo) => ({ 117 | tagable_type: 'photos', 118 | tagable_id: photo.id, 119 | tag_id: tag.id 120 | }))); 121 | }))); 122 | }); 123 | }); 124 | }); 125 | } 126 | 127 | module.exports = testInsert; 128 | -------------------------------------------------------------------------------- /test/orm/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The process we are gonna follow to "test" the orm layer of the system 3 | * is to basically construct a schema (using knex, which is a tested library) 4 | * which can stand against all features offered by this orm. 5 | * 6 | * The first test we run, we define our tables, so now they are available in all 7 | * subsequent tests. 8 | * 9 | * The second test that we run is our test for insertion, using which we populate our tables. 10 | * We check if tables got populated via knex. And knex is what we use to match our results 11 | * for all subsequent tests. 12 | */ 13 | 14 | const assert = require('assert'); 15 | 16 | const Tabel = require('../../src/Orm'); 17 | 18 | const config = require('../config'); 19 | 20 | const testTableDefinitions = require('./testTableDefinitions'); 21 | const testInsert = require('./testInsert'); 22 | const testQueryBuilding = require('./testQueryBuilding'); 23 | const testUpdate = require('./testUpdate'); 24 | const testDelete = require('./testDelete'); 25 | const testEagerLoads = require('./testEagerLoads'); 26 | const testScopesAndJoints = require('./testScopesAndJoints'); 27 | const testRelationJoints = require('./testRelationJoints'); 28 | const testReduce = require('./testReduce'); 29 | const testMap = require('./testMap'); 30 | const testShape = require('./testShape'); 31 | const testCache = require('./testCache'); 32 | const testAutoIncrementIdTables = require('./testAutoIncrementIdTables'); 33 | 34 | // handle promise errors 35 | process.on('unhandledRejection', err => { throw err; }); 36 | 37 | runTests(...process.argv.slice(2)); 38 | 39 | function runTests() { 40 | return (() => { 41 | const orm = new Tabel(config); 42 | 43 | return teardownTables(orm).then(() => setupTables(orm)) 44 | .then(() => [ 45 | testTableDefinitions, 46 | testInsert, 47 | testQueryBuilding, 48 | testUpdate, 49 | testDelete, 50 | testEagerLoads, 51 | testScopesAndJoints, 52 | testRelationJoints, 53 | testReduce, 54 | testMap, 55 | testShape, 56 | testCache, 57 | ].reduce((chain, test) => chain.then(() => test(assert, orm)), Promise.resolve())) 58 | .then(() => teardownTables(orm)) 59 | .then(() => orm.close()) 60 | ; 61 | })().then(() => { 62 | const orm = new Tabel(config); 63 | return teardownAutoIncrementingTables(orm) 64 | .then(() => setupAutoIncrementingTables(orm)) 65 | .then(() => testAutoIncrementIdTables(assert, orm)) 66 | .then(() => teardownAutoIncrementingTables(orm)) 67 | .then(() => orm.close()) 68 | ; 69 | }) 70 | } 71 | 72 | function setupTables({knex}) { 73 | return Promise.all([ 74 | knex.schema.createTable('users', (t) => { 75 | t.uuid('id').primary(); 76 | t.string('username'); 77 | t.string('password'); 78 | t.timestamps(); 79 | }), 80 | 81 | knex.schema.createTable('roles', (t) => { 82 | t.uuid('id').primary(); 83 | t.string('name').unique(); 84 | t.timestamps(); 85 | }), 86 | 87 | knex.schema.createTable('user_role', (t) => { 88 | t.uuid('user_id'); 89 | t.uuid('role_id'); 90 | t.timestamps(); 91 | 92 | t.primary(['user_id', 'role_id']); 93 | }), 94 | 95 | knex.schema.createTable('posts', (t) => { 96 | t.uuid('id').primary(); 97 | t.uuid('user_id'); 98 | t.string('title'); 99 | t.text('body'); 100 | t.timestamp('published_on').nullable().defaultTo(null); 101 | t.timestamps(); 102 | }), 103 | 104 | knex.schema.createTable('comments', (t) => { 105 | t.uuid('id').primary(); 106 | t.uuid('user_id'); 107 | t.uuid('post_id'); 108 | t.string('text', 500); 109 | t.boolean('is_flagged').defaultTo(false); 110 | t.timestamps(); 111 | }), 112 | 113 | knex.schema.createTable('photos', (t) => { 114 | t.uuid('id').primary(); 115 | t.string('doc_type'); 116 | t.uuid('doc_id'); 117 | t.string('url'); 118 | t.timestamps(); 119 | 120 | t.index(['doc_type', 'doc_id']); 121 | }), 122 | 123 | knex.schema.createTable('photo_details', (t) => { 124 | t.uuid('photo_id').primary(); 125 | t.string('title'); 126 | t.string('about', 1000); 127 | t.timestamps(); 128 | }), 129 | 130 | knex.schema.createTable('tags', (t) => { 131 | t.uuid('id').primary(); 132 | t.string('name').unique(); 133 | t.timestamps(); 134 | }), 135 | 136 | knex.schema.createTable('tagable_tag', (t) => { 137 | t.string('tagable_type'); 138 | t.uuid('tagable_id'); 139 | t.uuid('tag_id'); 140 | t.timestamps(); 141 | 142 | t.primary(['tagable_type', 'tagable_id', 'tag_id']); 143 | }) 144 | ]); 145 | } 146 | 147 | function teardownTables({knex}) { 148 | return Promise.all([ 149 | 'users', 'roles', 'user_role', 'posts', 'comments', 'photos', 150 | 'photo_details', 'tags', 'tagable_tag' 151 | ].map((t) => knex.schema.dropTableIfExists(t))); 152 | } 153 | 154 | function setupAutoIncrementingTables({knex}) { 155 | return Promise.all([ 156 | knex.schema.createTable('products', (t) => { 157 | t.increments('id'); 158 | t.integer('category_id').unsigned(); 159 | t.string('name'); 160 | t.timestamps(); 161 | }), 162 | 163 | knex.schema.createTable('categories', (t) => { 164 | t.increments('id'); 165 | t.string('name'); 166 | t.timestamps(); 167 | }), 168 | 169 | knex.schema.createTable('sellers', (t) => { 170 | t.increments('id'); 171 | t.string('name'); 172 | t.timestamps(); 173 | }), 174 | 175 | knex.schema.createTable('product_seller', (t) => { 176 | t.integer('product_id').unsigned(); 177 | t.integer('seller_id').unsigned(); 178 | 179 | t.primary(['product_id', 'seller_id']); 180 | }) 181 | ]); 182 | } 183 | 184 | function teardownAutoIncrementingTables({knex}) { 185 | return Promise.all(['products', 'categories', 'sellers', 'product_seller'].map(t => knex.schema.dropTableIfExists(t))); 186 | } 187 | -------------------------------------------------------------------------------- /src/relations/MorphTo.js: -------------------------------------------------------------------------------- 1 | const {assign, isString} = require('lodash'); 2 | 3 | const Relation = require('./Relation'); 4 | 5 | class MorphTo extends Relation { 6 | constructor(ownerTable, toTables, typeField, foreignKey) { 7 | super(ownerTable); 8 | assign(this, {fromTable: ownerTable.fork(), toTables, typeField, foreignKey}); 9 | } 10 | 11 | initRelation(fromModels=[]) { 12 | return fromModels.map((m) => assign(m, {[this.relationName]: null})); 13 | } 14 | 15 | getRelated(...args) { 16 | if (args.length === 0) { 17 | if (this.activeModel !== null) { 18 | return this.getRelated([this.activeModel]) 19 | .then((results) => { 20 | return results 21 | .filter((r) => r.type === this.activeModel[this.typeField]) 22 | .map(({models}) => models); 23 | }) 24 | .then(([relatedModel]) => relatedModel); 25 | } else { 26 | return Promise.resolve(null); 27 | } 28 | } 29 | 30 | const [fromModels] = args; 31 | const {toTables, typeField, foreignKey} = this; 32 | 33 | if (fromModels.length === 0) { 34 | return Promise.resolve([]); 35 | } else { 36 | return Promise.all(toTables.map((table) => { 37 | const fromKeys = fromModels 38 | .filter((m) => m[typeField] === table.tableName()) 39 | .map((m) => m[foreignKey]) 40 | ; 41 | 42 | return this.constraints.apply(table.fork()) 43 | .whereIn(table.key(), fromKeys).all() 44 | .then((models) => ({type: table.tableName(), models})); 45 | })); 46 | } 47 | } 48 | 49 | matchModels(fromModels=[], relatedModels=[]) { 50 | const {relationName, toTables, typeField, foreignKey} = this; 51 | 52 | const tableKeyDict = relatedModels.reduce((dict, {type, models}) => { 53 | const table = toTables.filter((t) => t.tableName() === type)[0]; 54 | 55 | return assign(dict, { 56 | [type]: models.reduce((keyDict, model) => { 57 | return assign(keyDict, {[model[table.key()]]: model}); 58 | }, {}) 59 | }); 60 | }, {}); 61 | 62 | return fromModels.map((m) => { 63 | if (m[typeField] in tableKeyDict && m[foreignKey] in tableKeyDict[m[typeField]]) { 64 | return assign(m, { 65 | [relationName]: tableKeyDict[m[typeField]][m[foreignKey]] 66 | }); 67 | } else { 68 | return assign(m, {[relationName]: null}); 69 | } 70 | }); 71 | } 72 | 73 | associate(...args) { 74 | if (args.length < 2) { 75 | throw new Error('bad method call'); 76 | } 77 | 78 | if (args.length === 2) { 79 | return this.associate(this.activeModel, ...args); 80 | } 81 | 82 | const [fromModel, relatedModel, tableName] = args; 83 | const table = this.toTables.filter((t) => t.tableName() === tableName)[0]; 84 | 85 | return this.fromTable.fork().whereKey(fromModel).update({ 86 | [this.typeField]: tableName, 87 | [this.foreignKey]: relatedModel[table.key()] 88 | }); 89 | } 90 | 91 | dissociate(...args) { 92 | if (args.length === 0) { 93 | return this.dissociate(this.activeModel); 94 | } 95 | 96 | const [fromModel] = args; 97 | 98 | return this.fromTable.fork().whereKey(fromModel).update({ 99 | [this.typeField]: null, 100 | [this.foreignKey]: null 101 | }); 102 | } 103 | 104 | update(...args) { 105 | if (args.length === 0) { 106 | throw new Error('bad method call'); 107 | } 108 | 109 | if (args.length === 1) { 110 | return this.update(this.activeModel, ...args); 111 | } 112 | 113 | const [fromModel, values] = args; 114 | const table = this.toTables.filter((t) => t.tableName() === fromModel[this.typeField])[0]; 115 | 116 | return this.constraints.apply(table.fork()) 117 | .whereKey(fromModel[this.foreignKey]) 118 | .update(values) 119 | ; 120 | } 121 | 122 | del(...args) { 123 | if (args.length === 0) { 124 | return this.del(this.activeModel); 125 | } 126 | 127 | const [fromModel] = args; 128 | const table = this.toTables.filter((t) => t.tableName() === fromModel[this.typeField])[0]; 129 | 130 | return this.constraints.apply(table.fork()) 131 | .whereKey(fromModel[this.foreignKey]) 132 | .del() 133 | ; 134 | } 135 | 136 | join(tableName, joiner=(() => {}), label=null) { 137 | if (this.toTables.map((t) => t.tableName()).indexOf(tableName) === -1) { 138 | return this.ownerTable; 139 | } 140 | 141 | label = this.jointLabel(`${tableName}${isString(label) ? `.${label}` : ''}`, {}); 142 | const toTable = this.toTables.filter((t) => t.tableName() === tableName)[0]; 143 | const {fromTable, typeField, foreignKey} = this; 144 | 145 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 146 | return this.ownerTable; 147 | } else { 148 | return this.ownerTable.joint((q) => { 149 | q.join(toTable.tableName(), (j) => { 150 | j.on(fromTable.c(typeField), '=', toTable.raw('?', [toTable.tableName()])) 151 | .on(fromTable.c(foreignKey), '=', toTable.keyCol()); 152 | 153 | joiner(j); 154 | }); 155 | }); 156 | } 157 | } 158 | 159 | leftJoin(tableName, joiner=(() => {}), label=null) { 160 | if (this.toTables.map((t) => t.tableName()).indexOf(tableName) === -1) { 161 | return this.ownerTable; 162 | } 163 | 164 | label = this.jointLabel(`${tableName}${isString(label) ? `.${label}` : ''}`, { 165 | isLeftJoin: true 166 | }); 167 | const toTable = this.toTables.filter((t) => t.tableName() === tableName)[0]; 168 | const {fromTable, typeField, foreignKey} = this; 169 | 170 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 171 | return this.ownerTable; 172 | } else { 173 | return this.ownerTable.joint((q) => { 174 | q.leftJoin(toTable.tableName(), (j) => { 175 | j.on(fromTable.c(typeField), '=', toTable.raw('?', [toTable.tableName()])) 176 | .on(fromTable.c(foreignKey), '=', toTable.keyCol()); 177 | 178 | joiner(j); 179 | }); 180 | }); 181 | } 182 | } 183 | } 184 | 185 | module.exports = MorphTo; 186 | -------------------------------------------------------------------------------- /test/orm/testTableDefinitions.js: -------------------------------------------------------------------------------- 1 | const Table = require('../../src/Table'); 2 | 3 | function testTableDefinitions(assert, orm) { 4 | orm.defineTable({ 5 | name: 'users', 6 | 7 | props: { 8 | uuid: true, 9 | timestamps: true 10 | }, 11 | 12 | relations: { 13 | roles() { 14 | return this.manyToMany('roles', 'user_role', 'user_id', 'role_id'); 15 | }, 16 | 17 | posts() { 18 | return this.hasMany('posts', 'user_id'); 19 | }, 20 | 21 | comments() { 22 | return this.hasMany('comments', 'user_id'); 23 | }, 24 | 25 | profilePhoto() { 26 | return this.morphOne('photos', 'doc'); 27 | }, 28 | 29 | receivedComments() { 30 | return this.hasManyThrough('comments', 'posts', 'user_id', 'post_id'); 31 | } 32 | }, 33 | 34 | methods: { 35 | hashPassword(password) { 36 | // this is just dummy stuff, do not use a base64 to 37 | // hash passwords in real life 38 | return new Buffer(password).toString('base64'); 39 | }, 40 | 41 | uuidFlag() { 42 | return this.props.uuid; 43 | } 44 | } 45 | }); 46 | 47 | orm.defineTable({ 48 | name: 'roles', 49 | 50 | props: { 51 | uuid: true, 52 | timestamps: true 53 | }, 54 | 55 | relations: { 56 | users() { 57 | return this.manyToMany('users', 'user_role', 'role_id', 'user_id'); 58 | } 59 | } 60 | }); 61 | 62 | orm.defineTable({ 63 | name: 'user_role', 64 | 65 | props: { 66 | key: ['user_id', 'role_id'], 67 | uuid: false, 68 | timestamps: true 69 | } 70 | }); 71 | 72 | orm.defineTable({ 73 | name: 'posts', 74 | 75 | props: { 76 | uuid: true, 77 | timestamps: true 78 | }, 79 | 80 | relations: { 81 | author() { 82 | return this.belongsTo('users', 'user_id'); 83 | }, 84 | 85 | comments() { 86 | return this.hasMany('comments', 'post_id'); 87 | }, 88 | 89 | photos() { 90 | return this.morphMany('photos', 'doc'); 91 | } 92 | } 93 | }); 94 | 95 | orm.defineTable({ 96 | name: 'comments', 97 | 98 | props: { 99 | uuid: true, 100 | timestamps: true 101 | }, 102 | 103 | relations: { 104 | user() { 105 | return this.belongsTo('users', 'user_id'); 106 | }, 107 | 108 | post() { 109 | return this.belongsTo('posts', 'post_id'); 110 | }, 111 | 112 | photos() { 113 | return this.morphMany('photos', 'doc'); 114 | } 115 | }, 116 | 117 | scopes: { 118 | whereNotFlagged() { 119 | return this.whereNot('is_flagged', true); 120 | }, 121 | 122 | whereFlagged() { 123 | return this.where('is_flagged', true); 124 | } 125 | } 126 | }); 127 | 128 | orm.defineTable({ 129 | name: 'photos', 130 | 131 | props: { 132 | uuid: true, 133 | timestamps: true 134 | }, 135 | 136 | relations: { 137 | doc() { 138 | return this.morphTo(['users', 'posts', 'comments'], 'doc_type', 'doc_id'); 139 | }, 140 | 141 | detail() { 142 | return this.hasOne('photo_details', 'photo_id'); 143 | } 144 | } 145 | }); 146 | 147 | orm.defineTable({ 148 | name: 'photo_details', 149 | 150 | props: { 151 | timestamps: true 152 | }, 153 | 154 | relations: { 155 | photo() { 156 | return this.belongsTo('photos', 'photo_id'); 157 | } 158 | } 159 | }); 160 | 161 | orm.defineTable({ 162 | name: 'tags', 163 | 164 | props: { 165 | uuid: true, 166 | timestamps: true 167 | }, 168 | 169 | relations: { 170 | posts() { 171 | return this.tagables('posts'); 172 | }, 173 | 174 | photos() { 175 | return this.tagables('photos'); 176 | } 177 | }, 178 | 179 | methods: { 180 | tagables(table) { 181 | return this.manyToMany(table, 'tagable_tag', 'tag_id', 'tagable_id', (j) => { 182 | j.on('tagable_tag.tagable_type', '=', this.raw('?', [table])); 183 | }).withPivot('tagable_type'); 184 | }, 185 | 186 | joinTagables(table) { 187 | return this.tagables(table).join((j) => { 188 | j.on('tagable_tag.tagable_type', '=', this.raw('?', ['posts'])); 189 | }, table); 190 | } 191 | }, 192 | 193 | joints: { 194 | joinPosts() { 195 | return this.joinTagables('posts'); 196 | }, 197 | 198 | joinPhotos() { 199 | return this.joinTagables('photos'); 200 | } 201 | } 202 | }); 203 | 204 | orm.defineTable({ 205 | name: 'tagable_tag', 206 | 207 | props: { 208 | key: ['tagable_type', 'tagable_id', 'tag_id'], 209 | timestamps: true 210 | } 211 | }); 212 | 213 | console.log('testing defined tables'); 214 | 215 | // we reached here means tables have been defined as expected 216 | Array.from(orm.tables.keys()).forEach((tableName) => { 217 | assert.ok(orm.tbl(tableName) instanceof Table, `orm.tbl(${tableName}) instance of Table`); 218 | }); 219 | 220 | // check to see if methods work as expected 221 | assert.deepEqual(orm.tbl('users').uuidFlag(), true, 'method test#1 pass'); 222 | assert.deepEqual( 223 | orm.tbl('users').hashPassword('foo'), 224 | new Buffer('foo').toString('base64'), 225 | 'method test #2 pass' 226 | ); 227 | 228 | // checking relation definitions 229 | Array.from(orm.tbl('tags').definedRelations).forEach((rel) => { 230 | assert.notEqual(['posts', 'users', 'photos'].indexOf(rel), -1, `${rel} is valid relation`); 231 | }); 232 | 233 | // checking method definitions 234 | Array.from(orm.tbl('tags').definedMethods).forEach((method) => { 235 | assert.notEqual(['tagables', 'joinTagables'].indexOf(method), -1, `${method} is valid method`); 236 | }); 237 | 238 | // checking joints defintions 239 | Array.from(orm.tbl('tags').definedJoints).forEach((joint) => { 240 | assert.notEqual(['joinPosts', 'joinUsers', 'joinPhotos'].indexOf(joint), -1, `${joint} is valid joint`); 241 | }); 242 | 243 | // checking scope definitions 244 | Array.from(orm.tbl('comments').definedScopes).forEach((scope) => { 245 | assert.notEqual(['whereFlagged', 'whereNotFlagged'].indexOf(scope), -1, `${scope} is valid scope`); 246 | }); 247 | } 248 | 249 | module.exports = testTableDefinitions; 250 | -------------------------------------------------------------------------------- /lib/relations/HasMany.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 10 | 11 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 12 | 13 | var _require = require('lodash'), 14 | assign = _require.assign, 15 | isArray = _require.isArray; 16 | 17 | var Relation = require('./Relation'); 18 | 19 | var HasMany = function (_Relation) { 20 | _inherits(HasMany, _Relation); 21 | 22 | function HasMany(ownerTable, toTable, foreignKey, key) { 23 | _classCallCheck(this, HasMany); 24 | 25 | var _this = _possibleConstructorReturn(this, (HasMany.__proto__ || Object.getPrototypeOf(HasMany)).call(this, ownerTable)); 26 | 27 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, foreignKey: foreignKey, key: key }); 28 | return _this; 29 | } 30 | 31 | _createClass(HasMany, [{ 32 | key: 'initRelation', 33 | value: function initRelation(fromModels) { 34 | var _this2 = this; 35 | 36 | return fromModels.map(function (m) { 37 | return assign(m, _defineProperty({}, _this2.relationName, [])); 38 | }); 39 | } 40 | }, { 41 | key: 'getRelated', 42 | value: function getRelated() { 43 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 44 | args[_key] = arguments[_key]; 45 | } 46 | 47 | if (args.length === 0) { 48 | if (this.activeModel !== null) { 49 | return this.getRelated([this.activeModel]); 50 | } else { 51 | return Promise.resolve([]); 52 | } 53 | } 54 | 55 | var fromModels = args[0]; 56 | var toTable = this.toTable, 57 | foreignKey = this.foreignKey, 58 | key = this.key; 59 | 60 | 61 | return this.constraints.apply(toTable.fork()).whereIn(foreignKey, fromModels.map(function (m) { 62 | return m[key]; 63 | })).all(); 64 | } 65 | }, { 66 | key: 'matchModels', 67 | value: function matchModels() { 68 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 69 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 70 | var relationName = this.relationName, 71 | foreignKey = this.foreignKey, 72 | key = this.key; 73 | 74 | 75 | var keyDict = relatedModels.reduce(function (dict, m) { 76 | var key = m[foreignKey]; 77 | 78 | if (!isArray(dict[key])) { 79 | return assign(dict, _defineProperty({}, key, [m])); 80 | } else { 81 | return assign(dict, _defineProperty({}, key, dict[key].concat(m))); 82 | } 83 | }, {}); 84 | 85 | return fromModels.map(function (m) { 86 | return assign(m, _defineProperty({}, relationName, isArray(keyDict[m[key]]) ? keyDict[m[key]] : [])); 87 | }); 88 | } 89 | }, { 90 | key: 'insert', 91 | value: function insert() { 92 | var _this3 = this; 93 | 94 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 95 | args[_key2] = arguments[_key2]; 96 | } 97 | 98 | if (args.length === 0) { 99 | throw new Error('bad method call'); 100 | } 101 | 102 | if (args.length === 1) { 103 | return this.insert.apply(this, [this.activeModel].concat(args)); 104 | } 105 | 106 | var fromModel = args[0], 107 | values = args[1]; 108 | 109 | 110 | return this.toTable.insert(function () { 111 | if (isArray(values)) { 112 | return values.map(function (v) { 113 | return assign(v, _defineProperty({}, _this3.foreignKey, fromModel[_this3.key])); 114 | }); 115 | } else { 116 | return assign(values, _defineProperty({}, _this3.foreignKey, fromModel[_this3.key])); 117 | } 118 | }()); 119 | } 120 | }, { 121 | key: 'update', 122 | value: function update() { 123 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 124 | args[_key3] = arguments[_key3]; 125 | } 126 | 127 | if (args.length === 0) { 128 | throw new Error('bad method call'); 129 | } 130 | 131 | if (args.length === 1) { 132 | return this.update.apply(this, [this.activeModel].concat(args)); 133 | } 134 | 135 | var fromModel = args[0], 136 | values = args[1]; 137 | 138 | 139 | return this.constraints.apply(this.toTable.fork()).where(this.foreignKey, fromModel[this.key]).update(assign(values, _defineProperty({}, this.foreignKey, fromModel[this.key]))); 140 | } 141 | }, { 142 | key: 'del', 143 | value: function del() { 144 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 145 | args[_key4] = arguments[_key4]; 146 | } 147 | 148 | if (args.length === 0) { 149 | return this.del(this.activeModel); 150 | } 151 | 152 | var fromModel = args[0]; 153 | 154 | 155 | return this.constraints.apply(this.toTable.fork()).where(this.foreignKey, fromModel[this.key]).del(); 156 | } 157 | }, { 158 | key: 'join', 159 | value: function join() { 160 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 161 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 162 | 163 | label = this.jointLabel(label, {}); 164 | var fromTable = this.fromTable, 165 | toTable = this.toTable, 166 | foreignKey = this.foreignKey, 167 | key = this.key; 168 | 169 | 170 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 171 | return this.ownerTable; 172 | } else { 173 | return this.ownerTable.joint(function (q) { 174 | q.join(toTable.tableName(), function (j) { 175 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 176 | joiner(j); 177 | }); 178 | }, label); 179 | } 180 | } 181 | }, { 182 | key: 'leftJoin', 183 | value: function leftJoin() { 184 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 185 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 186 | 187 | label = this.jointLabel(label, { isLeftJoin: true }); 188 | var fromTable = this.fromTable, 189 | toTable = this.toTable, 190 | foreignKey = this.foreignKey, 191 | key = this.key; 192 | 193 | 194 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 195 | return this.ownerTable; 196 | } else { 197 | return this.ownerTable.joint(function (q) { 198 | q.leftJoin(toTable.tableName(), function (j) { 199 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 200 | joiner(j); 201 | }); 202 | }, label); 203 | } 204 | } 205 | }]); 206 | 207 | return HasMany; 208 | }(Relation); 209 | 210 | module.exports = HasMany; -------------------------------------------------------------------------------- /lib/relations/HasOne.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 6 | 7 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 14 | 15 | var _require = require('lodash'), 16 | assign = _require.assign; 17 | 18 | var isUsableObject = require('isusableobject'); 19 | 20 | var Relation = require('./Relation'); 21 | 22 | var HasOne = function (_Relation) { 23 | _inherits(HasOne, _Relation); 24 | 25 | function HasOne(ownerTable, toTable, foreignKey, key) { 26 | _classCallCheck(this, HasOne); 27 | 28 | var _this = _possibleConstructorReturn(this, (HasOne.__proto__ || Object.getPrototypeOf(HasOne)).call(this, ownerTable)); 29 | 30 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, foreignKey: foreignKey, key: key }); 31 | return _this; 32 | } 33 | 34 | _createClass(HasOne, [{ 35 | key: 'initRelation', 36 | value: function initRelation(fromModels) { 37 | var _this2 = this; 38 | 39 | return fromModels.map(function (m) { 40 | return assign(m, _defineProperty({}, _this2.relationName, null)); 41 | }); 42 | } 43 | }, { 44 | key: 'getRelated', 45 | value: function getRelated() { 46 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 47 | args[_key] = arguments[_key]; 48 | } 49 | 50 | if (args.length === 0) { 51 | if (this.activeModel !== null) { 52 | return this.getRelated([this.activeModel]).then(function (_ref) { 53 | var _ref2 = _slicedToArray(_ref, 1), 54 | relatedModel = _ref2[0]; 55 | 56 | return relatedModel; 57 | }); 58 | } else { 59 | return Promise.resolve(null); 60 | } 61 | } 62 | 63 | var fromModels = args[0]; 64 | var toTable = this.toTable, 65 | foreignKey = this.foreignKey, 66 | key = this.key; 67 | 68 | 69 | return this.constraints.apply(toTable.fork()).whereIn(foreignKey, fromModels.map(function (m) { 70 | return m[key]; 71 | })).all(); 72 | } 73 | }, { 74 | key: 'matchModels', 75 | value: function matchModels() { 76 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 77 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 78 | var relationName = this.relationName, 79 | foreignKey = this.foreignKey, 80 | key = this.key; 81 | 82 | 83 | var keyDict = relatedModels.reduce(function (dict, m) { 84 | return assign(dict, _defineProperty({}, m[foreignKey], m)); 85 | }, {}); 86 | 87 | return fromModels.map(function (m) { 88 | return assign(m, _defineProperty({}, relationName, isUsableObject(keyDict[m[key]]) ? keyDict[m[key]] : null)); 89 | }); 90 | } 91 | }, { 92 | key: 'insert', 93 | value: function insert() { 94 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 95 | args[_key2] = arguments[_key2]; 96 | } 97 | 98 | if (args.length === 0) { 99 | throw new Error('bad method call'); 100 | } 101 | 102 | if (args.length === 1) { 103 | return this.insert.apply(this, [this.activeModel].concat(args)); 104 | } 105 | 106 | var fromModel = args[0], 107 | values = args[1]; 108 | 109 | 110 | return this.toTable.insert(assign(values, _defineProperty({}, this.foreignKey, fromModel[this.key]))); 111 | } 112 | }, { 113 | key: 'update', 114 | value: function update() { 115 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 116 | args[_key3] = arguments[_key3]; 117 | } 118 | 119 | if (args.length === 0) { 120 | throw new Error('bad method call'); 121 | } 122 | 123 | if (args.length === 1) { 124 | return this.update.apply(this, [this.activeModel].concat(args)); 125 | } 126 | 127 | var fromModel = args[0], 128 | values = args[1]; 129 | 130 | 131 | return this.constraints.apply(this.toTable.fork()).where(this.foreignKey, fromModel[this.key]).update(assign(values, _defineProperty({}, this.foreignKey, fromModel[this.key]))); 132 | } 133 | }, { 134 | key: 'del', 135 | value: function del() { 136 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 137 | args[_key4] = arguments[_key4]; 138 | } 139 | 140 | if (args.length === 0) { 141 | return this.del(this.activeModel); 142 | } 143 | 144 | var fromModel = args[0]; 145 | 146 | 147 | return this.constraints.apply(this.toTable.fork()).where(this.foreignKey, fromModel).del(); 148 | } 149 | }, { 150 | key: 'join', 151 | value: function join() { 152 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 153 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 154 | 155 | label = this.jointLabel(label, {}); 156 | var fromTable = this.fromTable, 157 | toTable = this.toTable, 158 | foreignKey = this.foreignKey, 159 | key = this.key; 160 | 161 | 162 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 163 | return this.ownerTable; 164 | } else { 165 | return this.ownerTable.joint(function (q) { 166 | q.join(toTable.tableName(), function (j) { 167 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 168 | joiner(j); 169 | }); 170 | }, label); 171 | } 172 | } 173 | }, { 174 | key: 'leftJoin', 175 | value: function leftJoin() { 176 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 177 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 178 | 179 | label = this.jointLabel(label, { isLeftJoin: true }); 180 | var fromTable = this.fromTable, 181 | toTable = this.toTable, 182 | foreignKey = this.foreignKey, 183 | key = this.key; 184 | 185 | 186 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 187 | return this.ownerTable; 188 | } else { 189 | return this.ownerTable.joint(function (q) { 190 | q.leftJoin(toTable.tableName(), function (j) { 191 | j.on(toTable.c(foreignKey), '=', fromTable.c(key)); 192 | joiner(j); 193 | }); 194 | }, label); 195 | } 196 | } 197 | }]); 198 | 199 | return HasOne; 200 | }(Relation); 201 | 202 | module.exports = HasOne; -------------------------------------------------------------------------------- /test/orm/testEagerLoads.js: -------------------------------------------------------------------------------- 1 | const {assign, isArray} = require('lodash'); 2 | 3 | function testEagerLoads(assert, orm) { 4 | const {table} = orm.exports; 5 | 6 | return Promise.all(Array.from(orm.tables.keys()).map((tableName) => ( 7 | table(tableName).all().then((models) => ({tableName, models})) 8 | ))).then((results) => results.reduce((all, {tableName, models}) => ( 9 | assign(all, {[tableName]: models}) 10 | ), {})).then((all) => { 11 | return (() => { 12 | console.log('testing belongsTo, hasMany, morphMany eagerload'); 13 | 14 | return table('posts').eagerLoad('author', 'comments', 'photos').all() 15 | .then((posts) => posts.forEach((post) => { 16 | assert.deepEqual(post.user_id, post.author.id, 'related author loaded'); 17 | assert.ok(all.users.map(({id}) => id).indexOf(post.author.id) > -1, 'valid author loaded'); 18 | 19 | post.comments.forEach((comment) => { 20 | assert.deepEqual(comment.post_id, post.id, 'related comment loaded'); 21 | assert.ok(all.comments.map(({id}) => id).indexOf(comment.id) > -1, 'valid comment loaded'); 22 | }); 23 | 24 | post.photos.forEach((photo) => { 25 | assert.ok( 26 | photo.doc_type === 'posts' && photo.doc_id === post.id, 27 | 'related photo loaded' 28 | ); 29 | assert.ok(all.photos.map(({id}) => id).indexOf(photo.id) > -1, 'valid photo loaded'); 30 | }); 31 | })) 32 | ; 33 | })().then(() => { 34 | console.log('testing hasManyThrough, manyToMany, morphOne eagerload'); 35 | 36 | return table('users').eagerLoad('receivedComments', 'roles', 'profilePhoto').all().then((users) => { 37 | users.forEach((user) => { 38 | user.receivedComments.forEach((comment) => { 39 | assert.ok( 40 | comment.through.user_id === user.id && comment.through.id === comment.post_id, 41 | 'related comment loaded' 42 | ); 43 | assert.ok(all.comments.map(({id}) => id).indexOf(comment.id) > -1, 'valid comment loaded'); 44 | }); 45 | 46 | user.roles.forEach((role) => { 47 | assert.ok( 48 | role.pivot.user_id === user.id && role.pivot.role_id === role.id, 49 | 'related roles loaded' 50 | ); 51 | assert.ok(all.roles.map(({id}) => id).indexOf(role.id) > -1, 'valid role loaded'); 52 | }); 53 | 54 | assert.ok( 55 | user.profilePhoto.doc_type === 'users' && user.profilePhoto.doc_id === user.id, 56 | 'related photo loaded' 57 | ); 58 | assert.ok(all.photos.map(({id}) => id).indexOf(user.profilePhoto.id) > -1, 'valid photo loaded'); 59 | }); 60 | }); 61 | }).then(() => { 62 | console.log('testing morphTo, hasOne eagerload'); 63 | 64 | return table('photos').eagerLoad('doc', 'detail').all().then((photos) => photos.forEach((photo) => { 65 | assert.ok( 66 | photo.doc_id === photo.doc.id, 67 | 'related doc loaded' 68 | ); 69 | 70 | assert.ok( 71 | all[photo.doc_type].map(({id}) => id).indexOf(photo.doc.id) > -1, 72 | 'valid doc loaded' 73 | ); 74 | 75 | assert.ok( 76 | photo.id === photo.detail.photo_id, 77 | 'related photo_detail loaded' 78 | ); 79 | assert.ok( 80 | all.photo_details.map(({photo_id}) => photo_id).indexOf(photo.detail.photo_id) > -1, 81 | 'valid photo_detail loaded' 82 | ); 83 | })); 84 | }).then(() => { 85 | console.log('testing one level eagerLoads with constraints'); 86 | }).then(() => { 87 | console.log('testing hasMany'); 88 | 89 | return table('posts').whereKey(all.posts.slice(0, 2).map(({id}) => id)).update({published_on: new Date()}) 90 | .then(() => table('users').eagerLoad({posts(t) { t.whereNotNull('published_on'); }}).all()) 91 | .then((users) => users.forEach((user) => { 92 | user.posts.forEach((post) => { 93 | assert.ok( 94 | post.published_on instanceof Date, 95 | 'valid post eagerLoaded' 96 | ); 97 | }); 98 | })) 99 | .then(() => table('posts').whereKey(all.posts.slice(0, 2).map(({id}) => id)).update({published_on: null})) 100 | ; 101 | }).then(() => { 102 | console.log('testing belongsTo'); 103 | 104 | return table('posts').eagerLoad({author: (t) => t.where('id', 'in', all.users.slice(0, 2).map(({id}) => id))}).all() 105 | .then((posts) => posts.forEach((post) => { 106 | assert.ok( 107 | post.author === null || 108 | all.users.slice(0, 2).map(({id}) => id).indexOf(post.author.id) > -1 109 | ); 110 | })) 111 | ; 112 | }).then(() => { 113 | console.log('testing hasOne'); 114 | 115 | return table('photos').eagerLoad( 116 | {detail: (t) => t.where('photo_id', 'in', all.photo_details.slice(0, 2).map(({photo_id}) => photo_id))} 117 | ).all().then((photos) => photos.forEach((photo) => { 118 | assert.ok( 119 | photo.detail === null || 120 | all.photo_details.slice(0, 2).map(({photo_id}) => photo_id).indexOf(photo.detail.photo_id) > -1 121 | ); 122 | })); 123 | }).then(() => { 124 | console.log('testing hasManyThrough'); 125 | 126 | return table('posts').whereKey(all.posts.slice(0, 2).map(({id}) => id)).update({published_on: new Date()}) 127 | .then(() => table('users').eagerLoad({receivedComments: (t) => t.whereNotNull('posts.published_on')}).all()) 128 | .then((users) => users.forEach((user) => { 129 | assert.ok(isArray(user.receivedComments), 'receivedComments should be an array'); 130 | user.receivedComments.forEach((comment) => { 131 | assert.ok(all.posts.map(({id}) => id).indexOf(comment.through.id) > -1); 132 | }); 133 | })) 134 | .then(() => table('posts').whereKey(all.posts.slice(0, 2).map(({id}) => id)).update({published_on: null})) 135 | ; 136 | }).then(() => { 137 | console.log('testing manyToMany'); 138 | 139 | return table('users').eagerLoad({roles: (t) => t.where('id', 'in', all.roles.slice(0, 2).map(({id}) => id))}).all() 140 | .then((users) => users.forEach((user) => { 141 | assert.ok(isArray(user.roles), 'roles are an array'); 142 | user.roles.forEach((role) => { 143 | assert.ok(all.roles.slice(0, 2).map(({id}) => id).indexOf(role.id) > -1); 144 | }); 145 | })) 146 | ; 147 | }).then(() => { 148 | console.log('testing morphMany'); 149 | 150 | return table('posts').eagerLoad( 151 | {photos: (t) => t.where('id', 'in', all.photos.filter(({doc_type}) => doc_type === 'posts').slice(0, 2).map(({id}) => id))} 152 | ).all().then((posts) => posts.forEach((post) => { 153 | assert.ok(isArray(post.photos), 'photos are an array'); 154 | post.photos.forEach((photo) => { 155 | assert.ok( 156 | all.photos.filter(({doc_type}) => doc_type === 'posts') 157 | .slice(0, 2).map(({id}) => id).indexOf(photo.id) > -1 158 | ); 159 | }); 160 | })); 161 | }).then(() => { 162 | console.log('testing morphOne'); 163 | 164 | return table('users').eagerLoad( 165 | {profilePhoto: (t) => t.where('id', 'in', all.photos.filter(({doc_type}) => doc_type === 'users').slice(0, 2).map(({id}) => id))} 166 | ).all().then((users) => users.forEach((user) => { 167 | assert.ok( 168 | user.profilePhoto === null || 169 | all.photos.filter(({doc_type}) => doc_type === 'users') 170 | .slice(0, 2).map(({id}) => id).indexOf(user.profilePhoto.id) > -1 171 | ); 172 | })); 173 | }).then(() => { 174 | console.log('testing nested eagerLoads'); 175 | }).then(() => { 176 | return table('users').eagerLoad('posts.comments.user', 'profilePhoto').all() 177 | .then((users) => users.forEach((user) => { 178 | assert.ok(user.profilePhoto, 'correct profile photo loaded'); 179 | user.posts.forEach((post) => { 180 | assert.ok(post, 'correct post loaded'); 181 | post.comments.forEach((comment) => { 182 | assert.ok(comment, 'correct comment loaded'); 183 | assert.ok(comment.user, 'correct comment-user loaded'); 184 | }); 185 | }); 186 | })) 187 | ; 188 | }).then(() => { 189 | return table('posts').eagerLoad( 190 | {'comments.user': (t) => t.where('id', 'in', all.users.slice(0, 2).map(({id}) => id))}, 191 | 'photos' 192 | ).all().then((posts) => posts.forEach((post) => { 193 | post.comments.forEach((comment) => { 194 | assert.ok(comment); 195 | assert.ok( 196 | comment.user === null || 197 | all.users.slice(0, 2).map(({id}) => id).indexOf(comment.user.id) > -1 198 | ); 199 | }); 200 | })); 201 | }); 202 | }).then(() => console.log('we will be lazy and not test constrained nested eagerLoads anymore')); 203 | } 204 | 205 | module.exports = testEagerLoads; 206 | -------------------------------------------------------------------------------- /lib/relations/BelongsTo.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 6 | 7 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 14 | 15 | var _require = require('lodash'), 16 | assign = _require.assign; 17 | 18 | var isUsableObject = require('isusableobject'); 19 | 20 | var Relation = require('./Relation'); 21 | 22 | var BelongsTo = function (_Relation) { 23 | _inherits(BelongsTo, _Relation); 24 | 25 | function BelongsTo(ownerTable, toTable, foreignKey, otherKey) { 26 | _classCallCheck(this, BelongsTo); 27 | 28 | var _this = _possibleConstructorReturn(this, (BelongsTo.__proto__ || Object.getPrototypeOf(BelongsTo)).call(this, ownerTable)); 29 | 30 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, foreignKey: foreignKey, otherKey: otherKey }); 31 | return _this; 32 | } 33 | 34 | _createClass(BelongsTo, [{ 35 | key: 'initRelation', 36 | value: function initRelation() { 37 | var _this2 = this; 38 | 39 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 40 | 41 | return fromModels.map(function (model) { 42 | return assign(model, _defineProperty({}, _this2.relationName, null)); 43 | }); 44 | } 45 | }, { 46 | key: 'getRelated', 47 | value: function getRelated() { 48 | var _this3 = this; 49 | 50 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 51 | args[_key] = arguments[_key]; 52 | } 53 | 54 | if (args.length === 0) { 55 | if (this.activeModel !== null) { 56 | return this.getRelated([this.activeModel]).then(function (_ref) { 57 | var _ref2 = _slicedToArray(_ref, 1), 58 | relatedModel = _ref2[0]; 59 | 60 | return relatedModel; 61 | }); 62 | } else { 63 | return Promise.resolve(null); 64 | } 65 | } 66 | 67 | var fromModels = args[0]; 68 | 69 | 70 | if (fromModels.length === 0) { 71 | return Promise.resolve([]); 72 | } else { 73 | var foreignKeys = fromModels.filter(function (m) { 74 | return !!m; 75 | }).map(function (m) { 76 | return m[_this3.foreignKey]; 77 | }); 78 | 79 | return this.constraints.apply(this.toTable.fork()).whereIn(this.otherKey, foreignKeys).all(); 80 | } 81 | } 82 | }, { 83 | key: 'matchModels', 84 | value: function matchModels() { 85 | var _this4 = this; 86 | 87 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 88 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 89 | 90 | var keyDict = relatedModels.reduce(function (dict, m) { 91 | return assign(dict, _defineProperty({}, m[_this4.otherKey], m)); 92 | }, {}); 93 | 94 | return fromModels.map(function (m) { 95 | return assign(m, _defineProperty({}, _this4.relationName, isUsableObject(keyDict[m[_this4.foreignKey]]) ? keyDict[m[_this4.foreignKey]] : null)); 96 | }); 97 | } 98 | }, { 99 | key: 'associate', 100 | value: function associate() { 101 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 102 | args[_key2] = arguments[_key2]; 103 | } 104 | 105 | if (args.length === 0) { 106 | throw new Error('bad method call'); 107 | } 108 | 109 | if (args.length === 1) { 110 | return this.associate.apply(this, [this.activeModel].concat(args)); 111 | } 112 | 113 | var fromModel = args[0], 114 | toModel = args[1]; 115 | 116 | 117 | return this.fromTable.whereKey(fromModel).update(_defineProperty({}, this.foreignKey, toModel[this.otherKey])); 118 | } 119 | }, { 120 | key: 'dissociate', 121 | value: function dissociate() { 122 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 123 | args[_key3] = arguments[_key3]; 124 | } 125 | 126 | if (args.length === 0) { 127 | return this.dissociate(this.activeModel); 128 | } 129 | 130 | var fromModel = args[0]; 131 | 132 | 133 | return this.fromTable.whereKey(fromModel).update(_defineProperty({}, this.foreignKey, null)); 134 | } 135 | }, { 136 | key: 'update', 137 | value: function update() { 138 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 139 | args[_key4] = arguments[_key4]; 140 | } 141 | 142 | if (args.length === 0) { 143 | throw new Error('bad method call'); 144 | } 145 | 146 | if (args.length === 1) { 147 | return this.update.apply(this, [this.activeModel].concat(args)); 148 | } 149 | 150 | var fromModel = args[0], 151 | values = args[1]; 152 | 153 | 154 | return this.constraints.apply(this.toTable.fork()).where(this.otherKey, fromModel[this.foreignKey]).update(values); 155 | } 156 | }, { 157 | key: 'del', 158 | value: function del() { 159 | for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { 160 | args[_key5] = arguments[_key5]; 161 | } 162 | 163 | if (args.length === 0) { 164 | return this.del(this.activeModel); 165 | } 166 | 167 | var fromModel = args[0]; 168 | 169 | 170 | return this.constraints.apply(this.toTable.fork()).where(this.otherKey, fromModel[this.foreignKey]).del(); 171 | } 172 | }, { 173 | key: 'join', 174 | value: function join() { 175 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 176 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 177 | 178 | label = this.jointLabel(label, {}); 179 | var fromTable = this.fromTable, 180 | toTable = this.toTable, 181 | foreignKey = this.foreignKey, 182 | otherKey = this.otherKey; 183 | 184 | 185 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 186 | return this.ownerTable; 187 | } else { 188 | return this.ownerTable.joint(function (q) { 189 | q.join(toTable.tableName(), function (j) { 190 | j.on(fromTable.c(foreignKey), '=', toTable.c(otherKey)); 191 | joiner(j); 192 | }); 193 | }, label); 194 | } 195 | } 196 | }, { 197 | key: 'leftJoin', 198 | value: function leftJoin() { 199 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 200 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 201 | 202 | label = this.jointLabel(label, { isLeftJoin: true }); 203 | var fromTable = this.fromTable, 204 | toTable = this.toTable, 205 | foreignKey = this.foreignKey, 206 | otherKey = this.otherKey; 207 | 208 | 209 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 210 | return this.ownerTable; 211 | } else { 212 | return this.ownerTable.joint(function (q) { 213 | q.leftJoin(toTable.tableName(), function (j) { 214 | j.on(fromTable.c(foreignKey), '=', toTable.c(otherKey)); 215 | joiner(j); 216 | }); 217 | }, label); 218 | } 219 | } 220 | }]); 221 | 222 | return BelongsTo; 223 | }(Relation); 224 | 225 | module.exports = BelongsTo; -------------------------------------------------------------------------------- /lib/relations/MorphMany.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 6 | 7 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 8 | 9 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 10 | 11 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 12 | 13 | var _require = require('lodash'), 14 | assign = _require.assign, 15 | isArray = _require.isArray; 16 | 17 | var Relation = require('./Relation'); 18 | var MorphTo = require('./MorphTo'); 19 | 20 | var MorphMany = function (_Relation) { 21 | _inherits(MorphMany, _Relation); 22 | 23 | function MorphMany(ownerTable, toTable, inverse) { 24 | _classCallCheck(this, MorphMany); 25 | 26 | if (!(inverse instanceof MorphTo)) { 27 | throw new Error('inverse should be a MorphTo relation'); 28 | } 29 | 30 | var _this = _possibleConstructorReturn(this, (MorphMany.__proto__ || Object.getPrototypeOf(MorphMany)).call(this, ownerTable)); 31 | 32 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, inverse: inverse }); 33 | return _this; 34 | } 35 | 36 | _createClass(MorphMany, [{ 37 | key: 'initRelation', 38 | value: function initRelation(fromModels) { 39 | var _this2 = this; 40 | 41 | return fromModels.map(function (m) { 42 | return assign(m, _defineProperty({}, _this2.relationName, [])); 43 | }); 44 | } 45 | }, { 46 | key: 'getRelated', 47 | value: function getRelated() { 48 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 49 | args[_key] = arguments[_key]; 50 | } 51 | 52 | if (args.length === 0) { 53 | if (this.activeModel !== null) { 54 | return this.getRelated([this.activeModel]); 55 | } else { 56 | return Promise.resolve([]); 57 | } 58 | } 59 | 60 | var fromModels = args[0]; 61 | var fromTable = this.fromTable, 62 | inverse = this.inverse; 63 | 64 | var toTable = this.constraints.apply(this.toTable.fork()); 65 | 66 | var foreignKey = inverse.foreignKey, 67 | typeField = inverse.typeField; 68 | 69 | var typeValue = fromTable.tableName(); 70 | 71 | return toTable.where(_defineProperty({}, typeField, typeValue)).whereIn(foreignKey, fromModels.map(function (m) { 72 | return m[fromTable.key()]; 73 | })).all(); 74 | } 75 | }, { 76 | key: 'matchModels', 77 | value: function matchModels() { 78 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 79 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 80 | var relationName = this.relationName, 81 | fromTable = this.fromTable, 82 | inverse = this.inverse; 83 | var foreignKey = inverse.foreignKey; 84 | 85 | 86 | var keyDict = relatedModels.reduce(function (dict, m) { 87 | var key = m[foreignKey]; 88 | 89 | if (!isArray(dict[key])) { 90 | return assign(dict, _defineProperty({}, key, [m])); 91 | } else { 92 | return assign(dict, _defineProperty({}, key, dict[key].concat(m))); 93 | } 94 | }, {}); 95 | 96 | return fromModels.map(function (m) { 97 | return assign(m, _defineProperty({}, relationName, isArray(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : [])); 98 | }); 99 | } 100 | }, { 101 | key: 'insert', 102 | value: function insert() { 103 | var _this3 = this; 104 | 105 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 106 | args[_key2] = arguments[_key2]; 107 | } 108 | 109 | if (args.length === 0) { 110 | throw new Error('bad method call'); 111 | } 112 | 113 | if (args.length === 1) { 114 | return this.insert.apply(this, [this.activeModel].concat(args)); 115 | } 116 | 117 | var fromModel = args[0], 118 | values = args[1]; 119 | var fromTable = this.fromTable, 120 | toTable = this.toTable, 121 | inverse = this.inverse; 122 | var foreignKey = inverse.foreignKey, 123 | typeField = inverse.typeField; 124 | 125 | 126 | return toTable.insert(function () { 127 | if (isArray(values)) { 128 | return values.map(function (v) { 129 | var _assign5; 130 | 131 | return assign(v, (_assign5 = {}, _defineProperty(_assign5, foreignKey, fromModel[fromTable.key()]), _defineProperty(_assign5, typeField, fromTable.tableName()), _assign5)); 132 | }); 133 | } else { 134 | var _assign6; 135 | 136 | return assign(values, (_assign6 = {}, _defineProperty(_assign6, foreignKey, fromModel[_this3.key]), _defineProperty(_assign6, typeField, fromTable.tableName()), _assign6)); 137 | } 138 | }()); 139 | } 140 | }, { 141 | key: 'update', 142 | value: function update() { 143 | var _assign7; 144 | 145 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 146 | args[_key3] = arguments[_key3]; 147 | } 148 | 149 | if (args.length === 0) { 150 | throw new Error('bad method call'); 151 | } 152 | 153 | if (args.length === 1) { 154 | return this.update.apply(this, [this.activeModel].concat(args)); 155 | } 156 | 157 | var fromModel = args[0], 158 | values = args[1]; 159 | var fromTable = this.fromTable, 160 | toTable = this.toTable, 161 | inverse = this.inverse; 162 | var foreignKey = inverse.foreignKey, 163 | typeField = inverse.typeField; 164 | 165 | 166 | return this.constraints.apply(toTable.fork()).where(typeField, fromTable.tableName()).where(foreignKey, fromModel[fromTable.key()]).update(assign(values, (_assign7 = {}, _defineProperty(_assign7, foreignKey, fromModel[fromTable.key()]), _defineProperty(_assign7, typeField, fromTable.tableName()), _assign7))); 167 | } 168 | }, { 169 | key: 'del', 170 | value: function del() { 171 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 172 | args[_key4] = arguments[_key4]; 173 | } 174 | 175 | if (args.length === 0) { 176 | return this.del(this.activeModel); 177 | } 178 | 179 | var fromModel = args[0]; 180 | var fromTable = this.fromTable, 181 | toTable = this.toTable, 182 | inverse = this.inverse; 183 | var foreignKey = inverse.foreignKey, 184 | typeField = inverse.typeField; 185 | 186 | 187 | return this.constraints.apply(toTable.fork()).where(typeField, fromTable.tableName()).where(foreignKey, fromModel[fromTable.key()]).del(); 188 | } 189 | }, { 190 | key: 'join', 191 | value: function join() { 192 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 193 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 194 | 195 | label = this.jointLabel(label, {}); 196 | var fromTable = this.fromTable, 197 | toTable = this.toTable, 198 | inverse = this.inverse; 199 | var foreignKey = inverse.foreignKey, 200 | typeField = inverse.typeField; 201 | 202 | 203 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 204 | return this.ownerTable; 205 | } else { 206 | return this.ownerTable.joint(function (q) { 207 | q.join(toTable.tableName(), function (j) { 208 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])).on(toTable.c(foreignKey), '=', fromTable.keyCol()); 209 | 210 | joiner(j); 211 | }); 212 | }, label); 213 | } 214 | } 215 | }, { 216 | key: 'leftJoin', 217 | value: function leftJoin() { 218 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 219 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 220 | 221 | label = this.jointLabel(label, { isLeftJoin: true }); 222 | var fromTable = this.fromTable, 223 | toTable = this.toTable, 224 | inverse = this.inverse; 225 | var foreignKey = inverse.foreignKey, 226 | typeField = inverse.typeField; 227 | 228 | 229 | if (this.ownerTable.scopeTrackhasJoint(label)) { 230 | return this.ownerTable; 231 | } else { 232 | return this.ownerTable.joint(function (q) { 233 | q.leftJoin(toTable.tableName(), function (j) { 234 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])).on(toTable.c(foreignKey), '=', fromTable.keyCol()); 235 | 236 | joiner(j); 237 | }); 238 | }, label); 239 | } 240 | } 241 | }]); 242 | 243 | return MorphMany; 244 | }(Relation); 245 | 246 | module.exports = MorphMany; -------------------------------------------------------------------------------- /lib/relations/MorphOne.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); 4 | 5 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 6 | 7 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 14 | 15 | var _require = require('lodash'), 16 | assign = _require.assign; 17 | 18 | var isUsableObject = require('isusableobject'); 19 | 20 | var Relation = require('./Relation'); 21 | var MorphTo = require('./Relation'); 22 | 23 | var MorphOne = function (_Relation) { 24 | _inherits(MorphOne, _Relation); 25 | 26 | function MorphOne(ownerTable, toTable, inverse) { 27 | _classCallCheck(this, MorphOne); 28 | 29 | if (!(inverse instanceof MorphTo)) { 30 | throw new Error('inverse should be a MorphTo relation'); 31 | } 32 | 33 | var _this = _possibleConstructorReturn(this, (MorphOne.__proto__ || Object.getPrototypeOf(MorphOne)).call(this, ownerTable)); 34 | 35 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, inverse: inverse }); 36 | return _this; 37 | } 38 | 39 | _createClass(MorphOne, [{ 40 | key: 'initRelation', 41 | value: function initRelation(fromModels) { 42 | var _this2 = this; 43 | 44 | return fromModels.map(function (m) { 45 | return assign(m, _defineProperty({}, _this2.relationName, [])); 46 | }); 47 | } 48 | }, { 49 | key: 'getRelated', 50 | value: function getRelated() { 51 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 52 | args[_key] = arguments[_key]; 53 | } 54 | 55 | if (args.length === 0) { 56 | if (this.activeModel !== null) { 57 | return this.getRelated([this.activeModel]).then(function (_ref) { 58 | var _ref2 = _slicedToArray(_ref, 1), 59 | relatedModel = _ref2[0]; 60 | 61 | return relatedModel; 62 | }); 63 | } else { 64 | return Promise.resolve(null); 65 | } 66 | } 67 | 68 | var fromModels = args[0]; 69 | var fromTable = this.fromTable, 70 | inverse = this.inverse; 71 | 72 | var toTable = this.constraints.apply(this.toTable.fork()); 73 | 74 | var foreignKey = inverse.foreignKey, 75 | typeField = inverse.typeField; 76 | 77 | var typeValue = fromTable.tableName(); 78 | 79 | return toTable.where(_defineProperty({}, typeField, typeValue)).whereIn(foreignKey, fromModels.map(function (m) { 80 | return m[fromTable.key()]; 81 | })).all(); 82 | } 83 | }, { 84 | key: 'matchModels', 85 | value: function matchModels() { 86 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 87 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 88 | var relationName = this.relationName, 89 | fromTable = this.fromTable, 90 | inverse = this.inverse; 91 | var foreignKey = inverse.foreignKey; 92 | 93 | 94 | var keyDict = relatedModels.reduce(function (dict, m) { 95 | return assign(dict, _defineProperty({}, m[foreignKey], m)); 96 | }, {}); 97 | 98 | return fromModels.map(function (m) { 99 | return assign(m, _defineProperty({}, relationName, isUsableObject(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : null)); 100 | }); 101 | } 102 | }, { 103 | key: 'insert', 104 | value: function insert() { 105 | var _assign4; 106 | 107 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 108 | args[_key2] = arguments[_key2]; 109 | } 110 | 111 | if (args.length === 0) { 112 | throw new Error('bad method call'); 113 | } 114 | 115 | if (args.length === 1) { 116 | return this.insert.apply(this, [this.activeModel].concat(args)); 117 | } 118 | 119 | var fromModel = args[0], 120 | values = args[1]; 121 | var fromTable = this.fromTable, 122 | toTable = this.toTable, 123 | inverse = this.inverse; 124 | var foreignKey = inverse.foreignKey, 125 | typeField = inverse.typeField; 126 | 127 | 128 | return toTable.insert(assign(values, (_assign4 = {}, _defineProperty(_assign4, foreignKey, fromModel[this.key]), _defineProperty(_assign4, typeField, fromTable.tableName()), _assign4))); 129 | } 130 | }, { 131 | key: 'update', 132 | value: function update() { 133 | var _assign5; 134 | 135 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 136 | args[_key3] = arguments[_key3]; 137 | } 138 | 139 | if (args.length === 0) { 140 | throw new Error('bad method call'); 141 | } 142 | 143 | if (args.length === 1) { 144 | return this.update.apply(this, [this.activeModel].concat(args)); 145 | } 146 | 147 | var fromModel = args[0], 148 | values = args[1]; 149 | var fromTable = this.fromTable, 150 | toTable = this.toTable, 151 | inverse = this.inverse; 152 | var foreignKey = inverse.foreignKey, 153 | typeField = inverse.typeField; 154 | 155 | 156 | return this.constraints.apply(toTable.fork()).where(typeField, fromTable.tableName()).where(foreignKey, fromModel[fromTable.key()]).update(assign(values, (_assign5 = {}, _defineProperty(_assign5, foreignKey, fromModel[fromTable.key()]), _defineProperty(_assign5, typeField, fromTable.tableName()), _assign5))); 157 | } 158 | }, { 159 | key: 'del', 160 | value: function del() { 161 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 162 | args[_key4] = arguments[_key4]; 163 | } 164 | 165 | if (args.length === 0) { 166 | return this.del(this.activeModel); 167 | } 168 | 169 | var fromModel = args[0]; 170 | var fromTable = this.fromTable, 171 | toTable = this.toTable, 172 | inverse = this.inverse; 173 | var foreignKey = inverse.foreignKey, 174 | typeField = inverse.typeField; 175 | 176 | 177 | return this.constraints.apply(toTable.fork()).where(typeField, fromTable.tableName()).where(foreignKey, fromModel[fromTable.key()]).del(); 178 | } 179 | }, { 180 | key: 'join', 181 | value: function join() { 182 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 183 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 184 | 185 | label = this.jointLabel(label, {}); 186 | var fromTable = this.fromTable, 187 | toTable = this.toTable, 188 | inverse = this.inverse; 189 | var foreignKey = inverse.foreignKey, 190 | typeField = inverse.typeField; 191 | 192 | 193 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 194 | return this.ownerTable; 195 | } else { 196 | return this.ownerTable.joint(function (q) { 197 | q.join(toTable.tableName(), function (j) { 198 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])).on(toTable.c(foreignKey), '=', fromTable.keyCol()); 199 | 200 | joiner(j); 201 | }); 202 | }, label); 203 | } 204 | } 205 | }, { 206 | key: 'leftJoin', 207 | value: function leftJoin() { 208 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 209 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 210 | 211 | label = this.jointLabel(label, { isLeftJoin: true }); 212 | var fromTable = this.fromTable, 213 | toTable = this.toTable, 214 | inverse = this.inverse; 215 | var foreignKey = inverse.foreignKey, 216 | typeField = inverse.typeField; 217 | 218 | 219 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 220 | return this.ownerTable; 221 | } else { 222 | return this.ownerTable.joint(function (q) { 223 | q.leftJoin(toTable.tableName(), function (j) { 224 | j.on(toTable.c(typeField), '=', fromTable.orm.raw('?', [fromTable.tableName()])).on(toTable.c(foreignKey), '=', fromTable.keyCol()); 225 | 226 | joiner(j); 227 | }); 228 | }, label); 229 | } 230 | } 231 | }]); 232 | 233 | return MorphOne; 234 | }(Relation); 235 | 236 | module.exports = MorphOne; -------------------------------------------------------------------------------- /lib/relations/HasManyThrough.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 4 | 5 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 6 | 7 | function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } 8 | 9 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 10 | 11 | function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } 14 | 15 | var _require = require('lodash'), 16 | assign = _require.assign, 17 | isArray = _require.isArray; 18 | 19 | var Relation = require('./Relation'); 20 | 21 | var HasManyThrough = function (_Relation) { 22 | _inherits(HasManyThrough, _Relation); 23 | 24 | function HasManyThrough(ownerTable, toTable, throughTable, firstKey, secondKey) { 25 | var joiner = arguments.length > 5 && arguments[5] !== undefined ? arguments[5] : function () {}; 26 | 27 | _classCallCheck(this, HasManyThrough); 28 | 29 | var _this = _possibleConstructorReturn(this, (HasManyThrough.__proto__ || Object.getPrototypeOf(HasManyThrough)).call(this, ownerTable)); 30 | 31 | assign(_this, { fromTable: ownerTable.fork(), toTable: toTable, throughTable: throughTable, firstKey: firstKey, secondKey: secondKey }); 32 | 33 | _this.throughFields = [throughTable.key(), firstKey]; 34 | 35 | _this.constrain(function (t) { 36 | t.scope(function (q) { 37 | q.join(_this.throughTable.tableName(), function (j) { 38 | j.on(_this.throughTable.keyCol(), '=', _this.toTable.c(secondKey)); 39 | joiner(j); 40 | }); 41 | }); 42 | }); 43 | return _this; 44 | } 45 | 46 | _createClass(HasManyThrough, [{ 47 | key: 'withThrough', 48 | value: function withThrough() { 49 | for (var _len = arguments.length, throughFields = Array(_len), _key = 0; _key < _len; _key++) { 50 | throughFields[_key] = arguments[_key]; 51 | } 52 | 53 | this.throughFields = throughFields.concat([this.throughTable.key(), this.firstKey]); 54 | return this; 55 | } 56 | }, { 57 | key: 'initRelation', 58 | value: function initRelation(fromModels) { 59 | var _this2 = this; 60 | 61 | return fromModels.map(function (m) { 62 | return assign(m, _defineProperty({}, _this2.relationName, [])); 63 | }); 64 | } 65 | }, { 66 | key: 'getRelated', 67 | value: function getRelated() { 68 | var _toTable$whereIn; 69 | 70 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 71 | args[_key2] = arguments[_key2]; 72 | } 73 | 74 | if (args.length === 0) { 75 | if (this.activeModel !== null) { 76 | return this.getRelated([this.activeModel]); 77 | } else { 78 | return Promise.resolve([]); 79 | } 80 | } 81 | 82 | var fromModels = args[0]; 83 | var fromTable = this.fromTable, 84 | throughTable = this.throughTable, 85 | firstKey = this.firstKey; 86 | 87 | var toTable = this.constraints.apply(this.toTable.fork()); 88 | 89 | var fromKeys = fromModels.map(function (m) { 90 | return m[fromTable.key()]; 91 | }); 92 | 93 | var cols = ['*'].concat(this.throughFields.map(function (field) { 94 | return throughTable.c(field) + ' as ' + throughTable.tableName() + '__' + field; 95 | })); 96 | 97 | return (_toTable$whereIn = toTable.whereIn(throughTable.c(firstKey), fromKeys)).select.apply(_toTable$whereIn, _toConsumableArray(cols)).all().then(function (relatedModels) { 98 | return relatedModels.map(function (model) { 99 | var through = Object.keys(model).filter(function (field) { 100 | return field.indexOf(throughTable.tableName() + '__') > -1; 101 | }).reduce(function (throughModel, field) { 102 | var strippedField = field.slice((throughTable.tableName() + '__').length); 103 | return assign({}, throughModel, _defineProperty({}, strippedField, model[field])); 104 | }, {}); 105 | 106 | return assign(Object.keys(model).filter(function (field) { 107 | return field.indexOf(throughTable.tableName() + '__') === -1; 108 | }).reduce(function (modelWithoutThroughs, field) { 109 | return assign({}, modelWithoutThroughs, _defineProperty({}, field, model[field])); 110 | }, {}), { through: through }); 111 | }); 112 | }); 113 | } 114 | }, { 115 | key: 'matchModels', 116 | value: function matchModels() { 117 | var fromModels = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; 118 | var relatedModels = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 119 | var relationName = this.relationName, 120 | firstKey = this.firstKey, 121 | fromTable = this.fromTable; 122 | 123 | 124 | var keyDict = relatedModels.reduce(function (dict, m) { 125 | var key = m.through[firstKey]; 126 | 127 | if (!isArray(dict[key])) { 128 | return assign(dict, _defineProperty({}, key, [m])); 129 | } else { 130 | return assign(dict, _defineProperty({}, key, dict[key].concat([m]))); 131 | } 132 | }, {}); 133 | 134 | return fromModels.map(function (m) { 135 | return assign(m, _defineProperty({}, relationName, isArray(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : [])); 136 | }); 137 | } 138 | }, { 139 | key: 'join', 140 | value: function join() { 141 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 142 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 143 | 144 | label = this.jointLabel(label, {}); 145 | var throughTable = this.throughTable, 146 | toTable = this.toTable, 147 | secondKey = this.secondKey; 148 | 149 | 150 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 151 | return this.ownerTable; 152 | } else { 153 | return this.joinThrough().joint(function (q) { 154 | q.join(toTable.tableName(), function (j) { 155 | j.on(throughTable.keyCol(), '=', toTable.c(secondKey)); 156 | joiner(j); 157 | }); 158 | }, label); 159 | } 160 | } 161 | }, { 162 | key: 'joinThrough', 163 | value: function joinThrough() { 164 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 165 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 166 | 167 | label = this.throughJointLabel(label, {}); 168 | var fromTable = this.fromTable, 169 | throughTable = this.throughTable, 170 | firstKey = this.firstKey; 171 | 172 | 173 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 174 | return this.ownerTable; 175 | } else { 176 | return this.ownerTable.joint(function (q) { 177 | q.join(throughTable.tableName(), function (j) { 178 | j.on(fromTable.keyCol(), '=', throughTable.c(firstKey)); 179 | joiner(j); 180 | }); 181 | }, label); 182 | } 183 | } 184 | }, { 185 | key: 'leftJoin', 186 | value: function leftJoin() { 187 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 188 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 189 | 190 | label = this.jointLabel(label, { isLeftJoin: true }); 191 | var throughTable = this.throughTable, 192 | toTable = this.toTable, 193 | secondKey = this.secondKey; 194 | 195 | 196 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 197 | return this.ownerTable; 198 | } else { 199 | return this.leftJoinThrough().joint(function (q) { 200 | q.leftJoin(toTable.tableName(), function (j) { 201 | j.on(throughTable.keyCol(), '=', toTable.c(secondKey)); 202 | joiner(j); 203 | }); 204 | }, label); 205 | } 206 | } 207 | }, { 208 | key: 'leftJoinThrough', 209 | value: function leftJoinThrough() { 210 | var joiner = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function () {}; 211 | var label = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; 212 | 213 | label = this.throughJointLabel(label, { isLeftJoin: true }); 214 | var fromTable = this.fromTable, 215 | throughTable = this.throughTable, 216 | firstKey = this.firstKey; 217 | 218 | 219 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 220 | return this.ownerTable; 221 | } else { 222 | return this.ownerTable.joint(function (q) { 223 | q.leftJoin(throughTable.tableName(), function (j) { 224 | j.on(fromTable.keyCol(), '=', throughTable.c(firstKey)); 225 | joiner(j); 226 | }); 227 | }, label); 228 | } 229 | } 230 | }]); 231 | 232 | return HasManyThrough; 233 | }(Relation); 234 | 235 | module.exports = HasManyThrough; -------------------------------------------------------------------------------- /src/Orm.js: -------------------------------------------------------------------------------- 1 | const knex = require('knex'); 2 | const {merge, isString} = require('lodash'); 3 | 4 | const KRedis = require('./kredis'); 5 | const Table = require('./Table'); 6 | const Scoper = require('./Scoper'); 7 | const Shape = require('./Shape'); 8 | const migrator = require('./migrator'); 9 | 10 | class Orm { 11 | constructor(config={}) { 12 | if ('db' in config) { 13 | this.knex = knex(config.db); 14 | } else { 15 | throw new Error(`no 'db' config found`); 16 | } 17 | 18 | if ('redis' in config) { 19 | this.cache = new KRedis(config.redis); 20 | } 21 | 22 | // tables definitions 23 | this.tableClasses = new Map(); 24 | 25 | // tables instances 26 | this.tables = new Map(); 27 | 28 | // tableColumns cache 29 | this.tableColumns = new Map(); 30 | 31 | // migrator 32 | this.migrator = migrator(this); 33 | 34 | // exports which can be exported in place of orm instance 35 | this.exports = { 36 | orm: this, 37 | table: (...args) => this.table(...args), 38 | trx: (...args) => this.trx(...args), 39 | raw: (...args) => this.raw(...args), 40 | migrator: this.migrator, 41 | cache: this.cache, 42 | knex: this.knex, 43 | scoper: (...args) => this.scoper(...args), 44 | shape: (...args) => this.shape(...args) 45 | }; 46 | } 47 | 48 | // raw expr helper 49 | raw(expr) { 50 | return this.knex.raw(expr); 51 | } 52 | 53 | // transaction helper 54 | transaction(promiseFn) { 55 | return this.knex.transaction(promiseFn); 56 | } 57 | 58 | // transaction shorthand 59 | // usage: 60 | // return orm.trx((t) => { 61 | // return orm('users', t).save([{}, {}, {}]); 62 | // }).then((users) => { 63 | // ... 64 | // }); 65 | trx(promiseFn) { 66 | let outerResult; 67 | 68 | return this.transaction((t) => { 69 | return promiseFn(t).then((result) => { 70 | return t.commit().then(() => { 71 | outerResult = result; 72 | return result; 73 | }); 74 | }).catch((e) => { 75 | t.rollback().then(() => { 76 | throw e; 77 | }); 78 | }); 79 | }).then(() => outerResult); 80 | } 81 | 82 | // method to close the database 83 | close() { 84 | const promises = [this.knex.destroy()]; 85 | 86 | if (this.cache) { 87 | promises.push(this.cache.disconnect()); 88 | } 89 | 90 | return Promise.all(promises); 91 | } 92 | 93 | // here, we load the columns of all the tables that have been 94 | // defined via the orm, and return a promise on completion 95 | // cos, if people wanna do that before starting the server 96 | // let em do that. we also call ioredis.connect if its available 97 | load() { 98 | const promises = Array.from(this.tables.keys).map((name) => this.table(name).load()); 99 | 100 | // if (this.cache) { 101 | // promises.push(this.cache.connect()); 102 | // } 103 | 104 | return Promise.all(promises); 105 | } 106 | 107 | // get a tableClass 108 | tableClass(tableName) { 109 | return this.tableClasses.get(tableName); 110 | } 111 | 112 | // get a table object 113 | table(tableName, trx=null) { 114 | if (!this.tables.has(tableName)) { 115 | throw new Error(`trying to access invalid table ${tableName}`); 116 | } 117 | 118 | const tbl = this.tables.get(tableName).fork(); 119 | 120 | if (trx !== null) { 121 | tbl.transacting(trx); 122 | } 123 | 124 | return tbl; 125 | } 126 | 127 | scoper(scopes) { 128 | return new Scoper(scopes); 129 | } 130 | 131 | shape(checks) { 132 | return new Shape(checks); 133 | } 134 | 135 | // shorthand for table 136 | tbl(tableName, trx=null) { 137 | return this.table(tableName, trx); 138 | } 139 | 140 | defineTable(params={}) { 141 | const tableName = params.name; 142 | 143 | if (!isString(tableName)) { 144 | throw new Error(`Invalid table-name: ${tableName} supplied via key 'name'`); 145 | } 146 | 147 | if (this.tableClasses.has(tableName)) { 148 | throw new Error(`Table '${tableName}' already defined`); 149 | } 150 | 151 | this.tableClasses.set(tableName, this.newTableClass(params)); 152 | this.instantitateTable(tableName, params); 153 | return this; 154 | } 155 | 156 | extendTable(tableName, {scopes={}, joints={}, relations={}, methods={}}) { 157 | if (!this.tableClasses.has(tableName)) { 158 | throw new Error(`Table '${tableName}' not defined yet`); 159 | } 160 | 161 | const TableClass = this.tableClass(tableName); 162 | const ExtendedTableClass = class extends TableClass {}; 163 | 164 | this.attachScopesToTableClass(ExtendedTableClass, scopes); 165 | this.attachJointsToTableClass(ExtendedTableClass, joints); 166 | this.attachRelationsToTableClass(ExtendedTableClass, relations); 167 | this.attachMethodsToTableClass(ExtendedTableClass, methods); 168 | 169 | this.tableClasses.set(tableName, ExtendedTableClass); 170 | this.instantitateTable(tableName); 171 | return this; 172 | } 173 | 174 | instantitateTable(tableName) { 175 | const TableClass = this.tableClasses.get(tableName); 176 | 177 | return this.tables.set(tableName, new TableClass(this)); 178 | } 179 | 180 | newTableClass(params) { 181 | return this.extendTableClass(Table, params); 182 | } 183 | 184 | extendTableClass(TableClass, params) { 185 | const {name, props, processors, scopes, joints, relations, methods} = merge( 186 | // the defaults 187 | { 188 | // the table's name, is required 189 | name: null, 190 | 191 | // table properties 192 | props: { 193 | key: 'id', 194 | // default key column, can be ['user_id', 'post_id'] for composite keys 195 | uuid: false, 196 | // by default we don't assume that you use an auto generated uuid as db id 197 | perPage: 25, 198 | // standard batch size per page used by `forPage` method 199 | // forPage method uses offset 200 | // avoid that and use a keyset in prod (http://use-the-index-luke.com/no-offset) 201 | timestamps: false 202 | // set to `true` if you want auto timestamps or 203 | // timestamps: ['created_at', 'updated_at'] (these are defaults when `true`) 204 | // will be assigned in this order only 205 | }, 206 | 207 | // predefined scopes on the table 208 | scopes: {}, 209 | // predefined joints on the table 210 | joints: {}, 211 | // relations definitions for the table 212 | relations: {}, 213 | // table methods defintions 214 | methods: {} 215 | }, 216 | // supplied params which will override the defaults 217 | params 218 | ); 219 | 220 | // the extended table class whose objects will behave as needed 221 | const ExtendedTableClass = class extends TableClass {}; 222 | 223 | // assign name to the table class 224 | ExtendedTableClass.prototype.name = name; 225 | 226 | // assign props to the table class 227 | ExtendedTableClass.prototype.props = props; 228 | 229 | // assign processors to the table class 230 | ExtendedTableClass.prototype.processors = processors; 231 | 232 | // store names of defined scopes, joints, relations, and methods 233 | ExtendedTableClass.prototype.definedScopes = new Set(); 234 | ExtendedTableClass.prototype.definedJoints = new Set(); 235 | ExtendedTableClass.prototype.definedRelations = new Set(); 236 | ExtendedTableClass.prototype.definedMethods = new Set(); 237 | 238 | // attach scopes, joints, relations and methods to tables 239 | // these are the only ones extendable after creation 240 | this.attachScopesToTableClass(ExtendedTableClass, scopes); 241 | this.attachJointsToTableClass(ExtendedTableClass, joints); 242 | this.attachRelationsToTableClass(ExtendedTableClass, relations); 243 | this.attachMethodsToTableClass(ExtendedTableClass, methods); 244 | 245 | // return the extended table class 246 | return ExtendedTableClass; 247 | } 248 | 249 | attachScopesToTableClass(TableClass, scopes) { 250 | // keep a record of defined scopes 251 | Object.keys(scopes).forEach((name) => { 252 | TableClass.prototype.definedScopes.add(name); 253 | }); 254 | 255 | // process and merge scopes with table class 256 | merge( 257 | TableClass.prototype, 258 | Object.keys(scopes).reduce((processed, name) => { 259 | return merge(processed, { 260 | [name](...args) { 261 | scopes[name].apply(this, args); 262 | // set the label of the last pushed scope 263 | this.scopeTrack.relabelLastScope(name); 264 | return this; 265 | } 266 | }); 267 | }, {}) 268 | ); 269 | } 270 | 271 | attachJointsToTableClass(TableClass, joints) { 272 | // keep a record of defined joints 273 | Object.keys(joints).forEach((name) => { 274 | TableClass.prototype.definedJoints.add(name); 275 | }); 276 | 277 | // process and merge joints with table class 278 | merge( 279 | TableClass.prototype, 280 | Object.keys(joints).reduce((processed, name) => { 281 | // predefined joints never take arguments 282 | return merge(processed, { 283 | [name]() { 284 | if (this.scopeTrack.hasJoint(name)) { 285 | return this; 286 | } else { 287 | joints[name].call(this); 288 | // set the label of the last pushed scope 289 | this.scopeTrack.relabelLastScope(name); 290 | // ensure that the last scope is a joint 291 | this.scopeTrack.convertLastScopeToJoint(); 292 | return this; 293 | } 294 | } 295 | }); 296 | }, {}) 297 | ); 298 | } 299 | 300 | attachRelationsToTableClass(TableClass, relations) { 301 | // keep a record of defined relations 302 | Object.keys(relations).forEach((name) => { 303 | TableClass.prototype.definedRelations.add(name); 304 | }); 305 | 306 | // process and merge relations with table class 307 | merge( 308 | TableClass.prototype, 309 | Object.keys(relations).reduce((processed, name) => { 310 | // const relation = relations[name]; 311 | return merge(processed, { 312 | [name](model) { 313 | if (model) { 314 | return relations[name].bind(this)().setName(name).forModel(model); 315 | } else { 316 | return relations[name].bind(this)().setName(name); 317 | } 318 | } 319 | }); 320 | }, {}) 321 | ); 322 | } 323 | 324 | attachMethodsToTableClass(TableClass, methods) { 325 | // keep a record of defined methods 326 | Object.keys(methods).forEach((name) => { 327 | TableClass.prototype.definedMethods.add(name); 328 | }); 329 | 330 | // process and merge relations with table class 331 | merge( 332 | TableClass.prototype, 333 | Object.keys(methods).reduce((processed, name) => { 334 | return merge(processed, {[name]: methods[name]}); 335 | }, {}) 336 | ); 337 | } 338 | } 339 | 340 | module.exports = Orm; 341 | -------------------------------------------------------------------------------- /src/relations/ManyToMany.js: -------------------------------------------------------------------------------- 1 | const {assign, isArray} = require('lodash'); 2 | const isUsableObject = require('isusableobject'); 3 | 4 | const Relation = require('./Relation'); 5 | 6 | class ManyToMany extends Relation { 7 | constructor(ownerTable, toTable, pivotTable, foreignKey, otherKey, joiner=(() =>{})) { 8 | super(ownerTable); 9 | assign(this, {fromTable: ownerTable.fork(), toTable, pivotTable, foreignKey, otherKey}); 10 | 11 | this.pivotFields = [foreignKey, otherKey]; 12 | 13 | this.constrain((t) => { 14 | t.scope((q) => q.join(this.pivotTable.tableName(), (j) => { 15 | j.on(this.pivotTable.c(this.otherKey), '=', toTable.keyCol()); 16 | joiner(j); 17 | })); 18 | }); 19 | } 20 | 21 | withPivot(...pivotFields) { 22 | this.pivotFields = pivotFields.concat([this.foreignKey, this.otherKey]); 23 | return this; 24 | } 25 | 26 | initRelation(fromModels=[]) { 27 | return fromModels.map((model) => assign(model, {[this.relationName]: []})); 28 | } 29 | 30 | getRelated(...args) { 31 | if (args.length === 0) { 32 | if (this.activeModel !== null) { 33 | return this.getRelated([this.activeModel]); 34 | } else { 35 | return Promise.resolve([]); 36 | } 37 | } 38 | 39 | const [fromModels] = args; 40 | 41 | const {fromTable, pivotTable, foreignKey} = this; 42 | const toTable = this.constraints.apply(this.toTable.fork()); 43 | 44 | const fromKeys = fromModels.map((m) => m[fromTable.key()]); 45 | 46 | const cols = ['*'].concat(this.pivotFields.map((field) => { 47 | return `${pivotTable.c(field)} as ${pivotTable.tableName()}__${field}`; 48 | })); 49 | 50 | return toTable.whereIn(pivotTable.c(foreignKey), fromKeys) 51 | .select(...cols).all().then((relatedModels) => { 52 | return relatedModels.map((model) => { 53 | const pivot = Object.keys(model) 54 | .filter((field) => field.indexOf(`${pivotTable.tableName()}__`) > -1) 55 | .reduce((pivotModel, field) => { 56 | const strippedField = field.slice(`${pivotTable.tableName()}__`.length); 57 | return assign({}, pivotModel, {[strippedField]: model[field]}); 58 | }, {}) 59 | ; 60 | 61 | return assign( 62 | Object.keys(model) 63 | .filter((field) => field.indexOf(`${pivotTable.tableName()}__`) === -1) 64 | .reduce((modelWithoutPivots, field) => { 65 | return assign({}, modelWithoutPivots, {[field]: model[field]}); 66 | }, {}), 67 | {pivot} 68 | ); 69 | }); 70 | }) 71 | ; 72 | } 73 | 74 | matchModels(fromModels=[], relatedModels=[]) { 75 | const {relationName, foreignKey, fromTable} = this; 76 | 77 | const keyDict = relatedModels.reduce((dict, m) => { 78 | const key = m.pivot[foreignKey]; 79 | 80 | if (!isArray(dict[key])) { 81 | return assign(dict, {[key]: [m]}); 82 | } else { 83 | return assign(dict, {[key]: dict[key].concat([m])}); 84 | } 85 | }, {}); 86 | 87 | return fromModels.map((m) => assign(m, { 88 | [relationName]: isArray(keyDict[m[fromTable.key()]]) ? keyDict[m[fromTable.key()]] : [] 89 | })); 90 | } 91 | 92 | sync(...args) { 93 | // if args length is 1 that means we have only values 94 | // if args length is 2 and fromModel is an 95 | // array, that means fromModel has not been provided. 96 | // if args length is 3 that means we have all 3 args 97 | if (args.length === 1) { 98 | return this.sync(this.activeModel, args[0], {}); 99 | } 100 | 101 | if (args.length === 2) { 102 | if (isArray(args[0])) { 103 | return this.sync(this.activeModel, ...args); 104 | } else if (isArray(args[1])) { 105 | return this.sync(args[0], args[1], {}); 106 | } else { 107 | throw new Error('bad method call'); 108 | } 109 | } 110 | 111 | const {fromTable, pivotTable, toTable, foreignKey, otherKey} = this; 112 | const [fromModel, relatedModels, extraFields] = args; 113 | 114 | return pivotTable.fork() 115 | .where(foreignKey, fromModel[fromTable.key()]) 116 | .del() 117 | .then(() => { 118 | if (relatedModels.length === 0) { 119 | return Promise.resolve([]); 120 | } 121 | 122 | const relatedKeys = relatedModels.map((m) => isUsableObject(m) ? m[toTable.key()] : m); 123 | const fromKey = fromModel[fromTable.key()]; 124 | 125 | const pivots = relatedKeys.map((k) => { 126 | return assign({}, extraFields, { 127 | [foreignKey]: fromKey, 128 | [otherKey]: k 129 | }); 130 | }); 131 | 132 | return pivotTable.insert(pivots); 133 | }) 134 | ; 135 | } 136 | 137 | attach(...args) { 138 | if (args.length === 0) { 139 | throw new Error('bad method call'); 140 | } 141 | 142 | if (args.length === 1) { 143 | return this.attach(this.activeModel, args[0], {}); 144 | } 145 | 146 | if (args.length === 2) { 147 | return this.attach(this.activeModel, ...args); 148 | } 149 | 150 | const [fromModel, relatedModel, extraFields] = args; 151 | 152 | if (isArray(relatedModel)) { 153 | return Promise.all(relatedModel.map((m) => this.attach(fromModel, m, extraFields))); 154 | } 155 | 156 | const {fromTable, pivotTable, toTable, foreignKey, otherKey} = this; 157 | 158 | const pivot = { 159 | [foreignKey]: fromModel[fromTable.key()], 160 | [otherKey]: relatedModel[toTable.key()] 161 | }; 162 | 163 | return pivotTable.insert(assign({}, extraFields, pivot)); 164 | } 165 | 166 | detach(...args) { 167 | if (args.length === 0) { 168 | throw new Error('bad method call'); 169 | } 170 | 171 | if (args.length === 1) { 172 | return this.detach(this.activeModel, args[0]); 173 | } 174 | 175 | if (args.length === 2) { 176 | return this.detach(this.activeModel, ...args); 177 | } 178 | 179 | const [fromModel, relatedModel] = args; 180 | const {fromTable, pivotTable, toTable, foreignKey, otherKey} = this; 181 | 182 | if (isArray(relatedModel)) { 183 | return Promise.all(relatedModel.map((m) => this.detach(fromModel, m))); 184 | } else { 185 | return pivotTable.fork().where({ 186 | [foreignKey]: fromModel[fromTable.key()], 187 | [otherKey]: relatedModel[toTable.key()] 188 | }).del(); 189 | } 190 | } 191 | 192 | insert(...args) { 193 | if (args.length === 0) { 194 | throw new Error('bad method call'); 195 | } 196 | 197 | if (args.length === 1) { 198 | return this.insert(this.activeModel, ...args); 199 | } 200 | 201 | const {fromTable, pivotTable, toTable, foreignKey, otherKey} = this; 202 | const [fromModel, values] = args; 203 | 204 | return toTable.insert(values).then((relatedModel) => { 205 | const newPivots = (() => { 206 | if (isArray(relatedModel)) { 207 | return relatedModel.map((m) => { 208 | const pivotFields = { 209 | [foreignKey]: fromModel[fromTable.key()], 210 | [otherKey]: m[toTable.key()] 211 | }; 212 | 213 | if ('pivot' in m) { 214 | return assign({}, pivotFields, m.pivot); 215 | } else { 216 | return pivotFields; 217 | } 218 | }); 219 | } else { 220 | const pivotFields = { 221 | [foreignKey]: fromModel[fromTable.key()], 222 | [otherKey]: relatedModel[toTable.key()] 223 | }; 224 | 225 | if ('pivot' in relatedModel) { 226 | return assign({}, pivotFields, relatedModel.pivot); 227 | } else { 228 | return pivotFields; 229 | } 230 | } 231 | })(); 232 | 233 | return pivotTable.insert(newPivots); 234 | }); 235 | } 236 | 237 | update(...args) { 238 | if (args.length === 0) { 239 | throw new Error('bad method call'); 240 | } 241 | 242 | if (args.length === 1) { 243 | return this.update(this.activeModel, ...args); 244 | } 245 | 246 | const {fromTable, pivotTable, foreignKey, otherKey} = this; 247 | const toTable = this.constraints.apply(this.toTable.fork()); 248 | const [fromModel, values] = args; 249 | 250 | if (isArray(values)) { 251 | return Promise.all(values.map((v) => this.update(fromModel, v))); 252 | } else { 253 | if ('pivot' in values) { 254 | const pivotCondition = { 255 | [foreignKey]: fromModel[fromTable.key()], 256 | [otherKey]: values[toTable.key()] 257 | }; 258 | 259 | return Promise.all([ 260 | pivotTable.fork().where(pivotCondition).update(values.pivot), 261 | toTable.fork().whereKey(values).update(values) 262 | ]); 263 | } else { 264 | return toTable.fork().whereKey(values).update(values); 265 | } 266 | } 267 | } 268 | 269 | del(...args) { 270 | if (args.length === 0) { 271 | return this.del(this.activeModel); 272 | } 273 | 274 | const {fromTable, pivotTable, toTable, foreignKey} = this; 275 | const [fromModel] = args; 276 | 277 | return this.constraints.apply(toTable.fork()) 278 | .where(pivotTable.c(foreignKey), fromModel[fromTable.key()]) 279 | .del().then(() => { 280 | return pivotTable.fork() 281 | .where(foreignKey, fromModel[fromTable.key()]) 282 | .del(); 283 | }) 284 | ; 285 | } 286 | 287 | join(joiner=(() => {}), label=null) { 288 | label = this.jointLabel(label, {}); 289 | const {pivotTable, toTable, otherKey} = this; 290 | 291 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 292 | return this.ownerTable; 293 | } else { 294 | return this.joinPivot().joint((q) => { 295 | q.join(toTable.tableName(), (j) => { 296 | j.on(toTable.keyCol(), '=', pivotTable.c(otherKey)); 297 | joiner(j); 298 | }); 299 | }); 300 | } 301 | } 302 | 303 | joinPivot(joiner=(() => {}), label=null) { 304 | label = this.pivotJointLabel(label, {}); 305 | const {pivotTable, fromTable, foreignKey} = this; 306 | 307 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 308 | return this.ownerTable; 309 | } else { 310 | return this.ownerTable.joint((q) => { 311 | q.join(pivotTable.tableName(), (j) => { 312 | j.on(fromTable.keyCol(), '=', pivotTable.c(foreignKey)); 313 | joiner(j); 314 | }); 315 | }, label); 316 | } 317 | } 318 | 319 | leftJoin(joiner=(() => {}), label=null) { 320 | label = this.jointLabel(label, {isLeftJoin: true}); 321 | const {pivotTable, toTable, otherKey} = this; 322 | 323 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 324 | return this.ownerTable; 325 | } else { 326 | return this.leftJoinPivot().joint((q) => { 327 | q.leftJoin(toTable.tableName(), (j) => { 328 | j.on(toTable.keyCol(), '=', pivotTable.c(otherKey)); 329 | joiner(j); 330 | }); 331 | }); 332 | } 333 | } 334 | 335 | leftJoinPivot(joiner=(() => {}), label=null) { 336 | label = this.pivotJointLabel(label, {isLeftJoin: true}); 337 | const {pivotTable, fromTable, foreignKey} = this; 338 | 339 | if (this.ownerTable.scopeTrack.hasJoint(label)) { 340 | return this.ownerTable; 341 | } else { 342 | return this.ownerTable.joint((q) => { 343 | q.leftJoin(pivotTable.tableName(), (j) => { 344 | j.on(fromTable.keyCol(), '=', pivotTable.c(foreignKey)); 345 | joiner(j); 346 | }); 347 | }, label); 348 | } 349 | } 350 | } 351 | 352 | module.exports = ManyToMany; 353 | --------------------------------------------------------------------------------