@lenne.tech/nest-server 11.32.3 → 11.33.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 (262) hide show
  1. package/.claude/rules/architecture.md +16 -0
  2. package/.claude/rules/configurable-features.md +11 -3
  3. package/.claude/rules/testing.md +101 -1
  4. package/CLAUDE.md +5 -0
  5. package/FRAMEWORK-API.md +58 -3
  6. package/bin/migrate.js +13 -3
  7. package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
  8. package/dist/core/common/helpers/content-disposition.helper.js +14 -0
  9. package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
  10. package/dist/core/common/helpers/file.helper.d.ts +21 -2
  11. package/dist/core/common/helpers/file.helper.js +69 -13
  12. package/dist/core/common/helpers/file.helper.js.map +1 -1
  13. package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
  14. package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
  15. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
  16. package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
  17. package/dist/core/common/helpers/project-name.helper.js +38 -0
  18. package/dist/core/common/helpers/project-name.helper.js.map +1 -0
  19. package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
  20. package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
  21. package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
  22. package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
  23. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
  24. package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
  25. package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
  26. package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
  27. package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
  28. package/dist/core/common/services/core-cron-jobs.service.js +278 -27
  29. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  30. package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
  31. package/dist/core/common/services/core-redis-pubsub.js +112 -0
  32. package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
  33. package/dist/core/common/services/core-redis.service.d.ts +25 -0
  34. package/dist/core/common/services/core-redis.service.js +154 -0
  35. package/dist/core/common/services/core-redis.service.js.map +1 -0
  36. package/dist/core/common/services/core-s3.service.d.ts +51 -0
  37. package/dist/core/common/services/core-s3.service.js +261 -0
  38. package/dist/core/common/services/core-s3.service.js.map +1 -0
  39. package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
  40. package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
  41. package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
  42. package/dist/core/common/services/rate-limit-store.d.ts +61 -0
  43. package/dist/core/common/services/rate-limit-store.js +235 -0
  44. package/dist/core/common/services/rate-limit-store.js.map +1 -0
  45. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
  46. package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
  47. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  48. package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
  49. package/dist/core/modules/ai/services/core-ai.service.js +24 -20
  50. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  51. package/dist/core/modules/auth/core-auth.module.js +4 -1
  52. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  53. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
  54. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
  55. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  56. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
  57. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
  58. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
  60. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
  61. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  62. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
  63. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
  64. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  65. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
  66. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
  67. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  68. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
  69. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  70. package/dist/core/modules/file/core-file.controller.d.ts +15 -3
  71. package/dist/core/modules/file/core-file.controller.js +90 -20
  72. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  73. package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
  74. package/dist/core/modules/file/core-file.resolver.js +23 -16
  75. package/dist/core/modules/file/core-file.resolver.js.map +1 -1
  76. package/dist/core/modules/file/core-file.service.d.ts +41 -7
  77. package/dist/core/modules/file/core-file.service.js +227 -7
  78. package/dist/core/modules/file/core-file.service.js.map +1 -1
  79. package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
  80. package/dist/core/modules/file/file-metadata.helper.js +33 -0
  81. package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
  82. package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
  83. package/dist/core/modules/file/file-roles.helper.js +53 -0
  84. package/dist/core/modules/file/file-roles.helper.js.map +1 -0
  85. package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
  86. package/dist/core/modules/file/file-storage.helper.js +65 -0
  87. package/dist/core/modules/file/file-storage.helper.js.map +1 -0
  88. package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
  89. package/dist/core/modules/file/filesystem-file.helper.js +77 -0
  90. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
  91. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
  92. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
  93. package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
  94. package/dist/core/modules/file/s3-file.helper.js +81 -0
  95. package/dist/core/modules/file/s3-file.helper.js.map +1 -0
  96. package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
  97. package/dist/core/modules/hub/core-hub.controller.js +24 -22
  98. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  99. package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
  100. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
  101. package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
  102. package/dist/core/modules/hub/hub-buffer.js +101 -0
  103. package/dist/core/modules/hub/hub-buffer.js.map +1 -0
  104. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
  105. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
  106. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
  108. package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
  109. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
  111. package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
  112. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
  114. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
  115. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
  116. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
  117. package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
  118. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  119. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +3 -6
  120. package/dist/core/modules/migrate/helpers/migration.helper.js +55 -8
  121. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  122. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  123. package/dist/core/modules/migrate/migration-runner.js +4 -0
  124. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  125. package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
  126. package/dist/core/modules/migrate/mongo-state-store.js +98 -9
  127. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  128. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
  129. package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
  130. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  131. package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
  132. package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
  133. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  134. package/dist/core/modules/tenant/core-tenant.module.js +3 -4
  135. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  136. package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
  137. package/dist/core/modules/tus/core-tus.controller.js +29 -3
  138. package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
  139. package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
  140. package/dist/core/modules/tus/core-tus.service.js +182 -28
  141. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  142. package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
  143. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  144. package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
  145. package/dist/core/modules/tus/tus-redis-locker.js +87 -0
  146. package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
  147. package/dist/core/modules/tus/tus.module.d.ts +1 -0
  148. package/dist/core/modules/tus/tus.module.js +30 -4
  149. package/dist/core/modules/tus/tus.module.js.map +1 -1
  150. package/dist/core.module.js +18 -1
  151. package/dist/core.module.js.map +1 -1
  152. package/dist/index.d.ts +10 -0
  153. package/dist/index.js +10 -0
  154. package/dist/index.js.map +1 -1
  155. package/dist/main.js +2 -1
  156. package/dist/main.js.map +1 -1
  157. package/dist/server/modules/file/file.controller.js.map +1 -1
  158. package/dist/server/modules/file/file.resolver.js +1 -12
  159. package/dist/server/modules/file/file.resolver.js.map +1 -1
  160. package/dist/server/modules/file/file.service.d.ts +5 -1
  161. package/dist/server/modules/file/file.service.js +12 -3
  162. package/dist/server/modules/file/file.service.js.map +1 -1
  163. package/dist/server/modules/user/avatar.controller.d.ts +5 -1
  164. package/dist/server/modules/user/avatar.controller.js +26 -10
  165. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  166. package/dist/server/modules/user/user.model.js +3 -2
  167. package/dist/server/modules/user/user.model.js.map +1 -1
  168. package/dist/server/modules/user/user.module.js +6 -2
  169. package/dist/server/modules/user/user.module.js.map +1 -1
  170. package/dist/server/modules/user/user.service.d.ts +1 -1
  171. package/dist/server/modules/user/user.service.js +6 -13
  172. package/dist/server/modules/user/user.service.js.map +1 -1
  173. package/dist/tsconfig.build.tsbuildinfo +1 -1
  174. package/docs/REQUEST-LIFECYCLE.md +57 -8
  175. package/docs/security-overrides.md +30 -5
  176. package/migration-guides/11.32.3-to-11.32.4.md +335 -0
  177. package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
  178. package/package.json +74 -42
  179. package/src/core/common/helpers/content-disposition.helper.ts +90 -0
  180. package/src/core/common/helpers/file.helper.ts +201 -20
  181. package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
  182. package/src/core/common/helpers/project-name.helper.ts +71 -0
  183. package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
  184. package/src/core/common/interfaces/server-options.interface.ts +439 -2
  185. package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
  186. package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
  187. package/src/core/common/services/core-cron-jobs.service.ts +589 -47
  188. package/src/core/common/services/core-redis-pubsub.ts +183 -0
  189. package/src/core/common/services/core-redis.service.ts +249 -0
  190. package/src/core/common/services/core-s3.service.ts +465 -0
  191. package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
  192. package/src/core/common/services/rate-limit-store.ts +492 -0
  193. package/src/core/modules/ai/README.md +25 -0
  194. package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
  195. package/src/core/modules/ai/services/core-ai.service.ts +33 -24
  196. package/src/core/modules/auth/core-auth.module.ts +7 -1
  197. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
  198. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
  199. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
  200. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
  201. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
  202. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
  203. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
  204. package/src/core/modules/file/README.md +324 -41
  205. package/src/core/modules/file/core-file.controller.ts +267 -19
  206. package/src/core/modules/file/core-file.resolver.ts +46 -12
  207. package/src/core/modules/file/core-file.service.ts +482 -16
  208. package/src/core/modules/file/file-metadata.helper.ts +142 -0
  209. package/src/core/modules/file/file-roles.helper.ts +110 -0
  210. package/src/core/modules/file/file-storage.helper.ts +163 -0
  211. package/src/core/modules/file/filesystem-file.helper.ts +184 -0
  212. package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
  213. package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
  214. package/src/core/modules/file/s3-file.helper.ts +209 -0
  215. package/src/core/modules/hub/README.md +48 -0
  216. package/src/core/modules/hub/core-hub.controller.ts +18 -16
  217. package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
  218. package/src/core/modules/hub/hub-buffer.ts +198 -0
  219. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
  220. package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
  221. package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
  222. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
  223. package/src/core/modules/migrate/README.md +68 -6
  224. package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
  225. package/src/core/modules/migrate/helpers/migration.helper.ts +144 -10
  226. package/src/core/modules/migrate/migration-runner.ts +10 -1
  227. package/src/core/modules/migrate/mongo-state-store.ts +179 -11
  228. package/src/core/modules/system-setup/README.md +7 -2
  229. package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
  230. package/src/core/modules/tenant/README.md +21 -1
  231. package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
  232. package/src/core/modules/tenant/core-tenant.module.ts +8 -4
  233. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
  234. package/src/core/modules/tus/README.md +35 -2
  235. package/src/core/modules/tus/core-tus.controller.ts +64 -12
  236. package/src/core/modules/tus/core-tus.service.ts +358 -34
  237. package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
  238. package/src/core/modules/tus/tus-redis-locker.ts +139 -0
  239. package/src/core/modules/tus/tus.module.ts +78 -6
  240. package/src/core.module.ts +26 -1
  241. package/src/index.ts +10 -0
  242. package/src/main.ts +7 -1
  243. package/src/server/modules/file/file.controller.ts +7 -3
  244. package/src/server/modules/file/file.resolver.ts +5 -17
  245. package/src/server/modules/file/file.service.ts +42 -3
  246. package/src/server/modules/user/avatar.controller.ts +39 -9
  247. package/src/server/modules/user/user.model.ts +9 -3
  248. package/src/server/modules/user/user.module.ts +12 -3
  249. package/src/server/modules/user/user.service.ts +20 -17
  250. package/src/test/README.md +7 -2
  251. package/src/core/common/helpers/logging.helper.spec.ts +0 -61
  252. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
  253. package/src/core/common/services/brevo.service.spec.ts +0 -266
  254. package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
  255. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
  256. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
  257. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
  258. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
  259. package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
  260. package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
  261. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
  262. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
