@ditojs/server 1.27.1 → 1.29.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "1.27.1",
3
+ "version": "1.29.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Server – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/master/packages/server",
@@ -22,10 +22,10 @@
22
22
  "node >= 18"
23
23
  ],
24
24
  "dependencies": {
25
- "@ditojs/admin": "^1.27.0",
26
- "@ditojs/build": "^1.25.0",
27
- "@ditojs/router": "^1.27.0",
28
- "@ditojs/utils": "^1.27.0",
25
+ "@ditojs/admin": "^1.29.0",
26
+ "@ditojs/build": "^1.29.0",
27
+ "@ditojs/router": "^1.29.0",
28
+ "@ditojs/utils": "^1.29.0",
29
29
  "@koa/cors": "^4.0.0",
30
30
  "@koa/multer": "^3.0.2",
31
31
  "@originjs/vite-plugin-commonjs": "^1.0.3",
@@ -36,19 +36,19 @@
36
36
  "data-uri-to-buffer": "^4.0.1",
37
37
  "eventemitter2": "^6.4.9",
38
38
  "file-type": "^18.2.1",
39
- "image-size": "^1.0.2",
40
39
  "koa": "^2.14.1",
41
- "koa-bodyparser": "^4.3.0",
40
+ "koa-bodyparser": "^4.4.0",
42
41
  "koa-compose": "^4.1.0",
43
42
  "koa-compress": "^5.1.0",
44
43
  "koa-conditional-get": "^3.0.0",
45
44
  "koa-etag": "^4.0.0",
46
- "koa-helmet": "^6.1.0",
45
+ "koa-helmet": "^7.0.1",
47
46
  "koa-mount": "^4.0.0",
48
47
  "koa-passport": "^6.0.0",
49
48
  "koa-response-time": "^2.1.0",
50
49
  "koa-session": "^6.4.0",
51
50
  "koa-static": "^5.0.0",
51
+ "leather": "^2.1.4",
52
52
  "mime-types": "^2.1.35",
53
53
  "multer": "^1.4.5-lts.1",
54
54
  "multer-s3": "https://github.com/ditojs/multer-s3#dito",
@@ -59,11 +59,11 @@
59
59
  "picocolors": "^1.0.0",
60
60
  "picomatch": "^2.3.1",
61
61
  "pino": "^8.11.0",
62
- "pino-pretty": "^9.4.0",
62
+ "pino-pretty": "^10.0.0",
63
63
  "pluralize": "^8.0.0",
64
64
  "repl": "^0.1.3",
65
65
  "uuid": "^9.0.0",
66
- "vite": "^4.1.4",
66
+ "vite": "^4.2.1",
67
67
  "vite-plugin-vue2": "^2.0.3",
68
68
  "vue": "^2.7.14",
69
69
  "vue-template-compiler": "^2.7.14"
@@ -74,23 +74,23 @@
74
74
  "objection": "^3.0.1"
75
75
  },
76
76
  "devDependencies": {
77
- "@aws-sdk/client-s3": "^3.282.0",
77
+ "@aws-sdk/client-s3": "^3.295.0",
78
78
  "@types/koa-bodyparser": "^4.3.10",
79
79
  "@types/koa-compress": "^4.0.3",
80
80
  "@types/koa-logger": "^3.1.2",
81
81
  "@types/koa-pino-logger": "^3.0.1",
82
82
  "@types/koa-response-time": "^2.1.2",
83
- "@types/koa-session": "^5.10.6",
83
+ "@types/koa-session": "^6.4.0",
84
84
  "@types/koa-static": "^4.0.2",
85
- "@types/koa__cors": "^3.3.1",
86
- "@types/node": "^18.14.6",
85
+ "@types/koa__cors": "^4.0.0",
86
+ "@types/node": "^18.15.5",
87
87
  "knex": "^2.4.2",
88
88
  "objection": "^3.0.1",
89
89
  "type-fest": "^3.6.1",
90
- "typescript": "^4.9.5"
90
+ "typescript": "^5.0.2"
91
91
  },
92
92
  "types": "types",
