@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 +8 -9
- package/src/app/Application.js +31 -19
- package/src/app/Validator.js +22 -20
- package/src/cli/console.js +1 -1
- package/src/cli/db/createMigration.js +11 -2
- package/src/cli/db/seed.js +1 -1
- package/src/controllers/Controller.js +2 -2
- package/src/errors/DatabaseError.js +16 -11
- package/src/errors/RelationError.js +17 -14
- package/src/errors/ResponseError.js +38 -17
- package/src/errors/index.js +0 -1
- package/src/models/Model.js +3 -4
- package/src/storage/DiskStorage.js +3 -3
- package/types/index.d.ts +1 -2
- package/src/errors/WrappedError.js +0 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
28
|
+
"@ditojs/admin": "^1.15.0",
|
|
29
29
|
"@ditojs/build": "^1.14.0",
|
|
30
|
-
"@ditojs/router": "^1.
|
|
31
|
-
"@ditojs/utils": "^1.
|
|
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.
|
|
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.
|
|
90
|
+
"@types/node": "^18.11.10",
|
|
92
91
|
"knex": "^2.3.0",
|
|
93
92
|
"objection": "^3.0.1",
|
|
94
|
-
"type-fest": "^3.
|
|
93
|
+
"type-fest": "^3.3.0",
|
|
95
94
|
"typescript": "^4.9.3"
|
|
96
95
|
},
|
|
97
96
|
"types": "types",
|
|
98
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "848aed21b94e81ac68f089c4a6bd335b1a97073d"
|
|
99
98
|
}
|
package/src/app/Application.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
693
|
-
|
|
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
|
-
|
|
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
|
-
|
|
909
|
+
const buffer = await response.arrayBuffer()
|
|
910
|
+
data = new DataView(buffer)
|
|
899
911
|
}
|
|
900
912
|
}
|
|
901
913
|
const importedFile = await storage.addFile(file, data)
|
package/src/app/Validator.js
CHANGED
|
@@ -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,
|
|
157
|
-
|
|
158
|
-
if (
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
}
|
package/src/cli/console.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import fs from 'fs
|
|
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
|
|
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
|
+
}
|
package/src/cli/db/seed.js
CHANGED
|
@@ -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,
|
|
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
|
|
188
|
+
throw err instanceof ResponseError ? err : new ResponseError(err)
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
191
|
])
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
19
|
+
export class DatabaseError extends ResponseError {
|
|
20
20
|
constructor(error, overrides) {
|
|
21
|
-
|
|
22
|
-
error
|
|
23
|
-
: error
|
|
24
|
-
: error
|
|
25
|
-
|
|
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 {
|
|
2
|
-
import { isObject } from '@ditojs/utils'
|
|
1
|
+
import { ResponseError } from './ResponseError.js'
|
|
3
2
|
|
|
4
|
-
export class RelationError extends
|
|
3
|
+
export class RelationError extends ResponseError {
|
|
5
4
|
constructor(error) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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(
|
|
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 } = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
|
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
|
+
}
|
package/src/errors/index.js
CHANGED
package/src/models/Model.js
CHANGED
|
@@ -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
|
|
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
|
|
241
|
+
: new ResponseError(err)
|
|
243
242
|
return Promise.reject(err)
|
|
244
243
|
})
|
|
245
244
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from 'fs
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
}
|