@ditojs/server 2.97.0 → 2.98.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.97.0",
3
+ "version": "2.98.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/main/packages/server",
@@ -25,10 +25,10 @@
25
25
  "node >= 18"
26
26
  ],
27
27
  "dependencies": {
28
- "@ditojs/admin": "^2.97.0",
29
- "@ditojs/build": "^2.97.0",
30
- "@ditojs/router": "^2.97.0",
31
- "@ditojs/utils": "^2.97.0",
28
+ "@ditojs/admin": "^2.98.0",
29
+ "@ditojs/build": "^2.98.0",
30
+ "@ditojs/router": "^2.98.0",
31
+ "@ditojs/utils": "^2.98.0",
32
32
  "@koa/cors": "^5.0.0",
33
33
  "@koa/etag": "^5.0.2",
34
34
  "@koa/multer": "^4.0.0",
@@ -90,5 +90,5 @@
90
90
  "objection": "^3.1.5",
91
91
  "typescript": "^6.0.3"
92
92
  },
93
- "gitHead": "a15b3aa1474b88b4dbefd114491605b5715a6526"
93
+ "gitHead": "70d0c1d3aa16844959e3c06d71d8253f21abad3f"
94
94
  }
@@ -482,6 +482,25 @@ export class Model extends objection.Model {
482
482
  }
483
483
  }
484
484
  }
485
+ // `discriminator`/`oneOf`/`anyOf`/`allOf` schemas don't always carry an
486
+ // explicit top-level `type`, so callers checking `property.type` (e.g.
487
+ // `loadDataPath()`, which gates traversal of nested data paths on the
488
+ // root being object/array) reject them. Fill in the type when it can be
489
+ // inferred unambiguously: a `discriminator` implies an object, and a
490
+ // homogeneous `oneOf`/`anyOf`/`allOf` adopts its branches' shared type.
491
+ if (property && !property.type) {
492
+ if (property.discriminator) {
493
+ property = { ...property, type: 'object' }
494
+ } else {
495
+ const branches = property.oneOf || property.anyOf || property.allOf
496
+ const types = new Set(
497
+ branches?.map(branch => branch?.type).filter(Boolean)
498
+ )
499
+ if (types.size === 1) {
500
+ property = { ...property, type: [...types][0] }
501
+ }
502
+ }
503
+ }
485
504
  return property ?? null
486
505
  }
487
506
 
@@ -66,3 +66,115 @@ describe('Model._mapAssetFiles: asset signing across data paths', () => {
66
66
  expect(json).toEqual({ name: 'no-files-here' })
67
67
  })
68
68
  })
69
+
70
+ describe('Model.getProperty: type inference for $ref/discriminator schemas', () => {
71
+ function getProperty({ name, properties, definitions = {} }) {
72
+ return Model.getProperty.call(
73
+ { definition: { properties }, jsonSchema: { definitions } },
74
+ name
75
+ )
76
+ }
77
+
78
+ it('returns the property unchanged when it has an explicit type', () => {
79
+ expect(
80
+ getProperty({
81
+ name: 'name',
82
+ properties: { name: { type: 'string' } }
83
+ })
84
+ ).toEqual({ type: 'string' })
85
+ })
86
+
87
+ it('infers type: object from a discriminator on a $ref schema', () => {
88
+ const property = getProperty({
89
+ name: 'content',
90
+ properties: { content: { $ref: '#StoryContent' } },
91
+ definitions: {
92
+ '#StoryContent': {
93
+ discriminator: { propertyName: 'type' },
94
+ oneOf: [
95
+ { type: 'object', properties: { type: { const: 'a' } } },
96
+ { type: 'object', properties: { type: { const: 'b' } } }
97
+ ]
98
+ }
99
+ }
100
+ })
101
+ expect(property.type).toBe('object')
102
+ expect(property.discriminator).toEqual({ propertyName: 'type' })
103
+ })
104
+
105
+ it('infers a shared type from a homogeneous oneOf', () => {
106
+ const property = getProperty({
107
+ name: 'content',
108
+ properties: {
109
+ content: { oneOf: [{ type: 'object' }, { type: 'object' }] }
110
+ }
111
+ })
112
+ expect(property.type).toBe('object')
113
+ })
114
+
115
+ it('infers a shared type from a homogeneous anyOf', () => {
116
+ const property = getProperty({
117
+ name: 'content',
118
+ properties: {
119
+ content: { anyOf: [{ type: 'array' }, { type: 'array' }] }
120
+ }
121
+ })
122
+ expect(property.type).toBe('array')
123
+ })
124
+
125
+ it('leaves type undefined when oneOf branches disagree', () => {
126
+ const property = getProperty({
127
+ name: 'content',
128
+ properties: {
129
+ content: { oneOf: [{ type: 'object' }, { type: 'string' }] }
130
+ }
131
+ })
132
+ expect(property.type).toBeUndefined()
133
+ })
134
+
135
+ it('returns null for an unknown property', () => {
136
+ expect(getProperty({ name: 'missing', properties: {} })).toBeNull()
137
+ })
138
+ })
139
+
140
+ describe('Model.getPropertyOrRelationAtDataPath: traversal through inferred types', () => {
141
+ function lookup({ dataPath, properties, definitions = {} }) {
142
+ const host = {
143
+ definition: { properties },
144
+ jsonSchema: { definitions },
145
+ getRelations: () => ({}),
146
+ getProperty(name) {
147
+ return Model.getProperty.call(this, name)
148
+ }
149
+ }
150
+ return Model.getPropertyOrRelationAtDataPath.call(host, dataPath)
151
+ }
152
+
153
+ it('exposes object type for a discriminator-rooted nested wildcard path', () => {
154
+ // Reproduces the original loadDataPath() crash on
155
+ // `content.article.items[**].file`: before the fix, `content` had
156
+ // no resolved `type`, so loadDataPath rejected the nested path.
157
+ const result = lookup({
158
+ dataPath: 'content.article.items[**].file',
159
+ properties: { content: { $ref: '#StoryContent' } },
160
+ definitions: {
161
+ '#StoryContent': {
162
+ discriminator: { propertyName: 'type' },
163
+ oneOf: [{ type: 'object' }]
164
+ }
165
+ }
166
+ })
167
+ expect(result.property.type).toBe('object')
168
+ expect(result.nestedDataPath).toBe('article/items/**/file')
169
+ expect(result.dataPath).toBe('content')
170
+ })
171
+
172
+ it('returns no nestedDataPath for a leaf property lookup', () => {
173
+ const result = lookup({
174
+ dataPath: 'name',
175
+ properties: { name: { type: 'string' } }
176
+ })
177
+ expect(result.property).toEqual({ type: 'string' })
178
+ expect(result.nestedDataPath).toBe('')
179
+ })
180
+ })