@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/server",
3
- "version": "2.52.0",
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.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": "542865f8e49144518b8c9ea1bc4521c0aa5456df"
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,60 @@
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(
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(entry => convertSchema(entry, options))
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
- mergeDefinitions(options.definitions, definitions, options)
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 mergeDefinitions(definitions, defs, options) {
134
- if (definitions && defs) {
135
- 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
- const definition = definitions[key]
146
- const converted = convertSchema(def, options)
147
- if (definition && !equals(definition, converted)) {
148
- throw new Error(
149
- `Duplicate nested definition for '${key}' with different schema: ${
150
- JSON.stringify(def)
151
- }`
152
- )
153
- }
154
- definitions[key] = converted
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
- 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
  })