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