Source: connection.js

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