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 { 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 { 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 { 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[] { const results = providers.reduce((structures: Promise[], 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 { 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; } }