@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.
Files changed (54) hide show
  1. package/package.json +11 -13
  2. package/src/app/Application.js +228 -212
  3. package/src/app/Validator.js +53 -43
  4. package/src/cli/console.js +6 -4
  5. package/src/cli/db/createMigration.js +59 -30
  6. package/src/cli/db/migrate.js +6 -4
  7. package/src/cli/db/reset.js +8 -5
  8. package/src/cli/db/rollback.js +6 -4
  9. package/src/cli/db/seed.js +2 -1
  10. package/src/cli/index.js +1 -1
  11. package/src/controllers/AdminController.js +100 -84
  12. package/src/controllers/CollectionController.js +37 -30
  13. package/src/controllers/Controller.js +83 -43
  14. package/src/controllers/ControllerAction.js +27 -15
  15. package/src/controllers/ModelController.js +4 -1
  16. package/src/controllers/RelationController.js +19 -21
  17. package/src/controllers/UsersController.js +3 -4
  18. package/src/decorators/parameters.js +3 -1
  19. package/src/decorators/scope.js +1 -1
  20. package/src/errors/ControllerError.js +2 -1
  21. package/src/errors/DatabaseError.js +20 -11
  22. package/src/graph/DitoGraphProcessor.js +48 -40
  23. package/src/graph/expression.js +6 -8
  24. package/src/graph/graph.js +20 -11
  25. package/src/lib/EventEmitter.js +12 -12
  26. package/src/middleware/handleConnectMiddleware.js +16 -10
  27. package/src/middleware/handleError.js +6 -5
  28. package/src/middleware/handleSession.js +78 -0
  29. package/src/middleware/handleUser.js +2 -2
  30. package/src/middleware/index.js +2 -0
  31. package/src/middleware/logRequests.js +3 -3
  32. package/src/middleware/setupRequestStorage.js +14 -0
  33. package/src/mixins/AssetMixin.js +62 -58
  34. package/src/mixins/SessionMixin.js +13 -10
  35. package/src/mixins/TimeStampedMixin.js +33 -29
  36. package/src/mixins/UserMixin.js +130 -116
  37. package/src/models/Model.js +245 -194
  38. package/src/models/definitions/filters.js +14 -13
  39. package/src/query/QueryBuilder.js +252 -195
  40. package/src/query/QueryFilters.js +3 -3
  41. package/src/query/QueryParameters.js +2 -2
  42. package/src/query/Registry.js +8 -10
  43. package/src/schema/keywords/_validate.js +10 -8
  44. package/src/schema/properties.test.js +247 -206
  45. package/src/schema/relations.js +42 -20
  46. package/src/schema/relations.test.js +36 -19
  47. package/src/services/Service.js +8 -14
  48. package/src/storage/S3Storage.js +5 -3
  49. package/src/storage/Storage.js +16 -14
  50. package/src/utils/function.js +7 -4
  51. package/src/utils/function.test.js +30 -6
  52. package/src/utils/object.test.js +5 -1
  53. package/types/index.d.ts +244 -257
  54. package/src/app/SessionStore.js +0 -31
