@knime/utils 1.1.0 → 1.1.2

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @knime/utils
2
2
 
3
+ ## 1.1.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 15a4479: add utilities for file extensions
8
+
9
+ ## 1.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies
14
+ - @knime/styles@1.1.1
15
+
3
16
  ## 1.1.0
4
17
 
5
18
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@knime/utils",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Internal utility functions and constants shared across @knime packages.",
5
5
  "homepage": "https://knime.github.io/webapps-common/",
6
6
  "license": "GPL 3 and Additional Permissions according to Sec. 7 (SEE the file LICENSE)",
@@ -10,13 +10,13 @@
10
10
  "consola": "3.2.3",
11
11
  "date-fns": "2.30.0",
12
12
  "date-fns-tz": "2.0.0",
13
- "@knime/styles": "1.1.0"
13
+ "@knime/styles": "1.1.1"
14
14
  },
15
15
  "main": "src/index.ts",
16
16
  "module": "src/index.ts",
17
17
  "files": [
18
18
  "src",
19
- "!src/__tests__",
19
+ "!src/**/__tests__/*",
20
20
  "test-utils",
21
21
  "CHANGELOG.md"
22
22
  ],
@@ -0,0 +1,68 @@
1
+ export const getFileExtension = (nameOrPath: string) => {
2
+ const basename = nameOrPath.split(/[\\/]/).pop();
3
+ const position = basename?.lastIndexOf(".") as number;
4
+
5
+ if (basename === "" || position < 1) {
6
+ return "";
7
+ }
8
+
9
+ return basename?.slice(position + 1);
10
+ };
11
+
12
+ /**
13
+ * Metadata for .knar files, which corresponds to exported KNIME Workflow Groups
14
+ * or Components
15
+ */
16
+ const KNAR = Object.freeze({
17
+ extension: "knar",
18
+ mimeType: "application/vnd.knime.workflow-group+zip",
19
+ matches(file: File) {
20
+ const extension = getFileExtension(file.name);
21
+ return extension === this.extension;
22
+ },
23
+ });
24
+
25
+ /**
26
+ * Metadata for .knwf files, which corresponds to exported KNIME Workflows
27
+ */
28
+ const KNWF = Object.freeze({
29
+ extension: "knwf",
30
+ mimeType: "application/vnd.knime.workflow+zip",
31
+ matches(file: File) {
32
+ const extension = getFileExtension(file.name);
33
+ return extension === this.extension;
34
+ },
35
+ });
36
+
37
+ export const knimeFileFormats = { KNAR, KNWF };
38
+
39
+ /**
40
+ * Gets the mime type from a given file. Defaults to `application/octet-stream`
41
+ * if mimeType cannot be detected
42
+ * @param file File
43
+ * @returns mimeType
44
+ */
45
+ export const getFileMimeType = (file: File): string => {
46
+ const DEFAULT_MIMETYPE = "application/octet-stream";
47
+
48
+ const extension = getFileExtension(file.name);
49
+
50
+ if (!extension) {
51
+ return DEFAULT_MIMETYPE;
52
+ }
53
+
54
+ if (KNWF.matches(file)) {
55
+ return KNWF.mimeType;
56
+ }
57
+
58
+ if (KNAR.matches(file)) {
59
+ return KNAR.mimeType;
60
+ }
61
+
62
+ // file has extension but unrecognizable mime type
63
+ if (!file.type) {
64
+ return DEFAULT_MIMETYPE;
65
+ }
66
+
67
+ return file.type;
68
+ };
package/src/index.ts CHANGED
@@ -45,6 +45,8 @@ export namespace UploadManagerNS {
45
45
  export type UploadStateName = _UploadStateName;
46
46
  }
47
47
 
48
+ export * from "./fileExtensions";
49
+
48
50
  export {
49
51
  uploadManager,
50
52
  promise,
@@ -56,12 +56,22 @@ const uploadChunkWithProgress = (params: {
56
56
  // https://datatracker.ietf.org/doc/html/rfc7232#section-2.3
57
57
  resolve({ partId: etag.replace(/^(?:W\/)?"|"$/g, "") });
58
58
  } else {
59
- reject(new Error(`Failed to upload chunk ${chunkIndex}`));
59
+ consola.error("Failed or unexpected XHR response", {
60
+ xhrStatus: xhr.status,
61
+ xhrResponse: xhr.response,
62
+ chunkIndex,
63
+ });
64
+ reject(new Error("Failed to upload file"));
60
65
  }
61
66
  };
62
67
 
63
- xhr.onerror = () =>
64
- reject(new Error(`Failed to upload chunk ${chunkIndex}`));
68
+ xhr.onerror = (event) => {
69
+ consola.error("Failed to upload chunk", {
70
+ xhrErrorEvent: event,
71
+ chunkIndex,
72
+ });
73
+ reject(new Error("Failed to upload file"));
74
+ };
65
75
 
66
76
  xhr.send(chunk);
67
77
  });
