From c9b83b5d71610f82bd1d99e837e29ad445758aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thea=20Sch=C3=B6bl?= Date: Fri, 28 Apr 2023 12:43:31 +0000 Subject: [PATCH] feat: update to of elasticsearch 8.4 --- README.md | 7 +- config/elasticsearch.ts | 6 +- integration-test.yml | 2 +- package-lock.json | 731 +++++++++--------- package.json | 10 +- src/app.ts | 6 +- src/storage/elasticsearch/aggregations.ts | 84 +- src/storage/elasticsearch/elasticsearch.ts | 401 +++------- src/storage/elasticsearch/monitoring.ts | 13 +- src/storage/elasticsearch/query.ts | 501 ------------ .../query/boost/boost-functions.ts | 75 ++ .../query/boost/scoring-functions.ts | 38 + src/storage/elasticsearch/query/filter.ts | 47 ++ .../query/filters/availability.ts | 38 + .../elasticsearch/query/filters/boolean.ts | 49 ++ .../elasticsearch/query/filters/date-range.ts | 48 ++ .../elasticsearch/query/filters/distance.ts | 38 + .../elasticsearch/query/filters/geo.ts | 33 + .../query/filters/numeric-range.ts | 47 ++ .../elasticsearch/query/filters/value.ts | 37 + src/storage/elasticsearch/query/query.ts | 114 +++ src/storage/elasticsearch/query/sort.ts | 41 + .../elasticsearch/query/sort/distance.ts | 35 + src/storage/elasticsearch/query/sort/ducet.ts | 27 + .../elasticsearch/query/sort/generic.ts | 27 + src/storage/elasticsearch/query/sort/price.ts | 71 ++ src/storage/elasticsearch/templating.ts | 11 - .../types/elasticsearch-config.ts | 121 +++ .../elasticsearch/types/elasticsearch.ts | 605 --------------- src/storage/elasticsearch/types/guards.ts | 69 -- src/storage/elasticsearch/types/util.ts | 20 + src/storage/elasticsearch/util/alias.ts | 60 ++ src/storage/elasticsearch/util/index.ts | 63 ++ .../elasticsearch/util/no-undefined.ts | 21 + src/storage/elasticsearch/util/retry.ts | 38 + test/common.ts | 5 +- .../elasticsearch/aggregations.spec.ts | 67 +- test/storage/elasticsearch/common.spec.ts | 90 --- .../elasticsearch/elasticsearch.spec.ts | 250 +++--- test/storage/elasticsearch/monitoring.spec.ts | 22 +- test/storage/elasticsearch/query.spec.ts | 113 ++- tsconfig.json | 4 +- 42 files changed, 1843 insertions(+), 2242 deletions(-) delete mode 100644 src/storage/elasticsearch/query.ts create mode 100644 src/storage/elasticsearch/query/boost/boost-functions.ts create mode 100644 src/storage/elasticsearch/query/boost/scoring-functions.ts create mode 100644 src/storage/elasticsearch/query/filter.ts create mode 100644 src/storage/elasticsearch/query/filters/availability.ts create mode 100644 src/storage/elasticsearch/query/filters/boolean.ts create mode 100644 src/storage/elasticsearch/query/filters/date-range.ts create mode 100644 src/storage/elasticsearch/query/filters/distance.ts create mode 100644 src/storage/elasticsearch/query/filters/geo.ts create mode 100644 src/storage/elasticsearch/query/filters/numeric-range.ts create mode 100644 src/storage/elasticsearch/query/filters/value.ts create mode 100644 src/storage/elasticsearch/query/query.ts create mode 100644 src/storage/elasticsearch/query/sort.ts create mode 100644 src/storage/elasticsearch/query/sort/distance.ts create mode 100644 src/storage/elasticsearch/query/sort/ducet.ts create mode 100644 src/storage/elasticsearch/query/sort/generic.ts create mode 100644 src/storage/elasticsearch/query/sort/price.ts create mode 100644 src/storage/elasticsearch/types/elasticsearch-config.ts delete mode 100644 src/storage/elasticsearch/types/elasticsearch.ts delete mode 100644 src/storage/elasticsearch/types/guards.ts create mode 100644 src/storage/elasticsearch/types/util.ts create mode 100644 src/storage/elasticsearch/util/alias.ts create mode 100644 src/storage/elasticsearch/util/index.ts create mode 100644 src/storage/elasticsearch/util/no-undefined.ts create mode 100644 src/storage/elasticsearch/util/retry.ts delete mode 100644 test/storage/elasticsearch/common.spec.ts diff --git a/README.md b/README.md index 2897a208..ceb956fb 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ you with everything you need to run this backend. # Local usage for development purposes ## Requirements -* Elasticsearch (5.6) +* Elasticsearch (8.4) + - [ICU analysis plugin](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html) + - OR Docker * Node.js (~14) / NPM -* Docker ### Startup Behaviour @@ -34,7 +35,7 @@ with the backend. To save you some work we provide a [docker image](https://gitlab.com/openstapps/database) which only needs to be executed to work with the backend. -Run `docker run -d -p 9200:9200 registry.gitlab.com/openstapps/database:master` +Run `docker run -d -p 9200:9200 registry.gitlab.com/openstapps/database:latest` Elasticsearch should be running at port 9200 now. If you have problems with getting elasticsearch to work, have a look in the diff --git a/config/elasticsearch.ts b/config/elasticsearch.ts index cd07f20c..ece20b0b 100644 --- a/config/elasticsearch.ts +++ b/config/elasticsearch.ts @@ -1,6 +1,6 @@ // tslint:disable:no-default-export // tslint:disable:no-magic-numbers -import {ElasticsearchConfigFile} from '../src/storage/elasticsearch/types/elasticsearch'; +import {ElasticsearchConfigFile} from '../src/storage/elasticsearch/types/elasticsearch-config'; /** * This is the default configuration for elasticsearch (a database) @@ -19,13 +19,13 @@ const config: ElasticsearchConfigFile = { internal: { database: { name: 'elasticsearch', - version: '5.6', + version: '8.4', query: { minMatch: '75%', queryType: 'dis_max', matchBoosting: 1.3, fuzziness: 'AUTO', - cutoffFrequency: 0.0, + cutoffFrequency: 0, tieBreaker: 0, }, }, diff --git a/integration-test.yml b/integration-test.yml index 7c2a6930..1ee6e824 100644 --- a/integration-test.yml +++ b/integration-test.yml @@ -15,7 +15,7 @@ services: elasticsearch: ports: - "9200:9200" - image: "registry.gitlab.com/openstapps/database:master" + image: "registry.gitlab.com/openstapps/database:latest" apicli: image: "registry.gitlab.com/openstapps/api/cli:latest" diff --git a/package-lock.json b/package-lock.json index 6fc93652..bc679176 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,45 +5,45 @@ "requires": true, "dependencies": { "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, "requires": { - "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.21.4.tgz", + "integrity": "sha512-LYvhNKfwWSPpocw8GI7gpK2nq3HSDuEPC/uSYaALSJu9xjsalaaYFOq0Pwt5KmVqwEbZlDu81aLXwBOmD/Fv9g==", "requires": { "@babel/highlight": "^7.18.6" } }, "@babel/compat-data": { - "version": "7.20.14", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.14.tgz", - "integrity": "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.21.4.tgz", + "integrity": "sha512-/DYyDpeCfaVinT40FPGdkkb+lYSKvsVuMjDAG7jPOWWiM1ibOaB9CXJAlc4d1QpP/U2q2P9jbrSlClKSErd55g==", "dev": true }, "@babel/core": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", - "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.21.4.tgz", + "integrity": "sha512-qt/YV149Jman/6AfmlxJ04LMIu8bMoyl3RB91yTFrxQmgbrSvQMy7cI8Q62FHx1t8wJ8B5fu0UDoLwHAhUo1QA==", "dev": true, "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-compilation-targets": "^7.20.7", - "@babel/helper-module-transforms": "^7.20.11", - "@babel/helpers": "^7.20.7", - "@babel/parser": "^7.20.7", + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", + "@babel/helper-compilation-targets": "^7.21.4", + "@babel/helper-module-transforms": "^7.21.2", + "@babel/helpers": "^7.21.0", + "@babel/parser": "^7.21.4", "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.12", - "@babel/types": "^7.20.7", + "@babel/traverse": "^7.21.4", + "@babel/types": "^7.21.4", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -60,37 +60,49 @@ } }, "@babel/generator": { - "version": "7.20.14", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", - "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.4.tgz", + "integrity": "sha512-NieM3pVIYW2SwGzKoqfPrQsf4xGs9M9AIG3ThppsSRmO+m7eQhmI6amajKMUeIO37wFfsvnvcxQFx6x6iqxDnA==", "dev": true, "requires": { - "@babel/types": "^7.20.7", + "@babel/types": "^7.21.4", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } } } }, "@babel/helper-compilation-targets": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", - "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.21.4.tgz", + "integrity": "sha512-Fa0tTuOXZ1iL8IeDFUWCzjZcn+sJGd9RZdH9esYVjEejGmzf+FFYQpMi/kZUk2kPy/q1H3/GPw7np8qar/stfg==", "dev": true, "requires": { - "@babel/compat-data": "^7.20.5", - "@babel/helper-validator-option": "^7.18.6", + "@babel/compat-data": "^7.21.4", + "@babel/helper-validator-option": "^7.21.0", "browserslist": "^4.21.3", "lru-cache": "^5.1.1", "semver": "^6.3.0" @@ -126,13 +138,13 @@ "dev": true }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.21.0.tgz", + "integrity": "sha512-HfK1aMRanKHpxemaY2gqBmL04iAPOPRj7DxtNbiDOrJK+gdwkiNRVpCpUJYbUT+aZyemKN8brqTOxzCaG6ExRg==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.20.7", + "@babel/types": "^7.21.0" } }, "@babel/helper-hoist-variables": { @@ -145,18 +157,18 @@ } }, "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.21.4.tgz", + "integrity": "sha512-orajc5T2PsRYUN3ZryCEFeMDYwyw09c/pZeaQEZPH0MpKzSvn3e0uXsDBu3k03VI+9DBiRo+l22BfKTpKwa/Wg==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.21.4" } }, "@babel/helper-module-transforms": { - "version": "7.20.11", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", - "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.21.2.tgz", + "integrity": "sha512-79yj2AR4U/Oqq/WOV7Lx6hUjau1Zfo4cI+JLAVYeMV5XIlbOhmjEk5ulbTc9fMpmlojzZHkUUxAiK+UKn+hNQQ==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.18.9", @@ -165,8 +177,8 @@ "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.10", - "@babel/types": "^7.20.7" + "@babel/traverse": "^7.21.2", + "@babel/types": "^7.21.2" } }, "@babel/helper-simple-access": { @@ -199,20 +211,20 @@ "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" }, "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz", + "integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==", "dev": true }, "@babel/helpers": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", - "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.21.0.tgz", + "integrity": "sha512-XXve0CBtOW0pd7MRzzmoyuSj0e3SEzj8pgyFxnTT1NJZL38BD1MK7yYrm8yefRPIDvNNe14xR4FdbHwpInD4rA==", "dev": true, "requires": { "@babel/template": "^7.20.7", - "@babel/traverse": "^7.20.13", - "@babel/types": "^7.20.7" + "@babel/traverse": "^7.21.0", + "@babel/types": "^7.21.0" } }, "@babel/highlight": { @@ -272,9 +284,9 @@ } }, "@babel/parser": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.13.tgz", - "integrity": "sha512-gFDLKMfpiXCsjt4za2JA9oTMn70CeseCehb11kRZgvd7+F67Hih3OHOK24cRrWECJ/ljfPGac6ygXAs/C8kIvw==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.21.4.tgz", + "integrity": "sha512-alVJj7k7zIxqBZ7BTRhz0IqJFxW1VJbm6N8JbcYhQ186df9ZBPbZBmWSqAMXwHGsCJdYks7z/voa3ibiS5bCIw==", "dev": true }, "@babel/template": { @@ -289,19 +301,19 @@ } }, "@babel/traverse": { - "version": "7.20.13", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", - "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.21.4.tgz", + "integrity": "sha512-eyKrRHKdyZxqDm+fV1iqL9UAHMoIg0nDaGqfIOd8rKH17m5snv7Gn4qgjBoFfLz9APvjFU/ICT00NVCv1Epp8Q==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", + "@babel/code-frame": "^7.21.4", + "@babel/generator": "^7.21.4", "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", + "@babel/helper-function-name": "^7.21.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.13", - "@babel/types": "^7.20.7", + "@babel/parser": "^7.21.4", + "@babel/types": "^7.21.4", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -315,9 +327,9 @@ } }, "@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.21.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.4.tgz", + "integrity": "sha512-rU2oY501qDxE8Pyo7i/Orqma4ziCOrby0/9mvbDUGEfvZjb279Nk9k19e2fiCxHbRRpY2ZyrgW1eq22mvmOIzA==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", @@ -334,17 +346,25 @@ } }, "@elastic/elasticsearch": { - "version": "5.6.22", - "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-5.6.22.tgz", - "integrity": "sha512-jg5VnRSFUQREi4gPQ773nKb3t4IaUsdAGJr5AAtOL2Mj2RSVDFKclPONjEMItcsBxDWdhDWZfhaFC/ymjCEQeA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@elastic/elasticsearch/-/elasticsearch-8.4.0.tgz", + "integrity": "sha512-0QZDBePnb5a+d76zjlMYq96IDf0AOuGP7JHugFUYlYwTC7rZvROuZSpoUsvpUjNH2CzMqWgNLIekIR6EHRMIQA==", "requires": { - "debug": "^4.1.1", - "decompress-response": "^4.2.0", - "into-stream": "^5.1.0", - "ms": "^2.1.1", - "once": "^1.4.0", - "pump": "^3.0.0", - "secure-json-parse": "^2.1.0" + "@elastic/transport": "^8.2.0", + "tslib": "^2.4.0" + } + }, + "@elastic/transport": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/@elastic/transport/-/transport-8.3.1.tgz", + "integrity": "sha512-jv/Yp2VLvv5tSMEOF8iGrtL2YsYHbpf4s+nDsItxUTLFTzuJGpnsB/xBlfsoT2kAYEnWHiSJuqrbRcpXEI/SEQ==", + "requires": { + "debug": "^4.3.4", + "hpagent": "^1.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0", + "tslib": "^2.4.0", + "undici": "^5.5.1" } }, "@es-joy/jsdoccomment": { @@ -512,19 +532,20 @@ "dev": true }, "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" } }, "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==" }, "@jridgewell/set-array": { "version": "1.1.2", @@ -533,9 +554,9 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" }, "@jridgewell/trace-mapping": { "version": "0.3.9", @@ -607,13 +628,31 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, "@openstapps/core": { - "version": "0.74.0", - "resolved": "https://registry.npmjs.org/@openstapps/core/-/core-0.74.0.tgz", - "integrity": "sha512-kiW5pwCmDNFmXCEdappJ8cmrUvoQnp0ICllg2PtZwNW0GL4yIIlwFRsGZeTkyPU4khOp3Vw7PZ9d0EKi76f97Q==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@openstapps/core/-/core-1.0.1.tgz", + "integrity": "sha512-+JlycoMcj+QIaXicOZYlNU07XlDc3zRIydYEoLGueAfkTbKt0ap5FWbNL+Hz8ve3ApQP2Hj+4FuU8H6QyLA0vQ==", "requires": { "@openstapps/core-tools": "0.34.0", "@types/geojson": "1.0.6", @@ -658,11 +697,12 @@ } }, "@openstapps/es-mapping-generator": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@openstapps/es-mapping-generator/-/es-mapping-generator-0.4.0.tgz", - "integrity": "sha512-vGnVrbZDj+H+r7ncJk+gqowIO322wanK9eRtyabMA4HyzeLSO9eXpaPruG0HWXGEbpr1rBwAkq/224xipCVzmg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@openstapps/es-mapping-generator/-/es-mapping-generator-0.6.0.tgz", + "integrity": "sha512-MOMt5AWHqR53PeU8lUR5JpyjpTkOKgeWqhNsuD2Xa7fUFzv3qeIZ9xkUYNnOgo9FpSEyuHDVv659s61tewfIeA==", "dev": true, "requires": { + "@elastic/elasticsearch": "8.4.0", "@openstapps/logger": "1.1.1", "commander": "9.5.0", "deepmerge": "4.2.2", @@ -926,12 +966,6 @@ "@types/node": "*" } }, - "@types/elasticsearch": { - "version": "5.0.40", - "resolved": "https://registry.npmjs.org/@types/elasticsearch/-/elasticsearch-5.0.40.tgz", - "integrity": "sha512-lhnbkC0XorAD7Dt7X+94cXUSHEdDNnEVk/DgFLHgIZQNhixV631Lj4+KpXunTT5rCHyj9RqK3TfO7QrOiwEeUQ==", - "dev": true - }, "@types/express": { "version": "4.17.16", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.16.tgz", @@ -945,14 +979,15 @@ } }, "@types/express-serve-static-core": { - "version": "4.17.33", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz", - "integrity": "sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==", + "version": "4.17.34", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.34.tgz", + "integrity": "sha512-fvr49XlCGoUj2Pp730AItckfjat4WNb0lb3kfrLWffd+RLeoGAMsq7UOy04PAPtoL01uKwcp6u8nhzpgpDYr3w==", "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", - "@types/range-parser": "*" + "@types/range-parser": "*", + "@types/send": "*" } }, "@types/geojson": { @@ -984,9 +1019,9 @@ } }, "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, "@types/minimist": { @@ -1068,10 +1103,20 @@ "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", "dev": true }, + "@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.1.tgz", + "integrity": "sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==", "dev": true, "requires": { "@types/mime": "*", @@ -1079,9 +1124,9 @@ } }, "@types/sinon": { - "version": "10.0.13", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.13.tgz", - "integrity": "sha512-UVjDqJblVNQYvVNUsj0PuYYw0ELRmgt1Nt5Vk0pT5f16ROGfcKJY8o1HVuMOJOpD727RrGB9EGvoaTQE5tgxZQ==", + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.14.tgz", + "integrity": "sha512-mn72up6cjaMyMuaPaa/AwKf6WtsSRysQC7wxFkCm1XcOKXPM1z+5Y4H5wjIVBz4gdAkjvZxVVfjA6ba1nHr5WQ==", "dev": true, "requires": { "@types/sinonjs__fake-timers": "*" @@ -1104,9 +1149,9 @@ "dev": true }, "@types/superagent": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.16.tgz", - "integrity": "sha512-tLfnlJf6A5mB6ddqF159GqcDizfzbMUB1/DeT59/wBNqzRTNNKsaw79A/1TZ84X+f/EwWH8FeuSkjlCLyqS/zQ==", + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-4.1.17.tgz", + "integrity": "sha512-FFK/rRjNy24U6J1BvQkaNWu2ohOIF/kxRQXRsbT141YQODcOcZjzlcc4DGdI2SkTa0rhmF+X14zu6ICjCGIg+w==", "dev": true, "requires": { "@types/cookiejar": "*", @@ -1307,12 +1352,12 @@ } }, "agentkeepalive": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.2.1.tgz", - "integrity": "sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", "requires": { "debug": "^4.1.0", - "depd": "^1.1.2", + "depd": "^2.0.0", "humanize-ms": "^1.2.1" } }, @@ -1392,18 +1437,6 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "arg": { @@ -1472,6 +1505,13 @@ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", "requires": { "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } } }, "better-ajv-errors": { @@ -1524,11 +1564,6 @@ "ms": "2.0.0" } }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1585,6 +1620,14 @@ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", "dev": true }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1686,9 +1729,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001449", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001449.tgz", - "integrity": "sha512-CPB+UL9XMT/Av+pJxCKGhdx+yg1hzplvFJQlJ2n68PyQGMz9L/E2zCyLdOL8uasbouTUgnPl+y0tccI/se+BEw==", + "version": "1.0.30001481", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001481.tgz", + "integrity": "sha512-KCqHwRnaa1InZBtqXzP98LPg0ajCVujMKjqKDhZEthIpAsJl/YEIa3YvXjGXPVqzZVguccuu7ga9KOE1J9rKPQ==", "dev": true }, "chai": { @@ -1756,9 +1799,9 @@ "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" }, "ci-info": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.1.tgz", - "integrity": "sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w==", + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", + "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", "dev": true }, "clean-regexp": { @@ -1797,13 +1840,6 @@ "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", "requires": { "mimic-response": "^1.0.0" - }, - "dependencies": { - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - } } }, "color-convert": { @@ -1890,13 +1926,6 @@ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "requires": { "safe-buffer": "5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - } } }, "content-type": { @@ -2123,7 +2152,8 @@ "core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true }, "cors": { "version": "2.8.5", @@ -2139,6 +2169,15 @@ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" }, + "cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.1" + } + }, "cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -2206,11 +2245,18 @@ } }, "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", "requires": { - "mimic-response": "^2.0.0" + "mimic-response": "^3.1.0" + }, + "dependencies": { + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" + } } }, "deep-eql": { @@ -2280,9 +2326,9 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" }, "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, "destroy": { "version": "1.2.0", @@ -2335,9 +2381,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", + "version": "1.4.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.376.tgz", + "integrity": "sha512-TFeOKd98TpJzRHkr4Aorn16QkMnuCQuGAE6IZ0wYF+qkbSfMPqjplvRppR02tMUpVxZz8nyBNvVm9lIZsqrbPQ==", "dev": true }, "emoji-regex": { @@ -2648,9 +2694,9 @@ } }, "eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -2672,18 +2718,18 @@ } }, "eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==" + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.0.tgz", + "integrity": "sha512-HPpKPUBQcAsZOsHAFwTtIKcYlCje62XB7SEAcxjtmW6TD1WVpkS6i6/hOVtTZIl4zGj/mBqpFVGvaDneik+VoQ==" }, "espree": { - "version": "9.4.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", - "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.5.1.tgz", + "integrity": "sha512-5yxtHSZXRSW5pvv3hAlXM5+/Oswi1AUFqBmbibKb5s6bp3rGIDkyXU6xCoyuuLhijr4SFwPrXRoZjz0AZDN9tg==", "requires": { "acorn": "^8.8.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.0" } }, "esprima": { @@ -2693,9 +2739,9 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "requires": { "estraverse": "^5.1.0" } @@ -2769,20 +2815,10 @@ "ms": "2.0.0" } }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" } } }, @@ -2980,15 +3016,6 @@ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, "fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -3090,6 +3117,36 @@ "yargs": "^16.2.0" }, "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -3241,27 +3298,12 @@ "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" - }, - "dependencies": { - "decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "requires": { - "mimic-response": "^3.1.0" - } - }, - "mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" - } } }, "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "grapheme-splitter": { "version": "1.0.4", @@ -3366,6 +3408,11 @@ } } }, + "hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==" + }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3387,13 +3434,6 @@ "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" - }, - "dependencies": { - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - } } }, "http-proxy-agent": { @@ -3514,15 +3554,6 @@ "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, - "into-stream": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-5.1.1.tgz", - "integrity": "sha512-krrAJ7McQxGGmvaYbB7Q1mcA+cRwg9Ij2RfWIeVesNBgVDZmzY/Fa4IpZUT3bmdRzMzdf/mzltCG2Dq99IZGBA==", - "requires": { - "from2": "^2.3.0", - "p-is-promise": "^3.0.0" - } - }, "ip": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", @@ -3549,18 +3580,18 @@ } }, "is-builtin-module": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.0.tgz", - "integrity": "sha512-phDA4oSGt7vl1n5tJvTWooWWAsXLY+2xCnxNqvKhGEzujg+A43wPlPOyDg3C8XQHN+6k/JTQWJ/j0dQh/qr+Hw==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", "dev": true, "requires": { "builtin-modules": "^3.3.0" } }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.0.tgz", + "integrity": "sha512-RECHCBCd/viahWmwj6enj19sKbHfJrddi/6cBDsNTKbNq0f7VeaUkBo60BqzvPqo/W54ChS62Z5qyun7cfOMqQ==", "dev": true, "requires": { "has": "^1.0.3" @@ -3657,7 +3688,8 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true }, "isexe": { "version": "2.0.0", @@ -3757,9 +3789,9 @@ } }, "js-sdsl": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.3.0.tgz", - "integrity": "sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ==" + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.4.0.tgz", + "integrity": "sha512-FfVSdx6pJ41Oa+CF7RDaFmTnCaFhua+SNYQX74riGOpl96x+2jQCqEfQ2bnXu/5DPCqlRuiqyvTJM0Qjz26IVg==" }, "js-tokens": { "version": "4.0.0", @@ -3994,9 +4026,9 @@ "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, "lru-cache": { - "version": "7.14.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.1.tgz", - "integrity": "sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA==" + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==" }, "lunr": { "version": "2.3.9", @@ -4236,9 +4268,9 @@ } }, "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" }, "min-indent": { "version": "1.0.1", @@ -4255,9 +4287,9 @@ } }, "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true }, "minimist-options": { @@ -4499,11 +4531,6 @@ "ms": "2.0.0" } }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4670,9 +4697,9 @@ } }, "node-releases": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.8.tgz", - "integrity": "sha512-dFSmB8fFHEH/s81Xi+Y/15DQY6VHW81nXRj86EMSL3lmuTmK1e+aT4wrFCkTbm+gSwkw4KpX+rT/pMM2c1mF+A==", + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", "dev": true }, "nodemailer": { @@ -4947,11 +4974,6 @@ "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==" }, - "p-is-promise": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", - "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==" - }, "p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -5173,7 +5195,8 @@ "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true }, "process-on-spawn": { "version": "1.0.0", @@ -5434,17 +5457,13 @@ } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, "readdirp": { @@ -8796,9 +8815,9 @@ } }, "regexp-tree": { - "version": "0.1.24", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.24.tgz", - "integrity": "sha512-s2aEVuLhvnVJW6s/iPgEGK6R+/xngd2jNQ+xy4bXNDKxZKJH6jpPHY6kVeVv1IeLCHgswRj+Kl3ELaDjG6V1iw==", + "version": "0.1.25", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.25.tgz", + "integrity": "sha512-szcL3aqw+vEeuxhL1AMYRyeMP+goYF5I/guaH10uJX5xbGyeQeNPPneaj3ZWVmGLCDxrVaaYekkr5R12gk4dJw==", "dev": true }, "regexpp": { @@ -8833,12 +8852,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.11.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } @@ -8908,9 +8927,9 @@ } }, "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safe-regex": { "version": "2.1.1", @@ -8922,9 +8941,9 @@ } }, "safe-stable-stringify": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.2.tgz", - "integrity": "sha512-gMxvPJYhP0O9n2pvcfYfIuYgbledAOJFcqRThtPRmjscaipiwcwPPKLytpVzMkG2HAN87Qmo2d4PtGiri1dSLA==" + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==" }, "safer-buffer": { "version": "2.1.2", @@ -8937,9 +8956,9 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, "semver": { - "version": "7.3.8", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", - "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==", "requires": { "lru-cache": "^6.0.0" }, @@ -8988,11 +9007,6 @@ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" } } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" } } }, @@ -9170,9 +9184,9 @@ } }, "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, "requires": { "spdx-expression-parse": "^3.0.0", @@ -9196,9 +9210,9 @@ } }, "spdx-license-ids": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.12.tgz", - "integrity": "sha512-rr+VVSXtRhO4OHbXUiAF7xW3Bo9DuuF6C5jH+q/x15j2jniycgKbxU09Hr0WqlSLUs4i4ltHGXqTe7VHclYWyA==", + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", "dev": true }, "split": { @@ -9217,19 +9231,6 @@ "dev": true, "requires": { "readable-stream": "^3.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "sprintf-js": { @@ -9251,6 +9252,11 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -9262,11 +9268,11 @@ } }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, "strip-ansi": { @@ -9361,12 +9367,9 @@ }, "dependencies": { "minipass": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", - "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", - "requires": { - "yallist": "^4.0.0" - } + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==" } } }, @@ -9453,19 +9456,6 @@ "dev": true, "requires": { "readable-stream": "3" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } } }, "tmp": { @@ -9527,9 +9517,9 @@ "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==" }, "typescript": { - "version": "4.9.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", - "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==" + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==" } } }, @@ -9559,10 +9549,9 @@ "integrity": "sha512-crvloFKZlPIysdVcP7Ej1w4HijBx7NmLdeorqfxOvt87DcUIbhKV4ZaSgCL+IQ+zzTgDx5zDuNHRvUbTIr9aqw==" }, "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz", + "integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==" }, "tsutils": { "version": "3.21.0", @@ -9571,6 +9560,14 @@ "dev": true, "requires": { "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } } }, "type-check": { @@ -9632,9 +9629,9 @@ } }, "marked": { - "version": "4.2.12", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.2.12.tgz", - "integrity": "sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true }, "minimatch": { @@ -9670,6 +9667,14 @@ "dev": true, "optional": true }, + "undici": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.22.0.tgz", + "integrity": "sha512-fR9RXCc+6Dxav4P9VV/sp5w3eFiSdOjJYsbtWfd4s5L5C4ogyuVpdKIVHeW0vV1MloM65/f7W45nR9ZxwVdyiA==", + "requires": { + "busboy": "^1.6.0" + } + }, "unique-filename": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz", @@ -9697,9 +9702,9 @@ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" }, "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", "dev": true, "requires": { "escalade": "^3.1.1", @@ -9775,9 +9780,9 @@ } }, "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", "dev": true }, "wide-align": { diff --git a/package.json b/package.json index 08ca195c..ad28e044 100644 --- a/package.json +++ b/package.json @@ -27,14 +27,14 @@ "start": "NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node ./lib/cli.js", "start-debug": "STAPPS_LOG_LEVEL=31 NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node ./lib/cli.js --require ts-node/register", "test": "npm run test-unit && npm run test-integration", - "test-unit": "env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 nyc mocha --require ts-node/register --exit 'test/**/*.spec.ts'", + "test-unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 nyc mocha --require ts-node/register --exit 'test/**/*.spec.ts'", "test-integration": "docker-compose -f integration-test.yml pull && docker-compose -f integration-test.yml up --build --abort-on-container-exit --exit-code-from apicli", "lint": "eslint -c .eslintrc.json --ignore-path .eslintignore --ext .ts src/ test/", "lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts src/ test/" }, "dependencies": { - "@elastic/elasticsearch": "5.6.22", - "@openstapps/core": "0.74.0", + "@elastic/elasticsearch": "8.4.0", + "@openstapps/core": "1.0.1", "@openstapps/core-tools": "0.34.0", "@openstapps/logger": "1.1.1", "@types/node": "14.18.36", @@ -57,14 +57,13 @@ }, "devDependencies": { "@openstapps/configuration": "0.34.0", - "@openstapps/es-mapping-generator": "0.4.0", + "@openstapps/es-mapping-generator": "0.6.0", "@openstapps/eslint-config": "1.1.0", "@testdeck/mocha": "0.3.3", "@types/chai": "4.3.4", "@types/chai-as-promised": "7.1.5", "@types/config": "3.3.0", "@types/cors": "2.8.13", - "@types/elasticsearch": "5.0.40", "@types/express": "4.17.16", "@types/geojson": "1.0.6", "@types/mocha": "10.0.1", @@ -80,6 +79,7 @@ "chai": "4.3.7", "chai-as-promised": "7.1.1", "conventional-changelog-cli": "2.2.2", + "cross-env": "7.0.3", "eslint": "8.33.0", "eslint-config-prettier": "8.6.0", "eslint-plugin-jsdoc": "39.7.4", diff --git a/src/app.ts b/src/app.ts index 6c98497e..0a4c024f 100644 --- a/src/app.ts +++ b/src/app.ts @@ -64,6 +64,7 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat }), ); + /* istanbul ignore if */ if (process.env.PROMETHEUS_MIDDLEWARE === 'true') { app.use(getPrometheusMiddleware()); } @@ -142,7 +143,10 @@ export async function configureApp(app: Express, databases: {[name: string]: Dat }); // validate config file - await validator.addSchemas(path.join('node_modules', '@openstapps', 'core', 'lib', 'schema')); + await validator.addSchemas( + // eslint-disable-next-line unicorn/prefer-module + path.join(path.dirname(require.resolve('@openstapps/core/package.json')), 'lib', 'schema'), + ); // validate the config file const configValidation = validator.validate(configFile, 'SCConfigFile'); diff --git a/src/storage/elasticsearch/aggregations.ts b/src/storage/elasticsearch/aggregations.ts index 5b01da46..0df62027 100644 --- a/src/storage/elasticsearch/aggregations.ts +++ b/src/storage/elasticsearch/aggregations.ts @@ -13,73 +13,45 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import {SCFacet, SCThingType} from '@openstapps/core'; -import {aggregations} from './templating'; -import {AggregationResponse} from './types/elasticsearch'; import { - isBucketAggregation, - isESAggMatchAllFilter, - isESNestedAggregation, - isESTermsFilter, - isNestedAggregation, -} from './types/guards'; + AggregateName, + AggregationsAggregate, + AggregationsFiltersAggregate, + AggregationsMultiTermsBucket, +} from '@elastic/elasticsearch/lib/api/types'; +import {SCFacet, SCThingType} from '@openstapps/core'; /** * Parses elasticsearch aggregations (response from es) to facets for the app * * @param aggregationResponse - aggregations response from elasticsearch */ -export function parseAggregations(aggregationResponse: AggregationResponse): SCFacet[] { +export function parseAggregations( + aggregationResponse: Record, +): SCFacet[] { const facets: SCFacet[] = []; - // get all names of the types an aggregation is on - for (const typeName in aggregations) { - if (aggregations.hasOwnProperty(typeName) && aggregationResponse.hasOwnProperty(typeName)) { - // the type object from the schema - const type = aggregations[typeName]; - // the "real" type object from the response - const realType = aggregationResponse[typeName]; + for (const aggregateName in aggregationResponse) { + const aggregation = aggregationResponse[aggregateName] as AggregationsMultiTermsBucket; + const type = aggregateName === '@all' ? {} : {onlyOnType: aggregateName as SCThingType}; - // both conditions must apply, else we have an error somewhere - if (isESNestedAggregation(type) && isNestedAggregation(realType)) { - for (const fieldName in type.aggs) { - if (type.aggs.hasOwnProperty(fieldName) && realType.hasOwnProperty(fieldName)) { - // the field object from the schema - const field = type.aggs[fieldName]; - // the "real" field object from the response - const realField = realType[fieldName]; + for (const field in aggregation) { + const fieldAggregate = aggregation[field] as AggregationsFiltersAggregate; + if (typeof fieldAggregate !== 'object') continue; - // this should always be true in theory... - if (isESTermsFilter(field) && isBucketAggregation(realField) && realField.buckets.length > 0) { - const facet: SCFacet = { - buckets: realField.buckets.map(bucket => { - return { - count: bucket.doc_count, - key: bucket.key, - }; - }), - field: fieldName, - }; - // if it's not for all types then create the appropriate field and set the type name - if (!isESAggMatchAllFilter(type.filter)) { - facet.onlyOnType = type.filter.type.value as SCThingType; - } - facets.push(facet); - } - } - } - // the last part here means that it is a bucket aggregation - } else if (isESTermsFilter(type) && !isNestedAggregation(realType) && realType.buckets.length > 0) { - facets.push({ - buckets: realType.buckets.map(bucket => { - return { - count: bucket.doc_count, - key: bucket.key, - }; - }), - field: typeName, - }); - } + const buckets = Object.values(fieldAggregate.buckets).map(bucket => { + return { + count: bucket.doc_count, + key: bucket.key as string, + }; + }); + if (buckets.length === 0) continue; + + facets.push({ + buckets, + field, + ...type, + }); } } diff --git a/src/storage/elasticsearch/elasticsearch.ts b/src/storage/elasticsearch/elasticsearch.ts index b9e76958..02ca1e2e 100644 --- a/src/storage/elasticsearch/elasticsearch.ts +++ b/src/storage/elasticsearch/elasticsearch.ts @@ -1,5 +1,5 @@ /* - * Copyright (C) 2019 StApps + * Copyright (C) 2022 StApps * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the @@ -13,58 +13,47 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import {ApiResponse, Client, events, RequestParams} from '@elastic/elasticsearch'; +import {Client, events} from '@elastic/elasticsearch'; import { - SCBulkResponse, - SCConfigFile, - SCFacet, - SCSearchQuery, - SCSearchResponse, - SCThings, - SCThingType, - SCUuid, -} from '@openstapps/core'; + AggregateName, + AggregationsMultiTermsBucket, + IndicesGetAliasResponse, + IndicesUpdateAliasesAction, + SearchHit, + SearchResponse, +} from '@elastic/elasticsearch/lib/api/types'; +import {SCConfigFile, SCSearchQuery, SCSearchResponse, SCThings, SCUuid} from '@openstapps/core'; import {Logger} from '@openstapps/logger'; -// we only have the @types package because some things type definitions are still missing from the official -// @elastic/elasticsearch package -import {IndicesUpdateAliasesParamsAction, SearchResponse} from 'elasticsearch'; import moment from 'moment'; import {MailQueue} from '../../notification/mail-queue'; import {Bulk} from '../bulk-storage'; import {Database} from '../database'; import {parseAggregations} from './aggregations'; import * as Monitoring from './monitoring'; -import {buildQuery, buildSort} from './query'; +import {buildQuery} from './query/query'; +import {buildSort} from './query/sort'; import {aggregations, putTemplate} from './templating'; import { - AggregationResponse, ElasticsearchConfig, - ElasticsearchObject, ElasticsearchQueryDisMaxConfig, ElasticsearchQueryQueryStringConfig, -} from './types/elasticsearch'; - -/** - * Matches index names such as stapps___ - */ -const indexRegex = /^stapps_([A-z0-9_]+)_([a-z0-9-_]+)_([-a-z0-9^_]+)$/; +} from './types/elasticsearch-config'; +import {ALL_INDICES_QUERY, getThingIndexName, parseIndexName, VALID_INDEX_REGEX} from './util'; +import {removeInvalidAliasChars} from './util/alias'; +import {noUndefined} from './util/no-undefined'; +import {retryCatch, RetryOptions} from './util/retry'; /** * A database interface for elasticsearch */ export class Elasticsearch implements Database { - /** - * Length of the index UID used for generation of its name - */ - static readonly INDEX_UID_LENGTH = 8; - /** * Holds a map of all elasticsearch indices that are available to search */ aliasMap: { - // each scType has a alias which can contain multiple sources + // each scType has an alias which can contain multiple sources [scType: string]: { - // each source is assigned a index name in elasticsearch + // each source is assigned an index name in elasticsearch [source: string]: string; }; }; @@ -97,89 +86,11 @@ export class Elasticsearch implements Database { return 'http://localhost:9200'; } - /** - * Gets the index name in elasticsearch for one SCThingType - * - * @param type SCThingType of data in the index - * @param source source of data in the index - * @param bulk bulk process which created this index - */ - static getIndex(type: SCThingType, source: string, bulk: SCBulkResponse) { - let out = type.toLowerCase(); - while (out.includes(' ')) { - out = out.replace(' ', '_'); - } - - return `stapps_${out}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`; - } - - /** - * Provides the index UID (for its name) from the bulk UID - * - * @param uid Bulk UID - */ - static getIndexUID(uid: SCUuid) { - return uid.slice(0, Math.max(0, Elasticsearch.INDEX_UID_LENGTH)); - } - - /** - * Generates a string which matches all indices - */ - static getListOfAllIndices(): string { - // map each SC type in upper camel case - return 'stapps_*_*_*'; - } - - /** - * Checks for invalid character in alias names and removes them - * - * @param alias The alias name - * @param uid The UID of the current bulk (for debugging purposes) - */ - static removeAliasChars(alias: string, uid: string | undefined): string { - let formattedAlias = alias; - - // spaces are included in some types, replace them with underscores - if (formattedAlias.includes(' ')) { - formattedAlias = formattedAlias.trim(); - formattedAlias = formattedAlias.split(' ').join('_'); - } - // List of invalid characters: https://www.elastic.co/guide/en/elasticsearch/reference/6.6/indices-create-index.html - for (const value of ['\\', '/', '*', '?', '"', '<', '>', '|', ',', '#']) { - if (formattedAlias.includes(value)) { - formattedAlias = formattedAlias.replace(value, ''); - Logger.warn(`Type of the bulk ${uid} contains an invalid character '${value}'. This can lead to two bulks - having the same alias despite having different types, as invalid characters are removed automatically. - New alias name is "${formattedAlias}."`); - } - } - for (const value of ['-', '_', '+']) { - if (formattedAlias.charAt(0) === value) { - formattedAlias = formattedAlias.slice(1); - Logger.warn(`Type of the bulk ${uid} begins with '${value}'. This can lead to two bulks having the same - alias despite having different types, as invalid characters are removed automatically. - New alias name is "${formattedAlias}."`); - } - } - if (formattedAlias === '.' || formattedAlias === '..') { - Logger.warn(`Type of the bulk ${uid} is ${formattedAlias}. This is an invalid name, please consider using - another one, as it will be replaced with 'alias_placeholder', which can lead to strange errors.`); - - return 'alias_placeholder'; - } - if (formattedAlias.includes(':')) { - Logger.warn(`Type of the bulk ${uid} contains a ':'. This isn't an issue now, but will be in future - Elasticsearch versions!`); - } - - return formattedAlias; - } - /** * Create a new interface for elasticsearch * * @param config an assembled config file - * @param mailQueue a mailqueue for monitoring + * @param mailQueue a mail queue for monitoring */ constructor(private readonly config: SCConfigFile, mailQueue?: MailQueue) { if ( @@ -192,7 +103,7 @@ export class Elasticsearch implements Database { this.client = new Client({ node: Elasticsearch.getElasticsearchUrl(), }); - this.client.on(events.REQUEST, async (error: Error | null, result: ApiResponse) => { + this.client.diagnostic.on(events.REQUEST, async (error: Error | null, result: unknown) => { if (error !== null) { await Logger.error(error); } @@ -210,73 +121,40 @@ export class Elasticsearch implements Database { /** * Gets a map which contains each alias and all indices that are associated with each alias */ - private async getAliasMap() { - // delay after which alias map will be fetched again - const RETRY_INTERVAL = 5000; - // maximum number of retries - const RETRY_COUNT = 3; - // create a list of old indices that are not in use - const oldIndicesToDelete: string[] = []; - - let aliases: - | { - [index: string]: { - /** - * Aliases of an index - */ - aliases: { - [K in SCThingType]: unknown; - }; - }; - } - | undefined; - - for (const retry of [...Array.from({length: RETRY_COUNT})].map((_, i) => i + 1)) { - if (typeof aliases !== 'undefined') { - break; - } - try { - const aliasResponse = await this.client.indices.getAlias({}); - aliases = aliasResponse.body; - } catch (error) { + private async getAliasMap(retryOptions: Partial> = {}) { + const aliasResponse = await retryCatch({ + maxRetries: 10, + retryInterval: 2000, + doAction: () => this.client.indices.getAlias(), + onFailedAttempt: (attempt, error, {maxRetries, retryInterval}) => { Logger.warn('Failed getting alias map:', error); - Logger.warn(`Retrying in ${RETRY_INTERVAL} milliseconds. (${retry} of ${RETRY_COUNT})`); - await new Promise(resolve => setTimeout(resolve, RETRY_INTERVAL)); - } - } + Logger.warn(`Retrying in ${retryInterval} milliseconds. (${attempt} of ${maxRetries})`); + }, + onFail: ({maxRetries}) => { + throw new TypeError(`Failed to retrieve alias map after ${maxRetries} attempts!`); + }, + ...retryOptions, + }); - if (typeof aliases === 'undefined') { - throw new TypeError(`Failed to retrieve alias map after ${RETRY_COUNT} attempts!`); - } + const aliases = Object.entries(aliasResponse) + .filter(([index]) => !index.startsWith('.')) + .map(([index, alias]) => ({ + index, + alias, + ...parseIndexName(index), + })); - for (const index in aliases) { - if (aliases.hasOwnProperty(index)) { - const matches = indexRegex.exec(index); - if (matches !== null) { - const type = matches[1]; - const source = matches[2]; - - // check if there is an alias for the current index - // check that alias equals type - const hasAlias = type in aliases[index].aliases; - if (hasAlias) { - if (typeof this.aliasMap[type] === 'undefined') { - this.aliasMap[type] = {}; - } - this.aliasMap[type][source] = index; - } else { - oldIndicesToDelete.push(index); - } - } - } + for (const {type, index, source} of aliases.filter(({type, alias}) => type in alias.aliases)) { + this.aliasMap[type] = this.aliasMap[type] || {}; + this.aliasMap[type][source] = index; } this.ready = true; - // delete old indices that are not used in any alias - if (oldIndicesToDelete.length > 0) { + const unusedIndices = aliases.filter(({type, alias}) => !(type in alias.aliases)).map(({index}) => index); + if (unusedIndices.length > 0) { await this.client.indices.delete({ - index: oldIndicesToDelete, + index: unusedIndices, }); Logger.warn(`Deleted old indices: oldIndicesToDelete`); } @@ -291,8 +169,8 @@ export class Elasticsearch implements Database { * @param uid an UID to use for the search * @returns an elasticsearch object containing the thing */ - private async getObject(uid: SCUuid): Promise | undefined> { - const searchResponse: ApiResponse> = await this.client.search({ + private async getObject(uid: SCUuid): Promise | undefined> { + const searchResponse = await this.client.search({ body: { query: { term: { @@ -303,43 +181,44 @@ export class Elasticsearch implements Database { }, }, from: 0, - index: Elasticsearch.getListOfAllIndices(), + index: ALL_INDICES_QUERY, size: 1, }); // return data from response - return searchResponse.body.hits.hits[0]; + return searchResponse.hits.hits[0]; } - /** - * Should be called, when a new bulk was created. Creates a new index and applies a the mapping to the index - * - * @param bulk the bulk process that was created - */ - public async bulkCreated(bulk: Bulk): Promise { - // if our es instance is not ready yet, we cannot serve this request + private async prepareBulkWrite(bulk: Bulk): Promise<{index: string; alias: string}> { if (!this.ready) { throw new Error('No connection to elasticsearch established yet.'); } - // index name for elasticsearch - const index: string = Elasticsearch.getIndex(bulk.type, bulk.source, bulk); - - // there already is an index with this type and source. We will index the new one and switch the alias to it - // the old one is deleted - const alias = Elasticsearch.removeAliasChars(bulk.type, bulk.uid); + const index = getThingIndexName(bulk.type, bulk.source, bulk); + const alias = removeInvalidAliasChars(bulk.type, bulk.uid); if (typeof this.aliasMap[alias] === 'undefined') { this.aliasMap[alias] = {}; } - if (!indexRegex.test(index)) { + if (!VALID_INDEX_REGEX.test(index)) { throw new Error( `Index names can only consist of lowercase letters from a-z, "-", "_" and integer numbers. Make sure to set the bulk "source" and "type" to names consisting of the characters above.`, ); } + return {index, alias}; + } + + /** + * Should be called, when a new bulk was created. Creates a new index and applies the mapping to the index + * + * @param bulk the bulk process that was created + */ + public async bulkCreated(bulk: Bulk): Promise { + const {index} = await this.prepareBulkWrite(bulk); + // re-apply the index template before each new bulk operation await putTemplate(this.client, bulk.type); await this.client.indices.create({ @@ -355,8 +234,7 @@ export class Elasticsearch implements Database { * @param bulk the bulk process that is expired */ public async bulkExpired(bulk: Bulk): Promise { - // index name for elasticsearch - const index: string = Elasticsearch.getIndex(bulk.type, bulk.source, bulk); + const index: string = getThingIndexName(bulk.type, bulk.source, bulk); Logger.info('Bulk expired. Deleting index', index); @@ -375,31 +253,11 @@ export class Elasticsearch implements Database { * @param bulk the new bulk process that should replace the old one with same type and source */ public async bulkUpdated(bulk: Bulk): Promise { - // if our es instance is not ready yet, we cannot serve this request - if (!this.ready) { - throw new Error('No connection to elasticsearch established yet.'); - } + const {index, alias} = await this.prepareBulkWrite(bulk); - // index name for elasticsearch - const index: string = Elasticsearch.getIndex(bulk.type, bulk.source, bulk); - - // alias for the indices - const alias = Elasticsearch.removeAliasChars(bulk.type, bulk.uid); - - if (typeof this.aliasMap[alias] === 'undefined') { - this.aliasMap[alias] = {}; - } - - if (!indexRegex.test(index)) { - throw new Error( - `Index names can only consist of lowercase letters from a-z, "-", "_" and integer numbers. - Make sure to set the bulk "source" and "type" to names consisting of the characters above.`, - ); - } - - // create the new index if it does not exists + // create the new index if it does not exist // eslint-disable-next-line unicorn/no-await-expression-member - if (!(await this.client.indices.exists({index})).body) { + if (!(await this.client.indices.exists({index}))) { // re-apply the index template before each new bulk operation await putTemplate(this.client, bulk.type); await this.client.indices.create({ @@ -412,7 +270,7 @@ export class Elasticsearch implements Database { // add our new index to the alias // this was type safe with @types/elasticsearch, the new package however provides no type definitions - const actions: IndicesUpdateAliasesParamsAction[] = [ + const actions: IndicesUpdateAliasesAction[] = [ { add: {index: index, alias: alias}, }, @@ -427,16 +285,10 @@ export class Elasticsearch implements Database { } // refresh the index (fsync changes) - await this.client.indices.refresh({ - index: index, - }); + await this.client.indices.refresh({index}); // execute our alias actions - await this.client.indices.updateAliases({ - body: { - actions, - }, - }); + await this.client.indices.updateAliases({actions}); // swap the index in our aliasMap this.aliasMap[alias][bulk.source] = index; @@ -457,7 +309,7 @@ export class Elasticsearch implements Database { public async get(uid: SCUuid): Promise { const object = await this.getObject(uid); - if (typeof object === 'undefined') { + if (typeof object?._source === 'undefined') { throw new TypeError('Item not found.'); } @@ -467,7 +319,7 @@ export class Elasticsearch implements Database { /** * Initialize the elasticsearch database (call all needed methods) */ - public async init(): Promise { + public async init(retryOptions: Partial> = {}): Promise { const monitoringConfiguration = this.config.internal.monitoring; if (typeof monitoringConfiguration !== 'undefined') { @@ -480,7 +332,7 @@ export class Elasticsearch implements Database { await Monitoring.setUp(monitoringConfiguration, this.client, this.mailQueue); } - return this.getAliasMap(); + return this.getAliasMap(retryOptions); } /** @@ -490,7 +342,7 @@ export class Elasticsearch implements Database { * @param bulk the bulk process which item belongs to */ public async post(object: SCThings, bulk: Bulk): Promise { - const object_: SCThings & {creation_date: string} = { + const thing: SCThings & {creation_date: string} = { ...object, creation_date: moment().format(), }; @@ -499,7 +351,7 @@ export class Elasticsearch implements Database { // check that the item will get replaced if the index is rolled over (index with the same name excluding ending uid) if (typeof item !== 'undefined') { - const indexOfNew = Elasticsearch.getIndex(object_.type, bulk.source, bulk); + const indexOfNew = getThingIndexName(thing.type, bulk.source, bulk); const oldIndex = item._index; // new item doesn't replace the old one @@ -509,22 +361,23 @@ export class Elasticsearch implements Database { ) { throw new Error( // eslint-disable-next-line unicorn/no-null - `Object "${object_.uid}" already exists. Object was: ${JSON.stringify(object_, null, 2)}`, + `Object "${thing.uid}" already exists. Object was: ${JSON.stringify(thing, null, 2)}`, ); } } // regular bulk update (item gets replaced when bulk is updated) - const searchResponse = await this.client.create({ - body: object_, - id: object_.uid, - index: Elasticsearch.getIndex(object_.type, bulk.source, bulk), + const searchResponse = await this.client.create({ + document: thing, + id: thing.uid, + index: getThingIndexName(thing.type, bulk.source, bulk), timeout: '90s', - type: object_.type, }); - if (!searchResponse.body.created) { - throw new Error(`Object creation Error: Instance was: ${JSON.stringify(object_)}`); + if (searchResponse.result !== 'created') { + throw new Error( + `Object creation Error (${searchResponse.result}: Instance was: ${JSON.stringify(thing)}`, + ); } } @@ -543,7 +396,6 @@ export class Elasticsearch implements Database { }, id: object.uid, index: item._index, - type: object.type.toLowerCase(), }); return; @@ -562,65 +414,46 @@ export class Elasticsearch implements Database { throw new TypeError('Database is undefined. You have to configure the query build'); } - // create elasticsearch configuration out of data from database configuration const esConfig: ElasticsearchConfig = { name: this.config.internal.database.name as 'elasticsearch', version: this.config.internal.database.version as string, - }; - - if (typeof this.config.internal.database.query !== 'undefined') { - esConfig.query = this.config.internal.database.query as + query: this.config.internal.database.query as | ElasticsearchQueryDisMaxConfig - | ElasticsearchQueryQueryStringConfig; - } + | ElasticsearchQueryQueryStringConfig + | undefined, + }; - const searchRequest: RequestParams.Search = { - body: { - aggs: aggregations, - query: buildQuery(parameters, this.config, esConfig), - }, + const query = { + aggs: aggregations, + query: buildQuery(parameters, this.config, esConfig), from: parameters.from, - index: Elasticsearch.getListOfAllIndices(), + index: ALL_INDICES_QUERY, size: parameters.size, + sort: typeof parameters.sort !== 'undefined' ? buildSort(parameters.sort) : undefined, }; - - if (typeof parameters.sort !== 'undefined') { - searchRequest.body.sort = buildSort(parameters.sort); - } - - // perform the search against elasticsearch - const response: ApiResponse> = await this.client.search(searchRequest); - - // gather pagination information - const pagination = { - count: response.body.hits.hits.length, - offset: typeof parameters.from === 'number' ? parameters.from : 0, - total: response.body.hits.total, - }; - - // gather statistics about this search - const stats = { - time: response.body.took, - }; - - // we only directly return the _source documents - // elasticsearch provides much more information, the user shouldn't see - const data = response.body.hits.hits.map(hit => { - return hit._source; // SCThing - }); - - let facets: SCFacet[] = []; - - // read the aggregations from elasticsearch and parse them to facets by our configuration - if (typeof response.body.aggregations !== 'undefined') { - facets = parseAggregations(response.body.aggregations as AggregationResponse); - } + const response: SearchResponse = await this.client.search(query); return { - data, - facets, - pagination, - stats, + data: response.hits.hits + .map(hit => { + // we only directly return the _source documents + // elasticsearch provides much more information, the user shouldn't see + return hit._source; + }) + .filter(noUndefined), + facets: + typeof response.aggregations !== 'undefined' + ? parseAggregations(response.aggregations as Record) + : [], + pagination: { + count: response.hits.hits.length, + offset: typeof parameters.from === 'number' ? parameters.from : 0, + total: + typeof response.hits.total === 'number' ? response.hits.total : response.hits.total?.value ?? 0, + }, + stats: { + time: response.took, + }, }; } } diff --git a/src/storage/elasticsearch/monitoring.ts b/src/storage/elasticsearch/monitoring.ts index 2af1b283..be7aba54 100644 --- a/src/storage/elasticsearch/monitoring.ts +++ b/src/storage/elasticsearch/monitoring.ts @@ -13,7 +13,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import {ApiResponse, Client, RequestParams} from '@elastic/elasticsearch'; +import {Client} from '@elastic/elasticsearch'; +import {SearchRequest} from '@elastic/elasticsearch/lib/api/types'; import { SCMonitoringConfiguration, SCMonitoringLogAction, @@ -23,9 +24,6 @@ import { SCThings, } from '@openstapps/core'; import {Logger} from '@openstapps/logger'; -// we only have the @types package because some things type definitions are still missing from the official -// @elastic/elasticsearch package -import {SearchResponse} from 'elasticsearch'; import cron from 'node-cron'; import {MailQueue} from '../../notification/mail-queue'; @@ -131,12 +129,11 @@ export async function setUp( cron.schedule(trigger.executionTime, async () => { // execute watch (search->condition->action) - const result: ApiResponse> = await esClient.search( - watcher.query as RequestParams.Search, - ); + const result = await esClient.search(watcher.query as SearchRequest); // check conditions - const total = result.body.hits.total; + const total = + typeof result.hits.total === 'number' ? result.hits.total : result.hits.total?.value ?? -1; for (const condition of watcher.conditions) { if (conditionFails(condition, total)) { diff --git a/src/storage/elasticsearch/query.ts b/src/storage/elasticsearch/query.ts deleted file mode 100644 index ef174e64..00000000 --- a/src/storage/elasticsearch/query.ts +++ /dev/null @@ -1,501 +0,0 @@ -/* - * Copyright (C) 2019-2021 StApps - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { - SCBackendConfigurationSearchBoostingContext, - SCBackendConfigurationSearchBoostingType, - SCConfigFile, - SCSearchBooleanFilter, - SCSearchContext, - SCSearchFilter, - SCSearchQuery, - SCSearchSort, - SCSportCoursePriceGroup, - SCThingsField, -} from '@openstapps/core'; -import { - ElasticsearchConfig, - ESBooleanFilter, - ESBooleanFilterArguments, - ESDateRange, - ESDateRangeFilter, - ESFunctionScoreQuery, - ESFunctionScoreQueryFunction, - ESGenericRange, - ESGenericSort, - ESGeoBoundingBoxFilter, - ESGeoDistanceFilter, - ESGeoDistanceFilterArguments, - ESGeoDistanceSort, - ESGeoDistanceSortArguments, - ESGeoShapeFilter, - ESNumericRangeFilter, - ESRangeFilter, - ESTermFilter, - ESTypeFilter, - ScriptSort, -} from './types/elasticsearch'; - -/** - * Escapes any reserved character that would otherwise not be accepted by Elasticsearch - * - * Elasticsearch as the following reserved characters: - * + - = && || > < ! ( ) { } [ ] ^ " ~ * ? : \ / - * It is possible to use all, with the exception of < and >, of them by escaping them with a \ - * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html - * - * @param string_ the string to escape the characters from - */ -function escapeESReservedCharacters(string_: string): string { - return string_.replace(/[+\-=!(){}\[\]^"~*?:\\/]|(&&)|(\|\|)/g, '\\$&'); -} - -/** - * Builds a boolean filter. Returns an elasticsearch boolean filter - * - * @param booleanFilter a search boolean filter for the retrieval of the data - * @returns elasticsearch boolean arguments object - */ -export function buildBooleanFilter(booleanFilter: SCSearchBooleanFilter): ESBooleanFilterArguments { - const result: ESBooleanFilterArguments = { - minimum_should_match: 0, - must: [], - must_not: [], - should: [], - }; - - if (booleanFilter.arguments.operation === 'and') { - result.must = booleanFilter.arguments.filters.map(filter => buildFilter(filter)); - } - - if (booleanFilter.arguments.operation === 'or') { - result.should = booleanFilter.arguments.filters.map(filter => buildFilter(filter)); - result.minimum_should_match = 1; - } - - if (booleanFilter.arguments.operation === 'not') { - result.must_not = booleanFilter.arguments.filters.map(filter => buildFilter(filter)); - } - - return result; -} - -/** - * Converts Array of Filters to elasticsearch query-syntax - * - * @param filter A search filter for the retrieval of the data - */ -export function buildFilter( - filter: SCSearchFilter, -): - | ESTermFilter - | ESGeoDistanceFilter - | ESBooleanFilter - | ESGeoShapeFilter - | ESBooleanFilter - | ESRangeFilter { - switch (filter.type) { - case 'value': - return Array.isArray(filter.arguments.value) - ? { - terms: { - [`${filter.arguments.field}.raw`]: filter.arguments.value, - }, - } - : { - term: { - [`${filter.arguments.field}.raw`]: filter.arguments.value, - }, - }; - case 'availability': - const scope = filter.arguments.scope?.charAt(0) ?? 's'; - const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`; - - return { - range: { - [filter.arguments.field]: { - gte: `${time}/${scope}`, - lt: `${time}+1${scope}/${scope}`, - relation: 'intersects', - }, - }, - }; - case 'distance': - const geoObject: ESGeoDistanceFilterArguments = { - distance: `${filter.arguments.distance}m`, - [`${filter.arguments.field}.point.coordinates`]: { - lat: filter.arguments.position[1], - lon: filter.arguments.position[0], - }, - }; - - return { - geo_distance: geoObject, - }; - case 'boolean': - return { - bool: buildBooleanFilter(filter), - }; - case 'numeric range': - const numericRangeObject: ESGenericRange = { - relation: filter.arguments.relation, - }; - if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { - numericRangeObject.gt = filter.arguments.bounds.lowerBound.limit; - } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { - numericRangeObject.gte = filter.arguments.bounds.lowerBound.limit; - } - if (filter.arguments.bounds.upperBound?.mode === 'exclusive') { - numericRangeObject.lt = filter.arguments.bounds.upperBound.limit; - } else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') { - numericRangeObject.lte = filter.arguments.bounds.upperBound.limit; - } - - const numericRangeFilter: ESNumericRangeFilter = {range: {}}; - numericRangeFilter.range[filter.arguments.field] = numericRangeObject; - - return numericRangeFilter; - case 'date range': - const dateRangeObject: ESDateRange = { - format: filter.arguments.format, - time_zone: filter.arguments.timeZone, - relation: filter.arguments.relation, - }; - if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { - dateRangeObject.gt = filter.arguments.bounds.lowerBound.limit; - } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { - dateRangeObject.gte = filter.arguments.bounds.lowerBound.limit; - } - if (filter.arguments.bounds.upperBound?.mode === 'exclusive') { - dateRangeObject.lt = filter.arguments.bounds.upperBound.limit; - } else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') { - dateRangeObject.lte = filter.arguments.bounds.upperBound.limit; - } - - const dateRangeFilter: ESDateRangeFilter = {range: {}}; - dateRangeFilter.range[filter.arguments.field] = dateRangeObject; - - return dateRangeFilter; - case 'geo': - // TODO: on ES upgrade, use just geo_shape filters - const geoShapeFilter: ESGeoShapeFilter = { - geo_shape: { - /** - * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3 - */ - // @ts-expect-error unfortunately, typescript is stupid and won't allow me to map this to an actual type. - ignore_unmapped: true, - [`${filter.arguments.field}.polygon`]: { - shape: filter.arguments.shape, - relation: filter.arguments.spatialRelation, - }, - }, - }; - - if ( - (typeof filter.arguments.spatialRelation === 'undefined' || - filter.arguments.spatialRelation === 'intersects') && - filter.arguments.shape.type === 'envelope' - ) { - return { - bool: { - minimum_should_match: 1, - should: [ - geoShapeFilter, - { - geo_bounding_box: { - /** - * https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_ignore_unmapped_3 - */ - ignore_unmapped: true, - [`${filter.arguments.field}.point.coordinates`]: { - top_left: filter.arguments.shape.coordinates[0], - bottom_right: filter.arguments.shape.coordinates[1], - }, - }, - }, - ], - }, - }; - } - - return geoShapeFilter; - } -} - -/** - * Builds scoring functions from boosting config - * - * @param boostings Backend boosting configuration for contexts and types - * @param context The context of the app from where the search was initiated - */ -function buildFunctions( - boostings: SCBackendConfigurationSearchBoostingContext, - context: SCSearchContext | undefined, -): ESFunctionScoreQueryFunction[] { - // default context - let functions: ESFunctionScoreQueryFunction[] = buildFunctionsForBoostingTypes( - boostings['default' as SCSearchContext], - ); - - if (typeof context !== 'undefined' && context !== 'default') { - // specific context provided, extend default context with additional boosts - functions = [...functions, ...buildFunctionsForBoostingTypes(boostings[context])]; - } - - return functions; -} - -/** - * Creates boost functions for all type boost configurations - * - * @param boostingTypes Array of type boosting configurations - */ -function buildFunctionsForBoostingTypes( - boostingTypes: SCBackendConfigurationSearchBoostingType[], -): ESFunctionScoreQueryFunction[] { - const functions: ESFunctionScoreQueryFunction[] = []; - - for (const boostingForOneSCType of boostingTypes) { - const typeFilter: ESTypeFilter = { - type: { - value: boostingForOneSCType.type, - }, - }; - - functions.push({ - filter: typeFilter, - weight: boostingForOneSCType.factor, - }); - - if (typeof boostingForOneSCType.fields !== 'undefined') { - const fields = boostingForOneSCType.fields; - - for (const fieldName in boostingForOneSCType.fields) { - if (boostingForOneSCType.fields.hasOwnProperty(fieldName)) { - const boostingForOneField = fields[fieldName]; - - for (const value in boostingForOneField) { - if (boostingForOneField.hasOwnProperty(value)) { - const factor = boostingForOneField[value]; - - // build term filter - const termFilter: ESTermFilter = { - term: {}, - }; - termFilter.term[`${fieldName}.raw`] = value; - - functions.push({ - filter: { - bool: { - must: [typeFilter, termFilter], - should: [], - }, - }, - weight: factor, - }); - } - } - } - } - } - } - - return functions; -} - -/** - * Builds body for Elasticsearch requests - * - * @param parameters Parameters for querying the backend - * @param defaultConfig Default configuration of the backend - * @param elasticsearchConfig Elasticsearch configuration - * @returns ElasticsearchQuery (body of a search-request) - */ -export function buildQuery( - parameters: SCSearchQuery, - defaultConfig: SCConfigFile, - elasticsearchConfig: ElasticsearchConfig, -): ESFunctionScoreQuery { - // if config provides an minMatch parameter we use query_string instead of match query - let query; - if (typeof elasticsearchConfig.query === 'undefined') { - query = { - query_string: { - analyzer: 'search_german', - default_field: 'name', - minimum_should_match: '90%', - query: typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query), - }, - }; - } else if (elasticsearchConfig.query.queryType === 'query_string') { - query = { - query_string: { - analyzer: 'search_german', - default_field: 'name', - minimum_should_match: elasticsearchConfig.query.minMatch, - query: typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query), - }, - }; - } else if (elasticsearchConfig.query.queryType === 'dis_max') { - if (parameters.query !== '*') { - query = { - dis_max: { - boost: 1.2, - queries: [ - { - match: { - name: { - boost: elasticsearchConfig.query.matchBoosting, - cutoff_frequency: elasticsearchConfig.query.cutoffFrequency, - fuzziness: elasticsearchConfig.query.fuzziness, - query: typeof parameters.query !== 'string' ? '*' : parameters.query, - }, - }, - }, - { - query_string: { - analyzer: 'search_german', - default_field: 'name', - minimum_should_match: elasticsearchConfig.query.minMatch, - query: - typeof parameters.query !== 'string' ? '*' : escapeESReservedCharacters(parameters.query), - }, - }, - ], - tie_breaker: elasticsearchConfig.query.tieBreaker, - }, - }; - } - } else { - throw new Error( - 'Unsupported query type. Check your config file and reconfigure your elasticsearch query', - ); - } - - const functionScoreQuery: ESFunctionScoreQuery = { - function_score: { - functions: buildFunctions(defaultConfig.internal.boostings, parameters.context), - query: { - bool: { - minimum_should_match: 0, // if we have no should, nothing can match - must: [], - should: [], - }, - }, - score_mode: 'multiply', - }, - }; - - const mustMatch = functionScoreQuery.function_score.query.bool.must; - - if (Array.isArray(mustMatch)) { - if (typeof query !== 'undefined') { - mustMatch.push(query); - } - - if (typeof parameters.filter !== 'undefined') { - mustMatch.push(buildFilter(parameters.filter)); - } - } - - return functionScoreQuery; -} - -/** - * converts query to - * - * @param sorts Sorting rules to apply to the data that is being queried - * @returns an array of sort queries - */ -export function buildSort(sorts: SCSearchSort[]): Array { - return sorts.map(sort => { - switch (sort.type) { - case 'generic': - const esGenericSort: ESGenericSort = {}; - esGenericSort[sort.arguments.field] = sort.order; - - return esGenericSort; - case 'ducet': - const esDucetSort: ESGenericSort = {}; - esDucetSort[`${sort.arguments.field}.sort`] = sort.order; - - return esDucetSort; - case 'distance': - const arguments_: ESGeoDistanceSortArguments = { - mode: 'avg', - order: sort.order, - unit: 'm', - }; - - arguments_[`${sort.arguments.field}.point.coordinates`] = { - lat: sort.arguments.position[1], - lon: sort.arguments.position[0], - }; - - return { - _geo_distance: arguments_, - }; - case 'price': - return { - _script: { - order: sort.order, - script: buildPriceSortScript(sort.arguments.universityRole, sort.arguments.field), - type: 'number' as const, - }, - }; - } - }); -} - -/** - * Provides a script for sorting search results by prices - * - * @param universityRole User group which consumes university services - * @param field Field in which wanted offers with prices are located - */ -export function buildPriceSortScript( - universityRole: keyof SCSportCoursePriceGroup, - field: SCThingsField, -): string { - return ` - // initialize the sort value with the maximum - double price = Double.MAX_VALUE; - - // if we have any offers - if (params._source.containsKey('${field}')) { - // iterate through all offers - for (offer in params._source.${field}) { - // if this offer contains a role specific price - if (offer.containsKey('prices') && offer.prices.containsKey('${universityRole}')) { - // if the role specific price is smaller than the cheapest we found - if (offer.prices.${universityRole} < price) { - // set the role specific price as cheapest for now - price = offer.prices.${universityRole}; - } - } else { // we have no role specific price for our role in this offer - // if the default price of this offer is lower than the cheapest we found - if (offer.price < price) { - // set this price as the cheapest - price = offer.price; - } - } - } - } - - // return cheapest price for our role - return price; - `; -} diff --git a/src/storage/elasticsearch/query/boost/boost-functions.ts b/src/storage/elasticsearch/query/boost/boost-functions.ts new file mode 100644 index 00000000..eeb711ba --- /dev/null +++ b/src/storage/elasticsearch/query/boost/boost-functions.ts @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslFunctionScoreContainer} from '@elastic/elasticsearch/lib/api/types'; +import {SCBackendConfigurationSearchBoostingType} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Creates boost functions for all type boost configurations + * + * @param boostingTypes Array of type boosting configurations + */ +export function buildFunctionsForBoostingTypes( + boostingTypes: SCBackendConfigurationSearchBoostingType[], +): QueryDslFunctionScoreContainer[] { + const functions: QueryDslFunctionScoreContainer[] = []; + + for (const boostingForOneSCType of boostingTypes) { + const typeFilter: QueryDslSpecificQueryContainer<'term'> = { + term: { + type: boostingForOneSCType.type, + }, + }; + + functions.push({ + filter: typeFilter, + weight: boostingForOneSCType.factor, + }); + + if (typeof boostingForOneSCType.fields !== 'undefined') { + const fields = boostingForOneSCType.fields; + + for (const fieldName in boostingForOneSCType.fields) { + if (boostingForOneSCType.fields.hasOwnProperty(fieldName)) { + const boostingForOneField = fields[fieldName]; + + for (const value in boostingForOneField) { + if (boostingForOneField.hasOwnProperty(value)) { + const factor = boostingForOneField[value]; + + // build term filter + const termFilter: QueryDslSpecificQueryContainer<'term'> = { + term: {}, + }; + termFilter.term[`${fieldName}.raw`] = value; + + functions.push({ + filter: { + bool: { + must: [typeFilter, termFilter], + should: [], + }, + }, + weight: factor, + }); + } + } + } + } + } + } + + return functions; +} diff --git a/src/storage/elasticsearch/query/boost/scoring-functions.ts b/src/storage/elasticsearch/query/boost/scoring-functions.ts new file mode 100644 index 00000000..58330e03 --- /dev/null +++ b/src/storage/elasticsearch/query/boost/scoring-functions.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslFunctionScoreContainer} from '@elastic/elasticsearch/lib/api/types'; +import {SCBackendConfigurationSearchBoostingContext, SCSearchContext} from '@openstapps/core'; +import {buildFunctionsForBoostingTypes} from './boost-functions'; + +/** + * Builds scoring functions from boosting config + * + * @param boostings Backend boosting configuration for contexts and types + * @param context The context of the app from where the search was initiated + */ +export function buildScoringFunctions( + boostings: SCBackendConfigurationSearchBoostingContext, + context: SCSearchContext | undefined, +): QueryDslFunctionScoreContainer[] { + // default context + let functions = buildFunctionsForBoostingTypes(boostings['default' as SCSearchContext]); + + if (typeof context !== 'undefined' && context !== 'default') { + // specific context provided, extend default context with additional boosts + functions = [...functions, ...buildFunctionsForBoostingTypes(boostings[context])]; + } + + return functions; +} diff --git a/src/storage/elasticsearch/query/filter.ts b/src/storage/elasticsearch/query/filter.ts new file mode 100644 index 00000000..70b7b8d7 --- /dev/null +++ b/src/storage/elasticsearch/query/filter.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslQueryContainer} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchFilter} from '@openstapps/core'; +import {buildBooleanFilter} from './filters/boolean'; +import {buildAvailabilityFilter} from './filters/availability'; +import {buildDateRangeFilter} from './filters/date-range'; +import {buildDistanceFilter} from './filters/distance'; +import {buildGeoFilter} from './filters/geo'; +import {buildNumericRangeFilter} from './filters/numeric-range'; +import {buildValueFilter} from './filters/value'; + +/** + * Converts Array of Filters to elasticsearch query-syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildFilter(filter: SCSearchFilter): QueryDslQueryContainer { + switch (filter.type) { + case 'value': + return buildValueFilter(filter); + case 'availability': + return buildAvailabilityFilter(filter); + case 'distance': + return buildDistanceFilter(filter); + case 'boolean': + return buildBooleanFilter(filter); + case 'numeric range': + return buildNumericRangeFilter(filter); + case 'date range': + return buildDateRangeFilter(filter); + case 'geo': + return buildGeoFilter(filter); + } +} diff --git a/src/storage/elasticsearch/query/filters/availability.ts b/src/storage/elasticsearch/query/filters/availability.ts new file mode 100644 index 00000000..5db3b2b9 --- /dev/null +++ b/src/storage/elasticsearch/query/filters/availability.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SCSearchAvailabilityFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts an availability filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildAvailabilityFilter( + filter: SCSearchAvailabilityFilter, +): QueryDslSpecificQueryContainer<'range'> { + const scope = filter.arguments.scope?.charAt(0) ?? 's'; + const time = typeof filter.arguments.time === 'undefined' ? 'now' : `${filter.arguments.time}||`; + + return { + range: { + [filter.arguments.field]: { + gte: `${time}/${scope}`, + lt: `${time}+1${scope}/${scope}`, + relation: 'intersects', + }, + }, + }; +} diff --git a/src/storage/elasticsearch/query/filters/boolean.ts b/src/storage/elasticsearch/query/filters/boolean.ts new file mode 100644 index 00000000..2a9a9f15 --- /dev/null +++ b/src/storage/elasticsearch/query/filters/boolean.ts @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslBoolQuery} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchBooleanFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; +import {buildFilter} from '../filter'; + +/** + * Converts a boolean filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildBooleanFilter(filter: SCSearchBooleanFilter): QueryDslSpecificQueryContainer<'bool'> { + const result: QueryDslBoolQuery = { + minimum_should_match: 0, + must: [], + must_not: [], + should: [], + }; + + if (filter.arguments.operation === 'and') { + result.must = filter.arguments.filters.map(it => buildFilter(it)); + } + + if (filter.arguments.operation === 'or') { + result.should = filter.arguments.filters.map(it => buildFilter(it)); + result.minimum_should_match = 1; + } + + if (filter.arguments.operation === 'not') { + result.must_not = filter.arguments.filters.map(it => buildFilter(it)); + } + + return { + bool: result, + }; +} diff --git a/src/storage/elasticsearch/query/filters/date-range.ts b/src/storage/elasticsearch/query/filters/date-range.ts new file mode 100644 index 00000000..1ce0757e --- /dev/null +++ b/src/storage/elasticsearch/query/filters/date-range.ts @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslDateRangeQuery} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchDateRangeFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts a date range filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildDateRangeFilter( + filter: SCSearchDateRangeFilter, +): QueryDslSpecificQueryContainer<'range'> { + const dateRangeObject: QueryDslDateRangeQuery = { + format: filter.arguments.format, + time_zone: filter.arguments.timeZone, + relation: filter.arguments.relation, + }; + if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { + dateRangeObject.gt = filter.arguments.bounds.lowerBound.limit; + } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { + dateRangeObject.gte = filter.arguments.bounds.lowerBound.limit; + } + if (filter.arguments.bounds.upperBound?.mode === 'exclusive') { + dateRangeObject.lt = filter.arguments.bounds.upperBound.limit; + } else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') { + dateRangeObject.lte = filter.arguments.bounds.upperBound.limit; + } + + return { + range: { + [filter.arguments.field]: dateRangeObject, + }, + }; +} diff --git a/src/storage/elasticsearch/query/filters/distance.ts b/src/storage/elasticsearch/query/filters/distance.ts new file mode 100644 index 00000000..b2ceb71b --- /dev/null +++ b/src/storage/elasticsearch/query/filters/distance.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslGeoDistanceQuery} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchDistanceFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts a distance filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildDistanceFilter( + filter: SCSearchDistanceFilter, +): QueryDslSpecificQueryContainer<'geo_distance'> { + const geoObject: QueryDslGeoDistanceQuery = { + distance: `${filter.arguments.distance}m`, + [`${filter.arguments.field}.point.coordinates`]: { + lat: filter.arguments.position[1], + lon: filter.arguments.position[0], + }, + }; + + return { + geo_distance: geoObject, + }; +} diff --git a/src/storage/elasticsearch/query/filters/geo.ts b/src/storage/elasticsearch/query/filters/geo.ts new file mode 100644 index 00000000..9196d220 --- /dev/null +++ b/src/storage/elasticsearch/query/filters/geo.ts @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SCGeoFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts a geo filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildGeoFilter(filter: SCGeoFilter): QueryDslSpecificQueryContainer<'geo_shape'> { + return { + geo_shape: { + ignore_unmapped: true, + [`${filter.arguments.field}.polygon`]: { + shape: filter.arguments.shape, + relation: filter.arguments.spatialRelation, + }, + }, + }; +} diff --git a/src/storage/elasticsearch/query/filters/numeric-range.ts b/src/storage/elasticsearch/query/filters/numeric-range.ts new file mode 100644 index 00000000..812cb1be --- /dev/null +++ b/src/storage/elasticsearch/query/filters/numeric-range.ts @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {QueryDslNumberRangeQuery} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchNumericRangeFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts a numeric range filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildNumericRangeFilter( + filter: SCSearchNumericRangeFilter, +): QueryDslSpecificQueryContainer<'range'> { + const numericRangeObject: QueryDslNumberRangeQuery = { + relation: filter.arguments.relation, + }; + if (filter.arguments.bounds.lowerBound?.mode === 'exclusive') { + numericRangeObject.gt = filter.arguments.bounds.lowerBound.limit; + } else if (filter.arguments.bounds.lowerBound?.mode === 'inclusive') { + numericRangeObject.gte = filter.arguments.bounds.lowerBound.limit; + } + if (filter.arguments.bounds.upperBound?.mode === 'exclusive') { + numericRangeObject.lt = filter.arguments.bounds.upperBound.limit; + } else if (filter.arguments.bounds.upperBound?.mode === 'inclusive') { + numericRangeObject.lte = filter.arguments.bounds.upperBound.limit; + } + + return { + range: { + [filter.arguments.field]: numericRangeObject, + }, + }; +} diff --git a/src/storage/elasticsearch/query/filters/value.ts b/src/storage/elasticsearch/query/filters/value.ts new file mode 100644 index 00000000..60288ecc --- /dev/null +++ b/src/storage/elasticsearch/query/filters/value.ts @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SCSearchValueFilter} from '@openstapps/core'; +import {QueryDslSpecificQueryContainer} from '../../types/util'; + +/** + * Converts a value filter to elasticsearch syntax + * + * @param filter A search filter for the retrieval of the data + */ +export function buildValueFilter( + filter: SCSearchValueFilter, +): QueryDslSpecificQueryContainer<'term'> | QueryDslSpecificQueryContainer<'terms'> { + return Array.isArray(filter.arguments.value) + ? { + terms: { + [`${filter.arguments.field}.raw`]: filter.arguments.value, + }, + } + : { + term: { + [`${filter.arguments.field}.raw`]: filter.arguments.value, + }, + }; +} diff --git a/src/storage/elasticsearch/query/query.ts b/src/storage/elasticsearch/query/query.ts new file mode 100644 index 00000000..3f158b89 --- /dev/null +++ b/src/storage/elasticsearch/query/query.ts @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {QueryDslQueryContainer} from '@elastic/elasticsearch/lib/api/types'; +import {SCConfigFile, SCSearchQuery} from '@openstapps/core'; +import {ElasticsearchConfig} from '../types/elasticsearch-config'; +import {buildFilter} from './filter'; +import {buildScoringFunctions} from './boost/scoring-functions'; + +/** + * Builds body for Elasticsearch requests + * + * @param parameters Parameters for querying the backend + * @param defaultConfig Default configuration of the backend + * @param elasticsearchConfig Elasticsearch configuration + * @returns ElasticsearchQuery (body of a search-request) + */ +export function buildQuery( + parameters: SCSearchQuery, + defaultConfig: SCConfigFile, + elasticsearchConfig: ElasticsearchConfig, +): QueryDslQueryContainer { + // if config provides an minMatch parameter we use query_string instead of match query + let query; + if (typeof elasticsearchConfig.query === 'undefined') { + query = { + query_string: { + analyzer: 'search_german', + default_field: 'name', + minimum_should_match: '90%', + query: typeof parameters.query !== 'string' ? '*' : parameters.query, + }, + }; + } else if (elasticsearchConfig.query.queryType === 'query_string') { + query = { + query_string: { + analyzer: 'search_german', + default_field: 'name', + minimum_should_match: elasticsearchConfig.query.minMatch, + query: typeof parameters.query !== 'string' ? '*' : parameters.query, + }, + }; + } else if (elasticsearchConfig.query.queryType === 'dis_max') { + if (typeof parameters.query === 'string' && parameters.query !== '*') { + query = { + dis_max: { + boost: 1.2, + queries: [ + { + match: { + name: { + boost: elasticsearchConfig.query.matchBoosting, + fuzziness: elasticsearchConfig.query.fuzziness, + query: parameters.query, + }, + }, + }, + { + query_string: { + default_field: 'name', + minimum_should_match: elasticsearchConfig.query.minMatch, + query: parameters.query, + }, + }, + ], + tie_breaker: elasticsearchConfig.query.tieBreaker, + }, + }; + } + } else { + throw new Error( + 'Unsupported query type. Check your config file and reconfigure your elasticsearch query', + ); + } + + const functionScoreQuery: QueryDslQueryContainer = { + function_score: { + functions: buildScoringFunctions(defaultConfig.internal.boostings, parameters.context), + query: { + bool: { + minimum_should_match: 0, // if we have no should, nothing can match + must: [], + should: [], + }, + }, + score_mode: 'multiply', + }, + }; + + const mustMatch = functionScoreQuery.function_score?.query?.bool?.must; + + if (Array.isArray(mustMatch)) { + if (typeof query !== 'undefined') { + mustMatch.push(query); + } + + if (typeof parameters.filter !== 'undefined') { + mustMatch.push(buildFilter(parameters.filter)); + } + } + + return functionScoreQuery; +} diff --git a/src/storage/elasticsearch/query/sort.ts b/src/storage/elasticsearch/query/sort.ts new file mode 100644 index 00000000..20733504 --- /dev/null +++ b/src/storage/elasticsearch/query/sort.ts @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Sort} from '@elastic/elasticsearch/lib/api/types'; +import {SCSearchSort} from '@openstapps/core'; +import {buildDistanceSort} from './sort/distance'; +import {buildDucetSort} from './sort/ducet'; +import {buildGenericSort} from './sort/generic'; +import {buildPriceSort} from './sort/price'; + +/** + * converts query to + * + * @param sorts Sorting rules to apply to the data that is being queried + * @returns an array of sort queries + */ +export function buildSort(sorts: SCSearchSort[]): Sort { + return sorts.map(sort => { + switch (sort.type) { + case 'generic': + return buildGenericSort(sort); + case 'ducet': + return buildDucetSort(sort); + case 'distance': + return buildDistanceSort(sort); + case 'price': + return buildPriceSort(sort); + } + }); +} diff --git a/src/storage/elasticsearch/query/sort/distance.ts b/src/storage/elasticsearch/query/sort/distance.ts new file mode 100644 index 00000000..3319f859 --- /dev/null +++ b/src/storage/elasticsearch/query/sort/distance.ts @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SortOptions} from '@elastic/elasticsearch/lib/api/types'; +import {SCDistanceSort} from '@openstapps/core'; + +/** + * Converts a distance sort to elasticsearch syntax + * + * @param sort A sorting definition + */ +export function buildDistanceSort(sort: SCDistanceSort): SortOptions { + return { + _geo_distance: { + mode: 'avg', + order: sort.order, + unit: 'm', + [`${sort.arguments.field}.point.coordinates`]: { + lat: sort.arguments.position[1], + lon: sort.arguments.position[0], + }, + }, + }; +} diff --git a/src/storage/elasticsearch/query/sort/ducet.ts b/src/storage/elasticsearch/query/sort/ducet.ts new file mode 100644 index 00000000..b0936d88 --- /dev/null +++ b/src/storage/elasticsearch/query/sort/ducet.ts @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SortOptions} from '@elastic/elasticsearch/lib/api/types'; +import {SCDucetSort} from '@openstapps/core'; + +/** + * Converts a ducet sort to elasticsearch syntax + * + * @param sort A sorting definition + */ +export function buildDucetSort(sort: SCDucetSort): SortOptions { + return { + [`${sort.arguments.field}.sort`]: sort.order, + }; +} diff --git a/src/storage/elasticsearch/query/sort/generic.ts b/src/storage/elasticsearch/query/sort/generic.ts new file mode 100644 index 00000000..38188c7c --- /dev/null +++ b/src/storage/elasticsearch/query/sort/generic.ts @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SortOptions} from '@elastic/elasticsearch/lib/api/types'; +import {SCGenericSort} from '@openstapps/core'; + +/** + * Converts a generic sort to elasticsearch syntax + * + * @param sort A sorting definition + */ +export function buildGenericSort(sort: SCGenericSort): SortOptions { + return { + [sort.arguments.field]: sort.order, + }; +} diff --git a/src/storage/elasticsearch/query/sort/price.ts b/src/storage/elasticsearch/query/sort/price.ts new file mode 100644 index 00000000..6bd319e4 --- /dev/null +++ b/src/storage/elasticsearch/query/sort/price.ts @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SortOptions} from '@elastic/elasticsearch/lib/api/types'; +import {SCPriceSort, SCSportCoursePriceGroup, SCThingsField} from '@openstapps/core'; + +/** + * Converts a price sort to elasticsearch syntax + * + * @param sort A sorting definition + */ +export function buildPriceSort(sort: SCPriceSort): SortOptions { + return { + _script: { + order: sort.order, + script: buildPriceSortScript(sort.arguments.universityRole, sort.arguments.field), + type: 'number' as const, + }, + }; +} + +/** + * Provides a script for sorting search results by prices + * + * @param universityRole User group which consumes university services + * @param field Field in which wanted offers with prices are located + */ +export function buildPriceSortScript( + universityRole: keyof SCSportCoursePriceGroup, + field: SCThingsField, +): string { + return ` + // initialize the sort value with the maximum + double price = Double.MAX_VALUE; + + // if we have any offers + if (params._source.containsKey('${field}')) { + // iterate through all offers + for (offer in params._source.${field}) { + // if this offer contains a role specific price + if (offer.containsKey('prices') && offer.prices.containsKey('${universityRole}')) { + // if the role specific price is smaller than the cheapest we found + if (offer.prices.${universityRole} < price) { + // set the role specific price as cheapest for now + price = offer.prices.${universityRole}; + } + } else { // we have no role specific price for our role in this offer + // if the default price of this offer is lower than the cheapest we found + if (offer.price < price) { + // set this price as the cheapest + price = offer.price; + } + } + } + } + + // return cheapest price for our role + return price; + `; +} diff --git a/src/storage/elasticsearch/templating.ts b/src/storage/elasticsearch/templating.ts index 4301f6ff..afb5ad9b 100644 --- a/src/storage/elasticsearch/templating.ts +++ b/src/storage/elasticsearch/templating.ts @@ -29,17 +29,6 @@ export const aggregations = JSON.parse( readFileSync(path.resolve(mappingsPath, 'aggregations.json'), 'utf8'), ) as AggregationSchema; -/** - * Re-applies all interfaces for every type - * - * @param client An elasticsearch client to use - */ -export async function refreshAllTemplates(client: Client) { - for (const type of Object.values(SCThingType)) { - await putTemplate(client, type as SCThingType); - } -} - /** * Prepares all indices * diff --git a/src/storage/elasticsearch/types/elasticsearch-config.ts b/src/storage/elasticsearch/types/elasticsearch-config.ts new file mode 100644 index 00000000..91231cc4 --- /dev/null +++ b/src/storage/elasticsearch/types/elasticsearch-config.ts @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +/** + * A configuration for using the Dis Max Query + * + * See https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-dis-max-query.html for further + * explanation of what the parameters mean + */ +export interface ElasticsearchQueryDisMaxConfig { + /** + * Relative (to a total number of documents) or absolute number to exclude meaningless matches that frequently appear + */ + cutoffFrequency: number; + + /** + * The maximum allowed Levenshtein Edit Distance (or number of edits) + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness + */ + fuzziness: number | string; + + /** + * Increase the importance (relevance score) of a field + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-boost.html + */ + matchBoosting: number; + + /** + * Minimal number (or percentage) of words that should match in a query + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html + */ + minMatch: string; + + /** + * Type of the query - in this case 'dis_max' which is a union of its subqueries + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-dis-max-query.html + */ + queryType: 'dis_max'; + + /** + * Changes behavior of default calculation of the score when multiple results match + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-multi-match-query.html#tie-breaker + */ + tieBreaker: number; +} + +/** + * A configuration for using Query String Query + * + * See https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-query-string-query.html for further + * explanation of what the parameters mean + */ +export interface ElasticsearchQueryQueryStringConfig { + /** + * Minimal number (or percentage) of words that should match in a query + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html + */ + minMatch: string; + + /** + * Type of the query - in this case 'query_string' which uses a query parser in order to parse content + * + * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html + */ + queryType: 'query_string'; +} + +/** + * An config file for the elasticsearch database interface + * + * The config file extends the SCConfig file by further defining how the database property + */ +export interface ElasticsearchConfigFile { + /** + * Configuration that is not visible to clients + */ + internal: { + /** + * Database configuration + */ + database: ElasticsearchConfig; + }; +} + +/** + * An elasticsearch configuration + */ +export interface ElasticsearchConfig { + /** + * Name of the database + */ + name: 'elasticsearch'; + + /** + * Configuration for using queries + */ + query?: ElasticsearchQueryDisMaxConfig | ElasticsearchQueryQueryStringConfig; + + /** + * Version of the used elasticsearch + */ + version: string; +} diff --git a/src/storage/elasticsearch/types/elasticsearch.ts b/src/storage/elasticsearch/types/elasticsearch.ts deleted file mode 100644 index 6804a4f9..00000000 --- a/src/storage/elasticsearch/types/elasticsearch.ts +++ /dev/null @@ -1,605 +0,0 @@ -/* - * Copyright (C) 2019-2021 StApps - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import {SCThing, SCThingType} from '@openstapps/core'; -// we only have the @types package because some things type definitions are still missing from the official -import {NameList} from 'elasticsearch'; -import {Polygon, Position} from 'geojson'; - -/** - * An elasticsearch aggregation bucket - */ -interface Bucket { - /** - * Number of documents in the aggregation bucket - */ - doc_count: number; - - /** - * Text representing the documents in the bucket - */ - key: string; -} - -/** - * An elasticsearch aggregation response - */ -export interface AggregationResponse { - /** - * The individual aggregations - */ - [field: string]: BucketAggregation | NestedAggregation; -} - -/** - * An elasticsearch bucket aggregation - */ -export interface BucketAggregation { - /** - * Buckets in an aggregation - */ - buckets: Bucket[]; - - /** - * Number of documents in an aggregation - */ - doc_count?: number; -} - -/** - * An aggregation that contains more aggregations nested inside - */ -export interface NestedAggregation { - /** - * Number of documents in an aggregation - */ - doc_count: number; - - /** - * Any nested responses - */ - [name: string]: BucketAggregation | number; -} - -/** - * A configuration for using the Dis Max Query - * - * See https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-dis-max-query.html for further - * explanation of what the parameters mean - */ -export interface ElasticsearchQueryDisMaxConfig { - /** - * Relative (to a total number of documents) or absolute number to exclude meaningless matches that frequently appear - */ - cutoffFrequency: number; - - /** - * The maximum allowed Levenshtein Edit Distance (or number of edits) - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/common-options.html#fuzziness - */ - fuzziness: number | string; - - /** - * Increase the importance (relevance score) of a field - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-boost.html - */ - matchBoosting: number; - - /** - * Minimal number (or percentage) of words that should match in a query - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html - */ - minMatch: string; - - /** - * Type of the query - in this case 'dis_max' which is a union of its subqueries - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-dis-max-query.html - */ - queryType: 'dis_max'; - - /** - * Changes behavior of default calculation of the score when multiple results match - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-multi-match-query.html#tie-breaker - */ - tieBreaker: number; -} - -/** - * A configuration for using Query String Query - * - * See https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-query-string-query.html for further - * explanation of what the parameters mean - */ -export interface ElasticsearchQueryQueryStringConfig { - /** - * Minimal number (or percentage) of words that should match in a query - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html - */ - minMatch: string; - - /** - * Type of the query - in this case 'query_string' which uses a query parser in order to parse content - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-query-string-query.html - */ - queryType: 'query_string'; -} - -/** - * A hit in an elasticsearch search result - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-fields.html - */ -export interface ElasticsearchObject { - /** - * Unique identifier of a document (object) - */ - _id: string; - - /** - * The index to which the document belongs - */ - _index: string; - - /** - * Relevancy of the document to a query - */ - _score: number; - - /** - * The original JSON representing the body of the document - */ - _source: T; - - /** - * The document's mapping type - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/mapping-type-field.html - */ - _type: string; - - /** - * Version of the document - */ - _version?: number; - - /** - * Used to index the same field in different ways for different purposes - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/multi-fields.html - */ - fields?: NameList; - - /** - * Used to highlight search results on one or more fields - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-highlighting.html - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - highlight?: any; - - /** - * Used in when nested/children documents match the query - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/search-request-inner-hits.html - */ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - inner_hits?: any; - - /** - * Queries that matched for documents in results - */ - matched_queries?: string[]; - - /** - * Sorting definition - */ - sort?: string[]; -} - -/** - * An config file for the elasticsearch database interface - * - * The config file extends the SCConfig file by further defining how the database property - */ -export interface ElasticsearchConfigFile { - /** - * Configuration that is not visible to clients - */ - internal: { - /** - * Database configuration - */ - database: ElasticsearchConfig; - }; -} - -/** - * An elasticsearch configuration - */ -export interface ElasticsearchConfig { - /** - * Name of the database - */ - name: 'elasticsearch'; - - /** - * Configuration for using queries - */ - query?: ElasticsearchQueryDisMaxConfig | ElasticsearchQueryQueryStringConfig; - - /** - * Version of the used elasticsearch - */ - version: string; -} - -/** - * An elasticsearch term filter - */ -export type ESTermFilter = - | { - /** - * Definition of a term to match - */ - term: { - [fieldName: string]: string; - }; - } - | { - /** - * Definition of terms to match (or) - */ - terms: { - [fieldName: string]: string[]; - }; - }; - -export interface ESGenericRange { - /** - * Greater than field - */ - gt?: T; - - /** - * Greater or equal than field - */ - gte?: T; - - /** - * Less than field - */ - lt?: T; - - /** - * Less or equal than field - */ - lte?: T; - - /** - * Relation of the range to a range field - * - * Intersects: Both ranges intersect - * Contains: Search range contains field range - * Within: Field range contains search range - */ - relation?: 'intersects' | 'within' | 'contains'; -} - -interface ESGenericRangeFilter> { - /** - * Range filter definition - */ - range: { - [fieldName: string]: T; - }; -} - -export interface ESDateRange extends ESGenericRange { - /** - * Optional date format override - */ - format?: string; - - /** - * Optional timezone specifier - */ - time_zone?: string; -} - -export type ESNumericRangeFilter = ESGenericRangeFilter>; -export type ESDateRangeFilter = ESGenericRangeFilter; -export type ESRangeFilter = ESNumericRangeFilter | ESDateRangeFilter; - -/** - * An elasticsearch type filter - */ -export interface ESTypeFilter { - /** - * Type filter definition - */ - type: { - /** - * Type name (SCThingType) to filter with - */ - value: SCThingType; - }; -} - -/** - * Filter arguments for an elasticsearch geo distance filter - */ -export interface ESGeoDistanceFilterArguments { - /** - * The radius of the circle centred on the specified location - */ - distance: string; - - [fieldName: string]: - | { - /** - * Latitude - */ - lat: number; - - /** - * Longitude - */ - lon: number; - } - | string; -} - -/** - * An elasticsearch geo distance filter - */ -export interface ESGeoDistanceFilter { - /** - * @see ESGeoDistanceFilterArguments - */ - geo_distance: ESGeoDistanceFilterArguments; -} - -/** - * A rectangular geo shape, representing the top-left and bottom-right corners - * - * This is an extension of the Geojson type - * http://geojson.org/geojson-spec.html - */ -export interface ESEnvelope { - /** - * The top-left and bottom-right corners of the bounding box - */ - coordinates: [Position, Position]; - - /** - * The type of the geometry - */ - type: 'envelope'; -} - -/** - * An Elasticsearch geo bounding box filter - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html - */ -export interface ESGeoBoundingBoxFilter { - /** - * An Elasticsearch geo bounding box filter - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-bounding-box-query.html - */ - geo_bounding_box: { - [fieldName: string]: { - /** - * Geo Shape - */ - bottom_right: Position; - - /** - * Geo Shape - */ - top_left: Position; - }; - }; -} - -/** - * An Elasticsearch geo shape filter - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html - */ -export interface ESGeoShapeFilter { - geo_shape: { - [fieldName: string]: { - /** - * Relation of the two shapes - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-geo-shape-query.html#_spatial_relations - */ - relation?: 'intersects' | 'disjoint' | 'within' | 'contains'; - - /** - * Geo Shape - */ - shape: Polygon | ESEnvelope; - }; - }; -} - -/** - * Filter arguments for an elasticsearch boolean filter - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-bool-query.html - */ -export interface ESBooleanFilterArguments { - /** - * Minimal number (or percentage) of words that should match in a query - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-minimum-should-match.html - */ - minimum_should_match?: number; - - /** - * The clause (query) must appear in matching documents and will contribute to the score. - */ - must?: T[]; - - /** - * The clause (query) must not appear in the matching documents. - */ - must_not?: T[]; - - /** - * The clause (query) should appear in the matching document. - */ - should?: T[]; -} - -/** - * An elasticsearch boolean filter - */ -export interface ESBooleanFilter { - /** - * @see ESBooleanFilterArguments - */ - bool: ESBooleanFilterArguments; -} - -/** - * An elasticsearch function score query - * - * @see https://www.elastic.co/guide/en/elasticsearch/reference/5.6/query-dsl-function-score-query.html - */ -export interface ESFunctionScoreQuery { - /** - * Function score definition - */ - function_score: { - /** - * Functions that compute score for query results (documents) - * - * @see ESFunctionScoreQueryFunction - */ - functions: ESFunctionScoreQueryFunction[]; - - /** - * @see ESBooleanFilter - */ - query: ESBooleanFilter; - - /** - * Specifies how the computed scores are combined - */ - score_mode: 'multiply'; - }; -} - -/** - * An function for an elasticsearch functions score query - */ -export interface ESFunctionScoreQueryFunction { - /** - * Function is applied only if a document matches the given filtering query - */ - filter: ESTermFilter | ESTypeFilter | ESBooleanFilter; - - /** - * Weight (importance) of the filter - */ - weight: number; -} - -/** - * An elasticsearch generic sort - */ -export interface ESGenericSort { - [field: string]: string; -} - -/** - * Sort arguments for an elasticsearch geo distance sort - */ -export interface ESGeoDistanceSortArguments { - /** - * What value to pick for sorting - */ - mode: 'avg' | 'max' | 'median' | 'min'; - - /** - * Order - */ - order: 'asc' | 'desc'; - - /** - * Value unit - */ - unit: 'm'; - - [field: string]: - | { - /** - * Latitude - */ - lat: number; - - /** - * Longitude - */ - lon: number; - } - | string; -} - -/** - * An elasticsearch geo distance sort - */ -export interface ESGeoDistanceSort { - /** - * @see ESGeoDistanceFilterArguments - */ - _geo_distance: ESGeoDistanceSortArguments; -} - -/** - * An elasticsearch script sort - */ -export interface ScriptSort { - /** - * A script - */ - _script: { - /** - * Order - */ - order: 'asc' | 'desc'; - - /** - * The custom script used for sorting - */ - script: string; - - /** - * What type is being sorted - */ - type: 'number' | 'string'; - }; -} diff --git a/src/storage/elasticsearch/types/guards.ts b/src/storage/elasticsearch/types/guards.ts deleted file mode 100644 index c5bf60bc..00000000 --- a/src/storage/elasticsearch/types/guards.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (C) 2019-2021 StApps - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { - ESAggMatchAllFilter, - ESAggTypeFilter, - ESNestedAggregation, - ESTermsFilter, -} from '@openstapps/es-mapping-generator/src/types/aggregation'; -import {BucketAggregation, NestedAggregation} from './elasticsearch'; - -/** - * Checks if the type is a BucketAggregation - * - * @param agg the type to check - */ -export function isBucketAggregation(agg: BucketAggregation | number): agg is BucketAggregation { - return typeof agg !== 'number'; -} - -/** - * Checks if the type is a NestedAggregation - * - * @param agg the type to check - */ -export function isNestedAggregation(agg: BucketAggregation | NestedAggregation): agg is NestedAggregation { - return typeof (agg as BucketAggregation).buckets === 'undefined'; -} - -/** - * Checks if the parameter is of type ESTermsFilter - * - * @param agg the value to check - */ -export function isESTermsFilter(agg: ESTermsFilter | ESNestedAggregation): agg is ESTermsFilter { - return typeof (agg as ESTermsFilter).terms !== 'undefined'; -} - -/** - * Checks if the parameter is of type ESTermsFilter - * - * @param agg the value to check - */ -export function isESNestedAggregation(agg: ESTermsFilter | ESNestedAggregation): agg is ESNestedAggregation { - return typeof (agg as ESNestedAggregation).aggs !== 'undefined'; -} - -/** - * Checks if the parameter is of type - * - * @param filter the filter to narrow the type of - */ -export function isESAggMatchAllFilter( - filter: ESAggTypeFilter | ESAggMatchAllFilter, -): filter is ESAggMatchAllFilter { - return filter.hasOwnProperty('match_all'); -} diff --git a/src/storage/elasticsearch/types/util.ts b/src/storage/elasticsearch/types/util.ts new file mode 100644 index 00000000..15bf449b --- /dev/null +++ b/src/storage/elasticsearch/types/util.ts @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +import {QueryDslQueryContainer} from '@elastic/elasticsearch/lib/api/types'; + +export type QueryDslSpecificQueryContainer = Required< + Pick +>; diff --git a/src/storage/elasticsearch/util/alias.ts b/src/storage/elasticsearch/util/alias.ts new file mode 100644 index 00000000..7cef01f8 --- /dev/null +++ b/src/storage/elasticsearch/util/alias.ts @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Logger} from '@openstapps/logger'; + +/** + * Checks for invalid character in alias names and removes them + * + * @param alias The alias name + * @param uid The UID of the current bulk (for debugging purposes) + */ +export function removeInvalidAliasChars(alias: string, uid: string | undefined): string { + let formattedAlias = alias; + + // spaces are included in some types, replace them with underscores + if (formattedAlias.includes(' ')) { + formattedAlias = formattedAlias.trim(); + formattedAlias = formattedAlias.split(' ').join('_'); + } + // List of invalid characters: https://www.elastic.co/guide/en/elasticsearch/reference/6.6/indices-create-index.html + for (const value of ['\\', '/', '*', '?', '"', '<', '>', '|', ',', '#']) { + if (formattedAlias.includes(value)) { + formattedAlias = formattedAlias.replace(value, ''); + Logger.warn(`Type of the bulk ${uid} contains an invalid character '${value}'. This can lead to two bulks + having the same alias despite having different types, as invalid characters are removed automatically. + New alias name is "${formattedAlias}."`); + } + } + for (const value of ['-', '_', '+']) { + if (formattedAlias.charAt(0) === value) { + formattedAlias = formattedAlias.slice(1); + Logger.warn(`Type of the bulk ${uid} begins with '${value}'. This can lead to two bulks having the same + alias despite having different types, as invalid characters are removed automatically. + New alias name is "${formattedAlias}."`); + } + } + if (formattedAlias === '.' || formattedAlias === '..') { + Logger.warn(`Type of the bulk ${uid} is ${formattedAlias}. This is an invalid name, please consider using + another one, as it will be replaced with 'alias_placeholder', which can lead to strange errors.`); + + return 'alias_placeholder'; + } + if (formattedAlias.includes(':')) { + Logger.warn(`Type of the bulk ${uid} contains a ':'. This isn't an issue now, but will be in future + Elasticsearch versions!`); + } + + return formattedAlias; +} diff --git a/src/storage/elasticsearch/util/index.ts b/src/storage/elasticsearch/util/index.ts new file mode 100644 index 00000000..24ba5999 --- /dev/null +++ b/src/storage/elasticsearch/util/index.ts @@ -0,0 +1,63 @@ +import {SCBulkResponse, SCThingType, SCUuid} from '@openstapps/core'; + +/** + * Length of the index UID used for generation of its name + */ +export const INDEX_UID_LENGTH = 8; + +/** + * A string which matches all indices + */ +export const ALL_INDICES_QUERY = 'stapps_*_*_*'; + +/** + * Matches index names such as stapps___ + */ +export const VALID_INDEX_REGEX = /^stapps_([A-z0-9_]+)_([a-z0-9-_]+)_([-a-z0-9^_]+)$/; + +export interface ParsedIndexName { + type: SCThingType; + source: string; + randomSuffix: string; +} + +/** + * + */ +export function parseIndexName(index: string): ParsedIndexName { + const match = VALID_INDEX_REGEX.exec(index); + if (!match) { + throw new SyntaxError(`Invalid index name ${index}!`); + } + + return { + type: match[1] as SCThingType, + source: match[2], + randomSuffix: match[3], + }; +} + +/** + * Gets the index name in elasticsearch for one SCThingType + * + * @param type SCThingType of data in the index + * @param source source of data in the index + * @param bulk bulk process which created this index + */ +export function getThingIndexName(type: SCThingType, source: string, bulk: SCBulkResponse) { + let out = type.toLowerCase(); + while (out.includes(' ')) { + out = out.replace(' ', '_'); + } + + return `stapps_${out}_${source}_${getIndexUID(bulk.uid)}`; +} + +/** + * Provides the index UID (for its name) from the bulk UID + * + * @param uid Bulk UID + */ +export function getIndexUID(uid: SCUuid) { + return uid.slice(0, Math.max(0, INDEX_UID_LENGTH)); +} diff --git a/src/storage/elasticsearch/util/no-undefined.ts b/src/storage/elasticsearch/util/no-undefined.ts new file mode 100644 index 00000000..118bab84 --- /dev/null +++ b/src/storage/elasticsearch/util/no-undefined.ts @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +/** + * Type guard for filter functions + */ +export function noUndefined(item: T | undefined): item is T { + return typeof item !== 'undefined'; +} diff --git a/src/storage/elasticsearch/util/retry.ts b/src/storage/elasticsearch/util/retry.ts new file mode 100644 index 00000000..acf17e08 --- /dev/null +++ b/src/storage/elasticsearch/util/retry.ts @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +export interface RetryOptions { + maxRetries: number; + retryInterval: number; + doAction: () => Promise; + onFailedAttempt: (attempt: number, error: unknown, options: RetryOptions) => void; + onFail: (options: RetryOptions) => never; +} + +/** + * Retries a throwing function at a set interval, until a maximum amount of attempts + */ +export async function retryCatch(options: RetryOptions): Promise { + for (let attempt = 0; attempt < options.maxRetries; attempt++) { + try { + return await options.doAction(); + } catch (error) { + options.onFailedAttempt(attempt, error, options); + await new Promise(resolve => setTimeout(resolve, options.retryInterval)); + } + } + + options.onFail(options); +} diff --git a/test/common.ts b/test/common.ts index ecf8a5fb..98fc11c8 100644 --- a/test/common.ts +++ b/test/common.ts @@ -16,6 +16,7 @@ import {SCConfigFile, SCSearchQuery, SCSearchResponse, SCThings, SCThingType, SCUuid} from '@openstapps/core'; import {Express} from 'express'; import moment from 'moment'; +import {getIndexUID} from '../src/storage/elasticsearch/util'; import {configureApp} from '../src/app'; import express from 'express'; import http from 'http'; @@ -24,7 +25,6 @@ import {MailQueue} from '../src/notification/mail-queue'; import {Bulk, BulkStorage} from '../src/storage/bulk-storage'; import getPort from 'get-port'; import {Database} from '../src/storage/database'; -import {Elasticsearch} from '../src/storage/elasticsearch/elasticsearch'; import {v4} from 'uuid'; /** @@ -147,5 +147,4 @@ export const getTransport = (verified: boolean) => { }; }; -export const getIndex = (uid?: string) => - `stapps_footype_foosource_${uid ?? Elasticsearch.getIndexUID(v4())}`; +export const getIndex = (uid?: string) => `stapps_footype_foosource_${uid ?? getIndexUID(v4())}`; diff --git a/test/storage/elasticsearch/aggregations.spec.ts b/test/storage/elasticsearch/aggregations.spec.ts index 48584cd6..ed727dca 100644 --- a/test/storage/elasticsearch/aggregations.spec.ts +++ b/test/storage/elasticsearch/aggregations.spec.ts @@ -13,13 +13,13 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import {AggregateName, AggregationsMultiTermsBucket} from '@elastic/elasticsearch/lib/api/types'; import {SCFacet, SCThingType} from '@openstapps/core'; import {expect} from 'chai'; import {parseAggregations} from '../../../src/storage/elasticsearch/aggregations'; -import {AggregationResponse} from '../../../src/storage/elasticsearch/types/elasticsearch'; describe('Aggregations', function () { - const aggregations: AggregationResponse = { + const aggregations: Record> = { 'catalog': { 'doc_count': 4, 'superCatalogs.categories': { @@ -76,14 +76,6 @@ describe('Aggregations', function () { buckets: [], }, }, - 'fooType': { - buckets: [ - { - doc_count: 321, - key: 'foo', - }, - ], - }, '@all': { doc_count: 17, type: { @@ -102,33 +94,6 @@ describe('Aggregations', function () { }; const expectedFacets: SCFacet[] = [ - { - buckets: [ - { - count: 13, - key: 'person', - }, - { - count: 4, - key: 'catalog', - }, - ], - field: 'type', - }, - { - buckets: [ - { - count: 8, - key: 'foobar', - }, - { - count: 2, - key: 'bar', - }, - ], - field: 'categories', - onlyOnType: SCThingType.AcademicEvent, - }, { buckets: [ { @@ -153,7 +118,33 @@ describe('Aggregations', function () { field: 'categories', onlyOnType: SCThingType.Catalog, }, - // no fooType as it doesn't appear in the aggregation schema + { + buckets: [ + { + count: 8, + key: 'foobar', + }, + { + count: 2, + key: 'bar', + }, + ], + field: 'categories', + onlyOnType: SCThingType.AcademicEvent, + }, + { + buckets: [ + { + count: 13, + key: 'person', + }, + { + count: 4, + key: 'catalog', + }, + ], + field: 'type', + }, ]; it('should parse the aggregations providing the appropriate facets', function () { diff --git a/test/storage/elasticsearch/common.spec.ts b/test/storage/elasticsearch/common.spec.ts deleted file mode 100644 index 45629424..00000000 --- a/test/storage/elasticsearch/common.spec.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2020 StApps - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import { - ESAggMatchAllFilter, - ESAggTypeFilter, - ESNestedAggregation, - ESTermsFilter, -} from '@openstapps/es-mapping-generator/src/types/aggregation'; -import {expect} from 'chai'; -import { - isNestedAggregation, - isBucketAggregation, - isESTermsFilter, - isESAggMatchAllFilter, - isESNestedAggregation, -} from '../../../lib/storage/elasticsearch/types/guards'; -import {BucketAggregation, NestedAggregation} from '../../../src/storage/elasticsearch/types/elasticsearch'; - -describe('Common', function () { - const bucketAggregation: BucketAggregation = {buckets: []}; - const esNestedAggregation: ESNestedAggregation = {aggs: {}, filter: {match_all: true}}; - const esTermsFilter: ESTermsFilter = {terms: {field: 'foo'}}; - - describe('isBucketAggregation', function () { - it('should be false for a number', function () { - expect(isBucketAggregation(123)).to.be.false; - }); - - it('should be true for a bucket aggregation', function () { - expect(isBucketAggregation(bucketAggregation)).to.be.true; - }); - }); - - describe('isNestedAggregation', function () { - it('should be false for a bucket aggregation', function () { - expect(isNestedAggregation(bucketAggregation)).to.be.false; - }); - - it('should be true for a nested aggregation', function () { - const nestedAggregation: NestedAggregation = {doc_count: 123}; - - expect(isNestedAggregation(nestedAggregation)).to.be.true; - }); - }); - - describe('isESTermsFilter', function () { - it('should be false for an elasticsearch nested aggregation', function () { - expect(isESTermsFilter(esNestedAggregation)).to.be.false; - }); - - it('should be true for an elasticsearch terms filter', function () { - expect(isESTermsFilter(esTermsFilter)).to.be.true; - }); - }); - - describe('isESNestedAggregation', function () { - it('should be false for an elasticsearch terms filter', function () { - expect(isESNestedAggregation(esTermsFilter)).to.be.false; - }); - - it('should be true for an elasticsearch nested aggregation', function () { - expect(isESNestedAggregation(esNestedAggregation)).to.be.true; - }); - }); - - describe('isESAggMatchAllFilter', function () { - it('should be false for an elasticsearch aggregation type filter', function () { - const aggregationTypeFilter: ESAggTypeFilter = {type: {value: 'foo'}}; - expect(isESAggMatchAllFilter(aggregationTypeFilter)).to.be.false; - }); - - it('should be true for an elasticsearch aggregation match all filter', function () { - const esAggMatchAllFilter: ESAggMatchAllFilter = {match_all: {}}; - expect(isESAggMatchAllFilter(esAggMatchAllFilter)).to.be.true; - }); - }); -}); diff --git a/test/storage/elasticsearch/elasticsearch.spec.ts b/test/storage/elasticsearch/elasticsearch.spec.ts index 6df70637..1b736c22 100644 --- a/test/storage/elasticsearch/elasticsearch.spec.ts +++ b/test/storage/elasticsearch/elasticsearch.spec.ts @@ -14,7 +14,14 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import {ApiResponse, Client} from '@elastic/elasticsearch'; +import {Client, Diagnostic} from '@elastic/elasticsearch'; +import Indices from '@elastic/elasticsearch/lib/api/api/indices'; +import { + CreateResponse, + SearchHit, + SearchResponse, + SortCombinations, +} from '@elastic/elasticsearch/lib/api/types'; import { SCBook, SCBulkResponse, @@ -30,22 +37,32 @@ import {Logger} from '@openstapps/logger'; import {SMTP} from '@openstapps/logger/lib/smtp'; import {expect, use} from 'chai'; import chaiAsPromised from 'chai-as-promised'; -import {SearchResponse} from 'elasticsearch'; import mockedEnv from 'mocked-env'; -import sinon from 'sinon'; +import {ALL_INDICES_QUERY, parseIndexName} from '../../../src/storage/elasticsearch/util'; +import * as queryModule from '../../../src/storage/elasticsearch/query/query'; +import * as sortModule from '../../../src/storage/elasticsearch/query/sort'; +import sinon, {SinonStub} from 'sinon'; +import {getIndexUID, getThingIndexName, INDEX_UID_LENGTH} from '../../../src/storage/elasticsearch/util'; +import * as utilModule from '../../../src/storage/elasticsearch/util'; +import {removeInvalidAliasChars} from '../../../src/storage/elasticsearch/util/alias'; import {configFile} from '../../../src/common'; import {MailQueue} from '../../../src/notification/mail-queue'; import {aggregations} from '../../../src/storage/elasticsearch/templating'; -import {ElasticsearchObject} from '../../../src/storage/elasticsearch/types/elasticsearch'; import {Elasticsearch} from '../../../src/storage/elasticsearch/elasticsearch'; import * as Monitoring from '../../../src/storage/elasticsearch/monitoring'; -import * as query from '../../../src/storage/elasticsearch/query'; import * as templating from '../../../src/storage/elasticsearch/templating'; import {bulk, DEFAULT_TEST_TIMEOUT, getTransport, getIndex} from '../../common'; import fs from 'fs'; use(chaiAsPromised); +/** + * + */ +function searchResponse(...hits: SearchHit[]): SearchResponse { + return {hits: {hits}, took: 0, timed_out: false, _shards: {total: 1, failed: 0, successful: 1}}; +} + describe('Elasticsearch', function () { // increase timeout for the suite this.timeout(DEFAULT_TEST_TIMEOUT); @@ -83,6 +100,14 @@ describe('Elasticsearch', function () { }); }); + describe('getAliasMap', function () { + it('should fail after retries', async function () { + const es = new Elasticsearch(configFile); + sandbox.stub(es.client.indices, 'getAlias').throws(); + await expect(es.init({maxRetries: 1, retryInterval: 10})).to.be.rejected; + }); + }); + describe('getIndex (including getIndexUID)', function () { const type = 'foo bar type'; const source = 'foo_source'; @@ -95,59 +120,63 @@ describe('Elasticsearch', function () { }; it('should provide index UID from the provided UID', function () { - const indexUID = Elasticsearch.getIndexUID(bulk.uid); + const indexUID = getIndexUID(bulk.uid); - expect(indexUID.length).to.be.equal(Elasticsearch.INDEX_UID_LENGTH); + expect(indexUID.length).to.be.equal(INDEX_UID_LENGTH); // test starting and ending character expect(indexUID[0]).to.be.equal(bulk.uid[0]); - expect(indexUID[indexUID.length - 1]).to.be.equal(bulk.uid[Elasticsearch.INDEX_UID_LENGTH - 1]); + expect(indexUID[indexUID.length - 1]).to.be.equal(bulk.uid[INDEX_UID_LENGTH - 1]); }); it('should provide index name from the provided data', function () { - expect(Elasticsearch.getIndex(type as SCThingType, source, bulk)).to.be.equal( - `stapps_${type.split(' ').join('_')}_${source}_${Elasticsearch.getIndexUID(bulk.uid)}`, + expect(getThingIndexName(type as SCThingType, source, bulk)).to.be.equal( + `stapps_${type.split(' ').join('_')}_${source}_${getIndexUID(bulk.uid)}`, ); }); + + it('should reject invalid index names', function () { + expect(() => parseIndexName(':)')).to.throw(SyntaxError); + }); }); describe('removeAliasChars', function () { it('should remove spaces from both ends', function () { - expect(Elasticsearch.removeAliasChars(' foobaralias ', 'bulk-uid')).to.be.equal('foobaralias'); + expect(removeInvalidAliasChars(' foobaralias ', 'bulk-uid')).to.be.equal('foobaralias'); }); it('should replace inner spaces with underscores', function () { - expect(Elasticsearch.removeAliasChars('foo bar alias', 'bulk-uid')).to.be.equal('foo_bar_alias'); + expect(removeInvalidAliasChars('foo bar alias', 'bulk-uid')).to.be.equal('foo_bar_alias'); }); it('should remove invalid characters', function () { - expect(Elasticsearch.removeAliasChars('f,o#o\\b|ar/* ', 'bulk-uid')).to.be.equal('foobaralias'); + expect(removeInvalidAliasChars('f,o#o\\b|ar/* ', 'bulk-uid')).to.be.equal('foobaralias'); }); it('should remove invalid starting characters', function () { - expect(Elasticsearch.removeAliasChars('-foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); - expect(Elasticsearch.removeAliasChars('_foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); - expect(Elasticsearch.removeAliasChars('+foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); + expect(removeInvalidAliasChars('-foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); + expect(removeInvalidAliasChars('_foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); + expect(removeInvalidAliasChars('+foobaralias', 'bulk-uid')).to.be.equal('foobaralias'); }); it('should replace with a placeholder in case of invalid alias', function () { - expect(Elasticsearch.removeAliasChars('.', 'bulk-uid')).to.contain('placeholder'); - expect(Elasticsearch.removeAliasChars('..', 'bulk-uid')).to.contain('placeholder'); + expect(removeInvalidAliasChars('.', 'bulk-uid')).to.contain('placeholder'); + expect(removeInvalidAliasChars('..', 'bulk-uid')).to.contain('placeholder'); }); it('should work with common cases', function () { expect( - Elasticsearch.removeAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid'), + removeInvalidAliasChars('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890', 'bulk-uid'), ).to.be.equal('the-quick-brown-fox-jumps-over-the-lazy-dog-1234567890'); - expect( - Elasticsearch.removeAliasChars('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG', 'bulk-uid'), - ).to.be.equal('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG'); + expect(removeInvalidAliasChars('THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG', 'bulk-uid')).to.be.equal( + 'THE_QUICK_BROWN_FOX_JUMPS_OVER_THE_LAZY_DOG', + ); }); it('should warn in case of characters that are invalid in future elasticsearch versions', function () { const sandbox = sinon.createSandbox(); const loggerWarnStub = sandbox.stub(Logger, 'warn'); - expect(Elasticsearch.removeAliasChars('foo:bar:alias', 'bulk-uid')).to.contain('foo:bar:alias'); + expect(removeInvalidAliasChars('foo:bar:alias', 'bulk-uid')).to.contain('foo:bar:alias'); expect(loggerWarnStub.called).to.be.true; }); }); @@ -182,7 +211,7 @@ describe('Elasticsearch', function () { it('should log an error in case of there is one when getting response from the elasticsearch client', async function () { const error = new Error('Foo Error'); const loggerErrorStub = sandbox.stub(Logger, 'error').resolves('foo'); - sandbox.stub(Client.prototype, 'on').yields(error); + sandbox.stub(Diagnostic.prototype, 'on').yields(error); new Elasticsearch(configFile); @@ -192,7 +221,7 @@ describe('Elasticsearch', function () { it('should log the result in the debug mode when getting response from the elasticsearch client', async function () { const fakeResponse = {foo: 'bar'}; const loggerLogStub = sandbox.stub(Logger, 'log'); - sandbox.stub(Client.prototype, 'on').yields(null, fakeResponse); + sandbox.stub(Diagnostic.prototype, 'on').yields(null, fakeResponse); new Elasticsearch(configFile); expect(loggerLogStub.calledWith(fakeResponse)).to.be.false; @@ -254,26 +283,24 @@ describe('Elasticsearch', function () { describe('Operations with bundle/index', async function () { const sandbox = sinon.createSandbox(); let es: Elasticsearch; + let createStub: SinonStub; + let deleteStub: SinonStub; + let refreshStub: SinonStub; + let updateAliasesStub: SinonStub; + let existsStub: SinonStub; const oldIndex = 'stapps_footype_foosource_oldindex'; beforeEach(function () { + sandbox + .stub(Indices.prototype, 'getAlias') + .resolves({[oldIndex]: {aliases: {[SCThingType.Book]: {}}}} as any); + sandbox.stub(Indices.prototype, 'putTemplate').resolves({} as any); + createStub = sandbox.stub(Indices.prototype, 'create').resolves({} as any); + deleteStub = sandbox.stub(Indices.prototype, 'delete').resolves({} as any); + existsStub = sandbox.stub(Indices.prototype, 'exists').resolves({} as any); + refreshStub = sandbox.stub(Indices.prototype, 'refresh').resolves({} as any); + updateAliasesStub = sandbox.stub(Indices.prototype, 'updateAliases').resolves({} as any); es = new Elasticsearch(configFile); - es.client.indices = { - // @ts-expect-error not assignable - getAlias: () => Promise.resolve({body: [{[oldIndex]: {aliases: {[SCThingType.Book]: {}}}}]}), - // @ts-expect-error not assignable - putTemplate: () => Promise.resolve({}), - // @ts-expect-error not assignable - create: () => Promise.resolve({}), - // @ts-expect-error not assignable - delete: () => Promise.resolve({}), - // @ts-expect-error not assignable - exists: () => Promise.resolve({}), - // @ts-expect-error not assignable - refresh: () => Promise.resolve({}), - // @ts-expect-error not assignable - updateAliases: () => Promise.resolve({}), - }; }); afterEach(function () { @@ -286,8 +313,8 @@ describe('Elasticsearch', function () { }); it('should reject (throw an error) if the index name is not valid', async function () { - sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex}`); sandbox.createStubInstance(Client, {}); + sandbox.stub(utilModule, 'getThingIndexName').returns(`invalid_${getIndex}`); await es.init(); return expect(es.bulkCreated(bulk)).to.be.rejectedWith('Index'); @@ -295,9 +322,8 @@ describe('Elasticsearch', function () { it('should create a new index', async function () { const index = getIndex(); - sandbox.stub(Elasticsearch, 'getIndex').returns(index); + sandbox.stub(utilModule, 'getThingIndexName').returns(index); const putTemplateStub = sandbox.stub(templating, 'putTemplate'); - const createStub = sandbox.stub(es.client.indices, 'create'); await es.init(); await es.bulkCreated(bulk); @@ -313,21 +339,19 @@ describe('Elasticsearch', function () { sandbox.restore(); }); it('should cleanup index in case of the expired bulk for bulk whose index is not in use', async function () { - sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex()); - const clientDeleteStub = sandbox.stub(es.client.indices, 'delete'); + sandbox.stub(utilModule, 'getThingIndexName').returns(getIndex()); await es.bulkExpired({...bulk, state: 'in progress'}); - expect(clientDeleteStub.called).to.be.true; + expect(deleteStub.called).to.be.true; }); it('should not cleanup index in case of the expired bulk for bulk whose index is in use', async function () { - sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex()); - const clientDeleteStub = sandbox.stub(es.client.indices, 'delete'); + sandbox.stub(utilModule, 'getThingIndexName').returns(getIndex()); await es.bulkExpired({...bulk, state: 'done'}); - expect(clientDeleteStub.called).to.be.false; + expect(deleteStub.called).to.be.false; }); }); @@ -337,13 +361,23 @@ describe('Elasticsearch', function () { }); it('should reject if the index name is not valid', async function () { - sandbox.stub(Elasticsearch, 'getIndex').returns(`invalid_${getIndex()}`); + sandbox.stub(utilModule, 'getThingIndexName').returns(`invalid_${getIndex()}`); sandbox.createStubInstance(Client, {}); await es.init(); return expect(es.bulkUpdated(bulk)).to.be.rejectedWith('Index'); }); + it("should create templates if index doesn't exist", async function () { + await es.init(); + existsStub.resolves(false); + const putTemplateSpy = sandbox.spy(templating, 'putTemplate'); + await es.bulkUpdated(bulk); + + expect(createStub.called).to.be.true; + expect(putTemplateSpy.called).to.be.true; + }); + it('should create a new index', async function () { const index = getIndex(); const expectedRefreshActions = [ @@ -354,15 +388,12 @@ describe('Elasticsearch', function () { remove: {index: oldIndex, alias: SCThingType.Book}, }, ]; - sandbox.stub(Elasticsearch, 'getIndex').returns(index); + sandbox.stub(utilModule, 'getThingIndexName').returns(index); sandbox.stub(es, 'aliasMap').value({ [SCThingType.Book]: { [bulk.source]: oldIndex, }, }); - const refreshStub = sandbox.stub(es.client.indices, 'refresh'); - const updateAliasesStub = sandbox.stub(es.client.indices, 'updateAliases'); - const deleteStub = sandbox.stub(es.client.indices, 'delete'); sandbox.stub(templating, 'putTemplate'); await es.init(); @@ -371,9 +402,7 @@ describe('Elasticsearch', function () { expect(refreshStub.calledWith({index})).to.be.true; expect( updateAliasesStub.calledWith({ - body: { - actions: expectedRefreshActions, - }, + actions: expectedRefreshActions, }), ).to.be.true; expect(deleteStub.called).to.be.true; @@ -394,20 +423,19 @@ describe('Elasticsearch', function () { }); it('should reject if object is not found', async function () { - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse()); return expect(es.get('123')).to.rejectedWith('found'); }); it('should provide the thing if object is found', async function () { - const foundObject: ElasticsearchObject = { + const foundObject: SearchHit = { _id: '', _index: '', _score: 0, - _type: '', _source: message as SCMessage, }; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [foundObject]}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject)); return expect(await es.get('123')).to.be.eql(message); }); @@ -428,56 +456,54 @@ describe('Elasticsearch', function () { it('should not post if the object already exists in an index which will not be rolled over', async function () { const index = getIndex(); const oldIndex = index.replace('foosource', 'barsource'); - const object: ElasticsearchObject = { + const object: SearchHit = { _id: '', _index: oldIndex, _score: 0, - _type: '', _source: message as SCMessage, }; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); - sandbox.stub(Elasticsearch, 'getIndex').returns(index); + sandbox.stub(es.client, 'search').resolves(searchResponse(object)); + sandbox.stub(utilModule, 'getThingIndexName').returns(index); - return expect(es.post(object._source, bulk)).to.rejectedWith('exist'); + return expect(es.post(object._source!, bulk)).to.rejectedWith('exist'); }); it('should not reject if the object already exists but in an index which will be rolled over', async function () { - const object: ElasticsearchObject = { + const object: SearchHit = { _id: '', _index: getIndex(), _score: 0, - _type: '', _source: message as SCMessage, }; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse(object)); // return index name with different generated UID (see getIndex method) - sandbox.stub(Elasticsearch, 'getIndex').returns(getIndex()); + sandbox.stub(utilModule, 'getThingIndexName').returns(getIndex()); - return expect(es.post(object._source, bulk)).to.not.rejectedWith('exist'); + return expect(es.post(object._source!, bulk)).to.not.rejectedWith('exist'); }); it('should reject if there is an object creation error on the elasticsearch side', async function () { - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); - sandbox.stub(es.client, 'create').resolves({body: {created: false}}); + sandbox.stub(es.client, 'search').resolves(searchResponse()); + sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse); return expect(es.post(message as SCMessage, bulk)).to.rejectedWith('creation'); }); it('should create a new object', async function () { let caughtParameter: any; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse()); // @ts-expect-error call const createStub = sandbox.stub(es.client, 'create').callsFake(parameter => { caughtParameter = parameter; - return Promise.resolve({body: {created: true}}); + return Promise.resolve({result: 'created'}); }); await es.post(message as SCMessage, bulk); expect(createStub.called).to.be.true; - expect(caughtParameter.body).to.be.eql({ + expect(caughtParameter.document).to.be.eql({ ...message, - creation_date: caughtParameter.body.creation_date, + creation_date: caughtParameter.document.creation_date, }); }); }); @@ -493,29 +519,27 @@ describe('Elasticsearch', function () { sandbox.restore(); }); it('should reject to put if the object does not already exist', async function () { - const object: ElasticsearchObject = { + const object: SearchHit = { _id: '', _index: getIndex(), _score: 0, - _type: '', _source: message as SCMessage, }; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: []}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse()); - return expect(es.put(object._source)).to.rejectedWith('exist'); + return expect(es.put(object._source!)).to.rejectedWith('exist'); }); // noinspection JSUnusedLocalSymbols it('should update the object if it already exists', async function () { let caughtParameter: any; - const object: ElasticsearchObject = { + const object: SearchHit = { _id: '', _index: getIndex(), _score: 0, - _type: '', _source: message as SCMessage, }; - sandbox.stub(es.client, 'search').resolves({body: {hits: {hits: [object]}}}); + sandbox.stub(es.client, 'search').resolves(searchResponse(object)); // @ts-expect-error unused // eslint-disable-next-line @typescript-eslint/no-unused-vars const stubUpdate = sandbox.stub(es.client, 'update').callsFake(parameters => { @@ -523,7 +547,7 @@ describe('Elasticsearch', function () { return Promise.resolve({body: {created: true}}); }); - await es.put(object._source); + await es.put(object._source!); expect(caughtParameter.body.doc).to.be.eql(object._source); }); @@ -532,18 +556,16 @@ describe('Elasticsearch', function () { describe('search', async function () { let es: Elasticsearch; const sandbox = sinon.createSandbox(); - const objectMessage: ElasticsearchObject = { + const objectMessage: SearchHit = { _id: '123', _index: getIndex(), _score: 0, - _type: '', _source: message as SCMessage, }; - const objectBook: ElasticsearchObject = { + const objectBook: SearchHit = { _id: '321', _index: getIndex(), _score: 0, - _type: '', _source: book as SCBook, }; const fakeEsAggregations = { @@ -565,26 +587,16 @@ describe('Elasticsearch', function () { }, }, }; - const fakeSearchResponse: Partial>> = { - body: { - took: 12, - timed_out: false, - // @ts-expect-error not assignable - _shards: {}, - // @ts-expect-error not assignable - hits: { - hits: [objectMessage, objectBook], - total: 123, - }, - aggregations: fakeEsAggregations, + const fakeSearchResponse: SearchResponse = { + took: 12, + timed_out: false, + // @ts-expect-error not assignable + _shards: {}, + hits: { + hits: [objectMessage, objectBook], + total: 123, }, - headers: {}, - // @ts-expect-error not assignable - meta: {}, - // @ts-expect-error not assignable - statusCode: {}, - // @ts-expect-error not assignable - warnings: {}, + aggregations: fakeEsAggregations, }; let searchStub: sinon.SinonStub; before(function () { @@ -625,9 +637,9 @@ describe('Elasticsearch', function () { const {pagination} = await es.search({from}); expect(pagination).to.be.eql({ - count: fakeSearchResponse.body!.hits.hits.length, + count: fakeSearchResponse.hits.hits.length, offset: from, - total: fakeSearchResponse.body!.hits.total, + total: fakeSearchResponse.hits.total, }); }); @@ -659,22 +671,20 @@ describe('Elasticsearch', function () { }, }, }; - const fakeResponse = {foo: 'bar'}; + const fakeResponse = {foo: 'bar'} as SortCombinations; const fakeBuildSortResponse = [fakeResponse]; // @ts-expect-error not assignable - sandbox.stub(query, 'buildQuery').returns(fakeResponse); - sandbox.stub(query, 'buildSort').returns(fakeBuildSortResponse); + sandbox.stub(queryModule, 'buildQuery').returns(fakeResponse); + sandbox.stub(sortModule, 'buildSort').returns(fakeBuildSortResponse); await es.search(parameters); sandbox.assert.calledWithMatch(searchStub, { - body: { - aggs: aggregations, - query: fakeResponse, - sort: fakeBuildSortResponse, - }, + aggs: aggregations, + query: fakeResponse, + sort: fakeBuildSortResponse, from: parameters.from, - index: Elasticsearch.getListOfAllIndices(), + index: ALL_INDICES_QUERY, size: parameters.size, }); }); diff --git a/test/storage/elasticsearch/monitoring.spec.ts b/test/storage/elasticsearch/monitoring.spec.ts index 52ccc35f..135f03e9 100644 --- a/test/storage/elasticsearch/monitoring.spec.ts +++ b/test/storage/elasticsearch/monitoring.spec.ts @@ -14,7 +14,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import {ApiResponse, Client} from '@elastic/elasticsearch'; +import {Client} from '@elastic/elasticsearch'; +import {SearchResponse} from '@elastic/elasticsearch/lib/api/types'; import { SCMonitoringConfiguration, SCMonitoringLogAction, @@ -23,7 +24,6 @@ import { SCThings, } from '@openstapps/core'; import {Logger} from '@openstapps/logger'; -import {SearchResponse} from 'elasticsearch'; import {MailQueue} from '../../../src/notification/mail-queue'; import {setUp} from '../../../src/storage/elasticsearch/monitoring'; @@ -111,16 +111,14 @@ describe('Monitoring', async function () { }); it('should log errors where conditions failed', async function () { - const fakeSearchResponse: Partial>> = { - body: { - took: 12, - timed_out: false, - // @ts-expect-error not assignable - _shards: {}, - // @ts-expect-error not assignable - hits: { - total: 123, - }, + const fakeSearchResponse: SearchResponse = { + took: 12, + timed_out: false, + // @ts-expect-error not assignable + _shards: {}, + // @ts-expect-error not assignable + hits: { + total: 123, }, }; const fakeClient = new Client({node: 'http://foohost:9200'}); diff --git a/test/storage/elasticsearch/query.spec.ts b/test/storage/elasticsearch/query.spec.ts index 7b4a108f..13fc8cd5 100644 --- a/test/storage/elasticsearch/query.spec.ts +++ b/test/storage/elasticsearch/query.spec.ts @@ -25,25 +25,14 @@ import { SCThingType, } from '@openstapps/core'; import {expect} from 'chai'; -import { - ESDateRangeFilter, - ESRangeFilter, - ESNumericRangeFilter, - ElasticsearchConfig, - ESBooleanFilter, - ESGenericSort, - ESGeoDistanceFilter, - ESGeoDistanceSort, - ESTermFilter, - ScriptSort, -} from '../../../src/storage/elasticsearch/types/elasticsearch'; +import {buildFilter} from '../../../src/storage/elasticsearch/query/filter'; +import {buildBooleanFilter} from '../../../src/storage/elasticsearch/query/filters/boolean'; +import {buildQuery} from '../../../src/storage/elasticsearch/query/query'; +import {buildSort} from '../../../src/storage/elasticsearch/query/sort'; +import {ElasticsearchConfig} from '../../../src/storage/elasticsearch/types/elasticsearch-config'; +import {QueryDslSpecificQueryContainer} from '../../../src/storage/elasticsearch/types/util'; import {configFile} from '../../../src/common'; -import { - buildBooleanFilter, - buildFilter, - buildQuery, - buildSort, -} from '../../../src/storage/elasticsearch/query'; +import {SortCombinations} from '@elastic/elasticsearch/lib/api/types'; describe('Query', function () { describe('buildBooleanFilter', function () { @@ -74,7 +63,7 @@ describe('Query', function () { or: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'or'}}, not: {...booleanFilter, arguments: {...booleanFilter.arguments, operation: 'not'}}, }; - const expectedEsFilters: Array = [ + const expectedEsFilters: Array> = [ { term: { 'type.raw': 'catalog', @@ -88,20 +77,20 @@ describe('Query', function () { ]; it('should create appropriate elasticsearch "and" filter argument', function () { - const {must} = buildBooleanFilter(booleanFilters.and); + const {must} = buildBooleanFilter(booleanFilters.and).bool; expect(must).to.be.eql(expectedEsFilters); }); it('should create appropriate elasticsearch "or" filter argument', function () { - const {should, minimum_should_match} = buildBooleanFilter(booleanFilters.or); + const {should, minimum_should_match} = buildBooleanFilter(booleanFilters.or).bool; expect(should).to.be.eql(expectedEsFilters); expect(minimum_should_match).to.be.equal(1); }); it('should create appropriate elasticsearch "not" filter argument', function () { - const {must_not} = buildBooleanFilter(booleanFilters.not); + const {must_not} = buildBooleanFilter(booleanFilters.not).bool; expect(must_not).to.be.eql(expectedEsFilters); }); @@ -196,6 +185,10 @@ describe('Query', function () { expect(() => buildQuery(parameters, config, esConfig)).to.throw('query type'); }); + + it('should accept other search contexts', function () { + expect(buildQuery({context: 'place', ...parameters}, config, esConfig)).to.be.an('object'); + }); }); describe('buildFilter', function () { @@ -267,7 +260,7 @@ describe('Query', function () { it('should build value filter', function () { const filter = buildFilter(searchFilters.value); - const expectedFilter: ESTermFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'term'> = { term: { 'type.raw': SCThingType.Dish, }, @@ -279,7 +272,7 @@ describe('Query', function () { it('should build numeric range filters', function () { for (const upperMode of ['inclusive', 'exclusive', null]) { for (const lowerMode of ['inclusive', 'exclusive', null]) { - const expectedFilter: ESNumericRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { price: { relation: undefined, @@ -304,7 +297,7 @@ describe('Query', function () { mode: bound as 'inclusive' | 'exclusive', limit: out, }; - expectedFilter.range.price[ + expectedFilter.range.price![ `${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}` ] = out; } @@ -312,7 +305,7 @@ describe('Query', function () { setBound('upperBound', upperMode); setBound('lowerBound', lowerMode); - const filter = buildFilter(rawFilter) as ESNumericRangeFilter; + const filter = buildFilter(rawFilter) as QueryDslSpecificQueryContainer<'term'>; expect(filter).to.deep.equal(expectedFilter); for (const bound of ['g', 'l']) { // @ts-expect-error implicit any @@ -330,7 +323,7 @@ describe('Query', function () { it('should build date range filters', function () { for (const upperMode of ['inclusive', 'exclusive', null]) { for (const lowerMode of ['inclusive', 'exclusive', null]) { - const expectedFilter: ESDateRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { price: { format: 'thisIsADummyFormat', @@ -359,7 +352,7 @@ describe('Query', function () { mode: bound as 'inclusive' | 'exclusive', limit: out, }; - expectedFilter.range.price[ + expectedFilter.range.price![ `${location === 'lowerBound' ? 'g' : 'l'}${bound === 'inclusive' ? 'te' : 't'}` ] = out; } @@ -367,7 +360,7 @@ describe('Query', function () { setBound('upperBound', upperMode); setBound('lowerBound', lowerMode); - const filter = buildFilter(rawFilter) as ESNumericRangeFilter; + const filter = buildFilter(rawFilter) as QueryDslSpecificQueryContainer<'range'>; expect(filter).to.deep.equal(expectedFilter); for (const bound of ['g', 'l']) { // @ts-expect-error implicit any @@ -394,7 +387,7 @@ describe('Query', function () { }, }); - const expectedFilter: ESRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { 'offers.availabilityRange': { gte: `test||/${scope}`, @@ -415,7 +408,7 @@ describe('Query', function () { }, }); - const expectedFilter: ESRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { 'offers.availabilityRange': { gte: 'test||/s', @@ -436,7 +429,7 @@ describe('Query', function () { }, }); - const expectedFilter: ESRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { 'offers.availabilityRange': { gte: `test||/d`, @@ -456,7 +449,7 @@ describe('Query', function () { }, }); - const expectedFilter: ESRangeFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'range'> = { range: { 'offers.availabilityRange': { gte: `now/d`, @@ -470,7 +463,7 @@ describe('Query', function () { it('should build distance filter', function () { const filter = buildFilter(searchFilters.distance); - const expectedFilter: ESGeoDistanceFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'geo_distance'> = { geo_distance: { 'distance': '1000m', 'geo.point.coordinates': { @@ -486,34 +479,18 @@ describe('Query', function () { it('should build geo filter for shapes and points', function () { const filter = buildFilter(searchFilters.geoPoint); const expectedFilter = { - bool: { - minimum_should_match: 1, - should: [ - { - geo_shape: { - 'geo.polygon': { - relation: undefined, - shape: { - type: 'envelope', - coordinates: [ - [50.123, 8.123], - [50.123, 8.123], - ], - }, - }, - 'ignore_unmapped': true, - }, + geo_shape: { + 'geo.polygon': { + relation: undefined, + shape: { + type: 'envelope', + coordinates: [ + [50.123, 8.123], + [50.123, 8.123], + ], }, - { - geo_bounding_box: { - 'geo.point.coordinates': { - bottom_right: [50.123, 8.123], - top_left: [50.123, 8.123], - }, - 'ignore_unmapped': true, - }, - }, - ], + }, + 'ignore_unmapped': true, }, }; @@ -543,7 +520,7 @@ describe('Query', function () { it('should build boolean filter', function () { const filter = buildFilter(searchFilters.boolean); - const expectedFilter: ESBooleanFilter = { + const expectedFilter: QueryDslSpecificQueryContainer<'bool'> = { bool: { minimum_should_match: 0, must: [ @@ -604,8 +581,8 @@ describe('Query', function () { }, }, ]; - let sorts: Array = []; - const expectedSorts: {[key: string]: ESGenericSort | ESGeoDistanceSort | ScriptSort} = { + let sorts: SortCombinations[] = []; + const expectedSorts: {[key: string]: SortCombinations} = { ducet: { 'name.sort': 'desc', }, @@ -632,7 +609,7 @@ describe('Query', function () { }, }; before(function () { - sorts = buildSort(searchSCSearchSort); + sorts = buildSort(searchSCSearchSort) as SortCombinations[]; }); it('should build ducet sort', function () { @@ -649,10 +626,10 @@ describe('Query', function () { it('should build price sort', function () { const priceSortNoScript = { - ...sorts[3], + ...(sorts[3] as any), _script: { - ...(sorts[3] as ScriptSort)._script, - script: (expectedSorts.price as ScriptSort)._script.script, + ...(sorts[3] as any)._script, + script: (expectedSorts.price as any)._script.script, }, }; expect(priceSortNoScript).to.be.eql(expectedSorts.price); diff --git a/tsconfig.json b/tsconfig.json index 4dbcef9c..c4b40d1f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,9 @@ "extends": "./node_modules/@openstapps/configuration/tsconfig.json", "compilerOptions": { "resolveJsonModule": true, - "useUnknownInCatchVariables": false + "skipLibCheck": true, + "useUnknownInCatchVariables": false, + "lib": ["ES2020"] }, "exclude": [ "./config/",