@commercetools-frontend-extensions/operations 0.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 +55 -0
- package/README.md +169 -0
- package/babel.config.js +6 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.d.ts +2 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.dev.js +2469 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.js +7 -0
- package/dist/commercetools-frontend-extensions-operations.cjs.prod.js +2461 -0
- package/dist/commercetools-frontend-extensions-operations.esm.js +2316 -0
- package/dist/declarations/src/@api/export-operations.d.ts +5 -0
- package/dist/declarations/src/@api/fetcher.d.ts +17 -0
- package/dist/declarations/src/@api/file-upload.d.ts +3 -0
- package/dist/declarations/src/@api/import-containers.d.ts +35 -0
- package/dist/declarations/src/@api/import-operations.d.ts +6 -0
- package/dist/declarations/src/@api/index.d.ts +8 -0
- package/dist/declarations/src/@api/process-file.d.ts +3 -0
- package/dist/declarations/src/@api/test-fixtures.d.ts +272 -0
- package/dist/declarations/src/@api/urls.d.ts +44 -0
- package/dist/declarations/src/@components/file-drop-area/active-drag-drop-area.d.ts +10 -0
- package/dist/declarations/src/@components/file-drop-area/disabled-drop-area.d.ts +5 -0
- package/dist/declarations/src/@components/file-drop-area/drop-area-wrapper.d.ts +11 -0
- package/dist/declarations/src/@components/file-drop-area/enabled-drop-area.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/file-drop-area.d.ts +14 -0
- package/dist/declarations/src/@components/file-drop-area/file-dropped-area.d.ts +6 -0
- package/dist/declarations/src/@components/file-drop-area/index.d.ts +7 -0
- package/dist/declarations/src/@components/file-drop-area/styles.d.ts +9 -0
- package/dist/declarations/src/@components/icons/file-icon.d.ts +2 -0
- package/dist/declarations/src/@components/icons/index.d.ts +2 -0
- package/dist/declarations/src/@components/icons/lock-icon.d.ts +2 -0
- package/dist/declarations/src/@components/index.d.ts +6 -0
- package/dist/declarations/src/@components/info-box/index.d.ts +1 -0
- package/dist/declarations/src/@components/info-box/info-box.d.ts +7 -0
- package/dist/declarations/src/@components/upload-separator/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-separator/upload-separator.d.ts +12 -0
- package/dist/declarations/src/@components/upload-settings/index.d.ts +1 -0
- package/dist/declarations/src/@components/upload-settings/upload-settings.d.ts +11 -0
- package/dist/declarations/src/@components/uploading-modal/index.d.ts +1 -0
- package/dist/declarations/src/@components/uploading-modal/uploading-modal.d.ts +12 -0
- package/dist/declarations/src/@constants/delimiters.d.ts +8 -0
- package/dist/declarations/src/@constants/import-tags.d.ts +7 -0
- package/dist/declarations/src/@constants/index.d.ts +4 -0
- package/dist/declarations/src/@constants/resource-links.d.ts +10 -0
- package/dist/declarations/src/@constants/upload-limits.d.ts +10 -0
- package/dist/declarations/src/@errors/http-error.d.ts +6 -0
- package/dist/declarations/src/@errors/index.d.ts +8 -0
- package/dist/declarations/src/@errors/invalid-response-error.d.ts +3 -0
- package/dist/declarations/src/@errors/no-resources-to-export-error.d.ts +3 -0
- package/dist/declarations/src/@errors/project-key-not-available-error.d.ts +3 -0
- package/dist/declarations/src/@errors/query-predicate-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-column-error.d.ts +3 -0
- package/dist/declarations/src/@errors/unexpected-operation-state-error.d.ts +4 -0
- package/dist/declarations/src/@errors/unexpected-resource-type-error.d.ts +3 -0
- package/dist/declarations/src/@hooks/index.d.ts +5 -0
- package/dist/declarations/src/@hooks/use-fetch-export-operations.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-container-details.d.ts +15 -0
- package/dist/declarations/src/@hooks/use-fetch-import-operations.d.ts +16 -0
- package/dist/declarations/src/@hooks/use-fetch-import-summaries.d.ts +20 -0
- package/dist/declarations/src/@hooks/use-import-container-upload.d.ts +18 -0
- package/dist/declarations/src/@types/api.d.ts +13 -0
- package/dist/declarations/src/@types/basic-error-data-type.d.ts +5 -0
- package/dist/declarations/src/@types/export-operation.d.ts +95 -0
- package/dist/declarations/src/@types/file-upload.d.ts +63 -0
- package/dist/declarations/src/@types/import-container.d.ts +53 -0
- package/dist/declarations/src/@types/import-operation.d.ts +13 -0
- package/dist/declarations/src/@types/import-states.d.ts +9 -0
- package/dist/declarations/src/@types/import-summary.d.ts +15 -0
- package/dist/declarations/src/@types/index.d.ts +9 -0
- package/dist/declarations/src/@types/shared.d.ts +7 -0
- package/dist/declarations/src/@utils/error-mapping.d.ts +19 -0
- package/dist/declarations/src/@utils/file-upload.d.ts +46 -0
- package/dist/declarations/src/@utils/form.d.ts +1 -0
- package/dist/declarations/src/@utils/format.d.ts +5 -0
- package/dist/declarations/src/@utils/import-container.d.ts +8 -0
- package/dist/declarations/src/@utils/index.d.ts +6 -0
- package/dist/declarations/src/@utils/url.d.ts +6 -0
- package/dist/declarations/src/index.d.ts +26 -0
- package/index.js +1 -0
- package/jest.test.config.js +11 -0
- package/package.json +63 -0
- package/src/@api/export-operations.ts +26 -0
- package/src/@api/fetcher.spec.ts +51 -0
- package/src/@api/fetcher.ts +127 -0
- package/src/@api/file-upload.spec.ts +83 -0
- package/src/@api/file-upload.ts +46 -0
- package/src/@api/import-containers.ts +256 -0
- package/src/@api/import-operations.ts +33 -0
- package/src/@api/index.ts +8 -0
- package/src/@api/process-file.spec.ts +74 -0
- package/src/@api/process-file.ts +53 -0
- package/src/@api/test-fixtures.ts +772 -0
- package/src/@api/urls.ts +118 -0
- package/src/@components/file-drop-area/active-drag-drop-area.tsx +33 -0
- package/src/@components/file-drop-area/disabled-drop-area.tsx +17 -0
- package/src/@components/file-drop-area/drop-area-wrapper.tsx +38 -0
- package/src/@components/file-drop-area/enabled-drop-area.tsx +27 -0
- package/src/@components/file-drop-area/file-drop-area.tsx +74 -0
- package/src/@components/file-drop-area/file-dropped-area.tsx +29 -0
- package/src/@components/file-drop-area/index.ts +7 -0
- package/src/@components/file-drop-area/styles.ts +67 -0
- package/src/@components/icons/file-icon.tsx +30 -0
- package/src/@components/icons/index.ts +2 -0
- package/src/@components/icons/lock-icon.tsx +34 -0
- package/src/@components/index.ts +6 -0
- package/src/@components/info-box/index.ts +1 -0
- package/src/@components/info-box/info-box.tsx +23 -0
- package/src/@components/upload-separator/index.ts +1 -0
- package/src/@components/upload-separator/upload-separator.tsx +61 -0
- package/src/@components/upload-settings/index.ts +1 -0
- package/src/@components/upload-settings/upload-settings.tsx +36 -0
- package/src/@components/uploading-modal/index.ts +1 -0
- package/src/@components/uploading-modal/uploading-modal.tsx +64 -0
- package/src/@constants/delimiters.ts +14 -0
- package/src/@constants/import-tags.ts +9 -0
- package/src/@constants/index.ts +4 -0
- package/src/@constants/resource-links.ts +61 -0
- package/src/@constants/upload-limits.ts +11 -0
- package/src/@errors/http-error.ts +17 -0
- package/src/@errors/index.ts +8 -0
- package/src/@errors/invalid-response-error.ts +6 -0
- package/src/@errors/no-resources-to-export-error.ts +6 -0
- package/src/@errors/project-key-not-available-error.ts +6 -0
- package/src/@errors/query-predicate-error.ts +10 -0
- package/src/@errors/unexpected-column-error.ts +6 -0
- package/src/@errors/unexpected-operation-state-error.ts +8 -0
- package/src/@errors/unexpected-resource-type-error.ts +6 -0
- package/src/@hooks/index.ts +5 -0
- package/src/@hooks/messages.ts +11 -0
- package/src/@hooks/use-fetch-export-operations.ts +34 -0
- package/src/@hooks/use-fetch-import-container-details.ts +31 -0
- package/src/@hooks/use-fetch-import-operations.ts +42 -0
- package/src/@hooks/use-fetch-import-summaries.ts +47 -0
- package/src/@hooks/use-fetch.spec.ts +76 -0
- package/src/@hooks/use-fetch.ts +80 -0
- package/src/@hooks/use-import-container-upload.spec.ts +294 -0
- package/src/@hooks/use-import-container-upload.ts +126 -0
- package/src/@types/api.ts +14 -0
- package/src/@types/basic-error-data-type.ts +5 -0
- package/src/@types/export-operation.ts +144 -0
- package/src/@types/file-upload.ts +81 -0
- package/src/@types/import-container.ts +104 -0
- package/src/@types/import-operation.ts +31 -0
- package/src/@types/import-states.ts +9 -0
- package/src/@types/import-summary.ts +22 -0
- package/src/@types/index.ts +9 -0
- package/src/@types/shared.ts +52 -0
- package/src/@utils/error-mapping.spec.ts +126 -0
- package/src/@utils/error-mapping.ts +39 -0
- package/src/@utils/file-upload.spec.ts +151 -0
- package/src/@utils/file-upload.ts +150 -0
- package/src/@utils/form.ts +20 -0
- package/src/@utils/format.spec.ts +62 -0
- package/src/@utils/format.ts +53 -0
- package/src/@utils/import-container.spec.ts +26 -0
- package/src/@utils/import-container.ts +34 -0
- package/src/@utils/index.ts +6 -0
- package/src/@utils/url.spec.ts +75 -0
- package/src/@utils/url.ts +18 -0
- package/src/index.ts +27 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { HttpError } from '../@errors'
|
|
2
|
+
import { rest } from 'msw'
|
|
3
|
+
import { setupServer } from 'msw/node'
|
|
4
|
+
import { fetcher } from './fetcher'
|
|
5
|
+
|
|
6
|
+
const mockServer = setupServer(
|
|
7
|
+
rest.get('http://localhost:8080/proxy/import/hello', (_, res, ctx) =>
|
|
8
|
+
res(
|
|
9
|
+
ctx.json({
|
|
10
|
+
message: 'hello',
|
|
11
|
+
})
|
|
12
|
+
)
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
mockServer.resetHandlers()
|
|
17
|
+
})
|
|
18
|
+
beforeAll(() => {
|
|
19
|
+
mockServer.listen()
|
|
20
|
+
})
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
mockServer.close()
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// TODO: write more tests
|
|
26
|
+
describe('fetcher', () => {
|
|
27
|
+
it('should add proxy prefix to url', async () => {
|
|
28
|
+
const response = await fetcher<{ message: string }>({
|
|
29
|
+
url: '/hello',
|
|
30
|
+
config: {
|
|
31
|
+
proxy: 'IMPORT',
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
expect(response.message).toBe('hello')
|
|
35
|
+
})
|
|
36
|
+
it('should throw error if response is unsuccessful', async () => {
|
|
37
|
+
mockServer.use(
|
|
38
|
+
rest.get('http://localhost:8080/proxy/import/hello', (_, res, ctx) => {
|
|
39
|
+
return res(ctx.status(404), ctx.json({ message: 'Not Found' }))
|
|
40
|
+
})
|
|
41
|
+
)
|
|
42
|
+
await expect(
|
|
43
|
+
fetcher<{ message: string }>({
|
|
44
|
+
url: '/hello',
|
|
45
|
+
config: {
|
|
46
|
+
proxy: 'IMPORT',
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
).rejects.toThrow(new HttpError(404, 'Not Found'))
|
|
50
|
+
})
|
|
51
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildApiUrl,
|
|
3
|
+
executeHttpClientRequest,
|
|
4
|
+
createHttpClientOptions,
|
|
5
|
+
} from '@commercetools-frontend/application-shell'
|
|
6
|
+
import { oidcStorage } from '@commercetools-frontend/application-shell-connectors'
|
|
7
|
+
import { type Fetcher } from '../@types'
|
|
8
|
+
import { HttpError } from '../@errors'
|
|
9
|
+
|
|
10
|
+
const addProxyPrefixToUrl = (uri: string, proxy?: string) => {
|
|
11
|
+
return proxy ? `/proxy/${proxy}${uri}` : uri
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const fetcher = async <Data>({ url, payload, config }: Fetcher) => {
|
|
15
|
+
const data = await executeHttpClientRequest<Promise<Data>>(
|
|
16
|
+
async (options: RequestInit) => {
|
|
17
|
+
const res = await fetch(
|
|
18
|
+
buildApiUrl(addProxyPrefixToUrl(url, config?.proxy)),
|
|
19
|
+
{
|
|
20
|
+
...options,
|
|
21
|
+
method: config?.method,
|
|
22
|
+
body: payload,
|
|
23
|
+
signal: config?.abortSignal,
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
if (!res.ok) {
|
|
27
|
+
const errorData = await res.json()
|
|
28
|
+
throw new HttpError(res.status, res.statusText, errorData)
|
|
29
|
+
}
|
|
30
|
+
const data = res.json()
|
|
31
|
+
return {
|
|
32
|
+
data,
|
|
33
|
+
statusCode: res.status,
|
|
34
|
+
getHeader: (key: string) => res.headers.get(key),
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
headers: {
|
|
39
|
+
'Content-Type': 'application/json',
|
|
40
|
+
...config?.headers,
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
)
|
|
44
|
+
return data
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const fetchUsingXhr = ({
|
|
48
|
+
url,
|
|
49
|
+
payload,
|
|
50
|
+
config,
|
|
51
|
+
onProgress,
|
|
52
|
+
onSuccess,
|
|
53
|
+
onError,
|
|
54
|
+
}: {
|
|
55
|
+
url: string
|
|
56
|
+
payload: FormData
|
|
57
|
+
config: {
|
|
58
|
+
abortSignal?: AbortSignal
|
|
59
|
+
proxy?: string
|
|
60
|
+
method: string
|
|
61
|
+
headers?: { [key: string]: string | null }
|
|
62
|
+
}
|
|
63
|
+
onProgress: (progress: number) => void
|
|
64
|
+
onSuccess: (data: any) => void
|
|
65
|
+
onError: (error: Error) => void
|
|
66
|
+
}): XMLHttpRequest => {
|
|
67
|
+
const options = createHttpClientOptions({
|
|
68
|
+
headers: {
|
|
69
|
+
'Content-Type': 'application/json',
|
|
70
|
+
...config?.headers,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const xhr = new XMLHttpRequest()
|
|
75
|
+
xhr.open(
|
|
76
|
+
config?.method,
|
|
77
|
+
buildApiUrl(addProxyPrefixToUrl(url, config?.proxy)),
|
|
78
|
+
true
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
xhr.withCredentials = true
|
|
82
|
+
|
|
83
|
+
if (options.headers) {
|
|
84
|
+
Object.keys(options.headers).forEach((key) => {
|
|
85
|
+
xhr.setRequestHeader(key, options.headers[key])
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
xhr.upload.onprogress = function (event) {
|
|
90
|
+
if (event.lengthComputable) {
|
|
91
|
+
const percentComplete = (event.loaded / event.total) * 100
|
|
92
|
+
onProgress(percentComplete)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const throwError = (errorData: Response) => {
|
|
97
|
+
onError(new HttpError(xhr.status, xhr.statusText, errorData))
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
xhr.onload = function () {
|
|
101
|
+
const data = JSON.parse(xhr.responseText)
|
|
102
|
+
// Code copied from `executeHttpClientRequest` to replicate the same behavior
|
|
103
|
+
const refreshedSessionToken = xhr.getResponseHeader(
|
|
104
|
+
'x-refreshed-session-token'
|
|
105
|
+
)
|
|
106
|
+
if (refreshedSessionToken) {
|
|
107
|
+
oidcStorage.setActiveSession(refreshedSessionToken)
|
|
108
|
+
}
|
|
109
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
110
|
+
onSuccess(data)
|
|
111
|
+
} else {
|
|
112
|
+
throwError(data)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
xhr.onerror = function () {
|
|
117
|
+
const errorData = JSON.parse(xhr.responseText)
|
|
118
|
+
throwError(errorData)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
xhr.onabort = function () {
|
|
122
|
+
onError(new DOMException('Aborted', 'AbortError'))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
xhr.send(payload)
|
|
126
|
+
return xhr
|
|
127
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { waitFor } from '@testing-library/react'
|
|
2
|
+
import { rest } from 'msw'
|
|
3
|
+
import { setupServer } from 'msw/node'
|
|
4
|
+
import { uploadFileForImport } from './file-upload'
|
|
5
|
+
import type { FileUploadResponse } from '../@types'
|
|
6
|
+
|
|
7
|
+
const validFileUploadResponse: FileUploadResponse = {
|
|
8
|
+
valid: 1,
|
|
9
|
+
results: [],
|
|
10
|
+
invalid: 0,
|
|
11
|
+
fileName: 'file.csv',
|
|
12
|
+
itemsCount: 0,
|
|
13
|
+
rowsCount: 0,
|
|
14
|
+
columnsCount: 0,
|
|
15
|
+
columns: [],
|
|
16
|
+
ignoredColumns: [],
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type ResourceTypeEntry = {
|
|
20
|
+
name: string
|
|
21
|
+
apiPath: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resourceTypes: Array<ResourceTypeEntry> = [
|
|
25
|
+
{
|
|
26
|
+
name: 'category',
|
|
27
|
+
apiPath: 'categories',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'product',
|
|
31
|
+
apiPath: 'products',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'inventory',
|
|
35
|
+
apiPath: 'inventories',
|
|
36
|
+
},
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
const mockServer = setupServer(
|
|
40
|
+
...resourceTypes.map(({ apiPath }) =>
|
|
41
|
+
rest.post(
|
|
42
|
+
`http://localhost:8080/proxy/import/test-with-big-data-key/${apiPath}/import-containers/container-key/file-upload`,
|
|
43
|
+
(_, res, ctx) => res(ctx.json(validFileUploadResponse))
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
mockServer.resetHandlers()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
beforeAll(() => {
|
|
53
|
+
mockServer.listen()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
mockServer.close()
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('uploadFileForImport', () => {
|
|
61
|
+
const projectKey = 'test-with-big-data-key'
|
|
62
|
+
const containerKey = 'container-key'
|
|
63
|
+
const file: File = new File([], 'file.csv')
|
|
64
|
+
|
|
65
|
+
it.each([['category'], ['product'], ['inventory']])(
|
|
66
|
+
'should post the file to the correct endpoint for the resource type %s',
|
|
67
|
+
async (resourceType) => {
|
|
68
|
+
const onSuccess = jest.fn()
|
|
69
|
+
uploadFileForImport({
|
|
70
|
+
projectKey,
|
|
71
|
+
importContainerKey: containerKey,
|
|
72
|
+
resourceType,
|
|
73
|
+
file,
|
|
74
|
+
onSuccess,
|
|
75
|
+
onProgress: jest.fn(),
|
|
76
|
+
onError: jest.fn(),
|
|
77
|
+
})
|
|
78
|
+
await waitFor(() =>
|
|
79
|
+
expect(onSuccess).toHaveBeenCalledWith(validFileUploadResponse)
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { MC_API_PROXY_TARGETS } from '@commercetools-frontend/constants'
|
|
2
|
+
import {
|
|
3
|
+
type FileUploadResponse,
|
|
4
|
+
type FileUploadRequestParameters,
|
|
5
|
+
hasRequiredFields,
|
|
6
|
+
} from '../@types'
|
|
7
|
+
import { fetchUsingXhr } from './fetcher'
|
|
8
|
+
import { getFileUploadURL } from './urls'
|
|
9
|
+
|
|
10
|
+
export function uploadFileForImport({
|
|
11
|
+
projectKey,
|
|
12
|
+
importContainerKey,
|
|
13
|
+
resourceType,
|
|
14
|
+
file,
|
|
15
|
+
abortSignal,
|
|
16
|
+
onSuccess,
|
|
17
|
+
onProgress,
|
|
18
|
+
onError,
|
|
19
|
+
}: FileUploadRequestParameters): XMLHttpRequest {
|
|
20
|
+
const uri = getFileUploadURL({ projectKey, resourceType, importContainerKey })
|
|
21
|
+
const formData = new FormData()
|
|
22
|
+
formData.append('file', file, file.name)
|
|
23
|
+
return fetchUsingXhr({
|
|
24
|
+
url: uri,
|
|
25
|
+
payload: formData,
|
|
26
|
+
config: {
|
|
27
|
+
abortSignal,
|
|
28
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: {
|
|
31
|
+
'Content-Type': null,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
onProgress,
|
|
35
|
+
onSuccess,
|
|
36
|
+
onError,
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function assertFileUploadResponse(
|
|
41
|
+
maybeFileUploadResponse: unknown
|
|
42
|
+
): asserts maybeFileUploadResponse is FileUploadResponse {
|
|
43
|
+
const requiredFields = ['results', 'valid']
|
|
44
|
+
if (hasRequiredFields(maybeFileUploadResponse, requiredFields)) return
|
|
45
|
+
throw new Error('Invalid response')
|
|
46
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ImportContainer,
|
|
3
|
+
ImportContainerPagedResponse,
|
|
4
|
+
} from '@commercetools/importapi-sdk'
|
|
5
|
+
import { MC_API_PROXY_TARGETS } from '@commercetools-frontend/constants'
|
|
6
|
+
import {
|
|
7
|
+
assertImportContainer,
|
|
8
|
+
assertImportContainerPagedResponse,
|
|
9
|
+
assertImportSummary,
|
|
10
|
+
ImportStates,
|
|
11
|
+
type ImportContainerQueryParams,
|
|
12
|
+
type CancelContainerResponse,
|
|
13
|
+
type ImportSummary,
|
|
14
|
+
type ExtendedImportContainer,
|
|
15
|
+
type ExtendedImportContainerDraft,
|
|
16
|
+
type ImportContainerDetails,
|
|
17
|
+
type ImportSummaries,
|
|
18
|
+
assertCancelContainerResponse,
|
|
19
|
+
} from '../@types'
|
|
20
|
+
import {
|
|
21
|
+
getImportContainersURL,
|
|
22
|
+
getImportContainerByKeyURL,
|
|
23
|
+
getImportSummaryURL,
|
|
24
|
+
getCreateImportContainerURL,
|
|
25
|
+
getDeleteImportContainerURL,
|
|
26
|
+
getImportContainerTasksURL,
|
|
27
|
+
} from './urls'
|
|
28
|
+
import { checkIfFileUploadImport } from '../@utils'
|
|
29
|
+
import { fetcher } from './fetcher'
|
|
30
|
+
|
|
31
|
+
export function getImportState(importSummary: ImportSummary): ImportStates {
|
|
32
|
+
const processing = importSummary.states.processing > 0
|
|
33
|
+
if (processing) return ImportStates.Processing
|
|
34
|
+
|
|
35
|
+
const waitForUnresolvedReferences =
|
|
36
|
+
importSummary.states.waitForMasterVariant > 0 ||
|
|
37
|
+
importSummary.states.unresolved > 0
|
|
38
|
+
if (waitForUnresolvedReferences)
|
|
39
|
+
return ImportStates.WaitForUnresolvedReferences
|
|
40
|
+
|
|
41
|
+
const partiallyCompleted =
|
|
42
|
+
(importSummary.states.imported > 0 &&
|
|
43
|
+
importSummary.states.imported < importSummary.total) ||
|
|
44
|
+
(importSummary.states.deleted > 0 &&
|
|
45
|
+
importSummary.states.deleted < importSummary.total)
|
|
46
|
+
if (partiallyCompleted) return ImportStates.PartiallyCompleted
|
|
47
|
+
|
|
48
|
+
const noRunning = importSummary.total === 0
|
|
49
|
+
if (noRunning) return ImportStates.NoRunningImports
|
|
50
|
+
|
|
51
|
+
const successfullyCompleted =
|
|
52
|
+
importSummary.states.imported === importSummary.total ||
|
|
53
|
+
importSummary.states.deleted === importSummary.total
|
|
54
|
+
if (successfullyCompleted) return ImportStates.SuccessfullyCompleted
|
|
55
|
+
|
|
56
|
+
const failed =
|
|
57
|
+
importSummary.states.rejected + importSummary.states.validationFailed ===
|
|
58
|
+
importSummary.total
|
|
59
|
+
if (failed) return ImportStates.Failed
|
|
60
|
+
|
|
61
|
+
const canceled = importSummary.states.canceled > 0
|
|
62
|
+
if (canceled) return ImportStates.Canceled
|
|
63
|
+
|
|
64
|
+
throw new Error(`Unsupported state ${JSON.stringify(importSummary.states)}`)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createImportContainerForFileUpload({
|
|
68
|
+
importContainerDraft,
|
|
69
|
+
projectKey,
|
|
70
|
+
}: {
|
|
71
|
+
importContainerDraft: ExtendedImportContainerDraft
|
|
72
|
+
projectKey: string
|
|
73
|
+
}): Promise<unknown> {
|
|
74
|
+
return fetcher({
|
|
75
|
+
url: getCreateImportContainerURL({ projectKey }),
|
|
76
|
+
payload: JSON.stringify({
|
|
77
|
+
retentionPolicy: {
|
|
78
|
+
strategy: 'ttl',
|
|
79
|
+
config: {
|
|
80
|
+
timeToLive: '54h', // 2 days and 6 hours
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
...importContainerDraft,
|
|
84
|
+
}),
|
|
85
|
+
config: {
|
|
86
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: {
|
|
89
|
+
accept: 'application/json',
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function deleteImportContainer({
|
|
97
|
+
projectKey,
|
|
98
|
+
importContainerKey,
|
|
99
|
+
}: {
|
|
100
|
+
projectKey: string
|
|
101
|
+
importContainerKey: string
|
|
102
|
+
}): Promise<unknown> {
|
|
103
|
+
return fetcher({
|
|
104
|
+
url: getDeleteImportContainerURL({ projectKey, importContainerKey }),
|
|
105
|
+
config: {
|
|
106
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
107
|
+
method: 'DELETE',
|
|
108
|
+
headers: {
|
|
109
|
+
accept: 'application/json',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function fetchImportSummary({
|
|
116
|
+
projectKey,
|
|
117
|
+
importContainerKey,
|
|
118
|
+
}: {
|
|
119
|
+
projectKey: string
|
|
120
|
+
importContainerKey: string
|
|
121
|
+
}): Promise<ImportSummary> {
|
|
122
|
+
const importSummary = await fetcher<ImportSummary>({
|
|
123
|
+
url: getImportSummaryURL({ projectKey, importContainerKey }),
|
|
124
|
+
config: {
|
|
125
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
126
|
+
method: 'GET',
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
assertImportSummary(importSummary)
|
|
131
|
+
|
|
132
|
+
return importSummary
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function fetchImportContainers({
|
|
136
|
+
projectKey,
|
|
137
|
+
queryParams,
|
|
138
|
+
}: {
|
|
139
|
+
projectKey: string
|
|
140
|
+
queryParams: ImportContainerQueryParams
|
|
141
|
+
}): Promise<ImportContainerPagedResponse> {
|
|
142
|
+
const importContainers = await fetcher<ImportContainerPagedResponse>({
|
|
143
|
+
url: getImportContainersURL({ projectKey, queryParams }),
|
|
144
|
+
config: {
|
|
145
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
146
|
+
method: 'GET',
|
|
147
|
+
},
|
|
148
|
+
})
|
|
149
|
+
assertImportContainerPagedResponse(importContainers)
|
|
150
|
+
|
|
151
|
+
return importContainers
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function fetchImportSummaries({
|
|
155
|
+
projectKey,
|
|
156
|
+
queryParams,
|
|
157
|
+
}: {
|
|
158
|
+
projectKey: string
|
|
159
|
+
queryParams: ImportContainerQueryParams
|
|
160
|
+
}): Promise<ImportSummaries> {
|
|
161
|
+
const importContainers = await fetchImportContainers({
|
|
162
|
+
projectKey,
|
|
163
|
+
queryParams,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
const results: Array<Promise<ImportContainerDetails>> =
|
|
167
|
+
importContainers.results.map(async (importContainer) => {
|
|
168
|
+
return await importContainerToContainerDetails(
|
|
169
|
+
projectKey,
|
|
170
|
+
importContainer
|
|
171
|
+
)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
results,
|
|
176
|
+
count: importContainers.count,
|
|
177
|
+
total: importContainers.total ?? 0,
|
|
178
|
+
queryParams: {
|
|
179
|
+
limit: importContainers.limit,
|
|
180
|
+
offset: importContainers.offset,
|
|
181
|
+
},
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export async function fetchImportContainerDetails({
|
|
186
|
+
projectKey,
|
|
187
|
+
importContainerKey,
|
|
188
|
+
}: {
|
|
189
|
+
projectKey: string
|
|
190
|
+
importContainerKey: string
|
|
191
|
+
}): Promise<ImportContainerDetails> {
|
|
192
|
+
const importContainer = await fetchImportContainerByKey({
|
|
193
|
+
projectKey,
|
|
194
|
+
importContainerKey,
|
|
195
|
+
})
|
|
196
|
+
return await importContainerToContainerDetails(projectKey, importContainer)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export async function fetchImportContainerByKey({
|
|
200
|
+
projectKey,
|
|
201
|
+
importContainerKey,
|
|
202
|
+
}: {
|
|
203
|
+
projectKey: string
|
|
204
|
+
importContainerKey: string
|
|
205
|
+
}): Promise<ImportContainer> {
|
|
206
|
+
const importContainer = await fetcher<ImportContainer>({
|
|
207
|
+
url: getImportContainerByKeyURL({ projectKey, importContainerKey }),
|
|
208
|
+
config: {
|
|
209
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
210
|
+
method: 'GET',
|
|
211
|
+
},
|
|
212
|
+
})
|
|
213
|
+
assertImportContainer(importContainer)
|
|
214
|
+
|
|
215
|
+
return importContainer
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function cancelImportContainerByKey({
|
|
219
|
+
projectKey,
|
|
220
|
+
importContainerKey,
|
|
221
|
+
}: {
|
|
222
|
+
projectKey: string
|
|
223
|
+
importContainerKey: string
|
|
224
|
+
}): Promise<CancelContainerResponse> {
|
|
225
|
+
const response = await fetcher<CancelContainerResponse>({
|
|
226
|
+
url: getImportContainerTasksURL({ projectKey, importContainerKey }),
|
|
227
|
+
payload: JSON.stringify({
|
|
228
|
+
task: 'cancel',
|
|
229
|
+
}),
|
|
230
|
+
config: {
|
|
231
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
232
|
+
method: 'POST',
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
assertCancelContainerResponse(response)
|
|
236
|
+
return response
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function importContainerToContainerDetails(
|
|
240
|
+
projectKey: string,
|
|
241
|
+
importContainer: ExtendedImportContainer
|
|
242
|
+
): Promise<ImportContainerDetails> {
|
|
243
|
+
const importSummary = await fetchImportSummary({
|
|
244
|
+
projectKey,
|
|
245
|
+
importContainerKey: importContainer.key,
|
|
246
|
+
})
|
|
247
|
+
const importState = getImportState(importSummary)
|
|
248
|
+
const isFileUploadImport = checkIfFileUploadImport(importContainer.tags)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
importContainer: importContainer,
|
|
252
|
+
importState,
|
|
253
|
+
importSummary,
|
|
254
|
+
isFileUploadImport,
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MC_API_PROXY_TARGETS } from '@commercetools-frontend/constants'
|
|
2
|
+
import {
|
|
3
|
+
assertImportOperationPagedResponse,
|
|
4
|
+
type ExtendedImportOperationPagedResponse,
|
|
5
|
+
type ImportOperationQueryParams,
|
|
6
|
+
} from '../@types'
|
|
7
|
+
import { fetcher } from './fetcher'
|
|
8
|
+
import { getImportOperationsURL } from './urls'
|
|
9
|
+
|
|
10
|
+
export async function fetchImportOperations({
|
|
11
|
+
projectKey,
|
|
12
|
+
importContainerKey,
|
|
13
|
+
queryParams,
|
|
14
|
+
}: {
|
|
15
|
+
projectKey: string
|
|
16
|
+
importContainerKey: string
|
|
17
|
+
queryParams: ImportOperationQueryParams
|
|
18
|
+
}): Promise<ExtendedImportOperationPagedResponse> {
|
|
19
|
+
const importOperations = await fetcher<ExtendedImportOperationPagedResponse>({
|
|
20
|
+
url: getImportOperationsURL({
|
|
21
|
+
projectKey,
|
|
22
|
+
importContainerKey,
|
|
23
|
+
queryParams,
|
|
24
|
+
}),
|
|
25
|
+
config: {
|
|
26
|
+
proxy: MC_API_PROXY_TARGETS.IMPORT,
|
|
27
|
+
method: 'GET',
|
|
28
|
+
},
|
|
29
|
+
})
|
|
30
|
+
assertImportOperationPagedResponse(importOperations)
|
|
31
|
+
|
|
32
|
+
return importOperations
|
|
33
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { plural } from 'pluralize'
|
|
2
|
+
import { rest } from 'msw'
|
|
3
|
+
import { setupServer } from 'msw/node'
|
|
4
|
+
import { processUploadedFile } from './process-file'
|
|
5
|
+
import { validProcessFileResponse } from './test-fixtures'
|
|
6
|
+
|
|
7
|
+
const mockServer = setupServer()
|
|
8
|
+
|
|
9
|
+
beforeAll(() => {
|
|
10
|
+
mockServer.listen()
|
|
11
|
+
})
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
mockServer.resetHandlers()
|
|
14
|
+
})
|
|
15
|
+
afterAll(() => {
|
|
16
|
+
mockServer.close()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('processUploadedFile', () => {
|
|
20
|
+
const projectKey = 'test-with-big-data'
|
|
21
|
+
const containerKey = 'container-key'
|
|
22
|
+
const resourceType = 'category'
|
|
23
|
+
|
|
24
|
+
it('should process the file with the correct endpoint for the current project', async () => {
|
|
25
|
+
mockServer.use(
|
|
26
|
+
rest.post(
|
|
27
|
+
`http://localhost:8080/proxy/import/${projectKey}/${plural(
|
|
28
|
+
resourceType
|
|
29
|
+
)}/import-containers/${containerKey}/process-file`,
|
|
30
|
+
(_, res, ctx) => {
|
|
31
|
+
return res(ctx.json(validProcessFileResponse))
|
|
32
|
+
}
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const response = await processUploadedFile({
|
|
37
|
+
projectKey,
|
|
38
|
+
importContainerKey: containerKey,
|
|
39
|
+
resourceType,
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
expect(response.message).toEqual(validProcessFileResponse.message)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should process the file with delete action using the tasks endpoint', async () => {
|
|
46
|
+
let requestBody: any
|
|
47
|
+
|
|
48
|
+
mockServer.use(
|
|
49
|
+
rest.post(
|
|
50
|
+
`http://localhost:8080/proxy/import/${projectKey}/import-containers/${containerKey}/tasks`,
|
|
51
|
+
async (req, res, ctx) => {
|
|
52
|
+
requestBody = await req.json()
|
|
53
|
+
return res(ctx.json(validProcessFileResponse))
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const response = await processUploadedFile({
|
|
59
|
+
projectKey,
|
|
60
|
+
importContainerKey: containerKey,
|
|
61
|
+
resourceType,
|
|
62
|
+
action: 'delete',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
expect(response.message).toEqual(validProcessFileResponse.message)
|
|
66
|
+
expect(requestBody).toEqual({
|
|
67
|
+
task: 'process-file',
|
|
68
|
+
parameter: {
|
|
69
|
+
resourceType,
|
|
70
|
+
action: 'delete',
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
})
|