UNPKG

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