dobo 2.0.1 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/FUNDING.yml +0 -0
- package/.github/workflows/repo-lockdown.yml +0 -0
- package/.jsdoc.conf.json +0 -0
- package/LICENSE +0 -0
- package/README.md +2 -2
- package/docs/Dobo.html +0 -0
- package/docs/data/search.json +0 -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 +0 -0
- package/docs/index.html +0 -0
- package/docs/index.js.html +0 -0
- package/docs/lib_collect-connections.js.html +0 -0
- package/docs/lib_collect-drivers.js.html +0 -0
- package/docs/lib_collect-features.js.html +0 -0
- package/docs/lib_collect-schemas.js.html +0 -0
- package/docs/lib_index.js.html +0 -0
- package/docs/method_model_create.js.html +0 -0
- package/docs/method_model_drop.js.html +0 -0
- package/docs/method_model_exists.js.html +0 -0
- package/docs/method_record_count.js.html +0 -0
- package/docs/method_record_create.js.html +0 -0
- package/docs/method_record_find-all.js.html +0 -0
- package/docs/method_record_find-one.js.html +0 -0
- package/docs/method_record_find.js.html +0 -0
- package/docs/method_record_get.js.html +0 -0
- package/docs/method_record_remove.js.html +0 -0
- package/docs/method_record_update.js.html +0 -0
- package/docs/method_record_upsert.js.html +0 -0
- package/docs/method_sanitize_body.js.html +0 -0
- package/docs/method_sanitize_date.js.html +0 -0
- package/docs/method_sanitize_id.js.html +0 -0
- package/docs/method_validate.js.html +0 -0
- package/docs/module-Lib.html +0 -0
- package/docs/scripts/core.js +476 -477
- package/docs/scripts/core.min.js +0 -0
- package/docs/scripts/resize.js +36 -36
- package/docs/scripts/search.js +105 -105
- package/docs/scripts/search.min.js +0 -0
- package/docs/scripts/third-party/Apache-License-2.0.txt +0 -0
- package/docs/scripts/third-party/fuse.js +1 -1
- package/docs/scripts/third-party/hljs-line-num-original.js +282 -285
- package/docs/scripts/third-party/hljs-line-num.js +1 -1
- package/docs/scripts/third-party/hljs-original.js +1195 -1202
- package/docs/scripts/third-party/hljs.js +1 -1
- package/docs/scripts/third-party/popper.js +1 -1
- package/docs/scripts/third-party/tippy.js +1 -1
- package/docs/scripts/third-party/tocbot.js +508 -509
- package/docs/scripts/third-party/tocbot.min.js +0 -0
- package/docs/static/bitcoin.jpeg +0 -0
- package/docs/static/home.md +0 -0
- package/docs/static/logo-ecosystem.png +0 -0
- package/docs/static/logo.png +0 -0
- package/docs/styles/clean-jsdoc-theme-base.css +0 -0
- package/docs/styles/clean-jsdoc-theme-dark.css +0 -0
- package/docs/styles/clean-jsdoc-theme-light.css +0 -0
- package/docs/styles/clean-jsdoc-theme-scrollbar.css +0 -0
- package/docs/styles/clean-jsdoc-theme-without-scrollbar.min.css +0 -0
- package/docs/styles/clean-jsdoc-theme.min.css +0 -0
- package/extend/bajo/intl/en-US.json +66 -28
- package/extend/bajo/intl/id.json +55 -27
- package/extend/bajoCli/applet/clear-record.js +22 -0
- package/extend/bajoCli/applet/connection.js +0 -0
- 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 +10 -17
- 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 +9 -7
- 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 +32 -54
- package/extend/dobo/feature/updated-at.js +14 -12
- package/extend/waibuMpa/route/attachment/@model/@id/@field/@file.js +2 -6
- package/extend/waibuStatic/virtual.json +0 -0
- package/index.js +284 -371
- package/lib/collect-connections.js +49 -21
- package/lib/collect-drivers.js +19 -33
- package/lib/collect-features.js +24 -17
- package/lib/collect-models.js +321 -0
- package/lib/factory/action.js +161 -0
- package/lib/factory/connection.js +62 -0
- package/lib/factory/driver.js +372 -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/{method/record/find.js → lib/factory/model/find-record.js} +103 -115
- 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 +56 -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/{method → lib/factory/model}/validate.js +38 -52
- package/lib/factory/model.js +150 -0
- package/lib/index.js +0 -0
- package/package.json +8 -4
- package/wiki/APPLETS.md +0 -0
- package/wiki/CHANGES.md +50 -0
- package/wiki/CONFIG.md +0 -0
- package/wiki/CONTRIBUTING.md +0 -0
- package/wiki/DEV-GUIDE.md +0 -0
- package/wiki/ECOSYSTEM.md +0 -0
- package/wiki/GETTING-STARTED.md +10 -10
- package/wiki/QUERY-LANGUAGE.md +0 -0
- package/wiki/USER-GUIDE.md +0 -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 -43
- package/extend/bajoCli/applet/record-find.js +0 -28
- 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-schemas.js +0 -91
- package/lib/exec-feature-hook.js +0 -13
- package/lib/exec-validation.js +0 -21
- package/lib/generic-prop-sanitizer.js +0 -32
- 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 -198
- 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 -32
- package/method/model/drop.js +0 -31
- package/method/model/exists.js +0 -37
- package/method/record/clear.js +0 -24
- package/method/record/count.js +0 -66
- package/method/record/create.js +0 -111
- package/method/record/find-all.js +0 -41
- package/method/record/find-one.js +0 -70
- package/method/record/get.js +0 -89
- package/method/record/remove.js +0 -72
- package/method/record/update.js +0 -104
- package/method/record/upsert.js +0 -51
- package/method/sanitize/body.js +0 -85
- package/method/sanitize/date.js +0 -27
- package/method/sanitize/id.js +0 -17
- package/method/stat/aggregate.js +0 -23
- package/method/stat/histogram.js +0 -26
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize payload body against its model
|
|
3
|
+
*
|
|
4
|
+
* @method
|
|
5
|
+
* @memberof Dobo
|
|
6
|
+
* @async
|
|
7
|
+
* @param {Object} [options={}]
|
|
8
|
+
* @param {Object} [options.body={}]
|
|
9
|
+
* @param {Object} [options.model={}]
|
|
10
|
+
* @param {boolean} [options.partial=false]
|
|
11
|
+
* @param {boolean} [options.strict=false]
|
|
12
|
+
* @param {Array} [options.extFields=[]]
|
|
13
|
+
* @returns {Object}
|
|
14
|
+
*/
|
|
15
|
+
async function sanitizeBody ({ body = {}, partial, strict, extFields = [], noDefault, truncateString, onlyTypes = [], action } = {}) {
|
|
16
|
+
const { isSet } = this.app.lib.aneka
|
|
17
|
+
const { callHandler } = this.app.bajo
|
|
18
|
+
const { omit, has, isString, isNaN } = this.app.lib._
|
|
19
|
+
const { sanitizeBoolean, sanitizeDate, sanitizeFloat, sanitizeInt, sanitizeObject, sanitizeString } = this.app.dobo
|
|
20
|
+
const result = {}
|
|
21
|
+
|
|
22
|
+
const sanitize = (name, type) => {
|
|
23
|
+
if (onlyTypes.length > 0 && !onlyTypes.includes(type)) return
|
|
24
|
+
if (['object', 'array'].includes(type)) result[name] = sanitizeObject(result[name])
|
|
25
|
+
else if (type === 'boolean') result[name] = sanitizeBoolean(result[name])
|
|
26
|
+
else if (['float', 'double'].includes(type)) result[name] = sanitizeFloat(result[name], strict)
|
|
27
|
+
else if (['integer', 'smallint'].includes(type)) result[name] = sanitizeInt(result[name], strict)
|
|
28
|
+
else if (['string', 'text'].includes(type)) result[name] = sanitizeString(result[name], strict)
|
|
29
|
+
else if (['datetime'].includes(type)) result[name] = sanitizeDate(result[name], { input: 'native' })
|
|
30
|
+
if (!strict && isNaN(result[name])) result[name] = null
|
|
31
|
+
if (['updateRecord', 'upsertRecord'].includes(action) && type === 'string' && result[name] === '') result[name] = null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const omitted = []
|
|
35
|
+
for (const prop of [...this.properties, ...extFields]) {
|
|
36
|
+
if (partial && !has(body, prop.name)) continue
|
|
37
|
+
result[prop.name] = body[prop.name]
|
|
38
|
+
if (body[prop.name] === null) continue
|
|
39
|
+
if (isSet(body[prop.name])) sanitize(prop.name, prop.type)
|
|
40
|
+
else {
|
|
41
|
+
if (isSet(prop.default) && !noDefault) {
|
|
42
|
+
result[prop.name] = prop.default
|
|
43
|
+
if (isString(prop.default) && prop.default.startsWith('handler:')) {
|
|
44
|
+
const [, ...args] = prop.default.split(':')
|
|
45
|
+
if (args.length > 0) result[prop.name] = await callHandler(args.join(':'))
|
|
46
|
+
} else sanitize(prop.name, prop.type)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (truncateString && isSet(result[prop.name]) && ['string', 'text'].includes(prop.type)) result[prop.name] = result[prop.name].slice(0, prop.maxLength)
|
|
50
|
+
if (prop.name.endsWith('Id') && prop.type === 'string' && ['smallint', 'integer'].includes(this.driver.idField.type)) result[prop.name] = result[prop.name] + ''
|
|
51
|
+
if (body[prop.name] === undefined) omitted.push(prop.name)
|
|
52
|
+
}
|
|
53
|
+
return omit(result, omitted)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default sanitizeBody
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sanitize record to conform with the model's definition
|
|
3
|
+
*
|
|
4
|
+
* @method
|
|
5
|
+
* @async
|
|
6
|
+
* @param {Object} [record] - Record object
|
|
7
|
+
* @param {Array} [options.fields] - Array of field names to be picked
|
|
8
|
+
* @param {Object} [options.hidden=[]] - Additional fields to be hidden in addition the one defined in model
|
|
9
|
+
* @param {boolean} [options.forceNoHidden] - Force ALL fields to be picked, thus ignoring hidden fields
|
|
10
|
+
* @returns {Object}
|
|
11
|
+
*/
|
|
12
|
+
async function sanitizeRecord (record = {}, opts = {}) {
|
|
13
|
+
const { fields = [], hidden = [], forceNoHidden } = opts
|
|
14
|
+
const { isEmpty, map, without } = this.app.lib._
|
|
15
|
+
const { fillObject } = this.app.lib.aneka
|
|
16
|
+
const allHidden = forceNoHidden ? [] : without([...this.hidden, ...hidden], 'id')
|
|
17
|
+
let newFields = [...fields]
|
|
18
|
+
if (isEmpty(newFields)) newFields = map(this.properties, prop => prop.name)
|
|
19
|
+
if (!newFields.includes('id')) newFields.unshift('id')
|
|
20
|
+
newFields = without(newFields, ...allHidden)
|
|
21
|
+
const newRecord = await this.sanitizeBody({ body: fillObject(record, newFields, null), noDefault: true })
|
|
22
|
+
if (record._ref) newRecord._ref = record._ref
|
|
23
|
+
return newRecord
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default sanitizeRecord
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getFilterAndOptions, execHook, execValidation, execModelHook, getSingleRef, handleReq } from './_util.js'
|
|
2
|
+
import { onlyTypes } from './create-record.js'
|
|
3
|
+
const action = 'updateRecord'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} TRecordUpdateOptions
|
|
7
|
+
* @see Model#updateRecord
|
|
8
|
+
* @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordUpdateResult}
|
|
9
|
+
* @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
|
|
10
|
+
* @property {boolean} [noModelHook=false] - If ```true```, no model's hook will be executed
|
|
11
|
+
* @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
|
|
12
|
+
* @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
|
|
13
|
+
* @property {boolean} [noBodySanitizer=false] - If ```true```, accept data payload as is without sanitization
|
|
14
|
+
* @property {boolean} [noRecordSanitizer=false] - If ```true```, accept result payload as is without sanitization
|
|
15
|
+
* @property {boolean} [noResult=false] - If ```true```, returns nothing
|
|
16
|
+
* @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its model's ```maxLength```
|
|
17
|
+
* @property {boolean} [partial=true] - If ```true``` (default), only updated values are saved. Otherwise replace all existing values with given payload
|
|
18
|
+
* @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
|
|
19
|
+
* @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
|
|
20
|
+
* @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Update a record by it's ID and body payload
|
|
25
|
+
*
|
|
26
|
+
* Example:
|
|
27
|
+
* ```javascript
|
|
28
|
+
* const { recordUpdate } = this.app.dobo
|
|
29
|
+
* const { body } = {
|
|
30
|
+
* name: 'Republic of Indonesia',
|
|
31
|
+
* phoneCode: '+62'
|
|
32
|
+
* }
|
|
33
|
+
* const result = await recordUpdate('CdbCountry', 'ID', body)
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* @method
|
|
37
|
+
* @memberof Dobo
|
|
38
|
+
* @async
|
|
39
|
+
* @instance
|
|
40
|
+
* @name recordUpdate
|
|
41
|
+
* @param {string} name - Model's name
|
|
42
|
+
* @param {(string|number)} id - Record's ID
|
|
43
|
+
* @param {Object} body - Body payload
|
|
44
|
+
* @param {TRecordUpdateOptions} [options={}]
|
|
45
|
+
* @returns {(TRecordUpdateResult|Object)} Returns updated record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} otherwise
|
|
46
|
+
*/
|
|
47
|
+
async function updateRecord (...args) {
|
|
48
|
+
if (args.length === 0) return this.action(action, ...args)
|
|
49
|
+
let [id, body = {}, opts = {}] = args
|
|
50
|
+
const { isSet } = this.app.lib.aneka
|
|
51
|
+
const { runHook } = this.app.bajo
|
|
52
|
+
const { cloneDeep, get } = this.app.lib._
|
|
53
|
+
const { dataOnly = true } = opts
|
|
54
|
+
const { options } = await getFilterAndOptions.call(this, null, opts, action)
|
|
55
|
+
options.partial = options.partial ?? true
|
|
56
|
+
const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation, partial } = options
|
|
57
|
+
const extFields = get(options, 'validation.extFields', [])
|
|
58
|
+
id = this.sanitizeId(id)
|
|
59
|
+
const input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString, partial, onlyTypes })
|
|
60
|
+
delete input.id
|
|
61
|
+
await execHook.call(this, 'beforeUpdateRecord', id, input, options)
|
|
62
|
+
await execModelHook.call(this, 'beforeUpdateRecord', id, input, options)
|
|
63
|
+
if (!noValidation) await execValidation.call(this, input, options)
|
|
64
|
+
const result = options.record ?? (await this.driver._updateRecord(this, id, input, options)) ?? {}
|
|
65
|
+
if (noResult) {
|
|
66
|
+
await runHook('cache:clear', this, 'update', id, body)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
if (!noResultSanitizer) {
|
|
70
|
+
result.data = await this.sanitizeRecord(result.data, options)
|
|
71
|
+
result.oldData = await this.sanitizeRecord(result.oldData, options)
|
|
72
|
+
}
|
|
73
|
+
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
74
|
+
await handleReq.call(this, result.data.id, 'updated', options)
|
|
75
|
+
await execModelHook.call(this, 'afterUpdateRecord', id, input, result, options)
|
|
76
|
+
await execHook.call(this, 'afterUpdateRecord', id, input, result, options)
|
|
77
|
+
await runHook('cache:clear', this, 'update', id, body, result)
|
|
78
|
+
return dataOnly ? result.data : result
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default updateRecord
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { getFilterAndOptions, execHook, execModelHook, execValidation, getSingleRef, handleReq } from './_util.js'
|
|
2
|
+
const action = 'upsertRecord'
|
|
3
|
+
|
|
4
|
+
async function native (body = {}, opts = {}) {
|
|
5
|
+
const { isSet } = this.app.lib.aneka
|
|
6
|
+
const { runHook } = this.app.bajo
|
|
7
|
+
const { cloneDeep, get } = this.app.lib._
|
|
8
|
+
const { dataOnly = true } = opts
|
|
9
|
+
const { options } = await getFilterAndOptions.call(this, null, opts, action)
|
|
10
|
+
const { truncateString, noResult, noBodySanitizer, noResultSanitizer, noValidation } = options
|
|
11
|
+
const extFields = get(options, 'validation.extFields', [])
|
|
12
|
+
let input = noBodySanitizer ? cloneDeep(body) : await this.sanitizeBody({ body, extFields, strict: true, truncateString })
|
|
13
|
+
if (!noValidation) input = await execValidation.call(this, input, options)
|
|
14
|
+
await execHook.call(this, 'beforeUpsertRecord', input, options)
|
|
15
|
+
await execModelHook.call(this, 'beforeUpsertRecord', input, options)
|
|
16
|
+
const result = options.record ?? (await this.driver._upsertRecord(this, input, options)) ?? {}
|
|
17
|
+
if (noResult) {
|
|
18
|
+
await runHook('cache:clear', this, 'upsert', body)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
|
|
22
|
+
if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
|
|
23
|
+
await handleReq.call(this, result.data.id, 'upserted', options)
|
|
24
|
+
await execModelHook.call(this, 'afterUpsertRecord', input, result, options)
|
|
25
|
+
await execHook.call(this, 'afterUpsertRecord', input, result, options)
|
|
26
|
+
await runHook('cache:clear', this, 'upsert', body, result)
|
|
27
|
+
return dataOnly ? result.data : result
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function manual (body = {}, options = {}) {
|
|
31
|
+
const { isSet } = this.app.lib.aneka
|
|
32
|
+
if (isSet(body.id)) body.id = this.sanitizeId(body.id)
|
|
33
|
+
let old = false
|
|
34
|
+
if (isSet(body.id)) {
|
|
35
|
+
try {
|
|
36
|
+
old = await this.driver._getRecord(this, body.id, { noHook: true, noModelHook: true })
|
|
37
|
+
} catch (err) {}
|
|
38
|
+
}
|
|
39
|
+
if (old) return await this.updateRecord(old.data.id, body, options)
|
|
40
|
+
return await this.createRecord(body, options)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {Object} TRecordUpdateOptions
|
|
45
|
+
* @see Model#updateRecord
|
|
46
|
+
* @property {boolean} [dataOnly=true] - If ```true``` (default) returns record's object. Otherwise {@link TRecordUpdateResult}
|
|
47
|
+
* @property {boolean} [noHook=false] - If ```true```, no model's hook will be executed
|
|
48
|
+
* @property {boolean} [noModelHook=false] - If ```true```, no model's hook will be executed
|
|
49
|
+
* @property {boolean} [noValidation=false] - If ```true```, no validation of data payload performed
|
|
50
|
+
* @property {boolean} [noCheckUnique=false] - If ```true```, no unique validation for ID performed
|
|
51
|
+
* @property {boolean} [noBodySanitizer=false] - If ```true```, accept data payload as is without sanitization
|
|
52
|
+
* @property {boolean} [noRecordSanitizer=false] - If ```true```, accept result payload as is without sanitization
|
|
53
|
+
* @property {boolean} [noResult=false] - If ```true```, returns nothing
|
|
54
|
+
* @property {boolean} [truncateString=true] - If ```true``` (default), string is truncated to its model's ```maxLength```
|
|
55
|
+
* @property {boolean} [partial=true] - If ```true``` (default), only updated values are saved. Otherwise replace all existing values with given payload
|
|
56
|
+
* @property {boolean} [fields=[]] - If not empty, return only these fields EXCLUDING hidden fields
|
|
57
|
+
* @property {boolean} [hidden=[]] - Additional fields to hide, in addition the one set in model's model
|
|
58
|
+
* @property {boolean} [forceNoHidden=false] - If ```true```, hidden fields will be ignored and ALL fields will be returned
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Update a record by it's ID and body payload
|
|
63
|
+
*
|
|
64
|
+
* Example:
|
|
65
|
+
* ```javascript
|
|
66
|
+
* const { recordUpdate } = this.app.dobo
|
|
67
|
+
* const { body } = {
|
|
68
|
+
* name: 'Republic of Indonesia',
|
|
69
|
+
* phoneCode: '+62'
|
|
70
|
+
* }
|
|
71
|
+
* const result = await recordUpdate('CdbCountry', 'ID', body)
|
|
72
|
+
* ```
|
|
73
|
+
*
|
|
74
|
+
* @method
|
|
75
|
+
* @memberof Dobo
|
|
76
|
+
* @async
|
|
77
|
+
* @instance
|
|
78
|
+
* @name recordUpdate
|
|
79
|
+
* @param {string} name - Model's name
|
|
80
|
+
* @param {(string|number)} id - Record's ID
|
|
81
|
+
* @param {Object} body - Body payload
|
|
82
|
+
* @param {TRecordUpdateOptions} [options={}]
|
|
83
|
+
* @returns {(TRecordUpdateResult|Object)} Returns updated record if ```options.dataOnly``` is set. {@link TRecordUpdateResult} otherwise
|
|
84
|
+
*/
|
|
85
|
+
async function upsertRecord (...args) {
|
|
86
|
+
if (args.length === 0) return this.action(action, ...args)
|
|
87
|
+
const [body = {}, opts = {}] = args
|
|
88
|
+
if (this.driver.upsertRecord) {
|
|
89
|
+
const { options } = await getFilterAndOptions.call(this, null, opts, action)
|
|
90
|
+
return await native.call(this, body, options)
|
|
91
|
+
}
|
|
92
|
+
return await manual.call(this, body, opts)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default upsertRecord
|
|
@@ -89,14 +89,11 @@ const validator = {
|
|
|
89
89
|
timestamp: ['timestamp']
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
function
|
|
93
|
-
|
|
94
|
-
const {
|
|
95
|
-
isPlainObject, get, each, isEmpty, isString, forOwn, keys,
|
|
96
|
-
find, isArray, has, cloneDeep, concat, without
|
|
97
|
-
} = this.app.lib._
|
|
92
|
+
function buildFromDbModel (opts = {}) {
|
|
93
|
+
const { isPlainObject, get, isEmpty, isString, keys, find, has, without } = this.app.lib._
|
|
94
|
+
const { fields = [], rule = {}, extFields = [] } = opts
|
|
98
95
|
const obj = {}
|
|
99
|
-
const { propType } = this.app.
|
|
96
|
+
const { propertyType: propType } = this.app.baseClass.Dobo
|
|
100
97
|
const refs = []
|
|
101
98
|
|
|
102
99
|
function getRuleKv (kvRule) {
|
|
@@ -116,36 +113,36 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
|
|
|
116
113
|
function applyFieldRules (prop, obj) {
|
|
117
114
|
const minMax = { min: false, max: false }
|
|
118
115
|
const rules = get(rule, prop.name, prop.rules ?? [])
|
|
119
|
-
if (!isArray(rules)) return rules
|
|
120
|
-
|
|
116
|
+
if (!Array.isArray(rules)) return rules
|
|
117
|
+
for (const r of rules) {
|
|
121
118
|
const types = validator[propType[prop.type].validator]
|
|
122
119
|
const { key, value } = getRuleKv(r)
|
|
123
120
|
if (keys(minMax).includes(key)) minMax[key] = true
|
|
124
121
|
if (key === 'ref') {
|
|
125
122
|
refs.push(prop.name)
|
|
126
123
|
obj = joi.ref(value)
|
|
127
|
-
|
|
124
|
+
continue
|
|
128
125
|
}
|
|
129
|
-
if (!key || !types.includes(key))
|
|
126
|
+
if (!key || !types.includes(key)) continue
|
|
130
127
|
obj = obj[key](value)
|
|
131
|
-
}
|
|
128
|
+
}
|
|
132
129
|
if (refs.includes(prop.name)) return obj
|
|
133
130
|
if (['string', 'text'].includes(prop.type)) {
|
|
134
|
-
|
|
135
|
-
if (
|
|
131
|
+
for (const k in minMax) {
|
|
132
|
+
if (minMax[k]) continue
|
|
136
133
|
if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
|
|
137
|
-
}
|
|
134
|
+
}
|
|
138
135
|
}
|
|
139
|
-
if (isArray(prop.values)) obj = obj.valid(...prop.values)
|
|
136
|
+
if (Array.isArray(prop.values)) obj = obj.valid(...prop.values)
|
|
140
137
|
if (!['id'].includes(prop.name) && prop.required) obj = obj.required()
|
|
141
138
|
return obj
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
const props =
|
|
141
|
+
const props = [...this.properties, ...extFields]
|
|
145
142
|
|
|
146
143
|
for (const p of props) {
|
|
147
144
|
if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
|
|
148
|
-
if (fields.length > 0 && !fields.includes(p.name)) continue
|
|
145
|
+
if (opts.partial && fields.length > 0 && !fields.includes(p.name)) continue
|
|
149
146
|
let item
|
|
150
147
|
switch (p.type) {
|
|
151
148
|
case 'text':
|
|
@@ -175,38 +172,37 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
|
|
|
175
172
|
break
|
|
176
173
|
}
|
|
177
174
|
if (item) {
|
|
178
|
-
if (item.$_root) obj[p.name] = item.allow(null)
|
|
175
|
+
if (item.$_root && !p.required) obj[p.name] = item.allow(null, '')
|
|
179
176
|
else obj[p.name] = item
|
|
180
177
|
}
|
|
181
178
|
}
|
|
182
179
|
if (isEmpty(obj)) return false
|
|
183
|
-
|
|
184
|
-
|
|
180
|
+
for (const r of this.rules) {
|
|
181
|
+
for (const k of without(keys(obj), ...refs)) {
|
|
185
182
|
const prop = find(props, { name: k })
|
|
186
|
-
if (!prop)
|
|
183
|
+
if (!prop) continue
|
|
187
184
|
const types = validator[propType[prop.type].validator]
|
|
188
185
|
const { key, value, columns = [] } = getRuleKv(r)
|
|
189
|
-
if (!types.includes(key))
|
|
186
|
+
if (!types.includes(key)) continue
|
|
190
187
|
if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
193
190
|
const result = joi.object(obj)
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const item = get(schema, `extRule.${k}`)
|
|
191
|
+
for (const k of ['with', 'xor', 'without']) {
|
|
192
|
+
const item = get(this, `extRule.${k}`)
|
|
197
193
|
if (item) result[k](...item)
|
|
198
|
-
}
|
|
194
|
+
}
|
|
199
195
|
return result
|
|
200
196
|
}
|
|
201
197
|
|
|
202
198
|
/**
|
|
203
|
-
* Validate value against JOI
|
|
199
|
+
* Validate value against JOI model
|
|
204
200
|
*
|
|
205
201
|
* @method
|
|
206
202
|
* @memberof Dobo
|
|
207
203
|
* @async
|
|
208
204
|
* @param {Object} value - value to validate
|
|
209
|
-
* @param {Object}
|
|
205
|
+
* @param {Object} joiModel - JOI model
|
|
210
206
|
* @param {Object} [options={}] - Options object
|
|
211
207
|
* @param {string} [options.ns=dobo] - Scope's namespace
|
|
212
208
|
* @param {Array} [options.fields=[]]
|
|
@@ -214,32 +210,22 @@ function buildFromDbSchema (schema, { fields = [], rule = {}, extFields = [] } =
|
|
|
214
210
|
* @param {Object} [options.params={}] - Validation parameters. See {@tutorial config} and {@link https://joi.dev/api/?v=17.13.3#anyvalidateasyncvalue-options|JOI validate's options}
|
|
215
211
|
* @returns {Object}
|
|
216
212
|
*/
|
|
217
|
-
async function validate (
|
|
218
|
-
const { defaultsDeep
|
|
219
|
-
const {
|
|
213
|
+
async function validate (body, joiModel, opts = {}) {
|
|
214
|
+
const { defaultsDeep } = this.app.lib.aneka
|
|
215
|
+
const { isEmpty } = this.app.lib._
|
|
216
|
+
let { fields, extFields = [], params = {}, partial } = opts
|
|
220
217
|
|
|
221
|
-
|
|
222
|
-
params = defaultsDeep(params, this.config.validationParams)
|
|
218
|
+
params = defaultsDeep(params, this.app.dobo.config.validationParams)
|
|
223
219
|
const { rule = {} } = params
|
|
224
220
|
delete params.rule
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
forOwn(value, (v, k) => {
|
|
228
|
-
if (!isSet(v)) return undefined
|
|
229
|
-
const p = find(schema.properties, { name: k })
|
|
230
|
-
if (!p) return undefined
|
|
231
|
-
for (const t of ['date|YYYY-MM-DD', 'time|HH:mm:ss']) {
|
|
232
|
-
const [type, input] = t.split('|')
|
|
233
|
-
if (p.type === type) value[k] = this.sanitizeDate(value[k], { input, output: 'native' })
|
|
234
|
-
}
|
|
235
|
-
})
|
|
236
|
-
joiSchema = buildFromDbSchema.call(this, schema, { fields, rule, extFields })
|
|
237
|
-
}
|
|
238
|
-
if (!joiSchema) return value
|
|
221
|
+
if (isEmpty(joiModel)) joiModel = buildFromDbModel.call(this, { fields, rule, extFields, partial })
|
|
222
|
+
if (!joiModel) return { value: body }
|
|
239
223
|
try {
|
|
240
|
-
return await
|
|
224
|
+
return await joiModel.validateAsync(body, params)
|
|
241
225
|
} catch (err) {
|
|
242
|
-
|
|
226
|
+
const payload = { details: err.details, statusCode: 422, code: 'DB_VALIDATION' }
|
|
227
|
+
if (err.values) payload.values = err.values
|
|
228
|
+
throw this.plugin.error('validationError', payload)
|
|
243
229
|
}
|
|
244
230
|
}
|
|
245
231
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { sanitizeAll, sanitizeRef } from '../collect-models.js'
|
|
2
|
+
import clearRecord from './model/clear-record.js'
|
|
3
|
+
import countRecord from './model/count-record.js'
|
|
4
|
+
import createRecord from './model/create-record.js'
|
|
5
|
+
import drop from './model/drop.js'
|
|
6
|
+
import findAllRecord from './model/find-all-record.js'
|
|
7
|
+
import findOneRecord from './model/find-one-record.js'
|
|
8
|
+
import findRecord from './model/find-record.js'
|
|
9
|
+
import getRecord from './model/get-record.js'
|
|
10
|
+
import createHistogram from './model/create-histogram.js'
|
|
11
|
+
import createAggregate from './model/create-aggregate.js'
|
|
12
|
+
import exists from './model/exists.js'
|
|
13
|
+
import build from './model/build.js'
|
|
14
|
+
import loadFixtures from './model/load-fixtures.js'
|
|
15
|
+
import removeRecord from './model/remove-record.js'
|
|
16
|
+
import updateRecord from './model/update-record.js'
|
|
17
|
+
import createAttachment from './model/create-attachment.js'
|
|
18
|
+
import updateAttachment from './model/update-attachment.js'
|
|
19
|
+
import getAttachment from './model/get-attachment.js'
|
|
20
|
+
import removeAttachment from './model/remove-attachment.js'
|
|
21
|
+
import findAttachment from './model/find-attachment.js'
|
|
22
|
+
import sanitizeBody from './model/sanitize-body.js'
|
|
23
|
+
import sanitizeRecord from './model/sanitize-record.js'
|
|
24
|
+
import sanitizeId from './model/sanitize-id.js'
|
|
25
|
+
import upsertRecord from './model/upsert-record.js'
|
|
26
|
+
import listAttachment from './model/list-attachment.js'
|
|
27
|
+
import validate from './model/validate.js'
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {string} TRecordSortKey
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Key value pairs used as sort information:
|
|
35
|
+
* - Key represent model's field name
|
|
36
|
+
* - value represent its sort order: ```1``` for ascending order, and ```-1``` for descending order
|
|
37
|
+
*
|
|
38
|
+
* Example: to sort by firstName (ascending) and lastName (descending)
|
|
39
|
+
* ```javascript
|
|
40
|
+
* const sort = {
|
|
41
|
+
* firstName: 1,
|
|
42
|
+
* lastName: -1
|
|
43
|
+
* }
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @typedef {Object.<string, TRecordSortKey>} TRecordSort
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {Object} TRecordPagination
|
|
51
|
+
* @property {number} limit - Number of records per page
|
|
52
|
+
* @property {number} page - Page number
|
|
53
|
+
* @property {number} skip - Records to skip
|
|
54
|
+
* @property {TRecordSort} sort - Sort order
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
async function modelFactory () {
|
|
58
|
+
const { Tools } = this.app.baseClass
|
|
59
|
+
const { defaults } = this.app.lib._
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Feature class
|
|
63
|
+
*
|
|
64
|
+
* ```this.plugin``` should be the one who owned this driver
|
|
65
|
+
*
|
|
66
|
+
* @class
|
|
67
|
+
*/
|
|
68
|
+
class DoboModel extends Tools {
|
|
69
|
+
constructor (plugin, options) {
|
|
70
|
+
super(plugin)
|
|
71
|
+
defaults(this, options)
|
|
72
|
+
this.driver = this.connection.driver
|
|
73
|
+
this.cacheable = this.cacheable ?? this.driver.cacheable ?? true
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
action = (name, ...args) => {
|
|
77
|
+
const action = new this.app.baseClass.DoboAction(this, name, ...args)
|
|
78
|
+
return action
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sanitizeObjectDef = async (obj) => {
|
|
82
|
+
await sanitizeAll.call(this.app.dobo, obj)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sanitizeRef = async (obj, fatal = true) => {
|
|
86
|
+
await sanitizeRef.call(this.app.dobo, obj, this.app.dobo.models, fatal)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getProperty = (name) => {
|
|
90
|
+
return this.properties.find(prop => prop.name === name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
hasProperty = (name) => {
|
|
94
|
+
return !!this.getProperty(name)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
build = build
|
|
98
|
+
exists = exists
|
|
99
|
+
drop = drop
|
|
100
|
+
|
|
101
|
+
createRecord = createRecord
|
|
102
|
+
getRecord = getRecord
|
|
103
|
+
updateRecord = updateRecord
|
|
104
|
+
upsertRecord = upsertRecord
|
|
105
|
+
removeRecord = removeRecord
|
|
106
|
+
clearRecord = clearRecord
|
|
107
|
+
findRecord = findRecord
|
|
108
|
+
findOneRecord = findOneRecord
|
|
109
|
+
findAllRecord = findAllRecord
|
|
110
|
+
|
|
111
|
+
createAggregate = createAggregate
|
|
112
|
+
createHistogram = createHistogram
|
|
113
|
+
countRecord = countRecord
|
|
114
|
+
|
|
115
|
+
createAttachment = createAttachment
|
|
116
|
+
getAttachment = getAttachment
|
|
117
|
+
updateAttachment = updateAttachment
|
|
118
|
+
removeAttachment = removeAttachment
|
|
119
|
+
findAttachment = findAttachment
|
|
120
|
+
listAttachment = listAttachment
|
|
121
|
+
|
|
122
|
+
loadFixtures = loadFixtures
|
|
123
|
+
sanitizeRecord = sanitizeRecord
|
|
124
|
+
sanitizeBody = sanitizeBody
|
|
125
|
+
sanitizeId = sanitizeId
|
|
126
|
+
validate = validate
|
|
127
|
+
|
|
128
|
+
// aliases
|
|
129
|
+
deleteRecord = removeRecord
|
|
130
|
+
clearRecords = clearRecord
|
|
131
|
+
countRecords = countRecord
|
|
132
|
+
findRecords = findRecord
|
|
133
|
+
findAllRecords = findAllRecord
|
|
134
|
+
listAttachments = listAttachment
|
|
135
|
+
|
|
136
|
+
getField = (name) => this.getProperty(name)
|
|
137
|
+
hasField = (name) => this.hasProperty(name)
|
|
138
|
+
|
|
139
|
+
dispose () {
|
|
140
|
+
super.dispose()
|
|
141
|
+
this.connection = null
|
|
142
|
+
this.driver = null
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.app.baseClass.DoboModel = DoboModel
|
|
147
|
+
return DoboModel
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export default modelFactory
|
package/lib/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dobo",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "DBMS for Bajo Framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"type": "module",
|
|
11
11
|
"bajo": {
|
|
12
|
-
"type": "plugin"
|
|
12
|
+
"type": "plugin",
|
|
13
|
+
"alias": "db",
|
|
14
|
+
"bootorder": 50
|
|
13
15
|
},
|
|
14
16
|
"repository": {
|
|
15
17
|
"type": "git",
|
|
@@ -31,8 +33,10 @@
|
|
|
31
33
|
"homepage": "https://github.com/ardhi/dobo#readme",
|
|
32
34
|
"dependencies": {
|
|
33
35
|
"@tryghost/nql": "^0.12.7",
|
|
34
|
-
"joi": "^
|
|
35
|
-
"mingo": "^
|
|
36
|
+
"joi": "^18.0.2",
|
|
37
|
+
"mingo": "^7.1.0",
|
|
38
|
+
"ulid": "^3.0.2",
|
|
39
|
+
"uuid": "^13.0.0"
|
|
36
40
|
},
|
|
37
41
|
"devDependencies": {
|
|
38
42
|
"clean-jsdoc-theme": "^4.3.0",
|
package/wiki/APPLETS.md
CHANGED
|
File without changes
|