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.
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 +291 -366
  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 +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/{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 +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/{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 +46 -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,67 @@ 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'
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
- object: {},
108
- array: {}
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.pluginClass.base {
117
+ class Dobo extends this.app.baseClass.Base {
128
118
  /**
129
- * @constant {string}
119
+ * @constant {string[]}
130
120
  * @memberof Dobo
131
- * @default 'db'
121
+ * @default ['count', 'avg', 'min', 'max', 'sum']
132
122
  */
133
- static alias = 'db'
123
+ static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
134
124
 
135
125
  /**
136
126
  * @constant {string[]}
137
127
  * @memberof Dobo
138
- * @default ['count', 'avg', 'min', 'max', 'sum']
128
+ * @default ['string', 'integer', 'smallint']
139
129
  */
140
- static aggregateTypes = ['count', 'avg', 'min', 'max', 'sum']
130
+ static idTypes = ['string', 'integer', 'smallint']
131
+
141
132
  /**
142
- * @constant {TPropType}
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 propType = propType
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
- createDefConnAtStart: true,
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.schemas = []
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.collectSchemas|Collecting all schemas}
234
+ * - {@link module:Lib.collectModels|Collecting all models}
216
235
  * @method
217
236
  * @async
218
237
  */
219
238
  init = async () => {
220
- const { buildCollections } = this.app.bajo
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
- 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'))
242
+ await collectConnections.call(this)
238
243
  await collectFeatures.call(this)
239
- await collectSchemas.call(this)
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=false] - Set ```true``` to ALWAYS rebuild model on start. Yes, only set it to ```true``` if you REALLY know what you're doing!!!
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
- 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)
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
- * Pick only fields defined from a record
269
+ * Get connection by name. It returns the first connection named after "{name}"
268
270
  *
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}
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
- 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)
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
- * 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
282
+ * Get driver by name. It returns the first driver named after "{name}"
321
283
  *
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}
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
- 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
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
- 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 }
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
- 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
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
- 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)
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
- 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
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
- getConnection = (name) => {
495
- const { find } = this.app.lib._
496
- 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)
497
383
  }
498
384
 
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 }
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
- 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
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
- getField = (name, model) => {
524
- const { getInfo } = this.app.dobo
525
- const { find } = this.app.lib._
526
- 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
+ }
527
413
 
528
- return find(schema.properties, { name })
414
+ sanitizeBoolean = (value) => {
415
+ return value === null ? null : (['true', true].includes(value))
529
416
  }
530
417
 
531
- hasField = (name, model) => {
532
- 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
533
424
  }
534
425
 
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))
426
+ sanitizeString = (value) => {
427
+ return value + ''
540
428
  }
541
429
 
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
- })
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