Skip to content

Commit

Permalink
Merge pull request #140 from iomega/paging
Browse files Browse the repository at this point in the history
Paging & sorting
  • Loading branch information
sverhoeven authored Apr 23, 2020
2 parents 76e24fe + 82eb5fd commit c038070
Show file tree
Hide file tree
Showing 24 changed files with 931 additions and 451 deletions.
15 changes: 13 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.6.1] 2020-04-16
## [0.6.2] - 2020-04-23

### Added

* Paging projects ([#137](https://github.com/iomega/paired-data-form/issues/137))

### Changed

* Sort projects moved from web application to elastic search ([#138](https://github.com/iomega/paired-data-form/issues/138))
* Allow search and filter to be combined

## [0.6.1] - 2020-04-16

### Added

Expand All @@ -18,7 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Enrichments cause Limit of total fields exceeded error in elastic search ([#131](https://github.com/iomega/paired-data-form/issues/131))

## [0.6.0] 2020-04-16
## [0.6.0] - 2020-04-16

Search functionality using elastic search has been added.

Expand Down
2 changes: 1 addition & 1 deletion api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "podp-api",
"version": "0.6.1",
"version": "0.6.2",
"description": "",
"license": "Apache-2.0",
"scripts": {
Expand Down
85 changes: 74 additions & 11 deletions api/src/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ describe('app', () => {
listProjects: async () => {
return [] as EnrichedProjectDocument[];
},
searchProjects: jest.fn().mockImplementation(async () => {
const data: EnrichedProjectDocument[] = [];
return {
data,
total: 0
};
}),
createProject: async () => {
return 'projectid1.1';
},
Expand Down Expand Up @@ -89,18 +96,68 @@ describe('app', () => {
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
data: []
data: [],
total: 0
};
expect(body).toEqual(expected);
});

describe.each([
['/api/projects', { size: 100, from: 0 }],
['/api/projects?size=12', { size: 12, from: 0 }],
['/api/projects?page=2', { size: 100, from: 200 }],
['/api/projects?size=12&page=3', { size: 12, from: 36 }],
['/api/projects?sort=nr_genomes', { size: 100, from: 0, sort: 'nr_genomes' }],
['/api/projects?order=asc', { size: 100, from: 0, order: 'asc' }],
['/api/projects?sort=nr_genomes&order=desc', { size: 100, from: 0, sort: 'nr_genomes', order: 'desc' }],
['/api/projects?q=Justin', { query: 'Justin', size: 100, from: 0 }],
['/api/projects?q=Justin&size=12&page=3', { query: 'Justin', size: 12, from: 36 }],
['/api/projects?fk=species&fv=somevalue', { filter: { key: 'species', value: 'somevalue' }, size: 100, from: 0 }],
['/api/projects?fk=species&fv=somevalue&size=12&page=3', { filter: { key: 'species', value: 'somevalue' }, size: 12, from: 36 }],
['/api/projects?q=Justin&fk=species&fv=somevalue', { query: 'Justin', filter: { key: 'species', value: 'somevalue' }, size: 100, from: 0 }],
])('GET %s', (url, expected) => {
it('should call store.searchProjects', async () => {
await supertest(app).get(url);
expect(store.searchProjects).toBeCalledWith(expected);
});
});

describe.each([
['/api/projects?fk=wrongfield&fv=somevalue', 'Invalid `fk`'],
['/api/projects?fk=species', 'Require both `fk` and `fv` to filter'],
['/api/projects?fv=somevalue', 'Require both `fk` and `fv` to filter'],
['/api/projects?size=foo', 'Size is not an integer'],
['/api/projects?size=-10', 'Size must be between `1` and `1000`'],
['/api/projects?size=1110', 'Size must be between `1` and `1000`'],
['/api/projects?page=foo', 'Page is not an integer'],
['/api/projects?page=-10', 'Page must be between `0` and `1000`'],
['/api/projects?page=1111', 'Page must be between `0` and `1000`'],
['/api/projects?sort=somebadfield', 'Invalid `sort`'],
['/api/projects?order=somebadorder', 'Invalid `order`, must be either `desc` or `asc`'],
])('GET %s', (url, expected) => {
let response: any;

beforeEach(async () => {
response = await supertest(app).get(url);
});

it('should return a 400 status', () => {
expect(response.status).toBe(400);
});

it('should return an error message', () => {
const body = JSON.parse(response.text);
expect(body.message).toEqual(expected);
});
});
});

describe('GET /api/pending/projects', () => {
it('should return zero project summaries', async () => {
const response = await supertest(app)
.get('/api/pending/projects')
.set('Authorization', 'Bearer ashdfjhasdlkjfhalksdjhflak')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand All @@ -115,7 +172,7 @@ describe('app', () => {
const response = await supertest(app)
.get('/api/pending/projects/projectid1.1')
.set('Authorization', 'Bearer ashdfjhasdlkjfhalksdjhflak')
;
;
expect(response.status).toBe(404);
});
});
Expand All @@ -125,7 +182,7 @@ describe('app', () => {
const response = await supertest(app)
.get('/api/pending/projects/projectid1.1')
.set('Authorization', 'Bearer incorrecttoken')
;
;
expect(response.status).toBe(401);
});
});
Expand Down Expand Up @@ -172,6 +229,12 @@ describe('app', () => {
listProjects: async () => {
return [eproject];
},
searchProjects: async () => {
return {
data: [eproject],
total: 1
};
},
createProject: async () => {
return 'projectid1.1';
},
Expand Down Expand Up @@ -209,7 +272,7 @@ describe('app', () => {
const response = await supertest(app)
.get('/api/pending/projects/projectid1.1')
.set('Authorization', 'Bearer ashdfjhasdlkjfhalksdjhflak')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand All @@ -225,7 +288,7 @@ describe('app', () => {
const response = await supertest(app)
.post('/api/pending/projects/projectid1.1')
.set('Authorization', 'Bearer ashdfjhasdlkjfhalksdjhflak')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand All @@ -241,7 +304,7 @@ describe('app', () => {
const response = await supertest(app)
.delete('/api/pending/projects/projectid1.1')
.set('Authorization', 'Bearer ashdfjhasdlkjfhalksdjhflak')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand All @@ -255,7 +318,7 @@ describe('app', () => {
it('should return project', async () => {
const response = await supertest(app)
.get('/api/projects/projectid1.1')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
expect(body).toEqual(project);
Expand All @@ -266,7 +329,7 @@ describe('app', () => {
it('should return enriched project', async () => {
const response = await supertest(app)
.get('/api/projects/projectid1.1/enriched')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand All @@ -281,7 +344,7 @@ describe('app', () => {
it('should return enriched project', async () => {
const response = await supertest(app)
.get('/api/projects/projectid1.1/history')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand Down Expand Up @@ -345,7 +408,7 @@ describe('app', () => {
it('should return version info', async () => {
const response = await supertest(app)
.get('/api/version')
;
;
expect(response.status).toBe(200);
const body = JSON.parse(response.text);
const expected: any = {
Expand Down
82 changes: 61 additions & 21 deletions api/src/controller.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Request, Response } from 'express';

import { ProjectDocumentStore, NotFoundException, ListOptions } from './projectdocumentstore';
import { ProjectDocumentStore, NotFoundException } from './projectdocumentstore';
import { Validator } from './validate';
import { Queue } from 'bull';
import { IOMEGAPairedOmicsDataPlatform as ProjectDocument } from './schema';
import { computeStats } from './util/stats';
import { summarizeProject, compareMetaboliteID } from './summarize';
import { summarizeProject } from './summarize';
import { ZENODO_DEPOSITION_ID } from './util/secrets';
import { FilterFields, DEFAULT_PAGE_SIZE, Order, SearchOptions, SortFields } from './store/search';


function getStore(req: Request) {
Expand Down Expand Up @@ -81,32 +82,71 @@ export async function denyProject(req: Request, res: Response) {
res.json({'message': 'Denied pending project'});
}

export async function listProjects(req: Request, res: Response) {
const store = getStore(req);
const options: ListOptions = {};
if (req.query.q) {
options.query = req.query.q;
function checkRange(svalue: string, label: string, min: number, max: number) {
const value = parseInt(svalue);
if (Number.isNaN(value)) {
throw `${label} is not an integer`;
}
if (value < min || value > max ) {
throw `${label} must be between \`${min}\` and \`${max}\``;
}
return value;
}

function validateSearchOptions(query: any) {
const options: SearchOptions = {};

if (query.q) {
options.query = query.q;
}
if (req.query.fk || req.query.fv) {
if (req.query.fk && req.query.fv) {
if (query.fk || query.fv) {
if (query.fk && query.fv) {
if (!(query.fk in FilterFields)) {
throw 'Invalid `fk`';
}
options.filter = {
key: req.query.fk,
value: req.query.fv
key: query.fk,
value: query.fv
};
} else {
res.status(400);
res.json({ message: 'Require both `fk` and `fv` to filter'});
return;
throw 'Require both `fk` and `fv` to filter';
}
if (req.query.query) {
res.status(400);
res.json({ message: 'Eiter search with `q` or filter with `fk` and `fv`'});
return;
}
if (query.size) {
options.size = checkRange(query.size, 'Size', 1, 1000);
} else {
options.size = DEFAULT_PAGE_SIZE;
}
if (query.page) {
options.from = checkRange(query.page, 'Page', 0, 1000) * options.size;
} else {
options.from = 0;
}
if (query.sort) {
if (!(query.sort in SortFields)) {
throw 'Invalid `sort`';
}
options.sort = query.sort;
}
if (query.order) {
if (!(query.order in Order)) {
throw 'Invalid `order`, must be either `desc` or `asc`';
}
options.order = Order[query.order as 'desc' | 'asc'];
}
return options;
}

export async function listProjects(req: Request, res: Response) {
const store = getStore(req);
try {
const options = validateSearchOptions(req.query);
const hits = await store.searchProjects(options);
res.json(hits);
} catch (message) {
res.status(400);
res.json({message});
}
const projects = await store.listProjects(options);
const data = projects.map(summarizeProject).sort(compareMetaboliteID).reverse();
res.json({data});
}

export async function getProject(req: Request, res: Response) {
Expand Down
27 changes: 19 additions & 8 deletions api/src/projectdocumentstore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,23 +137,23 @@ describe('ProjectDocumentStore', () => {
expect(approved_project).toEqual(submitted_project);
});

describe('listProjects()', () => {
describe('searchProjects()', () => {
describe('without arguments', () => {
it('should be listed', async () => {
it('should have project1 in list', async () => {
expect.assertions(1);

const projects = await store.listProjects();
const {data: projects} = await store.searchProjects();
const project_ids = new Set(projects.map(p => p._id));

expect(project_ids).toEqual(new Set([project_id]));
expect(project_ids).toEqual(new Set(['projectid1']));
});
});

describe('with query=Justin', () => {
it('should be listed', async () => {
it('should have project1 in list', async () => {
expect.assertions(1);

const projects = await store.listProjects({
const {data: projects} = await store.searchProjects({
query: 'Justin'
});
const project_ids = new Set(projects.map(p => p._id));
Expand All @@ -163,10 +163,10 @@ describe('ProjectDocumentStore', () => {
});

describe('with filter `principal_investigator=Pieter C. Dorrestein`', () => {
it('should be listed', async () => {
it('should have project1 in list', async () => {
expect.assertions(1);

const projects = await store.listProjects({
const {data: projects} = await store.searchProjects({
filter: {
key: 'principal_investigator',
value: 'Pieter C. Dorrestein'
Expand All @@ -179,6 +179,17 @@ describe('ProjectDocumentStore', () => {
});
});

describe('listProjects()', () => {
it('should have project1 in list', async () => {
expect.assertions(1);

const projects = await store.listProjects();
const project_ids = new Set(projects.map(p => p._id));

expect(project_ids).toEqual(new Set([project_id]));
});
});

describe('addEnrichments()', () => {
beforeEach(async () => {
client.index.mockClear();
Expand Down
Loading

0 comments on commit c038070

Please sign in to comment.