Skip to content

Commit

Permalink
feat: implement workforce analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
lykmapipo committed Apr 29, 2020
1 parent dc7b3d7 commit c949460
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 14 deletions.
43 changes: 40 additions & 3 deletions src/aggregations/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,47 @@
# Convention

## Aggregation
## Aggregations
- Each aggregation should be named after a `model` it derived from
- Each aggregation should expose it base
- Each base aggregation should support initial criteria
- All aggregations are runned against data seeds available in `test/fixtures`

## Metric Fields
- All extra metric fields should be added into `metric` object to avoid collision with other aggregation fields
## Fields
- All extra metric fields should be added into `metrics` object to avoid collision with other aggregation fields
- All extra time fields should be in their singular form e.g `active`
- All extra time fields should be suffixed with `Time` e.g `activeTime`

## Structures

### Party Analysis
```js
{
overview: {
focal: Number,
agency: Number,
level: Number,
area: Number,
group: Number,
role: Number,
active: Number,
inactive: Number
},
overall: {
levels: [{
level: { name: { en: String }, color: String, weight: Number },
agency: Number,
focal: Number
}],
groups: [{
group: { name: { en: String }, color: String, weight: Number },
agency: Number,
focal: Number
}],
roles: [{
role: { name: { en: String }, color: String, weight: Number },
agency: Number,
focal: Number
}]
}
}
```
100 changes: 98 additions & 2 deletions src/aggregations/party.aggregations.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isFunction, merge } from 'lodash';
import { waterfall } from 'async';
import { Party } from '@codetanzania/emis-stakeholder';

// start: constants
Expand Down Expand Up @@ -27,9 +29,24 @@ export const PARTY_BASE_METRIC_FIELDS = {
agency: {
$cond: { if: { $ne: ['$type', PARTY_TYPE_FOCAL] }, then: 1, else: 0 },
},
level: {
$cond: { if: { $not: '$level' }, then: 0, else: 1 },
},
area: {
$cond: { if: { $not: '$area' }, then: 0, else: 1 },
},
group: {
$cond: { if: { $not: '$group' }, then: 0, else: 1 },
},
role: {
$cond: { if: { $not: '$role' }, then: 0, else: 1 },
},
active: {
$cond: { if: { $not: '$deletedAt' }, then: 1, else: 0 },
},
inactive: {
$cond: { if: { $not: '$deletedAt' }, then: 0, else: 1 },
},
},
};

