Skip to content

Commit

Permalink
Release 1.0.3
Browse files Browse the repository at this point in the history
This release improves the module structure by separating the builers
into different files, also it improves the properties documentation
and adds a Stats Wrapper example

- Separated builders into files
- Improves documentation
- Adds example of stats wrapper
  • Loading branch information
Jonathan Casarrubias committed Apr 1, 2016
1 parent e106446 commit 6838de2
Show file tree
Hide file tree
Showing 10 changed files with 310 additions and 246 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Loopback Stats Mixin - CHANGELOG

The **loopback-stats-mixin** module change .

- **Version 1.0.3**.-
- Refactor builders into different files.
- Improved properties description for swagger by more specific details.
- Added Stats Wrapper example in README.md
- **Version 1.0.2**.-
- Added validation to verify micro-services to be wrapped actually exists.
- Added custom primary key support.
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ The following options are needed in order to create a micro-service to provide s
| description | String | No | Any | Loopback Explorer Description
| type | String | Yes | [model \| relation \| nested] | model, relation, nested
| relation | String | No | Model relation name | accounts iff Organization.accounts
| nested | String | No | Model nested property | locations iff Organization.locations
| count | Object | Yes | [on \| by \| as \| avg] | SEE COUNT OPTIONS

Different configurations can be specified depending on the needs, since you can create statistical information over Models, Relations and Nested Datasets, different configurations will be needed.
Expand All @@ -107,6 +108,55 @@ There are different ways to create statistical information and depending on the
| as | Number | No | Any numeric value (default: 1) | 1, 5, 10
| avg | Boolean | No | [true \| false] | true, false

STATS WRAPPER MIXIN
========

