dobo 2.0.0 → 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.
Files changed (197) hide show
  1. package/.github/FUNDING.yml +13 -0
  2. package/.github/workflows/repo-lockdown.yml +24 -0
  3. package/.jsdoc.conf.json +45 -0
  4. package/LICENSE +1 -1
  5. package/README.md +38 -19
  6. package/docs/Dobo.html +26 -0
  7. package/docs/data/search.json +1 -0
  8. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  9. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  10. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  11. package/docs/global.html +7 -0
  12. package/docs/index.html +3 -0
  13. package/docs/index.js.html +578 -0
  14. package/docs/lib_collect-connections.js.html +39 -0
  15. package/docs/lib_collect-drivers.js.html +52 -0
  16. package/docs/lib_collect-features.js.html +36 -0
  17. package/docs/lib_collect-schemas.js.html +94 -0
  18. package/docs/lib_index.js.html +6 -0
  19. package/docs/method_model_create.js.html +35 -0
  20. package/docs/method_model_drop.js.html +34 -0
  21. package/docs/method_model_exists.js.html +40 -0
  22. package/docs/method_record_count.js.html +69 -0
  23. package/docs/method_record_create.js.html +114 -0
  24. package/docs/method_record_find-all.js.html +44 -0
  25. package/docs/method_record_find-one.js.html +73 -0
  26. package/docs/method_record_find.js.html +118 -0
  27. package/docs/method_record_get.js.html +92 -0
  28. package/docs/method_record_remove.js.html +75 -0
  29. package/docs/method_record_update.js.html +107 -0
  30. package/docs/method_record_upsert.js.html +54 -0
  31. package/docs/method_sanitize_body.js.html +88 -0
  32. package/docs/method_sanitize_date.js.html +30 -0
  33. package/docs/method_sanitize_id.js.html +20 -0
  34. package/docs/method_validate.js.html +249 -0
  35. package/docs/module-Lib.html +3 -0
  36. package/docs/scripts/core.js +725 -0
  37. package/docs/scripts/core.min.js +23 -0
  38. package/docs/scripts/resize.js +90 -0
  39. package/docs/scripts/search.js +265 -0
  40. package/docs/scripts/search.min.js +6 -0
  41. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -0
  42. package/docs/scripts/third-party/fuse.js +9 -0
  43. package/docs/scripts/third-party/hljs-line-num-original.js +366 -0
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  45. package/docs/scripts/third-party/hljs-original.js +5164 -0
  46. package/docs/scripts/third-party/hljs.js +1 -0
  47. package/docs/scripts/third-party/popper.js +5 -0
  48. package/docs/scripts/third-party/tippy.js +1 -0
  49. package/docs/scripts/third-party/tocbot.js +671 -0
  50. package/docs/scripts/third-party/tocbot.min.js +1 -0
  51. package/docs/static/bitcoin.jpeg +0 -0
  52. package/docs/static/home.md +25 -0
  53. package/docs/static/logo-ecosystem.png +0 -0
  54. package/docs/static/logo.png +0 -0
  55. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  56. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  57. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  60. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  61. package/extend/bajo/intl/en-US.json +69 -30
  62. package/extend/bajo/intl/id.json +58 -29
  63. package/extend/bajoCli/applet/clear-record.js +22 -0
  64. package/extend/bajoCli/applet/connection.js +5 -5
  65. package/extend/bajoCli/applet/count-record.js +27 -0
  66. package/extend/bajoCli/applet/create-aggregate.js +33 -0
  67. package/extend/bajoCli/applet/create-histogram.js +33 -0
  68. package/extend/bajoCli/applet/create-record.js +39 -0
  69. package/extend/bajoCli/applet/find-record.js +27 -0
  70. package/extend/bajoCli/applet/get-record.js +27 -0
  71. package/extend/bajoCli/applet/lib/post-process.js +25 -26
  72. package/extend/bajoCli/applet/model.js +22 -0
  73. package/extend/bajoCli/applet/rebuild-model.js +91 -0
  74. package/extend/bajoCli/applet/remove-record.js +27 -0
  75. package/extend/bajoCli/applet/update-record.js +44 -0
  76. package/extend/bajoCli/applet.js +0 -0
  77. package/extend/dobo/driver/memory.js +170 -0
  78. package/extend/dobo/feature/created-at.js +10 -8
  79. package/extend/dobo/feature/dt.js +0 -0
  80. package/extend/dobo/feature/immutable.js +30 -0
  81. package/extend/dobo/feature/int-id.js +0 -0
  82. package/extend/dobo/feature/removed-at.js +35 -57
  83. package/extend/dobo/feature/updated-at.js +14 -12
  84. package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +5 -9
  85. package/extend/waibuStatic/virtual.json +0 -0
  86. package/index.js +420 -337
  87. package/lib/collect-connections.js +60 -21
  88. package/lib/collect-drivers.js +29 -35
  89. package/lib/collect-features.js +40 -0
  90. package/lib/collect-models.js +319 -0
  91. package/lib/factory/action.js +161 -0
  92. package/lib/factory/connection.js +62 -0
  93. package/lib/factory/driver.js +358 -0
  94. package/lib/factory/feature.js +33 -0
  95. package/lib/factory/model/_util.js +402 -0
  96. package/lib/factory/model/build.js +15 -0
  97. package/lib/factory/model/clear-record.js +17 -0
  98. package/lib/factory/model/count-record.js +17 -0
  99. package/lib/factory/model/create-aggregate.js +17 -0
  100. package/lib/factory/model/create-attachment.js +29 -0
  101. package/lib/factory/model/create-histogram.js +17 -0
  102. package/lib/factory/model/create-record.js +35 -0
  103. package/lib/factory/model/drop.js +15 -0
  104. package/lib/factory/model/exists.js +21 -0
  105. package/lib/factory/model/find-all-record.js +71 -0
  106. package/lib/factory/model/find-attachment.js +29 -0
  107. package/lib/factory/model/find-one-record.js +19 -0
  108. package/lib/factory/model/find-record.js +103 -0
  109. package/lib/factory/model/get-attachment.js +15 -0
  110. package/lib/factory/model/get-record.js +79 -0
  111. package/lib/factory/model/list-attachment.js +37 -0
  112. package/lib/{add-fixtures.js → factory/model/load-fixtures.js} +69 -67
  113. package/lib/factory/model/remove-attachment.js +15 -0
  114. package/lib/factory/model/remove-record.js +59 -0
  115. package/lib/factory/model/sanitize-body.js +62 -0
  116. package/lib/factory/model/sanitize-id.js +7 -0
  117. package/lib/factory/model/sanitize-record.js +26 -0
  118. package/lib/factory/model/update-attachment.js +9 -0
  119. package/lib/factory/model/update-record.js +81 -0
  120. package/lib/factory/model/upsert-record.js +95 -0
  121. package/lib/factory/model/validate.js +232 -0
  122. package/lib/factory/model.js +150 -0
  123. package/lib/index.js +3 -0
  124. package/package.json +45 -36
  125. package/wiki/APPLETS.md +57 -0
  126. package/wiki/CHANGES.md +46 -0
  127. package/wiki/CONFIG.md +25 -0
  128. package/wiki/CONTRIBUTING.md +5 -0
  129. package/wiki/DEV-GUIDE.md +1 -0
  130. package/wiki/ECOSYSTEM.md +20 -0
  131. package/wiki/GETTING-STARTED.md +166 -0
  132. package/{docs/query-language.md → wiki/QUERY-LANGUAGE.md} +0 -0
  133. package/wiki/USER-GUIDE.md +1 -0
  134. package/extend/bajoCli/applet/model-clear.js +0 -11
  135. package/extend/bajoCli/applet/model-rebuild.js +0 -101
  136. package/extend/bajoCli/applet/record-create.js +0 -41
  137. package/extend/bajoCli/applet/record-find.js +0 -27
  138. package/extend/bajoCli/applet/record-get.js +0 -24
  139. package/extend/bajoCli/applet/record-remove.js +0 -24
  140. package/extend/bajoCli/applet/record-update.js +0 -47
  141. package/extend/bajoCli/applet/schema.js +0 -22
  142. package/extend/bajoCli/applet/stat-count.js +0 -24
  143. package/lib/build-bulk-action.js +0 -12
  144. package/lib/check-unique.js +0 -39
  145. package/lib/collect-feature.js +0 -25
  146. package/lib/collect-schemas.js +0 -83
  147. package/lib/exec-feature-hook.js +0 -13
  148. package/lib/exec-validation.js +0 -21
  149. package/lib/generic-prop-sanitizer.js +0 -31
  150. package/lib/handle-attachment-upload.js +0 -16
  151. package/lib/mem-db/conn-sanitizer.js +0 -8
  152. package/lib/mem-db/instantiate.js +0 -41
  153. package/lib/mem-db/method/model/clear.js +0 -6
  154. package/lib/mem-db/method/model/create.js +0 -5
  155. package/lib/mem-db/method/model/drop.js +0 -5
  156. package/lib/mem-db/method/model/exists.js +0 -5
  157. package/lib/mem-db/method/record/create.js +0 -12
  158. package/lib/mem-db/method/record/find.js +0 -20
  159. package/lib/mem-db/method/record/get.js +0 -9
  160. package/lib/mem-db/method/record/remove.js +0 -13
  161. package/lib/mem-db/method/record/update.js +0 -15
  162. package/lib/mem-db/method/stat/count.js +0 -11
  163. package/lib/mem-db/start.js +0 -25
  164. package/lib/merge-attachment-info.js +0 -16
  165. package/lib/multi-rel-rows.js +0 -42
  166. package/lib/resolve-method.js +0 -16
  167. package/lib/sanitize-schema.js +0 -197
  168. package/lib/single-rel-rows.js +0 -38
  169. package/method/attachment/copy-uploaded.js +0 -34
  170. package/method/attachment/create.js +0 -29
  171. package/method/attachment/find.js +0 -27
  172. package/method/attachment/get-path.js +0 -12
  173. package/method/attachment/get.js +0 -12
  174. package/method/attachment/pre-check.js +0 -9
  175. package/method/attachment/remove.js +0 -11
  176. package/method/attachment/update.js +0 -7
  177. package/method/bulk/create.js +0 -46
  178. package/method/model/clear.js +0 -22
  179. package/method/model/create.js +0 -19
  180. package/method/model/drop.js +0 -19
  181. package/method/model/exists.js +0 -24
  182. package/method/record/clear.js +0 -24
  183. package/method/record/count.js +0 -44
  184. package/method/record/create.js +0 -71
  185. package/method/record/find-all.js +0 -25
  186. package/method/record/find-one.js +0 -56
  187. package/method/record/find.js +0 -52
  188. package/method/record/get.js +0 -47
  189. package/method/record/remove.js +0 -41
  190. package/method/record/update.js +0 -63
  191. package/method/record/upsert.js +0 -35
  192. package/method/sanitize/body.js +0 -70
  193. package/method/sanitize/date.js +0 -14
  194. package/method/sanitize/id.js +0 -7
  195. package/method/stat/aggregate.js +0 -23
  196. package/method/stat/histogram.js +0 -26
  197. package/method/validate.js +0 -157