Expand Down Expand Up @@ -141,9 +158,43 @@ export const PARTY_BASE_PROJECTION = {

// start: facets
// order: base, overall to specific
export const PARTY_FACET_OVERVIEW = {};

// start: functions
/**
* @constant
* @name PARTY_FACET_OVERVIEW
* @description Party overview facet.
* @type {object}
*
* @author lally elias <lallyelias87@gmail.com>
* @license MIT
* @since 0.2.0
* @version 0.1.0
*/
export const PARTY_FACET_OVERVIEW = {
overview: [
{
$group: {
_id: null,
total: { $sum: 1 },
agency: { $sum: '$metrics.agency' },
focal: { $sum: '$metrics.focal' },
level: { $sum: '$metrics.level' },
area: { $sum: '$metrics.area' },
group: { $sum: '$metrics.group' },
role: { $sum: '$metrics.role' },
active: { $sum: '$metrics.active' },
inactive: { $sum: '$metrics.inactive' },
},
},
{
$project: {
_id: 0,
},
},
],
};

// start: aggregations
// order: base to specific

/**
Expand Down Expand Up @@ -180,3 +231,48 @@ export const getPartyBaseAggregation = (criteria = {}) => {
// return party base aggregation
return base;
};

/**
* @function getPartyAnalysis
* @name getPartyAnalysis
* @description Create `Party` analysis.
* @param {object} [criteria={}] conditions which will be applied in analysis
* @param {Function} done callback to invoke on success or error
* @returns {object|Error} valid party analysis or error
*
* @author lally elias <lallyelias87@gmail.com>
* @license MIT
* @since 0.2.0
* @version 0.1.0
* @static
* @public
* @example
*
* getPartyAnalysis({ ... });
* //=> { data: { overview: { ... } }, ... }
*
*/
export const getPartyAnalysis = (criteria, done) => {
// normalize arguments
const filter = isFunction(criteria) ? {} : criteria;
const cb = isFunction(criteria) ? criteria : done;

// obtain party base aggregation
const base = getPartyBaseAggregation(filter);

// add facets
base.facet(PARTY_FACET_OVERVIEW);

// run aggregation
const aggregate = (next) => base.exec(next);
const normalize = (result, next) => {
// TODO: extract to utils
const data = merge(...result);
data.overview = merge(...data.overview);
return next(null, { data });
};
const tasks = [aggregate, normalize];

// return
return waterfall(tasks, cb);
};
11 changes: 8 additions & 3 deletions src/report.http.router.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { getString } from '@lykmapipo/env';
import { Router } from '@lykmapipo/express-rest-actions';

import { getPartyAnalysis } from './aggregations/party.aggregations';

/* constants */
const API_VERSION = getString('API_VERSION', '1.0.0');
const PATH_OVERVIEW = '/reports/overviews';
Expand Down Expand Up @@ -150,9 +152,12 @@ router.get(PATH_RESOURCES, (request, response) => {
* @version 1.0.0
* @public
*/
router.get(PATH_PARTIES, (request, response) => {
response.ok({
overview: { agency: 14, focal: 127 },
router.get(PATH_PARTIES, (request, response, next) => {
return getPartyAnalysis({}, (error, report) => {
if (error) {
return next(error);
}
return response.ok(report);
});
});

Expand Down
29 changes: 24 additions & 5 deletions test/integration/party.aggregations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,34 @@ import {
// enableDebug
} from '@lykmapipo/mongoose-test-helpers';

import { getPartyBaseAggregation } from '../../src';
import { getPartyBaseAggregation, getPartyAnalysis } from '../../src';

describe('Party Aggregations', () => {
it('should add extra metric fields', (done) => {
// enableDebug();
getPartyBaseAggregation().exec((error, found) => {
getPartyBaseAggregation().exec((error, report) => {
expect(error).to.not.exist;
expect(found).to.exist;
done(error, found);
expect(report).to.exist;
done(error, report);
});
});

it('should execute overview facet', (done) => {
getPartyAnalysis((error, report) => {
expect(error).to.not.exist;
expect(report).to.exist.and.be.an('object');
expect(report.data).to.exist.and.be.an('object');
expect(report.data.overview).to.be.eql({
total: 4,
agency: 2,
focal: 2,
level: 2,
area: 2,
group: 2,
role: 2,
active: 4,
inactive: 0,
});
done(error, report);
});
});
});
14 changes: 13 additions & 1 deletion test/integration/report.http.router.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,19 @@ describe('Reports Rest API', () => {
.expect('Content-Type', /json/)
.end((error, { body }) => {
expect(error).to.not.exist;
expect(body).to.exist;
expect(body).to.exist.and.be.an('object');
expect(body.data).to.exist.and.be.an('object');
expect(body.data.overview).to.be.eql({
total: 4,
agency: 2,
focal: 2,
level: 2,
area: 2,
group: 2,
role: 2,
active: 4,
inactive: 0,
});
done(error, body);
});
});
Expand Down
9 changes: 9 additions & 0 deletions test/unit/party.aggregations.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PARTY_BASE_GROUP_PROJECTION,
PARTY_BASE_ROLE_PROJECTION,
PARTY_BASE_PROJECTION,
PARTY_FACET_OVERVIEW,
getPartyBaseAggregation,
} from '../../src';

Expand All @@ -18,6 +19,10 @@ describe('Party Aggregations', () => {
expect(PARTY_BASE_METRIC_FIELDS.metrics.agency).to.exist.and.be.an(
'object'
);
expect(PARTY_BASE_METRIC_FIELDS.metrics.level).to.exist.and.be.an('object');
expect(PARTY_BASE_METRIC_FIELDS.metrics.area).to.exist.and.be.an('object');
expect(PARTY_BASE_METRIC_FIELDS.metrics.group).to.exist.and.be.an('object');
expect(PARTY_BASE_METRIC_FIELDS.metrics.role).to.exist.and.be.an('object');
expect(PARTY_BASE_METRIC_FIELDS.metrics.active).to.exist.and.be.an(
'object'
);
Expand All @@ -43,6 +48,10 @@ describe('Party Aggregations', () => {
expect(PARTY_BASE_PROJECTION).to.exist.and.be.an('object');
});

it('should have overview facet', () => {
expect(PARTY_FACET_OVERVIEW).to.exist.and.be.an('object');
});

it('should expose base factory', () => {
expect(getPartyBaseAggregation).to.exist.and.be.a('function');
});
Expand Down

0 comments on commit c949460

Please sign in to comment.