dobo 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +23 -1
  2. package/bajo/config.json +27 -0
  3. package/bajo/hook/bajoI18N.db@before-resource-merge.js +10 -0
  4. package/bajo/hook/bajoI18N@before-init.js +6 -0
  5. package/bajo/init.js +18 -0
  6. package/bajo/method/aggregate-types.js +1 -0
  7. package/bajo/method/attachment/copy-uploaded.js +32 -0
  8. package/bajo/method/attachment/create.js +27 -0
  9. package/bajo/method/attachment/find.js +27 -0
  10. package/bajo/method/attachment/get-path.js +11 -0
  11. package/bajo/method/attachment/get.js +12 -0
  12. package/bajo/method/attachment/pre-check.js +8 -0
  13. package/bajo/method/attachment/remove.js +11 -0
  14. package/bajo/method/attachment/update.js +7 -0
  15. package/bajo/method/build-match.js +34 -0
  16. package/bajo/method/build-query.js +13 -0
  17. package/bajo/method/bulk/create.js +45 -0
  18. package/bajo/method/get-info.js +14 -0
  19. package/bajo/method/get-schema.js +10 -0
  20. package/bajo/method/model/clear.js +20 -0
  21. package/bajo/method/model/create.js +11 -0
  22. package/bajo/method/model/drop.js +11 -0
  23. package/bajo/method/model/exists.js +17 -0
  24. package/bajo/method/pick-record.js +30 -0
  25. package/bajo/method/prep-pagination.js +66 -0
  26. package/bajo/method/prop-type.js +43 -0
  27. package/bajo/method/record/clear.js +22 -0
  28. package/bajo/method/record/create.js +61 -0
  29. package/bajo/method/record/find-all.js +15 -0
  30. package/bajo/method/record/find-one.js +39 -0
  31. package/bajo/method/record/find.js +41 -0
  32. package/bajo/method/record/get.js +38 -0
  33. package/bajo/method/record/remove.js +34 -0
  34. package/bajo/method/record/update.js +60 -0
  35. package/bajo/method/record/upsert.js +19 -0
  36. package/bajo/method/sanitize/body.js +67 -0
  37. package/bajo/method/sanitize/date.js +14 -0
  38. package/bajo/method/sanitize/id.js +7 -0
  39. package/bajo/method/stat/aggregate.js +23 -0
  40. package/bajo/method/stat/histogram.js +26 -0
  41. package/bajo/method/validate.js +154 -0
  42. package/bajo/method/validation-error-message.js +12 -0
  43. package/bajo/start.js +16 -0
  44. package/bajoCli/applet/connection.js +22 -0
  45. package/bajoCli/applet/lib/post-process.js +47 -0
  46. package/bajoCli/applet/model-clear.js +11 -0
  47. package/bajoCli/applet/model-rebuild.js +77 -0
  48. package/bajoCli/applet/record-create.js +41 -0
  49. package/bajoCli/applet/record-find.js +27 -0
  50. package/bajoCli/applet/record-get.js +24 -0
  51. package/bajoCli/applet/record-remove.js +24 -0
  52. package/bajoCli/applet/record-update.js +47 -0
  53. package/bajoCli/applet/schema.js +22 -0
  54. package/bajoCli/applet/shell.js +48 -0
  55. package/bajoCli/applet/stat-count.js +24 -0
  56. package/bajoCli/applet.js +1 -0
  57. package/bajoI18N/resource/en-US.json +82 -0
  58. package/bajoI18N/resource/id.json +143 -0
  59. package/dobo/feature/created-at.js +18 -0
  60. package/dobo/feature/dt.js +13 -0
  61. package/dobo/feature/int-id.js +13 -0
  62. package/dobo/feature/updated-at.js +23 -0
  63. package/lib/add-fixtures.js +53 -0
  64. package/lib/build-bulk-action.js +12 -0
  65. package/lib/check-unique.js +39 -0
  66. package/lib/collect-connections.js +25 -0
  67. package/lib/collect-drivers.js +32 -0
  68. package/lib/collect-feature.js +23 -0
  69. package/lib/collect-schemas.js +77 -0
  70. package/lib/exec-feature-hook.js +12 -0
  71. package/lib/exec-validation.js +27 -0
  72. package/lib/generic-prop-sanitizer.js +38 -0
  73. package/lib/handle-attachment-upload.js +16 -0
  74. package/lib/merge-attachment-info.js +16 -0
  75. package/lib/multi-rel-rows.js +34 -0
  76. package/lib/resolve-method.js +15 -0
  77. package/lib/sanitize-schema.js +180 -0
  78. package/lib/single-rel-rows.js +32 -0
  79. package/package.json +1 -1
  80. 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,7 @@
1
+ function sanitizeId (id, schema) {
2
+ const prop = schema.properties.find(p => p.name === 'id')
3
+ if (prop.type === 'integer') id = parseInt(id)
4
+ return id
5
+ }
6
+
7
+ export default sanitizeId
@@ -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