dobo 1.2.10 → 2.0.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 (153) 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 +726 -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 +369 -0
  44. package/docs/scripts/third-party/hljs-line-num.js +1 -0
  45. package/docs/scripts/third-party/hljs-original.js +5171 -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 +672 -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/{bajo → extend/bajo}/intl/en-US.json +3 -2
  62. package/{bajo → extend/bajo}/intl/id.json +3 -2
  63. package/{bajoCli → extend/bajoCli}/applet/connection.js +5 -5
  64. package/extend/bajoCli/applet/lib/post-process.js +53 -0
  65. package/extend/bajoCli/applet/model-clear.js +11 -0
  66. package/{bajoCli → extend/bajoCli}/applet/model-rebuild.js +13 -13
  67. package/{bajoCli → extend/bajoCli}/applet/record-create.js +10 -8
  68. package/{bajoCli → extend/bajoCli}/applet/record-find.js +6 -5
  69. package/{bajoCli → extend/bajoCli}/applet/record-get.js +5 -5
  70. package/{bajoCli → extend/bajoCli}/applet/record-remove.js +5 -5
  71. package/{bajoCli → extend/bajoCli}/applet/record-update.js +9 -9
  72. package/{bajoCli → extend/bajoCli}/applet/schema.js +4 -4
  73. package/{bajoCli → extend/bajoCli}/applet/stat-count.js +4 -4
  74. package/{dobo → extend/dobo}/feature/created-at.js +1 -1
  75. package/{dobo → extend/dobo}/feature/removed-at.js +3 -3
  76. package/{dobo → extend/dobo}/feature/updated-at.js +2 -2
  77. package/{waibuMpa → extend/waibuMpa}/route/attachment/@model/@id/@field/@file.js +3 -3
  78. package/index.js +230 -72
  79. package/lib/add-fixtures.js +7 -6
  80. package/lib/build-bulk-action.js +2 -2
  81. package/lib/check-unique.js +2 -2
  82. package/lib/collect-connections.js +15 -4
  83. package/lib/collect-drivers.js +15 -6
  84. package/lib/{collect-feature.js → collect-features.js} +13 -4
  85. package/lib/collect-schemas.js +22 -12
  86. package/lib/exec-feature-hook.js +1 -1
  87. package/lib/exec-validation.js +5 -5
  88. package/lib/generic-prop-sanitizer.js +6 -5
  89. package/lib/handle-attachment-upload.js +2 -2
  90. package/lib/index.js +3 -0
  91. package/lib/mem-db/conn-sanitizer.js +1 -1
  92. package/lib/mem-db/instantiate.js +4 -4
  93. package/lib/mem-db/method/record/find.js +1 -1
  94. package/lib/mem-db/method/record/get.js +1 -1
  95. package/lib/mem-db/method/record/remove.js +1 -1
  96. package/lib/mem-db/method/record/update.js +1 -1
  97. package/lib/mem-db/start.js +1 -1
  98. package/lib/merge-attachment-info.js +2 -2
  99. package/lib/multi-rel-rows.js +2 -2
  100. package/lib/resolve-method.js +3 -3
  101. package/lib/sanitize-schema.js +8 -7
  102. package/lib/single-rel-rows.js +2 -2
  103. package/{plugin-method → method}/attachment/copy-uploaded.js +2 -2
  104. package/{plugin-method → method}/attachment/create.js +3 -3
  105. package/{plugin-method → method}/attachment/find.js +2 -2
  106. package/{plugin-method → method}/attachment/get-path.js +3 -3
  107. package/{plugin-method → method}/attachment/get.js +1 -1
  108. package/{plugin-method → method}/attachment/pre-check.js +1 -1
  109. package/{plugin-method → method}/attachment/remove.js +1 -1
  110. package/{plugin-method → method}/bulk/create.js +6 -6
  111. package/{plugin-method → method}/model/clear.js +5 -5
  112. package/method/model/create.js +32 -0
  113. package/method/model/drop.js +31 -0
  114. package/method/model/exists.js +37 -0
  115. package/{plugin-method → method}/record/clear.js +5 -5
  116. package/{plugin-method → method}/record/count.js +27 -5
  117. package/{plugin-method → method}/record/create.js +46 -6
  118. package/{plugin-method → method}/record/find-all.js +16 -0
  119. package/{plugin-method → method}/record/find-one.js +20 -6
  120. package/method/record/find.js +115 -0
  121. package/method/record/get.js +89 -0
  122. package/method/record/remove.js +72 -0
  123. package/{plugin-method → method}/record/update.js +47 -6
  124. package/{plugin-method → method}/record/upsert.js +18 -2
  125. package/{plugin-method → method}/sanitize/body.js +18 -3
  126. package/method/sanitize/date.js +27 -0
  127. package/{plugin-method → method}/sanitize/id.js +10 -0
  128. package/{plugin-method → method}/stat/aggregate.js +4 -4
  129. package/{plugin-method → method}/stat/histogram.js +4 -4
  130. package/{plugin-method → method}/validate.js +96 -7
  131. package/package.json +41 -36
  132. package/wiki/APPLETS.md +57 -0
  133. package/wiki/CONFIG.md +25 -0
  134. package/wiki/CONTRIBUTING.md +5 -0
  135. package/wiki/DEV-GUIDE.md +1 -0
  136. package/wiki/ECOSYSTEM.md +20 -0
  137. package/wiki/GETTING-STARTED.md +166 -0
  138. package/wiki/USER-GUIDE.md +1 -0
  139. package/bajoCli/applet/lib/post-process.js +0 -47
  140. package/bajoCli/applet/model-clear.js +0 -11
  141. package/plugin-method/model/create.js +0 -19
  142. package/plugin-method/model/drop.js +0 -19
  143. package/plugin-method/model/exists.js +0 -24
  144. package/plugin-method/record/find.js +0 -52
  145. package/plugin-method/record/get.js +0 -47
  146. package/plugin-method/record/remove.js +0 -41
  147. package/plugin-method/sanitize/date.js +0 -14
  148. /package/{bajoCli → extend/bajoCli}/applet.js +0 -0
  149. /package/{dobo → extend/dobo}/feature/dt.js +0 -0
  150. /package/{dobo → extend/dobo}/feature/int-id.js +0 -0
  151. /package/{waibuStatic → extend/waibuStatic}/virtual.json +0 -0
  152. /package/{plugin-method → method}/attachment/update.js +0 -0
  153. /package/{docs/query-language.md → wiki/QUERY-LANGUAGE.md} +0 -0
