@ditojs/server 2.96.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 +6 -6
- package/src/app/Application.js +1 -1
- package/src/models/Model.js +33 -7
- package/src/models/Model.test.js +180 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
29
|
-
"@ditojs/build": "^2.
|
|
30
|
-
"@ditojs/router": "^2.
|
|
31
|
-
"@ditojs/utils": "^2.
|
|
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": "
|
|
93
|
+
"gitHead": "70d0c1d3aa16844959e3c06d71d8253f21abad3f"
|
|
94
94
|
}
|
package/src/app/Application.js
CHANGED
|
@@ -924,7 +924,7 @@ export class Application extends Koa {
|
|
|
924
924
|
changedFiles,
|
|
925
925
|
transaction = null
|
|
926
926
|
) {
|
|
927
|
-
let importedFiles
|
|
927
|
+
let importedFiles
|
|
928
928
|
const AssetModel = this.getModel('Asset')
|
|
929
929
|
if (AssetModel) {
|
|
930
930
|
importedFiles = await this.addForeignAssets(
|
package/src/models/Model.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
parseDataPath,
|
|
13
13
|
normalizeDataPath,
|
|
14
14
|
getValueAtDataPath,
|
|
15
|
+
getEntriesAtDataPath,
|
|
15
16
|
setValueAtDataPath,
|
|
16
17
|
mapConcurrently,
|
|
17
18
|
assignDeeply,
|
|
@@ -481,6 +482,25 @@ export class Model extends objection.Model {
|
|
|
481
482
|
}
|
|
482
483
|
}
|
|
483
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
|
+
}
|
|
484
504
|
return property ?? null
|
|
485
505
|
}
|
|
486
506
|
|
|
@@ -1018,14 +1038,20 @@ export class Model extends objection.Model {
|
|
|
1018
1038
|
const { assets } = this.definition
|
|
1019
1039
|
if (assets) {
|
|
1020
1040
|
for (const dataPath in assets) {
|
|
1021
|
-
const data = getValueAtDataPath(json, dataPath, noop)
|
|
1022
|
-
if (!data) continue
|
|
1023
1041
|
const storage = this.app.getStorage(assets[dataPath].storage)
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
)
|
|
1042
|
+
// Wildcard data paths (e.g. `**.file`) expand to multiple concrete
|
|
1043
|
+
// paths; iterate them so each match is written back to its own
|
|
1044
|
+
// location instead of the flattened wildcard result.
|
|
1045
|
+
const entries = getEntriesAtDataPath(json, dataPath, noop)
|
|
1046
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
1047
|
+
if (data) {
|
|
1048
|
+
setValueAtDataPath(
|
|
1049
|
+
json,
|
|
1050
|
+
path,
|
|
1051
|
+
mapAssetFiles(data, storage, callback)
|
|
1052
|
+
)
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1029
1055
|
}
|
|
1030
1056
|
}
|
|
1031
1057
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Model } from './Model.js'
|
|
2
|
+
|
|
3
|
+
function makeFakeModel(assets) {
|
|
4
|
+
const storage = {
|
|
5
|
+
signAssetFile(file) {
|
|
6
|
+
file.signature = `sig-of-${file.key}`
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
app: {
|
|
11
|
+
getStorage(name) {
|
|
12
|
+
return name === 'test' ? storage : null
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
definition: { assets }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function signCallback(file, storage) {
|
|
20
|
+
const signed = { ...file }
|
|
21
|
+
storage.signAssetFile(signed)
|
|
22
|
+
return signed
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('Model._mapAssetFiles: asset signing across data paths', () => {
|
|
26
|
+
it('writes the signature back to a literal data path', () => {
|
|
27
|
+
const model = makeFakeModel({ 'image.file': { storage: 'test' } })
|
|
28
|
+
const json = { image: { file: { key: 'foo.png' } } }
|
|
29
|
+
Model._mapAssetFiles.call(model, json, signCallback)
|
|
30
|
+
expect(json.image.file.signature).toBe('sig-of-foo.png')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('writes the signature back to every match of a deep wildcard', () => {
|
|
34
|
+
const model = makeFakeModel({ '**.file': { storage: 'test' } })
|
|
35
|
+
const json = {
|
|
36
|
+
content: {
|
|
37
|
+
file: { key: 'a.png' },
|
|
38
|
+
sections: [
|
|
39
|
+
{ file: { key: 'b.png' } },
|
|
40
|
+
{ file: { key: 'c.png' } }
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
Model._mapAssetFiles.call(model, json, signCallback)
|
|
45
|
+
expect(json.content.file.signature).toBe('sig-of-a.png')
|
|
46
|
+
expect(json.content.sections[0].file.signature).toBe('sig-of-b.png')
|
|
47
|
+
expect(json.content.sections[1].file.signature).toBe('sig-of-c.png')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('writes the signature back into wildcard array values', () => {
|
|
51
|
+
const model = makeFakeModel({ '**.files': { storage: 'test' } })
|
|
52
|
+
const json = {
|
|
53
|
+
content: {
|
|
54
|
+
files: [{ key: 'x.png' }, { key: 'y.png' }]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
Model._mapAssetFiles.call(model, json, signCallback)
|
|
58
|
+
expect(json.content.files[0].signature).toBe('sig-of-x.png')
|
|
59
|
+
expect(json.content.files[1].signature).toBe('sig-of-y.png')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('is a no-op when the wildcard matches nothing', () => {
|
|
63
|
+
const model = makeFakeModel({ '**.file': { storage: 'test' } })
|
|
64
|
+
const json = { name: 'no-files-here' }
|
|
65
|
+
Model._mapAssetFiles.call(model, json, signCallback)
|
|
66
|
+
expect(json).toEqual({ name: 'no-files-here' })
|
|
67
|
+
})
|
|
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
|
+
})
|