@ditojs/server 2.52.0 → 2.53.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": "2.52.0",
3
+ "version": "2.53.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",
@@ -26,12 +26,12 @@
26
26
  "node >= 18"
27
27
  ],
28
28
  "dependencies": {
29
- "@ditojs/admin": "^2.52.0",
30
- "@ditojs/build": "^2.52.0",
31
- "@ditojs/router": "^2.52.0",
32
- "@ditojs/utils": "^2.52.0",
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": "^3.1.0",
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": "^20.5.0",
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": "^4.0.0",
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": "^1.4.5-lts.2",
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.6.0",
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.701.0",
81
- "@aws-sdk/lib-storage": "^3.701.0",
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": "^22.15.18",
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": "dbfde6b475d824a88fbbbc1e007fd264e80d46fb"
93
+ "gitHead": "7f36fa29f11708527b85e37233b83ecf6bf40497"
94
94
  }
@@ -390,21 +390,13 @@ export class Model extends objection.Model {
390
390
  return this._getCached(
391
391
  'jsonSchema',
392
392
  () => {
393
- const definitions = {}
394
- const schema = convertSchema(
395
- {
396
- type: 'object',
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
@@ -1,17 +1,42 @@
1
1
  import { isObject, isArray, isString, equals } from '@ditojs/utils'
2
2
 
3
- export function convertSchema(schema, options = {}) {
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(schema, options = {}, rootDefinitions = null) {
14
+ const original = schema
15
+
16
+ const schemaCache = getSchemaCache(options)
17
+ if (schemaCache.has(original)) {
18
+ const { schema, definitions } = schemaCache.get(original)
19
+ mergeDefinitions(rootDefinitions, definitions)
20
+ return schema
21
+ }
22
+
23
+ let definitions = null
4
24
  if (isArray(schema)) {
5
25
  // Needed for allOf, anyOf, oneOf, not, items, see below:
6
- schema = schema.map(entry => convertSchema(entry, options))
26
+ schema = schema.map(entry => convertSchema(entry, options, rootDefinitions))
7
27
  } else if (isObject(schema)) {
28
+ const isRoot = rootDefinitions === null
29
+ rootDefinitions ??= {}
30
+
8
31
  // Create a shallow clone so we can modify and return:
9
32
  // Also collect and propagate the definitions up to the root schema through
10
33
  // `options.definitions`, as passed from `Model static get jsonSchema()`:
11
- const { definitions, ...rest } = schema
12
- mergeDefinitions(options.definitions, definitions, options)
34
+ const { definitions: defs, ...rest } = schema
35
+ definitions = defs
13
36
  schema = rest
14
37
  const { $ref, type } = schema
38
+ const jsonType = jsonTypes[type]
39
+
15
40
  if (schema.required === true) {
16
41
  // Our 'required' is not the same as JSON Schema's: Use the 'required'
17
42
  // format instead that only validates if the required value is not empty,
@@ -21,37 +46,22 @@ export function convertSchema(schema, options = {}) {
21
46
  schema = addFormat(schema, 'required')
22
47
  }
23
48
 
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
49
  // Convert array items
48
- schema.prefixItems &&= convertSchema(schema.prefixItems, options)
49
- schema.items &&= convertSchema(schema.items, options)
50
+ schema.prefixItems &&= convertSchema(
51
+ schema.prefixItems,
52
+ options,
53
+ rootDefinitions
54
+ )
55
+ schema.items &&= convertSchema(
56
+ schema.items,
57
+ options,
58
+ rootDefinitions
59
+ )
50
60
 
51
61
  // Handle nested allOf, anyOf, oneOf & co. fields
52
62
  for (const key of ['allOf', 'anyOf', 'oneOf', 'not', '$extend']) {
53
63
  if (key in schema) {
54
- schema[key] = convertSchema(schema[key], options)
64
+ schema[key] = convertSchema(schema[key], options, rootDefinitions)
55
65
  }
56
66
  }
57
67
 
@@ -62,18 +72,8 @@ export function convertSchema(schema, options = {}) {
62
72
  ? `#/definitions/${$ref}`
63
73
  : $ref
64
74
  } else if (isString(type)) {
65
- // Convert schema property notation to JSON schema
66
- const jsonType = jsonTypes[type]
67
75
  if (jsonType) {
68
76
  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
77
  } else if (['date', 'datetime', 'timestamp'].includes(type)) {
78
78
  // Date properties can be submitted both as a string or a Date object.
79
79
  // Provide validation through date-time format, which in Ajv appears
@@ -114,44 +114,108 @@ export function convertSchema(schema, options = {}) {
114
114
  if (schema.nullable && schema.enum && !schema.enum.includes(null)) {
115
115
  schema.enum.push(null)
116
116
  }
117
+
118
+ // Only convert properties after the schema is remembered, to avoid endless
119
+ // recursion in circular schema definitions.
120
+ let hasConvertedProperties = false
121
+ if (schema.properties) {
122
+ const { properties, required } = convertProperties(
123
+ schema.properties,
124
+ options,
125
+ rootDefinitions
126
+ )
127
+ schema.properties = properties
128
+ if (required.length > 0) {
129
+ schema.required = required
130
+ }
131
+ hasConvertedProperties = true
132
+ }
133
+ if (schema.patternProperties) {
134
+ // TODO: Don't we need to handle required here too?
135
+ const { properties } = convertProperties(
136
+ schema.patternProperties,
137
+ options,
138
+ rootDefinitions
139
+ )
140
+ schema.patternProperties = properties
141
+ hasConvertedProperties = true
142
+ }
143
+ if (
144
+ jsonType &&
145
+ (hasConvertedProperties || schema.discriminator) &&
146
+ !('unevaluatedProperties' in schema)
147
+ ) {
148
+ // Invert the logic of `unevaluatedProperties` so that it needs to be
149
+ // explicitly set to `true`:
150
+ schema.unevaluatedProperties = false
151
+ }
152
+
153
+ if (isRoot && hasDefinitions(rootDefinitions)) {
154
+ schema.definitions = rootDefinitions
155
+ }
156
+ }
157
+
158
+ const entry = { schema, definitions: null }
159
+ schemaCache.set(original, entry)
160
+
161
+ // To prevent circular references, we need to convert the definitions
162
+ // after the schema entry is cached.
163
+ if (definitions) {
164
+ definitions = convertDefinitions(definitions, options)
165
+ mergeDefinitions(rootDefinitions, definitions)
166
+ entry.definitions = definitions
117
167
  }
168
+
118
169
  return schema
119
170
  }
120
171
 
121
- function convertProperties(schemaProperties, options) {
172
+ function convertProperties(schemaProperties, options, definitions = {}) {
122
173
  const properties = {}
123
174
  const required = []
124
175
  for (const [key, property] of Object.entries(schemaProperties)) {
125
- properties[key] = convertSchema(property, options)
176
+ properties[key] = convertSchema(property, options, definitions)
126
177
  if (property?.required) {
127
178
  required.push(key)
128
179
  }
129
180
  }
130
- return { properties, required }
181
+ return { properties, required, definitions }
182
+ }
183
+
184
+ function hasDefinitions(definitions) {
185
+ return definitions && Object.keys(definitions).length > 0
186
+ }
187
+
188
+ function convertDefinitions(definitions, options) {
189
+ const converted = {}
190
+ for (const [key, schema] of Object.entries(definitions)) {
191
+ if (!key.startsWith('#')) {
192
+ throw new Error(
193
+ `Invalid definition '${
194
+ key
195
+ }', the name of nested Dito.js definitions must start with '#': ${
196
+ JSON.stringify(schema)
197
+ }`
198
+ )
199
+ }
200
+ converted[key] = convertSchema(schema, options, converted)
201
+ }
202
+ return hasDefinitions(converted) ? converted : null
131
203
  }
132
204
 
133
- function mergeDefinitions(definitions, defs, options) {
205
+ function mergeDefinitions(definitions, defs) {
134
206
  if (definitions && defs) {
135
207
  for (const [key, def] of Object.entries(defs)) {
136
- if (!key.startsWith('#')) {
137
- throw new Error(
138
- `Invalid definition '${
139
- key
140
- }', the name of nested Dito.js definitions must start with '#': ${
141
- JSON.stringify(def)
142
- }`
143
- )
144
- }
145
208
  const definition = definitions[key]
146
- const converted = convertSchema(def, options)
147
- if (definition && !equals(definition, converted)) {
209
+ if (definition && !equals(definition, def)) {
148
210
  throw new Error(
149
211
  `Duplicate nested definition for '${key}' with different schema: ${
150
- JSON.stringify(def)
212
+ JSON.stringify(def, null, 2)
213
+ }, ${
214
+ JSON.stringify(definition, null, 2)
151
215
  }`
152
216
  )
153
217
  }
154
- definitions[key] = converted
218
+ definitions[key] = def
155
219
  }
156
220
  }
157
221
  }
@@ -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
- prop3: {
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
- '#type2': {
696
- type: 'object',
697
- unevaluatedProperties: false,
698
- properties: {
699
- prop3: {
700
- type: 'string'
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
  })