93
- "gitHead": "b0658f0d25d0a234a4b7a3ad2df957a92a0d37e7",
93
+ "gitHead": "6e93fe59126eab3d5ee1961d9e03f0fbdc297fba",
94
94
  "scripts": {
95
95
  "types": "tsc --noEmit ./src/index.d.ts"
96
96
  },
@@ -40,7 +40,7 @@ export const AssetMixin = mixin(Model => class extends TimeStampedMixin(Model) {
40
40
  url: {
41
41
  type: 'string'
42
42
  },
43
- // These are only used when the storage defines `config.readImageSize`:
43
+ // These are only used when the storage defines `config.readDimensions`:
44
44
  width: {
45
45
  type: 'integer'
46
46
  },
@@ -42,7 +42,7 @@ export class Model extends objection.Model {
42
42
  const { hooks, assets } = this.definition
43
43
  this._configureEmitter(hooks)
44
44
  if (assets) {
45
- this._configureAssetsEvents(assets)
45
+ this._configureAssetsHooks(assets)
46
46
  }
47
47
  try {
48
48
  for (const relation of Object.values(this.getRelations())) {
@@ -923,7 +923,7 @@ export class Model extends objection.Model {
923
923
 
924
924
  // Assets handling
925
925
 
926
- static _configureAssetsEvents(assets) {
926
+ static _configureAssetsHooks(assets) {
927
927
  const assetDataPaths = Object.keys(assets)
928
928
 
929
929
  this.on([
@@ -931,34 +931,53 @@ export class Model extends objection.Model {
931
931
  'before:update',
932
932
  'before:delete'
933
933
  ], async ({ type, transaction, inputItems, asFindQuery }) => {
934
- const afterItems = type === 'before:delete'
935
- ? []
936
- : inputItems
934
+ const isInsert = type === 'before:insert'
935
+ const isDelete = type === 'before:delete'
937
936
  // Figure out which asset data paths where actually present in the
938
937
  // submitted data, and only compare these. But when deleting, use all.
939
- const dataPaths = afterItems.length > 0
940
- ? assetDataPaths.filter(
941
- path => getValueAtAssetDataPath(afterItems[0], path) !== undefined
938
+ const dataPaths = isDelete
939
+ ? assetDataPaths
940
+ : assetDataPaths.filter(
941
+ dataPath => (
942
+ // Skip check for wildcard data-paths.
943
+ /^\*\*?/.test(dataPath) ||
944
+ // Only keep normal data paths that match present properties.
945
+ (parseDataPath(dataPath)[0] in inputItems[0])
946
+ )
942
947
  )
943
- : assetDataPaths
944
-
945
948
  // `dataPaths` is empty in the case of an update/insert that does not
946
949
  // affect the assets.
947
950
  if (dataPaths.length === 0) return
948
951
 
952
+ const afterItems = isDelete
953
+ ? []
954
+ : inputItems
949
955
  // Load the model's asset files in their current state before the query is
950
- // executed.
951
- const beforeItems = type === 'before:insert'
956
+ // executed. For deletes, load the data for all asset data-paths.
957
+ // Otherwise, only load the columns present in the input data.
958
+ const beforeItems = isInsert
952
959
  ? []
953
- : await loadAssetDataPaths(asFindQuery(), dataPaths)
954
- const beforeFilesPerDataPath = getFilesPerAssetDataPath(
955
- beforeItems,
956
- dataPaths
957
- )
960
+ : isDelete
961
+ // When deleting it's ok to load all columns when data-paths contain
962
+ // wildcards unfiltered, since `afterItems` will be empty anyway.
963
+ ? await loadAssetDataPaths(asFindQuery(), dataPaths)
964
+ : await asFindQuery().select(
965
+ // Select only the properties that are present in the data,
966
+ // and which aren't the result of computed properties.
967
+ Object.keys(inputItems[0]).filter(key => {
968
+ const property = this.definition.properties[key]
969
+ return property && !property.computed
970
+ })
971
+ )
972
+
958
973
  const afterFilesPerDataPath = getFilesPerAssetDataPath(
959
974
  afterItems,
960
975
  dataPaths
961
976
  )
977
+ const beforeFilesPerDataPath = getFilesPerAssetDataPath(
978
+ beforeItems,
979
+ dataPaths
980
+ )
962
981
 
963
982
  const importedFiles = []
964
983
  const modifiedFiles = []
@@ -3,7 +3,7 @@ import { fileTypeFromBuffer } from 'file-type'
3
3
  import { Storage } from './Storage.js'
4
4
  import { PassThrough } from 'stream'
5
5
  import consumers from 'stream/consumers'
6
- import imageSize from 'image-size'
6
+ import { attributes as readMediaAttributes } from 'leather'
7
7
 
8
8
  export class S3Storage extends Storage {
9
9
  static type = 's3'
@@ -159,31 +159,9 @@ export class S3Storage extends Storage {
159
159
  }
160
160
 
161
161
  function getFileTypeFromBuffer(buffer) {
162
- const type = fileTypeFromBuffer(buffer)
163
- if (type) {
164
- return type.mime
165
- }
166
162
  try {
167
- const { type } = imageSize(buffer)
168
- return {
169
- jpg: 'image/jpeg',
170
- png: 'image/png',
171
- gif: 'image/gif',
172
- svg: 'image/svg+xml',
173
- webp: 'image/webp',
174
- tiff: 'image/tiff',
175
- j2c: 'image/jp2',
176
- jp2: 'image/jp2',
177
- ktx: 'image/ktx',
178
- bmp: 'image/bmp',
179
- tga: 'image/x-targa',
180
- cur: 'image/x-win-bitmap',
181
- icns: 'image/x-icon',
182
- ico: 'image/x-icon',
183
- pnm: 'image/x-portable-anymap',
184
- dds: 'image/vnd-ms.dds',
185
- psd: 'image/vnd.adobe.photoshop'
186
- }[type]
163
+ // Use leather as fall-back for better media file mime type detection.
164
+ return fileTypeFromBuffer(buffer)?.mime || readMediaAttributes(buffer)?.mime
187
165
  } catch (err) {}
188
166
  return null
189
167
  }
@@ -2,10 +2,11 @@ import path from 'path'
2
2
  import { URL } from 'url'
3
3
  import multer from '@koa/multer'
4
4
  import picomatch from 'picomatch'
5
- import imageSize from 'image-size'
6
5
  import { PassThrough } from 'stream'
6
+ import { attributes as readMediaAttributes } from 'leather'
7
7
  import { hyphenate, toPromiseCallback } from '@ditojs/utils'
8
8
  import { AssetFile } from './AssetFile.js'
9
+ import { deprecate } from '../utils/deprecate.js'
9
10
 
10
11
  const storageClasses = {}
11
12
 
@@ -87,7 +88,7 @@ export class Storage {
87
88
  type: storageFile.mimetype,
88
89
  size: storageFile.size,
89
90
  url: this._getFileUrl(storageFile),
90
- // In case `config.readImageSize` is set:
91
+ // In case `config.readDimensions` is set:
91
92
  width: storageFile.width,
92
93
  height: storageFile.height
93
94
  }
@@ -101,7 +102,7 @@ export class Storage {
101
102
  await this._addFile(file, data)
102
103
  file.size = Buffer.byteLength(data)
103
104
  file.url = this._getFileUrl(file)
104
- // TODO: Support `config.readImageSize`, but this can only be done once
105
+ // TODO: Support `config.readDimensions`, but this can only be done once
105
106
  // there are separate storage instances per model assets config!
106
107
  return this.convertAssetFile(file)
107
108
  }
@@ -126,10 +127,6 @@ export class Storage {
126
127
  return this._getFileUrl(file)
127
128
  }
128
129
 
129
- isImageFile(file) {
130
- return file.mimetype.startsWith('image/')
131
- }
132
-
133
130
  _getUrl(...parts) {
134
131
  return this.url
135
132
  ? new URL(path.posix.join(...parts), this.url).toString()
@@ -161,8 +158,20 @@ export class Storage {
161
158
  async _listKeys() {}
162
159
 
163
160
  async _handleUpload(req, file, config) {
164
- if (config.readImageSize && this.isImageFile(file)) {
165
- return this._handleImageFile(req, file)
161
+ if (config.readImageSize) {
162
+ deprecate(
163
+ `config.readImageSize is deprecated in favour of config.readDimensions`
164
+ )
165
+ }
166
+ if (
167
+ (
168
+ config.readDimensions ||
169
+ // TODO: `config.readImageSize` was deprecated in favour of
170
+ // `config.readDimensions` in March 2023. Remove in 1 year.
171
+ config.readImageSize
172
+ ) && /^(image|video)\//.test(file.mimetype)
173
+ ) {
174
+ return this._handleMediaFile(req, file)
166
175
  } else {
167
176
  return this._handleFile(req, file)
168
177
  }
@@ -183,7 +192,7 @@ export class Storage {
183
192
  })
184
193
  }
185
194
 
186
- async _handleImageFile(req, file) {
195
+ async _handleMediaFile(req, file) {
187
196
  const { size, stream } = await new Promise(resolve => {
188
197
  let data = null
189
198
 
@@ -204,9 +213,15 @@ export class Storage {
204
213
 
205
214
  const onData = chunk => {
206
215
  data = data ? Buffer.concat([data, chunk]) : chunk
207
- const size = imageSize(data)
208
- if (size) {
209
- done(size)
216
+ try {
217
+ const size = readMediaAttributes(data)
218
+ // On partial data, sometimes we get results back from leather without
219
+ // actual dimensions, so check for that.
220
+ if (size.mime && (size.width > 0 || size.height > 0)) {
221
+ done(size)
222
+ }
223
+ } catch {
224
+ // Ignore errors in `readMediaAttributes()` on partial data.
210
225
  }
211
226
  }
212
227
 
@@ -31,22 +31,22 @@ describe('describeFunction()', () => {
31
31
  .toBe('async function (a, b, c) { ... }')
32
32
  })
33
33
 
34
- it('describes lambdas with one param and a body', () => {
34
+ it('describes async lambdas with one param and a body', () => {
35
35
  expect(describeFunction(async a => { return a }))
36
36
  .toBe('async a => { ... }')
37
37
  })
38
38
 
39
- it('describes lambdas with one param and no body', () => {
39
+ it('describes async lambdas with one param and no body', () => {
40
40
  expect(describeFunction(async a => a))
41
41
  .toBe('async a => ...')
42
42
  })
43
43
 
44
- it('describes lambdas with multiple params and a body', () => {
44
+ it('describes async lambdas with multiple params and a body', () => {
45
45
  expect(describeFunction(async (a, b, c) => { return a + b + c }))
46
46
  .toBe('async (a, b, c) => { ... }')
47
47
  })
48
48
 
49
- it('describes lambdas with multiple params and no body', () => {
49
+ it('describes async lambdas with multiple params and no body', () => {
50
50
  expect(describeFunction(async (a, b, c) => a + b + c))
51
51
  .toBe('async (a, b, c) => ...')
52
52
  })
package/types/index.d.ts CHANGED
@@ -588,7 +588,7 @@ export type ModelFilters<$Model extends Model = Model> = Record<
588
588
 
589
589
  export interface ModelAsset {
590
590
  storage: string
591
- readImageSize?: boolean
591
+ readDimensions?: boolean
592
592
  }
593
593
 
594
594
  export type ModelAssets = Record<string, ModelAsset>
@@ -1666,9 +1666,9 @@ type AssetFileObject = {
1666
1666
  size: number
1667
1667
  // The public url of the file
1668
1668
  url: string
1669
- // The width of the image if the storage defines `config.readImageSize`
1669
+ // The width of the image if the storage defines `config.readDimensions`
1670
1670
  width: number
1671
- // The height of the image if the storage defines `config.readImageSize`
1671
+ // The height of the image if the storage defines `config.readDimensions`
1672
1672
  height: number
1673
1673
  }
1674
1674