optimade
Version:
Aggregating Optimade client for the online materials databases
237 lines (230 loc) • 10.5 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('isomorphic-unfetch')) :
typeof define === 'function' && define.amd ? define(['exports', 'isomorphic-unfetch'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.optimade = {}));
})(this, (function (exports) { 'use strict';
function allSettled(promises, catcher = () => null) {
return Promise.all(promises.map(promise => promise.catch(catcher)));
}
async function fetchWithTimeout(url, options = {}, timeout = 5000) {
const ac = new AbortController();
const signal = ac.signal;
const timer = setTimeout(() => ac.abort(), timeout);
try {
return await fetch(url, { ...options, signal });
}
catch (err) {
if (err.name === 'AbortError') {
throw new Error('Request timed out');
}
throw err;
}
finally {
clearTimeout(timer);
}
}
var version = "2.1.0";
class Optimade {
providersUrl = '';
corsProxyUrl = '';
providers = null;
apis = {};
reqStack = [];
constructor({ providersUrl = '', corsProxyUrl = '' } = {}) {
this.corsProxyUrl = corsProxyUrl;
this.providersUrl = this.wrapUrl(providersUrl);
}
async addProvider(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) => +number < 1000)
.map((number) => +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) {
const providers = 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, version = '') {
if (typeof provider === 'string') {
provider = this.providers[provider];
}
if (!provider) {
throw new Error('No provider found');
}
const url = this.wrapUrl(`${provider.attributes.base_url}/${version}`, '/info');
if (this.isDuplicatedReq(url)) {
return null;
}
const apis = await Optimade.getJSON(url);
return Optimade.apiVersion(apis);
}
async getStructures({ providerId, filter, page, limit, offset }) {
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 = await allSettled(apis.map(async (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, structure) => {
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 }) {
const results = providers.reduce((structures, providerId) => {
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;
}
async followLinks(api) {
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;
}
wrapUrl(url, tail = '') {
url = this.corsProxyUrl ? `${this.corsProxyUrl}/${url.replace('://', '/').replace('//', '/')}` : url;
return tail ? url.replace(/\/$/, '') + tail : url;
}
isDuplicatedReq(url) {
return this.reqStack.includes(url) || !this.reqStack.unshift(url);
}
static async getJSON(uri, params = null, headers = {}) {
const url = new URL(uri);
const timeout = 10000;
if (params) {
Object.entries(params).forEach((param) => 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 = await res.json();
const error = new Error(err.errors[0].detail);
error.response = err;
throw error;
}
if (res.status !== 204) {
return await res.json();
}
}
static isProviderValid(provider) {
return provider.attributes.base_url && !provider.attributes.base_url.includes('example');
}
static apiVersionUrl({ attributes: { api_version, available_api_versions } }) {
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 }) {
return Array.isArray(data) ?
data.find(({ attributes }) => attributes.api_version === meta.api_version) :
data;
}
}
exports.Optimade = Optimade;
Object.defineProperty(exports, '__esModule', { value: true });
}));