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
@@ -0,0 +1,402 @@
1
+ import path from 'path'
2
+ import nql from '@tryghost/nql'
3
+
4
+ export async function execHook (name, ...args) {
5
+ const { runHook } = this.app.bajo
6
+ const { camelCase, last } = this.app.lib._
7
+ const { noHook } = last(args)
8
+ const { ns } = this.app.dobo
9
+ if (!noHook) {
10
+ await runHook(`${ns}:${name}`, this.name, ...args)
11
+ await runHook(`${ns}.${camelCase(this.name)}:${name}`, ...args)
12
+ }
13
+ }
14
+
15
+ export async function execModelHook (name, ...args) {
16
+ const { last } = this.app.lib._
17
+ const { noModelHook } = last(args)
18
+ const results = []
19
+ if (!noModelHook) {
20
+ const hooks = this.hooks.filter(hook => hook.name === name)
21
+ for (const hook of hooks) {
22
+ await hook.handler.call(this, ...args)
23
+ }
24
+ }
25
+ return results
26
+ }
27
+
28
+ export async function execValidation (body, options = {}) {
29
+ const { uniq } = this.app.lib._
30
+ const { validation = {}, extFields = [], partial, req } = options
31
+ const fields = uniq([...Object.keys(body), ...(options.fields ?? [])])
32
+ await execHook.call(this, 'beforeRecordValidation', body, options)
33
+ const result = await this.validate(body, validation, { fields, extFields, partial, req })
34
+ await execHook.call(this, 'afterRecordValidation', body, result, options)
35
+ return result
36
+ }
37
+
38
+ /**
39
+ * Break any reference to the original and get the new options
40
+ *
41
+ * @param {Object} options
42
+ * @returns {Object}
43
+ */
44
+ export async function getFilterAndOptions (filter = {}, options = {}, action) {
45
+ const { cloneDeep, omit } = this.app.lib._
46
+ const keys = ['req', 'reply']
47
+ const nFilter = cloneDeep(omit(filter, keys))
48
+ const nOptions = cloneDeep(omit(options, keys))
49
+ nOptions.action = action
50
+ nOptions.dataOnly = false
51
+ nOptions.truncateString = nOptions.truncateString ?? false
52
+ nOptions.throwNotFound = nOptions.throwNotFound ?? true
53
+ for (const key of keys) {
54
+ nOptions[key] = options[key]
55
+ }
56
+
57
+ nFilter.query = buildFilterQuery.call(this, nFilter, nOptions) ?? {}
58
+ nFilter.match = buildFilterMatch.call(this, nFilter, nOptions) ?? {}
59
+ handleRegexInQuery.call(this, nFilter)
60
+ const { limit, page, skip, sort } = preparePagination.call(this, nFilter, nOptions)
61
+ nFilter.limit = limit
62
+ nFilter.page = page
63
+ nFilter.skip = skip
64
+ nFilter.sort = sort
65
+ if (nOptions.queryHandler) {
66
+ const scope = nOptions.req ? this.app[nOptions.req.ns] : this.plugin
67
+ nFilter.query = await options.queryHandler.call(scope, nFilter.query, nOptions.req)
68
+ }
69
+ return { filter: nFilter, options: nOptions }
70
+ }
71
+
72
+ export async function handleReq (id, trigger, options = {}) {
73
+ const { upperFirst } = this.app.lib._
74
+ if (options.req) {
75
+ if (options.req.file && trigger !== 'removed') await handleAttachmentUpload.call(this, id, trigger, options)
76
+ if (options.req.flash && !options.noFlash) options.req.flash('notify', options.req.t(`record${upperFirst(trigger)}`))
77
+ }
78
+ }
79
+
80
+ export async function mergeAttachmentInfo (rec, source, options = {}) {
81
+ if (!this.app.waibu) return
82
+ const { mimeType, stats, fullPath } = options
83
+ const { importPkg } = this.app.bajo
84
+ const { fs } = this.app.lib
85
+ const { pick } = this.app.lib._
86
+ const mime = await importPkg('waibu:mime')
87
+
88
+ if (mimeType) rec.mimeType = mime.getType(rec.file)
89
+ if (fullPath) rec.fullPath = source
90
+ if (stats) {
91
+ const s = fs.statSync(source)
92
+ rec.stats = pick(s, ['size', 'atime', 'ctime', 'mtime'])
93
+ }
94
+ }
95
+
96
+ export async function getAttachmentPath (id, fieldName, file, options = {}) {
97
+ const { getPluginDataDir } = this.app.bajo
98
+ const { fs } = this.app.lib
99
+ const dir = `${getPluginDataDir(this.app.dobo.ns)}/attachment/${this.name}/${id}`
100
+ if (options.dirOnly) return dir
101
+ const path = fieldName ? `${dir}/${fieldName}/${file}` : `${dir}/${file}`
102
+ if (!fs.existsSync(path)) throw this.app.dobo.error('notFound')
103
+ return path
104
+ }
105
+
106
+ export async function copyAttachment (id, options = {}) {
107
+ if (!this.app.waibu) return
108
+ if (!this.attachment) return
109
+ const { fs } = this.app.lib
110
+ const { req, setField, setFile, mimeType, stats } = options
111
+ const { dir, files } = await this.app.waibu.getUploadedFiles(req.id, false, true)
112
+ const result = []
113
+ if (files.length === 0) return result
114
+ for (const f of files) {
115
+ let [fieldName, ...parts] = path.basename(f).split('@')
116
+ if (parts.length === 0) continue
117
+ fieldName = setField ?? fieldName
118
+ const file = setFile ?? parts.join('@')
119
+ const opts = { source: f, fieldName, file, mimeType, stats, req, silent: true }
120
+ const rec = await this.createAttachment(id, opts)
121
+ if (!rec) continue
122
+ delete rec.dir
123
+ result.push(rec)
124
+ if (setField || setFile) break
125
+ }
126
+ fs.removeSync(dir)
127
+ return result
128
+ }
129
+
130
+ export async function handleAttachmentUpload (id, trigger, options = {}) {
131
+ if (!this.attachment) return
132
+ const { getPluginDataDir } = this.app.bajo
133
+ const { fs } = this.app.lib
134
+ const { req, mimeType, stats, setFile, setField } = options
135
+ if (trigger === 'removed') {
136
+ const dir = `${getPluginDataDir(this.app.dobo.ns)}/attachment/${this.name}/${id}`
137
+ await fs.remove(dir)
138
+ return
139
+ }
140
+ return copyAttachment.call(this, id, { req, mimeType, stats, setFile, setField })
141
+ }
142
+
143
+ export async function getSingleRef (record = {}, options = {}) {
144
+ const { isSet } = this.app.lib.aneka
145
+ const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
146
+ const refs = {}
147
+ options.refs = options.refs ?? []
148
+ if (props.length > 0) {
149
+ for (const prop of props) {
150
+ for (const key in prop.ref) {
151
+ if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) continue
152
+ const ref = prop.ref[key]
153
+ if (ref.fields.length === 0) continue
154
+ const rModel = this.plugin.getModel(key)
155
+ const query = {}
156
+ query[ref.propName] = record[prop.name]
157
+ if (ref.propName === 'id') query[ref.propName] = this.sanitizeId(query[ref.propName])
158
+ const rFilter = { query }
159
+ const rOptions = { dataOnly: true, refs: [] }
160
+ const results = await rModel.findRecord(rFilter, rOptions)
161
+ const fields = [...ref.fields]
162
+ const data = []
163
+ for (const res of results) {
164
+ data.push(await rModel.sanitizeRecord(res, { fields }))
165
+ }
166
+ refs[key] = ['1:1'].includes(ref.type) ? data[0] : data
167
+ }
168
+ }
169
+ }
170
+ record._ref = refs
171
+ }
172
+
173
+ export async function getMultiRefs (records = [], options = {}) {
174
+ const { isSet } = this.app.lib.aneka
175
+ const { uniq, map } = this.app.lib._
176
+ const props = this.properties.filter(p => isSet(p.ref) && !(options.hidden ?? []).includes(p.name))
177
+ options.refs = options.refs ?? []
178
+ if (props.length > 0) {
179
+ for (const prop of props) {
180
+ for (const key in prop.ref) {
181
+ if (!((typeof options.refs === 'string' && ['*', 'all'].includes(options.refs)) || options.refs.includes(key))) continue
182
+ const ref = prop.ref[key]
183
+ if (ref.fields.length === 0) continue
184
+ if (ref.type !== '1:1') continue
185
+ const rModel = this.plugin.getModel(key)
186
+ const matches = uniq(map(records, r => {
187
+ let v = r[prop.name]
188
+ if (ref.propName === 'id') v = this.sanitizeId(v)
189
+ return v
190
+ }))
191
+ const query = {}
192
+ query[ref.propName] = { $in: matches }
193
+ const rFilter = { query, limit: matches.length }
194
+ const rOptions = { dataOnly: true, refs: [] }
195
+ const results = await rModel.findRecord(rFilter, rOptions)
196
+ const fields = [...ref.fields]
197
+ if (!fields.includes(prop.name)) fields.push(prop.name)
198
+ for (const i in records) {
199
+ records[i]._ref = records[i]._ref ?? {}
200
+ const rec = records[i]
201
+ const res = results.find(res => (res[ref.propName] + '') === rec[prop.name] + '')
202
+ if (res) records[i]._ref[key] = await rModel.sanitizeRecord(res, { fields })
203
+ else records[i]._ref[key] = {}
204
+ }
205
+ }
206
+ }
207
+ }
208
+ }
209
+
210
+ export function buildFilterQuery (filter = {}, options = {}) {
211
+ const { trim, find, isString, isPlainObject } = this.app.lib._
212
+ let query = {}
213
+ if (isString(filter.query)) {
214
+ try {
215
+ filter.query = trim(filter.query)
216
+ filter.orgQuery = filter.query
217
+ if (trim(filter.query).startsWith('{')) query = JSON.parse(filter.query)
218
+ else if (filter.query.includes(':')) query = nql(filter.query).parse()
219
+ else {
220
+ const fields = this.sortables.filter(f => {
221
+ const field = find(this.properties, { name: f, type: 'string' })
222
+ return !!field
223
+ })
224
+ const parts = fields.map(f => {
225
+ if (filter.query[0] === '*') return `${f}:~$'${filter.query.replaceAll('*', '')}'`
226
+ if (filter.query[filter.length - 1] === '*') return `${f}:~^'${filter.query.replaceAll('*', '')}'`
227
+ return `${f}:~'${filter.query.replaceAll('*', '')}'`
228
+ })
229
+ if (parts.length === 1) query = nql(parts[0]).parse()
230
+ else if (parts.length > 1) query = nql(parts.join(',')).parse()
231
+ }
232
+ } catch (err) {
233
+ this.app.dobo.error('invalidQuery', { orgMessage: err.message })
234
+ }
235
+ } else if (isPlainObject(filter.query)) query = filter.query
236
+ return sanitizeQuery.call(this, query)
237
+ }
238
+
239
+ function sanitizeQuery (query = {}, parent) {
240
+ const { cloneDeep, isPlainObject, isArray, find } = this.app.lib._
241
+ const { isSet } = this.app.lib.aneka
242
+ const { dayjs } = this.app.lib
243
+ const obj = cloneDeep(query)
244
+ const keys = Object.keys(obj)
245
+ const sanitize = (key, val, p) => {
246
+ if (!isSet(val)) return val
247
+ const prop = find(this.properties, { name: key.startsWith('$') ? p : key })
248
+ if (!prop) return val
249
+ if (['datetime', 'date', 'time'].includes(prop.type)) {
250
+ const dt = dayjs(val)
251
+ return dt.isValid() ? dt.toDate() : val
252
+ } else if (['smallint', 'integer'].includes(prop.type)) return parseInt(val) || val
253
+ else if (['float', 'double'].includes(prop.type)) return parseFloat(val) || val
254
+ else if (['boolean'].includes(prop.type)) return !!val
255
+ return val
256
+ }
257
+ keys.forEach(k => {
258
+ const v = obj[k]
259
+ if (isPlainObject(v)) obj[k] = sanitizeQuery.call(this, v, k)
260
+ else if (isArray(v)) {
261
+ v.forEach((i, idx) => {
262
+ if (isPlainObject(i)) obj[k][idx] = sanitizeQuery.call(this, i, k)
263
+ })
264
+ } else obj[k] = sanitize(k, v, parent)
265
+ })
266
+ return obj
267
+ }
268
+
269
+ export function buildFilterMatch (filter = {}, options = {}) {
270
+ const { isPlainObject, trim, has, uniq } = this.app.lib._
271
+ let input = filter.match
272
+ if (isPlainObject(input)) return input
273
+ const split = (value) => {
274
+ let [field, val] = value.split(':').map(i => i.trim())
275
+ if (!val) {
276
+ val = field
277
+ field = '*'
278
+ }
279
+ return { field, value: val }
280
+ }
281
+ input = trim(input)
282
+ let items = {}
283
+ if (isPlainObject(input)) items = input
284
+ else if (input[0] === '{') {
285
+ try {
286
+ items = JSON.parse(input)
287
+ } catch (err) {}
288
+ } else {
289
+ for (const item of input.split('+').map(i => i.trim())) {
290
+ const part = split(item, ' ')
291
+ if (!items[part.field]) items[part.field] = []
292
+ items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
293
+ }
294
+ }
295
+ const matcher = {}
296
+ for (const index of this.indexes.filter(i => i.type === 'fulltext')) {
297
+ for (const f of index.fields) {
298
+ const value = []
299
+ if (typeof items[f] === 'string') items[f] = [items[f]]
300
+ if (has(items, f)) value.push(...items[f])
301
+ if (!matcher[f]) matcher[f] = []
302
+ matcher[f] = uniq([...matcher[f], ...value])
303
+ }
304
+ }
305
+ if (has(items, '*')) matcher['*'] = items['*']
306
+ return matcher
307
+ }
308
+
309
+ export function handleRegexInQuery (filter) {
310
+ if (this.driver.idField.name !== 'id') {
311
+ const query = JSON.stringify(filter.query ?? {}, (key, value) => {
312
+ if (value instanceof RegExp) return ['__REGEXP__', value.source, value.flags]
313
+ return value
314
+ }).replaceAll('"id"', `"${this.driver.idField.name}"`)
315
+ try {
316
+ filter.query = JSON.parse(query, (key, value) => {
317
+ if (Array.isArray(value) && value[0] === '__REGEXP__') return new RegExp(value[1], value[2])
318
+ return value
319
+ })
320
+ } catch (err) {}
321
+ const match = JSON.stringify(filter.match ?? {}).replaceAll('"id"', `"${this.driver.idField.name}"`)
322
+ try {
323
+ filter.match = JSON.parse(match)
324
+ } catch (err) {}
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Prepare records pagination:
330
+ * - making sure records limit is obeyed
331
+ * - making sure page is a positive value
332
+ * - if skip is given, recalculate limit to use skip instead of page number
333
+ * - Build sort info
334
+ *
335
+ * @method
336
+ * @async
337
+ * @param {Object} [filter={}] - Filter object
338
+ * @param {Object} options - Options
339
+ * @returns {TRecordPagination}
340
+ */
341
+ export function preparePagination (filter = {}, options = {}) {
342
+ const { isEmpty, map, each, isPlainObject, isString, trim, keys } = this.app.lib._
343
+ const config = this.app.dobo.config
344
+
345
+ const buildPageSkipLimit = (filter) => {
346
+ let limit = parseInt(filter.limit) || config.default.filter.limit
347
+ if (limit === -1) limit = config.default.filter.maxLimit
348
+ if (limit > config.default.filter.maxLimit) limit = config.default.filter.maxLimit
349
+ if (limit < 1) limit = 1
350
+ let page = parseInt(filter.page) || 1
351
+ if (page < 1) page = 1
352
+ let skip = (page - 1) * limit
353
+ if (filter.skip) {
354
+ skip = parseInt(filter.skip) || skip
355
+ page = undefined
356
+ }
357
+ if (skip < 0) skip = 0
358
+ return { page, skip, limit }
359
+ }
360
+
361
+ const buildSort = (input, allowSortUnindexed) => {
362
+ let sort
363
+ if (isEmpty(input)) {
364
+ const columns = map(this.properties ?? [], 'name')
365
+ each(config.default.filter.sort, s => {
366
+ const [col] = s.split(':')
367
+ if (columns.includes(col)) {
368
+ input = s
369
+ return false
370
+ }
371
+ })
372
+ }
373
+ if (!isEmpty(input)) {
374
+ if (isPlainObject(input)) sort = input
375
+ else if (isString(input)) {
376
+ const item = {}
377
+ each(input.split('+'), text => {
378
+ let [col, dir] = map(trim(text).split(':'), i => trim(i))
379
+ dir = (dir ?? '').toUpperCase()
380
+ dir = dir === 'DESC' ? -1 : parseInt(dir) || 1
381
+ item[col] = dir / Math.abs(dir)
382
+ })
383
+ sort = item
384
+ }
385
+ const items = keys(sort)
386
+ each(items, i => {
387
+ if (!this.sortables.includes(i) && !allowSortUnindexed) throw this.app.dobo.error('sortOnUnindexedField%s%s', i, this.name)
388
+ // if (model.fullText.fields.includes(i)) throw this.error('Can\'t sort on full-text index: \'%s@%s\'', i, model.name)
389
+ })
390
+ }
391
+ return sort
392
+ }
393
+
394
+ const { page, skip, limit } = buildPageSkipLimit(filter)
395
+ let sortInput = filter.sort
396
+ try {
397
+ sortInput = JSON.parse(sortInput)
398
+ } catch (err) {
399
+ }
400
+ const sort = buildSort(sortInput, options.allowSortUnindexed)
401
+ return { limit, page, skip, sort }
402
+ }
@@ -0,0 +1,15 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'build'
3
+
4
+ async function build (opts = {}) {
5
+ const { dataOnly = true } = opts
6
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
7
+ await execHook.call(this, 'beforeBuildModel', options)
8
+ await execModelHook.call(this, 'beforeBuildModel', options)
9
+ const result = (await this.driver._buildModel(this, options)) ?? {}
10
+ await execModelHook.call(this, 'afterBuildModel', result, options)
11
+ await execHook.call(this, 'afterBuildModel', result, options)
12
+ return dataOnly ? result.data : result
13
+ }
14
+
15
+ export default build
@@ -0,0 +1,17 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'clearRecord'
3
+
4
+ async function clearRecord (...args) {
5
+ if (args.length === 0) return this.action(action, ...args)
6
+ const [opts = {}] = args
7
+ const { dataOnly = true } = opts
8
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
9
+ await execHook.call(this, 'beforeClearRecord', options)
10
+ await execModelHook.call(this, 'beforeClearRecord', options)
11
+ const result = (await this.driver._clearRecord(this, options)) ?? {}
12
+ await execModelHook.call(this, 'afterClearRecord', result, options)
13
+ await execHook.call(this, 'afterClearRecord', result, options)
14
+ return dataOnly ? result.data : result
15
+ }
16
+
17
+ export default clearRecord
@@ -0,0 +1,17 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'countRecord'
3
+
4
+ async function countRecord (...args) {
5
+ if (args.length === 0) return this.action(action, ...args)
6
+ const [params = {}, opts = {}] = args
7
+ const { dataOnly = true } = opts
8
+ const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
9
+ await execHook.call(this, 'beforeCountRecord', options)
10
+ await execModelHook.call(this, 'beforeCountRecord', filter, options)
11
+ const result = (await this.driver._countRecord(this, filter, options)) ?? {}
12
+ await execModelHook.call(this, 'afterCountRecord', filter, result, options)
13
+ await execHook.call(this, 'afterCountRecord', filter, result, options)
14
+ return dataOnly ? result.data : result
15
+ }
16
+
17
+ export default countRecord
@@ -0,0 +1,17 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'createAggregate'
3
+
4
+ async function createAggregate (...args) {
5
+ if (args.length === 0) return this.action(action, ...args)
6
+ const [_filter = {}, params = {}, opts = {}] = args
7
+ const { dataOnly = true } = opts
8
+ const { filter, options } = await getFilterAndOptions.call(this, _filter, opts, action)
9
+ await execHook.call(this, 'beforeCreateAggregate', filter, params, options)
10
+ await execModelHook.call(this, 'beforeCreateAggregate', filter, params, options)
11
+ const result = (await this.driver._createAggregate(this, filter, params, options)) ?? {}
12
+ await execModelHook.call(this, 'afterCreateAggregate', filter, params, result, options)
13
+ await execHook.call(this, 'afterCreateAggregate', filter, params, result, options)
14
+ return dataOnly ? result.data : result
15
+ }
16
+
17
+ export default createAggregate
@@ -0,0 +1,29 @@
1
+ import { mergeAttachmentInfo, getAttachmentPath } from './_util.js'
2
+ const action = 'createAttachment'
3
+
4
+ async function createAttachment (...args) {
5
+ if (!this.attachment) return
6
+ if (args.length === 0) return this.action(action, ...args)
7
+ const [id, opts = {}] = args
8
+ const { fs } = this.app.lib
9
+ const { isEmpty } = this.app.lib._
10
+ const { source, fieldName = 'file', file, fullPath, stats, mimeType, req } = opts
11
+ if (isEmpty(file)) return
12
+ if (!source) throw this.plugin.error('isMissing%s', this.plugin.t('field.source'))
13
+ const baseDir = await getAttachmentPath.call(this, id, fieldName, file, { dirOnly: true })
14
+ let dir = `${baseDir}/${fieldName}`
15
+ if ((fieldName || '').endsWith('[]')) dir = `${baseDir}/${fieldName.replace('[]', '')}`
16
+ const dest = `${dir}/${file}`.replaceAll('//', '/')
17
+ await fs.ensureDir(dir)
18
+ await fs.copy(source, dest)
19
+ const rec = {
20
+ field: fieldName === '' ? undefined : fieldName,
21
+ dir,
22
+ file
23
+ }
24
+ await mergeAttachmentInfo.call(this, rec, dest, { mimeType, fullPath, stats })
25
+ if (!opts.noFlash && req && req.flash) req.flash('notify', req.t('attachmentUploaded'))
26
+ return rec
27
+ }
28
+
29
+ export default createAttachment
@@ -0,0 +1,17 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'createHistogram'
3
+
4
+ async function createHistogram (...args) {
5
+ if (args.length === 0) return this.action(action, ...args)
6
+ const [_filter = {}, params = {}, opts = {}] = args
7
+ const { dataOnly = true } = opts
8
+ const { filter, options } = await getFilterAndOptions.call(this, _filter, opts, action)
9
+ await execHook.call(this, 'beforeCreateHistogram', filter, params, options)
10
+ await execModelHook.call(this, 'beforeCreateHistogram', filter, params, options)
11
+ const result = (await this.driver._createHistogram(this, filter, params, options)) ?? {}
12
+ await execModelHook.call(this, 'afterCreateHistogram', filter, params, result, options)
13
+ await execHook.call(this, 'afterCreateHistogram', filter, params, result, options)
14
+ return dataOnly ? result.data : result
15
+ }
16
+
17
+ export default createHistogram
@@ -0,0 +1,35 @@
1
+ import { getFilterAndOptions, execHook, execValidation, execModelHook, getSingleRef, handleReq } from './_util.js'
2
+
3
+ export const onlyTypes = ['datetime', 'date', 'time', 'timestamp']
4
+ const action = 'createRecord'
5
+
6
+ async function createRecord (...args) {
7
+ if (args.length === 0) return this.action(action, ...args)
8
+ const [body = {}, opts = {}] = args
9
+ const { isSet } = this.app.lib.aneka
10
+ const { runHook } = this.app.bajo
11
+ const { cloneDeep, get } = this.app.lib._
12
+ const { dataOnly = true } = opts
13
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
14
+ const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation } = options
15
+ const extFields = get(options, 'validation.extFields', [])
16
+ const input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString, onlyTypes })
17
+ await execHook.call(this, 'beforeCreateRecord', input, options)
18
+ await execModelHook.call(this, 'beforeCreateRecord', input, options)
19
+ if (!noValidation) await execValidation.call(this, input, options)
20
+ let result = options.record ?? (await this.driver._createRecord(this, input, options)) ?? {}
21
+ if (noResult) {
22
+ await runHook('cache:clear', this, 'create', body)
23
+ return
24
+ }
25
+ result = result ?? {}
26
+ if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
27
+ if (isSet(options.refs)) await getSingleRef.call(this, { record: result.data, options })
28
+ await handleReq.call(this, result.data.id, 'created', options)
29
+ await execModelHook.call(this, 'afterCreateRecord', input, result, options)
30
+ await execHook.call(this, 'afterCreateRecord', input, result, options)
31
+ await runHook('cache:clear', this, 'create', body, result)
32
+ return dataOnly ? result.data : result
33
+ }
34
+
35
+ export default createRecord
@@ -0,0 +1,15 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'drop'
3
+
4
+ async function drop (opts = {}) {
5
+ const { dataOnly = true } = opts
6
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
7
+ await execHook.call(this, 'beforeDropModel', options)
8
+ await execModelHook.call(this, 'beforeDropModel', options)
9
+ const result = (await this.driver._dropModel(this, options)) ?? {}
10
+ await execModelHook.call(this, 'afterDropModel', result, options)
11
+ await execHook.call(this, 'afterModelDrop', result, options)
12
+ return dataOnly ? result.data : result
13
+ }
14
+
15
+ export default drop
@@ -0,0 +1,21 @@
1
+ import { getFilterAndOptions, execHook, execModelHook } from './_util.js'
2
+ const action = 'modelExists'
3
+
4
+ /**
5
+ * Method to check if the underlaying table/collection exists already
6
+ *
7
+ * @param {Object} [options]
8
+ * @returns {Object}
9
+ */
10
+ async function isExists (opts = {}) {
11
+ const { dataOnly = true } = opts
12
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
13
+ await execHook.call(this, 'beforeModelExists', options)
14
+ await execModelHook.call(this, 'beforeModelExists', options)
15
+ const result = (await this.driver._modelExists(this, options)) ?? {}
16
+ await execModelHook.call(this, 'afterModelExists', result, options)
17
+ await execHook.call(this, 'afterModelExists', result, options)
18
+ return dataOnly ? result.data : result
19
+ }
20
+
21
+ export default isExists
@@ -0,0 +1,71 @@
1
+ import { getMultiRefs, execHook, execModelHook, getFilterAndOptions } from './_util.js'
2
+ const action = 'findAllRecord'
3
+
4
+ async function native (filter, options, dataOnly) {
5
+ const { isSet } = this.app.lib.aneka
6
+ const { get, set } = this.plugin.cache ?? {}
7
+ if (dataOnly) options.count = false
8
+ let { noResultSanitizer, noCache } = options
9
+ if (!this.cacheable) noCache = true
10
+ await execHook.call(this, 'beforeFindAllRecord', filter, options)
11
+ await execModelHook.call(this, 'beforeFindAllRecord', filter, options)
12
+ if (get && !noCache && !options.record) {
13
+ const cachedResult = await get({ model: this.name, filter, options })
14
+ if (cachedResult) {
15
+ cachedResult.cached = true
16
+ await execModelHook.call(this, 'afterFindAllRecord', filter, cachedResult, options)
17
+ return dataOnly ? cachedResult.data : cachedResult
18
+ }
19
+ }
20
+ const result = options.record ?? (await this.driver._findAllRecord(this, filter, options)) ?? {}
21
+ if (!noResultSanitizer) {
22
+ for (const idx in result.data) {
23
+ result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
24
+ }
25
+ }
26
+ if (isSet(options.refs)) await getMultiRefs.call(this, { records: result.data, options })
27
+ await execModelHook.call(this, 'afterFindAllRecord', filter, result, options)
28
+ await execHook.call(this, 'afterFindAllRecord', filter, result, options)
29
+ if (set && !noCache) await set({ model: this.name, filter, options, result })
30
+ return dataOnly ? result.data : result
31
+ }
32
+
33
+ async function loop (params, opts, dataOnly) {
34
+ const { cloneDeep } = this.app.lib._
35
+ const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
36
+ const { maxLimit, hardLimit } = this.app.dobo.config.default.filter
37
+ const nFilter = cloneDeep(filter)
38
+ const nOptions = cloneDeep(options)
39
+ nOptions.count = false
40
+ nOptions.dataOnly = false
41
+ nFilter.limit = maxLimit
42
+ nFilter.page = 1
43
+ let count = 0
44
+ const data = []
45
+ for (;;) {
46
+ const result = await this.findRecord(nFilter, nOptions)
47
+ if (result.data.length === 0) break
48
+ if (count + result.data.length > hardLimit) {
49
+ const sliced = result.data.slice(0, hardLimit - count)
50
+ data.push(...sliced)
51
+ break
52
+ }
53
+ data.push(...result.data)
54
+ count = count + result.data.length
55
+ nFilter.page++
56
+ }
57
+ return dataOnly ? data : { data, count }
58
+ }
59
+
60
+ async function findAllRecord (...args) {
61
+ if (args.length === 0) return this.action(action, ...args)
62
+ const [params = {}, opts = {}] = args
63
+ const { dataOnly = true } = opts
64
+ if (this.driver.findAllRecord) {
65
+ const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
66
+ return await native.call(this, filter, options, dataOnly)
67
+ }
68
+ return await loop.call(this, params, opts, dataOnly)
69
+ }
70
+
71
+ export default findAllRecord