@commercetools-frontend-extensions/operations 0.0.0 → 3.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/CHANGELOG.md +6 -0
- package/README.md +81 -72
- package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +947 -143
- package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +947 -143
- package/dist/commercetools-frontend-extensions-operations.esm.js +898 -140
- package/dist/declarations/src/@api/file-import-jobs.d.ts +7 -0
- package/dist/declarations/src/@api/index.d.ts +1 -0
- package/dist/declarations/src/@api/test-fixtures.d.ts +8 -1
- package/dist/declarations/src/@api/urls.d.ts +30 -0
- package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +3 -2
- package/dist/declarations/src/@constants/file-import-job.d.ts +1 -0
- package/dist/declarations/src/@constants/import-limits.d.ts +6 -0
- package/dist/declarations/src/@constants/index.d.ts +2 -1
- package/dist/declarations/src/@errors/index.d.ts +5 -4
- package/dist/declarations/src/@errors/polling-aborted-error.d.ts +3 -0
- package/dist/declarations/src/@hooks/index.d.ts +3 -0
- package/dist/declarations/src/@hooks/use-fetch-file-import-job.d.ts +17 -0
- package/dist/declarations/src/@hooks/use-file-import-job-upload.d.ts +18 -0
- package/dist/declarations/src/@hooks/use-file-upload.d.ts +28 -0
- package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +2 -1
- package/dist/declarations/src/@types/export-operation.d.ts +3 -1
- package/dist/declarations/src/@types/file-import-job.d.ts +99 -0
- package/dist/declarations/src/@types/file-upload-result.d.ts +21 -0
- package/dist/declarations/src/@types/file-upload.d.ts +2 -2
- package/dist/declarations/src/@types/index.d.ts +2 -0
- package/dist/declarations/src/@utils/file-import-job-helpers.d.ts +12 -0
- package/dist/declarations/src/@utils/file-upload.d.ts +8 -0
- package/dist/declarations/src/@utils/index.d.ts +2 -0
- package/dist/declarations/src/@utils/poll-job-until-validated.d.ts +11 -0
- package/package.json +19 -20
- package/src/@api/fetcher.ts +10 -0
- package/src/@api/file-import-jobs.ts +217 -0
- package/src/@api/file-upload.spec.ts +4 -2
- package/src/@api/index.ts +1 -0
- package/src/@api/test-fixtures.ts +127 -5
- package/src/@api/urls.ts +77 -1
- package/src/@components/uploading-modal/uploading-modal.tsx +7 -5
- package/src/@constants/file-import-job.ts +1 -0
- package/src/@constants/import-limits.ts +13 -0
- package/src/@constants/index.ts +2 -1
- package/src/@errors/index.ts +5 -4
- package/src/@errors/polling-aborted-error.ts +6 -0
- package/src/@hooks/index.ts +3 -0
- package/src/@hooks/use-fetch-file-import-job.spec.ts +131 -0
- package/src/@hooks/use-fetch-file-import-job.ts +38 -0
- package/src/@hooks/use-fetch.spec.ts +1 -9
- package/src/@hooks/use-fetch.ts +4 -8
- package/src/@hooks/use-file-import-job-upload.spec.ts +273 -0
- package/src/@hooks/use-file-import-job-upload.ts +101 -0
- package/src/@hooks/use-file-upload.ts +223 -0
- package/src/@hooks/use-import-container-upload.spec.ts +16 -13
- package/src/@hooks/use-import-container-upload.ts +6 -2
- package/src/@types/export-operation.ts +3 -0
- package/src/@types/file-import-job.ts +165 -0
- package/src/@types/file-upload-result.ts +23 -0
- package/src/@types/file-upload.ts +2 -2
- package/src/@types/index.ts +2 -0
- package/src/@utils/error-mapping.ts +10 -9
- package/src/@utils/file-import-job-helpers.spec.ts +147 -0
- package/src/@utils/file-import-job-helpers.ts +47 -0
- package/src/@utils/file-upload.ts +39 -0
- package/src/@utils/index.ts +2 -0
- package/src/@utils/poll-job-until-validated.ts +76 -0
- package/dist/declarations/src/@constants/upload-limits.d.ts +0 -10
- package/src/@constants/upload-limits.ts +0 -11
- package/src/@hooks/messages.ts +0 -11
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import type { ResourceTypeId } from '@commercetools/importapi-sdk'
|
|
3
|
+
import { useImportContainerUpload } from './use-import-container-upload'
|
|
4
|
+
import { useFileImportJobUpload } from './use-file-import-job-upload'
|
|
5
|
+
import { getFileImportJobRecords, deleteImportContainer } from '../@api'
|
|
6
|
+
import { HttpError, PollingAbortedError } from '../@errors'
|
|
7
|
+
import { pollJobUntilValidated } from '../@utils'
|
|
8
|
+
import type {
|
|
9
|
+
ExtendedImportContainerDraft,
|
|
10
|
+
FileUploadResult,
|
|
11
|
+
FileImportJob,
|
|
12
|
+
} from '../@types'
|
|
13
|
+
|
|
14
|
+
export type ValidationProgress = {
|
|
15
|
+
processed: number
|
|
16
|
+
isValidating: boolean
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type FileUploadConfig = {
|
|
20
|
+
file: File
|
|
21
|
+
resourceType: ResourceTypeId
|
|
22
|
+
settings?: ExtendedImportContainerDraft['settings']
|
|
23
|
+
onSuccess: (result: FileUploadResult) => void
|
|
24
|
+
onError?: (error: unknown) => void
|
|
25
|
+
onProgress?: (progress: number) => void
|
|
26
|
+
onValidationProgress?: (job: FileImportJob) => void
|
|
27
|
+
abortSignal?: AbortSignal
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type FileUploadOptions = {
|
|
31
|
+
projectKey: string
|
|
32
|
+
useJobBasedFlow?: boolean
|
|
33
|
+
pollingInterval?: number
|
|
34
|
+
maxPollingAttempts?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const safeDeleteContainer = async ({
|
|
38
|
+
projectKey,
|
|
39
|
+
containerKey,
|
|
40
|
+
}: {
|
|
41
|
+
projectKey: string
|
|
42
|
+
containerKey: string
|
|
43
|
+
}) => {
|
|
44
|
+
try {
|
|
45
|
+
await deleteImportContainer({
|
|
46
|
+
projectKey,
|
|
47
|
+
importContainerKey: containerKey,
|
|
48
|
+
})
|
|
49
|
+
} catch {}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const useFileUpload = ({
|
|
53
|
+
projectKey,
|
|
54
|
+
useJobBasedFlow = false,
|
|
55
|
+
pollingInterval = 5000,
|
|
56
|
+
maxPollingAttempts = 120,
|
|
57
|
+
}: FileUploadOptions) => {
|
|
58
|
+
const [isUploading, setIsUploading] = React.useState(false)
|
|
59
|
+
const [progress, setProgress] = React.useState(0)
|
|
60
|
+
const [validationProgress, setValidationProgress] =
|
|
61
|
+
React.useState<ValidationProgress>({
|
|
62
|
+
processed: 0,
|
|
63
|
+
isValidating: false,
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
const containerUpload = useImportContainerUpload({ projectKey })
|
|
67
|
+
const jobUpload = useFileImportJobUpload({ projectKey })
|
|
68
|
+
|
|
69
|
+
const resetState = React.useCallback(() => {
|
|
70
|
+
setIsUploading(false)
|
|
71
|
+
setProgress(0)
|
|
72
|
+
setValidationProgress({ processed: 0, isValidating: false })
|
|
73
|
+
}, [])
|
|
74
|
+
|
|
75
|
+
const upload = React.useCallback(
|
|
76
|
+
async (config: FileUploadConfig) => {
|
|
77
|
+
setIsUploading(true)
|
|
78
|
+
setProgress(0)
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
if (useJobBasedFlow) {
|
|
82
|
+
await jobUpload.upload({
|
|
83
|
+
file: config.file,
|
|
84
|
+
resourceType: config.resourceType,
|
|
85
|
+
settings: config.settings,
|
|
86
|
+
abortSignal: config.abortSignal,
|
|
87
|
+
onSuccess: async (jobId, containerKey) => {
|
|
88
|
+
try {
|
|
89
|
+
setValidationProgress({ processed: 0, isValidating: true })
|
|
90
|
+
|
|
91
|
+
const validatedJob = await pollJobUntilValidated({
|
|
92
|
+
projectKey,
|
|
93
|
+
jobId,
|
|
94
|
+
importContainerKey: containerKey,
|
|
95
|
+
pollingInterval,
|
|
96
|
+
maxAttempts: maxPollingAttempts,
|
|
97
|
+
abortSignal: config.abortSignal,
|
|
98
|
+
onJobUpdate: (job) => {
|
|
99
|
+
const processed = job.summary?.total ?? 0
|
|
100
|
+
setValidationProgress({ processed, isValidating: true })
|
|
101
|
+
config.onValidationProgress?.(job)
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
// Handle rejected job with jobError for the new flow (like MissingCsvFieldIdentifier error)
|
|
106
|
+
// We wrap it in HttpError to reuse existing error handling (for the old flow) in consumers until BE supports this error type
|
|
107
|
+
if (validatedJob.jobError) {
|
|
108
|
+
throw new HttpError(
|
|
109
|
+
400,
|
|
110
|
+
validatedJob.jobError.message,
|
|
111
|
+
validatedJob.jobError
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let results: FileUploadResult['summary']['results'] = []
|
|
116
|
+
if (validatedJob.summary.invalid > 0) {
|
|
117
|
+
const recordsResponse = await getFileImportJobRecords({
|
|
118
|
+
projectKey,
|
|
119
|
+
importContainerKey: containerKey,
|
|
120
|
+
jobId,
|
|
121
|
+
limit: 500,
|
|
122
|
+
isValid: false,
|
|
123
|
+
})
|
|
124
|
+
results = recordsResponse.results
|
|
125
|
+
await safeDeleteContainer({ projectKey, containerKey })
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result: FileUploadResult = {
|
|
129
|
+
containerKey,
|
|
130
|
+
summary: {
|
|
131
|
+
...validatedJob.summary,
|
|
132
|
+
results,
|
|
133
|
+
},
|
|
134
|
+
jobId,
|
|
135
|
+
job: validatedJob,
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setIsUploading(false)
|
|
139
|
+
setValidationProgress({ processed: 0, isValidating: false })
|
|
140
|
+
config.onSuccess(result)
|
|
141
|
+
} catch (error) {
|
|
142
|
+
await safeDeleteContainer({ projectKey, containerKey })
|
|
143
|
+
resetState()
|
|
144
|
+
|
|
145
|
+
if (!(error instanceof PollingAbortedError)) {
|
|
146
|
+
config.onError?.(error)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
onProgress: (prog) => {
|
|
151
|
+
setProgress(prog)
|
|
152
|
+
config.onProgress?.(prog)
|
|
153
|
+
},
|
|
154
|
+
onError: (error) => {
|
|
155
|
+
resetState()
|
|
156
|
+
config.onError?.(error)
|
|
157
|
+
},
|
|
158
|
+
})
|
|
159
|
+
} else {
|
|
160
|
+
await containerUpload.upload({
|
|
161
|
+
file: config.file,
|
|
162
|
+
resourceType: config.resourceType,
|
|
163
|
+
settings: config.settings,
|
|
164
|
+
abortSignal: config.abortSignal,
|
|
165
|
+
onSuccess: async (fileUploadResponse, containerKey) => {
|
|
166
|
+
if (config.abortSignal?.aborted) {
|
|
167
|
+
await safeDeleteContainer({ projectKey, containerKey })
|
|
168
|
+
resetState()
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (fileUploadResponse.invalid > 0) {
|
|
173
|
+
await safeDeleteContainer({ projectKey, containerKey })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const result: FileUploadResult = {
|
|
177
|
+
containerKey,
|
|
178
|
+
summary: {
|
|
179
|
+
total: fileUploadResponse.itemsCount,
|
|
180
|
+
valid: fileUploadResponse.valid,
|
|
181
|
+
invalid: fileUploadResponse.invalid,
|
|
182
|
+
fieldsCount: fileUploadResponse.columnsCount,
|
|
183
|
+
fields: fileUploadResponse.fields || [],
|
|
184
|
+
ignoredFields: fileUploadResponse.ignoredFields || [],
|
|
185
|
+
results: fileUploadResponse.results || [],
|
|
186
|
+
},
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setIsUploading(false)
|
|
190
|
+
config.onSuccess(result)
|
|
191
|
+
},
|
|
192
|
+
onProgress: (prog) => {
|
|
193
|
+
setProgress(prog)
|
|
194
|
+
config.onProgress?.(prog)
|
|
195
|
+
},
|
|
196
|
+
onError: (error) => {
|
|
197
|
+
resetState()
|
|
198
|
+
config.onError?.(error)
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
resetState()
|
|
204
|
+
config.onError?.(error)
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
[
|
|
208
|
+
projectKey,
|
|
209
|
+
useJobBasedFlow,
|
|
210
|
+
pollingInterval,
|
|
211
|
+
maxPollingAttempts,
|
|
212
|
+
containerUpload,
|
|
213
|
+
jobUpload,
|
|
214
|
+
]
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
upload,
|
|
219
|
+
isUploading,
|
|
220
|
+
progress,
|
|
221
|
+
validationProgress,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -42,8 +42,8 @@ describe('useImportContainerUpload', () => {
|
|
|
42
42
|
itemsCount: 10,
|
|
43
43
|
rowsCount: 10,
|
|
44
44
|
columnsCount: 2,
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
fields: ['key', 'value'],
|
|
46
|
+
ignoredFields: [],
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
beforeEach(() => {
|
|
@@ -153,6 +153,7 @@ describe('useImportContainerUpload', () => {
|
|
|
153
153
|
})
|
|
154
154
|
|
|
155
155
|
it('should delete container if upload throws before starting', async () => {
|
|
156
|
+
const onError = jest.fn()
|
|
156
157
|
const error = new Error('Container creation failed')
|
|
157
158
|
mockCreateImportContainerForFileUpload.mockRejectedValue(error)
|
|
158
159
|
|
|
@@ -160,20 +161,22 @@ describe('useImportContainerUpload', () => {
|
|
|
160
161
|
useImportContainerUpload({ projectKey })
|
|
161
162
|
)
|
|
162
163
|
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
})
|
|
164
|
+
await act(async () => {
|
|
165
|
+
await result.current.upload({
|
|
166
|
+
file: mockFile,
|
|
167
|
+
resourceType: 'product',
|
|
168
|
+
onSuccess: jest.fn(),
|
|
169
|
+
onError,
|
|
170
170
|
})
|
|
171
|
-
)
|
|
171
|
+
})
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
await waitFor(() => {
|
|
174
|
+
expect(mockDeleteImportContainer).toHaveBeenCalledWith({
|
|
175
|
+
projectKey,
|
|
176
|
+
importContainerKey,
|
|
177
|
+
})
|
|
176
178
|
})
|
|
179
|
+
expect(onError).toHaveBeenCalledWith(error)
|
|
177
180
|
})
|
|
178
181
|
|
|
179
182
|
it('should update progress during upload', async () => {
|
|
@@ -20,6 +20,7 @@ export type UseImportContainerUploadConfig = {
|
|
|
20
20
|
onSuccess: (response: FileUploadResponse, importContainerKey: string) => void
|
|
21
21
|
onError?: (error: unknown) => void
|
|
22
22
|
onProgress?: (progress: number) => void
|
|
23
|
+
abortSignal?: AbortSignal
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export const useImportContainerUpload = ({
|
|
@@ -38,7 +39,8 @@ export const useImportContainerUpload = ({
|
|
|
38
39
|
onSuccess,
|
|
39
40
|
onError,
|
|
40
41
|
onProgress,
|
|
41
|
-
|
|
42
|
+
abortSignal,
|
|
43
|
+
}: UseImportContainerUploadConfig): Promise<XMLHttpRequest | undefined> => {
|
|
42
44
|
if (!projectKey) {
|
|
43
45
|
throw new ProjectKeyNotAvailableError()
|
|
44
46
|
}
|
|
@@ -66,6 +68,7 @@ export const useImportContainerUpload = ({
|
|
|
66
68
|
importContainerKey,
|
|
67
69
|
resourceType,
|
|
68
70
|
file,
|
|
71
|
+
abortSignal,
|
|
69
72
|
onSuccess: (response) => {
|
|
70
73
|
setIsUploading(false)
|
|
71
74
|
setProgress(100)
|
|
@@ -107,7 +110,8 @@ export const useImportContainerUpload = ({
|
|
|
107
110
|
|
|
108
111
|
setIsUploading(false)
|
|
109
112
|
setProgress(0)
|
|
110
|
-
|
|
113
|
+
onError?.(error)
|
|
114
|
+
return undefined
|
|
111
115
|
}
|
|
112
116
|
}
|
|
113
117
|
|
|
@@ -136,9 +136,12 @@ export type ValidationErrorCode =
|
|
|
136
136
|
| 'AttributeDefinitionNotFound'
|
|
137
137
|
| 'LocalizedFieldWithoutLocale'
|
|
138
138
|
| 'IncompleteField'
|
|
139
|
+
| 'AttributeLevelMismatch'
|
|
139
140
|
|
|
140
141
|
export type ValidationError = {
|
|
141
142
|
code: ValidationErrorCode
|
|
142
143
|
message: string
|
|
143
144
|
field?: string
|
|
145
|
+
requestedLevel?: string
|
|
146
|
+
actualLevel?: string
|
|
144
147
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { hasRequiredFields } from './shared'
|
|
2
|
+
|
|
3
|
+
export type FileImportJobState =
|
|
4
|
+
| 'queued'
|
|
5
|
+
| 'processing'
|
|
6
|
+
| 'validated'
|
|
7
|
+
| 'initialising'
|
|
8
|
+
| 'ready'
|
|
9
|
+
| 'rejected'
|
|
10
|
+
|
|
11
|
+
export interface FileImportJobSummary {
|
|
12
|
+
total: number
|
|
13
|
+
invalid: number
|
|
14
|
+
valid: number
|
|
15
|
+
fieldsCount: number
|
|
16
|
+
fields: string[]
|
|
17
|
+
ignoredFields: string[]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface FileImportJobValidationError {
|
|
21
|
+
code: string
|
|
22
|
+
message: string
|
|
23
|
+
rowValue?: Record<string, string>
|
|
24
|
+
metadata?: {
|
|
25
|
+
row?: number
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FileImportJob {
|
|
30
|
+
id: string
|
|
31
|
+
fileName: string
|
|
32
|
+
importContainerKey: string
|
|
33
|
+
state: FileImportJobState
|
|
34
|
+
summary: FileImportJobSummary
|
|
35
|
+
jobError?: FileImportJobValidationError | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CreateFileImportJobWithStreamPayload {
|
|
39
|
+
fileType: 'csv' | 'json'
|
|
40
|
+
fileName: string
|
|
41
|
+
file: File
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface CreateFileImportJobWithReferencePayload {
|
|
45
|
+
payloadType: 'fileReference'
|
|
46
|
+
fileName: string
|
|
47
|
+
fileUrl: string
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type CreateFileImportJobPayload =
|
|
51
|
+
| CreateFileImportJobWithStreamPayload
|
|
52
|
+
| CreateFileImportJobWithReferencePayload
|
|
53
|
+
|
|
54
|
+
export interface CreateFileImportJobParameters {
|
|
55
|
+
projectKey: string
|
|
56
|
+
resourceType: string
|
|
57
|
+
importContainerKey: string
|
|
58
|
+
payload: CreateFileImportJobPayload
|
|
59
|
+
onProgress?: (progress: number) => void
|
|
60
|
+
abortSignal?: AbortSignal
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface GetFileImportJobParameters {
|
|
64
|
+
projectKey: string
|
|
65
|
+
importContainerKey: string
|
|
66
|
+
jobId: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FileImportJobError {
|
|
70
|
+
code: string
|
|
71
|
+
message: string
|
|
72
|
+
field: string
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FileImportJobErrorRecord {
|
|
76
|
+
index: number
|
|
77
|
+
errors: FileImportJobError[]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface FileImportJobRecordsResponse {
|
|
81
|
+
results: FileImportJobErrorRecord[]
|
|
82
|
+
total: number
|
|
83
|
+
limit: number
|
|
84
|
+
offset: number
|
|
85
|
+
count: number
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface GetFileImportJobRecordsParameters {
|
|
89
|
+
projectKey: string
|
|
90
|
+
importContainerKey: string
|
|
91
|
+
jobId: string
|
|
92
|
+
limit?: number
|
|
93
|
+
offset?: number
|
|
94
|
+
isValid?: boolean
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ProcessFileImportJobParameters {
|
|
98
|
+
projectKey: string
|
|
99
|
+
resourceType: string
|
|
100
|
+
importContainerKey: string
|
|
101
|
+
jobId: string
|
|
102
|
+
action?: 'delete'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ProcessFileImportJobResponse {
|
|
106
|
+
message: string
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DeleteFileImportJobParameters {
|
|
110
|
+
projectKey: string
|
|
111
|
+
importContainerKey: string
|
|
112
|
+
jobId: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface ListFileImportJobsParameters {
|
|
116
|
+
projectKey: string
|
|
117
|
+
importContainerKey: string
|
|
118
|
+
limit?: number
|
|
119
|
+
offset?: number
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type ListFileImportJobsResponse = FileImportJob[]
|
|
123
|
+
|
|
124
|
+
export function assertFileImportJob(
|
|
125
|
+
maybeJob: unknown
|
|
126
|
+
): asserts maybeJob is FileImportJob {
|
|
127
|
+
const requiredFields = ['id', 'fileName', 'importContainerKey', 'state']
|
|
128
|
+
if (hasRequiredFields(maybeJob, requiredFields)) return
|
|
129
|
+
throw new Error('Invalid File Import Job response')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function assertFileImportJobRecordsResponse(
|
|
133
|
+
maybeRecords: unknown
|
|
134
|
+
): asserts maybeRecords is FileImportJobRecordsResponse {
|
|
135
|
+
const requiredFields = ['results', 'total', 'limit', 'offset', 'count']
|
|
136
|
+
if (!hasRequiredFields(maybeRecords, requiredFields)) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
'Invalid File Import Job records response: missing required fields'
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function assertProcessFileImportJobResponse(
|
|
144
|
+
maybeResponse: unknown
|
|
145
|
+
): asserts maybeResponse is ProcessFileImportJobResponse {
|
|
146
|
+
const requiredFields = ['message']
|
|
147
|
+
if (hasRequiredFields(maybeResponse, requiredFields)) return
|
|
148
|
+
throw new Error('Invalid Process File Import Job response')
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function assertListFileImportJobsResponse(
|
|
152
|
+
maybeResponse: unknown
|
|
153
|
+
): asserts maybeResponse is ListFileImportJobsResponse {
|
|
154
|
+
if (!Array.isArray(maybeResponse)) {
|
|
155
|
+
throw new Error('Invalid List File Import Jobs response: expected an array')
|
|
156
|
+
}
|
|
157
|
+
if (maybeResponse.length > 0) {
|
|
158
|
+
const requiredFields = ['id', 'fileName', 'importContainerKey', 'state']
|
|
159
|
+
if (!hasRequiredFields(maybeResponse[0], requiredFields)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
'Invalid List File Import Jobs response: missing required fields'
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { FileImportJob } from './file-import-job'
|
|
2
|
+
import type { RowErrorsResponse } from './file-upload'
|
|
3
|
+
|
|
4
|
+
export interface FileUploadResult {
|
|
5
|
+
containerKey: string
|
|
6
|
+
summary: {
|
|
7
|
+
total: number
|
|
8
|
+
valid: number
|
|
9
|
+
invalid: number
|
|
10
|
+
fieldsCount: number
|
|
11
|
+
fields: string[]
|
|
12
|
+
ignoredFields: string[]
|
|
13
|
+
/**
|
|
14
|
+
* Validation errors. Format is compatible between old and new flows:
|
|
15
|
+
* - Old flow: { row?: number, index?: number, errors: RowError[] }
|
|
16
|
+
* - New flow: { index: number, errors: FileImportJobError[] }
|
|
17
|
+
*/
|
|
18
|
+
results: Array<RowErrorsResponse>
|
|
19
|
+
}
|
|
20
|
+
// new job based flow-specific fields
|
|
21
|
+
jobId?: string
|
|
22
|
+
job?: FileImportJob
|
|
23
|
+
}
|
|
@@ -34,8 +34,8 @@ export type FileUploadResponse = {
|
|
|
34
34
|
itemsCount: number
|
|
35
35
|
rowsCount: number
|
|
36
36
|
columnsCount: number
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
fields: Array<string>
|
|
38
|
+
ignoredFields: Array<string>
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface FileUploadRequestParameters {
|
package/src/@types/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export * from './api'
|
|
|
2
2
|
export * from './basic-error-data-type'
|
|
3
3
|
export * from './export-operation'
|
|
4
4
|
export * from './file-upload'
|
|
5
|
+
export * from './file-upload-result'
|
|
6
|
+
export * from './file-import-job'
|
|
5
7
|
export * from './import-container'
|
|
6
8
|
export * from './import-operation'
|
|
7
9
|
export * from './import-states'
|
|
@@ -5,6 +5,7 @@ export function getFileUploadErrorsCount(
|
|
|
5
5
|
return errors.reduce((acc, curr) => (acc += curr.errors.length), 0)
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
// TODO: After fully migrating to new flow, remove `row` and only use `index`
|
|
8
9
|
export function mapUploadFileErrorsResponseToUploadFileErrorRows(
|
|
9
10
|
uploadFileErrorsResponse?: Array<{
|
|
10
11
|
row?: number
|
|
@@ -22,18 +23,18 @@ export function mapUploadFileErrorsResponseToUploadFileErrorRows(
|
|
|
22
23
|
if (!uploadFileErrorsResponse || !Array.isArray(uploadFileErrorsResponse))
|
|
23
24
|
return []
|
|
24
25
|
let idCounter = 1
|
|
25
|
-
return uploadFileErrorsResponse.flatMap((rowErrorsResponse) =>
|
|
26
|
-
|
|
26
|
+
return uploadFileErrorsResponse.flatMap((rowErrorsResponse) => {
|
|
27
|
+
// TODO: use `row` if available, otherwise fall back to `index`
|
|
28
|
+
// Old flow uses `row`, new flow uses `index`
|
|
29
|
+
const rowNumber = rowErrorsResponse.row ?? rowErrorsResponse.index
|
|
30
|
+
|
|
31
|
+
return rowErrorsResponse.errors.map((rowError) => ({
|
|
27
32
|
id: String(idCounter++),
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
: {}),
|
|
31
|
-
...(rowErrorsResponse.index !== undefined
|
|
32
|
-
? { index: rowErrorsResponse.index }
|
|
33
|
-
: {}),
|
|
33
|
+
row: rowNumber,
|
|
34
|
+
index: rowNumber,
|
|
34
35
|
field: rowError.field,
|
|
35
36
|
code: rowError.code,
|
|
36
37
|
validationMessage: rowError.message,
|
|
37
38
|
}))
|
|
38
|
-
)
|
|
39
|
+
})
|
|
39
40
|
}
|