@@ -1,5 +1,6 @@
1
- import { NotFoundException } from '@nestjs/common';
1
+ import { Logger, NotFoundException } from '@nestjs/common';
2
2
  import mongoose, { Connection, mongo, Types } from 'mongoose';
3
+ import { Readable } from 'stream';
3
4
 
4
5
  import { FilterArgs } from '../../common/args/filter.args';
5
6
  import { getObjectIds, getStringIds } from '../../common/helpers/db.helper';
@@ -7,16 +8,43 @@ import { convertFilterArgsToQuery } from '../../common/helpers/filter.helper';
7
8
  import { GridFSHelper } from '../../common/helpers/gridfs.helper';
8
9
  import { check } from '../../common/helpers/input.helper';
9
10
  import { prepareOutput } from '../../common/helpers/service.helper';
11
+ import { ConfigService } from '../../common/services/config.service';
12
+ import { CoreS3Service } from '../../common/services/core-s3.service';
10
13
  import { MaybePromise } from '../../common/types/maybe-promise.type';
11
14
  import { CoreFileInfo } from './core-file-info.model';
12
15
  import { FileServiceOptions } from './interfaces/file-service-options.interface';
13
- import { FileUpload } from './interfaces/file-upload.interface';
16
+ import { FileUploadSource } from './interfaces/file-upload.interface';
17
+ import {
18
+ assertFileStorageAvailable,
19
+ FileStorageDriver,
20
+ FileStorageResolution,
21
+ logFileStorage,
22
+ resolveFileStorage,
23
+ } from './file-storage.helper';
24
+ import {
25
+ DEFAULT_FILESYSTEM_DIR,
26
+ FILESYSTEM_FILES_COLLECTION,
27
+ FilesystemFileHelper,
28
+ FilesystemFileInfo,
29
+ } from './filesystem-file.helper';
30
+ import { S3_FILES_COLLECTION, S3FileHelper, S3FileInfo, streamToBuffer } from './s3-file.helper';
14
31
 
