@ditojs/server 2.0.4 → 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 +228 -212
- 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 +100 -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 +78 -0
- package/src/middleware/handleUser.js +2 -2
- package/src/middleware/index.js +2 -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/SessionStore.js +0 -31
package/src/app/Application.js
CHANGED
|
@@ -13,18 +13,25 @@ import compress from 'koa-compress'
|
|
|
13
13
|
import conditional from 'koa-conditional-get'
|
|
14
14
|
import mount from 'koa-mount'
|
|
15
15
|
import passport from 'koa-passport'
|
|
16
|
-
import session from 'koa-session'
|
|
17
16
|
import etag from 'koa-etag'
|
|
18
17
|
import helmet from 'koa-helmet'
|
|
19
18
|
import responseTime from 'koa-response-time'
|
|
20
19
|
import { Model, knexSnakeCaseMappers, ref } from 'objection'
|
|
21
20
|
import Router from '@ditojs/router'
|
|
22
21
|
import {
|
|
23
|
-
isArray,
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
isArray,
|
|
23
|
+
isObject,
|
|
24
|
+
asArray,
|
|
25
|
+
isPlainObject,
|
|
26
|
+
isModule,
|
|
27
|
+
hyphenate,
|
|
28
|
+
clone,
|
|
29
|
+
merge,
|
|
30
|
+
parseDataPath,
|
|
31
|
+
normalizeDataPath,
|
|
32
|
+
toPromiseCallback,
|
|
33
|
+
mapConcurrently
|
|
26
34
|
} from '@ditojs/utils'
|
|
27
|
-
import SessionStore from './SessionStore.js'
|
|
28
35
|
import { Validator } from './Validator.js'
|
|
29
36
|
import { EventEmitter } from '../lib/index.js'
|
|
30
37
|
import { Controller, AdminController } from '../controllers/index.js'
|
|
@@ -45,9 +52,12 @@ import {
|
|
|
45
52
|
findRoute,
|
|
46
53
|
handleError,
|
|
47
54
|
handleRoute,
|
|
55
|
+
handleSession,
|
|
48
56
|
handleUser,
|
|
49
|
-
logRequests
|
|
57
|
+
logRequests,
|
|
58
|
+
setupRequestStorage
|
|
50
59
|
} from '../middleware/index.js'
|
|
60
|
+
import { AsyncLocalStorage } from 'async_hooks'
|
|
51
61
|
|
|
52
62
|
export class Application extends Koa {
|
|
53
63
|
constructor({
|
|
@@ -72,9 +82,10 @@ export class Application extends Koa {
|
|
|
72
82
|
} = config
|
|
73
83
|
this.config = {
|
|
74
84
|
app,
|
|
75
|
-
log:
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
log:
|
|
86
|
+
log === false || log?.silent || process.env.DITO_SILENT
|
|
87
|
+
? {}
|
|
88
|
+
: getOptions(log),
|
|
78
89
|
assets: merge(defaultAssetOptions, getOptions(assets)),
|
|
79
90
|
logger: merge(defaultLoggerOptions, getOptions(logger)),
|
|
80
91
|
...rest
|
|
@@ -90,6 +101,7 @@ export class Application extends Koa {
|
|
|
90
101
|
this.controllers = Object.create(null)
|
|
91
102
|
this.server = null
|
|
92
103
|
this.isRunning = false
|
|
104
|
+
this.requestStorage = new AsyncLocalStorage()
|
|
93
105
|
|
|
94
106
|
// TODO: Rename setup to configure?
|
|
95
107
|
this.setupLogger()
|
|
@@ -118,12 +130,18 @@ export class Application extends Koa {
|
|
|
118
130
|
}
|
|
119
131
|
|
|
120
132
|
addRoute(
|
|
121
|
-
method,
|
|
133
|
+
method,
|
|
134
|
+
path,
|
|
135
|
+
transacted,
|
|
136
|
+
middlewares,
|
|
137
|
+
controller = null,
|
|
138
|
+
action = null
|
|
122
139
|
) {
|
|
123
140
|
middlewares = asArray(middlewares)
|
|
124
|
-
const middleware =
|
|
125
|
-
|
|
126
|
-
|
|
141
|
+
const middleware =
|
|
142
|
+
middlewares.length > 1
|
|
143
|
+
? compose(middlewares)
|
|
144
|
+
: middlewares[0]
|
|
127
145
|
// Instead of directly passing `handler`, pass a `route` object that also
|
|
128
146
|
// will be exposed through `ctx.route`, see `routerHandler()`:
|
|
129
147
|
const route = {
|
|
@@ -174,15 +192,16 @@ export class Application extends Koa {
|
|
|
174
192
|
}
|
|
175
193
|
|
|
176
194
|
async setupStorages() {
|
|
177
|
-
await Promise.all(
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
+
})
|
|
186
205
|
)
|
|
187
206
|
}
|
|
188
207
|
|
|
@@ -213,24 +232,25 @@ export class Application extends Koa {
|
|
|
213
232
|
}
|
|
214
233
|
|
|
215
234
|
async setupServices() {
|
|
216
|
-
await Promise.all(
|
|
217
|
-
.
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
+
})
|
|
234
254
|
)
|
|
235
255
|
}
|
|
236
256
|
|
|
@@ -270,16 +290,17 @@ export class Application extends Koa {
|
|
|
270
290
|
}
|
|
271
291
|
|
|
272
292
|
async setupModels() {
|
|
273
|
-
await Promise.all(
|
|
274
|
-
.
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
+
})
|
|
283
304
|
)
|
|
284
305
|
}
|
|
285
306
|
|
|
@@ -288,8 +309,7 @@ export class Application extends Koa {
|
|
|
288
309
|
if (log.schema || log.relations) {
|
|
289
310
|
for (const modelClass of models) {
|
|
290
311
|
const shouldLog = option => (
|
|
291
|
-
option === true ||
|
|
292
|
-
asArray(option).includes(modelClass.name)
|
|
312
|
+
option === true || asArray(option).includes(modelClass.name)
|
|
293
313
|
)
|
|
294
314
|
const data = {}
|
|
295
315
|
if (shouldLog(log.schema)) {
|
|
@@ -297,9 +317,10 @@ export class Application extends Koa {
|
|
|
297
317
|
}
|
|
298
318
|
if (shouldLog(log.relations)) {
|
|
299
319
|
data.relations = clone(modelClass.getRelationMappings(), {
|
|
300
|
-
processValue: value =>
|
|
301
|
-
|
|
302
|
-
|
|
320
|
+
processValue: value =>
|
|
321
|
+
Model.isPrototypeOf(value)
|
|
322
|
+
? `[Model: ${value.name}]`
|
|
323
|
+
: value
|
|
303
324
|
})
|
|
304
325
|
}
|
|
305
326
|
if (Object.keys(data).length > 0) {
|
|
@@ -350,19 +371,20 @@ export class Application extends Koa {
|
|
|
350
371
|
}
|
|
351
372
|
|
|
352
373
|
async setupControllers() {
|
|
353
|
-
await Promise.all(
|
|
354
|
-
.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
})
|
|
366
388
|
)
|
|
367
389
|
}
|
|
368
390
|
|
|
@@ -407,7 +429,7 @@ export class Application extends Koa {
|
|
|
407
429
|
wildcard || normalizedName,
|
|
408
430
|
...parseDataPath(nestedDataPath)
|
|
409
431
|
])
|
|
410
|
-
const assetConfigs = convertedAssets[normalizedName] ||= {}
|
|
432
|
+
const assetConfigs = (convertedAssets[normalizedName] ||= {})
|
|
411
433
|
assetConfigs[dataPath] = config
|
|
412
434
|
}
|
|
413
435
|
}
|
|
@@ -481,8 +503,8 @@ export class Application extends Koa {
|
|
|
481
503
|
asObject,
|
|
482
504
|
dataName,
|
|
483
505
|
validate: validate
|
|
484
|
-
// Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
485
|
-
|
|
506
|
+
? // Use `call()` to pass ctx as context to Ajv, see passContext:
|
|
507
|
+
data => validate.call(ctx, data)
|
|
486
508
|
: null
|
|
487
509
|
}
|
|
488
510
|
}
|
|
@@ -501,8 +523,10 @@ export class Application extends Koa {
|
|
|
501
523
|
// Remove knex SQL query and move to separate `sql` property.
|
|
502
524
|
// TODO: Fix this properly in Knex / Objection instead, see:
|
|
503
525
|
// https://gitter.im/Vincit/objection.js?at=5a68728f5a9ebe4f75ca40b0
|
|
504
|
-
const [, sql, message] =
|
|
526
|
+
const [, sql, message] = (
|
|
527
|
+
error.message.match(/^([\s\S]*) - ([\s\S]*?)$/) ||
|
|
505
528
|
[null, null, error.message]
|
|
529
|
+
)
|
|
506
530
|
return new DatabaseError(error, {
|
|
507
531
|
message,
|
|
508
532
|
// Only include the SQL query in the error if `log.errors.sql`is set.
|
|
@@ -520,7 +544,11 @@ export class Application extends Koa {
|
|
|
520
544
|
this.use(responseTime(getOptions(app.responseTime)))
|
|
521
545
|
}
|
|
522
546
|
if (log.requests) {
|
|
523
|
-
this.use(
|
|
547
|
+
this.use(
|
|
548
|
+
logRequests({
|
|
549
|
+
ignoreUrlPattern: /(\.js$|\.scss$|\.vue$|\/@vite\/|\/@fs\/|\/@id\/)/
|
|
550
|
+
})
|
|
551
|
+
)
|
|
524
552
|
}
|
|
525
553
|
// This needs to be positioned after the request logger to log the correct
|
|
526
554
|
// response status.
|
|
@@ -532,23 +560,28 @@ export class Application extends Koa {
|
|
|
532
560
|
this.use(cors(getOptions(app.cors)))
|
|
533
561
|
}
|
|
534
562
|
if (app.compress !== false) {
|
|
535
|
-
this.use(
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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
|
+
)
|
|
547
579
|
}
|
|
548
580
|
if (app.etag !== false) {
|
|
549
581
|
this.use(conditional())
|
|
550
582
|
this.use(etag())
|
|
551
583
|
}
|
|
584
|
+
this.use(setupRequestStorage(this.requestStorage))
|
|
552
585
|
|
|
553
586
|
// Controller-specific middleware
|
|
554
587
|
|
|
@@ -564,37 +597,7 @@ export class Application extends Koa {
|
|
|
564
597
|
this.use(createTransaction())
|
|
565
598
|
// 5. session
|
|
566
599
|
if (app.session) {
|
|
567
|
-
|
|
568
|
-
modelClass,
|
|
569
|
-
autoCommit = true,
|
|
570
|
-
...options
|
|
571
|
-
} = getOptions(app.session)
|
|
572
|
-
if (modelClass) {
|
|
573
|
-
// Create a ContextStore that resolved the specified model class,
|
|
574
|
-
// uses it to persist and retrieve the session, and automatically
|
|
575
|
-
// binds all db operations to `ctx.transaction`, if it is set.
|
|
576
|
-
// eslint-disable-next-line new-cap
|
|
577
|
-
options.ContextStore = SessionStore(modelClass)
|
|
578
|
-
}
|
|
579
|
-
options.autoCommit = false
|
|
580
|
-
this.use(session(options, this))
|
|
581
|
-
this.use(async (ctx, next) => {
|
|
582
|
-
const { transacted } = ctx.route
|
|
583
|
-
try {
|
|
584
|
-
await next()
|
|
585
|
-
if (autoCommit && transacted) {
|
|
586
|
-
// When transacted, only commit when there are no errors. Otherwise,
|
|
587
|
-
// the commit will fail and the original error will be lost.
|
|
588
|
-
await ctx.session.commit()
|
|
589
|
-
}
|
|
590
|
-
} finally {
|
|
591
|
-
// When not transacted, keep the original behavior of always
|
|
592
|
-
// committing.
|
|
593
|
-
if (autoCommit && !transacted) {
|
|
594
|
-
await ctx.session.commit()
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
})
|
|
600
|
+
this.use(handleSession(this, getOptions(app.session)))
|
|
598
601
|
}
|
|
599
602
|
// 6. passport
|
|
600
603
|
if (app.passport) {
|
|
@@ -612,18 +615,20 @@ export class Application extends Koa {
|
|
|
612
615
|
const { prettyPrint, ...options } = this.config.logger
|
|
613
616
|
const transport = prettyPrint
|
|
614
617
|
? pino.transport({
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
+
target: 'pino-pretty',
|
|
619
|
+
options: prettyPrint
|
|
620
|
+
})
|
|
621
|
+
: null
|
|
618
622
|
this.logger = pino(options, transport).child({ name: 'app' })
|
|
619
623
|
}
|
|
620
624
|
|
|
621
625
|
setupKnex() {
|
|
622
626
|
let { knex, log } = this.config
|
|
623
627
|
if (knex?.client) {
|
|
624
|
-
const snakeCaseOptions =
|
|
625
|
-
|
|
626
|
-
|
|
628
|
+
const snakeCaseOptions =
|
|
629
|
+
knex.normalizeDbNames === true
|
|
630
|
+
? {}
|
|
631
|
+
: knex.normalizeDbNames
|
|
627
632
|
if (snakeCaseOptions) {
|
|
628
633
|
knex = {
|
|
629
634
|
...knex,
|
|
@@ -702,11 +707,11 @@ export class Application extends Koa {
|
|
|
702
707
|
// stack traces and logging of error data.
|
|
703
708
|
return this.config.logger.prettyPrint
|
|
704
709
|
? util.inspect(copy, {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
+
colors: !!this.config.logger.prettyPrint.colorize,
|
|
711
|
+
compact: false,
|
|
712
|
+
depth: null,
|
|
713
|
+
maxArrayLength: null
|
|
714
|
+
})
|
|
710
715
|
: copy
|
|
711
716
|
}
|
|
712
717
|
|
|
@@ -714,9 +719,10 @@ export class Application extends Koa {
|
|
|
714
719
|
if (!error.expose && !this.silent) {
|
|
715
720
|
try {
|
|
716
721
|
const logger = ctx?.logger || this.logger
|
|
717
|
-
const level =
|
|
718
|
-
|
|
719
|
-
|
|
722
|
+
const level =
|
|
723
|
+
error instanceof ResponseError && error.status < 500
|
|
724
|
+
? 'info'
|
|
725
|
+
: 'error'
|
|
720
726
|
logger[level](this.formatError(error))
|
|
721
727
|
} catch (e) {
|
|
722
728
|
console.error('Could not log error', e)
|
|
@@ -771,7 +777,8 @@ export class Application extends Koa {
|
|
|
771
777
|
await Promise.race([
|
|
772
778
|
promise,
|
|
773
779
|
new Promise((resolve, reject) =>
|
|
774
|
-
setTimeout(
|
|
780
|
+
setTimeout(
|
|
781
|
+
reject,
|
|
775
782
|
timeout,
|
|
776
783
|
new Error(
|
|
777
784
|
`Timeout reached while stopping Dito.js server (${timeout}ms)`
|
|
@@ -813,9 +820,7 @@ export class Application extends Koa {
|
|
|
813
820
|
storage: storage.name,
|
|
814
821
|
count
|
|
815
822
|
}))
|
|
816
|
-
return AssetModel
|
|
817
|
-
.query(trx)
|
|
818
|
-
.insert(assets)
|
|
823
|
+
return AssetModel.query(trx).insert(assets)
|
|
819
824
|
}
|
|
820
825
|
return null
|
|
821
826
|
}
|
|
@@ -841,7 +846,10 @@ export class Application extends Koa {
|
|
|
841
846
|
const changeCount = async (files, increment) => {
|
|
842
847
|
if (files.length > 0) {
|
|
843
848
|
await AssetModel.query(trx)
|
|
844
|
-
.whereIn(
|
|
849
|
+
.whereIn(
|
|
850
|
+
'key',
|
|
851
|
+
files.map(file => file.key)
|
|
852
|
+
)
|
|
845
853
|
.increment('count', increment)
|
|
846
854
|
}
|
|
847
855
|
}
|
|
@@ -849,8 +857,9 @@ export class Application extends Koa {
|
|
|
849
857
|
changeCount(addedFiles, 1),
|
|
850
858
|
changeCount(removedFiles, -1)
|
|
851
859
|
])
|
|
852
|
-
const cleanupTimeThreshold =
|
|
853
|
-
|
|
860
|
+
const cleanupTimeThreshold = getDuration(
|
|
861
|
+
this.config.assets.cleanupTimeThreshold
|
|
862
|
+
)
|
|
854
863
|
if (cleanupTimeThreshold > 0) {
|
|
855
864
|
setTimeout(
|
|
856
865
|
// Don't pass `trx` here, as we want this delayed execution to
|
|
@@ -872,65 +881,69 @@ export class Application extends Koa {
|
|
|
872
881
|
const AssetModel = this.getModel('Asset')
|
|
873
882
|
if (AssetModel) {
|
|
874
883
|
// Find missing assets (copied from another system), and add them.
|
|
875
|
-
await mapConcurrently(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
if (
|
|
881
|
-
|
|
882
|
-
if (!
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
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
|
+
}...`
|
|
889
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
|
+
}
|
|
890
920
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
+
}')`
|
|
899
935
|
)
|
|
900
|
-
if (url.startsWith('file://')) {
|
|
901
|
-
const filepath = path.resolve(url.substring(7))
|
|
902
|
-
data = await fs.readFile(filepath)
|
|
903
|
-
} else {
|
|
904
|
-
const response = await fetch(url)
|
|
905
|
-
const arrayBuffer = await response.arrayBuffer()
|
|
906
|
-
// `fs.writeFile()` expects a Buffer, not an ArrayBuffer.
|
|
907
|
-
data = Buffer.from(arrayBuffer)
|
|
908
|
-
}
|
|
909
936
|
}
|
|
910
|
-
const importedFile = await storage.addFile(file, data)
|
|
911
|
-
await this.createAssets(storage, [importedFile], 0, trx)
|
|
912
|
-
// Merge back the changed file properties into the actual files
|
|
913
|
-
// object, so that the data from the static model hook can be used
|
|
914
|
-
// directly for the actual running query.
|
|
915
|
-
Object.assign(file, importedFile)
|
|
916
|
-
importedFiles.push(importedFile)
|
|
917
937
|
} else {
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
}')`
|
|
924
|
-
)
|
|
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.
|
|
925
943
|
}
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
Object.assign(file, asset.file)
|
|
930
|
-
// NOTE: No need to add `file` to `importedFiles`, since it's
|
|
931
|
-
// already been imported to the storage before.
|
|
932
|
-
}
|
|
933
|
-
}, { concurrency: storage.concurrency })
|
|
944
|
+
},
|
|
945
|
+
{ concurrency: storage.concurrency }
|
|
946
|
+
)
|
|
934
947
|
}
|
|
935
948
|
return importedFiles
|
|
936
949
|
}
|
|
@@ -951,11 +964,11 @@ export class Application extends Koa {
|
|
|
951
964
|
modifiedFiles.push(changedFile)
|
|
952
965
|
} else {
|
|
953
966
|
throw new AssetError(
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
967
|
+
`Unable to update modified asset from memory source: '${
|
|
968
|
+
file.name
|
|
969
|
+
}' ('${
|
|
970
|
+
file.key
|
|
971
|
+
}')`
|
|
959
972
|
)
|
|
960
973
|
}
|
|
961
974
|
}
|
|
@@ -968,28 +981,30 @@ export class Application extends Koa {
|
|
|
968
981
|
const AssetModel = this.getModel('Asset')
|
|
969
982
|
if (AssetModel) {
|
|
970
983
|
const { assets } = this.config
|
|
971
|
-
const cleanupTimeThreshold =
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
984
|
+
const cleanupTimeThreshold = getDuration(
|
|
985
|
+
timeThreshold ?? assets.cleanupTimeThreshold
|
|
986
|
+
)
|
|
987
|
+
const danglingTimeThreshold = getDuration(
|
|
988
|
+
timeThreshold ?? assets.danglingTimeThreshold
|
|
989
|
+
)
|
|
975
990
|
return AssetModel.transaction(trx, async trx => {
|
|
976
991
|
// Calculate the date math in JS instead of SQL, as there is no easy
|
|
977
992
|
// cross-SQL way to do `now() - interval X hours`:
|
|
978
993
|
const now = new Date()
|
|
979
994
|
const cleanupDate = subtractDuration(now, cleanupTimeThreshold)
|
|
980
995
|
const danglingDate = subtractDuration(now, danglingTimeThreshold)
|
|
981
|
-
const orphanedAssets = await AssetModel
|
|
982
|
-
.query(trx)
|
|
996
|
+
const orphanedAssets = await AssetModel.query(trx)
|
|
983
997
|
.where('count', 0)
|
|
984
|
-
.andWhere(
|
|
985
|
-
query
|
|
998
|
+
.andWhere(query =>
|
|
999
|
+
query
|
|
986
1000
|
.where('updatedAt', '<=', cleanupDate)
|
|
987
1001
|
.orWhere(
|
|
988
|
-
// Protect freshly created assets from being deleted again
|
|
989
|
-
// away,
|
|
990
|
-
query =>
|
|
991
|
-
|
|
992
|
-
|
|
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)
|
|
993
1008
|
)
|
|
994
1009
|
)
|
|
995
1010
|
if (orphanedAssets.length > 0) {
|
|
@@ -1005,15 +1020,16 @@ export class Application extends Koa {
|
|
|
1005
1020
|
return asset.key
|
|
1006
1021
|
}
|
|
1007
1022
|
)
|
|
1008
|
-
await AssetModel
|
|
1009
|
-
.query(trx)
|
|
1010
|
-
.delete()
|
|
1011
|
-
.whereIn('key', orphanedKeys)
|
|
1023
|
+
await AssetModel.query(trx).delete().whereIn('key', orphanedKeys)
|
|
1012
1024
|
}
|
|
1013
1025
|
return orphanedAssets
|
|
1014
1026
|
})
|
|
1015
1027
|
}
|
|
1016
1028
|
}
|
|
1029
|
+
|
|
1030
|
+
get requestLocals() {
|
|
1031
|
+
return this.requestStorage.getStore() ?? {}
|
|
1032
|
+
}
|
|
1017
1033
|
}
|
|
1018
1034
|
|
|
1019
1035
|
// Override Koa's events with our own EventEmitter that adds support for
|