From 31e92e4425bb2eab9d371394d59d53d82b30e736 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Thu, 26 Jul 2018 18:46:07 +0200 Subject: [PATCH] ModelServer JSON based web api --- src/servers/model/properties.ts | 8 +- .../properties/structure-quality-report.ts | 3 +- src/servers/model/query/atoms.ts | 9 +- src/servers/model/server/api-web.ts | 40 +++++---- src/servers/model/server/api.ts | 86 +++++++++---------- src/servers/model/server/query.ts | 2 +- src/servers/model/server/structure-wrapper.ts | 13 ++- 7 files changed, 92 insertions(+), 69 deletions(-) diff --git a/src/servers/model/properties.ts b/src/servers/model/properties.ts index 3f35e2415..b3486a48d 100644 --- a/src/servers/model/properties.ts +++ b/src/servers/model/properties.ts @@ -7,6 +7,10 @@ import { Model } from 'mol-model/structure'; import { StructureQualityReport } from './properties/structure-quality-report'; -export async function attachModelProperties(model: Model) { - await StructureQualityReport.attachFromPDBeApi(model); +export function attachModelProperties(model: Model): Promise<any>[] { + // return a list of promises that start attaching the props in parallel + // (if there are downloads etc.) + return [ + StructureQualityReport.attachFromPDBeApi(model) + ]; } \ No newline at end of file diff --git a/src/servers/model/properties/structure-quality-report.ts b/src/servers/model/properties/structure-quality-report.ts index 0f57a6855..cc498b6f3 100644 --- a/src/servers/model/properties/structure-quality-report.ts +++ b/src/servers/model/properties/structure-quality-report.ts @@ -46,7 +46,6 @@ const _structure_quality_report_fields: CifField<ResidueIndex, ExportCtx>[] = [ CifField.int<ResidueIndex, ExportCtx>('auth_seq_id', (i, d) => P.residue.auth_seq_id(d.residues[i])), CifField.str<ResidueIndex, ExportCtx>('auth_asym_id', (i, d) => P.chain.auth_asym_id(d.residues[i])), - CifField.str<ResidueIndex, ExportCtx>('issues', (i, d) => d.issues.get(d.residueIndex[d.residues[i].element])!.join(',')) ]; @@ -106,7 +105,7 @@ export namespace StructureQualityReport { if (model.customProperties.has(Descriptor)) return true; const id = model.label.toLowerCase(); - const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`); + const rawData = await fetch(`https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/${model.label.toLowerCase()}`, { timeout: 500 }); const json = await rawData.json(); const data = json[id]; if (!data) return false; diff --git a/src/servers/model/query/atoms.ts b/src/servers/model/query/atoms.ts index 3aca80108..524338e9f 100644 --- a/src/servers/model/query/atoms.ts +++ b/src/servers/model/query/atoms.ts @@ -8,6 +8,7 @@ import { QueryPredicate, StructureElement, StructureProperties as Props, Queries import { AtomsQueryParams } from 'mol-model/structure/query/queries/generators'; export function getAtomsTests(params: any): Partial<AtomsQueryParams>[] { + if (!params) return [{ }]; if (Array.isArray(params)) { return params.map(p => atomsTest(p)); } else { @@ -25,12 +26,14 @@ function atomsTest(params: any): Partial<AtomsQueryParams> { } function entityTest(params: any): QueryPredicate | undefined { - if (typeof params.entity_id === 'undefined') return void 0; + if (!params || typeof params.entity_id === 'undefined') return void 0; const p = Props.entity.id, id = '' + params.label_entity_id; return ctx => p(ctx.element) === id; } function chainTest(params: any): QueryPredicate | undefined { + if (!params) return void 0; + if (typeof params.label_asym_id !== 'undefined') { const p = Props.chain.label_asym_id, id = '' + params.label_asym_id; return ctx => p(ctx.element) === id; @@ -43,6 +46,8 @@ function chainTest(params: any): QueryPredicate | undefined { } function residueTest(params: any): QueryPredicate | undefined { + if (!params) return void 0; + const props: StructureElement.Property<any>[] = [], values: any[] = []; if (typeof params.label_seq_id !== 'undefined') { @@ -74,6 +79,8 @@ function residueTest(params: any): QueryPredicate | undefined { } function atomTest(params: any): QueryPredicate | undefined { + if (!params) return void 0; + const props: StructureElement.Property<any>[] = [], values: any[] = []; if (typeof params.label_atom_id !== 'undefined') { diff --git a/src/servers/model/server/api-web.ts b/src/servers/model/server/api-web.ts index ca96bbbe5..93332c55f 100644 --- a/src/servers/model/server/api-web.ts +++ b/src/servers/model/server/api-web.ts @@ -6,7 +6,6 @@ import * as express from 'express'; import Config from '../config'; -import { QueryDefinition, QueryList } from './api'; import { ConsoleLogger } from 'mol-util/console-logger'; import { resolveJob } from './query'; import { JobManager } from './jobs'; @@ -81,24 +80,35 @@ async function processNextJob() { } } -function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) { - app.get(makePath(':entryId/' + queryName), (req, res) => { - ConsoleLogger.log('Server', `Query '${req.params.entryId}/${queryName}'...`); +// function mapQuery(app: express.Express, queryName: string, queryDefinition: QueryDefinition) { +// app.get(makePath(':entryId/' + queryName), (req, res) => { +// ConsoleLogger.log('Server', `Query '${req.params.entryId}/${queryName}'...`); - if (JobManager.size >= Config.maxQueueLength) { - res.status(503).send('Too many queries, please try again later.'); - res.end(); - return; - } +// if (JobManager.size >= Config.maxQueueLength) { +// res.status(503).send('Too many queries, please try again later.'); +// res.end(); +// return; +// } + +// const jobId = JobManager.add('pdb', req.params.entryId, queryName, req.query); +// responseMap.set(jobId, res); +// if (JobManager.size === 1) processNextJob(); +// }); +// } - const jobId = JobManager.add('pdb', req.params.entryId, queryName, req.query); +export function initWebApi(app: express.Express) { + app.get(makePath('query'), (req, res) => { + const query = /\?(.*)$/.exec(req.url)![1]; + const args = JSON.parse(decodeURIComponent(query)); + const name = args.name; + const entryId = args.id; + const params = args.params || { }; + const jobId = JobManager.add('pdb', entryId, name, params); responseMap.set(jobId, res); if (JobManager.size === 1) processNextJob(); }); -} -export function initWebApi(app: express.Express) { - for (const q of QueryList) { - mapQuery(app, q.name, q.definition); - } + // for (const q of QueryList) { + // mapQuery(app, q.name, q.definition); + // } } \ No newline at end of file diff --git a/src/servers/model/server/api.ts b/src/servers/model/server/api.ts index 44135d39d..93504ed29 100644 --- a/src/servers/model/server/api.ts +++ b/src/servers/model/server/api.ts @@ -34,18 +34,18 @@ export interface QueryDefinition { structureTransform?: (params: any, s: Structure) => Promise<Structure> } -const AtomSiteParams = { - entity_id: <QueryParamInfo>{ name: 'entity_id', type: QueryParamType.String, description: 'Corresponds to the \'_entity.id\' or \'*.label_entity_id\' field, depending on the context.' }, +// const AtomSiteParams = { +// entity_id: <QueryParamInfo>{ name: 'entity_id', type: QueryParamType.String, description: 'Corresponds to the \'_entity.id\' or \'*.label_entity_id\' field, depending on the context.' }, - label_asym_id: <QueryParamInfo>{ name: 'label_asym_id', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.label_asym_id\' field.' }, - auth_asym_id: <QueryParamInfo>{ name: 'auth_asym_id', type: QueryParamType.String, exampleValue: 'A', description: 'Corresponds to the \'_atom_site.auth_asym_id\' field.' }, +// label_asym_id: <QueryParamInfo>{ name: 'label_asym_id', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.label_asym_id\' field.' }, +// auth_asym_id: <QueryParamInfo>{ name: 'auth_asym_id', type: QueryParamType.String, exampleValue: 'A', description: 'Corresponds to the \'_atom_site.auth_asym_id\' field.' }, - label_seq_id: <QueryParamInfo>{ name: 'label_seq_id', type: QueryParamType.Integer, description: 'Residue seq. number. Corresponds to the \'_atom_site.label_seq_id\' field.' }, - auth_seq_id: <QueryParamInfo>{ name: 'auth_seq_id', type: QueryParamType.Integer, exampleValue: '200', description: 'Author residue seq. number. Corresponds to the \'_atom_site.auth_seq_id\' field.' }, - label_comp_id: <QueryParamInfo>{ name: 'label_comp_id', type: QueryParamType.String, description: 'Residue name. Corresponds to the \'_atom_site.label_comp_id\' field.' }, - auth_comp_id: <QueryParamInfo>{ name: 'auth_comp_id', type: QueryParamType.String, exampleValue: 'REA', description: 'Author residue name. Corresponds to the \'_atom_site.auth_comp_id\' field.' }, - pdbx_PDB_ins_code: <QueryParamInfo>{ name: 'pdbx_PDB_ins_code', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.pdbx_PDB_ins_code\' field.' }, -}; +// label_seq_id: <QueryParamInfo>{ name: 'label_seq_id', type: QueryParamType.Integer, description: 'Residue seq. number. Corresponds to the \'_atom_site.label_seq_id\' field.' }, +// auth_seq_id: <QueryParamInfo>{ name: 'auth_seq_id', type: QueryParamType.Integer, exampleValue: '200', description: 'Author residue seq. number. Corresponds to the \'_atom_site.auth_seq_id\' field.' }, +// label_comp_id: <QueryParamInfo>{ name: 'label_comp_id', type: QueryParamType.String, description: 'Residue name. Corresponds to the \'_atom_site.label_comp_id\' field.' }, +// auth_comp_id: <QueryParamInfo>{ name: 'auth_comp_id', type: QueryParamType.String, exampleValue: 'REA', description: 'Author residue name. Corresponds to the \'_atom_site.auth_comp_id\' field.' }, +// pdbx_PDB_ins_code: <QueryParamInfo>{ name: 'pdbx_PDB_ins_code', type: QueryParamType.String, description: 'Corresponds to the \'_atom_site.pdbx_PDB_ins_code\' field.' }, +// }; const AtomSiteTestParams: QueryParamInfo = { name: 'atom_site', @@ -99,10 +99,10 @@ const QueryMap: { [id: string]: Partial<QueryDefinition> } = { }] }, 'residueInteraction': { - niceName: 'Residues Inside a Sphere', + niceName: 'Residue Interaction', description: 'Identifies all residues within the given radius from the source residue. Takes crystal symmetry into account.', query(p) { - const tests = getAtomsTests(p); + const tests = getAtomsTests(p.atom_site); const center = Queries.combinators.merge(tests.map(test => Queries.generators.atoms({ ...test, entityTest: test.entityTest @@ -114,17 +114,7 @@ const QueryMap: { [id: string]: Partial<QueryDefinition> } = { structureTransform(p, s) { return StructureSymmetry.builderSymmetryMates(s, p.radius).run(); }, - params: [ - AtomSiteParams.entity_id, - AtomSiteParams.label_asym_id, - AtomSiteParams.auth_asym_id, - AtomSiteParams.label_comp_id, - AtomSiteParams.auth_comp_id, - AtomSiteParams.pdbx_PDB_ins_code, - AtomSiteParams.label_seq_id, - AtomSiteParams.auth_seq_id, - RadiusParam, - ] + params: [ AtomSiteTestParams, RadiusParam ] }, }; @@ -148,30 +138,32 @@ export const QueryList = (function () { } })(); -function _normalizeQueryParams(params: { [p: string]: string }, paramList: QueryParamInfo[]): { [p: string]: string | number | boolean } { - const ret: any = {}; - for (const p of paramList) { - const key = p.name; - const value = params[key]; - if (typeof value === 'undefined' || (typeof value !== 'undefined' && value !== null && value['length'] === 0)) { - if (p.required) { - throw `The parameter '${key}' is required.`; - } - if (typeof p.defaultValue !== 'undefined') ret[key] = p.defaultValue; - } else { - switch (p.type) { - case QueryParamType.String: ret[key] = value; break; - case QueryParamType.Integer: ret[key] = parseInt(value); break; - case QueryParamType.Float: ret[key] = parseFloat(value); break; - } - - if (p.validation) p.validation(ret[key]); - } - } - - return ret; -} +// function _normalizeQueryParams(params: { [p: string]: string }, paramList: QueryParamInfo[]): { [p: string]: string | number | boolean } { +// const ret: any = {}; +// for (const p of paramList) { +// const key = p.name; +// const value = params[key]; +// if (typeof value === 'undefined' || (typeof value !== 'undefined' && value !== null && value['length'] === 0)) { +// if (p.required) { +// throw `The parameter '${key}' is required.`; +// } +// if (typeof p.defaultValue !== 'undefined') ret[key] = p.defaultValue; +// } else { +// switch (p.type) { +// case QueryParamType.JSON: ret[key] = JSON.parse(value); break; +// case QueryParamType.String: ret[key] = value; break; +// case QueryParamType.Integer: ret[key] = parseInt(value); break; +// case QueryParamType.Float: ret[key] = parseFloat(value); break; +// } + +// if (p.validation) p.validation(ret[key]); +// } +// } + +// return ret; +// } export function normalizeQueryParams(query: QueryDefinition, params: any) { - return _normalizeQueryParams(params, query.params); + return params; + //return _normalizeQueryParams(params, query.params); } \ No newline at end of file diff --git a/src/servers/model/server/query.ts b/src/servers/model/server/query.ts index 92b01d622..3fce312b7 100644 --- a/src/servers/model/server/query.ts +++ b/src/servers/model/server/query.ts @@ -137,7 +137,7 @@ const _model_server_params: CifWriter.Category<Job> = { instance(job) { const params: string[][] = []; for (const k of Object.keys(job.normalizedParams)) { - params.push([k, '' + job.normalizedParams[k]]); + params.push([k, JSON.stringify(job.normalizedParams[k])]); } return { data: params, diff --git a/src/servers/model/server/structure-wrapper.ts b/src/servers/model/server/structure-wrapper.ts index d70a75823..8774e578c 100644 --- a/src/servers/model/server/structure-wrapper.ts +++ b/src/servers/model/server/structure-wrapper.ts @@ -107,7 +107,10 @@ async function readStructure(key: string, sourceId: string, entryId: string) { perf.end('createModel'); perf.start('attachProps'); - await attachModelProperties(models[0]); + const modelProps = attachModelProperties(models[0]); + for (const p of modelProps) { + await tryAttach(key, p); + } perf.end('attachProps'); const structure = Structure.ofModel(models[0]); @@ -128,4 +131,12 @@ async function readStructure(key: string, sourceId: string, entryId: string) { }; return ret; +} + +async function tryAttach(key: string, promise: Promise<any>) { + try { + await promise; + } catch (e) { + ConsoleLogger.errorId(key, 'Custom prop:' + e); + } } \ No newline at end of file -- GitLab