UNPKG

9.11 kBJavaScriptView Raw
1import 'isomorphic-unfetch';
2
3function allSettled(promises, catcher = () => null) {
4 return Promise.all(promises.map(promise => promise.catch(catcher)));
5}
6async 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
24var version = "2.1.0";
25
26class 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
228export { Optimade };