package/index.js CHANGED
@@ -1,327 +1,349 @@
1
1
  import collectConnections from './lib/collect-connections.js'
2
2
  import collectDrivers from './lib/collect-drivers.js'
3
- import collectFeature from './lib/collect-feature.js'
4
- import collectSchemas from './lib/collect-schemas.js'
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'
3
+ import collectFeatures from './lib/collect-features.js'
4
+ import collectModels from './lib/collect-models.js'
9
5
 
6
+ /**
7
+ * @typedef {Object} TPropertyType
8
+ * @property {Object} integer
9
+ * @property {string} [integer.validator=number]
10
+ * @property {Object} smallint
11
+ * @property {string} [smallint.validator=number]
12
+ * @property {Object} text
13
+ * @property {string} [text.validator=string]
14
+ * @property {string} [text.textType=string]
15
+ * @property {string[]} [text.values=['text', 'mediumtext', 'longtext']]
16
+ * @property {Object} string
17
+ * @property {string} [string.validator=string]
18
+ * @property {maxLength} [string.maxLength=255]
19
+ * @property {minLength} [string.minLength=0]
20
+ * @property {Object} float
21
+ * @property {string} [float.validator=number]
22
+ * @property {Object} double
23
+ * @property {string} [double.validator=number]
24
+ * @property {Object} boolean
25
+ * @property {string} [boolean.validator=boolean]
26
+ * @property {Object} datetime
27
+ * @property {string} [datetime.validator=date]
28
+ * @property {Object} date
29
+ * @property {string} [date.validator=date]
30
+ * @property {Object} time
31
+ * @property {string} [time.validator=date]
32
+ * @property {Object} timestamp
33
+ * @property {string} [timestamp.validator=timestamp]
34
+ * @property {Object} object={}
35
+ * @property {Object} array={}
36
+ */
37
+ const propertyType = {
38
+ integer: {
39
+ validator: 'number',
40
+ rules: []
41
+ },
42
+ smallint: {
43
+ validator: 'number',
44
+ rules: []
45
+ },
46
+ text: {
47
+ validator: 'string',
48
+ textType: 'text',
49
+ values: ['text', 'mediumtext', 'longtext'],
50
+ rules: []
51
+ },
52
+ string: {
53
+ validator: 'string',
54
+ maxLength: 50,
55
+ minLength: 0,
56
+ rules: []
57
+ },
58
+ float: {
59
+ validator: 'number',
60
+ rules: []
61
+ },
62
+ double: {
63
+ validator: 'number',
64
+ rules: []
65
+ },
66
+ boolean: {
67
+ validator: 'boolean',
68
+ rules: []
69
+ },
70
+ date: {
71
+ validator: 'date',
72
+ rules: []
73
+ },
74
+ datetime: {
75
+ validator: 'date',
76
+ rules: []
77
+ },
78
+ time: {
79
+ validator: 'date',
80
+ rules: []
81
+ },
82
+ timestamp: {
83
+ validator: 'timestamp',
84
+ rules: []
85
+ },
86
+ object: {
87
+ validator: null,
88
+ rules: []
89
+ },
90
+ array: {
91
+ validator: null,
92
+ rules: []
93
+ }
94
+ }
95
+
96
+ const commonPropertyTypes = ['name', 'type', 'required', 'rules', 'validator', 'ref', 'default']
97
+
98
+ /**
99
+ * Plugin factory
100
+ *
101
+ * @param {string} pkgName - NPM package name
102
+ * @returns {class}
103
+ */
10
104
  async function factory (pkgName) {
11
105
  const me = this
106
+ const { breakNsPath } = this.app.bajo
107
+
108
+ const { find, filter, isString, map, pick, groupBy, isEmpty } = this.app.lib._
109
+
110
+ /**
111
+ * Dobo Database Framework for {@link https://github.com/ardhi/bajo|Bajo}.
112
+ *
113
+ * See {@tutorial ecosystem} for available drivers & tools
114
+ *
115
+ * @class
116
+ */
117
+ class Dobo extends this.app.baseClass.Base {
118
+ /**
119
+ * @constant {string[]}
120
+ * @memberof Dobo
121
+ * @default ['count', 'avg', 'min', 'max', 'sum']
122
+ */
123
+ static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
124
+
125
+ /**
126
+ * @constant {string[]}
127
+ * @memberof Dobo
128
+ * @default ['string', 'integer', 'smallint']
129
+ */
130
+ static idTypes = ['string', 'integer', 'smallint']
131
+
132
+ /**
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[]}
147
+ * @memberof Dobo
148
+ * @default ['index', 'unique', 'primary', 'fulltext']
149
+ */
150
+ static indexTypes = ['index', 'unique', 'primary', 'fulltext']
12
151
 
13
- return class Dobo extends this.lib.Plugin {
14
152
  constructor () {
15
153
  super(pkgName, me.app)
16
- this.alias = 'db'
17
154
  this.config = {
18
155
  connections: [],
19
- mergeProps: ['connections'],
20
156
  validationParams: {
21
157
  abortEarly: false,
22
158
  convert: false,
23
159
  allowUnknown: true
24
160
  },
25
161
  default: {
26
- property: {
27
- text: {
28
- kind: 'text'
29
- },
30
- string: {
31
- length: 50
32
- }
33
- },
34
162
  filter: {
35
163
  limit: 25,
36
164
  maxLimit: 200,
37
165
  hardLimit: 10000,
38
166
  sort: ['dt:-1', 'updatedAt:-1', 'updated_at:-1', 'createdAt:-1', 'createdAt:-1', 'ts:-1', 'username', 'name']
39
- },
40
- idField: {
41
- type: 'string',
42
- maxLength: 50,
43
- required: true,
44
- index: { type: 'primary' }
45
167
  }
46
168
  },
47
169
  memDb: {
48
- createDefConnAtStart: true,
49
- persistence: {
50
- syncPeriod: 1
51
- }
52
- }
53
- }
54
- this.aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
55
- this.propType = {
56
- integer: {
57
- validator: 'number'
58
- },
59
- smallint: {
60
- validator: 'number'
61
- },
62
- text: {
63
- validator: 'string',
64
- kind: 'text',
65
- choices: ['text', 'mediumtext', 'longtext']
66
- },
67
- string: {
68
- validator: 'string',
69
- maxLength: 255,
70
- minLength: 0
71
- },
72
- float: {
73
- validator: 'number'
74
- },
75
- double: {
76
- validator: 'number'
77
- },
78
- boolean: {
79
- validator: 'boolean'
80
- },
81
- date: {
82
- validator: 'date'
83
- },
84
- datetime: {
85
- validator: 'date'
170
+ autoSaveDur: '1s'
86
171
  },
87
- time: {
88
- validator: 'date'
89
- },
90
- timestamp: {
91
- validator: 'timestamp'
92
- },
93
- object: {},
94
- array: {}
172
+ applet: {
173
+ confirmation: false
174
+ }
95
175
  }
176
+
177
+ /**
178
+ * @type {Object[]}
179
+ */
180
+ this.drivers = []
181
+
182
+ /**
183
+ * @type {Object[]}
184
+ */
185
+ this.connections = []
186
+
187
+ /**
188
+ * @type {Object[]}
189
+ */
190
+ this.features = []
191
+
192
+ /**
193
+ * @type {Object[]}
194
+ */
195
+ this.models = []
96
196
  }
97
197
 
98
- init = async () => {
99
- const { buildCollections } = this.app.bajo
100
- const { fs } = this.lib
101
- const checkType = async (item, items) => {
102
- const { filter } = this.lib._
103
- const existing = filter(items, { type: 'dobo:memory' })
104
- if (existing.length > 1) this.fatal('onlyOneConnType%s', item.type)
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]))
105
224
  }
225
+ if (driver && !isEmpty(driver.constructor.propertyKeys)) keys.push(...driver.constructor.propertyKeys)
226
+ return uniq(keys)
227
+ }
106
228
 
