dobo 2.0.1 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/.github/FUNDING.yml +0 -0
  2. package/.github/workflows/repo-lockdown.yml +0 -0
  3. package/.jsdoc.conf.json +0 -0
  4. package/LICENSE +0 -0
  5. package/README.md +2 -2
  6. package/docs/Dobo.html +0 -0
  7. package/docs/data/search.json +0 -0
  8. package/docs/fonts/Inconsolata-Regular.ttf +0 -0
  9. package/docs/fonts/OpenSans-Regular.ttf +0 -0
  10. package/docs/fonts/WorkSans-Bold.ttf +0 -0
  11. package/docs/global.html +0 -0
  12. package/docs/index.html +0 -0
  13. package/docs/index.js.html +0 -0
  14. package/docs/lib_collect-connections.js.html +0 -0
  15. package/docs/lib_collect-drivers.js.html +0 -0
  16. package/docs/lib_collect-features.js.html +0 -0
  17. package/docs/lib_collect-schemas.js.html +0 -0
  18. package/docs/lib_index.js.html +0 -0
  19. package/docs/method_model_create.js.html +0 -0
  20. package/docs/method_model_drop.js.html +0 -0
  21. package/docs/method_model_exists.js.html +0 -0
  22. package/docs/method_record_count.js.html +0 -0
  23. package/docs/method_record_create.js.html +0 -0
  24. package/docs/method_record_find-all.js.html +0 -0
  25. package/docs/method_record_find-one.js.html +0 -0
  26. package/docs/method_record_find.js.html +0 -0
  27. package/docs/method_record_get.js.html +0 -0
  28. package/docs/method_record_remove.js.html +0 -0
  29. package/docs/method_record_update.js.html +0 -0
  30. package/docs/method_record_upsert.js.html +0 -0
  31. package/docs/method_sanitize_body.js.html +0 -0
  32. package/docs/method_sanitize_date.js.html +0 -0
  33. package/docs/method_sanitize_id.js.html +0 -0
  34. package/docs/method_validate.js.html +0 -0
  35. package/docs/module-Lib.html +0 -0
  36. package/docs/scripts/core.js +476 -477
  37. package/docs/scripts/core.min.js +0 -0
  38. package/docs/scripts/resize.js +36 -36
  39. package/docs/scripts/search.js +105 -105
  40. package/docs/scripts/search.min.js +0 -0
  41. package/docs/scripts/third-party/Apache-License-2.0.txt +0 -0
  42. package/docs/scripts/third-party/fuse.js +1 -1
  43. package/docs/scripts/third-party/hljs-line-num-original.js +282 -285
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -1
  45. package/docs/scripts/third-party/hljs-original.js +1195 -1202
  46. package/docs/scripts/third-party/hljs.js +1 -1
  47. package/docs/scripts/third-party/popper.js +1 -1
  48. package/docs/scripts/third-party/tippy.js +1 -1
  49. package/docs/scripts/third-party/tocbot.js +508 -509
  50. package/docs/scripts/third-party/tocbot.min.js +0 -0
  51. package/docs/static/bitcoin.jpeg +0 -0
  52. package/docs/static/home.md +0 -0
  53. package/docs/static/logo-ecosystem.png +0 -0
  54. package/docs/static/logo.png +0 -0
  55. package/docs/styles/clean-jsdoc-theme-base.css +0 -0
  56. package/docs/styles/clean-jsdoc-theme-dark.css +0 -0
  57. package/docs/styles/clean-jsdoc-theme-light.css +0 -0
  58. package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -0
  59. package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -0
  60. package/docs/styles/clean-jsdoc-theme.min.css +0 -0
  61. package/extend/bajo/intl/en-US.json +66 -28
  62. package/extend/bajo/intl/id.json +55 -27
  63. package/extend/bajoCli/applet/clear-record.js +22 -0
  64. package/extend/bajoCli/applet/connection.js +0 -0
  65. package/extend/bajoCli/applet/count-record.js +27 -0
  66. package/extend/bajoCli/applet/create-aggregate.js +33 -0
  67. package/extend/bajoCli/applet/create-histogram.js +33 -0
  68. package/extend/bajoCli/applet/create-record.js +39 -0
  69. package/extend/bajoCli/applet/find-record.js +27 -0
  70. package/extend/bajoCli/applet/get-record.js +27 -0
  71. package/extend/bajoCli/applet/lib/post-process.js +10 -17
  72. package/extend/bajoCli/applet/model.js +22 -0
  73. package/extend/bajoCli/applet/rebuild-model.js +91 -0
  74. package/extend/bajoCli/applet/remove-record.js +27 -0
  75. package/extend/bajoCli/applet/update-record.js +44 -0
  76. package/extend/bajoCli/applet.js +0 -0
  77. package/extend/dobo/driver/memory.js +170 -0
  78. package/extend/dobo/feature/created-at.js +9 -7
  79. package/extend/dobo/feature/dt.js +0 -0
  80. package/extend/dobo/feature/immutable.js +30 -0
  81. package/extend/dobo/feature/int-id.js +0 -0
  82. package/extend/dobo/feature/removed-at.js +32 -54
  83. package/extend/dobo/feature/updated-at.js +14 -12
  84. package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +2 -6
  85. package/extend/waibuStatic/virtual.json +0 -0
  86. package/index.js +284 -371
  87. package/lib/collect-connections.js +49 -21
  88. package/lib/collect-drivers.js +19 -33
  89. package/lib/collect-features.js +24 -17
  90. package/lib/collect-models.js +321 -0
  91. package/lib/factory/action.js +161 -0
  92. package/lib/factory/connection.js +62 -0
  93. package/lib/factory/driver.js +372 -0
  94. package/lib/factory/feature.js +33 -0
  95. package/lib/factory/model/_util.js +402 -0
  96. package/lib/factory/model/build.js +15 -0
  97. package/lib/factory/model/clear-record.js +17 -0
  98. package/lib/factory/model/count-record.js +17 -0
  99. package/lib/factory/model/create-aggregate.js +17 -0
  100. package/lib/factory/model/create-attachment.js +29 -0
  101. package/lib/factory/model/create-histogram.js +17 -0
  102. package/lib/factory/model/create-record.js +35 -0
  103. package/lib/factory/model/drop.js +15 -0
  104. package/lib/factory/model/exists.js +21 -0
  105. package/lib/factory/model/find-all-record.js +71 -0
  106. package/lib/factory/model/find-attachment.js +29 -0
  107. package/lib/factory/model/find-one-record.js +19 -0
  108. package/{method/record/find.js → lib/factory/model/find-record.js} +103 -115
  109. package/lib/factory/model/get-attachment.js +15 -0
  110. package/lib/factory/model/get-record.js +79 -0
  111. package/lib/factory/model/list-attachment.js +37 -0
  112. package/lib/{add-fixtures.js → factory/model/load-fixtures.js} +69 -67
  113. package/lib/factory/model/remove-attachment.js +15 -0
  114. package/lib/factory/model/remove-record.js +59 -0
  115. package/lib/factory/model/sanitize-body.js +56 -0
  116. package/lib/factory/model/sanitize-id.js +7 -0
  117. package/lib/factory/model/sanitize-record.js +26 -0
  118. package/lib/factory/model/update-attachment.js +9 -0
  119. package/lib/factory/model/update-record.js +81 -0
  120. package/lib/factory/model/upsert-record.js +95 -0
  121. package/{method → lib/factory/model}/validate.js +38 -52
  122. package/lib/factory/model.js +150 -0
  123. package/lib/index.js +0 -0
  124. package/package.json +8 -4
  125. package/wiki/APPLETS.md +0 -0
  126. package/wiki/CHANGES.md +50 -0
  127. package/wiki/CONFIG.md +0 -0
  128. package/wiki/CONTRIBUTING.md +0 -0
  129. package/wiki/DEV-GUIDE.md +0 -0
  130. package/wiki/ECOSYSTEM.md +0 -0
  131. package/wiki/GETTING-STARTED.md +10 -10
  132. package/wiki/QUERY-LANGUAGE.md +0 -0
  133. package/wiki/USER-GUIDE.md +0 -0
  134. package/extend/bajoCli/applet/model-clear.js +0 -11
  135. package/extend/bajoCli/applet/model-rebuild.js +0 -101
  136. package/extend/bajoCli/applet/record-create.js +0 -43
  137. package/extend/bajoCli/applet/record-find.js +0 -28
  138. package/extend/bajoCli/applet/record-get.js +0 -24
  139. package/extend/bajoCli/applet/record-remove.js +0 -24
  140. package/extend/bajoCli/applet/record-update.js +0 -47
  141. package/extend/bajoCli/applet/schema.js +0 -22
  142. package/extend/bajoCli/applet/stat-count.js +0 -24
  143. package/lib/build-bulk-action.js +0 -12
  144. package/lib/check-unique.js +0 -39
  145. package/lib/collect-schemas.js +0 -91
  146. package/lib/exec-feature-hook.js +0 -13
  147. package/lib/exec-validation.js +0 -21
  148. package/lib/generic-prop-sanitizer.js +0 -32
  149. package/lib/handle-attachment-upload.js +0 -16
  150. package/lib/mem-db/conn-sanitizer.js +0 -8
  151. package/lib/mem-db/instantiate.js +0 -41
  152. package/lib/mem-db/method/model/clear.js +0 -6
  153. package/lib/mem-db/method/model/create.js +0 -5
  154. package/lib/mem-db/method/model/drop.js +0 -5
  155. package/lib/mem-db/method/model/exists.js +0 -5
  156. package/lib/mem-db/method/record/create.js +0 -12
  157. package/lib/mem-db/method/record/find.js +0 -20
  158. package/lib/mem-db/method/record/get.js +0 -9
  159. package/lib/mem-db/method/record/remove.js +0 -13
  160. package/lib/mem-db/method/record/update.js +0 -15
  161. package/lib/mem-db/method/stat/count.js +0 -11
  162. package/lib/mem-db/start.js +0 -25
  163. package/lib/merge-attachment-info.js +0 -16
  164. package/lib/multi-rel-rows.js +0 -42
  165. package/lib/resolve-method.js +0 -16
  166. package/lib/sanitize-schema.js +0 -198
  167. package/lib/single-rel-rows.js +0 -38
  168. package/method/attachment/copy-uploaded.js +0 -34
  169. package/method/attachment/create.js +0 -29
  170. package/method/attachment/find.js +0 -27
  171. package/method/attachment/get-path.js +0 -12
  172. package/method/attachment/get.js +0 -12
  173. package/method/attachment/pre-check.js +0 -9
  174. package/method/attachment/remove.js +0 -11
  175. package/method/attachment/update.js +0 -7
  176. package/method/bulk/create.js +0 -46
  177. package/method/model/clear.js +0 -22
  178. package/method/model/create.js +0 -32
  179. package/method/model/drop.js +0 -31
  180. package/method/model/exists.js +0 -37
  181. package/method/record/clear.js +0 -24
  182. package/method/record/count.js +0 -66
  183. package/method/record/create.js +0 -111
  184. package/method/record/find-all.js +0 -41
  185. package/method/record/find-one.js +0 -70
  186. package/method/record/get.js +0 -89
  187. package/method/record/remove.js +0 -72
  188. package/method/record/update.js +0 -104
  189. package/method/record/upsert.js +0 -51
  190. package/method/sanitize/body.js +0 -85
  191. package/method/sanitize/date.js +0 -27
  192. package/method/sanitize/id.js +0 -17
  193. package/method/stat/aggregate.js +0 -23
  194. package/method/stat/histogram.js +0 -26