This mixin creates a [Remote Method](https://docs.strongloop.com/display/APIC/Remote+methods) that wraps multiple micro-service into 1 service bundle. It provides a way to group micro-services but also bundled services by creating a statistics tree allowing to fetch a full set of data for multiple stats in just one call.

#### EXAMPLE

The following is an example of how to create a `stats` bundled-service:

```json
"mixins": {
"StatsWrapper": [
{
"type": "model",
"method": "bundledStats",
"endpoint": "/bundled-stats",
"description": "Loopback Explorer Description",
"wraps": [
"microServiceStats1",
"microServiceStats2",
"microServiceStats3",
"microServiceStats4"
]
}
]
}
```

The code defined above would create a `localhost:3000/api/model/bundled-stats` endpoint that will result in an object containing the result from all of the micro-services wrapped.

`HIN: Wrapping can be done in multiple levels, so you can wrap a set of micro-services, but also a set of bundled-services by creating a tree of services.`

BOOT OPTIONS
=============

The following options are needed in order to create a bundled-service to provide statistical information regarding multiple micro-services.

`HINT: you can create as many bundled-services as you need.`

| Options | Type | Requried | Possible Values | Examples
|:-------------:|:-------------:|:-------------:|:---------------:| :------------------------:
| method | String | Yes | Any | stat, myStat, modelStat, etc
| endpoint | String | Yes | URL Form | /stats, /:id/stats
| description | String | No | Any | Loopback Explorer Description
| type | String | Yes | [model \| relation \| nested] | model, relation, nested
| relation | String | No | Model relation name | accounts iff Organization.accounts
| nested | String | No | Model nested property | locations iff Organization.locations
| wraps | [String] | Yes | Micro services name list | ['microService1', 'microService2', '..']


LICENSE
=============
Expand Down
25 changes: 25 additions & 0 deletions builders/accept-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';
/**
* Builds Parameters object for dynamic remote method
*/
module.exports = class AcceptBuilder {
/**
* Setters
*/
constructor(ctx) { this.ctx = ctx; }
/**
* Parse params according ctx type
*/
build() {
let accepts = [];
if (this.ctx.type === "relation" || this.ctx.type === "nested")
accepts.push({ arg: 'id', type: 'string', required: true, description: this.ctx.Model.definition.name + ' ID' });
if (this.ctx.type === "relation" && !this.ctx.relation)
accepts.push({ arg: 'relation', type: 'string', required: true, description: 'Relationship name' });
if (this.ctx.type === "nested")
accepts.push({ arg: 'nested', type: 'string', required: true, description: 'Nested array property name' });
accepts.push({ arg: 'range', type: 'string', required: true, description: 'Scale range (daily, weekly, monthly, annual)' });
accepts.push({ arg: 'where', type: 'object', description: 'Statement to filter ' + (this.ctx.relation || this.ctx.nested) });
return accepts;
}
}
23 changes: 23 additions & 0 deletions builders/params-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';
/**
* Builds Parameters object from dynamic arguments
*/
module.exports = class ParamsBuilder {
/**
* Setters
*/
constructor(ctx) { this.ctx = ctx; }
/**
* Parse params according ctx type
*/
build() {
if (this.ctx.type === "model")
return { range: this.ctx.arguments[0], where: this.ctx.arguments[1], next: this.ctx.arguments[2] };
if (this.ctx.type === "relation" && this.ctx.relation)
return { id: this.ctx.arguments[0], range: this.ctx.arguments[1], where: this.ctx.arguments[2], next: this.ctx.arguments[3] };
if (this.ctx.type === "relation" && !this.ctx.relation)
return { id: this.ctx.arguments[0], relation: this.ctx.arguments[1], range: this.ctx.arguments[2], where: this.ctx.arguments[3], next: this.ctx.arguments[4] };
if (this.ctx.type === "nested")
return { id: this.ctx.arguments[0], nested: this.ctx.arguments[1], range: this.ctx.arguments[2], where: this.ctx.arguments[3], next: this.ctx.arguments[4] };
}
}
14 changes: 14 additions & 0 deletions builders/pk-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';
/**
* Builds the primary key name depending on model configurations
*/
module.exports = class PrimaryKeyBuilder {
constructor(Model) { this.Model = Model; }
build() {
let pk = 'id';
if (!this.Model.settings.idInjection)
for (let key in this.Model.rawProperties)
if (this.Model.rawProperties[key].id) pk = key;
return pk;
}
}
52 changes: 52 additions & 0 deletions builders/query-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use strict';
/**
* Query Builder Dependencies
*/
var moment = require('moment');
/**
* Builds Loopback Query
*/
module.exports = class QueryBuilder {
/**
* Setters
*/
constructor(ctx) { this.ctx = ctx; }
onComplete(next) { this.finish = next; return this; }
/**
* Build Query
*/
build() {
// Build query object in scope
let query = {};
// lets add a where statement
query.where = this.ctx.params.where || {};
// If stat type is relation, then we set the root id
if ((this.ctx.type === 'relation' || this.ctx.type === 'nested') && this.ctx.params.id)
query.where[this.ctx.params.pk] = this.ctx.params.id;
// query.where[this.ctx.Model.settings.relations[this.ctx.params.relation].] = this.ctx.params.id;
// If stat type is relation, then we set the root id
if (this.ctx.type === 'relation' && this.ctx.params.relation)
query.include = this.ctx.params.relation;
// Set Range
if (this.ctx.params.range && this.ctx.count.on) {
query.where[this.ctx.count.on] = {};
switch (this.ctx.params.range) {
case 'weekly':
query.where[this.ctx.count.on].gt = moment(this.ctx.nowISOString).subtract(7, 'days').toDate();
break;
case 'monthly':
query.where[this.ctx.count.on].gt = moment(this.ctx.nowISOString).subtract(1, 'months').toDate();
break;
case 'annual':
query.where[this.ctx.count.on].gt = moment(this.ctx.nowISOString).subtract(1, 'years').toDate();
break;
case 'daily':
default:
query.where[this.ctx.count.on].gt = this.ctx.now.toDate();
break;
}
}
// Return result query
this.finish(null, query);
}
}
116 changes: 116 additions & 0 deletions builders/stats-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
'use strict';
/**
* Stats Builder Dependencies
*/
var moment = require('moment');
/**
* Builds Statistic Array from List of Resuls
*/
module.exports = class StatsBuilder {

constructor(ctx) { this.ctx = ctx; }

process(list) {
this.list = list;
let dataset = [];
let iterator = this.getIteratorCount();
for (let i = 1, dateIndex = iterator; i <= iterator; i++ , dateIndex--) {
let current = this.getCurrentMoment(dateIndex);
let count = this.getCurrentCount(current);
dataset.push({
date: current.toISOString(),
count: this.ctx.count.avg ? (count / list.length) : count
});
}
return dataset;
}

getCurrentCount(current) {
let count = 0;
this.list.forEach(item => {
let itemDate = moment(item[this.ctx.count.on]);
let itemFactor = this.getFactor(item);
switch (this.ctx.params.range) {
case 'weekly':
case 'monthly':
if (current.isSame(itemDate, 'day')) count = count + itemFactor;
break;
case 'annual':
if (current.isSame(itemDate, 'month')) count = count + itemFactor;
break;
case 'daily':
default:
if (current.isSame(itemDate, 'hour')) count = count + itemFactor;
break;
}
});
return count;
}

getFactor(item) {
let value;
// When count by index, the factor will always be 1
if (this.ctx.count.by === 'index')  {
value = 1;
} else {
// We get the value from the property, can be number or boolean
// When number we set that value as factor, else we evaluate
// the value depending on true/false value and this.ctx.count.as value
if (this.ctx.count.by.match(/\./)) {
value = this.ctx.count.by.split('.').reduce((a, b) => a[b] ? a[b] : 0, item);
} else {
value = item[this.ctx.count.by];
}
// When value is boolean we set 0, 1 or this.ctx.count.as to set a value when true
if (typeof value === 'boolean' && value === true) {
value = this.ctx.count.as ? this.ctx.count.as : 1;
} else if (typeof value === 'boolean' && value === false) {
value = 0;
}
}
// Make sure we send back a number
return typeof value === 'number' ? value : parseInt(value);
}

getCurrentMoment(index) {
let current;
switch (this.ctx.params.range) {
case 'weekly':
current = moment(this.ctx.nowISOString).subtract(index - 1, 'days');
break;
case 'monthly':
current = moment(this.ctx.nowISOString).subtract(index - 1, 'days');
break;
case 'annual':
current = moment(this.ctx.nowISOString).subtract(index - 1, 'months');
break;
case 'daily':
default:
current = moment(this.ctx.nowISOString).subtract(index - 1, 'hours');
break;
}
return current;
}

getIteratorCount() {
let iterator;

switch (this.ctx.params.range) {
case 'weekly':
iterator = 7;
break;
case 'monthly':
iterator = this.ctx.now.daysInMonth();
break;
case 'annual':
iterator = 12;
break;
case 'daily':
default:
iterator = 24;
break;
}

return iterator;
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "loopback-stats-mixin",
"version": "1.0.2",
"version": "1.0.3",
"author": "Jonathan Casarrubias <http://github.com/jonathan-casarrubias>",
"description": "A mixin to provide statistical functionallity for Loopback Models, Relations and Nested Datasets",
"main": "index.js",
Expand Down
Loading

0 comments on commit 6838de2

Please sign in to comment.