107
- fs.ensureDirSync(`${this.dir.data}/attachment`)
229
+ /**
230
+ * Initialize plugin and performing the following tasks:
231
+ * - {@link module:Lib.collectDrivers|Collecting all drivers}
232
+ * - {@link module:Lib.collectConnections|Collecting all connections}
233
+ * - {@link module:Lib.collectFeatures|Collecting all features}
234
+ * - {@link module:Lib.collectModels|Collecting all models}
235
+ * @method
236
+ * @async
237
+ */
238
+ init = async () => {
239
+ const { getPluginDataDir } = this.app.bajo
240
+ const { fs } = this.app.lib
108
241
  await collectDrivers.call(this)
109
- if (this.config.memDb.createDefConnAtStart) {
110
- this.config.connections.push({
111
- type: 'dobo:memory',
112
- name: 'memory'
113
- })
114
- }
115
- this.connections = await buildCollections({ ns: this.name, container: 'connections', handler: collectConnections, dupChecks: ['name', checkType] })
116
- if (this.connections.length === 0) this.log.warn('notFound%s', this.print.write('connection'))
117
- await collectFeature.call(this)
118
- await collectSchemas.call(this)
242
+ await collectConnections.call(this)
243
+ await collectFeatures.call(this)
244
+ await collectModels.call(this)
245
+ const attDir = `${getPluginDataDir('dobo')}/attachment`
246
+ fs.ensureDirSync(attDir)
119
247
  }