@@ -1,10 +1,32 @@
1
1
  import resolveMethod from '../../lib/resolve-method.js'
2
2
  import execFeatureHook from '../../lib/exec-feature-hook.js'
3
3
 
4
+ /**
5
+ * @typedef {Object} TRecordCountOptions
6
+ * @see Dobo#recordCount
7
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns array of records. Otherwise {@link TFindRecordResult}
8
+ * @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
9
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
10
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
11
+ */
12
+
13
+ /**
14
+ * Return the number of records found by given filter
15
+ *
16
+ * @method
17
+ * @memberof Dobo
18
+ * @async
19
+ * @instance
20
+ * @name recordCount
21
+ * @param {string} name - Model's name
22
+ * @param {TRecordFilter} [filter={}] - Filter object
23
+ * @param {TRecordCountOptions} [options={}]
24
+ * @returns {(TRecordCountResult|number)} Return ```number``` of records if ```options.dataOnly``` is set. {@link TRecordCountResult} otherwise
25
+ */
4
26
  async function count (name, filter = {}, opts = {}) {
5
27
  const { runHook } = this.app.bajo
6
28
  const { get, set } = this.cache ?? {}
7
- const { cloneDeep, camelCase, omit } = this.lib._
29
+ const { cloneDeep, camelCase, omit } = this.app.lib._
8
30
  delete opts.record
9
31
  const options = cloneDeep(omit(opts, ['req', 'reply']))
10
32
  options.req = opts.req
@@ -19,8 +41,8 @@ async function count (name, filter = {}, opts = {}) {
19
41
  if (options.queryHandler) filter.query = await options.queryHandler.call(opts.req ? this.app[opts.req.ns] : this, filter.query, opts.req)
20
42
  filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
21
43
  if (!noHook) {
22
- await runHook(`${this.name}:beforeRecordCount`, name, filter, options)
23
- await runHook(`${this.name}.${camelCase(name)}:beforeRecordCount`, filter, options)
44
+ await runHook(`${this.ns}:beforeRecordCount`, name, filter, options)
45
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordCount`, filter, options)
24
46
  }
25
47
  if (!noFeatureHook) await execFeatureHook.call(this, 'beforeCount', { schema, filter, options })
26
48
  if (get && !noCache && !options.record) {
@@ -33,8 +55,8 @@ async function count (name, filter = {}, opts = {}) {
33
55
  const record = options.record ?? (await handler.call(this.app[driver.ns], { schema, filter, options }))
34
56
  delete options.record
35
57
  if (!noHook) {
36
- await runHook(`${this.name}.${camelCase(name)}:afterRecordCount`, filter, options, record)
37
- await runHook(`${this.name}:afterRecordCount`, name, filter, options, record)
58
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordCount`, filter, options, record)
59
+ await runHook(`${this.ns}:afterRecordCount`, name, filter, options, record)
38
60
  }
39
61
  if (set && !noCache) await set({ model: name, filter, options, record })
40
62
  if (!noFeatureHook) await execFeatureHook.call(this, 'afterCount', { schema, filter, options, record })
@@ -6,11 +6,51 @@ import execValidation from '../../lib/exec-validation.js'
6
6
  import execFeatureHook from '../../lib/exec-feature-hook.js'
7
7
  import singleRelRows from '../../lib/single-rel-rows.js'
8
8
 
9
+ /**
10
+ * @typedef {Object} TRecordCreateOptions
11
+ * @see Dobo#recordCreate
12
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordCreateResult}
13
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
14
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
15
+ * @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
16
+ * @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
17
+ * @property {boolean} [noSanitize=false] - If ```true```, accept data payload as is without sanitization
18
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
19
+ * @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its schema's ```maxLemngth```
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 schema
22
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
23
+ */
24
+
25
+ /**
26
+ * Create a new record
27
+ *
28
+ * Example:
29
+ * ```javascript
30
+ * const { recordCreate } = this.app.dobo
31
+ * const { body } = {
32
+ * id: 'ID',
33
+ * name: 'Indonesia',
34
+ * iso3: 'IDN'
35
+ * }
36
+ * const result = await recordCreate('CdbCountry', body)
37
+ * ```
38
+ *
39
+ * @method
40
+ * @memberof Dobo
41
+ * @async
42
+ * @instance
43
+ * @name recordCreate
44
+ * @param {string} name - Model's name
45
+ * @param {Object} body - Data to be saved
46
+ * @param {TRecordCreateOptions} [options={}]
47
+ * @returns {(TRecordCreateResult|Object)} Returns newly created record if ```options.dataOnly``` is set. {@link TRecordCreateResult} otherwise
48
+ */
9
49
  async function create (name, input, opts = {}) {
10
50
  const { generateId, runHook } = this.app.bajo
11
- const { isSet } = this.lib.aneka
51
+ const { isSet } = this.app.lib.aneka
12
52
  const { clearModel } = this.cache ?? {}
13
- const { find, forOwn, cloneDeep, camelCase, omit, get, pick } = this.lib._
53
+ const { find, forOwn, cloneDeep, camelCase, omit, get, pick } = this.app.lib._
14
54
  delete opts.record
15
55
  const options = cloneDeep(omit(opts, ['req', 'reply']))
16
56
  options.req = opts.req
@@ -26,8 +66,8 @@ async function create (name, input, opts = {}) {
26
66
  const extFields = get(options, 'validation.extFields', [])
27
67
  let body = noSanitize ? cloneDeep(input) : await this.sanitizeBody({ body: input, schema, extFields, strict: true })
28
68
  if (!noHook) {
29
- await runHook(`${this.name}:beforeRecordCreate`, name, body, options)
30
- await runHook(`${this.name}.${camelCase(name)}:beforeRecordCreate`, body, options)
69
+ await runHook(`${this.ns}:beforeRecordCreate`, name, body, options)
70
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordCreate`, body, options)
31
71
  }
32
72
  if (!isSet(body.id)) {
33
73
  if (idField.type === 'string') {
@@ -61,8 +101,8 @@ async function create (name, input, opts = {}) {
61
101
  if (noResult) return
62
102
  record.data = await this.pickRecord({ record: record.data, fields, schema, hidden, forceNoHidden })
63
103
  if (!noHook) {
64
- await runHook(`${this.name}.${camelCase(name)}:afterRecordCreate`, nbody, options, record)
65
- await runHook(`${this.name}:afterRecordCreate`, name, nbody, options, record)
104
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordCreate`, nbody, options, record)
105
+ await runHook(`${this.ns}:afterRecordCreate`, name, nbody, options, record)
66
106
  }
67
107
  if (!noFeatureHook) await execFeatureHook.call(this, 'afterCreate', { schema, body: nbody, options, record })
68
108
  return dataOnly ? record.data : record
@@ -1,3 +1,19 @@
1
+ /**
2
+ * Find all records by model's name and given filter.
3
+ *
4
+ * The total number of records returned is limited by ```hardLimit``` value set in {@tutorial config} file.
5
+ *
6
+ * @see Dobo#recordFind
7
+ * @method
8
+ * @memberof Dobo
9
+ * @async
10
+ * @instance
11
+ * @name recordFindAll
12
+ * @param {string} name - Model's name
13
+ * @param {Object} [filter={}] - Filter object
14
+ * @param {TRecordFindOptions} [options={}]
15
+ * @returns {(TRecordFindResult|Array.<Object>)} Return ```array``` of records if ```options.dataOnly``` is set. {@link TRecordFindResult} otherwise
16
+ */
1
17
  async function findAll (name, filter = {}, options = {}) {
2
18
  const { maxLimit, hardLimit } = this.config.default.filter
3
19
  filter.page = 1
@@ -2,11 +2,25 @@ import resolveMethod from '../../lib/resolve-method.js'
2
2
  import singleRelRows from '../../lib/single-rel-rows.js'
3
3
  import execFeatureHook from '../../lib/exec-feature-hook.js'
4
4
 
5
+ /**
6
+ * Find the first record by model's name and given filter.
7
+ *
8
+ * @see Dobo#recordFind
9
+ * @method
10
+ * @memberof Dobo
11
+ * @async
12
+ * @instance
13
+ * @name recordFindOne
14
+ * @param {string} name - Model's name
15
+ * @param {Object} [filter={}] - Filter object
16
+ * @param {TRecordFindOptions} [options={}]
17
+ * @returns {(TRecordGetResult|Object)} Return record's ```object``` if ```options.dataOnly``` is set. {@link TRecordGetResult} otherwise
18
+ */
5
19
  async function findOne (name, filter = {}, opts = {}) {
6
- const { isSet } = this.lib.aneka
20
+ const { isSet } = this.app.lib.aneka
7
21
  const { runHook } = this.app.bajo
8
22
  const { get, set } = this.cache ?? {}
9
- const { cloneDeep, camelCase, omit, pick } = this.lib._
23
+ const { cloneDeep, camelCase, omit, pick } = this.app.lib._
10
24
  delete opts.record
11
25
  const options = cloneDeep(omit(opts, ['req', 'reply']))
12
26
  options.req = opts.req
@@ -24,8 +38,8 @@ async function findOne (name, filter = {}, opts = {}) {
24
38
  if (options.queryHandler) filter.query = await options.queryHandler.call(opts.req ? this.app[opts.req.ns] : this, filter.query, opts.req)
25
39
  filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
26
40
  if (!noHook) {
27
- await runHook(`${this.name}:beforeRecordFindOne`, name, filter, options)
28
- await runHook(`${this.name}.${camelCase(name)}:beforeRecordFindOne`, filter, options)
41
+ await runHook(`${this.ns}:beforeRecordFindOne`, name, filter, options)
42
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordFindOne`, filter, options)
29
43
  }
30
44
  if (!noFeatureHook) await execFeatureHook.call(this, 'beforeFindOne', { schema, filter, options })
31
45
  if (get && !noCache && !options.record) {
@@ -45,8 +59,8 @@ async function findOne (name, filter = {}, opts = {}) {
45
59
  record.data = await this.pickRecord({ record: record.data, fields, schema, hidden, forceNoHidden })
46
60
  record = pick(record, ['data'])
47
61
  if (!noHook) {
48
- await runHook(`${this.name}.${camelCase(name)}:afterRecordFindOne`, filter, options, record)
49
- await runHook(`${this.name}:afterRecordFindOne`, name, filter, options, record)
62
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordFindOne`, filter, options, record)
63
+ await runHook(`${this.ns}:afterRecordFindOne`, name, filter, options, record)
50
64
  }
51
65
  if (set && !noCache) await set({ model: name, filter, options, record })
52
66
  if (!noFeatureHook) await execFeatureHook.call(this, 'afterFindOne', { schema, filter, options, record })
@@ -0,0 +1,115 @@
1
+ import resolveMethod from '../../lib/resolve-method.js'
2
+ import multiRelRows from '../../lib/multi-rel-rows.js'
3
+ import execFeatureHook from '../../lib/exec-feature-hook.js'
4
+
5
+ /**
6
+ * @typedef {Object} TRecordFilter
7
+ * @see Dobo#recordFind
8
+ * @see Dobo#recordFindOne
9
+ * @see Dobo#recordFindAll
10
+ * @property {(string|Object)} [query={}] - Query definition. See {@tutorial query-language} for more
11
+ * @property {number} limit - Max number of records per page
12
+ * @property {number} page - Which page is the returned records currently at
13
+ * @property {number} skip - Records to skip
14
+ * @property {TRecordSort} sort - Sort order info
15
+ */
16
+
17
+ /**
18
+ * @typedef {Object} TRecordFindResult
19
+ * @see Dobo#recordFind
20
+ * @see Dobo#recordFindAll
21
+ * @see Dobo#recordGet
22
+ * @property {Array.<Object>} data - Array of returned records
23
+ * @property {boolean} success - Whether operation is successfull or failed
24
+ * @property {number} page - Which page is the returned records currently at
25
+ * @property {number} limit - Max number of records per page
26
+ * @property {number} count - Total number of records returned
27
+ * @property {number} pages - Total number of pages returned
28
+ */
29
+
30
+ /**
31
+ * @typedef {Object} TRecordFindOptions
32
+ * @see Dobo#recordFind
33
+ * @see Dobo#recordFindOne
34
+ * @see Dobo#recordFindAll
35
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns array of records. Otherwise {@link TFindRecordResult}
36
+ * @property {boolean} [count=false] - If ```true``` and ```dataOnly``` is also ```true```, the total number of records found will be returned
37
+ * @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
38
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
39
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
40
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
41
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's schema
42
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
43
+ */
44
+
45
+ /**
46
+ * Find records by model's name and given filter
47
+ *
48
+ * Example: find records from model **CdbCountry** where its id is 'ID' or 'MY',
49
+ * sorted by ```name``` in ascending order and return only its ```id```, ```name``` and ```iso3```
50
+ * ```javascript
51
+ * const { recordFind } = this.app.dobo
52
+ * const query = { id: { $in: ['ID', 'MY'] } }
53
+ * const sort = { name: 1 }
54
+ * const fields = ['id', 'name', 'iso3']
55
+ * const result = await recordFind('CdbCountry', { query, sort }, { fields })
56
+ * ```
57
+ *
58
+ * @method
59
+ * @memberof Dobo
60
+ * @async
61
+ * @instance
62
+ * @name recordFind
63
+ * @param {string} name - Model's name
64
+ * @param {Object} [filter={}] - Filter object
65
+ * @param {TRecordFindOptions} [options={}]
66
+ * @returns {(TRecordFindResult|Array.<Object>)} Return ```array``` of records if ```options.dataOnly``` is set. {@link TRecordFindResult} otherwise
67
+ */
68
+ async function find (name, filter = {}, opts = {}) {
69
+ const { isSet } = this.app.lib.aneka
70
+ const { runHook } = this.app.bajo
71
+ const { get, set } = this.cache ?? {}
72
+ const { cloneDeep, camelCase, omit } = this.app.lib._
73
+ delete opts.records
74
+ const options = cloneDeep(omit(opts, ['req', 'reply']))
75
+ options.req = opts.req
76
+ options.reply = opts.reply
77
+ options.dataOnly = options.dataOnly ?? true
78
+ let { fields, dataOnly, noHook, noCache, noFeatureHook, hidden, forceNoHidden } = options
79
+ options.count = options.count ?? false
80
+ options.dataOnly = false
81
+ await this.modelExists(name, true)
82
+ const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-find', options)
83
+ if (!schema.cacheable) noCache = true
84
+ filter.query = this.buildQuery({ filter, schema, options }) ?? {}
85
+ if (options.queryHandler) filter.query = await options.queryHandler.call(opts.req ? this.app[opts.req.ns] : this, filter.query, opts.req)
86
+ filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
87
+ if (!noHook) {
88
+ await runHook(`${this.ns}:beforeRecordFind`, name, filter, options)
89
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordFind`, filter, options)
90
+ }
91
+ if (!noFeatureHook) await execFeatureHook.call(this, 'beforeFind', { schema, filter, options })
92
+ if (get && !noCache && !options.records) {
93
+ const cachedResult = await get({ model: name, filter, options })
94
+ if (cachedResult) {
95
+ cachedResult.cached = true
96
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterFind', { schema, filter, options, records: cachedResult })
97
+ return dataOnly ? cachedResult.data : cachedResult
98
+ }
99
+ }
100
+ const records = options.records ?? (await handler.call(this.app[driver.ns], { schema, filter, options }))
101
+ delete options.records
102
+ if (isSet(options.rels)) await multiRelRows.call(this, { schema, records: records.data, options })
103
+ for (const idx in records.data) {
104
+ records.data[idx] = await this.pickRecord({ record: records.data[idx], fields, schema, hidden, forceNoHidden })
105
+ }
106
+ if (!noHook) {
107
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordFind`, filter, options, records)
108
+ await runHook(`${this.ns}:afterRecordFind`, name, filter, options, records)
109
+ }
110
+ if (set && !noCache) await set({ model: name, filter, options, records })
111
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterFind', { schema, filter, options, records })
112
+ return dataOnly ? records.data : records
113
+ }
114
+
115
+ export default find
@@ -0,0 +1,89 @@
1
+ import resolveMethod from '../../lib/resolve-method.js'
2
+ import singleRelRows from '../../lib/single-rel-rows.js'
3
+ import execFeatureHook from '../../lib/exec-feature-hook.js'
4
+
5
+ /**
6
+ * @typedef {Object} TRecordGetResult
7
+ * @see Dobo#recordGet
8
+ * @see Dobo#recordFindOne
9
+ * @property {Object} data - Returned record
10
+ * @property {boolean} success - Whether operation is successfull or failed
11
+ */
12
+
13
+ /**
14
+ * @typedef {Object} TRecordGetOptions
15
+ * @see Dobo#recordGet
16
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns array of records. Otherwise {@link TFindRecordResult}
17
+ * @property {boolean} [count=false] - If ```true``` and ```dataOnly``` is also ```true```, the total number of records found will be returned
18
+ * @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
19
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
20
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
21
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
22
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's schema
23
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
24
+ */
25
+
26
+ /**
27
+ * Get record by model's name and record ID
28
+ *
29
+ * Example:
30
+ * ```javascript
31
+ * const { recordGet } = this.app.dobo
32
+ * const fields = ['id', 'name', 'iso3']
33
+ * const result = await recordGet('CdbCountry', 'ID', { fields })
34
+ * ```
35
+ *
36
+ * @method
37
+ * @memberof Dobo
38
+ * @async
39
+ * @instance
40
+ * @name recordGet
41
+ * @param {string} name - Model's name
42
+ * @param {(string|number)} - Record's ID
43
+ * @param {TRecordGetOptions} [options={}]
44
+ * @returns {(TRecordGetResult|Object)} Return record's ```object``` if ```options.dataOnly``` is set. {@link TRecordGetResult} otherwise
45
+ */
46
+
47
+ async function get (name, id, opts = {}) {
48
+ const { isSet } = this.app.lib.aneka
49
+ const { runHook } = this.app.bajo
50
+ const { get, set } = this.cache ?? {}
51
+ const { cloneDeep, camelCase, omit } = this.app.lib._
52
+ delete opts.record
53
+ const options = cloneDeep(omit(opts, ['req', 'reply']))
54
+ options.req = opts.req
55
+ options.reply = opts.reply
56
+ options.dataOnly = options.dataOnly ?? true
57
+ let { fields, dataOnly, noHook, noCache, noFeatureHook, hidden = [], forceNoHidden } = options
58
+ await this.modelExists(name, true)
59
+ const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-get', options)
60
+ if (!schema.cacheable) noCache = true
61
+ id = this.sanitizeId(id, schema)
62
+ options.dataOnly = false
63
+ if (!noHook) {
64
+ await runHook(`${this.ns}:beforeRecordGet`, name, id, options)
65
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordGet`, id, options)
66
+ }
67
+ if (!noFeatureHook) await execFeatureHook.call(this, 'beforeGet', { schema, id, options })
68
+ if (get && !noCache && !options.record) {
69
+ const cachedResult = await get({ model: name, id, options })
70
+ if (cachedResult) {
71
+ cachedResult.cached = true
72
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterGet', { schema, id, options, record: cachedResult })
73
+ return dataOnly ? cachedResult.data : cachedResult
74
+ }
75
+ }
76
+ const record = options.record ?? (await handler.call(this.app[driver.ns], { schema, id, options }))
77
+ delete options.record
78
+ if (isSet(options.rels)) await singleRelRows.call(this, { schema, record: record.data, options })
79
+ record.data = await this.pickRecord({ record: record.data, fields, schema, hidden, forceNoHidden })
80
+ if (!noHook) {
81
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordGet`, id, options, record)
82
+ await runHook(`${this.ns}:afterRecordGet`, name, id, options, record)
83
+ }
84
+ if (set && !noCache) await set({ model: name, id, options, record })
85
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterGet', { schema, id, options, record })
86
+ return dataOnly ? record.data : record
87
+ }
88
+
89
+ export default get
@@ -0,0 +1,72 @@
1
+ import resolveMethod from '../../lib/resolve-method.js'
2
+ import handleAttachmentUpload from '../../lib/handle-attachment-upload.js'
3
+ import execFeatureHook from '../../lib/exec-feature-hook.js'
4
+
5
+ /**
6
+ * @typedef {Object} TRecordRemoveOptions
7
+ * @see Dobo#recordRemove
8
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns deleted record. Otherwise {@link TRecordRemoveResult}
9
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
10
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
11
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
12
+ * @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
13
+ * @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's schema
14
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
15
+ */
16
+
17
+ /**
18
+ * Remove existing record by it's ID. All attachments bound to this record will also be removed forever.
19
+ *
20
+ * Example:
21
+ * ```javascript
22
+ * const { recordRemove } = this.app.dobo
23
+ * const result = await recordRemove('CdbCountry', 'ID')
24
+ * ```
25
+ *
26
+ * @method
27
+ * @memberof Dobo
28
+ * @async
29
+ * @instance
30
+ * @name recordRemove
31
+ * @param {string} name - Model's name
32
+ * @param {(string|number)} id - Record's ID
33
+ * @param {TRecordRemoveOptions} [options={}]
34
+ * @returns {(TRecordRemoveResult|Object)} Return the removed record if ```options.dataOnly``` is set. {@link TRecordRemoveResult} otherwise
35
+ */
36
+ async function remove (name, id, opts = {}) {
37
+ const { runHook } = this.app.bajo
38
+ const { clearModel } = this.cache ?? {}
39
+ const { cloneDeep, camelCase, omit } = this.app.lib._
40
+ delete opts.record
41
+ const options = cloneDeep(omit(opts, ['req', 'reply']))
42
+ options.req = opts.req
43
+ options.reply = opts.reply
44
+ options.dataOnly = options.dataOnly ?? true
45
+ const { fields, dataOnly, noHook, noResult, noFeatureHook, hidden, forceNoHidden } = options
46
+ options.dataOnly = false
47
+ await this.modelExists(name, true)
48
+ const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-remove', options)
49
+ id = this.sanitizeId(id, schema)
50
+ if (!noHook) {
51
+ await runHook(`${this.ns}:beforeRecordRemove`, name, id, options)
52
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordRemove`, id, options)
53
+ }
54
+ if (!noFeatureHook) await execFeatureHook.call(this, 'beforeRemove', { schema, id, options })
55
+ const record = options.record ?? (await handler.call(this.app[driver.ns], { schema, id, options }))
56
+ delete options.record
57
+ if (options.req) {
58
+ if (options.req.file) await handleAttachmentUpload.call(this, { name: schema.name, id, options, action: 'remove' })
59
+ if (options.req.flash && !options.noFlash) options.req.flash('notify', options.req.t('recordRemoved'))
60
+ }
61
+ if (clearModel) await clearModel({ model: name, id, options, record })
62
+ if (noResult) return
63
+ record.oldData = options.record ? options.record.oldData : (await this.pickRecord({ record: record.oldData, fields, schema, hidden, forceNoHidden }))
64
+ if (!noHook) {
65
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordRemove`, id, options, record)
66
+ await runHook(`${this.ns}:afterRecordRemove`, name, id, options, record)
67
+ }
68
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterRemove', { schema, id, options, record })
69
+ return dataOnly ? record.oldData : record
70
+ }
71
+
72
+ export default remove
@@ -5,11 +5,52 @@ import execValidation from '../../lib/exec-validation.js'
5
5
  import execFeatureHook from '../../lib/exec-feature-hook.js'
6
6
  import singleRelRows from '../../lib/single-rel-rows.js'
7
7
 
8
+ /**
9
+ * @typedef {Object} TRecordUpdateOptions
10
+ * @see Dobo#recordUpdate
11
+ * @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordUpdateResult}
12
+ * @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
13
+ * @property {boolean} [noFeatureHook=false] - If ```true```, no model's feature hook will be executed
14
+ * @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
15
+ * @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
16
+ * @property {boolean} [noSanitize=false] - If ```true```, accept data payload as is without sanitization
17
+ * @property {boolean} [noResult=false] - If ```true```, returns nothing
18
+ * @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its schema's ```maxLemngth```
19
+ * @property {boolean} [partial=true] - If ```true``` (default), only updated values are saved. Otherwise replace all existing values with given payload
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 schema
22
+ * @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
23
+ */
24
+
25
+ /**
26
+ * Update a record by it's ID and body payload
27
+ *
28
+ * Example:
29
+ * ```javascript
30
+ * const { recordUpdate } = this.app.dobo
31
+ * const { body } = {
32
+ * name: 'Republic of Indonesia',
33
+ * phoneCode: '+62'
34
+ * }
35
+ * const result = await recordUpdate('CdbCountry', 'ID', body)
36
+ * ```
37
+ *
38
+ * @method
39
+ * @memberof Dobo
40
+ * @async
41
+ * @instance
42
+ * @name recordUpdate
43
+ * @param {string} name - Model's name
44
+ * @param {(string|number)} id - Record's ID
45
+ * @param {Object} body - Body payload
46
+ * @param {TRecordUpdateOptions} [options={}]
47
+ * @returns {(TRecordUpdateResult|Object)} Returns updated record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} otherwise
48
+ */
8
49
  async function update (name, id, input, opts = {}) {
9
- const { isSet } = this.lib.aneka
50
+ const { isSet } = this.app.lib.aneka
10
51
  const { runHook } = this.app.bajo
11
52
  const { clearModel } = this.cache ?? {}
12
- const { forOwn, find, cloneDeep, camelCase, omit, get } = this.lib._
53
+ const { forOwn, find, cloneDeep, camelCase, omit, get } = this.app.lib._
13
54
  delete opts.record
14
55
  const options = cloneDeep(omit(opts, ['req', 'reply']))
15
56
  options.req = opts.req
@@ -26,8 +67,8 @@ async function update (name, id, input, opts = {}) {
26
67
  let body = noSanitize ? input : await this.sanitizeBody({ body: input, schema, partial, strict: true, extFields })
27
68
  delete body.id
28
69
  if (!noHook) {
29
- await runHook(`${this.name}:beforeRecordUpdate`, name, id, body, options)
30
- await runHook(`${this.name}.${camelCase(name)}:beforeRecordUpdate`, id, body, options)
70
+ await runHook(`${this.ns}:beforeRecordUpdate`, name, id, body, options)
71
+ await runHook(`${this.ns}.${camelCase(name)}:beforeRecordUpdate`, id, body, options)
31
72
  }
32
73
  if (!noValidation) body = await execValidation.call(this, { name, body, options, partial })
33
74
  if (!noCheckUnique) await checkUnique.call(this, { schema, body, id })
@@ -53,8 +94,8 @@ async function update (name, id, input, opts = {}) {
53
94
  record.oldData = await this.pickRecord({ record: record.oldData, fields, schema, hidden, forceNoHidden })
54
95
  record.data = await this.pickRecord({ record: record.data, fields, schema, hidden, forceNoHidden })
55
96
  if (!noHook) {
56
- await runHook(`${this.name}.${camelCase(name)}:afterRecordUpdate`, id, nbody, options, record)
57
- await runHook(`${this.name}:afterRecordUpdate`, name, id, nbody, options, record)
97
+ await runHook(`${this.ns}.${camelCase(name)}:afterRecordUpdate`, id, nbody, options, record)
98
+ await runHook(`${this.ns}:afterRecordUpdate`, name, id, nbody, options, record)
58
99
  }
59
100
  if (!noFeatureHook) await execFeatureHook.call(this, 'afterUpdate', { schema, body: nbody, record })
60
101
  return dataOnly ? record.data : record
@@ -1,7 +1,23 @@
1
+ /**
2
+ * Update a record by payload's ID. If no record is found by given ID, a new one will be created instead.
3
+ *
4
+ * Missing ID in payload always results a new record creation.
5
+ * ```
6
+ *
7
+ * @method
8
+ * @memberof Dobo
9
+ * @async
10
+ * @instance
11
+ * @name recordUpsert
12
+ * @param {string} name - Model's name
13
+ * @param {Object} body - Body payload
14
+ * @param {TRecordUpsertOptions} [options={}]
15
+ * @returns {(TRecordUpdateResult|TRecordCreateResult|Object)} Returns updated/newly created record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} or {@link TRecordCreateResult} otherwise
16
+ */
1
17
  async function upsert (name, input, opts = {}) {
2
18
  const { generateId } = this.app.bajo
3
- const { find } = this.lib._
4
- const { cloneDeep, omit, merge } = this.lib._
19
+ const { find } = this.app.lib._
20
+ const { cloneDeep, omit, merge } = this.app.lib._
5
21
  const { query, omitOnUpdate = [], omitOnCreate = [] } = opts
6
22
  const options = cloneDeep(omit(opts, ['req', 'reply', 'query', 'omitOnUpdate', 'omitOnCreate']))
7
23
  options.req = opts.req
@@ -1,8 +1,23 @@
1
+ /**
2
+ * Sanitize payload body against schema
3
+ *
4
+ * @method
5
+ * @memberof Dobo
6
+ * @async
7
+ * @param {Object} [options={}]
8
+ * @param {Object} [options.body={}]
9
+ * @param {Object} [options.schema={}]
10
+ * @param {boolean} [options.partial=false]
11
+ * @param {boolean} [options.strict=false]
12
+ * @param {Array} [options.extFields=[]]
13
+ * @returns {Object}
14
+ */
15
+
1
16
  async function sanitizeBody ({ body = {}, schema = {}, partial, strict, extFields = [] }) {
2
- const { isSet } = this.lib.aneka
3
- const { dayjs } = this.lib
17
+ const { isSet } = this.app.lib.aneka
18
+ const { dayjs } = this.app.lib
4
19
  const { callHandler } = this.app.bajo
5
- const { has, isString, isNumber, concat, isNaN } = this.lib._
20
+ const { has, isString, isNumber, concat, isNaN } = this.app.lib._
6
21
  const result = {}
7
22
  for (const p of concat(schema.properties, extFields)) {
8
23
  if (partial && !has(body, p.name)) continue
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Sanitize value as a date/time value. Parse/format string using {@link https://day.js.org/docs/en/display/format|dayjs format}
3
+ *
4
+ * @method
5
+ * @memberof Dobo
6
+ * @param {(number|string)} value - Value to sanitize
7
+ * @param {Object} [options={}] - Options object
8
+ * @param {boolean} [options.silent=true] - If ```true``` (default) and value isn't valid, returns empty
9
+ * @param {string} [options.inputFormat] - If provided, parse value using this option
10
+ * @param {string} [options.outputFormat] - If not provided or ```native```, returns Javascript Date. Otherwise returns formatted date/time string
11
+ * @returns {(string|Date)}
12
+ */
13
+
14
+ function sanitizeDate (value, { inputFormat, outputFormat, silent = true } = {}) {
15
+ const { dayjs } = this.app.lib
16
+ if (value === 0) return null
17
+ if (!outputFormat) outputFormat = inputFormat
18
+ const dt = dayjs(value, inputFormat)
19
+ if (!dt.isValid()) {
20
+ if (silent) return null
21
+ throw this.error('invalidDate')
22
+ }
23
+ if (outputFormat === 'native' || !outputFormat) return dt.toDate()
24
+ return dt.format(outputFormat)
25
+ }
26
+
27
+ export default sanitizeDate
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Sanitize id according it's schema
3
+ *
4
+ * @method
5
+ * @memberof Dobo
6
+ * @param {(number|string)} id
7
+ * @param {Object} schema
8
+ * @returns {(number|string)}
9
+ */
10
+
1
11
  function sanitizeId (id, schema) {
2
12
  const prop = schema.properties.find(p => p.name === 'id')
3
13
  if (prop.type === 'integer') id = parseInt(id)