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.
Files changed (194) hide show
  1. package/.github/FUNDING.yml +0 -0
  2. package/.github/workflows/repo-lockdown.yml +0 -0
  3. package/.jsdoc.conf.json +0 -0
  4. package/LICENSE +0 -0
  5. package/README.md +2 -2
  6. package/docs/Dobo.html +0 -0
  7. package/docs/data/search.json +0 -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 +0 -0
  12. package/docs/index.html +0 -0
  13. package/docs/index.js.html +0 -0
  14. package/docs/lib_collect-connections.js.html +0 -0
  15. package/docs/lib_collect-drivers.js.html +0 -0
  16. package/docs/lib_collect-features.js.html +0 -0
  17. package/docs/lib_collect-schemas.js.html +0 -0
  18. package/docs/lib_index.js.html +0 -0
  19. package/docs/method_model_create.js.html +0 -0
  20. package/docs/method_model_drop.js.html +0 -0
  21. package/docs/method_model_exists.js.html +0 -0
  22. package/docs/method_record_count.js.html +0 -0
  23. package/docs/method_record_create.js.html +0 -0
  24. package/docs/method_record_find-all.js.html +0 -0
  25. package/docs/method_record_find-one.js.html +0 -0
  26. package/docs/method_record_find.js.html +0 -0
  27. package/docs/method_record_get.js.html +0 -0
  28. package/docs/method_record_remove.js.html +0 -0
  29. package/docs/method_record_update.js.html +0 -0
  30. package/docs/method_record_upsert.js.html +0 -0
  31. package/docs/method_sanitize_body.js.html +0 -0
  32. package/docs/method_sanitize_date.js.html +0 -0
  33. package/docs/method_sanitize_id.js.html +0 -0
  34. package/docs/method_validate.js.html +0 -0
  35. package/docs/module-Lib.html +0 -0
  36. package/docs/scripts/core.js +476 -477
  37. package/docs/scripts/core.min.js +0 -0
  38. package/docs/scripts/resize.js +36 -36
  39. package/docs/scripts/search.js +105 -105
  40. package/docs/scripts/search.min.js +0 -0
  41. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -0
  42. package/docs/scripts/third-party/fuse.js +1 -1
  43. package/docs/scripts/third-party/hljs-line-num-original.js +282 -285
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -1
  45. package/docs/scripts/third-party/hljs-original.js +1195 -1202
  46. package/docs/scripts/third-party/hljs.js +1 -1
  47. package/docs/scripts/third-party/popper.js +1 -1
  48. package/docs/scripts/third-party/tippy.js +1 -1
  49. package/docs/scripts/third-party/tocbot.js +508 -509
  50. package/docs/scripts/third-party/tocbot.min.js +0 -0
  51. package/docs/static/bitcoin.jpeg +0 -0
  52. package/docs/static/home.md +0 -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 +0 -0
  56. package/docs/styles/clean-jsdoc-theme-dark.css +0 -0
  57. package/docs/styles/clean-jsdoc-theme-light.css +0 -0
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -0
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -0
  60. package/docs/styles/clean-jsdoc-theme.min.css +0 -0
  61. package/extend/bajo/intl/en-US.json +66 -28
  62. package/extend/bajo/intl/id.json +55 -27
  63. package/extend/bajoCli/applet/clear-record.js +22 -0
  64. package/extend/bajoCli/applet/connection.js +0 -0
  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 +10 -17
  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 +9 -7
  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 +32 -54
  83. package/extend/dobo/feature/updated-at.js +14 -12
  84. package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +2 -6
  85. package/extend/waibuStatic/virtual.json +0 -0
  86. package/index.js +284 -371
  87. package/lib/collect-connections.js +49 -21
  88. package/lib/collect-drivers.js +19 -33
  89. package/lib/collect-features.js +24 -17
  90. package/lib/collect-models.js +321 -0
  91. package/lib/factory/action.js +161 -0
  92. package/lib/factory/connection.js +62 -0
  93. package/lib/factory/driver.js +372 -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/{method/record/find.js → lib/factory/model/find-record.js} +103 -115
  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 +56 -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/{method → lib/factory/model}/validate.js +38 -52
  122. package/lib/factory/model.js +150 -0
  123. package/lib/index.js +0 -0
  124. package/package.json +8 -4
  125. package/wiki/APPLETS.md +0 -0
  126. package/wiki/CHANGES.md +50 -0
  127. package/wiki/CONFIG.md +0 -0
  128. package/wiki/CONTRIBUTING.md +0 -0
  129. package/wiki/DEV-GUIDE.md +0 -0
  130. package/wiki/ECOSYSTEM.md +0 -0
  131. package/wiki/GETTING-STARTED.md +10 -10
  132. package/wiki/QUERY-LANGUAGE.md +0 -0
  133. package/wiki/USER-GUIDE.md +0 -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 -43
  137. package/extend/bajoCli/applet/record-find.js +0 -28
  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-schemas.js +0 -91
  146. package/lib/exec-feature-hook.js +0 -13
  147. package/lib/exec-validation.js +0 -21
  148. package/lib/generic-prop-sanitizer.js +0 -32
  149. package/lib/handle-attachment-upload.js +0 -16
  150. package/lib/mem-db/conn-sanitizer.js +0 -8
  151. package/lib/mem-db/instantiate.js +0 -41
  152. package/lib/mem-db/method/model/clear.js +0 -6
  153. package/lib/mem-db/method/model/create.js +0 -5
  154. package/lib/mem-db/method/model/drop.js +0 -5
  155. package/lib/mem-db/method/model/exists.js +0 -5
  156. package/lib/mem-db/method/record/create.js +0 -12
  157. package/lib/mem-db/method/record/find.js +0 -20
  158. package/lib/mem-db/method/record/get.js +0 -9
  159. package/lib/mem-db/method/record/remove.js +0 -13
  160. package/lib/mem-db/method/record/update.js +0 -15
  161. package/lib/mem-db/method/stat/count.js +0 -11
  162. package/lib/mem-db/start.js +0 -25
  163. package/lib/merge-attachment-info.js +0 -16
  164. package/lib/multi-rel-rows.js +0 -42
  165. package/lib/resolve-method.js +0 -16
  166. package/lib/sanitize-schema.js +0 -198
  167. package/lib/single-rel-rows.js +0 -38
  168. package/method/attachment/copy-uploaded.js +0 -34
  169. package/method/attachment/create.js +0 -29
  170. package/method/attachment/find.js +0 -27
  171. package/method/attachment/get-path.js +0 -12
  172. package/method/attachment/get.js +0 -12
  173. package/method/attachment/pre-check.js +0 -9
  174. package/method/attachment/remove.js +0 -11
  175. package/method/attachment/update.js +0 -7
  176. package/method/bulk/create.js +0 -46
  177. package/method/model/clear.js +0 -22
  178. package/method/model/create.js +0 -32
  179. package/method/model/drop.js +0 -31
  180. package/method/model/exists.js +0 -37
  181. package/method/record/clear.js +0 -24
  182. package/method/record/count.js +0 -66
  183. package/method/record/create.js +0 -111
  184. package/method/record/find-all.js +0 -41
  185. package/method/record/find-one.js +0 -70
  186. package/method/record/get.js +0 -89
  187. package/method/record/remove.js +0 -72
  188. package/method/record/update.js +0 -104
  189. package/method/record/upsert.js +0 -51
  190. package/method/sanitize/body.js +0 -85
  191. package/method/sanitize/date.js +0 -27
  192. package/method/sanitize/id.js +0 -17
  193. package/method/stat/aggregate.js +0 -23
  194. 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 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'