120
248
 
249
+ /**
250
+ * Start plugin
251
+ *
252
+ * @method
253
+ * @async
254
+ * @param {(string|Array)} [conns=all] - Which connections should be run on start
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!!!
256
+ */
121
257
  start = async (conns = 'all', noRebuild = true) => {
122
- const { importModule, breakNsPath } = this.app.bajo
123
- const { find, filter, isString, map } = this.lib._
124
258
  if (conns === 'all') conns = this.connections
125
259
  else if (isString(conns)) conns = filter(this.connections, { name: conns })
126
260
  else conns = map(conns, c => find(this.connections, { name: c }))
127
- for (const c of conns) {
128
- const { ns } = breakNsPath(c.type)
129
- const schemas = filter(this.schemas, { connection: c.name })
130
- const mod = c.type === 'dobo:memory' ? memDbInstantiate : await importModule(`${ns}:/extend/${this.name}/boot/instantiate.js`)
131
- await mod.call(this.app[ns], { connection: c, noRebuild, schemas })
132
- 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)
133
265
  }
134
- await memDbStart.call(this)
135
266
  }
136
267
 
137
- pickRecord = async ({ record, fields, schema = {}, hidden = [], forceNoHidden } = {}) => {
138
- const { isArray, pick, clone, isEmpty, omit } = this.lib._
139
- const { dayjs } = this.lib
140
-
141
- const transform = async ({ record, schema, hidden = [], forceNoHidden } = {}) => {
142
- if (record._id) {
143
- record.id = record._id
144
- delete record._id
145
- }
146
- const defHidden = [...schema.hidden, ...hidden]
147
- let result = {}
148
- for (const p of schema.properties) {
149
- if (!forceNoHidden && defHidden.includes(p.name)) continue
150
- result[p.name] = record[p.name] ?? null
151
- if (record[p.name] === null) continue
152
- switch (p.type) {
153
- case 'boolean': result[p.name] = !!result[p.name]; break
154
- case 'time': result[p.name] = dayjs(record[p.name]).format('HH:mm:ss'); break
155
- case 'date': result[p.name] = dayjs(record[p.name]).format('YYYY-MM-DD'); break
156
- case 'datetime': result[p.name] = dayjs(record[p.name]).toISOString(); break
157
- }
158
- }
159
- result = await this.sanitizeBody({ body: result, schema, partial: true, ignoreNull: true })
160
- if (record._rel) result._rel = record._rel
161
- return result
162
- }
163
-
164
- if (isEmpty(record)) return record
165
- if (hidden.length > 0) record = omit(record, hidden)
166
- if (!isArray(fields)) return await transform.call(this, { record, schema, hidden, forceNoHidden })
167
- const fl = clone(fields)
168
- if (!fl.includes('id')) fl.unshift('id')
169
- if (record._rel) fl.push('_rel')
170
- return pick(await transform.call(this, { record, schema, hidden, forceNoHidden }), fl)
268
+ /**
269
+ * Get connection by name. It returns the first connection named after "{name}"
270
+ *
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```
274
+ */
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
171
279
  }
