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,81 @@
1
+ import { getFilterAndOptions, execHook, execValidation, execModelHook, getSingleRef, handleReq } from './_util.js'
2
+ import { onlyTypes } from './create-record.js'
3
+ const action = 'updateRecord'
4
+
5
+ /**
6
+ * @typedef {Object} TRecordUpdateOptions
7
+ * @see Model#updateRecord
8
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordUpdateResult}
9
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
10
+ * @property {boolean} [noModelHook=false] - If ```true```, no model's hook will be executed
11
+ * @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
12
+ * @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
13
+ * @property {boolean} [noBodySanitizer=false] - If ```true```, accept data payload as is without sanitization
14
+ * @property {boolean} [noRecordSanitizer=false] - If ```true```, accept result payload as is without sanitization
15
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
16
+ * @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its model's ```maxLength```
17
+ * @property {boolean} [partial=true] - If ```true``` (default), only updated values are saved. Otherwise replace all existing values with given payload
18
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
19
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
20
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
21
+ */
22
+
23
+ /**
24
+ * Update a record by it's ID and body payload
25
+ *
26
+ * Example:
27
+ * ```javascript
28
+ * const { recordUpdate } = this.app.dobo
29
+ * const { body } = {
30
+ * name: 'Republic of Indonesia',
31
+ * phoneCode: '+62'
32
+ * }
33
+ * const result = await recordUpdate('CdbCountry', 'ID', body)
34
+ * ```
35
+ *
36
+ * @method
37
+ * @memberof Dobo
38
+ * @async
39
+ * @instance
40
+ * @name recordUpdate
41
+ * @param {string} name - Model's name
42
+ * @param {(string|number)} id - Record's ID
43
+ * @param {Object} body - Body payload
44
+ * @param {TRecordUpdateOptions} [options={}]
45
+ * @returns {(TRecordUpdateResult|Object)} Returns updated record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} otherwise
46
+ */
47
+ async function updateRecord (...args) {
48
+ if (args.length === 0) return this.action(action, ...args)
49
+ let [id, body = {}, opts = {}] = args
50
+ const { isSet } = this.app.lib.aneka
51
+ const { runHook } = this.app.bajo
52
+ const { cloneDeep, get } = this.app.lib._
53
+ const { dataOnly = true } = opts
54
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
55
+ options.partial = options.partial ?? true
56
+ const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation, partial } = options
57
+ const extFields = get(options, 'validation.extFields', [])
58
+ id = this.sanitizeId(id)
59
+ const input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString, partial, onlyTypes })
60
+ delete input.id
61
+ await execHook.call(this, 'beforeUpdateRecord', id, input, options)
62
+ await execModelHook.call(this, 'beforeUpdateRecord', id, input, options)
63
+ if (!noValidation) await execValidation.call(this, input, options)
64
+ const result = options.record ?? (await this.driver._updateRecord(this, id, input, options)) ?? {}
65
+ if (noResult) {
66
+ await runHook('cache:clear', this, 'update', id, body)
67
+ return
68
+ }
69
+ if (!noResultSanitizer) {
70
+ result.data = await this.sanitizeRecord(result.data, options)
71
+ result.oldData = await this.sanitizeRecord(result.oldData, options)
72
+ }
73
+ if (isSet(options.refs)) await getSingleRef.call(this, { record: result.data, options })
74
+ await handleReq.call(this, result.data.id, 'updated', options)
75
+ await execModelHook.call(this, 'afterUpdateRecord', id, input, result, options)
76
+ await execHook.call(this, 'afterUpdateRecord', id, input, result, options)
77
+ await runHook('cache:clear', this, 'update', id, body, result)
78
+ return dataOnly ? result.data : result
79
+ }
80
+
81
+ export default updateRecord
@@ -0,0 +1,95 @@
1
+ import { getFilterAndOptions, execHook, execModelHook, execValidation, getSingleRef, handleReq } from './_util.js'
2
+ const action = 'upsertRecord'
3
+
4
+ async function native (body = {}, opts = {}) {
5
+ const { isSet } = this.app.lib.aneka
6
+ const { runHook } = this.app.bajo
7
+ const { cloneDeep, get } = this.app.lib._
8
+ const { dataOnly = true } = opts
9
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
10
+ const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation } = options
11
+ const extFields = get(options, 'validation.extFields', [])
12
+ let input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString })
13
+ if (!noValidation) input = await execValidation.call(this, input, options)
14
+ await execHook.call(this, 'beforeUpsertRecord', input, options)
15
+ await execModelHook.call(this, 'beforeUpsertRecord', input, options)
16
+ const result = options.record ?? (await this.driver._upsertRecord(this, input, options)) ?? {}
17
+ if (noResult) {
18
+ await runHook('cache:clear', this, 'upsert', body)
19
+ return
20
+ }
21
+ if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
22
+ if (isSet(options.refs)) await getSingleRef.call(this, { record: result.data, options })
23
+ await handleReq.call(this, result.data.id, 'upserted', options)
24
+ await execModelHook.call(this, 'afterUpsertRecord', input, result, options)
25
+ await execHook.call(this, 'afterUpsertRecord', input, result, options)
26
+ await runHook('cache:clear', this, 'upsert', body, result)
27
+ return dataOnly ? result.data : result
28
+ }
29
+
30
+ async function manual (body = {}, options = {}) {
31
+ const { isSet } = this.app.lib.aneka
32
+ if (isSet(body.id)) body.id = this.sanitizeId(body.id)
33
+ let old = false
34
+ if (isSet(body.id)) {
35
+ try {
36
+ old = await this.driver._getRecord(this, body.id, { noHook: true, noModelHook: true })
37
+ } catch (err) {}
38
+ }
39
+ if (old) return await this.updateRecord(old.data.id, body, options)
40
+ return await this.createRecord(body, options)
41
+ }
42
+
43
+ /**
44
+ * @typedef {Object} TRecordUpdateOptions
45
+ * @see Model#updateRecord
46
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordUpdateResult}
47
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
48
+ * @property {boolean} [noModelHook=false] - If ```true```, no model's hook will be executed
49
+ * @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
50
+ * @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
51
+ * @property {boolean} [noBodySanitizer=false] - If ```true```, accept data payload as is without sanitization
52
+ * @property {boolean} [noRecordSanitizer=false] - If ```true```, accept result payload as is without sanitization
53
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
54
+ * @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its model's ```maxLength```
55
+ * @property {boolean} [partial=true] - If ```true``` (default), only updated values are saved. Otherwise replace all existing values with given payload
56
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
57
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
58
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
59
+ */
60
+
61
+ /**
62
+ * Update a record by it's ID and body payload
63
+ *
64
+ * Example:
65
+ * ```javascript
66
+ * const { recordUpdate } = this.app.dobo
67
+ * const { body } = {
68
+ * name: 'Republic of Indonesia',
69
+ * phoneCode: '+62'
70
+ * }
71
+ * const result = await recordUpdate('CdbCountry', 'ID', body)
72
+ * ```
73
+ *
74
+ * @method
75
+ * @memberof Dobo
76
+ * @async
77
+ * @instance
78
+ * @name recordUpdate
79
+ * @param {string} name - Model's name
80
+ * @param {(string|number)} id - Record's ID
81
+ * @param {Object} body - Body payload
82
+ * @param {TRecordUpdateOptions} [options={}]
83
+ * @returns {(TRecordUpdateResult|Object)} Returns updated record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} otherwise
84
+ */
85
+ async function upsertRecord (...args) {
86
+ if (args.length === 0) return this.action(action, ...args)
87
+ const [body = {}, opts = {}] = args
88
+ if (this.driver.upsertRecord) {
89
+ const { options } = await getFilterAndOptions.call(this, null, opts, action)
90
+ return await native.call(this, body, options)
91
+ }
92
+ return await manual.call(this, body, opts)
93
+ }
94
+
95
+ export default upsertRecord
@@ -0,0 +1,232 @@
1
+ import joi from 'joi'
2
+
3
+ const excludedTypes = ['object', 'array']
4
+ const excludedNames = []
5
+
6
+ /**
7
+ * @typedef {string[]} TValidatorString
8
+ * @property {string} 0=alphanum
9
+ * @property {string} 1=base64
10
+ * @property {string} 2=case
11
+ * @property {string} 3=creditCard
12
+ * @property {string} 4=dataUri
13
+ * @property {string} 5=email
14
+ * @property {string} 6=guid
15
+ * @property {string} 7=uuid
16
+ * @property {string} 8=hex
17
+ * @property {string} 9=hostname
18
+ * @property {string} 10=insenstive
19
+ * @property {string} 11=ip
20
+ * @property {string} 12=isoDate
21
+ * @property {string} 13=isoDuration
22
+ * @property {string} 14=length
23
+ * @property {string} 15=lowercase
24
+ * @property {string} 16=max
25
+ * @property {string} 17=min
26
+ * @property {string} 18=normalize
27
+ * @property {string} 19=pattern
28
+ * @property {string} 20=regex
29
+ * @property {string} 21=replace
30
+ * @property {string} 22=token
31
+ * @property {string} 23=trim
32
+ * @property {string} 24=truncate
33
+ * @property {string} 25=upercase
34
+ * @property {string} 26=uri
35
+ */
36
+
37
+ /**
38
+ * @typedef {string[]} TValidatorNumber
39
+ * @property {string} 0=great
40
+ * @property {string} 1=less
41
+ * @property {string} 2=max
42
+ * @property {string} 3=min
43
+ * @property {string} 4=multiple
44
+ * @property {string} 5=negative
45
+ * @property {string} 6=port
46
+ * @property {string} 7=positive
47
+ * @property {string} 8=sign
48
+ * @property {string} 9=unsafe
49
+ */
50
+
51
+ /**
52
+ * @typedef {string[]} TValidatorBoolean
53
+ * @property {string} 0=falsy
54
+ * @property {string} 1=sensitive
55
+ * @property {string} 2=truthy
56
+ */
57
+
58
+ /**
59
+ * @typedef {string[]} TValidatorDate
60
+ * @property {string} 0=greater
61
+ * @property {string} 1=iso
62
+ * @property {string} 2=less
63
+ * @property {string} 2=max
64
+ * @property {string} 2=min
65
+ */
66
+
67
+ /**
68
+ * @typedef {string[]} TValidatorTimestamp
69
+ * @property {string} 0=timestamp
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} TValidator
74
+ * @property {TValidatorString} string
75
+ * @property {TValidatorNumber} number
76
+ * @property {TValidatorBoolean} boolean
77
+ * @property {TValidatorDate} date
78
+ * @property {TValidatorTimestamp} timestamp
79
+ */
80
+ const validator = {
81
+ string: ['alphanum', 'base64', 'case', 'creditCard', 'dataUri', 'domain', 'email', 'guid',
82
+ 'uuid', 'hex', 'hostname', 'insensitive', 'ip', 'isoDate', 'isoDuration', 'length', 'lowercase',
83
+ 'max', 'min', 'normalize', 'pattern', 'regex', 'replace', 'token', 'trim', 'truncate',
84
+ 'uppercase', 'uri'],
85
+ number: ['great', 'less', 'max', 'min', 'multiple', 'negative', 'port', 'positive',
86
+ 'sign', 'unsafe'],
87
+ boolean: ['falsy', 'sensitive', 'truthy'],
88
+ date: ['greater', 'iso', 'less', 'max', 'min'],
89
+ timestamp: ['timestamp']
90
+ }
91
+
92
+ function buildFromDbModel (opts = {}) {
93
+ const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
94
+ const { fields = [], rule = {}, extFields = [] } = opts
95
+ const obj = {}
96
+ const { propertyType: propType } = this.app.baseClass.Dobo
97
+ const refs = []
98
+
99
+ function getRuleKv (kvRule) {
100
+ let key
101
+ let value
102
+ let columns
103
+ if (isPlainObject(kvRule)) {
104
+ key = kvRule.rule
105
+ value = kvRule.params
106
+ columns = kvRule.fields
107
+ } else if (isString(kvRule)) {
108
+ [key, value, columns] = kvRule.split(':')
109
+ }
110
+ return { key, value, columns }
111
+ }
112
+
113
+ function applyFieldRules (prop, obj) {
114
+ const minMax = { min: false, max: false }
115
+ const rules = get(rule, prop.name, prop.rules ?? [])
116
+ if (!Array.isArray(rules)) return rules
117
+ for (const r of rules) {
118
+ const types = validator[propType[prop.type].validator]
119
+ const { key, value } = getRuleKv(r)
120
+ if (keys(minMax).includes(key)) minMax[key] = true
121
+ if (key === 'ref') {
122
+ refs.push(prop.name)
123
+ obj = joi.ref(value)
124
+ continue
125
+ }
126
+ if (!key || !types.includes(key)) continue
127
+ obj = obj[key](value)
128
+ }
129
+ if (refs.includes(prop.name)) return obj
130
+ if (['string', 'text'].includes(prop.type)) {
131
+ for (const k in minMax) {
132
+ if (minMax[k]) continue
133
+ if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
134
+ }
135
+ }
136
+ if (Array.isArray(prop.values)) obj = obj.valid(...prop.values)
137
+ if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
138
+ return obj
139
+ }
140
+
141
+ const props = [...this.properties, ...extFields]
142
+
143
+ for (const p of props) {
144
+ if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
145
+ if (opts.partial && fields.length > 0 && !fields.includes(p.name)) continue
146
+ let item
147
+ switch (p.type) {
148
+ case 'text':
149
+ case 'string': {
150
+ item = applyFieldRules(p, joi.string())
151
+ break
152
+ }
153
+ case 'smallint':
154
+ case 'integer':
155
+ item = applyFieldRules(p, joi.number().integer())
156
+ break
157
+ case 'float':
158
+ case 'double':
159
+ if (p.precision) item = applyFieldRules(p, joi.number().precision(p.precision))
160
+ else item = applyFieldRules(p, joi.number())
161
+ break
162
+ case 'time':
163
+ case 'date':
164
+ case 'datetime':
165
+ item = applyFieldRules(p, joi.date())
166
+ break
167
+ case 'timestamp':
168
+ item = applyFieldRules(p, joi.number().integer())
169
+ break
170
+ case 'boolean':
171
+ item = applyFieldRules(p, joi.boolean())
172
+ break
173
+ }
174
+ if (item) {
175
+ if (item.$_root && !p.required) obj[p.name] = item.allow(null, '')
176
+ else obj[p.name] = item
177
+ }
178
+ }
179
+ if (isEmpty(obj)) return false
180
+ for (const r of this.rules) {
181
+ for (const k of without(keys(obj), ...refs)) {
182
+ const prop = find(props, { name: k })
183
+ if (!prop) continue
184
+ const types = validator[propType[prop.type].validator]
185
+ const { key, value, columns = [] } = getRuleKv(r)
186
+ if (!types.includes(key)) continue
187
+ if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
188
+ }
189
+ }
190
+ const result = joi.object(obj)
191
+ for (const k of ['with', 'xor', 'without']) {
192
+ const item = get(this, `extRule.${k}`)
193
+ if (item) result[k](...item)
194
+ }
195
+ return result
196
+ }
197
+
198
+ /**
199
+ * Validate value against JOI model
200
+ *
201
+ * @method
202
+ * @memberof Dobo
203
+ * @async
204
+ * @param {Object} value - value to validate
205
+ * @param {Object} joiModel - JOI model
206
+ * @param {Object} [options={}] - Options object
207
+ * @param {string} [options.ns=dobo] - Scope's namespace
208
+ * @param {Array} [options.fields=[]]
209
+ * @param {Array} [options.extFields=[]]
210
+ * @param {Object} [options.params={}] - Validation parameters. See {@tutorial config} and {@link https://joi.dev/api/?v=17.13.3#anyvalidateasyncvalue-options|JOI validate's options}
211
+ * @returns {Object}
212
+ */
213
+ async function validate (body, joiModel, opts = {}) {
214
+ const { defaultsDeep } = this.app.lib.aneka
215
+ const { isEmpty } = this.app.lib._
216
+ let { fields, extFields = [], params = {}, partial } = opts
217
+
218
+ params = defaultsDeep(params, this.app.dobo.config.validationParams)
219
+ const { rule = {} } = params
220
+ delete params.rule
221
+ if (isEmpty(joiModel)) joiModel = buildFromDbModel.call(this, { fields, rule, extFields, partial })
222
+ if (!joiModel) return { value: body }
223
+ try {
224
+ return await joiModel.validateAsync(body, params)
225
+ } catch (err) {
226
+ const payload = { details: err.details, statusCode: 422, code: 'DB_VALIDATION' }
227
+ if (err.values) payload.values = err.values
228
+ throw this.plugin.error('validationError', payload)
229
+ }
230
+ }
231
+
232
+ export default validate
@@ -0,0 +1,150 @@
1
+ import { sanitizeAll, sanitizeRef } from '../collect-models.js'
2
+ import clearRecord from './model/clear-record.js'
3
+ import countRecord from './model/count-record.js'
4
+ import createRecord from './model/create-record.js'
5
+ import drop from './model/drop.js'
6
+ import findAllRecord from './model/find-all-record.js'
7
+ import findOneRecord from './model/find-one-record.js'
8
+ import findRecord from './model/find-record.js'
9
+ import getRecord from './model/get-record.js'
10
+ import createHistogram from './model/create-histogram.js'
11
+ import createAggregate from './model/create-aggregate.js'
12
+ import exists from './model/exists.js'
13
+ import build from './model/build.js'
14
+ import loadFixtures from './model/load-fixtures.js'
15
+ import removeRecord from './model/remove-record.js'
16
+ import updateRecord from './model/update-record.js'
17
+ import createAttachment from './model/create-attachment.js'
18
+ import updateAttachment from './model/update-attachment.js'
19
+ import getAttachment from './model/get-attachment.js'
20
+ import removeAttachment from './model/remove-attachment.js'
21
+ import findAttachment from './model/find-attachment.js'
22
+ import sanitizeBody from './model/sanitize-body.js'
23
+ import sanitizeRecord from './model/sanitize-record.js'
24
+ import sanitizeId from './model/sanitize-id.js'
25
+ import upsertRecord from './model/upsert-record.js'
26
+ import listAttachment from './model/list-attachment.js'
27
+ import validate from './model/validate.js'
28
+
29
+ /**
30
+ * @typedef {string} TRecordSortKey
31
+ */
32
+
33
+ /**
34
+ * Key value pairs used as sort information:
35
+ * - Key represent model's field name
36
+ * - value represent its sort order: ```1``` for ascending order, and ```-1``` for descending order
37
+ *
38
+ * Example: to sort by firstName (ascending) and lastName (descending)
39
+ * ```javascript
40
+ * const sort = {
41
+ * firstName: 1,
42
+ * lastName: -1
43
+ * }
44
+ * ```
45
+ *
46
+ * @typedef {Object.<string, TRecordSortKey>} TRecordSort
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} TRecordPagination
51
+ * @property {number} limit - Number of records per page
52
+ * @property {number} page - Page number
53
+ * @property {number} skip - Records to skip
54
+ * @property {TRecordSort} sort - Sort order
55
+ */
56
+
57
+ async function modelFactory () {
58
+ const { Tools } = this.app.baseClass
59
+ const { defaults } = this.app.lib._
60
+
61
+ /**
62
+ * Feature class
63
+ *
64
+ * ```this.plugin``` should be the one who owned this driver
65
+ *
66
+ * @class
67
+ */
68
+ class DoboModel extends Tools {
69
+ constructor (plugin, options) {
70
+ super(plugin)
71
+ defaults(this, options)
72
+ this.driver = this.connection.driver
73
+ this.cacheable = this.cacheable ?? this.driver.cacheable ?? true
74
+ }
75
+
76
+ action = (name, ...args) => {
77
+ const action = new this.app.baseClass.DoboAction(this, name, ...args)
78
+ return action
79
+ }
80
+
81
+ sanitizeObjectDef = async (obj) => {
82
+ await sanitizeAll.call(this.app.dobo, obj)
83
+ }
84
+
85
+ sanitizeRef = async (obj, fatal = true) => {
86
+ await sanitizeRef.call(this.app.dobo, obj, this.app.dobo.models, fatal)
87
+ }
88
+
89
+ getProperty = (name) => {
90
+ return this.properties.find(prop => prop.name === name)
91
+ }
92
+
93
+ hasProperty = (name) => {
94
+ return !!this.getProperty(name)
95
+ }
96
+
97
+ build = build
98
+ exists = exists
99
+ drop = drop
100
+
101
+ createRecord = createRecord
102
+ getRecord = getRecord
103
+ updateRecord = updateRecord
104
+ upsertRecord = upsertRecord
105
+ removeRecord = removeRecord
106
+ clearRecord = clearRecord
107
+ findRecord = findRecord
108
+ findOneRecord = findOneRecord
109
+ findAllRecord = findAllRecord
110
+
111
+ createAggregate = createAggregate
112
+ createHistogram = createHistogram
113
+ countRecord = countRecord
114
+
115
+ createAttachment = createAttachment
116
+ getAttachment = getAttachment
117
+ updateAttachment = updateAttachment
118
+ removeAttachment = removeAttachment
119
+ findAttachment = findAttachment
120
+ listAttachment = listAttachment
121
+
122
+ loadFixtures = loadFixtures
123
+ sanitizeRecord = sanitizeRecord
124
+ sanitizeBody = sanitizeBody
125
+ sanitizeId = sanitizeId
126
+ validate = validate
127
+
128
+ // aliases
129
+ deleteRecord = removeRecord
130
+ clearRecords = clearRecord
131
+ countRecords = countRecord
132
+ findRecords = findRecord
133
+ findAllRecords = findAllRecord
134
+ listAttachments = listAttachment
135
+
136
+ getField = (name) => this.getProperty(name)
137
+ hasField = (name) => this.hasProperty(name)
138
+
139
+ dispose () {
140
+ super.dispose()
141
+ this.connection = null
142
+ this.driver = null
143
+ }
144
+ }
145
+
146
+ this.app.baseClass.DoboModel = DoboModel
147
+ return DoboModel
148
+ }
149
+
150
+ export default modelFactory
package/lib/index.js ADDED
@@ -0,0 +1,3 @@
1
+ /**
2
+ * @module Lib
3
+ */
package/package.json CHANGED
@@ -1,36 +1,45 @@
1
- {
2
- "name": "dobo",
3
- "version": "2.0.0",
4
- "description": "Database ORM/ODM for Bajo Framework",
5
- "main": "index.js",
6
- "scripts": {
7
- "test": "echo \"Error: no test specified\" && exit 1"
8
- },
9
- "type": "module",
10
- "bajo": {
11
- "type": "plugin"
12
- },
13
- "repository": {
14
- "type": "git",
15
- "url": "git+https://github.com/ardhi/dobo.git"
16
- },
17
- "keywords": [
18
- "database",
19
- "dobo",
20
- "db",
21
- "orm",
22
- "bajo",
23
- "framework"
24
- ],
25
- "author": "Ardhi Lukianto <ardhi@lukianto.com>",
26
- "license": "MIT",
27
- "bugs": {
28
- "url": "https://github.com/ardhi/dobo/issues"
29
- },
30
- "homepage": "https://github.com/ardhi/dobo#readme",
31
- "dependencies": {
32
- "@tryghost/nql": "^0.12.7",
33
- "joi": "^17.13.3",
34
- "mingo": "^6.5.1"
35
- }
36
- }
1
+ {
2
+ "name": "dobo",
3
+ "version": "2.2.0",
4
+ "description": "DBMS for Bajo Framework",
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "build-doc": "jsdoc -c .jsdoc.conf.json",
8
+ "test": "mocha"
9
+ },
10
+ "type": "module",
11
+ "bajo": {
12
+ "type": "plugin",
13
+ "alias": "db",
14
+ "bootorder": 50
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/ardhi/dobo.git"
19
+ },
20
+ "keywords": [
21
+ "database",
22
+ "dobo",
23
+ "db",
24
+ "orm",
25
+ "bajo",
26
+ "framework"
27
+ ],
28
+ "author": "Ardhi Lukianto <ardhi@lukianto.com>",
29
+ "license": "MIT",
30
+ "bugs": {
31
+ "url": "https://github.com/ardhi/dobo/issues"
32
+ },
33
+ "homepage": "https://github.com/ardhi/dobo#readme",
34
+ "dependencies": {
35
+ "@tryghost/nql": "^0.12.7",
36
+ "joi": "^18.0.2",
37
+ "mingo": "^7.1.0",
38
+ "ulid": "^3.0.2",
39
+ "uuid": "^13.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "clean-jsdoc-theme": "^4.3.0",
43
+ "jsdoc-plugin-intersection": "^1.0.4"
44
+ }
45
+ }