4
+ import collectModels from './lib/collect-models.js'
9
5
 
10
6
  /**
11
- * @typedef {string} TRecordSortKey
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 propType = {
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: 255,
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
- time: {
102
- validator: 'date'
74
+ object: {
75
+ validator: null,
76
+ rules: []
103
77
  },
104
- timestamp: {
105
- validator: 'timestamp'
106
- },
107
- object: {},
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.pluginClass.base {
105
+ class Dobo extends this.app.baseClass.Base {
128
106
  /**
129
- * @constant {string}
107
+ * @constant {string[]}
130
108
  * @memberof Dobo
131
- * @default 'db'
109
+ * @default ['count', 'avg', 'min', 'max', 'sum']
132
110
  */
133
- static alias = 'db'
111
+ static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
134
112
 
135
113
  /**
136
114
  * @constant {string[]}
137
115
  * @memberof Dobo
138
- * @default ['count', 'avg', 'min', 'max', 'sum']
116
+ * @default ['string', 'integer', 'smallint']
139
117
  */
140
- static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
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 {TPropType}
134
+ * @constant {string[]}
143
135
  * @memberof Dobo
136
+ * @default ['index', 'unique', 'primary', 'fulltext']
144
137
  */
145
- static propType = propType
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
- createDefConnAtStart: true,
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.schemas = []
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.collectSchemas|Collecting all schemas}
222
+ * - {@link module:Lib.collectModels|Collecting all models}
216
223
  * @method
217
224
  * @async
218
225
  */
