dobo 2.0.1 → 2.2.0
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 +291 -366
- 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 +319 -0
- package/lib/factory/action.js +161 -0
- package/lib/factory/connection.js +62 -0
- package/lib/factory/driver.js +358 -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 +62 -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 +46 -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,67 @@ 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'
|
|
67
|
+
validator: 'boolean',
|
|
68
|
+
rules: []
|
|
94
69
|
},
|
|
95
70
|
date: {
|
|
96
|
-
validator: 'date'
|
|
71
|
+
validator: 'date',
|
|
72
|
+
rules: []
|
|
97
73
|
},
|
|
98
74
|
datetime: {
|
|
99
|
-
validator: 'date'
|
|
75
|
+
validator: 'date',
|
|
76
|
+
rules: []
|
|
100
77
|
},
|
|
101
78
|
time: {
|
|
102
|
-
validator: 'date'
|
|
79
|
+
validator: 'date',
|
|
80
|
+
rules: []
|
|
103
81
|
},
|
|
104
82
|
timestamp: {
|
|
105
|
-
validator: 'timestamp'
|
|
83
|
+
validator: 'timestamp',
|
|
84
|
+
rules: []
|
|
85
|
+
},
|
|
86
|
+
object: {
|
|
87
|
+
validator: null,
|
|
88
|
+
rules: []
|
|
106
89
|
},
|
|
107
|
-
|
|
108
|
-
|
|
90
|
+
array: {
|
|
91
|
+
validator: null,
|
|
92
|
+
rules: []
|
|
93
|
+
}
|
|
109
94
|
}
|
|
110
95
|
|
|
96
|
+
const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default']
|
|
97
|
+
|
|
111
98
|
/**
|
|
112
99
|
* Plugin factory
|
|
113
100
|
*
|
|
@@ -116,6 +103,9 @@ const propType = {
|
|
|
116
103
|
*/
|
|
117
104
|
async function factory (pkgName) {
|
|
118
105
|
const me = this
|
|
106
|
+
const { breakNsPath } = this.app.bajo
|
|
107
|
+
|
|
108
|
+
const { find, filter, isString, map, pick, groupBy, isEmpty } = this.app.lib._
|
|
119
109
|
|
|
120
110
|
/**
|
|
121
111
|
* Dobo Database Framework for {@link https://github.com/ardhi/bajo|Bajo}.
|
|
@@ -124,25 +114,40 @@ async function factory (pkgName) {
|
|
|
124
114
|
*
|
|
125
115
|
* @class
|
|
126
116
|
*/
|
|
127
|
-
class Dobo extends this.app.
|
|
117
|
+
class Dobo extends this.app.baseClass.Base {
|
|
128
118
|
/**
|
|
129
|
-
* @constant {string}
|
|
119
|
+
* @constant {string[]}
|
|
130
120
|
* @memberof Dobo
|
|
131
|
-
* @default '
|
|
121
|
+
* @default ['count', 'avg', 'min', 'max', 'sum']
|
|
132
122
|
*/
|
|
133
|
-
static
|
|
123
|
+
static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
|
|
134
124
|
|
|
135
125
|
/**
|
|
136
126
|
* @constant {string[]}
|
|
137
127
|
* @memberof Dobo
|
|
138
|
-
* @default ['
|
|
128
|
+
* @default ['string', 'integer', 'smallint']
|
|
139
129
|
*/
|
|
140
|
-
static
|
|
130
|
+
static idTypes = ['string', 'integer', 'smallint']
|
|
131
|
+
|
|
141
132
|
/**
|
|
142
|
-
* @constant {
|
|
133
|
+
* @constant {string[]}
|
|
134
|
+
* @memberof Dobo
|
|
135
|
+
* @default ['daily', 'monthly', 'annually']
|
|
136
|
+
*/
|
|
137
|
+
static histogramTypes = ['daily', 'monthly', 'yearly']
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @constant {TPropertyType}
|
|
141
|
+
* @memberof Dobo
|
|
142
|
+
*/
|
|
143
|
+
static propertyType = propertyType
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @constant {string[]}
|
|
143
147
|
* @memberof Dobo
|
|
148
|
+
* @default ['index', 'unique', 'primary', 'fulltext']
|
|
144
149
|
*/
|
|
145
|
-
static
|
|
150
|
+
static indexTypes = ['index', 'unique', 'primary', 'fulltext']
|
|
146
151
|
|
|
147
152
|
constructor () {
|
|
148
153
|
super(pkgName, me.app)
|
|
@@ -154,32 +159,15 @@ async function factory (pkgName) {
|
|
|
154
159
|
allowUnknown: true
|
|
155
160
|
},
|
|
156
161
|
default: {
|
|
157
|
-
property: {
|
|
158
|
-
text: {
|
|
159
|
-
textType: 'text'
|
|
160
|
-
},
|
|
161
|
-
string: {
|
|
162
|
-
length: 50
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
162
|
filter: {
|
|
166
163
|
limit: 25,
|
|
167
164
|
maxLimit: 200,
|
|
168
165
|
hardLimit: 10000,
|
|
169
166
|
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
167
|
}
|
|
177
168
|
},
|
|
178
169
|
memDb: {
|
|
179
|
-
|
|
180
|
-
persistence: {
|
|
181
|
-
syncPeriodDur: '1s'
|
|
182
|
-
}
|
|
170
|
+
autoSaveDur: '1s'
|
|
183
171
|
},
|
|
184
172
|
applet: {
|
|
185
173
|
confirmation: false
|
|
@@ -204,7 +192,38 @@ async function factory (pkgName) {
|
|
|
204
192
|
/**
|
|
205
193
|
* @type {Object[]}
|
|
206
194
|
*/
|
|
207
|
-
this.
|
|
195
|
+
this.models = []
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get allowed property keys by field type
|
|
200
|
+
*
|
|
201
|
+
* @param {string} type
|
|
202
|
+
* @returns {string[]}
|
|
203
|
+
*/
|
|
204
|
+
getPropertyKeysByType = (type) => {
|
|
205
|
+
const keys = [...commonPropertyTypes]
|
|
206
|
+
if (['string'].includes(type)) keys.push('minLength', 'maxLength', 'values')
|
|
207
|
+
if (['text'].includes(type)) keys.push('textType')
|
|
208
|
+
if (['smallint', 'integer'].includes(type)) keys.push('autoInc', 'values')
|
|
209
|
+
if (['float', 'double'].includes(type)) keys.push('values')
|
|
210
|
+
return keys
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get all allowed property keys
|
|
215
|
+
*
|
|
216
|
+
* @returns {string[]}
|
|
217
|
+
*/
|
|
218
|
+
|
|
219
|
+
getAllPropertyKeys = (driver) => {
|
|
220
|
+
const { uniq, isEmpty } = this.app.lib._
|
|
221
|
+
const keys = [...commonPropertyTypes]
|
|
222
|
+
for (const type in propertyType) {
|
|
223
|
+
keys.push(...Object.keys(propertyType[type]))
|
|
224
|
+
}
|
|
225
|
+
if (driver && !isEmpty(driver.constructor.propertyKeys)) keys.push(...driver.constructor.propertyKeys)
|
|
226
|
+
return uniq(keys)
|
|
208
227
|
}
|
|
209
228
|
|
|
210
229
|
/**
|
|
@@ -212,31 +231,19 @@ async function factory (pkgName) {
|
|
|
212
231
|
* - {@link module:Lib.collectDrivers|Collecting all drivers}
|
|
213
232
|
* - {@link module:Lib.collectConnections|Collecting all connections}
|
|
214
233
|
* - {@link module:Lib.collectFeatures|Collecting all features}
|
|
215
|
-
* - {@link module:Lib.
|
|
234
|
+
* - {@link module:Lib.collectModels|Collecting all models}
|
|
216
235
|
* @method
|
|
217
236
|
* @async
|
|
218
237
|
*/
|
|
219
238
|
init = async () => {
|
|
220
|
-
const {
|
|
239
|
+
const { getPluginDataDir } = this.app.bajo
|
|
221
240
|
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
241
|
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'))
|
|
242
|
+
await collectConnections.call(this)
|
|
238
243
|
await collectFeatures.call(this)
|
|
239
|
-
await
|
|
244
|
+
await collectModels.call(this)
|
|
245
|
+
const attDir = `${getPluginDataDir('dobo')}/attachment`
|
|
246
|
+
fs.ensureDirSync(attDir)
|
|
240
247
|
}
|
|
241
248
|
|
|
242
249
|
/**
|
|
@@ -245,239 +252,98 @@ async function factory (pkgName) {
|
|
|
245
252
|
* @method
|
|
246
253
|
* @async
|
|
247
254
|
* @param {(string|Array)} [conns=all] - Which connections should be run on start
|
|
248
|
-
* @param {boolean} [noRebuild=
|
|
255
|
+
* @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
256
|
*/
|
|
250
257
|
start = async (conns = 'all', noRebuild = true) => {
|
|
251
|
-
const { importModule, breakNsPath } = this.app.bajo
|
|
252
|
-
const { find, filter, isString, map } = this.app.lib._
|
|
253
258
|
if (conns === 'all') conns = this.connections
|
|
254
259
|
else if (isString(conns)) conns = filter(this.connections, { name: conns })
|
|
255
260
|
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)
|
|
261
|
+
this.log.debug('dbInit')
|
|
262
|
+
for (const connection of conns) {
|
|
263
|
+
await connection.connect(noRebuild)
|
|
264
|
+
this.log.trace('dbInit%s%s%s', connection.driver.plugin.ns, connection.driver.name, connection.name)
|
|
262
265
|
}
|
|
263
|
-
await memDbStart.call(this)
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
/**
|
|
267
|
-
*
|
|
269
|
+
* Get connection by name. It returns the first connection named after "{name}"
|
|
268
270
|
*
|
|
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}
|
|
271
|
+
* @param {string} name - Connection name
|
|
272
|
+
* @param {boolean} [silent] - If ```true``` and connection is not found, it won't throw error
|
|
273
|
+
* @returns {Driver} Return connection instance or ```undefined``` if silent is ```true```
|
|
278
274
|
*/
|
|
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)
|
|
275
|
+
getConnection = (name, silent) => {
|
|
276
|
+
const conn = find(this.connections, { name })
|
|
277
|
+
if (!conn && !silent) throw this.error('unknown%s%s', this.t('connection'), name)
|
|
278
|
+
return conn
|
|
313
279
|
}
|
|
314
280
|
|
|
315
281
|
/**
|
|
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
|
|
282
|
+
* Get driver by name. It returns the first driver named after "{name}"
|
|
321
283
|
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* @param {
|
|
325
|
-
* @param {
|
|
326
|
-
* @
|
|
327
|
-
* @returns {TRecordPagination}
|
|
284
|
+
* Also support NsPath format for those who load the same named driver but from different provider/plugin.
|
|
285
|
+
*
|
|
286
|
+
* @param {string} name - Driver name
|
|
287
|
+
* @param {boolean} [silent] - If ```true``` and driver is not found, it won't throw error
|
|
288
|
+
* @returns {Driver} Returns driver instance or ```undefined``` if silent is ```true```
|
|
328
289
|
*/
|
|
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
|
|
290
|
+
getDriver = (name, silent) => {
|
|
291
|
+
const { breakNsPath } = this.app.bajo
|
|
292
|
+
let driver
|
|
293
|
+
if (!name.includes(':')) {
|
|
294
|
+
driver = find(this.drivers, { name })
|
|
295
|
+
if (driver) return driver
|
|
296
|
+
driver = filter(this.drivers, d => d.plugin.ns === name)
|
|
297
|
+
if (driver.length === 1) return driver[0]
|
|
298
|
+
if (!silent) throw this.error('unknown%s%s', this.t('driver'), name)
|
|
299
|
+
return
|
|
380
300
|
}
|
|
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 }
|
|
301
|
+
const { ns, path } = breakNsPath(name)
|
|
302
|
+
driver = find(this.drivers, d => d.name === path && d.plugin.ns === ns)
|
|
303
|
+
if (!driver && !silent) throw this.error('unknown%s%s', this.t('driver'), name)
|
|
304
|
+
return driver
|
|
389
305
|
}
|
|
390
306
|
|
|
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
|
|
307
|
+
/**
|
|
308
|
+
* Get feature by name. It returns the first driver named after "{name}"
|
|
309
|
+
*
|
|
310
|
+
* Also support NsPath format for those who load the same named driver but from different provider/plugin.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} - Feature name
|
|
313
|
+
* @returns {Feature} Return feature instance
|
|
314
|
+
*/
|
|
315
|
+
getFeature = name => {
|
|
316
|
+
if (!name.includes(':')) return find(this.features, { name })
|
|
317
|
+
const { ns, path } = breakNsPath(name)
|
|
318
|
+
const feat = find(this.features, d => d.name === path && d.plugin.ns === ns)
|
|
319
|
+
if (!feat) throw this.error('unknown%s%s', this.t('feature'), name)
|
|
320
|
+
return feat
|
|
422
321
|
}
|
|
423
322
|
|
|
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)
|
|
323
|
+
/**
|
|
324
|
+
* Get model by name
|
|
325
|
+
*
|
|
326
|
+
* @param {string} name - Model name
|
|
327
|
+
* @param {boolean} [silent] - If ```true``` and model is not found, it won't throw error
|
|
328
|
+
* @retuns {Model} Returns model instance or ```undefined``` if silent is ```true```
|
|
329
|
+
*/
|
|
330
|
+
getModel = (name, silent) => {
|
|
331
|
+
const { pascalCase } = this.app.lib.aneka
|
|
332
|
+
let model = find(this.models, { name })
|
|
333
|
+
if (!model) model = find(this.models, { name: pascalCase(name) })
|
|
334
|
+
if (!model && !silent) throw this.error('unknown%s%s', this.t('model'), name)
|
|
335
|
+
return model
|
|
451
336
|
}
|
|
452
337
|
|
|
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
|
|
338
|
+
/**
|
|
339
|
+
* Get all models that bound to connection ```name```
|
|
340
|
+
*
|
|
341
|
+
* @param {string} name - Connection name
|
|
342
|
+
* @returns {Array}
|
|
343
|
+
*/
|
|
344
|
+
getModelsByConnection = name => {
|
|
345
|
+
const conn = this.getConnection(name)
|
|
346
|
+
return this.models.filter(s => s.connection.name === conn.name)
|
|
481
347
|
}
|
|
482
348
|
|
|
483
349
|
validationErrorMessage = (err) => {
|
|
@@ -491,81 +357,140 @@ async function factory (pkgName) {
|
|
|
491
357
|
return text
|
|
492
358
|
}
|
|
493
359
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
360
|
+
/**
|
|
361
|
+
* Sanitize value as a date/time value. Parse/format string using {@link https://day.js.org/docs/en/display/format|dayjs format}
|
|
362
|
+
*
|
|
363
|
+
* @method
|
|
364
|
+
* @memberof Dobo
|
|
365
|
+
* @param {(number|string)} value - Value to sanitize
|
|
366
|
+
* @param {Object} [options={}] - Options object
|
|
367
|
+
* @param {boolean} [options.silent=true] - If ```true``` (default) and value isn't valid, returns empty
|
|
368
|
+
* @param {string} [options.inputFormat] - If provided, parse value using this option
|
|
369
|
+
* @param {string} [options.outputFormat] - If not provided or ```native```, returns Javascript Date. Otherwise returns formatted date/time string
|
|
370
|
+
* @returns {(string|Date)}
|
|
371
|
+
*/
|
|
372
|
+
sanitizeDate = (value, { inputFormat, outputFormat, silent = true } = {}) => {
|
|
373
|
+
const { dayjs } = this.app.lib
|
|
374
|
+
if (value === 0) return null
|
|
375
|
+
if (!outputFormat) outputFormat = inputFormat
|
|
376
|
+
const dt = dayjs(value, inputFormat)
|
|
377
|
+
if (!dt.isValid()) {
|
|
378
|
+
if (silent) return null
|
|
379
|
+
throw this.error('invalidDate')
|
|
380
|
+
}
|
|
381
|
+
if (outputFormat === 'native' || !outputFormat) return dt.toDate()
|
|
382
|
+
return dt.format(outputFormat)
|
|
497
383
|
}
|
|
498
384
|
|
|
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 }
|
|
385
|
+
sanitizeFloat = (value, { strict = false } = {}) => {
|
|
386
|
+
const { isNumber } = this.app.lib._
|
|
387
|
+
if (isNumber(value)) return value
|
|
388
|
+
if (strict) return Number(value)
|
|
389
|
+
return parseFloat(value) || null
|
|
511
390
|
}
|
|
512
391
|
|
|
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
|
|
392
|
+
sanitizeInt = (value, { strict = false } = {}) => {
|
|
393
|
+
const { isNumber } = this.app.lib._
|
|
394
|
+
if (isNumber(value)) return value
|
|
395
|
+
if (strict) return Number(value)
|
|
396
|
+
return parseInt(value) || null
|
|
521
397
|
}
|
|
522
398
|
|
|
523
|
-
|
|
524
|
-
const {
|
|
525
|
-
|
|
526
|
-
|
|
399
|
+
sanitizeObject = (value) => {
|
|
400
|
+
const { isString } = this.app.lib._
|
|
401
|
+
let result = null
|
|
402
|
+
if (isString(value)) {
|
|
403
|
+
try {
|
|
404
|
+
result = JSON.parse(value)
|
|
405
|
+
} catch (err) {}
|
|
406
|
+
} else {
|
|
407
|
+
try {
|
|
408
|
+
result = JSON.parse(JSON.stringify(value))
|
|
409
|
+
} catch (err) {}
|
|
410
|
+
}
|
|
411
|
+
return result
|
|
412
|
+
}
|
|
527
413
|
|
|
528
|
-
|
|
414
|
+
sanitizeBoolean = (value) => {
|
|
415
|
+
return value === null ? null : (['true', true].includes(value))
|
|
529
416
|
}
|
|
530
417
|
|
|
531
|
-
|
|
532
|
-
|
|
418
|
+
sanitizeTimestamp = (value) => {
|
|
419
|
+
const { isNumber } = this.app.lib._
|
|
420
|
+
const { dayjs } = this.app.lib
|
|
421
|
+
if (!isNumber(value)) return -1
|
|
422
|
+
const dt = dayjs.unix(value)
|
|
423
|
+
return dt.isValid() ? dt.unix() : -1
|
|
533
424
|
}
|
|
534
425
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
const all = this.memDb.storage[name] ?? []
|
|
538
|
-
if (fields.length === 0) return all
|
|
539
|
-
return map(all, item => pick(item, fields))
|
|
426
|
+
sanitizeString = (value) => {
|
|
427
|
+
return value + ''
|
|
540
428
|
}
|
|
541
429
|
|
|
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
|
-
|
|
430
|
+
_calcStats = (items, field, aggregates) => {
|
|
431
|
+
const { generateId, isSet } = this.app.lib.aneka
|
|
432
|
+
const result = { id: generateId, count: 0, avg: null, min: null, max: null }
|
|
433
|
+
let sum = 0
|
|
434
|
+
for (const item of items) {
|
|
435
|
+
const value = Number(item[field]) ?? 0
|
|
436
|
+
if (aggregates.includes('count')) result.count++
|
|
437
|
+
if (aggregates.includes('avg')) sum = sum + value
|
|
438
|
+
if (aggregates.includes('min') && (!isSet(result.min) || value < result.min)) result.min = value
|
|
439
|
+
if (aggregates.includes('max') && (!isSet(result.max) || value > result.max)) result.max = value
|
|
440
|
+
}
|
|
441
|
+
result.avg = sum / items.length
|
|
442
|
+
return result
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
calcAggregate = ({ data = [], group = '', field = '', aggregates = ['count'] } = {}) => {
|
|
446
|
+
this.checkAggregateParams({ group, field, aggregates })
|
|
447
|
+
const { pick, groupBy } = this.app.lib._
|
|
448
|
+
const grouped = groupBy(data, group)
|
|
449
|
+
const all = []
|
|
450
|
+
for (const key in grouped) {
|
|
451
|
+
const items = grouped[key]
|
|
452
|
+
const result = this._calcStats(items, field, aggregates)
|
|
453
|
+
result[group] = key
|
|
454
|
+
all.push(pick(result, ['id', group, ...aggregates]))
|
|
455
|
+
}
|
|
456
|
+
return all
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
calcHistogram = ({ data = [], type = '', group = '', field = '', aggregates = ['count'] } = {}) => {
|
|
460
|
+
this.checkHistogramParams({ group, field, type, aggregates })
|
|
461
|
+
const { dayjs } = this.app.lib
|
|
462
|
+
const pattern = { daily: ['YYYY-MM-DD', 'date'], monthly: ['YYYY-MM', 'month'], yearly: ['YYYY', 'year'] }
|
|
463
|
+
for (const d of data) {
|
|
464
|
+
d._group = dayjs(d[group]).format(pattern[type][0])
|
|
465
|
+
}
|
|
466
|
+
const grouped = groupBy(data, '_group')
|
|
467
|
+
const all = []
|
|
468
|
+
for (const key in grouped) {
|
|
469
|
+
const items = grouped[key]
|
|
470
|
+
const result = this._calcStats(items, field, aggregates)
|
|
471
|
+
const title = pattern[type][1]
|
|
472
|
+
result[title] = key
|
|
473
|
+
all.push(pick(result, ['id', title, ...aggregates]))
|
|
474
|
+
}
|
|
475
|
+
return all
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
checkAggregateParams = (params = {}) => {
|
|
479
|
+
let { group, field, aggregates } = params
|
|
480
|
+
if (isString(aggregates)) aggregates = [aggregates]
|
|
481
|
+
params.aggregates = aggregates
|
|
482
|
+
if (isEmpty(group)) throw this.error('fieldGroupMissing')
|
|
483
|
+
if (isEmpty(field)) throw this.error('fieldCalcMissing')
|
|
484
|
+
for (const agg of aggregates) {
|
|
485
|
+
if (!this.constructor.aggregateTypes.includes(agg)) throw this.error('unsupportedAggregateType%s', agg)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
checkHistogramParams = (params = {}) => {
|
|
490
|
+
this.checkAggregateParams(params)
|
|
491
|
+
const { type } = params
|
|
492
|
+
if (isEmpty(type)) throw this.error('histogramTypeMissing')
|
|
493
|
+
if (!this.constructor.histogramTypes.includes(type)) throw this.error('unsupportedHistogramType%s', type)
|
|
569
494
|
}
|
|
570
495
|
}
|
|
571
496
|
|