Skip to content

Commit 720f3c9

Browse files
Siim Samsaaqilniz
authored andcommitted
fix: null value not persisted for properties of type JSON, Any, or Object
Signed-off-by: Siim Sams <siim.sams@katanamrp.com>
1 parent c905f02 commit 720f3c9

File tree

5 files changed

+173
-3
lines changed

5 files changed

+173
-3
lines changed

lib/connectors/memory.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -496,13 +496,53 @@ Memory.prototype._findAllSkippingIncludes = function(model, filter) {
496496

497497
// field selection
498498
if (filter.fields) {
499+
if (filter.count) filter.fields.push('count');
500+
if (filter.max) filter.fields.push('max');
501+
if (filter.min) filter.fields.push('min');
502+
if (filter.sum) filter.fields.push('sum');
503+
if (filter.avg) filter.fields.push('avg');
499504
nodes = nodes.map(utils.selectFields(filter.fields));
500505
}
501506

502507
// limit/skip
503508
const skip = filter.skip || filter.offset || 0;
504509
const limit = filter.limit || nodes.length;
510+
// groupBy
505511
nodes = nodes.slice(skip, skip + limit);
512+
if (filter.groupBy) {
513+
nodes = utils.groupBy(nodes, filter.groupBy);
514+
const tempNodes = [];
515+
Object.keys(nodes).forEach(nodeKey => {
516+
let count = undefined;
517+
const tempNode = {...nodes[nodeKey][0]};
518+
if (filter.count) {
519+
count = nodes[nodeKey].filter((obj) => {
520+
const id = obj[filter.count];
521+
return obj[filter.count] === id;
522+
}).length;
523+
tempNode.count = count;
524+
}
525+
if (filter.max) {
526+
tempNode.max = Math.max(...nodes[nodeKey].map(o => o[filter.max]));
527+
}
528+
if (filter.min) {
529+
tempNode.min = Math.min(...nodes[nodeKey].map(o => o[filter.min]));
530+
}
531+
if (filter.sum) {
532+
tempNode.sum = nodes[nodeKey].reduce((accumulator, object) => {
533+
return accumulator + object[filter.sum];
534+
}, 0);
535+
}
536+
if (filter.avg) {
537+
tempNode.avg = nodes[nodeKey].reduce((accumulator, object) => {
538+
return accumulator + object[filter.avg];
539+
}, 0);
540+
tempNode.avg = tempNode.avg / nodes[nodeKey].length;
541+
}
542+
tempNodes.push(tempNode);
543+
});
544+
nodes = tempNodes;
545+
}
506546
}
507547
return nodes;
508548

lib/dao.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,10 +179,12 @@ DataAccessObject._forDB = function(data) {
179179
const res = {};
180180
for (const propName in data) {
181181
const type = this.getPropertyType(propName);
182-
if (type === 'JSON' || type === 'Any' || type === 'Object' || data[propName] instanceof Array) {
183-
res[propName] = JSON.stringify(data[propName]);
182+
const value = data[propName];
183+
if (value !== null && (type === 'JSON' || type === 'Any' ||
184+
type === 'Object' || value instanceof Array)) {
185+
res[propName] = JSON.stringify(value);
184186
} else {
185-
res[propName] = data[propName];
187+
res[propName] = value;
186188
}
187189
}
188190
return res;
@@ -1928,6 +1930,18 @@ DataAccessObject.find = function find(query, options, cb) {
19281930
}
19291931
}
19301932

1933+
const keys = Object.keys(data);
1934+
keys.forEach(key => {
1935+
if (
1936+
key.includes('sumOf') ||
1937+
key.includes('countOf') ||
1938+
key.includes('avgOf') ||
1939+
key.includes('minOf') ||
1940+
key.includes('maxOf')
1941+
) {
1942+
obj.__data[key] = data[key];
1943+
}
1944+
});
19311945
callback(null, obj);
19321946
}
19331947

lib/utils.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ exports.idsHaveDuplicates = idsHaveDuplicates;
3131
exports.isClass = isClass;
3232
exports.escapeRegExp = escapeRegExp;
3333
exports.applyParentProperty = applyParentProperty;
34+
exports.groupBy = groupBy;
3435

3536
const g = require('strong-globalize')();
3637
const traverse = require('traverse');
@@ -893,3 +894,16 @@ function applyParentProperty(element, parent) {
893894
});
894895
}
895896
}
897+
898+
function groupBy(items, key) {
899+
return items.reduce(
900+
(result, item) => ({
901+
...result,
902+
[item[key]]: [
903+
...(result[item[key]] || []),
904+
item,
905+
],
906+
}),
907+
{},
908+
);
909+
}

