@ditojs/server 2.52.0 → 2.53.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.
- package/package.json +14 -14
- package/src/models/Model.js +4 -12
- package/src/schema/schema.js +156 -64
- package/src/schema/schema.test.js +29 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.53.1",
|
|
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",
|
|
@@ -26,12 +26,12 @@
|
|
|
26
26
|
"node >= 18"
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ditojs/admin": "^2.
|
|
30
|
-
"@ditojs/build": "^2.
|
|
31
|
-
"@ditojs/router": "^2.
|
|
32
|
-
"@ditojs/utils": "^2.
|
|
29
|
+
"@ditojs/admin": "^2.53.0",
|
|
30
|
+
"@ditojs/build": "^2.53.0",
|
|
31
|
+
"@ditojs/router": "^2.53.0",
|
|
32
|
+
"@ditojs/utils": "^2.53.0",
|
|
33
33
|
"@koa/cors": "^5.0.0",
|
|
34
|
-
"@koa/multer": "^
|
|
34
|
+
"@koa/multer": "^4.0.0",
|
|
35
35
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
|
36
36
|
"ajv": "^8.17.1",
|
|
37
37
|
"ajv-formats": "^3.0.1",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"bytes": "^3.1.2",
|
|
40
40
|
"data-uri-to-buffer": "^6.0.2",
|
|
41
41
|
"eventemitter2": "^6.4.9",
|
|
42
|
-
"file-type": "^
|
|
42
|
+
"file-type": "^21.0.0",
|
|
43
43
|
"helmet": "^8.1.0",
|
|
44
44
|
"koa": "^3.0.0",
|
|
45
45
|
"koa-bodyparser": "^4.4.1",
|
|
46
46
|
"koa-compose": "^4.1.0",
|
|
47
47
|
"koa-compress": "^5.1.1",
|
|
48
48
|
"koa-conditional-get": "^3.0.0",
|
|
49
|
-
"koa-etag": "^
|
|
49
|
+
"koa-etag": "^5.0.0",
|
|
50
50
|
"koa-helmet": "^8.0.1",
|
|
51
51
|
"koa-mount": "^4.2.0",
|
|
52
52
|
"koa-passport": "^6.0.0",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"koa-static": "^5.0.0",
|
|
56
56
|
"leather": "^3.0.3",
|
|
57
57
|
"mime-types": "^3.0.1",
|
|
58
|
-
"multer": "^
|
|
58
|
+
"multer": "^2.0.1",
|
|
59
59
|
"multer-s3": "https://github.com/ditojs/multer-s3#dito",
|
|
60
60
|
"nanoid": "^5.1.5",
|
|
61
61
|
"parse-duration": "^2.1.4",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"passthrough-counter": "^1.0.0",
|
|
64
64
|
"picocolors": "^1.1.1",
|
|
65
65
|
"picomatch": "^4.0.2",
|
|
66
|
-
"pino": "^9.
|
|
66
|
+
"pino": "^9.7.0",
|
|
67
67
|
"pino-pretty": "^13.0.0",
|
|
68
68
|
"pluralize": "^8.0.0",
|
|
69
69
|
"repl": "^0.1.3",
|
|
@@ -77,18 +77,18 @@
|
|
|
77
77
|
"objection": "^3.0.1"
|
|
78
78
|
},
|
|
79
79
|
"devDependencies": {
|
|
80
|
-
"@aws-sdk/client-s3": "^3.
|
|
81
|
-
"@aws-sdk/lib-storage": "^3.
|
|
80
|
+
"@aws-sdk/client-s3": "^3.832.0",
|
|
81
|
+
"@aws-sdk/lib-storage": "^3.832.0",
|
|
82
82
|
"@types/koa-bodyparser": "^4.3.12",
|
|
83
83
|
"@types/koa-compress": "^4.0.6",
|
|
84
84
|
"@types/koa-response-time": "^2.1.5",
|
|
85
85
|
"@types/koa-session": "^6.4.5",
|
|
86
86
|
"@types/koa-static": "^4.0.4",
|
|
87
87
|
"@types/koa__cors": "^5.0.0",
|
|
88
|
-
"@types/node": "^
|
|
88
|
+
"@types/node": "^24.0.3",
|
|
89
89
|
"knex": "^3.1.0",
|
|
90
90
|
"objection": "^3.1.5",
|
|
91
91
|
"typescript": "^5.8.3"
|
|
92
92
|
},
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "542865f8e49144518b8c9ea1bc4521c0aa5456df"
|
|
94
94
|
}
|
package/src/models/Model.js
CHANGED
|
@@ -390,21 +390,13 @@ export class Model extends objection.Model {
|
|
|
390
390
|
return this._getCached(
|
|
391
391
|
'jsonSchema',
|
|
392
392
|
() => {
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
properties: this.definition.properties
|
|
398
|
-
},
|
|
399
|
-
{ definitions }
|
|
400
|
-
)
|
|
393
|
+
const schema = convertSchema({
|
|
394
|
+
type: 'object',
|
|
395
|
+
properties: this.definition.properties
|
|
396
|
+
})
|
|
401
397
|
addRelationSchemas(this, schema.properties)
|
|
402
398
|
// Merge in root-level schema additions
|
|
403
399
|
assignDeeply(schema, this.definition.schema)
|
|
404
|
-
// Merge in definitions
|
|
405
|
-
if (Object.keys(definitions).length > 0) {
|
|
406
|
-
schema.definitions = definitions
|
|
407
|
-
}
|
|
408
400
|
return {
|
|
409
401
|
$id: this.name,
|
|
410
402
|
...schema
|
package/src/schema/schema.js
CHANGED
|
@@ -1,17 +1,60 @@
|
|
|
1
1
|
import { isObject, isArray, isString, equals } from '@ditojs/utils'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const schemaCaches = {}
|
|
4
|
+
|
|
5
|
+
function getSchemaCache(options) {
|
|
6
|
+
const key = Object.entries(options || {})
|
|
7
|
+
.toSorted()
|
|
8
|
+
.map(([key, value]) => `${key}:${value}`)
|
|
9
|
+
.join(',') || 'default'
|
|
10
|
+
return (schemaCaches[key] ||= new WeakMap())
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function convertSchema(
|
|
14
|
+
schema,
|
|
15
|
+
options = {},
|
|
16
|
+
parentEntry = null
|
|
17
|
+
) {
|
|
18
|
+
const original = schema
|
|
19
|
+
const isRoot = parentEntry === null
|
|
20
|
+
|
|
21
|
+
const schemaCache = getSchemaCache(options)
|
|
22
|
+
if (schemaCache.has(original)) {
|
|
23
|
+
const { schema, definitions, parentEntries } = schemaCache.get(original)
|
|
24
|
+
parentEntries.push(parentEntry)
|
|
25
|
+
if (definitions) {
|
|
26
|
+
if (isRoot) {
|
|
27
|
+
return { ...schema, definitions }
|
|
28
|
+
} else {
|
|
29
|
+
mergeDefinitions(parentEntry.definitions, definitions)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return schema
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entry = {
|
|
36
|
+
schema: null,
|
|
37
|
+
definitions: {},
|
|
38
|
+
parentEntries: parentEntry ? [parentEntry] : []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// To prevent circular references, cache the entry before all conversion work.
|
|
42
|
+
schemaCache.set(original, entry)
|
|
43
|
+
|
|
44
|
+
let definitions = null
|
|
4
45
|
if (isArray(schema)) {
|
|
5
46
|
// Needed for allOf, anyOf, oneOf, not, items, see below:
|
|
6
|
-
schema = schema.map(
|
|
47
|
+
schema = schema.map(item => convertSchema(item, options, entry))
|
|
7
48
|
} else if (isObject(schema)) {
|
|
8
49
|
// Create a shallow clone so we can modify and return:
|
|
9
50
|
// Also collect and propagate the definitions up to the root schema through
|
|
10
51
|
// `options.definitions`, as passed from `Model static get jsonSchema()`:
|
|
11
|
-
const { definitions, ...rest } = schema
|
|
12
|
-
|
|
52
|
+
const { definitions: defs, ...rest } = schema
|
|
53
|
+
definitions = defs
|
|
13
54
|
schema = rest
|
|
14
55
|
const { $ref, type } = schema
|
|
56
|
+
const jsonType = jsonTypes[type]
|
|
57
|
+
|
|
15
58
|
if (schema.required === true) {
|
|
16
59
|
// Our 'required' is not the same as JSON Schema's: Use the 'required'
|
|
17
60
|
// format instead that only validates if the required value is not empty,
|
|
@@ -21,37 +64,14 @@ export function convertSchema(schema, options = {}) {
|
|
|
21
64
|
schema = addFormat(schema, 'required')
|
|
22
65
|
}
|
|
23
66
|
|
|
24
|
-
// Convert properties
|
|
25
|
-
let hasConvertedProperties = false
|
|
26
|
-
if (schema.properties) {
|
|
27
|
-
const { properties, required } = convertProperties(
|
|
28
|
-
schema.properties,
|
|
29
|
-
options
|
|
30
|
-
)
|
|
31
|
-
schema.properties = properties
|
|
32
|
-
if (required.length > 0) {
|
|
33
|
-
schema.required = required
|
|
34
|
-
}
|
|
35
|
-
hasConvertedProperties = true
|
|
36
|
-
}
|
|
37
|
-
if (schema.patternProperties) {
|
|
38
|
-
// TODO: Don't we need to handle required here too?
|
|
39
|
-
const { properties } = convertProperties(
|
|
40
|
-
schema.patternProperties,
|
|
41
|
-
options
|
|
42
|
-
)
|
|
43
|
-
schema.patternProperties = properties
|
|
44
|
-
hasConvertedProperties = true
|
|
45
|
-
}
|
|
46
|
-
|
|
47
67
|
// Convert array items
|
|
48
|
-
schema.prefixItems &&= convertSchema(schema.prefixItems, options)
|
|
49
|
-
schema.items &&= convertSchema(schema.items, options)
|
|
68
|
+
schema.prefixItems &&= convertSchema(schema.prefixItems, options, entry)
|
|
69
|
+
schema.items &&= convertSchema(schema.items, options, entry)
|
|
50
70
|
|
|
51
71
|
// Handle nested allOf, anyOf, oneOf & co. fields
|
|
52
72
|
for (const key of ['allOf', 'anyOf', 'oneOf', 'not', '$extend']) {
|
|
53
73
|
if (key in schema) {
|
|
54
|
-
schema[key] = convertSchema(schema[key], options)
|
|
74
|
+
schema[key] = convertSchema(schema[key], options, entry)
|
|
55
75
|
}
|
|
56
76
|
}
|
|
57
77
|
|
|
@@ -62,18 +82,8 @@ export function convertSchema(schema, options = {}) {
|
|
|
62
82
|
? `#/definitions/${$ref}`
|
|
63
83
|
: $ref
|
|
64
84
|
} else if (isString(type)) {
|
|
65
|
-
// Convert schema property notation to JSON schema
|
|
66
|
-
const jsonType = jsonTypes[type]
|
|
67
85
|
if (jsonType) {
|
|
68
86
|
schema.type = jsonType
|
|
69
|
-
if (
|
|
70
|
-
(hasConvertedProperties || schema.discriminator) &&
|
|
71
|
-
!('unevaluatedProperties' in schema)
|
|
72
|
-
) {
|
|
73
|
-
// Invert the logic of `unevaluatedProperties` so that it needs to be
|
|
74
|
-
// explicitly set to `true`:
|
|
75
|
-
schema.unevaluatedProperties = false
|
|
76
|
-
}
|
|
77
87
|
} else if (['date', 'datetime', 'timestamp'].includes(type)) {
|
|
78
88
|
// Date properties can be submitted both as a string or a Date object.
|
|
79
89
|
// Provide validation through date-time format, which in Ajv appears
|
|
@@ -114,15 +124,72 @@ export function convertSchema(schema, options = {}) {
|
|
|
114
124
|
if (schema.nullable && schema.enum && !schema.enum.includes(null)) {
|
|
115
125
|
schema.enum.push(null)
|
|
116
126
|
}
|
|
127
|
+
|
|
128
|
+
// Convert properties last. This is needed for circular references
|
|
129
|
+
// to work correctly, as the properties may reference the same schema
|
|
130
|
+
// that is being converted right now.
|
|
131
|
+
let hasConvertedProperties = false
|
|
132
|
+
if (schema.properties) {
|
|
133
|
+
const { properties, required } = convertProperties(
|
|
134
|
+
schema.properties,
|
|
135
|
+
options,
|
|
136
|
+
entry
|
|
137
|
+
)
|
|
138
|
+
schema.properties = properties
|
|
139
|
+
if (required.length > 0) {
|
|
140
|
+
schema.required = required
|
|
141
|
+
}
|
|
142
|
+
hasConvertedProperties = true
|
|
143
|
+
}
|
|
144
|
+
if (schema.patternProperties) {
|
|
145
|
+
// TODO: Don't we need to handle required here too?
|
|
146
|
+
const { properties } = convertProperties(
|
|
147
|
+
schema.patternProperties,
|
|
148
|
+
options,
|
|
149
|
+
entry
|
|
150
|
+
)
|
|
151
|
+
schema.patternProperties = properties
|
|
152
|
+
hasConvertedProperties = true
|
|
153
|
+
}
|
|
154
|
+
if (
|
|
155
|
+
jsonType &&
|
|
156
|
+
(hasConvertedProperties || schema.discriminator) &&
|
|
157
|
+
!('unevaluatedProperties' in schema)
|
|
158
|
+
) {
|
|
159
|
+
// Invert the logic of `unevaluatedProperties` so that it needs to be
|
|
160
|
+
// explicitly set to `true`:
|
|
161
|
+
schema.unevaluatedProperties = false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
entry.schema = schema
|
|
166
|
+
|
|
167
|
+
// Only convert definitions once `entry.schema` is set, so that it works as
|
|
168
|
+
// expected with circular references.
|
|
169
|
+
if (definitions) {
|
|
170
|
+
mergeDefinitions(
|
|
171
|
+
entry.definitions,
|
|
172
|
+
convertDefinitions(definitions, options, entry)
|
|
173
|
+
)
|
|
117
174
|
}
|
|
175
|
+
|
|
176
|
+
if (Object.keys(entry.definitions).length > 0) {
|
|
177
|
+
// Propagate the definitions up the parent entry chains, that due to
|
|
178
|
+
// circular references may not be up to date yet.
|
|
179
|
+
mergeDefinitionsRecursively(entry, entry.definitions)
|
|
180
|
+
if (isRoot) {
|
|
181
|
+
schema.definitions = entry.definitions
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
118
185
|
return schema
|
|
119
186
|
}
|
|
120
187
|
|
|
121
|
-
function convertProperties(schemaProperties, options) {
|
|
188
|
+
function convertProperties(schemaProperties, options, entry) {
|
|
122
189
|
const properties = {}
|
|
123
190
|
const required = []
|
|
124
191
|
for (const [key, property] of Object.entries(schemaProperties)) {
|
|
125
|
-
properties[key] = convertSchema(property, options)
|
|
192
|
+
properties[key] = convertSchema(property, options, entry)
|
|
126
193
|
if (property?.required) {
|
|
127
194
|
required.push(key)
|
|
128
195
|
}
|
|
@@ -130,28 +197,53 @@ function convertProperties(schemaProperties, options) {
|
|
|
130
197
|
return { properties, required }
|
|
131
198
|
}
|
|
132
199
|
|
|
133
|
-
function
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
200
|
+
function convertDefinitions(definitions, options, entry) {
|
|
201
|
+
let converted = null
|
|
202
|
+
for (const [key, schema] of Object.entries(definitions)) {
|
|
203
|
+
if (!key.startsWith('#')) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Invalid definition '${
|
|
206
|
+
key
|
|
207
|
+
}', the name of nested Dito.js definitions must start with '#': ${
|
|
208
|
+
JSON.stringify(schema)
|
|
209
|
+
}`
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
converted ??= {}
|
|
213
|
+
converted[key] = convertSchema(schema, options, entry)
|
|
214
|
+
}
|
|
215
|
+
return converted
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function mergeDefinitions(definitions, defs) {
|
|
219
|
+
for (const [key, def] of Object.entries(defs)) {
|
|
220
|
+
const definition = definitions[key]
|
|
221
|
+
if (definition && !equals(definition, def)) {
|
|
222
|
+
throw new Error(
|
|
223
|
+
`Duplicate nested definition for '${key}' with different schema: ${
|
|
224
|
+
JSON.stringify(def, null, 2)
|
|
225
|
+
}, ${
|
|
226
|
+
JSON.stringify(definition, null, 2)
|
|
227
|
+
}`
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
definitions[key] ??= def
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function mergeDefinitionsRecursively(
|
|
235
|
+
entry,
|
|
236
|
+
definitions,
|
|
237
|
+
visited = new WeakSet()
|
|
238
|
+
) {
|
|
239
|
+
if (!visited.has(entry)) {
|
|
240
|
+
visited.add(entry)
|
|
241
|
+
|
|
242
|
+
if (definitions !== entry.definitions) {
|
|
243
|
+
mergeDefinitions(entry.definitions, definitions)
|
|
244
|
+
}
|
|
245
|
+
for (const parentEntry of entry.parentEntries) {
|
|
246
|
+
mergeDefinitionsRecursively(parentEntry, definitions, visited)
|
|
155
247
|
}
|
|
156
248
|
}
|
|
157
249
|
}
|
|
@@ -633,7 +633,6 @@ describe('convertSchema()', () => {
|
|
|
633
633
|
})
|
|
634
634
|
|
|
635
635
|
it('supports nested Dito.js definitions', () => {
|
|
636
|
-
const definitions = {}
|
|
637
636
|
expect(
|
|
638
637
|
convertSchema(
|
|
639
638
|
{
|
|
@@ -653,8 +652,18 @@ describe('convertSchema()', () => {
|
|
|
653
652
|
'#type2': {
|
|
654
653
|
type: 'object',
|
|
655
654
|
properties: {
|
|
656
|
-
|
|
655
|
+
prop4: {
|
|
657
656
|
type: 'string'
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
prop5: {
|
|
660
|
+
$ref: '#type3'
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
|
|
664
|
+
definitions: {
|
|
665
|
+
'#type3': {
|
|
666
|
+
type: 'boolean'
|
|
658
667
|
}
|
|
659
668
|
}
|
|
660
669
|
}
|
|
@@ -667,8 +676,7 @@ describe('convertSchema()', () => {
|
|
|
667
676
|
type: 'integer'
|
|
668
677
|
}
|
|
669
678
|
}
|
|
670
|
-
}
|
|
671
|
-
{ definitions }
|
|
679
|
+
}
|
|
672
680
|
)
|
|
673
681
|
).toEqual({
|
|
674
682
|
type: 'object',
|
|
@@ -686,19 +694,25 @@ describe('convertSchema()', () => {
|
|
|
686
694
|
}
|
|
687
695
|
}
|
|
688
696
|
}
|
|
689
|
-
}
|
|
690
|
-
})
|
|
691
|
-
expect(definitions).toEqual({
|
|
692
|
-
'#type1': {
|
|
693
|
-
type: 'integer'
|
|
694
697
|
},
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
698
|
+
definitions: {
|
|
699
|
+
'#type1': {
|
|
700
|
+
type: 'integer'
|
|
701
|
+
},
|
|
702
|
+
'#type2': {
|
|
703
|
+
type: 'object',
|
|
704
|
+
unevaluatedProperties: false,
|
|
705
|
+
properties: {
|
|
706
|
+
prop4: {
|
|
707
|
+
type: 'string'
|
|
708
|
+
},
|
|
709
|
+
prop5: {
|
|
710
|
+
$ref: '#/definitions/#type3'
|
|
711
|
+
}
|
|
701
712
|
}
|
|
713
|
+
},
|
|
714
|
+
'#type3': {
|
|
715
|
+
type: 'boolean'
|
|
702
716
|
}
|
|
703
717
|
}
|
|
704
718
|
})
|