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 a2eaff1c7..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, @@ -73,6 +71,7 @@ param( [switch]$SkipMigrationTests, [switch]$ResolverDebug, [switch]$KeepContainer, + [switch]$SearchEngineLogs, [switch]$NoMemorySampler, [int]$MemoryInterval = 30, [switch]$Javadoc, @@ -88,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' } @@ -270,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)' @@ -286,10 +277,11 @@ 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' - 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 '' @@ -332,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' @@ -535,6 +518,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)" @@ -547,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.' @@ -571,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) @@ -608,6 +589,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 +654,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..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 @@ -278,6 +276,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 @@ -312,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)" @@ -326,10 +323,11 @@ 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)" - 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 @@ -356,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)" @@ -370,10 +366,11 @@ 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)" - 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 @@ -480,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 @@ -539,6 +530,9 @@ while [ "$1" != "" ]; do --keep-container) KEEP_CONTAINER=true ;; + --search-engine-logs) + IT_SEARCH_ENGINE_LOGS=true + ;; --no-memory-sampler) IT_MEMORY_SAMPLER=false ;; @@ -558,7 +552,6 @@ while [ "$1" != "" ]; do ;; --ci) NO_KARAF=true - USE_MAVEN_CACHE=false BUILD_NON_INTERACTIVE=true MAVEN_QUIET=true JAVADOC=true @@ -601,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() { @@ -913,6 +896,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)" @@ -931,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 @@ -981,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) @@ -1137,6 +1095,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 +1186,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/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/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/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..1178ce36a 100644 --- a/graphql/graphql-ui/package.json +++ b/graphql/graphql-ui/package.json @@ -15,18 +15,22 @@ "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" + "**/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", @@ -41,7 +45,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..2a6582f5e 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: + "@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: - "@tanstack/react-virtual" "^3.0.0-beta.60" - client-only "^0.0.1" + "@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-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== - 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-primitive" "2.1.7" -"@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== +"@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/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-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-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" @@ -1898,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== @@ -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== - 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== +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: - 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" @@ -2252,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" @@ -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" @@ -2330,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" @@ -2372,12 +2390,12 @@ 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.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== @@ -2406,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" @@ -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" @@ -2572,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" @@ -2587,6 +2625,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 +2642,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" @@ -2666,22 +2699,19 @@ 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== +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== dependencies: - picocolors "^0.2.1" - source-map "^0.6.1" + nanoid "^3.3.12" + picocolors "^1.1.1" + source-map-js "^1.2.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== - dependencies: - nanoid "^3.3.7" - picocolors "^1.0.0" - source-map-js "^1.2.0" +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" @@ -2698,12 +2728,25 @@ 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" + 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" @@ -2713,32 +2756,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: @@ -2832,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" @@ -2906,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" @@ -2940,11 +2987,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 +3005,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.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 +3026,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 +3045,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 +3066,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 +3157,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 +3274,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/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 d0051bb63..8f6f55b65 100644 --- a/itests/pom.xml +++ b/itests/pom.xml @@ -31,10 +31,10 @@ elasticsearch false itests-opensearch - - true false + + false @@ -356,6 +356,9 @@ + + ${project.build.directory}/elasticsearch0/logs/docker-console.log + ${project.build.directory}/elasticsearch-port.properties @@ -382,7 +385,7 @@ start - true + ${docker.showLogs} @@ -478,6 +481,9 @@ + + ${project.build.directory}/opensearch0/logs/docker-console.log + ${project.build.directory}/opensearch-port.properties @@ -504,7 +510,7 @@ start - true + ${docker.showLogs} 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 4fa624da5..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; @@ -23,6 +22,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 +66,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 +146,17 @@ 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; + + /** + * 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. @@ -227,6 +240,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 +335,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 @@ -470,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) { @@ -480,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) { @@ -528,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; @@ -634,12 +659,14 @@ 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"), 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), @@ -660,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) { @@ -685,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)); } @@ -717,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")); @@ -731,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)"); } @@ -739,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")); @@ -748,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", @@ -774,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); } @@ -1372,6 +1408,148 @@ public static void closeResponse(CloseableHttpResponse response) { } } + + private static final String IT_ZERO_REPLICAS_INDEX_TEMPLATE = "unomi-it-zero-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; + } + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + String baseUrl = getSearchEngineBaseUrl(); + 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); + } + } + + /** + * 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(); + 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 ({}): {}", context, health); + System.out.println("==== Cluster health (" + context + "): " + health); + if (health != null && health.contains("\"status\":\"red\"")) { + throw new IllegalStateException("Cluster still RED " + context + ": " + health); + } + } catch (RuntimeException e) { + throw e; + } catch (Exception e) { + throw new IllegalStateException("Failed to enforce zero replicas " + context, e); + } + } + + private void ensureZeroReplicaIndexTemplate(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\"}}"; + 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); + } + } + + /** + * 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); + 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/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..449bfa278 --- /dev/null +++ b/itests/src/test/java/org/apache/unomi/itests/RolloverIT.java @@ -0,0 +1,143 @@ +/* + * 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.shell.migration.utils.HttpUtils; +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.io.IOException; +import java.util.Iterator; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * Verifies that Unomi correctly wires up the event index's rollover lifecycle on both Elasticsearch (ILM) + * and OpenSearch (ISM) - see UNOMI-946/947. + *

+ * 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. + *

+ * 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 POLICY_ID = "context-unomi-rollover-policy"; + private static final long EXPECTED_MAX_DOCS = 300; + + @Test + public void testEventIndexRolloverIsProperlyConfigured() throws Exception { + try (CloseableHttpClient client = createSearchEngineHttpClient()) { + 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, writeIndex); + } else { + 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, 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", + EVENT_ALIAS, text(settings, "index.plugins.index_state_management.rollover_alias")); + + // 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/" + 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(writeIndex); + assertEquals(POLICY_ID, text(explainIndex, "policy_id")); + assertTrue("ISM management should be enabled for the event index", explainIndex.get("enabled").asBoolean()); + + 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()); + } + + 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, "/" + 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")); + + 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 JsonNode getJson(CloseableHttpClient client, String path) throws IOException { + String body = HttpUtils.executeGetRequest(client, getSearchEngineBaseUrl() + path, null); + return getObjectMapper().readTree(body); + } + + private static String text(JsonNode node, String field) { + JsonNode value = node.get(field); + return value != null ? value.asText() : 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/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..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 @@ -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) { @@ -160,6 +162,8 @@ public void waitForStartup() throws InterruptedException { } } + prepareSearchEngineAfterMigration(); + // Call super for starting Unomi and wait for the complete startup super.waitForStartup(); } 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", 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/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..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 @@ -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,167 @@ 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"))))); + + // 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("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( + 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); + attachRolloverPolicy(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); + } + } + } + } + + // 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 { 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 abc524a7c..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 @@ -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 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)"; } }