@opencloning/opencloningdb 1.7.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ # @opencloning/opencloningdb
2
+
3
+ ## 1.7.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [[`8e841bd`](https://github.com/OpenCloning/OpenCloning_frontend/commit/8e841bdee4797df61bcf8d1972e3c3581d24fbfb)]:
8
+ - @opencloning/ui@1.8.1
9
+ - @opencloning/utils@1.8.1
10
+
11
+ ## 1.7.0
12
+
13
+ ### Minor Changes
14
+
15
+ - [#694](https://github.com/OpenCloning/OpenCloning_frontend/pull/694) [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57) Thanks [@manulera](https://github.com/manulera)! - Add package and app for a dedicated OpenCloning database, OpenCloningDB
16
+
17
+ ### Patch Changes
18
+
19
+ - Updated dependencies [[`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57), [`2ff9010`](https://github.com/OpenCloning/OpenCloning_frontend/commit/2ff90103c1d9e8e984533e51ecf4dc7978756a57)]:
20
+ - @opencloning/ui@1.8.0
21
+ - @opencloning/utils@1.8.0
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "license": "MIT",
3
+ "name": "@opencloning/opencloningdb",
4
+ "version": "1.7.1",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "https://github.com/OpenCloning/OpenCloning_frontend.git",
13
+ "directory": "packages/opencloningdb"
14
+ },
15
+ "dependencies": {
16
+ "@mui/icons-material": "^5.18.0",
17
+ "@mui/material": "^5.18.0",
18
+ "@opencloning/ui": "1.8.1",
19
+ "@opencloning/utils": "1.8.1",
20
+ "axios": "^1.16.0"
21
+ },
22
+ "peerDependencies": {
23
+ "react": "^18.3.1",
24
+ "react-dom": "^18.3.1",
25
+ "react-redux": "^8.1.3"
26
+ },
27
+ "publishConfig": {
28
+ "registry": "https://registry.npmjs.org/"
29
+ }
30
+ }
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import GetPrimerComponent from './GetPrimerComponent';
3
+ import { clickMultiSelectOption } from '../../../cypress/e2e/common_functions';
4
+
5
+ const PRIMER_NAME = 'lacZ_attB1_fwd';
6
+
7
+ describe('<GetPrimerComponent />', () => {
8
+ it('successfully selects a primer and calls setPrimer with correct data', () => {
9
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
10
+ const setErrorSpy = cy.spy().as('setErrorSpy');
11
+ cy.setupOpenCloningDBTestAuth();
12
+
13
+ cy.interceptOpenCloningDBStub('get_primers_search_by_name', { alias: 'getPrimers' });
14
+ cy.interceptOpenCloningDBStub('get_primer', { alias: 'getPrimer' });
15
+
16
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
17
+
18
+ cy.get('input').type(PRIMER_NAME);
19
+ cy.get('.MuiAutocomplete-listbox li', { timeout: 10000 }).should('have.length.greaterThan', 0);
20
+ cy.wait('@getPrimers');
21
+
22
+ clickMultiSelectOption('Primer', PRIMER_NAME, 'div');
23
+
24
+ cy.getStub('get_primer').then((stub) => {
25
+ cy.wait('@getPrimer');
26
+ cy.get('@setPrimerSpy').should('have.been.calledWithMatch', {
27
+ name: stub.response.body.name,
28
+ sequence: stub.response.body.sequence,
29
+ 'database_id': stub.response.body.id,
30
+ });
31
+ });
32
+ cy.get('@setErrorSpy').should('have.been.calledWith', '');
33
+ });
34
+
35
+ it('clears primer and error when selection is cleared', () => {
36
+ const setPrimerSpy = cy.spy().as('setPrimerSpy');
37
+ const setErrorSpy = cy.spy().as('setErrorSpy');
38
+ cy.setupOpenCloningDBTestAuth();
39
+
40
+ cy.interceptOpenCloningDBStub('get_primers_search_by_name', { alias: 'getPrimers' });
41
+ cy.interceptOpenCloningDBStub('get_primer', { alias: 'getPrimer' });
42
+
43
+ cy.mount(<GetPrimerComponent setPrimer={setPrimerSpy} setError={setErrorSpy} />);
44
+
45
+ cy.get('input').type(PRIMER_NAME);
46
+ cy.get('.MuiAutocomplete-listbox li', { timeout: 10000 }).should('have.length.greaterThan', 0);
47
+ cy.wait('@getPrimers');
48
+ clickMultiSelectOption('Primer', PRIMER_NAME, 'div');
49
+ cy.wait('@getPrimer');
50
+ cy.get('@setPrimerSpy').should('have.been.calledWithMatch', { name: PRIMER_NAME });
51
+
52
+ cy.get('.MuiAutocomplete-clearIndicator').click();
53
+
54
+ cy.get('@setPrimerSpy').should('have.been.calledWith', null);
55
+ cy.get('@setErrorSpy').should('have.been.calledWith', '');
56
+ });
57
+ });
@@ -0,0 +1,43 @@
1
+ import React, { useCallback } from 'react';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import PrimerSelect from './PrimerSelect';
4
+ import { openCloningDBHttpClient } from './common';
5
+ import endpoints from './endpoints';
6
+ import { Box } from '@mui/material';
7
+
8
+ function GetPrimerComponent({ setPrimer, setError, existingDatabaseIds }) {
9
+ const { mutate, reset } = useMutation({
10
+ mutationFn: async (primerId) => {
11
+ const response = await openCloningDBHttpClient.get(endpoints.primer(primerId));
12
+ return response.data;
13
+ },
14
+ onSuccess: (primer) => {
15
+ /* database_id matches API primer model */
16
+ /* eslint-disable-next-line camelcase */
17
+ setPrimer({ name: primer.name, sequence: primer.sequence, database_id: primer.id });
18
+ setError('');
19
+ },
20
+ onError: (err) => {
21
+ setError(err?.response?.data?.detail || err?.message || 'Could not retrieve primer from OpenCloningDB');
22
+ setPrimer(null);
23
+ },
24
+ });
25
+
26
+ const handlePrimerSelect = useCallback(async (selectedPrimer) => {
27
+ if (selectedPrimer === null || selectedPrimer === '') {
28
+ reset();
29
+ setPrimer(null);
30
+ setError('');
31
+ return;
32
+ }
33
+ mutate(selectedPrimer.id);
34
+ }, [mutate, reset, setPrimer, setError]);
35
+
36
+ return (
37
+ <Box sx={{ minWidth: '400px' }}>
38
+ <PrimerSelect fullWidth setPrimer={handlePrimerSelect} filterDatabaseIds={existingDatabaseIds} />
39
+ </Box>
40
+ );
41
+ }
42
+
43
+ export default GetPrimerComponent;
@@ -0,0 +1,102 @@
1
+ import React from 'react';
2
+ import GetSequenceFileAndDatabaseIdComponent from './GetSequenceFileAndDatabaseIdComponent';
3
+ import { clickMultiSelectOption } from '../../../cypress/e2e/common_functions';
4
+ import endpoints from './endpoints';
5
+
6
+ const SEQUENCE_NAME = 'ase1_CDS_PCR';
7
+ const textFilePattern = `**${endpoints.sequenceTextFile('*')}`;
8
+
9
+ function expectCloningStrategyFile(file, sequenceId, expectedSequenceFile) {
10
+ expect(file.name).to.equal('cloning_strategy.json');
11
+ return cy.readFileAsText(file).then((content) => {
12
+ const parsed = JSON.parse(content);
13
+ expect(parsed.sources).to.have.length(1);
14
+ expect(parsed.sources[0].database_id).to.equal(sequenceId);
15
+ expect(parsed.sources[0].type).to.equal('DatabaseSource');
16
+ expect(parsed.sequences).to.have.length(1);
17
+ expect(parsed.sequences[0]).to.include({
18
+ ...expectedSequenceFile,
19
+ id: 1,
20
+ });
21
+ expect(parsed.primers).to.eql([]);
22
+ });
23
+ }
24
+
25
+ describe('<GetSequenceFileAndDatabaseIdComponent />', () => {
26
+ it('selects a sequence and calls setFile and setDatabaseId with correct data', () => {
27
+ const setFileSpy = cy.spy().as('setFileSpy');
28
+ const setDatabaseIdSpy = cy.spy().as('setDatabaseIdSpy');
29
+ cy.setupOpenCloningDBTestAuth();
30
+
31
+ cy.interceptOpenCloningDBStub('get_sequences_search_by_name', { alias: 'getSequences' });
32
+ cy.interceptOpenCloningDBStub('get_text_file_sequence', { alias: 'getSequenceFile' });
33
+
34
+ cy.mount(
35
+ <GetSequenceFileAndDatabaseIdComponent setFile={setFileSpy} setDatabaseId={setDatabaseIdSpy} />,
36
+ );
37
+
38
+ cy.get('input').type(SEQUENCE_NAME);
39
+ cy.get('.MuiAutocomplete-listbox li', { timeout: 10000 }).should('have.length.greaterThan', 0);
40
+ cy.wait('@getSequences').its('request.url').should('include', `name=${SEQUENCE_NAME}`);
41
+
42
+ clickMultiSelectOption('Sequence', RegExp(`^${SEQUENCE_NAME}$`), 'div');
43
+ cy.getStub('get_text_file_sequence').then((stub) => {
44
+ cy.get('@setDatabaseIdSpy').should('have.been.calledWith', stub.response.body.id);
45
+ cy.get('@setFileSpy').should('have.been.calledOnce');
46
+ return cy.get('@setFileSpy').then((spy) => {
47
+ const [file] = spy.lastCall.args;
48
+ return expectCloningStrategyFile(file, stub.response.body.id, stub.response.body.sequence);
49
+ });
50
+ });
51
+
52
+ });
53
+
54
+ it('shows a retry button when requesting the selected file fails and retries successfully', () => {
55
+ const setFileSpy = cy.spy().as('setFileSpy');
56
+ const setDatabaseIdSpy = cy.spy().as('setDatabaseIdSpy');
57
+
58
+ cy.setupOpenCloningDBTestAuth();
59
+ cy.interceptOpenCloningDBStub('get_sequences_search_by_name', { alias: 'getSequences' });
60
+
61
+ cy.getStub('get_text_file_sequence').then((textFileSequenceStub) => {
62
+ let callCount = 0;
63
+ cy.intercept('GET', textFilePattern, (req) => {
64
+ callCount += 1;
65
+ if (callCount === 1) {
66
+ req.reply({ statusCode: 500 });
67
+ return;
68
+ }
69
+
70
+ req.reply({
71
+ statusCode: textFileSequenceStub.response.status_code,
72
+ body: textFileSequenceStub.response.body,
73
+ headers: textFileSequenceStub.response.headers,
74
+ });
75
+ }).as('getSequenceFile');
76
+ });
77
+
78
+ cy.mount(
79
+ <GetSequenceFileAndDatabaseIdComponent setFile={setFileSpy} setDatabaseId={setDatabaseIdSpy} />,
80
+ );
81
+
82
+ cy.get('input').type(SEQUENCE_NAME);
83
+ cy.get('.MuiAutocomplete-listbox li', { timeout: 10000 }).should('have.length.greaterThan', 0);
84
+ cy.wait('@getSequences').its('request.url').should('include', `name=${SEQUENCE_NAME}`);
85
+
86
+ clickMultiSelectOption('Sequence', RegExp(`^${SEQUENCE_NAME}$`), 'div');
87
+
88
+ cy.wait('@getSequenceFile');
89
+ cy.contains('Failed to load sequence file.').should('exist');
90
+ cy.get('@setFileSpy').should('not.have.been.called');
91
+ cy.get('@setDatabaseIdSpy').should('not.have.been.called');
92
+
93
+ cy.contains('button', 'Retry').click();
94
+
95
+ cy.wait('@getSequenceFile');
96
+ cy.contains('Failed to load sequence file.').should('not.exist');
97
+ cy.get('@setFileSpy').should('have.been.calledOnce');
98
+ cy.get('@setDatabaseIdSpy').should('have.been.calledOnce');
99
+
100
+ });
101
+ });
102
+
@@ -0,0 +1,62 @@
1
+ import React from 'react';
2
+ import { useMutation } from '@tanstack/react-query';
3
+ import SequenceSelect from './SequenceSelect';
4
+ import { openCloningDBHttpClient } from './common';
5
+ import endpoints from './endpoints';
6
+ import { CircularProgress, FormControl } from '@mui/material';
7
+ import RetryAlert from '@opencloning/ui/components/form/RetryAlert';
8
+
9
+ function GetSequenceFileAndDatabaseIdComponent({ setFile, setDatabaseId }) {
10
+ const lastSelectedSequenceRef = React.useRef(null);
11
+
12
+ const { mutate, isPending, error, reset } = useMutation({
13
+ mutationFn: async (selectedSequence) => {
14
+ const selectedId = selectedSequence.id;
15
+ const response = await openCloningDBHttpClient.get(endpoints.sequenceTextFile(selectedId));
16
+ const sequence = { ...response.data, id: 1 };
17
+ const source = { id: 1, input: [], database_id: selectedId, type: 'DatabaseSource' };
18
+ const cloningStrategy = { sources: [source], sequences: [sequence], primers: [] };
19
+ const fileContent = JSON.stringify(cloningStrategy);
20
+
21
+ return {
22
+ databaseId: selectedId,
23
+ file: new File([fileContent], 'cloning_strategy.json', { type: 'application/json' }),
24
+ };
25
+ },
26
+ onSuccess: ({ file, databaseId }) => {
27
+ setFile(file);
28
+ setDatabaseId(databaseId);
29
+ },
30
+ onError: (requestError) => {
31
+ console.error('Error fetching cloning strategy:', requestError);
32
+ },
33
+ retry: false,
34
+ });
35
+
36
+ const onSequenceSelect = React.useCallback((selectedSequence) => {
37
+ if (!selectedSequence) {
38
+ lastSelectedSequenceRef.current = null;
39
+ reset();
40
+ return;
41
+ }
42
+
43
+ lastSelectedSequenceRef.current = selectedSequence;
44
+ mutate(selectedSequence);
45
+ }, [mutate, reset]);
46
+
47
+ const retryLoad = React.useCallback(() => {
48
+ if (lastSelectedSequenceRef.current) {
49
+ mutate(lastSelectedSequenceRef.current);
50
+ }
51
+ }, [mutate]);
52
+
53
+ return (
54
+ <FormControl fullWidth>
55
+ <SequenceSelect multiple={false} label="Sequence" onChange={onSequenceSelect} />
56
+ {isPending && <CircularProgress />}
57
+ {error && <RetryAlert onRetry={retryLoad}>Failed to load sequence file.</RetryAlert>}
58
+ </FormControl>
59
+ );
60
+ }
61
+
62
+ export default GetSequenceFileAndDatabaseIdComponent;
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import LoadHistoryComponent from './LoadHistoryComponent';
3
+
4
+ const DATABASE_ID = 10;
5
+
6
+ describe('<LoadHistoryComponent />', () => {
7
+ it('calls loadDatabaseFile with the file returned by the API', () => {
8
+ const loadDatabaseFileSpy = cy.spy().as('loadDatabaseFileSpy');
9
+ cy.setupOpenCloningDBTestAuth();
10
+
11
+ cy.interceptOpenCloningDBStub('get_cloning_strategy', { alias: 'getCloningStrategy' });
12
+
13
+ cy.mount(
14
+ <LoadHistoryComponent
15
+ databaseId={DATABASE_ID}
16
+ loadDatabaseFile={loadDatabaseFileSpy}
17
+ />,
18
+ );
19
+
20
+ cy.getStub('get_cloning_strategy').then((cloningStrategyStub) => {
21
+ const expectedContent = JSON.stringify(cloningStrategyStub.response.body);
22
+
23
+ cy.wait('@getCloningStrategy').its('request.url').should('include', cloningStrategyStub.endpoint);
24
+ cy.get('@loadDatabaseFileSpy').should('have.been.calledOnce');
25
+
26
+ cy.get('@loadDatabaseFileSpy').then((spy) => {
27
+ const [file, id, flag] = spy.lastCall.args;
28
+ expect(id).to.equal(DATABASE_ID);
29
+ expect(flag).to.equal(true);
30
+
31
+ return cy.readFileAsText(file).then((content) => {
32
+ expect(content).to.equal(expectedContent);
33
+ });
34
+ });
35
+ });
36
+ });
37
+
38
+ it('shows a loading spinner while the request is pending', () => {
39
+ cy.setupOpenCloningDBTestAuth();
40
+
41
+ cy.getStub('get_cloning_strategy').then((cloningStrategyStub) => {
42
+ cy.intercept({ method: cloningStrategyStub.method, pathname: cloningStrategyStub.endpoint }, (req) => {
43
+ req.reply({
44
+ delay: 1000,
45
+ statusCode: cloningStrategyStub.response.status_code,
46
+ body: cloningStrategyStub.response.body,
47
+ headers: cloningStrategyStub.response.headers,
48
+ });
49
+ }).as('getCloningStrategy');
50
+ });
51
+
52
+ cy.mount(
53
+ <LoadHistoryComponent
54
+ databaseId={DATABASE_ID}
55
+ loadDatabaseFile={cy.stub()}
56
+ />,
57
+ );
58
+
59
+ cy.get('.MuiCircularProgress-svg', { timeout: 500 }).should('exist');
60
+ cy.wait('@getCloningStrategy');
61
+ });
62
+
63
+ it('shows a retry button when the request fails, and retries on click', () => {
64
+ cy.setupOpenCloningDBTestAuth();
65
+
66
+ cy.getStub('get_cloning_strategy').then((cloningStrategyStub) => {
67
+ let callCount = 0;
68
+ cy.intercept({ method: cloningStrategyStub.method, pathname: cloningStrategyStub.endpoint }, (req) => {
69
+ callCount += 1;
70
+ if (callCount === 1) {
71
+ req.reply({ statusCode: 500 });
72
+ } else {
73
+ req.reply({
74
+ statusCode: cloningStrategyStub.response.status_code,
75
+ body: cloningStrategyStub.response.body,
76
+ headers: cloningStrategyStub.response.headers,
77
+ });
78
+ }
79
+ }).as('getCloningStrategy');
80
+ });
81
+
82
+ cy.mount(
83
+ <LoadHistoryComponent
84
+ databaseId={DATABASE_ID}
85
+ loadDatabaseFile={cy.stub()}
86
+ />,
87
+ );
88
+
89
+ cy.wait('@getCloningStrategy');
90
+ cy.contains('Failed to load history file.').should('exist');
91
+ cy.contains('button', 'Retry').should('exist').click();
92
+ cy.wait('@getCloningStrategy');
93
+ cy.contains('Failed to load history file.').should('not.exist');
94
+ });
95
+ });
@@ -0,0 +1,35 @@
1
+ import { CircularProgress } from '@mui/material';
2
+ import React from 'react';
3
+ import { useQuery } from '@tanstack/react-query';
4
+ import { openCloningDBHttpClient } from './common';
5
+ import RetryAlert from '@opencloning/ui/components/form/RetryAlert';
6
+ import endpoints from './endpoints';
7
+
8
+ function LoadHistoryComponent({ databaseId, loadDatabaseFile }) {
9
+ const url = endpoints.sequenceCloningStrategy(databaseId);
10
+
11
+ const { isLoading, error, data, refetch } = useQuery({
12
+ queryKey: ['cloningStrategy', databaseId],
13
+ queryFn: async () => {
14
+ const response = await openCloningDBHttpClient.get(url);
15
+ return response.data;
16
+ },
17
+ retry: false,
18
+ });
19
+
20
+ React.useEffect(() => {
21
+ if (data) {
22
+ const file = new File([JSON.stringify(data)], 'cloning_strategy.json', { type: 'application/json' });
23
+ loadDatabaseFile(file, databaseId, true);
24
+ }
25
+ }, [data, databaseId, loadDatabaseFile]);
26
+
27
+ return (
28
+ <div>
29
+ {isLoading && <CircularProgress />}
30
+ {error && <RetryAlert onRetry={refetch}>Failed to load history file.</RetryAlert>}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ export default LoadHistoryComponent;
@@ -0,0 +1,141 @@
1
+ import { Save as SaveIcon, Link as LinkIcon } from '@mui/icons-material';
2
+ import GetPrimerComponent from './GetPrimerComponent';
3
+ import GetSequenceFileAndDatabaseIdComponent from './GetSequenceFileAndDatabaseIdComponent';
4
+ import { openCloningDBHttpClient } from './common';
5
+ import endpoints from './endpoints';
6
+ import LoadHistoryComponent from './LoadHistoryComponent';
7
+ import SubmitToDatabaseComponent from './SubmitToDatabaseComponent';
8
+ import PrimersNotInDatabaseComponent from './PrimersNotInDatabaseComponent';
9
+
10
+ function isSubmissionDataValid(submissionData) {
11
+ return Boolean(submissionData.title);
12
+ }
13
+
14
+ async function submitSequenceToDatabase({ submissionData, substate, id }) {
15
+ // const substateCopy = cloneDeep(substate);
16
+ // const selectedSequence = substateCopy.sequences.find((s) => s.id === id);
17
+ // selectedSequence.name = submissionData.title;
18
+ // substateCopy.description = '';
19
+ // console.log(substateCopy);
20
+ const { sources, primers, sequences } = substate;
21
+ const { data } = await openCloningDBHttpClient.post(endpoints.postSequence, { sources, primers, sequences });
22
+
23
+ const primerIds = new Set(primers.map((p) => p.id));
24
+ const sequenceIds = new Set(sequences.map((s) => s.id));
25
+
26
+ const primerMappings = data.mappings.filter(({localId}) => primerIds.has(localId));
27
+ const sequenceMappings = data.mappings.filter(({localId}) => sequenceIds.has(localId));
28
+
29
+ return { databaseId: data.id, primerMappings, sequenceMappings };
30
+ }
31
+
32
+ async function getPrimer(databaseId) {
33
+ const response = await openCloningDBHttpClient.get(endpoints.primer(databaseId));
34
+ return { name: response.data.name, database_id: databaseId, sequence: response.data.sequence };
35
+ }
36
+
37
+ async function submitPrimerToDatabase({ submissionData, primer }) {
38
+ const payload = {
39
+ name: submissionData.title,
40
+ sequence: primer.sequence,
41
+ };
42
+ const response = await openCloningDBHttpClient.post(endpoints.postPrimer, payload);
43
+ return response.data.id;
44
+ }
45
+
46
+ async function submitSequencingFileToDatabase({ databaseId, sequencingFiles }) {
47
+ const formData = new FormData();
48
+ sequencingFiles.forEach((file) => {
49
+ if (file) {
50
+ formData.append('files', file);
51
+ }
52
+ });
53
+
54
+ try {
55
+ const resp = await openCloningDBHttpClient.post(
56
+ endpoints.sequenceSequencingFiles(databaseId),
57
+ formData,
58
+ {
59
+ headers: {
60
+ 'Content-Type': 'multipart/form-data',
61
+ },
62
+ },
63
+ );
64
+ return resp.data;
65
+ } catch (e) {
66
+ console.error(e);
67
+ throw new Error(e.response?.data?.detail || e.message || 'Error submitting sequencing files');
68
+ }
69
+ }
70
+
71
+ async function getSequencingFiles(databaseId) {
72
+ // Returns array of { name, getFile } where getFile is async and returns the file content
73
+ try {
74
+ const resp = await openCloningDBHttpClient.get(endpoints.sequenceSequencingFiles(databaseId));
75
+ const files = resp.data || [];
76
+ return files.map((fileInfo) => ({
77
+ id: fileInfo.id,
78
+ name: fileInfo.original_name,
79
+ getFile: async () => {
80
+ const downloadResp = await openCloningDBHttpClient.get(endpoints.sequencingFileDownload(fileInfo.id), {
81
+ responseType: 'blob',
82
+ });
83
+ return new File([downloadResp.data], fileInfo.original_name);
84
+ },
85
+ }));
86
+ } catch (e) {
87
+ console.error(e);
88
+ throw new Error(e.response?.data?.detail || e.message || 'Error getting sequencing files');
89
+ }
90
+ }
91
+
92
+ async function locateSequenceInDatabase(sequence) {
93
+ const response = await openCloningDBHttpClient.post(endpoints.sequenceSearch, sequence);
94
+ return response.data;
95
+ }
96
+
97
+ export default {
98
+ // Name of the database interface
99
+ name: 'OpenCloningDB',
100
+ // Returns a link to the sequence in the database
101
+ getSequenceLink: (databaseId) => `sequences/${databaseId}`,
102
+ // Returns a link to the primer in the database
103
+ getPrimerLink: (databaseId) => `primers/${databaseId}`,
104
+ // Component for selecting and loading sequence files from the database
105
+ GetSequenceFileAndDatabaseIdComponent,
106
+ // Component for selecting and loading primers from the database
107
+ GetPrimerComponent,
108
+ // Component for submitting resources to the database
109
+ SubmitToDatabaseComponent,
110
+ // Component for handling primers not yet in database
111
+ PrimersNotInDatabaseComponent,
112
+ // Function to submit a primer to the database
113
+ submitPrimerToDatabase,
114
+ // Function to submit a sequence and its history to the database
115
+ submitSequenceToDatabase,
116
+ // Function to validate submission data
117
+ isSubmissionDataValid,
118
+ // Icon displayed on the node corner to submit
119
+ SubmitIcon: SaveIcon,
120
+ // Icon displayed on the node corner for entities in the database
121
+ DatabaseIcon: LinkIcon,
122
+ // OPTIONAL =======================================================================
123
+ // Component for loading history from the database (can be hook-like does not have to render anything)
124
+ LoadHistoryComponent,
125
+ // Function to load sequences from url parameters
126
+ loadSequenceFromUrlParams: () => {},
127
+ // Function to get the primer ({name, database_id, sequence}) from the database
128
+ getPrimer,
129
+ // Function to get the name of a sequence from the database
130
+ getSequenceName: () => {},
131
+ // Function to get the sequencing files from the database, see docs for what the return value should be
132
+ getSequencingFiles,
133
+ // Autoload sequencing files (Boolean)
134
+ autoloadSequencingFiles: true,
135
+ // Omit unsaved intermediates disclaimer (Boolean)
136
+ omitUnsavedIntermediatesDisclaimer: true,
137
+ // Function to locate a sequence in the database
138
+ locateSequenceInDatabase,
139
+ // Function to submit sequencing files to the database
140
+ submitSequencingFileToDatabase,
141
+ };
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { getStub } from './testUtils.test';
3
+ import endpoints from './endpoints';
4
+
5
+ const multipartStub = getStub('post_sequence_sequencing_files');
6
+ const sequenceFileStub = multipartStub.body.multipart_files[0];
7
+ const databaseId = Number(multipartStub.endpoint.match(/\/sequences\/(\d+)\//)?.[1]);
8
+ const postMock = vi.fn().mockResolvedValue({
9
+ data: 'direct-response',
10
+ });
11
+
12
+ vi.mock('./common', async (importOriginal) => {
13
+ const actual = await importOriginal();
14
+ return {
15
+ ...actual,
16
+ openCloningDBHttpClient: {
17
+ ...actual.openCloningDBHttpClient,
18
+ post: (...args) => postMock(...args),
19
+ },
20
+ };
21
+ });
22
+
23
+ vi.mock('./GetPrimerComponent', () => ({ default: () => null }));
24
+ vi.mock('./GetSequenceFileAndDatabaseIdComponent', () => ({ default: () => null }));
25
+ vi.mock('./LoadHistoryComponent', () => ({ default: () => null }));
26
+ vi.mock('./SubmitToDatabaseComponent', () => ({ default: () => null }));
27
+ vi.mock('./PrimersNotInDatabaseComponent', () => ({ default: () => null }));
28
+
29
+ import OpenCloningDBInterface from './OpenCloningDBInterface';
30
+
31
+ function readFileAsText(file) {
32
+ return new Promise((resolve, reject) => {
33
+ const reader = new FileReader();
34
+ reader.onerror = () => reject(reader.error);
35
+ reader.onload = () => resolve(reader.result);
36
+ reader.readAsText(file);
37
+ });
38
+ }
39
+
40
+ describe('submitSequencingFileToDatabase multipart payload', () => {
41
+ it('submits FormData with files field and forwards response', async () => {
42
+ postMock.mockClear();
43
+ const sequenceFile = new File([sequenceFileStub.content], sequenceFileStub.filename, {
44
+ type: sequenceFileStub.content_type,
45
+ });
46
+
47
+ const result = await OpenCloningDBInterface.submitSequencingFileToDatabase({
48
+ databaseId,
49
+ sequencingFiles: [sequenceFile],
50
+ });
51
+
52
+ const [url, formData] = postMock.mock.calls[0];
53
+ expect(url).toBe(endpoints.sequenceSequencingFiles(databaseId));
54
+ expect(formData).toBeInstanceOf(FormData);
55
+
56
+ const files = formData.getAll('files');
57
+ expect(files).toHaveLength(1);
58
+ expect(files[0]).toBeInstanceOf(File);
59
+ expect(files[0].name).toBe(sequenceFileStub.filename);
60
+ expect(files[0].type).toBe(sequenceFileStub.content_type);
61
+ expect(await readFileAsText(files[0])).toBe(sequenceFileStub.content);
62
+
63
+ expect(result).toEqual('direct-response');
64
+ });
65
+ });