@ditojs/server 2.0.5 → 2.1.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 +11 -13
- package/src/app/Application.js +226 -179
- package/src/app/Validator.js +53 -43
- package/src/cli/console.js +6 -4
- package/src/cli/db/createMigration.js +59 -30
- package/src/cli/db/migrate.js +6 -4
- package/src/cli/db/reset.js +8 -5
- package/src/cli/db/rollback.js +6 -4
- package/src/cli/db/seed.js +2 -1
- package/src/cli/index.js +1 -1
- package/src/controllers/AdminController.js +98 -84
- package/src/controllers/CollectionController.js +37 -30
- package/src/controllers/Controller.js +83 -43
- package/src/controllers/ControllerAction.js +27 -15
- package/src/controllers/ModelController.js +4 -1
- package/src/controllers/RelationController.js +19 -21
- package/src/controllers/UsersController.js +3 -4
- package/src/decorators/parameters.js +3 -1
- package/src/decorators/scope.js +1 -1
- package/src/errors/ControllerError.js +2 -1
- package/src/errors/DatabaseError.js +20 -11
- package/src/graph/DitoGraphProcessor.js +48 -40
- package/src/graph/expression.js +6 -8
- package/src/graph/graph.js +20 -11
- package/src/lib/EventEmitter.js +12 -12
- package/src/middleware/handleConnectMiddleware.js +16 -10
- package/src/middleware/handleError.js +6 -5
- package/src/middleware/handleSession.js +33 -29
- package/src/middleware/handleUser.js +2 -2
- package/src/middleware/index.js +1 -0
- package/src/middleware/logRequests.js +3 -3
- package/src/middleware/setupRequestStorage.js +14 -0
- package/src/mixins/AssetMixin.js +62 -58
- package/src/mixins/SessionMixin.js +13 -10
- package/src/mixins/TimeStampedMixin.js +33 -29
- package/src/mixins/UserMixin.js +130 -116
- package/src/models/Model.js +245 -194
- package/src/models/definitions/filters.js +14 -13
- package/src/query/QueryBuilder.js +252 -195
- package/src/query/QueryFilters.js +3 -3
- package/src/query/QueryParameters.js +2 -2
- package/src/query/Registry.js +8 -10
- package/src/schema/keywords/_validate.js +10 -8
- package/src/schema/properties.test.js +247 -206
- package/src/schema/relations.js +42 -20
- package/src/schema/relations.test.js +36 -19
- package/src/services/Service.js +8 -14
- package/src/storage/S3Storage.js +5 -3
- package/src/storage/Storage.js +16 -14
- package/src/utils/function.js +7 -4
- package/src/utils/function.test.js +30 -6
- package/src/utils/object.test.js +5 -1
- package/types/index.d.ts +244 -257
package/src/app/Application.js
CHANGED
|
@@ -19,9 +19,18 @@ import responseTime from 'koa-response-time'
|
|
|
19
19
|
import { Model, knexSnakeCaseMappers, ref } from 'objection'
|
|
20
20
|
import Router from '@ditojs/router'
|
|
21
21
|
import {
|
|
22
|
-
isArray,
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
isArray,
|
|
23
|
+
isObject,
|
|
24
|
+
asArray,
|
|
25
|
+
isPlainObject,
|
|
26
|
+
isModule,
|
|
27
|
+
hyphenate,
|
|
28
|
+
clone,
|
|
29
|
+
merge,
|
|
30
|
+
parseDataPath,
|
|
31
|
+
normalizeDataPath,
|
|
32
|
+
toPromiseCallback,
|
|
33
|
+
mapConcurrently
|
|
25
34
|
} from '@ditojs/utils'
|
|
26
35
|
import { Validator } from './Validator.js'
|
|
27
36
|
import { EventEmitter } from '../lib/index.js'
|
|
@@ -45,8 +54,10 @@ import {
|
|
|
45
54
|
handleRoute,
|
|
46
55
|
handleSession,
|
|
47
56
|
handleUser,
|
|
48
|
-
logRequests
|
|
57
|
+
logRequests,
|
|
58
|
+
setupRequestStorage
|
|
49
59
|
} from '../middleware/index.js'
|
|
60
|
+
import { AsyncLocalStorage } from 'async_hooks'
|
|
50
61
|
|
|
51
62
|
export class Application extends Koa {
|
|
52
63
|
constructor({
|
|
@@ -71,9 +82,10 @@ export class Application extends Koa {
|
|
|
71
82
|
} = config
|
|
72
83
|
this.config = {
|
|
73
84
|
app,
|
|
74
|
-
log:
|
|
75
|
-
|
|
76
|
-
|
|
85
|
+
log:
|
|
86
|
+
log === false || log?.silent || process.env.DITO_SILENT
|
|
87
|
+
? {}
|
|
88
|
+
: getOptions(log),
|
|
77
89
|
assets: merge(defaultAssetOptions, getOptions(assets)),
|
|
78
90
|
logger: merge(defaultLoggerOptions, getOptions(logger)),
|
|
79
91
|
...rest
|
|
@@ -89,6 +101,7 @@ export class Application extends Koa {
|
|
|
89
101
|
this.controllers = Object.create(null)
|
|
90
102
|
this.server = null
|
|
91
103
|
this.isRunning = false
|
|
104
|
+
this.requestStorage = new AsyncLocalStorage()
|
|
92
105
|
|
|
93
106
|
// TODO: Rename setup to configure?
|
|
94
107
|
this.setupLogger()
|
|
@@ -117,12 +130,18 @@ export class Application extends Koa {
|
|
|
117
130
|
}
|
|
118
131
|
|
|
119
132
|
addRoute(
|
|
120
|
-
method,
|
|
133
|
+
method,
|
|
134
|
+
path,
|
|
135
|
+
transacted,
|
|
136
|
+
middlewares,
|
|
137
|
+
controller = null,
|
|
138
|
+
action = null
|
|
121
139
|
) {
|
|
122
140
|
middlewares = asArray(middlewares)
|
|
123
|
-
const middleware =
|
|
124
|
-
|
|
125
|
-
|
|
141
|
+
const middleware =
|
|
142
|
+
middlewares.length > 1
|
|
143
|
+
? compose(middlewares)
|
|
144
|
+
: middlewares[0]
|
|
126
145
|
// Instead of directly passing `handler`, pass a `route` object that also
|
|
127
146
|
// will be exposed through `ctx.route`, see `routerHandler()`:
|
|
128
147
|
const route = {
|
|
@@ -173,15 +192,16 @@ export class Application extends Koa {
|
|
|
173
192
|
}
|
|
174
193
|
|
|
175
194
|
async setupStorages() {
|
|
176
|
-
await Promise.all(
|
|
177
|
-
.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
195
|
+
await Promise.all(
|
|
196
|
+
Object.values(this.storages)
|
|
197
|
+
.filter(storage => !storage.initialized)
|
|
198
|
+
.map(async storage => {
|
|
199
|
+
// Different from models, services and controllers, storages can have
|
|
200
|
+
// async `setup()` methods, as used by `S3Storage`.
|
|
201
|
+
await storage.setup()
|
|
202
|
+
await storage.initialize()
|
|
203
|
+
storage.initialized = true
|
|
204
|
+
})
|
|
185
205
|
)
|
|
186
206
|
}
|
|
187
207
|
|
|
@@ -212,24 +232,25 @@ export class Application extends Koa {
|
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
async setupServices() {
|
|
215
|
-
await Promise.all(
|
|
216
|
-
.
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
235
|
+
await Promise.all(
|
|
236
|
+
Object.values(this.services)
|
|
237
|
+
.filter(service => !service.initialized)
|
|
238
|
+
.map(async service => {
|
|
239
|
+
const { name } = service
|
|
240
|
+
const config = this.config.services[name]
|
|
241
|
+
if (config === undefined) {
|
|
242
|
+
throw new Error(`Configuration missing for service '${name}'`)
|
|
243
|
+
}
|
|
244
|
+
// As a convention, the configuration of a service can be set to
|
|
245
|
+
// `false` in order to entirely deactivate the service.
|
|
246
|
+
if (config === false) {
|
|
247
|
+
delete this.services[name]
|
|
248
|
+
} else {
|
|
249
|
+
service.setup(config)
|
|
250
|
+
await service.initialize()
|
|
251
|
+
service.initialized = true
|
|
252
|
+
}
|
|
253
|
+
})
|
|
233
254
|
)
|
|
234
255
|
}
|
|
235
256
|
|
|
@@ -269,16 +290,17 @@ export class Application extends Koa {
|
|
|
269
290
|
}
|
|
270
291
|
|
|
271
292
|
async setupModels() {
|
|
272
|
-
await Promise.all(
|
|
273
|
-
.
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
293
|
+
await Promise.all(
|
|
294
|
+
Object.values(this.models)
|
|
295
|
+
.filter(modelClass => !modelClass.initialized)
|
|
296
|
+
.map(async modelClass => {
|
|
297
|
+
// While `setup()` is used for internal dito things, `initialize()` is
|
|
298
|
+
// called async and meant to be used by the user, without the need to
|
|
299
|
+
// call `super.initialize()`.
|
|
300
|
+
modelClass.setup()
|
|
301
|
+
await modelClass.initialize()
|
|
302
|
+
modelClass.initialized = true
|
|
303
|
+
})
|
|
282
304
|
)
|
|
283
305
|
}
|
|
284
306
|
|
|
@@ -287,8 +309,7 @@ export class Application extends Koa {
|
|
|
287
309
|
if (log.schema || log.relations) {
|
|
288
310
|
for (const modelClass of models) {
|
|
289
311
|
const shouldLog = option => (
|
|
290
|
-
option === true ||
|
|
291
|
-
asArray(option).includes(modelClass.name)
|
|
312
|
+
option === true || asArray(option).includes(modelClass.name)
|
|
292
313
|
)
|
|
293
314
|
const data = {}
|
|
294
315
|
if (shouldLog(log.schema)) {
|
|
@@ -296,9 +317,10 @@ export class Application extends Koa {
|
|
|
296
317
|
}
|
|
297
318
|
if (shouldLog(log.relations)) {
|
|
298
319
|
data.relations = clone(modelClass.getRelationMappings(), {
|
|
299
|
-
processValue: value =>
|
|
300
|
-
|
|
301
|
-
|
|
320
|
+
processValue: value =>
|
|
321
|
+
Model.isPrototypeOf(value)
|
|
322
|
+
? `[Model: ${value.name}]`
|
|
323
|
+
: value
|
|
302
324
|
})
|
|
303
325
|
}
|
|
304
326
|
if (Object.keys(data).length > 0) {
|
|
@@ -349,19 +371,20 @@ export class Application extends Koa {
|
|
|
349
371
|
}
|
|
350
372
|
|
|
351
373
|
async setupControllers() {
|
|
352
|
-
await Promise.all(
|
|
353
|
-
.
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
374
|
+
await Promise.all(
|
|
375
|
+
Object.values(this.controllers)
|
|
376
|
+
.filter(controller => !controller.initialized)
|
|
377
|
+
.map(async controller => {
|
|
378
|
+
controller.setup()
|
|
379
|
+
await controller.initialize()
|
|
380
|
+
// Each controller can also compose their own middleware (or app),
|
|
381
|
+
// e.g. as used in `AdminController`:
|
|
382
|
+
const composed = controller.compose()
|
|
383
|
+
if (composed) {
|
|
384
|
+
this.use(mount(controller.url, composed))
|
|
385
|
+
}
|
|
386
|
+
controller.initialized = true
|
|
387
|
+
})
|
|
365
388
|
)
|
|
366
389
|
}
|
|
367
390
|
|
|
@@ -406,7 +429,7 @@ export class Application extends Koa {
|
|
|
406
429
|
wildcard || normalizedName,
|
|
407
430
|
...parseDataPath(nestedDataPath)
|
|
408
431
|
])
|
|
409
|
-
const assetConfigs = convertedAssets[normalizedName] ||= {}
|
|
432
|
+
const assetConfigs = (convertedAssets[normalizedName] ||= {})
|
|
410
433
|
assetConfigs[dataPath] = config
|
|
411
434
|
}
|
|
412
435
|
}
|
|
@@ -480,8 +503,8 @@ export class Application extends Koa {
|
|
|
480
503
|
asObject,
|
|
481
504
|
dataName,
|
|
482
505
|
validate: validate
|
|
483
|
-
// Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
484
|
-
|
|
506
|
+
? // Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
507
|
+
data => validate.call(ctx, data)
|
|
485
508
|
: null
|
|
486
509
|
}
|
|
487
510
|
}
|
|
@@ -500,8 +523,10 @@ export class Application extends Koa {
|
|
|
500
523
|
// Remove knex SQL query and move to separate `sql` property.
|
|
501
524
|
// TODO: Fix this properly in Knex / Objection instead, see:
|
|
502
525
|
// https://gitter.im/Vincit/objection.js?at=5a68728f5a9ebe4f75ca40b0
|
|
503
|
-
const [, sql, message] =
|
|
526
|
+
const [, sql, message] = (
|
|
527
|
+
error.message.match(/^([\s\S]*) - ([\s\S]*?)$/) ||
|
|
504
528
|
[null, null, error.message]
|
|
529
|
+
)
|
|
505
530
|
return new DatabaseError(error, {
|
|
506
531
|
message,
|
|
507
532
|
// Only include the SQL query in the error if `log.errors.sql`is set.
|
|
@@ -519,7 +544,11 @@ export class Application extends Koa {
|
|
|
519
544
|
this.use(responseTime(getOptions(app.responseTime)))
|
|
520
545
|
}
|
|
521
546
|
if (log.requests) {
|
|
522
|
-
this.use(
|
|
547
|
+
this.use(
|
|
548
|
+
logRequests({
|
|
549
|
+
ignoreUrlPattern: /(\.js$|\.scss$|\.vue$|\/@vite\/|\/@fs\/|\/@id\/)/
|
|
550
|
+
})
|
|
551
|
+
)
|
|
523
552
|
}
|
|
524
553
|
// This needs to be positioned after the request logger to log the correct
|
|
525
554
|
// response status.
|
|
@@ -531,23 +560,28 @@ export class Application extends Koa {
|
|
|
531
560
|
this.use(cors(getOptions(app.cors)))
|
|
532
561
|
}
|
|
533
562
|
if (app.compress !== false) {
|
|
534
|
-
this.use(
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
563
|
+
this.use(
|
|
564
|
+
compress(
|
|
565
|
+
merge(
|
|
566
|
+
{
|
|
567
|
+
// Use a reasonable default for Brotli compression.
|
|
568
|
+
// See https://github.com/koajs/compress/issues/126
|
|
569
|
+
br: {
|
|
570
|
+
params: {
|
|
571
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
getOptions(app.compress)
|
|
576
|
+
)
|
|
577
|
+
)
|
|
578
|
+
)
|
|
546
579
|
}
|
|
547
580
|
if (app.etag !== false) {
|
|
548
581
|
this.use(conditional())
|
|
549
582
|
this.use(etag())
|
|
550
583
|
}
|
|
584
|
+
this.use(setupRequestStorage(this.requestStorage))
|
|
551
585
|
|
|
552
586
|
// Controller-specific middleware
|
|
553
587
|
|
|
@@ -581,18 +615,20 @@ export class Application extends Koa {
|
|
|
581
615
|
const { prettyPrint, ...options } = this.config.logger
|
|
582
616
|
const transport = prettyPrint
|
|
583
617
|
? pino.transport({
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
618
|
+
target: 'pino-pretty',
|
|
619
|
+
options: prettyPrint
|
|
620
|
+
})
|
|
621
|
+
: null
|
|
587
622
|
this.logger = pino(options, transport).child({ name: 'app' })
|
|
588
623
|
}
|
|
589
624
|
|
|
590
625
|
setupKnex() {
|
|
591
626
|
let { knex, log } = this.config
|
|
592
627
|
if (knex?.client) {
|
|
593
|
-
const snakeCaseOptions =
|
|
594
|
-
|
|
595
|
-
|
|
628
|
+
const snakeCaseOptions =
|
|
629
|
+
knex.normalizeDbNames === true
|
|
630
|
+
? {}
|
|
631
|
+
: knex.normalizeDbNames
|
|
596
632
|
if (snakeCaseOptions) {
|
|
597
633
|
knex = {
|
|
598
634
|
...knex,
|
|
@@ -671,11 +707,11 @@ export class Application extends Koa {
|
|
|
671
707
|
// stack traces and logging of error data.
|
|
672
708
|
return this.config.logger.prettyPrint
|
|
673
709
|
? util.inspect(copy, {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
710
|
+
colors: !!this.config.logger.prettyPrint.colorize,
|
|
711
|
+
compact: false,
|
|
712
|
+
depth: null,
|
|
713
|
+
maxArrayLength: null
|
|
714
|
+
})
|
|
679
715
|
: copy
|
|
680
716
|
}
|
|
681
717
|
|
|
@@ -683,9 +719,10 @@ export class Application extends Koa {
|
|
|
683
719
|
if (!error.expose && !this.silent) {
|
|
684
720
|
try {
|
|
685
721
|
const logger = ctx?.logger || this.logger
|
|
686
|
-
const level =
|
|
687
|
-
|
|
688
|
-
|
|
722
|
+
const level =
|
|
723
|
+
error instanceof ResponseError && error.status < 500
|
|
724
|
+
? 'info'
|
|
725
|
+
: 'error'
|
|
689
726
|
logger[level](this.formatError(error))
|
|
690
727
|
} catch (e) {
|
|
691
728
|
console.error('Could not log error', e)
|
|
@@ -740,7 +777,8 @@ export class Application extends Koa {
|
|
|
740
777
|
await Promise.race([
|
|
741
778
|
promise,
|
|
742
779
|
new Promise((resolve, reject) =>
|
|
743
|
-
setTimeout(
|
|
780
|
+
setTimeout(
|
|
781
|
+
reject,
|
|
744
782
|
timeout,
|
|
745
783
|
new Error(
|
|
746
784
|
`Timeout reached while stopping Dito.js server (${timeout}ms)`
|
|
@@ -782,9 +820,7 @@ export class Application extends Koa {
|
|
|
782
820
|
storage: storage.name,
|
|
783
821
|
count
|
|
784
822
|
}))
|
|
785
|
-
return AssetModel
|
|
786
|
-
.query(trx)
|
|
787
|
-
.insert(assets)
|
|
823
|
+
return AssetModel.query(trx).insert(assets)
|
|
788
824
|
}
|
|
789
825
|
return null
|
|
790
826
|
}
|
|
@@ -810,7 +846,10 @@ export class Application extends Koa {
|
|
|
810
846
|
const changeCount = async (files, increment) => {
|
|
811
847
|
if (files.length > 0) {
|
|
812
848
|
await AssetModel.query(trx)
|
|
813
|
-
.whereIn(
|
|
849
|
+
.whereIn(
|
|
850
|
+
'key',
|
|
851
|
+
files.map(file => file.key)
|
|
852
|
+
)
|
|
814
853
|
.increment('count', increment)
|
|
815
854
|
}
|
|
816
855
|
}
|
|
@@ -818,8 +857,9 @@ export class Application extends Koa {
|
|
|
818
857
|
changeCount(addedFiles, 1),
|
|
819
858
|
changeCount(removedFiles, -1)
|
|
820
859
|
])
|
|
821
|
-
const cleanupTimeThreshold =
|
|
822
|
-
|
|
860
|
+
const cleanupTimeThreshold = getDuration(
|
|
861
|
+
this.config.assets.cleanupTimeThreshold
|
|
862
|
+
)
|
|
823
863
|
if (cleanupTimeThreshold > 0) {
|
|
824
864
|
setTimeout(
|
|
825
865
|
// Don't pass `trx` here, as we want this delayed execution to
|
|
@@ -841,65 +881,69 @@ export class Application extends Koa {
|
|
|
841
881
|
const AssetModel = this.getModel('Asset')
|
|
842
882
|
if (AssetModel) {
|
|
843
883
|
// Find missing assets (copied from another system), and add them.
|
|
844
|
-
await mapConcurrently(
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
if (!
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
884
|
+
await mapConcurrently(
|
|
885
|
+
files,
|
|
886
|
+
async file => {
|
|
887
|
+
const asset = await AssetModel.query(trx).findOne('key', file.key)
|
|
888
|
+
if (!asset) {
|
|
889
|
+
if (file.data || file.url) {
|
|
890
|
+
let { data } = file
|
|
891
|
+
if (!data) {
|
|
892
|
+
const { url } = file
|
|
893
|
+
if (!storage.isImportSourceAllowed(url)) {
|
|
894
|
+
throw new AssetError(
|
|
895
|
+
`Unable to import asset from foreign source: '${
|
|
896
|
+
file.name
|
|
897
|
+
}' ('${
|
|
898
|
+
url
|
|
899
|
+
}'): The source needs to be explicitly allowed.`
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
this.logger.info(
|
|
903
|
+
`Asset ${
|
|
904
|
+
pico.green(`'${file.name}'`)
|
|
905
|
+
} is from a foreign source, fetching from ${
|
|
906
|
+
pico.green(`'${url}'`)
|
|
907
|
+
} and adding to storage ${
|
|
908
|
+
pico.green(`'${storage.name}'`)
|
|
909
|
+
}...`
|
|
858
910
|
)
|
|
911
|
+
if (url.startsWith('file://')) {
|
|
912
|
+
const filepath = path.resolve(url.substring(7))
|
|
913
|
+
data = await fs.readFile(filepath)
|
|
914
|
+
} else {
|
|
915
|
+
const response = await fetch(url)
|
|
916
|
+
const arrayBuffer = await response.arrayBuffer()
|
|
917
|
+
// `fs.writeFile()` expects a Buffer, not an ArrayBuffer.
|
|
918
|
+
data = Buffer.from(arrayBuffer)
|
|
919
|
+
}
|
|
859
920
|
}
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
921
|
+
const importedFile = await storage.addFile(file, data)
|
|
922
|
+
await this.createAssets(storage, [importedFile], 0, trx)
|
|
923
|
+
// Merge back the changed file properties into the actual files
|
|
924
|
+
// object, so that the data from the static model hook can be used
|
|
925
|
+
// directly for the actual running query.
|
|
926
|
+
Object.assign(file, importedFile)
|
|
927
|
+
importedFiles.push(importedFile)
|
|
928
|
+
} else {
|
|
929
|
+
throw new AssetError(
|
|
930
|
+
`Unable to import asset from foreign source: '${
|
|
931
|
+
file.name
|
|
932
|
+
}' ('${
|
|
933
|
+
file.key
|
|
934
|
+
}')`
|
|
868
935
|
)
|
|
869
|
-
if (url.startsWith('file://')) {
|
|
870
|
-
const filepath = path.resolve(url.substring(7))
|
|
871
|
-
data = await fs.readFile(filepath)
|
|
872
|
-
} else {
|
|
873
|
-
const response = await fetch(url)
|
|
874
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
875
|
-
// `fs.writeFile()` expects a Buffer, not an ArrayBuffer.
|
|
876
|
-
data = Buffer.from(arrayBuffer)
|
|
877
|
-
}
|
|
878
936
|
}
|
|
879
|
-
const importedFile = await storage.addFile(file, data)
|
|
880
|
-
await this.createAssets(storage, [importedFile], 0, trx)
|
|
881
|
-
// Merge back the changed file properties into the actual files
|
|
882
|
-
// object, so that the data from the static model hook can be used
|
|
883
|
-
// directly for the actual running query.
|
|
884
|
-
Object.assign(file, importedFile)
|
|
885
|
-
importedFiles.push(importedFile)
|
|
886
937
|
} else {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
}')`
|
|
893
|
-
)
|
|
938
|
+
// Asset is from a foreign source, but was already imported and can
|
|
939
|
+
// be reused. See above for an explanation of this merge.
|
|
940
|
+
Object.assign(file, asset.file)
|
|
941
|
+
// NOTE: No need to add `file` to `importedFiles`, since it's
|
|
942
|
+
// already been imported to the storage before.
|
|
894
943
|
}
|
|
895
|
-
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
Object.assign(file, asset.file)
|
|
899
|
-
// NOTE: No need to add `file` to `importedFiles`, since it's
|
|
900
|
-
// already been imported to the storage before.
|
|
901
|
-
}
|
|
902
|
-
}, { concurrency: storage.concurrency })
|
|
944
|
+
},
|
|
945
|
+
{ concurrency: storage.concurrency }
|
|
946
|
+
)
|
|
903
947
|
}
|
|
904
948
|
return importedFiles
|
|
905
949
|
}
|
|
@@ -920,11 +964,11 @@ export class Application extends Koa {
|
|
|
920
964
|
modifiedFiles.push(changedFile)
|
|
921
965
|
} else {
|
|
922
966
|
throw new AssetError(
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
967
|
+
`Unable to update modified asset from memory source: '${
|
|
968
|
+
file.name
|
|
969
|
+
}' ('${
|
|
970
|
+
file.key
|
|
971
|
+
}')`
|
|
928
972
|
)
|
|
929
973
|
}
|
|
930
974
|
}
|
|
@@ -937,28 +981,30 @@ export class Application extends Koa {
|
|
|
937
981
|
const AssetModel = this.getModel('Asset')
|
|
938
982
|
if (AssetModel) {
|
|
939
983
|
const { assets } = this.config
|
|
940
|
-
const cleanupTimeThreshold =
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
984
|
+
const cleanupTimeThreshold = getDuration(
|
|
985
|
+
timeThreshold ?? assets.cleanupTimeThreshold
|
|
986
|
+
)
|
|
987
|
+
const danglingTimeThreshold = getDuration(
|
|
988
|
+
timeThreshold ?? assets.danglingTimeThreshold
|
|
989
|
+
)
|
|
944
990
|
return AssetModel.transaction(trx, async trx => {
|
|
945
991
|
// Calculate the date math in JS instead of SQL, as there is no easy
|
|
946
992
|
// cross-SQL way to do `now() - interval X hours`:
|
|
947
993
|
const now = new Date()
|
|
948
994
|
const cleanupDate = subtractDuration(now, cleanupTimeThreshold)
|
|
949
995
|
const danglingDate = subtractDuration(now, danglingTimeThreshold)
|
|
950
|
-
const orphanedAssets = await AssetModel
|
|
951
|
-
.query(trx)
|
|
996
|
+
const orphanedAssets = await AssetModel.query(trx)
|
|
952
997
|
.where('count', 0)
|
|
953
|
-
.andWhere(
|
|
954
|
-
query
|
|
998
|
+
.andWhere(query =>
|
|
999
|
+
query
|
|
955
1000
|
.where('updatedAt', '<=', cleanupDate)
|
|
956
1001
|
.orWhere(
|
|
957
|
-
// Protect freshly created assets from being deleted again
|
|
958
|
-
// away,
|
|
959
|
-
query =>
|
|
960
|
-
|
|
961
|
-
|
|
1002
|
+
// Protect freshly created assets from being deleted again
|
|
1003
|
+
// right away, when `config.assets.cleanupTimeThreshold = 0`
|
|
1004
|
+
query =>
|
|
1005
|
+
query
|
|
1006
|
+
.where('updatedAt', '=', ref('createdAt'))
|
|
1007
|
+
.andWhere('updatedAt', '<=', danglingDate)
|
|
962
1008
|
)
|
|
963
1009
|
)
|
|
964
1010
|
if (orphanedAssets.length > 0) {
|
|
@@ -974,15 +1020,16 @@ export class Application extends Koa {
|
|
|
974
1020
|
return asset.key
|
|
975
1021
|
}
|
|
976
1022
|
)
|
|
977
|
-
await AssetModel
|
|
978
|
-
.query(trx)
|
|
979
|
-
.delete()
|
|
980
|
-
.whereIn('key', orphanedKeys)
|
|
1023
|
+
await AssetModel.query(trx).delete().whereIn('key', orphanedKeys)
|
|
981
1024
|
}
|
|
982
1025
|
return orphanedAssets
|
|
983
1026
|
})
|
|
984
1027
|
}
|
|
985
1028
|
}
|
|
1029
|
+
|
|
1030
|
+
get requestLocals() {
|
|
1031
|
+
return this.requestStorage.getStore() ?? {}
|
|
1032
|
+
}
|
|
986
1033
|
}
|
|
987
1034
|
|
|
988
1035
|
// Override Koa's events with our own EventEmitter that adds support for
|