@opencloning/opencloningdb 1.8.3 → 1.9.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 +82 -0
- package/package.json +4 -4
- package/src/GetSequencesInBulkComponent.cy.jsx +116 -0
- package/src/GetSequencesInBulkComponent.jsx +74 -0
- package/src/OpenCloningDBInterface.js +8 -0
- package/src/OpenCloningDBInterface.multipart.test.js +2 -0
- package/src/OpenCloningDBInterface.test.js +2 -0
- package/src/SubmitCloningStrategiesInBulkComponent.cy.jsx +179 -0
- package/src/SubmitCloningStrategiesInBulkComponent.jsx +105 -0
- package/src/TagMultiSelect.cy.jsx +88 -0
- package/src/TagMultiSelect.jsx +61 -0
- package/src/bulk_upload/BulkUploadModal.jsx +40 -0
- package/src/bulk_upload/BulkUploadPreview.jsx +41 -0
- package/src/bulk_upload/CloningStrategyBulkUploadPreviewTable.jsx +103 -0
- package/src/bulk_upload/bulk_upload_utils.js +76 -0
- package/src/bulk_upload/bulk_upload_utils.test.js +85 -0
- package/src/bulk_upload/cloning_strategy_bulk_api.js +28 -0
- package/src/bulk_upload/common.jsx +142 -0
- package/src/common.js +7 -2
- package/src/common.test.js +8 -7
- package/src/endpoints.js +28 -21
- package/src/endpoints.test.js +17 -0
- package/src/index.js +6 -1
- package/src/testUtils.test.js +3 -3
- package/src/useCreateTagMutation.js +21 -0
package/src/common.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import axios from 'axios';
|
|
2
|
-
import { attachAuthInterceptors, setHttpClientUnauthorizedHandler } from '@opencloning/utils/httpClientAuth';
|
|
2
|
+
import { attachAuthInterceptors, setHttpClientTokenGetter, setHttpClientUnauthorizedHandler } from '@opencloning/utils/httpClientAuth';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export const baseUrl = `${import.meta.env?.VITE_OPENCLONING_DB_BACKEND || 'http://localhost:8000'}/db`;
|
|
6
6
|
|
|
7
|
-
export
|
|
7
|
+
export {
|
|
8
|
+
setHttpClientTokenGetter,
|
|
9
|
+
setHttpClientUnauthorizedHandler as setUnauthorizedHandler,
|
|
10
|
+
};
|
|
8
11
|
|
|
9
12
|
export function setWorkspaceHeader(id) {
|
|
10
13
|
openCloningDBHttpClient.defaults.headers.common['X-Workspace-Id'] = id;
|
|
@@ -16,6 +19,8 @@ export function clearWorkspaceHeader() {
|
|
|
16
19
|
|
|
17
20
|
export const openCloningDBHttpClient = axios.create({
|
|
18
21
|
baseURL: baseUrl,
|
|
22
|
+
// Every url is appended to baseURL, so requests (and the auth token) only reach the backend
|
|
23
|
+
allowAbsoluteUrls: false,
|
|
19
24
|
paramsSerializer: (params) => {
|
|
20
25
|
const searchParams = new URLSearchParams();
|
|
21
26
|
|
package/src/common.test.js
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
baseUrl,
|
|
5
5
|
clearWorkspaceHeader,
|
|
6
6
|
openCloningDBHttpClient,
|
|
7
|
+
setHttpClientTokenGetter,
|
|
7
8
|
setUnauthorizedHandler,
|
|
8
9
|
setWorkspaceHeader,
|
|
9
10
|
} from './common';
|
|
@@ -13,15 +14,15 @@ const responseFulfilled = openCloningDBHttpClient.interceptors.response.handlers
|
|
|
13
14
|
const responseRejected = openCloningDBHttpClient.interceptors.response.handlers[0].rejected;
|
|
14
15
|
|
|
15
16
|
beforeEach(() => {
|
|
16
|
-
localStorage.clear();
|
|
17
17
|
clearWorkspaceHeader();
|
|
18
18
|
setUnauthorizedHandler(null);
|
|
19
|
+
setHttpClientTokenGetter(null);
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
afterEach(() => {
|
|
22
|
-
localStorage.clear();
|
|
23
23
|
clearWorkspaceHeader();
|
|
24
24
|
setUnauthorizedHandler(null);
|
|
25
|
+
setHttpClientTokenGetter(null);
|
|
25
26
|
});
|
|
26
27
|
|
|
27
28
|
describe('common', () => {
|
|
@@ -55,20 +56,20 @@ describe('common', () => {
|
|
|
55
56
|
expect(openCloningDBHttpClient.defaults.paramsSerializer()).toBe('');
|
|
56
57
|
});
|
|
57
58
|
|
|
58
|
-
it('adds the bearer token
|
|
59
|
-
|
|
59
|
+
it('adds the bearer token from the registered token getter', async () => {
|
|
60
|
+
setHttpClientTokenGetter(() => '__TEST_TOKEN__');
|
|
60
61
|
const config = { headers: {} };
|
|
61
62
|
|
|
62
|
-
const result = requestFulfilled(config);
|
|
63
|
+
const result = await requestFulfilled(config);
|
|
63
64
|
|
|
64
65
|
expect(result).toBe(config);
|
|
65
66
|
expect(config.headers.Authorization).toBe('Bearer __TEST_TOKEN__');
|
|
66
67
|
});
|
|
67
68
|
|
|
68
|
-
it('leaves request headers unchanged when
|
|
69
|
+
it('leaves request headers unchanged when no token getter is set', async () => {
|
|
69
70
|
const config = { headers: {} };
|
|
70
71
|
|
|
71
|
-
const result = requestFulfilled(config);
|
|
72
|
+
const result = await requestFulfilled(config);
|
|
72
73
|
|
|
73
74
|
expect(result).toBe(config);
|
|
74
75
|
expect(config.headers.Authorization).toBeUndefined();
|
package/src/endpoints.js
CHANGED
|
@@ -1,49 +1,56 @@
|
|
|
1
|
+
import { encodePathSegment as e } from '@opencloning/utils/urlUtils';
|
|
2
|
+
|
|
3
|
+
// Every interpolated value is encoded to prevent client-side path traversal
|
|
4
|
+
// (e.g. a sample uid like "../workspaces/1/users/5").
|
|
1
5
|
const endpoints = {
|
|
2
6
|
sequences: '/sequences',
|
|
3
|
-
sequence: (id) => `/sequences/${id}`,
|
|
4
|
-
sequenceTextFile: (id) => `/sequences/${id}/text_file_sequence`,
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
sequence: (id) => `/sequences/${e(id)}`,
|
|
8
|
+
sequenceTextFile: (id) => `/sequences/${e(id)}/text_file_sequence`,
|
|
9
|
+
sequenceTextFiles: '/sequences/text_file_sequences',
|
|
10
|
+
sequenceChangeAnnotation: (id) => `/sequences/${e(id)}/change_annotation`,
|
|
11
|
+
sequenceCloningStrategy: (id) => `/sequences/${e(id)}/cloning_strategy`,
|
|
12
|
+
sequenceChildren: (id) => `/sequences/${e(id)}/children`,
|
|
13
|
+
sequenceLines: (id) => `/sequences/${e(id)}/lines`,
|
|
14
|
+
sequenceChangeCircularity: (id) => `/sequences/${e(id)}/change_circularity`,
|
|
15
|
+
sequencePrimers: (id) => `/sequences/${e(id)}/primers`,
|
|
16
|
+
sequenceSequencingFiles: (id) => `/sequences/${e(id)}/sequencing_files`,
|
|
12
17
|
sequenceSearch: '/sequences/search',
|
|
13
18
|
sequencesValidateUpload: '/sequences/validate-upload',
|
|
14
19
|
sequencesBulk: '/sequences/bulk',
|
|
15
20
|
sequencesCloningStrategyBulkValidate: '/sequences/cloning_strategy/bulk/validate',
|
|
16
21
|
sequencesCloningStrategyBulk: '/sequences/cloning_strategy/bulk',
|
|
17
|
-
sequencingFileDownload: (id) => `/sequencing_files/${id}/download`,
|
|
18
|
-
sequenceSequencingFileDelete: (sequenceId, fileId) => `/sequences/${sequenceId}/sequencing_files/${fileId}`,
|
|
22
|
+
sequencingFileDownload: (id) => `/sequencing_files/${e(id)}/download`,
|
|
23
|
+
sequenceSequencingFileDelete: (sequenceId, fileId) => `/sequences/${e(sequenceId)}/sequencing_files/${e(fileId)}`,
|
|
19
24
|
postSequence: '/sequences',
|
|
20
25
|
primers: '/primers',
|
|
21
26
|
primersValidateUpload: '/primers/validate-upload',
|
|
22
27
|
primersBulk: '/primers/bulk',
|
|
23
|
-
primer: (id) => `/primers/${id}`,
|
|
24
|
-
primerTemplateSequences: (id) => `/primers/${id}/sequences`,
|
|
28
|
+
primer: (id) => `/primers/${e(id)}`,
|
|
29
|
+
primerTemplateSequences: (id) => `/primers/${e(id)}/sequences`,
|
|
25
30
|
postPrimer: '/primers',
|
|
26
31
|
lines: '/lines',
|
|
27
|
-
line: (id) => `/lines/${id}`,
|
|
28
|
-
lineChildren: (id) => `/lines/${id}/children`,
|
|
29
|
-
lineTags: (id) => `/lines/${id}/tags`,
|
|
30
|
-
inputEntityTags: (id) => `/input_entities/${id}/tags`,
|
|
32
|
+
line: (id) => `/lines/${e(id)}`,
|
|
33
|
+
lineChildren: (id) => `/lines/${e(id)}/children`,
|
|
34
|
+
lineTags: (id) => `/lines/${e(id)}/tags`,
|
|
35
|
+
inputEntityTags: (id) => `/input_entities/${e(id)}/tags`,
|
|
31
36
|
postLine: '/lines',
|
|
32
37
|
linesValidateUpload: '/lines/validate-upload',
|
|
33
38
|
linesBulk: '/lines/bulk',
|
|
34
39
|
tags: '/tags',
|
|
35
40
|
tagPost: '/tags',
|
|
36
|
-
tagUnlinkLine: (lineId, tagId) => `/lines/${lineId}/tags/${tagId}`,
|
|
37
|
-
tagUnlinkInputEntity: (inputEntityId, tagId) => `/input_entities/${inputEntityId}/tags/${tagId}`,
|
|
41
|
+
tagUnlinkLine: (lineId, tagId) => `/lines/${e(lineId)}/tags/${e(tagId)}`,
|
|
42
|
+
tagUnlinkInputEntity: (inputEntityId, tagId) => `/input_entities/${e(inputEntityId)}/tags/${e(tagId)}`,
|
|
38
43
|
sequenceSamples: '/sequence_samples',
|
|
39
44
|
postSequenceSample: '/sequence_samples',
|
|
40
|
-
sequenceSample: (uid) => `/sequence_samples/${uid}`,
|
|
45
|
+
sequenceSample: (uid) => `/sequence_samples/${e(uid)}`,
|
|
41
46
|
authToken: '/auth/token',
|
|
42
47
|
authRegister: '/auth/register',
|
|
43
48
|
authMe: '/auth/me',
|
|
44
49
|
workspaces: '/workspaces',
|
|
45
50
|
postWorkspace: '/workspaces',
|
|
46
|
-
workspace: (id) => `/workspaces/${id}`,
|
|
51
|
+
workspace: (id) => `/workspaces/${e(id)}`,
|
|
52
|
+
workspaceUsers: (id) => `/workspaces/${e(id)}/users`,
|
|
53
|
+
workspaceUser: (workspaceId, userId) => `/workspaces/${e(workspaceId)}/users/${e(userId)}`,
|
|
47
54
|
templateSequences: '/template_sequences',
|
|
48
55
|
templateSequencesValidateUpload: '/template_sequences/validate-upload',
|
|
49
56
|
templateSequencesBulk: '/template_sequences/bulk',
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import endpoints from './endpoints';
|
|
4
|
+
|
|
5
|
+
describe('endpoints', () => {
|
|
6
|
+
it('encodes path parameters so they cannot change the request path', () => {
|
|
7
|
+
expect(endpoints.sequenceSample('../workspaces/1/users/5')).toBe('/sequence_samples/..%2Fworkspaces%2F1%2Fusers%2F5');
|
|
8
|
+
expect(endpoints.primer('1/../../workspaces/2')).toBe('/primers/1%2F..%2F..%2Fworkspaces%2F2');
|
|
9
|
+
expect(() => endpoints.sequenceSample('..')).toThrow();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('keeps normal ids unchanged', () => {
|
|
13
|
+
expect(endpoints.sequence(12)).toBe('/sequences/12');
|
|
14
|
+
expect(endpoints.sequenceSequencingFileDelete(3, 4)).toBe('/sequences/3/sequencing_files/4');
|
|
15
|
+
expect(endpoints.sequenceSample('ML-7_a')).toBe('/sequence_samples/ML-7_a');
|
|
16
|
+
});
|
|
17
|
+
});
|
package/src/index.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { default as OpenCloningDBInterface } from './OpenCloningDBInterface.js';
|
|
2
|
-
export { openCloningDBHttpClient, setUnauthorizedHandler, setWorkspaceHeader, clearWorkspaceHeader, baseUrl, formatSequenceName } from './common.js';
|
|
2
|
+
export { openCloningDBHttpClient, setUnauthorizedHandler, setHttpClientTokenGetter, setWorkspaceHeader, clearWorkspaceHeader, baseUrl, formatSequenceName } from './common.js';
|
|
3
3
|
export { default as endpoints } from './endpoints.js';
|
|
4
4
|
export { default as SequenceSelect } from './SequenceSelect.jsx';
|
|
5
|
+
export { default as TagMultiSelect } from './TagMultiSelect.jsx';
|
|
6
|
+
export { default as SubmitCloningStrategiesInBulkComponent } from './SubmitCloningStrategiesInBulkComponent.jsx';
|
|
7
|
+
export { default as BulkUploadPreview } from './bulk_upload/BulkUploadPreview.jsx';
|
|
8
|
+
export { default as BulkUploadModal } from './bulk_upload/BulkUploadModal.jsx';
|
|
9
|
+
export { CommonTableRow } from './bulk_upload/common.jsx';
|
package/src/testUtils.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import { http, HttpResponse } from 'msw';
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { baseUrl } from './common';
|
|
4
|
+
import { baseUrl, setHttpClientTokenGetter } from './common';
|
|
5
5
|
|
|
6
6
|
const STUB_FOLDER = `${__dirname}/../../../OpenCloning_backend/stubs/db`;
|
|
7
7
|
|
|
@@ -75,11 +75,11 @@ export function addStubToServer(server, stub) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export function setupToken() {
|
|
78
|
-
|
|
78
|
+
setHttpClientTokenGetter(() => '__TEST_TOKEN__');
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
export function clearToken() {
|
|
82
|
-
|
|
82
|
+
setHttpClientTokenGetter(null);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
describe('test utility module', () => {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
2
|
+
import { openCloningDBHttpClient } from './common';
|
|
3
|
+
import endpoints from './endpoints';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Mutation to create a tag, called with the tag name, resolves to the created tag.
|
|
7
|
+
* onError receives the error if the creation fails.
|
|
8
|
+
*/
|
|
9
|
+
export default function useCreateTagMutation({ onError } = {}) {
|
|
10
|
+
const queryClient = useQueryClient();
|
|
11
|
+
return useMutation({
|
|
12
|
+
mutationFn: async (tagName) => {
|
|
13
|
+
const { data } = await openCloningDBHttpClient.post(endpoints.tagPost, { name: tagName });
|
|
14
|
+
return data;
|
|
15
|
+
},
|
|
16
|
+
onSuccess: () => {
|
|
17
|
+
queryClient.invalidateQueries({ queryKey: ['tags'] });
|
|
18
|
+
},
|
|
19
|
+
onError,
|
|
20
|
+
});
|
|
21
|
+
}
|