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
package/README.md CHANGED
@@ -1 +1,23 @@
1
- # dobo
1
+ # dobo
2
+
3
+ Plugin name: **bajoDb**, alias: **db**
4
+
5
+ ![GitHub package.json version](https://img.shields.io/github/package-json/v/ardhi/dobo) ![NPM Version](https://img.shields.io/npm/v/dobo)
6
+
7
+ > <br />**Attention**: I do NOT accept any pull request at the moment, thanks!<br /><br />
8
+
9
+ Database connectivity, tools, ORM/ODM support for [Bajo](https://github.com/ardhi/bajo)
10
+
11
+ ## Installation
12
+
13
+ Goto your ```<bajo-base-dir>``` and type:
14
+
15
+ ```bash
16
+ $ npm install dobo
17
+ ```
18
+
19
+ Now open your ```<bajo-data-dir>/config/.plugins``` and put ```dobo``` in it
20
+
21
+ ## License
22
+
23
+ [MIT](LICENSE)
@@ -0,0 +1,27 @@
1
+ {
2
+ "alias": "db",
3
+ "connections": [],
4
+ "dependencies": [],
5
+ "mergeProps": ["connections"],
6
+ "defaults": {
7
+ "property": {
8
+ "text": {
9
+ "kind": "text"
10
+ },
11
+ "string": {
12
+ "length": 50
13
+ }
14
+ },
15
+ "filter": {
16
+ "limit": 25,
17
+ "maxLimit": 200,
18
+ "sort": ["dt:-1", "updatedAt:-1", "updated_at:-1", "createdAt:-1", "createdAt:-1", "ts:-1", "username", "name"]
19
+ },
20
+ "idField": {
21
+ "type": "string",
22
+ "maxLength": 50,
23
+ "required": true,
24
+ "index": { "type": "primary" }
25
+ }
26
+ }
27
+ }
@@ -0,0 +1,10 @@
1
+ async function beforeResourceMerge (lng, content) {
2
+ const { eachPlugins, readConfig } = this.app.bajo
3
+ const { merge } = this.app.bajo.lib._
4
+ await eachPlugins(async function ({ file, ns }) {
5
+ const item = await readConfig(file, { ns })
6
+ merge(content, item)
7
+ }, { glob: `i18n/${lng}.json`, ns: this.name })
8
+ }
9
+
10
+ export default beforeResourceMerge
@@ -0,0 +1,6 @@
1
+ async function bajoI18nBeforeInit () {
2
+ const config = this.app.bajoI18N.config
3
+ if (!config.fallbackNS.includes(this.name)) config.fallbackNS.push(this.name)
4
+ }
5
+
6
+ export default bajoI18nBeforeInit
package/bajo/init.js ADDED
@@ -0,0 +1,18 @@
1
+ import collectConnections from '../lib/collect-connections.js'
2
+ import collectDrivers from '../lib/collect-drivers.js'
3
+ import collectFeature from '../lib/collect-feature.js'
4
+ import collectSchemas from '../lib/collect-schemas.js'
5
+
6
+ async function init () {
7
+ const { buildCollections } = this.app.bajo
8
+ const { fs } = this.app.bajo.lib
9
+ const cfg = this.config
10
+ fs.ensureDirSync(`${cfg.dir.data}/attachment`)
11
+ await collectDrivers.call(this)
12
+ this.connections = await buildCollections({ ns: this.name, handler: collectConnections, dupChecks: ['name'] })
13
+ if (this.connections.length === 0) this.log.warn('No %s found!', this.print.write('connection'))
14
+ await collectFeature.call(this)
15
+ await collectSchemas.call(this)
16
+ }
17
+
18
+ export default init
@@ -0,0 +1 @@
1
+ export default ['count', 'avg', 'min', 'max', 'sum']
@@ -0,0 +1,32 @@
1
+ import path from 'path'
2
+
3
+ async function copyUploaded (name, id, { req, setField, setFile, mimeType, stats, silent = true } = {}) {
4
+ const { fs } = this.app.bajo.lib
5
+ name = this.attachmentPreCheck(name)
6
+ if (!name) {
7
+ if (silent) return
8
+ throw this.error('Name must be provided')
9
+ }
10
+ if (!this.bajoWeb) {
11
+ if (silent) return
12
+ throw this.error('Plugin \'%s\' is missing')
13
+ }
14
+ const { dir, files } = await this.bajoWeb.getUploadedFiles(req.id, false, true)
15
+ const result = []
16
+ if (files.length === 0) return result
17
+ for (const f of files) {
18
+ let [field, ...parts] = path.basename(f).split('@')
19
+ if (parts.length === 0) continue
20
+ field = setField ?? field
21
+ const file = setFile ?? parts.join('@')
22
+ const opts = { source: f, field, file, mimeType, stats, req }
23
+ const rec = await this.attachmentCreate(name, id, opts)
24
+ delete rec.dir
25
+ result.push(rec)
26
+ if (setField || setFile) break
27
+ }
28
+ fs.removeSync(dir)
29
+ return result
30
+ }
31
+
32
+ export default copyUploaded
@@ -0,0 +1,27 @@
1
+ import mergeAttachmentInfo from '../../../lib/merge-attachment-info.js'
2
+
3
+ async function create (name, id, options = {}) {
4
+ const { fs } = this.app.bajo.lib
5
+ name = this.attachmentPreCheck(name)
6
+ if (!name) return
7
+ const { source, field, file } = options
8
+ if (!source) throw this.error('Invalid source')
9
+ const baseDir = await this.attachmentGetPath(name, id, field, file, { dirOnly: true })
10
+ const { fullPath, stats, mimeType, req } = options
11
+
12
+ let dir = `${baseDir}/${field}`
13
+ if ((field || '').endsWith('[]')) dir = `${baseDir}/${field.replace('[]', '')}`
14
+ const dest = `${dir}/${file}`.replaceAll('//', '/')
15
+ await fs.ensureDir(dir)
16
+ await fs.copy(source, dest)
17
+ const rec = {
18
+ field: field === '' ? undefined : field,
19
+ dir,
20
+ file
21
+ }
22
+ await mergeAttachmentInfo.call(this, rec, dest, { mimeType, fullPath, stats })
23
+ if (req && req.flash) req.flash('dbsuccess', { message: req.i18n.t('File successfully uploaded') })
24
+ return rec
25
+ }
26
+
27
+ export default create
@@ -0,0 +1,27 @@
1
+ import mergeAttachmentInfo from '../../../lib/merge-attachment-info.js'
2
+
3
+ async function find (name, id, options = {}) {
4
+ const { fastGlob, fs } = this.app.bajo.lib
5
+ const { getPluginDataDir } = this.app.bajo
6
+ name = this.attachmentPreCheck(name)
7
+ if (!name) return
8
+ const dir = `${getPluginDataDir(this.name)}/attachment/${name}/${id}`
9
+ if (!fs.existsSync(dir)) return []
10
+ const files = await fastGlob(`${dir}/**/*`)
11
+ const { fullPath, stats, mimeType } = options
12
+ const recs = []
13
+ for (const f of files) {
14
+ const item = f.replace(dir, '')
15
+ let [, field, file] = item.split('/')
16
+ if (!file) {
17
+ file = field
18
+ field = null
19
+ }
20
+ const rec = { field, file }
21
+ await mergeAttachmentInfo.call(this, rec, f, { mimeType, fullPath, stats })
22
+ recs.push(rec)
23
+ }
24
+ return recs
25
+ }
26
+
27
+ export default find
@@ -0,0 +1,11 @@
1
+ async function getPath (name, id, field, file, options = {}) {
2
+ const { pascalCase, getPluginDataDir } = this.app.bajo
3
+ const { fs } = this.app.bajo.lib
4
+ const dir = `${getPluginDataDir(this.name)}/attachment/${pascalCase(name)}/${id}`
5
+ if (options.dirOnly) return dir
6
+ const path = field ? `${dir}/${field}/${file}` : `${dir}/${file}`
7
+ if (!fs.existsSync(path)) throw this.error('notfound')
8
+ return path
9
+ }
10
+
11
+ export default getPath
@@ -0,0 +1,12 @@
1
+ async function get (name, id, field, file, options = {}) {
2
+ name = this.attachmentPreCheck(name)
3
+ if (!name) return
4
+ const { find } = this.app.bajo.lib._
5
+ const all = await this.attachmentFind(name, id, options)
6
+ if (field === 'null') field = null
7
+ const data = find(all, { field, file })
8
+ if (!data) throw this.error('notfound', { statusCode: 404 })
9
+ return data
10
+ }
11
+
12
+ export default get
@@ -0,0 +1,8 @@
1
+ function preCheck (name) {
2
+ name = this.app.bajo.pascalCase(name)
3
+ const schema = this.getSchema(name)
4
+ if (!schema.attachment) return false
5
+ return name
6
+ }
7
+
8
+ export default preCheck
@@ -0,0 +1,11 @@
1
+ async function remove (name, id, field, file, options = {}) {
2
+ const { fs } = this.app.bajo.lib
3
+ name = this.attachmentPreCheck(name)
4
+ if (!name) return
5
+ const path = await this.attachmentGetPath(name, id, field, file)
6
+ const { req } = options
7
+ await fs.remove(path)
8
+ if (req && req.flash) req.flash('dbsuccess', { message: req.i18n.t('File successfully removed') })
9
+ }
10
+
11
+ export default remove
@@ -0,0 +1,7 @@
1
+ import create from './create.js'
2
+
3
+ async function update (name, id, options = {}) {
4
+ return create.call(this, name, id, options)
5
+ }
6
+
7
+ export default update
@@ -0,0 +1,34 @@
1
+ function split (value, schema) {
2
+ let [field, val] = value.split(':').map(i => i.trim())
3
+ if (!val) {
4
+ val = field
5
+ field = '*'
6
+ }
7
+ return { field, value: val }
8
+ }
9
+
10
+ function buildMatch ({ input = '', schema, options }) {
11
+ const { isPlainObject, trim } = this.app.bajo.lib._
12
+ input = trim(input)
13
+ let items = {}
14
+ if (isPlainObject(input)) items = input
15
+ else if (input[0] === '{') items = JSON.parse(input)
16
+ else {
17
+ for (const item of input.split('+').map(i => i.trim())) {
18
+ const part = split.call(this, item, schema)
19
+ if (!items[part.field]) items[part.field] = []
20
+ items[part.field].push(...part.value.split(' ').filter(v => ![''].includes(v)))
21
+ }
22
+ }
23
+ const matcher = {}
24
+ for (const f of schema.fullText.fields) {
25
+ const value = []
26
+ if (typeof items[f] === 'string') items[f] = [items[f]]
27
+ if (Object.prototype.hasOwnProperty.call(items, f)) value.push(...items[f])
28
+ matcher[f] = value
29
+ }
30
+ if (Object.prototype.hasOwnProperty.call(items, '*')) matcher['*'] = items['*']
31
+ return matcher
32
+ }
33
+
34
+ export default buildMatch
@@ -0,0 +1,13 @@
1
+ import nql from '@tryghost/nql'
2
+
3
+ async function buildQuery ({ filter, schema, options = {} } = {}) {
4
+ const { trim, isString, isPlainObject } = this.app.bajo.lib._
5
+ let query = {}
6
+ if (isString(filter.query)) {
7
+ if (trim(filter.query).startsWith('{')) query = JSON.parse(filter.query)
8
+ else query = nql(filter.query).parse()
9
+ } else if (isPlainObject(filter.query)) query = filter.query
10
+ return query
11
+ }
12
+
13
+ export default buildQuery
@@ -0,0 +1,45 @@
1
+ import buildBulkAction from '../../../lib/build-bulk-action.js'
2
+ import execValidation from '../../../lib/exec-validation.js'
3
+ import execFeatureHook from '../../../lib/exec-feature-hook.js'
4
+
5
+ async function create (name, inputs, options) {
6
+ const { generateId, runHook, isSet } = this.app.bajo
7
+ const { clearColl } = this.cache ?? {}
8
+ const { find } = this.app.bajo.lib._
9
+ options.dataOnly = options.dataOnly ?? true
10
+ options.truncateString = options.truncateString ?? true
11
+ const { noHook, noValidation } = options
12
+ await this.modelExists(name, true)
13
+ const { handler, schema } = await buildBulkAction.call(this, name, 'create', options)
14
+ const idField = find(schema.properties, { name: 'id' })
15
+ const bodies = [...inputs]
16
+ for (let b of bodies) {
17
+ b.id = b.id ?? generateId(idField.type === 'integer' ? 'int' : undefined)
18
+ b = await this.sanitizeBody({ body: b, schema, strict: true })
19
+ if (!noValidation) b = await execValidation.call(this, { noHook, name, b, options })
20
+ }
21
+ if (!noHook) {
22
+ await runHook(`${this.name}:onBeforeBulkCreate`, name, bodies, options)
23
+ await runHook(`${this.name}.${name}:onBeforeBulkCreate`, bodies, options)
24
+ }
25
+ for (const idx in bodies) {
26
+ await execFeatureHook.call(this, 'beforeCreate', { schema, body: bodies[idx] })
27
+ // TODO: check unique?
28
+ for (const k in bodies[idx]) {
29
+ if (bodies[idx][k] === undefined) continue
30
+ const prop = find(schema.properties, { name: k })
31
+ if (options.truncateString && isSet(bodies[idx][k]) && prop && ['string', 'text'].includes(prop.type)) bodies[idx][k] = bodies[idx][k].slice(0, prop.maxLength)
32
+ }
33
+ }
34
+ await handler.call(this, { schema, bodies, options })
35
+ for (const idx in bodies) {
36
+ await execFeatureHook.call(this, 'afterCreate', { schema, body: bodies[idx] })
37
+ }
38
+ if (!noHook) {
39
+ await runHook(`${this.name}.${name}:onAfterBulkCreate`, bodies, options)
40
+ await runHook(`${this.name}:onAfterBulkCreate`, name, bodies, options)
41
+ }
42
+ if (clearColl) await clearColl({ model: name })
43
+ }
44
+
45
+ export default create
@@ -0,0 +1,14 @@
1
+ function getInfo (name) {
2
+ const { breakNsPath } = this.app.bajo
3
+ const { find, map } = this.app.bajo.lib._
4
+ const schema = this.getSchema(name)
5
+ const conn = find(this.connections, { name: schema.connection })
6
+ const [ns, type] = breakNsPath(conn.type)
7
+ const driver = find(this.drivers, { type, ns, driver: conn.driver })
8
+ const instance = find(this.app[driver.ns].instances, { name: schema.connection })
9
+ const opts = conn.type === 'mssql' ? { includeTriggerModifications: true } : undefined
10
+ const returning = [map(schema.properties, 'name'), opts]
11
+ return { instance, driver, connection: conn, returning, schema }
12
+ }
13
+
14
+ export default getInfo
@@ -0,0 +1,10 @@
1
+ function getSchema (input) {
2
+ const { find, isPlainObject, cloneDeep } = this.app.bajo.lib._
3
+ let name = isPlainObject(input) ? input.name : input
4
+ name = this.app.bajo.pascalCase(name)
5
+ const schema = find(this.schemas, { name })
6
+ if (!schema) throw this.error('Unknown model/schema \'%s\'', name)
7
+ return cloneDeep(schema)
8
+ }
9
+
10
+ export default getSchema
@@ -0,0 +1,20 @@
1
+ import resolveMethod from '../../../lib/resolve-method.js'
2
+
3
+ async function clear (name, options = {}) {
4
+ const { runHook } = this.app.bajo
5
+ await this.modelExists(name, true)
6
+ const { noHook } = options
7
+ const { handler, schema } = await resolveMethod.call(this, name, 'model-clear')
8
+ if (!noHook) {
9
+ await runHook(`${this.name}:onBeforeCollClear`, name, options)
10
+ await runHook(`${this.name}.${name}:onBeforeCollClear`, options)
11
+ }
12
+ const resp = await handler.call(this, { schema, options })
13
+ if (!noHook) {
14
+ await runHook(`${this.name}.${name}:onAfterCollClear`, options, resp)
15
+ await runHook(`${this.name}:onAfterCollClear`, name, options, resp)
16
+ }
17
+ return resp
18
+ }
19
+
20
+ export default clear
@@ -0,0 +1,11 @@
1
+ import resolveMethod from '../../../lib/resolve-method.js'
2
+
3
+ async function create (name, options = {}) {
4
+ const { runHook } = this.app.bajo
5
+ const { handler, schema } = await resolveMethod.call(this, name, 'model-create', options)
6
+ await runHook(`${this.name}:beforeCollCreate` + name, schema)
7
+ await handler.call(this, { schema, options })
8
+ await runHook(`${this.name}:afterCollCreate` + name, schema)
9
+ }
10
+
11
+ export default create
@@ -0,0 +1,11 @@
1
+ import resolveMethod from '../../../lib/resolve-method.js'
2
+
3
+ async function drop (name, options = {}) {
4
+ const { runHook } = this.app.bajo
5
+ const { handler, schema } = await resolveMethod.call(this, name, 'model-drop', options)
6
+ await runHook(`${this.name}:beforeCollDrop` + name, schema)
7
+ await handler.call(this, { schema, options })
8
+ await runHook(`${this.name}:afterCollDrop` + name, schema)
9
+ }
10
+
11
+ export default drop
@@ -0,0 +1,17 @@
1
+ import resolveMethod from '../../../lib/resolve-method.js'
2
+
3
+ const cache = {}
4
+
5
+ async function exists (name, thrown, options = {}) {
6
+ if (cache[name]) return cache[name]
7
+ const { runHook } = this.app.bajo
8
+ const { handler, schema } = await resolveMethod.call(this, name, 'model-exists', options)
9
+ await runHook(`${this.name}:beforeCollExists` + name, schema)
10
+ const exist = await handler.call(this, { schema, options })
11
+ await runHook(`${this.name}:afterCollExists` + name, schema, exist)
12
+ if (!exist && thrown) throw this.error('Model doesn\'t exist yet. Please do model rebuild first')
13
+ cache[name] = exist
14
+ return exist
15
+ }
16
+
17
+ export default exists
@@ -0,0 +1,30 @@
1
+ async function transform ({ record, schema, hidden = [] } = {}) {
2
+ const { dayjs } = this.app.bajo
3
+ if (record._id) {
4
+ record.id = record._id
5
+ delete record._id
6
+ }
7
+ const result = {}
8
+ for (const p of schema.properties) {
9
+ if (hidden.includes(p.name)) continue
10
+ result[p.name] = record[p.name] ?? null
11
+ if (record[p.name] === null) continue
12
+ switch (p.type) {
13
+ case 'time': result[p.name] = dayjs(record[p.name]).format('HH:mm:ss'); break
14
+ case 'date': result[p.name] = dayjs(record[p.name]).format('YYYY-MM-DD'); break
15
+ }
16
+ }
17
+ return await this.sanitizeBody({ body: result, schema, partial: true, ignoreNull: true })
18
+ }
19
+
20
+ async function pickRecord ({ record, fields, schema = {}, hidden = [] } = {}) {
21
+ const { isArray, pick, clone, isEmpty, omit } = this.app.bajo.lib._
22
+ if (isEmpty(record)) return record
23
+ if (hidden.length > 0) record = omit(record, hidden)
24
+ if (!isArray(fields)) return await transform.call(this, { record, schema, hidden })
25
+ const fl = clone(fields)
26
+ if (!fl.includes('id')) fl.unshift('id')
27
+ return pick(await transform.call(this, { record, schema, hidden }), fl)
28
+ }
29
+
30
+ export default pickRecord
@@ -0,0 +1,66 @@
1
+ function buildPageSkipLimit (filter) {
2
+ let limit = parseInt(filter.limit) || this.config.defaults.filter.limit
3
+ if (limit > this.config.defaults.filter.maxLimit) limit = this.config.defaults.filter.maxLimit
4
+ if (limit < 1) limit = 1
5
+ let page = parseInt(filter.page) || 1
6
+ if (page < 1) page = 1
7
+ let skip = (page - 1) * limit
8
+ if (filter.skip) {
9
+ skip = parseInt(filter.skip) || skip
10
+ page = undefined
11
+ }
12
+ if (skip < 0) skip = 0
13
+ return { page, skip, limit }
14
+ }
15
+
16
+ function buildSort (input, schema, allowSortUnindexed) {
17
+ const { isEmpty, map, each, isPlainObject, isString, trim, filter, keys } = this.app.bajo.lib._
18
+ let sort
19
+ if (schema && isEmpty(input)) {
20
+ const columns = map(schema.properties, 'name')
21
+ each(this.config.defaults.filter.sort, s => {
22
+ const [col] = s.split(':')
23
+ if (columns.includes(col)) {
24
+ input = s
25
+ return false
26
+ }
27
+ })
28
+ }
29
+ if (!isEmpty(input)) {
30
+ if (isPlainObject(input)) sort = input
31
+ else if (isString(input)) {
32
+ const item = {}
33
+ each(input.split('+'), text => {
34
+ let [col, dir] = map(trim(text).split(':'), i => trim(i))
35
+ dir = (dir ?? '').toUpperCase()
36
+ dir = dir === 'DESC' ? -1 : parseInt(dir) || 1
37
+ item[col] = dir / Math.abs(dir)
38
+ })
39
+ sort = item
40
+ }
41
+ if (schema) {
42
+ const indexes = map(filter(schema.properties, p => !!p.index), 'name')
43
+ each(schema.indexes, item => {
44
+ indexes.push(...item.fields)
45
+ })
46
+ const items = keys(sort)
47
+ each(items, i => {
48
+ if (!indexes.includes(i) && !allowSortUnindexed) throw this.error('Sort on unindexed field: \'%s@%s\'', i, schema.name)
49
+ // if (schema.fullText.fields.includes(i)) throw this.error('Can\'t sort on full-text index: \'%s@%s\'', i, schema.name)
50
+ })
51
+ }
52
+ }
53
+ return sort
54
+ }
55
+
56
+ async function prepPagination (filter = {}, schema, options = {}) {
57
+ const { page, skip, limit } = buildPageSkipLimit.call(this, filter)
58
+ let sortInput = filter.sort
59
+ try {
60
+ sortInput = JSON.parse(sortInput)
61
+ } catch (err) {}
62
+ const sort = buildSort.call(this, sortInput, schema, options.allowSortUnindexed)
63
+ return { limit, page, skip, sort }
64
+ }
65
+
66
+ export default prepPagination
@@ -0,0 +1,43 @@
1
+ const propType = {
2
+ integer: {
3
+ validator: 'number'
4
+ },
5
+ smallint: {
6
+ validator: 'number'
7
+ },
8
+ text: {
9
+ validator: 'string',
10
+ kind: 'text',
11
+ choices: ['text', 'mediumtext', 'longtext']
12
+ },
13
+ string: {
14
+ validator: 'string',
15
+ maxLength: 255,
16
+ minLength: 0
17
+ },
18
+ float: {
19
+ validator: 'number'
20
+ },
21
+ double: {
22
+ validator: 'number'
23
+ },
24
+ boolean: {
25
+ validator: 'boolean'
26
+ },
27
+ date: {
28
+ validator: 'date'
29
+ },
30
+ datetime: {
31
+ validator: 'date'
32
+ },
33
+ time: {
34
+ validator: 'date'
35
+ },
36
+ timestamp: {
37
+ validator: 'timestamp'
38
+ },
39
+ object: {},
40
+ array: {}
41
+ }
42
+
43
+ export default propType
@@ -0,0 +1,22 @@
1
+ import resolveMethod from '../../../lib/resolve-method.js'
2
+
3
+ async function clear (name, opts = {}) {
4
+ const { runHook } = this.app.bajo
5
+ await this.modelExists(name, true)
6
+ const { cloneDeep } = this.app.bajo.lib._
7
+ const options = cloneDeep(opts)
8
+ const { noHook } = options
9
+ const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-clear')
10
+ if (!noHook) {
11
+ await runHook(`${this.name}:onBeforeRecordClear`, name, options)
12
+ await runHook(`${this.name}.${name}:onBeforeRecordClear`, options)
13
+ }
14
+ const resp = await handler.call(this.app[driver.ns], { schema, options })
15
+ if (!noHook) {
16
+ await runHook(`${this.name}.${name}:onAfterRecordClear`, options, resp)
17
+ await runHook(`${this.name}:onAfterRecordClear`, name, options, resp)
18
+ }
19
+ return resp
20
+ }
21
+
22
+ export default clear
@@ -0,0 +1,61 @@
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 create (name, input, opts = {}) {
8
+ const { generateId, runHook, isSet } = this.app.bajo
9
+ const { clearColl } = this.cache ?? {}
10
+ const { get, find, forOwn, 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, hidden } = options
15
+ options.truncateString = options.truncateString ?? true
16
+ options.dataOnly = false
17
+ await this.modelExists(name, true)
18
+ const { handler, schema, driver } = await resolveMethod.call(this, name, 'record-create', options)
19
+ const idField = find(schema.properties, { name: 'id' })
20
+ if (!isSet(input.id)) {
21
+ if (idField.type === 'string') input.id = generateId()
22
+ else if (['integer', 'smallint'].includes(idField.type) && !idField.autoInc) input.id = generateId('int')
23
+ }
24
+ let body = noSanitize ? input : await this.sanitizeBody({ body: input, schema, strict: true })
25
+ if (!noHook) {
26
+ await runHook(`${this.name}:onBeforeRecordCreate`, name, body, options)
27
+ await runHook(`${this.name}.${name}:onBeforeRecordCreate`, body, options)
28
+ }
29
+ if (!noFeatureHook) await execFeatureHook.call(this, 'beforeCreate', { schema, body })
30
+ if (!noValidation) body = await execValidation.call(this, { noHook, name, body, options })
31
+ if (isSet(body.id) && !noCheckUnique) await checkUnique.call(this, { schema, body })
32
+ let record = {}
33
+ try {
34
+ const nbody = {}
35
+ forOwn(body, (v, k) => {
36
+ if (v === undefined) return undefined
37
+ const prop = find(schema.properties, { name: k })
38
+ if (options.truncateString && isSet(v) && prop && ['string', 'text'].includes(prop.type)) v = v.slice(0, prop.maxLength)
39
+ nbody[k] = v
40
+ })
41
+ record = await handler.call(this.app[driver.ns], { schema, body: nbody, options })
42
+ if (options.req) {
43
+ if (options.req.file) await handleAttachmentUpload.call(this, { name: schema.name, id: body.id, body, options, action: 'create' })
44
+ if (options.req.flash) options.req.flash('dbsuccess', { message: this.print.write('Record successfully created'), record })
45
+ }
46
+ } catch (err) {
47
+ if (get(options, 'req.flash')) options.req.flash('dberr', err)
48
+ throw err
49
+ }
50
+ if (!noFeatureHook) await execFeatureHook.call(this, 'afterCreate', { schema, body, record })
51
+ if (!noHook) {
52
+ await runHook(`${this.name}.${name}:onAfterRecordCreate`, body, options, record)
53
+ await runHook(`${this.name}:onAfterRecordCreate`, name, body, options, record)
54
+ }
55
+ if (clearColl) await clearColl({ model: name, body, options, record })
56
+ if (noResult) return
57
+ record.data = await this.pickRecord({ record: record.data, fields, schema, hidden })
58
+ return dataOnly ? record.data : record
59
+ }
60
+
61
+ export default create