@ditojs/server 2.49.1 → 2.51.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 -8
- package/src/app/Application.js +13 -1
- package/src/controllers/Controller.js +13 -2
- package/src/controllers/ControllerAction.js +56 -28
- package/src/models/Model.js +17 -14
- package/src/models/definitions/filters.js +17 -4
- package/src/schema/index.js +1 -1
- package/src/schema/keywords/index.js +0 -1
- package/src/schema/relations.js +2 -6
- package/src/schema/{properties.test.js → schema.test.js} +3 -3
- package/src/schema/types/_color.js +4 -0
- package/src/schema/types/index.js +1 -0
- package/types/index.d.ts +4 -4
- package/src/schema/keywords/_extend.js +0 -31
- package/src/utils/decorator.js +0 -10
- /package/src/schema/{properties.js → schema.js} +0 -0
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.51.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",
|
|
7
7
|
"author": "Jürg Lehni <juerg@scratchdisk.com> (http://scratchdisk.com)",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"main": "./src/index.js",
|
|
10
|
+
"types": "./types/index.d.ts",
|
|
10
11
|
"files": [
|
|
11
12
|
"src/",
|
|
12
13
|
"types/"
|
|
@@ -25,10 +26,10 @@
|
|
|
25
26
|
"node >= 18"
|
|
26
27
|
],
|
|
27
28
|
"dependencies": {
|
|
28
|
-
"@ditojs/admin": "^2.
|
|
29
|
-
"@ditojs/build": "^2.
|
|
30
|
-
"@ditojs/router": "^2.
|
|
31
|
-
"@ditojs/utils": "^2.
|
|
29
|
+
"@ditojs/admin": "^2.51.0",
|
|
30
|
+
"@ditojs/build": "^2.51.0",
|
|
31
|
+
"@ditojs/router": "^2.51.0",
|
|
32
|
+
"@ditojs/utils": "^2.51.0",
|
|
32
33
|
"@koa/cors": "^5.0.0",
|
|
33
34
|
"@koa/multer": "^3.1.0",
|
|
34
35
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
|
@@ -84,11 +85,10 @@
|
|
|
84
85
|
"@types/koa-session": "^6.4.5",
|
|
85
86
|
"@types/koa-static": "^4.0.4",
|
|
86
87
|
"@types/koa__cors": "^5.0.0",
|
|
87
|
-
"@types/node": "^22.15.
|
|
88
|
+
"@types/node": "^22.15.18",
|
|
88
89
|
"knex": "^3.1.0",
|
|
89
90
|
"objection": "^3.1.5",
|
|
90
91
|
"typescript": "^5.8.3"
|
|
91
92
|
},
|
|
92
|
-
"
|
|
93
|
-
"gitHead": "45e0fcf6cc3616b095ad32928cba034198b01ba0"
|
|
93
|
+
"gitHead": "0b8f114647510db71c50be9fdc331297b1e726ab"
|
|
94
94
|
}
|
package/src/app/Application.js
CHANGED
|
@@ -534,6 +534,15 @@ export class Application extends Koa {
|
|
|
534
534
|
const schema = properties
|
|
535
535
|
? convertSchema({ type: 'object', properties }, options)
|
|
536
536
|
: null
|
|
537
|
+
|
|
538
|
+
// Method to recursively check the compiled JSON schema and its sub-schemas
|
|
539
|
+
// to see if it has any `$ref` references to model schemas:
|
|
540
|
+
const hasModelRefs = schema => (
|
|
541
|
+
!!this.models[schema?.$ref] ||
|
|
542
|
+
(isArray(schema) || isPlainObject(schema)) &&
|
|
543
|
+
Object.values(schema).some(hasModelRefs)
|
|
544
|
+
)
|
|
545
|
+
|
|
537
546
|
const validate = this.compileValidator(schema, {
|
|
538
547
|
// For parameters, always coerce types, including arrays.
|
|
539
548
|
coerceTypes: 'array',
|
|
@@ -552,7 +561,10 @@ export class Application extends Koa {
|
|
|
552
561
|
validate: validate
|
|
553
562
|
? // Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
554
563
|
data => validate.call(ctx, data)
|
|
555
|
-
: null
|
|
564
|
+
: null,
|
|
565
|
+
get hasModelRefs() {
|
|
566
|
+
return hasModelRefs(schema)
|
|
567
|
+
}
|
|
556
568
|
}
|
|
557
569
|
}
|
|
558
570
|
|
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
asArray,
|
|
26
26
|
equals,
|
|
27
27
|
parseDataPath,
|
|
28
|
-
normalizeDataPath
|
|
28
|
+
normalizeDataPath,
|
|
29
|
+
deprecate
|
|
29
30
|
} from '@ditojs/utils'
|
|
30
31
|
|
|
31
32
|
export class Controller {
|
|
@@ -602,10 +603,20 @@ function convertActionObject(name, object, actions) {
|
|
|
602
603
|
transacted,
|
|
603
604
|
scope,
|
|
604
605
|
parameters,
|
|
606
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
607
|
+
// Remove this in 2026.
|
|
605
608
|
returns,
|
|
609
|
+
response = returns,
|
|
606
610
|
...rest
|
|
607
611
|
} = object
|
|
608
612
|
|
|
613
|
+
if (returns) {
|
|
614
|
+
deprecate(
|
|
615
|
+
'The `returns` property is deprecated in favour of `response`. ' +
|
|
616
|
+
'Update your handler definition to use `response` instead.'
|
|
617
|
+
)
|
|
618
|
+
}
|
|
619
|
+
|
|
609
620
|
// In order to support `super` calls in the `handler` function in object
|
|
610
621
|
// notation, deploy this crazy JS sorcery:
|
|
611
622
|
Object.setPrototypeOf(object, Object.getPrototypeOf(actions))
|
|
@@ -621,7 +632,7 @@ function convertActionObject(name, object, actions) {
|
|
|
621
632
|
handler.scope = scope ? asArray(scope) : null
|
|
622
633
|
|
|
623
634
|
processHandlerParameters(handler, 'parameters', parameters)
|
|
624
|
-
processHandlerParameters(handler, '
|
|
635
|
+
processHandlerParameters(handler, 'response', response)
|
|
625
636
|
|
|
626
637
|
return Object.assign(handler, rest)
|
|
627
638
|
}
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isString,
|
|
3
|
+
isObject,
|
|
4
|
+
asArray,
|
|
5
|
+
clone,
|
|
6
|
+
convertToJson,
|
|
7
|
+
deprecate
|
|
8
|
+
} from '@ditojs/utils'
|
|
2
9
|
|
|
3
10
|
export default class ControllerAction {
|
|
4
11
|
constructor(
|
|
@@ -17,11 +24,21 @@ export default class ControllerAction {
|
|
|
17
24
|
authorize,
|
|
18
25
|
transacted,
|
|
19
26
|
parameters,
|
|
27
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
28
|
+
// Remove this in 2026.
|
|
20
29
|
returns,
|
|
30
|
+
response = returns,
|
|
21
31
|
options = {},
|
|
22
32
|
...additional
|
|
23
33
|
} = handler
|
|
24
34
|
|
|
35
|
+
if (returns) {
|
|
36
|
+
deprecate(
|
|
37
|
+
'The `returns` property is deprecated in favour of `response`. ' +
|
|
38
|
+
'Update your handler definition to use `response` instead.'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
25
42
|
this.app = controller.app
|
|
26
43
|
this.controller = controller
|
|
27
44
|
this.actions = actions
|
|
@@ -57,19 +74,17 @@ export default class ControllerAction {
|
|
|
57
74
|
...options.parameters,
|
|
58
75
|
dataName: this.paramsName
|
|
59
76
|
})
|
|
60
|
-
this.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
)
|
|
77
|
+
this.response = this.app.compileParametersValidator(asArray(response), {
|
|
78
|
+
async: true,
|
|
79
|
+
// Use patch validation for response, as we often don't return the
|
|
80
|
+
// full model with all properties, but only a subset of them.
|
|
81
|
+
patch: true,
|
|
82
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
83
|
+
// Remove this in 2026.
|
|
84
|
+
...options.returns,
|
|
85
|
+
...options.response,
|
|
86
|
+
dataName: 'response'
|
|
87
|
+
})
|
|
73
88
|
// Copy over the additional properties, e.g. `cached` so application
|
|
74
89
|
// middleware can implement caching mechanisms:
|
|
75
90
|
Object.assign(this, additional)
|
|
@@ -109,8 +124,14 @@ export default class ControllerAction {
|
|
|
109
124
|
await this.controller.handleAuthorization(this.authorization, ctx, member)
|
|
110
125
|
const { identifier } = this
|
|
111
126
|
await this.controller.emitHook(`before:${identifier}`, false, ctx, ...args)
|
|
112
|
-
const
|
|
113
|
-
|
|
127
|
+
const response = await this.callHandler(ctx, ...args)
|
|
128
|
+
const result =
|
|
129
|
+
// Don't convert response to JSON if it isn't being validated (e.g. useful
|
|
130
|
+
// for streams or buffers), or if the response contains model references.
|
|
131
|
+
!this.response.validate || this.response.hasModelRefs
|
|
132
|
+
? response
|
|
133
|
+
: convertToJson(response)
|
|
134
|
+
return this.validateResponse(
|
|
114
135
|
await this.controller.emitHook(`after:${identifier}`, true, ctx, result)
|
|
115
136
|
)
|
|
116
137
|
}
|
|
@@ -215,31 +236,38 @@ export default class ControllerAction {
|
|
|
215
236
|
}
|
|
216
237
|
}
|
|
217
238
|
|
|
218
|
-
async
|
|
219
|
-
if (this.
|
|
220
|
-
const
|
|
239
|
+
async validateResponse(response) {
|
|
240
|
+
if (this.response.validate) {
|
|
241
|
+
const responseName = this.handler.response.name
|
|
242
|
+
const responseWrapped = !!responseName
|
|
221
243
|
// Use dataName if no name is given, see:
|
|
222
|
-
// Application.compileParametersValidator(
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
}
|
|
226
|
-
|
|
244
|
+
// Application.compileParametersValidator(response, { dataName })
|
|
245
|
+
const dataName = responseName || this.response.dataName
|
|
246
|
+
const wrapped = { [dataName]: response }
|
|
227
247
|
// If a named result is defined, return the data wrapped,
|
|
228
248
|
// otherwise return the original unwrapped result object.
|
|
229
|
-
const getResult = () => (
|
|
249
|
+
const getResult = () => (responseWrapped ? wrapped : response)
|
|
230
250
|
try {
|
|
231
|
-
await this.
|
|
251
|
+
await this.response.validate(wrapped)
|
|
232
252
|
return getResult()
|
|
233
253
|
} catch (error) {
|
|
254
|
+
// If the error contains errors, add them to the validation error:
|
|
255
|
+
const { errors } = error
|
|
256
|
+
const regexp = new RegExp(`^/${dataName}`)
|
|
234
257
|
throw this.createValidationError({
|
|
235
258
|
type: 'ResultValidation',
|
|
236
259
|
message: 'The returned action result is not valid',
|
|
237
|
-
errors:
|
|
260
|
+
errors: responseWrapped
|
|
261
|
+
? errors
|
|
262
|
+
: errors.map(error => ({
|
|
263
|
+
...error,
|
|
264
|
+
instancePath: error.instancePath.replace(regexp, '')
|
|
265
|
+
})),
|
|
238
266
|
json: getResult()
|
|
239
267
|
})
|
|
240
268
|
}
|
|
241
269
|
}
|
|
242
|
-
return
|
|
270
|
+
return response
|
|
243
271
|
}
|
|
244
272
|
|
|
245
273
|
async collectArguments(ctx, params) {
|
package/src/models/Model.js
CHANGED
|
@@ -6,8 +6,9 @@ import {
|
|
|
6
6
|
isFunction,
|
|
7
7
|
isPromise,
|
|
8
8
|
asArray,
|
|
9
|
-
|
|
9
|
+
clone,
|
|
10
10
|
equals,
|
|
11
|
+
flatten,
|
|
11
12
|
parseDataPath,
|
|
12
13
|
normalizeDataPath,
|
|
13
14
|
getValueAtDataPath,
|
|
@@ -181,19 +182,21 @@ export class Model extends objection.Model {
|
|
|
181
182
|
if (options.skipValidation) {
|
|
182
183
|
return json
|
|
183
184
|
}
|
|
184
|
-
if (!options.graph && !options.async) {
|
|
185
|
-
// Fall back to Objection's $validate() if we don't need any of our
|
|
186
|
-
// extensions (async and graph for now):
|
|
187
|
-
return super.$validate(json, options)
|
|
188
|
-
}
|
|
189
185
|
json ||= this
|
|
190
186
|
const inputJson = json
|
|
191
|
-
|
|
187
|
+
|
|
188
|
+
const shallow = !options.graph
|
|
192
189
|
if (shallow) {
|
|
193
|
-
|
|
194
|
-
|
|
190
|
+
json = clone(json, { shallow: true })
|
|
191
|
+
// Strip away relations.
|
|
192
|
+
for (const key of this.constructor.getRelationNames()) {
|
|
193
|
+
delete json[key]
|
|
194
|
+
}
|
|
195
195
|
// We can mutate `json` now that we took a copy of it.
|
|
196
|
-
options = {
|
|
196
|
+
options = {
|
|
197
|
+
...options,
|
|
198
|
+
mutable: true
|
|
199
|
+
}
|
|
197
200
|
}
|
|
198
201
|
|
|
199
202
|
const validator = this.constructor.getValidator()
|
|
@@ -209,7 +212,7 @@ export class Model extends objection.Model {
|
|
|
209
212
|
const handleResult = result => {
|
|
210
213
|
validator.afterValidate(args)
|
|
211
214
|
// If `json` was shallow-cloned, copy over the possible default values.
|
|
212
|
-
return shallow ? inputJson
|
|
215
|
+
return shallow ? Object.assign(inputJson, result) : result
|
|
213
216
|
}
|
|
214
217
|
// Handle both async and sync validation here:
|
|
215
218
|
return isPromise(result)
|
|
@@ -781,13 +784,13 @@ export class Model extends objection.Model {
|
|
|
781
784
|
if (deprecatedPrefixes[prefix]) {
|
|
782
785
|
prefix = deprecatedPrefixes[prefix]
|
|
783
786
|
deprecate(
|
|
784
|
-
`The ${
|
|
787
|
+
`The '${
|
|
785
788
|
modifier
|
|
786
|
-
} modifier is deprecated, use the ${
|
|
789
|
+
}' modifier is deprecated, use the '${
|
|
787
790
|
prefix
|
|
788
791
|
}${
|
|
789
792
|
modifier.slice(1)
|
|
790
|
-
} modifier instead.`
|
|
793
|
+
}' modifier instead.`
|
|
791
794
|
)
|
|
792
795
|
}
|
|
793
796
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isObject, isFunction } from '@ditojs/utils'
|
|
1
|
+
import { isObject, isFunction, deprecate } from '@ditojs/utils'
|
|
2
2
|
import { processHandlerParameters } from '../../utils/handler.js'
|
|
3
3
|
import { mergeReversed } from '../../utils/object.js'
|
|
4
4
|
import { QueryFilters } from '../../query/index.js'
|
|
@@ -24,9 +24,22 @@ export default function filters(values) {
|
|
|
24
24
|
function convertFilterObject(name, object) {
|
|
25
25
|
const addHandlerSettings = (handler, definition) => {
|
|
26
26
|
// Copy over parameters, returns and their validation options settings.
|
|
27
|
-
const {
|
|
27
|
+
const {
|
|
28
|
+
parameters,
|
|
29
|
+
// TODO: `returns` was deprecated in May 2025 in favour of `response`.
|
|
30
|
+
// Remove this in 2026.
|
|
31
|
+
returns,
|
|
32
|
+
response = returns,
|
|
33
|
+
...rest
|
|
34
|
+
} = definition
|
|
35
|
+
if (returns) {
|
|
36
|
+
deprecate(
|
|
37
|
+
'The `returns` property is deprecated in favour of `response`. ' +
|
|
38
|
+
'Update your handler definition to use `response` instead.'
|
|
39
|
+
)
|
|
40
|
+
}
|
|
28
41
|
processHandlerParameters(handler, 'parameters', parameters)
|
|
29
|
-
processHandlerParameters(handler, '
|
|
42
|
+
processHandlerParameters(handler, 'response', response)
|
|
30
43
|
return Object.assign(handler, rest)
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -62,7 +75,7 @@ function convertFilterObject(name, object) {
|
|
|
62
75
|
|
|
63
76
|
function wrapWithValidation(filter, name, app) {
|
|
64
77
|
if (filter) {
|
|
65
|
-
// TODO: Implement `
|
|
78
|
+
// TODO: Implement `response` validation for filters too.
|
|
66
79
|
// TODO: Share additional coercion handling with
|
|
67
80
|
// `ControllerAction#coerceValue()`
|
|
68
81
|
const { parameters, options = {} } = filter
|
package/src/schema/index.js
CHANGED
package/src/schema/relations.js
CHANGED
|
@@ -275,15 +275,11 @@ export function addRelationSchemas(modelClass, properties) {
|
|
|
275
275
|
const anyOf = []
|
|
276
276
|
if (isOneToOne) {
|
|
277
277
|
// Allow null-value on one-to-one relations
|
|
278
|
-
anyOf.push({
|
|
279
|
-
type: 'null'
|
|
280
|
-
})
|
|
278
|
+
anyOf.push({ type: 'null' })
|
|
281
279
|
}
|
|
282
280
|
if (!owner) {
|
|
283
281
|
// Allow reference objects for relations that don't own their data.
|
|
284
|
-
anyOf.push({
|
|
285
|
-
relate: $ref
|
|
286
|
-
})
|
|
282
|
+
anyOf.push({ relate: $ref })
|
|
287
283
|
}
|
|
288
284
|
// Finally the model itself
|
|
289
285
|
anyOf.push({ $ref })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { convertSchema } from './
|
|
1
|
+
import { convertSchema } from './schema.js'
|
|
2
2
|
|
|
3
3
|
describe('convertSchema()', () => {
|
|
4
4
|
it('expands objects with properties to full JSON schemas', () => {
|
|
@@ -147,7 +147,7 @@ describe('convertSchema()', () => {
|
|
|
147
147
|
})
|
|
148
148
|
})
|
|
149
149
|
|
|
150
|
-
it(`expands 'object' schemas with properties to JSON schemas allowing no
|
|
150
|
+
it(`expands 'object' schemas with properties to JSON schemas allowing no unevaluated properties`, () => {
|
|
151
151
|
expect(
|
|
152
152
|
convertSchema({
|
|
153
153
|
type: 'object',
|
|
@@ -171,7 +171,7 @@ describe('convertSchema()', () => {
|
|
|
171
171
|
})
|
|
172
172
|
})
|
|
173
173
|
|
|
174
|
-
it('preserves pre-existing settings for no
|
|
174
|
+
it('preserves pre-existing settings for no unevaluated properties', () => {
|
|
175
175
|
expect(
|
|
176
176
|
convertSchema({
|
|
177
177
|
type: 'object',
|
package/types/index.d.ts
CHANGED
|
@@ -980,7 +980,7 @@ export type BaseControllerActionOptions = {
|
|
|
980
980
|
*
|
|
981
981
|
* @see {@link https://github.com/ditojs/dito/blob/master/docs/model-properties.md Model Properties}
|
|
982
982
|
*/
|
|
983
|
-
|
|
983
|
+
response?: Schema & { name?: string }
|
|
984
984
|
/**
|
|
985
985
|
* The scope(s) to be applied to every query executed through the action.
|
|
986
986
|
*
|
|
@@ -1778,12 +1778,12 @@ export const authorize: (
|
|
|
1778
1778
|
export const parameters: (params: { [key: string]: Schema }) => Mixin
|
|
1779
1779
|
|
|
1780
1780
|
/**
|
|
1781
|
-
* Apply the
|
|
1781
|
+
* Apply the response mixin to a controller action, in order to provide a schema
|
|
1782
1782
|
* for the value returned from the action handler and optionally map the value
|
|
1783
1783
|
* to a key inside a returned object when it contains a `name` property.
|
|
1784
1784
|
*/
|
|
1785
|
-
export const
|
|
1786
|
-
|
|
1785
|
+
export const response: (
|
|
1786
|
+
response: Schema & { name?: string },
|
|
1787
1787
|
options: any
|
|
1788
1788
|
) => Mixin
|
|
1789
1789
|
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { resolve } from 'url'
|
|
2
|
-
import { MissingRefError } from 'ajv'
|
|
3
|
-
import { clone, mergeDeeply } from '@ditojs/utils'
|
|
4
|
-
|
|
5
|
-
export const $extend = {
|
|
6
|
-
macro(schemas, parentSchema, ctx) {
|
|
7
|
-
const [source, ...patch] = schemas.map(schema => {
|
|
8
|
-
const { $ref } = schema
|
|
9
|
-
if ($ref) {
|
|
10
|
-
const { baseId, self } = ctx
|
|
11
|
-
const id =
|
|
12
|
-
baseId && baseId !== '#'
|
|
13
|
-
? resolve(baseId, $ref)
|
|
14
|
-
: $ref
|
|
15
|
-
const validate = self.getSchema(id)
|
|
16
|
-
if (!validate) {
|
|
17
|
-
throw new MissingRefError(baseId, $ref)
|
|
18
|
-
}
|
|
19
|
-
schema = validate.schema
|
|
20
|
-
}
|
|
21
|
-
return schema
|
|
22
|
-
})
|
|
23
|
-
return mergeDeeply(clone(source), ...patch)
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
metaSchema: {
|
|
27
|
-
type: 'array',
|
|
28
|
-
items: { type: 'object' },
|
|
29
|
-
minItems: 2
|
|
30
|
-
}
|
|
31
|
-
}
|
package/src/utils/decorator.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export function createDecorator(handler) {
|
|
2
|
-
return (target, key, descriptor) => {
|
|
3
|
-
// Convert `descriptor.initializer()` to `descriptor.value`:
|
|
4
|
-
if (descriptor.initializer) {
|
|
5
|
-
descriptor.value = descriptor.initializer()
|
|
6
|
-
delete descriptor.initializer
|
|
7
|
-
}
|
|
8
|
-
handler(descriptor.value)
|
|
9
|
-
}
|
|
10
|
-
}
|
|
File without changes
|