15
32
  /**
16
33
  * Type for checking input
17
34
  */
18
35
  export type FileInputCheckType = 'file' | 'filename' | 'files' | 'filterArgs' | 'id';
19
36
 
37
+ /**
38
+ * Optional dependencies of CoreFileService.
39
+ *
40
+ * Both are required for the S3 storage driver (`file.storage: 's3'`); without
41
+ * them the service behaves exactly as before and stores everything in GridFS.
42
+ */
43
+ export interface CoreFileServiceOptions {
44
+ configService?: ConfigService;
45
+ s3Service?: CoreS3Service;
46
+ }
47
+
20
48
  /**
21
49
  * Abstract core file service
22
50
  */
@@ -24,29 +52,126 @@ export abstract class CoreFileService {
24
52
  // Use the native MongoDB driver's types (accessed via Mongoose's exports) to avoid BSON version conflicts
25
53
  files: mongo.GridFSBucket;
26
54
 
55
+ /** Metadata collection for files stored in S3 (no Mongoose schema) */
56
+ protected s3Files: mongo.Collection<any>;
57
+
58
+ /** Metadata collection for files stored on the local filesystem (no Mongoose schema) */
59
+ protected filesystemFiles: mongo.Collection<any>;
60
+
61
+ protected readonly logger = new Logger(CoreFileService.name);
62
+
63
+ /** Which driver new files are written to, and why */
64
+ protected readonly storageResolution: FileStorageResolution;
65
+
27
66
  /**
28
67
  * Include MongoDB connection and create File bucket
29
68
  */
30
69
  protected constructor(
31
70
  protected readonly connection: Connection,
32
71
  bucketName = 'fs',
72
+ protected readonly options?: CoreFileServiceOptions,
33
73
  ) {
34
74
  // Use the native MongoDB driver's GridFSBucket via Mongoose's mongo export to avoid BSON version conflicts
35
75
  this.files = new mongo.GridFSBucket(connection.db, { bucketName });
76
+ this.s3Files = connection.db.collection(S3_FILES_COLLECTION);
77
+ this.filesystemFiles = connection.db.collection(FILESYSTEM_FILES_COLLECTION);
78
+
79
+ // Resolve from the global config rather than the optional injected
80
+ // ConfigService: a project on the pre-11.33 constructor (`super(connection)`)
81
+ // forwards no services, and must still get a correctly derived driver.
82
+ this.storageResolution = resolveFileStorage(ConfigService.configFastButReadOnly);
83
+ assertFileStorageAvailable(this.storageResolution, this.isStorageAvailable(this.storageResolution.driver));
84
+ logFileStorage(this.storageResolution);
85
+ }
86
+
87
+ /**
88
+ * The driver new files are written to.
89
+ *
90
+ * Reads are not restricted to it: every store is consulted, so files written
91
+ * before a driver change stay readable and switching is forward-only with no
92
+ * migration step.
93
+ */
94
+ protected get storageDriver(): FileStorageDriver {
95
+ return this.storageResolution.driver;
96
+ }
97
+
98
+ /** Directory used by the `'filesystem'` driver */
99
+ protected get filesystemDir(): string {
100
+ return ConfigService.configFastButReadOnly?.file?.storageDir || DEFAULT_FILESYSTEM_DIR;
101
+ }
102
+
103
+ /**
104
+ * Whether a driver can actually be used right now.
105
+ *
106
+ * GridFS and the filesystem need nothing beyond what the constructor already
107
+ * has (a database connection, a writable directory). S3 needs both the client
108
+ * library and the service wired through `super()`.
109
+ */
110
+ protected isStorageAvailable(driver: FileStorageDriver): boolean {
111
+ return driver === 's3' ? !!this.options?.s3Service?.enabled : true;
112
+ }
113
+
114
+ /**
115
+ * @deprecated Use `storageDriver === 's3'`. Kept so an override or a project
116
+ * that read this keeps compiling.
117
+ */
118
+ protected get s3Storage(): boolean {
119
+ return this.storageDriver === 's3';
120
+ }
121
+
122
+ /** Whether new files go to the local filesystem */
123
+ protected get filesystemStorage(): boolean {
124
+ return this.storageDriver === 'filesystem';
125
+ }
126
+
127
+ /**
128
+ * Find the filesystem metadata of a file by ID (null when it is stored elsewhere)
129
+ */
130
+ protected async findFilesystemFileById(id: string | Types.ObjectId): Promise<FilesystemFileInfo | null> {
131
+ return FilesystemFileHelper.findFileById(this.filesystemFiles, getObjectIds(id));
132
+ }
133
+
134
+ /**
135
+ * Find the filesystem metadata of a file by filename (null when it is stored elsewhere)
136
+ */
137
+ protected async findFilesystemFileByName(filename: string): Promise<FilesystemFileInfo | null> {
138
+ return FilesystemFileHelper.findFileByName(this.filesystemFiles, filename);
36
139
  }
37
140
 
38
141
  /**
39
142
  * Save file in DB
40
143
  */
41
- async createFile(file: MaybePromise<FileUpload>, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
144
+ async createFile(file: MaybePromise<FileUploadSource>, serviceOptions?: FileServiceOptions): Promise<CoreFileInfo> {
42
145
  if (!(await this.checkRights(file, { ...serviceOptions, checkInputType: 'file' }))) {
43
146
  return null;
44
147
  }
45
148
  const { createReadStream, filename, mimetype } = await file;
46
149
  const readStream = createReadStream();
150
+ if (this.filesystemStorage) {
151
+ const fsFileInfo = await FilesystemFileHelper.writeFile(this.filesystemDir, this.filesystemFiles, {
152
+ body: readStream,
153
+ contentType: mimetype,
154
+ filename,
155
+ ...(serviceOptions?.metadata ? { metadata: serviceOptions.metadata } : {}),
156
+ });
157
+ return this.prepareOutput(fsFileInfo as unknown as CoreFileInfo, serviceOptions);
158
+ }
159
+ if (this.storageDriver === 's3') {
160
+ // Only buffer when the size is unknown: a GraphQL upload stream carries no length, and the
161
+ // S3 SDK needs one. Callers that know it (TUS) pass `body` + `contentLength` instead and
162
+ // stream straight through — see S3FileHelper.writeFile.
163
+ const s3FileInfo = await S3FileHelper.writeFile(this.options.s3Service, this.s3Files, {
164
+ buffer: await streamToBuffer(readStream),
165
+ contentType: mimetype,
166
+ filename,
167
+ ...(serviceOptions?.metadata ? { metadata: serviceOptions.metadata } : {}),
168
+ });
169
+ return this.prepareOutput(s3FileInfo as unknown as CoreFileInfo, serviceOptions);
170
+ }
47
171
  const fileInfo = await GridFSHelper.writeFileFromStream(this.files, readStream, {
48
172
  contentType: mimetype,
49
173
  filename,
174
+ ...(serviceOptions?.metadata ? { metadata: serviceOptions.metadata } : {}),
50
175
  });
51
176
  return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
52
177
  }
@@ -54,7 +179,10 @@ export abstract class CoreFileService {
54
179
  /**
55
180
  * Save files in DB
56
181
  */
57
- async createFiles(files: MaybePromise<FileUpload>[], serviceOptions?: FileServiceOptions): Promise<CoreFileInfo[]> {
182
+ async createFiles(
183
+ files: MaybePromise<FileUploadSource>[],
184
+ serviceOptions?: FileServiceOptions,
185
+ ): Promise<CoreFileInfo[]> {
58
186
  if (!(await this.checkRights(files, { ...serviceOptions, checkInputType: 'files' }))) {
59
187
  return null;
60
188
  }
@@ -69,9 +197,28 @@ export abstract class CoreFileService {
69
197
  * Duplicate file by name
70
198
  */
71
199
  async duplicateByName(name: string, newName: string): Promise<any> {
72
- return GridFSHelper.openDownloadStreamByName(this.files, name).pipe(
73
- GridFSHelper.openUploadStream(this.files, newName),
74
- );
200
+ // Route through the storage dispatch like every other read/write. Going straight to GridFS
201
+ // meant that with `file.storage: 's3'` the source simply is not there — and the resulting
202
+ // FileNotFound arrives on a stream with NO error handler, so it becomes an uncaught
203
+ // exception that takes the process down instead of a 404.
204
+ const nonGridFsSource = (await this.findS3FileByName(name)) || (await this.findFilesystemFileByName(name));
205
+ if (nonGridFsSource) {
206
+ const source = await this.getFileStreamByName(name);
207
+ return this.createFile({
208
+ createReadStream: () => source,
209
+ filename: newName,
210
+ mimetype: nonGridFsSource.contentType || 'application/octet-stream',
211
+ });
212
+ }
213
+
214
+ return new Promise((resolve, reject) => {
215
+ const downloadStream = GridFSHelper.openDownloadStreamByName(this.files, name);
216
+ downloadStream.on('error', reject);
217
+ const uploadStream = GridFSHelper.openUploadStream(this.files, newName);
218
+ uploadStream.on('error', reject);
219
+ uploadStream.on('finish', () => resolve(uploadStream));
220
+ downloadStream.pipe(uploadStream);
221
+ });
75
222
  }
76
223
 
77
224
  /**
@@ -80,6 +227,19 @@ export abstract class CoreFileService {
80
227
  async duplicateById(id: string): Promise<string> {
81
228
  const objectId = getObjectIds(id);
82
229
  const file = await this.getFileInfo(objectId);
230
+
231
+ // Same dispatch as duplicateByName: a file stored outside GridFS has no GridFS counterpart.
232
+ const nonGridFsSource = (await this.findS3FileById(objectId)) || (await this.findFilesystemFileById(objectId));
233
+ if (nonGridFsSource) {
234
+ const source = await this.getFileStream(objectId);
235
+ const copy = await this.createFile({
236
+ createReadStream: () => source,
237
+ filename: file.filename,
238
+ mimetype: file.contentType || 'application/octet-stream',
239
+ });
240
+ return copy.id;
241
+ }
242
+
83
243
  return new Promise((resolve, reject) => {
84
244
  const downloadStream = GridFSHelper.openDownloadStream(this.files, objectId);
85
245
 
@@ -112,8 +272,68 @@ export abstract class CoreFileService {
112
272
  return null;
113
273
  }
114
274
  const filterQuery = convertFilterArgsToQuery(filterArgs);
115
- const docs = await GridFSHelper.findFiles(this.files, filterQuery[0], filterQuery[1]);
116
- return this.prepareOutput(docs as unknown as CoreFileInfo[], serviceOptions);
275
+ if (this.storageDriver === 'gridfs') {
276
+ // Nothing was ever written to the other stores by this configuration, so the
277
+ // single-store path stays exact: no merging, no over-fetching.
278
+ const docs = await GridFSHelper.findFiles(this.files, filterQuery[0], filterQuery[1]);
279
+ return this.prepareOutput(docs as unknown as CoreFileInfo[], serviceOptions);
280
+ }
281
+
282
+ // Several stores, one page. Applying `limit`/`skip` to each and concatenating returns up to
283
+ // N times the page size and advances the offset independently in each store, so rows are both
284
+ // duplicated and skipped — and a project that switched drivers with files already written
285
+ // under the previous one (an explicitly supported state) hits exactly that. So: fetch enough
286
+ // from each to cover skip+limit, merge, then page the merged result once.
287
+ const { limit, skip, ...rest } = (filterQuery[1] ?? {}) as { limit?: number; skip?: number };
288
+ const upperBound = limit === undefined ? undefined : (skip ?? 0) + limit;
289
+ const pageOptions = upperBound === undefined ? rest : { ...rest, limit: upperBound };
290
+
291
+ const [s3Docs, fsDocs, docs] = await Promise.all([
292
+ S3FileHelper.findFiles(this.s3Files, filterQuery[0], pageOptions),
293
+ FilesystemFileHelper.findFiles(this.filesystemFiles, filterQuery[0], pageOptions),
294
+ GridFSHelper.findFiles(this.files, filterQuery[0], pageOptions),
295
+ ]);
296
+
297
+ // Each store returned its own rows already sorted; concatenating them does NOT
298
+ // preserve that order, so without this re-sort a `sort` in the filter args held
299
+ // only WITHIN a store and the merged page came out interleaved by store. Paging
300
+ // a wrongly-ordered merge also returns the wrong rows, not just the right rows
301
+ // in the wrong order.
302
+ const merged = this.sortMergedFileInfo([...s3Docs, ...fsDocs, ...docs], (rest as { sort?: any }).sort);
303
+ const paged = limit === undefined ? merged.slice(skip ?? 0) : merged.slice(skip ?? 0, (skip ?? 0) + limit);
304
+ return this.prepareOutput(paged as unknown as CoreFileInfo[], serviceOptions);
305
+ }
306
+
307
+ /**
308
+ * Re-apply a Mongo-style sort spec to rows merged from several stores.
309
+ *
310
+ * Only used on the multi-store path — the single-store path never merges and
311
+ * keeps the database's own ordering. Falls back to `uploadDate` descending
312
+ * (newest first), which is the order a file listing is expected in and the one
313
+ * each store already returns on its own.
314
+ */
315
+ protected sortMergedFileInfo<T extends Record<string, any>>(docs: T[], sort?: Record<string, any>): T[] {
316
+ const spec = Object.entries(sort ?? {}).filter(([, direction]) => direction === 1 || direction === -1);
317
+ const effective: [string, number][] = spec.length ? (spec as [string, number][]) : [['uploadDate', -1]];
318
+ return [...docs].sort((a, b) => {
319
+ for (const [field, direction] of effective) {
320
+ const left = a?.[field];
321
+ const right = b?.[field];
322
+ if (left === right) {
323
+ continue;
324
+ }
325
+ // Undefined sorts last regardless of direction — a store that does not
326
+ // carry the field must not win the page just because `undefined < x`.
327
+ if (left === undefined || left === null) {
328
+ return 1;
329
+ }
330
+ if (right === undefined || right === null) {
331
+ return -1;
332
+ }
333
+ return left < right ? -direction : direction;
334
+ }
335
+ return 0;
336
+ });
117
337
  }
118
338
 
119
339
  /**
@@ -123,7 +343,12 @@ export abstract class CoreFileService {
123
343
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
124
344
  return null;
125
345
  }
126
- const fileInfo = await GridFSHelper.findFileById(this.files, getObjectIds(id));
346
+ // Every store is consulted, newest driver first, so a change of `file.storage`
347
+ // never hides files written under the previous one.
348
+ const fileInfo =
349
+ (await this.findS3FileById(id)) ||
350
+ (await this.findFilesystemFileById(id)) ||
351
+ (await GridFSHelper.findFileById(this.files, getObjectIds(id)));
127
352
  return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
128
353
  }
129
354
 
@@ -134,30 +359,139 @@ export abstract class CoreFileService {
134
359
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
135
360
  return null;
136
361
  }
137
- const fileInfo = await GridFSHelper.findFileByName(this.files, filename);
362
+ const fileInfo =
363
+ (await this.findS3FileByName(filename)) ||
364
+ (await this.findFilesystemFileByName(filename)) ||
365
+ (await GridFSHelper.findFileByName(this.files, filename));
138
366
  return this.prepareOutput(fileInfo as unknown as CoreFileInfo, serviceOptions);
139
367
  }
140
368
 
141
369
  /**
142
370
  * Get file stream (for big files) via file ID
143
371
  */
144
- async getFileStream(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
372
+ async getFileStream(
373
+ id: string | Types.ObjectId,
374
+ serviceOptions?: FileServiceOptions,
375
+ knownStore?: FileStorageDriver,
376
+ ): Promise<Readable> {
145
377
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
146
378
  return null;
147
379
  }
380
+ // `knownStore` is a pure optimization, never an authorization shortcut: the
381
+ // rights check above already ran. A caller that has just resolved the file
382
+ // (the controller does, via resolveFile) can pass which store it came from so
383
+ // the same one-to-three metadata round trips are not repeated per download.
384
+ // Omitting it keeps the previous behaviour exactly.
385
+ const store = knownStore ?? (await this.locateFile(id));
386
+ if (store === 's3') {
387
+ return S3FileHelper.getStream(this.options.s3Service, id);
388
+ }
389
+ if (store === 'filesystem') {
390
+ return FilesystemFileHelper.getStream(this.filesystemDir, id);
391
+ }
148
392
  return GridFSHelper.openDownloadStream(this.files, getObjectIds(id)) as mongo.GridFSBucketReadStream;
149
393
  }
150
394
 
151
395
  /**
152
- * Get file stream (for big files) via filename
396
+ * Which store currently holds a file's bytes.
397
+ *
398
+ * Consults the stores in the same order as `getFileInfo()`. Answers `'gridfs'`
399
+ * for an unknown id, which is what the download path did before this existed:
400
+ * GridFS is the terminal fallback and reports the miss itself.
401
+ */
402
+ protected async locateFile(id: string | Types.ObjectId): Promise<FileStorageDriver> {
403
+ if (await this.findS3FileById(id)) {
404
+ return 's3';
405
+ }
406
+ if (await this.findFilesystemFileById(id)) {
407
+ return 'filesystem';
408
+ }
409
+ return 'gridfs';
410
+ }
411
+
412
+ /**
413
+ * Resolve a file's metadata AND which store holds its bytes, in one pass.
414
+ *
415
+ * A download used to answer the same question up to three times — once in
416
+ * `getFileInfo()`, once in `getDownloadUrl()` and once in `getFileStream()` —
417
+ * because each of them probed the stores in turn and then discarded which one
418
+ * had answered. The probe order here is unchanged; what is new is that the
419
+ * answering store is returned alongside the metadata, so the callers stop
420
+ * re-probing.
421
+ *
422
+ * The store is derived from WHICH COLLECTION produced the document, never from
423
+ * anything the document says about itself: the collection is the only source
424
+ * that cannot disagree with where the bytes actually are.
425
+ *
426
+ * Runs the SAME `checkRights()` as `getFileInfo()` — it is a replacement for that
427
+ * call, not a way around it.
153
428
  */
154
- async getFileStreamByName(
155
- filename: string,
429
+ async resolveFile(
430
+ id: string | Types.ObjectId,
156
431
  serviceOptions?: FileServiceOptions,
157
- ): Promise<mongo.GridFSBucketReadStream> {
432
+ ): Promise<{ info: CoreFileInfo; store: FileStorageDriver } | null> {
433
+ if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
434
+ return null;
435
+ }
436
+
437
+ const s3Info = await this.findS3FileById(id);
438
+ if (s3Info) {
439
+ return { info: await this.prepareOutput(s3Info as unknown as CoreFileInfo, serviceOptions), store: 's3' };
440
+ }
441
+
442
+ const fsInfo = await this.findFilesystemFileById(id);
443
+ if (fsInfo) {
444
+ // The COLLECTION a document was found in is what says where the bytes are —
445
+ // same as the S3 branch above. Reading the document's own `storage` marker
446
+ // instead would agree in every sound case and disagree in exactly one: a
447
+ // document sitting in `filesystem-files` while claiming `storage: 's3'`.
448
+ // Believing it there hands `knownStore: 's3'` to `getFileStream()`, which
449
+ // then reads bytes out of the wrong store.
450
+ return {
451
+ info: await this.prepareOutput(fsInfo as unknown as CoreFileInfo, serviceOptions),
452
+ store: 'filesystem',
453
+ };
454
+ }
455
+
456
+ const gridFsInfo = await GridFSHelper.findFileById(this.files, getObjectIds(id));
457
+ if (!gridFsInfo) {
458
+ return null;
459
+ }
460
+ return { info: await this.prepareOutput(gridFsInfo as unknown as CoreFileInfo, serviceOptions), store: 'gridfs' };
461
+ }
462
+
463
+ /**
464
+ * Get a presigned download URL for a file stored in S3.
465
+ *
466
+ * Returns undefined when the file is not in S3 or `s3.presignedDownloads` is
467
+ * disabled — callers then fall back to streaming the file themselves.
468
+ */
469
+ async getDownloadUrl(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions): Promise<string | undefined> {
470
+ if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
471
+ return undefined;
472
+ }
473
+ const fileInfo = await this.findS3FileById(id);
474
+ if (!fileInfo) {
475
+ return undefined;
476
+ }
477
+ return this.options.s3Service.getPresignedDownloadUrl(getObjectIds(id).toHexString(), fileInfo.filename);
478
+ }
479
+
480
+ /**
481
+ * Get file stream (for big files) via filename
482
+ */
483
+ async getFileStreamByName(filename: string, serviceOptions?: FileServiceOptions): Promise<Readable> {
158
484
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
159
485
  return null;
160
486
  }
487
+ const s3FileInfo = await this.findS3FileByName(filename);
488
+ if (s3FileInfo) {
489
+ return S3FileHelper.getStream(this.options.s3Service, s3FileInfo._id);
490
+ }
491
+ const fsFileInfo = await this.findFilesystemFileByName(filename);
492
+ if (fsFileInfo) {
493
+ return FilesystemFileHelper.getStream(this.filesystemDir, fsFileInfo._id);
494
+ }
161
495
  return GridFSHelper.openDownloadStreamByName(this.files, filename);
162
496
  }
163
497
 
@@ -168,6 +502,12 @@ export abstract class CoreFileService {
168
502
  if (!(await this.checkRights(id, { ...serviceOptions, checkInputType: 'id' }))) {
169
503
  return null;
170
504
  }
505
+ if (await this.findS3FileById(id)) {
506
+ return S3FileHelper.getBuffer(this.options.s3Service, id);
507
+ }
508
+ if (await this.findFilesystemFileById(id)) {
509
+ return FilesystemFileHelper.getBuffer(this.filesystemDir, id);
510
+ }
171
511
  return await GridFSHelper.readFileToBuffer(this.files, { _id: getObjectIds(id) });
172
512
  }
173
513
 
@@ -178,6 +518,14 @@ export abstract class CoreFileService {
178
518
  if (!(await this.checkRights(filename, { ...serviceOptions, checkInputType: 'filename' }))) {
179
519
  return null;
180
520
  }
521
+ const s3FileInfo = await this.findS3FileByName(filename);
522
+ if (s3FileInfo) {
523
+ return S3FileHelper.getBuffer(this.options.s3Service, s3FileInfo._id);
524
+ }
525
+ const fsFileInfo = await this.findFilesystemFileByName(filename);
526
+ if (fsFileInfo) {
527
+ return FilesystemFileHelper.getBuffer(this.filesystemDir, fsFileInfo._id);
528
+ }
181
529
  return await GridFSHelper.readFileToBuffer(this.files, { filename });
182
530
  }
183
531
 
@@ -190,6 +538,14 @@ export abstract class CoreFileService {
190
538
  }
191
539
  const objectId = getObjectIds(id);
192
540
  const fileInfo = await this.getFileInfo(objectId, serviceOptions);
541
+ if (await this.findS3FileById(objectId)) {
542
+ await S3FileHelper.deleteFile(this.options.s3Service, this.s3Files, objectId);
543
+ return fileInfo;
544
+ }
545
+ if (await this.findFilesystemFileById(objectId)) {
546
+ await FilesystemFileHelper.deleteFile(this.filesystemDir, this.filesystemFiles, objectId);
547
+ return fileInfo;
548
+ }
193
549
  await GridFSHelper.deleteFile(this.files, objectId);
194
550
  return fileInfo;
195
551
  }
@@ -212,9 +568,119 @@ export abstract class CoreFileService {
212
568
  // Helper methods
213
569
  // ===================================================================================================================
214
570
 
571
+ /**
572
+ * Find the S3 metadata of a file by ID (null when S3 is not usable, or the file is in another store)
573
+ *
574
+ * Gated on whether S3 is USABLE, not on whether it is the active write driver.
575
+ * Gating on the driver made the documented "reads consult every store, so
576
+ * switching is forward-only" promise true only in one direction: adopting S3
577
+ * kept GridFS readable, but switching back to `gridfs`/`filesystem` turned
578
+ * every S3-stored file into a 404. The metadata lookup is a cheap indexed
579
+ * `findOne`; the driver only decides where new bytes GO.
580
+ */
581
+ protected async findS3FileById(id: string | Types.ObjectId): Promise<null | S3FileInfo> {
582
+ if (!this.isStorageAvailable('s3')) {
583
+ return null;
584
+ }
585
+ return S3FileHelper.findFileById(this.s3Files, getObjectIds(id));
586
+ }
587
+
588
+ /**
589
+ * Find the S3 metadata of a file by filename (null when S3 is not usable, or the file is in another store)
590
+ *
591
+ * See {@link findS3FileById} for why this is gated on availability, not on the
592
+ * active driver.
593
+ */
594
+ protected async findS3FileByName(filename: string): Promise<null | S3FileInfo> {
595
+ if (!this.isStorageAvailable('s3')) {
596
+ return null;
597
+ }
598
+ return S3FileHelper.findFileByName(this.s3Files, filename);
599
+ }
600
+
601
+ /**
602
+ * Read a file's raw document, bypassing `prepareOutput`.
603
+ *
604
+ * `getFileInfo()` runs the result through `prepareOutput` → `check()`, which
605
+ * strips fields the current user may not see — including `metadata`. That is
606
+ * correct for a response, and useless for an authorization decision, which
607
+ * has to look at the very field being protected.
608
+ *
609
+ * So this is the read side of a per-file rule: use it inside an overridden
610
+ * `checkRights()`, never to build a response.
611
+ *
612
+ * Checks S3 metadata first, then the filesystem store, then GridFS — the same
613
+ * three stores in the same order as `getFileInfo()`, so a rule written against
614
+ * this sees the same file the download would serve. Getting that order wrong
615
+ * would make an owner check pass on a stale GridFS document while the bytes
616
+ * come from S3; omitting a store would make it decide on a file that is not
617
+ * the one being served at all (see {@link getRawFileInfoByName}).
618
+ *
619
+ * @param id file id
620
+ * @returns the raw document, or null when no file has that id
621
+ */
622
+ protected async getRawFileInfo(id: string | Types.ObjectId): Promise<null | Record<string, any>> {
623
+ const fileInfo =
624
+ (await this.findS3FileById(id)) ||
625
+ (await this.findFilesystemFileById(id)) ||
626
+ (await GridFSHelper.findFileById(this.files, getObjectIds(id)));
627
+ return (fileInfo as unknown as Record<string, any>) || null;
628
+ }
629
+
630
+ /**
631
+ * Read a file's raw document by filename, bypassing `prepareOutput`.
632
+ *
633
+ * Companion to {@link getRawFileInfo} for the `checkInputType: 'filename'`
634
+ * branch of `checkRights()`. Note that the filename route resolves the FIRST
635
+ * match, so a rule built on this is only as strong as filename uniqueness in
636
+ * your project — prefer authorizing by id where you can.
637
+ */
638
+ protected async getRawFileInfoByName(filename: string): Promise<null | Record<string, any>> {
639
+ // Same three stores, in the same order, as getRawFileInfo() and
640
+ // getFileInfoByName(). Skipping the filesystem store here made a by-name
641
+ // ownership rule see a DIFFERENT file set than the download serves: under
642
+ // `file.storage: 'filesystem'` it returned null for a file the route then
643
+ // happily streamed. That fails closed for the documented `!!raw && …` shape
644
+ // and OPEN for the equally natural `if (!raw) return true`.
645
+ const fileInfo =
646
+ (await this.findS3FileByName(filename)) ||
647
+ (await this.findFilesystemFileByName(filename)) ||
648
+ (await GridFSHelper.findFileByName(this.files, filename));
649
+ return (fileInfo as unknown as Record<string, any>) || null;
650
+ }
651
+
215
652
  /**
216
653
  * Check rights before processing file handling
217
654
  * Can throw an exception if the rights do not fit
655
+ *
656
+ * Returning `false` makes the caller answer as if the file did not exist
657
+ * (`404`), so a refusal never confirms that an id is real.
658
+ *
659
+ * The role decorators on the controller and resolver are the COARSE filter —
660
+ * they decide who may reach the endpoint at all, and are configurable via
661
+ * `file.downloadRoles` / `uploadRoles` / `deleteRoles`. This hook is the fine
662
+ * one: it is the only place that can express "…but only their OWN file".
663
+ *
664
+ * `options.currentUser` is populated by the core controller for both download
665
+ * routes. Metadata to compare against must be written at upload time via
666
+ * `serviceOptions.metadata` and read back with `getRawFileInfo()`.
667
+ *
668
+ * @example
669
+ * ```typescript
670
+ * protected override async checkRights(
671
+ * input: any,
672
+ * options?: FileServiceOptions & { checkInputType: FileInputCheckType },
673
+ * ): Promise<boolean> {
674
+ * if (options?.checkInputType !== 'id' || options.force) {
675
+ * return true;
676
+ * }
677
+ * if (options.currentUser?.hasRole([RoleEnum.ADMIN])) {
678
+ * return true;
679
+ * }
680
+ * const raw = await this.getRawFileInfo(input);
681
+ * return !!raw && String(raw.metadata?.ownerId) === String(options.currentUser?.id);
682
+ * }
683
+ * ```
218
684
  */
219
685
  protected checkRights(
220
686
  _input: any,