@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.
Files changed (53) hide show
  1. package/package.json +11 -13
  2. package/src/app/Application.js +226 -179
  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 +98 -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 +33 -29
  29. package/src/middleware/handleUser.js +2 -2
  30. package/src/middleware/index.js +1 -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
@@ -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, isObject, asArray, isPlainObject, isModule,
23
- hyphenate, clone, merge, parseDataPath, normalizeDataPath,
24
- 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
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: log === false || log?.silent || process.env.DITO_SILENT
75
- ? {}
76
- : getOptions(log),
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, path, transacted, middlewares, controller = null, action = null
133
+ method,
134
+ path,
135
+ transacted,
136
+ middlewares,
137
+ controller = null,
138
+ action = null
121
139
  ) {
122
140
  middlewares = asArray(middlewares)
123
- const middleware = middlewares.length > 1
124
- ? compose(middlewares)
125
- : middlewares[0]
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(Object.values(this.storages)
177
- .filter(storage => !storage.initialized)
178
- .map(async storage => {
179
- // Different from models, services and controllers, storages can have
180
- // async `setup()` methods, as used by `S3Storage`.
181
- await storage.setup()
182
- await storage.initialize()
183
- storage.initialized = true
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(Object.values(this.services)
216
- .filter(service => !service.initialized)
217
- .map(async service => {
218
- const { name } = service
219
- const config = this.config.services[name]
220
- if (config === undefined) {
221
- throw new Error(`Configuration missing for service '${name}'`)
222
- }
223
- // As a convention, the configuration of a service can be set to `false`
224
- // in order to entirely deactivate the service.
225
- if (config === false) {
226
- delete this.services[name]
227
- } else {
228
- service.setup(config)
229
- await service.initialize()
230
- service.initialized = true
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(Object.values(this.models)
273
- .filter(modelClass => !modelClass.initialized)
274
- .map(async modelClass => {
275
- // While `setup()` is used for internal dito things, `initialize()` is
276
- // called async and meant to be used by the user, without the need to
277
- // call `super.initialize()`.
278
- modelClass.setup()
279
- await modelClass.initialize()
280
- modelClass.initialized = true
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 => Model.isPrototypeOf(value)
300
- ? `[Model: ${value.name}]`
301
- : value
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(Object.values(this.controllers)
353
- .filter(controller => !controller.initialized)
354
- .map(async controller => {
355
- controller.setup()
356
- await controller.initialize()
357
- // Each controller can also compose their own middleware (or app), e.g.
358
- // as used in `AdminController`:
359
- const composed = controller.compose()
360
- if (composed) {
361
- this.use(mount(controller.url, composed))
362
- }
363
- controller.initialized = true
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
- ? data => validate.call(ctx, data)
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] = error.message.match(/^([\s\S]*) - ([\s\S]*?)$/) ||
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(logRequests())
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(compress(merge(
535
- {
536
- // Use a reasonable default for Brotli compression.
537
- // See https://github.com/koajs/compress/issues/126
538
- br: {
539
- params: {
540
- [zlib.constants.BROTLI_PARAM_QUALITY]: 4
541
- }
542
- }
543
- },
544
- getOptions(app.compress)
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
- target: 'pino-pretty',
585
- options: prettyPrint
586
- }) : null
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 = knex.normalizeDbNames === true
594
- ? {}
595
- : knex.normalizeDbNames
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
- colors: !!this.config.logger.prettyPrint.colorize,
675
- compact: false,
676
- depth: null,
677
- maxArrayLength: null
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 = error instanceof ResponseError && error.status < 500
687
- ? 'info'
688
- : 'error'
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(reject,
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('key', files.map(file => file.key))
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
- getDuration(this.config.assets.cleanupTimeThreshold)
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(files, async file => {
845
- const asset = await AssetModel.query(trx).findOne('key', file.key)
846
- if (!asset) {
847
- if (file.data || file.url) {
848
- let { data } = file
849
- if (!data) {
850
- const { url } = file
851
- if (!storage.isImportSourceAllowed(url)) {
852
- throw new AssetError(
853
- `Unable to import asset from foreign source: '${
854
- file.name
855
- }' ('${
856
- url
857
- }'): 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
+ }...`
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
- this.logger.info(
861
- `Asset ${
862
- pico.green(`'${file.name}'`)
863
- } is from a foreign source, fetching from ${
864
- pico.green(`'${url}'`)
865
- } and adding to storage ${
866
- pico.green(`'${storage.name}'`)
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
- throw new AssetError(
888
- `Unable to import asset from foreign source: '${
889
- file.name
890
- }' ('${
891
- file.key
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
- } else {
896
- // Asset is from a foreign source, but was already imported and can
897
- // be reused. See above for an explanation of this merge.
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
- `Unable to update modified asset from memory source: '${
924
- file.name
925
- }' ('${
926
- file.key
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
- getDuration(timeThreshold ?? assets.cleanupTimeThreshold)
942
- const danglingTimeThreshold =
943
- getDuration(timeThreshold ?? assets.danglingTimeThreshold)
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 => query
998
+ .andWhere(query =>
999
+ query
955
1000
  .where('updatedAt', '<=', cleanupDate)
956
1001
  .orWhere(
957
- // Protect freshly created assets from being deleted again right
958
- // away, .e.g. when `config.assets.cleanupTimeThreshold = 0`
959
- query => query
960
- .where('updatedAt', '=', ref('createdAt'))
961
- .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)
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