dobo 2.21.1 → 2.22.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.
@@ -215,7 +215,9 @@
215
215
  "type": "Type",
216
216
  "model": "Model",
217
217
  "age": "Age",
218
- "provider": "Provider"
218
+ "provider": "Provider",
219
+ "image": "Image",
220
+ "attachment": "Attachment"
219
221
  },
220
222
  "validationError": "Validation Error",
221
223
  "validation": {
@@ -213,7 +213,9 @@
213
213
  "type": "Tipe",
214
214
  "model": "Model",
215
215
  "age": "Usia",
216
- "provider": "Penyedia"
216
+ "provider": "Penyedia",
217
+ "image": "Gambar",
218
+ "attachment": "Lampiran"
217
219
  },
218
220
  "validationError": "Kesalahan Validasi",
219
221
  "validation": {
@@ -0,0 +1,32 @@
1
+ import path from 'path'
2
+
3
+ async function handler (val, rec, opts) {
4
+ const atts = await this.listAttachment({ id: rec.id })
5
+ if (atts.length === 0) return
6
+ let items = atts.filter(att => att.mimeType.startsWith('image/'))
7
+ if (opts.asLink && this.app.waibu) items = items.map(f => `<a href="${f.url}">${opts.baseName ? path.basename(f.file) : f.file}</a>`)
8
+ else if (opts.baseName) items = items.map(f => path.basename(f.file))
9
+ if (opts.single) return items[0]
10
+ return opts.returnAsArray ? items : items.join(', ')
11
+ }
12
+
13
+ async function image (opts = {}) {
14
+ opts.field = opts.field ?? 'image'
15
+ opts.baseName = opts.baseName ?? true
16
+ opts.single = opts.single ?? true
17
+ return {
18
+ properties: {
19
+ name: opts.field,
20
+ type: 'string',
21
+ virtual: true,
22
+ getValue: async function (val, rec) {
23
+ return await handler.call(this, val, rec, { ...opts, asLink: false })
24
+ },
25
+ format: async function (val, rec) {
26
+ return await handler.call(this, val, rec, { ...opts, asLink: true })
27
+ }
28
+ }
29
+ }
30
+ }
31
+
32
+ export default image
@@ -1,9 +1,18 @@
1
1
  import path from 'path'
2
2
 
3
+ async function handleNotFound (req, reply) {
4
+ const { isEmpty, isString } = this.app.lib._
5
+ const { routePath } = this.app.waibu
6
+ if (!req.query.notfound) throw this.error('_notFound', { noContent: true })
7
+ const ext = path.extname(req.params.file)
8
+ const replacer = isString(req.query.notfound) ? req.query.notfound : `waibuStatic.asset:/not-found${isEmpty(ext) ? '.png' : ext}`
9
+ return reply.redirectTo(routePath(replacer))
10
+ }
11
+
3
12
  async function attachment (req, reply) {
4
- const { isString, isEmpty, find, last } = this.app.lib._
13
+ const { isEmpty, find, last } = this.app.lib._
5
14
  const { pascalCase } = this.app.lib.aneka
6
- const { routePath } = this.app.waibu
15
+ const { createThumbnail } = this.app.bajoExtra ?? {}
7
16
  const { fs } = this.app.lib
8
17
  const mdl = this.app.dobo.getModel(req.params.model)
9
18
  const type = req.query.type
@@ -20,11 +29,24 @@ async function attachment (req, reply) {
20
29
  file === decodeURI(req.params.file)
21
30
  })
22
31
  }
23
- if (!item) {
24
- if (!req.query.notfound) throw this.error('_notFound', { noContent: true })
25
- const ext = path.extname(req.params.file)
26
- const replacer = isString(req.query.notfound) ? req.query.notfound : `waibuStatic.asset:/not-found${isEmpty(ext) ? '.png' : ext}`
27
- return reply.redirectTo(routePath(replacer))
32
+ if (!item) return await handleNotFound.call(this, req, reply)
33
+ if (req.query.thumbnail) {
34
+ const dir = path.dirname(item.file)
35
+ const ext = path.extname(item.file)
36
+ const base = path.basename(item.file, ext)
37
+ const dest = `${dir}/_tn/${base}-${req.query.thumbnail}.png`
38
+ if (createThumbnail && !fs.existsSync(dest)) {
39
+ const opts = {
40
+ dir: `${dir}/_tn`,
41
+ size: req.query.thumbnail,
42
+ silent: true,
43
+ format: '.png'
44
+ }
45
+ await createThumbnail(item.file, opts)
46
+ }
47
+ if (!fs.existsSync(dest)) await handleNotFound.call(this, req, reply)
48
+ item.mimeType = 'image/png'
49
+ item.file = dest
28
50
  }
29
51
  if (!isEmpty(item.mimeType)) reply.header('Content-Type', item.mimeType)
30
52
  const stream = fs.createReadStream(item.file)
package/index.js CHANGED
@@ -158,7 +158,10 @@ async function factory (pkgName) {
158
158
  ttlDur: '10s'
159
159
  },
160
160
  hardCap: 10000, // max returned records
161
- warnings: true
161
+ warnings: true,
162
+ attachment: {
163
+ thumbSizes: ['s', 'm', 'l']
164
+ }
162
165
  },