@@ -0,0 +1,56 @@
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, 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 (['string', 'text'].includes(type)) result[name] = sanitizeString(result[name], strict)
29
+ else if (['datetime'].includes(type)) result[name] = sanitizeDate(result[name], { input: 'native' })
30
+ if (!strict && isNaN(result[name])) result[name] = null
31
+ if (['updateRecord', 'upsertRecord'].includes(action) && type === 'string' && result[name] === '') result[name] = null
32
+ }
33
+
34
+ const omitted = []
35
+ for (const prop of [...this.properties, ...extFields]) {
36
+ if (partial && !has(body, prop.name)) continue
37
+ result[prop.name] = body[prop.name]
38
+ if (body[prop.name] === null) continue
39
+ if (isSet(body[prop.name])) sanitize(prop.name, prop.type)
40
+ else {
41
+ if (isSet(prop.default) && !noDefault) {
42
+ result[prop.name] = prop.default
43
+ if (isString(prop.default) && prop.default.startsWith('handler:')) {
44
+ const [, ...args] = prop.default.split(':')
45
+ if (args.length > 0) result[prop.name] = await callHandler(args.join(':'))
46
+ } else sanitize(prop.name, prop.type)
47
+ }
48
+ }
49
+ if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
50
+ if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
51
+ if (body[prop.name] === undefined) omitted.push(prop.name)
52
+ }
53
+ return omit(result, omitted)
54
+ }
55
+
56
+ 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
@@ -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, 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, 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
@@ -89,14 +89,11 @@ const validator = {
89
89
  timestamp: ['timestamp']
90
90
  }
