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
@@ -1,25 +1,64 @@
1
- async function defSanitizer (item) {
2
- return item
3
- }
1
+ import connectionFactory from './factory/connection.js'
2
+
3
+ /**
4
+ * Object to be passed as connection info in dobo config object
5
+ *
6
+ * @typedef TConnectionInfo
7
+ * @memberof module:Lib
8
+ * @property {string} name - Must be unique along all connections. Required
9
+ * @property {string} driver - Driver to use. If not in ```TNsPath``` format, it will be autodetected. Required
10
+ * @property {string} [host]
11
+ * @property {number} [port]
12
+ * @property {string} [username]
13
+ * @property {string} [password]
14
+ * @property {string} database - Database name. Required
15
+ * @property {string[]} [models] - List of models forced to use this connection
16
+ * @property {Object} [options] - Options specific to the driver
17
+ */
18
+
19
+ /**
20
+ * Collect all database connections from {@tutorial config}.
21
+ *
22
+ * @name collectConnections
23
+ * @memberof module:Lib
24
+ * @async
25
+ * @see Dobo#init
26
+ * @param {Object} [options={}]
27
+ * @param {Object} [options.item={}]
28
+ * @returns {Object}
29
+ */
30
+ async function collectConnections () {
31
+ const { buildCollections } = this.app.bajo
32
+ const { pullAt } = this.app.lib._
33
+ const { filterIndex } = this.app.lib.aneka
34
+ const DoboConnection = await connectionFactory.call(this)
4
35
 
5
- async function collectConnections ({ item, index, options }) {
6
- const conn = item
7
- const { importModule, breakNsPath } = this.app.bajo
8
- const { has, find, isEmpty } = this.lib._
9
- if (!has(conn, 'type')) this.fatal('mustValidDbType')
10
- let { ns, path: type } = breakNsPath(conn.type)
11
- if (isEmpty(type)) type = conn.type
12
- const driver = find(this.drivers, { ns, type })
13
- if (!driver) this.fatal('unsupportedDbType%s', conn.type)
14
- let file = `${ns}:/extend/${this.name}/lib/${type}/conn-sanitizer.js`
15
- if (conn.type === 'dobo:memory') file = `${ns}:/lib/mem-db/conn-sanitizer.js`
16
- if (driver.provider) file = `${driver.provider}:/extend/${ns}/lib/${type}/conn-sanitizer.js`
17
- let sanitizer = await importModule(file)
18
- if (!sanitizer) sanitizer = defSanitizer
19
- const result = await sanitizer.call(this, conn)
20
- result.proxy = result.proxy ?? false
21
- result.driver = driver.driver
22
- return result
36
+ async function handler ({ item }) {
37
+ const { has } = this.app.lib._
38
+ if (!has(item, 'driver')) this.fatal('unknownDbDriver%s')
39
+ let driver
40
+ try {
41
+ driver = this.getDriver(item.driver, true)
42
+ if (!driver) throw new Error()
43
+ } catch (err) {
44
+ this.fatal('unknownDbDriver%s', item.driver)
45
+ }
46
+ await driver.sanitizeConnection(item)
47
+ const conn = new DoboConnection(this, item)
48
+ conn.driver = driver
49
+ return conn
50
+ }
51
+ const memIndexes = filterIndex(this.config.connections, current => current.driver === 'dobo:memory' || current.name === 'memory')
52
+ const models = memIndexes.map(idx => [...(this.config.connections[idx].models ?? [])])
53
+ pullAt(this.config.connections, memIndexes)
54
+ this.config.connections.unshift({
55
+ driver: 'dobo:memory',
56
+ name: 'memory',
57
+ models
58
+ })
59
+ this.connections = await buildCollections({ ns: this.ns, container: 'connections', handler, dupChecks: ['name'] })
60
+ const defConn = this.connections.find(conn => conn.name === 'default')
61
+ if (!defConn) this.log.warn('noDefaultConnection')
23
62
  }
24
63
 
25
64
  export default collectConnections
@@ -1,41 +1,35 @@
1
+ import path from 'path'
2
+ import driverFactory from './factory/driver.js'
3
+
4
+ /**
5
+ * Collect all database drivers from loaded plugins
6
+ *
7
+ * @name collectDrivers
8
+ * @memberof module:Lib
9
+ * @async
10
+ * @see Dobo#init
11
+ */
1
12
  async function collectDrivers () {
2
- const { eachPlugins, readConfig, runHook } = this.app.bajo
3
- const { isString, find, pick, merge, cloneDeep } = this.lib._
13
+ const { eachPlugins, runHook } = this.app.bajo
14
+ const { importModule } = this.app.bajo
15
+ const { camelCase, isFunction } = this.app.lib._
16
+ const DoboDriver = await driverFactory.call(this)
17
+
18
+ this.log.trace('collecting%s', this.t('driver'))
4
19
  const me = this
5
- me.drivers = []
6
- // built-in memory driver
7
- me.drivers.push({
8
- type: 'memory',
9
- ns: me.name,
10
- driver: 'memory',
11
- idField: merge(cloneDeep(me.config.default.idField), { name: 'id' })
12
- })
13
- // others
14
- await runHook(`${this.name}:beforeCollectDrivers`)
20
+ await runHook(`${this.ns}:beforeCollectDrivers`)
15
21
  await eachPlugins(async function ({ file }) {
16
- const { name: ns } = this
17
- const info = await readConfig(file, { ns })
18
- if (!info.type) this.fatal('driverMustProvideDbType')
19
- if (!info.driver) this.fatal('driverMustHaveName')
20
- if (isString(info.type)) info.type = [info.type]
21
- if (!info.idField) info.idField = cloneDeep(me.config.default.idField)
22
- info.idField.name = 'id'
23
- for (const t of info.type) {
24
- const [type, provider] = t.split('@')
25
- const exists = find(me.drivers, { type, ns })
26
- if (exists) this.fatal('dbTypeAlreadySupportedByDriver%s%s', type, info.driver)
27
- const driver = pick(find(me.app[ns].drivers, { name: type }) ?? {}, ['dialect', 'idField', 'lowerCaseModel', 'returning'])
28
- const ext = {
29
- type,
30
- ns,
31
- provider,
32
- driver: info.driver,
33
- idField: info.idField
34
- }
35
- me.drivers.push(merge(ext, driver))
36
- }
37
- }, { glob: 'boot/driver.*', prefix: this.name })
38
- await runHook(`${this.name}:afterCollectDrivers`)
22
+ const name = camelCase(path.basename(file, '.js'))
23
+ const factory = await importModule(file)
24
+ if (!isFunction(factory)) this.fatal('invalidDriverClassFactory%s%s', this.ns, name)
25
+ const Cls = await factory.call(this)
26
+ const instance = new Cls(this, name)
27
+ if (!(instance instanceof DoboDriver)) this.fatal('invalidDriverClass%s%s', this.ns, name)
28
+ me.drivers.push(instance)
29
+ me.log.trace('- %s', name)
30
+ }, { glob: 'driver/*.js', prefix: this.ns })
31
+ await runHook(`${this.ns}:afterCollectDrivers`)
32
+ this.log.debug('collected%s%d', this.t('driver'), this.drivers.length)
39
33
  }