219
226
  init = async () => {
220
- const { buildCollections } = this.app.bajo
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
- if (this.config.memDb.createDefConnAtStart) {
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 collectSchemas.call(this)
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=false] - Set ```true``` to ALWAYS rebuild model on start. Yes, only set it to ```true``` if you REALLY know what you're doing!!!
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
- for (const c of conns) {
257
- const { ns } = breakNsPath(c.type)
258
- const schemas = filter(this.schemas, { connection: c.name })
259
- const mod = c.type === 'dobo:memory' ? memDbInstantiate : await importModule(`${ns}:/extend/${this.ns}/boot/instantiate.js`)
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
- * Pick only fields defined from a record
257
+ * Get connection by name. It returns the first connection named after "{name}"
268
258
  *
269
- * @method
270
- * @async
271
- * @param {Object} [options={}] - Options object
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
- pickRecord = async ({ record, fields, schema = {}, hidden = [], forceNoHidden } = {}) => {
280
- const { isArray, pick, clone, isEmpty, omit } = this.app.lib._
281
- const { dayjs } = this.app.lib
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
- * Prepare records pagination:
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
- * @method
323
- * @async
324
- * @param {Object} [filter={}] - Filter object
325
- * @param {Object} schema - Model's schema
326
- * @param {Object} options - Options
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
- prepPagination = async (filter = {}, schema, options = {}) => {
330
- const buildPageSkipLimit = (filter) => {
331
- let limit = parseInt(filter.limit) || this.config.default.filter.limit
332
- if (limit === -1) limit = this.config.default.filter.maxLimit
333
- if (limit > this.config.default.filter.maxLimit) limit = this.config.default.filter.maxLimit
334
- if (limit < 1) limit = 1
335
- let page = parseInt(filter.page) || 1
336
- if (page < 1) page = 1
337
- let skip = (page - 1) * limit
338
- if (filter.skip) {
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
- const { page, skip, limit } = buildPageSkipLimit.call(this, filter)
383
- let sortInput = filter.sort
384
- try {
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
- buildMatch = ({ input = '', schema, options }) => {
392
- const { isPlainObject, trim } = this.app.lib._
393
- if (isPlainObject(input)) return input
394
- const split = (value, schema) => {
395
- let [field, val] = value.split(':').map(i => i.trim())
396
- if (!val) {
397
- val = field
398
- field = '*'
399
- }
400
- return { field, value: val }
401
- }
402
- input = trim(input)
403
- let items = {}
404
- if (isPlainObject(input)) items = input
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
- buildQuery = ({ filter, schema, options = {} } = {}) => {
425
- const { trim, find, isString, isPlainObject } = this.app.lib._
426
- let query = {}
427
- if (isString(filter.query)) {
428
- try {
429
- filter.query = trim(filter.query)
430
- filter.orgQuery = filter.query
431
- if (trim(filter.query).startsWith('{')) query = JSON.parse(filter.query)
432
- else if (filter.query.includes(':')) query = nql(filter.query).parse()
433
- else {
434
- const fields = schema.sortables.filter(f => {
435
- const field = find(schema.properties, { name: f, type: 'string' })
436
- return !!field
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
- sanitizeQuery = (query, schema, parent) => {
454
- const { cloneDeep, isPlainObject, isArray, find } = this.app.lib._
455
- const { isSet } = this.app.lib.aneka
456
- const { dayjs } = this.app.lib
457
- const obj = cloneDeep(query)
458
- const keys = Object.keys(obj)
459
- const sanitize = (key, val, p) => {
460
- if (!isSet(val)) return val
461
- const prop = find(schema.properties, { name: key.startsWith('$') ? p : key })
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
- getConnection = (name) => {
495
- const { find } = this.app.lib._
496
- return find(this.connections, { name })
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
- getInfo = (name) => {
500
- const { breakNsPath } = this.app.bajo
501
- const { find, map, isEmpty } = this.app.lib._
502
- const schema = this.getSchema(name)
503
- const conn = this.getConnection(schema.connection)
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
- getSchema = (input, cloned = true) => {
514
- const { find, isPlainObject, cloneDeep } = this.app.lib._
515
- const { pascalCase } = this.app.lib.aneka
516
- let name = isPlainObject(input) ? input.name : input
517
- name = pascalCase(name)
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
- getField = (name, model) => {
524
- const { getInfo } = this.app.dobo
525
- const { find } = this.app.lib._
526
- const { schema } = getInfo(model)
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
- return find(schema.properties, { name })
402
+ sanitizeBoolean = (value) => {
403
+ return value === null ? null : (['true', true].includes(value))
529
404
  }
530
405
 
531
- hasField = (name, model) => {
532
- return !!this.getField(name, model)
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
- getMemdbStorage = (name, fields = []) => {
536
- const { map, pick } = this.app.lib._
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
- listAttachments = async ({ model, id = '*', field = '*', file = '*' } = {}, { uriEncoded = true } = {}) => {
543
- const { map, kebabCase } = this.app.lib._
544
- const { pascalCase } = this.app.lib.aneka
545
- const { importPkg, getPluginDataDir } = this.app.bajo
546
- const mime = await importPkg('waibu:mime')
547
- const { fastGlob } = this.app.lib
548
- const root = `${getPluginDataDir('dobo')}/attachment`
549
- model = pascalCase(model)
550
- let pattern = `${root}/${model}/${id}/${field}/${file}`
551
- if (uriEncoded) pattern = pattern.split('/').map(p => decodeURI(p)).join('/')
552
- return map(await fastGlob(pattern), f => {
553
- const mimeType = mime.getType(path.extname(f)) ?? ''
554
- const fullPath = f.replace(root, '')
555
- const row = {
556
- file: f,
557
- fileName: path.basename(fullPath),
558
- fullPath,
559
- mimeType,
560
- params: { model, id, field, file }
561
- }
562
- if (this.app.waibuMpa) {
563
- const { routePath } = this.app.waibu
564
- const [, _model, _id, _field, _file] = fullPath.split('/')
565
- row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${_field}/${_file}`)
566
- }
567
- return row
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