optimade
Version:
Aggregating Optimade client for the online materials databases
227 lines (178 loc) • 9.13 kB
text/typescript
import { allSettled, fetchWithTimeout } from './utils';
import { version } from '../package.json'
import type * as Types from './types';
export { Types };
export class Optimade {
private providersUrl = '';
private corsProxyUrl = '';
providers: Types.ProvidersMap | null = null;
apis: Types.ApisMap = {};
private reqStack: string[] = [];
constructor({ providersUrl = '', corsProxyUrl = '' }: { providersUrl?: string; corsProxyUrl?: string; } = {}) {
this.corsProxyUrl = corsProxyUrl;
this.providersUrl = this.wrapUrl(providersUrl);
}
async addProvider(provider: Types.Provider) {
if (!this.apis[provider.id]) { this.apis[provider.id] = []; }
try {
const ver = provider.attributes
&& provider.attributes.api_version ?
provider.attributes.api_version.charAt(0) : '';
const api = await this.getApis(provider, ver ? `v${ver}` : '');
if (api.attributes.available_endpoints.includes('structures')) {
this.apis[provider.id].push(api);
}
} catch (ignore) { console.log(ignore) }
if (!provider.attributes.query_limits) {
const formula = `chemical_formula_anonymous="A2B"`;
const url = `${provider.attributes.base_url}/v1/structures?filter=${formula}&page_limit=1000`;
try {
const res = await fetch(url).then(res => res.json());
const version = res.meta && res.meta.api_version || this.apis[provider.id][0].attributes.api_version;
const detail = (errors) => {
return errors
? errors.length
? errors[0].detail
: errors.detail
: '10';
};
const limits = detail(res.errors)
.match(/\d+/g)
.filter((number: string) => +number < 1000)
.map((number: string) => +number);
provider.attributes = {
...provider.attributes,
api_version: version,
query_limits: limits
}
} catch (error) {
console.log(error);
}
}
this.providers[provider.id] = provider
return this.providers;
}
async getProviders(api?: Types.Api): Promise<Types.ProvidersMap | null> {
const providers: Types.ProvidersResponse | null = await (api ?
this.followLinks(api).catch(() => null) :
Optimade.getJSON(this.providersUrl).catch(() => null)
);
if (!providers) { return null; }
if (!this.providers) { this.providers = {}; }
const data = providers.data.filter(Optimade.isProviderValid);
const ver = providers.meta && providers.meta.api_version ?
providers.meta.api_version.charAt(0) : '';
for (const provider of data) {
if (!this.apis[provider.id]) { this.apis[provider.id] = []; }
try {
const api = await this.getApis(provider, ver ? `v${ver}` : '');
if (!api) { continue; }
if (api.attributes.available_endpoints.includes('structures')) {
this.apis[provider.id].push(api);
if (!this.providers[provider.id]) {
this.providers[provider.id] = provider;
}
} else {
await this.getProviders(api);
}
} catch (ignore) { console.log(ignore) }
}
return this.providers;
}
async getApis(provider: Types.Provider | string, version = ''): Promise<Types.Api | null> {
if (typeof provider === 'string') {
provider = this.providers[provider];
}
if (!provider) { throw new Error('No provider found'); }
const url: string = this.wrapUrl(`${provider.attributes.base_url}/${version}`, '/info');
if (this.isDuplicatedReq(url)) { return null; }
const apis: Types.InfoResponse = await Optimade.getJSON(url);
return Optimade.apiVersion(apis);
}
async getStructures({ providerId, filter, page, limit, offset }: { providerId: string; filter: string; page: number; limit: number; offset: number; }): Promise<Types.StructuresResponse[] | Types.ResponseError> {
if (!this.apis[providerId]) { return null; }
const apis = this.apis[providerId].filter(api => api.attributes.available_endpoints.includes('structures'));
const provider = this.providers[providerId];
const structures: Types.StructuresResponse[] = await allSettled(apis.map(async (api: Types.Api) => {
const pageLimit = limit ? `&page_limit=${limit}` : '';
const pageNumber = page ? `&page_number=${page}` : '';
const pageOffset = offset ? `&page_offset=${offset}` : '';
const params = filter ? `${pageLimit + pageNumber + pageOffset}` : `?${pageLimit}`;
const url = this.wrapUrl(Optimade.apiVersionUrl(api), filter ? `/structures?filter=${filter + params}` : `/structures${params}`);
try {
return await Optimade.getJSON(url, {}, { Origin: 'https://cors.optimade.science', 'X-Requested-With': 'XMLHttpRequest' });
} catch (error) {
return error;
}
}));
return structures.reduce((structures: any[], structure: Types.StructuresResponse | Types.ResponseError): Types.StructuresResponse[] => {
console.dir(`optimade-client-${providerId}:`, structure);
if (structure instanceof Error || Object.keys(structure).includes('errors')) {
return structures.concat(structure);
} else {
structure.meta.pages = Math.ceil(structure.meta.data_returned / (limit || structure.data.length));
structure.meta.limits = provider.attributes.query_limits || [10];
return structures.concat(structure);
}
}, []);
}
getStructuresAll({ providers, filter, page, limit, offset, batch = true }: { providers: string[]; filter: string; page: number; limit: number; offset: number; batch?: boolean; }): Promise<Promise<Types.StructuresResult>[]> | Promise<Types.StructuresResult>[] {
const results = providers.reduce((structures: Promise<any>[], providerId: string) => {
const provider = this.providers[providerId];
if (provider) {
structures.push(allSettled([
this.getStructures({ providerId, filter, page, limit, offset }),
Promise.resolve(provider)
]));
}
return structures;
}, []);
return batch ? Promise.all(results) : results;
}
private async followLinks(api: Types.Api): Promise<Types.LinksResponse | null> {
if (!api.attributes.available_endpoints.includes('links')) { return null; }
const url = this.wrapUrl(Optimade.apiVersionUrl(api), '/links');
return !this.isDuplicatedReq(url) ? Optimade.getJSON(url) : null;
}
private wrapUrl(url: string, tail = ''): string {
url = this.corsProxyUrl ? `${this.corsProxyUrl}/${url.replace('://', '/').replace('//', '/')}` : url;
return tail ? url.replace(/\/$/, '') + tail : url;
}
private isDuplicatedReq(url: string): boolean {
return this.reqStack.includes(url) || !this.reqStack.unshift(url);
}
static async getJSON(uri: string, params = null, headers = {}) {
const url = new URL(uri);
const timeout = 10000;
if (params) {
Object.entries(params).forEach((param: [string, any]) => url.searchParams.append(...param));
}
Object.assign(headers, { 'User-Agent': `tilde-lab-optimade-client/${version}` })
const res = await fetchWithTimeout(url.toString(), { headers }, timeout);
if (!res.ok) {
const err: Types.ErrorResponse = await res.json();
const error: Types.ResponseError = new Error(err.errors[0].detail);
error.response = err;
throw error;
}
if (res.status !== 204) {
return await res.json();
}
}
static isProviderValid(provider: Types.Provider) {
return provider.attributes.base_url && !provider.attributes.base_url.includes('example');
}
static apiVersionUrl({ attributes: { api_version, available_api_versions } }: Types.Api) {
let url = available_api_versions[api_version];
if (!url && Array.isArray(available_api_versions)) {
const api = available_api_versions.find(({ version }) => version === api_version);
url = api && api.url;
}
return url;
}
static apiVersion({ data, meta }: Types.InfoResponse): Types.Api {
return Array.isArray(data) ?
data.find(({ attributes }) => attributes.api_version === meta.api_version) :
data;
}
}