@@ -1,31 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import flushPromises from "flush-promises";
3
-
4
- import { AbortError, createAbortablePromise } from "../createAbortablePromise";
5
-
6
- describe("createAbortablePromise", () => {
7
- it("should abort promise", async () => {
8
- const mockFetchCall = () =>
9
- new Promise((r) => setTimeout(() => r("resolved-value"), 300));
10
-
11
- const { abortController, runAbortablePromise } = createAbortablePromise();
12
-
13
- const catchSpy = vi.fn((e) => {
14
- if (e instanceof AbortError) {
15
- return "aborted";
16
- }
17
-
18
- throw e;
19
- });
20
-
21
- const promise = runAbortablePromise(() => mockFetchCall()).catch(catchSpy);
22
-
23
- const abortError = new AbortError("aborting");
24
- abortController.abort(abortError);
25
-
26
- await flushPromises();
27
-
28
- expect(catchSpy).toHaveBeenCalledWith(abortError);
29
- expect(promise).resolves.toBe("aborted");
30
- });
31
- });
@@ -1,73 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
-
3
- import { retryPromise } from "../retryPromise";
4
-
5
- describe("retryPromise", () => {
6
- it("should retry", async () => {
7
- let callCount = 0;
8
- const mockFetchCall = vi.fn(
9
- () =>
10
- new Promise((resolve, reject) => {
11
- callCount++;
12
-
13
- if (callCount < 3) {
14
- reject(new Error("failure"));
15
- return;
16
- }
17
-
18
- if (callCount >= 3) {
19
- resolve("success");
20
- }
21
- }),
22
- );
23
-
24
- const doneSpy = vi.fn();
25
- const catchSpy = vi.fn();
26
-
27
- const promise = retryPromise(() => mockFetchCall())
28
- .then(doneSpy)
29
- .catch(catchSpy);
30
-
31
- await promise;
32
-
33
- expect(doneSpy).toHaveBeenCalledOnce();
34
- expect(catchSpy).not.toHaveBeenCalled();
35
- expect(mockFetchCall).toHaveBeenCalledTimes(3);
36
- });
37
-
38
- it("should throw if retry count is exceeded", async () => {
39
- const error = new Error("failure");
40
- const mockFetchCall = vi.fn(() => Promise.reject(error));
41
-
42
- const doneSpy = vi.fn();
43
-
44
- const promise = retryPromise(() => mockFetchCall()).then(doneSpy);
45
-
46
- await promise.catch(() => {}); // ignore errors caught by test
47
- expect(() => promise).rejects.toThrowError(error);
48
-
49
- expect(doneSpy).not.toHaveBeenCalled();
50
- expect(mockFetchCall).toHaveBeenCalledTimes(6);
51
- });
52
-
53
- it("should not retry for specific errors", async () => {
54
- class MyError extends Error {}
55
- const myError = new MyError("failure");
56
- const mockFetchCall = vi.fn(() => Promise.reject(myError));
57
-
58
- const doneSpy = vi.fn();
59
-
60
- const promise = retryPromise(
61
- () => mockFetchCall(),
62
- 2,
63
- 0,
64
- (e) => e instanceof MyError,
65
- ).then(doneSpy);
66
-
67
- await promise.catch(() => {}); // ignore errors caught by test
68
- expect(() => promise).rejects.toThrowError(myError);
69
-
70
- expect(doneSpy).not.toHaveBeenCalled();
71
- expect(mockFetchCall).toHaveBeenCalledOnce();
72
- });
73
- });
@@ -1,302 +0,0 @@
1
- /* eslint-disable vitest/no-disabled-tests */
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { newServer } from "mock-xmlhttprequest";
4
-
5
- import { createUploadManager } from "..";
6
-
7
- describe("uploadManager", () => {
8
- const fiftyBytes = new Array(50).fill("A");
9
-
10
- const file = new File(fiftyBytes, "mock-file.txt", {
11
- type: "text/plain",
12
- });
13
-
14
- type MockXHRHandler = NonNullable<Parameters<typeof newServer>[0]>[string][1];
15
-
16
- const defaultUploadPartResponse: MockXHRHandler = {
17
- // status: 200 is the default
18
- headers: { "Content-Type": "application/json", Etag: '"partId"' },
19
- body: "{}",
20
- };
21
-
22
- type Options = {
23
- uploadPartResponse?: MockXHRHandler;
24
- };
25
-
26
- const setup = ({ uploadPartResponse }: Options = {}) => {
27
- const server = newServer({
28
- put: ["/success", uploadPartResponse ?? defaultUploadPartResponse],
29
- });
30
-
31
- server.install();
32
-
33
- return { server };
34
- };
35
-
36
- const mockUploadURLMetadata = { method: "PUT", url: "/success" };
37
- const resolveFilePartUploadURL = vi.fn();
38
-
39
- beforeEach(() => {
40
- resolveFilePartUploadURL.mockImplementation(() =>
41
- Promise.resolve(mockUploadURLMetadata),
42
- );
43
- });
44
-
45
- const createIdGenerator = () => {
46
- const generator = (function* () {
47
- let id = 0;
48
- while (true) {
49
- yield id++;
50
- }
51
- })();
52
-
53
- return () => `mock-upload--${generator.next().value}`;
54
- };
55
-
56
- const getUploadId = createIdGenerator();
57
-
58
- describe("success", () => {
59
- it("should handle `uploadFiles`", async () => {
60
- setup();
61
- const { uploadFiles } = createUploadManager({
62
- resolveFilePartUploadURL,
63
- });
64
-
65
- const uploadId = getUploadId();
66
- const { completed, failed, cancelled } = await uploadFiles([
67
- { uploadId, file },
68
- ]);
69
-
70
- expect(completed).toEqual([uploadId]);
71
- expect(failed.length).toBe(0);
72
- expect(cancelled.length).toBe(0);
73
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
74
- });
75
-
76
- it("should succeed if resolveFilePartUrl is retried successfully", async () => {
77
- let callCount = 0;
78
- resolveFilePartUploadURL.mockImplementation(() => {
79
- callCount++;
80
- if (callCount < 2) {
81
- return Promise.reject(new Error("error that will be retried"));
82
- }
83
-
84
- return Promise.resolve(mockUploadURLMetadata);
85
- });
86
-
87
- setup();
88
- const { uploadFiles } = createUploadManager({
89
- resolveFilePartUploadURL,
90
- });
91
-
92
- const uploadId = getUploadId();
93
- const { completed, failed, cancelled } = await uploadFiles([
94
- { uploadId, file },
95
- ]);
96
-
97
- expect(completed).toEqual([uploadId]);
98
- expect(failed.length).toBe(0);
99
- expect(cancelled.length).toBe(0);
100
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
101
- });
102
-
103
- it("should succeed if uploadPart request is retried successfully", async () => {
104
- let callCount = 0;
105
- setup({
106
- uploadPartResponse: (request) => {
107
- callCount++;
108
- if (callCount <= 3) {
109
- request.respond(400, {});
110
- }
111
-
112
- request.respond(200, { Etag: '"partId"' });
113
- },
114
- });
115
-
116
- const uploadId = getUploadId();
117
- const { uploadFiles } = createUploadManager({
118
- resolveFilePartUploadURL,
119
- });
120
-
121
- const { completed, failed, cancelled } = await uploadFiles([
122
- { uploadId, file },
123
- ]);
124
-
125
- expect(completed).toEqual([uploadId]);
126
- expect(failed.length).toBe(0);
127
- expect(cancelled.length).toBe(0);
128
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
129
- });
130
- });
131
-
132
- describe("callbacks", () => {
133
- it("should call onFileUploadComplete", async () => {
134
- setup();
135
- const onComplete = vi.fn();
136
- const onFailed = vi.fn();
137
- const { uploadFiles } = createUploadManager({
138
- resolveFilePartUploadURL,
139
- onFileUploadComplete: onComplete,
140
- onFileUploadFailed: onFailed,
141
- });
142
-
143
- const uploadId1 = getUploadId();
144
- const uploadId2 = getUploadId();
145
-
146
- const { completed, failed, cancelled } = await uploadFiles([
147
- { uploadId: uploadId1, file },
148
- { uploadId: uploadId2, file },
149
- ]);
150
-
151
- expect(completed).toEqual([uploadId1, uploadId2]);
152
- expect(failed.length).toBe(0);
153
- expect(cancelled.length).toBe(0);
154
- expect(onComplete).toHaveBeenCalledWith({
155
- uploadId: uploadId1,
156
- filePartIds: { 1: "partId" },
157
- });
158
- expect(onComplete).toHaveBeenCalledWith({
159
- uploadId: uploadId2,
160
- filePartIds: { 1: "partId" },
161
- });
162
- expect(onFailed).not.toHaveBeenCalled();
163
- });
164
-
165
- it("should call onFileUploadFailed", async () => {
166
- setup({
167
- uploadPartResponse: (request) => {
168
- request.respond(400, {});
169
- },
170
- });
171
- const onComplete = vi.fn();
172
- const onFailed = vi.fn();
173
- const { uploadFiles } = createUploadManager({
174
- resolveFilePartUploadURL,
175
- onFileUploadComplete: onComplete,
176
- onFileUploadFailed: onFailed,
177
- });
178
-
179
- const uploadId1 = getUploadId();
180
- const uploadId2 = getUploadId();
181
- const { completed, failed, cancelled } = await uploadFiles([
182
- { uploadId: uploadId1, file },
183
- { uploadId: uploadId2, file },
184
- ]);
185
-
186
- expect(completed.length).toBe(0);
187
- expect(failed).toEqual([uploadId1, uploadId2]);
188
- expect(cancelled.length).toBe(0);
189
- expect(onFailed).toHaveBeenCalledWith(
190
- uploadId1,
191
- new Error("Failed to upload chunk 0"),
192
- );
193
- expect(onFailed).toHaveBeenCalledWith(
194
- uploadId2,
195
- new Error("Failed to upload chunk 0"),
196
- );
197
- expect(onComplete).not.toHaveBeenCalled();
198
- });
199
-
200
- it("should call onProgress callback", async () => {
201
- setup();
202
- const onProgress = vi.fn();
203
-
204
- const { uploadFiles } = createUploadManager({
205
- resolveFilePartUploadURL,
206
- onProgress,
207
- });
208
-
209
- await uploadFiles([{ uploadId: getUploadId(), file }]);
210
-
211
- expect(onProgress).toHaveBeenCalled();
212
- });
213
- });
214
-
215
- describe("cancellation", () => {
216
- it("should handle cancellation", async () => {
217
- setup();
218
- const { uploadFiles, cancel } = createUploadManager({
219
- resolveFilePartUploadURL,
220
- });
221
-
222
- const uploadId = getUploadId();
223
- const upload = uploadFiles([{ uploadId, file }]);
224
-
225
- cancel(uploadId);
226
-
227
- const { completed, cancelled, failed } = await upload;
228
- expect(completed.length).toBe(0);
229
- expect(cancelled).toEqual([uploadId]);
230
- expect(failed.length).toBe(0);
231
- });
232
- });
233
-
234
- describe("failures", () => {
235
- it("should fail when resolving upload url fails", async () => {
236
- resolveFilePartUploadURL.mockImplementation(() =>
237
- Promise.reject(new Error("error")),
238
- );
239
-
240
- setup();
241
- const { uploadFiles } = createUploadManager({
242
- resolveFilePartUploadURL,
243
- });
244
-
245
- const uploadId = getUploadId();
246
- const { completed, failed, cancelled } = await uploadFiles([
247
- { uploadId, file },
248
- ]);
249
-
250
- expect(completed.length).toBe(0);
251
- expect(failed).toEqual([uploadId]);
252
- expect(cancelled.length).toBe(0);
253
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
254
- });
255
-
256
- it("should fail when uploading part request fails", async () => {
257
- setup({
258
- uploadPartResponse: (request) => {
259
- request.respond(400, {});
260
- },
261
- });
262
-
263
- const { uploadFiles } = createUploadManager({
264
- resolveFilePartUploadURL,
265
- });
266
-
267
- const uploadId = getUploadId();
268
-
269
- const { completed, failed, cancelled } = await uploadFiles([
270
- { uploadId, file },
271
- ]);
272
-
273
- expect(completed.length).toBe(0);
274
- expect(failed).toEqual([uploadId]);
275
- expect(cancelled.length).toBe(0);
276
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
277
- });
278
-
279
- it("should fail when etag is missing from upload part response", async () => {
280
- setup({
281
- uploadPartResponse: (request) => {
282
- request.respond(200, {});
283
- },
284
- });
285
-
286
- const { uploadFiles } = createUploadManager({
287
- resolveFilePartUploadURL,
288
- });
289
-
290
- const uploadId = getUploadId();
291
-
292
- const { completed, failed, cancelled } = await uploadFiles([
293
- { uploadId, file },
294
- ]);
295
-
296
- expect(completed.length).toBe(0);
297
- expect(failed).toEqual([uploadId]);
298
- expect(cancelled.length).toBe(0);
299
- expect(resolveFilePartUploadURL).toHaveBeenCalled();
300
- });
301
- });
302
- });