@kiyasov/platform-hono 1.6.1 → 2.0.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 (235) hide show
  1. package/.claude/settings.local.json +7 -1
  2. package/dist/cjs/src/adapters/hono-adapter.d.ts +1 -0
  3. package/dist/cjs/src/adapters/hono-adapter.js +14 -3
  4. package/dist/cjs/src/adapters/hono-adapter.js.map +1 -1
  5. package/dist/cjs/src/drivers/graphQLUpload/Upload.d.ts +5 -7
  6. package/dist/cjs/src/drivers/graphQLUpload/Upload.js.map +1 -1
  7. package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
  8. package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js +111 -58
  9. package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
  10. package/dist/cjs/src/drivers/graphQLUpload/index.d.ts +9 -3
  11. package/dist/cjs/src/drivers/graphQLUpload/index.js +21 -3
  12. package/dist/cjs/src/drivers/graphQLUpload/index.js.map +1 -1
  13. package/dist/cjs/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
  14. package/dist/cjs/src/drivers/graphQLUpload/processRequest.js +43 -37
  15. package/dist/cjs/src/drivers/graphQLUpload/processRequest.js.map +1 -1
  16. package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
  17. package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js +47 -0
  18. package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
  19. package/dist/cjs/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
  20. package/dist/cjs/src/drivers/graphQLUpload/storage/index.js +20 -0
  21. package/dist/cjs/src/drivers/graphQLUpload/storage/index.js.map +1 -0
  22. package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
  23. package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js +31 -0
  24. package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
  25. package/dist/cjs/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
  26. package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js +3 -0
  27. package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
  28. package/dist/cjs/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
  29. package/dist/cjs/src/drivers/graphQLUpload/utils/file.js +62 -0
  30. package/dist/cjs/src/drivers/graphQLUpload/utils/file.js.map +1 -0
  31. package/dist/cjs/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
  32. package/dist/cjs/src/drivers/graphQLUpload/utils/index.js +19 -0
  33. package/dist/cjs/src/drivers/graphQLUpload/utils/index.js.map +1 -0
  34. package/dist/cjs/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
  35. package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js +171 -0
  36. package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
  37. package/dist/cjs/src/multer/index.d.ts +1 -0
  38. package/dist/cjs/src/multer/index.js +1 -0
  39. package/dist/cjs/src/multer/index.js.map +1 -1
  40. package/dist/cjs/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
  41. package/dist/cjs/src/multer/interceptors/any-files-interceptor.js +6 -23
  42. package/dist/cjs/src/multer/interceptors/any-files-interceptor.js.map +1 -1
  43. package/dist/cjs/src/multer/interceptors/base-interceptor.d.ts +6 -0
  44. package/dist/cjs/src/multer/interceptors/base-interceptor.js +26 -0
  45. package/dist/cjs/src/multer/interceptors/base-interceptor.js.map +1 -0
  46. package/dist/cjs/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
  47. package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js +7 -24
  48. package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
  49. package/dist/cjs/src/multer/interceptors/file-interceptor.d.ts +2 -2
  50. package/dist/cjs/src/multer/interceptors/file-interceptor.js +6 -23
  51. package/dist/cjs/src/multer/interceptors/file-interceptor.js.map +1 -1
  52. package/dist/cjs/src/multer/interceptors/files-interceptor.d.ts +2 -2
  53. package/dist/cjs/src/multer/interceptors/files-interceptor.js +6 -23
  54. package/dist/cjs/src/multer/interceptors/files-interceptor.js.map +1 -1
  55. package/dist/cjs/src/multer/interceptors/index.d.ts +1 -0
  56. package/dist/cjs/src/multer/interceptors/index.js +1 -0
  57. package/dist/cjs/src/multer/interceptors/index.js.map +1 -1
  58. package/dist/cjs/src/multer/multipart/handlers/any-files.d.ts +2 -8
  59. package/dist/cjs/src/multer/multipart/handlers/any-files.js +12 -25
  60. package/dist/cjs/src/multer/multipart/handlers/any-files.js.map +1 -1
  61. package/dist/cjs/src/multer/multipart/handlers/base-handler.d.ts +42 -0
  62. package/dist/cjs/src/multer/multipart/handlers/base-handler.js +106 -0
  63. package/dist/cjs/src/multer/multipart/handlers/base-handler.js.map +1 -0
  64. package/dist/cjs/src/multer/multipart/handlers/file-fields.d.ts +3 -10
  65. package/dist/cjs/src/multer/multipart/handlers/file-fields.js +19 -33
  66. package/dist/cjs/src/multer/multipart/handlers/file-fields.js.map +1 -1
  67. package/dist/cjs/src/multer/multipart/handlers/index.d.ts +6 -1
  68. package/dist/cjs/src/multer/multipart/handlers/index.js +13 -0
  69. package/dist/cjs/src/multer/multipart/handlers/index.js.map +1 -1
  70. package/dist/cjs/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
  71. package/dist/cjs/src/multer/multipart/handlers/multiple-files.js +18 -36
  72. package/dist/cjs/src/multer/multipart/handlers/multiple-files.js.map +1 -1
  73. package/dist/cjs/src/multer/multipart/handlers/single-file.d.ts +2 -8
  74. package/dist/cjs/src/multer/multipart/handlers/single-file.js +11 -33
  75. package/dist/cjs/src/multer/multipart/handlers/single-file.js.map +1 -1
  76. package/dist/cjs/src/multer/multipart/index.d.ts +1 -1
  77. package/dist/cjs/src/multer/multipart/options.d.ts +10 -16
  78. package/dist/cjs/src/multer/multipart/options.js.map +1 -1
  79. package/dist/cjs/src/multer/multipart/request.js +14 -3
  80. package/dist/cjs/src/multer/multipart/request.js.map +1 -1
  81. package/dist/cjs/src/multer/storage/disk-storage.d.ts +2 -1
  82. package/dist/cjs/src/multer/storage/disk-storage.js +2 -1
  83. package/dist/cjs/src/multer/storage/disk-storage.js.map +1 -1
  84. package/dist/cjs/src/multer/storage/memory-storage.d.ts +2 -11
  85. package/dist/cjs/src/multer/storage/memory-storage.js +6 -4
  86. package/dist/cjs/src/multer/storage/memory-storage.js.map +1 -1
  87. package/dist/cjs/src/multer/storage/storage.d.ts +6 -5
  88. package/dist/cjs/src/multer/utils/file.d.ts +6 -0
  89. package/dist/cjs/src/multer/utils/file.js +62 -0
  90. package/dist/cjs/src/multer/utils/file.js.map +1 -0
  91. package/dist/cjs/src/multer/utils/index.d.ts +2 -0
  92. package/dist/cjs/src/multer/utils/index.js +19 -0
  93. package/dist/cjs/src/multer/utils/index.js.map +1 -0
  94. package/dist/cjs/src/multer/utils/validators.d.ts +18 -0
  95. package/dist/cjs/src/multer/utils/validators.js +171 -0
  96. package/dist/cjs/src/multer/utils/validators.js.map +1 -0
  97. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  98. package/dist/esm/src/adapters/hono-adapter.d.ts +1 -0
  99. package/dist/esm/src/adapters/hono-adapter.js +14 -3
  100. package/dist/esm/src/adapters/hono-adapter.js.map +1 -1
  101. package/dist/esm/src/drivers/graphQLUpload/Upload.d.ts +5 -7
  102. package/dist/esm/src/drivers/graphQLUpload/Upload.js.map +1 -1
  103. package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
  104. package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js +112 -59
  105. package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
  106. package/dist/esm/src/drivers/graphQLUpload/index.d.ts +9 -3
  107. package/dist/esm/src/drivers/graphQLUpload/index.js +7 -3
  108. package/dist/esm/src/drivers/graphQLUpload/index.js.map +1 -1
  109. package/dist/esm/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
  110. package/dist/esm/src/drivers/graphQLUpload/processRequest.js +42 -36
  111. package/dist/esm/src/drivers/graphQLUpload/processRequest.js.map +1 -1
  112. package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
  113. package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js +43 -0
  114. package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
  115. package/dist/esm/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
  116. package/dist/esm/src/drivers/graphQLUpload/storage/index.js +4 -0
  117. package/dist/esm/src/drivers/graphQLUpload/storage/index.js.map +1 -0
  118. package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
  119. package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js +27 -0
  120. package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
  121. package/dist/esm/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
  122. package/dist/esm/src/drivers/graphQLUpload/storage/storage.js +2 -0
  123. package/dist/esm/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
  124. package/dist/esm/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
  125. package/dist/esm/src/drivers/graphQLUpload/utils/file.js +54 -0
  126. package/dist/esm/src/drivers/graphQLUpload/utils/file.js.map +1 -0
  127. package/dist/esm/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
  128. package/dist/esm/src/drivers/graphQLUpload/utils/index.js +3 -0
  129. package/dist/esm/src/drivers/graphQLUpload/utils/index.js.map +1 -0
  130. package/dist/esm/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
  131. package/dist/esm/src/drivers/graphQLUpload/utils/validators.js +167 -0
  132. package/dist/esm/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
  133. package/dist/esm/src/multer/index.d.ts +1 -0
  134. package/dist/esm/src/multer/index.js +1 -0
  135. package/dist/esm/src/multer/index.js.map +1 -1
  136. package/dist/esm/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
  137. package/dist/esm/src/multer/interceptors/any-files-interceptor.js +6 -23
  138. package/dist/esm/src/multer/interceptors/any-files-interceptor.js.map +1 -1
  139. package/dist/esm/src/multer/interceptors/base-interceptor.d.ts +6 -0
  140. package/dist/esm/src/multer/interceptors/base-interceptor.js +23 -0
  141. package/dist/esm/src/multer/interceptors/base-interceptor.js.map +1 -0
  142. package/dist/esm/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
  143. package/dist/esm/src/multer/interceptors/file-fields-interceptor.js +7 -24
  144. package/dist/esm/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
  145. package/dist/esm/src/multer/interceptors/file-interceptor.d.ts +2 -2
  146. package/dist/esm/src/multer/interceptors/file-interceptor.js +6 -23
  147. package/dist/esm/src/multer/interceptors/file-interceptor.js.map +1 -1
  148. package/dist/esm/src/multer/interceptors/files-interceptor.d.ts +2 -2
  149. package/dist/esm/src/multer/interceptors/files-interceptor.js +6 -23
  150. package/dist/esm/src/multer/interceptors/files-interceptor.js.map +1 -1
  151. package/dist/esm/src/multer/interceptors/index.d.ts +1 -0
  152. package/dist/esm/src/multer/interceptors/index.js +1 -0
  153. package/dist/esm/src/multer/interceptors/index.js.map +1 -1
  154. package/dist/esm/src/multer/multipart/handlers/any-files.d.ts +2 -8
  155. package/dist/esm/src/multer/multipart/handlers/any-files.js +12 -25
  156. package/dist/esm/src/multer/multipart/handlers/any-files.js.map +1 -1
  157. package/dist/esm/src/multer/multipart/handlers/base-handler.d.ts +42 -0
  158. package/dist/esm/src/multer/multipart/handlers/base-handler.js +102 -0
  159. package/dist/esm/src/multer/multipart/handlers/base-handler.js.map +1 -0
  160. package/dist/esm/src/multer/multipart/handlers/file-fields.d.ts +3 -10
  161. package/dist/esm/src/multer/multipart/handlers/file-fields.js +19 -33
  162. package/dist/esm/src/multer/multipart/handlers/file-fields.js.map +1 -1
  163. package/dist/esm/src/multer/multipart/handlers/index.d.ts +6 -1
  164. package/dist/esm/src/multer/multipart/handlers/index.js +6 -1
  165. package/dist/esm/src/multer/multipart/handlers/index.js.map +1 -1
  166. package/dist/esm/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
  167. package/dist/esm/src/multer/multipart/handlers/multiple-files.js +18 -36
  168. package/dist/esm/src/multer/multipart/handlers/multiple-files.js.map +1 -1
  169. package/dist/esm/src/multer/multipart/handlers/single-file.d.ts +2 -8
  170. package/dist/esm/src/multer/multipart/handlers/single-file.js +11 -33
  171. package/dist/esm/src/multer/multipart/handlers/single-file.js.map +1 -1
  172. package/dist/esm/src/multer/multipart/index.d.ts +1 -1
  173. package/dist/esm/src/multer/multipart/options.d.ts +10 -16
  174. package/dist/esm/src/multer/multipart/options.js.map +1 -1
  175. package/dist/esm/src/multer/multipart/request.js +14 -3
  176. package/dist/esm/src/multer/multipart/request.js.map +1 -1
  177. package/dist/esm/src/multer/storage/disk-storage.d.ts +2 -1
  178. package/dist/esm/src/multer/storage/disk-storage.js +2 -1
  179. package/dist/esm/src/multer/storage/disk-storage.js.map +1 -1
  180. package/dist/esm/src/multer/storage/memory-storage.d.ts +2 -11
  181. package/dist/esm/src/multer/storage/memory-storage.js +6 -4
  182. package/dist/esm/src/multer/storage/memory-storage.js.map +1 -1
  183. package/dist/esm/src/multer/storage/storage.d.ts +6 -5
  184. package/dist/esm/src/multer/utils/file.d.ts +6 -0
  185. package/dist/esm/src/multer/utils/file.js +54 -0
  186. package/dist/esm/src/multer/utils/file.js.map +1 -0
  187. package/dist/esm/src/multer/utils/index.d.ts +2 -0
  188. package/dist/esm/src/multer/utils/index.js +3 -0
  189. package/dist/esm/src/multer/utils/index.js.map +1 -0
  190. package/dist/esm/src/multer/utils/validators.d.ts +18 -0
  191. package/dist/esm/src/multer/utils/validators.js +167 -0
  192. package/dist/esm/src/multer/utils/validators.js.map +1 -0
  193. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  194. package/package.json +6 -4
  195. package/src/adapters/hono-adapter.ts +18 -3
  196. package/src/drivers/graphQLUpload/Upload.ts +21 -14
  197. package/src/drivers/graphQLUpload/fs-capacitor.ts +240 -116
  198. package/src/drivers/graphQLUpload/index.ts +37 -3
  199. package/src/drivers/graphQLUpload/processRequest.ts +92 -38
  200. package/src/drivers/graphQLUpload/storage/capacitor-storage.ts +86 -0
  201. package/src/drivers/graphQLUpload/storage/index.ts +3 -0
  202. package/src/drivers/graphQLUpload/storage/memory-storage.ts +62 -0
  203. package/src/drivers/graphQLUpload/storage/storage.ts +52 -0
  204. package/src/drivers/graphQLUpload/utils/file.ts +109 -0
  205. package/src/drivers/graphQLUpload/utils/index.ts +2 -0
  206. package/src/drivers/graphQLUpload/utils/validators.ts +219 -0
  207. package/src/multer/index.ts +1 -0
  208. package/src/multer/interceptors/any-files-interceptor.ts +12 -43
  209. package/src/multer/interceptors/base-interceptor.ts +54 -0
  210. package/src/multer/interceptors/file-fields-interceptor.ts +14 -48
  211. package/src/multer/interceptors/file-interceptor.ts +12 -44
  212. package/src/multer/interceptors/files-interceptor.ts +13 -45
  213. package/src/multer/interceptors/index.ts +1 -0
  214. package/src/multer/multipart/handlers/any-files.ts +14 -32
  215. package/src/multer/multipart/handlers/base-handler.ts +204 -0
  216. package/src/multer/multipart/handlers/file-fields.ts +29 -57
  217. package/src/multer/multipart/handlers/index.ts +11 -1
  218. package/src/multer/multipart/handlers/multiple-files.ts +23 -54
  219. package/src/multer/multipart/handlers/single-file.ts +14 -47
  220. package/src/multer/multipart/index.ts +1 -1
  221. package/src/multer/multipart/options.ts +26 -8
  222. package/src/multer/multipart/request.ts +19 -3
  223. package/src/multer/storage/disk-storage.ts +2 -1
  224. package/src/multer/storage/memory-storage.ts +13 -6
  225. package/src/multer/storage/storage.ts +12 -5
  226. package/src/multer/utils/file.ts +109 -0
  227. package/src/multer/utils/index.ts +2 -0
  228. package/src/multer/utils/validators.ts +219 -0
  229. package/test/README.md +247 -0
  230. package/test/graphql-upload.test.ts +509 -0
  231. package/test/helpers.ts +70 -0
  232. package/test/integration.test.ts +197 -0
  233. package/test/interceptors-e2e.test.ts +362 -0
  234. package/test/multipart-upload.test.ts +354 -0
  235. package/test/smoke.test.ts +227 -0
