@ditojs/server 1.14.2 → 1.15.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.14.2",
3
+ "version": "1.15.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",
@@ -25,10 +25,10 @@
25
25
  "node >= 18"
26
26
  ],
27
27
  "dependencies": {
28
- "@ditojs/admin": "^1.14.1",
28
+ "@ditojs/admin": "^1.15.0",
29
29
  "@ditojs/build": "^1.14.0",
30
- "@ditojs/router": "^1.14.0",
31
- "@ditojs/utils": "^1.14.0",
30
+ "@ditojs/router": "^1.15.0",
31
+ "@ditojs/utils": "^1.15.0",
32
32
  "@koa/cors": "^4.0.0",
33
33
  "@koa/multer": "^3.0.2",
34
34
  "@originjs/vite-plugin-commonjs": "^1.0.3",
@@ -39,7 +39,6 @@
39
39
  "data-uri-to-buffer": "^4.0.0",
40
40
  "eventemitter2": "^6.4.9",
41
41
  "file-type": "^18.0.0",
42
- "fs-extra": "^10.1.0",
43
42
  "image-size": "^1.0.2",
44
43
  "is-svg": "^4.3.2",
45
44
  "koa": "^2.13.4",
@@ -79,7 +78,7 @@
79
78
  "objection": "^3.0.1"
80
79
  },
81
80
  "devDependencies": {
82
- "@aws-sdk/client-s3": "^3.200.0",
81
+ "@aws-sdk/client-s3": "^3.223.0",
83
82
  "@types/koa-bodyparser": "^4.3.10",
84
83
  "@types/koa-compress": "^4.0.3",
85
84
  "@types/koa-logger": "^3.1.2",
@@ -88,12 +87,12 @@
88
87
  "@types/koa-session": "^5.10.6",
89
88
  "@types/koa-static": "^4.0.2",
90
89
  "@types/koa__cors": "^3.3.0",
91
- "@types/node": "^18.11.9",
90
+ "@types/node": "^18.11.10",
92
91
  "knex": "^2.3.0",
93
92
  "objection": "^3.0.1",
94
- "type-fest": "^3.2.0",
93
+ "type-fest": "^3.3.0",
95
94
  "typescript": "^4.9.3"
96
95
  },
97
96
  "types": "types",
98
- "gitHead": "3165af9ce8e5c8143fd5940ff9d2ae7c3fa45d8e"
97
+ "gitHead": "848aed21b94e81ac68f089c4a6bd335b1a97073d"
99
98
  }
@@ -1,7 +1,7 @@
1
1
  import path from 'path'
2
2
  import util from 'util'
3
3
  import zlib from 'zlib'
4
- import fs from 'fs-extra'
4
+ import fs from 'fs/promises'
5
5
  import Koa from 'koa'
6
6
  import Knex from 'knex'
7
7
  import pico from 'picocolors'
@@ -289,12 +289,11 @@ export class Application extends Koa {
289
289
  data.schema = modelClass.getJsonSchema()
290
290
  }
291
291
  if (shouldLog(log.relations)) {
292
- data.relations = clone(
293
- modelClass.getRelationMappings(),
294
- value => Model.isPrototypeOf(value)
292
+ data.relations = clone(modelClass.getRelationMappings(), {
293
+ processValue: value => Model.isPrototypeOf(value)
295
294
  ? `[Model: ${value.name}]`
296
295
  : value
297
- )
296
+ })
298
297
  }