172
280
 
173
- prepPagination = async (filter = {}, schema, options = {}) => {
174
- const buildPageSkipLimit = (filter) => {
175
- let limit = parseInt(filter.limit) || this.config.default.filter.limit
176
- if (limit === -1) limit = this.config.default.filter.maxLimit
177
- if (limit > this.config.default.filter.maxLimit) limit = this.config.default.filter.maxLimit
178
- if (limit < 1) limit = 1
179
- let page = parseInt(filter.page) || 1
180
- if (page < 1) page = 1
181
- let skip = (page - 1) * limit
182
- if (filter.skip) {
183
- skip = parseInt(filter.skip) || skip
184
- page = undefined
185
- }
186
- if (skip < 0) skip = 0
187
- return { page, skip, limit }
188
- }
189
-
190
- const buildSort = (input, schema, allowSortUnindexed) => {
191
- const { isEmpty, map, each, isPlainObject, isString, trim, keys } = this.lib._
192
- let sort
193
- if (schema && isEmpty(input)) {
194
- const columns = map(schema.properties, 'name')
195
- each(this.config.default.filter.sort, s => {
196
- const [col] = s.split(':')
197
- if (columns.includes(col)) {
198
- input = s
199
- return false
200
- }
201
- })
202
- }
203
- if (!isEmpty(input)) {
204
- if (isPlainObject(input)) sort = input
205
- else if (isString(input)) {
206
- const item = {}
207
- each(input.split('+'), text => {
208
- let [col, dir] = map(trim(text).split(':'), i => trim(i))
209
- dir = (dir ?? '').toUpperCase()
210
- dir = dir === 'DESC' ? -1 : parseInt(dir) || 1
211
- item[col] = dir / Math.abs(dir)
212
- })
213
- sort = item
214
- }
215
- if (schema) {
216
- const items = keys(sort)
217
- each(items, i => {
218
- if (!schema.sortables.includes(i) && !allowSortUnindexed) throw this.error('sortOnUnindexedField%s%s', i, schema.name)
219
- // if (schema.fullText.fields.includes(i)) throw this.error('Can\'t sort on full-text index: \'%s@%s\'', i, schema.name)
220
- })
221
- }
222
- }
223
- return sort
281
+ /**
282
+ * Get driver by name. It returns the first driver named after "{name}"
283
+ *
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```
289
+ */
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
224
300
  }
225
-
226
- const { page, skip, limit } = buildPageSkipLimit.call(this, filter)
227
- let sortInput = filter.sort
228
- try {
229
- sortInput = JSON.parse(sortInput)
230
- } catch (err) {}
231
- const sort = buildSort.call(this, sortInput, schema, options.allowSortUnindexed)
232
- 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
233
305
  }