test/crud-with-options.test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,56 @@ describe('crud-with-options', function() {
272272
User.find({limit: 3});
273273
});
274274

275+
it('should allow filter with groupBy, count, max, min, sum & avg', function(done) {
276+
User.find({
277+
groupBy: ['vip'],
278+
count: 'vip',
279+
max: 'id',
280+
min: 'id',
281+
sum: 'id',
282+
avg: 'id',
283+
}, options, function(err, users) {
284+
should.not.exist(err);
285+
should.exist(users);
286+
users.length.should.be.above(0);
287+
users.forEach(user => {
288+
user.should.have.property('count', user.count);
289+
user.should.have.property('max');
290+
user.should.have.property('min');
291+
user.should.have.property('sum');
292+
user.should.have.property('avg');
293+
});
294+
done();
295+
});
296+
});
297+
298+
it('should allow filter with groupBy, aggregate methods and other filters', function(done) {
299+
User.find({
300+
groupBy: ['vip'],
301+
count: 'vip',
302+
max: 'id',
303+
min: 'id',
304+
sum: 'id',
305+
avg: 'id',
306+
limit: 1,
307+
fields: ['name', 'id'],
308+
}, options, function(err, users) {
309+
should.not.exist(err);
310+
should.exist(users);
311+
users.length.should.be.equal(1);
312+
users.forEach(user => {
313+
user.should.have.property('count', user.count);
314+
user.should.have.property('max');
315+
user.should.have.property('min');
316+
user.should.have.property('sum');
317+
user.should.have.property('avg');
318+
user.should.have.property('name');
319+
user.should.have.property('id');
320+
});
321+
done();
322+
});
323+
});
324+
275325
it('should skip trailing undefined args', function(done) {
276326
User.find({limit: 3}, function(err, users) {
277327
should.exists(users);

test/loopback-dl.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1363,6 +1363,58 @@ describe('Model define with scopes configuration', function() {
13631363
});
13641364
});
13651365

1366+
describe('DataAccessObject._forDB', function() {
1367+
const ds = new DataSource('memory');
1368+
const dao = ds.DataAccessObject;
1369+
1370+
it('should return input data if dataSource is not relational', function() {
1371+
const inputData = {testKey: 'testValue'};
1372+
dao.getDataSource = () => ({isRelational: () => false});
1373+
1374+
const outputData = dao._forDB(inputData);
1375+
1376+
assert.deepEqual(outputData, inputData);
1377+
});
1378+
1379+
it('should return JSON stringified values for appropriate types', function() {
1380+
const inputData = {
1381+
key1: [1, 2, 3],
1382+
key2: {subKey: 'value'},
1383+
key3: 'nonJSONvalue',
1384+
};
1385+
dao.getDataSource = () => ({isRelational: () => true});
1386+
dao.getPropertyType = (propName) => (propName !== 'key3' ? 'JSON' : 'String');
1387+
1388+
const outputData = dao._forDB(inputData);
1389+
1390+
assert.deepEqual(outputData, {
1391+
key1: JSON.stringify([1, 2, 3]),
1392+
key2: JSON.stringify({subKey: 'value'}),
1393+
key3: 'nonJSONvalue',
1394+
});
1395+
});
1396+
1397+
it('should return original value for non JSON, non Array types', function() {
1398+
const inputData = {key1: 'string', key2: 123, key3: true};
1399+
dao.getDataSource = () => ({isRelational: () => true});
1400+
dao.getPropertyType = () => 'String';
1401+
1402+
const outputData = dao._forDB(inputData);
1403+
1404+
assert.deepEqual(outputData, inputData);
1405+
});
1406+
1407+
it('should not process null values', function() {
1408+
const inputData = {key1: 'value', key2: null};
1409+
dao.getDataSource = () => ({isRelational: () => true});
1410+
dao.getPropertyType = (propName) => 'JSON';
1411+
1412+
const outputData = dao._forDB(inputData);
1413+
1414+
assert.deepEqual(outputData, {key1: JSON.stringify('value'), key2: null});
1415+
});
1416+
});
1417+
13661418
describe('DataAccessObject', function() {
13671419
let ds, model, where, error, filter;
13681420

0 commit comments

Comments
 (0)