Skip to content

Commit

Permalink
Merge pull request #135 from iomega/ionization-modes-stats
Browse files Browse the repository at this point in the history
Added ionization modes to stats page
  • Loading branch information
sverhoeven authored Apr 20, 2020
2 parents 30441f3 + f58f2c6 commit a7c576f
Show file tree
Hide file tree
Showing 15 changed files with 196 additions and 70 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [0.6.1] 2020-04-16

### Added

* Added ionization modes to stats page ([#132](https://github.com/iomega/paired-data-form/issues/132))

### Fixed

* Enrichments cause Limit of total fields exceeded error in elastic search ([#131](https://github.com/iomega/paired-data-form/issues/131))
Expand Down
1 change: 1 addition & 0 deletions api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ describe('app', () => {
'submitters': [],
'genome_types': [],
'instrument_types': [],
'ionization_modes': [],
'growth_media': [],
'solvents': [],
'species': [],
Expand Down
4 changes: 3 additions & 1 deletion api/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { okHandler } from './config/passport';
import { Validator } from './validate';
import { ProjectDocumentStore } from './projectdocumentstore';
import { IOMEGAPairedOmicsDataPlatform } from './schema';
import { loadSchema } from './util/schema';

export function builder(mystore: ProjectDocumentStore, myenrichqueue: Bull.Queue<[string, IOMEGAPairedOmicsDataPlatform]>) {
// Create Express server
Expand All @@ -19,7 +20,8 @@ export function builder(mystore: ProjectDocumentStore, myenrichqueue: Bull.Queue
// Express configuration
app.set('port', process.env.PORT || 3000);
app.set('store', mystore);
app.set('validator', new Validator());
app.set('schema', loadSchema());
app.set('validator', new Validator(app.get('schema')));
app.set('enrichqueue', myenrichqueue);
app.use(compression());
app.use(express.json({limit: '1mb'}));
Expand Down
8 changes: 6 additions & 2 deletions api/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ function getStore(req: Request) {
return req.app.get('store') as ProjectDocumentStore;
}

function getSchema(req: Request) {
return req.app.get('schema');
}

function getValidator(req: Request) {
return req.app.get('validator') as Validator;
}
Expand Down Expand Up @@ -159,9 +163,9 @@ export function notFoundHandler(error: any, req: Request, res: Response, next: a

export async function getStats(req: Request, res: Response) {
const store = getStore(req);
const validator = getValidator(req);
const schema = getSchema(req);
const projects = await store.listProjects();
const stats = computeStats(projects, validator.schema);
const stats = computeStats(projects, schema);
res.json(stats);
}

Expand Down
6 changes: 3 additions & 3 deletions api/src/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Validator } from './validate';
import { IOMEGAPairedOmicsDataPlatform } from './schema';
import { ProjectDocumentStore } from './projectdocumentstore';
import { loadSchema } from './util/schema';

const validator = new Validator();
const schemaVersion = (validator.schema as any).properties.version.default;
const schema = loadSchema();
const schemaVersion = schema.properties.version.default;

export interface Migration {
applicable(p: any): boolean;
Expand Down
48 changes: 46 additions & 2 deletions api/src/store/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,22 +163,64 @@ describe('new SearchEngine()', () => {
['instrument_type', 'Time-of-flight (TOF)'],
['growth_medium', 'A1 medium'],
['solvent', 'Butanol'],
['ionization_mode', 'Positive'],
['ionization_type', 'Electrospray Ionization (ESI)']
])('filter(\'%s\', \'%s\')', (key: FilterField, value) => {
let hits: any;
beforeAll(async () => {
beforeEach(async () => {
client.search.mockClear();
hits = await searchEngine.filter(key, value);
});

it('should have called index.search', () => {
expect(client.search).toBeCalled();
// TODO assert arguments
const called = client.search.mock.calls[0][0];
let expected = {
index: 'podp',
body: {
query: {
match: expect.anything()
}
}
};
if (key === 'submitter') {
expected = {
index: 'podp',
body: {
query: {
bool: {
should: [
{
match: expect.anything()
},
{
match: expect.anything()
}
],
},
},
},
} as any;
}
expect(called).toEqual(expected);
});

it('should return hits', async () => {
const expected = await genomeProject();
expect(hits).toEqual([expected]);
});
});

describe('filter(invalid field)', () => {
it('should throw Error', async () => {
expect.assertions(1);
try {
await searchEngine.filter('some invalid key' as any, 'somevalue');
} catch (error) {
expect(error).toEqual(new Error('Invalid filter field'));
}
});
});
});

describe('with a single metagenome project', () => {
Expand Down Expand Up @@ -254,6 +296,8 @@ async function esGenomeProject() {
project.experimental.extraction_methods[1].solvents[0].solvent_title = 'Butanol';
project.experimental.extraction_methods[2].solvents[0].solvent_title = 'Methanol';
project.experimental.instrumentation_methods[0].instrumentation.instrument_title = 'Time-of-flight (TOF)';
project.experimental.instrumentation_methods[0].mode_title = 'Positive';
project.experimental.instrumentation_methods[0].ionization_type_title = 'Electrospray Ionization (ESI)';
const esproject = {
_id: 'projectid1',
project,
Expand Down
47 changes: 29 additions & 18 deletions api/src/store/search.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Client } from '@elastic/elasticsearch';
import { EnrichedProjectDocument } from './enrichments';
import { loadSchema } from '../validate';
import { enum2map } from '../util/stats';
import { loadSchema, Lookups } from '../util/schema';

interface Hit {
_id: string;
Expand All @@ -10,38 +9,40 @@ interface Hit {
}

export function expandEnrichedProjectDocument(project: EnrichedProjectDocument, schema: any) {
const lookups = new Lookups(schema);
const doc = JSON.parse(JSON.stringify(project));

const growth_media_oneOf = schema.properties.experimental.properties.sample_preparation.items.properties.medium_details.dependencies.medium_type.oneOf[1].properties.medium.anyOf;
const growth_media_lookup = enum2map(growth_media_oneOf);
const metagenomic_environment_oneOf = schema.properties.experimental.properties.sample_preparation.items.properties.medium_details.dependencies.medium_type.oneOf[0].properties.metagenomic_environment.oneOf;
const metagenomic_environment_lookup = enum2map(metagenomic_environment_oneOf);
doc.project.experimental.sample_preparation.forEach((d: any) => {
if (d.medium_details.medium_type === 'metagenome') {
const metagenomic_environment_title = metagenomic_environment_lookup.get(d.medium_details.metagenomic_environment);
const metagenomic_environment_title = lookups.metagenomic_environment.get(d.medium_details.metagenomic_environment);
if (metagenomic_environment_title) {
d.medium_details.metagenomic_environment_title = metagenomic_environment_title;
}
}
const medium_title = growth_media_lookup.get(d.medium_details.medium);
const medium_title = lookups.growth_media.get(d.medium_details.medium);
if (medium_title) {
d.medium_details.medium_title = medium_title;
}
});

const instruments_type_lookup = enum2map(schema.properties.experimental.properties.instrumentation_methods.items.properties.instrumentation.properties.instrument.anyOf);
doc.project.experimental.instrumentation_methods.forEach((d: any) => {
const title = instruments_type_lookup.get(d.instrumentation.instrument);
if (title) {
d.instrumentation.instrument_title = title;
const instrument_title = lookups.instrument.get(d.instrumentation.instrument);
if (instrument_title) {
d.instrumentation.instrument_title = instrument_title;
}
const mode_title = lookups.ionization_mode.get(d.mode);
if (mode_title) {
d.mode_title = mode_title;
}
const ionization_type_title = lookups.ionization_type.get(d.ionization.ionization_type);
if (ionization_type_title) {
d.ionization_type_title = ionization_type_title;
}
});

const solvents_lookup_enum = schema.properties.experimental.properties.extraction_methods.items.properties.solvents.items.properties.solvent.anyOf;
const solvents_lookup = enum2map(solvents_lookup_enum);
doc.project.experimental.extraction_methods.forEach((m: any) => {
m.solvents.forEach((d: any) => {
const title = solvents_lookup.get(d.solvent);
const title = lookups.solvent.get(d.solvent);
if (title) {
d.solvent_title = title;
}
Expand All @@ -68,7 +69,11 @@ export function collapseHit(hit: Hit): EnrichedProjectDocument {
}
);
project.project.experimental.instrumentation_methods.forEach(
(d: any) => delete d.instrumentation.instrument_title
(d: any) => {
delete d.instrumentation.instrument_title;
delete d.mode_title;
delete d.ionization_type_title;
}
);
project.project.experimental.extraction_methods.forEach(
(m: any) => m.solvents.forEach(
Expand All @@ -91,7 +96,7 @@ export function collapseHit(hit: Hit): EnrichedProjectDocument {
return project;
}

export type FilterField = 'principal_investigator' | 'submitter' | 'genome_type' | 'species' | 'metagenomic_environment' | 'instrument_type' | 'growth_medium' | 'solvent';
export type FilterField = 'principal_investigator' | 'submitter' | 'genome_type' | 'species' | 'metagenomic_environment' | 'instrument_type' | 'ionization_mode' | 'ionization_type' | 'growth_medium' | 'solvent';

export class SearchEngine {
private schema: any;
Expand Down Expand Up @@ -203,11 +208,17 @@ export class SearchEngine {
species: 'enrichments.genomes.species.scientific_name.keyword',
metagenomic_environment: 'project.experimental.sample_preparation.medium_details.metagenomic_environment_title.keyword',
instrument_type: 'project.experimental.instrumentation_methods.instrumentation.instrument_title.keyword',
ionization_mode: 'project.experimental.instrumentation_methods.mode_title.keyword',
ionization_type: 'project.experimental.instrumentation_methods.ionization_type_title.keyword',
growth_medium: 'project.experimental.sample_preparation.medium_details.medium_title.keyword',
solvent: 'project.experimental.extraction_methods.solvents.solvent_title.keyword',
};
const eskey = key2eskey[key];
if (!eskey) {
throw new Error('Invalid filter field');
}
query.match = {};
query.match[key2eskey[key]] = value;
query.match[eskey] = value;
}
const { body } = await this.client.search({
index: this.index,
Expand Down
49 changes: 49 additions & 0 deletions api/src/util/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import fs from 'fs';
import path from 'path';

export const loadSchema = (fn = path.join(__dirname, '../../../app/public/schema.json')) => {
return JSON.parse(fs.readFileSync(fn, 'utf-8'));
};

type Lookup = Map<string, string>;

function enum2map(choices: any[]): Lookup {
return new Map<string, string>(
choices.map((c) => [c.enum[0], c.title])
);
}

export class Lookups {
readonly genome_type: Lookup;
readonly growth_media: Lookup;
readonly metagenomic_environment: Lookup;
readonly instrument: Lookup;
readonly ionization_mode: Lookup;
readonly ionization_type: Lookup;
readonly solvent: Lookup;

constructor(schema = loadSchema()) {
const genome_types_enum: string[] = schema.properties.genomes.items.properties.genome_ID.properties.genome_type.enum;
this.genome_type = new Map<string, string>(
genome_types_enum.map(s => [s, s])
);
this.growth_media = enum2map(
schema.properties.experimental.properties.sample_preparation.items.properties.medium_details.dependencies.medium_type.oneOf[1].properties.medium.anyOf
);
this.metagenomic_environment = enum2map(
schema.properties.experimental.properties.sample_preparation.items.properties.medium_details.dependencies.medium_type.oneOf[0].properties.metagenomic_environment.oneOf
);
this.instrument = enum2map(
schema.properties.experimental.properties.instrumentation_methods.items.properties.instrumentation.properties.instrument.anyOf
);
this.ionization_mode = enum2map(
schema.properties.experimental.properties.instrumentation_methods.items.properties.mode.anyOf
);
this.ionization_type = enum2map(
schema.properties.experimental.properties.instrumentation_methods.items.properties.ionization.properties.ionization_type.anyOf
);
this.solvent = enum2map(
schema.properties.experimental.properties.extraction_methods.items.properties.solvents.items.properties.solvent.anyOf
);
}
}
14 changes: 11 additions & 3 deletions api/src/util/stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Validator } from '../validate';
import { loadJSONDocument } from './io';
import { computeStats, IStats } from './stats';
import { IOMEGAPairedOmicsDataPlatform } from '../schema';
import { ProjectEnrichments } from '../enrich';
import { EXAMPLE_PROJECT_JSON_FN } from '../testhelpers';
import { loadSchema } from './schema';
jest.mock('../projectdocumentstore');


Expand All @@ -13,8 +13,7 @@ describe('computeStats()', () => {
let schema: object;

beforeAll(() => {
const validator = new Validator();
schema = validator.schema;
schema = loadSchema();
});

describe('with example un-enriched project', () => {
Expand Down Expand Up @@ -51,6 +50,9 @@ describe('computeStats()', () => {
'instrument_types': [
['Time-of-flight (TOF)', 1]
],
'ionization_modes': [
['Positive', 1]
],
'growth_media': [
['A1 medium', 1],
['R5 medium', 1],
Expand Down Expand Up @@ -136,6 +138,9 @@ describe('computeStats()', () => {
'instrument_types': [
['Time-of-flight (TOF)', 1]
],
'ionization_modes': [
['Positive', 1]
],
'growth_media': [
['A1 medium', 1],
['R5 medium', 1],
Expand Down Expand Up @@ -192,6 +197,9 @@ describe('computeStats()', () => {
'instrument_types': [
['Time-of-flight (TOF)', 1]
],
'ionization_modes': [
['Positive', 1]
],
'growth_media': [
['A1 medium', 1],
['R5 medium', 1],
Expand Down
Loading

0 comments on commit a7c576f

Please sign in to comment.