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.
- package/.github/FUNDING.yml +13 -0
- package/.github/workflows/repo-lockdown.yml +24 -0
- package/.jsdoc.conf.json +45 -0
- package/LICENSE +1 -1
- package/README.md +38 -19
- package/docs/Dobo.html +26 -0
- package/docs/data/search.json +1 -0
- package/docs/fonts/Inconsolata-Regular.ttf +0 -0
- package/docs/fonts/OpenSans-Regular.ttf +0 -0
- package/docs/fonts/WorkSans-Bold.ttf +0 -0
- package/docs/global.html +7 -0
- package/docs/index.html +3 -0
- package/docs/index.js.html +578 -0
- package/docs/lib_collect-connections.js.html +39 -0
- package/docs/lib_collect-drivers.js.html +52 -0
- package/docs/lib_collect-features.js.html +36 -0
- package/docs/lib_collect-schemas.js.html +94 -0
- package/docs/lib_index.js.html +6 -0
- package/docs/method_model_create.js.html +35 -0
- package/docs/method_model_drop.js.html +34 -0
- package/docs/method_model_exists.js.html +40 -0
- package/docs/method_record_count.js.html +69 -0
- package/docs/method_record_create.js.html +114 -0
- package/docs/method_record_find-all.js.html +44 -0
- package/docs/method_record_find-one.js.html +73 -0
- package/docs/method_record_find.js.html +118 -0
- package/docs/method_record_get.js.html +92 -0
- package/docs/method_record_remove.js.html +75 -0
- package/docs/method_record_update.js.html +107 -0
- package/docs/method_record_upsert.js.html +54 -0
- package/docs/method_sanitize_body.js.html +88 -0
- package/docs/method_sanitize_date.js.html +30 -0
- package/docs/method_sanitize_id.js.html +20 -0
- package/docs/method_validate.js.html +249 -0
- package/docs/module-Lib.html +3 -0
- package/docs/scripts/core.js +725 -0
- package/docs/scripts/core.min.js +23 -0
- package/docs/scripts/resize.js +90 -0
- package/docs/scripts/search.js +265 -0
- package/docs/scripts/search.min.js +6 -0
- package/docs/scripts/third-party/Apache-License-2.0.txt +202 -0
- package/docs/scripts/third-party/fuse.js +9 -0
- package/docs/scripts/third-party/hljs-line-num-original.js +366 -0
- package/docs/scripts/third-party/hljs-line-num.js +1 -0
- package/docs/scripts/third-party/hljs-original.js +5164 -0
- package/docs/scripts/third-party/hljs.js +1 -0
- package/docs/scripts/third-party/popper.js +5 -0
- package/docs/scripts/third-party/tippy.js +1 -0
- package/docs/scripts/third-party/tocbot.js +671 -0
- package/docs/scripts/third-party/tocbot.min.js +1 -0
- package/docs/static/bitcoin.jpeg +0 -0
- package/docs/static/home.md +25 -0
- package/docs/static/logo-ecosystem.png +0 -0
- package/docs/static/logo.png +0 -0
- package/docs/styles/clean-jsdoc-theme-base.css +1159 -0
- package/docs/styles/clean-jsdoc-theme-dark.css +412 -0
- package/docs/styles/clean-jsdoc-theme-light.css +482 -0
- package/docs/styles/clean-jsdoc-theme-scrollbar.css +30 -0
- package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +1 -0
- package/docs/styles/clean-jsdoc-theme.min.css +1 -0
- package/extend/bajo/intl/en-US.json +69 -30
- package/extend/bajo/intl/id.json +58 -29
- package/extend/bajoCli/applet/clear-record.js +22 -0
- package/extend/bajoCli/applet/connection.js +5 -5
- package/extend/bajoCli/applet/count-record.js +27 -0
- package/extend/bajoCli/applet/create-aggregate.js +33 -0
- package/extend/bajoCli/applet/create-histogram.js +33 -0
- package/extend/bajoCli/applet/create-record.js +39 -0
- package/extend/bajoCli/applet/find-record.js +27 -0
- package/extend/bajoCli/applet/get-record.js +27 -0
- package/extend/bajoCli/applet/lib/post-process.js +25 -26
- package/extend/bajoCli/applet/model.js +22 -0
- package/extend/bajoCli/applet/rebuild-model.js +91 -0
- package/extend/bajoCli/applet/remove-record.js +27 -0
- package/extend/bajoCli/applet/update-record.js +44 -0
- package/extend/bajoCli/applet.js +0 -0
- package/extend/dobo/driver/memory.js +170 -0
- package/extend/dobo/feature/created-at.js +10 -8
- package/extend/dobo/feature/dt.js +0 -0
- package/extend/dobo/feature/immutable.js +30 -0
- package/extend/dobo/feature/int-id.js +0 -0
- package/extend/dobo/feature/removed-at.js +35 -57
- package/extend/dobo/feature/updated-at.js +14 -12
- package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +5 -9
- package/extend/waibuStatic/virtual.json +0 -0
- package/index.js +420 -337
- package/lib/collect-connections.js +60 -21
- package/lib/collect-drivers.js +29 -35
- package/lib/collect-features.js +40 -0
- package/lib/collect-models.js +319 -0
- package/lib/factory/action.js +161 -0
- package/lib/factory/connection.js +62 -0
- package/lib/factory/driver.js +358 -0
- package/lib/factory/feature.js +33 -0
- package/lib/factory/model/_util.js +402 -0
- package/lib/factory/model/build.js +15 -0
- package/lib/factory/model/clear-record.js +17 -0
- package/lib/factory/model/count-record.js +17 -0
- package/lib/factory/model/create-aggregate.js +17 -0
- package/lib/factory/model/create-attachment.js +29 -0
- package/lib/factory/model/create-histogram.js +17 -0
- package/lib/factory/model/create-record.js +35 -0
- package/lib/factory/model/drop.js +15 -0
- package/lib/factory/model/exists.js +21 -0
- package/lib/factory/model/find-all-record.js +71 -0
- package/lib/factory/model/find-attachment.js +29 -0
- package/lib/factory/model/find-one-record.js +19 -0
- package/lib/factory/model/find-record.js +103 -0
- package/lib/factory/model/get-attachment.js +15 -0
- package/lib/factory/model/get-record.js +79 -0
- package/lib/factory/model/list-attachment.js +37 -0
- package/lib/{add-fixtures.js → factory/model/load-fixtures.js} +69 -67
- package/lib/factory/model/remove-attachment.js +15 -0
- package/lib/factory/model/remove-record.js +59 -0
- package/lib/factory/model/sanitize-body.js +62 -0
- package/lib/factory/model/sanitize-id.js +7 -0
- package/lib/factory/model/sanitize-record.js +26 -0
- package/lib/factory/model/update-attachment.js +9 -0
- package/lib/factory/model/update-record.js +81 -0
- package/lib/factory/model/upsert-record.js +95 -0
- package/lib/factory/model/validate.js +232 -0
- package/lib/factory/model.js +150 -0
- package/lib/index.js +3 -0
- package/package.json +45 -36
- package/wiki/APPLETS.md +57 -0
- package/wiki/CHANGES.md +46 -0
- package/wiki/CONFIG.md +25 -0
- package/wiki/CONTRIBUTING.md +5 -0
- package/wiki/DEV-GUIDE.md +1 -0
- package/wiki/ECOSYSTEM.md +20 -0
- package/wiki/GETTING-STARTED.md +166 -0
- package/{docs/query-language.md → wiki/QUERY-LANGUAGE.md} +0 -0
- package/wiki/USER-GUIDE.md +1 -0
- package/extend/bajoCli/applet/model-clear.js +0 -11
- package/extend/bajoCli/applet/model-rebuild.js +0 -101
- package/extend/bajoCli/applet/record-create.js +0 -41
- package/extend/bajoCli/applet/record-find.js +0 -27
- package/extend/bajoCli/applet/record-get.js +0 -24
- package/extend/bajoCli/applet/record-remove.js +0 -24
- package/extend/bajoCli/applet/record-update.js +0 -47
- package/extend/bajoCli/applet/schema.js +0 -22
- package/extend/bajoCli/applet/stat-count.js +0 -24
- package/lib/build-bulk-action.js +0 -12
- package/lib/check-unique.js +0 -39
- package/lib/collect-feature.js +0 -25
- package/lib/collect-schemas.js +0 -83
- package/lib/exec-feature-hook.js +0 -13
- package/lib/exec-validation.js +0 -21
- package/lib/generic-prop-sanitizer.js +0 -31
- package/lib/handle-attachment-upload.js +0 -16
- package/lib/mem-db/conn-sanitizer.js +0 -8
- package/lib/mem-db/instantiate.js +0 -41
- package/lib/mem-db/method/model/clear.js +0 -6
- package/lib/mem-db/method/model/create.js +0 -5
- package/lib/mem-db/method/model/drop.js +0 -5
- package/lib/mem-db/method/model/exists.js +0 -5
- package/lib/mem-db/method/record/create.js +0 -12
- package/lib/mem-db/method/record/find.js +0 -20
- package/lib/mem-db/method/record/get.js +0 -9
- package/lib/mem-db/method/record/remove.js +0 -13
- package/lib/mem-db/method/record/update.js +0 -15
- package/lib/mem-db/method/stat/count.js +0 -11
- package/lib/mem-db/start.js +0 -25
- package/lib/merge-attachment-info.js +0 -16
- package/lib/multi-rel-rows.js +0 -42
- package/lib/resolve-method.js +0 -16
- package/lib/sanitize-schema.js +0 -197
- package/lib/single-rel-rows.js +0 -38
- package/method/attachment/copy-uploaded.js +0 -34
- package/method/attachment/create.js +0 -29
- package/method/attachment/find.js +0 -27
- package/method/attachment/get-path.js +0 -12
- package/method/attachment/get.js +0 -12
- package/method/attachment/pre-check.js +0 -9
- package/method/attachment/remove.js +0 -11
- package/method/attachment/update.js +0 -7
- package/method/bulk/create.js +0 -46
- package/method/model/clear.js +0 -22
- package/method/model/create.js +0 -19
- package/method/model/drop.js +0 -19
- package/method/model/exists.js +0 -24
- package/method/record/clear.js +0 -24
- package/method/record/count.js +0 -44
- package/method/record/create.js +0 -71
- package/method/record/find-all.js +0 -25
- package/method/record/find-one.js +0 -56
- package/method/record/find.js +0 -52
- package/method/record/get.js +0 -47
- package/method/record/remove.js +0 -41
- package/method/record/update.js +0 -63
- package/method/record/upsert.js +0 -35
- package/method/sanitize/body.js +0 -70
- package/method/sanitize/date.js +0 -14
- package/method/sanitize/id.js +0 -7
- package/method/stat/aggregate.js +0 -23
- package/method/stat/histogram.js +0 -26
- package/method/validate.js +0 -157
|
@@ -1,25 +1,64 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
package/lib/collect-drivers.js
CHANGED
|
@@ -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,
|
|
3
|
-
const {
|
|
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
|
-
|
|
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
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|