@contember/bindx-uploader 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/UploaderError.d.ts +6 -0
  2. package/dist/UploaderError.d.ts.map +1 -0
  3. package/dist/components/MultiUploader.d.ts +43 -0
  4. package/dist/components/MultiUploader.d.ts.map +1 -0
  5. package/dist/components/Uploader.d.ts +58 -0
  6. package/dist/components/Uploader.d.ts.map +1 -0
  7. package/dist/components/UploaderDropzoneArea.d.ts +6 -0
  8. package/dist/components/UploaderDropzoneArea.d.ts.map +1 -0
  9. package/dist/components/UploaderDropzoneRoot.d.ts +8 -0
  10. package/dist/components/UploaderDropzoneRoot.d.ts.map +1 -0
  11. package/dist/components/UploaderEachFile.d.ts +14 -0
  12. package/dist/components/UploaderEachFile.d.ts.map +1 -0
  13. package/dist/components/UploaderFileStateSwitch.d.ts +24 -0
  14. package/dist/components/UploaderFileStateSwitch.d.ts.map +1 -0
  15. package/dist/components/UploaderHasFile.d.ts +14 -0
  16. package/dist/components/UploaderHasFile.d.ts.map +1 -0
  17. package/dist/components/index.d.ts +8 -0
  18. package/dist/components/index.d.ts.map +1 -0
  19. package/dist/contexts.d.ts +32 -0
  20. package/dist/contexts.d.ts.map +1 -0
  21. package/dist/extractors/getAudioFileDataExtractor.d.ts +9 -0
  22. package/dist/extractors/getAudioFileDataExtractor.d.ts.map +1 -0
  23. package/dist/extractors/getFileUrlDataExtractor.d.ts +9 -0
  24. package/dist/extractors/getFileUrlDataExtractor.d.ts.map +1 -0
  25. package/dist/extractors/getGenericFileMetadataExtractor.d.ts +12 -0
  26. package/dist/extractors/getGenericFileMetadataExtractor.d.ts.map +1 -0
  27. package/dist/extractors/getImageFileDataExtractor.d.ts +10 -0
  28. package/dist/extractors/getImageFileDataExtractor.d.ts.map +1 -0
  29. package/dist/extractors/getVideoFileDataExtractor.d.ts +11 -0
  30. package/dist/extractors/getVideoFileDataExtractor.d.ts.map +1 -0
  31. package/dist/extractors/index.d.ts +6 -0
  32. package/dist/extractors/index.d.ts.map +1 -0
  33. package/dist/extractors/types.d.ts +13 -0
  34. package/dist/extractors/types.d.ts.map +1 -0
  35. package/dist/fileTypes/createAnyFileType.d.ts +27 -0
  36. package/dist/fileTypes/createAnyFileType.d.ts.map +1 -0
  37. package/dist/fileTypes/createAudioFileType.d.ts +29 -0
  38. package/dist/fileTypes/createAudioFileType.d.ts.map +1 -0
  39. package/dist/fileTypes/createImageFileType.d.ts +47 -0
  40. package/dist/fileTypes/createImageFileType.d.ts.map +1 -0
  41. package/dist/fileTypes/createVideoFileType.d.ts +33 -0
  42. package/dist/fileTypes/createVideoFileType.d.ts.map +1 -0
  43. package/dist/fileTypes/index.d.ts +5 -0
  44. package/dist/fileTypes/index.d.ts.map +1 -0
  45. package/dist/hooks/index.d.ts +3 -0
  46. package/dist/hooks/index.d.ts.map +1 -0
  47. package/dist/hooks/useS3Client.d.ts +7 -0
  48. package/dist/hooks/useS3Client.d.ts.map +1 -0
  49. package/dist/hooks/useUploaderStateFiles.d.ts +8 -0
  50. package/dist/hooks/useUploaderStateFiles.d.ts.map +1 -0
  51. package/dist/index.d.ts +10 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/internal/hooks/index.d.ts +5 -0
  54. package/dist/internal/hooks/index.d.ts.map +1 -0
  55. package/dist/internal/hooks/useFillEntity.d.ts +17 -0
  56. package/dist/internal/hooks/useFillEntity.d.ts.map +1 -0
  57. package/dist/internal/hooks/useGetPreviewUrls.d.ts +6 -0
  58. package/dist/internal/hooks/useGetPreviewUrls.d.ts.map +1 -0
  59. package/dist/internal/hooks/useUploadState.d.ts +12 -0
  60. package/dist/internal/hooks/useUploadState.d.ts.map +1 -0
  61. package/dist/internal/hooks/useUploaderDoUpload.d.ts +7 -0
  62. package/dist/internal/hooks/useUploaderDoUpload.d.ts.map +1 -0
  63. package/dist/internal/utils/executeExtractors.d.ts +16 -0
  64. package/dist/internal/utils/executeExtractors.d.ts.map +1 -0
  65. package/dist/internal/utils/index.d.ts +4 -0
  66. package/dist/internal/utils/index.d.ts.map +1 -0
  67. package/dist/internal/utils/resolveAccept.d.ts +7 -0
  68. package/dist/internal/utils/resolveAccept.d.ts.map +1 -0
  69. package/dist/internal/utils/uploaderErrorHandler.d.ts +7 -0
  70. package/dist/internal/utils/uploaderErrorHandler.d.ts.map +1 -0
  71. package/dist/types.d.ts +168 -0
  72. package/dist/types.d.ts.map +1 -0
  73. package/dist/uploadClient/S3UploadClient.d.ts +19 -0
  74. package/dist/uploadClient/S3UploadClient.d.ts.map +1 -0
  75. package/dist/uploadClient/index.d.ts +3 -0
  76. package/dist/uploadClient/index.d.ts.map +1 -0
  77. package/dist/uploadClient/types.d.ts +29 -0
  78. package/dist/uploadClient/types.d.ts.map +1 -0
  79. package/dist/utils/attrAccept.d.ts +16 -0
  80. package/dist/utils/attrAccept.d.ts.map +1 -0
  81. package/dist/utils/index.d.ts +3 -0
  82. package/dist/utils/index.d.ts.map +1 -0
  83. package/dist/utils/urlSigner.d.ts +8 -0
  84. package/dist/utils/urlSigner.d.ts.map +1 -0
  85. package/package.json +35 -0
  86. package/src/UploaderError.ts +7 -0
  87. package/src/components/MultiUploader.tsx +210 -0
  88. package/src/components/Uploader.tsx +174 -0
  89. package/src/components/UploaderDropzoneArea.tsx +29 -0
  90. package/src/components/UploaderDropzoneRoot.tsx +32 -0
  91. package/src/components/UploaderEachFile.tsx +46 -0
  92. package/src/components/UploaderFileStateSwitch.tsx +66 -0
  93. package/src/components/UploaderHasFile.tsx +35 -0
  94. package/src/components/index.ts +7 -0
  95. package/src/contexts.ts +76 -0
  96. package/src/extractors/getAudioFileDataExtractor.ts +44 -0
  97. package/src/extractors/getFileUrlDataExtractor.ts +22 -0
  98. package/src/extractors/getGenericFileMetadataExtractor.ts +40 -0
  99. package/src/extractors/getImageFileDataExtractor.ts +50 -0
  100. package/src/extractors/getVideoFileDataExtractor.ts +60 -0
  101. package/src/extractors/index.ts +5 -0
  102. package/src/extractors/types.ts +14 -0
  103. package/src/fileTypes/createAnyFileType.ts +56 -0
  104. package/src/fileTypes/createAudioFileType.ts +67 -0
  105. package/src/fileTypes/createImageFileType.ts +87 -0
  106. package/src/fileTypes/createVideoFileType.ts +75 -0
  107. package/src/fileTypes/index.ts +4 -0
  108. package/src/hooks/index.ts +2 -0
  109. package/src/hooks/useS3Client.ts +26 -0
  110. package/src/hooks/useUploaderStateFiles.ts +22 -0
  111. package/src/index.ts +126 -0
  112. package/src/internal/hooks/index.ts +4 -0
  113. package/src/internal/hooks/useFillEntity.ts +89 -0
  114. package/src/internal/hooks/useGetPreviewUrls.ts +26 -0
  115. package/src/internal/hooks/useUploadState.ts +159 -0
  116. package/src/internal/hooks/useUploaderDoUpload.ts +129 -0
  117. package/src/internal/utils/executeExtractors.ts +46 -0
  118. package/src/internal/utils/index.ts +3 -0
  119. package/src/internal/utils/resolveAccept.ts +26 -0
  120. package/src/internal/utils/uploaderErrorHandler.ts +15 -0
  121. package/src/types.ts +242 -0
  122. package/src/uploadClient/S3UploadClient.ts +119 -0
  123. package/src/uploadClient/index.ts +2 -0
  124. package/src/uploadClient/types.ts +30 -0
  125. package/src/utils/attrAccept.ts +87 -0
  126. package/src/utils/index.ts +2 -0
  127. package/src/utils/urlSigner.ts +117 -0