234
306
 
235
- buildMatch = ({ input = '', schema, options }) => {
236
- const { isPlainObject, trim } = this.lib._
237
- if (isPlainObject(input)) return input
238
- const split = (value, schema) => {
239
- let [field, val] = value.split(':').map(i => i.trim())
240
- if (!val) {
241
- val = field
242
- field = '*'
243
- }
244
- return { field, value: val }
245
- }
246
- input = trim(input)
247
- let items = {}
248
- if (isPlainObject(input)) items = input
249
- else if (input[0] === '{') items = JSON.parse(input)
250
- else {
251
- for (const item of input.split('+').map(i => i.trim())) {
252
- const part = split(item, schema)
253
- if (!items[part.field]) items[part.field] = []
254
- items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
255
- }
256
- }
257
- const matcher = {}
258
- for (const f of schema.fullText.fields) {
259
- const value = []
260
- if (typeof items[f] === 'string') items[f] = [items[f]]
261
- if (Object.prototype.hasOwnProperty.call(items, f)) value.push(...items[f])
262
- matcher[f] = value
263
- }
264
- if (Object.prototype.hasOwnProperty.call(items, '*')) matcher['*'] = items['*']
265
- 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
266
321
  }
267
322
 
268
- buildQuery = ({ filter, schema, options = {} } = {}) => {
269
- const { trim, find, isString, isPlainObject } = this.lib._
270
- let query = {}
271
- if (isString(filter.query)) {
272
- try {
273
- filter.query = trim(filter.query)
274
- filter.orgQuery = filter.query
275
- if (trim(filter.query).startsWith('{')) query = JSON.parse(filter.query)
276
- else if (filter.query.includes(':')) query = nql(filter.query).parse()
277
- else {
278
- const fields = schema.sortables.filter(f => {
279
- const field = find(schema.properties, { name: f, type: 'string' })
280
- return !!field
281
- })
282
- const parts = fields.map(f => {
283
- if (filter.query[0] === '*') return `${f}:~$'${filter.query.replaceAll('*', '')}'`
284
- if (filter.query[filter.length - 1] === '*') return `${f}:~^'${filter.query.replaceAll('*', '')}'`
285
- return `${f}:~'${filter.query.replaceAll('*', '')}'`
286
- })
287
- if (parts.length === 1) query = nql(parts[0]).parse()
288
- else if (parts.length > 1) query = nql(parts.join(',')).parse()
289
- }
290
- } catch (err) {
291
- this.error('invalidQuery', { orgMessage: err.message })
292
- }
293
- } else if (isPlainObject(filter.query)) query = filter.query
294
- 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
295
336
  }
