Skip to content

Commit

Permalink
Merge pull request #161 from identity-com/CIV-2732_aggregation_pipeline
Browse files Browse the repository at this point in the history
Aggregation pipeline
  • Loading branch information
jpsantosbh authored Feb 19, 2021
2 parents 213b4e1 + 999116b commit 1cabf81
Show file tree
Hide file tree
Showing 4 changed files with 173 additions and 0 deletions.
54 changes: 54 additions & 0 deletions __test__/aggregate.test.js
Original file line number Diff line number Diff line change
@@ -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' }]);
});
});
54 changes: 54 additions & 0 deletions __test__/services/AggregationService.test.js
Original file line number Diff line number Diff line change
@@ -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' }]);
});
});
63 changes: 63 additions & 0 deletions src/AggregationHandler.js
Original file line number Diff line number Diff line change
@@ -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;
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down

0 comments on commit 1cabf81

Please sign in to comment.