dobo 2.0.1 → 2.2.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/.github/FUNDING.yml +0 -0
- package/.github/workflows/repo-lockdown.yml +0 -0
- package/.jsdoc.conf.json +0 -0
- package/LICENSE +0 -0
- package/README.md +2 -2
- package/docs/Dobo.html +0 -0
- package/docs/data/search.json +0 -0
- package/docs/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/global.html +0 -0
- package/docs/index.html +0 -0
- package/docs/index.js.html +0 -0
- package/docs/lib_collect-connections.js.html +0 -0
- package/docs/lib_collect-drivers.js.html +0 -0
- package/docs/lib_collect-features.js.html +0 -0
- package/docs/lib_collect-schemas.js.html +0 -0
- package/docs/lib_index.js.html +0 -0
- package/docs/method_model_create.js.html +0 -0
- package/docs/method_model_drop.js.html +0 -0
- package/docs/method_model_exists.js.html +0 -0
- package/docs/method_record_count.js.html +0 -0
- package/docs/method_record_create.js.html +0 -0
- package/docs/method_record_find-all.js.html +0 -0
- package/docs/method_record_find-one.js.html +0 -0
- package/docs/method_record_find.js.html +0 -0
- package/docs/method_record_get.js.html +0 -0
- package/docs/method_record_remove.js.html +0 -0
- package/docs/method_record_update.js.html +0 -0
- package/docs/method_record_upsert.js.html +0 -0
- package/docs/method_sanitize_body.js.html +0 -0
- package/docs/method_sanitize_date.js.html +0 -0
- package/docs/method_sanitize_id.js.html +0 -0
- package/docs/method_validate.js.html +0 -0
- package/docs/module-Lib.html +0 -0
- package/docs/scripts/core.js +476 -477
- package/docs/scripts/core.min.js +0 -0
- package/docs/scripts/resize.js +36 -36
- package/docs/scripts/search.js +105 -105
- package/docs/scripts/search.min.js +0 -0
- package/docs/scripts/third-party/Apache-License-2.0.txt +0 -0
- package/docs/scripts/third-party/fuse.js +1 -1
- package/docs/scripts/third-party/hljs-line-num-original.js +282 -285
- package/docs/scripts/third-party/hljs-line-num.js +1 -1
- package/docs/scripts/third-party/hljs-original.js +1195 -1202
- package/docs/scripts/third-party/hljs.js +1 -1
- package/docs/scripts/third-party/popper.js +1 -1
- package/docs/scripts/third-party/tippy.js +1 -1
- package/docs/scripts/third-party/tocbot.js +508 -509
- package/docs/scripts/third-party/tocbot.min.js +0 -0
- package/docs/static/bitcoin.jpeg +0 -0
- package/docs/static/home.md +0 -0
- package/docs/static/logo-ecosystem.png +0 -0
- package/docs/static/logo.png +0 -0
- package/docs/styles/clean-jsdoc-theme-base.css +0 -0
- package/docs/styles/clean-jsdoc-theme-dark.css +0 -0
- package/docs/styles/clean-jsdoc-theme-light.css +0 -0
- package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -0
- package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -0
- package/docs/styles/clean-jsdoc-theme.min.css +0 -0
- package/extend/bajo/intl/en-US.json +66 -28
- package/extend/bajo/intl/id.json +55 -27
- package/extend/bajoCli/applet/clear-record.js +22 -0
- package/extend/bajoCli/applet/connection.js +0 -0
- package/extend/bajoCli/applet/count-record.js +27 -0
- package/extend/bajoCli/applet/create-aggregate.js +33 -0
- package/extend/bajoCli/applet/create-histogram.js +33 -0
- package/extend/bajoCli/applet/create-record.js +39 -0
- package/extend/bajoCli/applet/find-record.js +27 -0
- package/extend/bajoCli/applet/get-record.js +27 -0
- package/extend/bajoCli/applet/lib/post-process.js +10 -17
- package/extend/bajoCli/applet/model.js +22 -0
- package/extend/bajoCli/applet/rebuild-model.js +91 -0
- package/extend/bajoCli/applet/remove-record.js +27 -0
- package/extend/bajoCli/applet/update-record.js +44 -0
- package/extend/bajoCli/applet.js +0 -0
- package/extend/dobo/driver/memory.js +170 -0
- package/extend/dobo/feature/created-at.js +9 -7
- package/extend/dobo/feature/dt.js +0 -0
- package/extend/dobo/feature/immutable.js +30 -0
- package/extend/dobo/feature/int-id.js +0 -0
- package/extend/dobo/feature/removed-at.js +32 -54
- package/extend/dobo/feature/updated-at.js +14 -12
- package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +2 -6
- package/extend/waibuStatic/virtual.json +0 -0
- package/index.js +284 -371
- package/lib/collect-connections.js +49 -21
- package/lib/collect-drivers.js +19 -33
- package/lib/collect-features.js +24 -17
- package/lib/collect-models.js +321 -0
- package/lib/factory/action.js +161 -0
- package/lib/factory/connection.js +62 -0
- package/lib/factory/driver.js +372 -0
- package/lib/factory/feature.js +33 -0
- package/lib/factory/model/_util.js +402 -0
- package/lib/factory/model/build.js +15 -0
- package/lib/factory/model/clear-record.js +17 -0
- package/lib/factory/model/count-record.js +17 -0
- package/lib/factory/model/create-aggregate.js +17 -0
- package/lib/factory/model/create-attachment.js +29 -0
- package/lib/factory/model/create-histogram.js +17 -0
- package/lib/factory/model/create-record.js +35 -0
- package/lib/factory/model/drop.js +15 -0
- package/lib/factory/model/exists.js +21 -0
- package/lib/factory/model/find-all-record.js +71 -0
- package/lib/factory/model/find-attachment.js +29 -0
- package/lib/factory/model/find-one-record.js +19 -0
- package/{method/record/find.js → lib/factory/model/find-record.js} +103 -115
- package/lib/factory/model/get-attachment.js +15 -0
- package/lib/factory/model/get-record.js +79 -0
- package/lib/factory/model/list-attachment.js +37 -0
- package/lib/{add-fixtures.js → factory/model/load-fixtures.js} +69 -67
- package/lib/factory/model/remove-attachment.js +15 -0
- package/lib/factory/model/remove-record.js +59 -0
- package/lib/factory/model/sanitize-body.js +56 -0
- package/lib/factory/model/sanitize-id.js +7 -0
- package/lib/factory/model/sanitize-record.js +26 -0
- package/lib/factory/model/update-attachment.js +9 -0
- package/lib/factory/model/update-record.js +81 -0
- package/lib/factory/model/upsert-record.js +95 -0
- package/{method → lib/factory/model}/validate.js +38 -52
- package/lib/factory/model.js +150 -0
- package/lib/index.js +0 -0
- package/package.json +8 -4
- package/wiki/APPLETS.md +0 -0
- package/wiki/CHANGES.md +50 -0
- package/wiki/CONFIG.md +0 -0
- package/wiki/CONTRIBUTING.md +0 -0
- package/wiki/DEV-GUIDE.md +0 -0
- package/wiki/ECOSYSTEM.md +0 -0
- package/wiki/GETTING-STARTED.md +10 -10
- package/wiki/QUERY-LANGUAGE.md +0 -0
- package/wiki/USER-GUIDE.md +0 -0
- package/extend/bajoCli/applet/model-clear.js +0 -11
- package/extend/bajoCli/applet/model-rebuild.js +0 -101
- package/extend/bajoCli/applet/record-create.js +0 -43
- package/extend/bajoCli/applet/record-find.js +0 -28
- package/extend/bajoCli/applet/record-get.js +0 -24
- package/extend/bajoCli/applet/record-remove.js +0 -24
- package/extend/bajoCli/applet/record-update.js +0 -47
- package/extend/bajoCli/applet/schema.js +0 -22
- package/extend/bajoCli/applet/stat-count.js +0 -24
- package/lib/build-bulk-action.js +0 -12
- package/lib/check-unique.js +0 -39
- package/lib/collect-schemas.js +0 -91
- package/lib/exec-feature-hook.js +0 -13
- package/lib/exec-validation.js +0 -21
- package/lib/generic-prop-sanitizer.js +0 -32
- package/lib/handle-attachment-upload.js +0 -16
- package/lib/mem-db/conn-sanitizer.js +0 -8
- package/lib/mem-db/instantiate.js +0 -41
- package/lib/mem-db/method/model/clear.js +0 -6
- package/lib/mem-db/method/model/create.js +0 -5
- package/lib/mem-db/method/model/drop.js +0 -5
- package/lib/mem-db/method/model/exists.js +0 -5
- package/lib/mem-db/method/record/create.js +0 -12
- package/lib/mem-db/method/record/find.js +0 -20
- package/lib/mem-db/method/record/get.js +0 -9
- package/lib/mem-db/method/record/remove.js +0 -13
- package/lib/mem-db/method/record/update.js +0 -15
- package/lib/mem-db/method/stat/count.js +0 -11
- package/lib/mem-db/start.js +0 -25
- package/lib/merge-attachment-info.js +0 -16
- package/lib/multi-rel-rows.js +0 -42
- package/lib/resolve-method.js +0 -16
- package/lib/sanitize-schema.js +0 -198
- package/lib/single-rel-rows.js +0 -38
- package/method/attachment/copy-uploaded.js +0 -34
- package/method/attachment/create.js +0 -29
- package/method/attachment/find.js +0 -27
- package/method/attachment/get-path.js +0 -12
- package/method/attachment/get.js +0 -12
- package/method/attachment/pre-check.js +0 -9
- package/method/attachment/remove.js +0 -11
- package/method/attachment/update.js +0 -7
- package/method/bulk/create.js +0 -46
- package/method/model/clear.js +0 -22
- package/method/model/create.js +0 -32
- package/method/model/drop.js +0 -31
- package/method/model/exists.js +0 -37
- package/method/record/clear.js +0 -24
- package/method/record/count.js +0 -66
- package/method/record/create.js +0 -111
- package/method/record/find-all.js +0 -41
- package/method/record/find-one.js +0 -70
- package/method/record/get.js +0 -89
- package/method/record/remove.js +0 -72
- package/method/record/update.js +0 -104
- package/method/record/upsert.js +0 -51
- package/method/sanitize/body.js +0 -85
- package/method/sanitize/date.js +0 -27
- package/method/sanitize/id.js +0 -17
- package/method/stat/aggregate.js +0 -23
- package/method/stat/histogram.js +0 -26
package/index.js
CHANGED
|
@@ -1,42 +1,10 @@
|
|
|
1
1
|
import collectConnections from './lib/collect-connections.js'
|
|
2
2
|
import collectDrivers from './lib/collect-drivers.js'
|
|
3
3
|
import collectFeatures from './lib/collect-features.js'
|
|
4
|
-
import
|
|
5
|
-
import memDbStart from './lib/mem-db/start.js'
|
|
6
|
-
import memDbInstantiate from './lib/mem-db/instantiate.js'
|
|
7
|
-
import nql from '@tryghost/nql'
|
|
8
|
-
import path from 'path'
|
|
4
|
+
import collectModels from './lib/collect-models.js'
|
|
9
5
|
|
|
10
6
|
/**
|
|
11
|
-
* @typedef {
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Key value pairs used as sort information:
|
|
16
|
-
* - Key represent model's field name
|
|
17
|
-
* - value represent its sort order: ```1``` for ascending order, and ```-1``` for descending order
|
|
18
|
-
*
|
|
19
|
-
* Example: to sort by firstName (ascending) and lastName (descending)
|
|
20
|
-
* ```javascript
|
|
21
|
-
* const sort = {
|
|
22
|
-
* firstName: 1,
|
|
23
|
-
* lastName: -1
|
|
24
|
-
* }
|
|
25
|
-
* ```
|
|
26
|
-
*
|
|
27
|
-
* @typedef {Object.<string, TRecordSortKey>} TRecordSort
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* @typedef {Object} TRecordPagination
|
|
32
|
-
* @property {number} limit - Number of records per page
|
|
33
|
-
* @property {number} page - Page number
|
|
34
|
-
* @property {number} skip - Records to skip
|
|
35
|
-
* @property {TRecordSort} sort - Sort order
|
|
36
|
-
*/
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* @typedef {Object} TPropType
|
|
7
|
+
* @typedef {Object} TPropertyType
|
|
40
8
|
* @property {Object} integer
|
|
41
9
|
* @property {string} [integer.validator=number]
|
|
42
10
|
* @property {Object} smallint
|
|
@@ -66,48 +34,55 @@ import path from 'path'
|
|
|
66
34
|
* @property {Object} object={}
|
|
67
35
|
* @property {Object} array={}
|
|
68
36
|
*/
|
|
69
|
-
const
|
|
37
|
+
const propertyType = {
|
|
70
38
|
integer: {
|
|
71
|
-
validator: 'number'
|
|
39
|
+
validator: 'number',
|
|
40
|
+
rules: []
|
|
72
41
|
},
|
|
73
42
|
smallint: {
|
|
74
|
-
validator: 'number'
|
|
43
|
+
validator: 'number',
|
|
44
|
+
rules: []
|
|
75
45
|
},
|
|
76
46
|
text: {
|
|
77
47
|
validator: 'string',
|
|
78
48
|
textType: 'text',
|
|
79
|
-
values: ['text', 'mediumtext', 'longtext']
|
|
49
|
+
values: ['text', 'mediumtext', 'longtext'],
|
|
50
|
+
rules: []
|
|
80
51
|
},
|
|
81
52
|
string: {
|
|
82
53
|
validator: 'string',
|
|
83
|
-
maxLength:
|
|
84
|
-
minLength: 0
|
|
54
|
+
maxLength: 50,
|
|
55
|
+
minLength: 0,
|
|
56
|
+
rules: []
|
|
85
57
|
},
|
|
86
58
|
float: {
|
|
87
|
-
validator: 'number'
|
|
59
|
+
validator: 'number',
|
|
60
|
+
rules: []
|
|
88
61
|
},
|
|
89
62
|
double: {
|
|
90
|
-
validator: 'number'
|
|
63
|
+
validator: 'number',
|
|
64
|
+
rules: []
|
|
91
65
|
},
|
|
92
66
|
boolean: {
|
|
93
|
-
validator: 'boolean'
|
|
94
|
-
|
|
95
|
-
date: {
|
|
96
|
-
validator: 'date'
|
|
67
|
+
validator: 'boolean',
|
|
68
|
+
rules: []
|
|
97
69
|
},
|
|
98
70
|
datetime: {
|
|
99
|
-
validator: 'date'
|
|
71
|
+
validator: 'date',
|
|
72
|
+
rules: []
|
|
100
73
|
},
|
|
101
|
-
|
|
102
|
-
validator:
|
|
74
|
+
object: {
|
|
75
|
+
validator: null,
|
|
76
|
+
rules: []
|
|
103
77
|
},
|
|
104
|
-
|
|
105
|
-
validator:
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
array: {}
|
|
78
|
+
array: {
|
|
79
|
+
validator: null,
|
|
80
|
+
rules: []
|
|
81
|
+
}
|
|
109
82
|
}
|
|
110
83
|
|
|
84
|
+
const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default']
|
|
85
|
+
|
|
111
86
|
/**
|
|
112
87
|
* Plugin factory
|
|
113
88
|
*
|
|
@@ -116,6 +91,9 @@ const propType = {
|
|
|
116
91
|
*/
|
|
117
92
|
async function factory (pkgName) {
|
|
118
93
|
const me = this
|
|
94
|
+
const { breakNsPath } = this.app.bajo
|
|
95
|
+
|
|
96
|
+
const { find, filter, isString, map, pick, groupBy, isEmpty } = this.app.lib._
|
|
119
97
|
|
|
120
98
|
/**
|
|
121
99
|
* Dobo Database Framework for {@link https://github.com/ardhi/bajo|Bajo}.
|
|
@@ -124,25 +102,40 @@ async function factory (pkgName) {
|
|
|
124
102
|
*
|
|
125
103
|
* @class
|
|
126
104
|
*/
|
|
127
|
-
class Dobo extends this.app.
|
|
105
|
+
class Dobo extends this.app.baseClass.Base {
|
|
128
106
|
/**
|
|
129
|
-
* @constant {string}
|
|
107
|
+
* @constant {string[]}
|
|
130
108
|
* @memberof Dobo
|
|
131
|
-
* @default '
|
|
109
|
+
* @default ['count', 'avg', 'min', 'max', 'sum']
|
|
132
110
|
*/
|
|
133
|
-
static
|
|
111
|
+
static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
|
|
134
112
|
|
|
135
113
|
/**
|
|
136
114
|
* @constant {string[]}
|
|
137
115
|
* @memberof Dobo
|
|
138
|
-
* @default ['
|
|
116
|
+
* @default ['string', 'integer', 'smallint']
|
|
139
117
|
*/
|
|
140
|
-
static
|
|
118
|
+
static idTypes = ['string', 'integer', 'smallint']
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @constant {string[]}
|
|
122
|
+
* @memberof Dobo
|
|
123
|
+
* @default ['daily', 'monthly', 'annually']
|
|
124
|
+
*/
|
|
125
|
+
static histogramTypes = ['daily', 'monthly', 'yearly']
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @constant {TPropertyType}
|
|
129
|
+
* @memberof Dobo
|
|
130
|
+
*/
|
|
131
|
+
static propertyType = propertyType
|
|
132
|
+
|
|
141
133
|
/**
|
|
142
|
-
* @constant {
|
|
134
|
+
* @constant {string[]}
|
|
143
135
|
* @memberof Dobo
|
|
136
|
+
* @default ['index', 'unique', 'primary', 'fulltext']
|
|
144
137
|
*/
|
|
145
|
-
static
|
|
138
|
+
static indexTypes = ['index', 'unique', 'primary', 'fulltext']
|
|
146
139
|
|
|
147
140
|
constructor () {
|
|
148
141
|
super(pkgName, me.app)
|
|
@@ -154,32 +147,15 @@ async function factory (pkgName) {
|
|
|
154
147
|
allowUnknown: true
|
|
155
148
|
},
|
|
156
149
|
default: {
|
|
157
|
-
property: {
|
|
158
|
-
text: {
|
|
159
|
-
textType: 'text'
|
|
160
|
-
},
|
|
161
|
-
string: {
|
|
162
|
-
length: 50
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
150
|
filter: {
|
|
166
151
|
limit: 25,
|
|
167
152
|
maxLimit: 200,
|
|
168
153
|
hardLimit: 10000,
|
|
169
154
|
sort: ['dt:-1', 'updatedAt:-1', 'updated_at:-1', 'createdAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
|
|
170
|
-
},
|
|
171
|
-
idField: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
maxLength: 50,
|
|
174
|
-
required: true,
|
|
175
|
-
index: { type: 'primary' }
|
|
176
155
|
}
|
|
177
156
|
},
|
|
178
157
|
memDb: {
|
|
179
|
-
|
|
180
|
-
persistence: {
|
|
181
|
-
syncPeriodDur: '1s'
|
|
182
|
-
}
|
|
158
|
+
autoSaveDur: '1s'
|
|
183
159
|
},
|
|
184
160
|
applet: {
|
|
185
161
|
confirmation: false
|
|
@@ -204,7 +180,38 @@ async function factory (pkgName) {
|
|
|
204
180
|
/**
|
|
205
181
|
* @type {Object[]}
|
|
206
182
|
*/
|
|
207
|
-
this.
|
|
183
|
+
this.models = []
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get allowed property keys by field type
|
|
188
|
+
*
|
|
189
|
+
* @param {string} type
|
|
190
|
+
* @returns {string[]}
|
|
191
|
+
*/
|
|
192
|
+
getPropertyKeysByType = (type) => {
|
|
193
|
+
const keys = [...commonPropertyTypes]
|
|
194
|
+
if (['string'].includes(type)) keys.push('minLength', 'maxLength', 'values')
|
|
195
|
+
if (['text'].includes(type)) keys.push('textType')
|
|
196
|
+
if (['smallint', 'integer'].includes(type)) keys.push('autoInc', 'values')
|
|
197
|
+
if (['float', 'double'].includes(type)) keys.push('values')
|
|
198
|
+
return keys
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Get all allowed property keys
|
|
203
|
+
*
|
|
204
|
+
* @returns {string[]}
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
getAllPropertyKeys = (driver) => {
|
|
208
|
+
const { uniq, isEmpty } = this.app.lib._
|
|
209
|
+
const keys = [...commonPropertyTypes]
|
|
210
|
+
for (const type in propertyType) {
|
|
211
|
+
keys.push(...Object.keys(propertyType[type]))
|
|
212
|
+
}
|
|
213
|
+
if (driver && !isEmpty(driver.constructor.propertyKeys)) keys.push(...driver.constructor.propertyKeys)
|
|
214
|
+
return uniq(keys)
|
|
208
215
|
}
|
|
209
216
|
|
|
210
217
|
/**
|
|
@@ -212,31 +219,19 @@ async function factory (pkgName) {
|
|
|
212
219
|
* - {@link module:Lib.collectDrivers|Collecting all drivers}
|
|
213
220
|
* - {@link module:Lib.collectConnections|Collecting all connections}
|
|
214
221
|
* - {@link module:Lib.collectFeatures|Collecting all features}
|
|
215
|
-
* - {@link module:Lib.
|
|
222
|
+
* - {@link module:Lib.collectModels|Collecting all models}
|
|
216
223
|
* @method
|
|
217
224
|
* @async
|
|
218
225
|
*/
|
|
219
226
|
init = async () => {
|
|
220
|
-
const {
|
|
227
|
+
const { getPluginDataDir } = this.app.bajo
|
|
221
228
|
const { fs } = this.app.lib
|
|
222
|
-
const checkType = async (item, items) => {
|
|
223
|
-
const { filter } = this.app.lib._
|
|
224
|
-
const existing = filter(items, { type: 'dobo:memory' })
|
|
225
|
-
if (existing.length > 1) this.fatal('onlyOneConnType%s', item.type)
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
fs.ensureDirSync(`${this.dir.data}/attachment`)
|
|
229
229
|
await collectDrivers.call(this)
|
|
230
|
-
|
|
231
|
-
this.config.connections.push({
|
|
232
|
-
type: 'dobo:memory',
|
|
233
|
-
name: 'memory'
|
|
234
|
-
})
|
|
235
|
-
}
|
|
236
|
-
this.connections = await buildCollections({ ns: this.ns, container: 'connections', handler: collectConnections, dupChecks: ['name', checkType] })
|
|
237
|
-
if (this.connections.length === 0) this.log.warn('notFound%s', this.t('connection'))
|
|
230
|
+
await collectConnections.call(this)
|
|
238
231
|
await collectFeatures.call(this)
|
|
239
|
-
await
|
|
232
|
+
await collectModels.call(this)
|
|
233
|
+
const attDir = `${getPluginDataDir('dobo')}/attachment`
|
|
234
|
+
fs.ensureDirSync(attDir)
|
|
240
235
|
}
|
|
241
236
|
|
|
242
237
|
/**
|
|
@@ -245,239 +240,98 @@ async function factory (pkgName) {
|
|
|
245
240
|
* @method
|
|
246
241
|
* @async
|
|
247
242
|
* @param {(string|Array)} [conns=all] - Which connections should be run on start
|
|
248
|
-
* @param {boolean} [noRebuild=
|
|
243
|
+
* @param {boolean} [noRebuild=true] - Set ```false``` to not rebuild model on start. Yes, only set it to ```false``` if you REALLY know what you're doing!!!
|
|
249
244
|
*/
|
|
250
245
|
start = async (conns = 'all', noRebuild = true) => {
|
|
251
|
-
const { importModule, breakNsPath } = this.app.bajo
|
|
252
|
-
const { find, filter, isString, map } = this.app.lib._
|
|
253
246
|
if (conns === 'all') conns = this.connections
|
|
254
247
|
else if (isString(conns)) conns = filter(this.connections, { name: conns })
|
|
255
248
|
else conns = map(conns, c => find(this.connections, { name: c }))
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await mod.call(this.app[ns], { connection: c, noRebuild, schemas })
|
|
261
|
-
this.log.trace('driverInstantiated%s%s', c.driver, c.name)
|
|
249
|
+
this.log.debug('dbInit')
|
|
250
|
+
for (const connection of conns) {
|
|
251
|
+
await connection.connect(noRebuild)
|
|
252
|
+
this.log.trace('dbInit%s%s%s', connection.driver.plugin.ns, connection.driver.name, connection.name)
|
|
262
253
|
}
|
|
263
|
-
await memDbStart.call(this)
|
|
264
254
|
}
|
|
265
255
|
|
|
266
256
|
/**
|
|
267
|
-
*
|
|
257
|
+
* Get connection by name. It returns the first connection named after "{name}"
|
|
268
258
|
*
|
|
269
|
-
* @
|
|
270
|
-
* @
|
|
271
|
-
* @
|
|
272
|
-
* @param {Object} options.record - Record to pick fields from
|
|
273
|
-
* @param {Array} options.fields - Array of field names to be picked
|
|
274
|
-
* @param {Object} options.schema - Associated record's schema
|
|
275
|
-
* @param {Object} [options.hidden=[]] - Additional fields to be hidden in addition the one defined in schema
|
|
276
|
-
* @param {boolean} [options.forceNoHidden] - Force ALL fields to be picked, thus ignoring hidden fields
|
|
277
|
-
* @returns {Object}
|
|
259
|
+
* @param {string} name - Connection name
|
|
260
|
+
* @param {boolean} [silent] - If ```true``` and connection is not found, it won't throw error
|
|
261
|
+
* @returns {Driver} Return connection instance or ```undefined``` if silent is ```true```
|
|
278
262
|
*/
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const transform = async ({ record, schema, hidden = [], forceNoHidden } = {}) => {
|
|
284
|
-
if (record._id) {
|
|
285
|
-
record.id = record._id
|
|
286
|
-
delete record._id
|
|
287
|
-
}
|
|
288
|
-
const defHidden = [...schema.hidden, ...hidden]
|
|
289
|
-
let result = {}
|
|
290
|
-
for (const p of schema.properties) {
|
|
291
|
-
if (!forceNoHidden && defHidden.includes(p.name)) continue
|
|
292
|
-
result[p.name] = record[p.name] ?? null
|
|
293
|
-
if (record[p.name] === null) continue
|
|
294
|
-
switch (p.type) {
|
|
295
|
-
case 'boolean': result[p.name] = !!result[p.name]; break
|
|
296
|
-
case 'time': result[p.name] = dayjs(record[p.name]).format('HH:mm:ss'); break
|
|
297
|
-
case 'date': result[p.name] = dayjs(record[p.name]).format('YYYY-MM-DD'); break
|
|
298
|
-
case 'datetime': result[p.name] = dayjs(record[p.name]).toISOString(); break
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
result = await this.sanitizeBody({ body: result, schema, partial: true, ignoreNull: true })
|
|
302
|
-
if (record._rel) result._rel = record._rel
|
|
303
|
-
return result
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (isEmpty(record)) return record
|
|
307
|
-
if (hidden.length > 0) record = omit(record, hidden)
|
|
308
|
-
if (!isArray(fields)) return await transform.call(this, { record, schema, hidden, forceNoHidden })
|
|
309
|
-
const fl = clone(fields)
|
|
310
|
-
if (!fl.includes('id')) fl.unshift('id')
|
|
311
|
-
if (record._rel) fl.push('_rel')
|
|
312
|
-
return pick(await transform.call(this, { record, schema, hidden, forceNoHidden }), fl)
|
|
263
|
+
getConnection = (name, silent) => {
|
|
264
|
+
const conn = find(this.connections, { name })
|
|
265
|
+
if (!conn && !silent) throw this.error('unknown%s%s', this.t('connection'), name)
|
|
266
|
+
return conn
|
|
313
267
|
}
|
|
314
268
|
|
|
315
269
|
/**
|
|
316
|
-
*
|
|
317
|
-
* - making sure records limit is obeyed
|
|
318
|
-
* - making sure page is a positive value
|
|
319
|
-
* - if skip is given, recalculate limit to use skip instead of page number
|
|
320
|
-
* - Build sort info
|
|
270
|
+
* Get driver by name. It returns the first driver named after "{name}"
|
|
321
271
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @param {
|
|
325
|
-
* @param {
|
|
326
|
-
* @
|
|
327
|
-
* @returns {TRecordPagination}
|
|
272
|
+
* Also support NsPath format for those who load the same named driver but from different provider/plugin.
|
|
273
|
+
*
|
|
274
|
+
* @param {string} name - Driver name
|
|
275
|
+
* @param {boolean} [silent] - If ```true``` and driver is not found, it won't throw error
|
|
276
|
+
* @returns {Driver} Returns driver instance or ```undefined``` if silent is ```true```
|
|
328
277
|
*/
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
skip = parseInt(filter.skip) || skip
|
|
340
|
-
page = undefined
|
|
341
|
-
}
|
|
342
|
-
if (skip < 0) skip = 0
|
|
343
|
-
return { page, skip, limit }
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const buildSort = (input, schema, allowSortUnindexed) => {
|
|
347
|
-
const { isEmpty, map, each, isPlainObject, isString, trim, keys } = this.app.lib._
|
|
348
|
-
let sort
|
|
349
|
-
if (schema && isEmpty(input)) {
|
|
350
|
-
const columns = map(schema.properties, 'name')
|
|
351
|
-
each(this.config.default.filter.sort, s => {
|
|
352
|
-
const [col] = s.split(':')
|
|
353
|
-
if (columns.includes(col)) {
|
|
354
|
-
input = s
|
|
355
|
-
return false
|
|
356
|
-
}
|
|
357
|
-
})
|
|
358
|
-
}
|
|
359
|
-
if (!isEmpty(input)) {
|
|
360
|
-
if (isPlainObject(input)) sort = input
|
|
361
|
-
else if (isString(input)) {
|
|
362
|
-
const item = {}
|
|
363
|
-
each(input.split('+'), text => {
|
|
364
|
-
let [col, dir] = map(trim(text).split(':'), i => trim(i))
|
|
365
|
-
dir = (dir ?? '').toUpperCase()
|
|
366
|
-
dir = dir === 'DESC' ? -1 : parseInt(dir) || 1
|
|
367
|
-
item[col] = dir / Math.abs(dir)
|
|
368
|
-
})
|
|
369
|
-
sort = item
|
|
370
|
-
}
|
|
371
|
-
if (schema) {
|
|
372
|
-
const items = keys(sort)
|
|
373
|
-
each(items, i => {
|
|
374
|
-
if (!schema.sortables.includes(i) && !allowSortUnindexed) throw this.error('sortOnUnindexedField%s%s', i, schema.name)
|
|
375
|
-
// if (schema.fullText.fields.includes(i)) throw this.error('Can\'t sort on full-text index: \'%s@%s\'', i, schema.name)
|
|
376
|
-
})
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
return sort
|
|
278
|
+
getDriver = (name, silent) => {
|
|
279
|
+
const { breakNsPath } = this.app.bajo
|
|
280
|
+
let driver
|
|
281
|
+
if (!name.includes(':')) {
|
|
282
|
+
driver = find(this.drivers, { name })
|
|
283
|
+
if (driver) return driver
|
|
284
|
+
driver = filter(this.drivers, d => d.plugin.ns === name)
|
|
285
|
+
if (driver.length === 1) return driver[0]
|
|
286
|
+
if (!silent) throw this.error('unknown%s%s', this.t('driver'), name)
|
|
287
|
+
return
|
|
380
288
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
sortInput = JSON.parse(sortInput)
|
|
386
|
-
} catch (err) {}
|
|
387
|
-
const sort = buildSort.call(this, sortInput, schema, options.allowSortUnindexed)
|
|
388
|
-
return { limit, page, skip, sort }
|
|
289
|
+
const { ns, path } = breakNsPath(name)
|
|
290
|
+
driver = find(this.drivers, d => d.name === path && d.plugin.ns === ns)
|
|
291
|
+
if (!driver && !silent) throw this.error('unknown%s%s', this.t('driver'), name)
|
|
292
|
+
return driver
|
|
389
293
|
}
|
|
390
294
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
else if (input[0] === '{') items = JSON.parse(input)
|
|
406
|
-
else {
|
|
407
|
-
for (const item of input.split('+').map(i => i.trim())) {
|
|
408
|
-
const part = split(item, schema)
|
|
409
|
-
if (!items[part.field]) items[part.field] = []
|
|
410
|
-
items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
const matcher = {}
|
|
414
|
-
for (const f of schema.fullText.fields) {
|
|
415
|
-
const value = []
|
|
416
|
-
if (typeof items[f] === 'string') items[f] = [items[f]]
|
|
417
|
-
if (Object.prototype.hasOwnProperty.call(items, f)) value.push(...items[f])
|
|
418
|
-
matcher[f] = value
|
|
419
|
-
}
|
|
420
|
-
if (Object.prototype.hasOwnProperty.call(items, '*')) matcher['*'] = items['*']
|
|
421
|
-
return matcher
|
|
295
|
+
/**
|
|
296
|
+
* Get feature by name. It returns the first driver named after "{name}"
|
|
297
|
+
*
|
|
298
|
+
* Also support NsPath format for those who load the same named driver but from different provider/plugin.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} - Feature name
|
|
301
|
+
* @returns {Feature} Return feature instance
|
|
302
|
+
*/
|
|
303
|
+
getFeature = name => {
|
|
304
|
+
if (!name.includes(':')) return find(this.features, { name })
|
|
305
|
+
const { ns, path } = breakNsPath(name)
|
|
306
|
+
const feat = find(this.features, d => d.name === path && d.plugin.ns === ns)
|
|
307
|
+
if (!feat) throw this.error('unknown%s%s', this.t('feature'), name)
|
|
308
|
+
return feat
|
|
422
309
|
}
|
|
423
310
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
})
|
|
438
|
-
const parts = fields.map(f => {
|
|
439
|
-
if (filter.query[0] === '*') return `${f}:~$'${filter.query.replaceAll('*', '')}'`
|
|
440
|
-
if (filter.query[filter.length - 1] === '*') return `${f}:~^'${filter.query.replaceAll('*', '')}'`
|
|
441
|
-
return `${f}:~'${filter.query.replaceAll('*', '')}'`
|
|
442
|
-
})
|
|
443
|
-
if (parts.length === 1) query = nql(parts[0]).parse()
|
|
444
|
-
else if (parts.length > 1) query = nql(parts.join(',')).parse()
|
|
445
|
-
}
|
|
446
|
-
} catch (err) {
|
|
447
|
-
this.error('invalidQuery', { orgMessage: err.message })
|
|
448
|
-
}
|
|
449
|
-
} else if (isPlainObject(filter.query)) query = filter.query
|
|
450
|
-
return this.sanitizeQuery(query, schema)
|
|
311
|
+
/**
|
|
312
|
+
* Get model by name
|
|
313
|
+
*
|
|
314
|
+
* @param {string} name - Model name
|
|
315
|
+
* @param {boolean} [silent] - If ```true``` and model is not found, it won't throw error
|
|
316
|
+
* @retuns {Model} Returns model instance or ```undefined``` if silent is ```true```
|
|
317
|
+
*/
|
|
318
|
+
getModel = (name, silent) => {
|
|
319
|
+
const { pascalCase } = this.app.lib.aneka
|
|
320
|
+
let model = find(this.models, { name })
|
|
321
|
+
if (!model) model = find(this.models, { name: pascalCase(name) })
|
|
322
|
+
if (!model && !silent) throw this.error('unknown%s%s', this.t('model'), name)
|
|
323
|
+
return model
|
|
451
324
|
}
|
|
452
325
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (!prop) return val
|
|
463
|
-
if (['datetime', 'date', 'time'].includes(prop.type)) {
|
|
464
|
-
const dt = dayjs(val)
|
|
465
|
-
return dt.isValid() ? dt.toDate() : val
|
|
466
|
-
} else if (['smallint', 'integer'].includes(prop.type)) return parseInt(val) || val
|
|
467
|
-
else if (['float', 'double'].includes(prop.type)) return parseFloat(val) || val
|
|
468
|
-
else if (['boolean'].includes(prop.type)) return !!val
|
|
469
|
-
return val
|
|
470
|
-
}
|
|
471
|
-
keys.forEach(k => {
|
|
472
|
-
const v = obj[k]
|
|
473
|
-
if (isPlainObject(v)) obj[k] = this.sanitizeQuery(v, schema, k)
|
|
474
|
-
else if (isArray(v)) {
|
|
475
|
-
v.forEach((i, idx) => {
|
|
476
|
-
if (isPlainObject(i)) obj[k][idx] = this.sanitizeQuery(i, schema, k)
|
|
477
|
-
})
|
|
478
|
-
} else obj[k] = sanitize(k, v, parent)
|
|
479
|
-
})
|
|
480
|
-
return obj
|
|
326
|
+
/**
|
|
327
|
+
* Get all models that bound to connection ```name```
|
|
328
|
+
*
|
|
329
|
+
* @param {string} name - Connection name
|
|
330
|
+
* @returns {Array}
|
|
331
|
+
*/
|
|
332
|
+
getModelsByConnection = name => {
|
|
333
|
+
const conn = this.getConnection(name)
|
|
334
|
+
return this.models.filter(s => s.connection.name === conn.name)
|
|
481
335
|
}
|
|
482
336
|
|
|
483
337
|
validationErrorMessage = (err) => {
|
|
@@ -491,81 +345,140 @@ async function factory (pkgName) {
|
|
|
491
345
|
return text
|
|
492
346
|
}
|
|
493
347
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
348
|
+
/**
|
|
349
|
+
* Sanitize value as a date/time value. Parse/format string using {@link https://day.js.org/docs/en/display/format|dayjs format}
|
|
350
|
+
*
|
|
351
|
+
* @method
|
|
352
|
+
* @memberof Dobo
|
|
353
|
+
* @param {(number|string)} value - Value to sanitize
|
|
354
|
+
* @param {Object} [options={}] - Options object
|
|
355
|
+
* @param {boolean} [options.silent=true] - If ```true``` (default) and value isn't valid, returns empty
|
|
356
|
+
* @param {string} [options.inputFormat] - If provided, parse value using this option
|
|
357
|
+
* @param {string} [options.outputFormat] - If not provided or ```native```, returns Javascript Date. Otherwise returns formatted date/time string
|
|
358
|
+
* @returns {(string|Date)}
|
|
359
|
+
*/
|
|
360
|
+
sanitizeDate = (value, { inputFormat, outputFormat, silent = true } = {}) => {
|
|
361
|
+
const { dayjs } = this.app.lib
|
|
362
|
+
if (value === 0) return null
|
|
363
|
+
if (!outputFormat) outputFormat = inputFormat
|
|
364
|
+
const dt = dayjs(value, inputFormat)
|
|
365
|
+
if (!dt.isValid()) {
|
|
366
|
+
if (silent) return null
|
|
367
|
+
throw this.error('invalidDate')
|
|
368
|
+
}
|
|
369
|
+
if (outputFormat === 'native' || !outputFormat) return dt.toDate()
|
|
370
|
+
return dt.format(outputFormat)
|
|
497
371
|
}
|
|
498
372
|
|
|
499
|
-
|
|
500
|
-
const {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
let { ns, path: type } = breakNsPath(conn.type)
|
|
505
|
-
if (isEmpty(type)) type = conn.type
|
|
506
|
-
const driver = find(this.drivers, { type, ns, driver: conn.driver })
|
|
507
|
-
const instance = find(this.app[driver.ns].instances, { name: schema.connection })
|
|
508
|
-
const opts = conn.type === 'mssql' ? { includeTriggerModifications: true } : undefined
|
|
509
|
-
const returning = [map(schema.properties, 'name'), opts]
|
|
510
|
-
return { instance, driver, connection: conn, returning, schema }
|
|
373
|
+
sanitizeFloat = (value, { strict = false } = {}) => {
|
|
374
|
+
const { isNumber } = this.app.lib._
|
|
375
|
+
if (isNumber(value)) return value
|
|
376
|
+
if (strict) return Number(value)
|
|
377
|
+
return parseFloat(value) || null
|
|
511
378
|
}
|
|
512
379
|
|
|
513
|
-
|
|
514
|
-
const {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const schema = find(this.schemas, { name })
|
|
519
|
-
if (!schema) throw this.error('unknownModelSchema%s', name)
|
|
520
|
-
return cloned ? cloneDeep(schema) : schema
|
|
380
|
+
sanitizeInt = (value, { strict = false } = {}) => {
|
|
381
|
+
const { isNumber } = this.app.lib._
|
|
382
|
+
if (isNumber(value)) return value
|
|
383
|
+
if (strict) return Number(value)
|
|
384
|
+
return parseInt(value) || null
|
|
521
385
|
}
|
|
522
386
|
|
|
523
|
-
|
|
524
|
-
const {
|
|
525
|
-
|
|
526
|
-
|
|
387
|
+
sanitizeObject = (value) => {
|
|
388
|
+
const { isString } = this.app.lib._
|
|
389
|
+
let result = null
|
|
390
|
+
if (isString(value)) {
|
|
391
|
+
try {
|
|
392
|
+
result = JSON.parse(value)
|
|
393
|
+
} catch (err) {}
|
|
394
|
+
} else {
|
|
395
|
+
try {
|
|
396
|
+
result = JSON.parse(JSON.stringify(value))
|
|
397
|
+
} catch (err) {}
|
|
398
|
+
}
|
|
399
|
+
return result
|
|
400
|
+
}
|
|
527
401
|
|
|
528
|
-
|
|
402
|
+
sanitizeBoolean = (value) => {
|
|
403
|
+
return value === null ? null : (['true', true].includes(value))
|
|
529
404
|
}
|
|
530
405
|
|
|
531
|
-
|
|
532
|
-
|
|
406
|
+
sanitizeTimestamp = (value) => {
|
|
407
|
+
const { isNumber } = this.app.lib._
|
|
408
|
+
const { dayjs } = this.app.lib
|
|
409
|
+
if (!isNumber(value)) return -1
|
|
410
|
+
const dt = dayjs.unix(value)
|
|
411
|
+
return dt.isValid() ? dt.unix() : -1
|
|
533
412
|
}
|
|
534
413
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const all = this.memDb.storage[name] ?? []
|
|
538
|
-
if (fields.length === 0) return all
|
|
539
|
-
return map(all, item => pick(item, fields))
|
|
414
|
+
sanitizeString = (value) => {
|
|
415
|
+
return value + ''
|
|
540
416
|
}
|
|
541
417
|
|
|
542
|
-
|
|
543
|
-
const {
|
|
544
|
-
const {
|
|
545
|
-
|
|
546
|
-
const
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
418
|
+
_calcStats = (items, field, aggregates) => {
|
|
419
|
+
const { generateId, isSet } = this.app.lib.aneka
|
|
420
|
+
const result = { id: generateId, count: 0, avg: null, min: null, max: null }
|
|
421
|
+
let sum = 0
|
|
422
|
+
for (const item of items) {
|
|
423
|
+
const value = Number(item[field]) ?? 0
|
|
424
|
+
if (aggregates.includes('count')) result.count++
|
|
425
|
+
if (aggregates.includes('avg')) sum = sum + value
|
|
426
|
+
if (aggregates.includes('min') && (!isSet(result.min) || value < result.min)) result.min = value
|
|
427
|
+
if (aggregates.includes('max') && (!isSet(result.max) || value > result.max)) result.max = value
|
|
428
|
+
}
|
|
429
|
+
result.avg = sum / items.length
|
|
430
|
+
return result
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
calcAggregate = ({ data = [], group = '', field = '', aggregates = ['count'] } = {}) => {
|
|
434
|
+
this.checkAggregateParams({ group, field, aggregates })
|
|
435
|
+
const { pick, groupBy } = this.app.lib._
|
|
436
|
+
const grouped = groupBy(data, group)
|
|
437
|
+
const all = []
|
|
438
|
+
for (const key in grouped) {
|
|
439
|
+
const items = grouped[key]
|
|
440
|
+
const result = this._calcStats(items, field, aggregates)
|
|
441
|
+
result[group] = key
|
|
442
|
+
all.push(pick(result, ['id', group, ...aggregates]))
|
|
443
|
+
}
|
|
444
|
+
return all
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
calcHistogram = ({ data = [], type = '', group = '', field = '', aggregates = ['count'] } = {}) => {
|
|
448
|
+
this.checkHistogramParams({ group, field, type, aggregates })
|
|
449
|
+
const { dayjs } = this.app.lib
|
|
450
|
+
const pattern = { daily: ['YYYY-MM-DD', 'date'], monthly: ['YYYY-MM', 'month'], yearly: ['YYYY', 'year'] }
|
|
451
|
+
for (const d of data) {
|
|
452
|
+
d._group = dayjs(d[group]).format(pattern[type][0])
|
|
453
|
+
}
|
|
454
|
+
const grouped = groupBy(data, '_group')
|
|
455
|
+
const all = []
|
|
456
|
+
for (const key in grouped) {
|
|
457
|
+
const items = grouped[key]
|
|
458
|
+
const result = this._calcStats(items, field, aggregates)
|
|
459
|
+
const title = pattern[type][1]
|
|
460
|
+
result[title] = key
|
|
461
|
+
all.push(pick(result, ['id', title, ...aggregates]))
|
|
462
|
+
}
|
|
463
|
+
return all
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
checkAggregateParams = (params = {}) => {
|
|
467
|
+
let { group, field, aggregates } = params
|
|
468
|
+
if (isString(aggregates)) aggregates = [aggregates]
|
|
469
|
+
params.aggregates = aggregates
|
|
470
|
+
if (isEmpty(group)) throw this.error('fieldGroupMissing')
|
|
471
|
+
if (isEmpty(field)) throw this.error('fieldCalcMissing')
|
|
472
|
+
for (const agg of aggregates) {
|
|
473
|
+
if (!this.constructor.aggregateTypes.includes(agg)) throw this.error('unsupportedAggregateType%s', agg)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
checkHistogramParams = (params = {}) => {
|
|
478
|
+
this.checkAggregateParams(params)
|
|
479
|
+
const { type } = params
|
|
480
|
+
if (isEmpty(type)) throw this.error('histogramTypeMissing')
|
|
481
|
+
if (!this.constructor.histogramTypes.includes(type)) throw this.error('unsupportedHistogramType%s', type)
|
|
569
482
|
}
|
|
570
483
|
}
|
|
571
484
|
|