296
337
 
297
- sanitizeQuery = (query, schema, parent) => {
298
- const { cloneDeep, isPlainObject, isArray, find } = this.lib._
299
- const { isSet } = this.lib.aneka
300
- const { dayjs } = this.lib
301
- const obj = cloneDeep(query)
302
- const keys = Object.keys(obj)
303
- const sanitize = (key, val, p) => {
304
- if (!isSet(val)) return val
305
- const prop = find(schema.properties, { name: key.startsWith('$') ? p : key })
306
- if (!prop) return val
307
- if (['datetime', 'date', 'time'].includes(prop.type)) {
308
- const dt = dayjs(val)
309
- return dt.isValid() ? dt.toDate() : val
310
- } else if (['smallint', 'integer'].includes(prop.type)) return parseInt(val) || val
311
- else if (['float', 'double'].includes(prop.type)) return parseFloat(val) || val
312
- else if (['boolean'].includes(prop.type)) return !!val
313
- return val
314
- }
315
- keys.forEach(k => {
316
- const v = obj[k]
317
- if (isPlainObject(v)) obj[k] = this.sanitizeQuery(v, schema, k)
318
- else if (isArray(v)) {
319
- v.forEach((i, idx) => {
320
- if (isPlainObject(i)) obj[k][idx] = this.sanitizeQuery(i, schema, k)
321
- })
322
- } else obj[k] = sanitize(k, v, parent)
323
- })
324
- 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)
325
347
  }