package/src/index.ts ADDED
@@ -0,0 +1,126 @@
1
+ // Types
2
+ export type {
3
+ // Upload client
4
+ UploadClient,
5
+ FileUploadResult,
6
+ FileUploadProgress,
7
+ UploadClientUploadArgs,
8
+ // File
9
+ FileWithMeta,
10
+ // State
11
+ UploaderFileState,
12
+ UploaderFileStateInitial,
13
+ UploaderFileStateUploading,
14
+ UploaderFileStateFinalizing,
15
+ UploaderFileStateSuccess,
16
+ UploaderFileStateError,
17
+ UploaderState,
18
+ // Events
19
+ BeforeUploadEvent,
20
+ StartUploadEvent,
21
+ ProgressEvent,
22
+ SuccessEvent,
23
+ AfterUploadEvent,
24
+ ErrorEvent,
25
+ UploaderEvents,
26
+ // Extractors
27
+ FileDataExtractor,
28
+ FileDataExtractorPopulator,
29
+ // File type
30
+ FileType,
31
+ DiscriminatedFileType,
32
+ DiscriminatedFileTypeMap,
33
+ // Field name utility type
34
+ FieldName,
35
+ // Options
36
+ UploaderOptions,
37
+ // Errors
38
+ UploaderErrorType,
39
+ UploaderErrorOptions,
40
+ } from './types.js'
41
+
42
+ // Error class
43
+ export { UploaderError } from './UploaderError.js'
44
+
45
+ // Contexts
46
+ export {
47
+ UploaderUploadFilesContext,
48
+ useUploaderUploadFiles,
49
+ UploaderStateContext,
50
+ useUploaderState,
51
+ UploaderOptionsContext,
52
+ useUploaderOptions,
53
+ UploaderFileStateContext,
54
+ useUploaderFileState,
55
+ UploaderClientContext,
56
+ useUploaderClient,
57
+ UploaderDropzoneStateContext,
58
+ useUploaderDropzoneState,
59
+ } from './contexts.js'
60
+
61
+ // Upload client
62
+ export {
63
+ S3UploadClient,
64
+ type S3UploadClientOptions,
65
+ type S3FileOptions,
66
+ type S3Acl,
67
+ type S3FileParameters,
68
+ type S3SignedUrlResponse,
69
+ type S3UrlSigner,
70
+ } from './uploadClient/index.js'
71
+
72
+ // Utils
73
+ export { createContentApiS3Signer, attrAccept, acceptToString } from './utils/index.js'
74
+
75
+ // Extractors
76
+ export {
77
+ getFileUrlDataExtractor,
78
+ type FileUrlDataExtractorProps,
79
+ getGenericFileMetadataExtractor,
80
+ type GenericFileMetadataExtractorProps,
81
+ getImageFileDataExtractor,
82
+ type ImageFileDataExtractorProps,
83
+ getVideoFileDataExtractor,
84
+ type VideoFileDataExtractorProps,
85
+ getAudioFileDataExtractor,
86
+ type AudioFileDataExtractorProps,
87
+ } from './extractors/index.js'
88
+
89
+ // File type creators
90
+ export {
91
+ createImageFileType,
92
+ type CreateImageFileTypeProps,
93
+ createVideoFileType,
94
+ type CreateVideoFileTypeProps,
95
+ createAudioFileType,
96
+ type CreateAudioFileTypeProps,
97
+ createAnyFileType,
98
+ type CreateAnyFileTypeProps,
99
+ } from './fileTypes/index.js'
100
+
101
+ // Hooks
102
+ export {
103
+ useUploaderStateFiles,
104
+ type StateFilter,
105
+ useS3Client,
106
+ } from './hooks/index.js'
107
+
108
+ // Components
109
+ export {
110
+ Uploader,
111
+ UploaderWithMeta,
112
+ type UploaderProps,
113
+ MultiUploader,
114
+ MultiUploaderWithMeta,
115
+ type MultiUploaderProps,
116
+ UploaderEachFile,
117
+ type UploaderEachFileProps,
118
+ UploaderHasFile,
119
+ type UploaderHasFileProps,
120
+ UploaderFileStateSwitch,
121
+ type UploaderFileStateSwitchProps,
122
+ UploaderDropzoneRoot,
123
+ type UploaderDropzoneRootProps,
124
+ UploaderDropzoneArea,
125
+ type UploaderDropzoneAreaProps,
126
+ } from './components/index.js'
@@ -0,0 +1,4 @@
1
+ export { useUploadState, type UseUploadStateResult } from './useUploadState.js'
2
+ export { useUploaderDoUpload } from './useUploaderDoUpload.js'
3
+ export { useGetPreviewUrls } from './useGetPreviewUrls.js'
4
+ export { useFillEntity, type UseFillEntityArgs } from './useFillEntity.js'
@@ -0,0 +1,89 @@
1
+ import { useCallback } from 'react'
2
+ import type { EntityRef, HasOneRef } from '@contember/bindx'
3
+ import type { FileType, StartUploadEvent, UploaderEvents } from '../../types.js'
4
+ import { resolveAcceptingSingleType } from '../utils/resolveAccept.js'
5
+ import { executeExtractors } from '../utils/executeExtractors.js'
6
+
7
+ export interface UseFillEntityArgs<TEntity = Record<string, unknown>> extends UploaderEvents {
8
+ /**
9
+ * The entity to fill. Can be:
10
+ * - EntityRef: fill the entity directly
11
+ * - HasOneRef: fill the related entity (disconnect first on upload start)
12
+ */
13
+ entity: EntityRef<TEntity> | HasOneRef<TEntity>
14
+ fileType: FileType<TEntity>
15
+ }
16
+
17
+ /**
18
+ * Checks if the entity is a HasOneRef (has $disconnect method)
19
+ */
20
+ const isHasOneRef = <TEntity>(entity: EntityRef<TEntity> | HasOneRef<TEntity>): entity is HasOneRef<TEntity> => {
21
+ return '$disconnect' in entity && typeof entity.$disconnect === 'function'
22
+ }
23
+
24
+ /**
25
+ * Gets the target entity for filling.
26
+ * For HasOneRef, returns the related entity. For EntityRef, returns the entity itself.
27
+ */
28
+ const getTargetEntity = <TEntity>(entity: EntityRef<TEntity> | HasOneRef<TEntity>): EntityRef<TEntity> => {
29
+ if (isHasOneRef(entity)) {
30
+ return entity.$entity
31
+ }
32
+ return entity
33
+ }
34
+
35
+ /**
36
+ * Hook that connects upload events to entity field population.
37
+ * Handles both direct EntityRef and HasOneRef (for has-one relations).
38
+ */
39
+ export const useFillEntity = <TEntity extends Record<string, unknown>>({
40
+ entity,
41
+ fileType,
42
+ ...events
43
+ }: UseFillEntityArgs<TEntity>): UploaderEvents => {
44
+ const handleBeforeUpload = useCallback(
45
+ async (event: Parameters<UploaderEvents['onBeforeUpload']>[0]): Promise<FileType | undefined> => {
46
+ if (!(await resolveAcceptingSingleType(event.file, fileType as FileType))) {
47
+ return undefined
48
+ }
49
+ return (await events.onBeforeUpload?.(event)) ?? (fileType as FileType)
50
+ },
51
+ [events, fileType],
52
+ )
53
+
54
+ const handleStartUpload = useCallback(
55
+ (event: StartUploadEvent) => {
56
+ // Disconnect existing relation before upload
57
+ if (isHasOneRef(entity)) {
58
+ entity.$disconnect()
59
+ }
60
+ events.onStartUpload?.(event)
61
+ },
62
+ [entity, events],
63
+ )
64
+
65
+ const handleAfterUpload = useCallback(
66
+ async (event: Parameters<UploaderEvents['onAfterUpload']>[0]) => {
67
+ await Promise.all([
68
+ (async () => {
69
+ const targetEntity = getTargetEntity(entity)
70
+ const extractionResult = await executeExtractors({
71
+ fileType: fileType as FileType,
72
+ result: event.result,
73
+ file: event.file,
74
+ })
75
+ extractionResult?.({ entity: targetEntity as EntityRef<unknown> })
76
+ })(),
77
+ events.onAfterUpload?.(event),
78
+ ])
79
+ },
80
+ [entity, events, fileType],
81
+ )
82
+
83
+ return {
84
+ ...events,
85
+ onBeforeUpload: handleBeforeUpload,
86
+ onStartUpload: handleStartUpload,
87
+ onAfterUpload: handleAfterUpload,
88
+ }
89
+ }
@@ -0,0 +1,26 @@
1
+ import { useCallback, useEffect, useRef } from 'react'
2
+
3
+ /**
4
+ * Hook that manages preview URL creation and cleanup.
5
+ * Returns a function that creates object URLs for file preview.
6
+ */
7
+ export const useGetPreviewUrls = (): ((file: File) => string) => {
8
+ const urlsRef = useRef<Set<string>>(new Set())
9
+
10
+ // Cleanup URLs on unmount
11
+ useEffect(() => {
12
+ const urls = urlsRef.current
13
+ return () => {
14
+ for (const url of urls) {
15
+ URL.revokeObjectURL(url)
16
+ }
17
+ urls.clear()
18
+ }
19
+ }, [])
20
+
21
+ return useCallback((file: File): string => {
22
+ const url = URL.createObjectURL(file)
23
+ urlsRef.current.add(url)
24
+ return url
25
+ }, [])
26
+ }
@@ -0,0 +1,159 @@
1
+ import { useCallback, useMemo, useState } from 'react'
2
+ import type {
3
+ AfterUploadEvent,
4
+ BeforeUploadEvent,
5
+ ErrorEvent,
6
+ ProgressEvent,
7
+ StartUploadEvent,
8
+ SuccessEvent,
9
+ UploaderEvents,
10
+ UploaderFileState,
11
+ UploaderState,
12
+ } from '../../types.js'
13
+
14
+ export interface UseUploadStateResult extends UploaderEvents {
15
+ files: UploaderState
16
+ purgeFinal: () => void
17
+ purgeAll: () => void
18
+ }
19
+
20
+ /**
21
+ * Hook that manages the upload state machine.
22
+ * Tracks file states through the upload lifecycle: initial -> uploading -> finalizing -> success/error
23
+ */
24
+ export const useUploadState = ({
25
+ onBeforeUpload,
26
+ onStartUpload,
27
+ onSuccess,
28
+ onError,
29
+ onProgress,
30
+ onAfterUpload,
31
+ }: UploaderEvents): UseUploadStateResult => {
32
+ const [files, setFiles] = useState<Record<string, UploaderFileState>>({})
33
+
34
+ const purgeFinal = useCallback(() => {
35
+ setFiles(files => {
36
+ return Object.fromEntries(
37
+ Object.entries(files).filter(
38
+ ([, file]) => file.state !== 'success' && file.state !== 'error',
39
+ ),
40
+ )
41
+ })
42
+ }, [])
43
+
44
+ const purgeAll = useCallback(() => {
45
+ setFiles({})
46
+ }, [])
47
+
48
+ const handleBeforeUpload = useCallback(
49
+ async (event: BeforeUploadEvent) => {
50
+ setFiles(files => ({
51
+ ...files,
52
+ [event.file.id]: { state: 'initial', file: event.file },
53
+ }))
54
+ return await onBeforeUpload?.(event)
55
+ },
56
+ [onBeforeUpload],
57
+ )
58
+
59
+ const handleStartUpload = useCallback(
60
+ (event: StartUploadEvent) => {
61
+ setFiles(files => ({
62
+ ...files,
63
+ [event.file.id]: {
64
+ state: 'uploading',
65
+ file: event.file,
66
+ progress: {
67
+ progress: 0,
68
+ uploadedBytes: 0,
69
+ totalBytes: event.file.file.size,
70
+ },
71
+ },
72
+ }))
73
+ onStartUpload?.(event)
74
+ },
75
+ [onStartUpload],
76
+ )
77
+
78
+ const handleProgress = useCallback(
79
+ (event: ProgressEvent) => {
80
+ setFiles(files => ({
81
+ ...files,
82
+ [event.file.id]: {
83
+ state: 'uploading',
84
+ file: event.file,
85
+ progress: event.progress,
86
+ },
87
+ }))
88
+ onProgress?.(event)
89
+ },
90
+ [onProgress],
91
+ )
92
+
93
+ const handleAfterUpload = useCallback(
94
+ async (event: AfterUploadEvent) => {
95
+ setFiles(files => ({
96
+ ...files,
97
+ [event.file.id]: {
98
+ state: 'finalizing',
99
+ file: event.file,
100
+ result: event.result,
101
+ },
102
+ }))
103
+ return await onAfterUpload?.(event)
104
+ },
105
+ [onAfterUpload],
106
+ )
107
+
108
+ const handleSuccess = useCallback(
109
+ (event: SuccessEvent) => {
110
+ setFiles(files => ({
111
+ ...files,
112
+ [event.file.id]: {
113
+ state: 'success',
114
+ file: event.file,
115
+ result: event.result,
116
+ dismiss: () =>
117
+ setFiles(files => {
118
+ const { [event.file.id]: _, ...rest } = files
119
+ return rest
120
+ }),
121
+ },
122
+ }))
123
+ onSuccess?.(event)
124
+ },
125
+ [onSuccess],
126
+ )
127
+
128
+ const handleError = useCallback(
129
+ (event: ErrorEvent) => {
130
+ setFiles(files => ({
131
+ ...files,
132
+ [event.file.id]: {
133
+ state: 'error',
134
+ file: event.file,
135
+ error: event.error,
136
+ dismiss: () =>
137
+ setFiles(files => {
138
+ const { [event.file.id]: _, ...rest } = files
139
+ return rest
140
+ }),
141
+ },
142
+ }))
143
+ onError?.(event)
144
+ },
145
+ [onError],
146
+ )
147
+
148
+ return {
149
+ files: useMemo(() => Object.values(files), [files]),
150
+ purgeFinal,
151
+ purgeAll,
152
+ onBeforeUpload: handleBeforeUpload,
153
+ onStartUpload: handleStartUpload,
154
+ onProgress: handleProgress,
155
+ onAfterUpload: handleAfterUpload,
156
+ onSuccess: handleSuccess,
157
+ onError: handleError,
158
+ }
159
+ }
@@ -0,0 +1,129 @@
1
+ import { useCallback } from 'react'
2
+ import type { FileWithMeta, UploaderEvents } from '../../types.js'
3
+ import { UploaderError } from '../../UploaderError.js'
4
+ import { useUploaderClient } from '../../contexts.js'
5
+ import { useGetPreviewUrls } from './useGetPreviewUrls.js'
6
+
7
+ /**
8
+ * Hook that orchestrates the file upload process.
9
+ * Handles file preparation, validation, and upload execution.
10
+ */
11
+ export const useUploaderDoUpload = ({
12
+ onBeforeUpload,
13
+ onError,
14
+ onProgress,
15
+ onSuccess,
16
+ onStartUpload,
17
+ onAfterUpload,
18
+ }: UploaderEvents): ((files: File[]) => Promise<void>) => {
19
+ const getPreviewUrl = useGetPreviewUrls()
20
+ const defaultUploader = useUploaderClient()
21
+
22
+ return useCallback(
23
+ async (files: File[]) => {
24
+ // Prepare files with metadata
25
+ const fileWithMeta = files.map((file): FileWithMeta => {
26
+ const abortController = new AbortController()
27
+ const previewUrl = getPreviewUrl(file)
28
+ abortController.signal.addEventListener('abort', () => {
29
+ URL.revokeObjectURL(previewUrl)
30
+ }, { once: true })
31
+
32
+ return {
33
+ id: Math.random().toString(36).substring(7),
34
+ file,
35
+ previewUrl,
36
+ abortController,
37
+ }
38
+ })
39
+
40
+ // Validate and prepare files
41
+ const preparePromises = await Promise.allSettled(
42
+ fileWithMeta.map(async file => {
43
+ try {
44
+ const result = await onBeforeUpload?.({
45
+ file,
46
+ reject: (reason: string): never => {
47
+ throw new UploaderError({
48
+ type: 'fileRejected',
49
+ endUserMessage: reason,
50
+ })
51
+ },
52
+ })
53
+ if (!result) {
54
+ throw new UploaderError({ type: 'fileRejected' })
55
+ }
56
+ return {
57
+ file,
58
+ fileType: result,
59
+ }
60
+ } catch (e) {
61
+ onError?.({ file, error: e })
62
+ return Promise.reject(e)
63
+ }
64
+ }),
65
+ )
66
+
67
+ // Filter successful preparations
68
+ const preparedFiles = preparePromises
69
+ .filter(
70
+ <T extends PromiseFulfilledResult<unknown>>(
71
+ p: T | PromiseRejectedResult,
72
+ ): p is T => p.status === 'fulfilled',
73
+ )
74
+ .map(p => p.value)
75
+
76
+ // Upload files
77
+ await Promise.allSettled(
78
+ preparedFiles.map(async ({ file, fileType }) => {
79
+ try {
80
+ if (file.abortController.signal.aborted) {
81
+ return
82
+ }
83
+
84
+ onStartUpload?.({ file, fileType })
85
+
86
+ const uploader = fileType.uploader ?? defaultUploader
87
+ if (!uploader) {
88
+ throw new Error(
89
+ 'No uploader. Please specify the uploader in FileType or using UploaderClientContext.',
90
+ )
91
+ }
92
+
93
+ const result = await uploader.upload({
94
+ file: file.file,
95
+ signal: file.abortController.signal,
96
+ onProgress: progress => {
97
+ onProgress?.({ file, progress, fileType })
98
+ },
99
+ })
100
+
101
+ if (file.abortController.signal.aborted) {
102
+ throw new UploaderError({ type: 'aborted' })
103
+ }
104
+
105
+ await onAfterUpload?.({ file, result, fileType })
106
+
107
+ if (file.abortController.signal.aborted) {
108
+ throw new UploaderError({ type: 'aborted' })
109
+ }
110
+
111
+ onSuccess?.({ file, result, fileType })
112
+ } catch (e) {
113
+ onError?.({ file, error: e, fileType })
114
+ }
115
+ }),
116
+ )
117
+ },
118
+ [
119
+ defaultUploader,
120
+ getPreviewUrl,
121
+ onAfterUpload,
122
+ onBeforeUpload,
123
+ onError,
124
+ onProgress,
125
+ onStartUpload,
126
+ onSuccess,
127
+ ],
128
+ )
129
+ }
@@ -0,0 +1,46 @@
1
+ import type { EntityRef } from '@contember/bindx'
2
+ import type { FileType, FileWithMeta, FileUploadResult } from '../../types.js'
3
+
4
+ interface ExecuteExtractorsArgs {
5
+ fileType: FileType
6
+ file: FileWithMeta
7
+ result: FileUploadResult
8
+ }
9
+
10
+ /**
11
+ * Executes all extractors for a file type and returns a populator function
12
+ * that can be used to fill entity fields.
13
+ */
14
+ export const executeExtractors = async ({
15
+ fileType,
16
+ file,
17
+ result,
18
+ }: ExecuteExtractorsArgs): Promise<((options: { entity: EntityRef<unknown> }) => void) | undefined> => {
19
+ const extractorsResult =
20
+ fileType.extractors?.map(it => {
21
+ return it.extractFileData?.(file)
22
+ }) ?? []
23
+
24
+ const extractionResult = await Promise.allSettled(extractorsResult)
25
+
26
+ if (file.abortController.signal.aborted) {
27
+ return undefined
28
+ }
29
+
30
+ return ({ entity }) => {
31
+ // Cast entity to the expected type - extractors use runtime field access via $fields proxy
32
+ const typedEntity = entity as EntityRef<Record<string, unknown>>
33
+
34
+ // First populate fields synchronously
35
+ fileType.extractors?.forEach(it => {
36
+ it.populateFields?.({ entity: typedEntity, result })
37
+ })
38
+
39
+ // Then populate from async extractors
40
+ extractionResult.forEach(it => {
41
+ if (it.status === 'fulfilled' && it.value) {
42
+ it.value({ entity: typedEntity, result })
43
+ }
44
+ })
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ export { resolveAcceptingSingleType } from './resolveAccept.js'
2
+ export { executeExtractors } from './executeExtractors.js'
3
+ export { uploaderErrorHandler } from './uploaderErrorHandler.js'
@@ -0,0 +1,26 @@
1
+ import type { FileType, FileWithMeta } from '../../types.js'
2
+ import { attrAccept } from '../../utils/attrAccept.js'
3
+
4
+ /**
5
+ * Checks if a file is accepted by a single file type.
6
+ * Returns false if rejected, true if accepted.
7
+ */
8
+ export const resolveAcceptingSingleType = async (
9
+ file: FileWithMeta,
10
+ fileType: FileType,
11
+ ): Promise<boolean> => {
12
+ // Check MIME type
13
+ if (fileType.accept && !attrAccept(file.file, fileType.accept)) {
14
+ return false
15
+ }
16
+
17
+ // Run custom validator
18
+ if (fileType.acceptFile) {
19
+ const result = await fileType.acceptFile(file)
20
+ if (result === false) {
21
+ return false
22
+ }
23
+ }
24
+
25
+ return true
26
+ }
@@ -0,0 +1,15 @@
1
+ import type { ErrorEvent } from '../../types.js'
2
+ import { UploaderError } from '../../UploaderError.js'
3
+
4
+ /**
5
+ * Default error handler that filters out expected errors (fileRejected, aborted)
6
+ * and logs unexpected errors.
7
+ */
8
+ export const uploaderErrorHandler = (event: ErrorEvent): void => {
9
+ if (event.error instanceof UploaderError) {
10
+ if (event.error.options.type === 'fileRejected' || event.error.options.type === 'aborted') {
11
+ return
12
+ }
13
+ }
14
+ console.error('Upload error:', event.error)
15
+ }