@tetherto/mdk-core 0.0.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +572 -0
- package/index.js +519 -0
- package/lib/address-policy.js +124 -0
- package/lib/local-discovery.js +321 -0
- package/lib/services/actions.service.js +68 -0
- package/lib/services/alerts.service.js +207 -0
- package/lib/services/comments.service.js +118 -0
- package/lib/services/log-history.service.js +307 -0
- package/lib/services/logs.service.js +249 -0
- package/lib/services/pool-utils/constants.js +20 -0
- package/lib/services/pool-utils/time.js +49 -0
- package/lib/services/pool.service.js +397 -0
- package/lib/services/provisioning.service.js +420 -0
- package/lib/services/settings.service.js +26 -0
- package/lib/services/snaps.service.js +181 -0
- package/lib/services/stats.service.js +205 -0
- package/lib/stats/README.md +80 -0
- package/lib/stats/index.js +57 -0
- package/lib/stats/ops/alerts_aggr.js +40 -0
- package/lib/stats/ops/alerts_group_cnt.js +43 -0
- package/lib/stats/ops/arr_concat.js +27 -0
- package/lib/stats/ops/array_obj_calc.js +41 -0
- package/lib/stats/ops/avg.js +42 -0
- package/lib/stats/ops/cnt.js +19 -0
- package/lib/stats/ops/group.js +38 -0
- package/lib/stats/ops/group_avg.js +53 -0
- package/lib/stats/ops/group_cnt.js +38 -0
- package/lib/stats/ops/group_max.js +40 -0
- package/lib/stats/ops/group_multiple_stats.js +40 -0
- package/lib/stats/ops/group_sum.js +40 -0
- package/lib/stats/ops/index.js +35 -0
- package/lib/stats/ops/nested_obj_concat.js +54 -0
- package/lib/stats/ops/obj_concat.js +33 -0
- package/lib/stats/ops/sum.js +27 -0
- package/lib/stats/utils.js +99 -0
- package/lib/templates/alerts.js +128 -0
- package/lib/templates/stats.js +476 -0
- package/lib/things/constants.js +65 -0
- package/lib/things/container.js +84 -0
- package/lib/things/miner.js +190 -0
- package/lib/things/powermeter.js +30 -0
- package/lib/things/sensor.js +18 -0
- package/lib/things/thing.js +93 -0
- package/lib/utils.js +603 -0
- package/lib/worker-infra.js +221 -0
- package/package.json +64 -1
- package/services.js +246 -0
- package/utils/compose-yaml.js +56 -0
- package/utils/constants.js +30 -0
- package/utils/index.js +29 -0
- package/utils/initialize.js +148 -0
- package/utils/service-bootstrap.js +271 -0
- package/worker.js +9 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const debug = require('debug')('mdk:stats')
|
|
4
|
+
const async = require('async')
|
|
5
|
+
const utilsStore = require('@tetherto/hp-svc-facs-store/utils')
|
|
6
|
+
const gLibStats = require('../stats')
|
|
7
|
+
|
|
8
|
+
const STAT_RTD = 'stat-rtd'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Aggregation over per-device `last` snapshots. `lib` is the stats spec
|
|
12
|
+
* module ({ specs, conf }) passed in directly. Device state comes from
|
|
13
|
+
* injected accessors:
|
|
14
|
+
* listDeviceIds() → [deviceId]
|
|
15
|
+
* getDeviceMeta(id) → { id, tags, info, opts, type }
|
|
16
|
+
* getLast(id) → { snap, alerts, err, ts } | undefined
|
|
17
|
+
* getRealtimeData(id) → latest snap without a device round-trip
|
|
18
|
+
*/
|
|
19
|
+
class StatsService {
|
|
20
|
+
constructor ({ lib, specTags, baseType, listDeviceIds, getDeviceMeta, getLast, getRealtimeData, processAlerts, logs, conf }) {
|
|
21
|
+
if (!logs) throw new Error('ERR_STATS_LOGS_REQUIRED')
|
|
22
|
+
this.lib = lib || null
|
|
23
|
+
this.specTags = specTags || []
|
|
24
|
+
this.baseType = baseType || 'thing'
|
|
25
|
+
this.listDeviceIds = listDeviceIds || (() => [])
|
|
26
|
+
this.getDeviceMeta = getDeviceMeta || (() => null)
|
|
27
|
+
this.getLast = getLast || (() => undefined)
|
|
28
|
+
this.getRealtimeData = getRealtimeData || null
|
|
29
|
+
this.processAlerts = processAlerts || (() => null)
|
|
30
|
+
this.logs = logs
|
|
31
|
+
this.conf = conf || {}
|
|
32
|
+
this._buildingStats = {}
|
|
33
|
+
this._collectingRtd = false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async buildStats (sk, fireTime) {
|
|
37
|
+
if (sk === STAT_RTD) {
|
|
38
|
+
return await this.saveRealTimeData()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!this.lib) {
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const lkr = `_buildingStats_${sk}`
|
|
46
|
+
|
|
47
|
+
if (this._buildingStats[lkr]) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this._buildingStats[lkr] = true
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
await this.buildStatsForKey(sk, fireTime)
|
|
55
|
+
} finally {
|
|
56
|
+
this._buildingStats[lkr] = false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async saveRealTimeData () {
|
|
61
|
+
if (this._collectingRtd) return
|
|
62
|
+
if (!this.getRealtimeData) return
|
|
63
|
+
this._collectingRtd = true
|
|
64
|
+
|
|
65
|
+
const thgsRtd = {}
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
await async.eachLimit(
|
|
69
|
+
this.listDeviceIds(),
|
|
70
|
+
this.conf.thingRtdConcurrency || 500,
|
|
71
|
+
async (deviceId) => {
|
|
72
|
+
const meta = this.getDeviceMeta(deviceId)
|
|
73
|
+
if (!meta) return
|
|
74
|
+
thgsRtd[deviceId] = { ...meta, last: this.getLast(deviceId) || {} }
|
|
75
|
+
try {
|
|
76
|
+
const rtd = await this.getRealtimeData(deviceId)
|
|
77
|
+
if (!rtd) return
|
|
78
|
+
|
|
79
|
+
const dev = { ...meta, last: this.getLast(deviceId) || {} }
|
|
80
|
+
thgsRtd[deviceId] = {
|
|
81
|
+
...meta,
|
|
82
|
+
last: { snap: rtd, alerts: this.processAlerts(dev) }
|
|
83
|
+
}
|
|
84
|
+
} catch (e) {
|
|
85
|
+
debug('ERR_RTD %s %s', deviceId, e.message)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
} finally {
|
|
90
|
+
this._collectingRtd = false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!Object.keys(thgsRtd).length) return
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const aggrData = this.aggrStats(Object.keys(thgsRtd), {}, thgsRtd)
|
|
97
|
+
const log = await this.logs.getBeeTimeLog(
|
|
98
|
+
`${STAT_RTD}-t-${this.baseType}`,
|
|
99
|
+
0,
|
|
100
|
+
true
|
|
101
|
+
)
|
|
102
|
+
await log.put(STAT_RTD, Buffer.from(JSON.stringify(aggrData)))
|
|
103
|
+
await this.logs.releaseBeeTimeLog(log)
|
|
104
|
+
} catch (e) {
|
|
105
|
+
debug('ERR_SAVING_RTD %s', e.message)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
aggrStats (thgIds, opts = {}, thgs = null) {
|
|
110
|
+
const acc = {}
|
|
111
|
+
if (!this.lib) return acc
|
|
112
|
+
|
|
113
|
+
const specs = this.lib.specs
|
|
114
|
+
const ids = thgIds || this.listDeviceIds()
|
|
115
|
+
|
|
116
|
+
this.specTags.forEach(stype => {
|
|
117
|
+
if (!specs[stype]) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const state = {}
|
|
122
|
+
state.ops = specs[stype].ops
|
|
123
|
+
|
|
124
|
+
for (const thgId of ids) {
|
|
125
|
+
const thg = thgs ? thgs[thgId] : this._deviceEntry(thgId)
|
|
126
|
+
if (!thg) continue
|
|
127
|
+
const last = thg.last || {}
|
|
128
|
+
const info = thg.info || {}
|
|
129
|
+
gLibStats.applyStats(
|
|
130
|
+
state,
|
|
131
|
+
acc,
|
|
132
|
+
{ last, info },
|
|
133
|
+
{ id: thg.id, tags: thg.tags, info, opts: thg.opts, type: thg.type }
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
gLibStats.tallyStats(state, acc)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
return acc
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_deviceEntry (deviceId) {
|
|
144
|
+
const meta = this.getDeviceMeta(deviceId)
|
|
145
|
+
if (!meta) return null
|
|
146
|
+
return { ...meta, last: this.getLast(deviceId) || {} }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async buildStatsForKey (logKey, fireTime) {
|
|
150
|
+
const now = Math.floor(fireTime.getTime() / 1000) * 1000
|
|
151
|
+
|
|
152
|
+
// Null prototype: a stored tag named `__proto__` or `constructor` must not
|
|
153
|
+
// resolve to a built-in and break the build for every device.
|
|
154
|
+
const tagSpecs = Object.create(null)
|
|
155
|
+
const thgIdsAll = this.listDeviceIds()
|
|
156
|
+
if (!this.lib) return
|
|
157
|
+
|
|
158
|
+
const skipTagPfxs = this.lib.conf?.skipTagPrefixes || []
|
|
159
|
+
|
|
160
|
+
thgIdsAll.forEach(thgId => {
|
|
161
|
+
const meta = this.getDeviceMeta(thgId)
|
|
162
|
+
if (!meta) return
|
|
163
|
+
|
|
164
|
+
const tags = Array.isArray(meta.tags) ? meta.tags : []
|
|
165
|
+
tags.forEach(tag => {
|
|
166
|
+
if (typeof tag !== 'string' || !tag) return
|
|
167
|
+
if (skipTagPfxs.find(p => tag.startsWith(p))) {
|
|
168
|
+
return
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!tagSpecs[tag]) {
|
|
172
|
+
tagSpecs[tag] = { thgIds: [] }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
tagSpecs[tag].thgIds.push(thgId)
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const tags = Object.keys(tagSpecs)
|
|
180
|
+
|
|
181
|
+
for (const tag of tags) {
|
|
182
|
+
const tagSpec = tagSpecs[tag]
|
|
183
|
+
|
|
184
|
+
const acc = this.aggrStats(tagSpec.thgIds)
|
|
185
|
+
const log = await this.logs.getBeeTimeLog(`${logKey}-${tag}`, 0, true)
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await log.put(
|
|
189
|
+
utilsStore.convIntToBin(now),
|
|
190
|
+
Buffer.from(JSON.stringify({
|
|
191
|
+
ts: now,
|
|
192
|
+
...acc
|
|
193
|
+
}))
|
|
194
|
+
)
|
|
195
|
+
} catch (e) {
|
|
196
|
+
debug('ERR_BUILD_STATS %s %s', tag, e.message)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
await this.logs.releaseBeeTimeLog(log)
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = StatsService
|
|
205
|
+
module.exports.STAT_RTD = STAT_RTD
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Lib-stats
|
|
2
|
+
|
|
3
|
+
Telemetry aggregation library. Provides a set of composable statistical operations that device Workers use to compute time-bucketed metrics (hashrate averages, power totals, group counts, etc.).
|
|
4
|
+
|
|
5
|
+
Part of `@tetherto/mdk-core` and exported as `libStats`. It is used by `StatsService`
|
|
6
|
+
([`lib/services/stats.service.js`](../services/stats.service.js)) and by each Worker plugin's `lib/templates/stats.js`, for example
|
|
7
|
+
[the Avalon plugin's](../../../../workers/miners/avalon/lib/templates/stats.js).
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
const { libStats } = require('@tetherto/mdk-core')
|
|
13
|
+
const { applyStats, tallyStats, defaults } = libStats
|
|
14
|
+
|
|
15
|
+
// Define what to compute
|
|
16
|
+
const state = {
|
|
17
|
+
ops: {
|
|
18
|
+
total_power: { op: 'sum', field: 'power_w' },
|
|
19
|
+
avg_hashrate: { op: 'avg', field: 'hashrate_mhs' },
|
|
20
|
+
by_container: { op: 'group_by', field: 'container' }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const pack = {}
|
|
25
|
+
|
|
26
|
+
// Feed entries one at a time
|
|
27
|
+
for (const entry of deviceSnapshots) {
|
|
28
|
+
applyStats(state, pack, entry, extData)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Finalize (resolve running accumulators to result values)
|
|
32
|
+
tallyStats(state, pack)
|
|
33
|
+
|
|
34
|
+
// pack.total_power, pack.avg_hashrate, pack.by_container now contain results
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `applyStats(state, pack, entry, ext)`
|
|
40
|
+
|
|
41
|
+
Apply all configured operations to a single `entry`. Accumulates intermediate state in `pack`. Call once per device snapshot per time bucket.
|
|
42
|
+
|
|
43
|
+
### `tallyStats(state, pack)`
|
|
44
|
+
|
|
45
|
+
Finalize the accumulation. Resolves running counters/sums into their final values and replaces each key in `pack` with its `.res` field.
|
|
46
|
+
|
|
47
|
+
### `defaults.timeframes`
|
|
48
|
+
|
|
49
|
+
Array of `[label, cronExpression]` pairs for the standard time buckets:
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
[
|
|
53
|
+
['5m', '0 */5 * * * *'],
|
|
54
|
+
['30m', '0 */30 * * * *'],
|
|
55
|
+
['3h', '0 0 */3 * * *'],
|
|
56
|
+
['1D', '0 0 0 * * *']
|
|
57
|
+
]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Operations
|
|
61
|
+
|
|
62
|
+
| Op name | File | Description |
|
|
63
|
+
|---------|------|-------------|
|
|
64
|
+
| `cnt` | [`ops/cnt.js`](./ops/cnt.js) | Count entries |
|
|
65
|
+
| `sum` | [`ops/sum.js`](./ops/sum.js) | Numeric sum |
|
|
66
|
+
| `avg` | [`ops/avg.js`](./ops/avg.js) | Running average |
|
|
67
|
+
| `group` | [`ops/group.js`](./ops/group.js) | Group entries by a field |
|
|
68
|
+
| `group_sum` | [`ops/group_sum.js`](./ops/group_sum.js) | Sum within groups |
|
|
69
|
+
| `group_avg` | [`ops/group_avg.js`](./ops/group_avg.js) | Average within groups |
|
|
70
|
+
| `group_cnt` | [`ops/group_cnt.js`](./ops/group_cnt.js) | Count within groups |
|
|
71
|
+
| `group_max` | [`ops/group_max.js`](./ops/group_max.js) | Max within groups |
|
|
72
|
+
| `group_multiple_stats` | [`ops/group_multiple_stats.js`](./ops/group_multiple_stats.js) | Multiple aggregations per group |
|
|
73
|
+
| `arr_concat` | [`ops/arr_concat.js`](./ops/arr_concat.js) | Concatenate arrays across entries |
|
|
74
|
+
| `obj_concat` | [`ops/obj_concat.js`](./ops/obj_concat.js) | Merge objects across entries |
|
|
75
|
+
| `nested_obj_concat` | [`ops/nested_obj_concat.js`](./ops/nested_obj_concat.js) | Deep-merge nested objects |
|
|
76
|
+
| `array_obj_calc` | [`ops/array_obj_calc.js`](./ops/array_obj_calc.js) | Compute over arrays of objects |
|
|
77
|
+
| `alerts_aggr` | [`ops/alerts_aggr.js`](./ops/alerts_aggr.js) | Aggregate alert states |
|
|
78
|
+
| `alerts_group_cnt` | [`ops/alerts_group_cnt.js`](./ops/alerts_group_cnt.js) | Count alerts by group |
|
|
79
|
+
|
|
80
|
+
Each operation module exports `{ calc, tally }`. `calc` is called per entry; `tally` is called once to finalize.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getVal, groupBy } = require('./utils')
|
|
4
|
+
const { getOp } = require('./ops')
|
|
5
|
+
|
|
6
|
+
function applyStats (state, pack, entry, ext) {
|
|
7
|
+
const opKeys = Object.keys(state.ops)
|
|
8
|
+
|
|
9
|
+
opKeys.forEach(ok => {
|
|
10
|
+
const op = state.ops[ok]
|
|
11
|
+
|
|
12
|
+
const met = getOp(op.op)
|
|
13
|
+
|
|
14
|
+
if (!met.calc) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pack[ok] = met.calc(op, pack[ok], entry, ext)
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function tallyStats (state, pack) {
|
|
23
|
+
const opKeys = Object.keys(state.ops)
|
|
24
|
+
|
|
25
|
+
opKeys.forEach(ok => {
|
|
26
|
+
const op = state.ops[ok]
|
|
27
|
+
|
|
28
|
+
const met = getOp(op.op)
|
|
29
|
+
|
|
30
|
+
if (!pack[ok]) {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (met.tally) {
|
|
35
|
+
pack[ok] = met.tally(op, pack[ok])
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
pack[ok] = pack[ok].res
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_TIMEFRAMES = [
|
|
43
|
+
['5m', '0 */5 * * * *'],
|
|
44
|
+
['30m', '0 */30 * * * *'],
|
|
45
|
+
['3h', '0 0 */3 * * *'],
|
|
46
|
+
['1D', '0 0 0 * * *']
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
defaults: {
|
|
51
|
+
timeframes: DEFAULT_TIMEFRAMES
|
|
52
|
+
},
|
|
53
|
+
applyStats,
|
|
54
|
+
tallyStats,
|
|
55
|
+
getVal,
|
|
56
|
+
groupBy
|
|
57
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('./../utils')
|
|
4
|
+
|
|
5
|
+
function alertsAggr (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: lUtils.createResMap() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const groups = lUtils.getVal(entry, op.src)
|
|
19
|
+
|
|
20
|
+
if (!groups) {
|
|
21
|
+
return cur
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Object.keys(groups).forEach(gk => {
|
|
25
|
+
if (!lUtils.isSafeKey(gk)) return
|
|
26
|
+
const gv = groups[gk]
|
|
27
|
+
|
|
28
|
+
if (!cur.res[gk]) {
|
|
29
|
+
cur.res[gk] = 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cur.res[gk] += gv
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return cur
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = {
|
|
39
|
+
calc: alertsAggr
|
|
40
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('./../utils')
|
|
4
|
+
|
|
5
|
+
function alertsGroupCnt (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: lUtils.createResMap() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const alerts = lUtils.getVal(entry, op.src)
|
|
19
|
+
|
|
20
|
+
if (!alerts) {
|
|
21
|
+
return cur
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
alerts.forEach(al => {
|
|
25
|
+
if (!al.severity) {
|
|
26
|
+
al.severity = 'medium'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!lUtils.isSafeKey(al.severity)) return
|
|
30
|
+
|
|
31
|
+
if (!cur.res[al.severity]) {
|
|
32
|
+
cur.res[al.severity] = 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cur.res[al.severity]++
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return cur
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
calc: alertsGroupCnt
|
|
43
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('./../utils')
|
|
4
|
+
|
|
5
|
+
function arrConcat (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: [] }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const val = lUtils.getVal(entry, op.src)
|
|
19
|
+
|
|
20
|
+
if (val !== null && val !== undefined) cur.res = cur.res.concat(val)
|
|
21
|
+
|
|
22
|
+
return cur
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
calc: arrConcat
|
|
27
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('../utils')
|
|
4
|
+
const { getOp } = require('./index')
|
|
5
|
+
|
|
6
|
+
function arrayObjCalc (op, cur, entry, ext) {
|
|
7
|
+
if (!cur) {
|
|
8
|
+
cur = { res: lUtils.createResMap() }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!entry) {
|
|
12
|
+
return cur
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (op.filter && !op.filter(entry)) {
|
|
16
|
+
return cur
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const arr = lUtils.getVal(entry, op.src)
|
|
20
|
+
arr?.forEach(obj => {
|
|
21
|
+
op.objOps.forEach(keyOp => {
|
|
22
|
+
if (!lUtils.isSafeKey(keyOp.src)) return
|
|
23
|
+
const val = lUtils.getVal(obj, keyOp.src)
|
|
24
|
+
if (!val) return
|
|
25
|
+
if (!cur.res[keyOp.src]) {
|
|
26
|
+
cur.res[keyOp.src] = val
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const calcFn = getOp(keyOp.op)
|
|
31
|
+
const calcRes = calcFn.calc(keyOp, { res: cur.res[keyOp.src] }, obj)
|
|
32
|
+
cur.res[keyOp.src] = calcRes.res
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
return cur
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
calc: arrayObjCalc
|
|
41
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('./../utils')
|
|
4
|
+
|
|
5
|
+
function avg (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { acc: 0, cnt: 0, res: 0 }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const val = lUtils.getVal(entry, op.src)
|
|
19
|
+
|
|
20
|
+
cur.acc += val ? +val : 0
|
|
21
|
+
|
|
22
|
+
cur.cnt++
|
|
23
|
+
|
|
24
|
+
return cur
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function tally (op, cur) {
|
|
28
|
+
if (cur.cnt) {
|
|
29
|
+
cur = {
|
|
30
|
+
res: cur.acc / cur.cnt
|
|
31
|
+
}
|
|
32
|
+
} else {
|
|
33
|
+
cur = { res: 0 }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return cur
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
calc: avg,
|
|
41
|
+
tally
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('./../utils')
|
|
4
|
+
|
|
5
|
+
function group (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: lUtils.createResMap() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (!op.src) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (op.filter && !op.filter(entry)) {
|
|
19
|
+
return cur
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!op.group) {
|
|
23
|
+
return cur
|
|
24
|
+
}
|
|
25
|
+
const groupName = op.group(entry, ext)
|
|
26
|
+
|
|
27
|
+
if (!groupName || !lUtils.isSafeKey(groupName)) {
|
|
28
|
+
return cur
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cur.res[groupName] = lUtils.getVal(entry, op.src)
|
|
32
|
+
|
|
33
|
+
return cur
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
calc: group
|
|
38
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('../utils')
|
|
4
|
+
|
|
5
|
+
function groupAvg (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: lUtils.createResMap() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!op.group) {
|
|
19
|
+
return cur
|
|
20
|
+
}
|
|
21
|
+
const groupName = op.group(entry, ext)
|
|
22
|
+
|
|
23
|
+
if (!groupName || !lUtils.isSafeKey(groupName)) {
|
|
24
|
+
return cur
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!cur.res[groupName]) {
|
|
28
|
+
cur.res[groupName] = { acc: 0, cnt: 0, res: 0 }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const val = lUtils.getVal(entry, op.src)
|
|
32
|
+
|
|
33
|
+
cur.res[groupName].acc += val ? +val : 0
|
|
34
|
+
cur.res[groupName].cnt++
|
|
35
|
+
|
|
36
|
+
return cur
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function tally (op, cur) {
|
|
40
|
+
for (const key in cur.res) {
|
|
41
|
+
if (cur.res[key].cnt) {
|
|
42
|
+
cur.res[key] = cur.res[key].acc / cur.res[key].cnt
|
|
43
|
+
} else {
|
|
44
|
+
cur.res[key] = null
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return cur
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
calc: groupAvg,
|
|
52
|
+
tally
|
|
53
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const lUtils = require('../utils')
|
|
4
|
+
|
|
5
|
+
function groupCnt (op, cur, entry, ext) {
|
|
6
|
+
if (!cur) {
|
|
7
|
+
cur = { res: lUtils.createResMap() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!entry) {
|
|
11
|
+
return cur
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (op.filter && !op.filter(entry)) {
|
|
15
|
+
return cur
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!op.group) {
|
|
19
|
+
return cur
|
|
20
|
+
}
|
|
21
|
+
const groupName = op.group(entry, ext)
|
|
22
|
+
|
|
23
|
+
if (!groupName || !lUtils.isSafeKey(groupName)) {
|
|
24
|
+
return cur
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (!cur.res[groupName]) {
|
|
28
|
+
cur.res[groupName] = 0
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
cur.res[groupName]++
|
|
32
|
+
|
|
33
|
+
return cur
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = {
|
|
37
|
+
calc: groupCnt
|
|
38
|
+
}
|