@kiyasov/platform-hono 1.6.0 → 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.
- package/.claude/settings.local.json +7 -1
- package/dist/cjs/src/adapters/hono-adapter.d.ts +1 -0
- package/dist/cjs/src/adapters/hono-adapter.js +14 -3
- package/dist/cjs/src/adapters/hono-adapter.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/Upload.d.ts +5 -7
- package/dist/cjs/src/drivers/graphQLUpload/Upload.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js +111 -58
- package/dist/cjs/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/index.d.ts +9 -3
- package/dist/cjs/src/drivers/graphQLUpload/index.js +21 -3
- package/dist/cjs/src/drivers/graphQLUpload/index.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.js +43 -37
- package/dist/cjs/src/drivers/graphQLUpload/processRequest.js.map +1 -1
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js +47 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.js +20 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/index.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js +31 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js +3 -0
- package/dist/cjs/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.js +62 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/file.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.js +19 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/index.js.map +1 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js +171 -0
- package/dist/cjs/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
- package/dist/cjs/src/multer/index.d.ts +1 -0
- package/dist/cjs/src/multer/index.js +1 -0
- package/dist/cjs/src/multer/index.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/any-files-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/base-interceptor.d.ts +6 -0
- package/dist/cjs/src/multer/interceptors/base-interceptor.js +26 -0
- package/dist/cjs/src/multer/interceptors/base-interceptor.js.map +1 -0
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js +7 -24
- package/dist/cjs/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/file-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/file-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/file-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/files-interceptor.d.ts +2 -2
- package/dist/cjs/src/multer/interceptors/files-interceptor.js +6 -23
- package/dist/cjs/src/multer/interceptors/files-interceptor.js.map +1 -1
- package/dist/cjs/src/multer/interceptors/index.d.ts +1 -0
- package/dist/cjs/src/multer/interceptors/index.js +1 -0
- package/dist/cjs/src/multer/interceptors/index.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/any-files.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/any-files.js +12 -25
- package/dist/cjs/src/multer/multipart/handlers/any-files.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/base-handler.d.ts +42 -0
- package/dist/cjs/src/multer/multipart/handlers/base-handler.js +106 -0
- package/dist/cjs/src/multer/multipart/handlers/base-handler.js.map +1 -0
- package/dist/cjs/src/multer/multipart/handlers/file-fields.d.ts +3 -10
- package/dist/cjs/src/multer/multipart/handlers/file-fields.js +19 -33
- package/dist/cjs/src/multer/multipart/handlers/file-fields.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/index.d.ts +6 -1
- package/dist/cjs/src/multer/multipart/handlers/index.js +13 -0
- package/dist/cjs/src/multer/multipart/handlers/index.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.js +18 -36
- package/dist/cjs/src/multer/multipart/handlers/multiple-files.js.map +1 -1
- package/dist/cjs/src/multer/multipart/handlers/single-file.d.ts +2 -8
- package/dist/cjs/src/multer/multipart/handlers/single-file.js +11 -33
- package/dist/cjs/src/multer/multipart/handlers/single-file.js.map +1 -1
- package/dist/cjs/src/multer/multipart/index.d.ts +1 -1
- package/dist/cjs/src/multer/multipart/options.d.ts +10 -16
- package/dist/cjs/src/multer/multipart/options.js.map +1 -1
- package/dist/cjs/src/multer/multipart/request.js +14 -3
- package/dist/cjs/src/multer/multipart/request.js.map +1 -1
- package/dist/cjs/src/multer/storage/disk-storage.d.ts +2 -1
- package/dist/cjs/src/multer/storage/disk-storage.js +2 -1
- package/dist/cjs/src/multer/storage/disk-storage.js.map +1 -1
- package/dist/cjs/src/multer/storage/memory-storage.d.ts +2 -11
- package/dist/cjs/src/multer/storage/memory-storage.js +6 -4
- package/dist/cjs/src/multer/storage/memory-storage.js.map +1 -1
- package/dist/cjs/src/multer/storage/storage.d.ts +6 -5
- package/dist/cjs/src/multer/utils/file.d.ts +6 -0
- package/dist/cjs/src/multer/utils/file.js +62 -0
- package/dist/cjs/src/multer/utils/file.js.map +1 -0
- package/dist/cjs/src/multer/utils/index.d.ts +2 -0
- package/dist/cjs/src/multer/utils/index.js +19 -0
- package/dist/cjs/src/multer/utils/index.js.map +1 -0
- package/dist/cjs/src/multer/utils/validators.d.ts +18 -0
- package/dist/cjs/src/multer/utils/validators.js +171 -0
- package/dist/cjs/src/multer/utils/validators.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/adapters/hono-adapter.d.ts +1 -0
- package/dist/esm/src/adapters/hono-adapter.js +14 -3
- package/dist/esm/src/adapters/hono-adapter.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/Upload.d.ts +5 -7
- package/dist/esm/src/drivers/graphQLUpload/Upload.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.d.ts +20 -8
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js +112 -59
- package/dist/esm/src/drivers/graphQLUpload/fs-capacitor.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/index.d.ts +9 -3
- package/dist/esm/src/drivers/graphQLUpload/index.js +7 -3
- package/dist/esm/src/drivers/graphQLUpload/index.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/processRequest.d.ts +8 -1
- package/dist/esm/src/drivers/graphQLUpload/processRequest.js +42 -36
- package/dist/esm/src/drivers/graphQLUpload/processRequest.js.map +1 -1
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.d.ts +15 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js +43 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/capacitor-storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.d.ts +3 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.js +4 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/index.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.d.ts +13 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js +27 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/memory-storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.d.ts +17 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.js +2 -0
- package/dist/esm/src/drivers/graphQLUpload/storage/storage.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.d.ts +6 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.js +54 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/file.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.d.ts +2 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.js +3 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/index.js.map +1 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.d.ts +18 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.js +167 -0
- package/dist/esm/src/drivers/graphQLUpload/utils/validators.js.map +1 -0
- package/dist/esm/src/multer/index.d.ts +1 -0
- package/dist/esm/src/multer/index.js +1 -0
- package/dist/esm/src/multer/index.js.map +1 -1
- package/dist/esm/src/multer/interceptors/any-files-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/any-files-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/any-files-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/base-interceptor.d.ts +6 -0
- package/dist/esm/src/multer/interceptors/base-interceptor.js +23 -0
- package/dist/esm/src/multer/interceptors/base-interceptor.js.map +1 -0
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.js +7 -24
- package/dist/esm/src/multer/interceptors/file-fields-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/file-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/file-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/file-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/files-interceptor.d.ts +2 -2
- package/dist/esm/src/multer/interceptors/files-interceptor.js +6 -23
- package/dist/esm/src/multer/interceptors/files-interceptor.js.map +1 -1
- package/dist/esm/src/multer/interceptors/index.d.ts +1 -0
- package/dist/esm/src/multer/interceptors/index.js +1 -0
- package/dist/esm/src/multer/interceptors/index.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/any-files.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/any-files.js +12 -25
- package/dist/esm/src/multer/multipart/handlers/any-files.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/base-handler.d.ts +42 -0
- package/dist/esm/src/multer/multipart/handlers/base-handler.js +102 -0
- package/dist/esm/src/multer/multipart/handlers/base-handler.js.map +1 -0
- package/dist/esm/src/multer/multipart/handlers/file-fields.d.ts +3 -10
- package/dist/esm/src/multer/multipart/handlers/file-fields.js +19 -33
- package/dist/esm/src/multer/multipart/handlers/file-fields.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/index.d.ts +6 -1
- package/dist/esm/src/multer/multipart/handlers/index.js +6 -1
- package/dist/esm/src/multer/multipart/handlers/index.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/multiple-files.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/multiple-files.js +18 -36
- package/dist/esm/src/multer/multipart/handlers/multiple-files.js.map +1 -1
- package/dist/esm/src/multer/multipart/handlers/single-file.d.ts +2 -8
- package/dist/esm/src/multer/multipart/handlers/single-file.js +11 -33
- package/dist/esm/src/multer/multipart/handlers/single-file.js.map +1 -1
- package/dist/esm/src/multer/multipart/index.d.ts +1 -1
- package/dist/esm/src/multer/multipart/options.d.ts +10 -16
- package/dist/esm/src/multer/multipart/options.js.map +1 -1
- package/dist/esm/src/multer/multipart/request.js +14 -3
- package/dist/esm/src/multer/multipart/request.js.map +1 -1
- package/dist/esm/src/multer/storage/disk-storage.d.ts +2 -1
- package/dist/esm/src/multer/storage/disk-storage.js +2 -1
- package/dist/esm/src/multer/storage/disk-storage.js.map +1 -1
- package/dist/esm/src/multer/storage/memory-storage.d.ts +2 -11
- package/dist/esm/src/multer/storage/memory-storage.js +6 -4
- package/dist/esm/src/multer/storage/memory-storage.js.map +1 -1
- package/dist/esm/src/multer/storage/storage.d.ts +6 -5
- package/dist/esm/src/multer/utils/file.d.ts +6 -0
- package/dist/esm/src/multer/utils/file.js +54 -0
- package/dist/esm/src/multer/utils/file.js.map +1 -0
- package/dist/esm/src/multer/utils/index.d.ts +2 -0
- package/dist/esm/src/multer/utils/index.js +3 -0
- package/dist/esm/src/multer/utils/index.js.map +1 -0
- package/dist/esm/src/multer/utils/validators.d.ts +18 -0
- package/dist/esm/src/multer/utils/validators.js +167 -0
- package/dist/esm/src/multer/utils/validators.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/package.json +8 -6
- package/src/adapters/hono-adapter.ts +18 -3
- package/src/drivers/graphQLUpload/Upload.ts +21 -14
- package/src/drivers/graphQLUpload/fs-capacitor.ts +240 -116
- package/src/drivers/graphQLUpload/index.ts +37 -3
- package/src/drivers/graphQLUpload/processRequest.ts +92 -38
- package/src/drivers/graphQLUpload/storage/capacitor-storage.ts +86 -0
- package/src/drivers/graphQLUpload/storage/index.ts +3 -0
- package/src/drivers/graphQLUpload/storage/memory-storage.ts +62 -0
- package/src/drivers/graphQLUpload/storage/storage.ts +52 -0
- package/src/drivers/graphQLUpload/utils/file.ts +109 -0
- package/src/drivers/graphQLUpload/utils/index.ts +2 -0
- package/src/drivers/graphQLUpload/utils/validators.ts +219 -0
- package/src/multer/index.ts +1 -0
- package/src/multer/interceptors/any-files-interceptor.ts +12 -43
- package/src/multer/interceptors/base-interceptor.ts +54 -0
- package/src/multer/interceptors/file-fields-interceptor.ts +14 -48
- package/src/multer/interceptors/file-interceptor.ts +12 -44
- package/src/multer/interceptors/files-interceptor.ts +13 -45
- package/src/multer/interceptors/index.ts +1 -0
- package/src/multer/multipart/handlers/any-files.ts +14 -32
- package/src/multer/multipart/handlers/base-handler.ts +204 -0
- package/src/multer/multipart/handlers/file-fields.ts +29 -57
- package/src/multer/multipart/handlers/index.ts +11 -1
- package/src/multer/multipart/handlers/multiple-files.ts +23 -54
- package/src/multer/multipart/handlers/single-file.ts +14 -47
- package/src/multer/multipart/index.ts +1 -1
- package/src/multer/multipart/options.ts +26 -8
- package/src/multer/multipart/request.ts +19 -3
- package/src/multer/storage/disk-storage.ts +2 -1
- package/src/multer/storage/memory-storage.ts +13 -6
- package/src/multer/storage/storage.ts +12 -5
- package/src/multer/utils/file.ts +109 -0
- package/src/multer/utils/index.ts +2 -0
- package/src/multer/utils/validators.ts +219 -0
- package/test/README.md +247 -0
- package/test/graphql-upload.test.ts +509 -0
- package/test/helpers.ts +70 -0
- package/test/integration.test.ts +197 -0
- package/test/interceptors-e2e.test.ts +362 -0
- package/test/multipart-upload.test.ts +354 -0
- package/test/smoke.test.ts +227 -0
|
@@ -1,4 +1,38 @@
|
|
|
1
|
+
// Core
|
|
2
|
+
export { Upload, FileUpload } from './Upload';
|
|
3
|
+
export { GraphQLUpload } from './GraphQLUpload';
|
|
4
|
+
export { processRequest, ProcessRequestOptions } from './processRequest';
|
|
5
|
+
|
|
6
|
+
// Streams
|
|
1
7
|
export * from './fs-capacitor';
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
export
|
|
8
|
+
|
|
9
|
+
// Storage - re-export with GQL prefix to avoid conflicts
|
|
10
|
+
export type {
|
|
11
|
+
StorageFile as GQLStorageFile,
|
|
12
|
+
StorageOptions as GQLStorageOptions,
|
|
13
|
+
} from './storage';
|
|
14
|
+
export { Storage as GQLStorage } from './storage/storage';
|
|
15
|
+
export {
|
|
16
|
+
CapacitorStorage as GQLCapacitorStorage,
|
|
17
|
+
CapacitorStorageFile as GQLCapacitorStorageFile,
|
|
18
|
+
} from './storage/capacitor-storage';
|
|
19
|
+
export {
|
|
20
|
+
MemoryStorage as GQLMemoryStorage,
|
|
21
|
+
MemoryStorageFile as GQLMemoryStorageFile,
|
|
22
|
+
} from './storage/memory-storage';
|
|
23
|
+
|
|
24
|
+
// Utils - prefix to avoid conflicts with multer
|
|
25
|
+
export {
|
|
26
|
+
formatBytes as gqlFormatBytes,
|
|
27
|
+
validateFileSize as gqlValidateFileSize,
|
|
28
|
+
getFileExtension as gqlGetFileExtension,
|
|
29
|
+
isAllowedFileType as gqlIsAllowedFileType,
|
|
30
|
+
sanitizeFilename as gqlSanitizeFilename,
|
|
31
|
+
getUniqueFilename as gqlGetUniqueFilename,
|
|
32
|
+
} from './utils/file';
|
|
33
|
+
|
|
34
|
+
export {
|
|
35
|
+
FileTypes as GQLFileTypes,
|
|
36
|
+
FileValidatorOptions as GQLFileValidatorOptions,
|
|
37
|
+
validateFile as gqlValidateFile,
|
|
38
|
+
} from './utils/validators';
|
|
@@ -1,56 +1,110 @@
|
|
|
1
1
|
import { Context } from 'hono';
|
|
2
|
-
import { Readable } from 'stream';
|
|
3
2
|
|
|
4
|
-
import {
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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,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
|
+
}
|