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 new file mode 100644 index 00000000..1c98df0a --- /dev/null +++ b/__test__/services/AggregationService.test.js @@ -0,0 +1,54 @@ +const aggregate = require('../../src/AggregationHandler'); + +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([{ 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;