├── .eslintrc.json ├── .github └── workflows │ ├── docs-latest.yml │ ├── docs.yml │ └── tests.yml ├── .gitignore ├── .jsdoc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── node │ ├── discovery.js │ └── package.json ├── oidc │ ├── openid-connect-popup.html │ └── openid-connect-redirect.html ├── typescript │ ├── .gitignore │ ├── discovery.ts │ └── package.json └── web │ ├── discovery.html │ └── workflow.html ├── jest.config.js ├── jsconfig.json ├── openeo.d.ts ├── package.json ├── src ├── authprovider.js ├── baseentity.js ├── basicprovider.js ├── browser.js ├── builder │ ├── builder.js │ ├── formula.js │ ├── node.js │ ├── parameter.js │ └── tapdigit.js ├── capabilities.js ├── connection.js ├── env.js ├── filetypes.js ├── job.js ├── logs.js ├── node.js ├── oidcprovider.js ├── openeo.js ├── pages.js ├── service.js ├── typedefs.js ├── userfile.js └── userprocess.js ├── tests ├── basic.test.js ├── builder.array_create.test.js ├── builder.evi.test.js ├── builder.misc.test.js ├── builder.namespaces.test.js ├── builder.s1.test.js ├── config.js ├── data │ ├── builder.array_create.example.json │ ├── builder.evi.example.json │ ├── builder.math.evi.example.json │ ├── builder.s1.example.json │ └── gee │ │ ├── collection.json │ │ ├── process.json │ │ └── processgraph.json ├── earthengine.test.js ├── eodc.test.js └── vito.test.js ├── tsconfig.json └── webpack.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "extends": [ 9 | "eslint:recommended" 10 | ], 11 | "parserOptions": { 12 | "ecmaVersion": 9 13 | }, 14 | "plugins": [ 15 | "jsdoc" 16 | ], 17 | "rules": { 18 | "no-constant-condition": ["error", { "checkLoops": false }], 19 | "no-template-curly-in-string": "error", 20 | "block-scoped-var": "error", 21 | "no-unreachable-loop": "error", 22 | "require-atomic-updates": "error", 23 | "max-classes-per-file": "error", 24 | "no-alert": "error", 25 | "no-caller": "error", 26 | "eqeqeq": "error", 27 | "no-constructor-return": "error", 28 | "no-eval": "error", 29 | "no-extend-native": "error", 30 | "no-extra-bind": "error", 31 | "no-implicit-coercion": "error", 32 | "no-invalid-this": "error", 33 | "no-multi-spaces": "error", 34 | "no-multi-str": "error", 35 | "no-return-assign": "error", 36 | "no-self-compare": "error", 37 | "no-sequences": "error", 38 | "no-throw-literal": "error", 39 | "no-unmodified-loop-condition": "error", 40 | "no-unused-expressions": "error", 41 | "no-useless-call": "error", 42 | "no-useless-concat": "error", 43 | "no-useless-return": "error", 44 | "no-void": "error", 45 | "no-warning-comments": "error", 46 | "radix": "error", 47 | "no-shadow": "error", 48 | "no-use-before-define": ["error", { "functions": false }], 49 | "arrow-spacing": "error", 50 | "no-confusing-arrow": "error", 51 | "no-duplicate-imports": "error", 52 | "no-var": "error", 53 | 54 | "jsdoc/check-access": 1, 55 | "jsdoc/check-alignment": 1, // Recommended 56 | "jsdoc/check-examples": 1, 57 | "jsdoc/check-indentation": 1, 58 | "jsdoc/check-param-names": 1, // Recommended 59 | "jsdoc/check-syntax": 1, 60 | "jsdoc/check-tag-names": 1, // Recommended 61 | "jsdoc/check-types": 1, // Recommended 62 | "jsdoc/implements-on-classes": 1, // Recommended 63 | "jsdoc/match-description": 1, 64 | "jsdoc/no-undefined-types": 0, // Recommended 65 | "jsdoc/require-description": 1, 66 | "jsdoc/require-hyphen-before-param-description": 1, 67 | "jsdoc/require-jsdoc": ["error", { 68 | "require": { 69 | "ArrowFunctionExpression": false, 70 | "ClassDeclaration": true, 71 | "ClassExpression": true, 72 | "FunctionDeclaration": true, 73 | "FunctionExpression": true, 74 | "MethodDefinition": true 75 | } 76 | }], 77 | "jsdoc/require-param": 1, // Recommended 78 | "jsdoc/require-param-name": 1, // Recommended 79 | "jsdoc/require-param-type": 1, // Recommended 80 | "jsdoc/require-returns": 1, // Recommended 81 | "jsdoc/require-returns-check": 1, // Recommended 82 | "jsdoc/require-returns-type": 1, // Recommended 83 | "jsdoc/valid-types": 1 // Recommended 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.github/workflows/docs-latest.yml: -------------------------------------------------------------------------------- 1 | name: Documentation (latest) 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/setup-node@v4 11 | - uses: actions/checkout@v4 12 | - run: npm install 13 | - run: npm run docs 14 | - name: get-npm-version 15 | id: package-version 16 | uses: martinbeentjes/npm-get-version-action@master 17 | - uses: peaceiris/actions-gh-pages@v4 18 | with: 19 | github_token: ${{ secrets.GITHUB_TOKEN }} 20 | publish_dir: docs/@openeo/js-client/${{ steps.package-version.outputs.current-version}} 21 | destination_dir: latest 22 | keep_files: true 23 | user_name: 'openEO CI' 24 | user_email: openeo.ci@uni-muenster.de -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Documentation (versioned) 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | deploy: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/setup-node@v4 10 | - uses: actions/checkout@v4 11 | - run: npm install 12 | - run: npm run docs 13 | - uses: peaceiris/actions-gh-pages@v4 14 | with: 15 | github_token: ${{ secrets.GITHUB_TOKEN }} 16 | publish_dir: docs/@openeo/js-client 17 | keep_files: true 18 | user_name: 'openEO CI' 19 | user_email: openeo.ci@uni-muenster.de -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | jobs: 4 | checks: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/setup-node@v4 8 | - uses: actions/checkout@v4 9 | - run: npm install 10 | - run: npm run lint 11 | - run: npm run build 12 | - run: npm run docs 13 | tests: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/setup-node@v4 17 | - uses: actions/checkout@v4 18 | - run: npm install 19 | - run: npm run test 20 | - run: npm run test_node -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # NPM 2 | node_modules/ 3 | /package-lock.json 4 | 5 | # Build related 6 | /report.html 7 | /openeo.js 8 | /openeo.min.js 9 | /openeo.node.js 10 | /openeo.node.min.js 11 | 12 | # Reports 13 | coverage/ 14 | docs/ 15 | 16 | # Temporary files from the earthengine tests 17 | /lorem.txt 18 | /randomnumber.txt 19 | /downloaded_file.txt 20 | -------------------------------------------------------------------------------- /.jsdoc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["plugins/markdown"], 3 | "recurseDepth": 10, 4 | "source": { 5 | "include": ["src/"], 6 | "includePattern": ".+\\.js(doc|x)?$", 7 | "excludePattern": "(^|\\/|\\\\)_" 8 | }, 9 | "sourceType": "module", 10 | "tags": { 11 | "allowUnknownTags": true, 12 | "dictionaries": ["jsdoc","closure"] 13 | }, 14 | "templates": { 15 | "cleverLinks": false, 16 | "monospaceLinks": false 17 | } 18 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.8.0] - 2025-04-25 11 | 12 | ### Added 13 | 14 | - New property `id` added to `FederationBackend` objects returned by `listFederation` in `Capabilities` 15 | - New functions `getFederationBackend` and `getFederationBackends` in `Capabilities` 16 | 17 | ### Fixed 18 | 19 | - Updated/fixed Typescript declaration 20 | 21 | ## [2.7.0] - 2025-01-04 22 | 23 | ### Added 24 | 25 | - New function `getMissingBackends` for `Logs` 26 | - New property `federation:backends` added to the array returned by `validateProcess` 27 | - New property `axios` in the `Environment` 28 | - New functions in `Connection` for paginating through lists: 29 | - `paginateProcesses` 30 | - `paginateCollections` 31 | - `paginateJobs` 32 | - `paginateFiles` 33 | - `paginateUserProcesses` 34 | - `paginateServices` 35 | 36 | ### Changed 37 | 38 | - The `listCollectionItems` function in `Connection` was completely rewritten. 39 | - The client may add a self link to the links in responses 40 | 41 | ## [2.6.0] - 2024-07-11 42 | 43 | ### Added 44 | 45 | - Support to retrieve conformance classes 46 | - Added `Connection.makeLinksAbsolute` 47 | - Added `AuthProvider.getDisplayName` 48 | - Hook to migrate capabilities 49 | - Support for `usage`, `log_level`, `links` in batch jobs and services 50 | - Support for `level` parameter for log requests in batch jobs and services 51 | - Support passing additional parameters to synchronous jobs. 52 | 53 | ### Changed 54 | 55 | - Updated axios from 0.x to 1.x 56 | 57 | ### Removed 58 | 59 | - Dependency for `node-abort-controller` not required any longer in nodeJS 60 | 61 | ### Fixed 62 | 63 | - Make the feature / endpoint mapping more robust 64 | - Add missing endpoints to feature / endpoint mapping 65 | - Fix AbortController handling 66 | - Clarify that the `log_level` parameter for jobs, services and sync. processing can be provided via the `additional` parameter 67 | 68 | ## [2.5.1] - 2022-04-08 69 | 70 | ### Fixed 71 | 72 | - Job status missing from batch job metadata 73 | 74 | ## [2.5.0] - 2022-04-07 75 | 76 | ### Changed 77 | 78 | - `listJobs`, `listServices` and `listUserProcesses` allow to update existing processes. 79 | - `listFiles`, `listJobs`, `listServices` and `listUserProcesses` return a `ResponseArray` instead of a plain `Array`. 80 | The ResonseArray now has the properties `links` and `federation:missing` from the responses. 81 | This is usually a non-breaking change, but if you mistakenly use `Object` functions (such as `Object.keys`) on an array, you'll get the additional properties, too. 82 | 83 | ### Fixed 84 | 85 | - Fine-tuned some Typescript declarations. 86 | 87 | ## [2.4.1] - 2022-01-13 88 | 89 | ### Fixed 90 | 91 | - Fixed regression for OIDC scopes introduced in 2.4.0 92 | 93 | ## [2.4.0] - 2022-01-10 94 | 95 | ### Added 96 | 97 | - OpenID Connect: Support requesting a refresh token easily via the `requestRefreshToken` parameter. 98 | 99 | ### Changed 100 | 101 | - The `namespace` parameter in `listProcesses` and `describeProcess` parses URLs to extract the namespace (experimental). 102 | 103 | ### Fixed 104 | 105 | - Fixed several type annotations in the documentation and the TypeScript declaration file. 106 | 107 | ## [2.3.1] - 2021-12-10 108 | 109 | ### Fixed 110 | 111 | - Process cache gets refreshed after the authentication details have changed (e.g. the user has logged in or out). 112 | 113 | ## [2.3.0] - 2021-12-10 114 | 115 | ### Added 116 | 117 | - New parameter `abortController` to allow cancellation of longer running requests (uploading files, sync. data processing). 118 | 119 | ### Changed 120 | 121 | - Rarely used `multihashes` dependency not included in the bundle by default, see Readme to include it in the browser. No change for node environments. 122 | 123 | ## [2.2.0] - 2021-08-20 124 | 125 | ### Added 126 | 127 | - New method `getUrl` to class `Connection` to get the main URL of the back-end, which the user provided (i.e. doesn't usually contain version part) 128 | 129 | ## [2.1.0] - 2021-08-18 130 | 131 | ### Added 132 | 133 | - Experimental support for process namespaces, see [API#348](https://github.com/Open-EO/openeo-api/pull/348). 134 | 135 | ### Changed 136 | 137 | - Internally, a new process registry is used to manage and cache processes. 138 | - `listProcesses` doesn't cache requests any longer. 139 | 140 | ### Fixed 141 | 142 | - Return a better error message if issues with reading batch job results occur in `Job.getResultsAsStac` 143 | - `getAll()` functions return only properties for values that are defined 144 | 145 | ## [2.0.1] - 2021-07-14 146 | 147 | ### Fixed 148 | 149 | - `Formula`: Formulas can reference more then just the first parameter by adding additional `$` at the beginning, e.g. `$$0` to access the first element of an array in the second parameter. 150 | 151 | ## [2.0.0] - 2021-07-06 152 | 153 | ### Added 154 | 155 | - Added events to `Connection` to populate changes for Auth Tokens (`tokenChanged`) and Auth Providers (`authProviderChanged`). 156 | - Added new methods to `Connection` for working with events: `on`, `off` and `emit`. 157 | 158 | ### Changed 159 | 160 | - OpenID Connect authentication has been rewritten. 161 | - Default grant type for OpenID Connect is "AuthCode w/ PKCE" instead of "Implicit". 162 | - Support for OpenID Connect session renewal via refresh tokens. 163 | - Updated STAC support to STAC v1.0.0. 164 | 165 | ### Removed 166 | 167 | - `OidcProvider`: Methods `getGrant`, `getScopes`, `getIssuer` and `getUser` removed. Use the properties `grant`, `scopes`, `issuer` and `user` instead. 168 | - Removed deprecated method `getResultsAsItem` in favor of `getResultsAsStac`. 169 | 170 | ## [1.3.2] - 2021-05-27 171 | 172 | ### Fixed 173 | 174 | - Fixed error handling for HTTP requests 175 | 176 | ## [1.3.1] - 2021-04-29 177 | 178 | ### Fixed 179 | 180 | - Invalid dependency version for @openeo/js-commons 181 | 182 | ## [1.3.0] - 2021-04-29 183 | 184 | ### Added 185 | 186 | - Custom process specifications can be added to the process builder after first initialization. 187 | - The process builder supports implicitly wrapping: 188 | - an array returned from a callback using the `array_create` process. 189 | - non-objects (e.g. numbers) returned from a callback using the `constant` process. 190 | - Support for "Default OpenID Connect Clients" (detecting default client IDs). 191 | 192 | ### Fixed 193 | 194 | - Arrow functions can be used as callbacks in process graph building. 195 | - Fixed nullable return types in TS declaraion 196 | - Fixed other minor issues in TS declaration 197 | 198 | ## [1.2.0] - 2021-03-11 199 | 200 | ### Added 201 | 202 | - Added new method `listCollectionItems` for searching and retrieving Items from `GET /collections/{collectionId}/items`. 203 | 204 | ### Changed 205 | 206 | - Methods returning STAC (e.g. `listCollections`, `describeCollection`, `getResultAsStac`, `listCollectionItems`) always return the data compliant to the latest STAC version (currently 1.0.0-rc.1). 207 | 208 | ## [1.1.0] - 2021-02-18 209 | 210 | ### Added 211 | 212 | - Added new method `getResultsAsStac` for Jobs, which includes support for STAC Collections returned by the API for batch job results. 213 | 214 | ### Deprecated 215 | 216 | - Deprecated method `getResultsAsItem` in favor of `getResultsAsStac`. 217 | 218 | ### Fixed 219 | 220 | - TypeScript declaration for Links has been fixed 221 | - Updated dependencies 222 | 223 | ## [1.0.3] - 2021-02-02 224 | 225 | ### Fixed 226 | 227 | - Updated axios dependency to fix a vulnerability. 228 | - Updated oidc-client dependency to support the OpenID Connect flow *AuthCode with PKCE*. Default is still the Implicit Flow. 229 | 230 | ## [1.0.2] - 2020-12-02 231 | 232 | ### Changed 233 | - Make client more flexible to allow setting the client to use OIDC Authentication Code Flow with PKCE instead of Implicit Flow in the future 234 | 235 | ### Fixed 236 | - Client doesn't throw errors on back-ends with version numbers >= 1.0.1 237 | 238 | ## [1.0.1] - 2020-11-19 239 | 240 | ### Fixed 241 | - Don't set unnecessary `withCredentials` option on HTTP requests for better CORS support. [openeo-api#41](https://github.com/Open-EO/openeo-api/issues/41) 242 | 243 | ## [1.0.0] - 2020-11-17 244 | 245 | ### Fixed 246 | - Throw better error message in case openeo-identifier can't be retrieved. [#37](https://github.com/Open-EO/openeo-js-client/issues/37) 247 | 248 | ## Prior releases 249 | 250 | All prior releases have been documented in the [GitHub Releases](https://github.com/Open-EO/openeo-js-client/releases). 251 | 252 | [Unreleased]: https://github.com/Open-EO/openeo-js-client/compare/v2.8.0...HEAD 253 | [2.8.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.7.0...v2.8.0 254 | [2.7.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.6.0...v2.7.0 255 | [2.6.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.5.1...v2.6.0 256 | [2.5.1]: https://github.com/Open-EO/openeo-js-client/compare/v2.5.0...v2.5.1 257 | [2.5.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.4.1...v2.5.0 258 | [2.4.1]: https://github.com/Open-EO/openeo-js-client/compare/v2.4.0...v2.4.1 259 | [2.4.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.3.1...v2.4.0 260 | [2.3.1]: https://github.com/Open-EO/openeo-js-client/compare/v2.3.0...v2.3.1 261 | [2.3.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.2.0...v2.3.0 262 | [2.2.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.1.0...v2.2.0 263 | [2.1.0]: https://github.com/Open-EO/openeo-js-client/compare/v2.0.1...v2.1.0 264 | [2.0.1]: https://github.com/Open-EO/openeo-js-client/compare/v2.0.0...v2.0.1 265 | [2.0.0]: https://github.com/Open-EO/openeo-js-client/compare/v1.3.2...v2.0.0 266 | [1.3.2]: https://github.com/Open-EO/openeo-js-client/compare/v1.3.1...v1.3.2 267 | [1.3.1]: https://github.com/Open-EO/openeo-js-client/compare/v1.3.0...v1.3.1 268 | [1.3.0]: https://github.com/Open-EO/openeo-js-client/compare/v1.2.0...v1.3.0 269 | [1.2.0]: https://github.com/Open-EO/openeo-js-client/compare/v1.1.0...v1.2.0 270 | [1.1.0]: https://github.com/Open-EO/openeo-js-client/compare/v1.0.3...v1.1.0 271 | [1.0.3]: https://github.com/Open-EO/openeo-js-client/compare/v1.0.2...v1.0.3 272 | [1.0.2]: https://github.com/Open-EO/openeo-js-client/compare/v1.0.1...v1.0.2 273 | [1.0.1]: https://github.com/Open-EO/openeo-js-client/compare/v1.0.0...v1.0.1 274 | [1.0.0]: https://github.com/Open-EO/openeo-js-client/compare/v1.0.0-rc.5...v1.0.0 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openeo-js-client 2 | 3 | JavaScript/TypeScript client for the openEO API. 4 | 5 | * [Documentation](https://open-eo.github.io/openeo-js-client/latest/). 6 | 7 | The version of this client is **2.8.0** and supports **openEO API versions 1.x.x**. 8 | Legacy versions are available as releases. 9 | See the [CHANGELOG](CHANGELOG.md) for recent changes. 10 | 11 | ## Usage 12 | 13 | This library can run in node.js or any recent browser supporting ECMAScript 2017. This excludes Internet Explorer, but includes Edge >= 15. 14 | 15 | An *experimental* Typescript declaration file is available so that you can use the library also in your TypeScript projects. 16 | 17 | ### Browser 18 | 19 | To use it in a browser environment simply add the following code to your HTML file: 20 | ```html 21 | 22 | 23 | 24 | 25 | ``` 26 | 27 | ### NodeJS 28 | 29 | To install it in a NodeJS environment run: 30 | `npm install @openeo/js-client` 31 | 32 | Afterwards, you can import the package: 33 | `const { OpenEO } = require('@openeo/js-client');` 34 | 35 | ### TypeScript 36 | 37 | Warning: The TypeScript integration is still **experimental**! Please help us improve it by opening issues or pull requests. 38 | 39 | To install it in a TypeScript environment run: 40 | `npm install @openeo/js-client` 41 | 42 | Afterwards, you can import the package: 43 | `import { OpenEO } from '@openeo/js-client';` 44 | 45 | ### Examples 46 | 47 | In the browser: 48 | * [Basic Discovery (promises)](examples/web/discovery.html) 49 | * [Run sync. job (async/await)](examples/web/workflow.html) 50 | 51 | In Node.js: 52 | * [Basic Discovery (promises)](examples/node/discovery.js) 53 | 54 | In Typescript: 55 | * [Basic Discovery (promises)](examples/typescript/discovery.ts) 56 | 57 | More information can be found in the [documentation](https://open-eo.github.io/openeo-js-client/latest/). 58 | 59 | ## Development 60 | 61 | ![JS Client Tests](https://github.com/Open-EO/openeo-js-client/workflows/JS%20Client%20Tests/badge.svg) 62 | 63 | Always make sure to adapt changes in the *.js files to the openeo.d.ts file. 64 | If changes are larger you may want to run `npm run tsd` and regenerate the declaration file and cherry-pick your changes from there. 65 | 66 | Generate a build: `npm run build` (generates `openeo.js` and `openeo.min.js`) 67 | 68 | Generate the documentation to the `docs/` folder: `npm run docs` 69 | 70 | Check against the coding guidelines: `npm run lint` 71 | 72 | Run tests: 73 | 74 | * `npm test` (all tests) 75 | * `npm test browser` (browser tests) 76 | * `npm test node` (node tests) 77 | * `npm test builder` (tests only the process builder) 78 | * `npm test earthengine` (full test suite using the Google Earth Engine back-end as server) 79 | 80 | # Contributions 81 | 82 | The authors acknowledge the financial support for the development of this package during the H2020 project "openEO" (Oct 2017 to Sept 2020) by the European Union, funded by call EO-2-2017: EO Big Data Shift, under grant number 776242. We also acknowledge the financial support received from ESA for the project "openEO Platform" (Sept 2020 to Sept 2023). 83 | 84 | This package received major contributions from the following organizations: 85 | 86 | [WWU Münster logo](https://www.uni-muenster.de/)   87 | [Solenix logo](https://www.solenix.ch) 88 | [Sinergise logo](https://www.sinergise.com) 89 | 90 | ## Interactive Web Editor 91 | 92 | There is an interactive web-based editor for coding using the openEO API, 93 | which is based on the JavaScript client. 94 | See [https://github.com/Open-EO/openeo-web-editor](https://github.com/Open-EO/openeo-web-editor) for more details. 95 | -------------------------------------------------------------------------------- /examples/node/discovery.js: -------------------------------------------------------------------------------- 1 | // Import the JS client 2 | const { OpenEO } = require('@openeo/js-client'); 3 | 4 | const url = "https://earthengine.openeo.org"; // Insert the openEO server URL here 5 | let connection = null; 6 | 7 | console.log('URL: ' + url); 8 | console.log('Client Version: ' + OpenEO.clientVersion()); 9 | 10 | OpenEO.connect(url) 11 | .then(c => { 12 | connection = c; 13 | return connection.capabilities(); 14 | }) 15 | .then(capabilities => { 16 | console.log('Server Version: ' + capabilities.apiVersion()); 17 | return connection.listCollections(); 18 | }) 19 | .then(collections => { 20 | console.log('Number of supported collections: ' + collections.collections.length); 21 | return connection.listProcesses(); 22 | }) 23 | .then(processes => { 24 | console.log('Number of supported processes: ' + processes.processes.length); 25 | }) 26 | .catch(err => console.error(err.message)); 27 | -------------------------------------------------------------------------------- /examples/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openeo/js-client-example-node", 3 | "version": "0.1.0", 4 | "author": "openEO Consortium", 5 | "contributors": [ 6 | { 7 | "name": "Christoph Friedrich" 8 | }, 9 | { 10 | "name": "Matthias Mohr" 11 | } 12 | ], 13 | "description": "Example showcasing the usage of the openEO JavaScript client in Node.js", 14 | "license": "Apache-2.0", 15 | "homepage": "http://openeo.org", 16 | "bugs": { 17 | "url": "https://github.com/Open-EO/openeo-js-client/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Open-EO/openeo-js-client.git" 22 | }, 23 | "main": "discovery.js", 24 | "dependencies": { 25 | "@openeo/js-client": "^1.0.0" 26 | }, 27 | "scripts": { 28 | "start": "node discovery.js" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/oidc/openid-connect-popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | openEO JS client - OpenID Connect example 6 | 7 | 8 | 9 | 10 | 11 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /examples/oidc/openid-connect-redirect.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | openEO JS client - OpenID Connect example 6 | 7 | 8 | 9 | 10 | 11 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /examples/typescript/.gitignore: -------------------------------------------------------------------------------- 1 | discovery.js 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /examples/typescript/discovery.ts: -------------------------------------------------------------------------------- 1 | // import the JS client main part (OpenEO) for the actual functionality 2 | // and some classes (Connection, Capabilities) and types (Collections, Processes) for TypeScript stuff 3 | import { OpenEO, Connection, Capabilities, Collections, Processes } from '@openeo/js-client'; 4 | 5 | let url: string = "https://earthengine.openeo.org"; // Insert the openEO server URL here 6 | let connection: Connection = null; // Reserve a variable for the connection and specify its type 7 | 8 | console.log('URL: ' + url); 9 | console.log('Client Version: ' + OpenEO.clientVersion()); 10 | 11 | OpenEO.connect(url) 12 | .then((c: Connection): Capabilities => { // specify parameter type and return type 13 | connection = c; 14 | return connection.capabilities(); 15 | }) 16 | .then((capabilities: Capabilities): Promise => { // as before, note the `Promise<>` generic 17 | console.log('Server Version: ' + capabilities.apiVersion()); 18 | return connection.listCollections(); 19 | }) 20 | .then((collections: Collections): Promise => { // note that `Promise` has become `Collections` 21 | console.log('Number of supported collections: ' + collections.collections.length); 22 | return connection.listProcesses(); 23 | }) 24 | .then((processes: Processes): void => { // final callback in chain doesn't return anything 25 | console.log('Number of supported processes: ' + processes.processes.length); 26 | return; 27 | }) 28 | .catch((err: Error) => console.error(err.message)); 29 | -------------------------------------------------------------------------------- /examples/typescript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openeo/js-client-example-typescript", 3 | "version": "0.1.0", 4 | "author": "openEO Consortium", 5 | "contributors": [ 6 | { 7 | "name": "Christoph Friedrich" 8 | }, 9 | { 10 | "name": "Matthias Mohr" 11 | } 12 | ], 13 | "description": "Example showcasing the usage of the openEO JavaScript client in Typescript", 14 | "license": "Apache-2.0", 15 | "homepage": "http://openeo.org", 16 | "bugs": { 17 | "url": "https://github.com/Open-EO/openeo-js-client/issues" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/Open-EO/openeo-js-client.git" 22 | }, 23 | "main": "discovery.ts", 24 | "dependencies": { 25 | "@openeo/js-client": "^1.0.0", 26 | "typescript": "^4.0.5" 27 | }, 28 | "scripts": { 29 | "build": "tsc discovery.ts", 30 | "start": "node discovery.js" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/web/discovery.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | openEO JS client - Discovery example 6 | 7 | 8 | 9 | 10 | 38 | 39 | 40 | 41 |

Server information

42 |

URL:

43 |

Versions

44 |
    45 |
  • Client Version: Loading...
  • 46 |
  • Server Version: Loading...
  • 47 |
48 |

EO Data Discovery

49 |

Number of supported collections: Loading...

50 |

Process Discovery

51 |

Number of supported processes: Loading...

52 | 53 | 54 | -------------------------------------------------------------------------------- /examples/web/workflow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | openEO JS client - Workflow example 6 | 7 | 8 | 9 | 10 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | // Indicates whether the coverage information should be collected while executing the test 4 | collectCoverage: true, 5 | 6 | globals: { 7 | 'oidc-client': require('oidc-client') 8 | }, 9 | 10 | moduleNameMapper: { 11 | "^@openeo/js-environment(.*)$": "/src$1", 12 | "axios": "/node_modules/axios/dist/node/axios.cjs" 13 | }, 14 | 15 | // The directory where Jest should output its coverage files 16 | coverageDirectory: "coverage", 17 | 18 | // Make calling deprecated APIs throw helpful error messages 19 | errorOnDeprecated: true, 20 | 21 | // Use this configuration option to add custom reporters to Jest 22 | "reporters": [ 23 | "default", 24 | ["./node_modules/jest-html-reporter", { 25 | "pageTitle": "Test Report for openeo-js-client", 26 | "outputPath": "./coverage/test-report.html" 27 | }] 28 | ] 29 | 30 | }; 31 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["./src/"], 3 | "compilerOptions": { 4 | "checkJs": true, 5 | "strict": true, 6 | "target": "es2017" 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@openeo/js-client", 3 | "version": "2.8.0", 4 | "author": "openEO Consortium", 5 | "contributors": [ 6 | { 7 | "name": "Matthias Mohr" 8 | }, 9 | { 10 | "name": "Christoph Friedrich" 11 | }, 12 | { 13 | "name": "Joep Neijt" 14 | } 15 | ], 16 | "description": "JavaScript client for the openEO API.", 17 | "license": "Apache-2.0", 18 | "homepage": "http://openeo.org", 19 | "bugs": { 20 | "url": "https://github.com/Open-EO/openeo-js-client/issues" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/Open-EO/openeo-js-client.git" 25 | }, 26 | "funding": { 27 | "type": "github", 28 | "url": "https://github.com/sponsors/m-mohr" 29 | }, 30 | "main": "src/openeo.js", 31 | "types": "openeo.d.ts", 32 | "files": [ 33 | "openeo.js", 34 | "openeo.d.ts", 35 | "openeo.min.js", 36 | "src/*" 37 | ], 38 | "devDependencies": { 39 | "@babel/core": "^7.12.9", 40 | "@babel/preset-env": "^7.12.7", 41 | "babel-loader": "^8.2.2", 42 | "eslint": "^7.14.0", 43 | "eslint-plugin-jsdoc": "^30.7.8", 44 | "jest": "^27.4.4", 45 | "jest-html-reporter": "^3.3.0", 46 | "jsdoc": "^4.0.0", 47 | "typescript": "^5.0.0", 48 | "unminified-webpack-plugin": "^3.0.0", 49 | "wait-for-expect": "^1.3.0", 50 | "webpack": "^5.65.0", 51 | "webpack-bundle-analyzer": "^4.0.0", 52 | "webpack-cli": "^4.9.1" 53 | }, 54 | "dependencies": { 55 | "@openeo/js-commons": "^1.5.0", 56 | "@radiantearth/stac-migrate": "^1.0.0", 57 | "axios": "^1.0.0", 58 | "oidc-client": "^1.11.5" 59 | }, 60 | "scripts": { 61 | "docs": "jsdoc -r -d docs/ -P package.json -R README.md -c .jsdoc", 62 | "build": "npm run lint && webpack", 63 | "tsd": "tsc --project tsconfig.json", 64 | "lint": "eslint src/", 65 | "test": " jest --env=jsdom --runInBand", 66 | "test_node": " jest --env=node --runInBand" 67 | }, 68 | "browserslist": [ 69 | "> 1%", 70 | "not ie > 0" 71 | ] 72 | } 73 | -------------------------------------------------------------------------------- /src/authprovider.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The base class for authentication providers such as Basic and OpenID Connect. 3 | * 4 | * @abstract 5 | */ 6 | class AuthProvider { 7 | 8 | /** 9 | * Creates a new OidcProvider instance to authenticate using OpenID Connect. 10 | * 11 | * @param {string} type - The type of the authentication procedure as specified by the API, e.g. `oidc` or `basic`. 12 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 13 | * @param {AuthProviderMeta} options - Options 14 | */ 15 | constructor(type, connection, options) { 16 | this.id = options.id || null; 17 | this.title = options.title || ""; 18 | this.description = options.description || ""; 19 | this.type = type; 20 | /** 21 | * @protected 22 | * @type {Connection} 23 | */ 24 | this.connection = connection; 25 | this.token = null; 26 | } 27 | 28 | /** 29 | * Get an identifier for the auth provider (combination of the type + provider identifier). 30 | * 31 | * @returns {string} 32 | */ 33 | getId() { 34 | let id = this.getType(); 35 | if (this.getProviderId().length > 0) { 36 | id += '.' + this.getProviderId(); 37 | } 38 | return id; 39 | } 40 | 41 | /** 42 | * Returns a display name for the authenticated user. 43 | * 44 | * @returns {string?} Name of the user or `null` 45 | */ 46 | getDisplayName() { 47 | return null; 48 | } 49 | 50 | /** 51 | * Returns the type of the authentication procedure as specified by the API, e.g. `oidc` or `basic`. 52 | * 53 | * @returns {string} 54 | */ 55 | getType() { 56 | return this.type; 57 | } 58 | 59 | /** 60 | * Returns the provider identifier, may not be available for all authentication methods. 61 | * 62 | * @returns {string} 63 | */ 64 | getProviderId() { 65 | return typeof this.id === 'string' ? this.id : ""; 66 | } 67 | 68 | /** 69 | * Returns the human-readable title for the authentication method / provider. 70 | * 71 | * @returns {string} 72 | */ 73 | getTitle() { 74 | return this.title; 75 | } 76 | 77 | /** 78 | * Returns the human-readable description for the authentication method / provider. 79 | * 80 | * @returns {string} 81 | */ 82 | getDescription() { 83 | return this.description; 84 | } 85 | 86 | /** 87 | * Returns the access token that is used as Bearer Token in API requests. 88 | * 89 | * Returns `null` if no access token has been set yet (i.e. not authenticated any longer). 90 | * 91 | * @returns {string | null} 92 | */ 93 | getToken() { 94 | if (typeof this.token === 'string') { 95 | return this.getType() + "/" + this.getProviderId() + "/" + this.token; 96 | } 97 | else { 98 | return null; 99 | } 100 | } 101 | 102 | /** 103 | * Sets the access token that is used as Bearer Token in API requests. 104 | * 105 | * Set to `null` to remove the access token. 106 | * 107 | * This also manages which auth provider is set for the connection. 108 | * 109 | * @param {?string} token 110 | */ 111 | setToken(token) { 112 | this.token = token; 113 | this.connection.emit('tokenChanged', token); 114 | if (this.token !== null) { 115 | this.connection.setAuthProvider(this); 116 | } 117 | else { 118 | this.connection.setAuthProvider(null); 119 | } 120 | } 121 | 122 | /** 123 | * Abstract method that extending classes implement the login process with. 124 | * 125 | * @async 126 | * @param {...*} args 127 | * @throws {Error} 128 | */ 129 | async login(...args) { 130 | throw new Error("Not implemented.", args); 131 | } 132 | 133 | /** 134 | * Logout from the established session. 135 | * 136 | * This is experimental and just removes the token for now. 137 | * May need to be overridden by sub-classes. 138 | * 139 | * @async 140 | */ 141 | async logout() { 142 | this.setToken(null); 143 | } 144 | 145 | } 146 | 147 | module.exports = AuthProvider; 148 | -------------------------------------------------------------------------------- /src/baseentity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The base class for entities such as Job, Process Graph, Service etc. 3 | * 4 | * @abstract 5 | */ 6 | class BaseEntity { 7 | 8 | /** 9 | * Creates an instance of this object. 10 | * 11 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 12 | * @param {Array.>} properties - A mapping from the API property names to the JS client property names (usually to convert between snake_case and camelCase), e.g. `["id", "title", ["process_graph", "processGraph"]]` 13 | */ 14 | constructor(connection, properties = []) { 15 | /** 16 | * @protected 17 | * @type {Connection} 18 | */ 19 | this.connection = connection; 20 | /** 21 | * @protected 22 | * @type {object.} 23 | */ 24 | this.apiToClientNames = {}; 25 | /** 26 | * @protected 27 | * @type {object.} 28 | */ 29 | this.clientToApiNames = {}; 30 | /** 31 | * @protected 32 | * @type {number} 33 | */ 34 | this.lastRefreshTime = 0; 35 | /** 36 | * Additional (non-standardized) properties received from the API. 37 | * 38 | * @protected 39 | * @type {object.} 40 | */ 41 | this.extra = {}; 42 | 43 | for(let i in properties) { 44 | let backend, client; 45 | if (Array.isArray(properties[i])) { 46 | backend = properties[i][0]; 47 | client = properties[i][1]; 48 | } 49 | else { 50 | backend = properties[i]; 51 | client = properties[i]; 52 | } 53 | this.apiToClientNames[backend] = client; 54 | this.clientToApiNames[client] = backend; 55 | } 56 | } 57 | 58 | /** 59 | * Returns a JSON serializable representation of the data that is API compliant. 60 | * 61 | * @returns {object.} 62 | */ 63 | toJSON() { 64 | let obj = {}; 65 | for(let key in this.clientToApiNames) { 66 | let apiKey = this.clientToApiNames[key]; 67 | if (typeof this[key] !== 'undefined') { 68 | obj[apiKey] = this[key]; 69 | } 70 | } 71 | return Object.assign(obj, this.extra); 72 | } 73 | 74 | /** 75 | * Converts the data from an API response into data suitable for our JS client models. 76 | * 77 | * @param {object.} metadata - JSON object originating from an API response. 78 | * @returns {BaseEntity} Returns the object itself. 79 | */ 80 | setAll(metadata) { 81 | for(let name in metadata) { 82 | if (typeof this.apiToClientNames[name] === 'undefined') { 83 | this.extra[name] = metadata[name]; 84 | } 85 | else { 86 | this[this.apiToClientNames[name]] = metadata[name]; 87 | } 88 | } 89 | this.lastRefreshTime = Date.now(); 90 | return this; 91 | } 92 | 93 | /** 94 | * Returns the age of the data in seconds. 95 | * 96 | * @returns {number} Age of the data in seconds as integer. 97 | */ 98 | getDataAge() { 99 | return (Date.now() - this.lastRefreshTime) / 1000; 100 | } 101 | 102 | /** 103 | * Returns all data in the model. 104 | * 105 | * @returns {object.} 106 | */ 107 | getAll() { 108 | let obj = {}; 109 | for(let backend in this.apiToClientNames) { 110 | let client = this.apiToClientNames[backend]; 111 | if (typeof this[client] !== 'undefined') { 112 | obj[client] = this[client]; 113 | } 114 | } 115 | return Object.assign(obj, this.extra); 116 | } 117 | 118 | /** 119 | * Get a value from the additional data that is not part of the core model, i.e. from proprietary extensions. 120 | * 121 | * @param {string} name - Name of the property. 122 | * @returns {*} The value, which could be of any type. 123 | */ 124 | get(name) { 125 | return typeof this.extra[name] !== 'undefined' ? this.extra[name] : null; 126 | } 127 | 128 | /** 129 | * Converts the object to a valid objects for API requests. 130 | * 131 | * @param {object.} parameters 132 | * @returns {object.} 133 | * @protected 134 | */ 135 | _convertToRequest(parameters) { 136 | let request = {}; 137 | for(let key in parameters) { 138 | if (typeof this.clientToApiNames[key] === 'undefined') { 139 | request[key] = parameters[key]; 140 | } 141 | else { 142 | request[this.clientToApiNames[key]] = parameters[key]; 143 | } 144 | } 145 | return request; 146 | } 147 | 148 | /** 149 | * Checks whether a features is supported by the API. 150 | * 151 | * @param {string} feature 152 | * @returns {boolean} 153 | * @protected 154 | * @see Capabilities#hasFeature 155 | */ 156 | _supports(feature) { 157 | return this.connection.capabilities().hasFeature(feature); 158 | } 159 | 160 | } 161 | 162 | module.exports = BaseEntity; 163 | -------------------------------------------------------------------------------- /src/basicprovider.js: -------------------------------------------------------------------------------- 1 | const Environment = require('./env'); 2 | const Utils = require('@openeo/js-commons/src/utils'); 3 | const AuthProvider = require('./authprovider'); 4 | 5 | /** 6 | * The Authentication Provider for HTTP Basic. 7 | * 8 | * @augments AuthProvider 9 | */ 10 | class BasicProvider extends AuthProvider { 11 | 12 | /** 13 | * Creates a new BasicProvider instance to authenticate using HTTP Basic. 14 | * 15 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 16 | */ 17 | constructor(connection) { 18 | super("basic", connection, { 19 | id: null, 20 | title: "HTTP Basic", 21 | description: "Login with username and password using the method HTTP Basic." 22 | }); 23 | this.username = null; 24 | } 25 | 26 | /** 27 | * Authenticate with HTTP Basic. 28 | * 29 | * @async 30 | * @param {string} username 31 | * @param {string} password 32 | * @returns {Promise} 33 | * @throws {Error} 34 | */ 35 | async login(username, password) { 36 | let response = await this.connection._send({ 37 | method: 'get', 38 | responseType: 'json', 39 | url: '/credentials/basic', 40 | headers: {'Authorization': 'Basic ' + Environment.base64encode(username + ':' + password)} 41 | }); 42 | if (!Utils.isObject(response.data) || typeof response.data.access_token !== 'string') { 43 | throw new Error("No access_token returned."); 44 | } 45 | this.username = username; 46 | this.setToken(response.data.access_token); 47 | } 48 | 49 | /** 50 | * Returns a display name for the authenticated user. 51 | * 52 | * @returns {string?} Name of the user or `null` 53 | */ 54 | getDisplayName() { 55 | return this.username; 56 | } 57 | 58 | /** 59 | * Logout from the established session. 60 | * 61 | * @async 62 | */ 63 | async logout() { 64 | this.username = null; 65 | await super.logout(); 66 | } 67 | 68 | } 69 | 70 | module.exports = BasicProvider; -------------------------------------------------------------------------------- /src/browser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Platform dependant utilities for the openEO JS Client. 3 | * 4 | * Browser implementation, don't use in other environments. 5 | * 6 | * @hideconstructor 7 | */ 8 | class Environment { 9 | 10 | /** 11 | * Returns the name of the Environment, here `Browser`. 12 | * 13 | * @returns {string} 14 | * @static 15 | */ 16 | static getName() { 17 | return 'Browser'; 18 | } 19 | 20 | /** 21 | * Returns the current URL of the browser window. 22 | * 23 | * @returns {string} 24 | * @static 25 | */ 26 | static getUrl() { 27 | return window.location.toString(); 28 | } 29 | 30 | /** 31 | * Sets the URL. 32 | * 33 | * Not supported in Browsers and only throws an Error! 34 | * 35 | * @param {string} uri 36 | * @static 37 | */ 38 | static setUrl(uri) { // eslint-disable-line no-unused-vars 39 | throw new Error("setUrl is not supported in a browser environment."); 40 | } 41 | 42 | /** 43 | * Handles errors from the API that are returned as Blobs. 44 | * 45 | * @ignore 46 | * @static 47 | * @param {Blob} error 48 | * @returns {Promise} 49 | */ 50 | static handleErrorResponse(error) { 51 | return new Promise((resolve, reject) => { 52 | let fileReader = new FileReader(); 53 | fileReader.onerror = event => { 54 | fileReader.abort(); 55 | reject(event.target.error); 56 | }; 57 | fileReader.onload = () => { 58 | // ArrayBuffer to String conversion is from https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String 59 | let res = fileReader.result instanceof ArrayBuffer ? String.fromCharCode.apply(null, new Uint16Array(fileReader.result)) : fileReader.result; 60 | let obj = typeof res === 'string' ? JSON.parse(res) : res; 61 | resolve(obj); 62 | }; 63 | fileReader.readAsText(error.response.data); 64 | }); 65 | } 66 | 67 | /** 68 | * Returns how binary responses from the servers are returned (`stream` or `blob`). 69 | * 70 | * @returns {string} 71 | * @static 72 | */ 73 | static getResponseType() { 74 | return 'blob'; 75 | } 76 | 77 | /** 78 | * Encodes a string into Base64 encoding. 79 | * 80 | * @static 81 | * @param {string} str - String to encode. 82 | * @returns {string} String encoded in Base64. 83 | */ 84 | static base64encode(str) { 85 | // btoa is JS's ugly name for encodeBase64 86 | return btoa(str); 87 | } 88 | 89 | /** 90 | * Detect the file name for the given data source. 91 | * 92 | * @ignore 93 | * @static 94 | * @param {*} source - An object from a file upload form. 95 | * @returns {string} 96 | */ 97 | static fileNameForUpload(source) { 98 | return source.name.split(/(\\|\/)/g).pop(); 99 | } 100 | 101 | /** 102 | * Get the data from the source that should be uploaded. 103 | * 104 | * @ignore 105 | * @static 106 | * @param {*} source - An object from a file upload form. 107 | * @returns {*} 108 | */ 109 | static dataForUpload(source) { 110 | return source; 111 | } 112 | 113 | /** 114 | * Downloads files to local storage and returns a list of file paths. 115 | * 116 | * Not supported in Browsers and only throws an Error! 117 | * 118 | * @static 119 | * @param {Connection} con 120 | * @param {Array.>} assets 121 | * @param {string} targetFolder 122 | * @throws {Error} 123 | */ 124 | static async downloadResults(con, assets, targetFolder) { // eslint-disable-line no-unused-vars 125 | throw new Error("downloadResults is not supported in a browser environment."); 126 | } 127 | 128 | /** 129 | * Offers data to download in the browser. 130 | * 131 | * This method may fail with overly big data. 132 | * 133 | * @async 134 | * @static 135 | * @param {*} data - Data to download. 136 | * @param {string} filename - File name that is suggested to the user. 137 | * @returns {Promise} 138 | * @see https://github.com/kennethjiang/js-file-download/blob/master/file-download.js 139 | */ 140 | static saveToFile(data, filename) { 141 | /* istanbul ignore next */ 142 | return new Promise((resolve, reject) => { 143 | try { 144 | if (!(data instanceof Blob)) { 145 | data = new Blob([data], {type: 'application/octet-stream'}); 146 | } 147 | let blobURL = window.URL.createObjectURL(data); 148 | let tempLink = document.createElement('a'); 149 | tempLink.style.display = 'none'; 150 | tempLink.href = blobURL; 151 | tempLink.setAttribute('download', filename || 'download'); 152 | if (typeof tempLink.download === 'undefined') { 153 | tempLink.setAttribute('target', '_blank'); 154 | } 155 | document.body.appendChild(tempLink); 156 | tempLink.click(); 157 | document.body.removeChild(tempLink); 158 | window.URL.revokeObjectURL(blobURL); 159 | resolve(); 160 | } catch (error) { 161 | console.error(error); 162 | reject(error); 163 | } 164 | }); 165 | } 166 | } 167 | 168 | module.exports = Environment; 169 | -------------------------------------------------------------------------------- /src/builder/formula.js: -------------------------------------------------------------------------------- 1 | const TapDigit = require("./tapdigit"); 2 | const Parameter = require("./parameter"); 3 | const BuilderNode = require('./node'); 4 | 5 | /** 6 | * This converts a mathematical formula into a openEO process for you. 7 | * 8 | * Operators: - (subtract), + (add), / (divide), * (multiply), ^ (power) 9 | * 10 | * It supports all mathematical functions (i.e. expects a number and returns a number) the back-end implements, e.g. `sqrt(x)`. 11 | * For namespaced processes, use for example `process@namespace(x)` - EXPERIMENTAL! 12 | * 13 | * Only available if a builder is specified in the constructor: 14 | * You can refer to output from processes with a leading `#`, e.g. `#loadco1` if the node to refer to has the key `loadco1`. 15 | * 16 | * Only available if a parent node is set via `setNode()`: 17 | * Parameters can be accessed simply by name. 18 | * If the first parameter is a (labeled) array, the value for a specific index or label can be accessed by typing the numeric index or textual label with a `$` in front, for example `$B1` for the label `B1` or `$0` for the first element in the array. Numeric labels are not supported. 19 | * You can access subsequent parameters by adding additional `$` at the beginning, e.g. `$$0` to access the first element of an array in the second parameter, `$$$0` for the same in the third parameter etc. 20 | * 21 | * An example that computes an EVI (assuming the labels for the bands are `NIR`, `RED` and `BLUE`): `2.5 * ($NIR - $RED) / (1 + $NIR + 6 * $RED + (-7.5 * $BLUE))` 22 | */ 23 | class Formula { 24 | 25 | /** 26 | * Creates a math formula object. 27 | * 28 | * @param {string} formula - A mathematical formula to parse.y 29 | */ 30 | constructor(formula) { 31 | let parser = new TapDigit.Parser(); 32 | /** 33 | * @type {object.} 34 | */ 35 | this.tree = parser.parse(formula); 36 | /** 37 | * @type {Builder | null} 38 | */ 39 | this.builder = null; 40 | } 41 | 42 | /** 43 | * The builder instance to use. 44 | * 45 | * @param {Builder} builder - The builder instance to add the formula to. 46 | */ 47 | setBuilder(builder) { 48 | this.builder = builder; 49 | } 50 | 51 | /** 52 | * Generates the processes for the formula specified in the constructor. 53 | * 54 | * Returns the last node that computes the result. 55 | * 56 | * @param {boolean} setResultNode - Set the `result` flag to `true`. 57 | * @returns {BuilderNode} 58 | * @throws {Error} 59 | */ 60 | generate(setResultNode = true) { 61 | let finalNode = this.parseTree(this.tree); 62 | if (!(finalNode instanceof BuilderNode)) { 63 | throw new Error('Invalid formula specified.'); 64 | } 65 | // Set result node 66 | if (setResultNode) { 67 | finalNode.result = true; 68 | } 69 | return finalNode; 70 | } 71 | 72 | /** 73 | * Walks through the tree generated by the TapDigit parser and generates process nodes. 74 | * 75 | * @protected 76 | * @param {object.} tree 77 | * @returns {object.} 78 | * @throws {Error} 79 | */ 80 | parseTree(tree) { 81 | let key = Object.keys(tree)[0]; // There's never more than one property so no loop required 82 | switch(key) { 83 | case 'Number': 84 | return parseFloat(tree.Number); 85 | case 'Identifier': 86 | return this.getRef(tree.Identifier); 87 | case 'Expression': 88 | return this.parseTree(tree.Expression); 89 | case 'FunctionCall': { 90 | let args = []; 91 | for(let i in tree.FunctionCall.args) { 92 | args.push(this.parseTree(tree.FunctionCall.args[i])); 93 | } 94 | return this.builder.process(tree.FunctionCall.name, args); 95 | } 96 | case 'Binary': 97 | return this.addOperatorProcess( 98 | tree.Binary.operator, 99 | this.parseTree(tree.Binary.left), 100 | this.parseTree(tree.Binary.right) 101 | ); 102 | case 'Unary': { 103 | let val = this.parseTree(tree.Unary.expression); 104 | if (tree.Unary.operator === '-') { 105 | if (typeof val === 'number') { 106 | return -val; 107 | } 108 | else { 109 | return this.addOperatorProcess('*', -1, val); 110 | } 111 | } 112 | else { 113 | return val; 114 | } 115 | } 116 | default: 117 | throw new Error('Operation ' + key + ' not supported.'); 118 | } 119 | } 120 | 121 | /** 122 | * Gets the reference for a value, e.g. from_node or from_parameter. 123 | * 124 | * @protected 125 | * @param {*} value 126 | * @returns {*} 127 | */ 128 | getRef(value) { 129 | // Convert native data types 130 | if (value === 'true') { 131 | return true; 132 | } 133 | else if (value === 'false') { 134 | return false; 135 | } 136 | else if (value === 'null') { 137 | return null; 138 | } 139 | 140 | // Output of a process 141 | if (typeof value === 'string' && value.startsWith('#')) { 142 | let nodeId = value.substring(1); 143 | if (nodeId in this.builder.nodes) { 144 | return { from_node: nodeId }; 145 | } 146 | } 147 | 148 | let callbackParams = this.builder.getParentCallbackParameters(); 149 | // Array labels / indices 150 | if (typeof value === 'string' && callbackParams.length > 0) { 151 | let prefix = value.match(/^\$+/); 152 | let count = prefix ? prefix[0].length : 0; 153 | if (count > 0 && callbackParams.length >= count) { 154 | let ref = value.substring(count); 155 | return callbackParams[count-1][ref]; 156 | } 157 | } 158 | 159 | // Everything else is a parameter 160 | let parameter = new Parameter(value); 161 | // Add new parameter if it doesn't exist 162 | this.builder.addParameter(parameter); 163 | return parameter; 164 | } 165 | 166 | /** 167 | * Adds a process node for an operator like +, -, *, / etc. 168 | * 169 | * @param {string} operator - The operator. 170 | * @param {number|object.} left - The left part for the operator. 171 | * @param {number|object.} right - The right part for the operator. 172 | * @returns {BuilderNode} 173 | * @throws {Error} 174 | */ 175 | addOperatorProcess(operator, left, right) { 176 | let processName = Formula.operatorMapping[operator]; 177 | let process = this.builder.spec(processName); 178 | if (processName && process) { 179 | let args = {}; 180 | if (!Array.isArray(process.parameters) || process.parameters.length < 2) { 181 | throw new Error("Process for operator " + operator + " must have at least two parameters"); 182 | } 183 | args[process.parameters[0].name || 'x'] = left; 184 | args[process.parameters[1].name || 'y'] = right; 185 | return this.builder.process(processName, args); 186 | } 187 | else { 188 | throw new Error('Operator ' + operator + ' not supported'); 189 | } 190 | } 191 | 192 | } 193 | 194 | /** 195 | * List of supported operators. 196 | * 197 | * All operators must have the parameters be name x and y. 198 | * 199 | * The key is the mathematical operator, the value is the process identifier. 200 | * 201 | * @type {object.} 202 | */ 203 | Formula.operatorMapping = { 204 | "-": "subtract", 205 | "+": "add", 206 | "/": "divide", 207 | "*": "multiply", 208 | "^": "power" 209 | }; 210 | 211 | module.exports = Formula; 212 | -------------------------------------------------------------------------------- /src/builder/node.js: -------------------------------------------------------------------------------- 1 | const Utils = require("@openeo/js-commons/src/utils"); 2 | const Parameter = require("./parameter"); 3 | 4 | /** 5 | * A class that represents a process node and also a result from a process. 6 | */ 7 | class BuilderNode { 8 | 9 | /** 10 | * Creates a new process node for the builder. 11 | * 12 | * @param {Builder} parent 13 | * @param {string} processId 14 | * @param {object.} [processArgs={}] 15 | * @param {?string} [processDescription=null] 16 | * @param {?string} [processNamespace=null] 17 | */ 18 | constructor(parent, processId, processArgs = {}, processDescription = null, processNamespace = null) { 19 | /** 20 | * The parent builder. 21 | * @type {Builder} 22 | */ 23 | this.parent = parent; 24 | 25 | /** 26 | * The specification of the process associated with this node. 27 | * @type {Process} 28 | * @readonly 29 | */ 30 | this.spec = this.parent.spec(processId, processNamespace); 31 | if (!this.spec) { 32 | throw new Error("Process doesn't exist: " + processId); 33 | } 34 | 35 | /** 36 | * The unique identifier for the node (not the process ID!). 37 | * @type {string} 38 | */ 39 | this.id = parent.generateId(processId); 40 | /** 41 | * The namespace of the process - EXPERIMENTAL! 42 | * @type {string} 43 | */ 44 | this.namespace = processNamespace; 45 | /** 46 | * The arguments for the process. 47 | * @type {object.} 48 | */ 49 | this.arguments = Array.isArray(processArgs) ? this.namedArguments(processArgs) : processArgs; 50 | /** 51 | * @ignore 52 | */ 53 | this._description = processDescription; 54 | /** 55 | * Is this the result node? 56 | * @type {boolean} 57 | */ 58 | this.result = false; 59 | 60 | this.addParametersToProcess(this.arguments); 61 | } 62 | 63 | /** 64 | * Converts a sorted array of arguments to an object with the respective parameter names. 65 | * 66 | * @param {Array.>} processArgs 67 | * @returns {object.} 68 | * @throws {Error} 69 | */ 70 | namedArguments(processArgs) { 71 | if (processArgs.length > (this.spec.parameters || []).length) { 72 | throw new Error("More arguments specified than parameters available."); 73 | } 74 | let obj = {}; 75 | if (Array.isArray(this.spec.parameters)) { 76 | for(let i = 0; i < this.spec.parameters.length; i++) { 77 | obj[this.spec.parameters[i].name] = processArgs[i]; 78 | } 79 | } 80 | return obj; 81 | } 82 | 83 | /** 84 | * Checks the arguments given for parameters and add them to the process. 85 | * 86 | * @param {object.|Array} processArgs 87 | */ 88 | addParametersToProcess(processArgs) { 89 | for(let key in processArgs) { 90 | let arg = processArgs[key]; 91 | if (arg instanceof Parameter) { 92 | if (Utils.isObject(arg.spec.schema)) { 93 | this.parent.addParameter(arg.spec); 94 | } 95 | } 96 | else if (arg instanceof BuilderNode) { 97 | this.addParametersToProcess(arg.arguments); 98 | } 99 | else if (Array.isArray(arg) || Utils.isObject(arg)) { 100 | this.addParametersToProcess(arg); 101 | } 102 | } 103 | } 104 | 105 | /** 106 | * Gets/Sets a description for the node. 107 | * 108 | * Can be used in a variety of ways: 109 | * 110 | * By default, this is a function: 111 | * `node.description()` - Returns the description. 112 | * `node.description("foo")` - Sets the description to "foo". Returns the node itself for method chaining. 113 | * 114 | * You can also "replace" the function (not supported in TypeScript!), 115 | * then it acts as normal property and the function is not available any longer: 116 | * `node.description = "foo"` - Sets the description to "foo". 117 | * Afterwards you can call `node.description` as normal object property. 118 | * 119 | * @param {string|undefined} description - Optional: If given, set the value. 120 | * @returns {string|BuilderNode} 121 | */ 122 | description(description) { 123 | if (typeof description === 'undefined') { 124 | return this._description; 125 | } 126 | else { 127 | this._description = description; 128 | return this; 129 | } 130 | } 131 | 132 | /** 133 | * Converts the given argument into something serializable... 134 | * 135 | * @protected 136 | * @param {*} arg - Argument 137 | * @param {string} name - Parameter name 138 | * @returns {*} 139 | */ 140 | exportArgument(arg, name) { 141 | const Formula = require('./formula'); 142 | if (Utils.isObject(arg)) { 143 | if (arg instanceof BuilderNode || arg instanceof Parameter) { 144 | return arg.ref(); 145 | } 146 | else if (arg instanceof Formula) { 147 | let builder = this.createBuilder(this, name); 148 | arg.setBuilder(builder); 149 | arg.generate(); 150 | return builder.toJSON(); 151 | } 152 | else if (arg instanceof Date) { 153 | return arg.toISOString(); 154 | } 155 | else if (typeof arg.toJSON === 'function') { 156 | return arg.toJSON(); 157 | } 158 | else { 159 | let obj = {}; 160 | for(let key in arg) { 161 | if (typeof arg[key] !== 'undefined') { 162 | obj[key] = this.exportArgument(arg[key], name); 163 | } 164 | } 165 | return obj; 166 | } 167 | } 168 | else if (Array.isArray(arg)) { 169 | return arg.map(element => this.exportArgument(element), name); 170 | } 171 | // export child process graph 172 | else if (typeof arg === 'function') { 173 | return this.exportCallback(arg, name); 174 | } 175 | else { 176 | return arg; 177 | } 178 | } 179 | 180 | /** 181 | * Creates a new Builder, usually for a callback. 182 | * 183 | * @protected 184 | * @param {?BuilderNode} [parentNode=null] 185 | * @param {?string} [parentParameter=null] 186 | * @returns {BuilderNode} 187 | */ 188 | createBuilder(parentNode = null, parentParameter = null) { 189 | const Builder = require('./builder'); 190 | let builder = new Builder(this.parent.processes, this.parent); 191 | if (parentNode !== null && parentParameter !== null) { 192 | builder.setParent(parentNode, parentParameter); 193 | } 194 | return builder; 195 | } 196 | 197 | /** 198 | * Returns the serializable process for the callback function given. 199 | * 200 | * @protected 201 | * @param {Function} arg - callback function 202 | * @param {string} name - Parameter name 203 | * @returns {object.} 204 | * @throws {Error} 205 | */ 206 | exportCallback(arg, name) { 207 | let builder = this.createBuilder(this, name); 208 | let params = builder.getParentCallbackParameters(); 209 | // Bind builder to this, so that this.xxx can be used for processes 210 | // Also pass builder as last parameter so that we can grab it in arrow functions 211 | let node = arg.bind(builder)(...params, builder); 212 | if (Array.isArray(node) && builder.supports('array_create')) { 213 | node = builder.array_create(node); 214 | } 215 | else if (!Utils.isObject(node) && builder.supports('constant')) { 216 | node = builder.constant(node); 217 | } 218 | if (node instanceof BuilderNode) { 219 | node.result = true; 220 | return builder.toJSON(); 221 | } 222 | else { 223 | throw new Error("Callback must return BuilderNode"); 224 | } 225 | } 226 | 227 | /** 228 | * Returns a JSON serializable representation of the data that is API compliant. 229 | * 230 | * @returns {object.} 231 | */ 232 | toJSON() { 233 | let obj = { 234 | process_id: this.spec.id, 235 | arguments: {} 236 | }; 237 | if (this.namespace) { 238 | obj.namespace = this.namespace; 239 | } 240 | for(let name in this.arguments) { 241 | if (typeof this.arguments[name] !== 'undefined') { 242 | obj.arguments[name] = this.exportArgument(this.arguments[name], name); 243 | } 244 | } 245 | if (typeof this.description !== 'function') { 246 | obj.description = this.description; 247 | } 248 | else if (typeof this._description === 'string') { 249 | obj.description = this._description; 250 | } 251 | if (this.result) { 252 | obj.result = true; 253 | } 254 | return obj; 255 | } 256 | 257 | /** 258 | * Returns the reference object for this node. 259 | * 260 | * @returns {FromNode} 261 | */ 262 | ref() { 263 | return { from_node: this.id }; 264 | } 265 | 266 | } 267 | 268 | module.exports = BuilderNode; 269 | -------------------------------------------------------------------------------- /src/builder/parameter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * A class that represents a process parameter. 5 | * 6 | * This is used for two things: 7 | * 1. You can create process parameters (placeholders) with `new Parameter()`. 8 | * 2. This is passed to functions for the parameters of the sub-process. 9 | * 10 | * For the second case, you can access array elements referred to by the parameter 11 | * with a simplified notation: 12 | * 13 | * ``` 14 | * function(data, context) { 15 | * data['B1'] // Accesses the B1 element of the array by label 16 | * data[1] // Accesses the second element of the array by index 17 | * } 18 | * ``` 19 | * 20 | * Those array calls create corresponding `array_element` nodes in the process. So it's 21 | * equivalent to 22 | * `this.array_element(data, undefined, 'B1')` or 23 | * `this.array_element(data, 1)` respectively. 24 | * 25 | * Simple access to numeric labels is not supported. You need to use `array_element` directly, e.g. 26 | * `this.array_element(data, undefined, 1)`. 27 | */ 28 | class Parameter { 29 | 30 | /** 31 | * Creates a new parameter instance, but proxies calls to it 32 | * so that array access is possible (see class description). 33 | * 34 | * @static 35 | * @param {Builder} builder 36 | * @param {string} parameterName 37 | * @returns {Proxy} 38 | */ 39 | static create(builder, parameterName) { 40 | let parameter = new Parameter(parameterName, null); 41 | if (typeof Proxy !== "undefined") { 42 | return new Proxy(parameter, { 43 | // @ts-ignore 44 | nodeCache: {}, 45 | /** 46 | * Getter for array access (see class description). 47 | * 48 | * @ignore 49 | * @param {object} target 50 | * @param {string|number|symbol} name 51 | * @param {?*} receiver 52 | * @returns {*} 53 | */ 54 | get(target, name, receiver) { 55 | if (!Reflect.has(target, name)) { 56 | // @ts-ignore 57 | if (!this.nodeCache[name]) { 58 | let args = { 59 | data: parameter 60 | }; 61 | if (typeof name === 'string' && name.match(/^(0|[1-9]\d*)$/)) { 62 | args.index = parseInt(name, 10); 63 | } 64 | else { 65 | args.label = name; 66 | } 67 | // We assume array_element exists 68 | // @ts-ignore 69 | this.nodeCache[name] = builder.process("array_element", args); 70 | } 71 | 72 | // @ts-ignore 73 | return this.nodeCache[name]; 74 | } 75 | return Reflect.get(target, name, receiver); 76 | }, 77 | /** 78 | * Setter for array access. 79 | * 80 | * Usually fails as write access to arrays is not supported. 81 | * 82 | * @ignore 83 | * @param {object} target 84 | * @param {string|number|symbol} name 85 | * @param {*} value 86 | * @param {?*} receiver 87 | * @returns {boolean} 88 | */ 89 | set(target, name, value, receiver) { 90 | if (!Reflect.has(target, name)) { 91 | throw new Error('Simplified array access is read-only'); 92 | } 93 | return Reflect.set(target, name, value, receiver); 94 | } 95 | }); 96 | } 97 | else { 98 | throw new Error('Simplified array access not supported, use array_element directly'); 99 | } 100 | } 101 | 102 | /** 103 | * Creates a new process parameter. 104 | * 105 | * @param {string} name - Name of the parameter. 106 | * @param {object.|string} schema - The schema for the parameter. Can be either an object compliant to JSON Schema or a string with a JSON Schema compliant data type, e.g. `string`. 107 | * @param {string} description - A description for the parameter 108 | * @param {*} defaultValue - An optional default Value for the parameter. If set, make the parameter optional. If not set, the parameter is required. Defaults to `undefined`. 109 | */ 110 | constructor(name, schema = {}, description = "", defaultValue = undefined) { 111 | this.name = name; 112 | this.spec = { 113 | name: name, 114 | schema: typeof schema === 'string' ? { type: schema } : schema, 115 | description: description, 116 | }; 117 | // No support for experimental and deprecated yet 118 | if (typeof defaultValue !== 'undefined') { 119 | this.spec.optional = true; 120 | this.spec.default = defaultValue; 121 | } 122 | } 123 | 124 | /** 125 | * Returns a JSON serializable representation of the data that is API compliant. 126 | * 127 | * @returns {object.} 128 | */ 129 | toJSON() { 130 | return this.spec; 131 | } 132 | 133 | /** 134 | * Returns the reference object for this parameter. 135 | * 136 | * @returns {FromParameter} 137 | */ 138 | ref() { 139 | return { from_parameter: this.name }; 140 | } 141 | 142 | } 143 | 144 | module.exports = Parameter; 145 | -------------------------------------------------------------------------------- /src/capabilities.js: -------------------------------------------------------------------------------- 1 | const Utils = require('@openeo/js-commons/src/utils'); 2 | 3 | const FEATURE_MAP = { 4 | // Discovery 5 | capabilities: true, 6 | listFileTypes: 'get /file_formats', 7 | listServiceTypes: 'get /service_types', 8 | listUdfRuntimes: 'get /udf_runtimes', 9 | // Collections 10 | listCollections: 'get /collections', 11 | describeCollection: 'get /collections/{}', 12 | listCollectionItems: 'get /collections/{}/items', 13 | describeCollectionItem: 'get /collections/{}/items/{}', 14 | describeCollectionQueryables: 'get /collections/{}/queryables', 15 | // Processes 16 | listProcesses: 'get /processes', 17 | describeProcess: 'get /processes', 18 | // Auth / Account 19 | listAuthProviders: true, 20 | authenticateOIDC: 'get /credentials/oidc', 21 | authenticateBasic: 'get /credentials/basic', 22 | describeAccount: 'get /me', 23 | // Files 24 | listFiles: 'get /files', 25 | getFile: 'get /files', // getFile is a virtual function and doesn't request an endpoint, but get /files should be available nevertheless. 26 | uploadFile: 'put /files/{}', 27 | downloadFile: 'get /files/{}', 28 | deleteFile: 'delete /files/{}', 29 | // User-Defined Processes 30 | validateProcess: 'post /validation', 31 | listUserProcesses: 'get /process_graphs', 32 | describeUserProcess: 'get /process_graphs/{}', 33 | getUserProcess: 'get /process_graphs/{}', 34 | setUserProcess: 'put /process_graphs/{}', 35 | replaceUserProcess: 'put /process_graphs/{}', 36 | deleteUserProcess: 'delete /process_graphs/{}', 37 | // Processing 38 | computeResult: 'post /result', 39 | listJobs: 'get /jobs', 40 | createJob: 'post /jobs', 41 | listServices: 'get /services', 42 | createService: 'post /services', 43 | getJob: 'get /jobs/{}', 44 | describeJob: 'get /jobs/{}', 45 | updateJob: 'patch /jobs/{}', 46 | deleteJob: 'delete /jobs/{}', 47 | estimateJob: 'get /jobs/{}/estimate', 48 | debugJob: 'get /jobs/{}/logs', 49 | startJob: 'post /jobs/{}/results', 50 | stopJob: 'delete /jobs/{}/results', 51 | listResults: 'get /jobs/{}/results', 52 | downloadResults: 'get /jobs/{}/results', 53 | // Web services 54 | describeService: 'get /services/{}', 55 | getService: 'get /services/{}', 56 | updateService: 'patch /services/{}', 57 | deleteService: 'delete /services/{}', 58 | debugService: 'get /services/{}/logs', 59 | }; 60 | 61 | /** 62 | * Capabilities of a back-end. 63 | */ 64 | class Capabilities { 65 | 66 | /** 67 | * Creates a new Capabilities object from an API-compatible JSON response. 68 | * 69 | * @param {object.} data - A capabilities response compatible to the API specification for `GET /`. 70 | * @throws {Error} 71 | */ 72 | constructor(data) { 73 | 74 | /** 75 | * @private 76 | * @type {object.} 77 | */ 78 | this.data = data; 79 | 80 | /** 81 | * @private 82 | * @ignore 83 | * @type {object.} 84 | */ 85 | this.featureMap = FEATURE_MAP; 86 | 87 | /** 88 | * @private 89 | * @type {Array.} 90 | */ 91 | this.features = []; 92 | 93 | this.validate(); 94 | this.init(); 95 | } 96 | 97 | /** 98 | * Validates the capabilities. 99 | * 100 | * Throws an error in case of an issue, otherwise just passes. 101 | * 102 | * @protected 103 | * @throws {Error} 104 | */ 105 | validate() { 106 | if(!Utils.isObject(this.data)) { 107 | throw new Error("No capabilities retrieved."); 108 | } 109 | else if(!this.data.api_version) { 110 | throw new Error("Invalid capabilities: No API version retrieved"); 111 | } 112 | else if(!Array.isArray(this.data.endpoints)) { 113 | throw new Error("Invalid capabilities: No endpoints retrieved"); 114 | } 115 | } 116 | 117 | /** 118 | * Initializes the class. 119 | * 120 | * @protected 121 | */ 122 | init() { 123 | this.features = this.data.endpoints 124 | // Flatten features and simplify variables to be compatible with the feature map. 125 | .map(e => e.methods.map(method => { 126 | const path = e.path.replace(/\{[^}]+\}/g, '{}'); 127 | return `${method} ${path}`.toLowerCase(); 128 | })) 129 | .reduce((flat, next) => flat.concat(next), []); // .flat(1) once browser support for ECMAscript 10/2019 gets better 130 | } 131 | 132 | /** 133 | * Returns the capabilities response as a JSON serializable representation of the data that is API compliant. 134 | * 135 | * @returns {object.} - A reference to the capabilities response. 136 | */ 137 | toJSON() { 138 | return this.data; 139 | } 140 | 141 | /** 142 | * Returns the openEO API version implemented by the back-end. 143 | * 144 | * @returns {string} openEO API version number. 145 | */ 146 | apiVersion() { 147 | return this.data.api_version; 148 | } 149 | 150 | /** 151 | * Returns the back-end version number. 152 | * 153 | * @returns {string} openEO back-end version number. 154 | */ 155 | backendVersion() { 156 | return this.data.backend_version; 157 | } 158 | 159 | /** 160 | * Returns the back-end title. 161 | * 162 | * @returns {string} Title 163 | */ 164 | title() { 165 | return typeof this.data.title === 'string' ? this.data.title : ""; 166 | } 167 | 168 | /** 169 | * Returns the back-end description. 170 | * 171 | * @returns {string} Description 172 | */ 173 | description() { 174 | return typeof this.data.description === 'string' ? this.data.description : ""; 175 | } 176 | 177 | /** 178 | * Is the back-end suitable for use in production? 179 | * 180 | * @returns {boolean} true = stable/production, false = unstable 181 | */ 182 | isStable() { 183 | return this.data.production === true; 184 | } 185 | 186 | /** 187 | * Returns the links. 188 | * 189 | * @returns {Array.} Array of link objects (href, title, rel, type) 190 | */ 191 | links() { 192 | return Array.isArray(this.data.links) ? this.data.links : []; 193 | } 194 | 195 | /** 196 | * Returns list of backends in the federation. 197 | * 198 | * @returns {Array.} Array of backends 199 | */ 200 | listFederation() { 201 | let federation = []; 202 | if (Utils.isObject(this.data.federation)) { 203 | // convert to array and add keys as `id` property 204 | for(const [key, backend] of Object.entries(this.data.federation)) { 205 | // fresh object to avoid `id` showing up in this.data.federation 206 | federation.push({ id: key, ...backend }); 207 | } 208 | } 209 | return federation; 210 | } 211 | 212 | /** 213 | * Given just the string ID of a backend within the federation, returns that backend's full details as a FederationBackend object. 214 | * 215 | * @param {string} backendId - The ID of a backend within the federation 216 | * @returns {FederationBackend} The full details of the backend 217 | */ 218 | getFederationBackend(backendId) { 219 | // Add `id` property to make it a proper FederationBackend object 220 | // If backendId doesn't exist in this.data.federation, will contain just the `id` field (intended behaviour) 221 | return { id: backendId, ...this.data.federation[backendId] } 222 | } 223 | 224 | /** 225 | * Given a list of string IDs of backends within the federation, returns those backends' full details as FederationBackend objects. 226 | * 227 | * @param {Array} backendIds - The IDs of backends within the federation 228 | * @returns {Array} An array in the same order as the input, containing for each position the full details of the backend 229 | */ 230 | getFederationBackends(backendIds) { 231 | // Let 'single case' function do the work, but pass `this` so that `this.data.federation` can be accessed in the callback context 232 | return backendIds.map(this.getFederationBackend, this); 233 | } 234 | 235 | /** 236 | * Lists all supported features. 237 | * 238 | * @returns {Array.} An array of supported features. 239 | */ 240 | listFeatures() { 241 | let features = []; 242 | for(let feature in this.featureMap) { 243 | if (this.featureMap[feature] === true || this.features.includes(this.featureMap[feature])) { 244 | features.push(feature); 245 | } 246 | } 247 | return features.sort(); 248 | } 249 | 250 | /** 251 | * Check whether a feature is supported by the back-end. 252 | * 253 | * @param {string} methodName - A feature name (corresponds to the JS client method names, see also the feature map for allowed values). 254 | * @returns {boolean} `true` if the feature is supported, otherwise `false`. 255 | */ 256 | hasFeature(methodName) { 257 | let feature = this.featureMap[methodName]; 258 | if (typeof feature === 'string') { 259 | feature = feature.toLowerCase(); 260 | } 261 | return feature === true || this.features.some(e => e === feature); 262 | } 263 | 264 | /** 265 | * Get the billing currency. 266 | * 267 | * @returns {string | null} The billing currency or `null` if not available. 268 | */ 269 | currency() { 270 | return (Utils.isObject(this.data.billing) && typeof this.data.billing.currency === 'string' ? this.data.billing.currency : null); 271 | } 272 | 273 | /** 274 | * List all billing plans. 275 | * 276 | * @returns {Array.} Billing plans 277 | */ 278 | listPlans() { 279 | if (Utils.isObject(this.data.billing) && Array.isArray(this.data.billing.plans)) { 280 | let defaultPlan = typeof this.data.billing.default_plan === 'string' ? this.data.billing.default_plan.toLowerCase() : null; 281 | return this.data.billing.plans.map(plan => { 282 | let addition = { 283 | default: (defaultPlan === plan.name.toLowerCase()) 284 | }; 285 | return Object.assign({}, plan, addition); 286 | }); 287 | } 288 | else { 289 | return []; 290 | } 291 | } 292 | 293 | /** 294 | * Migrates a response, if required. 295 | * 296 | * @param {AxiosResponse} response 297 | * @protected 298 | * @returns {AxiosResponse} 299 | */ 300 | migrate(response) { // eslint-disable-line no-unused-vars 301 | return response; 302 | } 303 | } 304 | 305 | module.exports = Capabilities; 306 | -------------------------------------------------------------------------------- /src/env.js: -------------------------------------------------------------------------------- 1 | let Environment = null; 2 | if (typeof window === 'undefined') { 3 | Environment = require('./node'); 4 | } 5 | else { 6 | Environment = require('./browser'); 7 | } 8 | /** 9 | * The axios instance to use for HTTP requests. 10 | * 11 | * @type {object} 12 | * @static 13 | */ 14 | Environment.axios = require('axios'); 15 | 16 | module.exports = Environment; -------------------------------------------------------------------------------- /src/filetypes.js: -------------------------------------------------------------------------------- 1 | const Utils = require('@openeo/js-commons/src/utils'); 2 | 3 | /** 4 | * Manages the files types supported by the back-end. 5 | */ 6 | class FileTypes { 7 | 8 | /** 9 | * Creates a new FileTypes object from an API-compatible JSON response. 10 | * 11 | * @param {FileTypesAPI} data - A capabilities response compatible to the API specification for `GET /file_formats`. 12 | */ 13 | constructor(data) { 14 | /** 15 | * @protected 16 | * @type {FileTypesAPI} 17 | */ 18 | this.data = { 19 | input: {}, 20 | output: {} 21 | }; 22 | if(!Utils.isObject(data)) { 23 | return; 24 | } 25 | for(let io of ['input', 'output']) { 26 | for(let type in data[io]) { 27 | if(!Utils.isObject(data[io])) { 28 | continue; 29 | } 30 | this.data[io][type.toUpperCase()] = data[io][type]; 31 | } 32 | } 33 | /** 34 | * A list of backends from the federation that are missing in the response data. 35 | * 36 | * @public 37 | * @type {Array.} 38 | */ 39 | this['federation:missing'] = data['federation:missing']; 40 | } 41 | 42 | /** 43 | * Returns the file types response as a JSON serializable representation of the data that is API compliant. 44 | * 45 | * @returns {FileTypesAPI} 46 | */ 47 | toJSON() { 48 | return this.data; 49 | } 50 | 51 | /** 52 | * Returns the input file formats. 53 | * 54 | * @returns {object.} 55 | */ 56 | getInputTypes() { 57 | return this.data.input; 58 | } 59 | 60 | /** 61 | * Returns the output file formats. 62 | * 63 | * @returns {object.} 64 | */ 65 | getOutputTypes() { 66 | return this.data.output; 67 | } 68 | 69 | /** 70 | * Returns a single input file format for a given identifier. 71 | * 72 | * Returns null if no input file format was found for the given identifier. 73 | * 74 | * @param {string} type - Case-insensitive file format identifier 75 | * @returns {FileType | null} 76 | */ 77 | getInputType(type) { 78 | return this._findType(type, 'input'); 79 | } 80 | 81 | /** 82 | * Returns a single output file format for a given identifier. 83 | * 84 | * Returns null if no output file format was found for the given identifier. 85 | * 86 | * @param {string} type - Case-insensitive file format identifier 87 | * @returns {FileType | null} 88 | */ 89 | getOutputType(type) { 90 | return this._findType(type, 'output'); 91 | } 92 | 93 | /** 94 | * Get a file type object from the list of input or output file formats. 95 | * 96 | * @param {string} type - Identifier of the file type 97 | * @param {string} io - Either `input` or `output` 98 | * @returns {FileType | null} 99 | * @protected 100 | */ 101 | _findType(type, io) { 102 | type = type.toUpperCase(); 103 | if (type in this.data[io]) { 104 | return this.data[io][type]; 105 | } 106 | return null; 107 | } 108 | 109 | } 110 | 111 | module.exports = FileTypes; 112 | -------------------------------------------------------------------------------- /src/job.js: -------------------------------------------------------------------------------- 1 | const Environment = require('./env'); 2 | const BaseEntity = require('./baseentity'); 3 | const Logs = require('./logs'); 4 | const Utils = require('@openeo/js-commons/src/utils'); 5 | const StacMigrate = require('@radiantearth/stac-migrate'); 6 | 7 | const STOP_STATUS = ['finished', 'canceled', 'error']; 8 | 9 | /** 10 | * A Batch Job. 11 | * 12 | * @augments BaseEntity 13 | */ 14 | class Job extends BaseEntity { 15 | 16 | /** 17 | * Creates an object representing a batch job stored at the back-end. 18 | * 19 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 20 | * @param {string} jobId - The batch job ID. 21 | */ 22 | constructor(connection, jobId) { 23 | super(connection, ["id", "title", "description", "process", "status", "progress", "created", "updated", "plan", "costs", "budget", "usage", ["log_level", "logLevel"], "links"]); 24 | /** 25 | * The identifier of the batch job. 26 | * @public 27 | * @readonly 28 | * @type {string} 29 | */ 30 | this.id = jobId; 31 | /** 32 | * @public 33 | * @readonly 34 | * @type {?string} 35 | */ 36 | this.title = undefined; 37 | /** 38 | * @public 39 | * @readonly 40 | * @type {?string} 41 | */ 42 | this.description = undefined; 43 | /** 44 | * The process chain to be executed. 45 | * @public 46 | * @readonly 47 | * @type {?Process} 48 | */ 49 | this.process = undefined; 50 | /** 51 | * The current status of a batch job. 52 | * One of "created", "queued", "running", "canceled", "finished" or "error". 53 | * @public 54 | * @readonly 55 | * @type {?string} 56 | */ 57 | this.status = undefined; 58 | /** 59 | * Indicates the process of a running batch job in percent. 60 | * @public 61 | * @readonly 62 | * @type {?number} 63 | */ 64 | this.progress = undefined; 65 | /** 66 | * Date and time of creation, formatted as a RFC 3339 date-time. 67 | * @public 68 | * @readonly 69 | * @type {?string} 70 | */ 71 | this.created = undefined; 72 | /** 73 | * Date and time of the last status change, formatted as a RFC 3339 date-time. 74 | * @public 75 | * @readonly 76 | * @type {?string} 77 | */ 78 | this.updated = undefined; 79 | /** 80 | * The billing plan to process and charge the batch job with. 81 | * @public 82 | * @readonly 83 | * @type {?string} 84 | */ 85 | this.plan = undefined; 86 | /** 87 | * An amount of money or credits in the currency specified by the back-end. 88 | * @public 89 | * @readonly 90 | * @type {?number} 91 | */ 92 | this.costs = undefined; 93 | /** 94 | * Maximum amount of costs the request is allowed to produce in the currency specified by the back-end. 95 | * @public 96 | * @readonly 97 | * @type {?number} 98 | */ 99 | this.budget = undefined; 100 | } 101 | 102 | /** 103 | * Updates the batch job data stored in this object by requesting the metadata from the back-end. 104 | * 105 | * @async 106 | * @returns {Promise} The update job object (this). 107 | * @throws {Error} 108 | */ 109 | async describeJob() { 110 | let response = await this.connection._get('/jobs/' + this.id); 111 | return this.setAll(response.data); 112 | } 113 | 114 | /** 115 | * Modifies the batch job at the back-end and afterwards updates this object, too. 116 | * 117 | * @async 118 | * @param {object} parameters - An object with properties to update, each of them is optional, but at least one of them must be specified. Additional properties can be set if the server supports them. 119 | * @param {Process} parameters.process - A new process. 120 | * @param {string} parameters.title - A new title. 121 | * @param {string} parameters.description - A new description. 122 | * @param {string} parameters.plan - A new plan. 123 | * @param {number} parameters.budget - A new budget. 124 | * @returns {Promise} The updated job object (this). 125 | * @throws {Error} 126 | */ 127 | async updateJob(parameters) { 128 | await this.connection._patch('/jobs/' + this.id, this._convertToRequest(parameters)); 129 | if (this._supports('describeJob')) { 130 | return await this.describeJob(); 131 | } 132 | else { 133 | return this.setAll(parameters); 134 | } 135 | } 136 | 137 | /** 138 | * Deletes the batch job from the back-end. 139 | * 140 | * @async 141 | * @throws {Error} 142 | */ 143 | async deleteJob() { 144 | await this.connection._delete('/jobs/' + this.id); 145 | } 146 | 147 | /** 148 | * Calculate an estimate (potentially time/costs/volume) for a batch job. 149 | * 150 | * @async 151 | * @returns {Promise} A response compatible to the API specification. 152 | * @throws {Error} 153 | */ 154 | async estimateJob() { 155 | let response = await this.connection._get('/jobs/' + this.id + '/estimate'); 156 | return response.data; 157 | } 158 | 159 | /** 160 | * Get logs for the batch job from the back-end. 161 | * 162 | * @param {?string} [level=null] - Minimum level of logs to return. 163 | * @returns {Logs} 164 | */ 165 | debugJob(level = null) { 166 | return new Logs(this.connection, '/jobs/' + this.id + '/logs', level); 167 | } 168 | 169 | /** 170 | * Checks for status changes and new log entries every x seconds. 171 | * 172 | * On every status change observed or on new log entries (if supported by the 173 | * back-end and not disabled via `requestLogs`), the callback is executed. 174 | * It may also be executed once at the beginning. 175 | * The callback receives the updated job (this object) and the logs (array) passed. 176 | * 177 | * The monitoring stops once the job has finished, was canceled or errored out. 178 | * 179 | * This is only supported if describeJob is supported by the back-end. 180 | * 181 | * Returns a function that can be called to stop monitoring the job manually. 182 | * 183 | * @param {Function} callback 184 | * @param {number} [interval=60] - Interval between update requests, in seconds as integer. 185 | * @param {boolean} [requestLogs=true] - Enables/Disables requesting logs 186 | * @returns {Function} 187 | * @throws {Error} 188 | */ 189 | monitorJob(callback, interval = 60, requestLogs = true) { 190 | if (typeof callback !== 'function' || interval < 1) { 191 | return; 192 | } 193 | let capabilities = this.connection.capabilities(); 194 | if (!capabilities.hasFeature('describeJob')) { 195 | throw new Error('Monitoring Jobs not supported by the back-end.'); 196 | } 197 | 198 | let lastStatus = this.status; 199 | let intervalId = null; 200 | let logIterator = null; 201 | if (capabilities.hasFeature('debugJob') && requestLogs) { 202 | logIterator = this.debugJob(); 203 | } 204 | let monitorFn = async () => { 205 | if (this.getDataAge() > 1) { 206 | await this.describeJob(); 207 | } 208 | let logs = logIterator ? await logIterator.nextLogs() : []; 209 | if (lastStatus !== this.status || logs.length > 0) { 210 | callback(this, logs); 211 | } 212 | lastStatus = this.status; 213 | if (STOP_STATUS.includes(this.status)) { 214 | stopFn(); // eslint-disable-line no-use-before-define 215 | } 216 | }; 217 | setTimeout(monitorFn, 0); 218 | intervalId = setInterval(monitorFn, interval * 1000); 219 | let stopFn = () => { 220 | if (intervalId) { 221 | clearInterval(intervalId); 222 | intervalId = null; 223 | } 224 | }; 225 | return stopFn; 226 | } 227 | 228 | /** 229 | * Starts / queues the batch job for processing at the back-end. 230 | * 231 | * @async 232 | * @returns {Promise} The updated job object (this). 233 | * @throws {Error} 234 | */ 235 | async startJob() { 236 | await this.connection._post('/jobs/' + this.id + '/results', {}); 237 | if (this._supports('describeJob')) { 238 | return await this.describeJob(); 239 | } 240 | return this; 241 | } 242 | 243 | /** 244 | * Stops / cancels the batch job processing at the back-end. 245 | * 246 | * @async 247 | * @returns {Promise} The updated job object (this). 248 | * @throws {Error} 249 | */ 250 | async stopJob() { 251 | await this.connection._delete('/jobs/' + this.id + '/results'); 252 | if (this._supports('describeJob')) { 253 | return await this.describeJob(); 254 | } 255 | return this; 256 | } 257 | 258 | /** 259 | * Retrieves the STAC Item or Collection produced for the job results. 260 | * 261 | * The Item or Collection returned always complies to the latest STAC version (currently 1.0.0). 262 | * 263 | * @async 264 | * @returns {Promise>} The JSON-based response compatible to the API specification, but also including a `costs` property if present in the headers. 265 | * @throws {Error} 266 | */ 267 | async getResultsAsStac() { 268 | let response = await this.connection._get('/jobs/' + this.id + '/results'); 269 | if (!Utils.isObject(response) || !Utils.isObject(response.data)) { 270 | throw new Error("Results received from the back-end are invalid"); 271 | } 272 | let data = StacMigrate.stac(response.data); 273 | if (!Utils.isObject(data.assets)) { 274 | data.assets = {}; 275 | } 276 | if (data.type === 'Feature') { // Item 277 | if (typeof response.headers['openeo-costs'] === 'number') { 278 | data.properties.costs = response.headers['openeo-costs']; 279 | } 280 | } 281 | else { // Collection 282 | if (typeof response.headers['openeo-costs'] === 'number') { 283 | data.costs = response.headers['openeo-costs']; 284 | } 285 | } 286 | 287 | return data; 288 | } 289 | 290 | /** 291 | * Retrieves download links. 292 | * 293 | * @async 294 | * @returns {Promise>} A list of links (object with href, rel, title, type and roles). 295 | * @throws {Error} 296 | */ 297 | async listResults() { 298 | let item = await this.getResultsAsStac(); 299 | if (Utils.isObject(item.assets)) { 300 | return Object.values(item.assets); 301 | } 302 | else { 303 | return []; 304 | } 305 | } 306 | 307 | /** 308 | * Downloads the results to the specified target folder. The specified target folder must already exist! 309 | * 310 | * NOTE: This method is only supported in a NodeJS environment. In a browser environment this method throws an exception! 311 | * 312 | * @async 313 | * @param {string} targetFolder - A target folder to store the file to, which must already exist. 314 | * @returns {Promise|void>} Depending on the environment: A list of file paths of the newly created files (Node), throws in Browsers. 315 | * @throws {Error} 316 | */ 317 | async downloadResults(targetFolder) { 318 | let list = await this.listResults(); 319 | return await Environment.downloadResults(this.connection, list, targetFolder); 320 | } 321 | } 322 | 323 | module.exports = Job; 324 | -------------------------------------------------------------------------------- /src/logs.js: -------------------------------------------------------------------------------- 1 | const Utils = require('@openeo/js-commons/src/utils'); 2 | 3 | /** 4 | * Interface to loop through the logs. 5 | */ 6 | class Logs { 7 | 8 | /** 9 | * Creates a new Logs instance to retrieve logs from a back-end. 10 | * 11 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 12 | * @param {string} endpoint - The relative endpoint to request the logs from, usually `/jobs/.../logs` or `/services/.../logs` with `...` being the actual job or service id. 13 | * @param {?string} [level=null] - Minimum level of logs to return. 14 | */ 15 | constructor(connection, endpoint, level = null) { 16 | /** 17 | * @protected 18 | * @type {Connection} 19 | */ 20 | this.connection = connection; 21 | /** 22 | * @protected 23 | * @type {string} 24 | */ 25 | this.endpoint = endpoint; 26 | /** 27 | * @protected 28 | * @type {string} 29 | */ 30 | this.lastId = ""; 31 | /** 32 | * @protected 33 | * @type {?string} 34 | */ 35 | this.level = level; 36 | /** 37 | * @protected 38 | * @type {Set} 39 | */ 40 | this.missing = new Set(); 41 | } 42 | 43 | /** 44 | * Retrieves the next log entries since the last request. 45 | * 46 | * Retrieves log entries only. 47 | * 48 | * @async 49 | * @param {number} limit - The number of log entries to retrieve per request, as integer. 50 | * @returns {Promise>} 51 | */ 52 | async nextLogs(limit = null) { 53 | let response = await this.next(limit); 54 | return Array.isArray(response.logs) ? response.logs : []; 55 | } 56 | 57 | /** 58 | * Retrieves the backend identifiers that are (partially) missing in the logs. 59 | * 60 | * This is only filled after the first request using `nextLogs` or `next`. 61 | * 62 | * @returns {Array.} 63 | * @see {Logs#nextLogs} 64 | * @see {Logs#next} 65 | */ 66 | getMissingBackends() { 67 | return Array.from(this.missing); 68 | } 69 | 70 | /** 71 | * Retrieves the next log entries since the last request. 72 | * 73 | * Retrieves the full response compliant to the API, including log entries and links. 74 | * 75 | * @async 76 | * @param {number} limit - The number of log entries to retrieve per request, as integer. 77 | * @returns {Promise} 78 | */ 79 | async next(limit = null) { 80 | let query = { 81 | offset: this.lastId 82 | }; 83 | if (limit > 0) { 84 | query.limit = limit; 85 | } 86 | if (this.level) { 87 | query.level = this.level; 88 | } 89 | let response = await this.connection._get(this.endpoint, query); 90 | if (Array.isArray(response.data.logs) && response.data.logs.length > 0) { 91 | response.data.logs = response.data.logs.filter(log => Utils.isObject(log) && typeof log.id === 'string'); 92 | this.lastId = response.data.logs[response.data.logs.length - 1].id; 93 | } 94 | else { 95 | response.data.logs = []; 96 | } 97 | 98 | response.data.links = Array.isArray(response.data.links) ? response.data.links : []; 99 | 100 | if (Array.isArray(response.data["federation:missing"])) { 101 | response.data["federation:missing"].forEach(backend => this.missing.add(backend)); 102 | } 103 | 104 | return response.data; 105 | } 106 | 107 | } 108 | 109 | module.exports = Logs; 110 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const url = require("url"); 3 | const path = require("path"); 4 | const Stream = require('stream'); 5 | 6 | /** 7 | * Platform dependant utilities for the openEO JS Client. 8 | * 9 | * Node.js implementation, don't use in other environments. 10 | * 11 | * @hideconstructor 12 | */ 13 | class Environment { 14 | 15 | /** 16 | * Returns the name of the Environment, here `Node`. 17 | * 18 | * @returns {string} 19 | * @static 20 | */ 21 | static getName() { 22 | return 'Node'; 23 | } 24 | 25 | /** 26 | * Returns the URL of the server instance. 27 | * 28 | * @returns {string} 29 | * @static 30 | */ 31 | static getUrl() { 32 | return Environment.url; 33 | } 34 | 35 | /** 36 | * Sets the URL of the server instance. 37 | * 38 | * @param {string} uri 39 | * @static 40 | */ 41 | static setUrl(uri) { 42 | Environment.url = uri; 43 | } 44 | 45 | /** 46 | * Handles errors from the API that are returned as Streams. 47 | * 48 | * @ignore 49 | * @static 50 | * @param {Stream.Readable} error 51 | * @returns {Promise} 52 | */ 53 | static handleErrorResponse(error) { 54 | return new Promise((resolve, reject) => { 55 | let chunks = []; 56 | error.response.data.on("data", chunk => chunks.push(chunk)); 57 | error.response.data.on("error", streamError => reject(streamError)); 58 | error.response.data.on("end", () => resolve(JSON.parse(Buffer.concat(chunks).toString()))); 59 | }); 60 | } 61 | 62 | /** 63 | * Returns how binary responses from the servers are returned (`stream` or `blob`). 64 | * 65 | * @returns {string} 66 | * @static 67 | */ 68 | static getResponseType() { 69 | return 'stream'; 70 | } 71 | 72 | /** 73 | * Encodes a string into Base64 encoding. 74 | * 75 | * @static 76 | * @param {string|Buffer} str - String to encode. 77 | * @returns {string} String encoded in Base64. 78 | */ 79 | static base64encode(str) { 80 | let buffer; 81 | if (str instanceof Buffer) { 82 | buffer = str; 83 | } else { 84 | buffer = Buffer.from(str.toString(), 'binary'); 85 | } 86 | return buffer.toString('base64'); 87 | } 88 | 89 | /** 90 | * Detect the file name for the given data source. 91 | * 92 | * @ignore 93 | * @static 94 | * @param {string} source - A path to a file as string. 95 | * @returns {string} 96 | */ 97 | static fileNameForUpload(source) { 98 | return path.basename(source); 99 | } 100 | 101 | /** 102 | * Get the data from the source that should be uploaded. 103 | * 104 | * @ignore 105 | * @static 106 | * @param {string} source - A path to a file as string. 107 | * @returns {Stream.Readable} 108 | */ 109 | static dataForUpload(source) { 110 | return fs.createReadStream(source); 111 | } 112 | 113 | /** 114 | * Downloads files to local storage and returns a list of file paths. 115 | * 116 | * @static 117 | * @param {Connection} con 118 | * @param {Array.>} assets 119 | * @param {string} targetFolder 120 | * @returns {Promise>} 121 | * @throws {Error} 122 | */ 123 | static async downloadResults(con, assets, targetFolder) { 124 | let files = []; 125 | const promises = assets.map(async (link) => { 126 | let parsedUrl = url.parse(link.href); 127 | let targetPath = path.join(targetFolder, path.basename(parsedUrl.pathname)); 128 | let data = await con.download(link.href, false); 129 | if (data instanceof Stream.Readable) { 130 | await Environment.saveToFile(data, targetPath); 131 | files.push(targetPath); 132 | } 133 | else { 134 | throw new Error("Data retrieved is not a Stream"); 135 | } 136 | }); 137 | 138 | await Promise.all(promises); 139 | return files; 140 | } 141 | 142 | /** 143 | * Streams data into a file. 144 | * 145 | * @static 146 | * @async 147 | * @param {Stream.Readable} data - Data stream to read from. 148 | * @param {string} filename - File path to store the data at. 149 | * @returns {Promise} 150 | * @throws {Error} 151 | */ 152 | static saveToFile(data, filename) { 153 | return new Promise((resolve, reject) => { 154 | let writeStream = fs.createWriteStream(filename); 155 | writeStream.on('close', (err) => { 156 | if (err) { 157 | return reject(err); 158 | } 159 | resolve(); 160 | }); 161 | data.pipe(writeStream); 162 | }); 163 | } 164 | } 165 | 166 | Environment.url = ''; 167 | 168 | module.exports = Environment; 169 | -------------------------------------------------------------------------------- /src/oidcprovider.js: -------------------------------------------------------------------------------- 1 | const Utils = require('@openeo/js-commons/src/utils'); 2 | const AuthProvider = require('./authprovider'); 3 | const Environment = require('./env'); 4 | const Oidc = require('oidc-client'); 5 | 6 | /** 7 | * The Authentication Provider for OpenID Connect. 8 | * 9 | * See the openid-connect-popup.html and openid-connect-redirect.html files in 10 | * the `/examples/oidc` folder for usage examples in the browser. 11 | * 12 | * If you want to implement OIDC in a non-browser environment, you can override 13 | * the OidcProvider or AuthProvider classes with custom behavior. 14 | * In this case you must provide a function that creates your new class to the 15 | * `Connection.setOidcProviderFactory()` method. 16 | * 17 | * @augments AuthProvider 18 | * @see Connection#setOidcProviderFactory 19 | */ 20 | class OidcProvider extends AuthProvider { 21 | 22 | /** 23 | * Checks whether the required OIDC client library `openid-client-js` is available. 24 | * 25 | * @static 26 | * @returns {boolean} 27 | */ 28 | static isSupported() { 29 | return Utils.isObject(Oidc) && Boolean(Oidc.UserManager); 30 | } 31 | 32 | /** 33 | * Finishes the OpenID Connect sign in (authentication) workflow. 34 | * 35 | * Must be called in the page that OpenID Connect redirects to after logging in. 36 | * 37 | * Supported only in Browser environments. 38 | * 39 | * @async 40 | * @static 41 | * @param {OidcProvider} provider - A OIDC provider to assign the user to. 42 | * @param {object.} [options={}] - Object with additional options. 43 | * @returns {Promise} For uiMethod = 'redirect' only: OIDC User 44 | * @throws {Error} 45 | * @see https://github.com/IdentityModel/oidc-client-js/wiki#other-optional-settings 46 | */ 47 | static async signinCallback(provider = null, options = {}) { 48 | let url = Environment.getUrl(); 49 | if (!provider) { 50 | // No provider options available, try to detect response mode from URL 51 | provider = new OidcProvider(null, {}); 52 | provider.setGrant(url.includes('?') ? 'authorization_code+pkce' : 'implicit'); 53 | } 54 | let providerOptions = provider.getOptions(options); 55 | let oidc = new Oidc.UserManager(providerOptions); 56 | return await oidc.signinCallback(url); 57 | } 58 | 59 | /** 60 | * Creates a new OidcProvider instance to authenticate using OpenID Connect. 61 | * 62 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 63 | * @param {OidcProviderMeta} options - OpenID Connect Provider details as returned by the API. 64 | */ 65 | constructor(connection, options) { 66 | super("oidc", connection, options); 67 | 68 | this.manager = null; 69 | this.listeners = {}; 70 | 71 | /** 72 | * The authenticated OIDC user. 73 | * 74 | * @type {Oidc.User} 75 | */ 76 | this.user = null; 77 | 78 | /** 79 | * The client ID to use for authentication. 80 | * 81 | * @type {string | null} 82 | */ 83 | this.clientId = null; 84 | 85 | /** 86 | * The grant type (flow) to use for this provider. 87 | * 88 | * Either "authorization_code+pkce" (default) or "implicit" 89 | * 90 | * @type {string} 91 | */ 92 | this.grant = "authorization_code+pkce"; // Set this before calling detectDefaultClient 93 | 94 | /** 95 | * The issuer, i.e. the link to the identity provider. 96 | * 97 | * @type {string} 98 | */ 99 | this.issuer = options.issuer || ""; 100 | 101 | /** 102 | * The scopes to be requested. 103 | * 104 | * @type {Array.} 105 | */ 106 | this.scopes = Array.isArray(options.scopes) && options.scopes.length > 0 ? options.scopes : ['openid']; 107 | 108 | /** 109 | * The scope that is used to request a refresh token. 110 | * 111 | * @type {string} 112 | */ 113 | this.refreshTokenScope = "offline_access"; 114 | 115 | /** 116 | * Any additional links. 117 | * 118 | * 119 | * @type {Array.} 120 | */ 121 | this.links = Array.isArray(options.links) ? options.links : []; 122 | 123 | /** 124 | * The default clients made available by the back-end. 125 | * 126 | * @type {Array.} 127 | */ 128 | this.defaultClients = Array.isArray(options.default_clients) ? options.default_clients : []; 129 | 130 | /** 131 | * The detected default Client. 132 | * 133 | * @type {OidcClient} 134 | */ 135 | this.defaultClient = this.detectDefaultClient(); 136 | } 137 | 138 | /** 139 | * Adds a listener to one of the following events: 140 | * 141 | * - AccessTokenExpiring: Raised prior to the access token expiring. 142 | * - AccessTokenExpired: Raised after the access token has expired. 143 | * - SilentRenewError: Raised when the automatic silent renew has failed. 144 | * 145 | * @param {string} event 146 | * @param {Function} callback 147 | * @param {string} [scope="default"] 148 | */ 149 | addListener(event, callback, scope = 'default') { 150 | this.manager.events[`add${event}`](callback); 151 | this.listeners[`${scope}:${event}`] = callback; 152 | } 153 | 154 | /** 155 | * Removes the listener for the given event that has been set with addListener. 156 | * 157 | * @param {string} event 158 | * @param {string} [scope="default"] 159 | * @see OidcProvider#addListener 160 | */ 161 | removeListener(event, scope = 'default') { 162 | this.manager.events[`remove${event}`](this.listeners[event]); 163 | delete this.listeners[`${scope}:${event}`]; 164 | } 165 | 166 | /** 167 | * Authenticate with OpenID Connect (OIDC). 168 | * 169 | * Supported only in Browser environments. 170 | * 171 | * @async 172 | * @param {object.} [options={}] - Object with authentication options. 173 | * @param {boolean} [requestRefreshToken=false] - If set to `true`, adds a scope to request a refresh token. 174 | * @returns {Promise} 175 | * @throws {Error} 176 | * @see https://github.com/IdentityModel/oidc-client-js/wiki#other-optional-settings 177 | * @see {OidcProvider#refreshTokenScope} 178 | */ 179 | async login(options = {}, requestRefreshToken = false) { 180 | if (!this.issuer || typeof this.issuer !== 'string') { 181 | throw new Error("No Issuer URL available for OpenID Connect"); 182 | } 183 | 184 | this.manager = new Oidc.UserManager(this.getOptions(options, requestRefreshToken)); 185 | this.addListener('UserLoaded', async () => this.setUser(await this.manager.getUser()), 'js-client'); 186 | this.addListener('AccessTokenExpired', () => this.setUser(null), 'js-client'); 187 | if (OidcProvider.uiMethod === 'popup') { 188 | await this.manager.signinPopup(); 189 | } 190 | else { 191 | await this.manager.signinRedirect(); 192 | } 193 | } 194 | 195 | /** 196 | * Logout from the established session. 197 | * 198 | * @async 199 | */ 200 | async logout() { 201 | if (this.manager !== null) { 202 | try { 203 | if (OidcProvider.uiMethod === 'popup') { 204 | await this.manager.signoutPopup(); 205 | } 206 | else { 207 | await this.manager.signoutRedirect({ 208 | post_logout_redirect_uri: Environment.getUrl() 209 | }); 210 | } 211 | } catch (error) { 212 | console.warn(error); 213 | } 214 | super.logout(); 215 | this.removeListener('UserLoaded', 'js-client'); 216 | this.removeListener('AccessTokenExpired', 'js-client'); 217 | this.manager = null; 218 | this.setUser(null); 219 | } 220 | } 221 | 222 | /** 223 | * Returns the options for the OIDC client library. 224 | * 225 | * Options can be overridden by custom options via the options parameter. 226 | * 227 | * @protected 228 | * @param {object.} options 229 | * @param {boolean} [requestRefreshToken=false] - If set to `true`, adds a scope to request a refresh token. 230 | * @returns {object.} 231 | * @see {OidcProvider#refreshTokenScope} 232 | */ 233 | getOptions(options = {}, requestRefreshToken = false) { 234 | let response_type = this.getResponseType(); 235 | let scope = this.scopes.slice(0); 236 | if (requestRefreshToken && !scope.includes(this.refreshTokenScope)) { 237 | scope.push(this.refreshTokenScope); 238 | } 239 | 240 | return Object.assign({ 241 | client_id: this.clientId, 242 | redirect_uri: OidcProvider.redirectUrl, 243 | authority: this.issuer.replace('/.well-known/openid-configuration', ''), 244 | scope: scope.join(' '), 245 | validateSubOnSilentRenew: true, 246 | response_type, 247 | response_mode: response_type.includes('code') ? 'query' : 'fragment' 248 | }, options); 249 | } 250 | 251 | /** 252 | * Get the response_type based on the grant type. 253 | * 254 | * @protected 255 | * @returns {string} 256 | * @throws {Error} 257 | */ 258 | getResponseType() { 259 | switch(this.grant) { 260 | case 'authorization_code+pkce': 261 | return 'code'; 262 | case 'implicit': 263 | return 'token id_token'; 264 | default: 265 | throw new Error('Grant Type not supported'); 266 | } 267 | } 268 | 269 | /** 270 | * Sets the grant type (flow) used for OIDC authentication. 271 | * 272 | * @param {string} grant - Grant Type 273 | * @throws {Error} 274 | */ 275 | setGrant(grant) { // 276 | switch(grant) { 277 | case 'authorization_code+pkce': 278 | case 'implicit': 279 | this.grant = grant; 280 | break; 281 | default: 282 | throw new Error('Grant Type not supported'); 283 | } 284 | } 285 | 286 | /** 287 | * Sets the Client ID for OIDC authentication. 288 | * 289 | * This may override a detected default client ID. 290 | * 291 | * @param {string | null} clientId 292 | */ 293 | setClientId(clientId) { 294 | this.clientId = clientId; 295 | } 296 | 297 | /** 298 | * Sets the OIDC User. 299 | * 300 | * @see https://github.com/IdentityModel/oidc-client-js/wiki#user 301 | * @param {Oidc.User | null} user - The OIDC User. Passing `null` resets OIDC authentication details. 302 | */ 303 | setUser(user) { 304 | if (!user) { 305 | this.user = null; 306 | this.setToken(null); 307 | } 308 | else { 309 | this.user = user; 310 | this.setToken(user.access_token); 311 | } 312 | } 313 | 314 | /** 315 | * Returns a display name for the authenticated user. 316 | * 317 | * @returns {string?} Name of the user or `null` 318 | */ 319 | getDisplayName() { 320 | if (this.user && Utils.isObject(this.user.profile)) { 321 | return this.user.profile.name || this.user.profile.preferred_username || this.user.profile.email || null; 322 | } 323 | return null; 324 | } 325 | 326 | /** 327 | * Detects the default OIDC client ID for the given redirect URL. 328 | * 329 | * Sets the grant and client ID accordingly. 330 | * 331 | * @returns {OidcClient | null} 332 | * @see OidcProvider#setGrant 333 | * @see OidcProvider#setClientId 334 | */ 335 | detectDefaultClient() { 336 | for(let grant of OidcProvider.grants) { 337 | let defaultClient = this.defaultClients.find(client => Boolean(client.grant_types.includes(grant) && Array.isArray(client.redirect_urls) && client.redirect_urls.find(url => url.startsWith(OidcProvider.redirectUrl)))); 338 | if (defaultClient) { 339 | this.setGrant(grant); 340 | this.setClientId(defaultClient.id); 341 | this.defaultClient = defaultClient; 342 | return defaultClient; 343 | } 344 | } 345 | 346 | return null; 347 | } 348 | 349 | } 350 | 351 | /** 352 | * The global "UI" method to use to open the login URL, either "redirect" (default) or "popup". 353 | * 354 | * @type {string} 355 | */ 356 | OidcProvider.uiMethod = 'redirect'; 357 | 358 | /** 359 | * The global redirect URL to use. 360 | * 361 | * By default uses the location of the browser, but removes fragment, query and 362 | * trailing slash. 363 | * The fragment conflicts with the fragment appended by the Implicit Flow and 364 | * the query conflicts with the query appended by the Authorization Code Flow. 365 | * The trailing slash is removed for consistency. 366 | * 367 | * @type {string} 368 | */ 369 | OidcProvider.redirectUrl = Environment.getUrl().split('#')[0].split('?')[0].replace(/\/$/, ''); 370 | 371 | /** 372 | * The supported OpenID Connect grants (flows). 373 | * 374 | * The grants are given as defined in openEO API, e.g. `implicit` and/or `authorization_code+pkce` 375 | * If not defined there, consult the OpenID Connect Discovery documentation. 376 | * 377 | * Lists the grants by priority so that the first grant is the default grant. 378 | * The default grant type since client version 2.0.0 is 'authorization_code+pkce'. 379 | * 380 | * @type {Array.} 381 | */ 382 | OidcProvider.grants = [ 383 | 'authorization_code+pkce', 384 | 'implicit' 385 | ]; 386 | 387 | module.exports = OidcProvider; 388 | -------------------------------------------------------------------------------- /src/openeo.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const Utils = require('@openeo/js-commons/src/utils'); 3 | const Versions = require('@openeo/js-commons/src/versions'); 4 | 5 | // API wrapper 6 | const Connection = require('./connection'); 7 | const Job = require('./job'); 8 | const Logs = require('./logs'); 9 | const UserFile = require('./userfile'); 10 | const UserProcess = require('./userprocess'); 11 | const Service = require('./service'); 12 | 13 | // Auth Providers 14 | const AuthProvider = require('./authprovider'); 15 | const BasicProvider = require('./basicprovider'); 16 | const OidcProvider = require('./oidcprovider'); 17 | 18 | // Response wrapper 19 | const Capabilities = require('./capabilities'); 20 | const FileTypes = require('./filetypes'); 21 | 22 | // Builder 23 | const Builder = require('./builder/builder'); 24 | const BuilderNode = require('./builder/node'); 25 | const Parameter = require('./builder/parameter'); 26 | const Formula = require('./builder/formula'); 27 | 28 | const MIN_API_VERSION = '1.0.0-rc.2'; 29 | const MAX_API_VERSION = '1.x.x'; 30 | 31 | /** 32 | * Main class to start with openEO. Allows to connect to a server. 33 | * 34 | * @hideconstructor 35 | */ 36 | class OpenEO { 37 | 38 | /** 39 | * Connect to a back-end with version discovery (recommended). 40 | * 41 | * Includes version discovery (request to `GET /well-known/openeo`) and connects to the most suitable version compatible to this JS client version. 42 | * Requests the capabilities and authenticates where required. 43 | * 44 | * @async 45 | * @param {string} url - The server URL to connect to. 46 | * @param {Options} [options={}] - Additional options for the connection. 47 | * @returns {Promise} 48 | * @throws {Error} 49 | * @static 50 | */ 51 | static async connect(url, options = {}) { 52 | let wellKnownUrl = Utils.normalizeUrl(url, '/.well-known/openeo'); 53 | let versionedUrl = url; 54 | let response = null; 55 | try { 56 | response = await axios.get(wellKnownUrl, {timeout: 5000}); 57 | 58 | if (!Utils.isObject(response.data) || !Array.isArray(response.data.versions)) { 59 | throw new Error("Well-Known Document doesn't list any versions."); 60 | } 61 | } catch(error) { 62 | console.warn("Can't read well-known document, connecting directly to the specified URL as fallback mechanism. Reason: " + error.message); 63 | } 64 | 65 | if (Utils.isObject(response)) { 66 | let version = Versions.findLatest(response.data.versions, true, MIN_API_VERSION, MAX_API_VERSION); 67 | if (version !== null) { 68 | versionedUrl = version.url; 69 | } 70 | else { 71 | throw new Error("Server not supported. Client only supports the API versions between " + MIN_API_VERSION + " and " + MAX_API_VERSION); 72 | } 73 | } 74 | 75 | let connection = await OpenEO.connectDirect(versionedUrl, options); 76 | connection.url = url; 77 | return connection; 78 | } 79 | 80 | /** 81 | * Connects directly to a back-end instance, without version discovery (NOT recommended). 82 | * 83 | * Doesn't do version discovery, therefore a URL of a versioned API must be specified. Requests the capabilities and authenticates where required. 84 | * 85 | * @async 86 | * @param {string} versionedUrl - The server URL to connect to. 87 | * @param {Options} [options={}] - Additional options for the connection. 88 | * @returns {Promise} 89 | * @throws {Error} 90 | * @static 91 | */ 92 | static async connectDirect(versionedUrl, options = {}) { 93 | let connection = new Connection(versionedUrl, options); 94 | 95 | // Check whether back-end is accessible and supports a compatible version. 96 | let capabilities = await connection.init(); 97 | if (Versions.compare(capabilities.apiVersion(), MIN_API_VERSION, "<") || Versions.compare(capabilities.apiVersion(), MAX_API_VERSION, ">")) { 98 | throw new Error("Client only supports the API versions between " + MIN_API_VERSION + " and " + MAX_API_VERSION); 99 | } 100 | 101 | return connection; 102 | } 103 | 104 | /** 105 | * Returns the version number of the client. 106 | * 107 | * Not to confuse with the API version(s) supported by the client. 108 | * 109 | * @returns {string} Version number (according to SemVer). 110 | */ 111 | static clientVersion() { 112 | return "2.8.0"; 113 | } 114 | 115 | } 116 | 117 | OpenEO.Environment = require('./env'); 118 | 119 | module.exports = { 120 | AbortController, 121 | AuthProvider, 122 | BasicProvider, 123 | Capabilities, 124 | Connection, 125 | FileTypes, 126 | Job, 127 | Logs, 128 | OidcProvider, 129 | OpenEO, 130 | Service, 131 | UserFile, 132 | UserProcess, 133 | Builder, 134 | BuilderNode, 135 | Parameter, 136 | Formula 137 | }; 138 | -------------------------------------------------------------------------------- /src/pages.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-classes-per-file */ 2 | 3 | const Job = require('./job.js'); 4 | const Service = require('./service.js'); 5 | const UserFile = require('./userfile.js'); 6 | const UserProcess = require('./userprocess.js'); 7 | const Utils = require('@openeo/js-commons/src/utils'); 8 | const StacMigrate = require('@radiantearth/stac-migrate'); 9 | 10 | const FED_MISSING = 'federation:missing'; 11 | 12 | /** 13 | * A class to handle pagination of resources. 14 | * 15 | * @abstract 16 | */ 17 | class Pages { 18 | /** 19 | * Creates an instance of Pages. 20 | * 21 | * @param {Connection} connection 22 | * @param {string} endpoint 23 | * @param {string} key 24 | * @param {Constructor} cls - Class 25 | * @param {object} [params={}] 26 | * @param {string} primaryKey 27 | */ 28 | constructor(connection, endpoint, key, cls, params = {}, primaryKey = "id") { 29 | this.connection = connection; 30 | this.nextUrl = endpoint; 31 | this.key = key; 32 | this.primaryKey = primaryKey; 33 | this.cls = cls; 34 | if (!(params.limit > 0)) { 35 | delete params.limit; 36 | } 37 | this.params = params; 38 | } 39 | 40 | /** 41 | * Returns true if there are more pages to fetch. 42 | * 43 | * @returns {boolean} 44 | */ 45 | hasNextPage() { 46 | return this.nextUrl !== null; 47 | } 48 | 49 | /** 50 | * Returns the next page of resources. 51 | * 52 | * @async 53 | * @param {Array.} oldObjects - Existing objects to update, if any. 54 | * @param {boolean} [toArray=true] - Whether to return the objects as a simplified array or as an object with all information. 55 | * @returns {Array.} 56 | * @throws {Error} 57 | */ 58 | async nextPage(oldObjects = [], toArray = true) { 59 | // Request data from server 60 | const response = await this.connection._get(this.nextUrl, this.params); 61 | 62 | let data = response.data; 63 | // Check response 64 | if (!Utils.isObject(data)) { 65 | throw new Error(`Response is invalid, is not an object`); 66 | } 67 | if (!Array.isArray(data[this.key])) { 68 | throw new Error(`Response is invalid, '${this.key}' property is not an array`); 69 | } 70 | 71 | // Update existing objects if needed 72 | let newObjects = data[this.key].map(updated => { 73 | let resource = oldObjects.find(old => old[this.primaryKey] === updated[this.primaryKey]); 74 | if (resource) { 75 | resource.setAll(updated); 76 | } 77 | else { 78 | resource = this._createObject(updated); 79 | } 80 | return resource; 81 | }); 82 | 83 | // Store objects in cache if needed 84 | newObjects = this._cache(newObjects); 85 | 86 | // Add self link if missing 87 | data.links = this._ensureArray(data.links); 88 | const selfLink = this.connection._getLinkHref(data.links, 'self'); 89 | if (!selfLink) { 90 | data.links.push({rel: 'self', href: this.nextUrl}); 91 | } 92 | 93 | // Check whether a next page is available 94 | this.nextUrl = this._getNextLink(response); 95 | // Don't append initial params to the next URL 96 | this.params = null; 97 | 98 | // Either return as ResponseArray or full API response body 99 | if (toArray) { 100 | newObjects.links = data.links; 101 | newObjects[FED_MISSING] = this._ensureArray(data[FED_MISSING]); 102 | return newObjects; 103 | } 104 | else { 105 | data[this.key] = newObjects; 106 | return data; 107 | } 108 | } 109 | 110 | /** 111 | * Ensures a variable is an array. 112 | * 113 | * @protected 114 | * @param {*} x 115 | * @returns {Array} 116 | */ 117 | _ensureArray(x) { 118 | return Array.isArray(x) ? x : []; 119 | } 120 | 121 | /** 122 | * Creates a facade for the object, if needed. 123 | * 124 | * @protected 125 | * @param {object} obj 126 | * @returns {object} 127 | */ 128 | _createObject(obj) { 129 | if (this.cls) { 130 | const cls = this.cls; 131 | const newObj = new cls(this.connection, obj[this.primaryKey]); 132 | newObj.setAll(obj); 133 | return newObj; 134 | } 135 | else { 136 | return obj; 137 | } 138 | } 139 | 140 | /** 141 | * Caches the plain objects if needed. 142 | * 143 | * @param {Array.} objects 144 | * @returns {Array.} 145 | */ 146 | _cache(objects) { 147 | return objects; 148 | } 149 | 150 | /** 151 | * Get the URL of the next page from a response. 152 | * 153 | * @protected 154 | * @param {AxiosResponse} response 155 | * @returns {string | null} 156 | */ 157 | _getNextLink(response) { 158 | const links = this.connection.makeLinksAbsolute(response.data.links, response); 159 | return this.connection._getLinkHref(links, 'next'); 160 | } 161 | 162 | /** 163 | * Makes this class asynchronously iterable. 164 | * 165 | * @returns {AsyncIterator} 166 | */ 167 | [Symbol.asyncIterator]() { 168 | return { 169 | self: this, 170 | /** 171 | * Get the next page of resources. 172 | * 173 | * @async 174 | * @returns {{done: boolean, value: Array.}} 175 | */ 176 | async next() { 177 | const done = !this.self.hasNextPage(); 178 | let value; 179 | if (!done) { 180 | value = await this.self.nextPage(); 181 | } 182 | return { done, value }; 183 | } 184 | } 185 | } 186 | 187 | } 188 | 189 | /** 190 | * Paginate through jobs. 191 | */ 192 | class JobPages extends Pages { 193 | /** 194 | * Paginate through jobs. 195 | * 196 | * @param {Connection} connection 197 | * @param {?number} limit 198 | */ 199 | constructor(connection, limit = null) { 200 | super(connection, "/jobs", "jobs", Job, {limit}); 201 | } 202 | } 203 | 204 | /** 205 | * Paginate through services. 206 | */ 207 | class ServicePages extends Pages { 208 | /** 209 | * Paginate through services. 210 | * 211 | * @param {Connection} connection 212 | * @param {?number} limit 213 | */ 214 | constructor(connection, limit = null) { 215 | super(connection, "/services", "services", Service, {limit}); 216 | } 217 | } 218 | 219 | /** 220 | * Paginate through user files. 221 | */ 222 | class UserFilePages extends Pages { 223 | /** 224 | * Paginate through user files. 225 | * 226 | * @param {Connection} connection 227 | * @param {?number} limit 228 | */ 229 | constructor(connection, limit = null) { 230 | super(connection, "/files", "files", UserFile, {limit}, "path"); 231 | } 232 | } 233 | 234 | /** 235 | * Paginate through processes. 236 | */ 237 | class ProcessPages extends Pages { 238 | /** 239 | * Paginate through processes. 240 | * 241 | * @param {Connection} connection 242 | * @param {?number} limit 243 | * @param {?string} namespace 244 | */ 245 | constructor(connection, limit = null, namespace = null) { 246 | if (!namespace) { 247 | namespace = 'backend'; 248 | } 249 | let endpoint; 250 | let cls = null 251 | if (namespace === 'user') { 252 | endpoint = '/process_graphs'; 253 | cls = UserProcess; 254 | } 255 | else { 256 | endpoint = '/processes'; 257 | if (namespace !== 'backend') { 258 | const normalized = connection.normalizeNamespace(namespace); 259 | endpoint += `/${normalized}`; 260 | } 261 | } 262 | super(connection, endpoint, "processes", cls, {limit}); 263 | this.namespace = namespace; 264 | } 265 | 266 | /** 267 | * Caches the objects to the ProcessRegistry. 268 | * 269 | * @param {Array.} objects 270 | * @returns {Array.} 271 | */ 272 | _cache(objects) { 273 | const plainObjects = objects.map(p => (typeof p.toJSON === 'function' ? p.toJSON() : p)); 274 | this.connection.processes.addAll(plainObjects, this.namespace); 275 | if (!this.cls) { 276 | for (let i in objects) { 277 | objects[i] = this.connection.processes.get(objects[i].id, this.namespace); 278 | } 279 | } 280 | return objects; 281 | } 282 | } 283 | 284 | /** 285 | * Paginate through collections. 286 | */ 287 | class CollectionPages extends Pages { 288 | /** 289 | * Paginate through collections. 290 | * 291 | * @param {Connection} connection 292 | * @param {?number} limit 293 | */ 294 | constructor(connection, limit = null) { 295 | super(connection, "/collections", "collections", null, {limit}); 296 | } 297 | 298 | /** 299 | * Migrates the STAC collection to the latest version. 300 | * 301 | * @param {object} obj 302 | * @returns {Collection} 303 | */ 304 | _createObject(obj) { 305 | if (obj.stac_version) { 306 | return StacMigrate.collection(obj); 307 | } 308 | return obj; 309 | } 310 | } 311 | 312 | /** 313 | * Paginate through collection items. 314 | */ 315 | class ItemPages extends Pages { 316 | /** 317 | * Paginate through collection items. 318 | * 319 | * @param {Connection} connection 320 | * @param {string} collectionId 321 | * @param {object} params 322 | */ 323 | constructor(connection, collectionId, params) { 324 | super(connection, `/collections/${collectionId}/items`, "features", null, params); 325 | } 326 | 327 | /** 328 | * Migrates the STAC item to the latest version. 329 | * 330 | * @param {object} obj 331 | * @returns {Item} 332 | */ 333 | _createObject(obj) { 334 | if (obj.stac_version) { 335 | return StacMigrate.item(obj); 336 | } 337 | return obj; 338 | } 339 | } 340 | 341 | module.exports = { 342 | Pages, 343 | CollectionPages, 344 | ItemPages, 345 | JobPages, 346 | ProcessPages, 347 | ServicePages, 348 | UserFilePages 349 | } 350 | -------------------------------------------------------------------------------- /src/service.js: -------------------------------------------------------------------------------- 1 | const BaseEntity = require('./baseentity'); 2 | const Logs = require('./logs'); 3 | 4 | /** 5 | * A Secondary Web Service. 6 | * 7 | * @augments BaseEntity 8 | */ 9 | class Service extends BaseEntity { 10 | 11 | /** 12 | * Creates an object representing a secondary web service stored at the back-end. 13 | * 14 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 15 | * @param {string} serviceId - The service ID. 16 | */ 17 | constructor(connection, serviceId) { 18 | super(connection, ["id", "title", "description", "process", "url", "type", "enabled", "configuration", "attributes", "created", "plan", "costs", "budget", "usage", ["log_level", "logLevel"], "links"]); 19 | /** 20 | * The identifier of the service. 21 | * @public 22 | * @readonly 23 | * @type {string} 24 | */ 25 | this.id = serviceId; 26 | /** 27 | * @public 28 | * @readonly 29 | * @type {?string} 30 | */ 31 | this.title = undefined; 32 | /** 33 | * @public 34 | * @readonly 35 | * @type {?string} 36 | */ 37 | this.description = undefined; 38 | /** 39 | * The process chain to be executed. 40 | * @public 41 | * @readonly 42 | * @type {?Process} 43 | */ 44 | this.process = undefined; 45 | /** 46 | * URL at which the secondary web service is accessible 47 | * @public 48 | * @readonly 49 | * @type {?string} 50 | */ 51 | this.url = undefined; 52 | /** 53 | * Web service type (protocol / standard) that is exposed. 54 | * @public 55 | * @readonly 56 | * @type {?string} 57 | */ 58 | this.type = undefined; 59 | /** 60 | * @public 61 | * @readonly 62 | * @type {?boolean} 63 | */ 64 | this.enabled = undefined; 65 | /** 66 | * Map of configuration settings, i.e. the setting names supported by the secondary web service combined with actual values. 67 | * @public 68 | * @readonly 69 | * @type {?object.} 70 | */ 71 | this.configuration = undefined; 72 | /** 73 | * Additional attributes of the secondary web service, e.g. available layers for a WMS based on the bands in the underlying GeoTiff. 74 | * @public 75 | * @readonly 76 | * @type {?object.} 77 | */ 78 | this.attributes = undefined; 79 | /** 80 | * Date and time of creation, formatted as a RFC 3339 date-time. 81 | * @public 82 | * @readonly 83 | * @type {?string} 84 | */ 85 | this.created = undefined; 86 | /** 87 | * The billing plan to process and charge the service with. 88 | * @public 89 | * @readonly 90 | * @type {?string} 91 | */ 92 | this.plan = undefined; 93 | /** 94 | * An amount of money or credits in the currency specified by the back-end. 95 | * @public 96 | * @readonly 97 | * @type {?number} 98 | */ 99 | this.costs = undefined; 100 | /** 101 | * Maximum amount of costs the request is allowed to produce in the currency specified by the back-end. 102 | * @public 103 | * @readonly 104 | * @type {?number} 105 | */ 106 | this.budget = undefined; 107 | } 108 | 109 | /** 110 | * Updates the data stored in this object by requesting the secondary web service metadata from the back-end. 111 | * 112 | * @async 113 | * @returns {Promise} The updates service object (this). 114 | * @throws {Error} 115 | */ 116 | async describeService() { 117 | let response = await this.connection._get('/services/' + this.id); 118 | return this.setAll(response.data); 119 | } 120 | 121 | /** 122 | * Modifies the secondary web service at the back-end and afterwards updates this object, too. 123 | * 124 | * @async 125 | * @param {object} parameters - An object with properties to update, each of them is optional, but at least one of them must be specified. Additional properties can be set if the server supports them. 126 | * @param {Process} parameters.process - A new process. 127 | * @param {string} parameters.title - A new title. 128 | * @param {string} parameters.description - A new description. 129 | * @param {boolean} parameters.enabled - Enables (`true`) or disables (`false`) the service. 130 | * @param {object.} parameters.configuration - A new set of configuration parameters to set for the service. 131 | * @param {string} parameters.plan - A new plan. 132 | * @param {number} parameters.budget - A new budget. 133 | * @returns {Promise} The updated service object (this). 134 | * @throws {Error} 135 | */ 136 | async updateService(parameters) { 137 | await this.connection._patch('/services/' + this.id, this._convertToRequest(parameters)); 138 | if (this._supports('describeService')) { 139 | return await this.describeService(); 140 | } 141 | else { 142 | return this.setAll(parameters); 143 | } 144 | } 145 | 146 | /** 147 | * Deletes the secondary web service from the back-end. 148 | * 149 | * @async 150 | * @throws {Error} 151 | */ 152 | async deleteService() { 153 | await this.connection._delete('/services/' + this.id); 154 | } 155 | 156 | /** 157 | * Get logs for the secondary web service from the back-end. 158 | * 159 | * @param {?string} [level=null] - Minimum level of logs to return. 160 | * @returns {Logs} 161 | */ 162 | debugService(level = null) { 163 | return new Logs(this.connection, '/services/' + this.id + '/logs', level); 164 | } 165 | 166 | /** 167 | * Checks for new log entries every x seconds. 168 | * 169 | * On every status change (enabled/disabled) observed or on new log entries 170 | * (if supported by the back-end and not disabled via `requestLogs`), the 171 | * callback is executed. It may also be executed once at the beginning. 172 | * The callback receives the updated service (this object) and the logs (array) passed. 173 | * 174 | * Returns a function that can be called to stop monitoring the service manually. 175 | * The monitoring must be stopped manually, otherwise it runs forever. 176 | * 177 | * This is only supported if describeService is supported by the back-end. 178 | * 179 | * @param {Function} callback 180 | * @param {number} [interval=60] - Interval between update requests, in seconds as integer. 181 | * @param {boolean} [requestLogs=true] - Enables/Disables requesting logs 182 | * @returns {Function} 183 | * @throws {Error} 184 | */ 185 | monitorService(callback, interval = 60, requestLogs = true) { 186 | if (typeof callback !== 'function' || interval < 1) { 187 | return; 188 | } 189 | let capabilities = this.connection.capabilities(); 190 | if (!capabilities.hasFeature('describeService')) { 191 | throw new Error('Monitoring Services not supported by the back-end.'); 192 | } 193 | 194 | let wasEnabled = this.enabled; 195 | let intervalId = null; 196 | let logIterator = null; 197 | if (capabilities.hasFeature('debugService') && requestLogs) { 198 | logIterator = this.debugService(); 199 | } 200 | let monitorFn = async () => { 201 | if (this.getDataAge() > 1) { 202 | await this.describeService(); 203 | } 204 | let logs = logIterator ? await logIterator.nextLogs() : []; 205 | if (wasEnabled !== this.enabled || logs.length > 0) { 206 | callback(this, logs); 207 | } 208 | wasEnabled = this.enabled; 209 | }; 210 | setTimeout(monitorFn, 0); 211 | intervalId = setInterval(monitorFn, interval * 1000); 212 | let stopFn = () => { 213 | if (intervalId) { 214 | clearInterval(intervalId); 215 | intervalId = null; 216 | } 217 | }; 218 | return stopFn; 219 | } 220 | } 221 | 222 | module.exports = Service; 223 | -------------------------------------------------------------------------------- /src/typedefs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * An error. 3 | * 4 | * @typedef ApiError 5 | * @type {object} 6 | * @property {string} id 7 | * @property {string} code 8 | * @property {string} message 9 | * @property {Array.} links 10 | */ 11 | 12 | /** 13 | * Authentication Provider details. 14 | * 15 | * @typedef AuthProviderMeta 16 | * @type {object} 17 | * @property {?string} id Provider identifier, may not be used for all authentication methods. 18 | * @property {string} title Title for the authentication method. 19 | * @property {string} description Description for the authentication method. 20 | */ 21 | 22 | /** 23 | * Response for a HTTP request. 24 | * 25 | * @typedef AxiosResponse 26 | * @type {object} 27 | * @property {*} data 28 | * @property {number} status 29 | * @property {string} statusText 30 | * @property {*} headers 31 | * @property {object.} config 32 | * @property {*} request 33 | */ 34 | 35 | /** 36 | * @typedef BillingPlan 37 | * @type {object} 38 | * @property {string} name Name of the billing plan. 39 | * @property {string} description A description of the billing plan, may include CommonMark syntax. 40 | * @property {boolean} paid `true` if it is a paid plan, otherwise `false`. 41 | * @property {string} url A URL pointing to a page describing the billing plan. 42 | * @property {boolean} default `true` if it is the default plan of the back-end, otherwise `false`. 43 | */ 44 | 45 | /** 46 | * @typedef Collections 47 | * @type {object} 48 | * @property {Array.} collections 49 | * @property {Array.} links 50 | * @property {Array.} ["federation:missing"] A list of backends from the federation that are missing in the response data. 51 | */ 52 | 53 | /** 54 | * @typedef Collection 55 | * @type {object.} 56 | */ 57 | 58 | /** 59 | * @typedef FileTypesAPI 60 | * @type {object} 61 | * @property {object.} input - File types supported to import 62 | * @property {object.} output - File types supported to export 63 | */ 64 | 65 | /** 66 | * @typedef FileType 67 | * @type {object} 68 | * @property {string} title 69 | * @property {string} description 70 | * @property {Array.} gis_data_types 71 | * @property {object.} parameters 72 | * @property {Array.} links 73 | */ 74 | 75 | /** 76 | * Reference to a parameter. 77 | * 78 | * @typedef FromNode 79 | * @type {object} 80 | * @property {string} from_node - The node identifier. 81 | */ 82 | 83 | /** 84 | * Reference to a parameter. 85 | * 86 | * @typedef FromParameter 87 | * @type {object} 88 | * @property {string} from_parameter - The name of the parameter. 89 | */ 90 | 91 | /** 92 | * @typedef Item 93 | * @type {object.} 94 | */ 95 | 96 | /** 97 | * @typedef ItemCollection 98 | * @type {object} 99 | * @property {Array.} features - The items in the collection. 100 | * @property {?Array.} links - Additional links, e.g. for pagination. 101 | * @property {?string} timeStamp This property indicates the time and date when the response was generated. 102 | * @property {?number} numberMatched The number (integer) of features of the feature type that match the selection parameters. 103 | * @property {?number} numberReturned The number (integer) of features in the feature collection. 104 | */ 105 | 106 | /** 107 | * @typedef JobEstimate 108 | * @type {object} 109 | * @property {?number} costs 110 | * @property {string} duration 111 | * @property {number} size in bytes as integer 112 | * @property {?number} downloads_included integer 113 | * @property {string} expires 114 | */ 115 | 116 | /** 117 | * A link to another resource. 118 | * 119 | * @typedef Link 120 | * @type {object} 121 | * @property {string} href The URL to the resource. 122 | * @property {?string} rel Relation type 123 | * @property {?string} type Media type 124 | * @property {?string} title Human-readable title 125 | * @property {?Array.} roles A list of roles, if link is originating from an asset. 126 | */ 127 | 128 | /** 129 | * @typedef LogsAPI 130 | * @type {object} 131 | * @property {Array.} logs 132 | * @property {Array.} links 133 | */ 134 | 135 | /** 136 | * A log entry. 137 | * 138 | * @typedef Log 139 | * @type {object} 140 | * @property {string} id 141 | * @property {string} code 142 | * @property {string} level 143 | * @property {string} message 144 | * @property {*} data 145 | * @property {Array.>} path 146 | * @property {Array.} links 147 | */ 148 | 149 | /** 150 | * Default OpenID Connect Client as returned by the API. 151 | * 152 | * @typedef OidcClient 153 | * @type {object} 154 | * @property {string} id Client ID 155 | * @property {Array.} grant_types Supported Grant Types 156 | * @property {Array.} redirect_urls Allowed Redirect URLs 157 | */ 158 | 159 | /** 160 | * OpenID Connect Provider details as returned by the API. 161 | * 162 | * @augments AuthProviderMeta 163 | * @typedef OidcProviderMeta 164 | * @type {object} 165 | * @property {string} id Provider identifier. 166 | * @property {string} title Title for the authentication method. 167 | * @property {string} description Description for the authentication method. 168 | * @property {string} issuer The OpenID Connect issuer location (authority). 169 | * @property {Array.} scopes OpenID Connect Scopes 170 | * @property {Array.} default_clients Default OpenID Connect Clients 171 | * @property {Array.} links Links 172 | */ 173 | 174 | /** 175 | * Connection options. 176 | * 177 | * @typedef Options 178 | * @type {object} 179 | * @property {boolean} addNamespaceToProcess Add a namespace property to processes if set to `true`. Defaults to `false`. 180 | */ 181 | 182 | /** 183 | * @typedef Processes 184 | * @type {object} 185 | * @property {Array.} processes 186 | * @property {Array.} links 187 | * @property {?Array.} namespaces EXPERIMENTAL! 188 | * @property {Array.} ["federation:missing"] A list of backends from the federation that are missing in the response data. 189 | */ 190 | 191 | /** 192 | * An openEO processing chain. 193 | * 194 | * @typedef Process 195 | * @type {object.} 196 | */ 197 | 198 | /** 199 | * A back-end in the federation. 200 | * 201 | * @typedef FederationBackend 202 | * @type {object} 203 | * @property {string} id ID of the back-end within the federation. 204 | * @property {string} url URL to the versioned API endpoint of the back-end. 205 | * @property {string} title Name of the back-end. 206 | * @property {string} description A description of the back-end and its specifics. 207 | * @property {string} status Current status of the back-end (online or offline). 208 | * @property {string} last_status_check The time at which the status of the back-end was checked last, formatted as a RFC 3339 date-time. 209 | * @property {string} last_successful_check If the `status` is `offline`: The time at which the back-end was checked and available the last time. Otherwise, this is equal to the property `last_status_check`. Formatted as a RFC 3339 date-time. 210 | * @property {boolean} experimental Declares the back-end to be experimental. 211 | * @property {boolean} deprecated Declares the back-end to be deprecated. 212 | */ 213 | 214 | /** 215 | * An array, but enriched with additional details from an openEO API response. 216 | * 217 | * Adds two properties: `links` and `federation:missing`. 218 | * 219 | * @typedef ResponseArray 220 | * @augments Array 221 | * @type {Array.<*>} 222 | * @property {Array.} links A list of related links. 223 | * @property {Array.} ["federation:missing"] A list of backends from the federation that are missing in the response data. 224 | */ 225 | 226 | /** 227 | * @typedef ServiceType 228 | * @type {object.} 229 | */ 230 | 231 | /** 232 | * @typedef SyncResult 233 | * @type {object} 234 | * @property {Stream.Readable|Blob} data The data as `Stream` in NodeJS environments or as `Blob` in browsers. 235 | * @property {?number} costs The costs for the request in the currency exposed by the back-end. 236 | * @property {?string} type The content media type returned by the back-end. 237 | * @property {Array.} logs Array of log entries as specified in the API. 238 | */ 239 | 240 | /** 241 | * @typedef UdfRuntime 242 | * @type {object.} 243 | */ 244 | 245 | /** 246 | * @typedef UserAccountStorage 247 | * @type {object} 248 | * @property {number} free in bytes as integer 249 | * @property {number} quota in bytes as integer 250 | */ 251 | 252 | /** 253 | * @typedef UserAccount 254 | * @type {object} 255 | * @property {string} user_id 256 | * @property {?string} name 257 | * @property {?string} default_plan 258 | * @property {?UserAccountStorage} storage 259 | * @property {?number} budget 260 | * @property {?Array.} links 261 | */ 262 | 263 | /** 264 | * An array, but enriched with additional details from an openEO API response. 265 | * 266 | * Adds the property `federation:backends`. 267 | * 268 | * @typedef ValidationResult 269 | * @augments Array 270 | * @type {Array.} 271 | * @property {Array.} ["federation:backends"] The back-ends that support / do not support the process. 272 | */ 273 | -------------------------------------------------------------------------------- /src/userfile.js: -------------------------------------------------------------------------------- 1 | const Environment = require('./env'); 2 | const BaseEntity = require('./baseentity'); 3 | 4 | /** 5 | * A File on the user workspace. 6 | * 7 | * @augments BaseEntity 8 | */ 9 | class UserFile extends BaseEntity { 10 | 11 | /** 12 | * Creates an object representing a file on the user workspace. 13 | * 14 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 15 | * @param {string} path - The path to the file, relative to the user workspace and without user ID. 16 | */ 17 | constructor(connection, path) { 18 | super(connection, ["path", "size", "modified"]); 19 | /** 20 | * Path to the file, relative to the user's directory. 21 | * @readonly 22 | * @public 23 | * @type {string} 24 | */ 25 | this.path = path; 26 | /** 27 | * File size in bytes as integer. 28 | * @readonly 29 | * @public 30 | * @type {number} 31 | */ 32 | this.size = undefined; 33 | /** 34 | * Date and time the file has lastly been modified, formatted as a RFC 3339 date-time. 35 | * @readonly 36 | * @public 37 | * @type {string} 38 | */ 39 | this.modified = undefined; 40 | } 41 | 42 | /** 43 | * Downloads a file from the user workspace into memory. 44 | * 45 | * This method has different behaviour depending on the environment. 46 | * Returns a stream in a NodeJS environment or a Blob in a browser environment. 47 | * 48 | * @async 49 | * @returns {Promise} - Return value depends on the target and environment, see method description for details. 50 | * @throws {Error} 51 | */ 52 | async retrieveFile() { 53 | return await this.connection.download('/files/' + this.path, true); 54 | } 55 | 56 | /** 57 | * Downloads a file from the user workspace and saves it. 58 | * 59 | * This method has different behaviour depending on the environment. 60 | * In a NodeJS environment writes the downloaded file to the target location on the file system. 61 | * In a browser environment offers the file for downloading using the specified name (folders are not supported). 62 | * 63 | * @async 64 | * @param {string} target - The target, see method description for details. 65 | * @returns {Promise|void>} - Return value depends on the target and environment, see method description for details. 66 | * @throws {Error} 67 | */ 68 | async downloadFile(target) { 69 | let data = await this.connection.download('/files/' + this.path, true); 70 | // @ts-ignore 71 | return await Environment.saveToFile(data, target); 72 | } 73 | 74 | /** 75 | * A callback that is executed on upload progress updates. 76 | * 77 | * @callback uploadStatusCallback 78 | * @param {number} percentCompleted - The percent (0-100) completed. 79 | * @param {UserFile} file - The file object corresponding to the callback. 80 | */ 81 | 82 | /** 83 | * Uploads a file to the user workspace. 84 | * If a file with the name exists, overwrites it. 85 | * 86 | * This method has different behaviour depending on the environment. 87 | * In a nodeJS environment the source must be a path to a file as string. 88 | * In a browser environment the source must be an object from a file upload form. 89 | * 90 | * @async 91 | * @param {*} source - The source, see method description for details. 92 | * @param {?uploadStatusCallback} statusCallback - Optionally, a callback that is executed on upload progress updates. 93 | * @param {?AbortController} [abortController=null] - An AbortController object that can be used to cancel the upload process. 94 | * @returns {Promise} 95 | * @throws {Error} 96 | */ 97 | async uploadFile(source, statusCallback = null, abortController = null) { 98 | let options = { 99 | method: 'put', 100 | url: '/files/' + this.path, 101 | data: Environment.dataForUpload(source), 102 | headers: { 103 | 'Content-Type': 'application/octet-stream' 104 | } 105 | }; 106 | if (typeof statusCallback === 'function') { 107 | options.onUploadProgress = (progressEvent) => { 108 | let percentCompleted = Math.round( (progressEvent.loaded * 100) / progressEvent.total ); 109 | statusCallback(percentCompleted, this); 110 | }; 111 | } 112 | 113 | let response = await this.connection._send(options, abortController); 114 | return this.setAll(response.data); 115 | } 116 | 117 | /** 118 | * Deletes the file from the user workspace. 119 | * 120 | * @async 121 | * @throws {Error} 122 | */ 123 | async deleteFile() { 124 | await this.connection._delete('/files/' + this.path); 125 | } 126 | } 127 | 128 | module.exports = UserFile; 129 | -------------------------------------------------------------------------------- /src/userprocess.js: -------------------------------------------------------------------------------- 1 | const BaseEntity = require('./baseentity'); 2 | const Utils = require('@openeo/js-commons/src/utils'); 3 | 4 | /** 5 | * A Stored Process Graph. 6 | * 7 | * @augments BaseEntity 8 | */ 9 | class UserProcess extends BaseEntity { 10 | 11 | /** 12 | * Creates an object representing a process graph stored at the back-end. 13 | * 14 | * @param {Connection} connection - A Connection object representing an established connection to an openEO back-end. 15 | * @param {string} id - ID of a stored process graph. 16 | */ 17 | constructor(connection, id) { 18 | super(connection, [ 19 | "id", 20 | "summary", 21 | "description", 22 | "categories", 23 | "parameters", 24 | "returns", 25 | "deprecated", 26 | "experimental", 27 | "exceptions", 28 | "examples", 29 | "links", 30 | ["process_graph", "processGraph"] 31 | ]); 32 | /** 33 | * The identifier of the process. 34 | * @public 35 | * @readonly 36 | * @type {string} 37 | */ 38 | this.id = id; 39 | /** 40 | * @public 41 | * @readonly 42 | * @type {?string} 43 | */ 44 | this.summary = undefined; 45 | /** 46 | * @public 47 | * @readonly 48 | * @type {?string} 49 | */ 50 | this.description = undefined; 51 | /** 52 | * A list of categories. 53 | * @public 54 | * @readonly 55 | * @type {?Array.} 56 | */ 57 | this.categories = undefined; 58 | /** 59 | * A list of parameters. 60 | * 61 | * @public 62 | * @readonly 63 | * @type {?Array.>} 64 | */ 65 | this.parameters = undefined; 66 | /** 67 | * Description of the data that is returned by this process. 68 | * @public 69 | * @readonly 70 | * @type {?object.} 71 | */ 72 | this.returns = undefined; 73 | /** 74 | * Specifies that the process or parameter is deprecated with the potential to be removed in any of the next versions. 75 | * @public 76 | * @readonly 77 | * @type {?boolean} 78 | */ 79 | this.deprecated = undefined; 80 | /** 81 | * Declares the process or parameter to be experimental, which means that it is likely to change or may produce unpredictable behaviour. 82 | * @public 83 | * @readonly 84 | * @type {?boolean} 85 | */ 86 | this.experimental = undefined; 87 | /** 88 | * Declares any exceptions (errors) that might occur during execution of this process. 89 | * @public 90 | * @readonly 91 | * @type {?object.} 92 | */ 93 | this.exceptions = undefined; 94 | /** 95 | * @public 96 | * @readonly 97 | * @type {?Array.>} 98 | */ 99 | this.examples = undefined; 100 | /** 101 | * Links related to this process. 102 | * @public 103 | * @readonly 104 | * @type {?Array.} 105 | */ 106 | this.links = undefined; 107 | /** 108 | * @public 109 | * @readonly 110 | * @type {?object.} 111 | */ 112 | this.processGraph = undefined; 113 | } 114 | 115 | /** 116 | * Updates the data stored in this object by requesting the process graph metadata from the back-end. 117 | * 118 | * @async 119 | * @returns {Promise} The updated process graph object (this). 120 | * @throws {Error} 121 | */ 122 | async describeUserProcess() { 123 | let response = await this.connection._get('/process_graphs/' + this.id); 124 | if (!Utils.isObject(response.data) || typeof response.data.id !== 'string') { 125 | throw new Error('Invalid response received for user process'); 126 | } 127 | this.connection.processes.add(response.data, 'user'); 128 | return this.setAll(response.data); 129 | } 130 | 131 | /** 132 | * Modifies the stored process graph at the back-end and afterwards updates this object, too. 133 | * 134 | * @async 135 | * @param {object} parameters - An object with properties to update, each of them is optional, but at least one of them must be specified. Additional properties can be set if the server supports them. 136 | * @param {Process} parameters.process - A new process. 137 | * @param {string} parameters.title - A new title. 138 | * @param {string} parameters.description - A new description. 139 | * @returns {Promise} The updated process graph object (this). 140 | * @throws {Error} 141 | */ 142 | async replaceUserProcess(parameters) { 143 | await this.connection._put('/process_graphs/' + this.id, this._convertToRequest(parameters)); 144 | if (this._supports('describeUserProcess')) { 145 | return this.describeUserProcess(); 146 | } 147 | else { 148 | let obj = this.setAll(parameters); 149 | this.connection.processes.add(obj.toJSON(), 'user'); 150 | return obj; 151 | } 152 | } 153 | 154 | /** 155 | * Deletes the stored process graph from the back-end. 156 | * 157 | * @async 158 | * @throws {Error} 159 | */ 160 | async deleteUserProcess() { 161 | await this.connection._delete('/process_graphs/' + this.id); 162 | this.connection.processes.remove(this.id, 'user'); 163 | } 164 | } 165 | 166 | module.exports = UserProcess; 167 | -------------------------------------------------------------------------------- /tests/basic.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const { OpenEO, OidcProvider } = require('../src/openeo'); 3 | const packageInfo = require('../package.json'); 4 | 5 | describe('Client Basics', () => { 6 | test('Check version number', () => { 7 | expect(OpenEO.clientVersion()).toBe(packageInfo.version); 8 | }); 9 | 10 | describe('OIDC', () => { 11 | test('isSupported', async () => { 12 | expect(OidcProvider.isSupported()).toBeTruthy(); 13 | }); 14 | }); 15 | }); -------------------------------------------------------------------------------- /tests/builder.array_create.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | describe('Process Graph Builder (array_create)', () => { 3 | 4 | const { Builder } = require('../src/openeo'); 5 | const expectedProcess = require('./data/builder.array_create.example.json'); 6 | const array_create = { 7 | "id": "array_create", 8 | "description": "Creates a new array, which by default is empty.\n\nThe second parameter `repeat` allows to add the given array multiple times to the new array.\n\nIn most cases you can simply pass a (native) array to processes directly, but this process is especially useful to create a new array that is getting returned by a child process, for example in ``apply_dimension()``.", 9 | "parameters": [ 10 | { 11 | "name": "data", 12 | "description": "A (native) array to fill the newly created array with. Defaults to an empty array.", 13 | "optional": true, 14 | "default": [], 15 | "schema": { 16 | "description": "Any data type is allowed." 17 | } 18 | }, 19 | { 20 | "name": "repeat", 21 | "description": "The number of times the (native) array specified in `data` is repeatedly added after each other to the new array being created. Defaults to `1`.", 22 | "optional": true, 23 | "default": 1, 24 | "schema": { 25 | "type": "integer", 26 | "minimum": 1 27 | } 28 | } 29 | ], 30 | "returns": { 31 | "description": "The newly created array.", 32 | "schema": { 33 | "type": "array", 34 | "items": { 35 | "description": "Any data type is allowed." 36 | } 37 | } 38 | } 39 | }; 40 | 41 | var builder; 42 | test('Add array_create to builder', async () => { 43 | // Create builder 44 | builder = await Builder.fromVersion('1.0.0'); 45 | expect(builder instanceof Builder).toBeTruthy(); 46 | 47 | // Add missing process array_create 48 | builder.addProcessSpec(array_create); 49 | // Check that process is supported now 50 | expect(builder.supports('array_create')).toBeTruthy(); 51 | // Check that process has been added correctly 52 | expect(builder.spec('array_create')).toEqual(array_create); 53 | 54 | }); 55 | 56 | test('Implicit array_create in callbacks', () => { 57 | // Create process (graph) 58 | var datacube = builder.load_collection("EXAMPLE", null, null); 59 | 60 | var process = function(data) { 61 | return [data[0], 1]; 62 | }; 63 | var result = builder.apply_dimension(datacube, process, "bands"); 64 | result.result = true; 65 | 66 | let pg = builder.toJSON(); 67 | 68 | // Check result 69 | expect(pg).toEqual(expectedProcess); 70 | 71 | }); 72 | 73 | }); -------------------------------------------------------------------------------- /tests/builder.evi.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | describe('Process Graph Builder (EVI)', () => { 3 | 4 | const { TESTBACKEND } = require('./config.js'); 5 | const FROM_URL = TESTBACKEND + '/v1.0/processes'; 6 | 7 | const { OpenEO, Connection, Builder, Parameter, Formula } = require('../src/openeo'); 8 | 9 | var con; 10 | test('Connect', async () => { 11 | con = await OpenEO.connect(TESTBACKEND); 12 | expect(con instanceof Connection).toBeTruthy(); 13 | }); 14 | 15 | test('Processes', async () => { 16 | var procs = await con.listProcesses(); 17 | expect(Array.isArray(procs.processes)).toBeTruthy(); 18 | var ids = procs.processes.map(x => x.id); 19 | expect(ids).toContain('load_collection'); 20 | expect(ids).toContain('reduce_dimension'); 21 | expect(ids).toContain('save_result'); 22 | expect(ids).toContain('min'); 23 | expect(ids).toContain('divide'); 24 | expect(ids).toContain('multiply'); 25 | expect(ids).toContain('sum'); 26 | expect(ids).toContain('subtract'); 27 | expect(ids).toContain('array_element'); 28 | }); 29 | 30 | const expectedProcessEvi = require('./data/builder.evi.example.json'); 31 | test('Builder', async () => { 32 | var builder = await con.buildProcess("evi"); 33 | build(builder); 34 | }); 35 | 36 | test('Builder with Math', async () => { 37 | var builder = await con.buildProcess("evi"); 38 | build(builder, true); 39 | }); 40 | 41 | test('Builder from URL', async () => { 42 | var builder = await Builder.fromURL(FROM_URL); 43 | builder.id = "evi"; 44 | build(builder); 45 | }); 46 | 47 | test('Builder for an openEO processes version', async () => { 48 | var builder = await Builder.fromVersion('1.0.0'); 49 | builder.id = "evi"; 50 | build(builder); 51 | }); 52 | 53 | function build(builder, math = false) { 54 | expect(builder instanceof Builder).toBeTruthy(); 55 | 56 | var datacube = builder.load_collection( 57 | new Parameter("collection-id", "string", "The ID of the collection to load"), 58 | { 59 | "west": 16.1, 60 | "east": 16.6, 61 | "north": 48.6, 62 | "south": 47.2 63 | }, 64 | ["2018-01-01", "2018-02-01"], 65 | ["B02", "B04", "B08"] 66 | ); 67 | var eviAlgorithm; 68 | var expectedProcess; 69 | if (math) { 70 | expectedProcess = require('./data/builder.math.evi.example.json'); 71 | eviAlgorithm = new Formula('2.5 * (($B08 - $B04) / ($$offset + $B08 + 6 * $B04 + -7.5 * $B02))'); 72 | } 73 | else { 74 | expectedProcess = expectedProcessEvi; 75 | eviAlgorithm = function(data, context) { 76 | var nir = data["B08"]; 77 | var red = data["B04"]; 78 | var blue = data["B02"]; 79 | var blue2 = data["B02"]; // Not needed for the algorithm, but tests whether multiple calls don't result in multiple array_element nodes. 80 | return this.multiply( 81 | 2.5, 82 | this.divide( 83 | this.subtract(nir, red), 84 | this.sum([ 85 | context["offset"], 86 | nir, 87 | this.multiply(6, red), 88 | this.multiply(-7.5, blue) 89 | ]) 90 | ) 91 | ); 92 | }; 93 | } 94 | var evi = builder.reduce_dimension(datacube, eviAlgorithm, "bands", {offset: 1}).description("Compute the EVI. Formula: 2.5 * (NIR - RED) / ($offset + NIR + 6*RED + -7.5*BLUE)"); 95 | 96 | var minTime = builder.reduce_dimension( 97 | evi, 98 | function(data) { 99 | return this.min(data); 100 | }, 101 | "t" 102 | ); 103 | var saved = builder.save_result(minTime, "PNG"); 104 | saved.result = true; 105 | let pg = builder.toJSON(); 106 | expect(Object.keys(pg.process_graph)).toEqual(Object.keys(expectedProcess.process_graph)); 107 | expect(pg).toEqual(expectedProcess); 108 | } 109 | }); -------------------------------------------------------------------------------- /tests/builder.misc.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | describe('Process Graph Builder (S1)', () => { 3 | 4 | const { Builder, Parameter } = require('../src/openeo'); 5 | test('No write access to array elements', async () => { 6 | var builder = await Builder.fromVersion('1.0.0'); 7 | var mean = function(data) { 8 | data['B1'] = 1; 9 | }; 10 | dc = builder.reduce_dimension(new Parameter('dc'), mean, "t"); 11 | expect(() => builder.toJSON()).toThrow(); 12 | }); 13 | 14 | }); -------------------------------------------------------------------------------- /tests/builder.namespaces.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | describe('Process Graph Builder (EVI)', () => { 3 | 4 | const { Builder, Formula } = require('../src/openeo'); 5 | 6 | test('Namespaces processes in builder', async () => { 7 | const builder = await Builder.fromVersion('1.1.0'); 8 | 9 | // Add namespaced processes 10 | builder.addProcessSpec({id: 'msi'}, "vito"); 11 | builder.addProcessSpec({id: 'ndvi'}, "@m.mohr"); 12 | 13 | // Create process (graph) 14 | builder.process("msi@vito"); 15 | builder.process("ndvi@@m.mohr"); 16 | 17 | let pg = builder.toJSON(); 18 | 19 | // Check result 20 | expect(pg).toEqual({ 21 | "process_graph": { 22 | "msi1": { 23 | process_id: 'msi', 24 | namespace: 'vito', 25 | arguments: {} 26 | }, 27 | "ndvi1": { 28 | process_id: 'ndvi', 29 | namespace: '@m.mohr', 30 | arguments: {} 31 | } 32 | } 33 | }); 34 | 35 | }); 36 | test('Namespaces processes in Formula', async () => { 37 | const builder = await Builder.fromVersion('1.1.0'); 38 | 39 | builder.addProcessSpec({ 40 | "id": "hello_world", 41 | "parameters": [], 42 | "returns": { 43 | "description": "The numerical value of Pi.", 44 | "schema": { 45 | "type": "number" 46 | } 47 | } 48 | }, "a-b"); 49 | 50 | builder.math("hello_world@a-b() + 1"); 51 | 52 | let pg = builder.toJSON(); 53 | 54 | // Check result 55 | expect(pg).toEqual({ 56 | process_graph: { 57 | hellow1: { 58 | process_id: 'hello_world', 59 | namespace: 'a-b', 60 | arguments: {} 61 | }, 62 | add1: { 63 | process_id: 'add', 64 | arguments: { 65 | x: { 66 | from_node: "hellow1" 67 | }, 68 | y: 1 69 | } 70 | } 71 | } 72 | }); 73 | 74 | }); 75 | }); -------------------------------------------------------------------------------- /tests/builder.s1.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | describe('Process Graph Builder (S1)', () => { 3 | 4 | const { Builder, Formula } = require('../src/openeo'); 5 | const expectedProcess = require('./data/builder.s1.example.json'); 6 | 7 | test('S1 with callback (function)', async () => build(false)); 8 | test('S1 with callback (arrow function)', async () => build(false, true)); 9 | test('S1 with Formula', async () => build(true)); 10 | 11 | async function build(useFormula, useArrow = false) { 12 | // Create builder 13 | var builder = await Builder.fromVersion('1.0.0'); 14 | expect(builder instanceof Builder).toBeTruthy(); 15 | 16 | // We are now loading the Sentinel-1 data over the Area of Interest 17 | var datacube = builder.load_collection( 18 | "COPERNICUS/S1_GRD", 19 | {west: 16.06, south: 48.06, east: 16.65, north: 48.35}, 20 | [new Date(Date.UTC(2017, 2, 1)), new Date(Date.UTC(2017, 5, 1))], // Check whether date objects are converted 21 | ["VV"] 22 | ); 23 | 24 | // Since we are creating a monthly RGB composite, we need three separated time ranges (March aas R, April as G and May as G). 25 | // Therefore, we split the datacube into three datacubes using a temporal filter. 26 | var march = builder.filter_temporal(datacube, ["2017-03-01", "2017-04-01"]); 27 | var april = builder.filter_temporal(datacube, ["2017-04-01", "2017-05-01"]); 28 | var may = builder.filter_temporal(datacube, ["2017-05-01", "2017-06-01"]); 29 | 30 | // We aggregate the timeseries values into a single image by reducing the time dimension using a mean reducer. 31 | var mean = function(data) { 32 | return this.mean(data); 33 | }; 34 | march = builder.reduce_dimension(march, mean, "t"); 35 | april = builder.reduce_dimension(april, mean, "t"); 36 | may = builder.reduce_dimension(may, mean, "t"); 37 | 38 | // Now the three images will be combined into the temporal composite. 39 | // We rename the bands to R, G and B as otherwise the bands are overlapping and the merge process would fail. 40 | march = builder.rename_labels(march, "bands", ["R"], ["VV"]); 41 | april = builder.rename_labels(april, "bands", ["G"], ["VV"]); 42 | may = builder.rename_labels(may, "bands", ["B"], ["VV"]); 43 | 44 | datacube = builder.merge_cubes(march, april); 45 | datacube = builder.merge_cubes(datacube, may); 46 | 47 | // To make the values match the RGB values from 0 to 255 in a PNG file, we need to scale them. 48 | // We can simplify expressing math formulas using the openEO Formula parser. 49 | let scale; 50 | if (useFormula) { 51 | scale = new Formula("linear_scale_range(x, -20, -5, 0, 255)"); 52 | } 53 | else if (useArrow) { 54 | scale = (x, c, b) => b.linear_scale_range(x, -20, -5, 0, 255); 55 | } 56 | else { 57 | scale = function(x) { 58 | return this.linear_scale_range(x, -20, -5, 0, 255); 59 | }; 60 | } 61 | datacube = builder.apply(datacube, scale); 62 | 63 | // Finally, save the result as PNG file. 64 | // In the options we specify which band should be used for "red", "green" and "blue" color. 65 | datacube = builder.save_result(datacube, "PNG", { 66 | red: "R", 67 | green: "G", 68 | blue: "B" 69 | }); 70 | 71 | datacube.result = true; 72 | 73 | let pg = builder.toJSON(); 74 | expect(pg).toEqual(expectedProcess); 75 | } 76 | 77 | }); -------------------------------------------------------------------------------- /tests/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | TESTBACKEND: 'https://earthengine.openeo.org', 3 | TESTPATH: '/v1.0', 4 | TESTUSERNAME: `js-client`, 5 | TESTPASSWORD: 'js-client', 6 | STAC_MIGRATE_VERSION: '1.0.0' 7 | }; -------------------------------------------------------------------------------- /tests/data/builder.array_create.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "process_graph": { 3 | "loadco1": { 4 | "process_id": "load_collection", 5 | "arguments": { 6 | "id": "EXAMPLE", 7 | "spatial_extent": null, 8 | "temporal_extent": null 9 | } 10 | }, 11 | "applyd1": { 12 | "process_id": "apply_dimension", 13 | "arguments": { 14 | "data": { 15 | "from_node": "loadco1" 16 | }, 17 | "process": { 18 | "process_graph": { 19 | "arraye1": { 20 | "process_id": "array_element", 21 | "arguments": { 22 | "data": { 23 | "from_parameter": "data" 24 | }, 25 | "index": 0 26 | } 27 | }, 28 | "arrayc1": { 29 | "process_id": "array_create", 30 | "arguments": { 31 | "data": [ 32 | { 33 | "from_node": "arraye1" 34 | }, 35 | 1 36 | ] 37 | }, 38 | "result": true 39 | } 40 | } 41 | }, 42 | "dimension": "bands" 43 | }, 44 | "result": true 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /tests/data/builder.evi.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evi", 3 | "parameters": [ 4 | { 5 | "name": "collection-id", 6 | "description": "The ID of the collection to load", 7 | "schema": { 8 | "type": "string" 9 | } 10 | } 11 | ], 12 | "process_graph": { 13 | "loadco1": { 14 | "process_id": "load_collection", 15 | "arguments": { 16 | "id": {"from_parameter": "collection-id"}, 17 | "spatial_extent": { 18 | "west": 16.1, 19 | "east": 16.6, 20 | "north": 48.6, 21 | "south": 47.2 22 | }, 23 | "temporal_extent": ["2018-01-01", "2018-02-01"], 24 | "bands": ["B02", "B04", "B08"] 25 | } 26 | }, 27 | "reduce1": { 28 | "process_id": "reduce_dimension", 29 | "description": "Compute the EVI. Formula: 2.5 * (NIR - RED) / ($offset + NIR + 6*RED + -7.5*BLUE)", 30 | "arguments": { 31 | "data": {"from_node": "loadco1"}, 32 | "dimension": "bands", 33 | "reducer": { 34 | "process_graph": { 35 | "arraye1": { 36 | "process_id": "array_element", 37 | "arguments": { 38 | "data": {"from_parameter": "data"}, 39 | "label": "B08" 40 | } 41 | }, 42 | "arraye2": { 43 | "process_id": "array_element", 44 | "arguments": { 45 | "data": {"from_parameter": "data"}, 46 | "label": "B04" 47 | } 48 | }, 49 | "arraye3": { 50 | "process_id": "array_element", 51 | "arguments": { 52 | "data": {"from_parameter": "data"}, 53 | "label": "B02" 54 | } 55 | }, 56 | "arraye4": { 57 | "process_id": "array_element", 58 | "arguments": { 59 | "data": {"from_parameter": "context"}, 60 | "label": "offset" 61 | } 62 | }, 63 | "subtra1": { 64 | "process_id": "subtract", 65 | "arguments": { 66 | "x": {"from_node": "arraye1"}, 67 | "y": {"from_node": "arraye2"} 68 | } 69 | }, 70 | "multip1": { 71 | "process_id": "multiply", 72 | "arguments": { 73 | "x": 6, 74 | "y": {"from_node": "arraye2"} 75 | } 76 | }, 77 | "multip2": { 78 | "process_id": "multiply", 79 | "arguments": { 80 | "x": -7.5, 81 | "y": {"from_node": "arraye3"} 82 | } 83 | }, 84 | "sum1": { 85 | "process_id": "sum", 86 | "arguments": { 87 | "data": [{"from_node": "arraye4"}, {"from_node": "arraye1"}, {"from_node": "multip1"}, {"from_node": "multip2"}] 88 | } 89 | }, 90 | "divide1": { 91 | "process_id": "divide", 92 | "arguments": { 93 | "x": {"from_node": "subtra1"}, 94 | "y": {"from_node": "sum1"} 95 | } 96 | }, 97 | "multip3": { 98 | "process_id": "multiply", 99 | "arguments": { 100 | "x": 2.5, 101 | "y": {"from_node": "divide1"} 102 | }, 103 | "result": true 104 | } 105 | } 106 | }, 107 | "context": { 108 | "offset": 1 109 | } 110 | } 111 | }, 112 | "reduce2": { 113 | "process_id": "reduce_dimension", 114 | "arguments": { 115 | "data": {"from_node": "reduce1"}, 116 | "dimension": "t", 117 | "reducer": { 118 | "process_graph": { 119 | "min1": { 120 | "process_id": "min", 121 | "arguments": { 122 | "data": {"from_parameter": "data"} 123 | }, 124 | "result": true 125 | } 126 | } 127 | } 128 | } 129 | }, 130 | "savere1": { 131 | "process_id": "save_result", 132 | "arguments": { 133 | "data": {"from_node": "reduce2"}, 134 | "format": "PNG" 135 | }, 136 | "result": true 137 | } 138 | } 139 | } -------------------------------------------------------------------------------- /tests/data/builder.math.evi.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "evi", 3 | "parameters": [ 4 | { 5 | "name": "collection-id", 6 | "description": "The ID of the collection to load", 7 | "schema": { 8 | "type": "string" 9 | } 10 | } 11 | ], 12 | "process_graph": { 13 | "loadco1": { 14 | "process_id": "load_collection", 15 | "arguments": { 16 | "id": {"from_parameter": "collection-id"}, 17 | "spatial_extent": { 18 | "west": 16.1, 19 | "east": 16.6, 20 | "north": 48.6, 21 | "south": 47.2 22 | }, 23 | "temporal_extent": ["2018-01-01", "2018-02-01"], 24 | "bands": ["B02", "B04", "B08"] 25 | } 26 | }, 27 | "reduce1": { 28 | "process_id": "reduce_dimension", 29 | "description": "Compute the EVI. Formula: 2.5 * (NIR - RED) / ($offset + NIR + 6*RED + -7.5*BLUE)", 30 | "arguments": { 31 | "data": {"from_node": "loadco1"}, 32 | "dimension": "bands", 33 | "reducer": { 34 | "process_graph": { 35 | "arraye1": { 36 | "process_id": "array_element", 37 | "arguments": { 38 | "data": {"from_parameter": "data"}, 39 | "label": "B08" 40 | } 41 | }, 42 | "arraye2": { 43 | "process_id": "array_element", 44 | "arguments": { 45 | "data": {"from_parameter": "data"}, 46 | "label": "B04" 47 | } 48 | }, 49 | "arraye3": { 50 | "process_id": "array_element", 51 | "arguments": { 52 | "data": {"from_parameter": "context"}, 53 | "label": "offset" 54 | } 55 | }, 56 | "arraye4": { 57 | "process_id": "array_element", 58 | "arguments": { 59 | "data": {"from_parameter": "data"}, 60 | "label": "B02" 61 | } 62 | }, 63 | "subtra1": { 64 | "process_id": "subtract", 65 | "arguments": { 66 | "x": {"from_node": "arraye1"}, 67 | "y": {"from_node": "arraye2"} 68 | } 69 | }, 70 | "multip1": { 71 | "process_id": "multiply", 72 | "arguments": { 73 | "x": 6, 74 | "y": {"from_node": "arraye2"} 75 | } 76 | }, 77 | "multip2": { 78 | "process_id": "multiply", 79 | "arguments": { 80 | "x": -7.5, 81 | "y": {"from_node": "arraye4"} 82 | } 83 | }, 84 | "add1": { 85 | "process_id": "add", 86 | "arguments": { 87 | "x": {"from_node": "arraye3"}, 88 | "y": {"from_node": "arraye1"} 89 | } 90 | }, 91 | "add2": { 92 | "process_id": "add", 93 | "arguments": { 94 | "x": {"from_node": "add1"}, 95 | "y": {"from_node": "multip1"} 96 | } 97 | }, 98 | "add3": { 99 | "process_id": "add", 100 | "arguments": { 101 | "x": {"from_node": "add2"}, 102 | "y": {"from_node": "multip2"} 103 | } 104 | }, 105 | "divide1": { 106 | "process_id": "divide", 107 | "arguments": { 108 | "x": {"from_node": "subtra1"}, 109 | "y": {"from_node": "add3"} 110 | } 111 | }, 112 | "multip3": { 113 | "process_id": "multiply", 114 | "arguments": { 115 | "x": 2.5, 116 | "y": {"from_node": "divide1"} 117 | }, 118 | "result": true 119 | } 120 | } 121 | }, 122 | "context": { 123 | "offset": 1 124 | } 125 | } 126 | }, 127 | "reduce2": { 128 | "process_id": "reduce_dimension", 129 | "arguments": { 130 | "data": {"from_node": "reduce1"}, 131 | "dimension": "t", 132 | "reducer": { 133 | "process_graph": { 134 | "min1": { 135 | "process_id": "min", 136 | "arguments": { 137 | "data": {"from_parameter": "data"} 138 | }, 139 | "result": true 140 | } 141 | } 142 | } 143 | } 144 | }, 145 | "savere1": { 146 | "process_id": "save_result", 147 | "arguments": { 148 | "data": {"from_node": "reduce2"}, 149 | "format": "PNG" 150 | }, 151 | "result": true 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /tests/data/builder.s1.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "process_graph": { 3 | "loadco1": { 4 | "process_id": "load_collection", 5 | "arguments": { 6 | "id": "COPERNICUS/S1_GRD", 7 | "spatial_extent": { 8 | "west": 16.06, 9 | "south": 48.06, 10 | "east": 16.65, 11 | "north": 48.35 12 | }, 13 | "temporal_extent": [ 14 | "2017-03-01T00:00:00.000Z", 15 | "2017-06-01T00:00:00.000Z" 16 | ], 17 | "bands": [ 18 | "VV" 19 | ] 20 | } 21 | }, 22 | "filter1": { 23 | "process_id": "filter_temporal", 24 | "arguments": { 25 | "data": { 26 | "from_node": "loadco1" 27 | }, 28 | "extent": [ 29 | "2017-03-01", 30 | "2017-04-01" 31 | ] 32 | } 33 | }, 34 | "filter2": { 35 | "process_id": "filter_temporal", 36 | "arguments": { 37 | "data": { 38 | "from_node": "loadco1" 39 | }, 40 | "extent": [ 41 | "2017-04-01", 42 | "2017-05-01" 43 | ] 44 | } 45 | }, 46 | "filter3": { 47 | "process_id": "filter_temporal", 48 | "arguments": { 49 | "data": { 50 | "from_node": "loadco1" 51 | }, 52 | "extent": [ 53 | "2017-05-01", 54 | "2017-06-01" 55 | ] 56 | } 57 | }, 58 | "reduce1": { 59 | "process_id": "reduce_dimension", 60 | "arguments": { 61 | "data": { 62 | "from_node": "filter1" 63 | }, 64 | "reducer": { 65 | "process_graph": { 66 | "mean1": { 67 | "process_id": "mean", 68 | "arguments": { 69 | "data": { 70 | "from_parameter": "data" 71 | } 72 | }, 73 | "result": true 74 | } 75 | } 76 | }, 77 | "dimension": "t" 78 | } 79 | }, 80 | "reduce2": { 81 | "process_id": "reduce_dimension", 82 | "arguments": { 83 | "data": { 84 | "from_node": "filter2" 85 | }, 86 | "reducer": { 87 | "process_graph": { 88 | "mean1": { 89 | "process_id": "mean", 90 | "arguments": { 91 | "data": { 92 | "from_parameter": "data" 93 | } 94 | }, 95 | "result": true 96 | } 97 | } 98 | }, 99 | "dimension": "t" 100 | } 101 | }, 102 | "reduce3": { 103 | "process_id": "reduce_dimension", 104 | "arguments": { 105 | "data": { 106 | "from_node": "filter3" 107 | }, 108 | "reducer": { 109 | "process_graph": { 110 | "mean1": { 111 | "process_id": "mean", 112 | "arguments": { 113 | "data": { 114 | "from_parameter": "data" 115 | } 116 | }, 117 | "result": true 118 | } 119 | } 120 | }, 121 | "dimension": "t" 122 | } 123 | }, 124 | "rename1": { 125 | "process_id": "rename_labels", 126 | "arguments": { 127 | "data": { 128 | "from_node": "reduce1" 129 | }, 130 | "dimension": "bands", 131 | "target": [ 132 | "R" 133 | ], 134 | "source": [ 135 | "VV" 136 | ] 137 | } 138 | }, 139 | "rename2": { 140 | "process_id": "rename_labels", 141 | "arguments": { 142 | "data": { 143 | "from_node": "reduce2" 144 | }, 145 | "dimension": "bands", 146 | "target": [ 147 | "G" 148 | ], 149 | "source": [ 150 | "VV" 151 | ] 152 | } 153 | }, 154 | "rename3": { 155 | "process_id": "rename_labels", 156 | "arguments": { 157 | "data": { 158 | "from_node": "reduce3" 159 | }, 160 | "dimension": "bands", 161 | "target": [ 162 | "B" 163 | ], 164 | "source": [ 165 | "VV" 166 | ] 167 | } 168 | }, 169 | "mergec1": { 170 | "process_id": "merge_cubes", 171 | "arguments": { 172 | "cube1": { 173 | "from_node": "rename1" 174 | }, 175 | "cube2": { 176 | "from_node": "rename2" 177 | } 178 | } 179 | }, 180 | "mergec2": { 181 | "process_id": "merge_cubes", 182 | "arguments": { 183 | "cube1": { 184 | "from_node": "mergec1" 185 | }, 186 | "cube2": { 187 | "from_node": "rename3" 188 | } 189 | } 190 | }, 191 | "apply1": { 192 | "process_id": "apply", 193 | "arguments": { 194 | "data": { 195 | "from_node": "mergec2" 196 | }, 197 | "process": { 198 | "process_graph": { 199 | "linear1": { 200 | "process_id": "linear_scale_range", 201 | "arguments": { 202 | "x": { 203 | "from_parameter": "x" 204 | }, 205 | "inputMin": -20, 206 | "inputMax": -5, 207 | "outputMin": 0, 208 | "outputMax": 255 209 | }, 210 | "result": true 211 | } 212 | } 213 | } 214 | } 215 | }, 216 | "savere1": { 217 | "process_id": "save_result", 218 | "arguments": { 219 | "data": { 220 | "from_node": "apply1" 221 | }, 222 | "format": "PNG", 223 | "options": { 224 | "red": "R", 225 | "green": "G", 226 | "blue": "B" 227 | } 228 | }, 229 | "result": true 230 | } 231 | } 232 | } -------------------------------------------------------------------------------- /tests/data/gee/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "stac_version": "1.0.0", 3 | "id": "AAFC/ACI", 4 | "title": "Canada AAFC Annual Crop Inventory", 5 | "gee:type": "image_collection", 6 | "description": "Starting in 2009, the Earth Observation Team of the Science and Technology\nBranch (STB) at Agriculture and Agri-Food Canada (AAFC) began the process\nof generating annual crop type digital maps. Focusing on the Prairie\nProvinces in 2009 and 2010, a Decision Tree (DT) based methodology was\napplied using optical (Landsat-5, AWiFS, DMC) and radar (Radarsat-2) based\nsatellite images. Beginning with the 2011 growing season, this activity has\nbeen extended to other provinces in support of a national crop inventory.\nTo date this approach can consistently deliver a crop inventory that meets\nthe overall target accuracy of at least 85% at a final spatial resolution of\n30m (56m in 2009 and 2010).\n", 7 | "license": "OGL-Canada-2.0", 8 | "links": [], 9 | "keywords": ["aafc", "canada", "crop", "landcover"], 10 | "providers": [ 11 | { 12 | "name": "Agriculture and Agri-Food Canada", 13 | "roles": ["producer", "licensor"], 14 | "url": "https://open.canada.ca/data/en/dataset/ba2645d5-4458-414d-b196-6303ac06c1c9" 15 | }, 16 | { 17 | "name": "Google Earth Engine", 18 | "roles": ["host"], 19 | "url": "https://developers.google.com/earth-engine/datasets/catalog/AAFC_ACI" 20 | } 21 | ], 22 | "extent": { 23 | "spatial": { "bbox": [[-135.17, 36.83, -51.24, 62.25]] }, 24 | "temporal": { 25 | "interval": [["2009-01-01T00:00:00Z", "2023-01-01T00:00:00Z"]] 26 | } 27 | }, 28 | "summaries": {}, 29 | "sci:citation": "Agriculture and Agri-Food Canada Annual Crop Inventory. {YEAR}", 30 | "stac_extensions": [ 31 | "https://stac-extensions.github.io/datacube/v2.2.0/schema.json" 32 | ], 33 | "cube:dimensions": { 34 | "x": { "type": "spatial", "axis": "x", "extent": [-135.17, -51.24] }, 35 | "y": { "type": "spatial", "axis": "y", "extent": [36.83, 62.25] }, 36 | "t": { 37 | "type": "temporal", 38 | "extent": ["2009-01-01T00:00:00Z", "2023-01-01T00:00:00Z"] 39 | }, 40 | "bands": { "type": "bands", "values": ["landcover"] } 41 | }, 42 | "assets": {} 43 | } 44 | -------------------------------------------------------------------------------- /tests/data/gee/process.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "min", 3 | "summary": "Minimum value", 4 | "description": "Computes the smallest value of an array of numbers, which is equal to the last element of a sorted (i.e., ordered) version of the array.\n\nAn array with solely no-data values returns the no-data value (or `null`).", 5 | "categories": ["math", "math > statistics", "reducer"], 6 | "parameters": [ 7 | { 8 | "name": "data", 9 | "description": "An array of numbers.", 10 | "schema": { "type": "array", "items": { "type": ["number", "null"] } } 11 | } 12 | ], 13 | "returns": { 14 | "description": "The minimum value.", 15 | "schema": { "type": ["number", "null"] } 16 | }, 17 | "examples": [ 18 | { "arguments": { "data": [1, 0, 3, 2] }, "returns": 0 }, 19 | { "arguments": { "data": [5, 2.5, null, -0.7] }, "returns": -0.7 }, 20 | { "arguments": { "data": [] }, "returns": null } 21 | ], 22 | "links": [ 23 | { 24 | "rel": "about", 25 | "href": "http://mathworld.wolfram.com/Minimum.html", 26 | "title": "Minimum explained by Wolfram MathWorld" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tests/data/gee/processgraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "1": { 3 | "process_id": "load_collection", 4 | "arguments": { 5 | "id": "IDAHO_EPSCOR/TERRACLIMATE", 6 | "spatial_extent": null, 7 | "temporal_extent": ["2017-07-01T00:00:00Z", "2017-07-31T23:59:59Z"], 8 | "bands": ["tmmx"] 9 | } 10 | }, 11 | "2": { 12 | "process_id": "save_result", 13 | "arguments": { 14 | "data": { "from_node": "3" }, 15 | "format": "PNG", 16 | "options": { "epsgCode": 4326 } 17 | }, 18 | "result": true 19 | }, 20 | "3": { 21 | "process_id": "apply", 22 | "arguments": { 23 | "data": { "from_node": "1" }, 24 | "process": { 25 | "process_graph": { 26 | "2": { 27 | "process_id": "linear_scale_range", 28 | "arguments": { 29 | "x": { "from_parameter": "x" }, 30 | "inputMin": -150, 31 | "inputMax": 450, 32 | "outputMax": 255 33 | }, 34 | "result": true 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/eodc.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const { OpenEO, Connection, Capabilities } = require('../src/openeo'); 3 | const { Utils } = require('@openeo/js-commons'); 4 | 5 | jest.setTimeout(30*1000); 6 | 7 | describe('EODC back-end', () => { 8 | const TESTBACKEND = 'https://openeo.eodc.eu'; 9 | 10 | const TESTCOLLECTION = 'SENTINEL2_L2A'; 11 | 12 | describe('Request Collection Items', () => { 13 | 14 | let con; 15 | // Skip this test for now, EODC back-end has no CORS headers 16 | test.skip('Connect', async () => { 17 | con = await OpenEO.connect(TESTBACKEND); 18 | expect(con instanceof Connection).toBeTruthy(); 19 | let cap = con.capabilities(); 20 | expect(cap instanceof Capabilities).toBeTruthy(); 21 | }); 22 | 23 | // Skip this test for now, EODC back-end has no CORS headers 24 | test.skip('Check collection', async () => { 25 | let col = await con.describeCollection(TESTCOLLECTION); 26 | console.log(col.id); 27 | expect(col.id).toBe(TESTCOLLECTION); 28 | expect(col).toHaveProperty("links"); 29 | expect(typeof con._getLinkHref(col.links, 'items')).toBe("string"); 30 | }); 31 | 32 | // Skip this test for now, EODC back-end requires Auth 33 | test.skip('Request three pages of items', async () => { 34 | let page = 1; 35 | let spatialExtent = [5.0,45.0,20.0,50.0]; 36 | let temporalExtent = [Date.UTC(2015, 0, 1), Date.UTC(2017, 0, 1)]; 37 | let limit = 5; 38 | for await(let response of con.listCollectionItems(TESTCOLLECTION, spatialExtent, temporalExtent, limit)) { 39 | expect(Utils.isObject(response)).toBeTruthy(); 40 | expect(response).toHaveProperty('features'); 41 | expect(Array.isArray(response.features)).toBeTruthy(); 42 | expect(response.features.length).toBe(limit); 43 | page++; 44 | if (page > 3) { 45 | break; 46 | } 47 | } 48 | }); 49 | 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/vito.test.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | const { OpenEO, Connection, Capabilities, BasicProvider, OidcProvider } = require('../src/openeo'); 3 | const { Utils } = require('@openeo/js-commons'); 4 | 5 | jest.setTimeout(30*1000); 6 | 7 | describe('VITO back-end', () => { 8 | const TESTBACKEND = 'https://openeo.vito.be'; 9 | 10 | let con; 11 | test('Connect', async () => { 12 | con = await OpenEO.connect(TESTBACKEND, {addNamespaceToProcess: true}); 13 | expect(con instanceof Connection).toBeTruthy(); 14 | expect(con.options.addNamespaceToProcess).toBeTruthy(); 15 | let cap = con.capabilities(); 16 | expect(cap instanceof Capabilities).toBeTruthy(); 17 | }); 18 | 19 | describe('OIDC', () => { 20 | test('listAuthProviders', async () => { 21 | let providers = await con.listAuthProviders(); 22 | expect(Array.isArray(providers)).toBeTruthy(); 23 | expect(providers.find(p => p instanceof BasicProvider)).toBeDefined(); 24 | expect(providers.find(p => p instanceof OidcProvider)).toBeDefined(); 25 | }); 26 | }); 27 | 28 | describe('UDFs', () => { 29 | test('listUdfRuntimes', async () => { 30 | let runtime = "Python"; 31 | let udfs = await con.listUdfRuntimes(); 32 | expect(Utils.isObject(udfs)).toBeTruthy(); 33 | expect(udfs).toHaveProperty(runtime); 34 | expect(Utils.isObject(udfs[runtime])).toBeTruthy(); 35 | expect(Utils.isObject(udfs[runtime].versions)).toBeTruthy(); 36 | expect(udfs[runtime].type).toBe("language"); 37 | }); 38 | }); 39 | 40 | describe('Request processes from namespace', () => { 41 | 42 | // Not implemented yet by VITO 43 | /* test('Check process namespace list', async () => { 44 | let p = await con.listProcesses(); 45 | expect(Array.isArray(p.namespaces)).toBeTruthy(); 46 | expect(p.namespaces).toContain("vito"); 47 | }); */ 48 | 49 | test('Check pre-defined process list does not contain "MSI"', async () => { 50 | let p = await con.listProcesses(); 51 | expect(Array.isArray(p.processes)).toBeTruthy(); 52 | let msi = p.processes.find(process => process.id === 'MSI'); 53 | expect(msi).toBeUndefined(); 54 | }); 55 | 56 | test('Request processes from namespace "vito"', async () => { 57 | let p = await con.listProcesses('vito'); 58 | expect(Array.isArray(p.processes)).toBeTruthy(); 59 | let msi = p.processes.find(process => process.id === 'MSI'); 60 | expect(Utils.isObject(msi)).toBeTruthy(); 61 | expect(msi.namespace).toBe('vito'); 62 | expect(msi.description).toBeDefined(); 63 | }); 64 | 65 | test('Request process "MSI" from namespace "vito"', async () => { 66 | let msi = await con.describeProcess('MSI', 'vito'); 67 | expect(Utils.isObject(msi)).toBeTruthy(); 68 | expect(msi.id).toBe('MSI'); 69 | expect(msi.namespace).toBe('vito'); 70 | expect(msi.description).toBeDefined(); 71 | expect(msi.process_graph).toBeDefined(); 72 | }); 73 | 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "compilerOptions": { 6 | // Tells TypeScript to read JS files 7 | "allowJs": true, 8 | // Generate d.ts files 9 | "declaration": true, 10 | // This compiler run should only output d.ts files 11 | "emitDeclarationOnly": true, 12 | // see https://github.com/babel/babel/issues/10237#issuecomment-513028440 13 | "moduleResolution": "node", 14 | // Emit the declaration into a single file. The generated file is invalid and needs some manual adoptions. 15 | "outFile": "openeo.d.ts", 16 | // See https://stackoverflow.com/questions/38209699/error-ts2403-subsequent-variable-declarations-must-have-the-same-type 17 | "skipLibCheck": true 18 | } 19 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 3 | const UnminifiedWebpackPlugin = require('unminified-webpack-plugin'); 4 | 5 | module.exports = [ 6 | // Web 7 | { 8 | target: "web", 9 | mode: 'production', 10 | entry: './src/openeo.js', 11 | output: { 12 | filename: 'openeo.min.js', 13 | path: path.resolve(__dirname), 14 | libraryTarget: 'umd' 15 | }, 16 | externals: { 17 | 'axios': 'axios', 18 | 'buffer': 'Buffer', 19 | 'fs': 'fs', 20 | 'oidc-client': 'Oidc', 21 | 'multihashes': 'multihashes', 22 | 'path': 'path', 23 | 'stream': 'Stream', 24 | 'url': 'url' 25 | }, 26 | resolve: { 27 | alias: { 28 | './env$': path.resolve(__dirname, 'src/browser.js') 29 | } 30 | }, 31 | module: { 32 | rules: [ 33 | { 34 | test: /\.js$/, 35 | exclude: /node_modules/, 36 | use: { 37 | loader: 'babel-loader', 38 | options: { 39 | presets: ['@babel/preset-env'] 40 | } 41 | } 42 | } 43 | ] 44 | }, 45 | plugins: [ 46 | new UnminifiedWebpackPlugin(), 47 | new BundleAnalyzerPlugin({ 48 | analyzerMode: 'static', 49 | openAnalyzer: false 50 | }) 51 | ], 52 | } 53 | ]; --------------------------------------------------------------------------------