From 3ea3ae5800d4e593eee139082eb10a0ee7a8e5df Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 9 Jul 2026 17:29:42 +0200 Subject: [PATCH 1/9] UNOMI-947/946: Platform hygiene, graphql-ui upgrade, and IT cluster health Batch dependency and frontend upgrades for 3.1 GA: align swagger-ui feature with pom version, bump error_prone/logback/jsch, upgrade GraphiQL 5 with relative URLs, and harden IT search cluster replica enforcement. --- .../unomi/healthcheck/HealthCheckConfig.java | 1 + .../ElasticSearchHealthCheckProvider.java | 11 +- ...apache.unomi.healthcheck-elasticsearch.cfg | 1 + extensions/router/router-core/pom.xml | 2 +- .../router/router-karaf-feature/pom.xml | 2 +- .../src/main/feature/feature.xml | 2 +- graphql/graphql-ui/package.json | 12 +- .../src/main/resources/assets/js/index.jsx | 36 +- .../main/resources/assets/styles/index.less | 1 - graphql/graphql-ui/webpack.config.js | 30 +- graphql/graphql-ui/yarn.lock | 1091 +++++++++-------- .../java/org/apache/unomi/itests/BaseIT.java | 104 ++ .../Migrate16xToCurrentVersionIT.java | 2 + kar/src/main/feature/feature.xml | 2 +- pom.xml | 2 +- services/pom.xml | 2 +- 16 files changed, 764 insertions(+), 537 deletions(-) diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java index b52c4c777..19fce3170 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/HealthCheckConfig.java @@ -41,6 +41,7 @@ public class HealthCheckConfig { public static final String CONFIG_ES_LOGIN = "esLogin"; public static final String CONFIG_ES_PASSWORD = "esPassword"; public static final String CONFIG_ES_TRUST_ALL_CERTIFICATES = "esHttpClient.trustAllCertificates"; + public static final String CONFIG_ES_MINIMAL_CLUSTER_STATE = "esMinimalClusterState"; public static final String CONFIG_OS_ADDRESSES = "osAddresses"; public static final String CONFIG_OS_SSL_ENABLED = "osSSLEnabled"; public static final String CONFIG_OS_LOGIN = "osLogin"; diff --git a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java index b07f279e7..b64ad50a4 100644 --- a/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java +++ b/extensions/healthcheck/src/main/java/org/apache/unomi/healthcheck/provider/ElasticSearchHealthCheckProvider.java @@ -110,6 +110,12 @@ private HealthCheckResponse refresh() { HealthCheckResponse.Builder builder = new HealthCheckResponse.Builder(); builder.name(NAME).down(); if (isActive) { + String minimalClusterState = config.get(HealthCheckConfig.CONFIG_ES_MINIMAL_CLUSTER_STATE); + if (StringUtils.isEmpty(minimalClusterState)) { + minimalClusterState = "green"; + } else { + minimalClusterState = minimalClusterState.toLowerCase(); + } String url = (config.get(HealthCheckConfig.CONFIG_ES_SSL_ENABLED).equals("true") ? "https://" : "http://").concat( config.get(HealthCheckConfig.CONFIG_ES_ADDRESSES).split(",")[0].trim()).concat("/_cluster/health"); CloseableHttpResponse response = null; @@ -122,7 +128,8 @@ private HealthCheckResponse refresh() { String content = EntityUtils.toString(entity); try { JsonNode root = mapper.readTree(content); - if (root.has("status") && "green".equals(root.get("status").asText())) { + String status = root.has("status") ? root.get("status").asText() : null; + if ("green".equals(status) || ("yellow".equals(status) && "yellow".equals(minimalClusterState))) { builder.live(); } if (root.has("cluster_name")) @@ -147,7 +154,7 @@ private HealthCheckResponse refresh() { builder.withData("unassigned_shards", root.get("unassigned_shards").asLong()); } catch (Exception parseEx) { // Fallback to simple LIVE detection - if (content.contains("\"status\":\"green\"")) { + if (content.contains("\"status\":\"green\"") || (content.contains("\"status\":\"yellow\"") && "yellow".equals(minimalClusterState))) { builder.live(); } } diff --git a/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg index 6f6ef2860..bb8e481d7 100644 --- a/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg +++ b/extensions/healthcheck/src/main/resources/org.apache.unomi.healthcheck-elasticsearch.cfg @@ -21,6 +21,7 @@ esSSLEnabled = ${org.apache.unomi.elasticsearch.sslEnable:-false} esLogin = ${org.apache.unomi.elasticsearch.username:-} esPassword = ${org.apache.unomi.elasticsearch.password:-} esHttpClient.trustAllCertificates = ${org.apache.unomi.elasticsearch.sslTrustAllCertificates:-false} +esMinimalClusterState = ${org.apache.unomi.elasticsearch.minimalClusterState:-GREEN} # Security configuration authentication.realm = ${org.apache.unomi.security.realm:-karaf} diff --git a/extensions/router/router-core/pom.xml b/extensions/router/router-core/pom.xml index 0ec3cfc1b..54f03113f 100644 --- a/extensions/router/router-core/pom.xml +++ b/extensions/router/router-core/pom.xml @@ -140,7 +140,7 @@ org.apache.servicemix.bundles org.apache.servicemix.bundles.jsch - 0.1.54_1 + 0.1.55_1 provided diff --git a/extensions/router/router-karaf-feature/pom.xml b/extensions/router/router-karaf-feature/pom.xml index 86badcd90..e78f8f2aa 100644 --- a/extensions/router/router-karaf-feature/pom.xml +++ b/extensions/router/router-karaf-feature/pom.xml @@ -82,7 +82,7 @@ org.apache.servicemix.bundles org.apache.servicemix.bundles.jsch - 0.1.54_1 + 0.1.55_1 org.apache.kafka diff --git a/extensions/router/router-karaf-feature/src/main/feature/feature.xml b/extensions/router/router-karaf-feature/src/main/feature/feature.xml index 2ff72507e..647b80914 100644 --- a/extensions/router/router-karaf-feature/src/main/feature/feature.xml +++ b/extensions/router/router-karaf-feature/src/main/feature/feature.xml @@ -20,7 +20,7 @@
Apache Karaf feature for the Apache Unomi Context Server extension
wrap unomi-services - mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jsch/0.1.54_1 + mvn:org.apache.servicemix.bundles/org.apache.servicemix.bundles.jsch/0.1.55_1 mvn:commons-net/commons-net/${commons-net.version} wrap:mvn:org.apache.kafka/kafka-clients/${kafka.client.version} mvn:org.apache.camel/camel-core/${camel.version} diff --git a/graphql/graphql-ui/package.json b/graphql/graphql-ui/package.json index 3551640f0..1baaea4e6 100644 --- a/graphql/graphql-ui/package.json +++ b/graphql/graphql-ui/package.json @@ -15,16 +15,14 @@ "build:production": "yarn webpack --mode=production" }, "dependencies": { - "@graphiql/react": "0.22.3", - "@graphiql/toolkit": "^0.9.1", - "graphiql": "^3.3.1", - "graphql": "^16.8.2", - "graphql-ws": "^5.16.0", + "graphiql": "^5.2.0", + "graphql": "^16.14.0", + "graphql-ws": "^5.16.2", "react": "^18.3.1", "react-dom": "^18.3.1" }, "resolutions": { - "**/terser": "4.8.1", + "**/terser": "5.39.0", "**/ansi-regex": "4.1.1", "**/minimist": "1.2.6" }, @@ -41,7 +39,7 @@ "less": "^4.2.0", "less-loader": "^11.1.3", "mini-css-extract-plugin": "^2.7.6", - "postcss": "^8.4.28", + "postcss": "^8.5.10", "postcss-loader": "^7.3.3", "source-map-loader": "^4.0.1" } diff --git a/graphql/graphql-ui/src/main/resources/assets/js/index.jsx b/graphql/graphql-ui/src/main/resources/assets/js/index.jsx index 4bbadb3f0..a0869796f 100644 --- a/graphql/graphql-ui/src/main/resources/assets/js/index.jsx +++ b/graphql/graphql-ui/src/main/resources/assets/js/index.jsx @@ -15,28 +15,38 @@ * limitations under the License. */ -import {GraphiQL} from 'graphiql'; -import {createGraphiQLFetcher} from '@graphiql/toolkit'; +import 'graphiql/setup-workers/webpack'; +import 'graphiql/graphiql.css'; +import { GraphiQL } from 'graphiql'; +import { createGraphiQLFetcher } from '@graphiql/toolkit'; +import { createClient } from 'graphql-ws'; import * as React from 'react'; -import * as ReactDOM from 'react-dom'; -import {createClient} from 'graphql-ws'; +import { createRoot } from 'react-dom/client'; + +function graphqlHttpUrl() { + const protocol = window.location.protocol === 'https:' ? 'https:' : 'http:'; + return protocol + '//' + window.location.host + '/graphql'; +} + +function graphqlWsUrl() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + return protocol + '//' + window.location.host + '/graphql'; +} function createFetcher() { return createGraphiQLFetcher({ - url: `http://localhost:8181/graphql`, - wsClient: createClient( - { - url: `ws://localhost:8181/graphql`, - }), + url: graphqlHttpUrl(), + wsClient: createClient({ url: graphqlWsUrl() }), }); } function QueryPlayground() { - return ( - - ); + return ; } document.addEventListener('DOMContentLoaded', function () { - ReactDOM.render(, document.getElementById('root')); + const rootElement = document.getElementById('root'); + if (rootElement) { + createRoot(rootElement).render(); + } }, false); diff --git a/graphql/graphql-ui/src/main/resources/assets/styles/index.less b/graphql/graphql-ui/src/main/resources/assets/styles/index.less index 5d614bc75..aaef75287 100644 --- a/graphql/graphql-ui/src/main/resources/assets/styles/index.less +++ b/graphql/graphql-ui/src/main/resources/assets/styles/index.less @@ -1,4 +1,3 @@ -@import "~graphiql/graphiql.css"; html, body { height: 100%; diff --git a/graphql/graphql-ui/webpack.config.js b/graphql/graphql-ui/webpack.config.js index 967388666..0b6098cb9 100644 --- a/graphql/graphql-ui/webpack.config.js +++ b/graphql/graphql-ui/webpack.config.js @@ -17,6 +17,15 @@ const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const path = require('path'); +const graphiqlPackages = [ + path.join(__dirname, 'node_modules/graphiql'), + path.join(__dirname, 'node_modules/@graphiql'), + path.join(__dirname, 'node_modules/monaco-editor'), + path.join(__dirname, 'node_modules/monaco-graphql'), + path.join(__dirname, 'node_modules/@codemirror'), + path.join(__dirname, 'node_modules/codemirror'), +]; + module.exports = (env, argv) => { const isProd = argv.mode === 'production'; @@ -32,20 +41,29 @@ module.exports = (env, argv) => { path: path.join(__dirname, '/target/assets'), }, resolve: { - extensions: ['.js', '.jsx', '.less', '.css'], + extensions: ['.js', '.jsx', '.mjs', '.less', '.css'], }, module: { rules: [ { - test: /\.(js|jsx)$/, - exclude: /node_modules/, + test: /\.(js|jsx|mjs)$/, + include: [ + path.join(__dirname, '/src/main/resources/assets'), + ...graphiqlPackages, + ], use: ['babel-loader'] }, { - test: /\.(png|svg|jpg|gif)$/, + test: /\.(png|svg|jpg|gif|woff|woff2|ttf|eot)$/, + type: 'asset/resource', + }, + { + test: /\.css$/, + include: graphiqlPackages, use: [ - 'file-loader', - ], + {loader: MiniCssExtractPlugin.loader, options: {publicPath: '../'}}, + {loader: 'css-loader', options: {sourceMap: !isProd}}, + ] }, { test: /\.less$/, diff --git a/graphql/graphql-ui/yarn.lock b/graphql/graphql-ui/yarn.lock index 43f7837e7..cd2ac5c3f 100644 --- a/graphql/graphql-ui/yarn.lock +++ b/graphql/graphql-ui/yarn.lock @@ -892,18 +892,6 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@emotion/is-prop-valid@^0.8.2": - version "0.8.8" - resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz#db28b1c4368a259b60a97311d6a952d4fd01ac1a" - integrity sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA== - dependencies: - "@emotion/memoize" "0.7.4" - -"@emotion/memoize@0.7.4": - version "0.7.4" - resolved "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz#19bf0f5af19149111c40d98bb0cf82119f5d9eeb" - integrity sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw== - "@floating-ui/core@^1.0.0": version "1.6.2" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.2.tgz#d37f3e0ac1f1c756c7de45db13303a266226851a" @@ -911,6 +899,13 @@ dependencies: "@floating-ui/utils" "^0.2.0" +"@floating-ui/core@^1.7.5": + version "1.7.5" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.5.tgz#d4af157a03330af5a60e69da7a4692507ada0622" + integrity sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ== + dependencies: + "@floating-ui/utils" "^0.2.11" + "@floating-ui/dom@^1.0.0": version "1.6.5" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.5.tgz#323f065c003f1d3ecf0ff16d2c2c4d38979f4cb9" @@ -919,6 +914,14 @@ "@floating-ui/core" "^1.0.0" "@floating-ui/utils" "^0.2.0" +"@floating-ui/dom@^1.7.6": + version "1.7.6" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.6.tgz#f915bba5abbb177e1f227cacee1b4d0634b187bf" + integrity sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ== + dependencies: + "@floating-ui/core" "^1.7.5" + "@floating-ui/utils" "^0.2.11" + "@floating-ui/react-dom@^2.0.0": version "2.1.0" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.0.tgz#4f0e5e9920137874b2405f7d6c862873baf4beff" @@ -926,47 +929,112 @@ dependencies: "@floating-ui/dom" "^1.0.0" +"@floating-ui/react-dom@^2.1.2": + version "2.1.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.8.tgz#5fb5a20d10aafb9505f38c24f38d00c8e1598893" + integrity sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A== + dependencies: + "@floating-ui/dom" "^1.7.6" + +"@floating-ui/react@^0.26.16": + version "0.26.28" + resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.26.28.tgz#93f44ebaeb02409312e9df9507e83aab4a8c0dc7" + integrity sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw== + dependencies: + "@floating-ui/react-dom" "^2.1.2" + "@floating-ui/utils" "^0.2.8" + tabbable "^6.0.0" + "@floating-ui/utils@^0.2.0": version "0.2.2" resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.2.tgz#d8bae93ac8b815b2bd7a98078cf91e2724ef11e5" integrity sha512-J4yDIIthosAsRZ5CPYP/jQvUAQtlZTTD/4suA08/FEnlxqW3sKS9iAhgsa9VYLZ6vDHn/ixJgIqRQPotoBjxIw== -"@graphiql/react@0.22.3", "@graphiql/react@^0.22.3": - version "0.22.3" - resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.22.3.tgz#aa0cd4260ad8e98dc00e0fc9c54cb1b3e227d980" - integrity sha512-rZGs0BbJ4ImFy6l489aXUEB3HzGVoD7hI8CycydNRXR6+qYgp/fuNFCXMJe+q9gDyC/XhBXni8Pdugk8HxJ05g== - dependencies: - "@graphiql/toolkit" "^0.9.1" - "@headlessui/react" "^1.7.15" - "@radix-ui/react-dialog" "^1.0.4" - "@radix-ui/react-dropdown-menu" "^2.0.5" - "@radix-ui/react-tooltip" "^1.0.6" - "@radix-ui/react-visually-hidden" "^1.0.3" - "@types/codemirror" "^5.60.8" +"@floating-ui/utils@^0.2.11", "@floating-ui/utils@^0.2.8": + version "0.2.11" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.11.tgz#a269e055e40e2f45873bae9d1a2fdccbd314ea3f" + integrity sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg== + +"@graphiql/plugin-doc-explorer@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@graphiql/plugin-doc-explorer/-/plugin-doc-explorer-0.4.2.tgz#6fc65422272812123f18a8da17f12ebd30c8c4af" + integrity sha512-jqRUSaP9pq2JdoovKaiNQoV4ZVcDP5nn+QEa++vEYh0nCn76836SAde2/LkYMc9NnN8/PHMKqeUBnClZ+AUtVQ== + dependencies: + "@headlessui/react" "^2.2" + react-compiler-runtime "19.1.0-rc.1" + zustand "^5" + +"@graphiql/plugin-history@^0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@graphiql/plugin-history/-/plugin-history-0.4.2.tgz#03ad765af6360b8e20f83840e1612c1e3d072656" + integrity sha512-kwQYc1gmmkLbJPRHI/df3wtYNKNBGHxVkbkd+tbnRuCkrpdMm6NygCQeproJFKHTRbd3lYBAolaBcfgWNd196A== + dependencies: + "@graphiql/toolkit" "^0.12.0" + react-compiler-runtime "19.1.0-rc.1" + zustand "^5" + +"@graphiql/react@^0.37.7": + version "0.37.7" + resolved "https://registry.yarnpkg.com/@graphiql/react/-/react-0.37.7.tgz#85cd4c6e12171a8bbfe574065f3d3ab231b96bea" + integrity sha512-yHK+9wk9xWggrDdAOEOPwNJFCgkAG01ZDVcwuteBw1DCkDaPyBYC6H7hkw5AVYw8PbhlgPOEWBwXzceb9IV4zw== + dependencies: + "@graphiql/toolkit" "^0.12.1" + "@radix-ui/react-dialog" "^1.1" + "@radix-ui/react-dropdown-menu" "^2.1" + "@radix-ui/react-tooltip" "^1.2" + "@radix-ui/react-visually-hidden" "^1.2" clsx "^1.2.1" - codemirror "^5.65.3" - codemirror-graphql "^2.0.12" - copy-to-clipboard "^3.2.0" - framer-motion "^6.5.1" - graphql-language-service "^5.2.1" + framer-motion "^12.12" + get-value "^3.0.1" + graphql-language-service "^5.5.2" + jsonc-parser "^3.3.1" markdown-it "^14.1.0" + monaco-editor "0.52.2" + monaco-graphql "^1.8.0" + prettier "^3.5.3" + react-compiler-runtime "19.1.0-rc.1" set-value "^4.1.0" + zustand "^5" -"@graphiql/toolkit@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@graphiql/toolkit/-/toolkit-0.9.1.tgz#44bfa83aed79c8c18affac49efbb81f8e87bade3" - integrity sha512-LVt9pdk0830so50ZnU2Znb2rclcoWznG8r8asqAENzV0U1FM1kuY0sdPpc/rBc9MmmNgnB6A+WZzDhq6dbhTHA== +"@graphiql/toolkit@^0.12.0", "@graphiql/toolkit@^0.12.1": + version "0.12.1" + resolved "https://registry.yarnpkg.com/@graphiql/toolkit/-/toolkit-0.12.1.tgz#c74866a8c2ab3a81ca5553d0503b05b6db5892bc" + integrity sha512-OuHVUIvPL7nz3uAdG9eyZ5cZx87JH+XMOHiXgv3foYZWBPW8FNMGUyFflxP70jhv7bX9UPyYmRsTIJ+GSlK0PQ== dependencies: "@n1ru4l/push-pull-async-iterable-iterator" "^3.1.0" meros "^1.1.4" -"@headlessui/react@^1.7.15": - version "1.7.19" - resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-1.7.19.tgz#91c78cf5fcb254f4a0ebe96936d48421caf75f40" - integrity sha512-Ll+8q3OlMJfJbAKM/+/Y2q6PPYbryqNTXDbryx7SXLIDamkF6iQFbriYHga0dY44PvDhvvBWCx1Xj4U5+G4hOw== +"@headlessui/react@^2.2": + version "2.2.10" + resolved "https://registry.yarnpkg.com/@headlessui/react/-/react-2.2.10.tgz#ad69258abcb9efea27a4b37e06e8c9cafd39ca63" + integrity sha512-5pVLNK9wlpxTUTy9GpgbX/SdcRh+HBnPktjM2wbiLTH4p+2EPHBO1aoSryUCuKUIItdDWO9ITlhUL8UnUN/oIA== dependencies: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" + "@floating-ui/react" "^0.26.16" + "@react-aria/focus" "^3.20.2" + "@react-aria/interactions" "^3.25.0" + "@tanstack/react-virtual" "^3.13.9" + use-sync-external-store "^1.5.0" + +"@internationalized/date@^3.12.2": + version "3.12.2" + resolved "https://registry.yarnpkg.com/@internationalized/date/-/date-3.12.2.tgz#08a65edd2a29775e22c168ddc029fb54bf9b8a85" + integrity sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/number@^3.6.7": + version "3.6.7" + resolved "https://registry.yarnpkg.com/@internationalized/number/-/number-3.6.7.tgz#5a0a8fa413b5f8679a59dcf37e2a74dc508b8371" + integrity sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg== + dependencies: + "@swc/helpers" "^0.5.0" + +"@internationalized/string@^3.2.9": + version "3.2.9" + resolved "https://registry.yarnpkg.com/@internationalized/string/-/string-3.2.9.tgz#0e50385c411eac1275d91cdeb56dd7fbc234997f" + integrity sha512-kzP/M/mbQxODlmOt4bIQZ2SBVUWUSqMLXooXixnX7noche8WHaQcA+nwFN1K2KCF/cp+LDUhcJsCicwkvhD1pg== + dependencies: + "@swc/helpers" "^0.5.0" "@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5": version "0.3.13" @@ -989,6 +1057,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== +"@jridgewell/source-map@^0.3.3": + version "0.3.11" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.11.tgz#b21835cbd36db656b857c2ad02ebd413cc13a9ba" + integrity sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" @@ -1007,7 +1083,7 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28": +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25", "@jridgewell/trace-mapping@^0.3.28": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== @@ -1015,343 +1091,313 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@motionone/animation@^10.12.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/animation/-/animation-10.18.0.tgz#868d00b447191816d5d5cf24b1cafa144017922b" - integrity sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw== - dependencies: - "@motionone/easing" "^10.18.0" - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/dom@10.12.0": - version "10.12.0" - resolved "https://registry.yarnpkg.com/@motionone/dom/-/dom-10.12.0.tgz#ae30827fd53219efca4e1150a5ff2165c28351ed" - integrity sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw== - dependencies: - "@motionone/animation" "^10.12.0" - "@motionone/generators" "^10.12.0" - "@motionone/types" "^10.12.0" - "@motionone/utils" "^10.12.0" - hey-listen "^1.0.8" - tslib "^2.3.1" - -"@motionone/easing@^10.18.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/easing/-/easing-10.18.0.tgz#7b82f6010dfee3a1bb0ee83abfbaff6edae0c708" - integrity sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg== - dependencies: - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/generators@^10.12.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/generators/-/generators-10.18.0.tgz#fe09ab5cfa0fb9a8884097feb7eb60abeb600762" - integrity sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg== - dependencies: - "@motionone/types" "^10.17.1" - "@motionone/utils" "^10.18.0" - tslib "^2.3.1" - -"@motionone/types@^10.12.0", "@motionone/types@^10.17.1": - version "10.17.1" - resolved "https://registry.yarnpkg.com/@motionone/types/-/types-10.17.1.tgz#cf487badbbdc9da0c2cb86ffc1e5d11147c6e6fb" - integrity sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A== - -"@motionone/utils@^10.12.0", "@motionone/utils@^10.18.0": - version "10.18.0" - resolved "https://registry.yarnpkg.com/@motionone/utils/-/utils-10.18.0.tgz#a59ff8932ed9009624bca07c56b28ef2bb2f885e" - integrity sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw== - dependencies: - "@motionone/types" "^10.17.1" - hey-listen "^1.0.8" - tslib "^2.3.1" - "@n1ru4l/push-pull-async-iterable-iterator@^3.1.0": version "3.2.0" resolved "https://registry.yarnpkg.com/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-3.2.0.tgz#c15791112db68dd9315d329d652b7e797f737655" integrity sha512-3fkKj25kEjsfObL6IlKPAlHYPq/oYwUkkQ03zsTTiDjD7vg/RxjdiLeCydqtxHZP0JgsXL3D/X5oAkMGzuUp/Q== -"@radix-ui/primitive@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2" - integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA== +"@radix-ui/primitive@1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.5.tgz#cfeb31acbf332c72eb0b13831963c21ad6ca3e32" + integrity sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg== -"@radix-ui/react-arrow@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz#744f388182d360b86285217e43b6c63633f39e7a" - integrity sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw== +"@radix-ui/react-arrow@1.1.11": + version "1.1.11" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.11.tgz#be2a068bffe4453bd6941fc44eed1f1ea0e8226f" + integrity sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A== dependencies: - "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-primitive" "2.1.7" -"@radix-ui/react-collection@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed" - integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw== +"@radix-ui/react-collection@1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.12.tgz#1ca96614de0643b39f786f6545c3db0b0e7daa8c" + integrity sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q== dependencies: - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-slot" "1.1.0" - -"@radix-ui/react-compose-refs@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" - integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-slot" "1.3.0" -"@radix-ui/react-context@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.0.tgz#6df8d983546cfd1999c8512f3a8ad85a6e7fcee8" - integrity sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A== - -"@radix-ui/react-dialog@^1.0.4": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.0.tgz#9d354a8c7f534d49fffd2544fb7b371cb49da71c" - integrity sha512-oiSJcsjbdC8JqbXrOuhOd7oaEaPp3x2L2zn6V7ie6SSpEjrAha/WabDX4po6laGwbhAu9DT0XxHL0DmcIXrR0A== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-focus-guards" "1.1.0" - "@radix-ui/react-focus-scope" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-portal" "1.1.0" - "@radix-ui/react-presence" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-slot" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - aria-hidden "^1.1.1" - react-remove-scroll "2.5.7" - -"@radix-ui/react-direction@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" - integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== - -"@radix-ui/react-dismissable-layer@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz#2cd0a49a732372513733754e6032d3fb7988834e" - integrity sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-escape-keydown" "1.1.0" - -"@radix-ui/react-dropdown-menu@^2.0.5": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.0.tgz#9e4693f3b300cd54e553a486ddd7a5df355b78ba" - integrity sha512-8fAz27yxVaYTkXMm5dVOcKCHOiio9b4nl7rO1HmK8rpzcEl0kSSmwFQsYDyJxB/Em48PvXTez/iaBj3VEd2N4g== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-menu" "2.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-focus-guards@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz#8e9abb472a9a394f59a1b45f3dd26cfe3fc6da13" - integrity sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw== - -"@radix-ui/react-focus-scope@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2" - integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA== - dependencies: - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - -"@radix-ui/react-id@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed" - integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-compose-refs@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.3.tgz#5f1e61e1a5f52800d31e7f8affa6d046e38f50d1" + integrity sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA== -"@radix-ui/react-menu@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.0.tgz#383d078f3e3708f4134e2d61a66c39b40c41c99c" - integrity sha512-0AxIUQJpimipHDgTVISZbdOY+wZzgICKAsqfI1rF2Hp0Jh3YSv9e9J1tYYyurPBONe5vKi3hZPtVt2E85Sac7A== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-collection" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-focus-guards" "1.1.0" - "@radix-ui/react-focus-scope" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.0" - "@radix-ui/react-portal" "1.1.0" - "@radix-ui/react-presence" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-roving-focus" "1.1.0" - "@radix-ui/react-slot" "1.1.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - aria-hidden "^1.1.1" - react-remove-scroll "2.5.7" - -"@radix-ui/react-popper@1.2.0": +"@radix-ui/react-context@1.2.0": version "1.2.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.2.0.tgz#a3e500193d144fe2d8f5d5e60e393d64111f2a7a" - integrity sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg== + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.2.0.tgz#81e75eb5bdeb55bbe019547f13fb5c78598d44e2" + integrity sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg== + +"@radix-ui/react-dialog@^1.1": + version "1.1.19" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.19.tgz#ecdf153deb213c2d92ac02e10955dd288ceb3b3e" + integrity sha512-+HhbN2+YtkRgVirjZ2afMeutQRuGOrdkWR5+EFC58SJojGmtyNQwYzgi6tHBpOxvFHefMtPeHdgtjz0BOGxFQg== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-dismissable-layer" "1.1.15" + "@radix-ui/react-focus-guards" "1.1.4" + "@radix-ui/react-focus-scope" "1.1.12" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-portal" "1.1.13" + "@radix-ui/react-presence" "1.1.7" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-slot" "1.3.0" + "@radix-ui/react-use-controllable-state" "1.2.3" + aria-hidden "^1.2.4" + react-remove-scroll "^2.7.2" + +"@radix-ui/react-direction@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.2.tgz#9cc69edd659d79fba4101ee0e2dbcffc2024504f" + integrity sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA== + +"@radix-ui/react-dismissable-layer@1.1.15": + version "1.1.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.15.tgz#accf8c7b6ec77e7544d2f6c411426d453ca66bcb" + integrity sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-callback-ref" "1.1.2" + "@radix-ui/react-use-effect-event" "0.0.3" + +"@radix-ui/react-dropdown-menu@^2.1": + version "2.1.20" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.20.tgz#dfc4a2fb67a382aed43d26f8d2717bf2d3a393a6" + integrity sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-menu" "2.1.20" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-controllable-state" "1.2.3" + +"@radix-ui/react-focus-guards@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.4.tgz#3ef07a117ae7aa1430442aaebd766507e69d391c" + integrity sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q== + +"@radix-ui/react-focus-scope@1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.12.tgz#5a8b099c80271a0667ff2e74b25ced3ea58e8ee3" + integrity sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-callback-ref" "1.1.2" + +"@radix-ui/react-id@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.2.tgz#6fe97e7289c7133b44f8c9c61fdddf2a6be1421d" + integrity sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.2" + +"@radix-ui/react-menu@2.1.20": + version "2.1.20" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.1.20.tgz#67416e26e9f0d64e724b8216fca84e68b369cc92" + integrity sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-collection" "1.1.12" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-direction" "1.1.2" + "@radix-ui/react-dismissable-layer" "1.1.15" + "@radix-ui/react-focus-guards" "1.1.4" + "@radix-ui/react-focus-scope" "1.1.12" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-popper" "1.3.3" + "@radix-ui/react-portal" "1.1.13" + "@radix-ui/react-presence" "1.1.7" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-roving-focus" "1.1.15" + "@radix-ui/react-slot" "1.3.0" + "@radix-ui/react-use-callback-ref" "1.1.2" + aria-hidden "^1.2.4" + react-remove-scroll "^2.7.2" + +"@radix-ui/react-popper@1.3.3": + version "1.3.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.3.3.tgz#3f27f929118d8cb8ffced905d33fa887b9e1432b" + integrity sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g== dependencies: "@floating-ui/react-dom" "^2.0.0" - "@radix-ui/react-arrow" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" - "@radix-ui/react-use-rect" "1.1.0" - "@radix-ui/react-use-size" "1.1.0" - "@radix-ui/rect" "1.1.0" - -"@radix-ui/react-portal@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.0.tgz#7d8591034d85478c172a91b1b879df8cf8b60bf0" - integrity sha512-0tXZ5O6qAVvuN9SWP0X+zadHf9hzHiMf/vxOU+kXO+fbtS8lS57MXa6EmikDxk9s/Bmkk80+dcxgbvisIyeqxg== - dependencies: - "@radix-ui/react-primitive" "2.0.0" + "@radix-ui/react-arrow" "1.1.11" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-callback-ref" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.2" + "@radix-ui/react-use-rect" "1.1.2" + "@radix-ui/react-use-size" "1.1.2" + "@radix-ui/rect" "1.1.2" + +"@radix-ui/react-portal@1.1.13": + version "1.1.13" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.13.tgz#8b80b8b33ef4fff449c6d3ab62492f4dca162c7e" + integrity sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA== + dependencies: + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-layout-effect" "1.1.2" + +"@radix-ui/react-presence@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.7.tgz#42b69b29984fb6431338988615bc8b9767814dad" + integrity sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.2" + +"@radix-ui/react-primitive@2.1.7": + version "2.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.7.tgz#1f487a06434770f865dbfb6c9a55bbefcbad8c82" + integrity sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ== + dependencies: + "@radix-ui/react-slot" "1.3.0" + +"@radix-ui/react-roving-focus@1.1.15": + version "1.1.15" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.15.tgz#fd24daf2b849b201c5e2626e1bf8bcbc57f6a715" + integrity sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-collection" "1.1.12" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-direction" "1.1.2" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-use-callback-ref" "1.1.2" + "@radix-ui/react-use-controllable-state" "1.2.3" + "@radix-ui/react-use-is-hydrated" "0.1.1" + "@radix-ui/react-use-layout-effect" "1.1.2" + +"@radix-ui/react-slot@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.3.0.tgz#e311c7a6c8d65b1af9e69af8e3318c6c7105a212" + integrity sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.3" + +"@radix-ui/react-tooltip@^1.2": + version "1.2.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.2.12.tgz#f38a646eda98036e66fd33fc4725621e2f7aa812" + integrity sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA== + dependencies: + "@radix-ui/primitive" "1.1.5" + "@radix-ui/react-compose-refs" "1.1.3" + "@radix-ui/react-context" "1.2.0" + "@radix-ui/react-dismissable-layer" "1.1.15" + "@radix-ui/react-id" "1.1.2" + "@radix-ui/react-popper" "1.3.3" + "@radix-ui/react-portal" "1.1.13" + "@radix-ui/react-presence" "1.1.7" + "@radix-ui/react-primitive" "2.1.7" + "@radix-ui/react-slot" "1.3.0" + "@radix-ui/react-use-controllable-state" "1.2.3" + "@radix-ui/react-visually-hidden" "1.2.7" + +"@radix-ui/react-use-callback-ref@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.2.tgz#ddc0bc1381ff3b62368c248808efc45a098bafde" + integrity sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw== -"@radix-ui/react-presence@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.0.tgz#227d84d20ca6bfe7da97104b1a8b48a833bfb478" - integrity sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ== +"@radix-ui/react-use-controllable-state@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.3.tgz#516996f6443207546aa15a59bc71cdf5b54e01d1" + integrity sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA== dependencies: - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-use-layout-effect" "1.1.0" + "@radix-ui/react-use-effect-event" "0.0.3" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-primitive@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884" - integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw== +"@radix-ui/react-use-effect-event@0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.3.tgz#e8f45e8e6ef64ce5bea7b5a9effc373f067e3530" + integrity sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA== dependencies: - "@radix-ui/react-slot" "1.1.0" + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-roving-focus@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e" - integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-collection" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-direction" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-use-callback-ref" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - -"@radix-ui/react-slot@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" - integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== - dependencies: - "@radix-ui/react-compose-refs" "1.1.0" +"@radix-ui/react-use-is-hydrated@0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.1.tgz#61a18cb03430a6d2e704eb2afd3067505ed0f292" + integrity sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A== -"@radix-ui/react-tooltip@^1.0.6": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.0.tgz#861c268b9c73472a32a312dbc0135ea581262ca6" - integrity sha512-DZZvEn5WUJyd9+JzVT/cTjt7m0rymjxpzJZMmb09lCWo8kRqOp4rsckFrGgocD5cR8e3gtaNINvWWqFMccvV/w== - dependencies: - "@radix-ui/primitive" "1.1.0" - "@radix-ui/react-compose-refs" "1.1.0" - "@radix-ui/react-context" "1.1.0" - "@radix-ui/react-dismissable-layer" "1.1.0" - "@radix-ui/react-id" "1.1.0" - "@radix-ui/react-popper" "1.2.0" - "@radix-ui/react-portal" "1.1.0" - "@radix-ui/react-presence" "1.1.0" - "@radix-ui/react-primitive" "2.0.0" - "@radix-ui/react-slot" "1.1.0" - "@radix-ui/react-use-controllable-state" "1.1.0" - "@radix-ui/react-visually-hidden" "1.1.0" - -"@radix-ui/react-use-callback-ref@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" - integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== +"@radix-ui/react-use-layout-effect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.2.tgz#c882e66497174d061f250e65251974b699c65b65" + integrity sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA== -"@radix-ui/react-use-controllable-state@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0" - integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw== +"@radix-ui/react-use-rect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.2.tgz#83b9de1ea8f6abd1425eb79f2930e00047cb8d19" + integrity sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw== dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" + "@radix-ui/rect" "1.1.2" -"@radix-ui/react-use-escape-keydown@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754" - integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw== +"@radix-ui/react-use-size@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.2.tgz#33eb275755424d7dda33ffa32c23ea85ca23be40" + integrity sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w== dependencies: - "@radix-ui/react-use-callback-ref" "1.1.0" - -"@radix-ui/react-use-layout-effect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" - integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== + "@radix-ui/react-use-layout-effect" "1.1.2" -"@radix-ui/react-use-rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz#13b25b913bd3e3987cc9b073a1a164bb1cf47b88" - integrity sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ== +"@radix-ui/react-visually-hidden@1.2.7", "@radix-ui/react-visually-hidden@^1.2": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.7.tgz#64fc5994ebd39b3b19f4e93c11da22bf03af684b" + integrity sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw== dependencies: - "@radix-ui/rect" "1.1.0" + "@radix-ui/react-primitive" "2.1.7" -"@radix-ui/react-use-size@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz#b4dba7fbd3882ee09e8d2a44a3eed3a7e555246b" - integrity sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw== - dependencies: - "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/rect@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.2.tgz#0761a82af55c7e302d5b509eaf1c97ea1fc5feea" + integrity sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA== -"@radix-ui/react-visually-hidden@1.1.0", "@radix-ui/react-visually-hidden@^1.0.3": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz#ad47a8572580f7034b3807c8e6740cd41038a5a2" - integrity sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ== +"@react-aria/focus@^3.20.2": + version "3.22.1" + resolved "https://registry.yarnpkg.com/@react-aria/focus/-/focus-3.22.1.tgz#7e7ba6a2250135728eb68ffa21e422ec25a16800" + integrity sha512-CPxtkyrBi/HYY5P3lE/57sQ6qfa0lN8E55TOm89H0kNGv0lKt+/0zP7lWERzBjRr5IxBVrQX4gFEowBN52LPaA== dependencies: - "@radix-ui/react-primitive" "2.0.0" - -"@radix-ui/rect@1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.1.0.tgz#f817d1d3265ac5415dadc67edab30ae196696438" - integrity sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg== + "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@tanstack/react-virtual@^3.0.0-beta.60": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.5.1.tgz#1ce466f530a10f781871360ed2bf7ff83e664f85" - integrity sha512-jIsuhfgy8GqA67PdWqg73ZB2LFE+HD9hjWL1L6ifEIZVyZVAKpYmgUG4WsKQ005aEyImJmbuimPiEvc57IY0Aw== +"@react-aria/interactions@^3.25.0": + version "3.28.1" + resolved "https://registry.yarnpkg.com/@react-aria/interactions/-/interactions-3.28.1.tgz#852294ae63f6a7d8aeeba3a1e7343daa8cd8507c" + integrity sha512-Bqb+HrD5I5MHS2SKBhISYqo2SW8Y2dfzgF/Y1lIJq7xqLxheo9vzxPGEHhz+XzkgGfoqEJx8A6a3C7uiqS3HWA== dependencies: - "@tanstack/virtual-core" "3.5.1" + "@react-types/shared" "^3.34.0" + "@swc/helpers" "^0.5.0" + react-aria "^3.48.0" -"@tanstack/virtual-core@3.5.1": - version "3.5.1" - resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.5.1.tgz#f519149bce9156d0e7954b9531df15f446f2fc12" - integrity sha512-046+AUSiDru/V9pajE1du8WayvBKeCvJ2NmKPy/mR8/SbKKrqmSbj7LJBfXE+nSq4f5TBXvnCzu0kcYebI9WdQ== +"@react-types/shared@^3.34.0", "@react-types/shared@^3.36.0": + version "3.36.0" + resolved "https://registry.yarnpkg.com/@react-types/shared/-/shared-3.36.0.tgz#52e713c6bae8e117967bf1d19d89db3a56219037" + integrity sha512-DkP/H0C2YjjS7gZWKNqOmU8a16qHPjQNdzMwmTq9SzplM6Iw0kVMTZ0OIoe6FOgGqa+FwMsE2QbPjh/n3g/jXQ== -"@types/codemirror@^0.0.90": - version "0.0.90" - resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-0.0.90.tgz#9c5edafce2a780b4f8bc5e3b699fe1f4727c8f17" - integrity sha512-8Z9+tSg27NPRGubbUPUCrt5DDG/OWzLph5BvcDykwR5D7RyZh5mhHG0uS1ePKV1YFCA+/cwc4Ey2AJAEFfV3IA== +"@swc/helpers@^0.5.0": + version "0.5.23" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.23.tgz#19287d0d86d962b111376039a50c792902c9a86a" + integrity sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== dependencies: - "@types/tern" "*" + tslib "^2.8.0" -"@types/codemirror@^5.60.8": - version "5.60.15" - resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.15.tgz#0f82be6f4126d1e59cf4c4830e56dcd49d3c3e8a" - integrity sha512-dTOvwEQ+ouKJ/rE9LT1Ue2hmP6H1mZv5+CCnNWu2qtiOe2LQa9lCprEY20HxiDmV/Bxh+dXjywmy5aKvoGjULA== +"@tanstack/react-virtual@^3.13.9": + version "3.14.5" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.14.5.tgz#183c959aeb85448899dcb1a4da55213e5d4f6078" + integrity sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A== dependencies: - "@types/tern" "*" + "@tanstack/virtual-core" "3.17.3" + +"@tanstack/virtual-core@3.17.3": + version "3.17.3" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.17.3.tgz#71ae7e658fe155392a2dcbf640035febdf2fdb05" + integrity sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw== "@types/eslint-scope@^3.7.3": version "3.7.7" @@ -1391,13 +1437,6 @@ dependencies: undici-types "~5.26.4" -"@types/tern@*": - version "0.23.9" - resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.9.tgz#6f6093a4a9af3e6bb8dde528e024924d196b367c" - integrity sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw== - dependencies: - "@types/estree" "*" - "@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": version "1.12.1" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" @@ -1554,6 +1593,11 @@ acorn@^8.7.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.0.tgz#1627bfa2e058148036133b8d9b51a700663c294c" integrity sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw== +acorn@^8.8.2: + version "8.17.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.17.0.tgz#1785adb84faf8d8add10369b93826fc2bd08f1fe" + integrity sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg== + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -1610,10 +1654,10 @@ argparse@^2.0.1: resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.1: - version "1.2.4" - resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" - integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== +aria-hidden@^1.2.3, aria-hidden@^1.2.4: + version "1.2.6" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.6.tgz#73051c9b088114c795b1ea414e9c0fff874ffc1a" + integrity sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA== dependencies: tslib "^2.0.0" @@ -1723,11 +1767,6 @@ chrome-trace-event@^1.0.2: dependencies: tslib "^1.9.0" -client-only@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" - integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== - clone-deep@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" @@ -1742,18 +1781,10 @@ clsx@^1.2.1: resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12" integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg== -codemirror-graphql@^2.0.12: - version "2.0.12" - resolved "https://registry.yarnpkg.com/codemirror-graphql/-/codemirror-graphql-2.0.12.tgz#75492b41f271a64eb207c923a4a536c3134d05c9" - integrity sha512-5UCqhWzck1jClCmRewFb8aSiabnAqiaRfsvIPfmbf6WJvOb8oiefJeHilclPPiZBzY8v/Et6EBMtOeKnWCoyng== - dependencies: - "@types/codemirror" "^0.0.90" - graphql-language-service "5.2.1" - -codemirror@^5.65.3: - version "5.65.16" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.16.tgz#efc0661be6bf4988a6a1c2fe6893294638cdb334" - integrity sha512-br21LjYmSlVL0vFCPWPfhzUCT34FM/pAdK7rRIZwa0rrtrIdotvP4Oh4GUHsu2E3IrQMCfRkL/fN3ytMNxVQvg== +clsx@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== color-convert@^1.9.0: version "1.9.3" @@ -1799,13 +1830,6 @@ copy-anything@^2.0.1: dependencies: is-what "^3.14.1" -copy-to-clipboard@^3.2.0: - version "3.3.1" - resolved "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.1.tgz#115aa1a9998ffab6196f93076ad6da3b913662ae" - integrity sha512-i13qo6kIHTTpCm8/Wup+0b1mVWETvu2kIMzKoK8FpkLkFxlt0znUAHcMzox+T8sPlqtZXq3CulEjQHsYiGFJUw== - dependencies: - toggle-selection "^1.0.6" - core-js-compat@^3.48.0: version "3.49.0" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.49.0.tgz#06145447d92f4aaf258a0c44f24b47afaeaffef6" @@ -1856,6 +1880,11 @@ cssesc@^3.0.0: resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== +debounce-promise@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/debounce-promise/-/debounce-promise-3.1.2.tgz#320fb8c7d15a344455cd33cee5ab63530b6dc7c5" + integrity sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg== + debug@^4.1.0: version "4.3.1" resolved "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee" @@ -2027,26 +2056,14 @@ flat@^5.0.2: resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== -framer-motion@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7" - integrity sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw== +framer-motion@^12.12: + version "12.42.2" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.42.2.tgz#8628ad31a9b5c1ea6f908ea1764784e33870b711" + integrity sha512-5XY9luDiu0oHfHBjpDthFMh0ES+122w6p/papSJBweMkO8Sn+PW2QaEgRblQBpWFnuvZS5qvarpt/hO2pjGmnw== dependencies: - "@motionone/dom" "10.12.0" - framesync "6.0.1" - hey-listen "^1.0.8" - popmotion "11.0.3" - style-value-types "5.0.0" - tslib "^2.1.0" - optionalDependencies: - "@emotion/is-prop-valid" "^0.8.2" - -framesync@6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/framesync/-/framesync-6.0.1.tgz#5e32fc01f1c42b39c654c35b16440e07a25d6f20" - integrity sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA== - dependencies: - tslib "^2.1.0" + motion-dom "^12.42.2" + motion-utils "^12.39.0" + tslib "^2.4.0" function-bind@^1.1.2: version "1.1.2" @@ -2063,6 +2080,13 @@ get-nonce@^1.0.0: resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== +get-value@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/get-value/-/get-value-3.0.1.tgz#5efd2a157f1d6a516d7524e124ac52d0a39ef5a8" + integrity sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA== + dependencies: + isobject "^3.0.1" + glob-to-regexp@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" @@ -2078,33 +2102,34 @@ graceful-fs@^4.2.11, graceful-fs@^4.2.4: resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphiql@^3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-3.3.1.tgz#60fc405c56492792c60d107c1f191aa6cf244034" - integrity sha512-UA29FQ418Pcxat54CvM//S5G+7DKG7XQ7s9UyAEdb7zMAKPANIDd222XEYNxG2I/FgAxsiq3ZTBpxwvPbB9Mcw== +graphiql@^5.2.0: + version "5.2.4" + resolved "https://registry.yarnpkg.com/graphiql/-/graphiql-5.2.4.tgz#099df61748e66add42fe9a1ee174250bc42777f5" + integrity sha512-UfxiFYWM3BYfydi/ljfFvVim2pShR5Law+23BFokvgJ3F7zEqRak8gTQdw5EBFKYZynR1DsvDrjj6QuGP/ByWQ== dependencies: - "@graphiql/react" "^0.22.3" - "@graphiql/toolkit" "^0.9.1" - graphql-language-service "^5.2.1" - markdown-it "^14.1.0" + "@graphiql/plugin-doc-explorer" "^0.4.2" + "@graphiql/plugin-history" "^0.4.2" + "@graphiql/react" "^0.37.7" + react-compiler-runtime "19.1.0-rc.1" -graphql-language-service@5.2.1, graphql-language-service@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/graphql-language-service/-/graphql-language-service-5.2.1.tgz#8929cf93deadecc0fbb78fe8a0c55d178ffa7833" - integrity sha512-8ewD6otGO43vg2TiEGjoLz3CweTwfaf4ZnqfNREqZXS2JSJGXtsRBOMMknCxMfFVh4x14ql3jyDrXcyAAtbmkQ== +graphql-language-service@^5.5.1, graphql-language-service@^5.5.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/graphql-language-service/-/graphql-language-service-5.5.2.tgz#264a5a73ec4b779ee848f1b2515c6392094664dd" + integrity sha512-NJhgEKTArkyNPcy4NRUFdbpNs5/F99LcvXbNtmGzNGwwruN8tBE3YPMjpYmp8KpBQtOx3uSuvXJlOOE3Vy2KRQ== dependencies: + debounce-promise "^3.1.2" nullthrows "^1.0.0" vscode-languageserver-types "^3.17.1" -graphql-ws@^5.16.0: - version "5.16.0" - resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.0.tgz#849efe02f384b4332109329be01d74c345842729" - integrity sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A== +graphql-ws@^5.16.2: + version "5.16.2" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-5.16.2.tgz#7b0306c1bdb0e97a05e800ccd523f46fb212e37c" + integrity sha512-E1uccsZxt/96jH/OwmLPuXMACILs76pKF2i3W861LpKBCYtGIyPQGtWLuBLkND4ox1KHns70e83PS4te50nvPQ== -graphql@^16.8.2: - version "16.8.2" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.2.tgz#54771c7ff195da913f5e70af8044a026d32eca2a" - integrity sha512-cvVIBILwuoSyD54U4cF/UXDh5yAobhNV/tPygI4lZhgOIJQE/WLWC4waBRb4I6bDVYb3OVx3lfHbaQOEoUD5sg== +graphql@^16.14.0: + version "16.14.2" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.14.2.tgz#83faf25869e3df727cc855161db5da85b0e5b2c0" + integrity sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA== has-flag@^3.0.0: version "3.0.0" @@ -2130,11 +2155,6 @@ hasown@^2.0.3: dependencies: function-bind "^1.1.2" -hey-listen@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" - integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== - iconv-lite@^0.6.3: version "0.6.3" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" @@ -2180,13 +2200,6 @@ interpret@^3.1.1: resolved "https://registry.yarnpkg.com/interpret/-/interpret-3.1.1.tgz#5be0ceed67ca79c6c4bc5cf0d7ee843dcea110c4" integrity sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ== -invariant@^2.2.4: - version "2.2.4" - resolved "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" - integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== - dependencies: - loose-envify "^1.0.0" - is-arrayish@^0.2.1: version "0.2.1" resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" @@ -2298,6 +2311,11 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +jsonc-parser@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.3.1.tgz#f2a524b4f7fd11e3d791e559977ad60b98b798b4" + integrity sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ== + kind-of@^6.0.2: version "6.0.3" resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" @@ -2377,7 +2395,7 @@ lodash@^4.17.19: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.1.0: version "1.4.0" resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -2463,6 +2481,31 @@ minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +monaco-editor@0.52.2: + version "0.52.2" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.52.2.tgz#53c75a6fcc6802684e99fd1b2700299857002205" + integrity sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ== + +monaco-graphql@^1.8.0: + version "1.8.0" + resolved "https://registry.yarnpkg.com/monaco-graphql/-/monaco-graphql-1.8.0.tgz#0d0f515ab9766dc9ac2749f9de0e21f9b3eaa783" + integrity sha512-rWvWUpJdtpTu6YF2qgeaR2HnGPFthUJKSposB38f5wtBKwHlISYZHZLD/LukoMWDEyegNLOF/1bPMRs0SZrNzA== + dependencies: + graphql-language-service "^5.5.1" + picomatch-browser "^2.2.6" + +motion-dom@^12.42.2: + version "12.42.2" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.42.2.tgz#b4661b9b3394ae7e990d76dc954bc1e321c59305" + integrity sha512-5gIMWLp/PycBtJRJWRgjxke5n8dlvkSn2DrYW+tr3XcqAZY1xZh6BJyooJXCM8wdfM7wfMjkBJNLge1CKPUIRA== + dependencies: + motion-utils "^12.39.0" + +motion-utils@^12.39.0: + version "12.39.0" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.39.0.tgz#e1c66f0e912999804bc5e69b4630c3bc794ef29f" + integrity sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ== + ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2473,10 +2516,10 @@ ms@^2.1.3: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== -nanoid@^3.3.7: - version "3.3.7" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" - integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== +nanoid@^3.3.12: + version "3.3.15" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.15.tgz#36c490fad8c6e86c824c940dfdde999b69ed4316" + integrity sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA== needle@^3.1.0: version "3.3.1" @@ -2587,6 +2630,11 @@ picocolors@^1.1.1: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== +picomatch-browser@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/picomatch-browser/-/picomatch-browser-2.2.6.tgz#e0626204575eb49f019f2f2feac24fc3b53e7a8a" + integrity sha512-0ypsOQt9D4e3hziV8O4elD9uN0z/jtUEfxVRtNaAAtXIyUx9m/SzlO020i8YNL2aL/E6blOvvHQcin6HZlFy/w== + pify@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz#4b2cd25c50d598735c50292224fd8c6df41e3231" @@ -2599,16 +2647,6 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -popmotion@11.0.3: - version "11.0.3" - resolved "https://registry.yarnpkg.com/popmotion/-/popmotion-11.0.3.tgz#565c5f6590bbcddab7a33a074bb2ba97e24b0cc9" - integrity sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA== - dependencies: - framesync "6.0.1" - hey-listen "^1.0.8" - style-value-types "5.0.0" - tslib "^2.1.0" - postcss-loader@^7.3.3: version "7.3.4" resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-7.3.4.tgz#aed9b79ce4ed7e9e89e56199d25ad1ec8f606209" @@ -2674,14 +2712,19 @@ postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: picocolors "^0.2.1" source-map "^0.6.1" -postcss@^8.4.28: - version "8.4.38" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" - integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== +postcss@^8.5.10: + version "8.5.16" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" + integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +prettier@^3.5.3: + version "3.9.4" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.4.tgz#a9c477cf1614376bd1f6bbc593d8c0d414bcec87" + integrity sha512-yWG/o/4oJfo036EKAfK6ACAoDOfHeRHx4tuxkfBZiauURiaSmYwlpOr5LQqKtIkRD2z1PLteme2WoxEnj4tHTg== prr@~1.0.1: version "1.0.1" @@ -2705,6 +2748,26 @@ randombytes@^2.1.0: dependencies: safe-buffer "^5.1.0" +react-aria@^3.48.0: + version "3.50.0" + resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.50.0.tgz#75cb002c8ff94be3bc1afb6f717b37c6d0548119" + integrity sha512-S0Os6QZk33fzUAKu1QLT9afoUaCBt1ZNdoiq0n2YMVgKIdNIQS8zxiZ8O9hYE6QyDkHKjD6q39LQZ+qaSAIgjw== + dependencies: + "@internationalized/date" "^3.12.2" + "@internationalized/number" "^3.6.7" + "@internationalized/string" "^3.2.9" + "@react-types/shared" "^3.36.0" + "@swc/helpers" "^0.5.0" + aria-hidden "^1.2.3" + clsx "^2.0.0" + react-stately "3.48.0" + use-sync-external-store "^1.6.0" + +react-compiler-runtime@19.1.0-rc.1: + version "19.1.0-rc.1" + resolved "https://registry.yarnpkg.com/react-compiler-runtime/-/react-compiler-runtime-19.1.0-rc.1.tgz#2535efd2e9fc9fc7d5ad47e970061dfbe38fb3f6" + integrity sha512-wCt6g+cRh8g32QT18/9blfQHywGjYu+4FlEc3CW1mx3pPxYzZZl1y+VtqxRgnKKBCFLIGUYxog4j4rs5YS86hw== + react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -2713,32 +2776,43 @@ react-dom@^18.3.1: loose-envify "^1.1.0" scheduler "^0.23.2" -react-remove-scroll-bar@^2.3.4: - version "2.3.6" - resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" - integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== +react-remove-scroll-bar@^2.3.7: + version "2.3.8" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz#99c20f908ee467b385b68a3469b4a3e750012223" + integrity sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q== dependencies: - react-style-singleton "^2.2.1" + react-style-singleton "^2.2.2" tslib "^2.0.0" -react-remove-scroll@2.5.7: - version "2.5.7" - resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz#15a1fd038e8497f65a695bf26a4a57970cac1ccb" - integrity sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA== +react-remove-scroll@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz#6442da56791117661978ae99cd29be9026fecca0" + integrity sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q== dependencies: - react-remove-scroll-bar "^2.3.4" - react-style-singleton "^2.2.1" + react-remove-scroll-bar "^2.3.7" + react-style-singleton "^2.2.3" tslib "^2.1.0" - use-callback-ref "^1.3.0" - use-sidecar "^1.1.2" - -react-style-singleton@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" - integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + use-callback-ref "^1.3.3" + use-sidecar "^1.1.3" + +react-stately@3.48.0: + version "3.48.0" + resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.48.0.tgz#80b658d91a20b35f9803102302268a477ba819e4" + integrity sha512-ImicSAG+lTotAe5izcs1fz49Zk48w7pDusqYg04WaPhCoej8BJ24soMu3iLXIrsi273s4P1gZrYGrqReMfgEEA== + dependencies: + "@internationalized/date" "^3.12.2" + "@internationalized/number" "^3.6.7" + "@internationalized/string" "^3.2.9" + "@react-types/shared" "^3.36.0" + "@swc/helpers" "^0.5.0" + use-sync-external-store "^1.6.0" + +react-style-singleton@^2.2.2, react-style-singleton@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.3.tgz#4265608be69a4d70cfe3047f2c6c88b2c3ace388" + integrity sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ== dependencies: get-nonce "^1.0.0" - invariant "^2.2.4" tslib "^2.0.0" react@^18.3.1: @@ -2940,11 +3014,16 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== -source-map-js@^1.0.2, source-map-js@^1.2.0: +source-map-js@^1.0.2: version "1.2.0" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== +source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + source-map-loader@^4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-4.0.2.tgz#1b378721b65adb21e874928a9fb22e8a340d06a5" @@ -2953,15 +3032,15 @@ source-map-loader@^4.0.1: iconv-lite "^0.6.3" source-map-js "^1.0.2" -source-map-support@~0.5.12: - version "0.5.19" - resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" - integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0, source-map@~0.6.1: +source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== @@ -2974,14 +3053,6 @@ style-loader@^1.2.1: loader-utils "^2.0.0" schema-utils "^2.7.0" -style-value-types@5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/style-value-types/-/style-value-types-5.0.0.tgz#76c35f0e579843d523187989da866729411fc8ad" - integrity sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA== - dependencies: - hey-listen "^1.0.8" - tslib "^2.1.0" - supports-color@^5.3.0: version "5.5.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" @@ -3001,6 +3072,11 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +tabbable@^6.0.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.5.0.tgz#a65101385a4fd6cbd580b7546da0170f307b535d" + integrity sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA== + tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -3017,35 +3093,36 @@ terser-webpack-plugin@^5.3.10: serialize-javascript "^6.0.1" terser "^5.26.0" -terser@4.8.1, terser@^5.26.0: - version "4.8.1" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.8.1.tgz#a00e5634562de2239fd404c649051bf6fc21144f" - integrity sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw== +terser@5.39.0, terser@^5.26.0: + version "5.39.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.0.tgz#0e82033ed57b3ddf1f96708d123cca717d86ca3a" + integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw== dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" commander "^2.20.0" - source-map "~0.6.1" - source-map-support "~0.5.12" + source-map-support "~0.5.20" to-fast-properties@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" integrity sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4= -toggle-selection@^1.0.6: - version "1.0.6" - resolved "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32" - integrity sha1-bkWxJj8gF/oKzH2J14sVuL932jI= - tslib@^1.9.0: version "1.14.1" resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.3.1: +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.3.0: version "2.6.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.3.tgz#0438f810ad7a9edcde7a241c3d80db693c8cbfe0" integrity sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ== +tslib@^2.4.0, tslib@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + uc.micro@^2.0.0, uc.micro@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.1.0.tgz#f8d3f7d0ec4c3dea35a7e3c8efa4cb8b45c9e7ee" @@ -3107,21 +3184,26 @@ uri-js@^4.2.2, uri-js@^4.4.1: dependencies: punycode "^2.1.0" -use-callback-ref@^1.3.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" - integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== +use-callback-ref@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.3.tgz#98d9fab067075841c5b2c6852090d5d0feabe2bf" + integrity sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg== dependencies: tslib "^2.0.0" -use-sidecar@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" - integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== +use-sidecar@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.3.tgz#10e7fd897d130b896e2c546c63a5e8233d00efdb" + integrity sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ== dependencies: detect-node-es "^1.1.0" tslib "^2.0.0" +use-sync-external-store@^1.5.0, use-sync-external-store@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz#b174bfa65cb2b526732d9f2ac0a408027876f32d" + integrity sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" @@ -3219,3 +3301,8 @@ yallist@^3.0.2: version "3.1.1" resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +zustand@^5: + version "5.0.14" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.14.tgz#18216c24fcb980cf36898f9c57520e67b1f77855" + integrity sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g== diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index 4fa624da5..f2193092f 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -23,6 +23,8 @@ import org.apache.camel.Route; import org.apache.camel.ServiceStatus; import org.apache.commons.io.IOUtils; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.HttpEntity; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.*; @@ -65,6 +67,7 @@ import org.apache.unomi.router.api.services.ImportExportConfigurationService; import org.apache.unomi.schema.api.SchemaService; import org.apache.unomi.services.UserListService; +import org.apache.unomi.shell.migration.utils.HttpUtils; import org.apache.unomi.shell.services.UnomiManagementService; import org.junit.After; import org.junit.Assert; @@ -144,6 +147,9 @@ public abstract class BaseIT extends KarafTestSupport { protected static boolean unomiStarted = false; protected static String searchEngine = SEARCH_ENGINE_ELASTICSEARCH; + private static boolean searchEngineConfiguredForTesting = false; + private static boolean searchEngineHealthVerifiedAfterStartup = false; + /** * JSON mapper for IT HTTP/JSON helpers. Initialized on first use (after Unomi features are up), * delegating to the same mapper as the running server. @@ -227,6 +233,7 @@ protected TestUtils.RequestResponse executeContextJSONRequest(org.apache.http.cl protected void checkSearchEngine() { searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); + configureSearchEngineForTesting(); } @Before @@ -321,6 +328,11 @@ public void waitForStartup() throws InterruptedException { // init httpClient without credentials provider - all auth handled via headers httpClient = initHttpClient(null); + if (!searchEngineHealthVerifiedAfterStartup) { + assertClusterHealthy("after Unomi startup"); + searchEngineHealthVerifiedAfterStartup = true; + } + // Initialize log checker if enabled if (isLogCheckingEnabled()) { // Use builder API - by default enable all patterns for backward compatibility @@ -634,6 +646,7 @@ public Option[] config() { editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.addresses", "localhost:" + getSearchPort()), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.taskWaitingPollingInterval", "50"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.rollover.maxDocs", "300"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.elasticsearch.minimalClusterState", "YELLOW"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.cluster.name", "contextElasticSearchITests"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.addresses", "localhost:" + getSearchPort()), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.username", "admin"), @@ -1372,6 +1385,97 @@ public static void closeResponse(CloseableHttpResponse response) { } } + + /** + * Configures the IT search engine container for single-node testing (UNOMI-946). + * Enforces zero replicas cluster-wide so indices stay GREEN on one node. + */ + protected void configureSearchEngineForTesting() { + if (searchEngineConfiguredForTesting) { + return; + } + searchEngineConfiguredForTesting = true; + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String baseUrl = getSearchEngineBaseUrl(); + String settingsBody = "{\"persistent\": {\"index.number_of_replicas\": \"0\"}}"; + HttpUtils.executePutRequest(client, baseUrl + "/_cluster/settings", settingsBody, null); + String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health", null); + LOGGER.info("Search engine baseline cluster health ({}): {}", searchEngine, health); + System.out.println("==== Search engine baseline cluster health (" + searchEngine + "): " + health); + } catch (Exception e) { + throw new IllegalStateException("Failed to configure search engine for IT testing", e); + } + } + + /** + * Zeros replica count on all existing indices (migration snapshot restore path). + */ + protected void fixRestoredIndexReplicas() { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String baseUrl = getSearchEngineBaseUrl(); + String settingsBody = "{\"index\": {\"number_of_replicas\": \"0\"}}"; + HttpUtils.executePutRequest(client, baseUrl + "/_all/_settings", settingsBody, null); + String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health?wait_for_status=green&timeout=30s", null); + LOGGER.info("Cluster health after fixing restored index replicas: {}", health); + if (health != null && health.contains("\"status\":\"red\"")) { + throw new IllegalStateException("Cluster still RED after fixing restored index replicas: " + health); + } + } catch (Exception e) { + throw new IllegalStateException("Failed to fix restored index replicas", e); + } + } + + /** + * Asserts the search cluster is healthy for IT: no index with replicas > 0, cluster GREEN. + */ + protected void assertClusterHealthy(String context) { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String baseUrl = getSearchEngineBaseUrl(); + String indicesJson = HttpUtils.executeGetRequest(client, baseUrl + "/_cat/indices?h=index,rep,health&format=json", null); + if (indicesJson != null && !indicesJson.isBlank()) { + JsonNode indices = getObjectMapper().readTree(indicesJson); + if (indices.isArray()) { + List violations = new ArrayList<>(); + for (JsonNode index : indices) { + String rep = index.path("rep").asText(""); + if (!rep.isEmpty() && !"0".equals(rep)) { + violations.add(index.path("index").asText("?") + " rep=" + rep); + } + } + if (!violations.isEmpty()) { + throw new IllegalStateException(context + ": indices with replicas > 0: " + String.join(", ", violations)); + } + } + } + String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health", null); + LOGGER.info("Cluster health ({}): {}", context, health); + if (health == null || (!health.contains("\"status\":\"green\"") && !health.contains("\"status\":\"yellow\""))) { + throw new IllegalStateException(context + ": cluster not green/yellow: " + health); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed cluster health assertion: " + context, e); + } + } + + protected static String getSearchEngineBaseUrl() { + if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { + return "http://localhost:" + getSearchPort(); + } + return "http://localhost:" + getSearchPort(); + } + + protected CloseableHttpClient createSearchEngineHttpClient() throws IOException { + if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { + BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(AuthScope.ANY, + new UsernamePasswordCredentials("admin", "Unomi.1ntegrat10n.Tests")); + return HttpUtils.initHttpClient(true, credentialsProvider); + } + return HttpUtils.initHttpClient(true, null); + } + /** * Gets the appropriate search engine port based on the configured search engine. * diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java index 402c3f549..c09b13df4 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java @@ -134,6 +134,8 @@ public void waitForStartup() throws InterruptedException { System.out.println("Snapshot status: " + snapshotStatus); LOGGER.info("Snapshot status: {}", snapshotStatus); + fixRestoredIndexReplicas(); + // Get initial counts of items to compare after migration initCounts(httpClient); } catch (Throwable t) { diff --git a/kar/src/main/feature/feature.xml b/kar/src/main/feature/feature.xml index 9165caf52..dfecb485e 100644 --- a/kar/src/main/feature/feature.xml +++ b/kar/src/main/feature/feature.xml @@ -238,7 +238,7 @@ - mvn:org.webjars/swagger-ui/3.23.8 + mvn:org.webjars/swagger-ui/${swagger-ui.version} diff --git a/pom.xml b/pom.xml index abc524a7c..2658f5bf2 100644 --- a/pom.xml +++ b/pom.xml @@ -127,7 +127,7 @@ 1.0 9.4.58.v20250814 2.0.0 - 2.1.3 + 2.50.0 1.1 1.14 3.2.0 diff --git a/services/pom.xml b/services/pom.xml index eb800cbda..1299d0542 100644 --- a/services/pom.xml +++ b/services/pom.xml @@ -166,7 +166,7 @@ ch.qos.logback logback-classic - 1.2.11 + 1.2.13 test From ffe5edb7ac7c23d938bf2880d84079c2850316ed Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 9 Jul 2026 18:01:50 +0200 Subject: [PATCH 2/9] Pin graphql-ui transitive deps via yarn resolutions Absorb Dependabot #804-808 security bumps (lodash, js-yaml, markdown-it, linkify-it, postcss, serialize-javascript) that the GraphiQL 5 lock refresh did not pick up automatically. --- graphql/graphql-ui/package.json | 8 +++- graphql/graphql-ui/yarn.lock | 77 +++++++++++---------------------- 2 files changed, 32 insertions(+), 53 deletions(-) diff --git a/graphql/graphql-ui/package.json b/graphql/graphql-ui/package.json index 1baaea4e6..1178ce36a 100644 --- a/graphql/graphql-ui/package.json +++ b/graphql/graphql-ui/package.json @@ -24,7 +24,13 @@ "resolutions": { "**/terser": "5.39.0", "**/ansi-regex": "4.1.1", - "**/minimist": "1.2.6" + "**/minimist": "1.2.6", + "**/lodash": "4.18.1", + "**/js-yaml": "4.3.0", + "**/linkify-it": "5.0.2", + "**/markdown-it": "14.3.0", + "**/serialize-javascript": "7.0.5", + "**/postcss": "8.5.16" }, "devDependencies": { "@babel/core": "^7.24.0", diff --git a/graphql/graphql-ui/yarn.lock b/graphql/graphql-ui/yarn.lock index cd2ac5c3f..2a6582f5e 100644 --- a/graphql/graphql-ui/yarn.lock +++ b/graphql/graphql-ui/yarn.lock @@ -1927,7 +1927,7 @@ enhanced-resolve@^5.17.0: graceful-fs "^4.2.4" tapable "^2.2.0" -entities@^4.4.0: +entities@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -2265,10 +2265,10 @@ jiti@^1.20.0: resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== +js-yaml@4.3.0, js-yaml@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.3.0.tgz#d1900572a7f7cf0b5f540c83673e60bad3436592" + integrity sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q== dependencies: argparse "^2.0.1" @@ -2348,10 +2348,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -linkify-it@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421" - integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ== +linkify-it@5.0.2, linkify-it@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.2.tgz#d3be0a693af3da9df3883f1e346a0e97461a8c19" + integrity sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q== dependencies: uc.micro "^2.0.0" @@ -2390,10 +2390,10 @@ lodash.debounce@^4.0.8: resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== -lodash@^4.17.19: - version "4.17.20" - resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" - integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== +lodash@4.18.1, lodash@^4.17.19: + version "4.18.1" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c" + integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q== loose-envify@^1.1.0: version "1.4.0" @@ -2424,14 +2424,14 @@ make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" -markdown-it@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.1.0.tgz#3c3c5992883c633db4714ccb4d7b5935d98b7d45" - integrity sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg== +markdown-it@14.3.0, markdown-it@^14.1.0: + version "14.3.0" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.3.0.tgz#8542fa5506e3530f7e2b08dc3885630135c5620e" + integrity sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw== dependencies: argparse "^2.0.1" - entities "^4.4.0" - linkify-it "^5.0.0" + entities "^4.5.0" + linkify-it "^5.0.2" mdurl "^2.0.0" punycode.js "^2.3.1" uc.micro "^2.1.0" @@ -2615,11 +2615,6 @@ path-type@^4.0.0: resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== -picocolors@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-0.2.1.tgz#570670f793646851d1ba135996962abad587859f" - integrity sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA== - picocolors@^1.0.0, picocolors@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" @@ -2704,15 +2699,7 @@ postcss-value-parser@^4.1.0: resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== -postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.39" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.39.tgz#9624375d965630e2e1f2c02a935c82a59cb48309" - integrity sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA== - dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" - -postcss@^8.5.10: +postcss@8.5.16, postcss@^7.0.14, postcss@^7.0.32, postcss@^7.0.5, postcss@^7.0.6, postcss@^8.5.10: version "8.5.16" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.16.tgz#1230ce0b5df354c24c0ea45f99ce5f6a88279d28" integrity sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg== @@ -2741,13 +2728,6 @@ punycode@^2.1.0: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - react-aria@^3.48.0: version "3.50.0" resolved "https://registry.yarnpkg.com/react-aria/-/react-aria-3.50.0.tgz#75cb002c8ff94be3bc1afb6f717b37c6d0548119" @@ -2906,11 +2886,6 @@ resolve@^1.22.11: path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" -safe-buffer@^5.1.0: - version "5.2.1" - resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - "safer-buffer@>= 2.1.2 < 3.0.0": version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -2980,12 +2955,10 @@ semver@^7.5.4: resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== -serialize-javascript@^6.0.1: - version "6.0.2" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" - integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== - dependencies: - randombytes "^2.1.0" +serialize-javascript@7.0.5, serialize-javascript@^6.0.1: + version "7.0.5" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-7.0.5.tgz#c798cc0552ffbb08981914a42a8756e339d0d5b1" + integrity sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw== set-value@^4.1.0: version "4.1.0" @@ -3040,7 +3013,7 @@ source-map-support@~0.5.20: buffer-from "^1.0.0" source-map "^0.6.0" -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.0: +source-map@^0.6.0, source-map@~0.6.0: version "0.6.1" resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== From 38a2e1ec8102530e091d8ee7cf50635a11416d37 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 9 Jul 2026 18:12:48 +0200 Subject: [PATCH 3/9] Fix IT search engine replica setup for ES and OpenSearch index.number_of_replicas is not a valid cluster persistent setting. Use a composable index template for new indices and _all/_settings for existing ones (migration snapshot restore path). --- .../java/org/apache/unomi/itests/BaseIT.java | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index f2193092f..a37d416dd 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -1386,9 +1386,12 @@ public static void closeResponse(CloseableHttpResponse response) { } + private static final String IT_ZERO_REPLICAS_INDEX_TEMPLATE = "unomi-it-zero-replicas"; + /** * Configures the IT search engine container for single-node testing (UNOMI-946). - * Enforces zero replicas cluster-wide so indices stay GREEN on one node. + * Uses a composable index template plus per-index settings so replicas stay at zero + * on one node (cluster settings do not accept index.number_of_replicas). */ protected void configureSearchEngineForTesting() { if (searchEngineConfiguredForTesting) { @@ -1397,8 +1400,8 @@ protected void configureSearchEngineForTesting() { searchEngineConfiguredForTesting = true; try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); - String settingsBody = "{\"persistent\": {\"index.number_of_replicas\": \"0\"}}"; - HttpUtils.executePutRequest(client, baseUrl + "/_cluster/settings", settingsBody, null); + applyZeroReplicaIndexTemplate(client, baseUrl); + zeroReplicasOnExistingIndices(client, baseUrl, false); String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health", null); LOGGER.info("Search engine baseline cluster health ({}): {}", searchEngine, health); System.out.println("==== Search engine baseline cluster health (" + searchEngine + "): " + health); @@ -1413,8 +1416,8 @@ protected void configureSearchEngineForTesting() { protected void fixRestoredIndexReplicas() { try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); - String settingsBody = "{\"index\": {\"number_of_replicas\": \"0\"}}"; - HttpUtils.executePutRequest(client, baseUrl + "/_all/_settings", settingsBody, null); + applyZeroReplicaIndexTemplate(client, baseUrl); + zeroReplicasOnExistingIndices(client, baseUrl, true); String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health?wait_for_status=green&timeout=30s", null); LOGGER.info("Cluster health after fixing restored index replicas: {}", health); if (health != null && health.contains("\"status\":\"red\"")) { @@ -1425,6 +1428,32 @@ protected void fixRestoredIndexReplicas() { } } + private void applyZeroReplicaIndexTemplate(CloseableHttpClient client, String baseUrl) throws IOException { + String templateBody = "{" + + "\"index_patterns\":[\"*\"]," + + "\"priority\":1," + + "\"template\":{\"settings\":{\"index.number_of_replicas\":\"0\"}}" + + "}"; + HttpUtils.executePutRequest(client, baseUrl + "/_index_template/" + IT_ZERO_REPLICAS_INDEX_TEMPLATE, templateBody, null); + } + + private void zeroReplicasOnExistingIndices(CloseableHttpClient client, String baseUrl, boolean waitForRelocation) + throws IOException { + String indicesJson = HttpUtils.executeGetRequest(client, baseUrl + "/_cat/indices?h=index&format=json", null); + if (indicesJson == null || indicesJson.isBlank() || "[]".equals(indicesJson.trim())) { + return; + } + JsonNode indices = getObjectMapper().readTree(indicesJson); + if (!indices.isArray() || indices.isEmpty()) { + return; + } + String settingsBody = "{\"index\":{\"number_of_replicas\":\"0\"}}"; + HttpUtils.executePutRequest(client, baseUrl + "/_all/_settings", settingsBody, null); + if (waitForRelocation) { + HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health?wait_for_no_relocating_shards=true&timeout=30s", null); + } + } + /** * Asserts the search cluster is healthy for IT: no index with replicas > 0, cluster GREEN. */ From 06bc7936573ef07ff750a8f9fa368c3d02ac6700 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Thu, 9 Jul 2026 18:20:09 +0200 Subject: [PATCH 4/9] Harden IT search engine setup for two-stage migration startup Split UNOMI-946 cluster prep into explicit stages: pre-Unomi baseline, post-snapshot restore, post-shell-migration, and post-Unomi health check. Apply zero-replica settings per index (ES/OS compatible) and re-enforce after migration before unomi:start. --- .../java/org/apache/unomi/itests/BaseIT.java | 48 ++++++++++++++----- .../Migrate16xToCurrentVersionIT.java | 2 + 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index a37d416dd..d11040fa4 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -1389,46 +1389,60 @@ public static void closeResponse(CloseableHttpResponse response) { private static final String IT_ZERO_REPLICAS_INDEX_TEMPLATE = "unomi-it-zero-replicas"; /** - * Configures the IT search engine container for single-node testing (UNOMI-946). - * Uses a composable index template plus per-index settings so replicas stay at zero - * on one node (cluster settings do not accept index.number_of_replicas). + * Stage 1 (pre-Unomi): index template for new indices and zero replicas on any existing indices. + * Safe to call before migration snapshot restore when the cluster is still empty. */ protected void configureSearchEngineForTesting() { if (searchEngineConfiguredForTesting) { return; } - searchEngineConfiguredForTesting = true; try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); - applyZeroReplicaIndexTemplate(client, baseUrl); + ensureZeroReplicaIndexTemplate(client, baseUrl); zeroReplicasOnExistingIndices(client, baseUrl, false); String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health", null); LOGGER.info("Search engine baseline cluster health ({}): {}", searchEngine, health); System.out.println("==== Search engine baseline cluster health (" + searchEngine + "): " + health); + searchEngineConfiguredForTesting = true; } catch (Exception e) { throw new IllegalStateException("Failed to configure search engine for IT testing", e); } } /** - * Zeros replica count on all existing indices (migration snapshot restore path). + * Stage 2a (post snapshot restore): fix replicas on restored 1.x indices before shell migration. */ protected void fixRestoredIndexReplicas() { + enforceZeroReplicasAndWaitForCluster("after snapshot restore"); + } + + /** + * Stage 2b (post shell migration, pre-Unomi start): indices created or reindexed during migration + * must also run with zero replicas on the single-node IT cluster. + */ + protected void prepareSearchEngineAfterMigration() { + enforceZeroReplicasAndWaitForCluster("after migration"); + } + + private void enforceZeroReplicasAndWaitForCluster(String context) { try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); - applyZeroReplicaIndexTemplate(client, baseUrl); + ensureZeroReplicaIndexTemplate(client, baseUrl); zeroReplicasOnExistingIndices(client, baseUrl, true); String health = HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health?wait_for_status=green&timeout=30s", null); - LOGGER.info("Cluster health after fixing restored index replicas: {}", health); + LOGGER.info("Cluster health ({}): {}", context, health); + System.out.println("==== Cluster health (" + context + "): " + health); if (health != null && health.contains("\"status\":\"red\"")) { - throw new IllegalStateException("Cluster still RED after fixing restored index replicas: " + health); + throw new IllegalStateException("Cluster still RED " + context + ": " + health); } + } catch (RuntimeException e) { + throw e; } catch (Exception e) { - throw new IllegalStateException("Failed to fix restored index replicas", e); + throw new IllegalStateException("Failed to enforce zero replicas " + context, e); } } - private void applyZeroReplicaIndexTemplate(CloseableHttpClient client, String baseUrl) throws IOException { + private void ensureZeroReplicaIndexTemplate(CloseableHttpClient client, String baseUrl) throws IOException { String templateBody = "{" + "\"index_patterns\":[\"*\"]," + "\"priority\":1," @@ -1448,18 +1462,26 @@ private void zeroReplicasOnExistingIndices(CloseableHttpClient client, String ba return; } String settingsBody = "{\"index\":{\"number_of_replicas\":\"0\"}}"; - HttpUtils.executePutRequest(client, baseUrl + "/_all/_settings", settingsBody, null); + for (JsonNode index : indices) { + String indexName = index.path("index").asText(null); + if (indexName == null || indexName.isBlank()) { + continue; + } + HttpUtils.executePutRequest(client, baseUrl + "/" + indexName + "/_settings", settingsBody, null); + } if (waitForRelocation) { HttpUtils.executeGetRequest(client, baseUrl + "/_cluster/health?wait_for_no_relocating_shards=true&timeout=30s", null); } } /** - * Asserts the search cluster is healthy for IT: no index with replicas > 0, cluster GREEN. + * Stage 3 (post-Unomi start): assert cluster health for single-node IT (ES and OpenSearch). */ protected void assertClusterHealthy(String context) { try (CloseableHttpClient client = createSearchEngineHttpClient()) { String baseUrl = getSearchEngineBaseUrl(); + ensureZeroReplicaIndexTemplate(client, baseUrl); + zeroReplicasOnExistingIndices(client, baseUrl, true); String indicesJson = HttpUtils.executeGetRequest(client, baseUrl + "/_cat/indices?h=index,rep,health&format=json", null); if (indicesJson != null && !indicesJson.isBlank()) { JsonNode indices = getObjectMapper().readTree(indicesJson); diff --git a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java index c09b13df4..214f71fa8 100644 --- a/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/migration/Migrate16xToCurrentVersionIT.java @@ -162,6 +162,8 @@ public void waitForStartup() throws InterruptedException { } } + prepareSearchEngineAfterMigration(); + // Call super for starting Unomi and wait for the complete startup super.waitForStartup(); } From 18fd7735932be5dafeff6bd41a353fb81e456177 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 10 Jul 2026 07:58:44 +0200 Subject: [PATCH 5/9] Redirect IT search engine Docker logs to files by default. Stream logs to the Maven console only when --search-engine-logs (or -SearchEngineLogs) is passed, while still writing them under itests/target/*0/logs/ for archives. --- build.ps1 | 11 +++++++++++ build.sh | 17 +++++++++++++++++ itests/pom.xml | 12 ++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index a2eaff1c7..967038dc5 100644 --- a/build.ps1 +++ b/build.ps1 @@ -73,6 +73,7 @@ param( [switch]$SkipMigrationTests, [switch]$ResolverDebug, [switch]$KeepContainer, + [switch]$SearchEngineLogs, [switch]$NoMemorySampler, [int]$MemoryInterval = 30, [switch]$Javadoc, @@ -286,6 +287,7 @@ function Show-Usage { Write-Host ' -SkipMigrationTests Skip migration-related tests' Write-Host ' -ResolverDebug Enable Karaf Resolver debug logging for integration tests' Write-Host ' -KeepContainer Keep search engine container running after tests' + Write-Host ' -SearchEngineLogs Stream search engine Docker logs to the Maven console during integration tests' Write-Host ' -NoMemorySampler Disable JVM/system memory sampling during integration tests' Write-Host ' -MemoryInterval SEC Memory sample interval in seconds (default: 30)' Write-Host ' -Javadoc Build and validate Javadoc after install' @@ -535,6 +537,10 @@ function Test-Requirements { Write-Status 'error' 'ItDebug enabled but integration tests are not enabled. Use -IntegrationTests.' $hasErrors = $true } + if ($SearchEngineLogs -and -not $IntegrationTests) { + Write-Status 'error' 'SearchEngineLogs enabled but integration tests are not enabled. Use -IntegrationTests.' + $hasErrors = $true + } if ($ItDebug) { if ($ItDebugPort -lt 1024 -or $ItDebugPort -gt 65535) { Write-Status 'error' "Integration test debug port: $ItDebugPort (invalid)" @@ -608,6 +614,10 @@ function Get-MavenArgumentList { } if ($SkipMigrationTests) { $args += '-Dit.test.exclude.pattern=**/migration/**/*IT.java' } if ($KeepContainer) { $args += '-Dit.keepContainer=true' } + if ($SearchEngineLogs) { + $args += '-Ddocker.showLogs=true' + Write-Host 'Search engine Docker logs will stream to the Maven console (also written under itests/target/*0/logs/)' + } if ($ResolverDebug) { $args += '-Dit.unomi.resolver.debug=true' } if ($SkipUnitTests) { $args += '-Pskip-unit-tests' } } else { @@ -669,6 +679,7 @@ function Write-ItRunTraceStart { "it.debug.suspend=$ItDebugSuspend" "skip.migration.tests=$SkipMigrationTests" "it.keep.container=$KeepContainer" + "it.search.engine.logs=$SearchEngineLogs" "it.memory.sampler=$($script:ItMemorySampler)" "it.memory.interval=$MemoryInterval" "maven.debug=$MavenDebug" diff --git a/build.sh b/build.sh index 9a8e9e2f7..dce11f5df 100755 --- a/build.sh +++ b/build.sh @@ -278,6 +278,7 @@ IT_DEBUG_SUSPEND=false SKIP_MIGRATION_TESTS=false RESOLVER_DEBUG=false KEEP_CONTAINER=false +IT_SEARCH_ENGINE_LOGS=false IT_MEMORY_SAMPLER=true IT_MEMORY_INTERVAL=30 JAVADOC=false @@ -326,6 +327,7 @@ EOF echo -e " ${CYAN}--skip-migration-tests${NC} Skip migration-related tests" echo -e " ${CYAN}--resolver-debug${NC} Enable Karaf Resolver debug logging for integration tests" echo -e " ${CYAN}--keep-container${NC} Keep search engine container running after tests (for post-failure inspection)" + echo -e " ${CYAN}--search-engine-logs${NC} Stream search engine Docker logs to the Maven console during integration tests" echo -e " ${CYAN}--no-memory-sampler${NC} Disable JVM/system memory sampling during integration tests" echo -e " ${CYAN}--memory-interval SEC${NC} Memory sample interval in seconds (default: 30)" echo -e " ${CYAN}--javadoc${NC} Build and validate Javadoc after install (fails on doclint errors)" @@ -370,6 +372,7 @@ EOF echo " --skip-migration-tests Skip migration-related tests" echo " --resolver-debug Enable Karaf Resolver debug logging for integration tests" echo " --keep-container Keep search engine container running after tests (for post-failure inspection)" + echo " --search-engine-logs Stream search engine Docker logs to the Maven console during integration tests" echo " --no-memory-sampler Disable JVM/system memory sampling during integration tests" echo " --memory-interval SEC Memory sample interval in seconds (default: 30)" echo " --javadoc Build and validate Javadoc after install (fails on doclint errors)" @@ -539,6 +542,9 @@ while [ "$1" != "" ]; do --keep-container) KEEP_CONTAINER=true ;; + --search-engine-logs) + IT_SEARCH_ENGINE_LOGS=true + ;; --no-memory-sampler) IT_MEMORY_SAMPLER=false ;; @@ -913,6 +919,11 @@ check_requirements() { has_errors=true fi + if [ "$IT_SEARCH_ENGINE_LOGS" = true ] && [ "$RUN_INTEGRATION_TESTS" = false ]; then + print_status "error" "Search engine logs (--search-engine-logs) enabled but integration tests are not enabled. Use --integration-tests." + has_errors=true + fi + if [ "$IT_DEBUG" = true ]; then if ! [[ "$IT_DEBUG_PORT" =~ ^[0-9]+$ ]] || [ "$IT_DEBUG_PORT" -lt 1024 ] || [ "$IT_DEBUG_PORT" -gt 65535 ]; then print_status "error" "✗ Integration test debug port: $IT_DEBUG_PORT (invalid)" @@ -1137,6 +1148,11 @@ if [ "$RUN_INTEGRATION_TESTS" = true ]; then echo "Search engine container will be kept running after tests" fi + if [ "$IT_SEARCH_ENGINE_LOGS" = true ]; then + MVN_OPTS="$MVN_OPTS -Ddocker.showLogs=true" + echo "Search engine Docker logs will stream to the Maven console (also written under itests/target/*0/logs/)" + fi + if [ "$RESOLVER_DEBUG" = true ]; then MVN_OPTS="$MVN_OPTS -Dit.unomi.resolver.debug=true" echo "Enabling Karaf Resolver debug logging for integration tests" @@ -1223,6 +1239,7 @@ write_it_run_trace_start() { echo "it.debug.suspend=$IT_DEBUG_SUSPEND" echo "skip.migration.tests=$SKIP_MIGRATION_TESTS" echo "it.keep.container=$KEEP_CONTAINER" + echo "it.search.engine.logs=$IT_SEARCH_ENGINE_LOGS" echo "it.memory.sampler=$IT_MEMORY_SAMPLER" echo "it.memory.interval=$IT_MEMORY_INTERVAL" echo "maven.debug=$MAVEN_DEBUG" diff --git a/itests/pom.xml b/itests/pom.xml index d0051bb63..06bf9ff2c 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -35,6 +35,8 @@ true false + + false @@ -356,6 +358,9 @@ + + ${project.build.directory}/elasticsearch0/logs/docker-console.log + ${project.build.directory}/elasticsearch-port.properties @@ -382,7 +387,7 @@ start - true + ${docker.showLogs} @@ -478,6 +483,9 @@ + + ${project.build.directory}/opensearch0/logs/docker-console.log + ${project.build.directory}/opensearch-port.properties @@ -504,7 +512,7 @@ start - true + ${docker.showLogs} From ebcc90316f80f53f376f6f9175fd21ca9b905831 Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 10 Jul 2026 16:55:11 +0200 Subject: [PATCH 6/9] UNOMI-875: PR7 platform hygiene - condition value-type validation, rollover template fixes, and IT tooling - Add OSGi-registered ValueTypeValidator/ValueTypeValidatorRegistry so ConditionContextHelper can validate resolved parameter values against their declared type at evaluation time, not just at definition time. Fix DateValueTypeValidator to accept ISO-8601/epoch-millis strings (the actual runtime shape of "date" condition parameters) instead of only pre-parsed Date/Instant/OffsetDateTime objects. - Fix OpenSearch rollover index templates to attach the ISM policy via the explicit policy_id custom setting instead of relying on ism_template pattern-matching, which never matched real index names; raise both engines' template priority above the IT catch-all template and add a create-then-verify retry loop to OpenSearch's rollover index creation, mirroring the existing ES logic. - Add RolloverIT and a per-search-engine TestTimingCache so the IT progress reporter can estimate remaining time from real historical per-test durations instead of a flat running average. - Remove the maven-build-cache-extension (workspace resolver staleness issues) and its --no-maven-cache/--purge-maven-cache flags from build.sh/build.ps1. --- .gitignore | 1 + .mvn/extensions.xml | 5 - .mvn/maven-build-cache-config.xml | 43 -- CLAUDE.md | 108 +++ bom/pom.xml | 2 +- build.ps1 | 27 +- build.sh | 57 +- .../META-INF/cxs/schemas/items/item.json | 28 + itests/README.md | 12 - itests/pom.xml | 2 - .../java/org/apache/unomi/itests/AllITs.java | 2 +- .../java/org/apache/unomi/itests/BaseIT.java | 67 +- .../apache/unomi/itests/ProgressListener.java | 62 +- .../apache/unomi/itests/ProgressSuite.java | 56 +- .../org/apache/unomi/itests/RolloverIT.java | 112 +++ .../apache/unomi/itests/TestTimingCache.java | 166 +++++ .../main/asciidoc/building-and-deploying.adoc | 39 - .../ElasticSearchPersistenceServiceImpl.java | 14 +- persistence-opensearch/core/pom.xml | 2 + .../OpenSearchPersistenceServiceImpl.java | 187 +++-- .../conditions/ConditionContextHelper.java | 182 ++++- .../ValueTypeValidatorRegistry.java | 69 ++ .../ConditionEvaluatorDispatcherImpl.java | 3 +- .../ConditionContextHelperTest.java | 692 +++++++++++++++++- pom.xml | 2 +- .../ConditionValidationServiceImpl.java | 4 +- .../validators/DateValueTypeValidator.java | 25 +- 27 files changed, 1611 insertions(+), 358 deletions(-) delete mode 100644 .mvn/maven-build-cache-config.xml create mode 100644 CLAUDE.md create mode 100644 itests/src/test/java/org/apache/unomi/itests/RolloverIT.java create mode 100644 itests/src/test/java/org/apache/unomi/itests/TestTimingCache.java create mode 100644 persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ValueTypeValidatorRegistry.java diff --git a/.gitignore b/.gitignore index 20420b42a..7297d5f05 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ dependency_tree.txt /.local-notes/ itests/snapshots_repository/ itests/archives/ +itests/.test-timing-cache-*.properties .env.local .venv* javadoc_* diff --git a/.mvn/extensions.xml b/.mvn/extensions.xml index 2397f14f1..d0eb2b622 100644 --- a/.mvn/extensions.xml +++ b/.mvn/extensions.xml @@ -20,11 +20,6 @@ --> - - org.apache.maven.extensions - maven-build-cache-extension - 1.2.1 - com.gradle develocity-maven-extension diff --git a/.mvn/maven-build-cache-config.xml b/.mvn/maven-build-cache-config.xml deleted file mode 100644 index 9057a9cce..000000000 --- a/.mvn/maven-build-cache-config.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - true - SHA-256 - true - - http://host:port - - - 3 - - - - - - - src/ - - - pom.xml - src/main/javagen/** - - - - \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..73593ec7b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,108 @@ + + +# Apache Unomi — CLAUDE.md + +Apache Unomi is a Java/OSGi customer data platform running on Apache Karaf. Multi-module Maven +reactor; most runtime code is packaged as OSGi bundles wired via Blueprint XML +(`OSGI-INF/blueprint/blueprint.xml` in each module), not Spring/CDI. + +## Dual persistence backends — the #1 thing to know + +Every persistence-related feature must be implemented **twice**, once per search engine, and the +two implementations must behave identically: + +- `persistence-elasticsearch/core` — `ElasticSearchPersistenceServiceImpl.java` (Elasticsearch, ILM + for rollover, `co.elastic.clients.elasticsearch.*` client) +- `persistence-opensearch/core` — `OpenSearchPersistenceServiceImpl.java` (OpenSearch, ISM for + rollover, `org.opensearch.client.opensearch.*` client) + +The two client libraries look similar (OpenSearch's Java client is a fork of Elastic's) but their +typed builder APIs differ in real, non-obvious ways — e.g. ES's `numberOfShards(String)` vs +OpenSearch's `numberOfShards(Integer)`. **Do not port code between the two by pattern-matching +alone — verify actual method signatures for the specific client version in use** (`mvn +dependency:tree` + decompile if needed; there is no live cluster available in most dev sandboxes to +test against directly). + +ISM (OpenSearch) and ILM (ES) are conceptually parallel but not identical: ISM policies attach to +indices either via an explicit `plugins.index_state_management.policy_id` index setting or via the +policy's own `ism_template.index_patterns` matching at index-creation time — prefer the explicit +`policy_id` setting (set via `IndexSettings.customSettings(...)`, since there's no typed builder +field for it) over template pattern-matching, which is fragile (a past bug: the pattern list was +built from raw item-type names like `"event"` that never matched real index names like +`"context-event-000001"`). + +**Composable index template priority**: `BaseIT` registers an IT-only catch-all template +(`unomi-it-zero-replicas`, pattern `*`, priority `1`) to force zero replicas on single-node test +clusters. Any real Unomi index template must use a priority well above that (convention: `100`) or +template registration fails outright — composable templates reject ambiguous same-priority +overlapping patterns rather than merging them. + +**Single-node test clusters legitimately stay `yellow` forever** (no second node to host replicas). +Code that blocks on `HealthStatus.Green` will hang/fail in ITs. OpenSearch's persistence service has +a `minimalClusterState` config + `getHealthStatus(minimalClusterState)` helper for this — reuse it. +**Elasticsearch's persistence service has no equivalent wired in** (only the separate +`extensions/healthcheck` bundle knows about yellow-tolerance) — this is a known, unaddressed parity +gap, not an oversight to silently "fix" by inventing new config without discussing it with the user +first. + +## Build + +- Root `pom.xml` aggregates ~40 modules. Key ones: `api`, `common`, `persistence-spi`, + `persistence-elasticsearch`, `persistence-opensearch`, `services`, `rest`, `graphql`, `wab`, + `kar` (Karaf feature repo), `itests`. +- `./build.sh` is the canonical local build entrypoint (aligns with CI). Prefer it over ad hoc + `mvn` invocations when validating a full build. +- The `itests` module only joins the reactor under the `integration-tests` (or `ci-build-itests`) + profile — `mvn -pl itests ...` alone will fail with "module not found in reactor" unless that + profile is active. +- Karaf `karaf-maven-plugin:verify` failures on unrelated modules (e.g. stale + `target/classes` "(Is a directory)" errors) are usually stale local build state, not real + regressions — don't chase them as if they were caused by your change without checking first. + +## Integration tests (`itests/`) + +- PaxExam + Karaf, `PerSuite` reactor strategy: **one shared Karaf container/cluster runs the + entire suite**, not one per test class. Tests must clean up after themselves (`@After`) or they + pollute later tests' assumptions. +- **`itests/pom.xml`'s failsafe config only includes `**/*AllITs.java`** — individual `*IT.java` + classes are silently never run unless explicitly registered in `AllITs.java`'s `@SuiteClasses` + list. This is easy to forget when adding a new IT. +- `BaseIT.java` is the shared base class: search-engine-agnostic HTTP helpers + (`createSearchEngineHttpClient()`, `getSearchEngineBaseUrl()`), `keepTrying(...)` for + poll-with-timeout assertions, and Karaf container config (`etc/custom.system.properties` + overrides) shared by ES and OpenSearch runs. IT-only config differences between the two engines + (e.g. `rollover.maxDocs`) must be set for **both** `org.apache.unomi.elasticsearch.*` and + `org.apache.unomi.opensearch.*` properties, or one engine silently runs untested. +- `itests/kt.sh` is a helper for driving/inspecting a running IT Karaf instance (`kt.sh t` to run, + `kt.sh l` to view the log) — see that script and any `itests/README*` for the current test-dev + workflow rather than assuming. +- Run engine-specific ITs via the search-engine system property; check `BaseIT` / + `AllITs`/`build.sh` for the exact flag rather than guessing. + +## Working with subagents / cost awareness + +Verifying an unfamiliar client library's exact API surface via jar decompilation (`javap`) is a +legitimate but expensive way to work when no live test environment is available — it burns a lot of +tool calls. When a fix's correctness can't be confirmed without actually running the code (e.g. +against a live Elasticsearch/OpenSearch container), prefer asking the user to run it and paste back +real output over exhaustively re-deriving behavior from bytecode. + +## Memory + +Session-persistent project memory for this repo lives outside the working tree (Claude's own memory +system) — check it for prior investigation before re-deriving things like client API quirks, +known-flaky tests, or in-progress uncommitted work state. diff --git a/bom/pom.xml b/bom/pom.xml index 2924ede0f..bf4113340 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -161,7 +161,7 @@ org.opensearch.client opensearch-java - ${opensearch.version} + ${opensearch.rest.client.version} com.google.guava diff --git a/build.ps1 b/build.ps1 index 967038dc5..355c1fcf0 100644 --- a/build.ps1 +++ b/build.ps1 @@ -57,8 +57,6 @@ param( [switch]$Debug, [int]$DebugPort = 5005, [switch]$DebugSuspend, - [switch]$NoMavenCache, - [switch]$PurgeMavenCache, [string]$KarafHome, [switch]$UseOpenSearch, [string]$Distribution, @@ -89,22 +87,16 @@ $PSDefaultParameterValues['*:ErrorAction'] = 'Stop' $script:BuildScriptInvocation = @($MyInvocation.Line) # Defaults aligned with build.sh -$script:UseMavenCache = -not $NoMavenCache $script:ItMemorySampler = -not $NoMemorySampler $script:KarafDebugSuspend = if ($DebugSuspend) { 'y' } else { 'n' } if ($Ci) { $NoKaraf = $true - $script:UseMavenCache = $false $env:BUILD_NON_INTERACTIVE = 'true' $MavenQuiet = $true $Javadoc = $true } -if ($PurgeMavenCache) { - $script:UseMavenCache = $false -} - if ($UseOpenSearch -and [string]::IsNullOrWhiteSpace($Distribution)) { $Distribution = 'unomi-distribution-opensearch' } @@ -271,8 +263,6 @@ function Show-Usage { Write-Host ' -Debug Run Karaf in debug mode' Write-Host ' -DebugPort PORT Set debug port (default: 5005)' Write-Host ' -DebugSuspend Suspend JVM until debugger connects' - Write-Host ' -NoMavenCache Disable Maven build cache' - Write-Host ' -PurgeMavenCache Purge local Maven cache before building' Write-Host ' -KarafHome PATH Set Karaf home directory for deployment' Write-Host ' -UseOpenSearch Use OpenSearch instead of ElasticSearch' Write-Host ' -Distribution DIST Set Unomi distribution (e.g. unomi-distribution-opensearch)' @@ -291,7 +281,7 @@ function Show-Usage { Write-Host ' -NoMemorySampler Disable JVM/system memory sampling during integration tests' Write-Host ' -MemoryInterval SEC Memory sample interval in seconds (default: 30)' Write-Host ' -Javadoc Build and validate Javadoc after install' - Write-Host ' -Ci CI mode: no Karaf, no Maven cache, non-interactive, Javadoc' + Write-Host ' -Ci CI mode: no Karaf, non-interactive, Javadoc' Write-Host ' -LogFile PATH Tee all output to PATH (console + file)' Write-Host ' -LogFileOnly With -LogFile: write to file only, suppress console' Write-Host '' @@ -334,15 +324,6 @@ function Initialize-ProjectVersion { Initialize-ProjectVersion -if ($PurgeMavenCache) { - Write-Host 'Purging Maven cache...' - $m2 = Join-Path (Get-UserHome) '.m2' - foreach ($dir in @('build-cache', 'dependency-cache', 'dependency-cache_v2')) { - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $m2 $dir) - } - Write-Host 'Maven cache purged.' -} - function Test-JavaRequirement { if (-not (Test-CommandExists 'java')) { Write-Status 'error' 'java not found' @@ -553,11 +534,6 @@ function Test-Requirements { } } - if ($Offline -and $PurgeMavenCache) { - Write-Status 'error' 'Cannot use -PurgeMavenCache in offline mode' - $hasErrors = $true - } - Write-Host '' if ($hasErrors) { Write-Status 'error' 'Critical requirements not met. Please fix the errors above.' @@ -577,7 +553,6 @@ function Get-MavenArgumentList { $args = @() if ($MavenDebug) { $args += '-X' } if ($Offline) { $args += '-o' } - if (-not $script:UseMavenCache) { $args += '-Dmaven.build.cache.enabled=false' } if ($MavenQuiet) { $args += '-ntp' } if ($env:MAVEN_EXTRA_OPTS) { $args += $env:MAVEN_EXTRA_OPTS.Split(' ', [System.StringSplitOptions]::RemoveEmptyEntries) diff --git a/build.sh b/build.sh index dce11f5df..d4dfb2f30 100755 --- a/build.sh +++ b/build.sh @@ -255,8 +255,6 @@ SKIP_UNIT_TESTS=false RUN_INTEGRATION_TESTS=false DEPLOY=false DEBUG=false -USE_MAVEN_CACHE=true -PURGE_MAVEN_CACHE=false MAVEN_DEBUG=false MAVEN_OFFLINE=false KARAF_DEBUG_PORT=5005 @@ -313,8 +311,6 @@ EOF echo -e " ${CYAN}--debug${NC} Run Karaf in debug mode" echo -e " ${CYAN}--debug-port PORT${NC} Set debug port (default: 5005)" echo -e " ${CYAN}--debug-suspend${NC} Suspend JVM until debugger connects" - echo -e " ${CYAN}--no-maven-cache${NC} Disable Maven build cache" - echo -e " ${CYAN}--purge-maven-cache${NC} Purge local Maven cache before building" echo -e " ${CYAN}--karaf-home PATH${NC} Set Karaf home directory for deployment" echo -e " ${CYAN}--use-opensearch${NC} Use OpenSearch instead of ElasticSearch" echo -e " ${CYAN}--distribution DIST${NC} Set Unomi distribution (e.g., unomi-distribution-opensearch)" @@ -331,7 +327,7 @@ EOF echo -e " ${CYAN}--no-memory-sampler${NC} Disable JVM/system memory sampling during integration tests" echo -e " ${CYAN}--memory-interval SEC${NC} Memory sample interval in seconds (default: 30)" echo -e " ${CYAN}--javadoc${NC} Build and validate Javadoc after install (fails on doclint errors)" - echo -e " ${CYAN}--ci${NC} CI mode: no Karaf, no Maven build cache, non-interactive, includes Javadoc" + echo -e " ${CYAN}--ci${NC} CI mode: no Karaf, non-interactive, includes Javadoc" echo -e " ${CYAN}--log-file PATH${NC} Tee all output to PATH (console + file)" echo -e " ${CYAN}--log-file-only${NC} With --log-file: write to file only, suppress console" else @@ -358,8 +354,6 @@ EOF echo " --debug Run Karaf in debug mode" echo " --debug-port PORT Set debug port (default: 5005)" echo " --debug-suspend Suspend JVM until debugger connects" - echo " --no-maven-cache Disable Maven build cache" - echo " --purge-maven-cache Purge local Maven cache before building" echo " --karaf-home PATH Set Karaf home directory for deployment" echo " --use-opensearch Use OpenSearch instead of ElasticSearch" echo " --distribution DIST Set Unomi distribution (e.g., unomi-distribution-opensearch)" @@ -376,7 +370,7 @@ EOF echo " --no-memory-sampler Disable JVM/system memory sampling during integration tests" echo " --memory-interval SEC Memory sample interval in seconds (default: 30)" echo " --javadoc Build and validate Javadoc after install (fails on doclint errors)" - echo " --ci CI mode: no Karaf, no Maven build cache, non-interactive, includes Javadoc" + echo " --ci CI mode: no Karaf, non-interactive, includes Javadoc" echo " --log-file PATH Tee all output to PATH (console + file)" echo " --log-file-only With --log-file: write to file only, suppress console" fi @@ -483,12 +477,6 @@ while [ "$1" != "" ]; do --debug-suspend) KARAF_DEBUG_SUSPEND=y ;; - --no-maven-cache) - USE_MAVEN_CACHE=false - ;; - --purge-maven-cache) - PURGE_MAVEN_CACHE=true - ;; --karaf-home) shift CONTEXT_SERVER_KARAF_HOME=$1 @@ -564,7 +552,6 @@ while [ "$1" != "" ]; do ;; --ci) NO_KARAF=true - USE_MAVEN_CACHE=false BUILD_NON_INTERACTIVE=true MAVEN_QUIET=true JAVADOC=true @@ -607,16 +594,6 @@ if [ -f "$DIRNAME/setenv.sh" ]; then . "$DIRNAME/setenv.sh" fi -# Purge Maven cache if requested -if [ "$PURGE_MAVEN_CACHE" = true ]; then - echo "Purging Maven cache..." - rm -rf ~/.m2/build-cache ~/.m2/dependency-cache ~/.m2/dependency-cache_v2 - echo "Maven cache purged." - # Disable the build cache for this run: a cold cache with the extension still active - # causes the workspace resolver to return target/classes instead of built JARs, - # breaking karaf-maven-plugin:verify. Disabling matches CI behaviour. - USE_MAVEN_CACHE=false -fi # Function to check if command exists command_exists() { @@ -942,17 +919,6 @@ check_requirements() { fi fi - if [ "$MAVEN_OFFLINE" = true ]; then - if [ "$PURGE_MAVEN_CACHE" = true ]; then - print_status "error" "Cannot use --purge-maven-cache in offline mode" - has_errors=true - fi - if [ "$USE_MAVEN_CACHE" = false ]; then - print_status "warning" "Using --no-maven-cache with offline mode may cause build failures" - has_warnings=true - fi - fi - # Final status and prompts echo if [ "$has_errors" = true ]; then @@ -992,25 +958,6 @@ fi if [ "$MAVEN_OFFLINE" = true ]; then MVN_OPTS="$MVN_OPTS -o" echo "Maven offline mode enabled" - - # Warn if purge cache is enabled with offline mode - if [ "$PURGE_MAVEN_CACHE" = true ]; then - echo "Warning: Purging Maven cache while in offline mode may cause build failures" - if is_non_interactive; then - print_status "warning" "Non-interactive mode: continuing despite purge + offline" - else - read -p "Continue anyway? (y/N) " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - exit 1 - fi - fi - fi -fi - -# Add Maven cache option -if [ "$USE_MAVEN_CACHE" = false ]; then - MVN_OPTS="$MVN_OPTS -Dmaven.build.cache.enabled=false" fi # Add Maven quiet mode (suppress download progress) diff --git a/extensions/json-schema/services/src/main/resources/META-INF/cxs/schemas/items/item.json b/extensions/json-schema/services/src/main/resources/META-INF/cxs/schemas/items/item.json index b5035f831..4f46c3bef 100644 --- a/extensions/json-schema/services/src/main/resources/META-INF/cxs/schemas/items/item.json +++ b/extensions/json-schema/services/src/main/resources/META-INF/cxs/schemas/items/item.json @@ -29,6 +29,34 @@ "minimum" : 0, "description" : "The item's version number" }, + "tenantId" : { + "type" : ["null","string"], + "description" : "The tenant that owns this item" + }, + "createdBy" : { + "type" : ["null","string"], + "description" : "The user or system that created this item" + }, + "lastModifiedBy" : { + "type" : ["null","string"], + "description" : "The user or system that last modified this item" + }, + "creationDate" : { + "type" : ["null","string","integer"], + "description" : "When this item was created (ISO-8601 string or epoch millis)" + }, + "lastModificationDate" : { + "type" : ["null","string","integer"], + "description" : "When this item was last modified (ISO-8601 string or epoch millis)" + }, + "sourceInstanceId" : { + "type" : ["null","string"], + "description" : "The source cluster instance that created this item" + }, + "lastSyncDate" : { + "type" : ["null","string","integer"], + "description" : "When this item was last synchronized (ISO-8601 string or epoch millis)" + }, "properties": { "type": ["null", "object"] } diff --git a/itests/README.md b/itests/README.md index e3929d611..ebb3d9708 100644 --- a/itests/README.md +++ b/itests/README.md @@ -282,18 +282,6 @@ mvn clean install -P integration-tests -Dit.test=org.apache.unomi.itests.Context See the [Maven Failsafe plugin docs](https://maven.apache.org/surefire/maven-failsafe-plugin/examples/single-test.html) for more filtering options. -### Bypassing the Maven Build Cache - -If a cached build is interfering with test execution, use `--purge-maven-cache` to wipe -the local Maven cache before building: - -```bash -./build.sh --integration-tests --purge-maven-cache -``` - -This removes `~/.m2/build-cache`, `~/.m2/dependency-cache`, and -`~/.m2/dependency-cache_v2`. It cannot be combined with `--offline`. - --- ## Debugging Integration Tests diff --git a/itests/pom.xml b/itests/pom.xml index 06bf9ff2c..8f6f55b65 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -31,8 +31,6 @@ elasticsearch false itests-opensearch - - true false diff --git a/itests/src/test/java/org/apache/unomi/itests/AllITs.java b/itests/src/test/java/org/apache/unomi/itests/AllITs.java index 3ea7dd340..8617b1dcb 100644 --- a/itests/src/test/java/org/apache/unomi/itests/AllITs.java +++ b/itests/src/test/java/org/apache/unomi/itests/AllITs.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License */ - package org.apache.unomi.itests; import org.apache.unomi.itests.migration.Migrate16xToCurrentVersionIT; @@ -81,6 +80,7 @@ TenantIT.class, SchedulerIT.class, EventsCollectorIT.class, + RolloverIT.class, HealthCheckIT.class }) public class AllITs { diff --git a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java index d11040fa4..53a07bedd 100644 --- a/itests/src/test/java/org/apache/unomi/itests/BaseIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/BaseIT.java @@ -14,7 +14,6 @@ * See the License for the specific language governing permissions and * limitations under the License */ - package org.apache.unomi.itests; import com.fasterxml.jackson.databind.JsonNode; @@ -150,6 +149,14 @@ public abstract class BaseIT extends KarafTestSupport { private static boolean searchEngineConfiguredForTesting = false; private static boolean searchEngineHealthVerifiedAfterStartup = false; + /** + * PaxExam invokes {@link #config()} once per test class to build that class's Option[] even + * though the {@code PerSuite} reactor strategy means only one container actually gets started + * for the whole suite. This flag keeps the informational logging in config() from repeating + * for every class. + */ + private static boolean configLogged = false; + /** * JSON mapper for IT HTTP/JSON helpers. Initialized on first use (after Unomi features are up), * delegating to the same mapper as the running server. @@ -482,6 +489,7 @@ protected boolean isLogCheckingEnabled() { /** * Add a substring to ignore for log checking * Useful for tests that expect certain errors/warnings + * * @param substring Literal substring or regex pattern to match against log messages */ protected void addIgnoredLogSubstring(String substring) { @@ -492,6 +500,7 @@ protected void addIgnoredLogSubstring(String substring) { /** * Add multiple substrings to ignore for log checking + * * @param substrings List of substrings (literal or regex) */ protected void addIgnoredLogSubstrings(List substrings) { @@ -540,12 +549,16 @@ public MavenArtifactUrlReference getKarafDistribution() { @Configuration public Option[] config() { - LOGGER.info("==== Configuring container"); - System.out.println("==== Configuring container"); + if (!configLogged) { + LOGGER.info("==== Configuring container"); + System.out.println("==== Configuring container"); + } searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); - LOGGER.info("Search Engine: {}", searchEngine); - System.out.println("Search Engine: " + searchEngine); + if (!configLogged) { + LOGGER.info("Search Engine: {}", searchEngine); + System.out.println("Search Engine: " + searchEngine); + } // Define features option based on search engine Option featuresOption; @@ -653,6 +666,7 @@ public Option[] config() { editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.password", "Unomi.1ntegrat10n.Tests"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslEnable", "false"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.sslTrustAllCertificates", "true"), + editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.rollover.maxDocs", "300"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.opensearch.minimalClusterState", "YELLOW"), editConfigurationFilePut("etc/custom.system.properties", "org.apache.unomi.migration.tenant.id", TEST_TENANT_ID), @@ -673,8 +687,10 @@ public Option[] config() { String karafDebug = System.getProperty("it.karaf.debug"); if (karafDebug != null) { - LOGGER.info("Found system Karaf Debug system property, activating configuration: {}", karafDebug); - System.out.println("Found system Karaf Debug system property, activating configuration: " + karafDebug); + if (!configLogged) { + LOGGER.info("Found system Karaf Debug system property, activating configuration: {}", karafDebug); + System.out.println("Found system Karaf Debug system property, activating configuration: " + karafDebug); + } String port = "5006"; boolean hold = true; if (karafDebug.trim().length() > 0) { @@ -698,17 +714,21 @@ public Option[] config() { if (Files.exists(path)) { final String jacocoOption = "-javaagent:" + agentFile + "=destfile=" + System.getProperty("user.dir") + "/target/jacoco.exec,includes=org.apache.unomi.*"; - LOGGER.info("set jacoco java agent: {}", jacocoOption); + if (!configLogged) { + LOGGER.info("set jacoco java agent: {}", jacocoOption); + } karafOptions.add(new VMOption(jacocoOption)); - } else { + } else if (!configLogged) { LOGGER.warn("Unable to set jacoco agent as {} was not found", agentFile); } // Allow overriding the Karaf JVM heap via -Dit.karaf.heap (e.g. 4g) String karafHeap = System.getProperty("it.karaf.heap", ""); if (!karafHeap.isEmpty()) { - LOGGER.info("Setting Karaf JVM heap to: {}", karafHeap); - System.out.println("Setting Karaf JVM heap to: " + karafHeap); + if (!configLogged) { + LOGGER.info("Setting Karaf JVM heap to: {}", karafHeap); + System.out.println("Setting Karaf JVM heap to: " + karafHeap); + } karafOptions.add(new VMOption("-Xms" + karafHeap)); karafOptions.add(new VMOption("-Xmx" + karafHeap)); } @@ -730,8 +750,10 @@ public Option[] config() { // Enable debug logging for Karaf Resolver to diagnose bundle refresh issues (default: disabled) boolean enableResolverDebug = Boolean.parseBoolean(System.getProperty(RESOLVER_DEBUG_PROPERTY, "false")); if (enableResolverDebug) { - LOGGER.info("Enabling debug logging for Karaf Resolver and Karaf features service"); - System.out.println("Enabling debug logging for Karaf Resolver and Karaf features service"); + if (!configLogged) { + LOGGER.info("Enabling debug logging for Karaf Resolver and Karaf features service"); + System.out.println("Enabling debug logging for Karaf Resolver and Karaf features service"); + } karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.osgiResolver.name", "org.osgi.service.resolver")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.osgiResolver.level", "DEBUG")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.karafFeatures.name", "org.apache.karaf.features")); @@ -744,7 +766,7 @@ public Option[] config() { karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.osgiPackageAdmin.level", "DEBUG")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.karafDeployer.name", "org.apache.karaf.features.core")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.karafDeployer.level", "DEBUG")); - } else { + } else if (!configLogged) { LOGGER.info("Karaf Resolver debug logging is disabled (set -Dit.unomi.resolver.debug=true to enable)"); System.out.println("Karaf Resolver debug logging is disabled (set -Dit.unomi.resolver.debug=true to enable)"); } @@ -752,8 +774,10 @@ public Option[] config() { // Enable Camel debug logging if requested (for test visibility into Camel operations) boolean enableCamelDebug = Boolean.parseBoolean(System.getProperty(CAMEL_DEBUG_PROPERTY, "false")); if (enableCamelDebug) { - LOGGER.info("Enabling debug logging for Apache Camel"); - System.out.println("Enabling debug logging for Apache Camel (set -Dit.unomi.camel.debug=true to enable)"); + if (!configLogged) { + LOGGER.info("Enabling debug logging for Apache Camel"); + System.out.println("Enabling debug logging for Apache Camel (set -Dit.unomi.camel.debug=true to enable)"); + } // Enable logging for Camel core, routes, and router components karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.camelCore.name", "org.apache.camel")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.camelCore.level", "DEBUG")); @@ -761,22 +785,20 @@ public Option[] config() { karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.camelRouter.level", "DEBUG")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.camelFile.name", "org.apache.camel.component.file")); karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", "log4j2.logger.camelFile.level", "DEBUG")); - } else { + } else if (!configLogged) { LOGGER.info("Camel debug logging is disabled (set -Dit.unomi.camel.debug=true to enable)"); System.out.println("Camel debug logging is disabled (set -Dit.unomi.camel.debug=true to enable)"); } - searchEngine = System.getProperty(SEARCH_ENGINE_PROPERTY, SEARCH_ENGINE_ELASTICSEARCH); - LOGGER.info("Search Engine: {}", searchEngine); - System.out.println("Search Engine: " + searchEngine); - // Configure in-memory log appender for log checking // The InMemoryLogAppender is part of the log4j-extension fragment bundle, // which is already included as a startup bundle. It attaches to the Pax Logging // Log4j2 bundle early in the startup process, ensuring the appender is discoverable. // We only configure it for integration tests, not for the default package. if (isLogCheckingEnabled()) { - LOGGER.info("Configuring in-memory log appender for log checking"); + if (!configLogged) { + LOGGER.info("Configuring in-memory log appender for log checking"); + } // Configure the appender in Log4j2 // The appender is already available via the log4j-extension fragment bundle karafOptions.add(editConfigurationFilePut("etc/org.ops4j.pax.logging.cfg", @@ -787,6 +809,7 @@ public Option[] config() { "log4j2.rootLogger.appenderRef.inMemory.ref", "InMemoryLogAppender")); } + configLogged = true; return Stream.of(super.config(), karafOptions.toArray(new Option[karafOptions.size()])).flatMap(Stream::of).toArray(Option[]::new); } diff --git a/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java b/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java index 72893b335..78d9653b1 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProgressListener.java @@ -28,7 +28,10 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.PriorityQueue; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** @@ -137,18 +140,31 @@ private static class TestTime { /** Formatter for human-readable timestamps */ private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /** Search engine under test (e.g. "elasticsearch", "opensearch"); timings are cached per engine */ + private final String searchEngine = System.getProperty(BaseIT.SEARCH_ENGINE_PROPERTY, BaseIT.SEARCH_ENGINE_ELASTICSEARCH); + /** Cached per-test durations loaded from {@link TestTimingCache} */ + private final Map cachedTimings; + /** Timing-cache keys for tests not yet completed in this run */ + private final Set remainingTestKeys; + /** * Creates a new ProgressListener instance. * * @param totalTests the total number of tests that will be executed * @param completedTests a thread-safe counter that tracks the number of completed tests * (this should be shared with the test runner for accurate progress tracking) + * + * @param testKeys timing-cache keys ("SimpleClassName#methodName") for every test in the run, + * used to look up historical per-test durations for the ETA calculation */ - public ProgressListener(int totalTests, AtomicInteger completedTests) { + public ProgressListener(int totalTests, AtomicInteger completedTests, List testKeys) { this.totalTests = totalTests; this.completedTests = completedTests; this.slowTests = new PriorityQueue<>((t1, t2) -> Long.compare(t1.time, t2.time)); this.ansiSupported = isAnsiSupported(); + this.cachedTimings = TestTimingCache.load(searchEngine); + this.remainingTestKeys = ConcurrentHashMap.newKeySet(); + this.remainingTestKeys.addAll(testKeys); } /** @@ -286,6 +302,12 @@ public void testFinished(Description description) { // Remove the smallest time, keeping only the top 5 longest slowTests.poll(); } + String testKey = TestTimingCache.keyFor(description); + remainingTestKeys.remove(testKey); + // Persist immediately (rather than batching until testRunFinished) so a run that gets killed + // mid-suite (Ctrl-C, CI timeout, a hung test force-killed) still leaves every test that did + // complete recorded in the cache for next time. + TestTimingCache.save(searchEngine, Collections.singletonMap(testKey, testDuration)); // Print test end boundary String testName = extractTestName(description); String durationStr = formatTime(testDuration); @@ -327,6 +349,8 @@ public void testFailure(Failure failure) { */ @Override public void testRunFinished(Result result) { + // No explicit save here: each test's duration is already persisted as it finishes (see + // testFinished), so the cache is up to date even if the run never reaches this point. long elapsedTime = System.currentTimeMillis() - startTime; String resultMessage = result.wasSuccessful() ? colorize("SUCCESS!", GREEN) @@ -429,6 +453,35 @@ private String escapeCsv(String value) { return value; } + /** + * Estimates the remaining time for the run by summing, for each test that has not completed yet, + * its historical duration from the search-engine-specific {@link TestTimingCache} when one exists, + * and falling back to this run's own flat average for any test with no cache entry (e.g. the very + * first run on a machine, or a newly added test). + * + * @param completed the number of tests completed so far + * @param elapsedTime the time elapsed since the run started, in milliseconds + * @return the estimated remaining time, in milliseconds + */ + private long estimateRemainingTime(int completed, long elapsedTime) { + // Avoid division by very low completed count; use a floor value + int stableCompleted = Math.max(completed, 1); + double averageTestTimeMillis = elapsedTime / (double) stableCompleted; + + long estimate = 0; + int uncachedRemaining = 0; + for (String key : remainingTestKeys) { + Long cached = cachedTimings.get(key); + if (cached != null) { + estimate += cached; + } else { + uncachedRemaining++; + } + } + estimate += (long) (averageTestTimeMillis * uncachedRemaining); + return estimate; + } + /** * Displays the current progress of the test run including progress bar, * percentage completion, estimated time remaining, and success/failure counts. @@ -438,12 +491,7 @@ private void displayProgress() { int completed = completedTests.get(); long elapsedTime = System.currentTimeMillis() - startTime; - // Avoid division by very low completed count; use a floor value - int stableCompleted = Math.max(completed, 1); - double averageTestTimeMillis = elapsedTime / (double) stableCompleted; - - // Calculate estimated time remaining - long estimatedRemainingTime = (long) (averageTestTimeMillis * (totalTests - completed)); + long estimatedRemainingTime = estimateRemainingTime(completed, elapsedTime); String progressBar = generateProgressBar(((double) completed / totalTests) * 100); String humanReadableTime = formatTime(estimatedRemainingTime); diff --git a/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java b/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java index 02d8da8e0..0931b06fa 100644 --- a/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java +++ b/itests/src/test/java/org/apache/unomi/itests/ProgressSuite.java @@ -23,6 +23,8 @@ import org.junit.runners.model.InitializationError; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicInteger; /** @@ -45,6 +47,7 @@ * *

Example usage:

*
{@code
+ *
  * @RunWith(ProgressSuite.class)
  * @Suite.SuiteClasses({
  *     TestClass1.class,
@@ -75,6 +78,8 @@ public class ProgressSuite extends Suite {
 
     /** Total number of test methods across all classes in the suite */
     private final int totalTests;
+    /** Cache keys ("SimpleClassName#methodName") for every test method in the suite */
+    private final List testKeys;
     /** Thread-safe counter for completed tests, shared with ProgressListener */
     private final AtomicInteger completedTests = new AtomicInteger(0);
 
@@ -84,7 +89,7 @@ public class ProgressSuite extends Suite {
      * 

The constructor initializes the suite by:

*
    *
  • Extracting test classes from the {@code @Suite.SuiteClasses} annotation
  • - *
  • Counting all test methods across the class hierarchies
  • + *
  • Collecting all test methods (as timing-cache keys) across the class hierarchies
  • *
  • Initializing the progress tracking infrastructure
  • *
* @@ -94,7 +99,8 @@ public class ProgressSuite extends Suite { */ public ProgressSuite(Class klass) throws InitializationError { super(klass, getAnnotatedClasses(klass)); - this.totalTests = countTestMethods(getAnnotatedClasses(klass)); + this.testKeys = collectTestKeys(getAnnotatedClasses(klass)); + this.totalTests = testKeys.size(); } /** @@ -114,42 +120,42 @@ private static Class[] getAnnotatedClasses(Class klass) throws Initializat } /** - * Counts the total number of test methods across all specified test classes. + * Collects the timing-cache keys for all test methods across all specified test classes. * - * @param testClasses array of test classes to count methods in - * @return the total number of methods annotated with {@code @Test} + * @param testClasses array of test classes to collect methods from + * @return a list of "SimpleClassName#methodName" keys, one per {@code @Test} method */ - private static int countTestMethods(Class[] testClasses) { - int count = 0; + private static List collectTestKeys(Class[] testClasses) { + List keys = new ArrayList<>(); for (Class testClass : testClasses) { - count += countTestMethodsInClassHierarchy(testClass); + collectTestKeysInClassHierarchy(testClass, testClass, keys); } - return count; + return keys; } /** - * Recursively counts test methods in a class and its entire inheritance hierarchy. + * Recursively collects test method keys in a class and its entire inheritance hierarchy. * - *

This method traverses the class hierarchy upward from the given class, - * counting all methods annotated with {@code @Test} in each class. It stops - * at {@code Object.class} to avoid counting system methods.

+ *

This method traverses the class hierarchy upward from the given class, recording a + * cache key for every method annotated with {@code @Test} in each class (keyed by the + * concrete leaf class, matching how JUnit reports the runtime test class). It stops at + * {@code Object.class} to avoid considering system methods.

* - * @param clazz the class to count test methods in (including superclasses) - * @return the number of test methods found in this class and its hierarchy + * @param declaringClass the class currently being walked (this class or a superclass) + * @param leafClass the concrete test class the methods will actually run on + * @param keys the list to append discovered test keys to */ - private static int countTestMethodsInClassHierarchy(Class clazz) { - int count = 0; - if (clazz == null || clazz == Object.class) { - return 0; // Stop at the base class + private static void collectTestKeysInClassHierarchy(Class declaringClass, Class leafClass, List keys) { + if (declaringClass == null || declaringClass == Object.class) { + return; // Stop at the base class } - for (Method method : clazz.getDeclaredMethods()) { + for (Method method : declaringClass.getDeclaredMethods()) { if (method.isAnnotationPresent(Test.class)) { - count++; + keys.add(TestTimingCache.keyFor(leafClass, method.getName())); } } // Recurse into the superclass - count += countTestMethodsInClassHierarchy(clazz.getSuperclass()); - return count; + collectTestKeysInClassHierarchy(declaringClass.getSuperclass(), leafClass, keys); } /** @@ -174,12 +180,12 @@ private static int countTestMethodsInClassHierarchy(Class clazz) { */ @Override public void run(RunNotifier notifier) { - ProgressListener listener = new ProgressListener(totalTests, completedTests); + ProgressListener listener = new ProgressListener(totalTests, completedTests, testKeys); Description suiteDescription = getDescription(); // We call this manually as we register the listener after this event has already been triggered. listener.testRunStarted(suiteDescription); - notifier.addListener(new ProgressListener(totalTests, completedTests)); + notifier.addListener(new ProgressListener(totalTests, completedTests, testKeys)); super.run(notifier); } diff --git a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java new file mode 100644 index 000000000..09d7021f2 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License + */ +package org.apache.unomi.itests; + +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.unomi.api.Event; +import org.apache.unomi.api.Profile; +import org.apache.unomi.api.Session; +import org.apache.unomi.shell.migration.utils.HttpUtils; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.ops4j.pax.exam.junit.PaxExam; +import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; +import org.ops4j.pax.exam.spi.reactors.PerSuite; + +import java.util.Date; + +/** + * Verifies that the event index actually rolls over once it crosses the configured + * rollover.maxDocs threshold, on both Elasticsearch (ILM) and OpenSearch (ISM) - see UNOMI-946/947. + *

+ * The IT config sets rollover.maxDocs=300 for both engines. The ILM/ISM background sweep that checks + * this threshold defaults to 10 minutes (ES) / 5 minutes (OS), so this test temporarily lowers the + * check interval to make the assertion feasible within a normal test timeout. + */ +@RunWith(PaxExam.class) +@ExamReactorStrategy(PerSuite.class) +public class RolloverIT extends BaseIT { + + private static final int EVENTS_TO_CREATE = 320; + private static final String PROFILE_ID = "rollover-it-profile"; + private static final String SESSION_ID = "rollover-it-session"; + private static final String EVENT_INDEX_PATTERN = "context-event-*"; + + @After + public void tearDown() throws Exception { + TestUtils.removeAllEvents(definitionsService, persistenceService); + TestUtils.removeAllSessions(definitionsService, persistenceService); + TestUtils.removeAllProfiles(definitionsService, persistenceService); + setPolicyCheckInterval(null); + } + + @Test + public void testEventIndexRollsOverPastMaxDocsThreshold() throws Exception { + setPolicyCheckInterval(SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) ? "1" : "1s"); + + Profile profile = new Profile(PROFILE_ID); + persistenceService.save(profile); + Session session = new Session(SESSION_ID, profile, new Date(), "rollover-it-scope"); + persistenceService.save(session); + + for (int i = 0; i < EVENTS_TO_CREATE; i++) { + persistenceService.save( + new Event("rollover-it-event-" + i, "view", session, profile, "rollover-it-scope", null, null, new Date())); + } + + // ISM's job interval is minute-granularity (vs ILM's second-granularity), so OpenSearch needs a + // much longer allowance to notice the crossed threshold and complete the rollover. + int retries = SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) ? 45 : 15; + keepTrying("Event index did not roll over to a second index after crossing the rollover.maxDocs threshold", + this::countEventRolloverIndices, count -> count >= 2, 2000, retries); + } + + private int countEventRolloverIndices() { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String indicesJson = HttpUtils.executeGetRequest(client, + getSearchEngineBaseUrl() + "/_cat/indices/" + EVENT_INDEX_PATTERN + "?h=index&format=json", null); + if (indicesJson == null || indicesJson.isBlank()) { + return 0; + } + JsonNode indices = getObjectMapper().readTree(indicesJson); + return indices.isArray() ? indices.size() : 0; + } catch (Exception e) { + throw new RuntimeException("Failed to count rollover indices matching " + EVENT_INDEX_PATTERN, e); + } + } + + private void setPolicyCheckInterval(String value) throws Exception { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String settingKey = SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) + ? "plugins.index_state_management.job_interval" + : "indices.lifecycle.poll_interval"; + String settingValue; + if (value == null) { + settingValue = "null"; + } else if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { + // job_interval is a plain integer number of minutes, not a quoted time value like poll_interval. + settingValue = value; + } else { + settingValue = "\"" + value + "\""; + } + String settingsBody = "{\"persistent\": {\"" + settingKey + "\": " + settingValue + "}}"; + HttpUtils.executePutRequest(client, getSearchEngineBaseUrl() + "/_cluster/settings", settingsBody, null); + } + } +} diff --git a/itests/src/test/java/org/apache/unomi/itests/TestTimingCache.java b/itests/src/test/java/org/apache/unomi/itests/TestTimingCache.java new file mode 100644 index 000000000..99e05e802 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/TestTimingCache.java @@ -0,0 +1,166 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.itests; + +import org.junit.runner.Description; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; + +/** + * Local, best-effort cache of individual IT execution times, used by {@link ProgressListener} to make its + * estimated-time-remaining calculation more accurate than a single flat running average. + *

+ * The cache is a plain properties file kept in the {@code itests} module directory (not {@code target/}) so + * it survives {@code mvn clean}, with one file per search engine since Elasticsearch and OpenSearch runs have + * different timing profiles and shouldn't be averaged together. + *

+ * This is a local developer convenience, not build state: all I/O failures are swallowed so a missing or + * unwritable cache (e.g. a read-only or ephemeral CI workspace) never fails the IT run - it just falls back + * to the flat average for every test, matching the prior behavior. + */ +final class TestTimingCache { + + private static final Logger LOGGER = LoggerFactory.getLogger(TestTimingCache.class); + + /** Weight given to a freshly observed duration when blending it into the persisted average. */ + private static final double SMOOTHING = 0.3; + + private TestTimingCache() { + } + + /** + * Builds the cache key correlating a completed JUnit test with a persisted timing entry. + * + * @param description the description of the test that just ran + * @return a "SimpleClassName#methodName" key + */ + static String keyFor(Description description) { + String displayName = description.getDisplayName(); + if (displayName.contains("(") && displayName.contains(")")) { + int methodEnd = displayName.indexOf('('); + int classStart = methodEnd + 1; + int classEnd = displayName.indexOf(')'); + if (methodEnd > 0 && classEnd > classStart) { + String methodName = displayName.substring(0, methodEnd); + String className = displayName.substring(classStart, classEnd); + int lastDot = className.lastIndexOf('.'); + String simpleClassName = (lastDot >= 0) ? className.substring(lastDot + 1) : className; + return simpleClassName + "#" + methodName; + } + } + return displayName; + } + + /** + * Builds the cache key for a test method discovered via reflection, before it has ever run. + * + * @param testClass the concrete test class the method will run on + * @param methodName the {@code @Test} method name + * @return a "SimpleClassName#methodName" key, matching {@link #keyFor(Description)} + */ + static String keyFor(Class testClass, String methodName) { + return testClass.getSimpleName() + "#" + methodName; + } + + /** + * Loads the previously persisted timings for the given search engine. + * + * @param searchEngine the search engine the current run targets (e.g. "elasticsearch", "opensearch") + * @return a mutable map of cache key to last known duration in milliseconds; empty if no cache + * exists yet or it could not be read + */ + static Map load(String searchEngine) { + Map timings = new HashMap<>(); + Path cacheFile = cacheFile(searchEngine); + if (!Files.isReadable(cacheFile)) { + return timings; + } + Properties props = new Properties(); + try (Reader reader = Files.newBufferedReader(cacheFile, StandardCharsets.UTF_8)) { + props.load(reader); + } catch (IOException e) { + LOGGER.debug("Unable to read test timing cache at {} (ETAs will use the in-run average instead): {}", + cacheFile, e.getMessage()); + return timings; + } + for (String key : props.stringPropertyNames()) { + try { + timings.put(key, Long.parseLong(props.getProperty(key))); + } catch (NumberFormatException e) { + // Ignore a malformed entry rather than failing the whole cache load + } + } + return timings; + } + + /** + * Merges freshly observed durations into the persisted cache for the given search engine, smoothing + * each updated entry with an exponential moving average so a single unusually slow/fast run doesn't + * swing future ETAs too far. + * + * @param searchEngine the search engine the run just executed against + * @param observedTimings durations (in milliseconds) observed during the run that just finished + */ + static void save(String searchEngine, Map observedTimings) { + if (observedTimings.isEmpty()) { + return; + } + Path cacheFile = cacheFile(searchEngine); + try { + Map merged = load(searchEngine); + for (Map.Entry entry : observedTimings.entrySet()) { + Long previous = merged.get(entry.getKey()); + long updated = previous == null + ? entry.getValue() + : Math.round(previous * (1 - SMOOTHING) + entry.getValue() * SMOOTHING); + merged.put(entry.getKey(), updated); + } + Properties props = new Properties(); + merged.forEach((key, value) -> props.setProperty(key, String.valueOf(value))); + + Path parent = cacheFile.toAbsolutePath().getParent(); + Path tempFile = Files.createTempFile(parent, "test-timing-cache", ".tmp"); + try (Writer writer = Files.newBufferedWriter(tempFile, StandardCharsets.UTF_8)) { + props.store(writer, "Apache Unomi IT test timing cache (local dev aid, safe to delete)"); + } + Files.move(tempFile, cacheFile, StandardCopyOption.REPLACE_EXISTING); + } catch (IOException | RuntimeException e) { + LOGGER.debug("Unable to persist test timing cache at {} (ETAs will just use the in-run average next time): {}", + cacheFile, e.getMessage()); + } + } + + private static Path cacheFile(String searchEngine) { + String normalizedEngine = (searchEngine == null || searchEngine.isEmpty()) + ? "unknown" + : searchEngine.toLowerCase(Locale.ROOT); + return Paths.get(System.getProperty("user.dir", "."), ".test-timing-cache-" + normalizedEngine + ".properties"); + } +} diff --git a/manual/src/main/asciidoc/building-and-deploying.adoc b/manual/src/main/asciidoc/building-and-deploying.adoc index acb54aa16..a5a0e0c1f 100644 --- a/manual/src/main/asciidoc/building-and-deploying.adoc +++ b/manual/src/main/asciidoc/building-and-deploying.adoc @@ -238,45 +238,6 @@ mvn jgitflow:feature-finish To merge the branch into master. -==== Maven Build Cache - -Apache Unomi uses the Maven Build Cache extension to significantly improve build efficiency by caching compiled artifacts and avoiding unnecessary recompilation (build time without/with cached artifacts ~2mn/~3s). - -The build cache is enabled by default and configured in the `.mvn/maven-build-cache-config.xml` file. The cache configuration includes: - -* Local caching with up to 3 builds cached -* SHA-256 hash algorithm for cache keys -* Includes all source directories (`src/`) -* Excludes `pom.xml` files and generated sources (`src/main/javagen/**`) - -Command line control: - -* Disable cache: `mvn -Dmaven.build.cache.enabled=false clean install` - ** Completely turns off the build cache functionality - ** Maven will not store or retrieve any build outputs from the cache - ** Performs a full build as if the cache were not present - ** Use this when you want to ensure no cache influence on the build - -* Skip cache (force rebuild): `mvn -Dmaven.build.cache.skipCache=true clean install` - ** Skips looking up artifacts in caches but still writes new results to cache - ** Forces Maven to rebuild everything without using cached artifacts - ** New build results will be stored in cache for future builds - ** Use this to force a complete rebuild while keeping cache functionality active - -* Enable cache (default): `mvn -Dmaven.build.cache.enabled=true clean install` - ** Enables full cache functionality (read and write) - ** Maven will use cached artifacts when available and store new results - ** This is the default behavior when the cache is enabled - -Purging the build cache: - -To completely reset the build cache and force a full rebuild, manually delete the cache directory: `rm -rf ~/.m2/build-cache` (Unix/Mac) or `rmdir /s %USERPROFILE%\.m2\build-cache` (Windows) - -For more information, see the official documentation: - -* https://maven.apache.org/extensions/maven-build-cache-extension/[Maven Build Cache Extension Overview] -* https://maven.apache.org/extensions/maven-build-cache-extension/parameters.html[Build Cache Parameters Reference] - ==== Installing a Search Engine Starting with version 1.2, Apache Unomi no longer embeds a search engine server. You will need to install either ElasticSearch or OpenSearch as a standalone service. diff --git a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java index dc90146c2..411273647 100644 --- a/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java +++ b/persistence-elasticsearch/core/src/main/java/org/apache/unomi/persistence/elasticsearch/ElasticSearchPersistenceServiceImpl.java @@ -1538,8 +1538,10 @@ private void internalCreateRolloverTemplate(String itemName) throws IOException IndexSettings indexSettings = buildIndexSettings(rolloverAlias, analysis); IndexTemplateMapping templateMapping = buildTemplateMapping(itemName, indexSettings); + // Priority must stay above any generic/catch-all templates (e.g. IT harnesses registering their own + // "*"-pattern template) - composable templates reject ambiguous same-priority overlapping patterns. PutIndexTemplateRequest request = PutIndexTemplateRequest.of(builder -> builder.name(templateName) - .indexPatterns(Collections.singletonList(getRolloverIndexForQuery(itemName))).template(templateMapping).priority(1L)); + .indexPatterns(Collections.singletonList(getRolloverIndexForQuery(itemName))).template(templateMapping).priority(100L)); PutIndexTemplateResponse response = esClient.indices().putIndexTemplate(request); if (!response.acknowledged()) { @@ -1606,8 +1608,14 @@ private void internalCreateRolloverIndex(String indexName) throws IOException { long delayMs = 200; while (retryCount < maxRetries) { - // Wait for cluster state to be ready - esClient.cluster().health(builder -> builder.waitForStatus(HealthStatus.Green).timeout(t -> t.time("5s"))); + // Best-effort wait for cluster state to settle; a single-node cluster may legitimately never + // reach green (see UNOMI-946), so a timeout here is not fatal - the real correctness check is + // the settings/mappings verification below. + try { + esClient.cluster().health(builder -> builder.waitForStatus(HealthStatus.Green).timeout(t -> t.time("5s"))); + } catch (Exception e) { + LOGGER.debug("Cluster did not reach green before creating rollover index {}, continuing anyway", fullIndexName, e); + } // Delay to allow cluster state to synchronize - increase delay on each retry try { diff --git a/persistence-opensearch/core/pom.xml b/persistence-opensearch/core/pom.xml index efff4c998..90adbc735 100644 --- a/persistence-opensearch/core/pom.xml +++ b/persistence-opensearch/core/pom.xml @@ -178,6 +178,8 @@ software.amazon.awssdk.*;resolution:=optional, sun.misc;resolution:=optional, sun.nio.ch;resolution:=optional, + tools.jackson.core*;resolution:=optional, + tools.jackson.databind*;resolution:=optional, * diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java index 05c2c50c6..e211c922b 100644 --- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java +++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.Json; -import jakarta.json.JsonArrayBuilder; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.stream.JsonParser; @@ -60,6 +59,7 @@ import org.opensearch.client.opensearch.OpenSearchClient; import org.opensearch.client.opensearch._types.*; import org.opensearch.client.opensearch._types.aggregations.*; +import org.opensearch.client.opensearch._types.analysis.CustomAnalyzer; import org.opensearch.client.opensearch._types.mapping.TypeMapping; import org.opensearch.client.opensearch._types.query_dsl.Query; import org.opensearch.client.opensearch.cluster.HealthRequest; @@ -75,6 +75,7 @@ import org.opensearch.client.opensearch.generic.Response; import org.opensearch.client.opensearch.indices.*; import org.opensearch.client.opensearch.indices.get_alias.IndexAliases; +import org.opensearch.client.opensearch.indices.put_index_template.IndexTemplateMapping; import org.opensearch.client.opensearch.tasks.GetTasksResponse; import org.opensearch.client.transport.OpenSearchTransport; import org.opensearch.client.transport.endpoints.BooleanResponse; @@ -1338,7 +1339,9 @@ public boolean registerRolloverLifecyclePolicy() { Boolean result = new InClassLoaderExecute(metricsService, this.getClass().getName() + ".createRolloverLifecyclePolicy", this.bundleContext, this.fatalIllegalStateErrors, throwExceptions) { protected Boolean execute(Object... args) throws IOException { try { - String policyName = indexPrefix + "-rollover-lifecycle-policy"; + // Same name computation as internalCreateRolloverTemplate's policy_id custom setting, + // and mirrors ES's registerRolloverLifecyclePolicy policy naming. + String policyName = indexPrefix + "-" + ROLLOVER_LIFECYCLE_NAME; // Upon initial OpenSearch startup, the .opendistro-ism-config index may not exist yet // Check if the .opendistro-ism-config index exists @@ -1407,17 +1410,11 @@ protected Boolean execute(Object... args) throws IOException { .add("default_state", "ingest") .add("description", "Rollover lifecycle policy"); - // Add ISM template if rollover indices are specified - if (rolloverIndices != null && !rolloverIndices.isEmpty()) { - JsonArrayBuilder indexPatternsBuilder = Json.createArrayBuilder(); - for (String pattern : rolloverIndices) { - indexPatternsBuilder.add(pattern); - } - - policyBuilder.add("ism_template", Json.createObjectBuilder() - .add("index_patterns", indexPatternsBuilder) - .add("priority", 100)); - } + // Policy attachment is handled deterministically via the plugins.index_state_management.policy_id + // custom setting on each per-item rollover index template (internalCreateRolloverTemplate), + // not via ism_template pattern-matching: rolloverIndices holds item type names (e.g. "event"), + // which never match real index names (e.g. "context-event-000001"), so an ism_template block + // here would never auto-attach the policy. // Wrap the policy and create JsonObject policyWrapper = Json.createObjectBuilder() @@ -1488,50 +1485,134 @@ private void internalCreateRolloverTemplate(String itemName) throws IOException LOGGER.warn("Couldn't find mapping for item {}, won't create rollover index template", itemName); return; } - String indexSource = - " {" + - " \"number_of_shards\" : " + rolloverIndexNumberOfShards + "," + - " \"number_of_replicas\" : " + rolloverIndexNumberOfReplicas + "," + - " \"mapping.total_fields.limit\" : " + rolloverIndexMappingTotalFieldsLimit + "," + - " \"max_docvalue_fields_search\" : " + rolloverIndexMaxDocValueFieldsSearch + "," + - " \"plugins.index_state_management.rollover_alias\": \"" + rolloverAlias + "\"" + - " },"; - String analysisSource = - " {" + - " \"analyzer\": {" + - " \"folding\": {" + - " \"type\":\"custom\"," + - " \"tokenizer\": \"keyword\"," + - " \"filter\": [ \"lowercase\", \"asciifolding\" ]" + - " }\n" + - " }\n" + - " }\n"; - Map settings = new HashMap<>(); - settings.put("index", getJsonData(indexSource)); - settings.put("analysis", getJsonData(analysisSource)); - client.indices().putTemplate(p-> - p.name(rolloverAlias + "-rollover-template") - .indexPatterns(Collections.singletonList(getRolloverIndexForQuery(itemName))) - .order(1) - .settings(settings) - .mappings(getTypeMapping(mappings.get(itemName))) - ); - } + String templateName = rolloverAlias + "-rollover-template"; - private JsonData getJsonData(String input) { - JsonpMapper mapper = client._transport().jsonpMapper(); - JsonParser parser = mapper - .jsonProvider() - .createParser(new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8))); - return JsonData._DESERIALIZER.deserialize(parser, mapper); + IndexSettingsAnalysis analysis = IndexSettingsAnalysis.of(an -> an.analyzer("folding", + analyzerBuilder -> analyzerBuilder.custom(CustomAnalyzer.of( + customAnalyzer -> customAnalyzer.tokenizer("keyword").filter("lowercase", "asciifolding"))))); + + // plugins.index_state_management.rollover_alias and .policy_id are OpenSearch ISM settings with no + // typed builder equivalent (they are not the same as the ElasticSearch-only index.lifecycle.* + // ILM settings), so they are passed through as raw custom settings. Setting policy_id directly here, + // rather than relying solely on the policy's ism_template pattern-matching, mirrors how ES binds + // index.lifecycle.name per-template so every rollover index is deterministically attached. + IndexSettings indexSettings = IndexSettings.of(builder -> builder + .numberOfShards(Integer.valueOf(rolloverIndexNumberOfShards)) + .numberOfReplicas(Integer.valueOf(rolloverIndexNumberOfReplicas)) + .mapping(m -> m.totalFields(t -> t.limit(Long.valueOf(rolloverIndexMappingTotalFieldsLimit)))) + .maxDocvalueFieldsSearch(Integer.valueOf(rolloverIndexMaxDocValueFieldsSearch)) + .customSettings("plugins.index_state_management.rollover_alias", JsonData.of(rolloverAlias)) + .customSettings("plugins.index_state_management.policy_id", JsonData.of(indexPrefix + "-" + ROLLOVER_LIFECYCLE_NAME)) + .analysis(analysis)); + + IndexTemplateMapping templateMapping = IndexTemplateMapping.of( + t -> t.settings(indexSettings).mappings(getTypeMapping(mappings.get(itemName)))); + + // Priority must stay above any generic/catch-all templates (e.g. IT harnesses registering their own + // "*"-pattern template) - composable templates reject ambiguous same-priority overlapping patterns. + PutIndexTemplateRequest request = PutIndexTemplateRequest.of(builder -> builder.name(templateName) + .indexPatterns(Collections.singletonList(getRolloverIndexForQuery(itemName))).template(templateMapping) + .priority(100)); + + PutIndexTemplateResponse response = client.indices().putIndexTemplate(request); + if (!response.acknowledged()) { + throw new IOException("Failed to create index template " + templateName + " - not acknowledged"); + } } private void internalCreateRolloverIndex(String indexName) throws IOException { - CreateIndexResponse createIndexResponse = client.indices().create(c->c - .index(indexName + "-000001") - .aliases(indexName, a->a.isWriteIndex(true))); - LOGGER.info("Index created: [{}], acknowledge: [{}], shards acknowledge: [{}]", createIndexResponse.index(), - createIndexResponse.acknowledged(), createIndexResponse.shardsAcknowledged()); + String fullIndexName = indexName + "-000001"; + + // Retry mechanism to ensure the template is actually applied, not just that it exists in metadata + // (mirrors ElasticSearchPersistenceServiceImpl.internalCreateRolloverIndex, added there because + // cluster state may not be fully synchronized even though the template exists in metadata). + int maxRetries = 3; + int retryCount = 0; + long delayMs = 200; + + while (retryCount < maxRetries) { + client.cluster().health(builder -> builder.waitForStatus(getHealthStatus(minimalClusterState)).timeout(t -> t.time("5s"))); + + try { + Thread.sleep(delayMs * (retryCount + 1)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException("Interrupted while waiting for template propagation", e); + } + + if (retryCount > 0) { + try { + BooleanResponse exists = client.indices().exists(e -> e.index(fullIndexName)); + if (exists.value()) { + client.indices().delete(d -> d.index(fullIndexName)); + LOGGER.debug("Deleted index {} before retry {}", fullIndexName, retryCount); + } + } catch (IOException e) { + LOGGER.warn("Failed to delete index {} before retry: {}", fullIndexName, e.getMessage()); + } + } + + CreateIndexResponse createIndexResponse = client.indices().create(c -> c + .index(fullIndexName) + .aliases(indexName, a -> a.isWriteIndex(true))); + LOGGER.info("Index created: [{}], acknowledge: [{}], shards acknowledge: [{}]", createIndexResponse.index(), + createIndexResponse.acknowledged(), createIndexResponse.shardsAcknowledged()); + + GetIndicesSettingsResponse settingsResponse = client.indices().getSettings(g -> g.index(fullIndexName)); + GetMappingResponse mappingResponse = client.indices().getMapping(g -> g.index(fullIndexName)); + + IndexState indexState = settingsResponse.get(fullIndexName); + var indexMappingRecord = mappingResponse.get(fullIndexName); + + if (indexState == null || indexState.settings() == null) { + LOGGER.warn("Could not retrieve index settings for {} to verify template application. Retrying...", fullIndexName); + retryCount++; + if (retryCount < maxRetries) { + continue; + } else { + throw new IOException("Could not retrieve index settings for " + fullIndexName + " after " + maxRetries + " attempts"); + } + } + + if (indexMappingRecord == null || indexMappingRecord.mappings() == null) { + LOGGER.warn("Could not retrieve index mappings for {} to verify template application. Retrying...", fullIndexName); + retryCount++; + if (retryCount < maxRetries) { + continue; + } else { + throw new IOException("Could not retrieve index mappings for " + fullIndexName + " after " + maxRetries + " attempts"); + } + } + + boolean hasFoldingAnalyzer = false; + IndexSettings nestedIndexSettings = indexState.settings().index(); + IndexSettingsAnalysis analysis = nestedIndexSettings != null ? nestedIndexSettings.analysis() : null; + if (analysis != null && analysis.analyzer() != null && analysis.analyzer().get("folding") != null) { + hasFoldingAnalyzer = true; + } + + boolean hasDynamicTemplates = false; + var dynamicTemplates = indexMappingRecord.mappings().dynamicTemplates(); + if (dynamicTemplates != null && !dynamicTemplates.isEmpty()) { + hasDynamicTemplates = true; + } + + if (hasFoldingAnalyzer && hasDynamicTemplates) { + LOGGER.debug("Template successfully applied to index {} - folding analyzer and dynamic templates present", fullIndexName); + return; + } else { + LOGGER.warn("Template not applied to index {} - folding analyzer: {}, dynamic templates: {}. Retrying...", + fullIndexName, hasFoldingAnalyzer, hasDynamicTemplates); + retryCount++; + if (retryCount < maxRetries) { + continue; + } else { + throw new IOException("Template was not applied to index " + fullIndexName + + " after " + maxRetries + " attempts. Folding analyzer: " + hasFoldingAnalyzer + + ", Dynamic templates: " + hasDynamicTemplates); + } + } + } } private void internalCreateIndex(String indexName, String mappingSource) throws IOException { diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelper.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelper.java index e338fafea..86a8aebe4 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelper.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelper.java @@ -21,7 +21,7 @@ import org.apache.unomi.api.Parameter; import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.services.DefinitionsService; +import org.apache.unomi.api.services.ValueTypeValidator; import org.apache.unomi.scripting.ScriptExecutor; import org.apache.unomi.tracing.api.RequestTracer; import org.apache.unomi.tracing.api.TracerService; @@ -116,25 +116,26 @@ private static void loadMappingFile() throws IOException { } public static Condition getContextualCondition(Condition condition, Map context, ScriptExecutor scriptExecutor) { - return getContextualCondition(condition, context, scriptExecutor, null, null); + return getContextualCondition(condition, context, scriptExecutor, false, null, null); } /** * Resolves parameter references and script expressions in a condition, - * with optional type validation. + * with optional resolved-parameter type validation. * * @param condition the condition to resolve * @param context context map for parameter resolution * @param scriptExecutor executor for script expressions - * @param definitionsService optional service for parameter type information + * @param validateParameterTypes when {@code true}, warn on resolved values that do not match the + * condition type parameter definitions * @return resolved condition with all parameter references resolved */ public static Condition getContextualCondition( Condition condition, Map context, ScriptExecutor scriptExecutor, - DefinitionsService definitionsService) { - return getContextualCondition(condition, context, scriptExecutor, definitionsService, null); + boolean validateParameterTypes) { + return getContextualCondition(condition, context, scriptExecutor, validateParameterTypes, null, null); } /** @@ -144,7 +145,7 @@ public static Condition getContextualCondition( * @param condition the condition to resolve * @param context context map for parameter resolution * @param scriptExecutor executor for script expressions - * @param definitionsService optional service for parameter type information + * @param validateParameterTypes when {@code true}, warn on resolved parameter type mismatches * @param tracerService optional tracer service for validation warnings * @return resolved condition with all parameter references resolved */ @@ -152,12 +153,36 @@ public static Condition getContextualCondition( Condition condition, Map context, ScriptExecutor scriptExecutor, - DefinitionsService definitionsService, + boolean validateParameterTypes, TracerService tracerService) { + return getContextualCondition(condition, context, scriptExecutor, validateParameterTypes, tracerService, null); + } + + /** + * Resolves parameter references and script expressions in a condition, + * with optional type validation, execution tracing, and value-type validators. + * + * @param condition the condition to resolve + * @param context context map for parameter resolution + * @param scriptExecutor executor for script expressions + * @param validateParameterTypes when {@code true}, warn on resolved parameter type mismatches + * @param tracerService optional tracer service for validation warnings + * @param valueTypeValidators optional validators keyed by {@link ValueTypeValidator#getValueTypeId()} (lowercase); + * when {@code null}, uses {@link ValueTypeValidatorRegistry#getValidators()} + * @return resolved condition with all parameter references resolved + */ + public static Condition getContextualCondition( + Condition condition, + Map context, + ScriptExecutor scriptExecutor, + boolean validateParameterTypes, + TracerService tracerService, + Map valueTypeValidators) { - // Debug logging + Map effectiveValidators = resolveValidators(valueTypeValidators); - if (!hasContextualParameter(condition.getParameterValues())) { + boolean needsResolution = hasContextualParameter(condition.getParameterValues()); + if (!needsResolution && !validateParameterTypes) { return condition; } @@ -177,18 +202,20 @@ public static Condition getContextualCondition( } } - ConditionType conditionType = condition.getConditionType(); Map parameterDefs = null; - if (conditionType != null && definitionsService != null) { - parameterDefs = new HashMap<>(); - for (Parameter param : conditionType.getParameters()) { - parameterDefs.put(param.getId(), param); + if (validateParameterTypes) { + ConditionType conditionType = condition.getConditionType(); + if (conditionType != null && conditionType.getParameters() != null) { + parameterDefs = new HashMap<>(); + for (Parameter param : conditionType.getParameters()) { + parameterDefs.put(param.getId(), param); + } } } Object rawValues = parseParameterWithValidation( context, condition.getParameterValues(), scriptExecutor, - parameterDefs, tracerService, condition.getConditionTypeId()); + parameterDefs, tracerService, condition.getConditionTypeId(), effectiveValidators); if (rawValues == null || rawValues == RESOLUTION_ERROR) { return null; @@ -203,7 +230,11 @@ public static Condition getContextualCondition( @SuppressWarnings("unchecked") private static Object parseParameter(Map context, Object value, ScriptExecutor scriptExecutor) { - return parseParameterWithValidation(context, value, scriptExecutor, null, null, null); + return parseParameterWithValidation(context, value, scriptExecutor, null, null, null, resolveValidators(null)); + } + + private static Map resolveValidators(Map valueTypeValidators) { + return valueTypeValidators != null ? valueTypeValidators : ValueTypeValidatorRegistry.getValidators(); } @SuppressWarnings("unchecked") @@ -213,11 +244,12 @@ private static Object parseParameterWithValidation( ScriptExecutor scriptExecutor, Map parameterDefs, TracerService tracerService, - String conditionTypeId) { + String conditionTypeId, + Map valueTypeValidators) { return parseParameterWithValidationRecursive( context, value, scriptExecutor, parameterDefs, - tracerService, conditionTypeId, new HashSet<>(), 0); + tracerService, conditionTypeId, valueTypeValidators, new HashSet<>(), 0); } /** @@ -242,6 +274,7 @@ private static Object parseParameterWithValidationRecursive( Map parameterDefs, TracerService tracerService, String conditionTypeId, + Map valueTypeValidators, Set resolutionChain, int depth) { @@ -315,7 +348,7 @@ private static Object parseParameterWithValidationRecursive( if (resolvedValue != null && isParameterReference(resolvedValue)) { Object furtherResolved = parseParameterWithValidationRecursive( context, resolvedValue, scriptExecutor, parameterDefs, - tracerService, conditionTypeId, resolutionChain, depth + 1); + tracerService, conditionTypeId, valueTypeValidators, resolutionChain, depth + 1); // If further resolution returns null due to cycle or max depth, propagate it // But if it's just a missing parameter, return null (not a cycle) return furtherResolved; @@ -336,7 +369,7 @@ private static Object parseParameterWithValidationRecursive( Object parameter = parseParameterWithValidationRecursive( context, paramValue, scriptExecutor, parameterDefs, - tracerService, conditionTypeId, resolutionChain, depth); + tracerService, conditionTypeId, valueTypeValidators, resolutionChain, depth); // If resolution returned an error marker, return null for entire map if (parameter == RESOLUTION_ERROR) { @@ -346,7 +379,7 @@ private static Object parseParameterWithValidationRecursive( // Validate type if parameter definition is available and value is not null if (parameter != null && paramDef != null && paramDef.getType() != null) { validateParameterType(paramName, parameter, paramDef, - conditionTypeId, tracerService); + conditionTypeId, tracerService, valueTypeValidators); } // Always put the value, even if null (missing parameter is valid) @@ -358,7 +391,7 @@ private static Object parseParameterWithValidationRecursive( for (Object o : ((List) value)) { Object parameter = parseParameterWithValidationRecursive( context, o, scriptExecutor, parameterDefs, - tracerService, conditionTypeId, resolutionChain, depth); + tracerService, conditionTypeId, valueTypeValidators, resolutionChain, depth); // If resolution returned an error marker, skip this element if (parameter == RESOLUTION_ERROR) { continue; @@ -386,33 +419,108 @@ private static void validateParameterType( Object resolvedValue, Parameter paramDef, String conditionTypeId, - TracerService tracerService) { + TracerService tracerService, + Map valueTypeValidators) { if (resolvedValue == null || paramDef == null || paramDef.getType() == null) { return; } - String expectedType = paramDef.getType().toLowerCase(); + if (paramDef.isMultivalued()) { + if (!(resolvedValue instanceof Collection)) { + String message = String.format( + "Parameter '%s' in condition type '%s' must be a collection for multivalued parameter", + paramName, conditionTypeId); + LOGGER.warn(message + " (value type: {})", getValueType(resolvedValue)); + traceParameterTypeMismatch(tracerService, paramName, "collection", getValueType(resolvedValue), + resolvedValue, conditionTypeId); + return; + } + int index = 0; + for (Object item : (Collection) resolvedValue) { + if (item != null) { + validateSingleParameterValue( + paramName + "[" + index + "]", item, paramDef, conditionTypeId, tracerService, valueTypeValidators); + } + index++; + } + return; + } + + if (resolvedValue instanceof Collection) { + String message = String.format( + "Parameter '%s' in condition type '%s' cannot be a collection for non-multivalued parameter", + paramName, conditionTypeId); + LOGGER.warn(message + " (value: {})", resolvedValue); + traceParameterTypeMismatch(tracerService, paramName, paramDef.getType().toLowerCase(Locale.ROOT), + "collection", resolvedValue, conditionTypeId); + return; + } + + validateSingleParameterValue(paramName, resolvedValue, paramDef, conditionTypeId, tracerService, valueTypeValidators); + } + + private static void validateSingleParameterValue( + String paramName, + Object resolvedValue, + Parameter paramDef, + String conditionTypeId, + TracerService tracerService, + Map valueTypeValidators) { + + String expectedType = paramDef.getType().toLowerCase(Locale.ROOT); + + ValueTypeValidator validator = valueTypeValidators.get(expectedType); + if (validator != null) { + if (!validator.validate(resolvedValue)) { + String message = String.format( + "Parameter '%s' in condition type '%s' has invalid value for type '%s'", + paramName, conditionTypeId, expectedType); + LOGGER.warn(message + " (value: {}). {}", resolvedValue, validator.getValueTypeDescription()); + traceParameterTypeMismatch(tracerService, paramName, expectedType, getValueType(resolvedValue), + resolvedValue, conditionTypeId); + } + return; + } + String actualType = getValueType(resolvedValue); - if (!isTypeCompatible(actualType, expectedType)) { + // No registered validator for this type (e.g. plain unit tests with no OSGi container running, + // so ValueTypeValidatorRegistry is empty). "date" parameters are almost always ISO-8601 strings + // here too, so fall back to the same canonical parser the built-in date validator uses, rather + // than the coarser actualType/expectedType string matching below. + boolean compatible = "date".equals(expectedType) + ? DateUtils.getDate(resolvedValue) != null + : isTypeCompatible(actualType, expectedType); + + if (!compatible) { String message = String.format( "Parameter '%s' in condition type '%s' resolved to type '%s' but expected '%s'", paramName, conditionTypeId, actualType, expectedType); LOGGER.warn(message + " (value: {})", resolvedValue); - if (tracerService != null && tracerService.isTracingEnabled()) { - RequestTracer tracer = tracerService.getCurrentTracer(); - if (tracer != null && tracer.isEnabled()) { - Map traceContext = new HashMap<>(); - traceContext.put("parameter", paramName); - traceContext.put("expectedType", expectedType); - traceContext.put("actualType", actualType); - traceContext.put("value", resolvedValue); - traceContext.put("conditionTypeId", conditionTypeId); - tracer.trace("Parameter type mismatch detected", traceContext); - } + traceParameterTypeMismatch(tracerService, paramName, expectedType, actualType, resolvedValue, conditionTypeId); + } + } + + private static void traceParameterTypeMismatch( + TracerService tracerService, + String paramName, + String expectedType, + String actualType, + Object resolvedValue, + String conditionTypeId) { + if (tracerService != null && tracerService.isTracingEnabled()) { + RequestTracer tracer = tracerService.getCurrentTracer(); + if (tracer != null && tracer.isEnabled()) { + Map traceContext = new HashMap<>(); + traceContext.put("parameter", paramName); + traceContext.put("expectedType", expectedType); + traceContext.put("actualType", actualType); + traceContext.put("value", resolvedValue); + traceContext.put("conditionTypeId", conditionTypeId); + tracer.trace("Parameter type mismatch detected", traceContext); } } } diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ValueTypeValidatorRegistry.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ValueTypeValidatorRegistry.java new file mode 100644 index 000000000..31818b394 --- /dev/null +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/ValueTypeValidatorRegistry.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.unomi.persistence.spi.conditions; + +import org.apache.unomi.api.services.ValueTypeValidator; +import org.osgi.service.component.annotations.Component; +import org.osgi.service.component.annotations.Reference; +import org.osgi.service.component.annotations.ReferenceCardinality; +import org.osgi.service.component.annotations.ReferencePolicy; + +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Holds OSGi-registered {@link ValueTypeValidator} services for runtime condition parameter validation. + *

+ * Used by {@link ConditionContextHelper} when callers do not pass an explicit validator map. + */ +@Component(service = ValueTypeValidatorRegistry.class, immediate = true) +public class ValueTypeValidatorRegistry { + + private static volatile ValueTypeValidatorRegistry instance; + + private final Map validatorsByTypeId = new ConcurrentHashMap<>(); + + public ValueTypeValidatorRegistry() { + instance = this; + } + + /** + * Returns the validators registered in the running container, keyed by lowercase type id. + * Returns an empty map when the registry component is not active (e.g. unit tests). + * + * @return validators keyed by lowercase type id + */ + public static Map getValidators() { + ValueTypeValidatorRegistry registry = instance; + if (registry == null) { + return Collections.emptyMap(); + } + return registry.validatorsByTypeId; + } + + @Reference(service = ValueTypeValidator.class, cardinality = ReferenceCardinality.MULTIPLE, policy = ReferencePolicy.DYNAMIC) + public void bindValidator(ValueTypeValidator validator) { + validatorsByTypeId.put(validator.getValueTypeId().toLowerCase(Locale.ROOT), validator); + } + + public void unbindValidator(ValueTypeValidator validator) { + validatorsByTypeId.remove(validator.getValueTypeId().toLowerCase(Locale.ROOT)); + } +} diff --git a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/evaluator/impl/ConditionEvaluatorDispatcherImpl.java b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/evaluator/impl/ConditionEvaluatorDispatcherImpl.java index 8c5b34cd0..d4be6b6a0 100644 --- a/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/evaluator/impl/ConditionEvaluatorDispatcherImpl.java +++ b/persistence-spi/src/main/java/org/apache/unomi/persistence/spi/conditions/evaluator/impl/ConditionEvaluatorDispatcherImpl.java @@ -186,7 +186,8 @@ public boolean eval(Condition condition, Item item, Map context) final ConditionEvaluatorDispatcher dispatcher = this; try { // Use effective condition for evaluation - Condition contextualCondition = ConditionContextHelper.getContextualCondition(effectiveCondition, context, scriptExecutor, definitionsService, tracerService); + Condition contextualCondition = ConditionContextHelper.getContextualCondition( + effectiveCondition, context, scriptExecutor, true, tracerService); if (contextualCondition == null) { if (tracer != null) { tracer.endOperation(false, "Contextual condition is null"); diff --git a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelperTest.java b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelperTest.java index b9577595c..3ee6e6f11 100644 --- a/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelperTest.java +++ b/persistence-spi/src/test/java/org/apache/unomi/persistence/spi/conditions/ConditionContextHelperTest.java @@ -19,15 +19,19 @@ import org.apache.unomi.api.Parameter; import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.conditions.ConditionType; -import org.apache.unomi.api.services.DefinitionsService; +import org.apache.unomi.api.services.ValueTypeValidator; import org.apache.unomi.scripting.ScriptExecutor; +import org.apache.unomi.tracing.api.RequestTracer; +import org.apache.unomi.tracing.api.TracerService; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.time.Instant; import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.*; @@ -44,8 +48,10 @@ public class ConditionContextHelperTest { private ScriptExecutor scriptExecutor; @Mock - private DefinitionsService definitionsService; + private TracerService tracerService; + @Mock + private RequestTracer requestTracer; private Map context; @@ -361,7 +367,7 @@ public void testTypeValidationWithCorrectType() { condition.setConditionType(conditionType); Condition resolved = ConditionContextHelper.getContextualCondition( - condition, context, scriptExecutor, definitionsService); + condition, context, scriptExecutor, true); assertNotNull("Resolved condition should not be null", resolved); assertEquals("Integer parameter should resolve correctly", 42, resolved.getParameterValues().get("testParam")); @@ -383,7 +389,7 @@ public void testTypeValidationWithTypeMismatch() { condition.setConditionType(conditionType); Condition resolved = ConditionContextHelper.getContextualCondition( - condition, context, scriptExecutor, definitionsService); + condition, context, scriptExecutor, true); assertNotNull("Resolved condition should not be null", resolved); // Type mismatch should log warning but still resolve @@ -407,7 +413,7 @@ public void testTypeValidationWithChain() { condition.setConditionType(conditionType); Condition resolved = ConditionContextHelper.getContextualCondition( - condition, context, scriptExecutor, definitionsService); + condition, context, scriptExecutor, true); assertNotNull("Resolved condition should not be null", resolved); assertEquals("Parameter chain should resolve and validate type correctly", 42, resolved.getParameterValues().get("testParam")); @@ -451,10 +457,8 @@ public void testConditionWithoutParameterReferences() { Condition condition = createConditionWithParameter("testParam", "directValue"); Condition resolved = ConditionContextHelper.getContextualCondition( condition, context, scriptExecutor); - - // Should return the same condition instance when no references present - assertNotNull("Resolved condition should not be null", resolved); - assertEquals("Condition without parameter references should remain unchanged", "directValue", resolved.getParameterValues().get("testParam")); + + assertSame("Condition without contextual parameters should be returned as-is", condition, resolved); } @Test @@ -528,8 +532,678 @@ public void testGetContextualCondition_nullScriptExecutor_scriptExpression_retur assertNull("getContextualCondition must return null when scriptExecutor is null and a script:: value is present", resolved); } + @Test + public void testRegisteredValueTypeValidatorAcceptsComparisonOperator() { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Arrays.asList( + new Parameter("propertyName", "string", false), + new Parameter("comparisonOperator", "comparisonOperator", false), + new Parameter("propertyValue", "string", false))); + + Condition condition = new Condition(conditionType); + Map params = new HashMap<>(); + params.put("propertyName", "eventType"); + params.put("comparisonOperator", "equals"); + params.put("propertyValue", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "eventTypeValue"); + condition.setParameterValues(params); + context.put("eventTypeValue", "view"); + + Map validators = Collections.singletonMap( + "comparisonoperator", + new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "comparisonOperator"; + } + + @Override + public boolean validate(Object value) { + return "equals".equals(value); + } + + @Override + public String getValueTypeDescription() { + return "Value must be a valid comparison operator"; + } + }); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, validators); + + assertNotNull(resolved); + assertEquals("equals", resolved.getParameter("comparisonOperator")); + assertEquals("view", resolved.getParameter("propertyValue")); + } + + @Test + public void testValueTypeValidatorRegistryUsedWhenExplicitMapIsNull() { + ValueTypeValidatorRegistry registry = new ValueTypeValidatorRegistry(); + ValueTypeValidator comparisonValidator = new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "comparisonOperator"; + } + + @Override + public boolean validate(Object value) { + return "equals".equals(value); + } + + @Override + public String getValueTypeDescription() { + return "Value must be a valid comparison operator"; + } + }; + registry.bindValidator(comparisonValidator); + try { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Arrays.asList( + new Parameter("propertyName", "string", false), + new Parameter("comparisonOperator", "comparisonOperator", false), + new Parameter("propertyValue", "string", false))); + + Condition condition = new Condition(conditionType); + Map params = new HashMap<>(); + params.put("propertyName", "eventType"); + params.put("comparisonOperator", "equals"); + params.put("propertyValue", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "eventTypeValue"); + condition.setParameterValues(params); + context.put("eventTypeValue", "view"); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null); + + assertNotNull(resolved); + assertEquals("equals", resolved.getParameter("comparisonOperator")); + } finally { + registry.unbindValidator(comparisonValidator); + } + } + + // ========== Public utility API tests ========== + + @Test + public void testIsParameterReference() { + assertTrue(ConditionContextHelper.isParameterReference(ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "key")); + assertTrue(ConditionContextHelper.isParameterReference(ConditionContextHelper.SCRIPT_EXPRESSION_PREFIX + "return 1;")); + assertFalse(ConditionContextHelper.isParameterReference("equals")); + assertFalse(ConditionContextHelper.isParameterReference(null)); + assertFalse(ConditionContextHelper.isParameterReference(42)); + assertFalse(ConditionContextHelper.isParameterReference("parameter:not-a-reference")); + } + + @Test + public void testHasContextualParameter() { + assertFalse(ConditionContextHelper.hasContextualParameter(null)); + assertFalse(ConditionContextHelper.hasContextualParameter("literal")); + assertFalse(ConditionContextHelper.hasContextualParameter(Collections.emptyMap())); + + Map withRef = Collections.singletonMap( + "k", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "x"); + assertTrue(ConditionContextHelper.hasContextualParameter(withRef)); + + List listWithScript = Collections.singletonList( + ConditionContextHelper.SCRIPT_EXPRESSION_PREFIX + "return true;"); + assertTrue(ConditionContextHelper.hasContextualParameter(listWithScript)); + + Condition nested = createConditionWithParameter("p", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "x"); + assertTrue(ConditionContextHelper.hasContextualParameter(nested)); + } + + @Test + public void testFoldToASCII() { + assertNull(ConditionContextHelper.foldToASCII((String) null)); + assertEquals("cafe", ConditionContextHelper.foldToASCII("Caf\u00E9")); + + String[] array = new String[] { "Caf\u00E9" }; + assertEquals("cafe", ConditionContextHelper.foldToASCII(array)[0]); + + Collection folded = ConditionContextHelper.foldToASCII( + new ArrayList<>(Collections.singletonList("Caf\u00E9"))); + assertEquals("cafe", folded.iterator().next()); + + assertEquals("cafe", ConditionContextHelper.forceFoldToASCII("Caf\u00E9")); + assertNull(ConditionContextHelper.forceFoldToASCII(null)); + } + + // ========== Context merge and resolution edge cases ========== + + @Test + public void testNullContextIsInitialized() { + String value = ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "literalParam"; + Condition condition = createConditionWithParameter("literalParam", "fromCondition"); + condition.getParameterValues().put("resolved", value); + + Condition resolved = ConditionContextHelper.getContextualCondition(condition, null, scriptExecutor); + + assertNotNull(resolved); + assertEquals("fromCondition", resolved.getParameter("resolved")); + } + + @Test + public void testConditionLiteralsMergedIntoContextButDoNotOverwrite() { + context.put("sharedKey", "contextWins"); + + Map params = new HashMap<>(); + params.put("sharedKey", "conditionValue"); + params.put("onlyOnCondition", "conditionOnly"); + params.put("resolved", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "onlyOnCondition"); + + Condition condition = new Condition(); + condition.setParameterValues(params); + + Condition resolved = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor); + + assertNotNull(resolved); + assertEquals("conditionOnly", resolved.getParameter("resolved")); + assertEquals("contextWins", context.get("sharedKey")); + } + + @Test + public void testReferenceToSiblingLiteralOnSameCondition() { + Map params = new HashMap<>(); + params.put("propertyValue", "view"); + params.put("propertyName", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "nameKey"); + params.put("nameKey", "eventType"); + + Condition condition = new Condition(); + condition.setParameterValues(params); + + Condition resolved = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor); + + assertNotNull(resolved); + assertEquals("eventType", resolved.getParameter("propertyName")); + } + + @Test + public void testResolutionDepthAtLimitSucceeds() { + for (int i = 1; i < 50; i++) { + context.put("param" + i, ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param" + (i + 1)); + } + context.put("param50", "finalValue"); + + Condition condition = createConditionWithParameter( + "testParam", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param1"); + Condition resolved = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor); + + assertNotNull(resolved); + assertEquals("finalValue", resolved.getParameter("testParam")); + } + + @Test + public void testCyclicReferenceInListSkipsElementButKeepsOthers() { + context.put("good", "ok"); + + List list = new ArrayList<>(); + list.add(ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "good"); + list.add(ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "bad"); + list.add("literal"); + context.put("bad", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "bad"); + + Condition condition = new Condition(); + condition.setParameterValues(Collections.singletonMap("items", list)); + + Condition resolved = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor); + + assertNotNull(resolved); + @SuppressWarnings("unchecked") + List resolvedList = (List) resolved.getParameter("items"); + assertEquals(2, resolvedList.size()); + assertEquals("ok", resolvedList.get(0)); + assertEquals("literal", resolvedList.get(1)); + } + + @Test + public void testCyclicReferenceInNestedMapNullsInnerMap() { + Map nested = new HashMap<>(); + nested.put("key", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "cycle"); + context.put("cycle", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "cycle"); + + Condition condition = new Condition(); + condition.setParameterValues(Collections.singletonMap("nested", nested)); + + Condition resolved = ConditionContextHelper.getContextualCondition(condition, context, scriptExecutor); + + assertNotNull(resolved); + assertNull(resolved.getParameter("nested")); + } + + @Test + public void testNestedConditionTriggersResolutionButIsNotDeepResolved() { + Condition inner = createConditionWithParameter( + "inner", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "value"); + context.put("value", "resolved"); + + Condition outer = new Condition(); + outer.setParameterValues(Collections.singletonMap("subCondition", inner)); + + Condition resolved = ConditionContextHelper.getContextualCondition(outer, context, scriptExecutor); + + assertNotNull(resolved); + assertTrue(resolved.getParameter("subCondition") instanceof Condition); + Condition resolvedInner = (Condition) resolved.getParameter("subCondition"); + assertSame(inner, resolvedInner); + assertEquals( + ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "value", + resolvedInner.getParameter("inner")); + } + + // ========== Type validation edge cases ========== + + @Test + public void testTypeCompatibilityLongForIntegerParameter() { + context.put("param1", 42L); + + Condition condition = createIntegerParameterCondition( + ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param1"); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertEquals(42L, resolved.getParameter("testParam")); + } + + @Test + public void testTypeCompatibilityDateTypes() { + Date now = new Date(); + context.put("param1", now); + + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("dateCondition"); + conditionType.setParameters(Collections.singletonList(new Parameter("testParam", "date", false))); + + Condition condition = createConditionWithParameter( + "testParam", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param1"); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertEquals(now, resolved.getParameter("testParam")); + } + + @Test + public void testTypeCompatibilityInstantForDateParameter() { + Instant instant = Instant.parse("2024-01-15T10:00:00Z"); + context.put("param1", instant); + + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("dateCondition"); + conditionType.setParameters(Collections.singletonList(new Parameter("testParam", "date", false))); + + Condition condition = createConditionWithParameter( + "testParam", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param1"); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertEquals(instant, resolved.getParameter("testParam")); + } + + @Test + public void testValidateParameterTypesDisabledSkipsValidation() { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Collections.singletonList( + new Parameter("comparisonOperator", "comparisonOperator", false))); + + Map params = new HashMap<>(); + params.put("comparisonOperator", "equals"); + params.put("other", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "missing"); + + Condition condition = new Condition(conditionType); + condition.setParameterValues(params); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, false); + + assertNotNull(resolved); + assertEquals("equals", resolved.getParameter("comparisonOperator")); + assertNull(resolved.getParameter("other")); + } + + // ========== Multivalued parameter validation ========== + + @Test + public void testMultivaluedStringListResolvesReferencesWithoutCollectionWarning() { + context.put("tagA", "news"); + context.put("tagB", "sports"); + + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Collections.singletonList( + new Parameter("propertyValues", "string", true))); + + List values = Arrays.asList( + ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "tagA", + ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "tagB", + "static"); + Condition condition = createConditionWithParameter("propertyValues", values); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + @SuppressWarnings("unchecked") + List resolvedValues = (List) resolved.getParameter("propertyValues"); + assertEquals(Arrays.asList("news", "sports", "static"), resolvedValues); + } + + @Test + public void testMultivaluedStringListValidatesEachElement() { + AtomicInteger validationCalls = new AtomicInteger(); + ValueTypeValidator stringValidator = new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "string"; + } + + @Override + public boolean validate(Object value) { + validationCalls.incrementAndGet(); + return value instanceof String; + } + + @Override + public String getValueTypeDescription() { + return "Value must be a string"; + } + }; + + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Collections.singletonList( + new Parameter("propertyValues", "string", true))); + + List values = Arrays.asList("a", 42, "c"); + Condition condition = createConditionWithParameter("propertyValues", values); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, + Collections.singletonMap("string", stringValidator)); + + assertNotNull(resolved); + assertEquals(3, validationCalls.get()); + } + + @Test + public void testMultivaluedScalarValueStillResolvesButIsInvalidShape() { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Collections.singletonList( + new Parameter("propertyValues", "string", true))); + + Condition condition = createConditionWithParameter("propertyValues", "onlyOne"); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertEquals("onlyOne", resolved.getParameter("propertyValues")); + } + + @Test + public void testNonMultivaluedCollectionStillResolvesButIsInvalidShape() { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Collections.singletonList( + new Parameter("propertyValue", "string", false))); + + Condition condition = createConditionWithParameter( + "propertyValue", Collections.singletonList("unexpected-list")); + condition.setConditionType(conditionType); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertEquals(Collections.singletonList("unexpected-list"), resolved.getParameter("propertyValue")); + } + + @Test + public void testMultivaluedConditionListValidatesEachElement() { + AtomicInteger validationCalls = new AtomicInteger(); + ValueTypeValidator conditionValidator = new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "condition"; + } + + @Override + public boolean validate(Object value) { + validationCalls.incrementAndGet(); + return value instanceof Condition; + } + + @Override + public String getValueTypeDescription() { + return "Value must be a condition"; + } + }; + + ConditionType innerType = new ConditionType(); + innerType.setItemId("eventPropertyCondition"); + + Condition sub1 = new Condition(innerType); + Condition sub2 = new Condition(innerType); + List subConditions = Arrays.asList(sub1, sub2, "not-a-condition"); + + ConditionType booleanType = new ConditionType(); + booleanType.setItemId("booleanCondition"); + booleanType.setParameters(Collections.singletonList( + new Parameter("subConditions", "Condition", true))); + + Condition condition = new Condition(booleanType); + condition.setParameterValues(Collections.singletonMap("subConditions", subConditions)); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, + Collections.singletonMap("condition", conditionValidator)); + + assertNotNull(resolved); + assertEquals(3, validationCalls.get()); + @SuppressWarnings("unchecked") + List resolvedSubs = (List) resolved.getParameter("subConditions"); + assertEquals(3, resolvedSubs.size()); + assertSame(sub1, resolvedSubs.get(0)); + assertSame(sub2, resolvedSubs.get(1)); + assertEquals("not-a-condition", resolvedSubs.get(2)); + } + + @Test + public void testMultivaluedConditionListTypeCheckWithoutCustomValidator() { + ConditionType innerType = new ConditionType(); + innerType.setItemId("eventPropertyCondition"); + + Condition sub = new Condition(innerType); + List subConditions = Arrays.asList(sub, "invalid"); + + ConditionType booleanType = new ConditionType(); + booleanType.setItemId("booleanCondition"); + booleanType.setParameters(Collections.singletonList( + new Parameter("subConditions", "Condition", true))); + + Condition condition = new Condition(booleanType); + condition.setParameterValues(Collections.singletonMap("subConditions", subConditions)); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, Collections.emptyMap()); + + assertNotNull(resolved); + @SuppressWarnings("unchecked") + List resolvedSubs = (List) resolved.getParameter("subConditions"); + assertEquals(2, resolvedSubs.size()); + assertTrue(resolvedSubs.get(0) instanceof Condition); + assertEquals("invalid", resolvedSubs.get(1)); + } + + @Test + public void testInvalidComparisonOperatorStillResolvesWithValidator() { + Condition condition = createEventPropertyConditionWithReference( + "notARealOperator", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "eventTypeValue"); + context.put("eventTypeValue", "view"); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, + Collections.singletonMap("comparisonoperator", comparisonOperatorValidator())); + + assertNotNull(resolved); + assertEquals("notARealOperator", resolved.getParameter("comparisonOperator")); + } + + @Test + public void testExplicitEmptyValidatorMapDoesNotUseRegistry() { + ValueTypeValidator validator = comparisonOperatorValidator(); + ValueTypeValidatorRegistry registry = new ValueTypeValidatorRegistry(); + registry.bindValidator(validator); + try { + Condition condition = createEventPropertyConditionWithReference( + "equals", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "eventTypeValue"); + context.put("eventTypeValue", "view"); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, + Collections.emptyMap()); + + assertNotNull(resolved); + assertEquals("equals", resolved.getParameter("comparisonOperator")); + } finally { + registry.unbindValidator(validator); + } + } + + @Test + public void testExplicitValidatorMapOverridesRegistry() { + ValueTypeValidator registryValidator = comparisonOperatorValidator(); + ValueTypeValidatorRegistry registry = new ValueTypeValidatorRegistry(); + registry.bindValidator(registryValidator); + try { + Condition condition = createEventPropertyConditionWithReference( + "equals", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "eventTypeValue"); + context.put("eventTypeValue", "view"); + + ValueTypeValidator strictValidator = new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "comparisonOperator"; + } + + @Override + public boolean validate(Object value) { + return false; + } + + @Override + public String getValueTypeDescription() { + return "always invalid"; + } + }; + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, null, + Collections.singletonMap("comparisonoperator", strictValidator)); + + assertNotNull(resolved); + assertEquals("equals", resolved.getParameter("comparisonOperator")); + } finally { + registry.unbindValidator(registryValidator); + } + } + + @Test + public void testValidationMismatchTracedWhenTracingEnabled() { + when(tracerService.isTracingEnabled()).thenReturn(true); + when(tracerService.getCurrentTracer()).thenReturn(requestTracer); + when(requestTracer.isEnabled()).thenReturn(true); + + context.put("param1", "notAnInteger"); + Condition condition = createIntegerParameterCondition( + ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "param1"); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true, tracerService); + + assertNotNull(resolved); + verify(requestTracer).trace(eq("Parameter type mismatch detected"), anyMap()); + } + + @Test + public void testNullParameterValueSkipsTypeValidation() { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("testCondition"); + conditionType.setParameters(Collections.singletonList(new Parameter("testParam", "integer", false))); + + Map params = new HashMap<>(); + params.put("testParam", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "missing"); + params.put("trigger", ConditionContextHelper.PARAMETER_REFERENCE_PREFIX + "alsoMissing"); + + Condition condition = new Condition(conditionType); + condition.setParameterValues(params); + + Condition resolved = ConditionContextHelper.getContextualCondition( + condition, context, scriptExecutor, true); + + assertNotNull(resolved); + assertNull(resolved.getParameter("testParam")); + } + // ========== Helper Methods ========== + private Condition createIntegerParameterCondition(Object testParamValue) { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("testCondition"); + conditionType.setParameters(Collections.singletonList(new Parameter("testParam", "integer", false))); + + Condition condition = createConditionWithParameter("testParam", testParamValue); + condition.setConditionType(conditionType); + return condition; + } + + private Condition createEventPropertyConditionWithReference(String comparisonOperator, Object propertyValue) { + ConditionType conditionType = new ConditionType(); + conditionType.setItemId("eventPropertyCondition"); + conditionType.setParameters(Arrays.asList( + new Parameter("propertyName", "string", false), + new Parameter("comparisonOperator", "comparisonOperator", false), + new Parameter("propertyValue", "string", false))); + + Condition condition = new Condition(conditionType); + Map params = new HashMap<>(); + params.put("propertyName", "eventType"); + params.put("comparisonOperator", comparisonOperator); + params.put("propertyValue", propertyValue); + condition.setParameterValues(params); + return condition; + } + + private static ValueTypeValidator comparisonOperatorValidator() { + return new ValueTypeValidator() { + @Override + public String getValueTypeId() { + return "comparisonOperator"; + } + + @Override + public boolean validate(Object value) { + return "equals".equals(value); + } + + @Override + public String getValueTypeDescription() { + return "Value must be a valid comparison operator"; + } + }; + } + private Condition createConditionWithParameter(String paramName, Object paramValue) { Condition condition = new Condition(); Map paramValues = new HashMap<>(); diff --git a/pom.xml b/pom.xml index 2658f5bf2..02bd8aa79 100644 --- a/pom.xml +++ b/pom.xml @@ -67,7 +67,7 @@ 9.4.3 9.4.3 3.7.0 - 3.7.0 + 3.9.0 5.2.1 2.28.14 1.1.0.Final diff --git a/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java index 810e9386e..748604e0f 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java +++ b/services/src/main/java/org/apache/unomi/services/impl/validation/ConditionValidationServiceImpl.java @@ -74,7 +74,7 @@ public void unbindValidator(ValueTypeValidator validator) { * Sets the TypeResolutionService for automatic type resolution during validation. * This allows the validation service to automatically resolve condition types * if they haven't been resolved yet, including nested conditions. - * + * * @param typeResolutionService the type resolution service to use */ public void setTypeResolutionService(TypeResolutionService typeResolutionService) { @@ -446,7 +446,7 @@ private List validateSingleParameterValue(String paramName, Obj // Parameter reference or script expression - skip all type and value validation; // validation happens when the reference is resolved at evaluation time if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Skipping type validation for parameter reference: {}={}", + LOGGER.debug("Skipping type validation for parameter reference: {}={}", paramName, value); } return errors; diff --git a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/DateValueTypeValidator.java b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/DateValueTypeValidator.java index e8f3cf5b0..4c38de76b 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/validation/validators/DateValueTypeValidator.java +++ b/services/src/main/java/org/apache/unomi/services/impl/validation/validators/DateValueTypeValidator.java @@ -17,13 +17,10 @@ package org.apache.unomi.services.impl.validation.validators; import org.apache.unomi.api.services.ValueTypeValidator; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.OffsetDateTime; -import java.time.ZonedDateTime; -import java.util.Date; - +import org.apache.unomi.persistence.spi.conditions.DateUtils; +/** + * Date value type validator. + */ public class DateValueTypeValidator implements ValueTypeValidator { @Override public String getValueTypeId() { @@ -35,16 +32,16 @@ public boolean validate(Object value) { if (value == null) { return true; } - // Accept Date and modern date/time types - return value instanceof Date - || value instanceof OffsetDateTime - || value instanceof ZonedDateTime - || value instanceof LocalDateTime - || value instanceof Instant; + // Condition parameters of type "date" are almost always plain ISO-8601 strings (from JSON + // condition definitions or persisted event/profile properties), not already-parsed Date + // objects, so delegate to the same parser used at actual condition-evaluation time + // (e.g. PropertyConditionEvaluator) rather than only accepting typed date/time instances. + return DateUtils.getDate(value) != null; } @Override public String getValueTypeDescription() { - return "Value must be a date (Date, OffsetDateTime, ZonedDateTime, LocalDateTime, or Instant)"; + return "Value must be a date (Date, OffsetDateTime, ZonedDateTime, LocalDateTime, Instant, " + + "or a parseable ISO-8601 / epoch-millis string)"; } } From 419b07e7f3399c25fa0369bf87d5e22d84f8b95f Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 10 Jul 2026 20:43:29 +0200 Subject: [PATCH 7/9] Fix OpenSearch rollover policy attachment and simplify RolloverIT The index.plugins.index_state_management.policy_id custom setting was missing its "index." prefix, and even with the prefix OpenSearch accepts but doesn't act on it - ISM's own explain endpoint confirmed total_managed_indices stayed at 0. Attach the policy explicitly via the ISM Add Policy API instead, checking both the HTTP status and the response body's failure fields. RolloverIT now asserts the rollover setup is correct on both engines (index settings, ISM/ILM explain, and policy conditions) instead of waiting minutes for an actual rollover sweep - ILM/ISM's background job scheduling is the search engine's own tested behavior, not something Unomi needs to prove end-to-end on every CI run. --- .../org/apache/unomi/itests/RolloverIT.java | 134 +++++++++--------- .../OpenSearchPersistenceServiceImpl.java | 47 +++++- 2 files changed, 109 insertions(+), 72 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java index 09d7021f2..f40dc8339 100644 --- a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java @@ -18,95 +18,99 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.unomi.api.Event; -import org.apache.unomi.api.Profile; -import org.apache.unomi.api.Session; import org.apache.unomi.shell.migration.utils.HttpUtils; -import org.junit.After; import org.junit.Test; import org.junit.runner.RunWith; import org.ops4j.pax.exam.junit.PaxExam; import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy; import org.ops4j.pax.exam.spi.reactors.PerSuite; -import java.util.Date; +import java.io.IOException; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; /** - * Verifies that the event index actually rolls over once it crosses the configured - * rollover.maxDocs threshold, on both Elasticsearch (ILM) and OpenSearch (ISM) - see UNOMI-946/947. + * Verifies that Unomi correctly wires up the event index's rollover lifecycle on both Elasticsearch (ILM) + * and OpenSearch (ISM) - see UNOMI-946/947. *

- * The IT config sets rollover.maxDocs=300 for both engines. The ILM/ISM background sweep that checks - * this threshold defaults to 10 minutes (ES) / 5 minutes (OS), so this test temporarily lowers the - * check interval to make the assertion feasible within a normal test timeout. + * This deliberately does not wait for an actual rollover to happen. ILM/ISM evaluate their managed indices + * on a periodic background sweep (minute-granularity), and on OpenSearch a job_interval change only takes + * effect after the currently scheduled sweep completes - which can add 5+ minutes of pure scheduling + * latency before the engine even starts evaluating a newly-managed index, on top of however long the + * rollover action itself then takes. Whether ILM/ISM actually execute a rollover once its conditions are + * met is the search engine's own, already-tested responsibility; what Unomi needs to guarantee is that the + * index/template/policy setup is correct so that engine can do its job, so this test asserts on that setup + * directly instead of waiting on it. */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerSuite.class) public class RolloverIT extends BaseIT { - private static final int EVENTS_TO_CREATE = 320; - private static final String PROFILE_ID = "rollover-it-profile"; - private static final String SESSION_ID = "rollover-it-session"; - private static final String EVENT_INDEX_PATTERN = "context-event-*"; + private static final String EVENT_ALIAS = "context-event"; + private static final String EVENT_WRITE_INDEX = "context-event-000001"; + private static final String POLICY_ID = "context-unomi-rollover-policy"; + private static final long EXPECTED_MAX_DOCS = 300; - @After - public void tearDown() throws Exception { - TestUtils.removeAllEvents(definitionsService, persistenceService); - TestUtils.removeAllSessions(definitionsService, persistenceService); - TestUtils.removeAllProfiles(definitionsService, persistenceService); - setPolicyCheckInterval(null); + @Test + public void testEventIndexRolloverIsProperlyConfigured() throws Exception { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + JsonNode indexRoot = getJson(client, "/" + EVENT_WRITE_INDEX + "/_settings?flat_settings=true").get(EVENT_WRITE_INDEX); + assertTrue("Expected the event write index " + EVENT_WRITE_INDEX + " to already exist", indexRoot != null); + JsonNode settings = indexRoot.get("settings"); + + if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { + assertOpenSearchRolloverConfigured(client, settings); + } else { + assertElasticsearchRolloverConfigured(client, settings); + } + } } - @Test - public void testEventIndexRollsOverPastMaxDocsThreshold() throws Exception { - setPolicyCheckInterval(SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) ? "1" : "1s"); + private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, JsonNode settings) throws IOException { + assertEquals("event index should reference the Unomi rollover policy", + POLICY_ID, text(settings, "index.plugins.index_state_management.policy_id")); + assertEquals("event index rollover_alias should be the event write alias", + EVENT_ALIAS, text(settings, "index.plugins.index_state_management.rollover_alias")); - Profile profile = new Profile(PROFILE_ID); - persistenceService.save(profile); - Session session = new Session(SESSION_ID, profile, new Date(), "rollover-it-scope"); - persistenceService.save(session); + // The index setting above is accepted by OpenSearch but is inert on its own (verified empirically: + // GET _plugins/_ism/explain reported total_managed_indices: 0 despite the setting being present) - + // ISM only actually manages an index via ism_template pattern-matching or an explicit Add Policy + // call, so confirm the real attachment here too, not just the setting. + JsonNode explain = getJson(client, "/_plugins/_ism/explain/" + EVENT_WRITE_INDEX); + assertTrue("ISM should actually be managing the event index, not just have an inert policy_id setting", + explain.get("total_managed_indices").asInt() >= 1); + JsonNode explainIndex = explain.get(EVENT_WRITE_INDEX); + assertEquals(POLICY_ID, text(explainIndex, "policy_id")); + assertTrue("ISM management should be enabled for the event index", explainIndex.get("enabled").asBoolean()); - for (int i = 0; i < EVENTS_TO_CREATE; i++) { - persistenceService.save( - new Event("rollover-it-event-" + i, "view", session, profile, "rollover-it-scope", null, null, new Date())); - } + JsonNode rolloverAction = getJson(client, "/_plugins/_ism/policies/" + POLICY_ID) + .get("policy").get("states").get(0).get("actions").get(0).get("rollover"); + assertEquals(EXPECTED_MAX_DOCS, rolloverAction.get("min_doc_count").asLong()); + } - // ISM's job interval is minute-granularity (vs ILM's second-granularity), so OpenSearch needs a - // much longer allowance to notice the crossed threshold and complete the rollover. - int retries = SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) ? 45 : 15; - keepTrying("Event index did not roll over to a second index after crossing the rollover.maxDocs threshold", - this::countEventRolloverIndices, count -> count >= 2, 2000, retries); + private void assertElasticsearchRolloverConfigured(CloseableHttpClient client, JsonNode settings) throws IOException { + assertEquals("event index should reference the Unomi rollover policy", + POLICY_ID, text(settings, "index.lifecycle.name")); + assertEquals("event index rollover_alias should be the event write alias", + EVENT_ALIAS, text(settings, "index.lifecycle.rollover_alias")); + + JsonNode explainIndex = getJson(client, "/" + EVENT_WRITE_INDEX + "/_ilm/explain").get("indices").get(EVENT_WRITE_INDEX); + assertTrue("ILM should actually be managing the event index", explainIndex.get("managed").asBoolean()); + assertEquals(POLICY_ID, text(explainIndex, "policy")); + + JsonNode rolloverAction = getJson(client, "/_ilm/policy/" + POLICY_ID) + .get(POLICY_ID).get("policy").get("phases").get("hot").get("actions").get("rollover"); + assertEquals(EXPECTED_MAX_DOCS, rolloverAction.get("max_docs").asLong()); } - private int countEventRolloverIndices() { - try (CloseableHttpClient client = createSearchEngineHttpClient()) { - String indicesJson = HttpUtils.executeGetRequest(client, - getSearchEngineBaseUrl() + "/_cat/indices/" + EVENT_INDEX_PATTERN + "?h=index&format=json", null); - if (indicesJson == null || indicesJson.isBlank()) { - return 0; - } - JsonNode indices = getObjectMapper().readTree(indicesJson); - return indices.isArray() ? indices.size() : 0; - } catch (Exception e) { - throw new RuntimeException("Failed to count rollover indices matching " + EVENT_INDEX_PATTERN, e); - } + private JsonNode getJson(CloseableHttpClient client, String path) throws IOException { + String body = HttpUtils.executeGetRequest(client, getSearchEngineBaseUrl() + path, null); + return getObjectMapper().readTree(body); } - private void setPolicyCheckInterval(String value) throws Exception { - try (CloseableHttpClient client = createSearchEngineHttpClient()) { - String settingKey = SEARCH_ENGINE_OPENSEARCH.equals(searchEngine) - ? "plugins.index_state_management.job_interval" - : "indices.lifecycle.poll_interval"; - String settingValue; - if (value == null) { - settingValue = "null"; - } else if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - // job_interval is a plain integer number of minutes, not a quoted time value like poll_interval. - settingValue = value; - } else { - settingValue = "\"" + value + "\""; - } - String settingsBody = "{\"persistent\": {\"" + settingKey + "\": " + settingValue + "}}"; - HttpUtils.executePutRequest(client, getSearchEngineBaseUrl() + "/_cluster/settings", settingsBody, null); - } + private static String text(JsonNode node, String field) { + JsonNode value = node.get(field); + return value != null ? value.asText() : null; } } diff --git a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java index e211c922b..c8c3969b9 100644 --- a/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java +++ b/persistence-opensearch/core/src/main/java/org/apache/unomi/persistence/opensearch/OpenSearchPersistenceServiceImpl.java @@ -1491,18 +1491,21 @@ private void internalCreateRolloverTemplate(String itemName) throws IOException analyzerBuilder -> analyzerBuilder.custom(CustomAnalyzer.of( customAnalyzer -> customAnalyzer.tokenizer("keyword").filter("lowercase", "asciifolding"))))); - // plugins.index_state_management.rollover_alias and .policy_id are OpenSearch ISM settings with no - // typed builder equivalent (they are not the same as the ElasticSearch-only index.lifecycle.* - // ILM settings), so they are passed through as raw custom settings. Setting policy_id directly here, - // rather than relying solely on the policy's ism_template pattern-matching, mirrors how ES binds - // index.lifecycle.name per-template so every rollover index is deterministically attached. + // index.plugins.index_state_management.rollover_alias and .policy_id are OpenSearch ISM settings with + // no typed builder equivalent (they are not the same as the ElasticSearch-only index.lifecycle.* + // ILM settings), so they are passed through as raw custom settings. Unlike ES's index.lifecycle.name, + // setting policy_id here does NOT actually cause ISM to manage the index - verified via + // GET _plugins/_ism/explain/, which reports total_managed_indices: 0 even though the setting is + // present on the index. These are kept as documentation/metadata on the index; actual attachment + // happens via the explicit Add Policy call in attachRolloverPolicy(), invoked from + // internalCreateRolloverIndex() once the index is confirmed created. IndexSettings indexSettings = IndexSettings.of(builder -> builder .numberOfShards(Integer.valueOf(rolloverIndexNumberOfShards)) .numberOfReplicas(Integer.valueOf(rolloverIndexNumberOfReplicas)) .mapping(m -> m.totalFields(t -> t.limit(Long.valueOf(rolloverIndexMappingTotalFieldsLimit)))) .maxDocvalueFieldsSearch(Integer.valueOf(rolloverIndexMaxDocValueFieldsSearch)) - .customSettings("plugins.index_state_management.rollover_alias", JsonData.of(rolloverAlias)) - .customSettings("plugins.index_state_management.policy_id", JsonData.of(indexPrefix + "-" + ROLLOVER_LIFECYCLE_NAME)) + .customSettings("index.plugins.index_state_management.rollover_alias", JsonData.of(rolloverAlias)) + .customSettings("index.plugins.index_state_management.policy_id", JsonData.of(indexPrefix + "-" + ROLLOVER_LIFECYCLE_NAME)) .analysis(analysis)); IndexTemplateMapping templateMapping = IndexTemplateMapping.of( @@ -1599,6 +1602,7 @@ private void internalCreateRolloverIndex(String indexName) throws IOException { if (hasFoldingAnalyzer && hasDynamicTemplates) { LOGGER.debug("Template successfully applied to index {} - folding analyzer and dynamic templates present", fullIndexName); + attachRolloverPolicy(fullIndexName); return; } else { LOGGER.warn("Template not applied to index {} - folding analyzer: {}, dynamic templates: {}. Retrying...", @@ -1615,6 +1619,35 @@ private void internalCreateRolloverIndex(String indexName) throws IOException { } } + // Explicitly attaches the rollover policy to a concrete index via ISM's Add Policy API. The + // index.plugins.index_state_management.policy_id custom setting (set in internalCreateRolloverTemplate) is + // stored on the index but does not cause ISM to actually manage it - this call is the real attachment + // mechanism, mirroring what ism_template pattern-matching would do automatically if used instead. + private void attachRolloverPolicy(String fullIndexName) throws IOException { + String policyName = indexPrefix + "-" + ROLLOVER_LIFECYCLE_NAME; + JsonObject addPolicyBody = Json.createObjectBuilder().add("policy_id", policyName).build(); + Response response = client.generic().execute( + Requests.builder() + .method("POST") + .endpoint("_plugins/_ism/add/" + fullIndexName) + .json(addPolicyBody) + .build() + ); + // The Add Policy API can return HTTP 200 while still reporting a per-index failure in the body + // (e.g. {"updated_indices":1,"failures":false,"failed_indices":[]}), so the body must be checked too. + String responseBody = response.getBody().isPresent() ? response.getBody().get().bodyAsString() : ""; + boolean failures = response.getStatus() != 200; + if (!failures && !responseBody.isEmpty()) { + Map parsed = new ObjectMapper().readValue(responseBody, Map.class); + failures = Boolean.TRUE.equals(parsed.get("failures")) + || Integer.valueOf(0).equals(parsed.get("updated_indices")); + } + if (failures) { + throw new IOException("Failed to attach ISM policy " + policyName + " to index " + fullIndexName + + " - status: " + response.getStatus() + ", body: " + responseBody); + } + } + private void internalCreateIndex(String indexName, String mappingSource) throws IOException { CreateIndexResponse createIndexResponse = client.indices().create(c -> c .index(indexName) From 29a9531fc6edea77bce535043c35c6f40841cc0f Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 10 Jul 2026 22:18:10 +0200 Subject: [PATCH 8/9] Fix condition value-type validation warnings in personalization test fixtures propertyValueInteger was quoted as a JSON string instead of a number, and pastEventCondition blocks never set the recommended operator parameter. Both were silently tolerated by the lenient runtime evaluators but now trip the stricter condition validation added in this branch. Values are unchanged (operator defaults to eventsOccurred already), so behavior is identical - this just quiets the warnings. --- .../personalization-score-interests.json | 16 ++++++++-------- itests/src/test/resources/personalization.json | 10 ++++++++++ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/itests/src/test/resources/personalization-score-interests.json b/itests/src/test/resources/personalization-score-interests.json index 0f19d62a5..1136e421d 100644 --- a/itests/src/test/resources/personalization-score-interests.json +++ b/itests/src/test/resources/personalization-score-interests.json @@ -28,7 +28,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "25", + "propertyValueInteger": 25, "comparisonOperator": "greaterThan" } } @@ -46,7 +46,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "35", + "propertyValueInteger": 35, "comparisonOperator": "greaterThan" } } @@ -64,7 +64,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "25", + "propertyValueInteger": 25, "comparisonOperator": "greaterThan" } } @@ -82,7 +82,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "25", + "propertyValueInteger": 25, "comparisonOperator": "greaterThan" } } @@ -100,7 +100,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "25", + "propertyValueInteger": 25, "comparisonOperator": "greaterThan" } } @@ -118,7 +118,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "35", + "propertyValueInteger": 35, "comparisonOperator": "greaterThan" } } @@ -136,7 +136,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "35", + "propertyValueInteger": 35, "comparisonOperator": "greaterThan" } }, @@ -157,7 +157,7 @@ "type": "profilePropertyCondition", "parameterValues": { "propertyName": "properties.age", - "propertyValueInteger": "25", + "propertyValueInteger": 25, "comparisonOperator": "greaterThan" } }, diff --git a/itests/src/test/resources/personalization.json b/itests/src/test/resources/personalization.json index 652d50a19..6d53b808f 100644 --- a/itests/src/test/resources/personalization.json +++ b/itests/src/test/resources/personalization.json @@ -26,6 +26,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -76,6 +77,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -123,6 +125,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -170,6 +173,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -217,6 +221,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -264,6 +269,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -311,6 +317,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -358,6 +365,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -405,6 +413,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", @@ -452,6 +461,7 @@ { "condition": { "parameterValues": { + "operator": "eventsOccurred", "minimumEventCount": 1, "eventCondition": { "type": "booleanCondition", From 433d1d6186aa3f7998898bf2cb2858218008aecf Mon Sep 17 00:00:00 2001 From: Serge Huber Date: Fri, 10 Jul 2026 22:24:45 +0200 Subject: [PATCH 9/9] Resolve the event write index dynamically in RolloverIT The IT suite runs with rollover.maxDocs=300 shared across the whole PerSuite container, so by the time RolloverIT runs - often after hundreds of other tests have created events - context-event-000001 may have already rolled over for real. Once that happens on OpenSearch, ISM has nothing left to transition to and marks the old index's management as completed/disabled, failing a hardcoded check even though rollover worked correctly. Resolve the current write index via the alias's is_write_index flag instead, same as production code already does for session rollover indices. --- .../org/apache/unomi/itests/RolloverIT.java | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java index f40dc8339..449bfa278 100644 --- a/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java +++ b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java @@ -26,6 +26,8 @@ import org.ops4j.pax.exam.spi.reactors.PerSuite; import java.io.IOException; +import java.util.Iterator; +import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -42,32 +44,57 @@ * met is the search engine's own, already-tested responsibility; what Unomi needs to guarantee is that the * index/template/policy setup is correct so that engine can do its job, so this test asserts on that setup * directly instead of waiting on it. + *

+ * The write index is resolved dynamically via the alias's {@code is_write_index} flag rather than assumed + * to be {@code context-event-000001}: the IT suite runs with a deliberately low rollover.maxDocs=300 (see + * BaseIT) shared by the whole PerSuite container, so by the time this test runs - often after hundreds of + * other tests have created events - the index may have already rolled over for real. On OpenSearch, once + * that happens ISM has nothing left to transition to (the rollover policy's single state has no further + * transitions) and marks that now-rolled-over index's management as completed/disabled, which would make a + * hardcoded context-event-000001 check fail even though the rollover machinery worked correctly. */ @RunWith(PaxExam.class) @ExamReactorStrategy(PerSuite.class) public class RolloverIT extends BaseIT { private static final String EVENT_ALIAS = "context-event"; - private static final String EVENT_WRITE_INDEX = "context-event-000001"; private static final String POLICY_ID = "context-unomi-rollover-policy"; private static final long EXPECTED_MAX_DOCS = 300; @Test public void testEventIndexRolloverIsProperlyConfigured() throws Exception { try (CloseableHttpClient client = createSearchEngineHttpClient()) { - JsonNode indexRoot = getJson(client, "/" + EVENT_WRITE_INDEX + "/_settings?flat_settings=true").get(EVENT_WRITE_INDEX); - assertTrue("Expected the event write index " + EVENT_WRITE_INDEX + " to already exist", indexRoot != null); + String writeIndex = resolveCurrentWriteIndex(client); + JsonNode indexRoot = getJson(client, "/" + writeIndex + "/_settings?flat_settings=true").get(writeIndex); + assertTrue("Expected the event write index " + writeIndex + " to already exist", indexRoot != null); JsonNode settings = indexRoot.get("settings"); if (SEARCH_ENGINE_OPENSEARCH.equals(searchEngine)) { - assertOpenSearchRolloverConfigured(client, settings); + assertOpenSearchRolloverConfigured(client, settings, writeIndex); } else { - assertElasticsearchRolloverConfigured(client, settings); + assertElasticsearchRolloverConfigured(client, settings, writeIndex); + } + } + } + + // Resolves the index currently holding the event alias's write pointer, rather than assuming it is + // still context-event-000001 - see the class javadoc for why that assumption doesn't hold on a full + // suite run. Mirrors the is_write_index alias lookup OpenSearchPersistenceServiceImpl itself uses for + // session rollover indices. + private String resolveCurrentWriteIndex(CloseableHttpClient client) throws IOException { + JsonNode aliasInfo = getJson(client, "/_alias/" + EVENT_ALIAS); + Iterator> indices = aliasInfo.fields(); + while (indices.hasNext()) { + Map.Entry entry = indices.next(); + JsonNode isWriteIndex = entry.getValue().path("aliases").path(EVENT_ALIAS).path("is_write_index"); + if (isWriteIndex.asBoolean(false)) { + return entry.getKey(); } } + throw new AssertionError("Could not find a write index for alias " + EVENT_ALIAS); } - private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, JsonNode settings) throws IOException { + private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { assertEquals("event index should reference the Unomi rollover policy", POLICY_ID, text(settings, "index.plugins.index_state_management.policy_id")); assertEquals("event index rollover_alias should be the event write alias", @@ -77,10 +104,10 @@ private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, Json // GET _plugins/_ism/explain reported total_managed_indices: 0 despite the setting being present) - // ISM only actually manages an index via ism_template pattern-matching or an explicit Add Policy // call, so confirm the real attachment here too, not just the setting. - JsonNode explain = getJson(client, "/_plugins/_ism/explain/" + EVENT_WRITE_INDEX); + JsonNode explain = getJson(client, "/_plugins/_ism/explain/" + writeIndex); assertTrue("ISM should actually be managing the event index, not just have an inert policy_id setting", explain.get("total_managed_indices").asInt() >= 1); - JsonNode explainIndex = explain.get(EVENT_WRITE_INDEX); + JsonNode explainIndex = explain.get(writeIndex); assertEquals(POLICY_ID, text(explainIndex, "policy_id")); assertTrue("ISM management should be enabled for the event index", explainIndex.get("enabled").asBoolean()); @@ -89,13 +116,13 @@ private void assertOpenSearchRolloverConfigured(CloseableHttpClient client, Json assertEquals(EXPECTED_MAX_DOCS, rolloverAction.get("min_doc_count").asLong()); } - private void assertElasticsearchRolloverConfigured(CloseableHttpClient client, JsonNode settings) throws IOException { + private void assertElasticsearchRolloverConfigured(CloseableHttpClient client, JsonNode settings, String writeIndex) throws IOException { assertEquals("event index should reference the Unomi rollover policy", POLICY_ID, text(settings, "index.lifecycle.name")); assertEquals("event index rollover_alias should be the event write alias", EVENT_ALIAS, text(settings, "index.lifecycle.rollover_alias")); - JsonNode explainIndex = getJson(client, "/" + EVENT_WRITE_INDEX + "/_ilm/explain").get("indices").get(EVENT_WRITE_INDEX); + JsonNode explainIndex = getJson(client, "/" + writeIndex + "/_ilm/explain").get("indices").get(writeIndex); assertTrue("ILM should actually be managing the event index", explainIndex.get("managed").asBoolean()); assertEquals(POLICY_ID, text(explainIndex, "policy"));