1 | (function (global, factory) {
|
2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('isomorphic-unfetch')) :
|
3 | typeof define === 'function' && define.amd ? define(['exports', 'isomorphic-unfetch'], factory) :
|
4 | (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.optimade = {}));
|
5 | })(this, (function (exports) { 'use strict';
|
6 |
|
7 | function allSettled(promises, catcher = () => null) {
|
8 | return Promise.all(promises.map(promise => promise.catch(catcher)));
|
9 | }
|
10 | async function fetchWithTimeout(url, options = {}, timeout = 5000) {
|
11 | const ac = new AbortController();
|
12 | const signal = ac.signal;
|
13 | const timer = setTimeout(() => ac.abort(), timeout);
|
14 | try {
|
15 | return await fetch(url, { ...options, signal });
|
16 | }
|
17 | catch (err) {
|
18 | if (err.name === 'AbortError') {
|
19 | throw new Error('Request timed out');
|
20 | }
|
21 | throw err;
|
22 | }
|
23 | finally {
|
24 | clearTimeout(timer);
|
25 | }
|
26 | }
|
27 |
|
28 | var version = "2.1.0";
|
29 |
|
30 | class Optimade {
|
31 | providersUrl = '';
|
32 | corsProxyUrl = '';
|
33 | providers = null;
|
34 | apis = {};
|
35 | reqStack = [];
|
36 | constructor({ providersUrl = '', corsProxyUrl = '' } = {}) {
|
37 | this.corsProxyUrl = corsProxyUrl;
|
38 | this.providersUrl = this.wrapUrl(providersUrl);
|
39 | }
|
40 | async addProvider(provider) {
|
41 | if (!this.apis[provider.id]) {
|
42 | this.apis[provider.id] = [];
|
43 | }
|
44 | try {
|
45 | const ver = provider.attributes
|
46 | && provider.attributes.api_version ?
|
47 | provider.attributes.api_version.charAt(0) : '';
|
48 | const api = await this.getApis(provider, ver ? `v${ver}` : '');
|
49 | if (api.attributes.available_endpoints.includes('structures')) {
|
50 | this.apis[provider.id].push(api);
|
51 | }
|
52 | }
|
53 | catch (ignore) {
|
54 | console.log(ignore);
|
55 | }
|
56 | if (!provider.attributes.query_limits) {
|
57 | const formula = `chemical_formula_anonymous="A2B"`;
|
58 | const url = `${provider.attributes.base_url}/v1/structures?filter=${formula}&page_limit=1000`;
|
59 | try {
|
60 | const res = await fetch(url).then(res => res.json());
|
61 | const version = res.meta && res.meta.api_version || this.apis[provider.id][0].attributes.api_version;
|
62 | const detail = (errors) => {
|
63 | return errors
|
64 | ? errors.length
|
65 | ? errors[0].detail
|
66 | : errors.detail
|
67 | : '10';
|
68 | };
|
69 | const limits = detail(res.errors)
|
70 | .match(/\d+/g)
|
71 | .filter((number) => +number < 1000)
|
72 | .map((number) => +number);
|
73 | provider.attributes = {
|
74 | ...provider.attributes,
|
75 | api_version: version,
|
76 | query_limits: limits
|
77 | };
|
78 | }
|
79 | catch (error) {
|
80 | console.log(error);
|
81 | }
|
82 | }
|
83 | this.providers[provider.id] = provider;
|
84 | return this.providers;
|
85 | }
|
86 | async getProviders(api) {
|
87 | const providers = await (api ?
|
88 | this.followLinks(api).catch(() => null) :
|
89 | Optimade.getJSON(this.providersUrl).catch(() => null));
|
90 | if (!providers) {
|
91 | return null;
|
92 | }
|
93 | if (!this.providers) {
|
94 | this.providers = {};
|
95 | }
|
96 | const data = providers.data.filter(Optimade.isProviderValid);
|
97 | const ver = providers.meta && providers.meta.api_version ?
|
98 | providers.meta.api_version.charAt(0) : '';
|
99 | for (const provider of data) {
|
100 | if (!this.apis[provider.id]) {
|
101 | this.apis[provider.id] = [];
|
102 | }
|
103 | try {
|
104 | const api = await this.getApis(provider, ver ? `v${ver}` : '');
|
105 | if (!api) {
|
106 | continue;
|
107 | }
|
108 | if (api.attributes.available_endpoints.includes('structures')) {
|
109 | this.apis[provider.id].push(api);
|
110 | if (!this.providers[provider.id]) {
|
111 | this.providers[provider.id] = provider;
|
112 | }
|
113 | }
|
114 | else {
|
115 | await this.getProviders(api);
|
116 | }
|
117 | }
|
118 | catch (ignore) {
|
119 | console.log(ignore);
|
120 | }
|
121 | }
|
122 | return this.providers;
|
123 | }
|
124 | async getApis(provider, version = '') {
|
125 | if (typeof provider === 'string') {
|
126 | provider = this.providers[provider];
|
127 | }
|
128 | if (!provider) {
|
129 | throw new Error('No provider found');
|
130 | }
|
131 | const url = this.wrapUrl(`${provider.attributes.base_url}/${version}`, '/info');
|
132 | if (this.isDuplicatedReq(url)) {
|
133 | return null;
|
134 | }
|
135 | const apis = await Optimade.getJSON(url);
|
136 | return Optimade.apiVersion(apis);
|
137 | }
|
138 | async getStructures({ providerId, filter, page, limit, offset }) {
|
139 | if (!this.apis[providerId]) {
|
140 | return null;
|
141 | }
|
142 | const apis = this.apis[providerId].filter(api => api.attributes.available_endpoints.includes('structures'));
|
143 | const provider = this.providers[providerId];
|
144 | const structures = await allSettled(apis.map(async (api) => {
|
145 | const pageLimit = limit ? `&page_limit=${limit}` : '';
|
146 | const pageNumber = page ? `&page_number=${page}` : '';
|
147 | const pageOffset = offset ? `&page_offset=${offset}` : '';
|
148 | const params = filter ? `${pageLimit + pageNumber + pageOffset}` : `?${pageLimit}`;
|
149 | const url = this.wrapUrl(Optimade.apiVersionUrl(api), filter ? `/structures?filter=${filter + params}` : `/structures${params}`);
|
150 | try {
|
151 | return await Optimade.getJSON(url, {}, { Origin: 'https://cors.optimade.science', 'X-Requested-With': 'XMLHttpRequest' });
|
152 | }
|
153 | catch (error) {
|
154 | return error;
|
155 | }
|
156 | }));
|
157 | return structures.reduce((structures, structure) => {
|
158 | console.dir(`optimade-client-${providerId}:`, structure);
|
159 | if (structure instanceof Error || Object.keys(structure).includes('errors')) {
|
160 | return structures.concat(structure);
|
161 | }
|
162 | else {
|
163 | structure.meta.pages = Math.ceil(structure.meta.data_returned / (limit || structure.data.length));
|
164 | structure.meta.limits = provider.attributes.query_limits || [10];
|
165 | return structures.concat(structure);
|
166 | }
|
167 | }, []);
|
168 | }
|
169 | getStructuresAll({ providers, filter, page, limit, offset, batch = true }) {
|
170 | const results = providers.reduce((structures, providerId) => {
|
171 | const provider = this.providers[providerId];
|
172 | if (provider) {
|
173 | structures.push(allSettled([
|
174 | this.getStructures({ providerId, filter, page, limit, offset }),
|
175 | Promise.resolve(provider)
|
176 | ]));
|
177 | }
|
178 | return structures;
|
179 | }, []);
|
180 | return batch ? Promise.all(results) : results;
|
181 | }
|
182 | async followLinks(api) {
|
183 | if (!api.attributes.available_endpoints.includes('links')) {
|
184 | return null;
|
185 | }
|
186 | const url = this.wrapUrl(Optimade.apiVersionUrl(api), '/links');
|
187 | return !this.isDuplicatedReq(url) ? Optimade.getJSON(url) : null;
|
188 | }
|
189 | wrapUrl(url, tail = '') {
|
190 | url = this.corsProxyUrl ? `${this.corsProxyUrl}/${url.replace('://', '/').replace('//', '/')}` : url;
|
191 | return tail ? url.replace(/\/$/, '') + tail : url;
|
192 | }
|
193 | isDuplicatedReq(url) {
|
194 | return this.reqStack.includes(url) || !this.reqStack.unshift(url);
|
195 | }
|
196 | static async getJSON(uri, params = null, headers = {}) {
|
197 | const url = new URL(uri);
|
198 | const timeout = 10000;
|
199 | if (params) {
|
200 | Object.entries(params).forEach((param) => url.searchParams.append(...param));
|
201 | }
|
202 | Object.assign(headers, { 'User-Agent': `tilde-lab-optimade-client/${version}` });
|
203 | const res = await fetchWithTimeout(url.toString(), { headers }, timeout);
|
204 | if (!res.ok) {
|
205 | const err = await res.json();
|
206 | const error = new Error(err.errors[0].detail);
|
207 | error.response = err;
|
208 | throw error;
|
209 | }
|
210 | if (res.status !== 204) {
|
211 | return await res.json();
|
212 | }
|
213 | }
|
214 | static isProviderValid(provider) {
|
215 | return provider.attributes.base_url && !provider.attributes.base_url.includes('example');
|
216 | }
|
217 | static apiVersionUrl({ attributes: { api_version, available_api_versions } }) {
|
218 | let url = available_api_versions[api_version];
|
219 | if (!url && Array.isArray(available_api_versions)) {
|
220 | const api = available_api_versions.find(({ version }) => version === api_version);
|
221 | url = api && api.url;
|
222 | }
|
223 | return url;
|
224 | }
|
225 | static apiVersion({ data, meta }) {
|
226 | return Array.isArray(data) ?
|
227 | data.find(({ attributes }) => attributes.api_version === meta.api_version) :
|
228 | data;
|
229 | }
|
230 | }
|
231 |
|
232 | exports.Optimade = Optimade;
|
233 |
|
234 | Object.defineProperty(exports, '__esModule', { value: true });
|
235 |
|
236 | }));
|