326
348
 
327
349
  validationErrorMessage = (err) => {
@@ -335,83 +357,144 @@ async function factory (pkgName) {
335
357
  return text
336
358
  }
337
359
 
338
- getConnection = (name) => {
339
- const { find } = this.lib._
340
- return find(this.connections, { name })
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)
341
383
  }
342
384
 
343
- getInfo = (name) => {
344
- const { breakNsPath } = this.app.bajo
345
- const { find, map, isEmpty } = this.lib._
346
- const schema = this.getSchema(name)
347
- const conn = this.getConnection(schema.connection)
348
- let { ns, path: type } = breakNsPath(conn.type)
349
- if (isEmpty(type)) type = conn.type
350
- const driver = find(this.drivers, { type, ns, driver: conn.driver })
351
- const instance = find(this.app[driver.ns].instances, { name: schema.connection })
352
- const opts = conn.type === 'mssql' ? { includeTriggerModifications: true } : undefined
353
- const returning = [map(schema.properties, 'name'), opts]
354
- 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
355
390
  }
356
391
 
357
- getSchema = (input, cloned = true) => {
358
- const { find, isPlainObject, cloneDeep } = this.lib._
359
- const { pascalCase } = this.lib.aneka
360
- let name = isPlainObject(input) ? input.name : input
361
- name = pascalCase(name)
362
- const schema = find(this.schemas, { name })
363
- if (!schema) throw this.error('unknownModelSchema%s', name)
364
- 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
365
397
  }
366
398
 
367
- getField = (name, model) => {
368
- const { getInfo } = this.app.dobo
369
- const { find } = this.lib._
370
- const { schema } = getInfo(model)
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
+ }
371
413
 
372
- return find(schema.properties, { name })
414
+ sanitizeBoolean = (value) => {
415
+ return value === null ? null : (['true', true].includes(value))
373
416
  }
374
417
 
375
- hasField = (name, model) => {
376
- return !!this.getField(name, model)
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
377
424
  }
378
425
 
379
- getMemdbStorage = (name, fields = []) => {
380
- const { map, pick } = this.lib._
381
- const all = this.memDb.storage[name] ?? []
382
- if (fields.length === 0) return all
383
- return map(all, item => pick(item, fields))
426
+ sanitizeString = (value) => {
427
+ return value + ''
384
428
  }
385
429
 
386
- listAttachments = async ({ model, id = '*', field = '*', file = '*' } = {}, { uriEncoded = true } = {}) => {
387
- const { map, kebabCase } = this.lib._
388
- const { pascalCase } = this.lib.aneka
389
- const { importPkg, getPluginDataDir } = this.app.bajo
390
- const mime = await importPkg('waibu:mime')
391
- const { fastGlob } = this.lib
392
- const root = `${getPluginDataDir('dobo')}/attachment`
393
- model = pascalCase(model)
394
- let pattern = `${root}/${model}/${id}/${field}/${file}`
395
- if (uriEncoded) pattern = pattern.split('/').map(p => decodeURI(p)).join('/')
396
- return map(await fastGlob(pattern), f => {
397
- const mimeType = mime.getType(path.extname(f)) ?? ''
398
- const fullPath = f.replace(root, '')
399
- const row = {
400
- file: f,
401
- fileName: path.basename(fullPath),
402
- fullPath,
403
- mimeType,
404
- params: { model, id, field, file }
405
- }
406
- if (this.app.waibuMpa) {
407
- const { routePath } = this.app.waibu
408
- const [, _model, _id, _field, _file] = fullPath.split('/')
409
- row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${_field}/${_file}`)
410
- }
411
- return row
412
- })
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)
413
494
  }
414
495
  }
496
+
497
+ return Dobo
415
498
  }
416
499
 
417
500
  export default factory