@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,56 +1,110 @@
1
1
  import { Context } from 'hono';
2
- import { Readable } from 'stream';
3
2
 
4
- import { WriteStream, Upload } from '.';
3
+ import { CapacitorStorage } from './storage/capacitor-storage';
4
+ import { Storage } from './storage/storage';
5
+ import { FileUpload, Upload } from './Upload';
5
6
 
7
+ /**
8
+ * Options for processing GraphQL file uploads
9
+ */
10
+ export interface ProcessRequestOptions {
11
+ /** Storage implementation to use for uploaded files */
12
+ storage?: Storage<FileUpload>;
13
+ /** Maximum file size in bytes */
14
+ maxFileSize?: number;
15
+ /** Temporary directory for capacitor storage */
16
+ tmpDir?: string;
17
+ }
18
+
19
+ /**
20
+ * Sets a value in an object using a dot-notation path
21
+ * @param obj - Target object
22
+ * @param path - Dot-notation path (e.g., 'user.profile.avatar')
23
+ * @param value - Value to set
24
+ */
25
+ function setByPath(
26
+ obj: Record<string, unknown>,
27
+ path: string,
28
+ value: unknown,
29
+ ): void {
30
+ const segments = path.split('.');
31
+ let current = obj;
32
+
33
+ for (let i = 0; i < segments.length - 1; i++) {
34
+ const segment = segments[i];
35
+ if (!current[segment] || typeof current[segment] !== 'object') {
36
+ current[segment] = {};
37
+ }
38
+ current = current[segment] as Record<string, unknown>;
39
+ }
40
+
41
+ current[segments[segments.length - 1]] = value;
42
+ }
43
+
44
+ /**
45
+ * Processes a GraphQL multipart request with file uploads
46
+ * @param ctx - Hono context
47
+ * @param options - Processing options
48
+ * @returns Processed operations with Upload promises
49
+ */
6
50
  export async function processRequest(
7
51
  ctx: Context,
52
+ options?: ProcessRequestOptions,
8
53
  ): Promise<Record<string, unknown>> {
9
54
  const body = await ctx.req.parseBody();
10
- const operations = JSON.parse(body.operations as string);
11
- const map = new Map(Object.entries(JSON.parse(body.map as string)));
12
55
 
13
- for (const [fieldName, file] of Object.entries(body)) {
14
- if (
15
- fieldName === 'operations' ||
16
- fieldName === 'map' ||
17
- !(file instanceof File)
18
- )
19
- continue;
56
+ const operations = JSON.parse(body.operations as string) as Record<
57
+ string,
58
+ unknown
59
+ >;
60
+ const fileMap = new Map(
61
+ Object.entries(JSON.parse(body.map as string) as Record<string, string[]>),
62
+ );
63
+
64
+ // Determine storage strategy
65
+ // Default to CapacitorStorage for GraphQL uploads (supports createReadStream)
66
+ const storage =
67
+ options?.storage ??
68
+ new CapacitorStorage({
69
+ maxSize: options?.maxFileSize,
70
+ tmpDir: options?.tmpDir,
71
+ });
20
72
 
21
- const fileKeys = map.get(fieldName);
22
- if (!Array.isArray(fileKeys) || !fileKeys.length) continue;
73
+ // Process each file upload
74
+ for (const [fieldName, value] of Object.entries(body)) {
75
+ if (fieldName === 'operations' || fieldName === 'map') continue;
76
+ if (!(value instanceof File)) continue;
23
77
 
24
- const buffer = Buffer.from(await file.arrayBuffer());
25
- const capacitor = new WriteStream();
26
- Readable.from(buffer).pipe(capacitor);
78
+ const fileKeys = fileMap.get(fieldName);
79
+ if (!fileKeys?.length) continue;
80
+
81
+ // Extract the actual field name from the GraphQL path
82
+ // e.g., "variables.file" -> "file", "variables.files.0" -> "files"
83
+ const firstPath = fileKeys[0];
84
+ const pathParts = firstPath.split('.');
85
+ let actualFieldName = pathParts[pathParts.length - 1];
86
+
87
+ // If the last part is a number (array index), get the parent key
88
+ if (/^\d+$/.test(actualFieldName) && pathParts.length > 1) {
89
+ actualFieldName = pathParts[pathParts.length - 2];
90
+ }
27
91
 
92
+ // Create upload promise
28
93
  const upload = new Upload();
29
94
 
30
- upload.file = {
31
- filename: file.name,
32
- mimetype: file.type,
33
- fieldName,
34
- encoding: '7bit',
35
- createReadStream: (options) => {
36
- const stream = capacitor.createReadStream(options);
37
- stream.on('close', () => {
38
- capacitor.release();
39
- });
40
- return stream;
41
- },
42
- capacitor,
43
- };
44
- upload.resolve(upload.file);
95
+ // Handle file in background
96
+ storage
97
+ .handleFile(value, ctx.req, actualFieldName)
98
+ .then((file) => {
99
+ upload.resolve(file);
100
+ })
101
+ .catch((error) => {
102
+ upload.reject(error);
103
+ });
45
104
 
105
+ // Map upload to all specified paths in operations
46
106
  for (const fileKey of fileKeys) {
47
- const pathSegments = fileKey.split('.');
48
- let current = operations;
49
- for (let i = 0; i < pathSegments.length - 1; i++) {
50
- if (!current[pathSegments[i]]) current[pathSegments[i]] = {};
51
- current = current[pathSegments[i]];
52
- }
53
- current[pathSegments[pathSegments.length - 1]] = upload;
107
+ setByPath(operations, fileKey, upload);
54
108
  }
55
109
  }
56
110
 
@@ -0,0 +1,86 @@
1
+ import { HonoRequest } from 'hono';
2
+ import { Readable } from 'stream';
3
+
4
+ import { ReadStream, ReadStreamOptions, WriteStream } from '../fs-capacitor';
5
+ import { FileUpload } from '../Upload';
6
+ import { Storage, StorageOptions } from './storage';
7
+
8
+ /**
9
+ * File upload with stream support for capacitor storage
10
+ */
11
+ export interface CapacitorStorageFile extends FileUpload {
12
+ /** Original File object */
13
+ file: File;
14
+ /** Creates a readable stream for the file content */
15
+ createReadStream(options?: ReadStreamOptions): ReadStream;
16
+ /** The write stream capacitor for this file */
17
+ capacitor: WriteStream;
18
+ }
19
+
20
+ /**
21
+ * Capacitor-based storage that uses temporary files with stream support.
22
+ * Allows multiple concurrent reads and automatic cleanup.
23
+ */
24
+ export class CapacitorStorage implements Storage<CapacitorStorageFile> {
25
+ public readonly options?: StorageOptions;
26
+
27
+ constructor(options?: StorageOptions) {
28
+ this.options = options;
29
+ }
30
+
31
+ public async handleFile(
32
+ file: File,
33
+ _req: HonoRequest,
34
+ fieldName: string,
35
+ ): Promise<CapacitorStorageFile> {
36
+ // Check file size limit
37
+ if (this.options?.maxSize && file.size > this.options.maxSize) {
38
+ throw new Error(
39
+ `File "${file.name}" exceeds maximum size of ${this.options.maxSize} bytes`,
40
+ );
41
+ }
42
+
43
+ // Create capacitor and write file content
44
+ const capacitor = new WriteStream(
45
+ this.options?.tmpDir ? { tmpdir: () => this.options.tmpDir! } : undefined,
46
+ );
47
+
48
+ const buffer = Buffer.from(await file.arrayBuffer());
49
+ Readable.from(buffer).pipe(capacitor);
50
+
51
+ // Wait for the stream to finish
52
+ await new Promise<void>((resolve, reject) => {
53
+ capacitor.on('finish', resolve);
54
+ capacitor.on('error', reject);
55
+ });
56
+
57
+ return {
58
+ fieldName: fieldName,
59
+ originalFilename: file.name,
60
+ mimetype: file.type,
61
+ encoding: '7bit',
62
+ size: file.size,
63
+ uploadedAt: new Date().toISOString(),
64
+ file,
65
+ createReadStream: (options?: ReadStreamOptions) => {
66
+ const stream = capacitor.createReadStream(options);
67
+ stream.on('close', () => {
68
+ capacitor.release();
69
+ });
70
+ stream.on('error', () => {
71
+ capacitor.release();
72
+ });
73
+ return stream;
74
+ },
75
+ capacitor,
76
+ };
77
+ }
78
+
79
+ public async removeFile(
80
+ file: CapacitorStorageFile,
81
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
82
+ force?: boolean,
83
+ ): Promise<void> {
84
+ file.capacitor.release();
85
+ }
86
+ }
@@ -0,0 +1,3 @@
1
+ export * from './storage';
2
+ export * from './capacitor-storage';
3
+ export * from './memory-storage';
@@ -0,0 +1,62 @@
1
+ import { HonoRequest } from 'hono';
2
+
3
+ import { FileUpload } from '../Upload';
4
+ import { Storage, StorageOptions } from './storage';
5
+
6
+ /**
7
+ * In-memory storage file with buffer
8
+ */
9
+ export interface MemoryStorageFile extends FileUpload {
10
+ /** Buffer containing the file data */
11
+ buffer: Buffer;
12
+ /** Original File object */
13
+ file: File;
14
+ }
15
+
16
+ /**
17
+ * In-memory storage implementation for file uploads.
18
+ * Files are stored as buffers in memory. Suitable for small files and testing.
19
+ */
20
+ export class MemoryStorage implements Storage<MemoryStorageFile> {
21
+ public readonly options?: StorageOptions;
22
+
23
+ constructor(options?: StorageOptions) {
24
+ this.options = options;
25
+ }
26
+
27
+ public async handleFile(
28
+ file: File,
29
+ _req: HonoRequest,
30
+ fieldName: string,
31
+ ): Promise<MemoryStorageFile> {
32
+ // Check file size limit
33
+ if (this.options?.maxSize && file.size > this.options.maxSize) {
34
+ throw new Error(
35
+ `File "${file.name}" exceeds maximum size of ${this.options.maxSize} bytes`,
36
+ );
37
+ }
38
+
39
+ const buffer = Buffer.from(await file.arrayBuffer());
40
+
41
+ return {
42
+ fieldName: fieldName,
43
+ originalFilename: file.name,
44
+ mimetype: file.type,
45
+ encoding: '7bit',
46
+ size: buffer.length,
47
+ uploadedAt: new Date().toISOString(),
48
+ buffer,
49
+ file,
50
+ };
51
+ }
52
+
53
+ public async removeFile(
54
+ file: MemoryStorageFile,
55
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
56
+ force?: boolean,
57
+ ): Promise<void> {
58
+ if ('buffer' in file) {
59
+ delete (file as MemoryStorageFile).buffer;
60
+ }
61
+ }
62
+ }
@@ -0,0 +1,52 @@
1
+ import { HonoRequest } from 'hono';
2
+
3
+ /**
4
+ * Represents a file that has been uploaded and stored.
5
+ * Provides metadata about the file and optionally access to its content.
6
+ */
7
+ export interface StorageFile {
8
+ /** Field name in the multipart form */
9
+ fieldName: string;
10
+ /** Original filename provided by client */
11
+ originalFilename: string;
12
+ /** MIME type of the file */
13
+ mimetype: string;
14
+ /** Encoding type (e.g., '7bit', '8bit', 'binary') */
15
+ encoding: string;
16
+ /** Size of the file in bytes */
17
+ size: number;
18
+ /** Timestamp when file was uploaded (ISO 8601) */
19
+ uploadedAt?: string;
20
+ }
21
+
22
+ /**
23
+ * Storage handler options
24
+ */
25
+ export interface StorageOptions {
26
+ /** Maximum file size in bytes */
27
+ maxSize?: number;
28
+ /** Temporary directory path (for disk storage) */
29
+ tmpDir?: string;
30
+ }
31
+
32
+ /**
33
+ * Abstract storage interface for handling uploaded files.
34
+ * Implementations can store files in memory, on disk, or in cloud storage.
35
+ */
36
+ export interface Storage<TFile extends StorageFile = StorageFile> {
37
+ /**
38
+ * Handles an uploaded file, storing it according to the implementation strategy.
39
+ * @param file - The file to store
40
+ * @param req - The Hono request object
41
+ * @param fieldName - The field name from the multipart form
42
+ * @returns Information about the stored file
43
+ */
44
+ handleFile(file: File, req: HonoRequest, fieldName: string): Promise<TFile>;
45
+
46
+ /**
47
+ * Removes a previously stored file.
48
+ * @param file - The file metadata to remove
49
+ * @param force - Force removal even if storage options don't specify it
50
+ */
51
+ removeFile(file: TFile, force?: boolean): Promise<void>;
52
+ }
@@ -0,0 +1,109 @@
1
+ /**
2
+ * File utilities for GraphQL 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';
@@ -0,0 +1,219 @@
1
+ import { isAllowedFileType } from './file';
2
+
3
+ /**
4
+ * Common file type groups for validation
5
+ */
6
+ export const FileTypes = {
7
+ // Images
8
+ IMAGES: [
9
+ 'image/jpeg',
10
+ 'image/png',
11
+ 'image/gif',
12
+ 'image/webp',
13
+ 'image/svg+xml',
14
+ 'image/avif',
15
+ 'image/heic',
16
+ 'image/heif',
17
+ 'image/tiff',
18
+ 'image/bmp',
19
+ 'image/x-icon',
20
+ '.jpg',
21
+ '.jpeg',
22
+ '.png',
23
+ '.gif',
24
+ '.webp',
25
+ '.svg',
26
+ '.avif',
27
+ '.heic',
28
+ '.heif',
29
+ '.tiff',
30
+ '.bmp',
31
+ '.ico',
32
+ ] as const,
33
+
34
+ // Documents
35
+ DOCUMENTS: [
36
+ // PDF
37
+ 'application/pdf',
38
+ '.pdf',
39
+ // Microsoft Word
40
+ 'application/msword',
41
+ '.doc',
42
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
43
+ '.docx',
44
+ // Microsoft Excel
45
+ 'application/vnd.ms-excel',
46
+ '.xls',
47
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
48
+ '.xlsx',
49
+ // Microsoft PowerPoint
50
+ 'application/vnd.ms-powerpoint',
51
+ '.ppt',
52
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
53
+ '.pptx',
54
+ // OpenDocument formats
55
+ 'application/vnd.oasis.opendocument.text',
56
+ '.odt',
57
+ 'application/vnd.oasis.opendocument.spreadsheet',
58
+ '.ods',
59
+ 'application/vnd.oasis.opendocument.presentation',
60
+ '.odp',
61
+ // Apple iWork
62
+ 'application/vnd.apple.pages',
63
+ '.pages',
64
+ 'application/vnd.apple.numbers',
65
+ '.numbers',
66
+ 'application/vnd.apple.keynote',
67
+ '.key',
68
+ // Rich text
69
+ 'application/rtf',
70
+ '.rtf',
71
+ 'text/plain',
72
+ '.txt',
73
+ // CSV
74
+ 'text/csv',
75
+ '.csv',
76
+ ] as const,
77
+
78
+ // Videos
79
+ VIDEOS: [
80
+ 'video/mp4',
81
+ 'video/mpeg',
82
+ 'video/quicktime',
83
+ 'video/webm',
84
+ 'video/x-msvideo',
85
+ 'video/x-matroska',
86
+ 'video/quicktime',
87
+ 'video/x-flv',
88
+ '.mp4',
89
+ '.mpeg',
90
+ '.mov',
91
+ '.webm',
92
+ '.avi',
93
+ '.mkv',
94
+ '.flv',
95
+ ] as const,
96
+
97
+ // Audio
98
+ AUDIO: [
99
+ 'audio/mpeg',
100
+ 'audio/mp4',
101
+ 'audio/wav',
102
+ 'audio/ogg',
103
+ 'audio/webm',
104
+ 'audio/flac',
105
+ 'audio/aac',
106
+ 'audio/x-m4a',
107
+ 'audio/x-wav',
108
+ '.mp3',
109
+ '.m4a',
110
+ '.wav',
111
+ '.ogg',
112
+ '.flac',
113
+ '.aac',
114
+ ] as const,
115
+
116
+ // Archives
117
+ ARCHIVES: [
118
+ 'application/zip',
119
+ 'application/x-zip-compressed',
120
+ '.zip',
121
+ 'application/x-rar-compressed',
122
+ '.rar',
123
+ 'application/x-7z-compressed',
124
+ '.7z',
125
+ 'application/x-tar',
126
+ '.tar',
127
+ 'application/gzip',
128
+ '.gz',
129
+ 'application/x-gzip',
130
+ 'application/x-bzip2',
131
+ '.bz2',
132
+ 'application/x-compress',
133
+ '.Z',
134
+ 'application/x-apple-diskimage',
135
+ '.dmg',
136
+ 'application/x-iso9660-image',
137
+ '.iso',
138
+ ] as const,
139
+
140
+ // 3D Models
141
+ MODELS_3D: [
142
+ 'model/stl',
143
+ '.stl',
144
+ 'model/obj',
145
+ '.obj',
146
+ 'model/gltf-binary',
147
+ '.glb',
148
+ 'model/gltf+json',
149
+ '.gltf',
150
+ 'model/fbx',
151
+ '.fbx',
152
+ 'model/dae',
153
+ '.dae',
154
+ 'model/vnd.collada+xml',
155
+ ] as const,
156
+
157
+ // Fonts
158
+ FONTS: [
159
+ 'font/ttf',
160
+ '.ttf',
161
+ 'font/otf',
162
+ '.otf',
163
+ 'font/woff',
164
+ '.woff',
165
+ 'font/woff2',
166
+ '.woff2',
167
+ 'application/x-font-ttf',
168
+ 'application/x-font-opentype',
169
+ ] as const,
170
+ } as const;
171
+
172
+ /**
173
+ * Validator configuration for file uploads
174
+ */
175
+ export interface FileValidatorOptions {
176
+ /** Maximum file size in bytes */
177
+ maxSize?: number;
178
+ /** Allowed MIME types or extensions */
179
+ allowedTypes?: string[];
180
+ /** Denied MIME types or extensions */
181
+ deniedTypes?: string[];
182
+ }
183
+
184
+ /**
185
+ * Validates a file against the given options
186
+ * @param file - File to validate
187
+ * @param options - Validation options
188
+ * @returns Object with isValid flag and error message
189
+ */
190
+ export function validateFile(
191
+ file: File,
192
+ options: FileValidatorOptions,
193
+ ): { isValid: boolean; error?: string } {
194
+ // Check file size
195
+ if (options.maxSize && file.size > options.maxSize) {
196
+ return {
197
+ isValid: false,
198
+ error: `File "${file.name}" exceeds maximum size of ${options.maxSize} bytes`,
199
+ };
200
+ }
201
+
202
+ // Check denied types
203
+ if (options.deniedTypes && isAllowedFileType(file, options.deniedTypes)) {
204
+ return {
205
+ isValid: false,
206
+ error: `File type "${file.type}" is not allowed`,
207
+ };
208
+ }
209
+
210
+ // Check allowed types
211
+ if (options.allowedTypes && !isAllowedFileType(file, options.allowedTypes)) {
212
+ return {
213
+ isValid: false,
214
+ error: `File type "${file.type}" is not allowed. Allowed types: ${options.allowedTypes.join(', ')}`,
215
+ };
216
+ }
217
+
218
+ return { isValid: true };
219
+ }
@@ -2,3 +2,4 @@ export * from './interceptors';
2
2
  export * from './decorators';
3
3
  export * from './storage';
4
4
  export * from './multipart';
5
+ export * from './utils';