@@ -1,11 +1,9 @@
1
1
  import { BadRequestException } from '@nestjs/common';
2
- import { BodyData } from 'hono/utils/body';
3
2
 
4
3
  import { StorageFile } from '../../storage/storage';
5
- import { removeStorageFiles } from '../file';
6
- import { filterUpload } from '../filter';
7
4
  import { UploadOptions } from '../options';
8
- import { THonoRequest, getParts } from '../request';
5
+ import { THonoRequest } from '../request';
6
+ import { FileHandler, FileFieldsResult } from './base-handler';
9
7
 
10
8
  export interface UploadField {
11
9
  name: string;
@@ -16,74 +14,48 @@ export type UploadFieldMapEntry = Required<Pick<UploadField, 'maxCount'>>;
16
14
 
17
15
  /**
18
16
  * Converts an array of upload fields into a map for easy lookup.
19
- * @param {UploadField[]} uploadFields - Array of upload field definitions.
20
- * @returns {Map<string, UploadFieldMapEntry>} A map of field names to their max count settings.
21
17
  */
22
- export const uploadFieldsToMap = (uploadFields: UploadField[]) =>
18
+ export const uploadFieldsToMap = (
19
+ uploadFields: UploadField[],
20
+ ): Map<string, UploadFieldMapEntry> =>
23
21
  new Map(
24
22
  uploadFields.map(({ name, ...opts }) => [name, { maxCount: 1, ...opts }]),
25
23
  );
26
24
 
27
25
  /**
28
26
  * Handles multipart file fields by processing form-data parts.
29
- * @param {THonoRequest} req - The incoming request object.
30
- * @param {Map<string, UploadFieldMapEntry>} fieldsMap - A map of allowed upload fields.
31
- * @param {UploadOptions} options - Upload options including storage handler.
32
- * @returns {Promise<{ body: BodyData, files: Record<string, StorageFile[]>, remove: () => Promise<void> }>} The parsed body and files with a removal function.
33
27
  */
34
28
  export const handleMultipartFileFields = async (
35
29
  req: THonoRequest,
36
30
  fieldsMap: Map<string, UploadFieldMapEntry>,
37
31
  options: UploadOptions,
38
- ): Promise<{
39
- body: BodyData;
40
- files: Record<string, StorageFile[]>;
41
- remove: () => Promise<void>;
42
- }> => {
43
- const parts = getParts(req, options);
44
- const body: BodyData = {};
32
+ ): Promise<FileFieldsResult> => {
33
+ const handler = new FileHandler(req, options);
45
34
  const files: Record<string, StorageFile[]> = {};
46
35
 
47
- /**
48
- * Removes stored files in case of an error or cleanup.
49
- * @param {boolean} [error=false] - Whether the removal is due to an error.
50
- * @returns {Promise<void>} Resolves after files are removed.
51
- */
52
- const removeFiles = async (error?: boolean): Promise<void> => {
53
- const allFiles = Object.values(files).flat();
54
- return removeStorageFiles(options.storage!, allFiles, error);
55
- };
56
-
57
- try {
58
- for await (const [fieldName, part] of Object.entries(parts)) {
59
- if (!(part instanceof File)) {
60
- body[fieldName] = part;
61
- continue;
62
- }
63
-
64
- const fieldOptions = fieldsMap.get(fieldName);
65
- if (!fieldOptions) {
66
- throw new BadRequestException(
67
- `Field ${fieldName} doesn't accept files`,
68
- );
69
- }
70
-
71
- files[fieldName] = files[fieldName] || [];
72
- if (files[fieldName].length >= fieldOptions.maxCount) {
73
- throw new BadRequestException(
74
- `Field ${fieldName} accepts max ${fieldOptions.maxCount} files`,
75
- );
76
- }
36
+ await handler.process(async (fieldName, part) => {
37
+ const fieldOptions = fieldsMap.get(fieldName);
38
+ if (!fieldOptions) {
39
+ throw new BadRequestException(`Field ${fieldName} doesn't accept files`);
40
+ }
77
41
 
78
- const file = await options.storage!.handleFile(part, req, fieldName);
79
- if (await filterUpload(options, req, file)) {
80
- files[fieldName].push(file);
81
- }
42
+ files[fieldName] = files[fieldName] || [];
43
+ handler.validateMaxCount(
44
+ fieldName,
45
+ files[fieldName].length,
46
+ fieldOptions.maxCount,
47
+ );
48
+
49
+ const storageFile = await handler.handleSingleFile(fieldName, part);
50
+ if (storageFile) {
51
+ files[fieldName].push(storageFile);
52
+ handler.addFile(fieldName, storageFile);
82
53
  }
83
- } catch (error) {
84
- await removeFiles(true);
85
- throw error;
86
- }
54
+ });
87
55
 
88
- return { body, files, remove: removeFiles };
56
+ return {
57
+ body: handler.getBody(),
58
+ files,
59
+ remove: handler.createRemoveFunction(),
60
+ };
89
61
  };
@@ -1 +1,11 @@
1
- export { UploadField } from './file-fields';
1
+ export { UploadField, uploadFieldsToMap } from './file-fields';
2
+ export { handleMultipartSingleFile } from './single-file';
3
+ export { handleMultipartMultipleFiles } from './multiple-files';
4
+ export { handleMultipartFileFields } from './file-fields';
5
+ export { handleMultipartAnyFiles } from './any-files';
6
+ export {
7
+ FileHandler,
8
+ SingleFileResult,
9
+ MultipleFilesResult,
10
+ FileFieldsResult,
11
+ } from './base-handler';
@@ -1,70 +1,39 @@
1
1
  import { BadRequestException } from '@nestjs/common';
2
- import { BodyData } from 'hono/utils/body';
3
2
 
4
- import { StorageFile } from '../../storage';
5
- import { removeStorageFiles } from '../file';
6
- import { filterUpload } from '../filter';
3
+ import { StorageFile } from '../../storage/storage';
7
4
  import { UploadOptions } from '../options';
8
- import { THonoRequest, getParts } from '../request';
5
+ import { THonoRequest } from '../request';
6
+ import { FileHandler, MultipleFilesResult } from './base-handler';
9
7
 
10
8
  export const handleMultipartMultipleFiles = async (
11
9
  req: THonoRequest,
12
10
  fieldname: string,
13
11
  maxCount: number,
14
12
  options: UploadOptions,
15
- ) => {
16
- const parts = getParts(req, options);
17
- const body: BodyData = {};
18
-
13
+ ): Promise<MultipleFilesResult> => {
14
+ const handler = new FileHandler(req, options);
19
15
  const files: StorageFile[] = [];
20
16
 
21
- const removeFiles = async (error?: boolean) => {
22
- return await removeStorageFiles(options.storage!, files, error);
23
- };
24
-
25
- try {
26
- for await (const [partFieldName, part] of Object.entries(parts)) {
27
- if (!(part instanceof File || Array.isArray(part))) {
28
- body[partFieldName] = part;
29
- continue;
30
- }
31
-
32
- const partArray = Array.isArray(part) ? part : [part];
33
-
34
- for (const singlePart of partArray) {
35
- if (!(singlePart instanceof File)) {
36
- throw new BadRequestException(
37
- `Field ${partFieldName} contains invalid file data`,
38
- );
39
- }
40
-
41
- if (partFieldName !== fieldname) {
42
- throw new BadRequestException(
43
- `Field ${partFieldName} doesn't accept files`,
44
- );
45
- }
46
-
47
- if (files.length >= maxCount) {
48
- throw new BadRequestException(
49
- `Field ${partFieldName} accepts max ${maxCount} files`,
50
- );
51
- }
17
+ await handler.process(async (fieldName, part) => {
18
+ if (!(part instanceof File)) {
19
+ throw new BadRequestException(
20
+ `Field ${fieldName} contains invalid file data`,
21
+ );
22
+ }
52
23
 
53
- const file = await options.storage!.handleFile(
54
- singlePart,
55
- req,
56
- partFieldName,
57
- );
24
+ handler.validateFieldName(fieldName, fieldname);
25
+ handler.validateMaxCount(fieldName, files.length, maxCount);
58
26
 
59
- if (await filterUpload(options, req, file)) {
60
- files.push(file);
61
- }
62
- }
27
+ const storageFile = await handler.handleSingleFile(fieldName, part);
28
+ if (storageFile) {
29
+ files.push(storageFile);
30
+ handler.addFile(fieldName, storageFile);
63
31
  }
64
- } catch (error) {
65
- await removeFiles(error);
66
- throw error;
67
- }
32
+ });
68
33
 
69
- return { body, files, remove: () => removeFiles() };
34
+ return {
35
+ body: handler.getBody(),
36
+ files,
37
+ remove: handler.createRemoveFunction(),
38
+ };
70
39
  };
@@ -1,10 +1,7 @@
1
- import { BadRequestException } from '@nestjs/common';
2
- import { BodyData } from 'hono/utils/body';
3
-
4
- import { StorageFile } from '../../storage';
5
- import { filterUpload } from '../filter';
1
+ import { StorageFile } from '../../storage/storage';
6
2
  import { UploadOptions } from '../options';
7
- import { THonoRequest, getParts } from '../request';
3
+ import { THonoRequest } from '../request';
4
+ import { FileHandler, SingleFileResult } from './base-handler';
8
5
 
9
6
  /**
10
7
  * Handles a single file upload in a multipart request.
@@ -17,53 +14,23 @@ export const handleMultipartSingleFile = async (
17
14
  req: THonoRequest,
18
15
  fieldname: string,
19
16
  options: UploadOptions,
20
- ) => {
21
- const parts = getParts(req, options);
22
- const body: BodyData = {};
17
+ ): Promise<SingleFileResult> => {
18
+ const handler = new FileHandler(req, options);
23
19
  let file: StorageFile | undefined;
24
20
 
25
- /**
26
- * Removes uploaded file in case of an error or cleanup.
27
- * @param error - Whether the removal is due to an error.
28
- */
29
- const removeFiles = async (error?: boolean) => {
30
- if (!file) return;
31
- await options.storage!.removeFile(file, error);
32
- };
33
-
34
- try {
35
- for await (const [partFieldName, part] of Object.entries(parts)) {
36
- if (!(part instanceof File)) {
37
- body[partFieldName] = part;
38
- continue;
39
- }
40
-
41
- if (partFieldName !== fieldname) {
42
- throw new BadRequestException(
43
- `Field "${partFieldName}" doesn't accept file.`,
44
- );
45
- }
46
-
47
- if (file) {
48
- throw new BadRequestException(
49
- `Field "${fieldname}" accepts only one file.`,
50
- );
51
- }
52
-
53
- const _file = await options.storage!.handleFile(part, req, partFieldName);
21
+ await handler.process(async (fieldName, part) => {
22
+ handler.validateFieldName(fieldName, fieldname);
23
+ handler.validateSingleFile(file);
54
24
 
55
- if (await filterUpload(options, req, _file)) {
56
- file = _file;
57
- }
25
+ const storageFile = await handler.handleSingleFile(fieldName, part);
26
+ if (storageFile) {
27
+ file = storageFile;
58
28
  }
59
- } catch (error) {
60
- await removeFiles(true);
61
- throw error;
62
- }
29
+ });
63
30
 
64
31
  return {
65
- body,
32
+ body: handler.getBody(),
66
33
  file,
67
- remove: () => removeFiles(),
34
+ remove: handler.createRemoveFunction(),
68
35
  };
69
36
  };
@@ -1,3 +1,3 @@
1
1
  export * from './options';
2
2
  export * from './filter';
3
- export { UploadFilterFile, UploadFilterHandler } from './filter';
3
+ export { type UploadFilterFile, type UploadFilterHandler } from './filter';
@@ -1,20 +1,38 @@
1
- import busboy from 'busboy';
2
-
3
- import { DiskStorage, MemoryStorage, Storage } from '../storage';
1
+ import { DiskStorage, MemoryStorage, Storage, StorageFile } from '../storage';
4
2
  import { UploadFilterHandler } from './filter';
5
3
 
6
- export type UploadOptions = busboy.BusboyConfig & {
4
+ /**
5
+ * Upload limits configuration
6
+ */
7
+ export interface UploadLimits {
8
+ /** Maximum file size in bytes */
9
+ fileSize?: number;
10
+ /** Maximum number of files for a field */
11
+ files?: number;
12
+ /** Maximum number of fields (for file fields upload) */
13
+ fields?: number;
14
+ }
15
+
16
+ /**
17
+ * File upload options
18
+ */
19
+ export type UploadOptions = {
20
+ /** Destination directory for disk storage */
7
21
  dest?: string;
8
- storage?: Storage;
22
+ /** Storage implementation */
23
+ storage?: Storage<StorageFile>;
24
+ /** File filter function */
9
25
  filter?: UploadFilterHandler;
26
+ /** Upload limits */
27
+ limits?: UploadLimits;
10
28
  };
11
29
 
12
30
  export const DEFAULT_UPLOAD_OPTIONS: Partial<UploadOptions> = {
13
31
  storage: new MemoryStorage(),
14
32
  };
15
33
 
16
- export const transformUploadOptions = (opts?: UploadOptions) => {
17
- if (opts == null) return DEFAULT_UPLOAD_OPTIONS;
34
+ export const transformUploadOptions = (opts?: UploadOptions): UploadOptions => {
35
+ if (opts == null) return DEFAULT_UPLOAD_OPTIONS as UploadOptions;
18
36
 
19
37
  if (opts.dest != null) {
20
38
  return {
@@ -26,5 +44,5 @@ export const transformUploadOptions = (opts?: UploadOptions) => {
26
44
  };
27
45
  }
28
46
 
29
- return { ...DEFAULT_UPLOAD_OPTIONS, ...opts };
47
+ return { ...DEFAULT_UPLOAD_OPTIONS, ...opts } as UploadOptions;
30
48
  };
@@ -36,10 +36,26 @@ export const getParts = (
36
36
  ): BodyData => {
37
37
  const parts = req.body ?? {};
38
38
 
39
- for (const [key, file] of Object.entries(parts)) {
40
- if (file instanceof File) {
39
+ for (const [key, value] of Object.entries(parts)) {
40
+ // Handle array of files
41
+ if (Array.isArray(value) && value.every((item) => item instanceof File)) {
41
42
  const maxSize = options?.limits?.fileSize;
42
- if (maxSize && file.size > maxSize) {
43
+ if (maxSize) {
44
+ for (const file of value) {
45
+ if (file.size > maxSize) {
46
+ throw new BadRequestException(
47
+ `File "${key}" is too large. Maximum size is ${maxSize} bytes.`,
48
+ );
49
+ }
50
+ }
51
+ }
52
+ continue;
53
+ }
54
+
55
+ // Handle single file
56
+ if (value instanceof File) {
57
+ const maxSize = options?.limits?.fileSize;
58
+ if (maxSize && value.size > maxSize) {
43
59
  throw new BadRequestException(
44
60
  `File "${key}" is too large. Maximum size is ${maxSize} bytes.`,
45
61
  );
@@ -77,7 +77,8 @@ export class DiskStorage
77
77
  path,
78
78
  mimetype: file.type,
79
79
  encoding: 'utf-8',
80
- fieldname: fieldName,
80
+ fieldName: fieldName,
81
+ uploadedAt: new Date().toISOString(),
81
82
  };
82
83
  }
83
84
 
@@ -8,26 +8,33 @@ export interface MemoryStorageFile extends StorageFile {
8
8
  }
9
9
 
10
10
  export class MemoryStorage implements Storage<MemoryStorageFile> {
11
- public async handleFile(file: File, _req: HonoRequest, fieldName: string) {
11
+ public async handleFile(
12
+ file: File,
13
+ _req: HonoRequest,
14
+ fieldName: string,
15
+ ): Promise<MemoryStorageFile> {
12
16
  const buffer = await file
13
17
  .stream()
14
18
  .pipeTo(new WritableStream())
15
19
  .then(() => file.arrayBuffer())
16
- .then((buffer) => Buffer.from(buffer));
20
+ .then((buf) => Buffer.from(buf));
17
21
 
18
22
  return {
19
23
  buffer,
20
24
  size: buffer.length,
21
25
  encoding: 'utf-8',
22
26
  mimetype: file.type,
23
- fieldname: fieldName,
27
+ fieldName: fieldName,
24
28
  originalFilename: file.name,
29
+ uploadedAt: new Date().toISOString(),
25
30
  stream: () => file.stream(),
26
- lastModified: file.lastModified,
27
31
  };
28
32
  }
29
33
 
30
- public async removeFile(file: MemoryStorageFile) {
31
- delete file.buffer;
34
+ public async removeFile(file: StorageFile): Promise<void> {
35
+ // Check if it's a MemoryStorageFile before deleting buffer
36
+ if ('buffer' in file) {
37
+ delete (file as MemoryStorageFile).buffer;
38
+ }
32
39
  }
33
40
  }
@@ -1,16 +1,23 @@
1
1
  import { HonoRequest } from 'hono';
2
2
 
3
3
  export interface StorageFile {
4
- size: number;
5
- fieldname: string;
6
- encoding: string;
7
- mimetype: string;
4
+ /** Field name in the multipart form */
5
+ fieldName: string;
6
+ /** Original filename provided by client */
8
7
  originalFilename: string;
8
+ /** MIME type of the file */
9
+ mimetype: string;
10
+ /** Encoding type (e.g., '7bit', '8bit', 'binary') */
11
+ encoding: string;
12
+ /** Size of the file in bytes */
13
+ size: number;
14
+ /** Timestamp when file was uploaded (ISO 8601) */
15
+ uploadedAt?: string;
9
16
  }
10
17
 
11
18
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
12
19
  export interface Storage<T extends StorageFile = StorageFile, K = any> {
13
20
  handleFile: (file: File, req: HonoRequest, fieldName: string) => Promise<T>;
14
- removeFile: (file: T, force?: boolean) => Promise<void> | void;
21
+ removeFile: (file: T | StorageFile, force?: boolean) => Promise<void> | void;
15
22
  options?: K;
16
23
  }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * File utilities for upload handling
3
+ */
4
+
5
+ /**
6
+ * Format bytes to human-readable size
7
+ * @param bytes - Size in bytes
8
+ * @returns Formatted size string (e.g., "1.5 MB")
9
+ */
10
+ export function formatBytes(bytes: number): string {
11
+ if (bytes === 0) return '0 Bytes';
12
+
13
+ const k = 1024;
14
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
15
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
16
+
17
+ return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
18
+ }
19
+
20
+ /**
21
+ * Validate file size against limit
22
+ * @param file - File to validate
23
+ * @param maxSize - Maximum size in bytes
24
+ * @returns true if file size is within limit
25
+ * @throws Error if file exceeds limit
26
+ */
27
+ export function validateFileSize(file: File, maxSize: number): void {
28
+ if (file.size > maxSize) {
29
+ throw new Error(
30
+ `File "${file.name}" (${formatBytes(file.size)}) exceeds maximum size of ${formatBytes(maxSize)}`,
31
+ );
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Get file extension from filename
37
+ * @param filename - Name of the file
38
+ * @returns File extension without dot, or empty string
39
+ */
40
+ export function getFileExtension(filename: string): string {
41
+ const ext = filename.lastIndexOf('.');
42
+ return ext === -1 ? '' : filename.slice(ext + 1);
43
+ }
44
+
45
+ /**
46
+ * Check if file is of expected type
47
+ * @param file - File to check
48
+ * @param allowedTypes - Array of allowed MIME types or extensions
49
+ * @returns true if file type is allowed
50
+ */
51
+ export function isAllowedFileType(file: File, allowedTypes: string[]): boolean {
52
+ return allowedTypes.some((type) => {
53
+ if (type.startsWith('.')) {
54
+ // Extension check
55
+ return getFileExtension(file.name).toLowerCase() === type.slice(1);
56
+ }
57
+ // MIME type check (supports wildcards like 'image/*')
58
+ if (type.endsWith('/*')) {
59
+ const prefix = type.slice(0, -2);
60
+ return file.type.startsWith(prefix);
61
+ }
62
+ return file.type === type;
63
+ });
64
+ }
65
+
66
+ /**
67
+ * Generate a safe filename from user input
68
+ * @param filename - Original filename
69
+ * @returns Safe filename without special characters
70
+ */
71
+ export function sanitizeFilename(filename: string): string {
72
+ return filename
73
+ .replace(/[^a-zA-Z0-9.-]/g, '_')
74
+ .replace(/_{2,}/g, '_')
75
+ .replace(/^\.+|\.+$/g, '');
76
+ }
77
+
78
+ /**
79
+ * Generate a unique filename if file already exists
80
+ * @param filename - Original filename
81
+ * @param existingFilenames - Set of existing filenames
82
+ * @returns Unique filename
83
+ */
84
+ export function getUniqueFilename(
85
+ filename: string,
86
+ existingFilenames?: Set<string>,
87
+ ): string {
88
+ if (!existingFilenames?.has(filename)) {
89
+ return filename;
90
+ }
91
+
92
+ const name = filename.lastIndexOf('.');
93
+ if (name === -1) {
94
+ return `${filename}_1`;
95
+ }
96
+
97
+ const baseName = filename.slice(0, name);
98
+ const ext = filename.slice(name);
99
+
100
+ let counter = 1;
101
+ let newFilename = `${baseName}_${counter}${ext}`;
102
+
103
+ while (existingFilenames.has(newFilename)) {
104
+ counter++;
105
+ newFilename = `${baseName}_${counter}${ext}`;
106
+ }
107
+
108
+ return newFilename;
109
+ }
@@ -0,0 +1,2 @@
1
+ export * from './file';
2
+ export * from './validators';