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