dobo 2.0.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/.github/FUNDING.yml +13 -0
  2. package/.github/workflows/repo-lockdown.yml +24 -0
  3. package/.jsdoc.conf.json +45 -0
  4. package/LICENSE +1 -1
  5. package/README.md +38 -19
  6. package/docs/Dobo.html +26 -0
  7. package/docs/data/search.json +1 -0
  8. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  9. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  10. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  11. package/docs/global.html +7 -0
  12. package/docs/index.html +3 -0
  13. package/docs/index.js.html +578 -0
  14. package/docs/lib_collect-connections.js.html +39 -0
  15. package/docs/lib_collect-drivers.js.html +52 -0
  16. package/docs/lib_collect-features.js.html +36 -0
  17. package/docs/lib_collect-schemas.js.html +94 -0
  18. package/docs/lib_index.js.html +6 -0
  19. package/docs/method_model_create.js.html +35 -0
  20. package/docs/method_model_drop.js.html +34 -0
  21. package/docs/method_model_exists.js.html +40 -0
  22. package/docs/method_record_count.js.html +69 -0
  23. package/docs/method_record_create.js.html +114 -0
  24. package/docs/method_record_find-all.js.html +44 -0
  25. package/docs/method_record_find-one.js.html +73 -0
  26. package/docs/method_record_find.js.html +118 -0
  27. package/docs/method_record_get.js.html +92 -0
  28. package/docs/method_record_remove.js.html +75 -0
  29. package/docs/method_record_update.js.html +107 -0
  30. package/docs/method_record_upsert.js.html +54 -0
  31. package/docs/method_sanitize_body.js.html +88 -0
  32. package/docs/method_sanitize_date.js.html +30 -0
  33. package/docs/method_sanitize_id.js.html +20 -0
  34. package/docs/method_validate.js.html +249 -0
  35. package/docs/module-Lib.html +3 -0
  36. package/docs/scripts/core.js +725 -0
  37. package/docs/scripts/core.min.js +23 -0
  38. package/docs/scripts/resize.js +90 -0
  39. package/docs/scripts/search.js +265 -0
  40. package/docs/scripts/search.min.js +6 -0
  41. package/docs/scripts/third-party/Apache-License-2.0.txt +202 -0
  42. package/docs/scripts/third-party/fuse.js +9 -0
  43. package/docs/scripts/third-party/hljs-line-num-original.js +366 -0
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  45. package/docs/scripts/third-party/hljs-original.js +5164 -0
  46. package/docs/scripts/third-party/hljs.js +1 -0
  47. package/docs/scripts/third-party/popper.js +5 -0
  48. package/docs/scripts/third-party/tippy.js +1 -0
  49. package/docs/scripts/third-party/tocbot.js +671 -0
  50. package/docs/scripts/third-party/tocbot.min.js +1 -0
  51. package/docs/static/bitcoin.jpeg +0 -0
  52. package/docs/static/home.md +25 -0
  53. package/docs/static/logo-ecosystem.png +0 -0
  54. package/docs/static/logo.png +0 -0
  55. package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
  56. package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
  57. package/docs/styles/clean-jsdoc-theme-light.css +482 -0
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
  60. package/docs/styles/clean-jsdoc-theme.min.css +1 -0
  61. package/extend/bajo/intl/en-US.json +69 -30
  62. package/extend/bajo/intl/id.json +58 -29
  63. package/extend/bajoCli/applet/clear-record.js +22 -0
  64. package/extend/bajoCli/applet/connection.js +5 -5
  65. package/extend/bajoCli/applet/count-record.js +27 -0
  66. package/extend/bajoCli/applet/create-aggregate.js +33 -0
  67. package/extend/bajoCli/applet/create-histogram.js +33 -0
  68. package/extend/bajoCli/applet/create-record.js +39 -0
  69. package/extend/bajoCli/applet/find-record.js +27 -0
  70. package/extend/bajoCli/applet/get-record.js +27 -0
  71. package/extend/bajoCli/applet/lib/post-process.js +25 -26
  72. package/extend/bajoCli/applet/model.js +22 -0
  73. package/extend/bajoCli/applet/rebuild-model.js +91 -0
  74. package/extend/bajoCli/applet/remove-record.js +27 -0
  75. package/extend/bajoCli/applet/update-record.js +44 -0
  76. package/extend/bajoCli/applet.js +0 -0
  77. package/extend/dobo/driver/memory.js +170 -0
  78. package/extend/dobo/feature/created-at.js +10 -8
  79. package/extend/dobo/feature/dt.js +0 -0
  80. package/extend/dobo/feature/immutable.js +30 -0
  81. package/extend/dobo/feature/int-id.js +0 -0
  82. package/extend/dobo/feature/removed-at.js +35 -57
  83. package/extend/dobo/feature/updated-at.js +14 -12
  84. package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +5 -9
  85. package/extend/waibuStatic/virtual.json +0 -0
  86. package/index.js +420 -337
  87. package/lib/collect-connections.js +60 -21
  88. package/lib/collect-drivers.js +29 -35
  89. package/lib/collect-features.js +40 -0
  90. package/lib/collect-models.js +319 -0
  91. package/lib/factory/action.js +161 -0
  92. package/lib/factory/connection.js +62 -0
  93. package/lib/factory/driver.js +358 -0
  94. package/lib/factory/feature.js +33 -0
  95. package/lib/factory/model/_util.js +402 -0
  96. package/lib/factory/model/build.js +15 -0
  97. package/lib/factory/model/clear-record.js +17 -0
  98. package/lib/factory/model/count-record.js +17 -0
  99. package/lib/factory/model/create-aggregate.js +17 -0
  100. package/lib/factory/model/create-attachment.js +29 -0
  101. package/lib/factory/model/create-histogram.js +17 -0
  102. package/lib/factory/model/create-record.js +35 -0
  103. package/lib/factory/model/drop.js +15 -0
  104. package/lib/factory/model/exists.js +21 -0
  105. package/lib/factory/model/find-all-record.js +71 -0
  106. package/lib/factory/model/find-attachment.js +29 -0
  107. package/lib/factory/model/find-one-record.js +19 -0
  108. package/lib/factory/model/find-record.js +103 -0
  109. package/lib/factory/model/get-attachment.js +15 -0
  110. package/lib/factory/model/get-record.js +79 -0
  111. package/lib/factory/model/list-attachment.js +37 -0
  112. package/lib/{add-fixtures.js → factory/model/load-fixtures.js} +69 -67
  113. package/lib/factory/model/remove-attachment.js +15 -0
  114. package/lib/factory/model/remove-record.js +59 -0
  115. package/lib/factory/model/sanitize-body.js +62 -0
  116. package/lib/factory/model/sanitize-id.js +7 -0
  117. package/lib/factory/model/sanitize-record.js +26 -0
  118. package/lib/factory/model/update-attachment.js +9 -0
  119. package/lib/factory/model/update-record.js +81 -0
  120. package/lib/factory/model/upsert-record.js +95 -0
  121. package/lib/factory/model/validate.js +232 -0
  122. package/lib/factory/model.js +150 -0
  123. package/lib/index.js +3 -0
  124. package/package.json +45 -36
  125. package/wiki/APPLETS.md +57 -0
  126. package/wiki/CHANGES.md +46 -0
  127. package/wiki/CONFIG.md +25 -0
  128. package/wiki/CONTRIBUTING.md +5 -0
  129. package/wiki/DEV-GUIDE.md +1 -0
  130. package/wiki/ECOSYSTEM.md +20 -0
  131. package/wiki/GETTING-STARTED.md +166 -0
  132. package/{docs/query-language.md → wiki/QUERY-LANGUAGE.md} +0 -0
  133. package/wiki/USER-GUIDE.md +1 -0
  134. package/extend/bajoCli/applet/model-clear.js +0 -11
  135. package/extend/bajoCli/applet/model-rebuild.js +0 -101
  136. package/extend/bajoCli/applet/record-create.js +0 -41
  137. package/extend/bajoCli/applet/record-find.js +0 -27
  138. package/extend/bajoCli/applet/record-get.js +0 -24
  139. package/extend/bajoCli/applet/record-remove.js +0 -24
  140. package/extend/bajoCli/applet/record-update.js +0 -47
  141. package/extend/bajoCli/applet/schema.js +0 -22
  142. package/extend/bajoCli/applet/stat-count.js +0 -24
  143. package/lib/build-bulk-action.js +0 -12
  144. package/lib/check-unique.js +0 -39
  145. package/lib/collect-feature.js +0 -25
  146. package/lib/collect-schemas.js +0 -83
  147. package/lib/exec-feature-hook.js +0 -13
  148. package/lib/exec-validation.js +0 -21
  149. package/lib/generic-prop-sanitizer.js +0 -31
  150. package/lib/handle-attachment-upload.js +0 -16
  151. package/lib/mem-db/conn-sanitizer.js +0 -8
  152. package/lib/mem-db/instantiate.js +0 -41
  153. package/lib/mem-db/method/model/clear.js +0 -6
  154. package/lib/mem-db/method/model/create.js +0 -5
  155. package/lib/mem-db/method/model/drop.js +0 -5
  156. package/lib/mem-db/method/model/exists.js +0 -5
  157. package/lib/mem-db/method/record/create.js +0 -12
  158. package/lib/mem-db/method/record/find.js +0 -20
  159. package/lib/mem-db/method/record/get.js +0 -9
  160. package/lib/mem-db/method/record/remove.js +0 -13
  161. package/lib/mem-db/method/record/update.js +0 -15
  162. package/lib/mem-db/method/stat/count.js +0 -11
  163. package/lib/mem-db/start.js +0 -25
  164. package/lib/merge-attachment-info.js +0 -16
  165. package/lib/multi-rel-rows.js +0 -42
  166. package/lib/resolve-method.js +0 -16
  167. package/lib/sanitize-schema.js +0 -197
  168. package/lib/single-rel-rows.js +0 -38
  169. package/method/attachment/copy-uploaded.js +0 -34
  170. package/method/attachment/create.js +0 -29
  171. package/method/attachment/find.js +0 -27
  172. package/method/attachment/get-path.js +0 -12
  173. package/method/attachment/get.js +0 -12
  174. package/method/attachment/pre-check.js +0 -9
  175. package/method/attachment/remove.js +0 -11
  176. package/method/attachment/update.js +0 -7
  177. package/method/bulk/create.js +0 -46
  178. package/method/model/clear.js +0 -22
  179. package/method/model/create.js +0 -19
  180. package/method/model/drop.js +0 -19
  181. package/method/model/exists.js +0 -24
  182. package/method/record/clear.js +0 -24
  183. package/method/record/count.js +0 -44
  184. package/method/record/create.js +0 -71
  185. package/method/record/find-all.js +0 -25
  186. package/method/record/find-one.js +0 -56
  187. package/method/record/find.js +0 -52
  188. package/method/record/get.js +0 -47
  189. package/method/record/remove.js +0 -41
  190. package/method/record/update.js +0 -63
  191. package/method/record/upsert.js +0 -35
  192. package/method/sanitize/body.js +0 -70
  193. package/method/sanitize/date.js +0 -14
  194. package/method/sanitize/id.js +0 -7
  195. package/method/stat/aggregate.js +0 -23
  196. package/method/stat/histogram.js +0 -26
  197. package/method/validate.js +0 -157
