dobo 1.0.0 → 1.0.2
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/README.md +23 -1
- package/bajo/config.json +27 -0
- package/bajo/hook/bajoI18N.db@before-resource-merge.js +10 -0
- package/bajo/hook/bajoI18N@before-init.js +6 -0
- package/bajo/init.js +18 -0
- package/bajo/method/aggregate-types.js +1 -0
- package/bajo/method/attachment/copy-uploaded.js +32 -0
- package/bajo/method/attachment/create.js +27 -0
- package/bajo/method/attachment/find.js +27 -0
- package/bajo/method/attachment/get-path.js +11 -0
- package/bajo/method/attachment/get.js +12 -0
- package/bajo/method/attachment/pre-check.js +8 -0
- package/bajo/method/attachment/remove.js +11 -0
- package/bajo/method/attachment/update.js +7 -0
- package/bajo/method/build-match.js +34 -0
- package/bajo/method/build-query.js +13 -0
- package/bajo/method/bulk/create.js +45 -0
- package/bajo/method/get-info.js +14 -0
- package/bajo/method/get-schema.js +10 -0
- package/bajo/method/model/clear.js +20 -0
- package/bajo/method/model/create.js +11 -0
- package/bajo/method/model/drop.js +11 -0
- package/bajo/method/model/exists.js +17 -0
- package/bajo/method/pick-record.js +30 -0
- package/bajo/method/prep-pagination.js +66 -0
- package/bajo/method/prop-type.js +43 -0
- package/bajo/method/record/clear.js +22 -0
- package/bajo/method/record/create.js +61 -0
- package/bajo/method/record/find-all.js +15 -0
- package/bajo/method/record/find-one.js +39 -0
- package/bajo/method/record/find.js +41 -0
- package/bajo/method/record/get.js +38 -0
- package/bajo/method/record/remove.js +34 -0
- package/bajo/method/record/update.js +60 -0
- package/bajo/method/record/upsert.js +19 -0
- package/bajo/method/sanitize/body.js +67 -0
- package/bajo/method/sanitize/date.js +14 -0
- package/bajo/method/sanitize/id.js +7 -0
- package/bajo/method/stat/aggregate.js +23 -0
- package/bajo/method/stat/histogram.js +26 -0
- package/bajo/method/validate.js +154 -0
- package/bajo/method/validation-error-message.js +12 -0
- package/bajo/start.js +16 -0
- package/bajoCli/applet/connection.js +22 -0
- package/bajoCli/applet/lib/post-process.js +47 -0
- package/bajoCli/applet/model-clear.js +11 -0
- package/bajoCli/applet/model-rebuild.js +77 -0
- package/bajoCli/applet/record-create.js +41 -0
- package/bajoCli/applet/record-find.js +27 -0
- package/bajoCli/applet/record-get.js +24 -0
- package/bajoCli/applet/record-remove.js +24 -0
- package/bajoCli/applet/record-update.js +47 -0
- package/bajoCli/applet/schema.js +22 -0
- package/bajoCli/applet/shell.js +48 -0
- package/bajoCli/applet/stat-count.js +24 -0
- package/bajoCli/applet.js +1 -0
- package/bajoI18N/resource/en-US.json +82 -0
- package/bajoI18N/resource/id.json +143 -0
- package/dobo/feature/created-at.js +18 -0
- package/dobo/feature/dt.js +13 -0
- package/dobo/feature/int-id.js +13 -0
- package/dobo/feature/updated-at.js +23 -0
- package/lib/add-fixtures.js +53 -0
- package/lib/build-bulk-action.js +12 -0
- package/lib/check-unique.js +39 -0
- package/lib/collect-connections.js +25 -0
- package/lib/collect-drivers.js +32 -0
- package/lib/collect-feature.js +23 -0
- package/lib/collect-schemas.js +77 -0
- package/lib/exec-feature-hook.js +12 -0
- package/lib/exec-validation.js +27 -0
- package/lib/generic-prop-sanitizer.js +38 -0
- package/lib/handle-attachment-upload.js +16 -0
- package/lib/merge-attachment-info.js +16 -0
- package/lib/multi-rel-rows.js +34 -0
- package/lib/resolve-method.js +15 -0
- package/lib/sanitize-schema.js +180 -0
- package/lib/single-rel-rows.js +32 -0
- package/package.json +2 -1
- package/waibuStatic/virtual.json +4 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
async function findAll (name, filter = {}, options = {}) {
|
|
2
|
+
filter.page = 1
|
|
3
|
+
filter.limit = 100
|
|
4
|
+
options.dataOnly = true
|
|
5
|
+
const all = []
|
|
6
|
+
for (;;) {
|
|
7
|
+
const results = await this.recordFind(name, filter, options)
|
|
8
|
+
if (results.length === 0) break
|
|
9
|
+
all.push(...results)
|
|
10
|
+
filter.page++
|
|
11
|
+
}
|
|
12
|
+
return all
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default findAll
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
import singleRelRows from '../../../lib/single-rel-rows.js'
|
|
3
|
+
|
|
4
|
+
async function findOne (name, filter = {}, opts = {}) {
|
|
5
|
+
const { runHook, isSet } = this.app.bajo
|
|
6
|
+
const { get, set } = this.cache ?? {}
|
|
7
|
+
const { cloneDeep } = this.app.bajo.lib._
|
|
8
|
+
const options = cloneDeep(opts)
|
|
9
|
+
options.dataOnly = options.dataOnly ?? true
|
|
10
|
+
const { fields, dataOnly, noHook, noCache, hidden } = options
|
|
11
|
+
await this.modelExists(name, true)
|
|
12
|
+
filter.limit = 1
|
|
13
|
+
options.count = false
|
|
14
|
+
options.dataOnly = false
|
|
15
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-find')
|
|
16
|
+
if (!noHook) {
|
|
17
|
+
await runHook(`${this.name}:onBeforeRecordFindOne`, name, filter, options)
|
|
18
|
+
await runHook(`${this.name}.${name}:onBeforeRecordFindOne`, filter, options)
|
|
19
|
+
}
|
|
20
|
+
if (get && !noCache) {
|
|
21
|
+
const cachedResult = await get({ model: name, filter, options })
|
|
22
|
+
if (cachedResult) {
|
|
23
|
+
cachedResult.cached = true
|
|
24
|
+
return dataOnly ? cachedResult.data : cachedResult
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const record = await handler.call(this.app[driver.ns], { schema, filter, options })
|
|
28
|
+
record.data = record.data[0]
|
|
29
|
+
if (!noHook) {
|
|
30
|
+
await runHook(`${this.name}.${name}:onAfterRecordFindOne`, filter, options, record)
|
|
31
|
+
await runHook(`${this.name}:onAfterRecordFindOne`, name, filter, options, record)
|
|
32
|
+
}
|
|
33
|
+
record.data = await this.pickRecord({ record: record.data, fields, schema, hidden })
|
|
34
|
+
if (isSet(options.rels)) await singleRelRows.call(this, { schema, record: record.data, options })
|
|
35
|
+
if (set && !noCache) await set({ model: name, filter, options, record })
|
|
36
|
+
return dataOnly ? record.data : record
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default findOne
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
import multiRelRows from '../../../lib/multi-rel-rows.js'
|
|
3
|
+
|
|
4
|
+
async function find (name, filter = {}, opts = {}) {
|
|
5
|
+
const { runHook, isSet } = this.app.bajo
|
|
6
|
+
const { get, set } = this.cache ?? {}
|
|
7
|
+
const { cloneDeep } = this.app.bajo.lib._
|
|
8
|
+
const options = cloneDeep(opts)
|
|
9
|
+
options.dataOnly = options.dataOnly ?? true
|
|
10
|
+
const { fields, dataOnly, noHook, noCache, hidden } = options
|
|
11
|
+
options.count = options.count ?? false
|
|
12
|
+
options.dataOnly = false
|
|
13
|
+
await this.modelExists(name, true)
|
|
14
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-find')
|
|
15
|
+
filter.query = await this.buildQuery({ filter, schema, options }) ?? {}
|
|
16
|
+
filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
|
|
17
|
+
if (!noHook) {
|
|
18
|
+
await runHook(`${this.name}:onBeforeRecordFind`, name, filter, options)
|
|
19
|
+
await runHook(`${this.name}.${name}:onBeforeRecordFind`, filter, options)
|
|
20
|
+
}
|
|
21
|
+
if (get && !noCache) {
|
|
22
|
+
const cachedResult = await get({ model: name, filter, options })
|
|
23
|
+
if (cachedResult) {
|
|
24
|
+
cachedResult.cached = true
|
|
25
|
+
return dataOnly ? cachedResult.data : cachedResult
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const records = await handler.call(this.app[driver.ns], { schema, filter, options })
|
|
29
|
+
if (!noHook) {
|
|
30
|
+
await runHook(`${this.name}.${name}:onAfterRecordFind`, filter, options, records)
|
|
31
|
+
await runHook(`${this.name}:onAfterRecordFind`, name, filter, options, records)
|
|
32
|
+
}
|
|
33
|
+
for (const idx in records.data) {
|
|
34
|
+
records.data[idx] = await this.pickRecord({ record: records.data[idx], fields, schema, hidden })
|
|
35
|
+
}
|
|
36
|
+
if (isSet(options.rels)) await multiRelRows.call(this, { schema, records: records.data, options })
|
|
37
|
+
if (set && !noCache) await set({ model: name, filter, options, records })
|
|
38
|
+
return dataOnly ? records.data : records
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default find
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
import singleRelRows from '../../../lib/single-rel-rows.js'
|
|
3
|
+
|
|
4
|
+
async function get (name, id, opts = {}) {
|
|
5
|
+
const { runHook, isSet } = this.app.bajo
|
|
6
|
+
const { get, set } = this.cache ?? {}
|
|
7
|
+
const { cloneDeep } = this.app.bajo.lib._
|
|
8
|
+
const options = cloneDeep(opts)
|
|
9
|
+
options.dataOnly = options.dataOnly ?? true
|
|
10
|
+
const { fields, dataOnly, noHook, noCache, hidden = [] } = options
|
|
11
|
+
await this.modelExists(name, true)
|
|
12
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-get')
|
|
13
|
+
id = this.sanitizeId(id, schema)
|
|
14
|
+
options.dataOnly = false
|
|
15
|
+
if (!noHook) {
|
|
16
|
+
await runHook(`${this.name}:onBeforeRecordGet`, name, id, options)
|
|
17
|
+
await runHook(`${this.name}.${name}:onBeforeRecordGet`, id, options)
|
|
18
|
+
}
|
|
19
|
+
if (get && !noCache) {
|
|
20
|
+
const cachedResult = await get({ model: name, id, options })
|
|
21
|
+
if (cachedResult) {
|
|
22
|
+
cachedResult.cached = true
|
|
23
|
+
return dataOnly ? cachedResult.data : cachedResult
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const record = await handler.call(this.app[driver.ns], { schema, id, options })
|
|
27
|
+
if (!noHook) {
|
|
28
|
+
await runHook(`${this.name}.${name}:onAfterRecordGet`, id, options, record)
|
|
29
|
+
await runHook(`${this.name}:onAfterRecordGet`, name, id, options, record)
|
|
30
|
+
}
|
|
31
|
+
record.data = await this.pickRecord({ record: record.data, fields, schema, hidden })
|
|
32
|
+
if (isSet(options.rels)) await singleRelRows.call(this, { schema, record: record.data, options })
|
|
33
|
+
|
|
34
|
+
if (set && !noCache) await set({ model: name, id, options, record })
|
|
35
|
+
return dataOnly ? record.data : record
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export default get
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
import handleAttachmentUpload from '../../../lib/handle-attachment-upload.js'
|
|
3
|
+
|
|
4
|
+
async function remove (name, id, opts = {}) {
|
|
5
|
+
const { runHook } = this.app.bajo
|
|
6
|
+
const { clearColl } = this.cache ?? {}
|
|
7
|
+
const { cloneDeep } = this.app.bajo.lib._
|
|
8
|
+
const options = cloneDeep(opts)
|
|
9
|
+
options.dataOnly = options.dataOnly ?? true
|
|
10
|
+
const { fields, dataOnly, noHook, noResult, hidden } = options
|
|
11
|
+
options.dataOnly = false
|
|
12
|
+
await this.modelExists(name, true)
|
|
13
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-remove')
|
|
14
|
+
id = this.sanitizeId(id, schema)
|
|
15
|
+
if (!noHook) {
|
|
16
|
+
await runHook(`${this.name}:onBeforeRecordRemove`, name, id, options)
|
|
17
|
+
await runHook(`${this.name}.${name}:onBeforeRecordRemove`, id, options)
|
|
18
|
+
}
|
|
19
|
+
const record = await handler.call(this.app[driver.ns], { schema, id, options })
|
|
20
|
+
if (options.req) {
|
|
21
|
+
if (options.req.file) await handleAttachmentUpload.call(this, { name: schema.name, id, options, action: 'remove' })
|
|
22
|
+
if (options.req.flash) options.req.flash('dbsuccess', { message: this.print.write('Record successfully removed'), record })
|
|
23
|
+
}
|
|
24
|
+
if (!noHook) {
|
|
25
|
+
await runHook(`${this.name}.${name}:onAfterRecordRemove`, id, options, record)
|
|
26
|
+
await runHook(`${this.name}:onAfterRecordRemove`, name, id, options, record)
|
|
27
|
+
}
|
|
28
|
+
if (clearColl) await clearColl({ model: name, id, options, record })
|
|
29
|
+
if (noResult) return
|
|
30
|
+
record.oldData = await this.pickRecord({ record: record.oldData, fields, schema, hidden })
|
|
31
|
+
return dataOnly ? record.oldData : record
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default remove
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
import checkUnique from '../../../lib/check-unique.js'
|
|
3
|
+
import handleAttachmentUpload from '../../../lib/handle-attachment-upload.js'
|
|
4
|
+
import execValidation from '../../../lib/exec-validation.js'
|
|
5
|
+
import execFeatureHook from '../../../lib/exec-feature-hook.js'
|
|
6
|
+
|
|
7
|
+
async function update (name, id, input, opts = {}) {
|
|
8
|
+
const { runHook, isSet } = this.app.bajo
|
|
9
|
+
const { clearColl } = this.cache ?? {}
|
|
10
|
+
const { get, forOwn, find, cloneDeep } = this.app.bajo.lib._
|
|
11
|
+
const options = cloneDeep(opts)
|
|
12
|
+
options.dataOnly = options.dataOnly ?? true
|
|
13
|
+
input = cloneDeep(input)
|
|
14
|
+
const { fields, dataOnly, noHook, noValidation, noCheckUnique, noFeatureHook, noResult, noSanitize, partial = true, hidden } = options
|
|
15
|
+
options.dataOnly = true
|
|
16
|
+
options.truncateString = options.truncateString ?? true
|
|
17
|
+
await this.modelExists(name, true)
|
|
18
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-update')
|
|
19
|
+
id = this.sanitizeId(id, schema)
|
|
20
|
+
let body = noSanitize ? input : await this.sanitizeBody({ body: input, schema, partial, strict: true })
|
|
21
|
+
delete body.id
|
|
22
|
+
if (!noHook) {
|
|
23
|
+
await runHook(`${this.name}:onBeforeRecordUpdate`, name, id, body, options)
|
|
24
|
+
await runHook(`${this.name}.${name}:onBeforeRecordUpdate`, id, body, options)
|
|
25
|
+
}
|
|
26
|
+
if (!noFeatureHook) await execFeatureHook.call(this, 'beforeUpdate', { schema, body })
|
|
27
|
+
if (!noValidation) body = await execValidation.call(this, { noHook, name, body, options, partial })
|
|
28
|
+
if (!noCheckUnique) await checkUnique.call(this, { schema, body, id })
|
|
29
|
+
let record
|
|
30
|
+
const nbody = {}
|
|
31
|
+
forOwn(body, (v, k) => {
|
|
32
|
+
if (v === undefined) return undefined
|
|
33
|
+
const prop = find(schema.properties, { name: k })
|
|
34
|
+
if (options.truncateString && isSet(v) && prop && ['string', 'text'].includes(prop.type)) v = v.slice(0, prop.maxLength)
|
|
35
|
+
nbody[k] = v
|
|
36
|
+
})
|
|
37
|
+
delete nbody.id
|
|
38
|
+
try {
|
|
39
|
+
record = await handler.call(this.app[driver.ns], { schema, id, body: nbody, options })
|
|
40
|
+
if (options.req) {
|
|
41
|
+
if (options.req.file) await handleAttachmentUpload.call(this, { name: schema.name, id, body, options, action: 'update' })
|
|
42
|
+
if (options.req.flash) options.req.flash('dbsuccess', { message: this.print.write('Record successfully updated'), record })
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (get(options, 'req.flash')) options.req.flash('dberr', err)
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
48
|
+
if (!noFeatureHook) await execFeatureHook.call(this, 'afterUpdate', { schema, body: nbody, record })
|
|
49
|
+
if (!noHook) {
|
|
50
|
+
await runHook(`${this.name}.${name}:onAfterRecordUpdate`, id, nbody, options, record)
|
|
51
|
+
await runHook(`${this.name}:onAfterRecordUpdate`, name, id, nbody, options, record)
|
|
52
|
+
}
|
|
53
|
+
if (clearColl) await clearColl({ model: name, id, body: nbody, options, record })
|
|
54
|
+
if (noResult) return
|
|
55
|
+
record.oldData = await this.pickRecord({ record: record.oldData, fields, schema, hidden })
|
|
56
|
+
record.data = await this.pickRecord({ record: record.data, fields, schema, hidden })
|
|
57
|
+
return dataOnly ? record.data : record
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default update
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
async function upsert (name, input, opts = {}) {
|
|
2
|
+
const { generateId } = this.app.bajo
|
|
3
|
+
const { find } = this.app.bajo.lib._
|
|
4
|
+
const { cloneDeep } = this.app.bajo.lib._
|
|
5
|
+
const options = cloneDeep(opts)
|
|
6
|
+
options.dataOnly = options.dataOnly ?? true
|
|
7
|
+
await this.modelExists(name, true)
|
|
8
|
+
const { schema } = this.getInfo(name)
|
|
9
|
+
const idField = find(schema.properties, { name: 'id' })
|
|
10
|
+
let id
|
|
11
|
+
if (idField.type === 'string') id = input.id ?? generateId()
|
|
12
|
+
else if (idField.type === 'integer') id = input.id ?? generateId('int')
|
|
13
|
+
id = this.sanitizeId(id, schema)
|
|
14
|
+
const old = await this.recordGet(name, id, { thrownNotFound: false, dataOnly: true, noHook: true, noCache: true })
|
|
15
|
+
if (old) return await this.recordUpdate(name, id, input, options)
|
|
16
|
+
return await this.recordCreate(name, input, options)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default upsert
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
async function sanitizeBody ({ body = {}, schema = {}, partial, strict }) {
|
|
2
|
+
const { isSet, dayjs } = this.app.bajo
|
|
3
|
+
const { has, get, isString, isNumber } = this.app.bajo.lib._
|
|
4
|
+
const result = {}
|
|
5
|
+
for (const p of schema.properties) {
|
|
6
|
+
if (partial && !has(body, p.name)) continue
|
|
7
|
+
if (['object', 'array'].includes(p.type)) {
|
|
8
|
+
if (isString(body[p.name])) {
|
|
9
|
+
try {
|
|
10
|
+
result[p.name] = JSON.parse(body[p.name])
|
|
11
|
+
} catch (err) {
|
|
12
|
+
result[p.name] = null
|
|
13
|
+
}
|
|
14
|
+
} else {
|
|
15
|
+
try {
|
|
16
|
+
result[p.name] = JSON.parse(JSON.stringify(body[p.name]))
|
|
17
|
+
} catch (err) {}
|
|
18
|
+
}
|
|
19
|
+
} else result[p.name] = body[p.name]
|
|
20
|
+
if (isSet(body[p.name])) {
|
|
21
|
+
if (p.type === 'boolean') result[p.name] = result[p.name] === null ? null : (!!result[p.name])
|
|
22
|
+
if (['float', 'double'].includes(p.type)) {
|
|
23
|
+
if (isNumber(body[p.name])) result[p.name] = body[p.name]
|
|
24
|
+
else if (strict) {
|
|
25
|
+
result[p.name] = Number(body[p.name])
|
|
26
|
+
} else {
|
|
27
|
+
result[p.name] = parseFloat(body[p.name]) || null
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (['integer', 'smallint'].includes(p.type)) {
|
|
31
|
+
if (isNumber(body[p.name])) result[p.name] = body[p.name]
|
|
32
|
+
else if (strict) {
|
|
33
|
+
result[p.name] = Number(body[p.name])
|
|
34
|
+
} else {
|
|
35
|
+
result[p.name] = parseInt(body[p.name]) || null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (p.type === 'timestamp') {
|
|
39
|
+
if (!isNumber(body[p.name])) result[p.name] = -1
|
|
40
|
+
else {
|
|
41
|
+
const dt = dayjs.unix(body[p.name])
|
|
42
|
+
result[p.name] = dt.isValid() ? dt.unix() : -1
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
for (const t of ['datetime', 'date|YYYY-MM-DD', 'time|HH:mm:ss']) {
|
|
46
|
+
const [type, input] = t.split('|')
|
|
47
|
+
if (p.type === type) result[p.name] = this.sanitizeDate(body[p.name], { input })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
if (p.default) {
|
|
52
|
+
result[p.name] = p.default
|
|
53
|
+
if (isString(p.default) && p.default.startsWith('helper:')) {
|
|
54
|
+
const helper = p.default.split(':')[1]
|
|
55
|
+
const method = get(this, helper)
|
|
56
|
+
if (method) result[p.name] = await this[method]()
|
|
57
|
+
} else {
|
|
58
|
+
if (['float', 'double'].includes(p.type)) result[p.name] = parseFloat(result[p.name]) || null
|
|
59
|
+
if (['integer', 'smallint'].includes(p.type)) result[p.name] = parseInt(result[p.name]) || null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export default sanitizeBody
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
function sanitizeDate (value, { input, output, silent = true } = {}) {
|
|
2
|
+
const { dayjs } = this.app.bajo.lib
|
|
3
|
+
if (value === 0) return null
|
|
4
|
+
if (!output) output = input
|
|
5
|
+
const dt = dayjs(value, input)
|
|
6
|
+
if (!dt.isValid()) {
|
|
7
|
+
if (silent) return -1
|
|
8
|
+
throw this.error('Invalid date')
|
|
9
|
+
}
|
|
10
|
+
if (output === 'native' || !output) return dt.toDate()
|
|
11
|
+
return dt.format(output)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default sanitizeDate
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
|
|
3
|
+
async function aggregate (name, filter = {}, options = {}) {
|
|
4
|
+
const { runHook } = this.app.bajo
|
|
5
|
+
const { dataOnly = true, noHook, aggregate } = options
|
|
6
|
+
options.dataOnly = false
|
|
7
|
+
await this.modelExists(name, true)
|
|
8
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'stat-aggregate')
|
|
9
|
+
if (!noHook) {
|
|
10
|
+
await runHook(`${this.name}:onBeforeStatAggregate`, name, aggregate, filter, options)
|
|
11
|
+
await runHook(`${this.name}.${name}:onBeforeStatAggregate`, aggregate, filter, options)
|
|
12
|
+
}
|
|
13
|
+
const rec = await handler.call(this.app[driver.ns], { schema, filter, options })
|
|
14
|
+
filter.query = await this.buildQuery({ filter, schema, options }) ?? {}
|
|
15
|
+
filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
|
|
16
|
+
if (!noHook) {
|
|
17
|
+
await runHook(`${this.name}.${name}:onAfterStatAggregate`, aggregate, filter, options, rec)
|
|
18
|
+
await runHook(`${this.name}:onAfterStatAggregate`, name, aggregate, filter, options, rec)
|
|
19
|
+
}
|
|
20
|
+
return dataOnly ? rec.data : rec
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default aggregate
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import resolveMethod from '../../../lib/resolve-method.js'
|
|
2
|
+
|
|
3
|
+
const types = ['daily', 'monthly', 'yearly']
|
|
4
|
+
|
|
5
|
+
async function histogram (name, filter = {}, options = {}) {
|
|
6
|
+
const { runHook, join } = this.app.bajo
|
|
7
|
+
const { dataOnly = true, noHook, type } = options
|
|
8
|
+
options.dataOnly = false
|
|
9
|
+
if (!types.includes(type)) throw this.error('Histogram type must be one of these: %s', join(types))
|
|
10
|
+
await this.modelExists(name, true)
|
|
11
|
+
const { handler, schema, driver } = await resolveMethod.call(this, name, 'stat-histogram')
|
|
12
|
+
filter.query = await this.buildQuery({ filter, schema, options }) ?? {}
|
|
13
|
+
filter.match = this.buildMatch({ input: filter.match, schema, options }) ?? {}
|
|
14
|
+
if (!noHook) {
|
|
15
|
+
await runHook(`${this.name}:onBeforeStatHistogram`, name, type, filter, options)
|
|
16
|
+
await runHook(`${this.name}.${name}:onBeforeStatHistogram`, type, filter, options)
|
|
17
|
+
}
|
|
18
|
+
const rec = await handler.call(this.app[driver.ns], { schema, type, filter, options })
|
|
19
|
+
if (!noHook) {
|
|
20
|
+
await runHook(`${this.name}.${name}:onAfterStatHistogram`, type, filter, options, rec)
|
|
21
|
+
await runHook(`${this.name}:onAfterStatHistogram`, name, type, filter, options, rec)
|
|
22
|
+
}
|
|
23
|
+
return dataOnly ? rec.data : rec
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default histogram
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import joi from 'joi'
|
|
2
|
+
|
|
3
|
+
const excludedTypes = ['object', 'array']
|
|
4
|
+
const excludedNames = []
|
|
5
|
+
|
|
6
|
+
const validator = {
|
|
7
|
+
string: ['alphanum', 'base64', 'case', 'creditCard', 'dataUri', 'domain', 'email', 'guid',
|
|
8
|
+
'uuid', 'hex', 'hostname', 'insensitive', 'ip', 'isoDate', 'isoDuration', 'length', 'lowercase',
|
|
9
|
+
'max', 'min', 'normalize', 'pattern', 'regex', 'replace', 'token', 'trim', 'truncate',
|
|
10
|
+
'uppercase', 'uri'],
|
|
11
|
+
number: ['great', 'less', 'max', 'min', 'multiple', 'negative', 'port', 'positive',
|
|
12
|
+
'sign', 'unsafe'],
|
|
13
|
+
boolean: ['falsy', 'sensitive', 'truthy'],
|
|
14
|
+
date: ['greater', 'iso', 'less', 'max', 'min'],
|
|
15
|
+
timestamp: ['timestamp']
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildFromDbSchema (schema, { fields = [], rule = {}, extProperties = [] } = {}) {
|
|
19
|
+
// if (schema.validation) return schema.validation
|
|
20
|
+
const {
|
|
21
|
+
isPlainObject, get, each, isEmpty, isString, forOwn, keys,
|
|
22
|
+
find, isArray, has, cloneDeep, concat
|
|
23
|
+
} = this.app.bajo.lib._
|
|
24
|
+
const obj = {}
|
|
25
|
+
const me = this
|
|
26
|
+
|
|
27
|
+
function getRuleKv (rule) {
|
|
28
|
+
let key
|
|
29
|
+
let value
|
|
30
|
+
let columns
|
|
31
|
+
if (isPlainObject(rule)) {
|
|
32
|
+
key = rule.rule
|
|
33
|
+
value = rule.params
|
|
34
|
+
columns = rule.fields
|
|
35
|
+
} else if (isString(rule)) {
|
|
36
|
+
[key, value, columns] = rule.split(':')
|
|
37
|
+
}
|
|
38
|
+
return { key, value, columns }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function applyFieldRules (prop, obj) {
|
|
42
|
+
const minMax = { min: false, max: false }
|
|
43
|
+
const rules = get(rule, prop.name, prop.rules ?? [])
|
|
44
|
+
if (!isArray(rules)) return rules
|
|
45
|
+
let isRef
|
|
46
|
+
each(rules, r => {
|
|
47
|
+
const types = validator[me.propType[prop.type].validator]
|
|
48
|
+
const { key, value } = getRuleKv(r)
|
|
49
|
+
if (key === 'ref') {
|
|
50
|
+
isRef = true
|
|
51
|
+
obj = joi.ref(value)
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
if (!key || !types.includes(key)) return undefined
|
|
55
|
+
if (keys(minMax).includes(key)) minMax[key] = true
|
|
56
|
+
obj = obj[key](value)
|
|
57
|
+
})
|
|
58
|
+
if (!isRef && ['string', 'text'].includes(prop.type)) {
|
|
59
|
+
forOwn(minMax, (v, k) => {
|
|
60
|
+
if (v) return undefined
|
|
61
|
+
if (has(prop, `${k}Length`)) obj = obj[k](prop[`${k}Length`])
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
if (!isRef && !['id'].includes(prop.name) && prop.required) obj = obj.required()
|
|
65
|
+
return obj
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const props = concat(cloneDeep(schema.properties), extProperties)
|
|
69
|
+
|
|
70
|
+
for (const p of props) {
|
|
71
|
+
if (excludedTypes.includes(p.type) || excludedNames.includes(p.name)) continue
|
|
72
|
+
if (fields.length > 0 && !fields.includes(p.name)) continue
|
|
73
|
+
let item
|
|
74
|
+
switch (p.type) {
|
|
75
|
+
case 'text':
|
|
76
|
+
case 'string': {
|
|
77
|
+
item = applyFieldRules(p, joi.string())
|
|
78
|
+
break
|
|
79
|
+
}
|
|
80
|
+
case 'smallint':
|
|
81
|
+
case 'integer':
|
|
82
|
+
item = applyFieldRules(p, joi.number().integer())
|
|
83
|
+
break
|
|
84
|
+
case 'float':
|
|
85
|
+
case 'double':
|
|
86
|
+
if (p.precision) item = applyFieldRules(p, joi.number().precision(p.precision))
|
|
87
|
+
else item = applyFieldRules(p, joi.number())
|
|
88
|
+
break
|
|
89
|
+
case 'time':
|
|
90
|
+
case 'date':
|
|
91
|
+
case 'datetime':
|
|
92
|
+
item = applyFieldRules(p, joi.date())
|
|
93
|
+
break
|
|
94
|
+
case 'timestamp':
|
|
95
|
+
item = applyFieldRules(p, joi.number().integer())
|
|
96
|
+
break
|
|
97
|
+
case 'boolean':
|
|
98
|
+
item = applyFieldRules(p, joi.boolean())
|
|
99
|
+
break
|
|
100
|
+
}
|
|
101
|
+
if (item) {
|
|
102
|
+
if (item.$_root) obj[p.name] = item.allow(null)
|
|
103
|
+
else obj[p.name] = item
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (isEmpty(obj)) return false
|
|
107
|
+
each(get(schema, 'globalRules', []), r => {
|
|
108
|
+
each(keys(obj), k => {
|
|
109
|
+
const prop = find(props, { name: k })
|
|
110
|
+
if (!prop) return undefined
|
|
111
|
+
const types = validator[me.propType[prop.type].validator]
|
|
112
|
+
const { key, value, columns = [] } = getRuleKv(r)
|
|
113
|
+
if (!types.includes(key)) return undefined
|
|
114
|
+
if (columns.length === 0 || columns.includes(k)) obj[k] = obj[k][key](value)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
const result = joi.object(obj)
|
|
118
|
+
if (fields.length === 0) return result
|
|
119
|
+
each(['with', 'xor', 'without'], k => {
|
|
120
|
+
const item = get(schema, `extRule.${k}`)
|
|
121
|
+
if (item) result[k](...item)
|
|
122
|
+
})
|
|
123
|
+
return result
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function validate (value, joiSchema, { ns, fields, extProperties, params } = {}) {
|
|
127
|
+
const { defaultsDeep, isSet } = this.app.bajo
|
|
128
|
+
const { isString, forOwn, find } = this.app.bajo.lib._
|
|
129
|
+
|
|
130
|
+
ns = ns ?? [this.name]
|
|
131
|
+
params = defaultsDeep(params, { abortEarly: false, convert: false, rule: undefined, allowUnknown: true })
|
|
132
|
+
const { rule } = params
|
|
133
|
+
if (isString(joiSchema)) {
|
|
134
|
+
const { schema } = this.getInfo(joiSchema)
|
|
135
|
+
forOwn(value, (v, k) => {
|
|
136
|
+
if (!isSet(v)) return undefined
|
|
137
|
+
const p = find(schema.properties, { name: k })
|
|
138
|
+
if (!p) return undefined
|
|
139
|
+
for (const t of ['date|YYYY-MM-DD', 'time|HH:mm:ss']) {
|
|
140
|
+
const [type, input] = t.split('|')
|
|
141
|
+
if (p.type === type) value[k] = this.sanitizeDate(value[k], { input, output: 'native' })
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
joiSchema = buildFromDbSchema.call(this, schema, { fields, rule, extProperties })
|
|
145
|
+
}
|
|
146
|
+
if (!joiSchema) return value
|
|
147
|
+
try {
|
|
148
|
+
return await joiSchema.validateAsync(value, params)
|
|
149
|
+
} catch (err) {
|
|
150
|
+
throw this.error('Validation Error', { details: err.details, values: err.values, ns, statusCode: 422, code: 'DB_VALIDATION' })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export default validate
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
function validationErrorMessage (err) {
|
|
2
|
+
let text = err.message
|
|
3
|
+
if (err.details) {
|
|
4
|
+
text += ' -> '
|
|
5
|
+
text += this.app.bajo.join(err.details.map((d, idx) => {
|
|
6
|
+
return `${d.field}@${err.model}: ${d.error} (${d.value})`
|
|
7
|
+
}))
|
|
8
|
+
}
|
|
9
|
+
return text
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default validationErrorMessage
|
package/bajo/start.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
async function start (conns = 'all', noRebuild = true) {
|
|
2
|
+
const { importModule, breakNsPath } = this.app.bajo
|
|
3
|
+
const { find, filter, isString, map } = this.app.bajo.lib._
|
|
4
|
+
if (conns === 'all') conns = this.connections
|
|
5
|
+
else if (isString(conns)) conns = filter(this.connections, { name: conns })
|
|
6
|
+
else conns = map(conns, c => find(this.connections, { name: c }))
|
|
7
|
+
for (const c of conns) {
|
|
8
|
+
const [ns] = breakNsPath(c.type)
|
|
9
|
+
const schemas = filter(this.schemas, { connection: c.name })
|
|
10
|
+
const mod = await importModule(`${ns}:/${this.name}/boot/instantiate.js`)
|
|
11
|
+
await mod.call(this.app[ns], { connection: c, noRebuild, schemas })
|
|
12
|
+
this.log.trace('- Driver \'%s:%s\' instantiated', c.driver, c.name)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default start
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
async function connection ({ path, args }) {
|
|
2
|
+
const { importPkg } = this.app.bajo
|
|
3
|
+
const { isEmpty, map, find } = this.app.bajo.lib._
|
|
4
|
+
const select = await importPkg('bajoCli:@inquirer/select')
|
|
5
|
+
const { getOutputFormat, writeOutput } = this.app.bajoCli
|
|
6
|
+
const format = getOutputFormat()
|
|
7
|
+
if (isEmpty(this.connections)) return this.print.fail('No connection found!', { exit: this.app.bajo.applet })
|
|
8
|
+
let name = args[0]
|
|
9
|
+
if (isEmpty(name)) {
|
|
10
|
+
const choices = map(this.connections, s => ({ value: s.name }))
|
|
11
|
+
name = await select({
|
|
12
|
+
message: this.print.write('Please choose a connection:'),
|
|
13
|
+
choices
|
|
14
|
+
})
|
|
15
|
+
}
|
|
16
|
+
const result = find(this.connections, { name })
|
|
17
|
+
if (!result) return this.print.fail('Can\'t find %s named \'%s\'', this.print.write('connection'), name, { exit: this.app.bajo.applet })
|
|
18
|
+
this.print.info('Done!')
|
|
19
|
+
await writeOutput(result, path, format)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export default connection
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const conns = []
|
|
2
|
+
|
|
3
|
+
async function postProcess ({ handler, params, path, processMsg, noConfirmation, options = {} } = {}) {
|
|
4
|
+
const { print, getConfig, saveAsDownload, importPkg, spinner } = this.app.bajo
|
|
5
|
+
const { prettyPrint } = this.app.bajoCli.helper
|
|
6
|
+
const { find, get } = this.app.bajo.lib._
|
|
7
|
+
const [stripAnsi, confirm] = await importPkg('bajoCli:strip-ansi', 'bajoCli:@inquirer/confirm')
|
|
8
|
+
const config = getConfig()
|
|
9
|
+
if (!noConfirmation && config.confirmation === false) noConfirmation = true
|
|
10
|
+
params.push({ fields: config.fields, dataOnly: !config.full })
|
|
11
|
+
|
|
12
|
+
const schema = find(this.schemas, { name: params[0] })
|
|
13
|
+
if (!schema) return print.fail('No schema found!', { exit: config.tool })
|
|
14
|
+
let cont = true
|
|
15
|
+
if (!noConfirmation) {
|
|
16
|
+
const answer = await confirm({ message: print.write('Are you sure to continue?'), default: false })
|
|
17
|
+
if (!answer) {
|
|
18
|
+
print.fail('Aborted!')
|
|
19
|
+
cont = false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (!cont) return
|
|
23
|
+
const spin = spinner().start(`${processMsg}...`)
|
|
24
|
+
const { connection } = this.getInfo(schema)
|
|
25
|
+
if (!conns.includes(connection.name)) {
|
|
26
|
+
await this.start(connection.name)
|
|
27
|
+
conns.push(connection.name)
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const resp = await this[handler](...params)
|
|
31
|
+
spin.succeed('Done!')
|
|
32
|
+
const result = config.pretty ? (await prettyPrint(resp)) : JSON.stringify(resp, null, 2)
|
|
33
|
+
if (config.save) {
|
|
34
|
+
const id = resp.id ?? get(resp, 'data.id') ?? get(resp, 'oldData.id')
|
|
35
|
+
const base = path === 'recordFind' ? params[0] : (params[0] + '/' + id)
|
|
36
|
+
const file = `/${path}/${base}.${config.pretty ? 'txt' : 'json'}`
|
|
37
|
+
await saveAsDownload(file, stripAnsi(result))
|
|
38
|
+
} else console.log(result)
|
|
39
|
+
} catch (err) {
|
|
40
|
+
if (config.log.tool) {
|
|
41
|
+
spin.stop()
|
|
42
|
+
console.error(err)
|
|
43
|
+
} else spin.fail('Error: %s', err.message)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default postProcess
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import postProcess from './lib/post-process.js'
|
|
2
|
+
|
|
3
|
+
async function modelClear ({ path, args, options }) {
|
|
4
|
+
const { print } = this.app.bajo
|
|
5
|
+
const { isEmpty } = this.app.bajo.lib._
|
|
6
|
+
if (isEmpty(this.schemas)) return print.fail('No schema found!', { exit: this.app.bajo.applet })
|
|
7
|
+
const [schema] = args
|
|
8
|
+
await postProcess.call(this, { handler: 'modelClear', params: [schema], path, processMsg: 'Clear records', options })
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default modelClear
|