@@ -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, isObject, asArray, isPlainObject, isModule,
24
- hyphenate, clone, merge, parseDataPath, normalizeDataPath,
25
- toPromiseCallback, mapConcurrently
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: log === false || log?.silent || process.env.DITO_SILENT
76
- ? {}
77
- : getOptions(log),
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, path, transacted, middlewares, controller = null, action = null
133
+ method,
134
+ path,
135
+ transacted,
136
+ middlewares,
137
+ controller = null,
138
+ action = null
122
139
  ) {
123
140
  middlewares = asArray(middlewares)
124
- const middleware = middlewares.length > 1
125
- ? compose(middlewares)
126
- : middlewares[0]
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(Object.values(this.storages)
178
- .filter(storage => !storage.initialized)
179
- .map(async storage => {
180
- // Different from models, services and controllers, storages can have
181
- // async `setup()` methods, as used by `S3Storage`.
182
- await storage.setup()
183
- await storage.initialize()
184
- storage.initialized = true
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(Object.values(this.services)
217
- .filter(service => !service.initialized)
218
- .map(async service => {
219
- const { name } = service
220
- const config = this.config.services[name]
221
- if (config === undefined) {
222
- throw new Error(`Configuration missing for service '${name}'`)
223
- }
224
- // As a convention, the configuration of a service can be set to `false`
225
- // in order to entirely deactivate the service.
226
- if (config === false) {
227
- delete this.services[name]
228
- } else {
229
- service.setup(config)
230
- await service.initialize()
231
- service.initialized = true
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(Object.values(this.models)
274
- .filter(modelClass => !modelClass.initialized)
275
- .map(async modelClass => {
276
- // While `setup()` is used for internal dito things, `initialize()` is
277
- // called async and meant to be used by the user, without the need to
278
- // call `super.initialize()`.
279
- modelClass.setup()
280
- await modelClass.initialize()
281
- modelClass.initialized = true
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 => Model.isPrototypeOf(value)
301
- ? `[Model: ${value.name}]`
302
- : value
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(Object.values(this.controllers)
354
- .filter(controller => !controller.initialized)
355
- .map(async controller => {
356
- controller.setup()
357
- await controller.initialize()
358
- // Each controller can also compose their own middleware (or app), e.g.
359
- // as used in `AdminController`:
360
- const composed = controller.compose()
361
- if (composed) {
362
- this.use(mount(controller.url, composed))
363
- }
364
- controller.initialized = true
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
- ? data => validate.call(ctx, data)
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] = error.message.match(/^([\s\S]*) - ([\s\S]*?)$/) ||
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(logRequests())
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(compress(merge(
536
- {
537
- // Use a reasonable default for Brotli compression.
538
- // See https://github.com/koajs/compress/issues/126
539
- br: {
540
- params: {
541
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4
542
- }
543
- }
544
- },
545
- getOptions(app.compress)
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
- const {
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
- target: 'pino-pretty',
616
- options: prettyPrint
617
- }) : null
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 = knex.normalizeDbNames === true
625
- ? {}
626
- : knex.normalizeDbNames
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
- colors: !!this.config.logger.prettyPrint.colorize,
706
- compact: false,
707
- depth: null,
708
- maxArrayLength: null
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 = error instanceof ResponseError && error.status < 500
718
- ? 'info'
719
- : 'error'
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(reject,
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('key', files.map(file => file.key))
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
- getDuration(this.config.assets.cleanupTimeThreshold)
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(files, async file => {
876
- const asset = await AssetModel.query(trx).findOne('key', file.key)
877
- if (!asset) {
878
- if (file.data || file.url) {
879
- let { data } = file
880
- if (!data) {
881
- const { url } = file
882
- if (!storage.isImportSourceAllowed(url)) {
883
- throw new AssetError(
884
- `Unable to import asset from foreign source: '${
885
- file.name
886
- }' ('${
887
- url
888
- }'): The source needs to be explicitly allowed.`
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
- this.logger.info(
892
- `Asset ${
893
- pico.green(`'${file.name}'`)
894
- } is from a foreign source, fetching from ${
895
- pico.green(`'${url}'`)
896
- } and adding to storage ${
897
- pico.green(`'${storage.name}'`)
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
- throw new AssetError(
919
- `Unable to import asset from foreign source: '${
920
- file.name
921
- }' ('${
922
- file.key
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
- } else {
927
- // Asset is from a foreign source, but was already imported and can
928
- // be reused. See above for an explanation of this merge.
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
- `Unable to update modified asset from memory source: '${
955
- file.name
956
- }' ('${
957
- file.key
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
- getDuration(timeThreshold ?? assets.cleanupTimeThreshold)
973
- const danglingTimeThreshold =
974
- getDuration(timeThreshold ?? assets.danglingTimeThreshold)
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 => query
998
+ .andWhere(query =>
999
+ query
986
1000
  .where('updatedAt', '<=', cleanupDate)
987
1001
  .orWhere(
988
- // Protect freshly created assets from being deleted again right
989
- // away, .e.g. when `config.assets.cleanupTimeThreshold = 0`
990
- query => query
991
- .where('updatedAt', '=', ref('createdAt'))
992
- .andWhere('updatedAt', '<=', danglingDate)
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