163
166
  memDb: {
164
167
  autoSaveDur: '1s'
@@ -33,7 +33,7 @@ async function sanitizeProp (model, prop, indexes) {
33
33
  if (prop.virtual) {
34
34
  const keys = Object.keys(propType)
35
35
  if (!keys.includes(prop.type)) this.fatal('unknownPropType%s%s', `${prop.name}:${prop.type}`, model.name)
36
- for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable', 'feature']) {
36
+ for (const key of ['required', 'rules', 'index', 'validator', 'ref', 'rulesMsg', 'immutable']) {
37
37
  delete prop[key]
38
38
  }
39
39
  model.properties.push(prop)
@@ -2,6 +2,8 @@ import { mergeAttachmentInfo, getAttachmentPath } from './_util.js'
2
2
  const action = 'createAttachment'
3
3
 
4
4
  async function createAttachment (...args) {
5
+ const { createThumbnail } = this.app.bajoExtra
6
+ const { thumbSizes: size } = this.app.dobo.config.default.attachment
5
7
  if (!this.attachment) return
6
8
  if (args.length === 0) return this.action(action, ...args)
7
9
  const [id, opts = {}] = args
@@ -18,6 +20,9 @@ async function createAttachment (...args) {
18
20
  const dest = `${dir}/${file}`.replaceAll('//', '/')
19
21
  await fs.ensureDir(dir)
20
22
  await fs.copy(source, dest)
23
+ try {
24
+ if (createThumbnail) await createThumbnail(dest, { dir: `${dir}/_tn`, size, format: ['.png'] })
25
+ } catch (err) {}
21
26
  const rec = {
22
27
  field: field === '' ? undefined : field,
23
28
  dir,
@@ -20,13 +20,13 @@ async function createRecord (...args) {
20
20
  await execDynHook.call(this, 'beforeCreateRecord', input, options)
21
21
  if (!noValidation) await execValidation.call(this, input, options)
22
22
  let result = options.record ?? (await this.driver._createRecord(this, input, options)) ?? {}
23
+ await handleReq.call(this, result.data.id, 'created', options)
23
24
  if (noResult) return
24
25
  result = result ?? {}
25
26
  const { warnings } = getDefaultValues(options)
26
27
  if (!warnings) delete result.warnings
27
28
  if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
28
29
  if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
29
- await handleReq.call(this, result.data.id, 'created', options)
30
30
  await execDynHook.call(this, 'afterCreateRecord', input, result, options)
31
31
  await execModelHook.call(this, 'afterCreateRecord', input, result, options)
32
32
  await execHook.call(this, 'afterCreateRecord', input, result, options)
@@ -1,14 +1,23 @@
1
1
  import { getAttachmentPath } from './_util.js'
2
+ import path from 'path'
2
3
  const action = 'removeAttachment'
3
4
 
4
5
  async function removeAttachment (...args) {
5
6
  if (!this.attachment) return
6
7
  if (args.length === 0) return this.action(action, ...args)
7
8
  const [id, field, file, opts = {}] = args
8
- const { fs } = this.app.lib
9
- const path = await getAttachmentPath.call(this, id, field, file)
9
+ const { fs, fastGlob } = this.app.lib
10
+ const fullPath = await getAttachmentPath.call(this, id, field, file)
10
11
  const { req } = opts
11
- await fs.remove(path)
12
+ await fs.remove(fullPath)
13
+ const dir = path.dirname(fullPath)
14
+ const ext = path.extname(fullPath)
15
+ const base = path.basename(fullPath, ext)
16
+ const pattern = `${dir}/_tn/${base}-*.*`
17
+ const files = await fastGlob(pattern)
18
+ for (const f of files) {
19
+ await fs.remove(f)
20
+ }
12
21
  if (!opts.noFlash && req && req.flash) req.flash('notify', req.t('attachmentRemoved'))
13
22
  }
14
23
 
@@ -45,13 +45,13 @@ async function removeRecord (...args) {
45
45
  await execModelHook.call(this, 'beforeRemoveRecord', id, options)
46
46
  await execDynHook.call(this, 'beforeRemoveRecord', id, options)
47
47
  const result = options.record ?? (await this.driver._removeRecord(this, id, options)) ?? {}
48
+ await handleReq.call(this, result.oldData.id, 'removed', options)
48
49
  await clearCache.call(this, id)
49
50
  if (noResult) return
50
51
  const { warnings } = getDefaultValues(options)
51
52
  if (!warnings) delete result.warnings
52
53
  if (!noResultSanitizer) result.oldData = await this.sanitizeRecord(result.oldData, options)
53
54
  if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
54
- await handleReq.call(this, result.oldData.id, 'removed', options)
55
55
  await execDynHook.call(this, 'afterRemoveRecord', id, result, options)
56
56
  await execModelHook.call(this, 'afterRemoveRecord', id, result, options)
57
57
  await execHook.call(this, 'afterRemoveRecord', id, result, options)
@@ -67,6 +67,7 @@ async function updateRecord (...args) {
67
67
  await execDynHook.call(this, 'beforeUpdateRecord', id, input, options)
68
68
  if (!noValidation) await execValidation.call(this, input, options)
69
69
  const result = options.record ?? (await this.driver._updateRecord(this, id, input, options)) ?? {}
70
+ await handleReq.call(this, result.data.id, 'updated', options)
70
71
  await clearCache.call(this, id)
71
72
  if (noResult) return
72
73
  const { warnings } = getDefaultValues(options)
@@ -76,7 +77,6 @@ async function updateRecord (...args) {
76
77
  result.data = await this.sanitizeRecord(result.data, options)
77
78
  result.oldData = await this.sanitizeRecord(result.oldData, options)
78
79
  }
79
- await handleReq.call(this, result.data.id, 'updated', options)
80
80
  await execDynHook.call(this, 'afterUpdateRecord', id, input, result, options)
81
81
  await execModelHook.call(this, 'afterUpdateRecord', id, input, result, options)
82
82
  await execHook.call(this, 'afterUpdateRecord', id, input, result, options)
@@ -16,13 +16,13 @@ async function native (body = {}, opts = {}) {
16
16
  await execModelHook.call(this, 'beforeUpsertRecord', input, options)
17
17
  await execDynHook.call(this, 'beforeUpsertRecord', input, options)
18
18
  const result = options.record ?? (await this.driver._upsertRecord(this, input, options)) ?? {}
19
+ await handleReq.call(this, result.data.id, 'upserted', options)
19
20
  await clearCache.call(this, body.id)
20
21
  if (noResult) return
21
22
  const { warnings } = getDefaultValues(options)
22
23
  if (!warnings) delete result.warnings
23
24
  if (isSet(options.refs)) await getSingleRef.call(this, result.data, options)
24
25
  if (!noResultSanitizer) result.data = await this.sanitizeRecord(result.data, options)
25
- await handleReq.call(this, result.data.id, 'upserted', options)
26
26
  await execDynHook.call(this, 'afterUpsertRecord', input, result, options)
27
27
  await execModelHook.call(this, 'afterUpsertRecord', input, result, options)
28
28
  await execHook.call(this, 'afterUpsertRecord', input, result, options)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dobo",
3
- "version": "2.21.1",
3
+ "version": "2.22.1",
4
4
  "description": "DBMS for Bajo Framework",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/wiki/CHANGES.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-05-03
4
+
5
+ - [2.22.1] Bug fix in ```dobo:image``` feature
6
+
7
+ ## 2026-05-02
8
+
9
+ - [2.22.0] Add auto thumbnail creation when image attachment is uploaded
10
+ - [2.22.0] Add feature to get the thumbnail instead of attachment file in attachment route
11
+ - [2.22.0] Add ```dobo:image``` feature
12
+ - [2.22.0] Bug fix in ```model.createRecord()```
13
+ - [2.22.0] Bug fix in ```model.updateRecord()```
14
+ - [2.22.0] Bug fix in ```model.upsertRecord()```
15
+ - [2.22.0] Bug fix in ```model.removeRecord()```
16
+ - [2.22.0] Remove attachment now also remove corresponding thumbnails
17
+
3
18
  ## 2026-04-28
4
19
 
5
20
  - [2.21.1] Bug fix in ```collect-models.js```