91
91
 
92
- function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } = {}) {
93
- // if (schema.validation) return schema.validation
94
- const {
95
- isPlainObject, get, each, isEmpty, isString, forOwn, keys,
96
- find, isArray, has, cloneDeep, concat, without
97
- } = this.app.lib._
92
+ function buildFromDbModel (opts = {}) {
93
+ const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
94
+ const { fields = [], rule = {}, extFields = [] } = opts
98
95
  const obj = {}
99
- const { propType } = this.app.pluginClass.dobo
96
+ const { propertyType: propType } = this.app.baseClass.Dobo
100
97
  const refs = []
101
98
 
102
99
  function getRuleKv (kvRule) {
@@ -116,36 +113,36 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
116
113
  function applyFieldRules (prop, obj) {
117
114
  const minMax = { min: false, max: false }
118
115
  const rules = get(rule, prop.name, prop.rules ?? [])
119
- if (!isArray(rules)) return rules
120
- each(rules, r => {
116
+ if (!Array.isArray(rules)) return rules
117
+ for (const r of rules) {
121
118
  const types = validator[propType[prop.type].validator]
122
119
  const { key, value } = getRuleKv(r)
123
120
  if (keys(minMax).includes(key)) minMax[key] = true
124
121
  if (key === 'ref') {
125
122
  refs.push(prop.name)
126
123
  obj = joi.ref(value)
127
- return undefined
124
+ continue
128
125
  }
129
- if (!key || !types.includes(key)) return undefined
126
+ if (!key || !types.includes(key)) continue
130
127
  obj = obj[key](value)
131
- })
128
+ }
132
129
  if (refs.includes(prop.name)) return obj
133
130
  if (['string', 'text'].includes(prop.type)) {
134
- forOwn(minMax, (v, k) => {
135
- if (v) return undefined
131
+ for (const k in minMax) {
132
+ if (minMax[k]) continue
136
133
  if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
137
- })
134
+ }
138
135
  }
139
- if (isArray(prop.values)) obj = obj.valid(...prop.values)
136
+ if (Array.isArray(prop.values)) obj = obj.valid(...prop.values)
140
137
  if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
141
138
  return obj
142
139
  }
143
140
 
144
- const props = concat(cloneDeep(schema.properties), extFields)
141
+ const props = [...this.properties, ...extFields]
145
142
 
146
143
  for (const p of props) {
147
144
  if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
148
- if (fields.length > 0 && !fields.includes(p.name)) continue
145
+ if (opts.partial && fields.length > 0 && !fields.includes(p.name)) continue
149
146
  let item
150
147
  switch (p.type) {
151
148
  case 'text':
@@ -175,38 +172,37 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
175
172
  break
176
173
  }
177
174
  if (item) {
178
- if (item.$_root) obj[p.name] = item.allow(null)
175
+ if (item.$_root && !p.required) obj[p.name] = item.allow(null, '')
179
176
  else obj[p.name] = item
180
177
  }
181
178
  }
182
179
  if (isEmpty(obj)) return false
183
- each(get(schema, 'globalRules', []), r => {
184
- each(without(keys(obj), ...refs), k => {
180
+ for (const r of this.rules) {
181
+ for (const k of without(keys(obj), ...refs)) {
185
182
  const prop = find(props, { name: k })
186
- if (!prop) return undefined
183
+ if (!prop) continue
187
184
  const types = validator[propType[prop.type].validator]
188
185
  const { key, value, columns = [] } = getRuleKv(r)
189
- if (!types.includes(key)) return undefined
186
+ if (!types.includes(key)) continue
190
187
  if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
191
- })
192
- })
188
+ }
189
+ }
193
190
  const result = joi.object(obj)
194
- if (fields.length === 0) return result
195
- each(['with', 'xor', 'without'], k => {
196
- const item = get(schema, `extRule.${k}`)
191
+ for (const k of ['with', 'xor', 'without']) {
192
+ const item = get(this, `extRule.${k}`)
197
193
  if (item) result[k](...item)
198
- })
194
+ }
199
195
  return result
200
196
  }
201
197
 
202
198
  /**
203
- * Validate value against JOI schema
199
+ * Validate value against JOI model
204
200
  *
205
201
  * @method
206
202
  * @memberof Dobo
207
203
  * @async
208
204
  * @param {Object} value - value to validate
209
- * @param {Object} joiSchema - JOI schema
205
+ * @param {Object} joiModel - JOI model
210
206
  * @param {Object} [options={}] - Options object
211
207
  * @param {string} [options.ns=dobo] - Scope's namespace
212
208
  * @param {Array} [options.fields=[]]
@@ -214,32 +210,22 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
214
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}
215
211
  * @returns {Object}
216
212
  */
217
- async function validate (value, joiSchema, { ns, fields, extFields, params } = {}) {
218
- const { defaultsDeep, isSet } = this.app.lib.aneka
219
- const { isString, forOwn, find } = this.app.lib._
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
220
217
 
221
- ns = ns ?? [this.ns]
222
- params = defaultsDeep(params, this.config.validationParams)
218
+ params = defaultsDeep(params, this.app.dobo.config.validationParams)
223
219
  const { rule = {} } = params
224
220
  delete params.rule
225
- if (isString(joiSchema)) {
226
- const { schema } = this.getInfo(joiSchema)
227
- forOwn(value, (v, k) => {
228
- if (!isSet(v)) return undefined
229
- const p = find(schema.properties, { name: k })
230
- if (!p) return undefined
231
- for (const t of ['date|YYYY-MM-DD', 'time|HH:mm:ss']) {
232
- const [type, input] = t.split('|')
233
- if (p.type === type) value[k] = this.sanitizeDate(value[k], { input, output: 'native' })
234
- }
235
- })
236
- joiSchema = buildFromDbSchema.call(this, schema, { fields, rule, extFields })
237
- }
238
- if (!joiSchema) return value
221
+ if (isEmpty(joiModel)) joiModel = buildFromDbModel.call(this, { fields, rule, extFields, partial })
222
+ if (!joiModel) return { value: body }
239
223
  try {
240
- return await joiSchema.validateAsync(value, params)
224
+ return await joiModel.validateAsync(body, params)
241
225
  } catch (err) {
242
- throw this.error('validationError', { details: err.details, values: err.values, ns, statusCode: 422, code: 'DB_VALIDATION' })
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)
243
229
  }
244
230
  }
245
231
 
@@ -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 CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.0.1",
3
+ "version": "2.2.1",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -9,7 +9,9 @@
9
9
  },
10
10
  "type": "module",
11
11
  "bajo": {
12
- "type": "plugin"
12
+ "type": "plugin",
13
+ "alias": "db",
14
+ "bootorder": 50
13
15
  },
14
16
  "repository": {
15
17
  "type": "git",
@@ -31,8 +33,10 @@
31
33
  "homepage": "https://github.com/ardhi/dobo#readme",
32
34
  "dependencies": {
33
35
  "@tryghost/nql": "^0.12.7",
34
- "joi": "^17.13.3",
35
- "mingo": "^6.5.1"
36
+ "joi": "^18.0.2",
37
+ "mingo": "^7.1.0",
38
+ "ulid": "^3.0.2",
39
+ "uuid": "^13.0.0"
36
40
  },
37
41
  "devDependencies": {
38
42
  "clean-jsdoc-theme": "^4.3.0",
package/wiki/APPLETS.md CHANGED
File without changes