From c55e292af1f182a2e46c7c0c8447a96390156010 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Tue, 19 Jan 2016 15:34:26 -0500 Subject: [PATCH 1/7] Stable Version 0.11.8 --- dist/js-data-sql.js | 46 ++++++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/dist/js-data-sql.js b/dist/js-data-sql.js index a167300..0370f42 100644 --- a/dist/js-data-sql.js +++ b/dist/js-data-sql.js @@ -232,29 +232,35 @@ module.exports = })(); } else if (def.type === 'belongsTo' || def.type === 'hasOne' && def.localKey) { if (instance) { - task = _this.find(resourceConfig.getResource(relationName), DSUtils.get(instance, def.localKey), __options).then(function (relatedItem) { - instance[def.localField] = relatedItem; - return relatedItem; - }); + var id = DSUtils.get(instance, def.localKey); + if (id) { + task = _this.find(resourceConfig.getResource(relationName), DSUtils.get(instance, def.localKey), __options).then(function (relatedItem) { + instance[def.localField] = relatedItem; + return relatedItem; + }); + } } else { - task = _this.findAll(resourceConfig.getResource(relationName), { - where: _defineProperty({}, relationDef.idAttribute, { - 'in': DSUtils.filter(items.map(function (item) { - return DSUtils.get(item, def.localKey); - }), function (x) { - return x; + var ids = DSUtils.filter(items.map(function (item) { + return DSUtils.get(item, def.localKey); + }), function (x) { + return x; + }); + if (ids.length) { + task = _this.findAll(resourceConfig.getResource(relationName), { + where: _defineProperty({}, relationDef.idAttribute, { + 'in': ids }) - }) - }, __options).then(function (relatedItems) { - DSUtils.forEach(items, function (item) { - DSUtils.forEach(relatedItems, function (relatedItem) { - if (relatedItem[relationDef.idAttribute] === item[def.localKey]) { - item[def.localField] = relatedItem; - } + }, __options).then(function (relatedItems) { + DSUtils.forEach(items, function (item) { + DSUtils.forEach(relatedItems, function (relatedItem) { + if (relatedItem[relationDef.idAttribute] === item[def.localKey]) { + item[def.localField] = relatedItem; + } + }); }); + return relatedItems; }); - return relatedItems; - }); + } } } @@ -503,6 +509,8 @@ module.exports = } } else if (op === 'like') { query = query.where(field, 'like', v); + } else if (op === '|like') { + query = query.orWhere(field, 'like', v); } else if (op === '|==' || op === '|===') { if (v === null) { query = query.orWhereNull(field); diff --git a/package.json b/package.json index a9c93aa..3d14273 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "js-data-sql", "description": "Postgres/MySQL/MariaDB/SQLite3 adapter for js-data.", - "version": "0.11.7", + "version": "0.11.8", "homepage": "http://www.js-data.io/docs/dssqladapter", "repository": { "type": "git", From 4dfb3aad0644d6877d552a306d06e305fd5e535d Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 01:11:47 -0500 Subject: [PATCH 2/7] Allow setting DEBUG=true env variable for debug knex logging in tests --- mocha.start.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mocha.start.js b/mocha.start.js index 9c42f3f..9667a3d 100644 --- a/mocha.start.js +++ b/mocha.start.js @@ -34,7 +34,8 @@ var config = { host: process.env.DB_HOST || 'localhost', user: process.env.DB_USER || process.env.C9_USER || 'ubuntu', database: process.env.DB_NAME || (process.env.C9_USER ? 'c9' : 'circle_test') - } + }, + debug: process.env.DEBUG || false }; TestRunner.init({ From 89ede85e8566359c145b7f64f2f247af16066eb3 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 01:13:26 -0500 Subject: [PATCH 3/7] Use arrow functions consistently --- src/index.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 5dc7df5..0a0290d 100644 --- a/src/index.js +++ b/src/index.js @@ -96,7 +96,7 @@ function loadWithRelations (items, resourceConfig, options) { if (instance) { foreignKeyFilter = { '==': instance[resourceConfig.idAttribute] } } else { - foreignKeyFilter = { 'in': items.map(function (item) { return item[resourceConfig.idAttribute] }) } + foreignKeyFilter = { 'in': items.map(item => item[resourceConfig.idAttribute]) } } task = this.findAll(resourceConfig.getResource(relationName), { where: { @@ -167,7 +167,7 @@ function loadWithRelations (items, resourceConfig, options) { }) } } else { - let ids = DSUtils.filter(items.map(function (item) { return DSUtils.get(item, def.localKey) }), x => x) + let ids = DSUtils.filter(items.map(item => DSUtils.get(item, def.localKey)), x => x) if (ids.length) { task = this.findAll(resourceConfig.getResource(relationName), { where: { @@ -266,7 +266,7 @@ class DSSqlAdapter { updateAll (resourceConfig, attrs, params, options) { attrs = DSUtils.removeCircular(DSUtils.omit(attrs, resourceConfig.relationFields || [])) return this.filterQuery(resourceConfig, params, options).then(items => { - return items.map(function (item) { return item[resourceConfig.idAttribute] }) + return items.map(item => item[resourceConfig.idAttribute]) }).then(ids => { return this.filterQuery(resourceConfig, params, options).update(attrs).then(() => { let _params = {where: {}} From b190930fb73aff87df4e4b2a193af145b15b4bc4 Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 01:14:34 -0500 Subject: [PATCH 4/7] Support querying against hasMany relations using WHERE EXISTS. --- src/index.js | 111 +++++++++++++++++++++++-------------------- test/findAll.spec.js | 20 ++++++++ 2 files changed, 79 insertions(+), 52 deletions(-) diff --git a/src/index.js b/src/index.js index 0a0290d..8c227d0 100644 --- a/src/index.js +++ b/src/index.js @@ -18,50 +18,6 @@ function getTable (resourceConfig) { return resourceConfig.table || underscore(resourceConfig.name) } -/** - * Lookup and apply table joins to query if field contains a `.` - * @param {string} field - Field defined in where filter - * @param {object} query - knex query to modify - * @param {object} resourceConfig - Resource of primary query/table - * @param {string[]} existingJoins - Array of fully qualitifed field names for - * any existing table joins for query - * @returns {string} - field updated to perspective of applied joins - */ -function applyTableJoins (field, query, resourceConfig, existingJoins) { - if (DSUtils.contains(field, '.')) { - let parts = field.split('.') - let localResourceConfig = resourceConfig - - let relationPath = [] - while (parts.length >= 2) { - let relationName = parts.shift() - let relationResourceConfig = resourceConfig.getResource(relationName) - relationPath.push(relationName) - - if (!existingJoins.some(t => t === relationPath.join('.'))) { - let [relation] = localResourceConfig.relationList.filter(r => r.relation === relationName) - if (relation) { - let table = getTable(localResourceConfig) - let localId = `${table}.${relation.localKey}` - - let relationTable = getTable(relationResourceConfig) - let foreignId = `${relationTable}.${relationResourceConfig.idAttribute}` - - query.join(relationTable, localId, foreignId) - existingJoins.push(relationPath.join('.')) - } else { - // hopefully a qualified local column - } - } - localResourceConfig = relationResourceConfig - } - - field = `${getTable(localResourceConfig)}.${parts[0]}` - } - - return field -} - function loadWithRelations (items, resourceConfig, options) { let tasks = [] let instance = Array.isArray(items) ? null : items @@ -291,6 +247,7 @@ class DSSqlAdapter { filterQuery (resourceConfig, params, options) { let table = getTable(resourceConfig) + let joinedTables = [] let query if (params instanceof Object.getPrototypeOf(this.query.client).QueryBuilder) { @@ -308,8 +265,6 @@ class DSSqlAdapter { params.orderBy = params.orderBy || params.sort params.skip = params.skip || params.offset - let joinedTables = [] - DSUtils.forEach(DSUtils.keys(params), k => { let v = params[k] if (!DSUtils.contains(reserved, k)) { @@ -331,16 +286,68 @@ class DSSqlAdapter { '==': criteria } } - - DSUtils.forOwn(criteria, (v, op) => { - // Apply table joins (if needed) + + let processRelationField = (field) => { + let parts = field.split('.') + let localResourceConfig = resourceConfig + let relationPath = [] + + while (parts.length >= 2) { + let relationName = parts.shift() + let relationResourceConfig = resourceConfig.getResource(relationName) + relationPath.push(relationName) + + if (localResourceConfig.relationList) { + let [relation] = localResourceConfig.relationList.filter(r => r.relation === relationName) + if (relation) { + if (relation.type === 'belongsTo' || relation.type === 'hasOne') { + // Apply table join for belongsTo/hasOne property (if not done already) + if (!joinedTables.some(t => t === relationPath.join('.'))) { + let table = getTable(localResourceConfig) + let localId = `${table}.${relation.localKey}` + + let relationTable = getTable(relationResourceConfig) + let foreignId = `${relationTable}.${relationResourceConfig.idAttribute}` + + query.join(relationTable, localId, foreignId) + joinedTables.push(relationPath.join('.')) + } + } else if (relation.type === 'hasMany') { + // Perform `WHERE EXISTS` subquery for hasMany property + let table = getTable(localResourceConfig) + let localId = `${table}.${localResourceConfig.idAttribute}` + + let relationTable = getTable(relationResourceConfig) + let foreignId = `${relationTable}.${relation.foreignKey}` + + let existsParams = { + [foreignId]: {'===': knex.raw(localId)}, + [parts[0]]: criteria + }; + query.whereExists(this.filterQuery(relationResourceConfig, existsParams, options)); + criteria = null; // criteria handled by EXISTS subquery + } + } else { + // hopefully a qualified local column + } + + localResourceConfig = relationResourceConfig + } + } + + return `${getTable(localResourceConfig)}.${parts[0]}` + } + + if (DSUtils.contains(field, '.')) { if (DSUtils.contains(field, ',')) { let splitFields = field.split(',').map(c => c.trim()) - field = splitFields.map(splitField => applyTableJoins(splitField, query, resourceConfig, joinedTables)).join(',') + field = splitFields.map(splitField => processRelationField(splitField)).join(',') } else { - field = applyTableJoins(field, query, resourceConfig, joinedTables) + field = processRelationField(field, query, resourceConfig, joinedTables) } - + } + + DSUtils.forOwn(criteria, (v, op) => { if (op === '==' || op === '===') { if (v === null) { query = query.whereNull(field) diff --git a/test/findAll.spec.js b/test/findAll.spec.js index 0f8dba1..087ac79 100644 --- a/test/findAll.spec.js +++ b/test/findAll.spec.js @@ -45,6 +45,26 @@ describe('DSSqlAdapter#findAll', function () { assert.equal(posts[1].content, 'bar'); assert.equal(posts[2].content, 'baz'); }); + + it('should filter using a hasMany relation', function* () { + let user1 = yield adapter.create(User, {name: 'Sean'}); + let post1 = yield adapter.create(Post, {userId: user1.id, content: 'foo'}); + let post2 = yield adapter.create(Post, {userId: user1.id, content: 'bar'}); + let post3 = yield adapter.create(Post, {userId: user1.id, content: 'baz'}); + + let user2 = yield adapter.create(User, {name: 'Jason'}); + let post4 = yield adapter.create(Post, {userId: user2.id, content: 'foo'}); + let post5 = yield adapter.create(Post, {userId: user2.id, content: 'bar'}); + + let user3 = yield adapter.create(User, {name: 'Ed'}); + let post6 = yield adapter.create(Post, {userId: user3.id, content: 'bar'}); + let post7 = yield adapter.create(Post, {userId: user3.id, content: 'baz'}); + + let users = yield adapter.findAll(User, {where: {'post.content': {'==': 'foo'} }}); + assert.equal(users.length, 2); + assert.equal(users[0].name, 'Sean'); + assert.equal(users[1].name, 'Jason'); + }); describe('near', function () { beforeEach(function * () { From 30640719215223baa537db87e62e85b04d8b369a Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 01:48:22 -0500 Subject: [PATCH 5/7] Allow specifying relation's name or localField in query --- src/index.js | 63 ++++++++++++++++++++++---------------------- test/findAll.spec.js | 22 +++++++++++++++- 2 files changed, 52 insertions(+), 33 deletions(-) diff --git a/src/index.js b/src/index.js index 8c227d0..9fe3fdb 100644 --- a/src/index.js +++ b/src/index.js @@ -294,45 +294,44 @@ class DSSqlAdapter { while (parts.length >= 2) { let relationName = parts.shift() - let relationResourceConfig = resourceConfig.getResource(relationName) - relationPath.push(relationName) - - if (localResourceConfig.relationList) { - let [relation] = localResourceConfig.relationList.filter(r => r.relation === relationName) - if (relation) { - if (relation.type === 'belongsTo' || relation.type === 'hasOne') { - // Apply table join for belongsTo/hasOne property (if not done already) - if (!joinedTables.some(t => t === relationPath.join('.'))) { - let table = getTable(localResourceConfig) - let localId = `${table}.${relation.localKey}` - - let relationTable = getTable(relationResourceConfig) - let foreignId = `${relationTable}.${relationResourceConfig.idAttribute}` - - query.join(relationTable, localId, foreignId) - joinedTables.push(relationPath.join('.')) - } - } else if (relation.type === 'hasMany') { - // Perform `WHERE EXISTS` subquery for hasMany property + let [relation] = localResourceConfig.relationList.filter(r => r.relation === relationName || r.localField === relationName) + + if (relation) { + let relationResourceConfig = resourceConfig.getResource(relation.relation) + relationPath.push(relation.relation) + + if (relation.type === 'belongsTo' || relation.type === 'hasOne') { + // Apply table join for belongsTo/hasOne property (if not done already) + if (!joinedTables.some(t => t === relationPath.join('.'))) { let table = getTable(localResourceConfig) - let localId = `${table}.${localResourceConfig.idAttribute}` + let localId = `${table}.${relation.localKey}` let relationTable = getTable(relationResourceConfig) - let foreignId = `${relationTable}.${relation.foreignKey}` - - let existsParams = { - [foreignId]: {'===': knex.raw(localId)}, - [parts[0]]: criteria - }; - query.whereExists(this.filterQuery(relationResourceConfig, existsParams, options)); - criteria = null; // criteria handled by EXISTS subquery + let foreignId = `${relationTable}.${relationResourceConfig.idAttribute}` + + query.join(relationTable, localId, foreignId) + joinedTables.push(relationPath.join('.')) } - } else { - // hopefully a qualified local column + } else if (relation.type === 'hasMany') { + // Perform `WHERE EXISTS` subquery for hasMany property + let table = getTable(localResourceConfig) + let localId = `${table}.${localResourceConfig.idAttribute}` + + let relationTable = getTable(relationResourceConfig) + let foreignId = `${relationTable}.${relation.foreignKey}` + + let existsParams = { + [foreignId]: {'===': knex.raw(localId)}, + [parts[0]]: criteria + }; + query.whereExists(this.filterQuery(relationResourceConfig, existsParams, options)); + criteria = null; // criteria handled by EXISTS subquery } localResourceConfig = relationResourceConfig - } + } else { + // hopefully a qualified local column + } } return `${getTable(localResourceConfig)}.${parts[0]}` diff --git a/test/findAll.spec.js b/test/findAll.spec.js index 087ac79..d98a8b1 100644 --- a/test/findAll.spec.js +++ b/test/findAll.spec.js @@ -46,7 +46,7 @@ describe('DSSqlAdapter#findAll', function () { assert.equal(posts[2].content, 'baz'); }); - it('should filter using a hasMany relation', function* () { + it("should filter using a hasMany relation's name", function* () { let user1 = yield adapter.create(User, {name: 'Sean'}); let post1 = yield adapter.create(Post, {userId: user1.id, content: 'foo'}); let post2 = yield adapter.create(Post, {userId: user1.id, content: 'bar'}); @@ -65,6 +65,26 @@ describe('DSSqlAdapter#findAll', function () { assert.equal(users[0].name, 'Sean'); assert.equal(users[1].name, 'Jason'); }); + + it("should filter using a hasMany relation's localField", function* () { + let user1 = yield adapter.create(User, {name: 'Sean'}); + let post1 = yield adapter.create(Post, {userId: user1.id, content: 'foo'}); + let post2 = yield adapter.create(Post, {userId: user1.id, content: 'bar'}); + let post3 = yield adapter.create(Post, {userId: user1.id, content: 'baz'}); + + let user2 = yield adapter.create(User, {name: 'Jason'}); + let post4 = yield adapter.create(Post, {userId: user2.id, content: 'foo'}); + let post5 = yield adapter.create(Post, {userId: user2.id, content: 'bar'}); + + let user3 = yield adapter.create(User, {name: 'Ed'}); + let post6 = yield adapter.create(Post, {userId: user3.id, content: 'bar'}); + let post7 = yield adapter.create(Post, {userId: user3.id, content: 'baz'}); + + let users = yield adapter.findAll(User, {where: {'posts.content': {'==': 'foo'} }}); + assert.equal(users.length, 2); + assert.equal(users[0].name, 'Sean'); + assert.equal(users[1].name, 'Jason'); + }); describe('near', function () { beforeEach(function * () { From 30c2d1d7b7614c8eb40ee18f5eb5e4e9daf3606f Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 01:58:23 -0500 Subject: [PATCH 6/7] Prefer "native" impl instead of DSUtils/mout.js --- src/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 9fe3fdb..20a149d 100644 --- a/src/index.js +++ b/src/index.js @@ -84,12 +84,12 @@ function loadWithRelations (items, resourceConfig, options) { if (instance) { let itemKeys = instance[def.localKeys] || [] - itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys) + itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys) localKeys = localKeys.concat(itemKeys || []) } else { DSUtils.forEach(items, item => { let itemKeys = item[def.localKeys] || [] - itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys) + itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys) localKeys = localKeys.concat(itemKeys || []) }) } @@ -175,7 +175,7 @@ class DSSqlAdapter { .where(resourceConfig.idAttribute, toString(id)) .then(rows => { if (!rows.length) { - return DSUtils.Promise.reject(new Error('Not Found!')) + return Promise.reject(new Error('Not Found!')) } else { instance = rows[0] return loadWithRelations.call(this, instance, resourceConfig, options) @@ -265,7 +265,7 @@ class DSSqlAdapter { params.orderBy = params.orderBy || params.sort params.skip = params.skip || params.offset - DSUtils.forEach(DSUtils.keys(params), k => { + DSUtils.forEach(Object.keys(params), k => { let v = params[k] if (!DSUtils.contains(reserved, k)) { if (DSUtils.isObject(v)) { From e11c77a3131cf845858ea55fc3c47a332281bdbd Mon Sep 17 00:00:00 2001 From: Sean Lynch Date: Fri, 22 Jan 2016 08:53:13 -0500 Subject: [PATCH 7/7] Stable Version 0.11.9 --- dist/js-data-sql.js | 148 +++++++++++++++++++++++--------------------- package.json | 2 +- 2 files changed, 77 insertions(+), 73 deletions(-) diff --git a/dist/js-data-sql.js b/dist/js-data-sql.js index 0370f42..99b1adb 100644 --- a/dist/js-data-sql.js +++ b/dist/js-data-sql.js @@ -47,10 +47,10 @@ module.exports = 'use strict'; - 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; }; })(); - 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"); } }; })(); + 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; }; })(); + function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 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; } @@ -68,66 +68,6 @@ module.exports = return resourceConfig.table || underscore(resourceConfig.name); } - /** - * Lookup and apply table joins to query if field contains a `.` - * @param {string} field - Field defined in where filter - * @param {object} query - knex query to modify - * @param {object} resourceConfig - Resource of primary query/table - * @param {string[]} existingJoins - Array of fully qualitifed field names for - * any existing table joins for query - * @returns {string} - field updated to perspective of applied joins - */ - function applyTableJoins(field, query, resourceConfig, existingJoins) { - if (DSUtils.contains(field, '.')) { - (function () { - var parts = field.split('.'); - var localResourceConfig = resourceConfig; - - var relationPath = []; - - var _loop = function _loop() { - var relationName = parts.shift(); - var relationResourceConfig = resourceConfig.getResource(relationName); - relationPath.push(relationName); - - if (!existingJoins.some(function (t) { - return t === relationPath.join('.'); - })) { - var _localResourceConfig$ = localResourceConfig.relationList.filter(function (r) { - return r.relation === relationName; - }); - - var _localResourceConfig$2 = _slicedToArray(_localResourceConfig$, 1); - - var relation = _localResourceConfig$2[0]; - - if (relation) { - var table = getTable(localResourceConfig); - var localId = table + '.' + relation.localKey; - - var relationTable = getTable(relationResourceConfig); - var foreignId = relationTable + '.' + relationResourceConfig.idAttribute; - - query.join(relationTable, localId, foreignId); - existingJoins.push(relationPath.join('.')); - } else { - // hopefully a qualified local column - } - } - localResourceConfig = relationResourceConfig; - }; - - while (parts.length >= 2) { - _loop(); - } - - field = getTable(localResourceConfig) + '.' + parts[0]; - })(); - } - - return field; - } - function loadWithRelations(items, resourceConfig, options) { var _this = this; @@ -198,12 +138,12 @@ module.exports = if (instance) { var itemKeys = instance[def.localKeys] || []; - itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys); localKeys = localKeys.concat(itemKeys || []); } else { DSUtils.forEach(items, function (item) { var itemKeys = item[def.localKeys] || []; - itemKeys = Array.isArray(itemKeys) ? itemKeys : DSUtils.keys(itemKeys); + itemKeys = Array.isArray(itemKeys) ? itemKeys : Object.keys(itemKeys); localKeys = localKeys.concat(itemKeys || []); }); } @@ -296,7 +236,7 @@ module.exports = var query = options && options.transaction || this.query; return query.select('*').from(getTable(resourceConfig)).where(resourceConfig.idAttribute, toString(id)).then(function (rows) { if (!rows.length) { - return DSUtils.Promise.reject(new Error('Not Found!')); + return Promise.reject(new Error('Not Found!')); } else { instance = rows[0]; return loadWithRelations.call(_this2, instance, resourceConfig, options); @@ -386,7 +326,10 @@ module.exports = }, { key: 'filterQuery', value: function filterQuery(resourceConfig, params, options) { + var _this7 = this; + var table = getTable(resourceConfig); + var joinedTables = []; var query = undefined; if (params instanceof Object.getPrototypeOf(this.query.client).QueryBuilder) { @@ -404,9 +347,7 @@ module.exports = params.orderBy = params.orderBy || params.sort; params.skip = params.skip || params.offset; - var joinedTables = []; - - DSUtils.forEach(DSUtils.keys(params), function (k) { + DSUtils.forEach(Object.keys(params), function (k) { var v = params[k]; if (!DSUtils.contains(reserved, k)) { if (DSUtils.isObject(v)) { @@ -428,19 +369,82 @@ module.exports = }; } - DSUtils.forOwn(criteria, function (v, op) { - // Apply table joins (if needed) + var processRelationField = function processRelationField(field) { + var parts = field.split('.'); + var localResourceConfig = resourceConfig; + var relationPath = []; + + var _loop = function _loop() { + var relationName = parts.shift(); + + var _localResourceConfig$ = localResourceConfig.relationList.filter(function (r) { + return r.relation === relationName || r.localField === relationName; + }); + + var _localResourceConfig$2 = _slicedToArray(_localResourceConfig$, 1); + + var relation = _localResourceConfig$2[0]; + + if (relation) { + var relationResourceConfig = resourceConfig.getResource(relation.relation); + relationPath.push(relation.relation); + + if (relation.type === 'belongsTo' || relation.type === 'hasOne') { + // Apply table join for belongsTo/hasOne property (if not done already) + if (!joinedTables.some(function (t) { + return t === relationPath.join('.'); + })) { + var _table = getTable(localResourceConfig); + var localId = _table + '.' + relation.localKey; + + var relationTable = getTable(relationResourceConfig); + var foreignId = relationTable + '.' + relationResourceConfig.idAttribute; + + query.join(relationTable, localId, foreignId); + joinedTables.push(relationPath.join('.')); + } + } else if (relation.type === 'hasMany') { + var _existsParams; + + // Perform `WHERE EXISTS` subquery for hasMany property + var _table2 = getTable(localResourceConfig); + var localId = _table2 + '.' + localResourceConfig.idAttribute; + + var relationTable = getTable(relationResourceConfig); + var foreignId = relationTable + '.' + relation.foreignKey; + + var existsParams = (_existsParams = {}, _defineProperty(_existsParams, foreignId, { '===': knex.raw(localId) }), _defineProperty(_existsParams, parts[0], criteria), _existsParams); + query.whereExists(_this7.filterQuery(relationResourceConfig, existsParams, options)); + criteria = null; // criteria handled by EXISTS subquery + } + + localResourceConfig = relationResourceConfig; + } else { + // hopefully a qualified local column + } + }; + + while (parts.length >= 2) { + _loop(); + } + + return getTable(localResourceConfig) + '.' + parts[0]; + }; + + if (DSUtils.contains(field, '.')) { if (DSUtils.contains(field, ',')) { var splitFields = field.split(',').map(function (c) { return c.trim(); }); field = splitFields.map(function (splitField) { - return applyTableJoins(splitField, query, resourceConfig, joinedTables); + return processRelationField(splitField); }).join(','); } else { - field = applyTableJoins(field, query, resourceConfig, joinedTables); + field = processRelationField(field, query, resourceConfig, joinedTables); } + } + DSUtils.forOwn(criteria, function (v, op) { if (op === '==' || op === '===') { if (v === null) { query = query.whereNull(field); diff --git a/package.json b/package.json index 3d14273..591041d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "js-data-sql", "description": "Postgres/MySQL/MariaDB/SQLite3 adapter for js-data.", - "version": "0.11.8", + "version": "0.11.9", "homepage": "http://www.js-data.io/docs/dssqladapter", "repository": { "type": "git",