40
34
 
41
35
  export default collectDrivers
@@ -0,0 +1,40 @@
1
+ import path from 'path'
2
+ import featureFactory from './factory/feature.js'
3
+
4
+ /**
5
+ * Object to be passed as feature input in ```{model}.features```
6
+ *
7
+ * @typedef TFeatureInput
8
+ * @memberof module:Lib
9
+ * @property {string} name - Accept ```TNsPath```. If standard string is given, ```ns``` is set to ```dobo```
10
+ * @property {any} [param]
11
+ */
12
+
13
+ /**
14
+ * Collect all database features from all loaded plugins
15
+ *
16
+ * @name collectFeatures
17
+ * @memberof module:Lib
18
+ * @async
19
+ * @see Dobo#init
20
+ */
21
+ async function collectFeature () {
22
+ const { eachPlugins } = this.app.bajo
23
+ const DoboFeature = await featureFactory.call(this)
24
+
25
+ this.log.trace('collecting%s', this.t('feature'))
26
+ const me = this
27
+ await eachPlugins(async function ({ file }) {
28
+ const { importModule } = this.app.bajo
29
+ const { camelCase, isFunction } = this.app.lib._
30
+
31
+ const name = camelCase(path.basename(file, '.js'))
32
+ const handler = await importModule(file)
33
+ if (!isFunction(handler)) this.fatal('invalidFeatureHandler%s%s', this.ns, name)
34
+ me.features.push(new DoboFeature(this, { name, handler }))
35
+ me.log.trace('- %s:%s', this.ns, name)
36
+ }, { glob: 'feature/*.js', prefix: this.ns })
37
+ this.log.debug('collected%s%d', this.t('feature'), this.features.length)
38
+ }
39
+
40
+ export default collectFeature
@@ -0,0 +1,319 @@
1
+ import path from 'path'
2
+ import modelFactory from './factory/model.js'
3
+
4
+ /**
5
+ * Sanitize one single property of a model
6
+ *
7
+ * @param {Object} model - Loaded model
8
+ * @param {Object} prop - Property to check
9
+ * @param {Array} [indexes] - Container array to fill up found index
10
+ */
11
+ async function sanitizeProp (model, prop, indexes) {
12
+ const { isEmpty, isString, keys, pick } = this.app.lib._
13
+ const allPropKeys = this.getAllPropertyKeys(model.connection.driver)
14
+ const propType = this.constructor.propertyType
15
+ if (isString(prop)) {
16
+ let [name, type, maxLength, idx, required] = prop.split(',').map(m => m.trim())
17
+ if (isEmpty(type)) type = 'string'
18
+ maxLength = isEmpty(maxLength) ? propType.string.maxLength : parseInt(maxLength)
19
+ prop = { name, type, maxLength }
20
+ if (!isEmpty(idx)) prop.index = idx
21
+ prop.required = required === 'true'
22
+ }
23
+ prop.type = prop.type ?? 'string'
24
+ if (prop.index) {
25
+ if (prop.index === true || prop.index === 'true') prop.index = 'index'
26
+ const [idx, idxName] = prop.index.split(':')
27
+ const index = { name: idxName ?? `${model.collName}_${prop.name}_${idx}`, fields: [prop.name], type: idx }
28
+ indexes.push(index)
29
+ }
30
+ if (prop.hidden) model.hidden.push(prop.name)
31
+ if (keys(propType).includes(prop.type)) model.properties.push(pick(prop, allPropKeys))
32
+ else {
33
+ const feature = this.getFeature(prop.type)
34
+ if (!feature) this.fatal('unknownPropType%s%s', prop.type, model.name)
35
+ await applyFeature.call(this, model, feature, { name: prop.name }, indexes)
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Collect all properties it can be found on model
41
+ *
42
+ * @param {Object} model - Model
43
+ * @param {Array} [inputs] - Array of properties
44
+ * @param {Array} [indexes] - Container array to fill up found index
45
+ */
46
+ async function findAllProps (model, inputs = [], indexes = [], isExtender) {
47
+ const { isPlainObject, cloneDeep } = this.app.lib._
48
+ const isIdProp = inputs.find(p => {
49
+ return isPlainObject(p) ? p.name === 'id' : p.startsWith('id,')
50
+ })
51
+ if (!isExtender && !isIdProp) {
52
+ const idField = cloneDeep(model.connection.driver.idField)
53
+ idField.name = 'id'
54
+ inputs.unshift(idField)
55
+ }
56
+ for (const prop of inputs) {
57
+ await sanitizeProp.call(this, model, prop, indexes)
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Apply each feature found
63
+ * @param {Object} model - Model
64
+ * @param {Object} feature - Feature to apply
65
+ * @param {Object} options - Options to the feature
66
+ * @returns {Array} New properties found in feature
67
+ */
68
+ async function applyFeature (model, feature, options, indexes) {
69
+ const { isArray } = this.app.lib._
70
+ const item = await feature.handler(options)
71
+ if (item.rules) model.rules.push(...item.rules)
72
+ if (!isArray(item.properties)) item.properties = [item.properties]
73
+ for (const prop of item.properties) {
74
+ await sanitizeProp.call(this, model, prop, indexes)
75
+ }
76
+ if (item.hooks) {
77
+ item.hooks = item.hooks.map(hook => {
78
+ hook.level = hook.level ?? 999
79
+ return hook
80
+ })
81
+ model.hooks.push(...item.hooks)
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Collect all features it can be found on model
87
+ *
88
+ * @param {Object} model - Model
89
+ * @param {Array} [inputs] - Array of properties
90
+ */
91
+ async function findAllFeats (model, inputs = [], indexes = []) {
92
+ const { isString, omit } = this.app.lib._
93
+ for (let feat of inputs) {
94
+ if (isString(feat)) feat = { name: feat }
95
+ const featName = feat.name.indexOf(':') === -1 ? `dobo:${feat.name}` : feat.name
96
+ const feature = this.app.dobo.getFeature(featName)
97
+ if (!feature) this.fatal('invalidFeature%s%s', model.name, featName)
98
+ await applyFeature.call(this, model, feature, omit(feat, 'name'), indexes)
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Collect all indexes it can be found on model
104
+ *
105
+ * @param {Object} model - Model
106
+ * @param {Array} [inputs] - Array of properties
107
+ */
108
+ async function findAllIndexes (model, inputs = []) {
109
+ const indexes = []
110
+ for (const index of inputs) {
111
+ index.type = index.type ?? 'index'
112
+ index.fields = index.fields ?? []
113
+ if (!index.name) index.name = `${model.name}_${index.fields.join('_')}_${index.type}`
114
+ indexes.push(index)
115
+ }
116
+ model.indexes = indexes
117
+ }
118
+
119
+ /**
120
+ * Sanitize any reference/relationship found in properties
121
+ *
122
+ * @param {Object} model - Model
123
+ * @param {Array} [models] - All model match agaist. Defaults to ```dobo.models```
124
+ */
125
+ export async function sanitizeRef (model, models, fatal) {
126
+ const { find, isString, pullAt } = this.app.lib._
127
+ if (!models) models = this.models
128
+ for (const prop of model.properties) {
129
+ const ignored = []
130
+ for (const key in prop.ref ?? {}) {
131
+ let ref = prop.ref[key]
132
+ if (isString(ref)) {
133
+ ref = { propName: ref }
134
+ }
135
+ ref.type = ref.type ?? '1:1'
136
+ const rModel = find(models, { name: ref.model })
137
+ if (!rModel) {
138
+ if (fatal) this.fatal('unknownModelForRef%s%s%s', ref.model, model.name, prop.name)
139
+ else ignored.push(ref.model)
140
+ }
141
+ const rProp = find(rModel.properties, { name: ref.propName })
142
+ if (!rProp) {
143
+ if (fatal) this.fatal('unknownPropForRef%s%s%s%s', ref.model, ref.propName, model.name, prop.name)
144
+ else ignored.push(ref.model)
145
+ }
146
+ ref.fields = ref.fields ?? '*'
147
+ if (['*', 'all'].includes(ref.fields)) ref.fields = rModel.properties.map(p => p.name)
148
+ if (ref.fields.length > 0 && !ref.fields.includes('id')) ref.fields.unshift('id')
149
+ const removed = []
150
+ for (const idx in ref.fields) {
151
+ const p = find(rModel.properties, { name: ref.fields[idx] })
152
+ if (!p) removed.push(ref.fields[idx])
153
+ }
154
+ pullAt(ref.fields, removed)
155
+ prop.ref[key] = ref
156
+ }
157
+ for (const key of ignored) {
158
+ delete prop.ref[key]
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Sanitize all but reference, because a reference needs all models to be available first
165
+ *
166
+ * @param {Object} model - Model
167
+ */
168
+ export async function sanitizeAll (model) {
169
+ const { runHook } = this.app.bajo
170
+ const { pick, keys, map, uniq, camelCase, filter } = this.app.lib._
171
+ const { defaultsDeep } = this.app.lib.aneka
172
+ const allPropNames = uniq(map(model.properties, 'name'))
173
+ const propType = this.constructor.propertyType
174
+ const indexTypes = this.constructor.indexTypes
175
+
176
+ await runHook(`dobo.${camelCase(model.name)}:beforeSanitizeModel`, model)
177
+ // properties
178
+ for (const idx in model.properties) {
179
+ let prop = model.properties[idx]
180
+ const def = propType[prop.type]
181
+ prop = pick(defaultsDeep(prop, def), this.getPropertyKeysByType(prop.type))
182
+ if (!keys(propType).includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}.${def.name}`, prop.type)
183
+ if (prop.type === 'string') {
184
+ prop.minLength = parseInt(prop.minLength) ?? 0
185
+ prop.maxLength = parseInt(prop.maxLength) ?? 255
186
+ if (prop.minLength > 0) prop.required = true
187
+ if (prop.minLength === 0) delete prop.minLength
188
+ }
189
+ model.properties[idx] = prop
190
+ }
191
+ // indexes
192
+ for (const index of model.indexes) {
193
+ if (!indexTypes.includes(index.type)) this.fatal('unknownIndexType%s%s', index.type, model.name)
194
+ for (const field of index.fields) {
195
+ if (!allPropNames.includes(field)) this.fatal('unknownPropNameOnIndex%s%s', field, model.name)
196
+ }
197
+ }
198
+ await runHook(`dobo.${camelCase(model.name)}:afterSanitizeModel`, model)
199
+ // sortables
200
+ model.sortables = []
201
+ for (const index of model.indexes) {
202
+ model.sortables.push(...index.fields)
203
+ }
204
+ model.hidden = filter(uniq(model.hidden), prop => allPropNames.includes(prop))
205
+ }
206
+
207
+ /**
208
+ * Create schema for model
209
+ *
210
+ * @param {Object} item - Source item
211
+ * @returns {Object} Sanitized model
212
+ */
213
+ async function createSchema (item) {
214
+ const { readConfig } = this.app.bajo
215
+ const { fastGlob } = this.app.lib
216
+ const { find, isPlainObject, orderBy } = this.app.lib._
217
+ const { mergeObjectsByKey } = this.app.lib.aneka
218
+ if (item.file && !item.base) item.base = path.basename(item.file, path.extname(item.file))
219
+ item.attachment = item.attachment ?? true
220
+ const feats = item.features ?? []
221
+ const props = item.properties ?? []
222
+ const indexes = item.indexes ?? []
223
+ item.features = []
224
+ item.properties = []
225
+ item.indexes = []
226
+ item.hidden = item.hidden ?? []
227
+ item.rules = item.rules ?? []
228
+ item.buildLevel = item.buildLevel ?? 999
229
+ const conn = item.connection ?? 'default'
230
+ item.connection = null
231
+ item.hooks = item.hooks ?? []
232
+ item.disabled = item.disabled ?? []
233
+ if (item.disabled === 'all') item.disabled = ['find', 'get', 'create', 'update', 'remove']
234
+ else if (item.disabled === 'readonly') item.disabled = ['create', 'update', 'remove']
235
+ // Is there any overwritten connection?
236
+ const newConn = find(this.connections, c => c.options.models.includes(item.name))
237
+ if (newConn) item.connection = newConn
238
+ else {
239
+ item.connection = this.getConnection(conn, true)
240
+ if (!item.connection && conn === 'default') item.connection = this.getConnection('memory')
241
+ }
242
+ if (!item.connection) this.fatal('unknownConn%s%s', conn, item.name)
243
+ await findAllProps.call(this, item, props, indexes)
244
+ await findAllFeats.call(this, item, feats, indexes)
245
+ await findAllIndexes.call(this, item, indexes)
246
+ // item extender
247
+ if (item.base) {
248
+ for (const ns of this.app.getAllNs()) {
249
+ const plugin = this.app[ns]
250
+ const glob = `${plugin.dir.pkg}/extend/dobo/extend/${item.ns}/item/${item.base}.*`
251
+ const files = await fastGlob(glob)
252
+ for (const file of files) {
253
+ const extender = await readConfig(file, { ns: plugin.ns, ignoreError: true })
254
+ if (!isPlainObject(extender)) this.plugin.fatal('invalidModelExtender%s%s', ns, item.name)
255
+ await findAllProps.call(this, item, extender.properties ?? [], indexes, true)
256
+ await findAllFeats.call(this, item, extender.features ?? [], indexes, true)
257
+ await findAllIndexes.call(this, item, extender.indexes ?? [], true)
258
+ }
259
+ }
260
+ }
261
+ for (const key of ['properties', 'indexes']) {
262
+ item[key] = mergeObjectsByKey(item[key], 'name')
263
+ }
264
+ item.hooks = orderBy(item.hooks, ['name', 'level'])
265
+ delete item.features
266
+ delete item.base
267
+ await sanitizeAll.call(this, item)
268
+ return item
269
+ }
270
+
271
+ /**
272
+ * Collect all models from loaded plugins and create the models
273
+ *
274
+ * @name collectModels
275
+ * @memberof module:lib
276
+ * @async
277
+ * @see Dobo#init
278
+ */
279
+ async function collectModels () {
280
+ const { eachPlugins } = this.app.bajo
281
+ const { orderBy, has } = this.app.lib._
282
+ const DoboModel = await modelFactory.call(this)
283
+
284
+ this.log.trace('collecting%s', this.t('model'))
285
+ const me = this
286
+ let schemas = []
287
+ await eachPlugins(async function ({ file }) {
288
+ const { readConfig } = this.app.bajo
289
+ const { pascalCase } = this.app.lib.aneka
290
+ const { isPlainObject } = this.app.lib._
291
+
292
+ const base = path.basename(file, path.extname(file))
293
+ const defName = pascalCase(`${this.alias} ${base}`)
294
+ const item = await readConfig(file, { ns: this.ns, ignoreError: true })
295
+ if (!isPlainObject(item)) me.fatal('invalidModel%s', defName)
296
+ item.name = item.name ?? defName
297
+ item.collName = item.collName ?? item.name
298
+ item.file = file
299
+ const schema = await createSchema.call(me, item)
300
+ schema.ns = this.ns
301
+ schemas.push(item)
302
+ }, { glob: 'model/*.*', prefix: this.ns })
303
+ schemas = orderBy(schemas, ['buildLevel', 'name'])
304
+ for (const schema of schemas) {
305
+ const plugin = this.app[schema.ns]
306
+ delete schema.ns
307
+ await sanitizeRef.call(this, schema, schemas, true)
308
+ const idProp = schema.properties.find(p => p.name === 'id')
309
+ if (!this.constructor.idTypes.includes(idProp.type)) this.fatal('invalidIdType%s%s', schema.name, this.constructor.idTypes.join(', '))
310
+ if (idProp.type === 'string' && !has(idProp, 'maxLength')) idProp.maxLength = 50
311
+ // schema.properties = without(schema.properties, undefined)
312
+ const model = new DoboModel(plugin, schema)
313
+ me.models.push(model)
314
+ me.log.trace('- %s', model.name)
315
+ }
316
+ this.log.debug('collected%s%d', this.t('model'), this.models.length)
317
+ }
318
+
319
+ export default collectModels
@@ -0,0 +1,161 @@
1
+ const methods = {
2
+ createRecord: ['body'],
3
+ updateRecord: ['id', 'body'],
4
+ upsertRecord: ['body'],
5
+ removeRecord: ['id'],
6
+ getRecord: ['id'],
7
+ findRecord: ['filter'],
8
+ findOneRecord: ['filter'],
9
+ findAllRecord: ['filter'],
10
+ findAllRecords: ['filter'],
11
+ countRecord: ['params'],
12
+ createAggregate: ['params'],
13
+ createHistogram: ['params'],
14
+ createAttachment: ['id'],
15
+ findAttachment: ['id'],
16
+ getAttachment: ['id', 'fieldName', 'file'],
17
+ listAttachment: ['params'],
18
+ removeAttachment: ['id', 'fieldName', 'file'],
19
+ updateAttachment: ['id']
20
+ }
21
+
22
+ const options = {
23
+ truncateString: true,
24
+ noResult: true,
25
+ noBodySanitizer: true,
26
+ noResultSanitizer: true,
27
+ noValidation: true,
28
+ dataOnly: true,
29
+ noHook: true,
30
+ noModelHook: true,
31
+ extFields: [],
32
+ fields: [],
33
+ noFlash: true,
34
+ hidden: [],
35
+ refs: [],
36
+ types: [],
37
+ type: undefined,
38
+ group: undefined,
39
+ aggregates: [],
40
+ field: undefined,
41
+ queryHandler: undefined,
42
+ count: true,
43
+ noCache: true,
44
+ partial: true,
45
+ // attachment
46
+ mimeType: true,
47
+ fullPath: true,
48
+ stats: true,
49
+ dirOnly: true,
50
+ setField: undefined,
51
+ setFile: undefined,
52
+ source: undefined,
53
+ uriEncoded: true
54
+ }
55
+
56
+ async function actionFactory () {
57
+ const { Tools } = this.app.baseClass
58
+
59
+ /**
60
+ * Action class enables you to call model's method with all parameters and options in chainable methods
61
+ * in addition of the normall call. Examples:
62
+ *
63
+ * ```javascript
64
+ * // method 1:
65
+ * const result = await model.action().getRecord('rec-id').noHook().dataOnly(false).run()
66
+ * // method 2:
67
+ * const result = await model.action('getRecord').id('rec-id').noHook().dataOnly(false).run()
68
+ * // method 3:
69
+ * const result = await model.action('getRecord', 'rec-id').noHook().dataOnly(false).run()
70
+ * // method 4:
71
+ * const action = await model.getRecord() // Important: no parameter passed
72
+ * const result = action.id('rec-id').noHook().dataOnly(false).run()
73
+ * // Instead of chaining options as methods, you can also pass options object to the run() method
74
+ * const result = await model.action('getRecord', 'rec-id').run({ noHook: true, dataOnly: false })
75
+ * ```
76
+ *
77
+ * @class
78
+ */
79
+ class DoboAction extends Tools {
80
+ constructor (model, name, ...args) {
81
+ super(model.plugin)
82
+ this.model = model
83
+ this.name = name
84
+ this._options = {}
85
+ if (name) this._setArgs(name, args)
86
+ // create methods
87
+ for (const method in methods) {
88
+ this[method] = (...args) => {
89
+ this.name = method
90
+ this._setArgs(method, args)
91
+ this._options = {}
92
+ return this
93
+ }
94
+ }
95
+ // create options builder methods
96
+ for (const option in options) {
97
+ this[option] = (value) => {
98
+ this._options[option] = value ?? options[option]
99
+ return this
100
+ }
101
+ }
102
+ }
103
+
104
+ _setArgs = (method, args) => {
105
+ if (args.length === 0) return
106
+ for (const idx in methods[method]) {
107
+ this['_' + methods[method][idx]] = args[idx]
108
+ }
109
+ }
110
+
111
+ id = value => {
112
+ this._id = value
113
+ return this
114
+ }
115
+
116
+ body = value => {
117
+ this._body = value
118
+ return this
119
+ }
120
+
121
+ filter = value => {
122
+ this._filter = value
123
+ return this
124
+ }
125
+
126
+ fieldName = value => {
127
+ this._fieldName = value
128
+ return this
129
+ }
130
+
131
+ file = value => {
132
+ this._file = value
133
+ return this
134
+ }
135
+
136
+ options = (value = {}) => {
137
+ for (const k in value) {
138
+ this._options[k] = value
139
+ }
140
+ return this
141
+ }
142
+
143
+ run = async (value = {}) => {
144
+ this.options(value)
145
+ const args = methods[this.name].map(item => this['_' + item])
146
+ args.push(this._options)
147
+ return await this.model[this.name](...args)
148
+ }
149
+
150
+ dispose () {
151
+ super.dispose()
152
+ this.model = null
153
+ this._options = null
154
+ }
155
+ }
156
+
157
+ this.app.baseClass.DoboAction = DoboAction
158
+ return DoboAction
159
+ }
160
+
161
+ export default actionFactory