1 | const _ = require("lodash");
|
2 | const moment = require("moment");
|
3 | const querystring = require("querystring");
|
4 | const { safeToArray } = require("./util");
|
5 |
|
6 |
|
7 | const timeDims = [
|
8 | "year",
|
9 | "quarter",
|
10 | "month",
|
11 | "week",
|
12 | "isoWeek",
|
13 | "day",
|
14 | "hour",
|
15 | "min",
|
16 | "second",
|
17 | ];
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | const isTimeGroup = field => {
|
24 | const splitted = field.split(".");
|
25 |
|
26 | if (splitted.length <= 1) return false;
|
27 |
|
28 | return timeDims.includes(_.last(splitted));
|
29 | };
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 |
|
41 |
|
42 | const parseGroup = group => {
|
43 | const parsed = _.uniq(group).map(g => {
|
44 | const ret = { field: g };
|
45 | if (isTimeGroup(g)) {
|
46 | ret.field = _.slice(g.split("."), 0, -1).join(".");
|
47 | ret.timeDim = _.last(g.split("."));
|
48 |
|
49 | ret.key = (val, dim) => {
|
50 | return moment(val)
|
51 | .startOf(dim)
|
52 | .toISOString();
|
53 | };
|
54 | }
|
55 | return ret;
|
56 | });
|
57 |
|
58 | const chain = _.chain(parsed)
|
59 | .groupBy(p => p.field)
|
60 | .mapValues(o => {
|
61 | if (o.length <= 1) return o[0];
|
62 | else {
|
63 | return _.maxBy(o, g => _.indexOf(timeDims, g.timeDim));
|
64 | }
|
65 | });
|
66 |
|
67 | return chain.values().value();
|
68 | };
|
69 |
|
70 | function handleAggs(aggs = {}, req, res) {
|
71 | const path = req.path;
|
72 | const query = req.cusQuery || {};
|
73 | const agg = aggs[path];
|
74 | const data = res.locals.data;
|
75 |
|
76 | if (!agg) return data;
|
77 | if (!_.isArray(data)) return data;
|
78 |
|
79 | const { _group, _limit = 10, _start = 0, _order, _sort } = query;
|
80 |
|
81 | const group = safeToArray(_group);
|
82 |
|
83 | if (!group) return data;
|
84 |
|
85 | const parsed = parseGroup(group);
|
86 |
|
87 | const grouped = _.groupBy(data, r => {
|
88 | return querystring.stringify(
|
89 | parsed.map(g => {
|
90 | if (g.key) {
|
91 | return g.key(_.get(r, g.field), g.timeDim);
|
92 | }
|
93 |
|
94 | return _.get(r, g.field);
|
95 | })
|
96 | );
|
97 | });
|
98 |
|
99 | const result = _.keys(grouped).map(k => {
|
100 | const fileds = querystring.parse(k);
|
101 |
|
102 | const records = grouped[k];
|
103 |
|
104 |
|
105 | const dimensions = {};
|
106 |
|
107 | for (let i = 0; i < parsed.length; i++) {
|
108 | dimensions[parsed[i].field] = fileds[i];
|
109 | }
|
110 |
|
111 |
|
112 | const metrics = _.mapValues(agg, (aggConfig = "sum", m) => {
|
113 | let aggFun;
|
114 | if (_.isString(aggConfig)) {
|
115 | switch (aggConfig) {
|
116 | case "sum":
|
117 | aggFun = rs => _.sumBy(rs, r => r[m]);
|
118 | break;
|
119 | case "avg":
|
120 | aggFun = rs => _.sumBy(rs, r => r[m]) / rs.length;
|
121 | break;
|
122 | default:
|
123 | throw new Error("Not support aggregation config: " + aggConfig);
|
124 | }
|
125 | }
|
126 | if (_.isFunction(aggConfig)) {
|
127 | aggFun = aggConfig;
|
128 | }
|
129 |
|
130 | return aggFun(records);
|
131 | });
|
132 |
|
133 | return {
|
134 | id: querystring.stringify(dimensions),
|
135 | ...dimensions,
|
136 | ...metrics,
|
137 | };
|
138 | });
|
139 |
|
140 |
|
141 | res.setHeader("X-Total-Count", result.length);
|
142 |
|
143 | let chain = _.chain(result);
|
144 |
|
145 |
|
146 | if (_sort) {
|
147 | const _sortSet = _sort.split(",");
|
148 | const _orderSet = (_order || "").split(",").map(s => s.toLowerCase());
|
149 | chain = chain.orderBy(_sortSet, _orderSet);
|
150 | }
|
151 |
|
152 | chain = chain.slice(_start, _start + _limit);
|
153 |
|
154 |
|
155 |
|
156 | return chain.value();
|
157 | }
|
158 |
|
159 | module.exports.handleAggs = handleAggs;
|
160 | module.exports.parseGroup = parseGroup;
|