Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions lint-baseline.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"warnings": 931,
"warnings": 879,
"rules": {
"@angular-eslint/no-input-rename": 7,
"@angular-eslint/no-output-native": 6,
Expand All @@ -8,7 +8,7 @@
"@typescript-eslint/no-empty-object-type": 1,
"@typescript-eslint/no-explicit-any": 96,
"@typescript-eslint/no-namespace": 14,
"@typescript-eslint/no-non-null-assertion": 203,
"@typescript-eslint/no-non-null-assertion": 151,
"@typescript-eslint/no-unsafe-argument": 76,
"@typescript-eslint/no-unsafe-assignment": 146,
"@typescript-eslint/no-unsafe-call": 173,
Expand Down
4 changes: 2 additions & 2 deletions projects/reactome-gsa-form/src/lib/model/analysis.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ export namespace Request {
parameters: Parameter[];
}

interface Dataset {
export interface Dataset {
data: string;
design: DataInformation;
name: string;
type: string;
parameters?: Parameter[];
}

interface DataInformation {
export interface DataInformation {
analysisGroup: string[];
comparison: Comparison;
samples: string[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* anything to do with the decision being made here.
*/
import { describe, expect, it } from 'vitest';
import { submittedParameters } from './analysis.service';
import { refusal, submittedDesign, submittedParameters } from './analysis.service';

describe('what a GSA request carries', () => {
it('leaves out a field the reader never filled in', () => {
Expand Down Expand Up @@ -42,3 +42,81 @@ describe('what a GSA request carries', () => {
);
});
});

describe('the statistical design a GSA request carries', () => {
const dataset = (design: object, columns: [string, string[]][]) =>
({
summary: { title: 'A dataset' },
statisticalDesign: { defaultCovariances: new Set(), covariances: [], ...design },
annotationColumns: new Map(columns),
}) as never;
const columns: [string, string[]][] = [
['sample_ids', ['s1', 's2']],
['condition', ['a', 'b']],
['batch', ['1', '2']],
];

it('names the columns of a complete design, its chosen covariates with them', () => {
expect(
submittedDesign(
dataset(
{
analysisGroup: 'condition',
comparisonGroup1: 'a',
comparisonGroup2: 'b',
covariances: [
{ name: 'batch', value: true },
{ name: 'unused', value: false },
],
},
columns
)
)
).toEqual({
analysisGroup: ['a', 'b'],
samples: ['s1', 's2'],
comparison: { group1: 'a', group2: 'b' },
batch: ['1', '2'],
});
});

it('says a design without its groups is not complete, rather than sending it', () => {
expect(submittedDesign(dataset({ analysisGroup: 'condition' }, columns))).toBe(
'The statistical design of "A dataset" is not complete'
);
});

it('says so too for a covariate with no column', () => {
expect(
submittedDesign(
dataset(
{
analysisGroup: 'condition',
comparisonGroup1: 'a',
comparisonGroup2: 'b',
covariances: [{ name: 'missing', value: true }],
},
columns
)
)
).toBe(
'The statistical design of "A dataset" names "missing", which its samples have no column for'
);
});

it('names an analysis group the samples have no column for', () => {
// The default group comes from the service, not the annotation header.
expect(
submittedDesign(
dataset({ analysisGroup: 'tissue', comparisonGroup1: 'a', comparisonGroup2: 'b' }, columns)
)
).toBe(
'The statistical design of "A dataset" names "tissue", which its samples have no column for'
);
});

it('is refused as a 400 with its reason, never the 503 the form retries', () => {
const refused = refusal('why');
expect([refused.status, refused.error]).toEqual([400, { detail: 'why' }]);
});
});
82 changes: 57 additions & 25 deletions projects/reactome-gsa-form/src/lib/services/analysis.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,46 @@ export function submittedParameters(parameters: Parameter[] | undefined) {
.map((param) => ({ name: param.name, value: String(param.value) }));
}

/**
* A dataset's statistical design as the analysis is asked for it: the
* annotation columns its design names, and the two groups compared. The form
* asks for a complete design before it submits (selectStatisticalDesignComplete);
* one that is not is said, rather than sent without its groups for the service
* to refuse.
*/
export function submittedDesign(dataset: Dataset): Request.DataInformation | string {
const design = dataset.statisticalDesign;
const column = (name: string | undefined) =>
name === undefined ? undefined : dataset.annotationColumns.get(name);
const analysisGroup = column(design.analysisGroup);
const samples = column('sample_ids');
const { comparisonGroup1: group1, comparisonGroup2: group2 } = design;
const incomplete = `The statistical design of "${dataset.summary.title}" is not complete`;
// A name the design holds and the samples have no column for: the default
// analysis group comes from the service, not from the annotation header, so
// the form can show a design complete that names a column the samples lack.
const noColumn = (name: string) =>
`The statistical design of "${dataset.summary.title}" names "${name}", which its samples have no column for`;
if (design.analysisGroup && !analysisGroup) return noColumn(design.analysisGroup);
if (!analysisGroup || !samples || !group1 || !group2) return incomplete;
const covariates: Record<string, string[]> = {};
for (const covariate of design.covariances.filter((cov) => cov.value)) {
const values = column(covariate.name);
if (!values) return covariate.name ? noColumn(covariate.name) : incomplete;
covariates[covariate.name] = values;
}
return { analysisGroup, samples, comparison: { group1, group2 }, ...covariates };
}

/**
* A design the form will not send, as a refused request: a 400 with its reason
* in `detail`, where extractErrorMessage reads the service's. Never a 503,
* which the effect retries every minute as the service being busy.
*/
export function refusal(reason: string): HttpErrorResponse {
return new HttpErrorResponse({ status: 400, error: { detail: reason } });
}

@Injectable({
providedIn: 'root',
})
Expand All @@ -41,34 +81,26 @@ export class AnalysisService {
readonly reportStatusUrl = computed(() => `${this.config().apiRoot}/report_status/`);

submitQuery(method: Method, parameters: Parameter[], datasets: Dataset[]): Observable<string> {
const submitted: Request.Dataset[] = [];
for (const dataset of datasets) {
const design = submittedDesign(dataset);
// As a refused request is, its reason where the service puts one: the
// effect answers it with loadFailure, which shows the reason.
if (typeof design === 'string') return throwError(() => refusal(design));
submitted.push({
data: dataset.summary.id,
name: dataset.summary.title,
type: dataset.summary.type,
parameters: submittedParameters(
dataset.summary.parameters?.filter((para) => para.scope !== 'common')
),
design,
});
}
const query: Request.Query = {
methodName: method.name || 'Method name',
parameters: submittedParameters(parameters),
datasets: datasets.map((dataset: Dataset) => ({
data: dataset.summary!.id,
name: dataset.summary!.title,
type: dataset.summary!.type,
parameters: submittedParameters(
dataset.summary!.parameters?.filter((para) => para.scope !== 'common')
),
design: {
analysisGroup: dataset.annotationColumns.get(dataset.statisticalDesign.analysisGroup!)!,
samples: dataset.annotationColumns.get('sample_ids')!,
comparison: {
group1: dataset.statisticalDesign!.comparisonGroup1 as string,
group2: dataset.statisticalDesign!.comparisonGroup2 as string,
},
...dataset
.statisticalDesign!.covariances.filter((cov) => cov.value)
.reduce(
(covs, cov) => ({
...covs,
[cov.name]: dataset.annotationColumns.get(cov.name!)!,
}),
{}
),
},
})),
datasets: submitted,
};
return this.http.post(this.submitAnalysisUrl(), query, { responseType: 'text' });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export const datasetSourceFeature = createFeature({
.filter(
(datasetSource) =>
datasetSource.data_types?.some((type) => method.data_types.includes(type)) ||
method.data_types.includes(datasetSource.type!) ||
method.data_types.includes(datasetSource.id!)
(datasetSource.type !== undefined &&
method.data_types.includes(datasetSource.type)) ||
method.data_types.includes(datasetSource.id)
);
}),
selectDatasetSource: (id: string) => createSelector(selectEntities, (entities) => entities[id]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,11 +175,12 @@ export class DatasetEffects {
this.actions$.pipe(
ofType(datasetActions.setLoadStatus),
filter(({ loadingStatus }) => loadingStatus.status === 'complete'),
// A finished load names the dataset it made; one that does not cannot be
// summarised, and fails as a summary that could not be had.
map(({ loadingStatus, id }) =>
datasetActions.getSummary({
datasetId: loadingStatus.dataset_id!,
id,
})
loadingStatus.dataset_id
? datasetActions.getSummary({ datasetId: loadingStatus.dataset_id, id })
: datasetActions.getSummaryError({ error: 'no dataset id', id })
)
)
);
Expand All @@ -205,14 +206,15 @@ export class DatasetEffects {
}),
map(({ summary, id }) => {
setTimeout(() => (this.tour.paused ? this.tour.resume() : null), 1000);
const table: string[][] = !summary.sample_metadata
const metadata = summary.sample_metadata;
const table: string[][] = !metadata
? [[''], ...summary.sample_ids.map((sampleId) => [sampleId])]
: [
['', ...summary.sample_metadata.map((col) => col.name)],
['', ...metadata.map((col) => col.name)],
...summary.sample_ids.map((sampleId, i) => [
sampleId,
// A missing value stays missing (sent as null), as it always was.
...summary.sample_metadata!.map((column) => column.values[i] as string),
...metadata.map((column) => column.values[i] as string),
]),
];
return datasetActions.setAnnotations({ annotations: table, id });
Expand Down
Loading
Loading