@@ -0,0 +1,29 @@
1
+ import { mergeAttachmentInfo } from './_util.js'
2
+ const action = 'findAttachment'
3
+
4
+ async function findAttachment (...args) {
5
+ if (!this.attachment) return
6
+ if (args.length === 0) return this.action(action, ...args)
7
+ const [id, opts = {}] = args
8
+ const { fastGlob, fs } = this.app.lib
9
+ const { getPluginDataDir } = this.app.bajo
10
+ const dir = `${getPluginDataDir(this.ns)}/attachment/${this.name}/${id}`
11
+ if (!fs.existsSync(dir)) return []
12
+ const files = await fastGlob(`${dir}/**/*`)
13
+ const { fullPath, stats, mimeType } = opts
14
+ const recs = []
15
+ for (const f of files) {
16
+ const item = f.replace(dir, '')
17
+ let [, fieldName, file] = item.split('/')
18
+ if (!file) {
19
+ file = fieldName
20
+ fieldName = null
21
+ }
22
+ const rec = { fieldName, file }
23
+ await mergeAttachmentInfo.call(this, rec, f, { mimeType, fullPath, stats })
24
+ recs.push(rec)
25
+ }
26
+ return recs
27
+ }
28
+
29
+ export default findAttachment
@@ -0,0 +1,19 @@
1
+ const action = 'findOneRecord'
2
+
3
+ async function findOneRecord (...args) {
4
+ if (args.length === 0) return this.action(action, ...args)
5
+ const [params = {}, opts = {}] = args
6
+ const { cloneDeep } = this.app.lib._
7
+ const { dataOnly = true } = opts
8
+ if (dataOnly) opts.count = false
9
+ const nFilter = cloneDeep(params)
10
+ const nOptions = cloneDeep(opts)
11
+ nOptions.count = false
12
+ nOptions.dataOnly = false
13
+ nFilter.limit = 1
14
+ const result = await this.findRecord(nFilter, nOptions)
15
+ const data = result.data[0]
16
+ return dataOnly ? data : { data }
17
+ }
18
+
19
+ export default findOneRecord
@@ -0,0 +1,103 @@
1
+ import { getFilterAndOptions, execHook, execModelHook, getMultiRefs } from './_util.js'
2
+ const action = 'findRecord'
3
+
4
+ /**
5
+ * @typedef {Object} TRecordFilter
6
+ * @see Dobo#recordFind
7
+ * @see Dobo#recordFindOne
8
+ * @see Dobo#recordFindAll
9
+ * @property {(string|Object)} [query={}] - Query definition. See {@tutorial query-language} for more
10
+ * @property {number} limit - Max number of records per page
11
+ * @property {number} page - Which page is the returned records currently at
12
+ * @property {number} skip - Records to skip
13
+ * @property {TRecordSort} sort - Sort order info
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} TRecordFindResult
18
+ * @see Dobo#recordFind
19
+ * @see Dobo#recordFindAll
20
+ * @see Dobo#recordGet
21
+ * @property {Array.<Object>} data - Array of returned records
22
+ * @property {boolean} success - Whether operation is successfull or failed
23
+ * @property {number} page - Which page is the returned records currently at
24
+ * @property {number} limit - Max number of records per page
25
+ * @property {number} count - Total number of records returned
26
+ * @property {number} pages - Total number of pages returned
27
+ */
28
+
29
+ /**
30
+ * @typedef {Object} TRecordFindOptions
31
+ * @see Dobo#recordFind
32
+ * @see Dobo#recordFindOne
33
+ * @see Dobo#recordFindAll
34
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns array of records. Otherwise {@link TFindRecordResult}
35
+ * @property {boolean} [count=false] - If ```true``` and ```dataOnly``` is also ```true```, the total number of records found will be returned
36
+ * @property {boolean} [noCache=true] - If ```true``` (default), result set won't be cached. This will overwrite model's ```cacheable``` property. Only applicable if {@link https://github.com/ardhi/bajo-cache|bajo-cache} is loaded
37
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
38
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
39
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
40
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
41
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
42
+ */
43
+
44
+ /**
45
+ * Find records by model's name and given filter
46
+ *
47
+ * Example: find records from model **CdbCountry** where its id is 'ID' or 'MY',
48
+ * sorted by ```name``` in ascending order and return only its ```id```, ```name``` and ```iso3```
49
+ * ```javascript
50
+ * const { recordFind } = this.app.dobo
51
+ * const query = { id: { $in: ['ID', 'MY'] } }
52
+ * const sort = { name: 1 }
53
+ * const fields = ['id', 'name', 'iso3']
54
+ * const result = await recordFind('CdbCountry', { query, sort }, { fields })
55
+ * ```
56
+ *
57
+ * @method
58
+ * @memberof Dobo
59
+ * @async
60
+ * @instance
61
+ * @name recordFind
62
+ * @param {string} name - Model's name
63
+ * @param {Object} [filter={}] - Filter object
64
+ * @param {TRecordFindOptions} [options={}]
65
+ * @returns {(TRecordFindResult|Array.<Object>)} Return ```array``` of records if ```options.dataOnly``` is set. {@link TRecordFindResult} otherwise
66
+ */
67
+ async function findRecord (...args) {
68
+ if (args.length === 0) return this.action(action, ...args)
69
+ const [params = {}, opts = {}] = args
70
+ const { isSet } = this.app.lib.aneka
71
+ const { runHook } = this.app.bajo
72
+ const { dataOnly = true } = opts
73
+ const { filter, options } = await getFilterAndOptions.call(this, params, opts, action)
74
+ if (dataOnly) options.count = false
75
+ let { noResultSanitizer, noCache } = options
76
+ if (!this.cacheable || !options.record) noCache = true
77
+ if (!noCache) {
78
+ try {
79
+ await runHook('cache:getByFilter', this, filter)
80
+ } catch (err) {
81
+ if (err.code === 'CACHED_RESULT') {
82
+ const result = err.payload
83
+ result.cache = true
84
+ return dataOnly ? result.data : result
85
+ }
86
+ }
87
+ }
88
+ await execHook.call(this, 'beforeFindRecord', filter, options)
89
+ await execModelHook.call(this, 'beforeFindRecord', filter, options)
90
+ const result = options.record ?? (await this.driver._findRecord(this, filter, options)) ?? {}
91
+ if (!noResultSanitizer) {
92
+ for (const idx in result.data) {
93
+ result.data[idx] = await this.sanitizeRecord(result.data[idx], options)
94
+ }
95
+ }
96
+ if (isSet(options.refs)) await getMultiRefs.call(this, { records: result.data, options })
97
+ await execModelHook.call(this, 'afterFindRecord', filter, result, options)
98
+ await execHook.call(this, 'afterFindRecord', filter, result, options)
99
+ if (!noCache) await runHook('cache:setByFilter', this, filter, result)
100
+ return dataOnly ? result.data : result
101
+ }
102
+
103
+ export default findRecord
@@ -0,0 +1,15 @@
1
+ const action = 'getAttachment'
2
+
3
+ async function getAttachment (...args) {
4
+ if (!this.attachment) return
5
+ if (args.length === 0) return this.action(action, ...args)
6
+ let [id, fieldName, file, opts = {}] = args
7
+ const { find } = this.app.lib._
8
+ const all = await this.findAttachment(id, opts)
9
+ if (fieldName === 'null') fieldName = null
10
+ const data = find(all, { fieldName, file })
11
+ if (!data) throw this.error('notFound', { statusCode: 404 })
12
+ return data
13
+ }
14
+
15
+ export default getAttachment
@@ -0,0 +1,79 @@
1
+ import { getFilterAndOptions, execHook, execModelHook, getSingleRef } from './_util.js'
2
+ const action = 'getRecord'
3
+
4
+ /**
5
+ * @typedef {Object} TRecordGetResult
6
+ * @see Dobo#recordGet
7
+ * @see Dobo#recordFindOne
8
+ * @property {Object} data - Returned record
9
+ * @property {boolean} success - Whether operation is successfull or failed
10
+ */
11
+
12
+ /**
13
+ * @typedef {Object} TRecordgetFilterAndOptions
14
+ * @see Dobo#recordGet
15
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns array of records. Otherwise {@link TFindRecordResult}
16
+ * @property {boolean} [count=false] - If ```true``` and ```dataOnly``` is also ```true```, the total number of records found will be returned
17
+ * @property {boolean} [noCache=true] - If ```true``` (default), result set won't be cached. This will overwrite model's ```cacheable``` property. Only applicable if {@link https://github.com/ardhi/bajo-cache|bajo-cache} is loaded
18
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
19
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
20
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
21
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
22
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
23
+ */
24
+
25
+ /**
26
+ * Get record by model's name and record ID
27
+ *
28
+ * Example:
29
+ * ```javascript
30
+ * const { recordGet } = this.app.dobo
31
+ * const fields = ['id', 'name', 'iso3']
32
+ * const result = await recordGet('CdbCountry', 'ID', { fields })
33
+ * ```
34
+ *
35
+ * @method
36
+ * @memberof Dobo
37
+ * @async
38
+ * @instance
39
+ * @name recordGet
40
+ * @param {string} name - Model's name
41
+ * @param {(string|number)} - Record's ID
42
+ * @param {TRecordgetFilterAndOptions} [options={}]
43
+ * @returns {(TRecordGetResult|Object)} Return record's ```object``` if ```options.dataOnly``` is set. {@link TRecordGetResult} otherwise
44
+ */
45
+ async function getRecord (...args) {
46
+ if (args.length === 0) return this.action(action, ...args)
47
+ let [id, opts = {}] = args
48
+ const { isEmpty } = this.app.lib._
49
+ const { isSet } = this.app.lib.aneka
50
+ const { runHook } = this.app.bajo
51
+ const { dataOnly = true } = opts
52
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
53
+ let { noResultSanitizer, noCache } = options
54
+ if (!this.cacheable || !options.record) noCache = true
55
+ id = this.sanitizeId(id)
56
+ await execHook.call(this, 'beforeGetRecord', id, options)
57
+ await execModelHook.call(this, 'beforeGetRecord', id, options)
58
+ if (!noCache) {
59
+ try {
60
+ await runHook('cache:getById', this, id)
61
+ } catch (err) {
62
+ if (err.code === 'CACHED_RESULT') {
63
+ const result = err.payload
64
+ result.cache = true
65
+ return dataOnly ? result.data : result
66
+ }
67
+ }
68
+ }
69
+ const result = options.record ?? (await this.driver._getRecord(this, id, options)) ?? {}
70
+ if (isEmpty(result.data) && !options.throwNotFound) return dataOnly ? undefined : { data: undefined }
71
+ if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
72
+ if (isSet(options.refs)) await getSingleRef.call(this, { record: result.data, options })
73
+ await execModelHook.call(this, 'afterGetRecord', id, result.data, options)
74
+ await execHook.call(this, 'afterGetRecord', id, result, options)
75
+ if (!noCache) await runHook('cache:setById', this, id, result)
76
+ return dataOnly ? result.data : result
77
+ }
78
+
79
+ export default getRecord
@@ -0,0 +1,37 @@
1
+ import path from 'path'
2
+ const action = 'listAttachment'
3
+
4
+ async function listAttachment (...args) {
5
+ if (!this.attachment) return
6
+ if (args.length === 0) return this.action(action, ...args)
7
+ const [params = {}, opts = {}] = args
8
+ const { map, kebabCase } = this.app.lib._
9
+ const { importPkg, getPluginDataDir } = this.app.bajo
10
+ const mime = await importPkg('waibu:mime')
11
+ const { fastGlob } = this.app.lib
12
+
13
+ const { id = '*', fieldName = '*', file = '*' } = params
14
+ const { uriEncoded = true } = opts
15
+ const root = `${getPluginDataDir('dobo')}/attachment`
16
+ let pattern = `${root}/${this.name}/${id}/${fieldName}/${file}`
17
+ if (uriEncoded) pattern = pattern.split('/').map(p => decodeURI(p)).join('/')
18
+ return map(await fastGlob(pattern), f => {
19
+ const mimeType = mime.getType(path.extname(f)) ?? ''
20
+ const fullPath = f.replace(root, '')
21
+ const row = {
22
+ file: f,
23
+ fileName: path.basename(fullPath),
24
+ fullPath,
25
+ mimeType,
26
+ params: { model: this.name, id, fieldName, file }
27
+ }
28
+ if (this.app.waibuMpa) {
29
+ const { routePath } = this.app.waibu
30
+ const [, _model, _id, _fieldName, _file] = fullPath.split('/')
31
+ row.url = routePath(`dobo:/attachment/${kebabCase(_model)}/${_id}/${_fieldName}/${_file}`)
32
+ }
33
+ return row
34
+ })
35
+ }
36
+
37
+ export default listAttachment
@@ -1,67 +1,69 @@
1
- import path from 'path'
2
-
3
- async function addFixture (name, { spinner } = {}) {
4
- const { resolvePath, readConfig, eachPlugins, getPluginDataDir } = this.app.bajo
5
- const { isEmpty, isArray, isString } = this.lib._
6
- const { fs } = this.lib
7
- const { schema, connection } = this.getInfo(name)
8
- if (connection.proxy) {
9
- this.log.warn('proxiedConnBound%s', schema.name)
10
- return
11
- }
12
- const result = { success: 0, failed: 0 }
13
- const base = path.basename(schema.file, path.extname(schema.file))
14
- // original
15
- const pattern = resolvePath(`${path.dirname(schema.file)}/../fixture/${base}.*`)
16
- let items = await readConfig(pattern, { ns: schema.ns, ignoreError: true })
17
- if (isEmpty(items)) items = []
18
- // override
19
- const overrides = await readConfig(`${this.app.main.dir.pkg}/extend/dobo/override/${schema.ns}/fixture/${base}.*`, { ns: this.name, ignoreError: true })
20
-
21
- if (isArray(overrides) && !isEmpty(overrides)) items = overrides
22
- // extend
23
- await eachPlugins(async function ({ dir }) {
24
- const { name: ns } = this
25
- const extend = await readConfig(`${dir}/extend/dobo/extend/${schema.ns}/fixture/${base}.*`, { ns, ignoreError: true })
26
- if (isArray(extend) && !isEmpty(extend)) items.push(...extend)
27
- })
28
- if (isEmpty(items)) return result
29
- const opts = { noHook: true, noCache: true }
30
- for (const item of items) {
31
- try {
32
- for (const k in item) {
33
- const v = item[k]
34
- if (typeof v === 'string' && v.slice(0, 2) === '?:') {
35
- let [, model, field, ...query] = v.split(':')
36
- if (!field) field = 'id'
37
- const recs = await this.recordFind(model, { query: query.join(':') }, opts)
38
- item[k] = (recs[0] ?? {})[field]
39
- }
40
- if (v === null) item[k] = undefined
41
- }
42
- const resp = await this.recordCreate(schema.name, item, { force: true })
43
- if (isArray(item._attachments) && item._attachments.length > 0) {
44
- for (let att of item._attachments) {
45
- if (isString(att)) att = { field: 'file', file: att }
46
- const fname = path.basename(att.file)
47
- if (fs.existsSync(att.file)) {
48
- const dest = `${getPluginDataDir(schema.ns)}/${resp.id}/${att.field}/${fname}`
49
- try {
50
- fs.copySync(att.file, dest)
51
- } catch (err) {}
52
- }
53
- }
54
- }
55
- result.success++
56
- if (spinner) spinner.setText('recordsAdded%s%d%d', schema.name, result.success, items.length)
57
- } catch (err) {
58
- console.log(err)
59
- err.model = schema.name
60
- if (this.app.bajo.applet) this.print.fail(this.validationErrorMessage(err))
61
- result.failed++
62
- }
63
- }
64
- return result
65
- }
66
-
67
- export default addFixture
1
+ import path from 'path'
2
+
3
+ async function loadFixtures ({ spinner } = {}) {
4
+ const { readConfig, eachPlugins, getPluginDataDir } = this.app.bajo
5
+ const { resolvePath } = this.app.lib.aneka
6
+ const { isEmpty, isArray, isString } = this.app.lib._
7
+ const { fs } = this.app.lib
8
+ if (this.connection.proxy) {
9
+ this.log.warn('proxiedConnBound%s', this.name)
10
+ return
11
+ }
12
+ const result = { success: 0, failed: 0 }
13
+ const base = path.basename(this.file, path.extname(this.file))
14
+ // original
15
+ const pattern = resolvePath(`${path.dirname(this.file)}/../fixture/${base}.*`)
16
+ let items = await readConfig(pattern, { ns: this.plugin.ns, ignoreError: true })
17
+ if (isEmpty(items)) items = []
18
+ // override
19
+ const overrides = await readConfig(`${this.app.main.dir.pkg}/extend/dobo/override/${this.plugin.ns}/fixture/${base}.*`, { ns: this.plugin.ns, ignoreError: true })
20
+
21
+ if (isArray(overrides) && !isEmpty(overrides)) items = overrides
22
+ // extend
23
+ const me = this
24
+ await eachPlugins(async function ({ dir }) {
25
+ const { ns } = this
26
+ const extend = await readConfig(`${dir}/extend/dobo/extend/${me.plugin.ns}/fixture/${base}.*`, { ns, ignoreError: true })
27
+ if (isArray(extend) && !isEmpty(extend)) items.push(...extend)
28
+ })
29
+ if (isEmpty(items)) return result
30
+ const opts = { noHook: true, noCache: true }
31
+ for (const item of items) {
32
+ try {
33
+ for (const k in item) {
34
+ const v = item[k]
35
+ if (typeof v === 'string' && v.slice(0, 2) === '?:') {
36
+ let [, model, field, ...query] = v.split(':')
37
+ if (!field) field = 'id'
38
+ const ref = this.app.dobo.getModel(model)
39
+ const recs = await ref.findRecord({ query: query.join(':') }, opts)
40
+ item[k] = (recs[0] ?? {})[field]
41
+ }
42
+ if (v === null) item[k] = undefined
43
+ }
44
+ const resp = await this.createRecord(item, { force: true })
45
+ if (isArray(item._attachments) && item._attachments.length > 0) {
46
+ for (let att of item._attachments) {
47
+ if (isString(att)) att = { field: 'file', file: att }
48
+ const fname = path.basename(att.file)
49
+ if (fs.existsSync(att.file)) {
50
+ const dest = `${getPluginDataDir(this.plugin.ns)}/${resp.id}/${att.field}/${fname}`
51
+ try {
52
+ fs.copySync(att.file, dest)
53
+ } catch (err) {}
54
+ }
55
+ }
56
+ }
57
+ result.success++
58
+ if (spinner) spinner.setText('recordsAdded%s%d%d', this.name, result.success, items.length)
59
+ } catch (err) {
60
+ console.log(err)
61
+ err.model = this.name
62
+ if (this.app.applet) this.plugin.print.fail(this.app.dobo.validationErrorMessage(err))
63
+ result.failed++
64
+ }
65
+ }
66
+ return result
67
+ }
68
+
69
+ export default loadFixtures
@@ -0,0 +1,15 @@
1
+ import { getAttachmentPath } from './_util.js'
2
+ const action = 'removeAttachment'
3
+
4
+ async function removeAttachment (...args) {
5
+ if (!this.attachment) return
6
+ if (args.length === 0) return this.action(action, ...args)
7
+ const [id, fieldName, file, opts = {}] = args
8
+ const { fs } = this.app.lib
9
+ const path = await getAttachmentPath.call(this, id, fieldName, file)
10
+ const { req } = opts
11
+ await fs.remove(path)
12
+ if (!opts.noFlash && req && req.flash) req.flash('notify', req.t('attachmentRemoved'))
13
+ }
14
+
15
+ export default removeAttachment
@@ -0,0 +1,59 @@
1
+ import { getFilterAndOptions, execHook, execModelHook, getSingleRef, handleReq } from './_util.js'
2
+ const action = 'removeRecord'
3
+
4
+ /**
5
+ * @typedef {Object} TRecordRemoveOptions
6
+ * @see Dobo#recordRemove
7
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns deleted record. Otherwise {@link TRecordRemoveResult}
8
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
9
+ * @property {boolean} [noModelHook=false] - If ```true```, no model's feature hook will be executed
10
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
11
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
12
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
13
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
14
+ */
15
+
16
+ /**
17
+ * Remove existing record by it's ID. All attachments bound to this record will also be removed forever.
18
+ *
19
+ * Example:
20
+ * ```javascript
21
+ * const { recordRemove } = this.app.dobo
22
+ * const result = await recordRemove('CdbCountry', 'ID')
23
+ * ```
24
+ *
25
+ * @method
26
+ * @memberof Model
27
+ * @async
28
+ * @instance
29
+ * @name removeRecord
30
+ * @param {(string|number)} id - Record's ID
31
+ * @param {TRecordRemoveOptions} [options={}]
32
+ * @returns {(TRecordRemoveResult|Object)} Return the removed record if ```options.dataOnly``` is set. {@link TRecordRemoveResult} otherwise
33
+ */
34
+ async function removeRecord (...args) {
35
+ if (args.length === 0) return this.action(action, ...args)
36
+ let [id, opts = {}] = args
37
+ const { isSet } = this.app.lib.aneka
38
+ const { runHook } = this.app.bajo
39
+ const { dataOnly = true } = opts
40
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
41
+ const { noResult, noResultSanitizer } = options
42
+ id = this.sanitizeId(id)
43
+ await execHook.call(this, 'beforeRemoveRecord', id, options)
44
+ await execModelHook.call(this, 'beforeRemoveRecord', id, options)
45
+ const result = options.record ?? (await this.driver._removeRecord(this, id, options)) ?? {}
46
+ if (noResult) {
47
+ await runHook('cache:clear', this, 'remove', id)
48
+ return
49
+ }
50
+ if (!noResultSanitizer) result.oldData = await this.sanitizeRecord(result.oldData, options)
51
+ if (isSet(options.refs)) await getSingleRef.call(this, { record: result.data, options })
52
+ await handleReq.call(this, result.oldData.id, 'removed', options)
53
+ await execModelHook.call(this, 'afterRemoveRecord', id, result, options)
54
+ await execHook.call(this, 'afterRemoveRecord', id, result, options)
55
+ await runHook('cache:clear', this, 'remove', id, result)
56
+ return dataOnly ? result.oldData : result
57
+ }
58
+
59
+ export default removeRecord
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Sanitize payload body against its model
3
+ *
4
+ * @method
5
+ * @memberof Dobo
6
+ * @async
7
+ * @param {Object} [options={}]
8
+ * @param {Object} [options.body={}]
9
+ * @param {Object} [options.model={}]
10
+ * @param {boolean} [options.partial=false]
11
+ * @param {boolean} [options.strict=false]
12
+ * @param {Array} [options.extFields=[]]
13
+ * @returns {Object}
14
+ */
15
+ async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDefault, truncateString, onlyTypes = [], action } = {}) {
16
+ const { isSet } = this.app.lib.aneka
17
+ const { callHandler } = this.app.bajo
18
+ const { omit, has, isString, isNaN } = this.app.lib._
19
+ const { sanitizeBoolean, sanitizeDate, sanitizeFloat, sanitizeTimestamp, sanitizeInt, sanitizeObject, sanitizeString } = this.app.dobo
20
+ const result = {}
21
+
22
+ const sanitize = (name, type) => {
23
+ if (onlyTypes.length > 0 && !onlyTypes.includes(type)) return
24
+ if (['object', 'array'].includes(type)) result[name] = sanitizeObject(result[name])
25
+ else if (type === 'boolean') result[name] = sanitizeBoolean(result[name])
26
+ else if (['float', 'double'].includes(type)) result[name] = sanitizeFloat(result[name], strict)
27
+ else if (['integer', 'smallint'].includes(type)) result[name] = sanitizeInt(result[name], strict)
28
+ else if (type === 'timestamp') result[name] = sanitizeTimestamp(result[name])
29
+ else if (['string', 'text'].includes(type)) result[name] = sanitizeString(result[name], strict)
30
+ else {
31
+ for (const t of ['datetime|native', 'date|YYYY-MM-DD', 'time|HH:mm:ss']) {
32
+ const [ptype, input] = t.split('|')
33
+ if (ptype === type) result[name] = sanitizeDate(result[name], { input })
34
+ }
35
+ }
36
+ if (!strict && isNaN(result[name])) result[name] = null
37
+ if (['updateRecord', 'upsertRecord'].includes(action) && type === 'string' && result[name] === '') result[name] = null
38
+ }
39
+
40
+ const omitted = []
41
+ for (const prop of [...this.properties, ...extFields]) {
42
+ if (partial && !has(body, prop.name)) continue
43
+ result[prop.name] = body[prop.name]
44
+ if (body[prop.name] === null) continue
45
+ if (isSet(body[prop.name])) sanitize(prop.name, prop.type)
46
+ else {
47
+ if (isSet(prop.default) && !noDefault) {
48
+ result[prop.name] = prop.default
49
+ if (isString(prop.default) && prop.default.startsWith('handler:')) {
50
+ const [, ...args] = prop.default.split(':')
51
+ if (args.length > 0) result[prop.name] = await callHandler(args.join(':'))
52
+ } else sanitize(prop.name, prop.type)
53
+ }
54
+ }
55
+ if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
56
+ if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
57
+ if (body[prop.name] === undefined) omitted.push(prop.name)
58
+ }
59
+ return omit(result, omitted)
60
+ }
61
+
62
+ export default sanitizeBody
@@ -0,0 +1,7 @@
1
+ function sanitizeId (id) {
2
+ const prop = this.properties.find(p => p.name === 'id')
3
+ if (prop.type === 'integer') id = parseInt(id)
4
+ return id
5
+ }
6
+
7
+ export default sanitizeId
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Sanitize record to conform with the model's definition
3
+ *
4
+ * @method
5
+ * @async
6
+ * @param {Object} [record] - Record object
7
+ * @param {Array} [options.fields] - Array of field names to be picked
8
+ * @param {Object} [options.hidden=[]] - Additional fields to be hidden in addition the one defined in model
9
+ * @param {boolean} [options.forceNoHidden] - Force ALL fields to be picked, thus ignoring hidden fields
10
+ * @returns {Object}
11
+ */
12
+ async function sanitizeRecord (record = {}, opts = {}) {
13
+ const { fields = [], hidden = [], forceNoHidden } = opts
14
+ const { isEmpty, map, without } = this.app.lib._
15
+ const { fillObject } = this.app.lib.aneka
16
+ const allHidden = forceNoHidden ? [] : without([...this.hidden, ...hidden], 'id')
17
+ let newFields = [...fields]
18
+ if (isEmpty(newFields)) newFields = map(this.properties, prop => prop.name)
19
+ if (!newFields.includes('id')) newFields.unshift('id')
20
+ newFields = without(newFields, ...allHidden)
21
+ const newRecord = await this.sanitizeBody({ body: fillObject(record, newFields, null), noDefault: true })
22
+ if (record._ref) newRecord._ref = record._ref
23
+ return newRecord
24
+ }
25
+
26
+ export default sanitizeRecord
@@ -0,0 +1,9 @@
1
+ const action = 'updateAttachment'
2
+
3
+ async function updateAttachment (...args) {
4
+ if (args.length === 0) return this.action(action, ...args)
5
+ const [id, opts = {}] = args
6
+ return this.createAttachment(this, id, opts)
7
+ }
8
+
9
+ export default updateAttachment