Skip to content

Commit 03acf8b

Browse files
joneubankAzher2Ali
andauthored
Refactor Base Schema to not require Donor/Specimen/Sample (#878)
* Remove Donor, Specimen, and Sample from Analysis Data Classes in SONG * Reverting the changes related to latest develop * Remove Donor, Specimen, and Samples from Base Schema and Allow in Dynamic Schemas * Remove Donor-Related Entities from StudyController and Search APIs * Add External Validation Rules for Analysis Properties Based on Dynamic Schemas * Resolving the build issues and updating the PR with missing files * Changes related to feedback * Changes to ValidationService related to externalValiation * Changes related to the feedback comments * Migrate file_types to options column in analysis_schema * External valdiation URL template for study and value tokens * Retrieve all options data with analysis type * Remove sneaky throws from buildSchema with JSON parsing failure case * Fix paginated data lookup by removing stored procedure reference to file_type enum * Schema registrations sets options values correctly (#879) - when missing, uses value from previous schema or sets as empty (default) value - when set as empty array, replaces current value with empty array (removes optional validations) - sets options in get registration by version query * Change schema option name to plural `externalValidations` * Docs grammar improvements and update property to externalValidations * Remove blank lines * Options property description improvements --------- Co-authored-by: Azher2Ali <[email protected]>
1 parent 434ddf2 commit 03acf8b

File tree

94 files changed

+673
-6121
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+673
-6121
lines changed

docs/custom-schemas.md

+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# Custom Analysis Schemas
2+
3+
## Minimal Example
4+
5+
```json
6+
{
7+
"name": "minimalExample",
8+
"options":{},
9+
"schema":{
10+
"type": "object",
11+
"required":[
12+
"experiment"
13+
],
14+
"properties":{
15+
"experiment":{}
16+
}
17+
}
18+
}
19+
```
20+
21+
## Options
22+
23+
The `options` property defines extra validations for this analysis schema, such as restrictions on file types and checks on the data with an external service. The `options` property is not required. Similarly, each property in `options` is also optional. If no value is provided for one of the `options` properties, a default configuration will be used for the analysis. If this is an update to an existing analysis type, you can omit any option and its value will be maintained from the previous version.
24+
25+
```json
26+
{
27+
"fileTypes":["bam", "cram"],
28+
"externalValidations":[
29+
{
30+
"url": "http://localhost:8099/",
31+
"jsonPath": "experiment.someId"
32+
}
33+
]
34+
},
35+
```
36+
37+
If you want to remove the previous value of an option so that this validation is no longer required, for instance removing the restriction on file types so that any file type could be provided, you should provide an empty list for that option.
38+
39+
In the example below, both `fileTypes` and `externalValidations` properties are set to empty arrays, which means that these validations will not be applied to submitted analysis:
40+
41+
```json
42+
{
43+
"options": {
44+
"fileTypes": [],
45+
"externalValidations": []
46+
}
47+
}
48+
```
49+
50+
### File Types
51+
52+
`options.fileTypes` can be provided an array of strings. These represent the file types (file extensions) that are allowed to be uploaded for this type of analysis.
53+
54+
If an empty array is provided, then any file type will be allowed. If an array of file types is provided, then an analysis will be invalid if it contains files of a type not listed.
55+
56+
```json
57+
{
58+
"options": {
59+
"fileTypes": ["bam","cram"]
60+
}
61+
}
62+
```
63+
64+
### External Validation
65+
66+
External validations configure Song to check a value in the analysis against an external service by sending an HTTP GET request to a configurable URL. The service should respond with a 2XX status message to indicate the value "is valid".
67+
68+
As an example, if the project clinical data is being managed in a separate service, we can add an external validation to the donor id field of our custom scheme. This will send the donor id to the external service which can confirm that we have previously registered that donor.
69+
70+
This might look like the following:
71+
72+
```json
73+
{
74+
"url": "http://example.com/{study}/donor/{value}",
75+
"jsonPath": "experiment.donorId"
76+
}
77+
```
78+
79+
The URL provided is a template, with two variables that will be replaced during validation. Song will replace the token `{value}` with the value read from the analysis at the property as defined in the `jsonPath`. Song will also replace the token `{study}` with the study ID for the Analysis.
80+
81+
Continuing the above example, if the following analysis was submitted:
82+
83+
```json
84+
{
85+
"studyId": "ABC123",
86+
"analysisType": {
87+
"name": "minimalExample"
88+
},
89+
"files": [
90+
{
91+
"dataType": "text",
92+
"fileName": "file1.txt",
93+
"fileSize": 123,
94+
"fileType": "txt",
95+
"fileAccess": "open",
96+
"fileMd5sum": "595f44fec1e92a71d3e9e77456ba80d1"
97+
}
98+
],
99+
"experiment": {
100+
"donorId": "id01"
101+
}
102+
}
103+
```
104+
105+
Song would attempt to validate the donorId by sending a validation request to `http://example.com/ABC123/donor/id01`.
106+
107+
The URL parsing allows using either the `{study}` or `{value}` placeholders multiple times (e.g. `http://example.com/{study}-{value}/{value}`), each instance will be interpolated accordingly.
108+
109+
> [!Warning]
110+
> The URL may cause errors in Song if it contains any tokens matching the `{word}` format other than `{study}` and `{value}`

song-client/src/main/java/bio/overture/song/client/command/SearchCommand.java

+3-25
Original file line numberDiff line numberDiff line change
@@ -54,31 +54,13 @@ public class SearchCommand extends Command {
5454
/** Long Switch Constants */
5555
private static final String FILE_ID_SWITCH = "--file-id";
5656

57-
private static final String SAMPLE_ID_SWITCH = "--sample-id";
58-
private static final String SPECIMEN_ID_SWITCH = "--specimen-id";
59-
private static final String DONOR_ID_SWITCH = "--donor-id";
6057
private static final String ANALYSIS_ID_SWITCH = "--analysis-id";
6158

6259
@Parameter(
6360
names = {F_SWITCH, FILE_ID_SWITCH},
6461
required = false)
6562
private String fileId;
6663

67-
@Parameter(
68-
names = {SA_SWITCH, SAMPLE_ID_SWITCH},
69-
required = false)
70-
private String sampleId;
71-
72-
@Parameter(
73-
names = {SP_SWITCH, SPECIMEN_ID_SWITCH},
74-
required = false)
75-
private String specimenId;
76-
77-
@Parameter(
78-
names = {D_SWITCH, DONOR_ID_SWITCH},
79-
required = false)
80-
private String donorId;
81-
8264
@Parameter(
8365
names = {A_SWITCH, ANALYSIS_ID_SWITCH},
8466
required = false)
@@ -92,8 +74,7 @@ public void run() throws IOException {
9274
val status = checkRules();
9375
if (!status.hasErrors()) {
9476
if (isIdSearchMode()) {
95-
status.outputPrettyJson(
96-
songApi.idSearch(config.getStudyId(), sampleId, specimenId, donorId, fileId));
77+
status.outputPrettyJson(songApi.idSearch(config.getStudyId(), fileId));
9778
} else if (isAnalysisSearchMode()) {
9879
status.outputPrettyJson(songApi.getAnalysis(config.getStudyId(), analysisId));
9980
} else {
@@ -108,18 +89,15 @@ private boolean isAnalysisSearchMode() {
10889
}
10990

11091
private boolean isIdSearchMode() {
111-
return nonNull(fileId) || nonNull(sampleId) || nonNull(specimenId) || nonNull(donorId);
92+
return nonNull(fileId);
11293
}
11394

11495
private Status checkRules() {
11596
val fileTerm = createParamTerm(F_SWITCH, FILE_ID_SWITCH, fileId, Objects::nonNull);
116-
val sampleTerm = createParamTerm(SA_SWITCH, SAMPLE_ID_SWITCH, sampleId, Objects::nonNull);
117-
val specimenTerm = createParamTerm(SP_SWITCH, SPECIMEN_ID_SWITCH, specimenId, Objects::nonNull);
118-
val donorTerm = createParamTerm(D_SWITCH, DONOR_ID_SWITCH, donorId, Objects::nonNull);
11997
val analysisIdTerm =
12098
createParamTerm(A_SWITCH, ANALYSIS_ID_SWITCH, analysisId, Objects::nonNull);
12199

122-
val idSearchMode = createModeRule(ID_MODE, fileTerm, sampleTerm, specimenTerm, donorTerm);
100+
val idSearchMode = createModeRule(ID_MODE, fileTerm);
123101
val analysisSearchMode = createModeRule(ANALYSIS_MODE, analysisIdTerm);
124102
val ruleProcessor = createRuleProcessor(idSearchMode, analysisSearchMode);
125103
return ruleProcessor.check();

song-client/src/test/java/bio/overture/song/client/HappyPathClientMainTest.java

+1-27
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,10 @@
3939
import bio.overture.song.core.model.Analysis;
4040
import bio.overture.song.core.model.AnalysisType;
4141
import bio.overture.song.core.model.AnalysisTypeId;
42-
import bio.overture.song.core.model.Donor;
4342
import bio.overture.song.core.model.FileDTO;
4443
import bio.overture.song.core.model.FileUpdateRequest;
4544
import bio.overture.song.core.model.FileUpdateResponse;
4645
import bio.overture.song.core.model.PageDTO;
47-
import bio.overture.song.core.model.Sample;
48-
import bio.overture.song.core.model.Specimen;
4946
import bio.overture.song.core.model.SubmitResponse;
5047
import bio.overture.song.core.utils.RandomGenerator;
5148
import bio.overture.song.sdk.ManifestClient;
@@ -280,9 +277,6 @@ public void testAnalysisSearch() {
280277
@Test
281278
public void testIdSearch() {
282279
val expectedFile = new FileDTO().setObjectId("FI1");
283-
val expectedSample = Sample.builder().sampleId("SA1").build();
284-
val expectedSpecimen = Specimen.builder().specimenId("SP1").build();
285-
val expectedDonor = Donor.builder().donorId("DO1").build();
286280
val expectedAnalyses =
287281
List.of(
288282
Analysis.builder()
@@ -292,27 +286,7 @@ public void testIdSearch() {
292286
.studyId(DUMMY_STUDY_ID)
293287
.build());
294288

295-
when(songApi.idSearch(DUMMY_STUDY_ID, expectedSample.getSampleId(), null, null, null))
296-
.thenReturn(expectedAnalyses);
297-
when(songApi.idSearch(DUMMY_STUDY_ID, null, expectedSpecimen.getSpecimenId(), null, null))
298-
.thenReturn(expectedAnalyses);
299-
when(songApi.idSearch(DUMMY_STUDY_ID, null, null, expectedDonor.getDonorId(), null))
300-
.thenReturn(expectedAnalyses);
301-
when(songApi.idSearch(DUMMY_STUDY_ID, null, null, null, expectedFile.getObjectId()))
302-
.thenReturn(expectedAnalyses);
303-
assertOutputJson(objectToTree(expectedAnalyses), "search", "-d", expectedDonor.getDonorId());
304-
assertOutputJson(
305-
objectToTree(expectedAnalyses), "search", "--donor-id", expectedDonor.getDonorId());
306-
assertOutputJson(
307-
objectToTree(expectedAnalyses), "search", "-sp", expectedSpecimen.getSpecimenId());
308-
assertOutputJson(
309-
objectToTree(expectedAnalyses),
310-
"search",
311-
"--specimen-id",
312-
expectedSpecimen.getSpecimenId());
313-
assertOutputJson(objectToTree(expectedAnalyses), "search", "-sa", expectedSample.getSampleId());
314-
assertOutputJson(
315-
objectToTree(expectedAnalyses), "search", "--sample-id", expectedSample.getSampleId());
289+
when(songApi.idSearch(DUMMY_STUDY_ID, expectedFile.getObjectId())).thenReturn(expectedAnalyses);
316290
assertOutputJson(objectToTree(expectedAnalyses), "search", "-f", expectedFile.getObjectId());
317291
assertOutputJson(
318292
objectToTree(expectedAnalyses), "search", "--file-id", expectedFile.getObjectId());

song-core/src/main/java/bio/overture/song/core/exceptions/ServerErrors.java

-24
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,6 @@
3434

3535
public enum ServerErrors implements ServerError {
3636
STUDY_ID_DOES_NOT_EXIST(NOT_FOUND),
37-
DONOR_DOES_NOT_EXIST(NOT_FOUND),
38-
SPECIMEN_DOES_NOT_EXIST(NOT_FOUND),
39-
SAMPLE_DOES_NOT_EXIST(NOT_FOUND),
4037
UPLOAD_REPOSITORY_CREATE_RECORD(UNPROCESSABLE_ENTITY),
4138
ANALYSIS_REPOSITORY_CREATE_RECORD(UNPROCESSABLE_ENTITY),
4239
SEQUENCING_READ_REPOSITORY_CREATE_RECORD(UNPROCESSABLE_ENTITY),
@@ -45,18 +42,9 @@ public enum ServerErrors implements ServerError {
4542
INFO_REPOSITORY_UPDATE_RECORD(UNPROCESSABLE_ENTITY),
4643
INFO_REPOSITORY_DELETE_RECORD(UNPROCESSABLE_ENTITY),
4744
FILE_REPOSITORY_UPDATE_RECORD(UNPROCESSABLE_ENTITY),
48-
DONOR_REPOSITORY_UPDATE_RECORD(UNPROCESSABLE_ENTITY),
49-
SAMPLE_REPOSITORY_UPDATE_RECORD(UNPROCESSABLE_ENTITY),
50-
SPECIMEN_REPOSITORY_UPDATE_RECORD(UNPROCESSABLE_ENTITY),
5145
STUDY_REPOSITORY_CREATE_RECORD(UNPROCESSABLE_ENTITY),
5246
FILE_REPOSITORY_DELETE_RECORD(UNPROCESSABLE_ENTITY),
53-
DONOR_REPOSITORY_DELETE_RECORD(UNPROCESSABLE_ENTITY),
54-
SPECIMEN_REPOSITORY_DELETE_RECORD(UNPROCESSABLE_ENTITY),
55-
SAMPLE_REPOSITORY_DELETE_RECORD(UNPROCESSABLE_ENTITY),
5647
GENERATOR_CLOCK_MOVED_BACKWARDS(INTERNAL_SERVER_ERROR),
57-
DONOR_ID_IS_CORRUPTED(INTERNAL_SERVER_ERROR),
58-
SAMPLE_ID_IS_CORRUPTED(INTERNAL_SERVER_ERROR),
59-
SPECIMEN_ID_IS_CORRUPTED(INTERNAL_SERVER_ERROR),
6048
PAYLOAD_PARSING(UNPROCESSABLE_ENTITY),
6149
UPLOAD_ID_NOT_FOUND(NOT_FOUND),
6250
FILE_NOT_FOUND(NOT_FOUND),
@@ -68,7 +56,6 @@ public enum ServerErrors implements ServerError {
6856
UPLOAD_ID_NOT_VALIDATED(CONFLICT),
6957
ANALYSIS_ID_NOT_CREATED(INTERNAL_SERVER_ERROR),
7058
ANALYSIS_MISSING_FILES(INTERNAL_SERVER_ERROR),
71-
ANALYSIS_MISSING_SAMPLES(INTERNAL_SERVER_ERROR),
7259
ANALYSIS_ID_NOT_FOUND(NOT_FOUND),
7360
SEQUENCING_READ_NOT_FOUND(NOT_FOUND),
7461
VARIANT_CALL_NOT_FOUND(NOT_FOUND),
@@ -80,23 +67,12 @@ public enum ServerErrors implements ServerError {
8067
GATEWAY_SERVICE_NOT_FOUND(BAD_GATEWAY),
8168
SERVICE_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE),
8269
NOT_IMPLEMENTED_YET(NOT_IMPLEMENTED),
83-
SAMPLE_REPOSITORY_CREATE_RECORD(INTERNAL_SERVER_ERROR),
8470
FILE_REPOSITORY_CREATE_RECORD(INTERNAL_SERVER_ERROR),
85-
SPECIMEN_RECORD_FAILED(INTERNAL_SERVER_ERROR),
86-
DONOR_RECORD_FAILED(INTERNAL_SERVER_ERROR),
8771
ANALYSIS_STATE_UPDATE_FAILED(INTERNAL_SERVER_ERROR),
8872
SEARCH_TERM_SYNTAX(BAD_REQUEST),
8973
ANALYSIS_TYPE_INCORRECT_VERSION(CONFLICT),
9074
ANALYSIS_ID_COLLISION(CONFLICT),
9175
INFO_ALREADY_EXISTS(CONFLICT),
92-
SAMPLE_ALREADY_EXISTS(CONFLICT),
93-
DONOR_ALREADY_EXISTS(CONFLICT),
94-
SPECIMEN_ALREADY_EXISTS(CONFLICT),
95-
SAMPLE_TO_SPECIMEN_ID_MISMATCH(CONFLICT),
96-
MISMATCHING_SAMPLE_DATA(CONFLICT),
97-
MISMATCHING_SPECIMEN_DATA(CONFLICT),
98-
MISMATCHING_DONOR_DATA(CONFLICT),
99-
SPECIMEN_TO_DONOR_ID_MISMATCH(CONFLICT),
10076
VARIANT_CALL_CORRUPTED_DUPLICATE(INTERNAL_SERVER_ERROR),
10177
SEQUENCING_READ_CORRUPTED_DUPLICATE(INTERNAL_SERVER_ERROR),
10278
STUDY_ALREADY_EXISTS(CONFLICT),

song-core/src/main/java/bio/overture/song/core/model/Analysis.java

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ public class Analysis extends DynamicData {
2323
private String studyId;
2424
private AnalysisStates analysisState;
2525
private AnalysisTypeId analysisType;
26-
private List<CompositeSample> samples;
2726
private List<FileDTO> files;
2827

2928
private LocalDateTime createdAt;

song-core/src/main/java/bio/overture/song/core/model/AnalysisType.java

+22
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.fasterxml.jackson.annotation.JsonInclude;
44
import com.fasterxml.jackson.databind.JsonNode;
55
import java.time.LocalDateTime;
6+
import java.util.ArrayList;
7+
import java.util.List;
68
import javax.validation.constraints.NotNull;
79
import lombok.AllArgsConstructor;
810
import lombok.Builder;
@@ -22,4 +24,24 @@ public class AnalysisType {
2224

2325
@JsonInclude(JsonInclude.Include.NON_NULL)
2426
private JsonNode schema;
27+
28+
public List<String> getFileTypes() {
29+
if (this.options == null) {
30+
return new ArrayList<String>();
31+
}
32+
if (this.options.getFileTypes() == null) {
33+
return new ArrayList<String>();
34+
}
35+
return this.options.getFileTypes();
36+
}
37+
38+
public List<ExternalValidation> getExternalValidations() {
39+
if (this.options == null) {
40+
return new ArrayList<ExternalValidation>();
41+
}
42+
if (this.options.getExternalValidations() == null) {
43+
return new ArrayList<ExternalValidation>();
44+
}
45+
return this.options.getExternalValidations();
46+
}
2547
}
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package bio.overture.song.core.model;
22

33
import java.util.List;
4-
import lombok.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Builder;
6+
import lombok.Data;
7+
import lombok.NoArgsConstructor;
58

69
@Data
710
@Builder
811
@NoArgsConstructor
912
@AllArgsConstructor
1013
public class AnalysisTypeOptions {
1114
private List<String> fileTypes;
15+
private List<ExternalValidation> externalValidations;
1216
}

song-core/src/main/java/bio/overture/song/core/model/CompositeSample.java

-14
This file was deleted.

song-core/src/main/java/bio/overture/song/core/model/Donor.java

-22
This file was deleted.

0 commit comments

Comments
 (0)