1 | import { allSettled, fetchWithTimeout } from './utils';
|
2 | import { version } from '../package.json'
|
3 |
|
4 | import type * as Types from './types';
|
5 | export { Types };
|
6 |
|
7 | export 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 |