From 94fe234ef164597b0ba84bad5f60aee1773e0ed7 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Thu, 18 Feb 2021 10:21:05 -0300 Subject: [PATCH 1/2] AggregationService --- __test__/services/AggregationService.test.js | 22 +++++++++++ src/services/AggregationService.js | 40 ++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 __test__/services/AggregationService.test.js create mode 100644 src/services/AggregationService.js diff --git a/__test__/services/AggregationService.test.js b/__test__/services/AggregationService.test.js new file mode 100644 index 00000000..db6e1df7 --- /dev/null +++ b/__test__/services/AggregationService.test.js @@ -0,0 +1,22 @@ +const aggregate = require('../../src/services/AggregationService'); + +describe('Aggregation Service', () => { + it('should throw Invalid Operator', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + expect(() => { + aggregate(collection, [{ $toString: null }]); + }).toThrow('Invalid operator: $toString'); + }); + + it('should return a collection with same equality', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = aggregate(collection, [{ none: null }]); + expect(result).toStrictEqual(collection); + }); + + it('should return the first 2 elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = aggregate(collection, [{ $limit: 2 }]); + expect(result).toStrictEqual(collection.slice(0, 2)); + }); +}); diff --git a/src/services/AggregationService.js b/src/services/AggregationService.js new file mode 100644 index 00000000..936ef23a --- /dev/null +++ b/src/services/AggregationService.js @@ -0,0 +1,40 @@ +const _ = require('lodash'); + +class BaseAggregationOperator { + static filter(credentials) { + const filtered = [...credentials]; + return filtered; + } +} + +class LimitOperator extends BaseAggregationOperator { + static filter(credentials, params) { + if (!_.isNumber(params)) { + throw new Error('Limit param must be a integer'); + } + return [...(_.slice(credentials, 0, params))]; + } +} + + +const AGGREGATION_OPERATORS_MAP = { + none: BaseAggregationOperator.filter, + $limit: LimitOperator.filter, +}; + +function aggregate(credentials, stages) { + let filtered = [...credentials]; + _.forEach(stages, (stage) => { + const operator = _.keys(stage)[0]; + + if (!_.includes(_.keys(AGGREGATION_OPERATORS_MAP), operator)) { + throw new Error(`Invalid operator: ${operator}`); + } + const params = stage[operator]; + const operatorImplementation = AGGREGATION_OPERATORS_MAP[operator]; + filtered = operatorImplementation(filtered, params); + }); + return filtered; +} + +module.exports = aggregate; From 999116bcf1d822f87d20326425c1e21a3ae81306 Mon Sep 17 00:00:00 2001 From: Joao Santos Date: Fri, 19 Feb 2021 12:39:57 -0300 Subject: [PATCH 2/2] operators implementation --- __test__/aggregate.test.js | 54 +++++++++++++++++ __test__/services/AggregationService.test.js | 36 ++++++++++- src/AggregationHandler.js | 63 ++++++++++++++++++++ src/index.js | 2 + src/services/AggregationService.js | 40 ------------- 5 files changed, 153 insertions(+), 42 deletions(-) create mode 100644 __test__/aggregate.test.js create mode 100644 src/AggregationHandler.js delete mode 100644 src/services/AggregationService.js diff --git a/__test__/aggregate.test.js b/__test__/aggregate.test.js new file mode 100644 index 00000000..aa695a13 --- /dev/null +++ b/__test__/aggregate.test.js @@ -0,0 +1,54 @@ +const CredentialCommons = require('../src/index'); + +describe('CredentialCommons.aggregate', () => { + it('should throw Invalid Operator', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + expect(() => { + CredentialCommons.aggregate(collection, [{ $toString: null }]); + }).toThrow('Invalid operator: $toString'); + }); + + it('should return a collection with same equality', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ none: null }]); + expect(result).toStrictEqual(collection); + }); + + it('should return the first 2 elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ $limit: 2 }]); + expect(result).toStrictEqual([{ k: 'a' }, { k: 'b' }]); + }); + + it('should return the first elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ $first: 'true' }]); + expect(result).toStrictEqual([{ k: 'a' }]); + }); + + it('should return the last elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ $last: 'true' }]); + expect(result).toStrictEqual([{ k: 'c' }]); + }); + + it('should return in ascending order ', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ $sort: { k: 'ASC' } }]); + expect(result).toStrictEqual([{ k: 'a' }, { k: 'b' }, { k: 'c' }]); + }); + + it('should return in descending order ', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [{ $sort: { k: 'DES' } }]); + expect(result).toStrictEqual([{ k: 'c' }, { k: 'b' }, { k: 'a' }]); + }); + + it('should apply operations in order', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = CredentialCommons.aggregate(collection, [ + { $sort: { k: 'DES' } }, + { $limit: 2 }]); + expect(result).toStrictEqual([{ k: 'c' }, { k: 'b' }]); + }); +}); diff --git a/__test__/services/AggregationService.test.js b/__test__/services/AggregationService.test.js index db6e1df7..1c98df0a 100644 --- a/__test__/services/AggregationService.test.js +++ b/__test__/services/AggregationService.test.js @@ -1,4 +1,4 @@ -const aggregate = require('../../src/services/AggregationService'); +const aggregate = require('../../src/AggregationHandler'); describe('Aggregation Service', () => { it('should throw Invalid Operator', () => { @@ -17,6 +17,38 @@ describe('Aggregation Service', () => { it('should return the first 2 elements only', () => { const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; const result = aggregate(collection, [{ $limit: 2 }]); - expect(result).toStrictEqual(collection.slice(0, 2)); + expect(result).toStrictEqual([{ k: 'a' }, { k: 'b' }]); + }); + + it('should return the first elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = aggregate(collection, [{ $first: 'true' }]); + expect(result).toStrictEqual([{ k: 'a' }]); + }); + + it('should return the last elements only', () => { + const collection = [{ k: 'a' }, { k: 'b' }, { k: 'c' }]; + const result = aggregate(collection, [{ $last: 'true' }]); + expect(result).toStrictEqual([{ k: 'c' }]); + }); + + it('should return in ascending order ', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = aggregate(collection, [{ $sort: { k: 'ASC' } }]); + expect(result).toStrictEqual([{ k: 'a' }, { k: 'b' }, { k: 'c' }]); + }); + + it('should return in descending order ', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = aggregate(collection, [{ $sort: { k: 'DES' } }]); + expect(result).toStrictEqual([{ k: 'c' }, { k: 'b' }, { k: 'a' }]); + }); + + it('should apply operations in order', () => { + const collection = [{ k: 'b' }, { k: 'a' }, { k: 'c' }]; + const result = aggregate(collection, [ + { $sort: { k: 'DES' } }, + { $limit: 2 }]); + expect(result).toStrictEqual([{ k: 'c' }, { k: 'b' }]); }); }); diff --git a/src/AggregationHandler.js b/src/AggregationHandler.js new file mode 100644 index 00000000..7479dfda --- /dev/null +++ b/src/AggregationHandler.js @@ -0,0 +1,63 @@ +const _ = require('lodash'); + +const validateEmptyParametersOperators = (parameters) => { + if (!_.isEmpty(parameters)) { throw new Error('parameters should be empty'); } + return true; +}; +const validateNotEmptyParametersOperators = (parameters) => { + if (_.isEmpty(parameters)) { throw new Error('parameters should not be empty'); } + return true; +}; +const validatePathParametersOperators = (parameters) => { + if (!_.isString(parameters)) { throw new Error('parameters should be string'); } + return true; +}; +const validateNumberParametersOperators = (parameters) => { + if (!_.isNumber(parameters)) { throw new Error('parameters should be number'); } + return true; +}; +const validateObjectParametersOperators = (parameters) => { + if (!_.isObject(parameters)) { throw new Error('parameters should be object'); } + return true; +}; + +const sort = (colllection, params) => { + const path = _.keys(params)[0]; + const order = params[path]; + const ordered = _.sortBy(colllection, path); + return order === 'ASC' ? ordered : _.reverse(ordered); +}; + +const AGGREGATION_OPERATORS_MAP = { + none: (collection, params) => (validateEmptyParametersOperators(params) + ? [...collection] : null), + $limit: (collection, params) => (validateNumberParametersOperators(params) + ? [...(_.slice(collection, 0, params))] : null), + $min: (collection, params) => (validatePathParametersOperators(params) + ? [...(_.minBy(collection, params))] : null), + $max: (collection, params) => (validatePathParametersOperators(params) + ? [...(_.maxBy(collection, params))] : null), + $first: (collection, params) => (validateNotEmptyParametersOperators(params) + ? [_.first(collection)] : null), + $last: (collection, params) => (validateNotEmptyParametersOperators(params) + ? [_.last(collection)] : null), + $sort: (collection, params) => (validateObjectParametersOperators(params) + ? [...(sort(collection, params))] : null), +}; + +function aggregate(credentials, stages) { + let filtered = [...credentials]; + _.forEach(stages, (stage) => { + const operator = _.keys(stage)[0]; + + if (!_.includes(_.keys(AGGREGATION_OPERATORS_MAP), operator)) { + throw new Error(`Invalid operator: ${operator}`); + } + const params = stage[operator]; + const operatorImplementation = AGGREGATION_OPERATORS_MAP[operator]; + filtered = operatorImplementation(filtered, params); + }); + return filtered; +} + +module.exports = aggregate; diff --git a/src/index.js b/src/index.js index 995c1b79..7e18b4f6 100644 --- a/src/index.js +++ b/src/index.js @@ -7,6 +7,7 @@ const errors = require('./errors'); const constants = require('./constants'); const claimDefinitions = require('./claim/definitions'); const credentialDefinitions = require('./creds/definitions'); +const aggregate = require('./AggregationHandler'); /** * Entry Point for Civic Credential Commons @@ -20,6 +21,7 @@ function CredentialCommons() { this.isValidGlobalIdentifier = isValidGlobalIdentifier; this.isClaimRelated = isClaimRelated; this.services = services; + this.aggregate = aggregate; this.errors = errors; this.constants = constants; this.claimDefinitions = claimDefinitions; diff --git a/src/services/AggregationService.js b/src/services/AggregationService.js deleted file mode 100644 index 936ef23a..00000000 --- a/src/services/AggregationService.js +++ /dev/null @@ -1,40 +0,0 @@ -const _ = require('lodash'); - -class BaseAggregationOperator { - static filter(credentials) { - const filtered = [...credentials]; - return filtered; - } -} - -class LimitOperator extends BaseAggregationOperator { - static filter(credentials, params) { - if (!_.isNumber(params)) { - throw new Error('Limit param must be a integer'); - } - return [...(_.slice(credentials, 0, params))]; - } -} - - -const AGGREGATION_OPERATORS_MAP = { - none: BaseAggregationOperator.filter, - $limit: LimitOperator.filter, -}; - -function aggregate(credentials, stages) { - let filtered = [...credentials]; - _.forEach(stages, (stage) => { - const operator = _.keys(stage)[0]; - - if (!_.includes(_.keys(AGGREGATION_OPERATORS_MAP), operator)) { - throw new Error(`Invalid operator: ${operator}`); - } - const params = stage[operator]; - const operatorImplementation = AGGREGATION_OPERATORS_MAP[operator]; - filtered = operatorImplementation(filtered, params); - }); - return filtered; -} - -module.exports = aggregate;