From 720f3c982d8a3400b50a489182625d5404a56dea Mon Sep 17 00:00:00 2001 From: Siim Sams Date: Mon, 19 Jun 2023 18:24:13 +0300 Subject: [PATCH] fix: null value not persisted for properties of type JSON, Any, or Object Signed-off-by: Siim Sams --- lib/connectors/memory.js | 40 ++++++++++++++++++++++++++ lib/dao.js | 20 +++++++++++-- lib/utils.js | 14 +++++++++ test/crud-with-options.test.js | 50 ++++++++++++++++++++++++++++++++ test/loopback-dl.test.js | 52 ++++++++++++++++++++++++++++++++++ 5 files changed, 173 insertions(+), 3 deletions(-) diff --git a/lib/connectors/memory.js b/lib/connectors/memory.js index 5c6be09cb..3858043a6 100644 --- a/lib/connectors/memory.js +++ b/lib/connectors/memory.js @@ -496,13 +496,53 @@ Memory.prototype._findAllSkippingIncludes = function(model, filter) { // field selection if (filter.fields) { + if (filter.count) filter.fields.push('count'); + if (filter.max) filter.fields.push('max'); + if (filter.min) filter.fields.push('min'); + if (filter.sum) filter.fields.push('sum'); + if (filter.avg) filter.fields.push('avg'); nodes = nodes.map(utils.selectFields(filter.fields)); } // limit/skip const skip = filter.skip || filter.offset || 0; const limit = filter.limit || nodes.length; + // groupBy nodes = nodes.slice(skip, skip + limit); + if (filter.groupBy) { + nodes = utils.groupBy(nodes, filter.groupBy); + const tempNodes = []; + Object.keys(nodes).forEach(nodeKey => { + let count = undefined; + const tempNode = {...nodes[nodeKey][0]}; + if (filter.count) { + count = nodes[nodeKey].filter((obj) => { + const id = obj[filter.count]; + return obj[filter.count] === id; + }).length; + tempNode.count = count; + } + if (filter.max) { + tempNode.max = Math.max(...nodes[nodeKey].map(o => o[filter.max])); + } + if (filter.min) { + tempNode.min = Math.min(...nodes[nodeKey].map(o => o[filter.min])); + } + if (filter.sum) { + tempNode.sum = nodes[nodeKey].reduce((accumulator, object) => { + return accumulator + object[filter.sum]; + }, 0); + } + if (filter.avg) { + tempNode.avg = nodes[nodeKey].reduce((accumulator, object) => { + return accumulator + object[filter.avg]; + }, 0); + tempNode.avg = tempNode.avg / nodes[nodeKey].length; + } + tempNodes.push(tempNode); + }); + nodes = tempNodes; + } } return nodes; diff --git a/lib/dao.js b/lib/dao.js index ada5541c1..555fd779f 100644 --- a/lib/dao.js +++ b/lib/dao.js @@ -179,10 +179,12 @@ DataAccessObject._forDB = function(data) { const res = {}; for (const propName in data) { const type = this.getPropertyType(propName); - if (type === 'JSON' || type === 'Any' || type === 'Object' || data[propName] instanceof Array) { - res[propName] = JSON.stringify(data[propName]); + const value = data[propName]; + if (value !== null && (type === 'JSON' || type === 'Any' || + type === 'Object' || value instanceof Array)) { + res[propName] = JSON.stringify(value); } else { - res[propName] = data[propName]; + res[propName] = value; } } return res; @@ -1928,6 +1930,18 @@ DataAccessObject.find = function find(query, options, cb) { } } + const keys = Object.keys(data); + keys.forEach(key => { + if ( + key.includes('sumOf') || + key.includes('countOf') || + key.includes('avgOf') || + key.includes('minOf') || + key.includes('maxOf') + ) { + obj.__data[key] = data[key]; + } + }); callback(null, obj); } diff --git a/lib/utils.js b/lib/utils.js index 4b80cb682..e64a33adb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -31,6 +31,7 @@ exports.idsHaveDuplicates = idsHaveDuplicates; exports.isClass = isClass; exports.escapeRegExp = escapeRegExp; exports.applyParentProperty = applyParentProperty; +exports.groupBy = groupBy; const g = require('strong-globalize')(); const traverse = require('traverse'); @@ -893,3 +894,16 @@ function applyParentProperty(element, parent) { }); } } + +function groupBy(items, key) { + return items.reduce( + (result, item) => ({ + ...result, + [item[key]]: [ + ...(result[item[key]] || []), + item, + ], + }), + {}, + ); +} diff --git a/test/crud-with-options.test.js b/test/crud-with-options.test.js index c1a9bd5cf..83c28e54d 100644 --- a/test/crud-with-options.test.js +++ b/test/crud-with-options.test.js @@ -272,6 +272,56 @@ describe('crud-with-options', function() { User.find({limit: 3}); }); + it('should allow filter with groupBy, count, max, min, sum & avg', function(done) { + User.find({ + groupBy: ['vip'], + count: 'vip', + max: 'id', + min: 'id', + sum: 'id', + avg: 'id', + }, options, function(err, users) { + should.not.exist(err); + should.exist(users); + users.length.should.be.above(0); + users.forEach(user => { + user.should.have.property('count', user.count); + user.should.have.property('max'); + user.should.have.property('min'); + user.should.have.property('sum'); + user.should.have.property('avg'); + }); + done(); + }); + }); + + it('should allow filter with groupBy, aggregate methods and other filters', function(done) { + User.find({ + groupBy: ['vip'], + count: 'vip', + max: 'id', + min: 'id', + sum: 'id', + avg: 'id', + limit: 1, + fields: ['name', 'id'], + }, options, function(err, users) { + should.not.exist(err); + should.exist(users); + users.length.should.be.equal(1); + users.forEach(user => { + user.should.have.property('count', user.count); + user.should.have.property('max'); + user.should.have.property('min'); + user.should.have.property('sum'); + user.should.have.property('avg'); + user.should.have.property('name'); + user.should.have.property('id'); + }); + done(); + }); + }); + it('should skip trailing undefined args', function(done) { User.find({limit: 3}, function(err, users) { should.exists(users); diff --git a/test/loopback-dl.test.js b/test/loopback-dl.test.js index a009be55a..e135224ce 100644 --- a/test/loopback-dl.test.js +++ b/test/loopback-dl.test.js @@ -1363,6 +1363,58 @@ describe('Model define with scopes configuration', function() { }); }); +describe('DataAccessObject._forDB', function() { + const ds = new DataSource('memory'); + const dao = ds.DataAccessObject; + + it('should return input data if dataSource is not relational', function() { + const inputData = {testKey: 'testValue'}; + dao.getDataSource = () => ({isRelational: () => false}); + + const outputData = dao._forDB(inputData); + + assert.deepEqual(outputData, inputData); + }); + + it('should return JSON stringified values for appropriate types', function() { + const inputData = { + key1: [1, 2, 3], + key2: {subKey: 'value'}, + key3: 'nonJSONvalue', + }; + dao.getDataSource = () => ({isRelational: () => true}); + dao.getPropertyType = (propName) => (propName !== 'key3' ? 'JSON' : 'String'); + + const outputData = dao._forDB(inputData); + + assert.deepEqual(outputData, { + key1: JSON.stringify([1, 2, 3]), + key2: JSON.stringify({subKey: 'value'}), + key3: 'nonJSONvalue', + }); + }); + + it('should return original value for non JSON, non Array types', function() { + const inputData = {key1: 'string', key2: 123, key3: true}; + dao.getDataSource = () => ({isRelational: () => true}); + dao.getPropertyType = () => 'String'; + + const outputData = dao._forDB(inputData); + + assert.deepEqual(outputData, inputData); + }); + + it('should not process null values', function() { + const inputData = {key1: 'value', key2: null}; + dao.getDataSource = () => ({isRelational: () => true}); + dao.getPropertyType = (propName) => 'JSON'; + + const outputData = dao._forDB(inputData); + + assert.deepEqual(outputData, {key1: JSON.stringify('value'), key2: null}); + }); +}); + describe('DataAccessObject', function() { let ds, model, where, error, filter;