299
298
  if (Object.keys(data).length > 0) {
300
299
  console.info(
@@ -594,7 +593,7 @@ export class Application extends Koa {
594
593
  prettyPrint: {
595
594
  colorize: true,
596
595
  // List of keys to ignore in pretty mode.
597
- ignore: 'req,res,durationMs,user,requestId,err.message',
596
+ ignore: 'req,res,durationMs,user,requestId',
598
597
  // SYS to use system time and not UTC.
599
598
  translateTime: 'SYS:HH:MM:ss.l'
600
599
  },
@@ -689,20 +688,32 @@ export class Application extends Koa {
689
688
  return this.config.app.normalizePaths ? hyphenate(path) : path
690
689
  }
691
690
 
692
- logError(err, ctx) {
693
- if (!err.expose && !this.silent) {
691
+ formatError(error) {
692
+ // Shallow-clone the error to be able to delete hidden properties.
693
+ const copy = clone(error, { shallow: true })
694
+ // Remove headers added by the CORS middleware.
695
+ delete copy.headers
696
+ if (this.config.log.errors?.stack === false) {
697
+ delete copy.stack
698
+ delete copy.cause
699
+ }
700
+ // Use `util.inspect()` instead of Pino's internal error logging for better
701
+ // stack traces and logging of error data.
702
+ return util.inspect(copy, {
703
+ compact: false,
704
+ depth: null,
705
+ maxArrayLength: null
706
+ })
707
+ }
708
+
709
+ logError(error, ctx) {
710
+ if (!error.expose && !this.silent) {
694
711
  try {
695
- const clone = structuredClone(err)
696
- // Remove headers added by the CORS middleware.
697
- delete clone.headers
698
- if (this.config.log.errors?.stack === false) {
699
- delete clone.stack
700
- delete clone.cause
701
- }
702
- const level =
703
- err instanceof ResponseError && err.status < 500 ? 'info' : 'error'
704
712
  const logger = ctx?.logger || this.logger
705
- logger[level](clone)
713
+ const level = error instanceof ResponseError && error.status < 500
714
+ ? 'info'
715
+ : 'error'
716
+ logger[level](this.formatError(error))
706
717
  } catch (e) {
707
718
  console.error('Could not log error', e)
708
719
  }
@@ -895,7 +906,8 @@ export class Application extends Koa {
895
906
  data = await fs.readFile(filepath)
896
907
  } else {
897
908
  const response = await fetch(url)
898
- data = await response.arrayBuffer()
909
+ const buffer = await response.arrayBuffer()
910
+ data = new DataView(buffer)
899
911
  }
900
912
  }
901
913
  const importedFile = await storage.addFile(file, data)
@@ -153,27 +153,29 @@ export class Validator extends objection.Validator {
153
153
 
154
154
  processSchema(jsonSchema, options = {}) {
155
155
  const { patch, async } = options
156
- const schema = clone(jsonSchema, value => {
157
- if (isObject(value)) {
158
- if (patch) {
159
- // Remove all required keywords and formats from schema for patch
160
- // validation.
161
- delete value.required
162
- if (value.format === 'required') {
163
- delete value.format
156
+ const schema = clone(jsonSchema, {
157
+ processValue: value => {
158
+ if (isObject(value)) {
159
+ if (patch) {
160
+ // Remove all required keywords and formats from schema for patch
161
+ // validation.
162
+ delete value.required
163
+ if (value.format === 'required') {
164
+ delete value.format
165
+ }
164
166
  }
165
- }
166
- // Convert async `validate()` keywords to `validateAsync()`:
167
- if (isAsync(value.validate)) {
168
- value.validateAsync = value.validate
169
- delete value.validate
170
- }
171
- if (!async) {
172
- // Remove all async keywords for synchronous validation.
173
- for (const key in value) {
174
- const keyword = this.getKeyword(key)
175
- if (keyword?.async) {
176
- delete value[key]
167
+ // Convert async `validate()` keywords to `validateAsync()`:
168
+ if (isAsync(value.validate)) {
169
+ value.validateAsync = value.validate
170
+ delete value.validate
171
+ }
172
+ if (!async) {
173
+ // Remove all async keywords for synchronous validation.
174
+ for (const key in value) {
175
+ const keyword = this.getKeyword(key)
176
+ if (keyword?.async) {
177
+ delete value[key]
178
+ }
177
179
  }
178
180
  }
179
181
  }
@@ -1,6 +1,6 @@
1
1
  import repl from 'repl'
2
2
  import path from 'path'
3
- import fs from 'fs-extra'
3
+ import fs from 'fs/promises'
4
4
  import pico from 'picocolors'
5
5
  import objection from 'objection'
6
6
  import { isFunction, deindent } from '@ditojs/utils'
@@ -1,5 +1,5 @@
1
1
  import path from 'path'
2
- import fs from 'fs-extra'
2
+ import fs from 'fs/promises'
3
3
  import pico from 'picocolors'
4
4
  import { getRelationClass, isThroughRelationClass } from '@ditojs/server'
5
5
  import {
@@ -50,7 +50,7 @@ export async function createMigration(app, name, ...modelNames) {
50
50
  : ''
51
51
  const filename = `${getTimestamp()}_${name}.js`
52
52
  const file = path.join(migrationDir, filename)
53
- if (await fs.exists(file)) {
53
+ if (await exists(file)) {
54
54
  // This should never happen, but let's be on the safe side here:
55
55
  console.info(pico.red(`Migration '${filename}' already exists.`))
56
56
  return false
@@ -204,3 +204,12 @@ function getTimestamp() {
204
204
  padDate(d.getMinutes()) +
205
205
  padDate(d.getSeconds())
206
206
  }
207
+
208
+ async function exists(path) {
209
+ try {
210
+ await fs.access(path)
211
+ return true
212
+ } catch {
213
+ return false
214
+ }
215
+ }
@@ -1,5 +1,5 @@
1
1
  import path from 'path'
2
- import fs from 'fs-extra'
2
+ import fs from 'fs/promises'
3
3
  import pico from 'picocolors'
4
4
  import util from 'util'
5
5
  import pluralize from 'pluralize'
@@ -3,7 +3,7 @@ import { EventEmitter } from '../lib/index.js'
3
3
  import ControllerAction from './ControllerAction.js'
4
4
  import MemberAction from './MemberAction.js'
5
5
  import {
6
- ResponseError, WrappedError, ControllerError, AuthorizationError
6
+ ResponseError, ControllerError, AuthorizationError
7
7
  } from '../errors/index.js'
8
8
  import {
9
9
  getOwnProperty, getOwnKeys, getAllKeys, processHandlerParameters,
@@ -185,7 +185,7 @@ export class Controller {
185
185
  ctx.body = res
186
186
  }
187
187
  } catch (err) {
188
- throw err instanceof ResponseError ? err : new WrappedError(err)
188
+ throw err instanceof ResponseError ? err : new ResponseError(err)
189
189
  }
190
190
  }
191
191
  ])
@@ -1,4 +1,4 @@
1
- import { WrappedError } from './WrappedError.js'
1
+ import { ResponseError } from './ResponseError.js'
2
2
  // TODO: Import directly once we can move to Objection 3 and this is fixed:
3
3
  // import {
4
4
  // DBError,
@@ -16,16 +16,21 @@ const {
16
16
  ConstraintViolationError
17
17
  } = objection
18
18
 
19
- export class DatabaseError extends WrappedError {
19
+ export class DatabaseError extends ResponseError {
20
20
  constructor(error, overrides) {
21
- const status =
22
- error instanceof CheckViolationError ? 400
23
- : error instanceof NotNullViolationError ? 400
24
- : error instanceof ConstraintViolationError ? 409
25
- : error instanceof DataError ? 400
26
- : error instanceof DBError ? 500
27
- : 400
28
- overrides = { type: error.constructor.name, status, ...overrides }
29
- super(error, overrides, { message: 'Database error', status })
21
+ super(error, {
22
+ type: error.constructor.name,
23
+ message: 'Database error',
24
+ status: getStatus(error)
25
+ }, overrides)
30
26
  }
31
27
  }
28
+
29
+ function getStatus(error) {
30
+ return error instanceof CheckViolationError ? 400
31
+ : error instanceof NotNullViolationError ? 400
32
+ : error instanceof ConstraintViolationError ? 409
33
+ : error instanceof DataError ? 400
34
+ : error instanceof DBError ? 500
35
+ : 400
36
+ }
@@ -1,18 +1,21 @@
1
- import { WrappedError } from './WrappedError.js'
2
- import { isObject } from '@ditojs/utils'
1
+ import { ResponseError } from './ResponseError.js'
3
2
 
4
- export class RelationError extends WrappedError {
3
+ export class RelationError extends ResponseError {
5
4
  constructor(error) {
6
- let overrides
7
- if (isObject(error)) {
8
- // Adjust Objection.js error messages to point to the right property.
9
- const parse = str => str?.replace(/\brelationMappings\b/g, 'relations')
10
- const { message, stack } = error
11
- overrides = {
12
- message: parse(message),
13
- stack: parse(stack)
14
- }
15
- }
16
- super(error, overrides, { message: 'Relation error', status: 400 })
5
+ super(
6
+ error,
7
+ { message: 'Relation error', status: 400 },
8
+ error instanceof Error ? getFormattedOverrides(error) : null
9
+ )
10
+ }
11
+ }
12
+
13
+ function getFormattedOverrides(error) {
14
+ // Adjust Objection.js error messages to point to the right property.
15
+ const format = str => str?.replace(/\brelationMappings\b/g, 'relations')
16
+ const { message, stack } = error
17
+ return {
18
+ message: format(message),
19
+ stack: format(stack)
17
20
  }
18
21
  }
@@ -1,33 +1,54 @@
1
1
  import { isPlainObject, isString } from '@ditojs/utils'
2
2
 
3
3
  export class ResponseError extends Error {
4
- constructor(error, defaults = { message: 'Response error', status: 400 }) {
4
+ constructor(
5
+ error,
6
+ defaults = { message: 'Response error', status: 500 },
7
+ overrides
8
+ ) {
5
9
  const object = isPlainObject(error)
6
10
  ? error
7
11
  : error instanceof Error
8
- ? {
9
- // Copy error into object so they can be merged with defaults after.
10
- // First copy everything that is enumerable, unless the error is from
11
- // axios, in which case we don't want to leak config information.
12
- ...(error.isAxiosError ? null : error),
13
- // Also explicitly copy message, status and code.
14
- message: error.message,
15
- status: error.status,
16
- code: error.code
17
- }
12
+ ? getErrorObject(error)
18
13
  : isString(error)
19
14
  ? { message: error }
20
15
  : error || {}
21
- const { status, cause, ...data } = { ...defaults, ...object }
22
- const { message, code } = data
23
- super(message, { cause })
24
- this.name = this.constructor.name
16
+ const { message, status, stack, cause, ...data } = {
17
+ ...defaults,
18
+ ...object,
19
+ ...overrides
20
+ }
21
+ super(message, cause ? { cause } : {})
25
22
  this.status = status
26
- this.code = code
27
23
  this.data = data
24
+ // Allow `stack` overrides, e.g. for `RelationError`.
25
+ if (stack != null) {
26
+ this.stack = stack
27
+ }
28
28
  }
29
29
 
30
30
  toJSON() {
31
- return this.data
31
+ return {
32
+ // Include the message in the JSON data sent back.
33
+ message: this.message,
34
+ ...this.data
35
+ }
32
36
  }
33
37
  }
38
+
39
+ function getErrorObject(error) {
40
+ const object = {
41
+ // For generic errors, explicitly copy message.
42
+ message: error.message,
43
+ ...error.toJSON?.()
44
+ }
45
+ // Additionally copy status and code if present.
46
+ if (error.status != null) {
47
+ object.status = error.status
48
+ }
49
+ if (error.code != null) {
50
+ object.code = error.code
51
+ }
52
+ object.cause = error
53
+ return object
54
+ }
@@ -11,4 +11,3 @@ export * from './NotImplementedError.js'
11
11
  export * from './QueryBuilderError.js'
12
12
  export * from './RelationError.js'
13
13
  export * from './ValidationError.js'
14
- export * from './WrappedError.js'
@@ -16,8 +16,7 @@ import {
16
16
  ResponseError,
17
17
  GraphError, ModelError,
18
18
  NotFoundError,
19
- RelationError,
20
- WrappedError
19
+ RelationError
21
20
  } from '../errors/index.js'
22
21
  import RelationAccessor from './RelationAccessor.js'
23
22
  import definitions from './definitions/index.js'
@@ -181,7 +180,7 @@ export class Model extends objection.Model {
181
180
  const shallow = json.$isObjectionModel && !options.graph
182
181
  if (shallow) {
183
182
  // Strip away relations and other internal stuff.
184
- json = json.clone({ shallow: true })
183
+ json = json.$clone({ shallow: true })
185
184
  // We can mutate `json` now that we took a copy of it.
186
185
  options = { ...options, mutable: true }
187
186
  }
@@ -239,7 +238,7 @@ export class Model extends objection.Model {
239
238
  // TODO: Shouldn't this wrapping happen on the Controller level?
240
239
  err = err instanceof ResponseError ? err
241
240
  : err instanceof objection.DBError ? this.app.createDatabaseError(err)
242
- : new WrappedError(err)
241
+ : new ResponseError(err)
243
242
  return Promise.reject(err)
244
243
  })
245
244
  }
@@ -1,4 +1,4 @@
1
- import fs from 'fs-extra'
1
+ import fs from 'fs/promises'
2
2
  import path from 'path'
3
3
  import multer from '@koa/multer'
4
4
  import { Storage } from './Storage.js'
@@ -15,7 +15,7 @@ export class DiskStorage extends Storage {
15
15
  // Add `storageFile.key` property to internal storage file object.
16
16
  storageFile.key = this.getUniqueKey(storageFile.originalname)
17
17
  const dir = this._getPath(this._getNestedFolder(storageFile.key))
18
- fs.ensureDir(dir)
18
+ fs.mkdir(dir, { recursive: true })
19
19
  .then(() => cb(null, dir))
20
20
  .catch(cb)
21
21
  },
@@ -41,7 +41,7 @@ export class DiskStorage extends Storage {
41
41
  async _addFile(file, buffer) {
42
42
  const filePath = this._getFilePath(file)
43
43
  const dir = path.dirname(filePath)
44
- await fs.ensureDir(dir)
44
+ await fs.mkdir(dir, { recursive: true })
45
45
  await fs.writeFile(filePath, buffer)
46
46
  return file
47
47
  }
package/types/index.d.ts CHANGED
@@ -1617,8 +1617,7 @@ export class ResponseError extends Error {
1617
1617
  export class AssetError extends ResponseError {}
1618
1618
  export class AuthenticationError extends ResponseError {}
1619
1619
  export class AuthorizationError extends ResponseError {}
1620
- export class WrappedError extends ResponseError {}
1621
- export class DatabaseError extends WrappedError {
1620
+ export class DatabaseError extends ResponseError {
1622
1621
  constructor(
1623
1622
  error:
1624
1623
  | dbErrors.CheckViolationError
@@ -1,18 +0,0 @@
1
- import { ResponseError } from './ResponseError.js'
2
-
3
- export class WrappedError extends ResponseError {
4
- constructor(error, overrides, defaults = {
5
- message: 'Wrapped error',
6
- status: 400
7
- }) {
8
- super(
9
- overrides
10
- ? Object.setPrototypeOf({ ...overrides }, error)
11
- : error,
12
- defaults
13
- )
14
- if (error?.stack) {
15
- this.stack = error.stack
16
- }
17
- }
18
- }