@ditojs/server 1.24.0 → 1.25.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 +9 -9
- package/src/app/Application.js +72 -71
- package/src/controllers/AdminController.js +1 -1
- package/src/controllers/CollectionController.js +1 -1
- package/src/controllers/Controller.js +14 -9
- package/src/controllers/ControllerAction.js +2 -2
- package/src/controllers/MemberAction.js +2 -2
- package/src/controllers/ModelController.js +1 -1
- package/src/graph/graph.js +5 -4
- package/src/middleware/handleConnectMiddleware.js +1 -1
- package/src/mixins/AssetMixin.js +1 -1
- package/src/models/Model.js +30 -15
- package/src/query/QueryBuilder.js +11 -5
- package/src/schema/properties.js +2 -2
- package/src/schema/properties.test.js +2 -2
- package/src/storage/DiskStorage.js +9 -6
- package/src/storage/S3Storage.js +4 -6
- package/src/storage/Storage.js +1 -0
- package/src/utils/emitter.js +5 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.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",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"node >= 18"
|
|
23
23
|
],
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@ditojs/admin": "^1.
|
|
26
|
-
"@ditojs/build": "^1.
|
|
27
|
-
"@ditojs/router": "^1.
|
|
28
|
-
"@ditojs/utils": "^1.
|
|
25
|
+
"@ditojs/admin": "^1.25.0",
|
|
26
|
+
"@ditojs/build": "^1.25.0",
|
|
27
|
+
"@ditojs/router": "^1.25.0",
|
|
28
|
+
"@ditojs/utils": "^1.25.0",
|
|
29
29
|
"@koa/cors": "^4.0.0",
|
|
30
30
|
"@koa/multer": "^3.0.2",
|
|
31
31
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"multer": "^1.4.5-lts.1",
|
|
54
54
|
"multer-s3": "^3.0.1",
|
|
55
55
|
"nanoid": "^4.0.1",
|
|
56
|
-
"parse-duration": "^1.0.
|
|
56
|
+
"parse-duration": "^1.0.3",
|
|
57
57
|
"passport-local": "^1.0.0",
|
|
58
58
|
"passthrough-counter": "^1.0.0",
|
|
59
59
|
"picocolors": "^1.0.0",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"objection": "^3.0.1"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@aws-sdk/client-s3": "^3.
|
|
77
|
+
"@aws-sdk/client-s3": "^3.282.0",
|
|
78
78
|
"@types/koa-bodyparser": "^4.3.10",
|
|
79
79
|
"@types/koa-compress": "^4.0.3",
|
|
80
80
|
"@types/koa-logger": "^3.1.2",
|
|
@@ -83,14 +83,14 @@
|
|
|
83
83
|
"@types/koa-session": "^5.10.6",
|
|
84
84
|
"@types/koa-static": "^4.0.2",
|
|
85
85
|
"@types/koa__cors": "^3.3.1",
|
|
86
|
-
"@types/node": "^18.14.
|
|
86
|
+
"@types/node": "^18.14.6",
|
|
87
87
|
"knex": "^2.4.2",
|
|
88
88
|
"objection": "^3.0.1",
|
|
89
89
|
"type-fest": "^3.6.1",
|
|
90
90
|
"typescript": "^4.9.5"
|
|
91
91
|
},
|
|
92
92
|
"types": "types",
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "29e7f8d8c613c48a856d90d1ee876e85c8d84c08",
|
|
94
94
|
"scripts": {
|
|
95
95
|
"types": "tsc --noEmit ./src/index.d.ts"
|
|
96
96
|
},
|
package/src/app/Application.js
CHANGED
|
@@ -22,7 +22,8 @@ import { Model, knexSnakeCaseMappers, ref } from 'objection'
|
|
|
22
22
|
import Router from '@ditojs/router'
|
|
23
23
|
import {
|
|
24
24
|
isArray, isObject, isString, asArray, isPlainObject, isModule,
|
|
25
|
-
hyphenate, clone, merge, parseDataPath, normalizeDataPath, toPromiseCallback
|
|
25
|
+
hyphenate, clone, merge, parseDataPath, normalizeDataPath, toPromiseCallback,
|
|
26
|
+
mapConcurrently
|
|
26
27
|
} from '@ditojs/utils'
|
|
27
28
|
import SessionStore from './SessionStore.js'
|
|
28
29
|
import { Validator } from './Validator.js'
|
|
@@ -389,22 +390,23 @@ export class Application extends Koa {
|
|
|
389
390
|
for (const [assetDataPath, config] of Object.entries(assets)) {
|
|
390
391
|
const {
|
|
391
392
|
property,
|
|
393
|
+
relation,
|
|
394
|
+
wildcard,
|
|
392
395
|
nestedDataPath,
|
|
393
|
-
name
|
|
394
|
-
index
|
|
396
|
+
name
|
|
395
397
|
} = modelClass.getPropertyOrRelationAtDataPath(assetDataPath)
|
|
396
|
-
if (
|
|
398
|
+
if (relation) {
|
|
399
|
+
throw new Error('Assets on nested relations are not supported')
|
|
400
|
+
} else if (property || wildcard) {
|
|
397
401
|
const normalizedName = normalizeDbNames
|
|
398
402
|
? this.normalizeIdentifier(name)
|
|
399
403
|
: name
|
|
400
404
|
const dataPath = normalizeDataPath([
|
|
401
|
-
normalizedName,
|
|
405
|
+
wildcard || normalizedName,
|
|
402
406
|
...parseDataPath(nestedDataPath)
|
|
403
407
|
])
|
|
404
408
|
const assetConfigs = convertedAssets[normalizedName] ||= {}
|
|
405
409
|
assetConfigs[dataPath] = config
|
|
406
|
-
} else {
|
|
407
|
-
throw new Error('Nested graph properties are not supported yet')
|
|
408
410
|
}
|
|
409
411
|
}
|
|
410
412
|
assetConfig[normalizedModelName] = convertedAssets
|
|
@@ -813,11 +815,13 @@ export class Application extends Koa {
|
|
|
813
815
|
? parseDuration(cleanupTimeThreshold)
|
|
814
816
|
: cleanupTimeThreshold
|
|
815
817
|
|
|
816
|
-
|
|
818
|
+
let importedFiles = []
|
|
817
819
|
const AssetModel = this.getModel('Asset')
|
|
818
820
|
if (AssetModel) {
|
|
819
|
-
importedFiles.
|
|
820
|
-
|
|
821
|
+
importedFiles = await this.addForeignAssets(
|
|
822
|
+
storage,
|
|
823
|
+
addedFiles,
|
|
824
|
+
trx
|
|
821
825
|
)
|
|
822
826
|
if (
|
|
823
827
|
addedFiles.length > 0 ||
|
|
@@ -854,24 +858,23 @@ export class Application extends Koa {
|
|
|
854
858
|
const AssetModel = this.getModel('Asset')
|
|
855
859
|
if (AssetModel) {
|
|
856
860
|
// Find missing assets (copied from another system), and add them.
|
|
857
|
-
await
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
throw new AssetError(
|
|
861
|
+
await mapConcurrently(files, async file => {
|
|
862
|
+
const asset = await AssetModel.query(trx).findOne('key', file.key)
|
|
863
|
+
if (!asset) {
|
|
864
|
+
if (file.data || file.url) {
|
|
865
|
+
let { data } = file
|
|
866
|
+
if (!data) {
|
|
867
|
+
const { url } = file
|
|
868
|
+
if (!storage.isImportSourceAllowed(url)) {
|
|
869
|
+
throw new AssetError(
|
|
867
870
|
`Unable to import asset from foreign source: '${
|
|
868
871
|
file.name
|
|
869
872
|
}' ('${
|
|
870
873
|
url
|
|
871
874
|
}'): The source needs to be explicitly allowed.`
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
+
)
|
|
876
|
+
}
|
|
877
|
+
console.info(
|
|
875
878
|
`${
|
|
876
879
|
pico.red('INFO:')
|
|
877
880
|
} Asset ${
|
|
@@ -881,41 +884,40 @@ export class Application extends Koa {
|
|
|
881
884
|
} and adding to storage ${
|
|
882
885
|
pico.green(`'${storage.name}'`)
|
|
883
886
|
}...`
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}
|
|
887
|
+
)
|
|
888
|
+
if (url.startsWith('file://')) {
|
|
889
|
+
const filepath = path.resolve(url.substring(7))
|
|
890
|
+
data = await fs.readFile(filepath)
|
|
891
|
+
} else {
|
|
892
|
+
const response = await fetch(url)
|
|
893
|
+
const buffer = await response.arrayBuffer()
|
|
894
|
+
data = new DataView(buffer)
|
|
893
895
|
}
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
896
|
+
}
|
|
897
|
+
const importedFile = await storage.addFile(file, data)
|
|
898
|
+
await this.createAssets(storage, [importedFile], 0, trx)
|
|
899
|
+
// Merge back the changed file properties into the actual files
|
|
900
|
+
// object, so that the data from the static model hook can be used
|
|
901
|
+
// directly for the actual running query.
|
|
902
|
+
Object.assign(file, importedFile)
|
|
903
|
+
importedFiles.push(importedFile)
|
|
904
|
+
} else {
|
|
905
|
+
throw new AssetError(
|
|
903
906
|
`Unable to import asset from foreign source: '${
|
|
904
907
|
file.name
|
|
905
908
|
}' ('${
|
|
906
909
|
file.key
|
|
907
910
|
}')`
|
|
908
|
-
|
|
909
|
-
}
|
|
910
|
-
} else {
|
|
911
|
-
// Asset is from a foreign source, but was already imported and can
|
|
912
|
-
// be reused. See above for an explanation of this merge.
|
|
913
|
-
Object.assign(file, asset.file)
|
|
914
|
-
// NOTE: No need to add `file` to `importedFiles`, since it's
|
|
915
|
-
// already been imported to the storage before.
|
|
911
|
+
)
|
|
916
912
|
}
|
|
917
|
-
}
|
|
918
|
-
|
|
913
|
+
} else {
|
|
914
|
+
// Asset is from a foreign source, but was already imported and can
|
|
915
|
+
// be reused. See above for an explanation of this merge.
|
|
916
|
+
Object.assign(file, asset.file)
|
|
917
|
+
// NOTE: No need to add `file` to `importedFiles`, since it's
|
|
918
|
+
// already been imported to the storage before.
|
|
919
|
+
}
|
|
920
|
+
}, { concurrency: storage.concurrency })
|
|
919
921
|
}
|
|
920
922
|
return importedFiles
|
|
921
923
|
}
|
|
@@ -924,29 +926,27 @@ export class Application extends Koa {
|
|
|
924
926
|
const modifiedFiles = []
|
|
925
927
|
const AssetModel = this.getModel('Asset')
|
|
926
928
|
if (AssetModel) {
|
|
927
|
-
await
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
throw new AssetError(
|
|
929
|
+
await mapConcurrently(files, async file => {
|
|
930
|
+
if (file.data) {
|
|
931
|
+
const asset = await AssetModel.query(trx).findOne('key', file.key)
|
|
932
|
+
if (asset) {
|
|
933
|
+
const changedFile = await storage.addFile(file, file.data)
|
|
934
|
+
// Merge back the changed file properties into the actual files
|
|
935
|
+
// object, so that the data from the static model hook can be used
|
|
936
|
+
// directly for the actual running query.
|
|
937
|
+
Object.assign(file, changedFile)
|
|
938
|
+
modifiedFiles.push(changedFile)
|
|
939
|
+
} else {
|
|
940
|
+
throw new AssetError(
|
|
940
941
|
`Unable to update modified asset from memory source: '${
|
|
941
942
|
file.name
|
|
942
943
|
}' ('${
|
|
943
944
|
file.key
|
|
944
945
|
}')`
|
|
945
|
-
|
|
946
|
-
}
|
|
946
|
+
)
|
|
947
947
|
}
|
|
948
|
-
}
|
|
949
|
-
)
|
|
948
|
+
}
|
|
949
|
+
})
|
|
950
950
|
}
|
|
951
951
|
return modifiedFiles
|
|
952
952
|
}
|
|
@@ -967,8 +967,9 @@ export class Application extends Koa {
|
|
|
967
967
|
// .e.g. when `config.assets.cleanupTimeThreshold = 0`
|
|
968
968
|
.andWhere('updatedAt', '>', ref('createdAt'))
|
|
969
969
|
if (orphanedAssets.length > 0) {
|
|
970
|
-
const orphanedKeys = await
|
|
971
|
-
orphanedAssets
|
|
970
|
+
const orphanedKeys = await mapConcurrently(
|
|
971
|
+
orphanedAssets,
|
|
972
|
+
async asset => {
|
|
972
973
|
try {
|
|
973
974
|
await this.getStorage(asset.storage).removeFile(asset.file)
|
|
974
975
|
} catch (error) {
|
|
@@ -976,7 +977,7 @@ export class Application extends Koa {
|
|
|
976
977
|
asset.error = error
|
|
977
978
|
}
|
|
978
979
|
return asset.key
|
|
979
|
-
}
|
|
980
|
+
}
|
|
980
981
|
)
|
|
981
982
|
await AssetModel
|
|
982
983
|
.query(trx)
|
|
@@ -29,7 +29,7 @@ export class AdminController extends Controller {
|
|
|
29
29
|
}
|
|
30
30
|
// If no mode is specified, use `production` since that's just the hosting
|
|
31
31
|
// of the pre-built admin files. `development` serves the admin directly
|
|
32
|
-
// sources with HRM, and thus should be
|
|
32
|
+
// sources with HRM, and thus should be explicitly activated.
|
|
33
33
|
this.mode = this.config.mode || (
|
|
34
34
|
this.app.config.env === 'development' ? 'development' : 'production'
|
|
35
35
|
)
|
|
@@ -36,7 +36,7 @@ export class CollectionController extends Controller {
|
|
|
36
36
|
this.assets = modelClass.definition.assets || null
|
|
37
37
|
} else if (isObject(this.assets)) {
|
|
38
38
|
// Merge in the assets definition from the model into the assets config.
|
|
39
|
-
// That way, we can still use `allow` and `authorize` to
|
|
39
|
+
// That way, we can still use `allow` and `authorize` to control the
|
|
40
40
|
// upload access, while keeping the assets definitions in one central
|
|
41
41
|
// location on the model.
|
|
42
42
|
this.assets = {
|
|
@@ -228,14 +228,10 @@ export class Controller {
|
|
|
228
228
|
const tokens = parseDataPath(dataPath)
|
|
229
229
|
const getDataPath = callback => normalizeDataPath(tokens.map(callback))
|
|
230
230
|
|
|
231
|
-
// Replace wildcards with numbered indices and convert to '/'-notation:
|
|
232
|
-
let index = 0
|
|
233
|
-
const multipleWildcards = tokens.filter(token => token === '*').length > 1
|
|
234
231
|
const normalizedPath = getDataPath(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
: ':index'
|
|
232
|
+
// Router supports both shallow & deep wildcards, no normalization needed.
|
|
233
|
+
token => token === '*' || token === '**'
|
|
234
|
+
? token
|
|
239
235
|
: this.app.normalizePath(token)
|
|
240
236
|
)
|
|
241
237
|
|
|
@@ -243,7 +239,16 @@ export class Controller {
|
|
|
243
239
|
// against, but convert wildcards (*) to match both numeric ids and words,
|
|
244
240
|
// e.g. 'create':
|
|
245
241
|
const matchDataPath = new RegExp(
|
|
246
|
-
`^${
|
|
242
|
+
`^${
|
|
243
|
+
getDataPath(
|
|
244
|
+
// Use the exact same regexps as in `Router`:
|
|
245
|
+
token => token === '*'
|
|
246
|
+
? '[^/]+' // shallow wildcard
|
|
247
|
+
: token === '**'
|
|
248
|
+
? '.+?' // deep wildcard
|
|
249
|
+
: token
|
|
250
|
+
)
|
|
251
|
+
}$`
|
|
247
252
|
)
|
|
248
253
|
|
|
249
254
|
const url = this.getUrl('controller', `upload/${normalizedPath}`)
|
|
@@ -557,7 +562,7 @@ function convertActionObject(name, object, actions) {
|
|
|
557
562
|
...rest
|
|
558
563
|
} = object
|
|
559
564
|
|
|
560
|
-
// In order to
|
|
565
|
+
// In order to support `super` calls in the `handler` function in object
|
|
561
566
|
// notation, deploy this crazy JS sorcery:
|
|
562
567
|
Object.setPrototypeOf(object, Object.getPrototypeOf(actions))
|
|
563
568
|
|
|
@@ -123,7 +123,7 @@ export default class ControllerAction {
|
|
|
123
123
|
type, // String: What type should this validated against / coerced to.
|
|
124
124
|
from, // String: Allow parameters to be 'borrowed' from other objects.
|
|
125
125
|
root, // Boolean: Use full root object, instead of data at given property.
|
|
126
|
-
member // Boolean: Fetch member instance
|
|
126
|
+
member // Boolean: Fetch member instance instead of data from request.
|
|
127
127
|
} of this.parameters.list) {
|
|
128
128
|
// Don't validate member parameters as they get resolved separately after.
|
|
129
129
|
if (member) continue
|
|
@@ -271,7 +271,7 @@ export default class ControllerAction {
|
|
|
271
271
|
// - `"key1":X, "key2":Y` (curly braces are added and parsed through
|
|
272
272
|
// `JSON.parse()`)
|
|
273
273
|
// - `key1:X,key2:Y` (a simple parser is applied, splitting into
|
|
274
|
-
// entries and key/value pairs,
|
|
274
|
+
// entries and key/value pairs, values are parsed with
|
|
275
275
|
// `JSON.parse()`, falling back to string.
|
|
276
276
|
if (/"/.test(value)) {
|
|
277
277
|
// Just add the curly braces and parse as JSON
|
|
@@ -10,12 +10,12 @@ export default class MemberAction extends ControllerAction {
|
|
|
10
10
|
// @override
|
|
11
11
|
async getMember(ctx, param) {
|
|
12
12
|
// member parameters can provide special query parameters as well,
|
|
13
|
-
// and they can even
|
|
13
|
+
// and they can even control `forUpdate()` behavior:
|
|
14
14
|
// {
|
|
15
15
|
// member: true,
|
|
16
16
|
// query: { ... },
|
|
17
17
|
// forUpdate: true,
|
|
18
|
-
// modify: query => query.
|
|
18
|
+
// modify: query => query.debug()
|
|
19
19
|
// }
|
|
20
20
|
// These are passed on to and handled in `CollectionController#getMember()`.
|
|
21
21
|
// For handling of `member: true` and calling of `MemberAction.getMember()`,
|
|
@@ -44,7 +44,7 @@ export class ModelController extends CollectionController {
|
|
|
44
44
|
this, object, relationInstance, relationDefinition
|
|
45
45
|
)
|
|
46
46
|
// RelationController instances are not registered with the app, but are
|
|
47
|
-
//
|
|
47
|
+
// managed by their parent controller instead.
|
|
48
48
|
relation.configure()
|
|
49
49
|
relation.setup()
|
|
50
50
|
return relation
|
package/src/graph/graph.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isObject, isArray, asArray } from '@ditojs/utils'
|
|
1
|
+
import { isObject, isArray, asArray, mapConcurrently } from '@ditojs/utils'
|
|
2
2
|
import { QueryBuilder } from '../query/index.js'
|
|
3
3
|
import { collectExpressionPaths, expressionPathToString } from './expression.js'
|
|
4
4
|
|
|
@@ -212,8 +212,9 @@ export async function populateGraph(rootModelClass, graph, expr, trx) {
|
|
|
212
212
|
// Load all found models by ids asynchronously, within provided transaction.
|
|
213
213
|
// NOTE: Using the same transaction means that all involved tables need to
|
|
214
214
|
// be in the same database.
|
|
215
|
-
await
|
|
216
|
-
groups
|
|
215
|
+
await mapConcurrently(
|
|
216
|
+
groups,
|
|
217
|
+
async ({ modelClass, modify, expr, ids, modelsById }) => {
|
|
217
218
|
const query = modelClass.query(trx).findByIds(ids).modify(modify)
|
|
218
219
|
if (expr) {
|
|
219
220
|
// TODO: Make algorithm configurable through options.
|
|
@@ -224,7 +225,7 @@ export async function populateGraph(rootModelClass, graph, expr, trx) {
|
|
|
224
225
|
for (const model of models) {
|
|
225
226
|
modelsById[model.$id()] = model
|
|
226
227
|
}
|
|
227
|
-
}
|
|
228
|
+
}
|
|
228
229
|
)
|
|
229
230
|
|
|
230
231
|
// Finally populate the targets with the loaded models.
|
|
@@ -40,7 +40,7 @@ export function handleConnectMiddleware(middleware, {
|
|
|
40
40
|
if (isArray(headers)) {
|
|
41
41
|
// Convert raw headers array to object.
|
|
42
42
|
headers = Object.fromEntries(headers.reduce(
|
|
43
|
-
// Translate raw array to [field, value]
|
|
43
|
+
// Translate raw array to [field, value] tuples.
|
|
44
44
|
(entries, value, index) => {
|
|
45
45
|
if (index & 1) { // Odd: value
|
|
46
46
|
entries[entries.length - 1].push(value)
|
package/src/mixins/AssetMixin.js
CHANGED
|
@@ -36,7 +36,7 @@ export const AssetMixin = mixin(Model => class extends TimeStampedMixin(Model) {
|
|
|
36
36
|
type: 'integer',
|
|
37
37
|
required: true
|
|
38
38
|
},
|
|
39
|
-
// Use for storages configured for files to be
|
|
39
|
+
// Use for storages configured for files to be publicly accessible:
|
|
40
40
|
url: {
|
|
41
41
|
type: 'string'
|
|
42
42
|
},
|
package/src/models/Model.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import objection from 'objection'
|
|
2
2
|
import {
|
|
3
3
|
isString, isObject, isArray, isFunction, isPromise, asArray, merge, flatten,
|
|
4
|
-
parseDataPath, normalizeDataPath, getValueAtDataPath
|
|
4
|
+
parseDataPath, normalizeDataPath, getValueAtDataPath,
|
|
5
|
+
mapConcurrently
|
|
5
6
|
} from '@ditojs/utils'
|
|
6
7
|
import { QueryBuilder } from '../query/index.js'
|
|
7
8
|
import { EventEmitter, KnexHelper } from '../lib/index.js'
|
|
@@ -617,9 +618,13 @@ export class Model extends objection.Model {
|
|
|
617
618
|
const parsedDataPath = parseDataPath(dataPath)
|
|
618
619
|
let index = 0
|
|
619
620
|
|
|
620
|
-
const getResult = (
|
|
621
|
-
|
|
622
|
-
|
|
621
|
+
const getResult = ({
|
|
622
|
+
property = null,
|
|
623
|
+
relation = null,
|
|
624
|
+
wildcard = null
|
|
625
|
+
} = {}) => {
|
|
626
|
+
const found = !!(property || relation || wildcard)
|
|
627
|
+
const name = wildcard ? '*' : parsedDataPath[index]
|
|
623
628
|
const next = index + 1
|
|
624
629
|
const dataPath = found
|
|
625
630
|
? normalizeDataPath(parsedDataPath.slice(0, next))
|
|
@@ -634,18 +639,20 @@ export class Model extends objection.Model {
|
|
|
634
639
|
return {
|
|
635
640
|
property,
|
|
636
641
|
relation,
|
|
642
|
+
wildcard,
|
|
637
643
|
dataPath,
|
|
638
644
|
nestedDataPath,
|
|
639
645
|
name,
|
|
640
|
-
expression
|
|
641
|
-
index
|
|
646
|
+
expression
|
|
642
647
|
}
|
|
643
648
|
}
|
|
644
649
|
|
|
645
650
|
const [firstToken, ...otherTokens] = parsedDataPath
|
|
646
651
|
const property = this.definition.properties[firstToken]
|
|
647
652
|
if (property) {
|
|
648
|
-
return getResult(property)
|
|
653
|
+
return getResult({ property })
|
|
654
|
+
} else if (firstToken === '*' || firstToken === '**') {
|
|
655
|
+
return getResult({ wildcard: firstToken })
|
|
649
656
|
} else {
|
|
650
657
|
let relation = this.getRelations()[firstToken]
|
|
651
658
|
if (relation) {
|
|
@@ -654,27 +661,32 @@ export class Model extends objection.Model {
|
|
|
654
661
|
index++
|
|
655
662
|
const property = relatedModelClass.definition.properties[token]
|
|
656
663
|
if (property) {
|
|
657
|
-
return getResult(property)
|
|
664
|
+
return getResult({ property, relation })
|
|
658
665
|
} else if (token === '*') {
|
|
659
666
|
if (relation.isOneToOne()) {
|
|
660
667
|
// Do not support wildcards on one-to-one relations:
|
|
661
|
-
return getResult()
|
|
668
|
+
return getResult() // Not found.
|
|
662
669
|
} else {
|
|
663
670
|
continue
|
|
664
671
|
}
|
|
672
|
+
} else if (token === '**') {
|
|
673
|
+
throw new ModelError(
|
|
674
|
+
this,
|
|
675
|
+
'Deep wildcards on relations are unsupported.'
|
|
676
|
+
)
|
|
665
677
|
} else {
|
|
666
678
|
// Found a relation? Keep iterating.
|
|
667
679
|
relation = relatedModelClass.getRelations()[token]
|
|
668
680
|
if (relation) {
|
|
669
681
|
relatedModelClass = relation.relatedModelClass
|
|
670
682
|
} else {
|
|
671
|
-
return getResult()
|
|
683
|
+
return getResult() // Not found.
|
|
672
684
|
}
|
|
673
685
|
}
|
|
674
686
|
}
|
|
675
687
|
if (relation) {
|
|
676
688
|
// Still here? Found a relation at the end of the data-path.
|
|
677
|
-
return getResult(
|
|
689
|
+
return getResult({ relation })
|
|
678
690
|
}
|
|
679
691
|
}
|
|
680
692
|
}
|
|
@@ -704,6 +716,10 @@ export class Model extends objection.Model {
|
|
|
704
716
|
return query.ignoreScope(modifier.slice(1))
|
|
705
717
|
case '#': // Select column:
|
|
706
718
|
return query.select(modifier.slice(1))
|
|
719
|
+
case '*': // Select all columns:
|
|
720
|
+
if (modifier.length === 1) {
|
|
721
|
+
return query.select('*')
|
|
722
|
+
}
|
|
707
723
|
}
|
|
708
724
|
}
|
|
709
725
|
super.modifierNotFound(query, modifier)
|
|
@@ -958,10 +974,9 @@ export class Model extends objection.Model {
|
|
|
958
974
|
importedFiles.map(file => `'${file.name}'`)
|
|
959
975
|
}`
|
|
960
976
|
)
|
|
961
|
-
await
|
|
962
|
-
importedFiles
|
|
963
|
-
|
|
964
|
-
)
|
|
977
|
+
await mapConcurrently(
|
|
978
|
+
importedFiles,
|
|
979
|
+
file => file.storage.removeFile(file)
|
|
965
980
|
)
|
|
966
981
|
}
|
|
967
982
|
if (modifiedFiles.length > 0) {
|
|
@@ -48,7 +48,7 @@ export class QueryBuilder extends objection.QueryBuilder {
|
|
|
48
48
|
// to not mess with special selects such as `count`, etc:
|
|
49
49
|
this._ignoreGraph = !isNormalFind
|
|
50
50
|
// All scopes in `_scopes` were already checked against `_allowScopes`.
|
|
51
|
-
// They
|
|
51
|
+
// They themselves are allowed to apply / request other scopes that
|
|
52
52
|
// aren't listed, so clear `_applyScope` and restore again after:
|
|
53
53
|
const { _allowScopes } = this
|
|
54
54
|
this._allowScopes = null
|
|
@@ -347,16 +347,22 @@ export class QueryBuilder extends objection.QueryBuilder {
|
|
|
347
347
|
|
|
348
348
|
const {
|
|
349
349
|
property,
|
|
350
|
+
relation,
|
|
351
|
+
wildcard,
|
|
350
352
|
expression,
|
|
351
353
|
nestedDataPath,
|
|
352
|
-
name
|
|
353
|
-
index
|
|
354
|
+
name
|
|
354
355
|
} = this.modelClass().getPropertyOrRelationAtDataPath(parsedDataPath)
|
|
355
356
|
|
|
356
357
|
if (nestedDataPath) {
|
|
357
358
|
// Once a JSON data type is reached, even if it's not at the end of the
|
|
358
359
|
// provided path, load it and assume we're done with the loading part.
|
|
359
|
-
if (
|
|
360
|
+
if (
|
|
361
|
+
!(
|
|
362
|
+
wildcard ||
|
|
363
|
+
property && ['object', 'array'].includes(property.type)
|
|
364
|
+
)
|
|
365
|
+
) {
|
|
360
366
|
throw new QueryBuilderError(
|
|
361
367
|
`Unable to load full data-path '${
|
|
362
368
|
dataPath
|
|
@@ -369,7 +375,7 @@ export class QueryBuilder extends objection.QueryBuilder {
|
|
|
369
375
|
|
|
370
376
|
// Handle the special case of root-level property separately,
|
|
371
377
|
// because `withGraph('(#propertyName)')` is not supported.
|
|
372
|
-
if (
|
|
378
|
+
if (name && !relation) {
|
|
373
379
|
this.select(name)
|
|
374
380
|
} else {
|
|
375
381
|
this.withGraph(expression, options)
|
package/src/schema/properties.js
CHANGED
|
@@ -12,7 +12,7 @@ export function convertSchema(schema, options = {}) {
|
|
|
12
12
|
// Our 'required' is not the same as JSON Schema's: Use the 'required'
|
|
13
13
|
// format instead that only validates if the required value is not empty,
|
|
14
14
|
// meaning neither nullish nor an empty string. The JSON schema `required`
|
|
15
|
-
// array is generated
|
|
15
|
+
// array is generated separately below through `convertProperties()`.
|
|
16
16
|
delete schema.required
|
|
17
17
|
schema = addFormat(schema, 'required')
|
|
18
18
|
}
|
|
@@ -58,7 +58,7 @@ export function convertSchema(schema, options = {}) {
|
|
|
58
58
|
schema.type = jsonType
|
|
59
59
|
if (hasConvertedProperties && !('additionalProperties' in schema)) {
|
|
60
60
|
// Invert the logic of `additionalProperties` so that it needs to be
|
|
61
|
-
//
|
|
61
|
+
// explicitly set to `true`:
|
|
62
62
|
schema.additionalProperties = false
|
|
63
63
|
}
|
|
64
64
|
} else if (['date', 'datetime', 'timestamp'].includes(type)) {
|
|
@@ -69,7 +69,7 @@ describe('convertSchema()', () => {
|
|
|
69
69
|
})
|
|
70
70
|
})
|
|
71
71
|
|
|
72
|
-
it(`expands 'text'
|
|
72
|
+
it(`expands 'text' types to 'string' JSON schema types`, () => {
|
|
73
73
|
expect(convertSchema({
|
|
74
74
|
type: 'object',
|
|
75
75
|
properties: {
|
|
@@ -159,7 +159,7 @@ describe('convertSchema()', () => {
|
|
|
159
159
|
})
|
|
160
160
|
})
|
|
161
161
|
|
|
162
|
-
it('preserves
|
|
162
|
+
it('preserves pre-existing settings for no additional properties', () => {
|
|
163
163
|
expect(convertSchema({
|
|
164
164
|
type: 'object',
|
|
165
165
|
properties: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs/promises'
|
|
2
2
|
import path from 'path'
|
|
3
3
|
import multer from '@koa/multer'
|
|
4
|
+
import { mapConcurrently } from '@ditojs/utils'
|
|
4
5
|
import { Storage } from './Storage.js'
|
|
5
6
|
|
|
6
7
|
export class DiskStorage extends Storage {
|
|
@@ -82,12 +83,14 @@ export class DiskStorage extends Storage {
|
|
|
82
83
|
|
|
83
84
|
const files = []
|
|
84
85
|
const list1 = await readDir()
|
|
85
|
-
await
|
|
86
|
-
list1
|
|
86
|
+
await mapConcurrently(
|
|
87
|
+
list1,
|
|
88
|
+
async level1 => {
|
|
87
89
|
if (level1.isDirectory() && level1.name.length === 1) {
|
|
88
90
|
const list2 = await readDir(level1.name)
|
|
89
|
-
await
|
|
90
|
-
list2
|
|
91
|
+
await mapConcurrently(
|
|
92
|
+
list2,
|
|
93
|
+
async level2 => {
|
|
91
94
|
if (level2.isDirectory() && level2.name.length === 1) {
|
|
92
95
|
const nestedFolder = this._getPath(level1.name, level2.name)
|
|
93
96
|
for (const file of await fs.readdir(nestedFolder)) {
|
|
@@ -96,10 +99,10 @@ export class DiskStorage extends Storage {
|
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
}
|
|
102
|
+
}
|
|
100
103
|
)
|
|
101
104
|
}
|
|
102
|
-
}
|
|
105
|
+
}
|
|
103
106
|
)
|
|
104
107
|
return files
|
|
105
108
|
}
|
package/src/storage/S3Storage.js
CHANGED
|
@@ -47,6 +47,7 @@ export class S3Storage extends Storage {
|
|
|
47
47
|
let data = null
|
|
48
48
|
|
|
49
49
|
const done = type => {
|
|
50
|
+
stream.off('data', onData)
|
|
50
51
|
const outStream = new PassThrough()
|
|
51
52
|
outStream.write(data)
|
|
52
53
|
stream.pipe(outStream)
|
|
@@ -58,16 +59,13 @@ export class S3Storage extends Storage {
|
|
|
58
59
|
// 2. Try reading the mimetype from the first chunk.
|
|
59
60
|
const type = getFileTypeFromBuffer(chunk)
|
|
60
61
|
if (type) {
|
|
61
|
-
stream.off('data', onData)
|
|
62
62
|
done(type)
|
|
63
63
|
} else {
|
|
64
64
|
// 3. If that fails, keep collecting all chunks and determine
|
|
65
65
|
// the mimetype using the full data.
|
|
66
|
-
stream.once('end', () =>
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
)
|
|
70
|
-
})
|
|
66
|
+
stream.once('end', () => done(
|
|
67
|
+
getFileTypeFromBuffer(data) || 'application/octet-stream'
|
|
68
|
+
))
|
|
71
69
|
}
|
|
72
70
|
}
|
|
73
71
|
data = data ? Buffer.concat([data, chunk]) : chunk
|
package/src/storage/Storage.js
CHANGED
package/src/utils/emitter.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { mapConcurrently } from '@ditojs/utils'
|
|
2
|
+
|
|
1
3
|
export function emitAsync(emitter, event, ...args) {
|
|
2
|
-
return
|
|
3
|
-
emitter.listeners(event)
|
|
4
|
-
|
|
5
|
-
)
|
|
4
|
+
return mapConcurrently(
|
|
5
|
+
emitter.listeners(event),
|
|
6
|
+
listener => listener.call(emitter, ...args)
|
|
6
7
|
)
|
|
7
8
|
}
|