@openeo/js-client 2.5.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/connection.js CHANGED
@@ -1,1212 +1,1310 @@
1
- const Environment = require('./env');
2
- const Utils = require('@openeo/js-commons/src/utils');
3
- const ProcessRegistry = require('@openeo/js-commons/src/processRegistry');
4
- const axios = require('axios').default;
5
- const StacMigrate = require('@radiantearth/stac-migrate');
6
-
7
- const AuthProvider = require('./authprovider');
8
- const BasicProvider = require('./basicprovider');
9
- const OidcProvider = require('./oidcprovider');
10
-
11
- const Capabilities = require('./capabilities');
12
- const FileTypes = require('./filetypes');
13
- const UserFile = require('./userfile');
14
- const Job = require('./job');
15
- const UserProcess = require('./userprocess');
16
- const Service = require('./service');
17
-
18
- const Builder = require('./builder/builder');
19
- const BuilderNode = require('./builder/node');
20
-
21
- /**
22
- * A connection to a back-end.
23
- */
24
- class Connection {
25
-
26
- /**
27
- * Creates a new Connection.
28
- *
29
- * @param {string} baseUrl - The versioned URL or the back-end instance.
30
- * @param {Options} [options={}] - Additional options for the connection.
31
- * @param {?string} [url=null] - User-provided URL of the backend connected to.
32
- */
33
- constructor(baseUrl, options = {}, url = null) {
34
- /**
35
- * User-provided URL of the backend connected to.
36
- *
37
- * `null` if not given and the connection was directly made to a versioned instance of the back-end.
38
- *
39
- * @protected
40
- * @type {string | null}
41
- */
42
- this.url = url;
43
- /**
44
- * The versioned URL or the back-end instance.
45
- *
46
- * @protected
47
- * @type {string}
48
- */
49
- this.baseUrl = Utils.normalizeUrl(baseUrl);
50
- /**
51
- * Auth Provider cache
52
- *
53
- * @protected
54
- * @type {Array.<AuthProvider> | null}
55
- */
56
- this.authProviderList = null;
57
- /**
58
- * Current auth provider
59
- *
60
- * @protected
61
- * @type {AuthProvider | null}
62
- */
63
- this.authProvider = null;
64
- /**
65
- * Capability cache
66
- *
67
- * @protected
68
- * @type {Capabilities | null}
69
- */
70
- this.capabilitiesObject = null;
71
- /**
72
- * Listeners for events.
73
- *
74
- * @protected
75
- * @type {object.<string|Function>}
76
- */
77
- this.listeners = {};
78
- /**
79
- * Additional options for the connection.
80
- *
81
- * @protected
82
- * @type {Options}
83
- */
84
- this.options = options;
85
- /**
86
- * Process cache
87
- *
88
- * @protected
89
- * @type {ProcessRegistry}
90
- */
91
- this.processes = new ProcessRegistry([], Boolean(options.addNamespaceToProcess));
92
- this.processes.listeners.push((...args) => this.emit('processesChanged', ...args));
93
- }
94
-
95
- /**
96
- * Initializes the connection by requesting the capabilities.
97
- *
98
- * @async
99
- * @protected
100
- * @returns {Promise<Capabilities>} Capabilities
101
- */
102
- async init() {
103
- let response = await this._get('/');
104
- this.capabilitiesObject = new Capabilities(response.data);
105
- return this.capabilitiesObject;
106
- }
107
-
108
- /**
109
- * Refresh the cache for processes.
110
- *
111
- * @async
112
- * @protected
113
- * @returns {Promise}
114
- */
115
- async refreshProcessCache() {
116
- if (this.processes.count() === 0) {
117
- return;
118
- }
119
- let promises = this.processes.namespaces().map(namespace => {
120
- let fn = () => Promise.resolve();
121
- if (namespace === 'user') {
122
- let userProcesses = this.processes.namespace('user');
123
- if (!this.isAuthenticated()) {
124
- fn = () => (this.processes.remove(null, 'user') ? Promise.resolve() : Promise.reject(new Error("Can't clear user processes")));
125
- }
126
- else if (this.capabilities().hasFeature('listUserProcesses')) {
127
- fn = () => this.listUserProcesses(userProcesses);
128
- }
129
- }
130
- else if (this.capabilities().hasFeature('listProcesses')) {
131
- fn = () => this.listProcesses(namespace);
132
- }
133
- return fn().catch(error => console.warn(`Could not update processes for namespace '${namespace}' due to an error: ${error.message}`));
134
- });
135
- return await Promise.all(promises);
136
- }
137
-
138
- /**
139
- * Returns the URL of the versioned back-end instance currently connected to.
140
- *
141
- * @returns {string} The versioned URL or the back-end instance.
142
- */
143
- getBaseUrl() {
144
- return this.baseUrl;
145
- }
146
-
147
- /**
148
- * Returns the user-provided URL of the back-end currently connected to.
149
- *
150
- * @returns {string} The URL or the back-end.
151
- */
152
- getUrl() {
153
- return this.url || this.baseUrl;
154
- }
155
-
156
- /**
157
- * Returns the capabilities of the back-end.
158
- *
159
- * @returns {Capabilities} Capabilities
160
- */
161
- capabilities() {
162
- return this.capabilitiesObject;
163
- }
164
-
165
- /**
166
- * List the supported output file formats.
167
- *
168
- * @async
169
- * @returns {Promise<FileTypes>} A response compatible to the API specification.
170
- * @throws {Error}
171
- */
172
- async listFileTypes() {
173
- let response = await this._get('/file_formats');
174
- return new FileTypes(response.data);
175
- }
176
-
177
- /**
178
- * List the supported secondary service types.
179
- *
180
- * @async
181
- * @returns {Promise<object.<string, ServiceType>>} A response compatible to the API specification.
182
- * @throws {Error}
183
- */
184
- async listServiceTypes() {
185
- let response = await this._get('/service_types');
186
- return response.data;
187
- }
188
-
189
- /**
190
- * List the supported UDF runtimes.
191
- *
192
- * @async
193
- * @returns {Promise<object.<string, UdfRuntime>>} A response compatible to the API specification.
194
- * @throws {Error}
195
- */
196
- async listUdfRuntimes() {
197
- let response = await this._get('/udf_runtimes');
198
- return response.data;
199
- }
200
-
201
- /**
202
- * List all collections available on the back-end.
203
- *
204
- * The collections returned always comply to the latest STAC version (currently 1.0.0).
205
- *
206
- * @async
207
- * @returns {Promise<Collections>} A response compatible to the API specification.
208
- * @throws {Error}
209
- */
210
- async listCollections() {
211
- let response = await this._get('/collections');
212
- if (Utils.isObject(response.data) && Array.isArray(response.data.collections)) {
213
- response.data.collections = response.data.collections.map(collection => StacMigrate.collection(collection));
214
- }
215
- return response.data;
216
- }
217
-
218
- /**
219
- * Get further information about a single collection.
220
- *
221
- * The collection returned always complies to the latest STAC version (currently 1.0.0).
222
- *
223
- * @async
224
- * @param {string} collectionId - Collection ID to request further metadata for.
225
- * @returns {Promise<Collection>} - A response compatible to the API specification.
226
- * @throws {Error}
227
- */
228
- async describeCollection(collectionId) {
229
- let response = await this._get('/collections/' + collectionId);
230
- return StacMigrate.collection(response.data);
231
- }
232
-
233
- /**
234
- * Loads items for a specific image collection.
235
- * May not be available for all collections.
236
- *
237
- * The items returned always comply to the latest STAC version (currently 1.0.0).
238
- *
239
- * This is an experimental API and is subject to change.
240
- *
241
- * @async
242
- * @param {string} collectionId - Collection ID to request items for.
243
- * @param {?Array.<number>} [spatialExtent=null] - Limits the items to the given bounding box in WGS84:
244
- * 1. Lower left corner, coordinate axis 1
245
- * 2. Lower left corner, coordinate axis 2
246
- * 3. Upper right corner, coordinate axis 1
247
- * 4. Upper right corner, coordinate axis 2
248
- * @param {?Array} [temporalExtent=null] - Limits the items to the specified temporal interval.
249
- * The interval has to be specified as an array with exactly two elements (start, end) and
250
- * each must be either an RFC 3339 compatible string or a Date object.
251
- * Also supports open intervals by setting one of the boundaries to `null`, but never both.
252
- * @param {?number} [limit=null] - The amount of items per request/page as integer. If `null` (default), the back-end decides.
253
- * @yields {Promise<ItemCollection>} A response compatible to the API specification.
254
- * @throws {Error}
255
- */
256
- async * listCollectionItems(collectionId, spatialExtent = null, temporalExtent = null, limit = null) {
257
- let page = 1;
258
- let nextUrl = '/collections/' + collectionId + '/items';
259
- while(nextUrl) {
260
- let params = {};
261
- if (page === 1) {
262
- if (Array.isArray(spatialExtent)) {
263
- params.bbox = spatialExtent.join(',');
264
- }
265
- if (Array.isArray(temporalExtent)) {
266
- params.datetime = temporalExtent
267
- .map(e => {
268
- if (e instanceof Date) {
269
- return e.toISOString();
270
- }
271
- else if (typeof e === 'string') {
272
- return e;
273
- }
274
- return '..'; // Open date range
275
- })
276
- .join('/');
277
- }
278
- if (limit > 0) {
279
- params.limit = limit;
280
- }
281
- }
282
-
283
- let response = await this._get(nextUrl, params);
284
- if (Utils.isObject(response.data) && Array.isArray(response.data.features)) {
285
- response.data.features = response.data.features.map(item => StacMigrate.item(item));
286
- }
287
- yield response.data;
288
-
289
- page++;
290
- nextUrl = this._getLinkHref(response.data.links);
291
- }
292
- }
293
-
294
- /**
295
- * Normalisation of the namespace to a value that is compatible with the OpenEO specs - EXPERIMENTAL.
296
- *
297
- * This is required to support UDP that are shared as public. These can only be executed with providing the full URL
298
- * (e.g. https://<backend>/processes/<namespace>/<process_id>) as the namespace value in the processing graph. For other
299
- * parts of the API (such as the listing of the processes, only the name of the namespace is required.
300
- *
301
- * This function will extract the short name of the namespace from a shareable URL.
302
- *
303
- * @protected
304
- * @param {?string} namespace - Namespace of the process
305
- * @returns {?string}
306
- */
307
- normalizeNamespace(namespace) {
308
- // The pattern in https://github.com/Open-EO/openeo-api/pull/348 doesn't include the double colon yet - the regexp may change in the future
309
- const matches = namespace.match( /^https?:\/\/.*\/processes\/(@?[\w\-.~:]+)\/?/i);
310
- return matches && matches.length > 1 ? matches[1] : namespace;
311
- }
312
-
313
- /**
314
- * List processes available on the back-end.
315
- *
316
- * Requests pre-defined processes by default.
317
- * Set the namespace parameter to request processes from a specific namespace.
318
- *
319
- * Note: The list of namespaces can be retrieved by calling `listProcesses` without a namespace given.
320
- * The namespaces are then listed in the property `namespaces`.
321
- *
322
- * @async
323
- * @param {?string} [namespace=null] - Namespace of the processes (default to `null`, i.e. pre-defined processes). EXPERIMENTAL!
324
- * @returns {Promise<Processes>} - A response compatible to the API specification.
325
- * @throws {Error}
326
- */
327
- async listProcesses(namespace = null) {
328
- if (!namespace) {
329
- namespace = 'backend';
330
- }
331
- let path = (namespace === 'backend') ? '/processes' : `/processes/${this.normalizeNamespace(namespace)}`;
332
- let response = await this._get(path);
333
-
334
- if (!Utils.isObject(response.data) || !Array.isArray(response.data.processes)) {
335
- throw new Error('Invalid response received for processes');
336
- }
337
-
338
- // Store processes in cache
339
- this.processes.remove(null, namespace);
340
- this.processes.addAll(response.data.processes, namespace);
341
-
342
- return Object.assign(response.data, {processes: this.processes.namespace(namespace)});
343
- }
344
-
345
- /**
346
- * Get information about a single process.
347
- *
348
- * @async
349
- * @param {string} processId - Collection ID to request further metadata for.
350
- * @param {?string} [namespace=null] - Namespace of the process (default to `null`, i.e. pre-defined processes). EXPERIMENTAL!
351
- * @returns {Promise<?Process>} - A single process as object, or `null` if none is found.
352
- * @throws {Error}
353
- * @see Connection#listProcesses
354
- */
355
- async describeProcess(processId, namespace = null) {
356
- if (!namespace) {
357
- namespace = 'backend';
358
- }
359
- if (namespace === 'backend') {
360
- await this.listProcesses();
361
- }
362
- else {
363
- let response = await this._get(`/processes/${this.normalizeNamespace(namespace)}/${processId}`);
364
- if (!Utils.isObject(response.data) || typeof response.data.id !== 'string') {
365
- throw new Error('Invalid response received for process');
366
- }
367
- this.processes.add(response.data, namespace);
368
- }
369
- return this.processes.get(processId, namespace);
370
- }
371
-
372
- /**
373
- * Returns an object to simply build user-defined processes based upon pre-defined processes.
374
- *
375
- * @async
376
- * @param {string} id - A name for the process.
377
- * @returns {Promise<Builder>}
378
- * @throws {Error}
379
- * @see Connection#listProcesses
380
- */
381
- async buildProcess(id) {
382
- await this.listProcesses();
383
- return new Builder(this.processes, null, id);
384
- }
385
-
386
- /**
387
- * List all authentication methods supported by the back-end.
388
- *
389
- * @async
390
- * @returns {Promise<Array.<AuthProvider>>} An array containing all supported AuthProviders (including all OIDC providers and HTTP Basic).
391
- * @throws {Error}
392
- * @see AuthProvider
393
- */
394
- async listAuthProviders() {
395
- if (this.authProviderList !== null) {
396
- return this.authProviderList;
397
- }
398
-
399
- this.authProviderList = [];
400
- let cap = this.capabilities();
401
-
402
- // Add OIDC providers
403
- if (cap.hasFeature('authenticateOIDC')) {
404
- let res = await this._get('/credentials/oidc');
405
- let oidcFactory = this.getOidcProviderFactory();
406
- if (Utils.isObject(res.data) && Array.isArray(res.data.providers) && typeof oidcFactory === 'function') {
407
- for(let i in res.data.providers) {
408
- let obj = oidcFactory(res.data.providers[i]);
409
- if (obj instanceof AuthProvider) {
410
- this.authProviderList.push(obj);
411
- }
412
- }
413
- }
414
- }
415
-
416
- // Add Basic provider
417
- if (cap.hasFeature('authenticateBasic')) {
418
- this.authProviderList.push(new BasicProvider(this));
419
- }
420
-
421
- return this.authProviderList;
422
- }
423
-
424
- /**
425
- * This function is meant to create the OIDC providers used for authentication.
426
- *
427
- * The function gets passed a single argument that contains the
428
- * provider information as provided by the API, e.g. having the properties
429
- * `id`, `issuer`, `title` etc.
430
- *
431
- * The function must return an instance of AuthProvider or any derived class.
432
- * May return `null` if the instance can't be created.
433
- *
434
- * @callback oidcProviderFactoryFunction
435
- * @param {object.<string, *>} providerInfo - The provider information as provided by the API, having the properties `id`, `issuer`, `title` etc.
436
- * @returns {AuthProvider | null}
437
- */
438
-
439
- /**
440
- * Sets a factory function that creates custom OpenID Connect provider instances.
441
- *
442
- * You only need to call this if you have implemented a new AuthProvider based
443
- * on the AuthProvider interface (or OIDCProvider class), e.g. to use a
444
- * OIDC library other than oidc-client-js.
445
- *
446
- * @param {?oidcProviderFactoryFunction} [providerFactoryFunc=null]
447
- * @see AuthProvider
448
- */
449
- setOidcProviderFactory(providerFactoryFunc) {
450
- this.oidcProviderFactory = providerFactoryFunc;
451
- }
452
-
453
- /**
454
- * Get the OpenID Connect provider factory.
455
- *
456
- * Returns `null` if OIDC is not supported by the client or an instance
457
- * can't be created for whatever reason.
458
- *
459
- * @returns {oidcProviderFactoryFunction | null}
460
- * @see AuthProvider
461
- */
462
- getOidcProviderFactory() {
463
- if (typeof this.oidcProviderFactory === 'function') {
464
- return this.oidcProviderFactory;
465
- }
466
- else {
467
- if (OidcProvider.isSupported()) {
468
- return providerInfo => new OidcProvider(this, providerInfo);
469
- }
470
- else {
471
- return null;
472
- }
473
- }
474
- }
475
-
476
- /**
477
- * Authenticates with username and password against a back-end supporting HTTP Basic Authentication.
478
- *
479
- * DEPRECATED in favor of using `listAuthProviders` and `BasicProvider`.
480
- *
481
- * @async
482
- * @deprecated
483
- * @param {string} username
484
- * @param {string} password
485
- * @see BasicProvider
486
- * @see Connection#listAuthProviders
487
- */
488
- async authenticateBasic(username, password) {
489
- let basic = new BasicProvider(this);
490
- await basic.login(username, password);
491
- }
492
-
493
- /**
494
- * Returns whether the user is authenticated (logged in) at the back-end or not.
495
- *
496
- * @returns {boolean} `true` if authenticated, `false` if not.
497
- */
498
- isAuthenticated() {
499
- return (this.authProvider !== null);
500
- }
501
-
502
- /**
503
- * Emits the given event.
504
- *
505
- * @protected
506
- * @param {string} event
507
- * @param {...*} args
508
- */
509
- emit(event, ...args) {
510
- if (typeof this.listeners[event] === 'function') {
511
- this.listeners[event](...args);
512
- }
513
- }
514
-
515
- /**
516
- * Registers a listener with the given event.
517
- *
518
- * Currently supported:
519
- * - authProviderChanged(provider): Raised when the auth provider has changed.
520
- * - tokenChanged(token): Raised when the access token has changed.
521
- * - processesChanged(type, data, namespace): Raised when the process registry has changed (i.e. a process was added, updated or deleted).
522
- *
523
- * @param {string} event
524
- * @param {Function} callback
525
- */
526
- on(event, callback) {
527
- this.listeners[event] = callback;
528
- }
529
-
530
- /**
531
- * Removes a listener from the given event.
532
- *
533
- * @param {string} event
534
- */
535
- off(event) {
536
- delete this.listeners[event];
537
- }
538
-
539
- /**
540
- * Returns the AuthProvider.
541
- *
542
- * @returns {AuthProvider | null}
543
- */
544
- getAuthProvider() {
545
- return this.authProvider;
546
- }
547
-
548
- /**
549
- * Sets the AuthProvider.
550
- *
551
- * @param {AuthProvider} provider
552
- */
553
- setAuthProvider(provider) {
554
- if (provider === this.authProvider) {
555
- return;
556
- }
557
- if (provider instanceof AuthProvider) {
558
- this.authProvider = provider;
559
- }
560
- else {
561
- this.authProvider = null;
562
- }
563
- this.emit('authProviderChanged', this.authProvider);
564
- // Update process cache on auth changes: https://github.com/Open-EO/openeo-js-client/issues/55
565
- this.refreshProcessCache();
566
- }
567
-
568
- /**
569
- * Sets the authentication token for the connection.
570
- *
571
- * This creates a new custom `AuthProvider` with the given details and returns it.
572
- * After calling this function you can make requests against the API.
573
- *
574
- * This is NOT recommended to use. Only use if you know what you are doing.
575
- * It is recommended to authenticate through `listAuthProviders` or related functions.
576
- *
577
- * @param {string} type - The authentication type, e.g. `basic` or `oidc`.
578
- * @param {string} providerId - The provider identifier. For OIDC the `id` of the provider.
579
- * @param {string} token - The actual access token as given by the authentication method during the login process.
580
- * @returns {AuthProvider}
581
- */
582
- setAuthToken(type, providerId, token) {
583
- let provider = new AuthProvider(type, this, {
584
- id: providerId,
585
- title: "Custom",
586
- description: ""
587
- });
588
- provider.setToken(token);
589
- this.setAuthProvider(provider);
590
- return provider;
591
- }
592
-
593
- /**
594
- * Get information about the authenticated user.
595
- *
596
- * Updates the User ID if available.
597
- *
598
- * @async
599
- * @returns {Promise<UserAccount>} A response compatible to the API specification.
600
- * @throws {Error}
601
- */
602
- async describeAccount() {
603
- let response = await this._get('/me');
604
- return response.data;
605
- }
606
-
607
- /**
608
- * Lists all files from the user workspace.
609
- *
610
- * @async
611
- * @returns {Promise<ResponseArray.<UserFile>>} A list of files.
612
- * @throws {Error}
613
- */
614
- async listFiles() {
615
- let response = await this._get('/files');
616
- let files = response.data.files.map(
617
- f => new UserFile(this, f.path).setAll(f)
618
- );
619
- return this._toResponseArray(files, response.data);
620
- }
621
-
622
- /**
623
- * A callback that is executed on upload progress updates.
624
- *
625
- * @callback uploadStatusCallback
626
- * @param {number} percentCompleted - The percent (0-100) completed.
627
- * @param {UserFile} file - The file object corresponding to the callback.
628
- */
629
-
630
- /**
631
- * Uploads a file to the user workspace.
632
- * If a file with the name exists, overwrites it.
633
- *
634
- * This method has different behaviour depending on the environment.
635
- * In a nodeJS environment the source must be a path to a file as string.
636
- * In a browser environment the source must be an object from a file upload form.
637
- *
638
- * @async
639
- * @param {*} source - The source, see method description for details.
640
- * @param {?string} [targetPath=null] - The target path on the server, relative to the user workspace. Defaults to the file name of the source file.
641
- * @param {?uploadStatusCallback} [statusCallback=null] - Optionally, a callback that is executed on upload progress updates.
642
- * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the upload process.
643
- * @returns {Promise<UserFile>}
644
- * @throws {Error}
645
- */
646
- async uploadFile(source, targetPath = null, statusCallback = null, abortController = null) {
647
- if (targetPath === null) {
648
- targetPath = Environment.fileNameForUpload(source);
649
- }
650
- let file = await this.getFile(targetPath);
651
- return await file.uploadFile(source, statusCallback, abortController);
652
- }
653
-
654
- /**
655
- * Opens a (existing or non-existing) file without reading any information or creating a new file at the back-end.
656
- *
657
- * @async
658
- * @param {string} path - Path to the file, relative to the user workspace.
659
- * @returns {Promise<UserFile>} A file.
660
- * @throws {Error}
661
- */
662
- async getFile(path) {
663
- return new UserFile(this, path);
664
- }
665
-
666
- /**
667
- * Takes a UserProcess, BuilderNode or a plain object containing process nodes
668
- * and converts it to an API compliant object.
669
- *
670
- * @param {UserProcess|BuilderNode|object.<string, *>} process - Process to be normalized.
671
- * @param {object.<string, *>} additional - Additional properties to be merged with the resulting object.
672
- * @returns {object.<string, *>}
673
- * @protected
674
- */
675
- _normalizeUserProcess(process, additional = {}) {
676
- if (process instanceof UserProcess) {
677
- process = process.toJSON();
678
- }
679
- else if (process instanceof BuilderNode) {
680
- process.result = true;
681
- process = process.parent.toJSON();
682
- }
683
- else if (Utils.isObject(process) && !Utils.isObject(process.process_graph)) {
684
- process = {
685
- process_graph: process
686
- };
687
- }
688
- return Object.assign({}, additional, {process: process});
689
- }
690
-
691
- /**
692
- * Validates a user-defined process at the back-end.
693
- *
694
- * @async
695
- * @param {Process} process - User-defined process to validate.
696
- * @returns {Promise<Array.<ApiError>>} errors - A list of API compatible error objects. A valid process returns an empty list.
697
- * @throws {Error}
698
- */
699
- async validateProcess(process) {
700
- let response = await this._post('/validation', this._normalizeUserProcess(process).process);
701
- if (Array.isArray(response.data.errors)) {
702
- return response.data.errors;
703
- }
704
- else {
705
- throw new Error("Invalid validation response received.");
706
- }
707
- }
708
-
709
- /**
710
- * Lists all user-defined processes of the authenticated user.
711
- *
712
- * @async
713
- * @param {Array.<UserProcess>} [oldProcesses=[]] - A list of existing user-defined processes to update.
714
- * @returns {Promise<ResponseArray.<UserProcess>>} A list of user-defined processes.
715
- * @throws {Error}
716
- */
717
- async listUserProcesses(oldProcesses = []) {
718
- let response = await this._get('/process_graphs');
719
-
720
- if (!Utils.isObject(response.data) || !Array.isArray(response.data.processes)) {
721
- throw new Error('Invalid response received for processes');
722
- }
723
-
724
- // Remove existing processes from cache
725
- this.processes.remove(null, 'user');
726
-
727
- // Update existing processes if needed
728
- let newProcesses = response.data.processes.map(newProcess => {
729
- let process = oldProcesses.find(oldProcess => oldProcess.id === newProcess.id);
730
- if (!process) {
731
- process = new UserProcess(this, newProcess.id);
732
- }
733
- return process.setAll(newProcess);
734
- });
735
-
736
- // Store plain JS variant (i.e. no Job objects involved) of processes in cache
737
- let jsonProcesses = oldProcesses.length > 0 ? newProcesses.map(p => p.toJSON()) : response.data.processes;
738
- this.processes.addAll(jsonProcesses, 'user');
739
-
740
- return this._toResponseArray(newProcesses, response.data);
741
- }
742
-
743
- /**
744
- * Creates a new stored user-defined process at the back-end.
745
- *
746
- * @async
747
- * @param {string} id - Unique identifier for the process.
748
- * @param {Process} process - A user-defined process.
749
- * @returns {Promise<UserProcess>} The new user-defined process.
750
- * @throws {Error}
751
- */
752
- async setUserProcess(id, process) {
753
- let pg = new UserProcess(this, id);
754
- return await pg.replaceUserProcess(process);
755
- }
756
-
757
- /**
758
- * Get all information about a user-defined process.
759
- *
760
- * @async
761
- * @param {string} id - Identifier of the user-defined process.
762
- * @returns {Promise<UserProcess>} The user-defined process.
763
- * @throws {Error}
764
- */
765
- async getUserProcess(id) {
766
- let pg = new UserProcess(this, id);
767
- return await pg.describeUserProcess();
768
- }
769
-
770
- /**
771
- * Executes a process synchronously and returns the result as the response.
772
- *
773
- * Please note that requests can take a very long time of several minutes or even hours.
774
- *
775
- * @async
776
- * @param {Process} process - A user-defined process.
777
- * @param {?string} [plan=null] - The billing plan to use for this computation.
778
- * @param {?number} [budget=null] - The maximum budget allowed to spend for this computation.
779
- * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the processing request.
780
- * @returns {Promise<SyncResult>} - An object with the data and some metadata.
781
- */
782
- async computeResult(process, plan = null, budget = null, abortController = null) {
783
- let requestBody = this._normalizeUserProcess(
784
- process,
785
- {
786
- plan: plan,
787
- budget: budget
788
- }
789
- );
790
- let response = await this._post('/result', requestBody, Environment.getResponseType(), abortController);
791
- let syncResult = {
792
- data: response.data,
793
- costs: null,
794
- type: null,
795
- logs: []
796
- };
797
-
798
- if (typeof response.headers['openeo-costs'] === 'number') {
799
- syncResult.costs = response.headers['openeo-costs'];
800
- }
801
-
802
- if (typeof response.headers['content-type'] === 'string') {
803
- syncResult.type = response.headers['content-type'];
804
- }
805
-
806
- let links = Array.isArray(response.headers.link) ? response.headers.link : [response.headers.link];
807
- for(let link of links) {
808
- if (typeof link !== 'string') {
809
- continue;
810
- }
811
- let logs = link.match(/^<([^>]+)>;\s?rel="monitor"/i);
812
- if (Array.isArray(logs) && logs.length > 1) {
813
- try {
814
- let logsResponse = await this._get(logs[1]);
815
- if (Utils.isObject(logsResponse.data) && Array.isArray(logsResponse.data.logs)) {
816
- syncResult.logs = logsResponse.data.logs;
817
- }
818
- } catch(error) {
819
- console.warn(error);
820
- }
821
- }
822
- }
823
-
824
- return syncResult;
825
- }
826
-
827
- /**
828
- * Executes a process synchronously and downloads to result the given path.
829
- *
830
- * Please note that requests can take a very long time of several minutes or even hours.
831
- *
832
- * This method has different behaviour depending on the environment.
833
- * If a NodeJs environment, writes the downloaded file to the target location on the file system.
834
- * In a browser environment, offers the file for downloading using the specified name (folders are not supported).
835
- *
836
- * @async
837
- * @param {Process} process - A user-defined process.
838
- * @param {string} targetPath - The target, see method description for details.
839
- * @param {?string} [plan=null] - The billing plan to use for this computation.
840
- * @param {?number} [budget=null] - The maximum budget allowed to spend for this computation.
841
- * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the processing request.
842
- * @throws {Error}
843
- */
844
- async downloadResult(process, targetPath, plan = null, budget = null, abortController = null) {
845
- let response = await this.computeResult(process, plan, budget, abortController);
846
- // @ts-ignore
847
- await Environment.saveToFile(response.data, targetPath);
848
- }
849
-
850
- /**
851
- * Lists all batch jobs of the authenticated user.
852
- *
853
- * @async
854
- * @param {Array.<Job>} [oldJobs=[]] - A list of existing jobs to update.
855
- * @returns {Promise<ResponseArray.<Job>>} A list of jobs.
856
- * @throws {Error}
857
- */
858
- async listJobs(oldJobs = []) {
859
- let response = await this._get('/jobs');
860
- let newJobs = response.data.jobs.map(newJob => {
861
- delete newJob.status;
862
- let job = oldJobs.find(oldJob => oldJob.id === newJob.id);
863
- if (!job) {
864
- job = new Job(this, newJob.id);
865
- }
866
- return job.setAll(newJob);
867
- });
868
- return this._toResponseArray(newJobs, response.data);
869
- }
870
-
871
- /**
872
- * Creates a new batch job at the back-end.
873
- *
874
- * @async
875
- * @param {Process} process - A user-define process to execute.
876
- * @param {?string} [title=null] - A title for the batch job.
877
- * @param {?string} [description=null] - A description for the batch job.
878
- * @param {?string} [plan=null] - The billing plan to use for this batch job.
879
- * @param {?number} [budget=null] - The maximum budget allowed to spend for this batch job.
880
- * @param {object.<string, *>} [additional={}] - Proprietary parameters to pass for the batch job.
881
- * @returns {Promise<Job>} The stored batch job.
882
- * @throws {Error}
883
- */
884
- async createJob(process, title = null, description = null, plan = null, budget = null, additional = {}) {
885
- additional = Object.assign({}, additional, {
886
- title: title,
887
- description: description,
888
- plan: plan,
889
- budget: budget
890
- });
891
- let requestBody = this._normalizeUserProcess(process, additional);
892
- let response = await this._post('/jobs', requestBody);
893
- if (typeof response.headers['openeo-identifier'] !== 'string') {
894
- throw new Error("Response did not contain a Job ID. Job has likely been created, but may not show up yet.");
895
- }
896
- let job = new Job(this, response.headers['openeo-identifier']).setAll(requestBody);
897
- if (this.capabilities().hasFeature('describeJob')) {
898
- return await job.describeJob();
899
- }
900
- else {
901
- return job;
902
- }
903
- }
904
-
905
- /**
906
- * Get all information about a batch job.
907
- *
908
- * @async
909
- * @param {string} id - Batch Job ID.
910
- * @returns {Promise<Job>} The batch job.
911
- * @throws {Error}
912
- */
913
- async getJob(id) {
914
- let job = new Job(this, id);
915
- return await job.describeJob();
916
- }
917
-
918
- /**
919
- * Lists all secondary web services of the authenticated user.
920
- *
921
- * @async
922
- * @param {Array.<Service>} [oldServices=[]] - A list of existing services to update.
923
- * @returns {Promise<ResponseArray.<Job>>} A list of services.
924
- * @throws {Error}
925
- */
926
- async listServices(oldServices = []) {
927
- let response = await this._get('/services');
928
- let newServices = response.data.services.map(newService => {
929
- let service = oldServices.find(oldService => oldService.id === newService.id);
930
- if (!service) {
931
- service = new Service(this, newService.id);
932
- }
933
- return service.setAll(newService);
934
- });
935
- return this._toResponseArray(newServices, response.data);
936
- }
937
-
938
- /**
939
- * Creates a new secondary web service at the back-end.
940
- *
941
- * @async
942
- * @param {Process} process - A user-defined process.
943
- * @param {string} type - The type of service to be created (see `Connection.listServiceTypes()`).
944
- * @param {?string} [title=null] - A title for the service.
945
- * @param {?string} [description=null] - A description for the service.
946
- * @param {boolean} [enabled=true] - Enable the service (`true`, default) or not (`false`).
947
- * @param {object.<string, *>} [configuration={}] - Configuration parameters to pass to the service.
948
- * @param {?string} [plan=null] - The billing plan to use for this service.
949
- * @param {?number} [budget=null] - The maximum budget allowed to spend for this service.
950
- * @param {object.<string, *>} [additional={}] - Proprietary parameters to pass for the batch job.
951
- * @returns {Promise<Service>} The stored service.
952
- * @throws {Error}
953
- */
954
- async createService(process, type, title = null, description = null, enabled = true, configuration = {}, plan = null, budget = null, additional = {}) {
955
- let requestBody = this._normalizeUserProcess(process, Object.assign({
956
- title: title,
957
- description: description,
958
- type: type,
959
- enabled: enabled,
960
- configuration: configuration,
961
- plan: plan,
962
- budget: budget
963
- }, additional));
964
- let response = await this._post('/services', requestBody);
965
- if (typeof response.headers['openeo-identifier'] !== 'string') {
966
- throw new Error("Response did not contain a Service ID. Service has likely been created, but may not show up yet.");
967
- }
968
- let service = new Service(this, response.headers['openeo-identifier']).setAll(requestBody);
969
- if (this.capabilities().hasFeature('describeService')) {
970
- return service.describeService();
971
- }
972
- else {
973
- return service;
974
- }
975
- }
976
-
977
- /**
978
- * Get all information about a secondary web service.
979
- *
980
- * @async
981
- * @param {string} id - Service ID.
982
- * @returns {Promise<Service>} The service.
983
- * @throws {Error}
984
- */
985
- async getService(id) {
986
- let service = new Service(this, id);
987
- return await service.describeService();
988
- }
989
-
990
- /**
991
- * Adds additional response details to the array.
992
- *
993
- * Adds links and federation:missing.
994
- *
995
- * @protected
996
- * @param {Array.<*>} arr
997
- * @param {object.<string, *>} response
998
- * @returns {ResponseArray}
999
- */
1000
- _toResponseArray(arr, response) {
1001
- arr.links = Array.isArray(response.links) ? response.links : [];
1002
- arr['federation:missing'] = Array.isArray(response['federation:missing']) ? response['federation:missing'] : [];
1003
- return arr;
1004
- }
1005
-
1006
- /**
1007
- * Get the a link with the given rel type.
1008
- *
1009
- * @protected
1010
- * @param {Array.<Link>} links - An array of links.
1011
- * @param {string} rel - Relation type to find, defaults to `next`.
1012
- * @returns {string | null}
1013
- * @throws {Error}
1014
- */
1015
- _getLinkHref(links, rel = 'next') {
1016
- if (Array.isArray(links)) {
1017
- let nextLink = links.find(link => Utils.isObject(link) && link.rel === rel && typeof link.href === 'string');
1018
- if (nextLink) {
1019
- return nextLink.href;
1020
- }
1021
- }
1022
- return null;
1023
- }
1024
-
1025
- /**
1026
- * Sends a GET request.
1027
- *
1028
- * @protected
1029
- * @async
1030
- * @param {string} path
1031
- * @param {object.<string, *>} query
1032
- * @param {string} responseType - Response type according to axios, defaults to `json`.
1033
- * @returns {Promise<AxiosResponse>}
1034
- * @throws {Error}
1035
- * @see https://github.com/axios/axios#request-config
1036
- */
1037
- async _get(path, query, responseType) {
1038
- return await this._send({
1039
- method: 'get',
1040
- responseType: responseType,
1041
- url: path,
1042
- // Timeout for capabilities requests as they are used for a quick first discovery to check whether the server is a openEO back-end.
1043
- // Without timeout connecting with a wrong server url may take forever.
1044
- timeout: path === '/' ? 5000 : 0,
1045
- params: query
1046
- });
1047
- }
1048
-
1049
- /**
1050
- * Sends a POST request.
1051
- *
1052
- * @protected
1053
- * @async
1054
- * @param {string} path
1055
- * @param {*} body
1056
- * @param {string} responseType - Response type according to axios, defaults to `json`.
1057
- * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the request.
1058
- * @returns {Promise<AxiosResponse>}
1059
- * @throws {Error}
1060
- * @see https://github.com/axios/axios#request-config
1061
- */
1062
- async _post(path, body, responseType, abortController = null) {
1063
- let options = {
1064
- method: 'post',
1065
- responseType: responseType,
1066
- url: path,
1067
- data: body
1068
- };
1069
- return await this._send(options, abortController);
1070
- }
1071
-
1072
- /**
1073
- * Sends a PUT request.
1074
- *
1075
- * @protected
1076
- * @async
1077
- * @param {string} path
1078
- * @param {*} body
1079
- * @returns {Promise<AxiosResponse>}
1080
- * @throws {Error}
1081
- */
1082
- async _put(path, body) {
1083
- return await this._send({
1084
- method: 'put',
1085
- url: path,
1086
- data: body
1087
- });
1088
- }
1089
-
1090
- /**
1091
- * Sends a PATCH request.
1092
- *
1093
- * @protected
1094
- * @async
1095
- * @param {string} path
1096
- * @param {*} body
1097
- * @returns {Promise<AxiosResponse>}
1098
- * @throws {Error}
1099
- */
1100
- async _patch(path, body) {
1101
- return await this._send({
1102
- method: 'patch',
1103
- url: path,
1104
- data: body
1105
- });
1106
- }
1107
-
1108
- /**
1109
- * Sends a DELETE request.
1110
- *
1111
- * @protected
1112
- * @async
1113
- * @param {string} path
1114
- * @returns {Promise<AxiosResponse>}
1115
- * @throws {Error}
1116
- */
1117
- async _delete(path) {
1118
- return await this._send({
1119
- method: 'delete',
1120
- url: path
1121
- });
1122
- }
1123
-
1124
- /**
1125
- * Downloads data from a URL.
1126
- *
1127
- * May include authorization details where required.
1128
- *
1129
- * @param {string} url - An absolute or relative URL to download data from.
1130
- * @param {boolean} authorize - Send authorization details (`true`) or not (`false`).
1131
- * @returns {Promise<Stream.Readable|Blob>} - Returns the data as `Stream` in NodeJS environments or as `Blob` in browsers
1132
- * @throws {Error}
1133
- */
1134
- async download(url, authorize) {
1135
- let result = await this._send({
1136
- method: 'get',
1137
- responseType: Environment.getResponseType(),
1138
- url: url,
1139
- authorization: authorize
1140
- });
1141
- return result.data;
1142
- }
1143
-
1144
- /**
1145
- * Sends a HTTP request.
1146
- *
1147
- * Options mostly conform to axios,
1148
- * see {@link https://github.com/axios/axios#request-config}.
1149
- *
1150
- * Automatically sets a baseUrl and the authorization information.
1151
- * Default responseType is `json`.
1152
- *
1153
- * Tries to smoothly handle error responses by providing an object for all response types,
1154
- * instead of Streams or Blobs for non-JSON response types.
1155
- *
1156
- * @protected
1157
- * @async
1158
- * @param {object.<string, *>} options
1159
- * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the request.
1160
- * @returns {Promise<AxiosResponse>}
1161
- * @throws {Error}
1162
- * @see https://github.com/axios/axios
1163
- */
1164
- async _send(options, abortController = null) {
1165
- options.baseURL = this.baseUrl;
1166
- if (this.isAuthenticated() && (typeof options.authorization === 'undefined' || options.authorization === true)) {
1167
- if (!options.headers) {
1168
- options.headers = {};
1169
- }
1170
- options.headers.Authorization = 'Bearer ' + this.authProvider.getToken();
1171
- }
1172
- if (!options.responseType) {
1173
- options.responseType = 'json';
1174
- }
1175
- if (abortController) {
1176
- options.signal = abortController.signal;
1177
- }
1178
-
1179
- try {
1180
- return await axios(options);
1181
- } catch(error) {
1182
- const checkContentType = type => (typeof type === 'string' && type.indexOf('/json') !== -1);
1183
- const enrichError = (origin, response) => {
1184
- if (typeof response.message === 'string') {
1185
- origin.message = response.message;
1186
- }
1187
- origin.code = typeof response.code === 'string' ? response.code : "";
1188
- origin.id = response.id;
1189
- origin.links = Array.isArray(response.links) ? response.links : [];
1190
- return origin;
1191
- };
1192
- if (Utils.isObject(error.response) && Utils.isObject(error.response.data) && (checkContentType(error.response.data.type) || (Utils.isObject(error.response.headers) && checkContentType(error.response.headers['content-type'])))) {
1193
- // JSON error responses are Blobs and streams if responseType is set as such, so convert to JSON if required.
1194
- // See: https://github.com/axios/axios/issues/815
1195
- if (options.responseType === Environment.getResponseType()) {
1196
- try {
1197
- let errorResponse = await Environment.handleErrorResponse(error);
1198
- throw enrichError(error, errorResponse);
1199
- } catch (error2) {
1200
- console.error(error2);
1201
- }
1202
- }
1203
- else {
1204
- throw enrichError(error, error.response.data);
1205
- }
1206
- }
1207
- throw error;
1208
- }
1209
- }
1210
- }
1211
-
1212
- module.exports = Connection;
1
+ const Environment = require('./env');
2
+ const Utils = require('@openeo/js-commons/src/utils');
3
+ const ProcessRegistry = require('@openeo/js-commons/src/processRegistry');
4
+ const axios = require('axios');
5
+ const StacMigrate = require('@radiantearth/stac-migrate');
6
+
7
+ const AuthProvider = require('./authprovider');
8
+ const BasicProvider = require('./basicprovider');
9
+ const OidcProvider = require('./oidcprovider');
10
+
11
+ const Capabilities = require('./capabilities');
12
+ const FileTypes = require('./filetypes');
13
+ const UserFile = require('./userfile');
14
+ const Job = require('./job');
15
+ const UserProcess = require('./userprocess');
16
+ const Service = require('./service');
17
+
18
+ const Builder = require('./builder/builder');
19
+ const BuilderNode = require('./builder/node');
20
+
21
+ const CONFORMANCE_RELS = [
22
+ 'conformance',
23
+ 'http://www.opengis.net/def/rel/ogc/1.0/conformance'
24
+ ];
25
+
26
+ /**
27
+ * A connection to a back-end.
28
+ */
29
+ class Connection {
30
+
31
+ /**
32
+ * Creates a new Connection.
33
+ *
34
+ * @param {string} baseUrl - The versioned URL or the back-end instance.
35
+ * @param {Options} [options={}] - Additional options for the connection.
36
+ * @param {?string} [url=null] - User-provided URL of the backend connected to.
37
+ */
38
+ constructor(baseUrl, options = {}, url = null) {
39
+ /**
40
+ * User-provided URL of the backend connected to.
41
+ *
42
+ * `null` if not given and the connection was directly made to a versioned instance of the back-end.
43
+ *
44
+ * @protected
45
+ * @type {string | null}
46
+ */
47
+ this.url = url;
48
+ /**
49
+ * The versioned URL or the back-end instance.
50
+ *
51
+ * @protected
52
+ * @type {string}
53
+ */
54
+ this.baseUrl = Utils.normalizeUrl(baseUrl);
55
+ /**
56
+ * Auth Provider cache
57
+ *
58
+ * @protected
59
+ * @type {Array.<AuthProvider> | null}
60
+ */
61
+ this.authProviderList = null;
62
+ /**
63
+ * Current auth provider
64
+ *
65
+ * @protected
66
+ * @type {AuthProvider | null}
67
+ */
68
+ this.authProvider = null;
69
+ /**
70
+ * Capability cache
71
+ *
72
+ * @protected
73
+ * @type {Capabilities | null}
74
+ */
75
+ this.capabilitiesObject = null;
76
+ /**
77
+ * Listeners for events.
78
+ *
79
+ * @protected
80
+ * @type {object.<string|Function>}
81
+ */
82
+ this.listeners = {};
83
+ /**
84
+ * Additional options for the connection.
85
+ *
86
+ * @protected
87
+ * @type {Options}
88
+ */
89
+ this.options = options;
90
+ /**
91
+ * Process cache
92
+ *
93
+ * @protected
94
+ * @type {ProcessRegistry}
95
+ */
96
+ this.processes = new ProcessRegistry([], Boolean(options.addNamespaceToProcess));
97
+ this.processes.listeners.push((...args) => this.emit('processesChanged', ...args));
98
+ }
99
+
100
+ /**
101
+ * Initializes the connection by requesting the capabilities.
102
+ *
103
+ * @async
104
+ * @protected
105
+ * @returns {Promise<Capabilities>} Capabilities
106
+ * @throws {Error}
107
+ */
108
+ async init() {
109
+ let response = await this._get('/');
110
+ let data = Object.assign({}, response.data);
111
+ data.links = this.makeLinksAbsolute(data.links, response);
112
+
113
+ if (!Array.isArray(data.conformsTo) && Array.isArray(data.links)) {
114
+ let conformanceLink = this._getLinkHref(data.links, CONFORMANCE_RELS);
115
+ if (conformanceLink) {
116
+ let response2 = await this._get(conformanceLink);
117
+ if (Utils.isObject(response2.data) && Array.isArray(response2.data.conformsTo)) {
118
+ data.conformsTo = response2.data.conformsTo;
119
+ }
120
+ }
121
+ }
122
+
123
+ this.capabilitiesObject = new Capabilities(data);
124
+ return this.capabilitiesObject;
125
+ }
126
+
127
+ /**
128
+ * Refresh the cache for processes.
129
+ *
130
+ * @async
131
+ * @protected
132
+ * @returns {Promise}
133
+ */
134
+ async refreshProcessCache() {
135
+ if (this.processes.count() === 0) {
136
+ return;
137
+ }
138
+ let promises = this.processes.namespaces().map(namespace => {
139
+ let fn = () => Promise.resolve();
140
+ if (namespace === 'user') {
141
+ let userProcesses = this.processes.namespace('user');
142
+ if (!this.isAuthenticated()) {
143
+ fn = () => (this.processes.remove(null, 'user') ? Promise.resolve() : Promise.reject(new Error("Can't clear user processes")));
144
+ }
145
+ else if (this.capabilities().hasFeature('listUserProcesses')) {
146
+ fn = () => this.listUserProcesses(userProcesses);
147
+ }
148
+ }
149
+ else if (this.capabilities().hasFeature('listProcesses')) {
150
+ fn = () => this.listProcesses(namespace);
151
+ }
152
+ return fn().catch(error => console.warn(`Could not update processes for namespace '${namespace}' due to an error: ${error.message}`));
153
+ });
154
+ return await Promise.all(promises);
155
+ }
156
+
157
+ /**
158
+ * Returns the URL of the versioned back-end instance currently connected to.
159
+ *
160
+ * @returns {string} The versioned URL or the back-end instance.
161
+ */
162
+ getBaseUrl() {
163
+ return this.baseUrl;
164
+ }
165
+
166
+ /**
167
+ * Returns the user-provided URL of the back-end currently connected to.
168
+ *
169
+ * @returns {string} The URL or the back-end.
170
+ */
171
+ getUrl() {
172
+ return this.url || this.baseUrl;
173
+ }
174
+
175
+ /**
176
+ * Returns the capabilities of the back-end.
177
+ *
178
+ * @returns {Capabilities} Capabilities
179
+ */
180
+ capabilities() {
181
+ return this.capabilitiesObject;
182
+ }
183
+
184
+ /**
185
+ * List the supported output file formats.
186
+ *
187
+ * @async
188
+ * @returns {Promise<FileTypes>} A response compatible to the API specification.
189
+ * @throws {Error}
190
+ */
191
+ async listFileTypes() {
192
+ let response = await this._get('/file_formats');
193
+ return new FileTypes(response.data);
194
+ }
195
+
196
+ /**
197
+ * List the supported secondary service types.
198
+ *
199
+ * @async
200
+ * @returns {Promise<object.<string, ServiceType>>} A response compatible to the API specification.
201
+ * @throws {Error}
202
+ */
203
+ async listServiceTypes() {
204
+ let response = await this._get('/service_types');
205
+ return response.data;
206
+ }
207
+
208
+ /**
209
+ * List the supported UDF runtimes.
210
+ *
211
+ * @async
212
+ * @returns {Promise<object.<string, UdfRuntime>>} A response compatible to the API specification.
213
+ * @throws {Error}
214
+ */
215
+ async listUdfRuntimes() {
216
+ let response = await this._get('/udf_runtimes');
217
+ return response.data;
218
+ }
219
+
220
+ /**
221
+ * List all collections available on the back-end.
222
+ *
223
+ * The collections returned always comply to the latest STAC version (currently 1.0.0).
224
+ *
225
+ * @async
226
+ * @returns {Promise<Collections>} A response compatible to the API specification.
227
+ * @throws {Error}
228
+ */
229
+ async listCollections() {
230
+ let response = await this._get('/collections');
231
+ if (Utils.isObject(response.data) && Array.isArray(response.data.collections)) {
232
+ response.data.collections = response.data.collections.map(collection => {
233
+ if (collection.stac_version) {
234
+ return StacMigrate.collection(collection);
235
+ }
236
+ return collection;
237
+ });
238
+ }
239
+ return response.data;
240
+ }
241
+
242
+ /**
243
+ * Get further information about a single collection.
244
+ *
245
+ * The collection returned always complies to the latest STAC version (currently 1.0.0).
246
+ *
247
+ * @async
248
+ * @param {string} collectionId - Collection ID to request further metadata for.
249
+ * @returns {Promise<Collection>} - A response compatible to the API specification.
250
+ * @throws {Error}
251
+ */
252
+ async describeCollection(collectionId) {
253
+ let response = await this._get('/collections/' + collectionId);
254
+ if (response.data.stac_version) {
255
+ return StacMigrate.collection(response.data);
256
+ }
257
+ else {
258
+ return response.data;
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Loads items for a specific image collection.
264
+ * May not be available for all collections.
265
+ *
266
+ * The items returned always comply to the latest STAC version (currently 1.0.0).
267
+ *
268
+ * This is an experimental API and is subject to change.
269
+ *
270
+ * @async
271
+ * @param {string} collectionId - Collection ID to request items for.
272
+ * @param {?Array.<number>} [spatialExtent=null] - Limits the items to the given bounding box in WGS84:
273
+ * 1. Lower left corner, coordinate axis 1
274
+ * 2. Lower left corner, coordinate axis 2
275
+ * 3. Upper right corner, coordinate axis 1
276
+ * 4. Upper right corner, coordinate axis 2
277
+ * @param {?Array} [temporalExtent=null] - Limits the items to the specified temporal interval.
278
+ * The interval has to be specified as an array with exactly two elements (start, end) and
279
+ * each must be either an RFC 3339 compatible string or a Date object.
280
+ * Also supports open intervals by setting one of the boundaries to `null`, but never both.
281
+ * @param {?number} [limit=null] - The amount of items per request/page as integer. If `null` (default), the back-end decides.
282
+ * @yields {Promise<ItemCollection>} A response compatible to the API specification.
283
+ * @throws {Error}
284
+ */
285
+ async * listCollectionItems(collectionId, spatialExtent = null, temporalExtent = null, limit = null) {
286
+ let page = 1;
287
+ let nextUrl = '/collections/' + collectionId + '/items';
288
+ while(nextUrl) {
289
+ let params = {};
290
+ if (page === 1) {
291
+ if (Array.isArray(spatialExtent)) {
292
+ params.bbox = spatialExtent.join(',');
293
+ }
294
+ if (Array.isArray(temporalExtent)) {
295
+ params.datetime = temporalExtent
296
+ .map(e => {
297
+ if (e instanceof Date) {
298
+ return e.toISOString();
299
+ }
300
+ else if (typeof e === 'string') {
301
+ return e;
302
+ }
303
+ return '..'; // Open date range
304
+ })
305
+ .join('/');
306
+ }
307
+ if (limit > 0) {
308
+ params.limit = limit;
309
+ }
310
+ }
311
+
312
+ let response = await this._get(nextUrl, params);
313
+ if (Utils.isObject(response.data) && Array.isArray(response.data.features)) {
314
+ response.data.features = response.data.features.map(item => {
315
+ if (item.stac_version) {
316
+ return StacMigrate.item(item);
317
+ }
318
+ return item;
319
+ });
320
+ }
321
+ yield response.data;
322
+
323
+ page++;
324
+ let links = this.makeLinksAbsolute(response.data.links);
325
+ nextUrl = this._getLinkHref(links, 'next');
326
+ }
327
+ }
328
+
329
+ /**
330
+ * Normalisation of the namespace to a value that is compatible with the OpenEO specs - EXPERIMENTAL.
331
+ *
332
+ * This is required to support UDP that are shared as public. These can only be executed with providing the full URL
333
+ * (e.g. https://<backend>/processes/<namespace>/<process_id>) as the namespace value in the processing graph. For other
334
+ * parts of the API (such as the listing of the processes, only the name of the namespace is required.
335
+ *
336
+ * This function will extract the short name of the namespace from a shareable URL.
337
+ *
338
+ * @protected
339
+ * @param {?string} namespace - Namespace of the process
340
+ * @returns {?string}
341
+ */
342
+ normalizeNamespace(namespace) {
343
+ // The pattern in https://github.com/Open-EO/openeo-api/pull/348 doesn't include the double colon yet - the regexp may change in the future
344
+ const matches = namespace.match( /^https?:\/\/.*\/processes\/(@?[\w\-.~:]+)\/?/i);
345
+ return matches && matches.length > 1 ? matches[1] : namespace;
346
+ }
347
+
348
+ /**
349
+ * List processes available on the back-end.
350
+ *
351
+ * Requests pre-defined processes by default.
352
+ * Set the namespace parameter to request processes from a specific namespace.
353
+ *
354
+ * Note: The list of namespaces can be retrieved by calling `listProcesses` without a namespace given.
355
+ * The namespaces are then listed in the property `namespaces`.
356
+ *
357
+ * @async
358
+ * @param {?string} [namespace=null] - Namespace of the processes (default to `null`, i.e. pre-defined processes). EXPERIMENTAL!
359
+ * @returns {Promise<Processes>} - A response compatible to the API specification.
360
+ * @throws {Error}
361
+ */
362
+ async listProcesses(namespace = null) {
363
+ if (!namespace) {
364
+ namespace = 'backend';
365
+ }
366
+ let path = (namespace === 'backend') ? '/processes' : `/processes/${this.normalizeNamespace(namespace)}`;
367
+ let response = await this._get(path);
368
+
369
+ if (!Utils.isObject(response.data) || !Array.isArray(response.data.processes)) {
370
+ throw new Error('Invalid response received for processes');
371
+ }
372
+
373
+ // Store processes in cache
374
+ this.processes.remove(null, namespace);
375
+ this.processes.addAll(response.data.processes, namespace);
376
+
377
+ return Object.assign(response.data, {processes: this.processes.namespace(namespace)});
378
+ }
379
+
380
+ /**
381
+ * Get information about a single process.
382
+ *
383
+ * @async
384
+ * @param {string} processId - Collection ID to request further metadata for.
385
+ * @param {?string} [namespace=null] - Namespace of the process (default to `null`, i.e. pre-defined processes). EXPERIMENTAL!
386
+ * @returns {Promise<?Process>} - A single process as object, or `null` if none is found.
387
+ * @throws {Error}
388
+ * @see Connection#listProcesses
389
+ */
390
+ async describeProcess(processId, namespace = null) {
391
+ if (!namespace) {
392
+ namespace = 'backend';
393
+ }
394
+ if (namespace === 'backend') {
395
+ await this.listProcesses();
396
+ }
397
+ else {
398
+ let response = await this._get(`/processes/${this.normalizeNamespace(namespace)}/${processId}`);
399
+ if (!Utils.isObject(response.data) || typeof response.data.id !== 'string') {
400
+ throw new Error('Invalid response received for process');
401
+ }
402
+ this.processes.add(response.data, namespace);
403
+ }
404
+ return this.processes.get(processId, namespace);
405
+ }
406
+
407
+ /**
408
+ * Returns an object to simply build user-defined processes based upon pre-defined processes.
409
+ *
410
+ * @async
411
+ * @param {string} id - A name for the process.
412
+ * @returns {Promise<Builder>}
413
+ * @throws {Error}
414
+ * @see Connection#listProcesses
415
+ */
416
+ async buildProcess(id) {
417
+ await this.listProcesses();
418
+ return new Builder(this.processes, null, id);
419
+ }
420
+
421
+ /**
422
+ * List all authentication methods supported by the back-end.
423
+ *
424
+ * @async
425
+ * @returns {Promise<Array.<AuthProvider>>} An array containing all supported AuthProviders (including all OIDC providers and HTTP Basic).
426
+ * @throws {Error}
427
+ * @see AuthProvider
428
+ */
429
+ async listAuthProviders() {
430
+ if (this.authProviderList !== null) {
431
+ return this.authProviderList;
432
+ }
433
+
434
+ this.authProviderList = [];
435
+ let cap = this.capabilities();
436
+
437
+ // Add OIDC providers
438
+ if (cap.hasFeature('authenticateOIDC')) {
439
+ let res = await this._get('/credentials/oidc');
440
+ let oidcFactory = this.getOidcProviderFactory();
441
+ if (Utils.isObject(res.data) && Array.isArray(res.data.providers) && typeof oidcFactory === 'function') {
442
+ for(let i in res.data.providers) {
443
+ let obj = oidcFactory(res.data.providers[i]);
444
+ if (obj instanceof AuthProvider) {
445
+ this.authProviderList.push(obj);
446
+ }
447
+ }
448
+ }
449
+ }
450
+
451
+ // Add Basic provider
452
+ if (cap.hasFeature('authenticateBasic')) {
453
+ this.authProviderList.push(new BasicProvider(this));
454
+ }
455
+
456
+ return this.authProviderList;
457
+ }
458
+
459
+ /**
460
+ * This function is meant to create the OIDC providers used for authentication.
461
+ *
462
+ * The function gets passed a single argument that contains the
463
+ * provider information as provided by the API, e.g. having the properties
464
+ * `id`, `issuer`, `title` etc.
465
+ *
466
+ * The function must return an instance of AuthProvider or any derived class.
467
+ * May return `null` if the instance can't be created.
468
+ *
469
+ * @callback oidcProviderFactoryFunction
470
+ * @param {object.<string, *>} providerInfo - The provider information as provided by the API, having the properties `id`, `issuer`, `title` etc.
471
+ * @returns {AuthProvider | null}
472
+ */
473
+
474
+ /**
475
+ * Sets a factory function that creates custom OpenID Connect provider instances.
476
+ *
477
+ * You only need to call this if you have implemented a new AuthProvider based
478
+ * on the AuthProvider interface (or OIDCProvider class), e.g. to use a
479
+ * OIDC library other than oidc-client-js.
480
+ *
481
+ * @param {?oidcProviderFactoryFunction} [providerFactoryFunc=null]
482
+ * @see AuthProvider
483
+ */
484
+ setOidcProviderFactory(providerFactoryFunc) {
485
+ this.oidcProviderFactory = providerFactoryFunc;
486
+ }
487
+
488
+ /**
489
+ * Get the OpenID Connect provider factory.
490
+ *
491
+ * Returns `null` if OIDC is not supported by the client or an instance
492
+ * can't be created for whatever reason.
493
+ *
494
+ * @returns {oidcProviderFactoryFunction | null}
495
+ * @see AuthProvider
496
+ */
497
+ getOidcProviderFactory() {
498
+ if (typeof this.oidcProviderFactory === 'function') {
499
+ return this.oidcProviderFactory;
500
+ }
501
+ else {
502
+ if (OidcProvider.isSupported()) {
503
+ return providerInfo => new OidcProvider(this, providerInfo);
504
+ }
505
+ else {
506
+ return null;
507
+ }
508
+ }
509
+ }
510
+
511
+ /**
512
+ * Authenticates with username and password against a back-end supporting HTTP Basic Authentication.
513
+ *
514
+ * DEPRECATED in favor of using `listAuthProviders` and `BasicProvider`.
515
+ *
516
+ * @async
517
+ * @deprecated
518
+ * @param {string} username
519
+ * @param {string} password
520
+ * @see BasicProvider
521
+ * @see Connection#listAuthProviders
522
+ */
523
+ async authenticateBasic(username, password) {
524
+ let basic = new BasicProvider(this);
525
+ await basic.login(username, password);
526
+ }
527
+
528
+ /**
529
+ * Returns whether the user is authenticated (logged in) at the back-end or not.
530
+ *
531
+ * @returns {boolean} `true` if authenticated, `false` if not.
532
+ */
533
+ isAuthenticated() {
534
+ return (this.authProvider !== null);
535
+ }
536
+
537
+ /**
538
+ * Emits the given event.
539
+ *
540
+ * @protected
541
+ * @param {string} event
542
+ * @param {...*} args
543
+ */
544
+ emit(event, ...args) {
545
+ if (typeof this.listeners[event] === 'function') {
546
+ this.listeners[event](...args);
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Registers a listener with the given event.
552
+ *
553
+ * Currently supported:
554
+ * - authProviderChanged(provider): Raised when the auth provider has changed.
555
+ * - tokenChanged(token): Raised when the access token has changed.
556
+ * - processesChanged(type, data, namespace): Raised when the process registry has changed (i.e. a process was added, updated or deleted).
557
+ *
558
+ * @param {string} event
559
+ * @param {Function} callback
560
+ */
561
+ on(event, callback) {
562
+ this.listeners[event] = callback;
563
+ }
564
+
565
+ /**
566
+ * Removes a listener from the given event.
567
+ *
568
+ * @param {string} event
569
+ */
570
+ off(event) {
571
+ delete this.listeners[event];
572
+ }
573
+
574
+ /**
575
+ * Returns the AuthProvider.
576
+ *
577
+ * @returns {AuthProvider | null}
578
+ */
579
+ getAuthProvider() {
580
+ return this.authProvider;
581
+ }
582
+
583
+ /**
584
+ * Sets the AuthProvider.
585
+ *
586
+ * @param {AuthProvider} provider
587
+ */
588
+ setAuthProvider(provider) {
589
+ if (provider === this.authProvider) {
590
+ return;
591
+ }
592
+ if (provider instanceof AuthProvider) {
593
+ this.authProvider = provider;
594
+ }
595
+ else {
596
+ this.authProvider = null;
597
+ }
598
+ this.emit('authProviderChanged', this.authProvider);
599
+ // Update process cache on auth changes: https://github.com/Open-EO/openeo-js-client/issues/55
600
+ this.refreshProcessCache();
601
+ }
602
+
603
+ /**
604
+ * Sets the authentication token for the connection.
605
+ *
606
+ * This creates a new custom `AuthProvider` with the given details and returns it.
607
+ * After calling this function you can make requests against the API.
608
+ *
609
+ * This is NOT recommended to use. Only use if you know what you are doing.
610
+ * It is recommended to authenticate through `listAuthProviders` or related functions.
611
+ *
612
+ * @param {string} type - The authentication type, e.g. `basic` or `oidc`.
613
+ * @param {string} providerId - The provider identifier. For OIDC the `id` of the provider.
614
+ * @param {string} token - The actual access token as given by the authentication method during the login process.
615
+ * @returns {AuthProvider}
616
+ */
617
+ setAuthToken(type, providerId, token) {
618
+ let provider = new AuthProvider(type, this, {
619
+ id: providerId,
620
+ title: "Custom",
621
+ description: ""
622
+ });
623
+ provider.setToken(token);
624
+ this.setAuthProvider(provider);
625
+ return provider;
626
+ }
627
+
628
+ /**
629
+ * Get information about the authenticated user.
630
+ *
631
+ * Updates the User ID if available.
632
+ *
633
+ * @async
634
+ * @returns {Promise<UserAccount>} A response compatible to the API specification.
635
+ * @throws {Error}
636
+ */
637
+ async describeAccount() {
638
+ let response = await this._get('/me');
639
+ return response.data;
640
+ }
641
+
642
+ /**
643
+ * Lists all files from the user workspace.
644
+ *
645
+ * @async
646
+ * @returns {Promise<ResponseArray.<UserFile>>} A list of files.
647
+ * @throws {Error}
648
+ */
649
+ async listFiles() {
650
+ let response = await this._get('/files');
651
+ let files = response.data.files.map(
652
+ f => new UserFile(this, f.path).setAll(f)
653
+ );
654
+ return this._toResponseArray(files, response.data);
655
+ }
656
+
657
+ /**
658
+ * A callback that is executed on upload progress updates.
659
+ *
660
+ * @callback uploadStatusCallback
661
+ * @param {number} percentCompleted - The percent (0-100) completed.
662
+ * @param {UserFile} file - The file object corresponding to the callback.
663
+ */
664
+
665
+ /**
666
+ * Uploads a file to the user workspace.
667
+ * If a file with the name exists, overwrites it.
668
+ *
669
+ * This method has different behaviour depending on the environment.
670
+ * In a nodeJS environment the source must be a path to a file as string.
671
+ * In a browser environment the source must be an object from a file upload form.
672
+ *
673
+ * @async
674
+ * @param {*} source - The source, see method description for details.
675
+ * @param {?string} [targetPath=null] - The target path on the server, relative to the user workspace. Defaults to the file name of the source file.
676
+ * @param {?uploadStatusCallback} [statusCallback=null] - Optionally, a callback that is executed on upload progress updates.
677
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the upload process.
678
+ * @returns {Promise<UserFile>}
679
+ * @throws {Error}
680
+ */
681
+ async uploadFile(source, targetPath = null, statusCallback = null, abortController = null) {
682
+ if (targetPath === null) {
683
+ targetPath = Environment.fileNameForUpload(source);
684
+ }
685
+ let file = await this.getFile(targetPath);
686
+ return await file.uploadFile(source, statusCallback, abortController);
687
+ }
688
+
689
+ /**
690
+ * Opens a (existing or non-existing) file without reading any information or creating a new file at the back-end.
691
+ *
692
+ * @async
693
+ * @param {string} path - Path to the file, relative to the user workspace.
694
+ * @returns {Promise<UserFile>} A file.
695
+ * @throws {Error}
696
+ */
697
+ async getFile(path) {
698
+ return new UserFile(this, path);
699
+ }
700
+
701
+ /**
702
+ * Takes a UserProcess, BuilderNode or a plain object containing process nodes
703
+ * and converts it to an API compliant object.
704
+ *
705
+ * @param {UserProcess|BuilderNode|object.<string, *>} process - Process to be normalized.
706
+ * @param {object.<string, *>} additional - Additional properties to be merged with the resulting object.
707
+ * @returns {object.<string, *>}
708
+ * @protected
709
+ */
710
+ _normalizeUserProcess(process, additional = {}) {
711
+ if (process instanceof UserProcess) {
712
+ process = process.toJSON();
713
+ }
714
+ else if (process instanceof BuilderNode) {
715
+ process.result = true;
716
+ process = process.parent.toJSON();
717
+ }
718
+ else if (Utils.isObject(process) && !Utils.isObject(process.process_graph)) {
719
+ process = {
720
+ process_graph: process
721
+ };
722
+ }
723
+ return Object.assign({}, additional, {process: process});
724
+ }
725
+
726
+ /**
727
+ * Validates a user-defined process at the back-end.
728
+ *
729
+ * @async
730
+ * @param {Process} process - User-defined process to validate.
731
+ * @returns {Promise<Array.<ApiError>>} errors - A list of API compatible error objects. A valid process returns an empty list.
732
+ * @throws {Error}
733
+ */
734
+ async validateProcess(process) {
735
+ let response = await this._post('/validation', this._normalizeUserProcess(process).process);
736
+ if (Array.isArray(response.data.errors)) {
737
+ return response.data.errors;
738
+ }
739
+ else {
740
+ throw new Error("Invalid validation response received.");
741
+ }
742
+ }
743
+
744
+ /**
745
+ * Lists all user-defined processes of the authenticated user.
746
+ *
747
+ * @async
748
+ * @param {Array.<UserProcess>} [oldProcesses=[]] - A list of existing user-defined processes to update.
749
+ * @returns {Promise<ResponseArray.<UserProcess>>} A list of user-defined processes.
750
+ * @throws {Error}
751
+ */
752
+ async listUserProcesses(oldProcesses = []) {
753
+ let response = await this._get('/process_graphs');
754
+
755
+ if (!Utils.isObject(response.data) || !Array.isArray(response.data.processes)) {
756
+ throw new Error('Invalid response received for processes');
757
+ }
758
+
759
+ // Remove existing processes from cache
760
+ this.processes.remove(null, 'user');
761
+
762
+ // Update existing processes if needed
763
+ let newProcesses = response.data.processes.map(newProcess => {
764
+ let process = oldProcesses.find(oldProcess => oldProcess.id === newProcess.id);
765
+ if (!process) {
766
+ process = new UserProcess(this, newProcess.id);
767
+ }
768
+ return process.setAll(newProcess);
769
+ });
770
+
771
+ // Store plain JS variant (i.e. no Job objects involved) of processes in cache
772
+ let jsonProcesses = oldProcesses.length > 0 ? newProcesses.map(p => p.toJSON()) : response.data.processes;
773
+ this.processes.addAll(jsonProcesses, 'user');
774
+
775
+ return this._toResponseArray(newProcesses, response.data);
776
+ }
777
+
778
+ /**
779
+ * Creates a new stored user-defined process at the back-end.
780
+ *
781
+ * @async
782
+ * @param {string} id - Unique identifier for the process.
783
+ * @param {Process} process - A user-defined process.
784
+ * @returns {Promise<UserProcess>} The new user-defined process.
785
+ * @throws {Error}
786
+ */
787
+ async setUserProcess(id, process) {
788
+ let pg = new UserProcess(this, id);
789
+ return await pg.replaceUserProcess(process);
790
+ }
791
+
792
+ /**
793
+ * Get all information about a user-defined process.
794
+ *
795
+ * @async
796
+ * @param {string} id - Identifier of the user-defined process.
797
+ * @returns {Promise<UserProcess>} The user-defined process.
798
+ * @throws {Error}
799
+ */
800
+ async getUserProcess(id) {
801
+ let pg = new UserProcess(this, id);
802
+ return await pg.describeUserProcess();
803
+ }
804
+
805
+ /**
806
+ * Executes a process synchronously and returns the result as the response.
807
+ *
808
+ * Please note that requests can take a very long time of several minutes or even hours.
809
+ *
810
+ * @async
811
+ * @param {Process} process - A user-defined process.
812
+ * @param {?string} [plan=null] - The billing plan to use for this computation.
813
+ * @param {?number} [budget=null] - The maximum budget allowed to spend for this computation.
814
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the processing request.
815
+ * @param {object.<string, *>} [additional={}] - Other parameters to pass for the batch job, e.g. `log_level`.
816
+ * @returns {Promise<SyncResult>} - An object with the data and some metadata.
817
+ */
818
+ async computeResult(process, plan = null, budget = null, abortController = null, additional = {}) {
819
+ let requestBody = this._normalizeUserProcess(
820
+ process,
821
+ Object.assign({}, additional, {
822
+ plan: plan,
823
+ budget: budget
824
+ })
825
+ );
826
+ let response = await this._post('/result', requestBody, Environment.getResponseType(), abortController);
827
+ let syncResult = {
828
+ data: response.data,
829
+ costs: null,
830
+ type: null,
831
+ logs: []
832
+ };
833
+
834
+ if (typeof response.headers['openeo-costs'] === 'number') {
835
+ syncResult.costs = response.headers['openeo-costs'];
836
+ }
837
+
838
+ if (typeof response.headers['content-type'] === 'string') {
839
+ syncResult.type = response.headers['content-type'];
840
+ }
841
+
842
+ let links = Array.isArray(response.headers.link) ? response.headers.link : [response.headers.link];
843
+ for(let link of links) {
844
+ if (typeof link !== 'string') {
845
+ continue;
846
+ }
847
+ let logs = link.match(/^<([^>]+)>;\s?rel="monitor"/i);
848
+ if (Array.isArray(logs) && logs.length > 1) {
849
+ try {
850
+ let logsResponse = await this._get(logs[1]);
851
+ if (Utils.isObject(logsResponse.data) && Array.isArray(logsResponse.data.logs)) {
852
+ syncResult.logs = logsResponse.data.logs;
853
+ }
854
+ } catch(error) {
855
+ console.warn(error);
856
+ }
857
+ }
858
+ }
859
+
860
+ return syncResult;
861
+ }
862
+
863
+ /**
864
+ * Executes a process synchronously and downloads to result the given path.
865
+ *
866
+ * Please note that requests can take a very long time of several minutes or even hours.
867
+ *
868
+ * This method has different behaviour depending on the environment.
869
+ * If a NodeJs environment, writes the downloaded file to the target location on the file system.
870
+ * In a browser environment, offers the file for downloading using the specified name (folders are not supported).
871
+ *
872
+ * @async
873
+ * @param {Process} process - A user-defined process.
874
+ * @param {string} targetPath - The target, see method description for details.
875
+ * @param {?string} [plan=null] - The billing plan to use for this computation.
876
+ * @param {?number} [budget=null] - The maximum budget allowed to spend for this computation.
877
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the processing request.
878
+ * @throws {Error}
879
+ */
880
+ async downloadResult(process, targetPath, plan = null, budget = null, abortController = null) {
881
+ let response = await this.computeResult(process, plan, budget, abortController);
882
+ // @ts-ignore
883
+ await Environment.saveToFile(response.data, targetPath);
884
+ }
885
+
886
+ /**
887
+ * Lists all batch jobs of the authenticated user.
888
+ *
889
+ * @async
890
+ * @param {Array.<Job>} [oldJobs=[]] - A list of existing jobs to update.
891
+ * @returns {Promise<ResponseArray.<Job>>} A list of jobs.
892
+ * @throws {Error}
893
+ */
894
+ async listJobs(oldJobs = []) {
895
+ let response = await this._get('/jobs');
896
+ let newJobs = response.data.jobs.map(newJob => {
897
+ let job = oldJobs.find(oldJob => oldJob.id === newJob.id);
898
+ if (!job) {
899
+ job = new Job(this, newJob.id);
900
+ }
901
+ return job.setAll(newJob);
902
+ });
903
+ return this._toResponseArray(newJobs, response.data);
904
+ }
905
+
906
+ /**
907
+ * Creates a new batch job at the back-end.
908
+ *
909
+ * @async
910
+ * @param {Process} process - A user-define process to execute.
911
+ * @param {?string} [title=null] - A title for the batch job.
912
+ * @param {?string} [description=null] - A description for the batch job.
913
+ * @param {?string} [plan=null] - The billing plan to use for this batch job.
914
+ * @param {?number} [budget=null] - The maximum budget allowed to spend for this batch job.
915
+ * @param {object.<string, *>} [additional={}] - Other parameters to pass for the batch job, e.g. `log_level`.
916
+ * @returns {Promise<Job>} The stored batch job.
917
+ * @throws {Error}
918
+ */
919
+ async createJob(process, title = null, description = null, plan = null, budget = null, additional = {}) {
920
+ additional = Object.assign({}, additional, {
921
+ title: title,
922
+ description: description,
923
+ plan: plan,
924
+ budget: budget
925
+ });
926
+ let requestBody = this._normalizeUserProcess(process, additional);
927
+ let response = await this._post('/jobs', requestBody);
928
+ if (typeof response.headers['openeo-identifier'] !== 'string') {
929
+ throw new Error("Response did not contain a Job ID. Job has likely been created, but may not show up yet.");
930
+ }
931
+ let job = new Job(this, response.headers['openeo-identifier']).setAll(requestBody);
932
+ if (this.capabilities().hasFeature('describeJob')) {
933
+ return await job.describeJob();
934
+ }
935
+ else {
936
+ return job;
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Get all information about a batch job.
942
+ *
943
+ * @async
944
+ * @param {string} id - Batch Job ID.
945
+ * @returns {Promise<Job>} The batch job.
946
+ * @throws {Error}
947
+ */
948
+ async getJob(id) {
949
+ let job = new Job(this, id);
950
+ return await job.describeJob();
951
+ }
952
+
953
+ /**
954
+ * Lists all secondary web services of the authenticated user.
955
+ *
956
+ * @async
957
+ * @param {Array.<Service>} [oldServices=[]] - A list of existing services to update.
958
+ * @returns {Promise<ResponseArray.<Job>>} A list of services.
959
+ * @throws {Error}
960
+ */
961
+ async listServices(oldServices = []) {
962
+ let response = await this._get('/services');
963
+ let newServices = response.data.services.map(newService => {
964
+ let service = oldServices.find(oldService => oldService.id === newService.id);
965
+ if (!service) {
966
+ service = new Service(this, newService.id);
967
+ }
968
+ return service.setAll(newService);
969
+ });
970
+ return this._toResponseArray(newServices, response.data);
971
+ }
972
+
973
+ /**
974
+ * Creates a new secondary web service at the back-end.
975
+ *
976
+ * @async
977
+ * @param {Process} process - A user-defined process.
978
+ * @param {string} type - The type of service to be created (see `Connection.listServiceTypes()`).
979
+ * @param {?string} [title=null] - A title for the service.
980
+ * @param {?string} [description=null] - A description for the service.
981
+ * @param {boolean} [enabled=true] - Enable the service (`true`, default) or not (`false`).
982
+ * @param {object.<string, *>} [configuration={}] - Configuration parameters to pass to the service.
983
+ * @param {?string} [plan=null] - The billing plan to use for this service.
984
+ * @param {?number} [budget=null] - The maximum budget allowed to spend for this service.
985
+ * @param {object.<string, *>} [additional={}] - Other parameters to pass for the service, e.g. `log_level`.
986
+ * @returns {Promise<Service>} The stored service.
987
+ * @throws {Error}
988
+ */
989
+ async createService(process, type, title = null, description = null, enabled = true, configuration = {}, plan = null, budget = null, additional = {}) {
990
+ let requestBody = this._normalizeUserProcess(process, Object.assign({
991
+ title: title,
992
+ description: description,
993
+ type: type,
994
+ enabled: enabled,
995
+ configuration: configuration,
996
+ plan: plan,
997
+ budget: budget
998
+ }, additional));
999
+ let response = await this._post('/services', requestBody);
1000
+ if (typeof response.headers['openeo-identifier'] !== 'string') {
1001
+ throw new Error("Response did not contain a Service ID. Service has likely been created, but may not show up yet.");
1002
+ }
1003
+ let service = new Service(this, response.headers['openeo-identifier']).setAll(requestBody);
1004
+ if (this.capabilities().hasFeature('describeService')) {
1005
+ return service.describeService();
1006
+ }
1007
+ else {
1008
+ return service;
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Get all information about a secondary web service.
1014
+ *
1015
+ * @async
1016
+ * @param {string} id - Service ID.
1017
+ * @returns {Promise<Service>} The service.
1018
+ * @throws {Error}
1019
+ */
1020
+ async getService(id) {
1021
+ let service = new Service(this, id);
1022
+ return await service.describeService();
1023
+ }
1024
+
1025
+ /**
1026
+ * Adds additional response details to the array.
1027
+ *
1028
+ * Adds links and federation:missing.
1029
+ *
1030
+ * @protected
1031
+ * @param {Array.<*>} arr
1032
+ * @param {object.<string, *>} response
1033
+ * @returns {ResponseArray}
1034
+ */
1035
+ _toResponseArray(arr, response) {
1036
+ arr.links = Array.isArray(response.links) ? response.links : [];
1037
+ arr['federation:missing'] = Array.isArray(response['federation:missing']) ? response['federation:missing'] : [];
1038
+ return arr;
1039
+ }
1040
+
1041
+ /**
1042
+ * Get the a link with the given rel type.
1043
+ *
1044
+ * @protected
1045
+ * @param {Array.<Link>} links - An array of links.
1046
+ * @param {string|Array.<string>} rel - Relation type(s) to find.
1047
+ * @returns {string | null}
1048
+ * @throws {Error}
1049
+ */
1050
+ _getLinkHref(links, rel) {
1051
+ if (!Array.isArray(rel)) {
1052
+ rel = [rel];
1053
+ }
1054
+ if (Array.isArray(links)) {
1055
+ let link = links.find(l => Utils.isObject(l) && rel.includes(l.rel) && typeof l.href === 'string');
1056
+ if (link) {
1057
+ return link.href;
1058
+ }
1059
+ }
1060
+ return null;
1061
+ }
1062
+
1063
+ /**
1064
+ * Makes all links in the list absolute.
1065
+ *
1066
+ * @param {Array.<Link>} links - An array of links.
1067
+ * @param {?string|AxiosResponse} [base=null] - The base url to use for relative links, or an response to derive the url from.
1068
+ * @returns {Array.<Link>}
1069
+ */
1070
+ makeLinksAbsolute(links, base = null) {
1071
+ if (!Array.isArray(links)) {
1072
+ return links;
1073
+ }
1074
+ let baseUrl = null;
1075
+ if (Utils.isObject(base) && base.headers && base.config && base.request) { // AxiosResponse
1076
+ baseUrl = base.config.baseURL + base.config.url;
1077
+ }
1078
+ else if (typeof base !== 'string') {
1079
+ baseUrl = this._getLinkHref(links, 'self');
1080
+ }
1081
+ else {
1082
+ baseUrl = base;
1083
+ }
1084
+ if (!baseUrl) {
1085
+ return links;
1086
+ }
1087
+ return links.map((link) => {
1088
+ if (!Utils.isObject(link) || typeof link.href !== 'string') {
1089
+ return link;
1090
+ }
1091
+ try {
1092
+ let url = new URL(link.href, baseUrl);
1093
+ return Object.assign({}, link, {href: url.toString()});
1094
+ } catch(error) {
1095
+ return link;
1096
+ }
1097
+ });
1098
+ }
1099
+
1100
+ /**
1101
+ * Sends a GET request.
1102
+ *
1103
+ * @protected
1104
+ * @async
1105
+ * @param {string} path
1106
+ * @param {object.<string, *>} query
1107
+ * @param {string} responseType - Response type according to axios, defaults to `json`.
1108
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the request.
1109
+ * @returns {Promise<AxiosResponse>}
1110
+ * @throws {Error}
1111
+ * @see https://github.com/axios/axios#request-config
1112
+ */
1113
+ async _get(path, query, responseType, abortController = null) {
1114
+ return await this._send({
1115
+ method: 'get',
1116
+ responseType: responseType,
1117
+ url: path,
1118
+ // Timeout for capabilities requests as they are used for a quick first discovery to check whether the server is a openEO back-end.
1119
+ // Without timeout connecting with a wrong server url may take forever.
1120
+ timeout: path === '/' ? 5000 : 0,
1121
+ params: query
1122
+ }, abortController);
1123
+ }
1124
+
1125
+ /**
1126
+ * Sends a POST request.
1127
+ *
1128
+ * @protected
1129
+ * @async
1130
+ * @param {string} path
1131
+ * @param {*} body
1132
+ * @param {string} responseType - Response type according to axios, defaults to `json`.
1133
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the request.
1134
+ * @returns {Promise<AxiosResponse>}
1135
+ * @throws {Error}
1136
+ * @see https://github.com/axios/axios#request-config
1137
+ */
1138
+ async _post(path, body, responseType, abortController = null) {
1139
+ let options = {
1140
+ method: 'post',
1141
+ responseType: responseType,
1142
+ url: path,
1143
+ data: body
1144
+ };
1145
+ return await this._send(options, abortController);
1146
+ }
1147
+
1148
+ /**
1149
+ * Sends a PUT request.
1150
+ *
1151
+ * @protected
1152
+ * @async
1153
+ * @param {string} path
1154
+ * @param {*} body
1155
+ * @returns {Promise<AxiosResponse>}
1156
+ * @throws {Error}
1157
+ */
1158
+ async _put(path, body) {
1159
+ return await this._send({
1160
+ method: 'put',
1161
+ url: path,
1162
+ data: body
1163
+ });
1164
+ }
1165
+
1166
+ /**
1167
+ * Sends a PATCH request.
1168
+ *
1169
+ * @protected
1170
+ * @async
1171
+ * @param {string} path
1172
+ * @param {*} body
1173
+ * @returns {Promise<AxiosResponse>}
1174
+ * @throws {Error}
1175
+ */
1176
+ async _patch(path, body) {
1177
+ return await this._send({
1178
+ method: 'patch',
1179
+ url: path,
1180
+ data: body
1181
+ });
1182
+ }
1183
+
1184
+ /**
1185
+ * Sends a DELETE request.
1186
+ *
1187
+ * @protected
1188
+ * @async
1189
+ * @param {string} path
1190
+ * @returns {Promise<AxiosResponse>}
1191
+ * @throws {Error}
1192
+ */
1193
+ async _delete(path) {
1194
+ return await this._send({
1195
+ method: 'delete',
1196
+ url: path
1197
+ });
1198
+ }
1199
+
1200
+ /**
1201
+ * Downloads data from a URL.
1202
+ *
1203
+ * May include authorization details where required.
1204
+ *
1205
+ * @param {string} url - An absolute or relative URL to download data from.
1206
+ * @param {boolean} authorize - Send authorization details (`true`) or not (`false`).
1207
+ * @returns {Promise<Stream.Readable|Blob>} - Returns the data as `Stream` in NodeJS environments or as `Blob` in browsers
1208
+ * @throws {Error}
1209
+ */
1210
+ async download(url, authorize) {
1211
+ let result = await this._send({
1212
+ method: 'get',
1213
+ responseType: Environment.getResponseType(),
1214
+ url: url,
1215
+ authorization: authorize
1216
+ });
1217
+ return result.data;
1218
+ }
1219
+
1220
+ /**
1221
+ * Get the authorization header for requests.
1222
+ *
1223
+ * @protected
1224
+ * @returns {object.<string, string>}
1225
+ */
1226
+ _getAuthHeaders() {
1227
+ const headers = {};
1228
+ if (this.isAuthenticated()) {
1229
+ headers.Authorization = 'Bearer ' + this.authProvider.getToken();
1230
+ }
1231
+ return headers;
1232
+ }
1233
+
1234
+ /**
1235
+ * Sends a HTTP request.
1236
+ *
1237
+ * Options mostly conform to axios,
1238
+ * see {@link https://github.com/axios/axios#request-config}.
1239
+ *
1240
+ * Automatically sets a baseUrl and the authorization information.
1241
+ * Default responseType is `json`.
1242
+ *
1243
+ * Tries to smoothly handle error responses by providing an object for all response types,
1244
+ * instead of Streams or Blobs for non-JSON response types.
1245
+ *
1246
+ * @protected
1247
+ * @async
1248
+ * @param {object.<string, *>} options
1249
+ * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the request.
1250
+ * @returns {Promise<AxiosResponse>}
1251
+ * @throws {Error}
1252
+ * @see https://github.com/axios/axios
1253
+ */
1254
+ async _send(options, abortController = null) {
1255
+ options.baseURL = this.baseUrl;
1256
+ if (typeof options.authorization === 'undefined' || options.authorization === true) {
1257
+ if (!options.headers) {
1258
+ options.headers = {};
1259
+ }
1260
+ Object.assign(options.headers, this._getAuthHeaders());
1261
+ }
1262
+ if (!options.responseType) {
1263
+ options.responseType = 'json';
1264
+ }
1265
+ if (abortController) {
1266
+ options.signal = abortController.signal;
1267
+ }
1268
+
1269
+ try {
1270
+ let response = await axios(options);
1271
+ let capabilities = this.capabilities();
1272
+ if (capabilities) {
1273
+ response = capabilities.migrate(response);
1274
+ }
1275
+ return response;
1276
+ } catch(error) {
1277
+ if (axios.isCancel(error)) {
1278
+ throw error;
1279
+ }
1280
+ const checkContentType = type => (typeof type === 'string' && type.indexOf('/json') !== -1);
1281
+ const enrichError = (origin, response) => {
1282
+ if (typeof response.message === 'string') {
1283
+ origin.message = response.message;
1284
+ }
1285
+ origin.code = typeof response.code === 'string' ? response.code : "";
1286
+ origin.id = response.id;
1287
+ origin.links = Array.isArray(response.links) ? response.links : [];
1288
+ return origin;
1289
+ };
1290
+ if (Utils.isObject(error.response) && Utils.isObject(error.response.data) && (checkContentType(error.response.data.type) || (Utils.isObject(error.response.headers) && checkContentType(error.response.headers['content-type'])))) {
1291
+ // JSON error responses are Blobs and streams if responseType is set as such, so convert to JSON if required.
1292
+ // See: https://github.com/axios/axios/issues/815
1293
+ if (options.responseType === Environment.getResponseType()) {
1294
+ try {
1295
+ let errorResponse = await Environment.handleErrorResponse(error);
1296
+ throw enrichError(error, errorResponse);
1297
+ } catch (error2) {
1298
+ console.error(error2);
1299
+ }
1300
+ }
1301
+ else {
1302
+ throw enrichError(error, error.response.data);
1303
+ }
1304
+ }
1305
+ throw error;
1306
+ }
1307
+ }
1308
+ }
1309
+
1310
+ module.exports = Connection;