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,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.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Database ORM/ODM for Bajo Framework",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"database",
|
|
16
|
+
"dobo",
|
|
16
17
|
"db",
|
|
17
18
|
"orm",
|
|
18
19
|
"bajo",
|