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,53 @@
1
+ import path from 'path'
2
+
3
+ async function addFixture (name, spin) {
4
+ const { resolvePath, readConfig, eachPlugins, getPluginDataDir } = this.app.bajo
5
+ const { isEmpty, isArray } = this.app.bajo.lib._
6
+ const { schema, connection } = this.getInfo(name)
7
+ if (connection.proxy) {
8
+ this.log.warn('\'%s\' is bound to a proxied connection, skipped!', schema.name)
9
+ return
10
+ }
11
+ const result = { success: 0, failed: 0 }
12
+ const base = path.basename(schema.file, path.extname(schema.file))
13
+ // original
14
+ const pattern = resolvePath(`${path.dirname(schema.file)}/../fixture/${base}.*`)
15
+ let items = await readConfig(pattern, { ns: schema.ns, ignoreError: true })
16
+ if (isEmpty(items)) items = []
17
+ // override
18
+ const overrides = await readConfig(`${getPluginDataDir(this.name)}/fixture/override/${schema.name}.*`, { ns: this.name, ignoreError: true })
19
+ if (isArray(overrides) && !isEmpty(overrides)) items = overrides
20
+ // extend
21
+ const me = this
22
+ await eachPlugins(async function ({ dir, ns }) {
23
+ const extend = await readConfig(`${dir}/${me.name}/fixture/extend/${schema.name}.*`, { ns, ignoreError: true })
24
+ if (isArray(extend) && !isEmpty(extend)) items.push(...extend)
25
+ })
26
+ if (isEmpty(items)) return result
27
+ const opts = { noHook: true, noCache: true }
28
+ for (const item of items) {
29
+ try {
30
+ for (const k in item) {
31
+ const v = item[k]
32
+ if (typeof v === 'string' && v.slice(0, 2) === '?:') {
33
+ let [, model, field, ...query] = v.split(':')
34
+ if (!field) field = 'id'
35
+ const recs = await this.recordFind(model, { query: query.join(':') }, opts)
36
+ item[k] = (recs[0] ?? {})[field]
37
+ }
38
+ if (v === null) item[k] = undefined
39
+ }
40
+ await this.recordCreate(schema.name, item, { force: true })
41
+ result.success++
42
+ if (spin) spin.setText('%s: %d of %d records added', schema.name, result.success, items.length)
43
+ } catch (err) {
44
+ err.model = schema.name
45
+ if (this.app.bajo.applet) this.print.fail(this.validationErrorMessage(err))
46
+ // else this.log.error('Add fixture \'%s@%s\' error: %s', schema.name, schema.connection, err.message)
47
+ result.failed++
48
+ }
49
+ }
50
+ return result
51
+ }
52
+
53
+ export default addFixture
@@ -0,0 +1,12 @@
1
+ async function buildBulkAction (name, action, options = {}) {
2
+ const { fs, importModule } = this.app.bajo
3
+ const { camelCase } = this.app.bajo.lib._
4
+ const { schema, driver, connection } = await this.getInfo(name)
5
+ if (!options.force && (schema.disabled ?? []).includes(action)) throw this.error('Method \'%s:%s\' is disabled', camelCase('bulk ' + action), name)
6
+ const file = `${driver.plugin}:/${this.name}/method/bulk/${action}.js`
7
+ if (!fs.existsSync(file)) throw this.error('Method \'%s:%s\' is unsupported', camelCase('bulk ' + action), name)
8
+ const handler = await importModule(file)
9
+ return { handler, schema, driver, connection }
10
+ }
11
+
12
+ export default buildBulkAction
@@ -0,0 +1,39 @@
1
+ async function checkUnique ({ schema, body, id }) {
2
+ const { isSet } = this.app.bajo
3
+ const { filter, map, set } = this.app.bajo.lib._
4
+ const singles = map(filter(schema.properties, p => (p.index ?? {}).type === 'unique'), 'name')
5
+ const opts = { noHook: true, noCache: true, thrownNotFound: false }
6
+ let old = {}
7
+ if (id) old = (await this.recordGet(schema.name, id, opts)) ?? {}
8
+ for (const s of singles) {
9
+ if (!isSet(body[s])) continue
10
+ if (id && body[s] === old[s]) continue
11
+ const query = set({}, s, body[s])
12
+ const resp = await this.recordFind(schema.name, { query, limit: 1 }, opts)
13
+ if (resp.length !== 0) {
14
+ const details = [{ field: s, error: 'Unique constraint error', value: id }]
15
+ throw this.error('Unique constraint error', { details })
16
+ }
17
+ }
18
+ const multis = filter(schema.indexes, i => i.type === 'unique')
19
+ for (const m of multis) {
20
+ const query = {}
21
+ let empty = true
22
+ let same = true
23
+ for (const f of m.fields) {
24
+ if (body[f]) empty = false
25
+ if (body[f] !== old[f]) same = false
26
+ query[f] = body[f]
27
+ }
28
+ if (empty || same) continue
29
+ const resp = await this.recordFind(schema.name, { query, limit: 1 }, { noHook: true, noCache: true })
30
+ if (resp.length !== 0) {
31
+ const details = map(m.fields, f => {
32
+ return { field: f, error: 'Unique constraint error' }
33
+ })
34
+ throw this.error('Unique constraint error', { details })
35
+ }
36
+ }
37
+ }
38
+
39
+ export default checkUnique
@@ -0,0 +1,25 @@
1
+ async function defSanitizer (item) {
2
+ // if (!item.connection) fatal('\'%s@%s\' key is required', 'connection', item.name, { payload: item })
3
+ const { merge } = this.app.bajo.lib._
4
+ return merge({}, item)
5
+ }
6
+
7
+ async function collectConnections ({ item, index, options }) {
8
+ const conn = item
9
+ const { importModule, breakNsPath } = this.app.bajo
10
+ const { has, find } = this.app.bajo.lib._
11
+ if (!has(conn, 'type')) this.fatal('Connection must have a valid DB type')
12
+ const [ns, type] = breakNsPath(conn.type)
13
+ const driver = find(this.drivers, { ns, type })
14
+ if (!driver) this.fatal('Unsupported DB type \'%s\'', conn.type)
15
+ let file = `${ns}:/${this.name}/lib/${type}/conn-sanitizer.js`
16
+ if (driver.provider) file = `${driver.provider}:/${ns}/lib/${type}/conn-sanitizer.js`
17
+ let sanitizer = await importModule(file)
18
+ if (!sanitizer) sanitizer = defSanitizer
19
+ const result = await sanitizer.call(this, conn)
20
+ result.proxy = result.proxy ?? false
21
+ result.driver = driver.driver
22
+ return result
23
+ }
24
+
25
+ export default collectConnections
@@ -0,0 +1,32 @@
1
+ async function collectDrivers () {
2
+ const { eachPlugins, readConfig, runHook } = this.app.bajo
3
+ const { isString, find, pick, merge } = this.app.bajo.lib._
4
+ const me = this
5
+ me.drivers = []
6
+ await runHook(`${this.name}:beforeCollectDrivers`)
7
+ await eachPlugins(async function ({ file, ns }) {
8
+ const info = await readConfig(file, { ns })
9
+ if (!info.type) this.fatal('A DB driver must provide at least one database type')
10
+ if (!info.driver) this.fatal('A DB driver must have a driver name')
11
+ if (isString(info.type)) info.type = [info.type]
12
+ if (!info.idField) info.idField = me.config.defaults.idField
13
+ info.idField.name = 'id'
14
+ for (const t of info.type) {
15
+ const [type, provider] = t.split('@')
16
+ const exists = find(me.drivers, { type, ns })
17
+ if (exists) this.fatal('Database type \'%s\' already supported by driver \'%s\'', type, info.driver)
18
+ const driver = pick(find(me.app[ns].drivers, { name: type }) ?? {}, ['dialect', 'idField', 'lowerCaseColl', 'returning'])
19
+ const ext = {
20
+ type,
21
+ ns,
22
+ provider,
23
+ driver: info.driver,
24
+ idField: info.idField
25
+ }
26
+ me.drivers.push(merge(ext, driver))
27
+ }
28
+ }, { glob: 'boot/driver.*', baseNs: this.name })
29
+ await runHook(`${this.name}:afterCollectDrivers`)
30
+ }
31
+
32
+ export default collectDrivers
@@ -0,0 +1,23 @@
1
+ import path from 'path'
2
+
3
+ async function handler ({ file, alias, ns }) {
4
+ const { importModule } = this.app.bajo
5
+ const { camelCase, isFunction } = this.app.bajo.lib._
6
+ let name = camelCase(path.basename(file, '.js'))
7
+ if (ns !== this.name) name = `${ns}:${name}`
8
+ const mod = await importModule(file)
9
+ if (!isFunction(mod)) this.fatal('Feature \'%s\' should be an async function', name)
10
+ this.feature = this.feature ?? {}
11
+ this.feature[name] = mod
12
+ this.log.trace('- %s', name)
13
+ }
14
+
15
+ async function collectFeature () {
16
+ const { eachPlugins } = this.app.bajo
17
+ this.feature = {}
18
+ this.log.trace('Loading DB feature')
19
+ await eachPlugins(handler, { glob: 'feature/*.js', baseNs: this.ns })
20
+ this.log.debug('Total loaded features: %d', Object.keys(this.feature).length)
21
+ }
22
+
23
+ export default collectFeature
@@ -0,0 +1,77 @@
1
+ import path from 'path'
2
+ import sanitizeSchema from './sanitize-schema.js'
3
+
4
+ async function handler ({ file, alias, ns }) {
5
+ const { readConfig, pascalCase, eachPlugins } = this.app.bajo
6
+ const { isPlainObject, each, find, has, isArray, forOwn, isString, merge } = this.app.bajo.lib._
7
+ const { fastGlob } = this.app.bajo.lib
8
+
9
+ const defName = pascalCase(`${alias} ${path.basename(file, path.extname(file))}`)
10
+ const mod = await readConfig(file, { ns, ignoreError: true })
11
+ if (!isPlainObject(mod)) this.fatal('Invalid schema \'%s\'', defName)
12
+ mod.name = mod.name ?? defName
13
+ mod.modelName = mod.modelName ?? mod.name
14
+ if (!mod.connection) mod.connection = 'default'
15
+ mod.file = file
16
+ mod.ns = ns
17
+ mod.attachment = mod.attachment ?? true
18
+ mod.feature = mod.feature ?? []
19
+ const feats = []
20
+ if (isArray(mod.feature)) {
21
+ each(mod.feature, f => {
22
+ if (isString(f)) feats.push({ name: f })
23
+ else if (isPlainObject(f)) feats.push(f)
24
+ })
25
+ } else if (isPlainObject(mod.feature)) {
26
+ forOwn(mod.feature, (v, k) => {
27
+ feats.push(merge({}, v, { name: k }))
28
+ })
29
+ }
30
+ mod.feature = feats
31
+ if ((mod.properties ?? []).length === 0) this.fatal('Schema \'%s\' doesn\'t have properties at all', mod.name)
32
+ // schema extender
33
+ const me = this
34
+ await eachPlugins(async function (opts) {
35
+ const glob = `${opts.dir}/${me.name}/schema/extend/${mod.name}.*`
36
+ const files = await fastGlob(glob)
37
+ for (const file of files) {
38
+ const extender = await readConfig(file, { ns: opts.ns, ignoreError: true })
39
+ if (!isPlainObject(extender)) return undefined
40
+ each(extender.properties ?? [], p => {
41
+ if (isString(p) && mod.properties.includes(p)) return undefined
42
+ else if (find(mod.properties, { name: p.name })) return undefined
43
+ mod.properties.push(p)
44
+ })
45
+ const feats = []
46
+ if (isArray(extender.feature)) {
47
+ each(extender.feature, f => {
48
+ if (isString(f)) feats.push({ name: f })
49
+ else if (isPlainObject(f)) feats.push(f)
50
+ })
51
+ } else if (isPlainObject(extender.feature)) {
52
+ forOwn(extender.feature, (v, k) => {
53
+ feats.push(merge({}, v, { name: k }))
54
+ })
55
+ }
56
+ if (feats.length > 0) mod.feature.push(...feats)
57
+ if (opts.plugin === this.app.bajo.mainNs) {
58
+ each(['connection', 'modelName'], i => {
59
+ if (has(extender, i)) mod[i] = extender[i]
60
+ })
61
+ }
62
+ mod.extender = mod.extender ?? []
63
+ mod.extender.push(opts.plugin)
64
+ }
65
+ })
66
+ return mod
67
+ }
68
+
69
+ async function collectSchemas () {
70
+ const { eachPlugins } = this.app.bajo
71
+ const { isEmpty } = this.app.bajo.lib._
72
+ const result = await eachPlugins(handler, { glob: 'schema/*.*', baseNs: this.name })
73
+ if (isEmpty(result)) this.log.warn('No %s found!', this.print.write('schema'))
74
+ else await sanitizeSchema.call(this, result)
75
+ }
76
+
77
+ export default collectSchemas
@@ -0,0 +1,12 @@
1
+ async function execFeatureHook (name, { schema, body } = {}) {
2
+ const { get } = this.app.bajo.lib._
3
+ for (const f of schema.feature) {
4
+ const fn = get(this.feature, f.name)
5
+ if (!fn) continue
6
+ const input = await fn.call(this, f)
7
+ const hook = get(input, 'hook.' + name)
8
+ if (hook) await hook.call(this, { schema, body })
9
+ }
10
+ }
11
+
12
+ export default execFeatureHook
@@ -0,0 +1,27 @@
1
+ async function execValidation ({ noHook, name, body, options, partial }) {
2
+ const { runHook } = this.app.bajo
3
+ const { get, keys } = this.app.bajo.lib._
4
+ if (!noHook) {
5
+ await runHook(`${this.name}:onBeforeRecordValidation`, name, body, options)
6
+ await runHook(`${this.name}.${name}:onBeforeRecordValidation`, body, options)
7
+ }
8
+ const { validation = {} } = options
9
+ if (partial) {
10
+ validation.fields = keys(body)
11
+ }
12
+ try {
13
+ body = await this.validate(body, name, validation)
14
+ } catch (err) {
15
+ if (err.code === 'DB_VALIDATION' && get(options, 'req.flash')) {
16
+ options.req.flash('validation', err)
17
+ }
18
+ throw err
19
+ }
20
+ if (!noHook) {
21
+ await runHook(`${this.name}:onAfterRecordValidation`, name, body, options)
22
+ await runHook(`${this.name}.${name}:onAfterRecordValidation`, body, options)
23
+ }
24
+ return body
25
+ }
26
+
27
+ export default execValidation
@@ -0,0 +1,38 @@
1
+ const indexTypes = ['default', 'unique', 'primary', 'fulltext']
2
+
3
+ async function genericPropSanitizer ({ prop, schema, driver }) {
4
+ const { join } = this.app.bajo
5
+ const { has, get, each } = this.app.bajo.lib._
6
+ const def = this.propType[prop.type]
7
+ // detect from drivers
8
+ /*
9
+ if (prop.name === 'id' && ['smallint', 'integer'].includes(prop.type) && driver.driver !== 'knex') {
10
+ fatal('Integer types of ID only supported by knex driver')
11
+ }
12
+ */
13
+ if (prop.type === 'string') {
14
+ def.minLength = prop.minLength ?? 0
15
+ def.maxLength = prop.maxLength ?? 255
16
+ if (has(prop, 'length')) def.maxLength = prop.length
17
+ if (prop.required && def.minLength === 0) def.minLength = 1
18
+ if (def.minLength > 0) prop.required = true
19
+ }
20
+ if (prop.autoInc && !['smallint', 'integer'].includes(prop.type)) delete prop.autoInc
21
+ each(['minLength', 'maxLength', 'kind'], p => {
22
+ if (!has(def, p)) {
23
+ delete prop[p]
24
+ return undefined
25
+ }
26
+ prop[p] = get(prop, p, get(this.config, `defaults.property.${prop.type}.${p}`, def[p]))
27
+ if (def.choices && !def.choices.includes(prop[p])) {
28
+ this.fatal('Unsupported %s \'%s\' for \'%s@%s\'. Allowed choices: %s',
29
+ p, prop[p], prop.name, schema.name, join(def.choices))
30
+ }
31
+ })
32
+ if (prop.index && !indexTypes.includes(prop.index.type)) {
33
+ this.fatal('Unsupported index type %s for \'%s@%s\'. Allowed choices: %s',
34
+ prop.index.type, prop.name, schema.name, join(indexTypes))
35
+ }
36
+ }
37
+
38
+ export default genericPropSanitizer
@@ -0,0 +1,16 @@
1
+ async function handleAttachmentUpload ({ action, name, id, options = {} } = {}) {
2
+ const { getPluginDataDir } = this.app.bajo
3
+ const { fs } = this.app.bajo.lib
4
+ const { req, mimeType, stats, setFile, setField } = options
5
+
6
+ name = this.attachmentPreCheck(name)
7
+ if (!name) return
8
+ if (action === 'remove') {
9
+ const dir = `${getPluginDataDir(this.name)}/attachment/${name}/${id}`
10
+ await fs.remove(dir)
11
+ return
12
+ }
13
+ return this.attachmentCopyUploaded(name, id, { req, mimeType, stats, setFile, setField })
14
+ }
15
+
16
+ export default handleAttachmentUpload
@@ -0,0 +1,16 @@
1
+ async function mergeAttachmentInfo (rec, source, { mimeType, stats, fullPath }) {
2
+ const { importPkg } = this.app.bajo
3
+ const { fs } = this.app.bajo.lib
4
+ const { pick } = this.app.bajo.lib._
5
+ if (!this.bajoWeb) return
6
+ const mime = await importPkg('bajoWeb:mime')
7
+
8
+ if (mimeType) rec.mimeType = mime.getType(rec.file)
9
+ if (fullPath) rec.fullPath = source
10
+ if (stats) {
11
+ const s = fs.statSync(source)
12
+ rec.stats = pick(s, ['size', 'atime', 'ctime', 'mtime'])
13
+ }
14
+ }
15
+
16
+ export default mergeAttachmentInfo
@@ -0,0 +1,34 @@
1
+ async function multiRelRows ({ schema, records, options = {} }) {
2
+ const { isSet } = this.app.bajo
3
+ const props = schema.properties.filter(p => isSet(p.rel))
4
+ const rels = {}
5
+ options.rels = options.rels ?? []
6
+ if (props.length > 0) {
7
+ for (const prop of props) {
8
+ for (const key in prop.rel) {
9
+ if (!((typeof options.rels === 'string' && ['*', 'all'].includes(options.rels)) || options.rels.includes(key))) continue
10
+ const val = prop.rel[key]
11
+ if (val.fields.length === 0) continue
12
+ if (val.type !== 'one-to-one') continue
13
+ const matches = records.map(r => r[prop.name])
14
+ const relschema = this.getSchema(val.schema)
15
+ const query = {}
16
+ query[val.propName] = { $in: matches }
17
+ const relFilter = { query, limit: matches.length }
18
+ const relOptions = { dataOnly: true, rels: [] }
19
+ const results = await this.recordFind(relschema.name, relFilter, relOptions)
20
+ const fields = [...val.fields]
21
+ if (!fields.includes(prop.name)) fields.push(prop.name)
22
+ for (const i in records) {
23
+ records[i]._rel = records[i]._rel ?? {}
24
+ const rec = records[i]
25
+ const res = results.find(r => r[val.propName] === rec[prop.name])
26
+ if (res) records[i]._rel[key] = await this.pickRecord({ record: res, fields, schema: relschema })
27
+ }
28
+ }
29
+ }
30
+ }
31
+ return rels
32
+ }
33
+
34
+ export default multiRelRows
@@ -0,0 +1,15 @@
1
+ async function resolveMethod (name, method, options = {}) {
2
+ const { importModule } = this.app.bajo
3
+ const { fs } = this.app.bajo.lib
4
+ const { camelCase } = this.app.bajo.lib._
5
+ const { schema, driver, connection } = this.getInfo(name)
6
+ if (!options.force && (schema.disabled ?? []).includes(method)) throw this.error('Method \'%s@%s\' is disabled', camelCase(method), name)
7
+ const cfg = this.app[driver.ns].config
8
+ const [group, action] = method.split('-')
9
+ const file = `${cfg.dir.pkg}/${this.name}/method/${group}/${action}.js`
10
+ if (!fs.existsSync(file)) throw this.error('Method \'%s@%s\' is unsupported', camelCase(method), name)
11
+ const handler = await importModule(file)
12
+ return { handler, schema, driver, connection }
13
+ }
14
+
15
+ export default resolveMethod
@@ -0,0 +1,180 @@
1
+ import genericPropSanitizer from './generic-prop-sanitizer.js'
2
+
3
+ async function sanitizeFeature (item) {
4
+ const { get, isPlainObject, mergeWith, isArray } = this.app.bajo.lib._
5
+ for (const f of item.feature) {
6
+ const feature = get(this.feature, f.name) // source from collectFeature
7
+ if (!feature) this.fatal('Unknown feature \'%s@%s\'', f.name, item.name)
8
+ let [ns, path] = f.name.split(':')
9
+ if (!path) ns = this.name
10
+ const input = await feature.call(this.app[ns], f)
11
+ let props = input.properties
12
+ if (isPlainObject(props)) props = [props]
13
+ if (props.length > 0) item.properties.push(...props)
14
+ item.globalRules = item.globalRules ?? []
15
+ if (input.globalRules) {
16
+ item.globalRules = mergeWith(item.globalRules, input.globalRules, (oval, sval) => {
17
+ if (isArray(oval)) return oval.concat(sval)
18
+ })
19
+ }
20
+ }
21
+ }
22
+
23
+ async function sanitizeIndexes (item) {
24
+ for (const idx of item.indexes) {
25
+ if (!(typeof idx.fields === 'string' || Array.isArray(idx.fields))) this.fatal('Only accept array of field names or single string of field name \'%s@%s\'', 'indexes', item.name)
26
+ }
27
+ }
28
+
29
+ async function sanitizeFullText (item) {
30
+ for (const f of item.fullText.fields ?? []) {
31
+ const prop = item.properties.find(p => p.name === f)
32
+ if (!prop) this.fatal('Invalid field name \'%s@%s\'', f, item.name)
33
+ // if (prop.type !== 'text') fatal.call(this, 'Fulltext index only available for field type \'text\' in \'%s@%s\'', f, item.name)
34
+ }
35
+ }
36
+
37
+ async function sanitizeSchema (items) {
38
+ const { freeze, fatal, importModule, defaultsDeep, join, breakNsPath } = this.app.bajo
39
+ const { map, keys, findIndex, find, each, isString, get, isPlainObject } = this.app.bajo.lib._
40
+ const properties = keys(this.propType)
41
+ const schemas = []
42
+ this.log.debug('Loading DB schemas')
43
+ for (const k in items) {
44
+ this.log.trace('- %s (%d)', k, keys(items[k]).length)
45
+ for (const f in items[k]) {
46
+ const item = items[k][f]
47
+ const conn = find(this.connections, { name: item.connection })
48
+ if (!conn) fatal.call(this, 'Unknown connection \'%s@%s\'', item.name, item.connection)
49
+ item.fullText = item.fullText ?? { fields: [] }
50
+ item.indexes = item.indexes ?? []
51
+ const [ns, type] = breakNsPath(conn.type)
52
+ const driver = find(this.drivers, { type, ns, driver: conn.driver })
53
+ if (driver.lowerCaseColl) item.modelName = item.modelName.toLowerCase()
54
+ let file = `${ns}:/${this.name}/lib/${type}/prop-sanitizer.js`
55
+ let propSanitizer = await importModule(file)
56
+ if (!propSanitizer) propSanitizer = genericPropSanitizer
57
+ for (const idx in item.properties) {
58
+ let prop = item.properties[idx]
59
+ if (isString(prop)) {
60
+ let [name, type, maxLength, index, required] = prop.split(':')
61
+ if (!type) type = 'string'
62
+ maxLength = maxLength ?? 255
63
+ prop = { name, type }
64
+ if (type === 'string') prop.maxLength = parseInt(maxLength) || undefined
65
+ if (index) prop.index = { type: index === 'true' ? 'default' : index }
66
+ prop.required = required === 'true'
67
+ item.properties[idx] = prop
68
+ } else {
69
+ if (isString(prop.index)) prop.index = { type: prop.index }
70
+ else if (prop.index === true) prop.index = { type: 'default' }
71
+ }
72
+ }
73
+ const idx = findIndex(item.properties, { name: 'id' })
74
+ if (idx === -1) item.properties.unshift(driver.idField)
75
+ item.feature = item.feature ?? []
76
+ await sanitizeFeature.call(this, item)
77
+ item.disabled = item.disabled ?? []
78
+ if (item.readonly) {
79
+ item.disabled = ['create', 'update', 'remove']
80
+ delete item.readonly
81
+ }
82
+ for (const idx in item.properties) {
83
+ let prop = item.properties[idx]
84
+ if (!prop.type) {
85
+ prop.type = 'string'
86
+ prop.maxLength = 255
87
+ }
88
+ if (!properties.includes(prop.type)) {
89
+ let success = false
90
+ const feature = get(this.feature, isPlainObject(prop.type) ? prop.type.name : prop.type)
91
+ if (feature) {
92
+ let opts = { fieldName: prop.name }
93
+ if (isPlainObject(prop.type)) opts = defaultsDeep(opts, prop.type)
94
+ else opts.name = prop.type
95
+ const feat = find(item.feature, opts)
96
+ if (!feat) item.feature.push(opts)
97
+ const input = await feature.call(this, opts)
98
+ if (input.properties && input.properties.length > 0) {
99
+ prop = input.properties[0]
100
+ success = true
101
+ }
102
+ }
103
+ if (!success) fatal.call(this, 'Unsupported property type \'%s@%s\' in \'%s\'', prop.type, prop.name, item.name)
104
+ }
105
+ if (prop.index) {
106
+ if (prop.index === 'unique') prop.index = { type: 'unique' }
107
+ else if (prop.index === 'fulltext') prop.index = { type: 'fulltext' }
108
+ else if (prop.index === 'primary') prop.index = { type: 'primary' }
109
+ else if (prop.index === true) prop.index = { type: 'default' }
110
+ }
111
+
112
+ await propSanitizer.call(this, { prop, schema: item, connection: conn, driver })
113
+ if (prop.index && prop.index.type === 'primary' && prop.name !== 'id') fatal.call(this, 'Primary index should only be used for \'id\' field')
114
+ if (prop.index && prop.index.type === 'fulltext') {
115
+ item.fullText.fields.push(prop.name)
116
+ prop.index.type = 'default'
117
+ }
118
+ item.properties[idx] = prop
119
+ }
120
+ await sanitizeIndexes.call(this, item)
121
+ await sanitizeFullText.call(this, item)
122
+ const all = []
123
+ each(item.properties, p => {
124
+ if (!all.includes(p.name)) all.push(p.name)
125
+ else fatal.call(this, 'Field \'%s@%s\' should be used only once', p.name, item.name)
126
+ })
127
+ file = `${ns}:/${this.name}/lib/${type}/schema-sanitizer.js`
128
+ const schemaSanitizer = await importModule(file)
129
+ if (schemaSanitizer) await schemaSanitizer.call(this, { schema: item, connection: conn, driver })
130
+ schemas.push(item)
131
+ }
132
+ }
133
+ for (const i in schemas) {
134
+ const schema = schemas[i]
135
+ for (const i2 in schema.properties) {
136
+ const prop = schema.properties[i2]
137
+ if (prop.type !== 'string') delete prop.maxLength
138
+ if (prop.rel) {
139
+ for (const key in prop.rel) {
140
+ let def = prop.rel[key]
141
+ if (isString(def)) {
142
+ const [rschema, rfield] = def.split(':')
143
+ def = { schema: rschema, propName: rfield }
144
+ }
145
+ def.type = def.type ?? 'one-to-one'
146
+ const rel = find(schemas, { name: def.schema })
147
+ if (!rel) {
148
+ fatal.call(this, 'No schema found for relationship \'%s@%s:%s\'', `${def.schema}:${def.propName}`, schema.name, prop.name)
149
+ }
150
+ const rprop = find(rel.properties, { name: def.propName })
151
+ if (!rprop) fatal.call(this, 'No property found for relationship \'%s@%s:%s\'', `${def.schema}:${def.propName}`, schema.name, prop.name)
152
+ def.fields = def.fields ?? []
153
+ if (['*', 'all'].includes(def.fields)) {
154
+ const relschema = find(schemas, { name: def.schema })
155
+ def.fields = relschema.properties.map(p => p.name)
156
+ }
157
+ if (def.fields.length > 0 && !def.fields.includes('id')) def.fields.push('id')
158
+ for (const f of def.fields) {
159
+ const p = find(rel.properties, { name: f })
160
+ if (!p) fatal.call(this, 'Unknown property for field \'%s\' in relationship \'%s@%s:%s\'', p, `${def.schema}:${def.propName}`, schema.name, prop.name)
161
+ }
162
+ prop.type = rprop.type
163
+ if (rprop.type === 'string') prop.maxLength = rprop.maxLength
164
+ else {
165
+ delete prop.maxLength
166
+ delete prop.minLength
167
+ }
168
+ prop.rel[key] = def
169
+ }
170
+ // TODO: propSanitizer must be called again
171
+ }
172
+ schema.properties[i2] = prop
173
+ }
174
+ }
175
+ this.schemas = schemas
176
+ freeze(this.schemas)
177
+ this.log.debug('Loaded schemas: %s', join(map(this.schemas, 'name')))
178
+ }
179
+
180
+ export default sanitizeSchema
@@ -0,0 +1,32 @@
1
+ async function singleRelRows ({ schema, record, options = {} }) {
2
+ const { isSet } = this.app.bajo
3
+ const props = schema.properties.filter(p => isSet(p.rel) && !(options.hidden ?? []).includes(p.name))
4
+ const rels = {}
5
+ options.rels = options.rels ?? []
6
+ if (props.length > 0) {
7
+ for (const prop of props) {
8
+ for (const key in prop.rel) {
9
+ if (!((typeof options.rels === 'string' && ['*', 'all'].includes(options.rels)) || options.rels.includes(key))) continue
10
+ const val = prop.rel[key]
11
+ if (val.fields.length === 0) continue
12
+ const relschema = this.getSchema(val.schema)
13
+ const relFilter = options.filter
14
+ const query = {}
15
+ query[val.propName] = record[prop.name]
16
+ relFilter.query = query
17
+ const relOptions = { dataOnly: true, rels: [] }
18
+ const results = await this.recordFind(relschema.name, relFilter, relOptions)
19
+ const fields = [...val.fields]
20
+ if (!fields.includes(prop.name)) fields.push(prop.name)
21
+ const data = []
22
+ for (const res of results) {
23
+ data.push(await this.pickRecord({ record: res, fields, schema: relschema }))
24
+ }
25
+ rels[key] = ['one-to-one'].includes(val.type) ? data[0] : data
26
+ }
27
+ }
28
+ }
29
+ record._rel = rels
30
+ }
31
+
32
+ export default singleRelRows
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Database ORM/ODM for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -0,0 +1,4 @@
1
+ [{
2
+ "prefix": "attachment",
3
+ "root": "data:/attachment"
4
+ }]