@ditojs/server 1.25.0 → 1.26.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 +5 -5
- package/src/app/Application.js +64 -50
- package/src/controllers/CollectionController.js +1 -0
- package/src/controllers/Controller.js +37 -19
- package/src/controllers/RelationController.js +5 -0
- package/src/services/Service.js +1 -1
- package/src/storage/DiskStorage.js +0 -1
- package/src/storage/S3Storage.js +4 -6
- package/src/storage/Storage.js +6 -4
- package/src/utils/date.js +15 -0
- package/src/utils/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ditojs/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.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.
|
|
25
|
+
"@ditojs/admin": "^1.26.0",
|
|
26
26
|
"@ditojs/build": "^1.25.0",
|
|
27
|
-
"@ditojs/router": "^1.
|
|
28
|
-
"@ditojs/utils": "^1.
|
|
27
|
+
"@ditojs/router": "^1.26.0",
|
|
28
|
+
"@ditojs/utils": "^1.26.0",
|
|
29
29
|
"@koa/cors": "^4.0.0",
|
|
30
30
|
"@koa/multer": "^3.0.2",
|
|
31
31
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"typescript": "^4.9.5"
|
|
91
91
|
},
|
|
92
92
|
"types": "types",
|
|
93
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "198c9832c863003c8c8fcd147bf61a5f99a39c91",
|
|
94
94
|
"scripts": {
|
|
95
95
|
"types": "tsc --noEmit ./src/index.d.ts"
|
|
96
96
|
},
|
package/src/app/Application.js
CHANGED
|
@@ -6,7 +6,6 @@ import Koa from 'koa'
|
|
|
6
6
|
import Knex from 'knex'
|
|
7
7
|
import pico from 'picocolors'
|
|
8
8
|
import pino from 'pino'
|
|
9
|
-
import parseDuration from 'parse-duration'
|
|
10
9
|
import bodyParser from 'koa-bodyparser'
|
|
11
10
|
import cors from '@koa/cors'
|
|
12
11
|
import compose from 'koa-compose'
|
|
@@ -21,9 +20,9 @@ import responseTime from 'koa-response-time'
|
|
|
21
20
|
import { Model, knexSnakeCaseMappers, ref } from 'objection'
|
|
22
21
|
import Router from '@ditojs/router'
|
|
23
22
|
import {
|
|
24
|
-
isArray, isObject,
|
|
25
|
-
hyphenate, clone, merge, parseDataPath, normalizeDataPath,
|
|
26
|
-
mapConcurrently
|
|
23
|
+
isArray, isObject, asArray, isPlainObject, isModule,
|
|
24
|
+
hyphenate, clone, merge, parseDataPath, normalizeDataPath,
|
|
25
|
+
toPromiseCallback, mapConcurrently
|
|
27
26
|
} from '@ditojs/utils'
|
|
28
27
|
import SessionStore from './SessionStore.js'
|
|
29
28
|
import { Validator } from './Validator.js'
|
|
@@ -32,7 +31,7 @@ import { Controller, AdminController } from '../controllers/index.js'
|
|
|
32
31
|
import { Service } from '../services/index.js'
|
|
33
32
|
import { Storage } from '../storage/index.js'
|
|
34
33
|
import { convertSchema } from '../schema/index.js'
|
|
35
|
-
import { deprecate } from '../utils/index.js'
|
|
34
|
+
import { getDuration, subtractDuration, deprecate } from '../utils/index.js'
|
|
36
35
|
import {
|
|
37
36
|
ResponseError,
|
|
38
37
|
ValidationError,
|
|
@@ -66,6 +65,7 @@ export class Application extends Koa {
|
|
|
66
65
|
// Pluck keys out of `config.app` to keep them secret
|
|
67
66
|
app: { keys, ...app } = {},
|
|
68
67
|
log,
|
|
68
|
+
assets,
|
|
69
69
|
logger,
|
|
70
70
|
...rest
|
|
71
71
|
} = config
|
|
@@ -74,6 +74,7 @@ export class Application extends Koa {
|
|
|
74
74
|
log: log === false || log?.silent || process.env.DITO_SILENT
|
|
75
75
|
? {}
|
|
76
76
|
: getOptions(log),
|
|
77
|
+
assets: merge(defaultAssetOptions, getOptions(assets)),
|
|
77
78
|
logger: merge(defaultLoggerOptions, getOptions(logger)),
|
|
78
79
|
...rest
|
|
79
80
|
}
|
|
@@ -805,16 +806,6 @@ export class Application extends Koa {
|
|
|
805
806
|
removedFiles,
|
|
806
807
|
trx = null
|
|
807
808
|
) {
|
|
808
|
-
const {
|
|
809
|
-
assets: {
|
|
810
|
-
cleanupTimeThreshold = 0
|
|
811
|
-
} = {}
|
|
812
|
-
} = this.config
|
|
813
|
-
// Only remove unused assets that haven't seen changes for given time frame.
|
|
814
|
-
const timeThreshold = isString(cleanupTimeThreshold)
|
|
815
|
-
? parseDuration(cleanupTimeThreshold)
|
|
816
|
-
: cleanupTimeThreshold
|
|
817
|
-
|
|
818
809
|
let importedFiles = []
|
|
819
810
|
const AssetModel = this.getModel('Asset')
|
|
820
811
|
if (AssetModel) {
|
|
@@ -837,18 +828,20 @@ export class Application extends Koa {
|
|
|
837
828
|
changeCount(addedFiles, 1),
|
|
838
829
|
changeCount(removedFiles, -1)
|
|
839
830
|
])
|
|
840
|
-
|
|
831
|
+
const cleanupTimeThreshold =
|
|
832
|
+
getDuration(this.config.assets.cleanupTimeThreshold)
|
|
833
|
+
if (cleanupTimeThreshold > 0) {
|
|
841
834
|
setTimeout(
|
|
842
835
|
// Don't pass `trx` here, as we want this delayed execution to
|
|
843
836
|
// create its own transaction.
|
|
844
|
-
() => this.releaseUnusedAssets(
|
|
845
|
-
|
|
837
|
+
() => this.releaseUnusedAssets(),
|
|
838
|
+
cleanupTimeThreshold
|
|
846
839
|
)
|
|
847
840
|
}
|
|
848
841
|
}
|
|
849
842
|
// Also execute releaseUnusedAssets() immediately in the same
|
|
850
843
|
// transaction, to potentially clean up other pending assets.
|
|
851
|
-
await this.releaseUnusedAssets(
|
|
844
|
+
await this.releaseUnusedAssets(null, trx)
|
|
852
845
|
return importedFiles
|
|
853
846
|
}
|
|
854
847
|
}
|
|
@@ -867,31 +860,28 @@ export class Application extends Koa {
|
|
|
867
860
|
const { url } = file
|
|
868
861
|
if (!storage.isImportSourceAllowed(url)) {
|
|
869
862
|
throw new AssetError(
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
863
|
+
`Unable to import asset from foreign source: '${
|
|
864
|
+
file.name
|
|
865
|
+
}' ('${
|
|
866
|
+
url
|
|
867
|
+
}'): The source needs to be explicitly allowed.`
|
|
875
868
|
)
|
|
876
869
|
}
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
pico.green(`'${storage.name}'`)
|
|
886
|
-
}...`
|
|
870
|
+
this.logger.info(
|
|
871
|
+
`Asset ${
|
|
872
|
+
pico.green(`'${file.name}'`)
|
|
873
|
+
} is from a foreign source, fetching from ${
|
|
874
|
+
pico.green(`'${url}'`)
|
|
875
|
+
} and adding to storage ${
|
|
876
|
+
pico.green(`'${storage.name}'`)
|
|
877
|
+
}...`
|
|
887
878
|
)
|
|
888
879
|
if (url.startsWith('file://')) {
|
|
889
880
|
const filepath = path.resolve(url.substring(7))
|
|
890
881
|
data = await fs.readFile(filepath)
|
|
891
882
|
} else {
|
|
892
883
|
const response = await fetch(url)
|
|
893
|
-
|
|
894
|
-
data = new DataView(buffer)
|
|
884
|
+
data = await response.arrayBuffer()
|
|
895
885
|
}
|
|
896
886
|
}
|
|
897
887
|
const importedFile = await storage.addFile(file, data)
|
|
@@ -903,11 +893,11 @@ export class Application extends Koa {
|
|
|
903
893
|
importedFiles.push(importedFile)
|
|
904
894
|
} else {
|
|
905
895
|
throw new AssetError(
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
896
|
+
`Unable to import asset from foreign source: '${
|
|
897
|
+
file.name
|
|
898
|
+
}' ('${
|
|
899
|
+
file.key
|
|
900
|
+
}')`
|
|
911
901
|
)
|
|
912
902
|
}
|
|
913
903
|
} else {
|
|
@@ -951,21 +941,34 @@ export class Application extends Koa {
|
|
|
951
941
|
return modifiedFiles
|
|
952
942
|
}
|
|
953
943
|
|
|
954
|
-
async releaseUnusedAssets(timeThreshold =
|
|
944
|
+
async releaseUnusedAssets(timeThreshold = null, trx = null) {
|
|
955
945
|
const AssetModel = this.getModel('Asset')
|
|
956
946
|
if (AssetModel) {
|
|
947
|
+
const { assets } = this.config
|
|
948
|
+
const cleanupTimeThreshold =
|
|
949
|
+
getDuration(timeThreshold ?? assets.cleanupTimeThreshold)
|
|
950
|
+
const danglingTimeThreshold =
|
|
951
|
+
getDuration(timeThreshold ?? assets.danglingTimeThreshold)
|
|
957
952
|
return AssetModel.transaction(trx, async trx => {
|
|
958
|
-
//
|
|
959
|
-
//
|
|
960
|
-
const
|
|
961
|
-
|
|
953
|
+
// Calculate the date math in JS instead of SQL, as there is no easy
|
|
954
|
+
// cross-SQL way to do `now() - interval X hours`:
|
|
955
|
+
const now = new Date()
|
|
956
|
+
const cleanupDate = subtractDuration(now, cleanupTimeThreshold)
|
|
957
|
+
const danglingDate = subtractDuration(now, danglingTimeThreshold)
|
|
962
958
|
const orphanedAssets = await AssetModel
|
|
963
959
|
.query(trx)
|
|
964
960
|
.where('count', 0)
|
|
965
|
-
.andWhere(
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
961
|
+
.andWhere(
|
|
962
|
+
query => query
|
|
963
|
+
.where('updatedAt', '<=', cleanupDate)
|
|
964
|
+
.orWhere(
|
|
965
|
+
// Protect freshly created assets from being deleted again right
|
|
966
|
+
// away, .e.g. when `config.assets.cleanupTimeThreshold = 0`
|
|
967
|
+
query => query
|
|
968
|
+
.where('updatedAt', '=', ref('createdAt'))
|
|
969
|
+
.andWhere('updatedAt', '<=', danglingDate)
|
|
970
|
+
)
|
|
971
|
+
)
|
|
969
972
|
if (orphanedAssets.length > 0) {
|
|
970
973
|
const orphanedKeys = await mapConcurrently(
|
|
971
974
|
orphanedAssets,
|
|
@@ -998,6 +1001,17 @@ function getOptions(options) {
|
|
|
998
1001
|
return isObject(options) ? options : {}
|
|
999
1002
|
}
|
|
1000
1003
|
|
|
1004
|
+
const defaultAssetOptions = {
|
|
1005
|
+
// Only remove unused or dangling assets that haven't seen changes for
|
|
1006
|
+
// these given time frames. Set to `0` to clean up instantly.
|
|
1007
|
+
cleanupTimeThreshold: '24h',
|
|
1008
|
+
// Dangling assets are those that got uploaded but never actually persisted in
|
|
1009
|
+
// the model. This can happen when the admin uploads a file but doesn't store
|
|
1010
|
+
// the associated form. This cannot be set to 0 or else the the file would be
|
|
1011
|
+
// deleted immediately after upload.
|
|
1012
|
+
danglingTimeThreshold: '24h'
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1001
1015
|
const { err, req, res } = pino.stdSerializers
|
|
1002
1016
|
const defaultLoggerOptions = {
|
|
1003
1017
|
level: 'info',
|
|
@@ -24,6 +24,7 @@ export class CollectionController extends Controller {
|
|
|
24
24
|
|
|
25
25
|
// @override
|
|
26
26
|
setup() {
|
|
27
|
+
this.logController()
|
|
27
28
|
this.collection = this.setupActions('collection')
|
|
28
29
|
this.member = this.isOneToOne ? {} : this.setupActions('member')
|
|
29
30
|
this.assets = this.setupAssets()
|
|
@@ -26,7 +26,7 @@ export class Controller {
|
|
|
26
26
|
constructor(app, namespace) {
|
|
27
27
|
this.app = app
|
|
28
28
|
this.namespace = namespace
|
|
29
|
-
this.
|
|
29
|
+
this.logRoutes = this.app.config.log.routes
|
|
30
30
|
this.level = 0
|
|
31
31
|
}
|
|
32
32
|
|
|
@@ -60,21 +60,12 @@ export class Controller {
|
|
|
60
60
|
// mapped parameters or wildcards. Consider `path` / `route` instead?
|
|
61
61
|
const url = path ? `/${path}` : ''
|
|
62
62
|
this.url = namespace ? `/${namespace}${url}` : url
|
|
63
|
-
this.log(
|
|
64
|
-
`${
|
|
65
|
-
namespace ? pico.green(`/${namespace}/`) : ''
|
|
66
|
-
}${
|
|
67
|
-
pico.cyan(path)
|
|
68
|
-
}${
|
|
69
|
-
pico.white(':')
|
|
70
|
-
}`,
|
|
71
|
-
this.level
|
|
72
|
-
)
|
|
73
63
|
}
|
|
74
64
|
}
|
|
75
65
|
|
|
76
66
|
// @overridable
|
|
77
67
|
setup() {
|
|
68
|
+
this.logController()
|
|
78
69
|
this.actions ||= this.reflectActionsObject()
|
|
79
70
|
// Now that the instance fields are reflected in the `controller` object
|
|
80
71
|
// we can use the normal inheritance mechanism through `setupActions()`:
|
|
@@ -94,6 +85,40 @@ export class Controller {
|
|
|
94
85
|
// middleware. For normal routes, use `this.app.addRoute()` instead.
|
|
95
86
|
}
|
|
96
87
|
|
|
88
|
+
// @overridable
|
|
89
|
+
logController() {
|
|
90
|
+
const { path, namespace } = this
|
|
91
|
+
this.logRoute(
|
|
92
|
+
`${
|
|
93
|
+
namespace ? pico.green(`/${namespace}/`) : ''
|
|
94
|
+
}${
|
|
95
|
+
pico.cyan(path)
|
|
96
|
+
}${
|
|
97
|
+
pico.white(':')
|
|
98
|
+
}`,
|
|
99
|
+
this.level
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
logRoute(str, indent = 0) {
|
|
104
|
+
if (this.logRoutes) {
|
|
105
|
+
console.info(`${' '.repeat(indent)}${str}`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Only use this method to get a logger instance that is bound to the context,
|
|
110
|
+
// otherwise use the cached getter.
|
|
111
|
+
getLogger(ctx) {
|
|
112
|
+
const logger = ctx?.logger ?? this.app.logger
|
|
113
|
+
return logger.child({ name: this.name })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
get logger() {
|
|
117
|
+
const value = this.getLogger()
|
|
118
|
+
Object.defineProperty(this, 'logger', { value })
|
|
119
|
+
return value
|
|
120
|
+
}
|
|
121
|
+
|
|
97
122
|
reflectActionsObject() {
|
|
98
123
|
// On base controllers, the actions can be defined directly in the class
|
|
99
124
|
// instead of inside an actions object, as is done with model and relation
|
|
@@ -122,7 +147,7 @@ export class Controller {
|
|
|
122
147
|
}
|
|
123
148
|
|
|
124
149
|
setupRoute(method, url, transacted, authorize, action, middlewares) {
|
|
125
|
-
this.
|
|
150
|
+
this.logRoute(
|
|
126
151
|
`${
|
|
127
152
|
pico.magenta(method.toUpperCase())
|
|
128
153
|
} ${
|
|
@@ -382,7 +407,6 @@ export class Controller {
|
|
|
382
407
|
// so add its own keys to the already allowed inherited keys so far.
|
|
383
408
|
Object.assign(allowMap, getFilteredMap(getOwnKeys(current)))
|
|
384
409
|
}
|
|
385
|
-
// console.log('allow', Object.keys(allowMap))
|
|
386
410
|
}
|
|
387
411
|
|
|
388
412
|
const handleAuthorize = authorize => {
|
|
@@ -538,12 +562,6 @@ export class Controller {
|
|
|
538
562
|
throw new AuthorizationError()
|
|
539
563
|
}
|
|
540
564
|
}
|
|
541
|
-
|
|
542
|
-
log(str, indent = 0) {
|
|
543
|
-
if (this.logging) {
|
|
544
|
-
console.info(`${' '.repeat(indent)}${str}`)
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
565
|
}
|
|
548
566
|
|
|
549
567
|
EventEmitter.mixin(Controller.prototype)
|
|
@@ -48,6 +48,11 @@ export class RelationController extends CollectionController {
|
|
|
48
48
|
super.configure()
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// @override
|
|
52
|
+
logController() {
|
|
53
|
+
// The parent controller logs itself already, that's enough.
|
|
54
|
+
}
|
|
55
|
+
|
|
51
56
|
// @override
|
|
52
57
|
inheritValues(type) {
|
|
53
58
|
// Since RelationController are mapped to nested `relations` objects in
|
package/src/services/Service.js
CHANGED
|
@@ -32,7 +32,7 @@ export class Service {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
// Only use this method to get a logger instance that is bound to the context,
|
|
35
|
-
// otherwise use the getter.
|
|
35
|
+
// otherwise use the cached getter.
|
|
36
36
|
getLogger(ctx) {
|
|
37
37
|
const logger = ctx?.logger ?? this.app.logger
|
|
38
38
|
return logger.child({ name: this.#loggerName })
|
package/src/storage/S3Storage.js
CHANGED
|
@@ -111,12 +111,10 @@ export class S3Storage extends Storage {
|
|
|
111
111
|
ContentType: file.type,
|
|
112
112
|
Body: data
|
|
113
113
|
})
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
location: result.Location
|
|
119
|
-
}
|
|
114
|
+
// In `Storage.addFile()` this will get overridden with the result of
|
|
115
|
+
// `_getUrl()` if it exists, but is used as a fallback otherwise,
|
|
116
|
+
// see `_getFileUrl()`.
|
|
117
|
+
file.url = result.Location
|
|
120
118
|
}
|
|
121
119
|
|
|
122
120
|
// @override
|
package/src/storage/Storage.js
CHANGED
|
@@ -18,7 +18,9 @@ export class Storage {
|
|
|
18
18
|
this.name = config.name
|
|
19
19
|
this.url = config.url
|
|
20
20
|
this.path = config.path
|
|
21
|
-
|
|
21
|
+
// Use a default concurrency of 8 for storage IO, e.g. the importing of
|
|
22
|
+
// foreign assets.
|
|
23
|
+
this.concurrency = config.concurrency ?? 8
|
|
22
24
|
// The actual multer storage object.
|
|
23
25
|
this.storage = null
|
|
24
26
|
}
|
|
@@ -96,10 +98,10 @@ export class Storage {
|
|
|
96
98
|
}
|
|
97
99
|
|
|
98
100
|
async addFile(file, data) {
|
|
99
|
-
|
|
101
|
+
await this._addFile(file, data)
|
|
100
102
|
file.size = Buffer.byteLength(data)
|
|
101
|
-
file.url = this._getFileUrl(
|
|
102
|
-
// TODO: Support `config.readImageSize`, but this can only be done
|
|
103
|
+
file.url = this._getFileUrl(file)
|
|
104
|
+
// TODO: Support `config.readImageSize`, but this can only be done once
|
|
103
105
|
// there are separate storage instances per model assets config!
|
|
104
106
|
return this.convertAssetFile(file)
|
|
105
107
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { isNumber } from '@ditojs/utils'
|
|
2
|
+
import parseDuration from 'parse-duration'
|
|
3
|
+
|
|
4
|
+
export function getDuration(duration) {
|
|
5
|
+
return isNumber(duration) ? duration : parseDuration(duration)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function addDuration(date, duration) {
|
|
9
|
+
date.setMilliseconds(date.getMilliseconds() + getDuration(duration))
|
|
10
|
+
return date
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function subtractDuration(date, duration) {
|
|
14
|
+
return addDuration(date, -getDuration(duration))
|
|
15
|
+
}
|
package/src/utils/index.js
CHANGED