diff --git a/.github/workflows/early-access.yml b/.github/workflows/early-access.yml index d17f9bbd..3be076f2 100644 --- a/.github/workflows/early-access.yml +++ b/.github/workflows/early-access.yml @@ -56,16 +56,29 @@ jobs: id: vars shell: bash run: | - VERSION=$(grep '^version\s*=\s*' gradle.properties | cut -d'=' -f2 | xargs) - echo "VERSION=$VERSION" >> "$GITHUB_OUTPUT" - if [[ "$VERSION" == *-SNAPSHOT ]]; then - echo "SNAPSHOT=true" >> "$GITHUB_OUTPUT" + BASE_VERSION=$(grep '^version\s*=\s*' gradle.properties | cut -d'=' -f2 | xargs) + echo "BASE_VERSION=$BASE_VERSION" >> "$GITHUB_OUTPUT" + + # Check if current version is already a release (RC, SNAPSHOT, or tagged release) + if [[ "$BASE_VERSION" == *-SNAPSHOT ]] || [[ "$BASE_VERSION" == *-RC* ]]; then + echo "SHOULD_RELEASE_SNAPSHOT=false" >> "$GITHUB_OUTPUT" + echo "Skipping snapshot release: version is already a pre-release ($BASE_VERSION)" else - echo "SNAPSHOT=false" >> "$GITHUB_OUTPUT" + # Check if this exact version has been released as a tag + if git tag --list | grep -q "^v${BASE_VERSION}$"; then + echo "SHOULD_RELEASE_SNAPSHOT=false" >> "$GITHUB_OUTPUT" + echo "Skipping snapshot release: version $BASE_VERSION has already been released" + else + # Create snapshot version + SNAPSHOT_VERSION="${BASE_VERSION}-SNAPSHOT" + echo "VERSION=$SNAPSHOT_VERSION" >> "$GITHUB_OUTPUT" + echo "SHOULD_RELEASE_SNAPSHOT=true" >> "$GITHUB_OUTPUT" + echo "Will release snapshot version: $SNAPSHOT_VERSION" + fi fi - - name: Release - if: ${{ steps.vars.outputs.SNAPSHOT }} + - name: Release Snapshot + if: ${{ steps.vars.outputs.SHOULD_RELEASE_SNAPSHOT == 'true' }} uses: jreleaser/release-action@v2 with: arguments: release diff --git a/build.gradle b/build.gradle index c6750959..9d64d977 100644 --- a/build.gradle +++ b/build.gradle @@ -21,6 +21,16 @@ apply from: "spotless.gradle" subprojects { apply from: "$rootDir/gradle/build-conventions.gradle" + + // Skip Javadoc for demo modules + if (project.path.startsWith(':demos:')) { + tasks.withType(Javadoc) { + onlyIf { false } + } + tasks.matching { it.name == 'javadocJar' }.configureEach { + onlyIf { false } + } + } } tasks.register('aggregateTestReport', TestReport) { @@ -62,7 +72,7 @@ tasks.register('aggregateJavadoc', Javadoc) { 'https://docs.oracle.com/en/java/javase/21/docs/api/', 'https://docs.spring.io/spring-framework/docs/current/javadoc-api/', 'https://docs.spring.io/spring-data/redis/docs/current/api/', - 'https://docs.spring.io/spring-boot/docs/current/api/' + 'https://docs.spring.io/spring-boot/api/java/' ) } diff --git a/demos/build.gradle b/demos/build.gradle index cb6ada7f..8f7732fb 100644 --- a/demos/build.gradle +++ b/demos/build.gradle @@ -18,6 +18,8 @@ tasks.matching { it.name.startsWith('publish') }.configureEach { enabled = false } +// Javadoc generation is disabled for demo modules in the root build.gradle + repositories { mavenLocal() mavenCentral() diff --git a/docs/antora-playbook.yml b/docs/antora-playbook.yml index a503dad2..b5c344b8 100644 --- a/docs/antora-playbook.yml +++ b/docs/antora-playbook.yml @@ -29,7 +29,8 @@ asciidoc: toc: ~ xrefstyle: short # Javadoc-specific attributes - javadoc-base-url: '{attachmentsdir}/javadoc' + # Using relative paths from the page location + javadoc-base-url: '_attachments/javadoc' javadoc-core-url: '{javadoc-base-url}/modules/redis-om-spring' javadoc-ai-url: '{javadoc-base-url}/modules/redis-om-spring-ai' javadoc-aggregate-url: '{javadoc-base-url}/aggregate' \ No newline at end of file diff --git a/docs/build.gradle b/docs/build.gradle index ef633c28..7185e66e 100644 --- a/docs/build.gradle +++ b/docs/build.gradle @@ -12,9 +12,9 @@ description = 'Redis OM Spring Documents Site' // Define properties similar to Maven pom.xml ext { - nodeVersion = '20.5.0' // Note: no 'v' prefix for node-gradle plugin - npmVersion = '9.8.0' - antoraVersion = '3.1.4' + nodeVersion = '20.17.0' // LTS version + npmVersion = '10.9.2' + antoraVersion = '3.1.12' } // Configure Node.js plugin @@ -22,7 +22,7 @@ node { version = nodeVersion npmVersion = project.ext.npmVersion download = true - nodeProjectDir = file("${project.projectDir}") + nodeProjectDir = layout.projectDirectory.asFile } // Install Antora CLI and generator @@ -42,11 +42,11 @@ task copyJavadocs(type: Copy) { dependsOn ':aggregateJavadoc', ':generateModuleJavadocs' from rootProject.layout.buildDirectory.dir('docs/javadoc') - into "${project.projectDir}/content/modules/ROOT/attachments/javadoc" + into layout.projectDirectory.dir("content/modules/ROOT/attachments/javadoc") doFirst { - mkdir "${project.projectDir}/content/modules/ROOT/attachments" - mkdir "${project.projectDir}/content/modules/ROOT/attachments/javadoc" + mkdir layout.projectDirectory.dir("content/modules/ROOT/attachments").asFile + mkdir layout.projectDirectory.dir("content/modules/ROOT/attachments/javadoc").asFile logger.lifecycle("Copying Javadoc documentation to Antora attachments...") } } @@ -65,12 +65,10 @@ task generateSite(type: NodeTask) { '--to-dir=build/site' ] - // Capture buildDir at configuration time to avoid execution-time project access - def buildDirectory = project.buildDir - outputs.dir("${buildDirectory}/site") + outputs.dir(layout.buildDirectory.dir("site")) doFirst { - mkdir "${buildDirectory}/site" + mkdir layout.buildDirectory.dir("site").get().asFile logger.lifecycle("Building multi-version documentation with Antora...") } } @@ -82,7 +80,7 @@ assemble.dependsOn generateSite // Clean up Antora-related directories when cleaning the project clean { delete 'node_modules' - delete project.buildDir - delete "${project.projectDir}/content/modules/ROOT/assets/javadoc" - delete "${project.projectDir}/content/modules/ROOT/attachments/javadoc" + delete layout.buildDirectory + delete layout.projectDirectory.dir("content/modules/ROOT/assets/javadoc") + delete layout.projectDirectory.dir("content/modules/ROOT/attachments/javadoc") } \ No newline at end of file diff --git a/docs/content/modules/ROOT/nav.adoc b/docs/content/modules/ROOT/nav.adoc index b19588e2..5c4baffb 100644 --- a/docs/content/modules/ROOT/nav.adoc +++ b/docs/content/modules/ROOT/nav.adoc @@ -20,6 +20,7 @@ * xref:json_mappings.adoc[Redis JSON Basics] * xref:document-annotation.adoc[Document Annotation] * xref:json-repositories.adoc[Document Repositories] +* xref:json-map-fields.adoc[Map Field Mappings] .Indexing and Search * xref:search.adoc[Redis Query Engine Integration] diff --git a/docs/content/modules/ROOT/pages/api-reference.adoc b/docs/content/modules/ROOT/pages/api-reference.adoc index e288f205..0f1fddd2 100644 --- a/docs/content/modules/ROOT/pages/api-reference.adoc +++ b/docs/content/modules/ROOT/pages/api-reference.adoc @@ -23,7 +23,7 @@ The API documentation is automatically generated from the latest release and inc The core Redis OM Spring module provides the fundamental functionality for object mapping, repositories, and search capabilities. -link:{attachmentsdir}/javadoc/modules/redis-om-spring/index.html[Redis OM Spring Core API^, role="external"] +xref:attachment$javadoc/modules/redis-om-spring/index.html[Redis OM Spring Core API^, role="external", window="_blank"] === Key Packages @@ -39,7 +39,7 @@ link:{attachmentsdir}/javadoc/modules/redis-om-spring/index.html[Redis OM Spring The AI extension module provides vector embedding and similarity search capabilities with multiple AI provider integrations. -link:{attachmentsdir}/javadoc/modules/redis-om-spring-ai/index.html[Redis OM Spring AI API^, role="external"] +xref:attachment$javadoc/modules/redis-om-spring-ai/index.html[Redis OM Spring AI API^, role="external", window="_blank"] === Key Packages @@ -51,7 +51,7 @@ link:{attachmentsdir}/javadoc/modules/redis-om-spring-ai/index.html[Redis OM Spr For a unified view of all modules and their interactions: -link:{attachmentsdir}/javadoc/aggregate/index.html[Complete API Reference^, role="external"] +xref:attachment$javadoc/aggregate/index.html[Complete API Reference^, role="external", window="_blank"] This aggregated documentation provides: diff --git a/docs/content/modules/ROOT/pages/configuration.adoc b/docs/content/modules/ROOT/pages/configuration.adoc index 31bcef01..4a324368 100644 --- a/docs/content/modules/ROOT/pages/configuration.adoc +++ b/docs/content/modules/ROOT/pages/configuration.adoc @@ -90,13 +90,15 @@ spring: ssl: false timeout: 60000 # Connection timeout in milliseconds - # Connection pool settings (optional) - lettuce: + # Jedis connection pool settings (Redis OM Spring uses Jedis by default) + jedis: pool: - max-active: 8 - max-idle: 8 - min-idle: 0 - max-wait: -1ms + enabled: true + max-active: 8 # Maximum connections in the pool + max-idle: 8 # Maximum idle connections + min-idle: 0 # Minimum idle connections + max-wait: -1ms # Maximum wait time for connection (-1 = indefinite) + time-between-eviction-runs: 30s # How often to evict idle connections ---- === Cluster Configuration @@ -137,6 +139,202 @@ spring: For more details on Redis Sentinel configuration, see the xref:sentinel.adoc[Redis Sentinel Support] page. +=== Connection Pool Configuration + +Redis OM Spring uses Jedis as its Redis client, which provides robust connection pooling capabilities. The pool configuration can be customized through Spring Boot properties: + +==== Basic Pool Configuration + +[source,yaml] +---- +spring: + data: + redis: + jedis: + pool: + enabled: true # Enable connection pooling + max-active: 8 # Maximum number of connections in the pool + max-idle: 8 # Maximum number of idle connections + min-idle: 0 # Minimum number of idle connections + max-wait: -1ms # Maximum wait time for a connection (-1 = indefinite) + + # Eviction configuration + time-between-eviction-runs: 30s # How often to run the eviction thread + min-evictable-idle-time: 60s # Minimum time before idle connections can be evicted + num-tests-per-eviction-run: -1 # Number of connections to test per eviction run (-1 = test all) + + # Connection validation + test-on-borrow: false # Test connection before borrowing from pool + test-on-return: false # Test connection when returning to pool + test-while-idle: true # Test connections while idle +---- + +==== Advanced Pool Configuration with Java Config + +For more advanced configuration scenarios, you can create a custom `JedisConnectionFactory` bean: + +[source,java] +---- +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import redis.clients.jedis.JedisPoolConfig; +import java.time.Duration; + +@Configuration +public class RedisPoolConfiguration { + + @Bean + public JedisConnectionFactory jedisConnectionFactory(RedisProperties redisProperties) { + // Create pool configuration + JedisPoolConfig poolConfig = new JedisPoolConfig(); + + // Connection pool size + poolConfig.setMaxTotal(16); // Max total connections + poolConfig.setMaxIdle(8); // Max idle connections + poolConfig.setMinIdle(4); // Min idle connections + poolConfig.setMaxWait(Duration.ofSeconds(5)); // Max wait time + + // Eviction settings + poolConfig.setTimeBetweenEvictionRuns(Duration.ofSeconds(30)); + poolConfig.setMinEvictableIdleTime(Duration.ofMinutes(1)); + poolConfig.setNumTestsPerEvictionRun(3); + + // Connection validation + poolConfig.setTestOnBorrow(true); // Validate before borrowing + poolConfig.setTestOnReturn(false); // Don't validate on return + poolConfig.setTestWhileIdle(true); // Validate idle connections + poolConfig.setTestOnCreate(true); // Validate newly created connections + + // LIFO behavior (Last In First Out) + poolConfig.setLifo(true); + + // Block when pool exhausted + poolConfig.setBlockWhenExhausted(true); + + // Build Jedis client configuration + JedisClientConfiguration clientConfig = JedisClientConfiguration.builder() + .connectTimeout(Duration.ofSeconds(2)) + .readTimeout(Duration.ofSeconds(2)) + .usePooling() + .poolConfig(poolConfig) + .build(); + + // Create connection factory + RedisStandaloneConfiguration redisConfig = new RedisStandaloneConfiguration(); + redisConfig.setHostName(redisProperties.getHost()); + redisConfig.setPort(redisProperties.getPort()); + redisConfig.setPassword(redisProperties.getPassword()); + + return new JedisConnectionFactory(redisConfig, clientConfig); + } +} +---- + +==== Using JedisClientConfigurationBuilderCustomizer + +Spring Boot provides a customizer interface for fine-tuning the Jedis client configuration: + +[source,java] +---- +import org.springframework.boot.autoconfigure.data.redis.JedisClientConfigurationBuilderCustomizer; +import org.springframework.data.redis.connection.jedis.JedisClientConfiguration; +import org.springframework.stereotype.Component; +import redis.clients.jedis.JedisPoolConfig; +import java.time.Duration; + +@Component +public class JedisPoolCustomizer implements JedisClientConfigurationBuilderCustomizer { + + @Override + public void customize(JedisClientConfiguration.JedisClientConfigurationBuilder clientConfigurationBuilder) { + // Create custom pool configuration + JedisPoolConfig poolConfig = new JedisPoolConfig(); + poolConfig.setMaxTotal(20); + poolConfig.setMaxIdle(10); + poolConfig.setMinIdle(5); + poolConfig.setMaxWait(Duration.ofSeconds(3)); + poolConfig.setTestOnBorrow(true); + + // Apply the custom pool configuration + clientConfigurationBuilder.usePooling().poolConfig(poolConfig); + + // Set timeouts + clientConfigurationBuilder + .connectTimeout(Duration.ofSeconds(2)) + .readTimeout(Duration.ofSeconds(2)); + } +} +---- + +==== Pool Monitoring and Metrics + +To monitor your connection pool usage, you can access pool statistics: + +[source,java] +---- +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.stereotype.Component; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.util.Pool; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Component +public class PoolMonitor { + + private static final Logger logger = LoggerFactory.getLogger(PoolMonitor.class); + + @Autowired + private JedisConnectionFactory connectionFactory; + + public void logPoolStats() { + Pool pool = connectionFactory.getPool(); + if (pool != null) { + logger.info("Active connections: {}", pool.getNumActive()); + logger.info("Idle connections: {}", pool.getNumIdle()); + logger.info("Waiting threads: {}", pool.getNumWaiters()); + } + } +} +---- + +==== Common Pool Configuration Scenarios + +.High-traffic applications +[source,yaml] +---- +spring: + data: + redis: + jedis: + pool: + max-active: 50 # Higher connection limit + max-idle: 25 # Keep more idle connections + min-idle: 10 # Maintain minimum pool size + max-wait: 2000ms # Fail fast if pool exhausted + test-on-borrow: true # Ensure connection validity +---- + +.Resource-constrained environments +[source,yaml] +---- +spring: + data: + redis: + jedis: + pool: + max-active: 5 # Limited connections + max-idle: 2 # Minimal idle connections + min-idle: 0 # No minimum required + max-wait: 5000ms # Wait longer for connections + time-between-eviction-runs: 60s # Less frequent eviction +---- + == Redis OM Spring Configuration Properties Redis OM Spring adds specific configuration properties to customize its behavior. @@ -159,6 +357,10 @@ Redis OM Spring adds specific configuration properties to customize its behavior |`true` |Enable wildcard pattern support for repository find operations +|`redis.om.repository.throw-on-save-all-failure` +|`false` +|Throw exceptions on `saveAll()` failures instead of logging warnings (new in 1.0.0) + |`redis.om.index-creation-mode.create-and-replace` |`true` |Create and replace indexes on startup @@ -181,6 +383,7 @@ redis: # Repository Configuration repository: support-wildcard-scan: true + throw-on-save-all-failure: false # Set to true to throw exceptions on bulk save failures # Index Creation index-creation-mode: diff --git a/docs/content/modules/ROOT/pages/entity-streams.adoc b/docs/content/modules/ROOT/pages/entity-streams.adoc index fc432dbd..e19e860d 100644 --- a/docs/content/modules/ROOT/pages/entity-streams.adoc +++ b/docs/content/modules/ROOT/pages/entity-streams.adoc @@ -48,6 +48,13 @@ List redisInc = entityStream.of(Company.class) List companies = entityStream.of(Company.class) .filter(Company$.NAME.eq("RedisInc").and(Company$.YEAR_FOUNDED.eq(2011))) .collect(Collectors.toList()); + +// Combining predicates with different field types (new in 1.0.0) +// Use andAny() or orAny() when combining different field types +List filtered = entityStream.of(Company.class) + .filter(Company$.NAME.eq("RedisInc") + .andAny(Company$.EMPLOYEE_COUNT.gt(100L))) + .collect(Collectors.toList()); ---- === OR Conditions diff --git a/docs/content/modules/ROOT/pages/hash-mappings.adoc b/docs/content/modules/ROOT/pages/hash-mappings.adoc index 1d844df9..38f7cd04 100644 --- a/docs/content/modules/ROOT/pages/hash-mappings.adoc +++ b/docs/content/modules/ROOT/pages/hash-mappings.adoc @@ -1,21 +1,61 @@ [[hash.mappings]] -= Redis Hash Mappings += Redis Hash Mappings with RediSearch :page-toclevels: 3 :experimental: :source-highlighter: highlight.js == Introduction -Spring Data Redis (SDR), the library that Redis OM Spring extends, provides mapping of Spring Entities to Redis Hashes. Redis OM Spring enhances this capability by integrating with the Query Engine available in Redis 8.0.0+ (or via Redis Stack for older versions). +Redis OM Spring fundamentally transforms how Redis Hashes work by adding RediSearch indexing capabilities on top of the standard hash storage. This is a key distinction from Spring Data Redis, which only provides basic hash operations. -This integration brings powerful search and indexing capabilities to Redis Hashes, while maintaining compatibility with existing Spring Data Redis code. +=== The Core Difference -== Key Benefits of Redis OM Spring Hash Mapping +**Spring Data Redis**: Stores entities as Redis Hashes with basic secondary indexing using Redis Sets +**Redis OM Spring**: Stores entities as Redis Hashes PLUS creates RediSearch indexes for powerful querying -* **Enhanced Query Capabilities**: Full text search, numeric ranges, geo-spatial queries -* **Backward Compatibility**: Works with existing Spring Data Redis code -* **Simple Annotations**: Easy to add indexing without changing your data model -* **Performance**: Efficient storage format for simple objects +[source,bash] +---- +# Spring Data Redis approach (basic) +HSET person:123 name "John" email "john@example.com" # Store hash +SADD person:name:John person:123 # Basic index + +# Redis OM Spring approach (enhanced) +HSET person:123 name "John" email "john@example.com" # Store hash (same) +FT.CREATE PersonIdx ON HASH ... # RediSearch index (NEW!) +---- + +Redis OM Spring creates full RediSearch indexes using `FT.CREATE` commands, providing: + +== Key Differences from Spring Data Redis + +[cols="1,2,2"] +|=== +|Feature |Spring Data Redis |Redis OM Spring + +|Indexing Mechanism +|Redis Sets for @Indexed fields +|RediSearch indexes via FT.CREATE + +|Query Performance +|O(N) scans for complex queries +|O(log N) index-based queries + +|Search Capabilities +|Basic equality checks only +|Full-text search, ranges, aggregations + +|Field Types +|Simple hash fields +|TEXT, TAG, NUMERIC, GEO, VECTOR fields + +|Repository Type +|CrudRepository/RedisRepository +|RedisEnhancedRepository + +|Query Methods +|Limited to findBy patterns +|Complex queries, @Query, Entity Streams +|=== == Basic Usage @@ -167,9 +207,9 @@ found.ifPresent(person -> { repository.deleteById(newPerson.getId()); ---- -== How Redis OM Spring Stores Redis Hashes +== How Data is Stored and Indexed -Redis Hashes are flat key-value structures. When storing Java objects, Redis OM Spring follows these mapping rules: +Redis OM Spring stores data in standard Redis Hashes (just like Spring Data Redis) but additionally creates RediSearch indexes for querying: === Object-to-Hash Mapping @@ -207,25 +247,39 @@ Redis Hashes are flat key-value structures. When storing Java objects, Redis OM |`addresses.[0].city = "Tear"` |=== -Additionally, Redis OM Spring adds a `_class` attribute to store type information: +The data is stored in a standard Redis Hash: [source,text] ---- -_class = com.example.Person -id = 01HXYZ123ABC -name = Mat Cauthon -email = mat@bandoftheredhand.com -roles.[0] = general -roles.[1] = gambler -address.city = Tear -address.street = High Street ----- +# Standard Redis Hash (same as Spring Data Redis) +HSET people:01HXYZ123ABC + _class "com.example.Person" + id "01HXYZ123ABC" + name "Mat Cauthon" + email "mat@bandoftheredhand.com" + roles.[0] "general" + roles.[1] "gambler" + address.city "Tear" + address.street "High Street" + +# But Redis OM Spring ALSO creates a RediSearch index +FT.CREATE PersonIdx ON HASH PREFIX 1 people: SCHEMA + name TAG SORTABLE + email TEXT + roles TAG SEPARATOR | + address.city TAG +---- + +This dual approach means: +- Your data remains compatible with Spring Data Redis +- You get powerful search capabilities through RediSearch +- Queries use the index for performance, not scanning == Indexing and Searching -=== Simple Property Indexes +=== How Redis OM Spring Creates Indexes -Use the `@Indexed` annotation to create secondary indexes for fields: +When you annotate fields with `@Indexed`, Redis OM Spring creates a RediSearch index: [source,java] ---- @@ -234,25 +288,32 @@ public class Person { @Id private String id; - @Indexed + @Indexed // Creates a TAG field in RediSearch private String name; - @Indexed + @Searchable // Creates a TEXT field for full-text search private String email; + + @Indexed // Creates a TAG field for set values + private Set roles; } ---- -This creates Redis Sets for each value: +This generates a RediSearch index creation command: [source,text] ---- -SADD people:name:Mat people:01HXYZ123ABC -SADD people:email:mat@example.com people:01HXYZ123ABC +FT.CREATE PersonIdx ON HASH PREFIX 1 people: SCHEMA + name TAG SORTABLE + email TEXT + roles TAG SEPARATOR | ---- -=== Geospatial Indexes +NOTE: This is fundamentally different from Spring Data Redis, which would only create Redis Sets for indexed fields. -For location-based queries, use the `@Indexed` annotation on Point fields: +=== Advanced Field Types + +Redis OM Spring supports specialized field types through RediSearch: [source,java] ---- @@ -261,19 +322,33 @@ public class Company { @Id private String id; - @Indexed + @Indexed // Creates a GEO field in RediSearch private Point location; - @Searchable(sortable = true) + @Searchable(sortable = true) // Creates a TEXT field with SORTABLE private String name; + + @NumericIndexed // Creates a NUMERIC field for range queries + private Integer yearFounded; + + @TagIndexed // Creates a TAG field for exact matches + private Set categories; + + @VectorIndexed(algorithm = VectorAlgorithm.HNSW) // Vector similarity search + private byte[] embedding; } ---- -This enables geo-spatial queries: +The RediSearch index supports complex queries on these fields: [source,text] ---- -GEOADD CompanyIdx:location 13.361389 38.115556 Company:01HXYZ123ABC +FT.CREATE CompanyIdx ON HASH PREFIX 1 Company: SCHEMA + location GEO + name TEXT SORTABLE + yearFounded NUMERIC SORTABLE + categories TAG SEPARATOR | + embedding VECTOR HNSW 6 DIM 768 DISTANCE_METRIC COSINE ---- === Query Methods @@ -306,6 +381,50 @@ public interface PersonRepository extends RedisEnhancedRepository findByNameAndRolesContaining(String name, String role); + +// Translates to RediSearch query +FT.SEARCH PersonIdx "@name:{John} @roles:{admin}" +---- + +=== Entity Streams + +Redis OM Spring provides a fluent API for complex queries: + +[source,java] +---- +@Autowired +EntityStream entityStream; + +// Complex query using Entity Streams +List admins = entityStream + .of(Person.class) + .filter(Person$.ROLES.contains("admin")) + .filter(Person$.NAME.startsWith("J")) + .sorted(Person$.EMAIL, SortOrder.ASC) + .collect(Collectors.toList()); +---- + == Time To Live (TTL) You can set expiration times for entities: @@ -393,11 +512,40 @@ public class RedisConfig { } ---- -== Redis Cluster Considerations +== Migration from Spring Data Redis + +Migrating from Spring Data Redis to Redis OM Spring is straightforward: -When using Redis Cluster, it's important to ensure that related data is stored in the same hash slot to enable atomic operations and efficient queries. +1. **Change the repository interface**: + ```java + // Before: Spring Data Redis + public interface PersonRepository extends CrudRepository { } + + // After: Redis OM Spring + public interface PersonRepository extends RedisEnhancedRepository { } + ``` -Use the `@IdAsHashTag` annotation to ensure that keys for an entity and its indexes are stored in the same hash slot: +2. **Enable enhanced repositories**: + ```java + // Before + @EnableRedisRepositories + + // After + @EnableRedisEnhancedRepositories + ``` + +3. **Add indexing annotations** (optional but recommended): + ```java + @Indexed // For exact matches + @Searchable // For full-text search + @NumericIndexed // For numeric ranges + ``` + +Your existing data remains compatible, and you immediately gain access to powerful search capabilities. + +== Redis Cluster Considerations + +When using Redis Cluster with RediSearch indexes, use the `@IdAsHashTag` annotation to ensure proper data locality: [source,java] ---- @@ -415,14 +563,28 @@ public class HashWithHashTagId { == Performance Considerations -* Redis Hashes are very efficient for simple data structures -* Each query operation requires multiple Redis commands (index lookup + hash retrieval) -* For complex nested objects, consider using xref:json_mappings.adoc[Redis JSON] instead -* Writing objects to a Redis hash deletes and re-creates the whole hash, so data not mapped is lost +* **Index-based queries**: Redis OM Spring uses RediSearch indexes, providing O(log N) query performance vs O(N) scans in Spring Data Redis +* **Storage efficiency**: Data is still stored as standard Redis Hashes, maintaining the same memory efficiency +* **Index overhead**: RediSearch indexes add some memory overhead but enable dramatic query performance improvements +* **Complex objects**: For deeply nested structures, consider xref:json_mappings.adoc[Redis JSON] documents +* **Write behavior**: Updates replace the entire hash (same as Spring Data Redis), unmapped data is lost + +== Summary + +Redis OM Spring enhances Redis Hash entities with RediSearch indexing, providing: + +* **Full compatibility** with Spring Data Redis hash storage +* **Powerful search capabilities** through RediSearch indexes +* **O(log N) query performance** instead of O(N) scans +* **Rich query methods** including full-text search, ranges, and aggregations +* **Advanced field types** like vectors, geo-spatial, and more + +The key takeaway: Redis OM Spring doesn't change how hashes are stored, it adds a powerful search layer on top. == Next Steps * xref:json_mappings.adoc[Redis JSON Mappings] - Compare with JSON document mapping * xref:repository-queries.adoc[Repository Query Methods] - Learn about query capabilities * xref:entity-streams.adoc[Entity Streams] - Explore fluent query API -* xref:search.adoc[Redis Query Engine Integration] - Understand the search capabilities \ No newline at end of file +* xref:search.adoc[Redis Query Engine Integration] - Understand the search capabilities +* xref:index-annotations.adoc[Index Annotations] - Deep dive into indexing options \ No newline at end of file diff --git a/docs/content/modules/ROOT/pages/json-map-fields.adoc b/docs/content/modules/ROOT/pages/json-map-fields.adoc new file mode 100644 index 00000000..a7c97d1f --- /dev/null +++ b/docs/content/modules/ROOT/pages/json-map-fields.adoc @@ -0,0 +1,373 @@ += Map Field Mappings +:page-toclevels: 3 +:experimental: +:source-highlighter: highlight.js + +== Introduction + +Redis OM Spring provides comprehensive support for `Map` fields in JSON documents, allowing you to store dynamic key-value pairs where keys are strings and values can be of various types. This feature is particularly useful for storing flexible, schema-less data within your entities. + +Map fields are automatically indexed using Redis JSON path expressions, enabling powerful query capabilities on map values regardless of their keys. + +== Supported Value Types + +Redis OM Spring supports Maps with the following value types: + +=== Basic Types +* `String` - Indexed as TAG fields +* `Boolean` - Indexed as NUMERIC fields (stored as 1/0) +* `Integer`, `Long`, `Double`, `Float`, `BigDecimal` - Indexed as NUMERIC fields +* `UUID`, `Ulid` - Indexed as TAG fields +* Enum types - Indexed as TAG fields + +=== Temporal Types +* `LocalDateTime`, `LocalDate` - Indexed as NUMERIC fields +* `Date`, `Instant`, `OffsetDateTime` - Indexed as NUMERIC fields (epoch milliseconds) + +=== Spatial Types +* `Point` - Indexed as GEO fields for spatial queries + +== Basic Usage + +=== Entity Definition with Map Fields + +[source,java] +---- +@Data +@Document +public class Product { + @Id + private String id; + + @Indexed + private String name; + + // Map of string attributes + @Indexed + private Map attributes = new HashMap<>(); + + // Map of numeric specifications + @Indexed + private Map specifications = new HashMap<>(); + + // Map of boolean features + @Indexed + private Map features = new HashMap<>(); + + // Map of temporal data + @Indexed + private Map timestamps = new HashMap<>(); +} +---- + +=== Populating Map Fields + +[source,java] +---- +Product product = new Product(); +product.setName("Smartphone"); + +// Add string attributes +product.getAttributes().put("brand", "TechCorp"); +product.getAttributes().put("model", "X2000"); +product.getAttributes().put("color", "Black"); + +// Add numeric specifications +product.getSpecifications().put("screenSize", 6.5); +product.getSpecifications().put("weight", 175.5); +product.getSpecifications().put("batteryCapacity", 4500.0); + +// Add boolean features +product.getFeatures().put("hasNFC", true); +product.getFeatures().put("hasWirelessCharging", true); +product.getFeatures().put("has5G", false); + +// Add temporal data +product.getTimestamps().put("manufactured", LocalDateTime.now()); +product.getTimestamps().put("lastUpdated", LocalDateTime.now()); + +productRepository.save(product); +---- + +== Querying Map Fields + +=== Repository Query Methods + +Redis OM Spring provides special query method naming conventions for Map fields using the `MapContains` suffix: + +[source,java] +---- +public interface ProductRepository extends RedisDocumentRepository { + + // Find by string value in map + List findByAttributesMapContains(String value); + + // Find by numeric value in map + List findBySpecificationsMapContains(Double value); + + // Find by boolean value in map + List findByFeaturesMapContains(Boolean value); + + // Numeric comparisons on map values + List findBySpecificationsMapContainsGreaterThan(Double value); + List findBySpecificationsMapContainsLessThan(Double value); + + // Temporal queries on map values + List findByTimestampsMapContainsAfter(LocalDateTime date); + List findByTimestampsMapContainsBefore(LocalDateTime date); +} +---- + +=== Query Examples + +[source,java] +---- +// Find products with "TechCorp" as any attribute value +List techCorpProducts = repository.findByAttributesMapContains("TechCorp"); + +// Find products with any specification value greater than 1000 +List highSpecProducts = repository.findBySpecificationsMapContainsGreaterThan(1000.0); + +// Find products with NFC feature enabled +List nfcProducts = repository.findByFeaturesMapContains(true); + +// Find products updated after a specific date +LocalDateTime lastWeek = LocalDateTime.now().minusWeeks(1); +List recentlyUpdated = repository.findByTimestampsMapContainsAfter(lastWeek); +---- + +== Advanced Examples + +=== Working with Complex Value Types + +[source,java] +---- +@Data +@Document +public class UserProfile { + @Id + private String id; + + @Indexed + private String username; + + // UUIDs for external system references + @Indexed + private Map externalIds = new HashMap<>(); + + // Enum values for various statuses + @Indexed + private Map statuses = new HashMap<>(); + + // Geographic locations + @Indexed + private Map locations = new HashMap<>(); + + // Monetary values with high precision + @Indexed + private Map balances = new HashMap<>(); + + public enum Status { + ACTIVE, INACTIVE, PENDING, SUSPENDED + } +} +---- + +[source,java] +---- +// Repository interface +public interface UserProfileRepository extends RedisDocumentRepository { + List findByExternalIdsMapContains(UUID uuid); + List findByStatusesMapContains(UserProfile.Status status); + List findByBalancesMapContainsGreaterThan(BigDecimal amount); +} + +// Usage example +UserProfile profile = new UserProfile(); +profile.setUsername("john_doe"); + +// Add external IDs +UUID googleId = UUID.randomUUID(); +profile.getExternalIds().put("google", googleId); +profile.getExternalIds().put("facebook", UUID.randomUUID()); + +// Set statuses +profile.getStatuses().put("account", UserProfile.Status.ACTIVE); +profile.getStatuses().put("subscription", UserProfile.Status.PENDING); + +// Add locations +profile.getLocations().put("home", new Point(-122.4194, 37.7749)); // San Francisco +profile.getLocations().put("work", new Point(-74.0059, 40.7128)); // New York + +// Set balances +profile.getBalances().put("usd", new BigDecimal("1234.56")); +profile.getBalances().put("eur", new BigDecimal("987.65")); + +repository.save(profile); + +// Query examples +List googleUsers = repository.findByExternalIdsMapContains(googleId); +List activeUsers = repository.findByStatusesMapContains(UserProfile.Status.ACTIVE); +List highBalanceUsers = repository.findByBalancesMapContainsGreaterThan( + new BigDecimal("1000.00") +); +---- + +=== Combining Multiple Map Queries + +[source,java] +---- +@Data +@Document +public class Event { + @Id + private String id; + + @Indexed + private String name; + + @Indexed + private Map metadata = new HashMap<>(); + + @Indexed + private Map metrics = new HashMap<>(); + + @Indexed + private Map timeline = new HashMap<>(); +} + +public interface EventRepository extends RedisDocumentRepository { + // Combine multiple map queries + List findByMetadataMapContainsAndMetricsMapContainsGreaterThan( + String metadataValue, Integer metricThreshold + ); + + List findByNameAndTimelineMapContainsAfter( + String name, LocalDateTime after + ); +} +---- + +== Important Considerations + +=== Indexing + +* Map fields must be annotated with `@Indexed` to be searchable +* Each Map field creates a single index for all its values, regardless of keys +* The index uses JSONPath expressions (e.g., `$.fieldName.*`) to capture all values + +=== Performance + +* Map value queries search across all values in the map, not specific keys +* For large maps, consider the performance implications of indexing all values +* Numeric and temporal comparisons are efficient due to NUMERIC indexing + +=== Type Consistency + +* All values in a Map must be of the same declared type +* Mixed-type maps are not supported for indexed fields +* Type conversion follows standard Redis OM Spring serialization rules + +=== Temporal Precision + +* Date/time values may experience precision loss during serialization +* Millisecond precision is preserved for most temporal types +* Consider using tolerance when comparing temporal values in tests + +=== Boolean Values + +* Boolean values in Maps are indexed as NUMERIC fields (1 for true, 0 for false) +* This differs from regular Boolean entity fields, which are indexed as TAG fields +* Queries work transparently with both `true`/`false` parameters + +== Query Patterns + +=== Equality Queries + +For exact value matching across all map entries: + +[source,java] +---- +// Find entities where any map value equals the parameter +List findByMapFieldMapContains(ValueType value); +---- + +=== Range Queries (Numeric/Temporal) + +For numeric and temporal value types: + +[source,java] +---- +// Greater than +List findByMapFieldMapContainsGreaterThan(ValueType value); + +// Less than +List findByMapFieldMapContainsLessThan(ValueType value); + +// Temporal queries +List findByMapFieldMapContainsAfter(TemporalType value); +List findByMapFieldMapContainsBefore(TemporalType value); +---- + +=== Combining with Other Fields + +Map queries can be combined with regular field queries: + +[source,java] +---- +List findByRegularFieldAndMapFieldMapContains( + String regularValue, MapValueType mapValue +); +---- + +== Limitations + +* **No key-based queries**: You cannot query for specific keys, only values +* **No partial matching**: String values in maps use TAG indexing (exact match only) +* **GEO queries**: Point values support equality through proximity search with minimal radius +* **Collection values**: Maps with collection-type values are not supported + +== Best Practices + +1. **Use meaningful value types**: Choose value types that match your query requirements +2. **Consider index size**: Large maps with many entries will create larger indexes +3. **Consistent naming**: Use clear, descriptive names for Map fields +4. **Initialize maps**: Always initialize Map fields to avoid null pointer exceptions +5. **Document value semantics**: Document what each potential key represents in your maps + +== Migration Guide + +If you're migrating from a schema with fixed fields to using Maps: + +1. Create the Map field with appropriate value type +2. Add `@Indexed` annotation +3. Migrate data by populating the Map with key-value pairs +4. Update repository methods to use `MapContains` pattern +5. Test queries thoroughly, especially for numeric and temporal types + +[source,java] +---- +// Before: Fixed fields +@Document +public class OldProduct { + private String color; + private String size; + private String material; +} + +// After: Flexible Map +@Document +public class NewProduct { + @Indexed + private Map attributes = new HashMap<>(); +} + +// Migration code +oldProduct.getColor() -> newProduct.getAttributes().put("color", oldProduct.getColor()); +oldProduct.getSize() -> newProduct.getAttributes().put("size", oldProduct.getSize()); +oldProduct.getMaterial() -> newProduct.getAttributes().put("material", oldProduct.getMaterial()); +---- + +== Conclusion + +Map field support in Redis OM Spring provides a powerful way to handle dynamic, schema-less data within your Redis JSON documents. With comprehensive type support and intuitive query methods, you can build flexible data models while maintaining full search capabilities. \ No newline at end of file diff --git a/docs/content/modules/ROOT/pages/json-repositories.adoc b/docs/content/modules/ROOT/pages/json-repositories.adoc index f86e3105..79460f0f 100644 --- a/docs/content/modules/ROOT/pages/json-repositories.adoc +++ b/docs/content/modules/ROOT/pages/json-repositories.adoc @@ -251,6 +251,112 @@ public interface CompanyRepository extends RedisDocumentRepository> updates = new ArrayList<>(); + + // Prepare multiple updates + Company update1 = new Company(); + update1.setId("company:1"); + update1.setPubliclyListed(true); + updates.add(Example.of(update1)); + + Company update2 = new Company(); + update2.setId("company:2"); + update2.setYearFounded(2012); + updates.add(Example.of(update2)); + + // Execute all updates in a single pipelined operation + repository.updateAll(updates); +} +---- + +=== Update Nested Objects + +Partial updates work with nested structures: + +[source,java] +---- +public void updateNestedObject() { + Company updateProbe = new Company(); + updateProbe.setId("company:1"); + + // Update nested address object + Address newAddress = new Address(); + newAddress.setCity("Mountain View"); + newAddress.setCountry("USA"); + updateProbe.setAddress(newAddress); + + repository.update(Example.of(updateProbe)); + // Only the address field is updated +} +---- + +=== Important Notes + +* **ID Required**: The update probe must have a non-null ID +* **Non-null Fields**: Only non-null fields in the probe are updated +* **Atomic Updates**: Each field is updated atomically using JSON.SET +* **Existing Documents**: Updates only affect existing documents (uses XX flag) +* **Performance**: Bulk updates use pipelining for better performance + == Example Usage Here's a complete example showing repository usage: diff --git a/docs/content/modules/ROOT/pages/json_mappings.adoc b/docs/content/modules/ROOT/pages/json_mappings.adoc index cc3e69af..ef9412aa 100644 --- a/docs/content/modules/ROOT/pages/json_mappings.adoc +++ b/docs/content/modules/ROOT/pages/json_mappings.adoc @@ -205,6 +205,45 @@ public class Company { } ---- +=== Map Field Support + +Redis OM Spring provides comprehensive support for `Map` fields, enabling dynamic key-value pairs with full indexing and query capabilities: + +[source,java] +---- +@Document +public class Product { + @Id + private String id; + + @Indexed + private Map attributes; // String values + + @Indexed + private Map specifications; // Numeric values + + @Indexed + private Map features; // Boolean flags + + @Indexed + private Map events; // Temporal data +} +---- + +Query Map fields using the `MapContains` pattern: + +[source,java] +---- +public interface ProductRepository extends RedisDocumentRepository { + // Find by any map value + List findByAttributesMapContains(String value); + List findBySpecificationsMapContainsGreaterThan(Double value); + List findByFeaturesMapContains(Boolean hasFeature); +} +---- + +For comprehensive coverage of Map field capabilities, see xref:json-map-fields.adoc[Map Field Mappings]. + == Indexing and Search === Field-Level Indexing diff --git a/docs/content/modules/ROOT/pages/redis-repositories.adoc b/docs/content/modules/ROOT/pages/redis-repositories.adoc index 290c2503..eb85c56b 100644 --- a/docs/content/modules/ROOT/pages/redis-repositories.adoc +++ b/docs/content/modules/ROOT/pages/redis-repositories.adoc @@ -147,7 +147,7 @@ The following table lists the methods implemented from https://docs.spring.io/sp |Returns all instances of the type T with the given IDs. | List saveAll(Iterable entities) -|Saves all given entities. +|Saves all given entities. By default, errors are logged as warnings. Set `redis.om.repository.throw-on-save-all-failure=true` to throw exceptions on failures (new in 1.0.0). |=== === PagingSortingRepository diff --git a/docs/package-lock.json b/docs/package-lock.json index 8c1a39a0..9f03e40d 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -8,21 +8,21 @@ "name": "redis-om-spring-docs", "version": "1.0.0", "dependencies": { - "@antora/cli": "^3.1.4", - "@antora/site-generator": "^3.1.10", - "@antora/site-generator-default": "^3.1.4" + "@antora/cli": "^3.1.12", + "@antora/site-generator": "3.1.12", + "@antora/site-generator-default": "^3.1.12" }, "devDependencies": { "http-server": "^14.1.1" } }, "node_modules/@antora/asciidoc-loader": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.10.tgz", - "integrity": "sha512-np0JkOV37CK7V4eDZUZXf4fQuCKYW3Alxl8FlyzBevXi2Ujv29O82JLbHbv1cyTsvGkGNNB+gzJIx9XBsQ7+Nw==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.12.tgz", + "integrity": "sha512-KP81whxkQzyNIZi/lnX3FEHUEUV2YAsxb07Dsx6qjVMIX8yMFRB/s6EAYgQralLnZDC1uCfLDXEIFLMUEGsDIw==", "license": "MPL-2.0", "dependencies": { - "@antora/logger": "3.1.10", + "@antora/logger": "3.1.12", "@antora/user-require-helper": "~3.0", "@asciidoctor/core": "~2.2" }, @@ -31,14 +31,15 @@ } }, "node_modules/@antora/cli": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/cli/-/cli-3.1.4.tgz", - "integrity": "sha512-bLDt10VSOcqsHOM5kubjvx9HfdqzLESWEM4Hv0zOPsG3drXKZM/PkStDj6wVt2J6B4OtruusLH2CETKkd9vfGQ==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/cli/-/cli-3.1.12.tgz", + "integrity": "sha512-j6CPjaW+OcEMecZFWOa38WsT4UvNVXEwUJRRTITtGbIk4m7z7RMrFCeai4AUX6cidX9LgDkFq6w2mjrYeVSS+A==", + "license": "MPL-2.0", "dependencies": { - "@antora/logger": "3.1.4", - "@antora/playbook-builder": "3.1.4", - "@antora/user-require-helper": "~2.0", - "commander": "~10.0" + "@antora/logger": "3.1.12", + "@antora/playbook-builder": "3.1.12", + "@antora/user-require-helper": "~3.0", + "commander": "~11.1" }, "bin": { "antora": "bin/antora" @@ -47,217 +48,14 @@ "node": ">=16.0.0" } }, - "node_modules/@antora/cli/node_modules/@antora/expand-path-helper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-2.0.0.tgz", - "integrity": "sha512-CSMBGC+tI21VS2kGW3PV7T2kQTM5eT3f2GTPVLttwaNYbNxDve08en/huzszHJfxo11CcEs26Ostr0F2c1QqeA==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@antora/cli/node_modules/@antora/logger": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.4.tgz", - "integrity": "sha512-E5B9NnZoe4wHOv1MWWYqaGDSOlADHGZd5mZJFvuA0guJCbO3amAhi3ZZ12tOOF3nvpZ3UABFMf9aIzojlERQJw==", - "dependencies": { - "@antora/expand-path-helper": "~2.0", - "pino": "~8.14", - "pino-pretty": "~10.0", - "sonic-boom": "~3.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/cli/node_modules/@antora/playbook-builder": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.4.tgz", - "integrity": "sha512-Vf3bx6Wqz4ATrKsYoOqu1UctNu8/H/WSVvrAHgsweoD5vPHElISQ2XH6cyOvRNf93Qn1ckQSgOLjpd9N2KE0aA==", - "dependencies": { - "@iarna/toml": "~2.2", - "convict": "~6.2", - "js-yaml": "~4.1", - "json5": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/cli/node_modules/@antora/user-require-helper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-2.0.0.tgz", - "integrity": "sha512-5fMfBZfw4zLoFdDAPMQX6Frik90uvfD8rXOA4UpXPOUikkX4uT1Rk6m0/4oi8oS3fcjiIl0k/7Nc+eTxW5TcQQ==", - "dependencies": { - "@antora/expand-path-helper": "~2.0" - }, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@antora/cli/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@antora/cli/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@antora/cli/node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/@antora/cli/node_modules/help-me/node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@antora/cli/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@antora/cli/node_modules/pino": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.14.2.tgz", - "integrity": "sha512-zKu9aWeSWTy1JgvxIpZveJKKsAr4+6uNMZ0Vf0KRwzl/UNZA3XjHiIl/0WwqLMkDwuHuDkT5xAgPA2jpKq4whA==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.1.0", - "thread-stream": "^2.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/@antora/cli/node_modules/pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/@antora/cli/node_modules/pino-pretty": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.0.1.tgz", - "integrity": "sha512-yrn00+jNpkvZX/NrPVCPIVHAfTDy3ahF0PND9tKqZk4j9s+loK8dpzrJj4dGb7i+WLuR50ussuTAiWoMWU+qeA==", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^4.0.1", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^3.0.0", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "pino-pretty": "bin.js" - } - }, - "node_modules/@antora/cli/node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - }, - "node_modules/@antora/cli/node_modules/process-warning": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", - "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" - }, - "node_modules/@antora/cli/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@antora/cli/node_modules/sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/@antora/cli/node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/@antora/content-aggregator": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.10.tgz", - "integrity": "sha512-OT6ZcCA7LrtNfrAZUr3hFh+Z/1isKpsfnqFjCDC66NEMqIyzJO99jq0CM66rYlYhyX7mb5BwEua8lHcwpOXNow==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.12.tgz", + "integrity": "sha512-l+6KhqQHfoficcgm4tzDmFhHJ0mDNZBWAhwO60b9HzWdcc9hPDo87KU2t/vwxC6P+o9Vzdc6xa9EWgwHqw9/HQ==", "license": "MPL-2.0", "dependencies": { "@antora/expand-path-helper": "~3.0", - "@antora/logger": "3.1.10", + "@antora/logger": "3.1.12", "@antora/user-require-helper": "~3.0", "braces": "~3.0", "cache-directory": "~2.0", @@ -277,13 +75,13 @@ } }, "node_modules/@antora/content-classifier": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.10.tgz", - "integrity": "sha512-3JJl4IIiTX00v/MirK603NoqIcHjGYAaRWt3Q4U03tI1Fv2Aho/ypO3FE45069jFf0Dx2uDJfp5kapb9gaIjdQ==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.12.tgz", + "integrity": "sha512-Q+X5w3U2yoZmsFTRi5jWvx67PEq++S6gdO5PJ4dQ0r4nmDaMYdhRtfrxZeni4RmWtDxF+f0GZYw8RgyhEIBRdw==", "license": "MPL-2.0", "dependencies": { - "@antora/asciidoc-loader": "3.1.10", - "@antora/logger": "3.1.10", + "@antora/asciidoc-loader": "3.1.12", + "@antora/logger": "3.1.12", "mime-types": "~2.1", "vinyl": "~3.0" }, @@ -292,12 +90,12 @@ } }, "node_modules/@antora/document-converter": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.10.tgz", - "integrity": "sha512-qi9ctgcKal8tZtWflVo66w+4zCJoBmUKRV+eA9aRRR09KDdU9r514vu1adWNgniPppISr90zD13V5l2JUy/2CQ==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.12.tgz", + "integrity": "sha512-dOh/X0XddSW6Ho529jmD0E6M97RVRMOR4G1xdBJ6O9RUPZfOz2ERijufdi8jZrz5aDrJLpB1hzAbFccCGlzMHA==", "license": "MPL-2.0", "dependencies": { - "@antora/asciidoc-loader": "3.1.10" + "@antora/asciidoc-loader": "3.1.12" }, "engines": { "node": ">=16.0.0" @@ -313,9 +111,9 @@ } }, "node_modules/@antora/file-publisher": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.10.tgz", - "integrity": "sha512-DPR/0d1P+kr3qV4T0Gh81POEO/aCmNWIp/oLUYAhr0HHOcFzgpTUUoLStgcYynZPFRIB7EYKSab+oYSCK17DGA==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.12.tgz", + "integrity": "sha512-psinQkI3IrARRejK7fFKVGYYE5z01jDUHHcUSDRR48eriDO/K/wnky2n2BbKPQsPjYZQP8LyPfVKUsTadsxx8A==", "license": "MPL-2.0", "dependencies": { "@antora/expand-path-helper": "~3.0", @@ -328,561 +126,117 @@ } }, "node_modules/@antora/logger": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.10.tgz", - "integrity": "sha512-WSuIxEP2tVrhWtTj/sIrwBDjpi4ldB/1Kpiu4PXmY4/qeWP8thW6u8nXdwdDcWss5zqkZWjourvWKwVq7y8Wjg==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.12.tgz", + "integrity": "sha512-u+rw3aFW7ScPyG/aWcxGnKJl/++34kzvdZEi3FIGjqtgsEmZ0A4UKbH3t4LZOiQiv9UdcHAiv5+lxQRx+7LgBQ==", "license": "MPL-2.0", "dependencies": { "@antora/expand-path-helper": "~3.0", "pino": "~9.2", "pino-pretty": "~11.2", - "sonic-boom": "~4.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/navigation-builder": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.10.tgz", - "integrity": "sha512-aLMK49nYsSB3mEZbLkmUXDAUYmscv2AFWu+5c3eqVGkQ6Wgyd79WQ6Bz3/TN9YqkzGL+PqGs0G39F0VQzD23Hw==", - "license": "MPL-2.0", - "dependencies": { - "@antora/asciidoc-loader": "3.1.10" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/page-composer": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.10.tgz", - "integrity": "sha512-JoEg8J8HVsnPmAgUrYSGzf0C8rQefXyCi/18ucy0utyfUvlJNsZvUbGUPx62Het9p0JP0FkAz2MTLyDlNdArVg==", - "license": "MPL-2.0", - "dependencies": { - "@antora/logger": "3.1.10", - "handlebars": "~4.7", - "require-from-string": "~2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/playbook-builder": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.10.tgz", - "integrity": "sha512-UB8UmRYfkKgActTUlotdVS4FKGjaZgTnSXE7Fns1xb3/3HRanWvI+Yze1OmCkGC33cTpoQFnSYp7ySEH8LaiBw==", - "license": "MPL-2.0", - "dependencies": { - "@iarna/toml": "~2.2", - "convict": "~6.2", - "js-yaml": "~4.1", - "json5": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/redirect-producer": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.10.tgz", - "integrity": "sha512-IbWJGh6LmsxJQ821h0B9JfooofFZBgFLZxsbp/IoTLkBFGLFAY5tDRvB6rvubfNLRoSjM8VjEUXGqVLlwZOb+g==", - "license": "MPL-2.0", - "dependencies": { - "vinyl": "~3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.10.tgz", - "integrity": "sha512-NCULYtwUjIyr5FGCymhfG/zDVUmZ6pfmCPorka8mAzo4/GDx1T7bgaRL9rEIyf2AMqcm7apQiAz03mpU4kucsw==", - "license": "MPL-2.0", - "dependencies": { - "@antora/asciidoc-loader": "3.1.10", - "@antora/content-aggregator": "3.1.10", - "@antora/content-classifier": "3.1.10", - "@antora/document-converter": "3.1.10", - "@antora/file-publisher": "3.1.10", - "@antora/logger": "3.1.10", - "@antora/navigation-builder": "3.1.10", - "@antora/page-composer": "3.1.10", - "@antora/playbook-builder": "3.1.10", - "@antora/redirect-producer": "3.1.10", - "@antora/site-mapper": "3.1.10", - "@antora/site-publisher": "3.1.10", - "@antora/ui-loader": "3.1.10", - "@antora/user-require-helper": "~3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/site-generator-default/-/site-generator-default-3.1.4.tgz", - "integrity": "sha512-BSsS1nrmT8v0uRyobDfNes6llUp3MoXKSbQTJ6PE8wNd8SLjXajTq6IXw6XJ8LnWzMFRvj0nPwqpA10jPDfbmg==", - "dependencies": { - "@antora/site-generator": "3.1.4" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/asciidoc-loader": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/asciidoc-loader/-/asciidoc-loader-3.1.4.tgz", - "integrity": "sha512-ttsPR1J6gt7gGiCPrtfKX6tSbCHztzpRP0t+/6m+o3JCwXnjoD+dCi73hp8UCkyS+EU+GTo/FeXclYjZnUMXhQ==", - "dependencies": { - "@antora/logger": "3.1.4", - "@antora/user-require-helper": "~2.0", - "@asciidoctor/core": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/content-aggregator": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/content-aggregator/-/content-aggregator-3.1.4.tgz", - "integrity": "sha512-qurPCaV8w6S1u9aN53NWVyznVxo/Tnhiy2gFqQQPPZZkecGiEqiXm2bd6DF5WPDnb1GMWn87qX7b7uwxuJXB3Q==", - "dependencies": { - "@antora/expand-path-helper": "~2.0", - "@antora/logger": "3.1.4", - "@antora/user-require-helper": "~2.0", - "braces": "~3.0", - "cache-directory": "~2.0", - "glob-stream": "~7.0", - "hpagent": "~1.2", - "isomorphic-git": "~1.21", - "js-yaml": "~4.1", - "multi-progress": "~4.0", - "picomatch": "~2.3", - "progress": "~2.0", - "should-proxy": "~1.0", - "simple-get": "~4.0", - "vinyl": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/content-classifier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/content-classifier/-/content-classifier-3.1.4.tgz", - "integrity": "sha512-23/C9uXPCGc7eCyCbr/BnppUzrXuY0uJJsemuVM0CF0woRJ+/Gat0wXwvTvZF4C7Lt1WBKlf2yT0Uk7hDOvg8A==", - "dependencies": { - "@antora/asciidoc-loader": "3.1.4", - "@antora/logger": "3.1.4", - "mime-types": "~2.1", - "vinyl": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/document-converter": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/document-converter/-/document-converter-3.1.4.tgz", - "integrity": "sha512-zKpT/I025yHfI0RMnNHDhH24Uj4XVLwIPOjzCWLiogIKXxUkFGltQ55V2Ph7LMysYF7/3RVwpQx3cYhJv5QXBQ==", - "dependencies": { - "@antora/asciidoc-loader": "3.1.4" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/expand-path-helper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antora/expand-path-helper/-/expand-path-helper-2.0.0.tgz", - "integrity": "sha512-CSMBGC+tI21VS2kGW3PV7T2kQTM5eT3f2GTPVLttwaNYbNxDve08en/huzszHJfxo11CcEs26Ostr0F2c1QqeA==", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/file-publisher": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/file-publisher/-/file-publisher-3.1.4.tgz", - "integrity": "sha512-pHCy+wOkzjVPRF16fY4AzFcMt2B2c0r+CE5Er1quOhl19jL7wwaw3OmCuzgJ/BmZynDFyerlfLu2MPlWJVL1+Q==", - "dependencies": { - "@antora/expand-path-helper": "~2.0", - "@antora/user-require-helper": "~2.0", - "gulp-vinyl-zip": "~2.5", - "vinyl": "~2.2", - "vinyl-fs": "~3.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/logger": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/logger/-/logger-3.1.4.tgz", - "integrity": "sha512-E5B9NnZoe4wHOv1MWWYqaGDSOlADHGZd5mZJFvuA0guJCbO3amAhi3ZZ12tOOF3nvpZ3UABFMf9aIzojlERQJw==", - "dependencies": { - "@antora/expand-path-helper": "~2.0", - "pino": "~8.14", - "pino-pretty": "~10.0", - "sonic-boom": "~3.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/navigation-builder": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.4.tgz", - "integrity": "sha512-HfH77gDKiL4ZYUWtWtuJnJunWdALDyql198SNcLlD/Vs2ZatO3qucP6YZXSX6k8aqj9XU4L8xkhltr24iDWlkg==", - "dependencies": { - "@antora/asciidoc-loader": "3.1.4" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/page-composer": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.4.tgz", - "integrity": "sha512-9eZEqjhC7jgOpJLRIN5kQ3cmy2NiajAq9n4bmZdVsOYUdkyleiOdUSLuxwQOSY0PkKxEQTqBKzAFG9WL1pTODQ==", - "dependencies": { - "@antora/logger": "3.1.4", - "handlebars": "~4.7", - "require-from-string": "~2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/playbook-builder": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.4.tgz", - "integrity": "sha512-Vf3bx6Wqz4ATrKsYoOqu1UctNu8/H/WSVvrAHgsweoD5vPHElISQ2XH6cyOvRNf93Qn1ckQSgOLjpd9N2KE0aA==", - "dependencies": { - "@iarna/toml": "~2.2", - "convict": "~6.2", - "js-yaml": "~4.1", - "json5": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/redirect-producer": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.4.tgz", - "integrity": "sha512-1AdPwmCo1VCHpzL9AOgmte6zmzeNffdUXGl3oDE91wsNCNK/deZw4TgImVjou5NY5m34UTprIZSTZ5IsLt5ccQ==", - "dependencies": { - "vinyl": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/site-generator": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.4.tgz", - "integrity": "sha512-RG2w0U+8tljULY7iQqLW2FECaQJguSBd2HhR0exEiw/eHv66q6ANzsYI3AGTmLUqU/F2n30+gSZCeepVyO1gWA==", - "dependencies": { - "@antora/asciidoc-loader": "3.1.4", - "@antora/content-aggregator": "3.1.4", - "@antora/content-classifier": "3.1.4", - "@antora/document-converter": "3.1.4", - "@antora/file-publisher": "3.1.4", - "@antora/logger": "3.1.4", - "@antora/navigation-builder": "3.1.4", - "@antora/page-composer": "3.1.4", - "@antora/playbook-builder": "3.1.4", - "@antora/redirect-producer": "3.1.4", - "@antora/site-mapper": "3.1.4", - "@antora/site-publisher": "3.1.4", - "@antora/ui-loader": "3.1.4", - "@antora/user-require-helper": "~2.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/site-mapper": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.4.tgz", - "integrity": "sha512-HWtOnxv467sdafG39AxPtEFKrZxkWpBaxmRGzgPELFUdnZxaI4fzeIyVj7CE8AJunVq5KMIqZpca2redLC5CJA==", - "dependencies": { - "@antora/content-classifier": "3.1.4", - "vinyl": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/site-publisher": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.4.tgz", - "integrity": "sha512-mBw9eDgtblL54S2NSJ5tbHReA+0tSURFYu7wUTiZ7knnM8c0nyJmcwrOyv5lImaFlPHC3qt1oXuewyhAeqSYDA==", - "dependencies": { - "@antora/file-publisher": "3.1.4" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/ui-loader": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.4.tgz", - "integrity": "sha512-J1eGOr4bdou6Kb7RfhSNMLwNHPywZ42OB3wUsBFgWtRNDah+iMN1A1OzmtAixcc9FlmKm6oKjyw3TpD9D65hrA==", - "dependencies": { - "@antora/expand-path-helper": "~2.0", - "braces": "~3.0", - "cache-directory": "~2.0", - "glob-stream": "~7.0", - "gulp-vinyl-zip": "~2.5", - "hpagent": "~1.2", - "js-yaml": "~4.1", - "picomatch": "~2.3", - "should-proxy": "~1.0", - "simple-get": "~4.0", - "vinyl": "~2.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/@antora/user-require-helper": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@antora/user-require-helper/-/user-require-helper-2.0.0.tgz", - "integrity": "sha512-5fMfBZfw4zLoFdDAPMQX6Frik90uvfD8rXOA4UpXPOUikkX4uT1Rk6m0/4oi8oS3fcjiIl0k/7Nc+eTxW5TcQQ==", - "dependencies": { - "@antora/expand-path-helper": "~2.0" - }, - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@antora/site-generator-default/node_modules/help-me": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/help-me/-/help-me-4.2.0.tgz", - "integrity": "sha512-TAOnTB8Tz5Dw8penUuzHVrKNKlCIbwwbHnXraNJxPwf8LRtE2HlM84RYuezMFcwOJmoYOCWVDyJ8TQGxn9PgxA==", - "dependencies": { - "glob": "^8.0.0", - "readable-stream": "^3.6.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/isomorphic-git": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.21.0.tgz", - "integrity": "sha512-ZqCAUM63CYepA3fB8H7NVyPSiOkgzIbQ7T+QPrm9xtYgQypN9JUJ5uLMjB5iTfomdJf3mdm6aSxjZwnT6ubvEA==", - "dependencies": { - "async-lock": "^1.1.0", - "clean-git-ref": "^2.0.1", - "crc-32": "^1.2.0", - "diff3": "0.0.3", - "ignore": "^5.1.4", - "minimisted": "^2.0.0", - "pako": "^1.0.10", - "pify": "^4.0.1", - "readable-stream": "^3.4.0", - "sha.js": "^2.4.9", - "simple-get": "^4.0.1" - }, - "bin": { - "isogit": "cli.cjs" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@antora/site-generator-default/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@antora/site-generator-default/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/@antora/site-generator-default/node_modules/pino": { - "version": "8.14.2", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.14.2.tgz", - "integrity": "sha512-zKu9aWeSWTy1JgvxIpZveJKKsAr4+6uNMZ0Vf0KRwzl/UNZA3XjHiIl/0WwqLMkDwuHuDkT5xAgPA2jpKq4whA==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.0.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.1.0", - "thread-stream": "^2.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/@antora/site-generator-default/node_modules/pino-abstract-transport": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.0.0.tgz", - "integrity": "sha512-c7vo5OpW4wIS42hUVcT5REsL8ZljsUfBjqV/e2sFxmFEFZiq1XLUp5EYLtuDH6PEHq9W1egWqRbnLUP5FuZmOA==", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/@antora/site-generator-default/node_modules/pino-pretty": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-10.0.1.tgz", - "integrity": "sha512-yrn00+jNpkvZX/NrPVCPIVHAfTDy3ahF0PND9tKqZk4j9s+loK8dpzrJj4dGb7i+WLuR50ussuTAiWoMWU+qeA==", - "dependencies": { - "colorette": "^2.0.7", - "dateformat": "^4.6.3", - "fast-copy": "^3.0.0", - "fast-safe-stringify": "^2.1.1", - "help-me": "^4.0.1", - "joycon": "^3.1.1", - "minimist": "^1.2.6", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^1.0.0", - "pump": "^3.0.0", - "readable-stream": "^4.0.0", - "secure-json-parse": "^2.4.0", - "sonic-boom": "^3.0.0", - "strip-json-comments": "^3.1.1" + "sonic-boom": "~4.0" }, - "bin": { - "pino-pretty": "bin.js" + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@antora/site-generator-default/node_modules/pino-pretty/node_modules/readable-stream": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz", - "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==", + "node_modules/@antora/navigation-builder": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/navigation-builder/-/navigation-builder-3.1.12.tgz", + "integrity": "sha512-C8Ty/yQYiAr793Xsox+AD8tdDzJYUVKs6pBoSNQQcSmabej93s0WnaSiezmBJAlj2/n5KysKG6/fQDqKnCAGyg==", + "license": "MPL-2.0", "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" + "@antora/asciidoc-loader": "3.1.12" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=16.0.0" } }, - "node_modules/@antora/site-generator-default/node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - }, - "node_modules/@antora/site-generator-default/node_modules/process-warning": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz", - "integrity": "sha512-n9wh8tvBe5sFmsqlg+XQhaQLumwpqoAUruLwjCopgTmUBjJ/fjtBsJzKleCaIGBOMXYEhp1YfKl4d7rJ5ZKJGA==" + "node_modules/@antora/page-composer": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/page-composer/-/page-composer-3.1.12.tgz", + "integrity": "sha512-DpGYXwEoo9Ku/Udz/vQGKAbLqibyesL/MI+hG4ykaj43NzN5kxAfmV1UCb9ZrB2x8JCkPEJQb5ibZntz93b2MA==", + "license": "MPL-2.0", + "dependencies": { + "@antora/logger": "3.1.12", + "handlebars": "~4.7", + "require-from-string": "~2.0" + }, + "engines": { + "node": ">=16.0.0" + } }, - "node_modules/@antora/site-generator-default/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "node_modules/@antora/playbook-builder": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/playbook-builder/-/playbook-builder-3.1.12.tgz", + "integrity": "sha512-5sizDOdg5SWMs84EWGRBzmej1V23tpGBwHOqjI2BJEGh8Sase1qC5uZXx6aJDEVbzmlGOm4RvysWbn9oap248A==", + "license": "MPL-2.0", + "dependencies": { + "@iarna/toml": "~2.2", + "convict": "~6.2", + "js-yaml": "~4.1", + "json5": "~2.2" + }, "engines": { - "node": ">= 0.10" + "node": ">=16.0.0" } }, - "node_modules/@antora/site-generator-default/node_modules/sonic-boom": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", - "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", + "node_modules/@antora/redirect-producer": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/redirect-producer/-/redirect-producer-3.1.12.tgz", + "integrity": "sha512-bVoKC52nKzLn1ROM9zcotYoakr7gdfrBVPwWRSCvNu+Yx0iUOiCWJAA6+DsY7ut4l4LlaERp1oTvAhViX9TQCQ==", + "license": "MPL-2.0", "dependencies": { - "atomic-sleep": "^1.0.0" + "vinyl": "~3.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@antora/site-generator-default/node_modules/thread-stream": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.7.0.tgz", - "integrity": "sha512-qQiRWsU/wvNolI6tbbCKd9iKaTnCXsTwVxhhKM6nctPdujTyztjlbUkUTUymidWcMnZ5pWR0ej4a0tjsW021vw==", + "node_modules/@antora/site-generator": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/site-generator/-/site-generator-3.1.12.tgz", + "integrity": "sha512-vUpbK6bdvP3zuVxmvhF3Rz+okNXD95HWHw6xb08qEraZOWPZHOXl23twqprmk12O0PunN9WZd6/Pf14zhD208A==", + "license": "MPL-2.0", "dependencies": { - "real-require": "^0.2.0" + "@antora/asciidoc-loader": "3.1.12", + "@antora/content-aggregator": "3.1.12", + "@antora/content-classifier": "3.1.12", + "@antora/document-converter": "3.1.12", + "@antora/file-publisher": "3.1.12", + "@antora/logger": "3.1.12", + "@antora/navigation-builder": "3.1.12", + "@antora/page-composer": "3.1.12", + "@antora/playbook-builder": "3.1.12", + "@antora/redirect-producer": "3.1.12", + "@antora/site-mapper": "3.1.12", + "@antora/site-publisher": "3.1.12", + "@antora/ui-loader": "3.1.12", + "@antora/user-require-helper": "~3.0" + }, + "engines": { + "node": ">=16.0.0" } }, - "node_modules/@antora/site-generator-default/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "node_modules/@antora/site-generator-default": { + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/site-generator-default/-/site-generator-default-3.1.12.tgz", + "integrity": "sha512-viu2ZkfTQrdca7TabKO/awubC3/hLfRi1ZM+SfFJUILZj7g+EvWIR3VHdQDJtxaMykNIdeAfsVB6T69ZNoyg4g==", + "license": "MPL-2.0", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "@antora/site-generator": "3.1.12" }, "engines": { - "node": ">= 0.10" + "node": ">=16.0.0" } }, "node_modules/@antora/site-mapper": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.10.tgz", - "integrity": "sha512-KY1j/y0uxC2Y7RAo4r4yKv9cgFm8aZoRylZXEODJnwj3tffbZ2ZdRzSWHp6fN0QX/Algrr9JNd9CWrjcj2f3Zw==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/site-mapper/-/site-mapper-3.1.12.tgz", + "integrity": "sha512-qWZjlB5OrTyN8gsrIeDeqXBuu83y+LOfve4aFrNZ2Klby8ObURMT/gGWicsXNA/q0tlrsouhN9hRLqpFoEj63g==", "license": "MPL-2.0", "dependencies": { - "@antora/content-classifier": "3.1.10", + "@antora/content-classifier": "3.1.12", "vinyl": "~3.0" }, "engines": { @@ -890,21 +244,21 @@ } }, "node_modules/@antora/site-publisher": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.10.tgz", - "integrity": "sha512-G4xcUWvgth8oeEQwiu9U1cE0miQtYHwKHOobUbDBt2Y6LlC5H31zQQmAyvMwTsGRlvYRgLVtG6j9d6JBwQ6w9Q==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/site-publisher/-/site-publisher-3.1.12.tgz", + "integrity": "sha512-oeNNXcsKPzNxM/TgixlOHJCUaIGZ4WnpacN5EfeU3PfymJVADMsaAGjKwKd+miDdsjNAQKR8GrKtO5rKUj3PqA==", "license": "MPL-2.0", "dependencies": { - "@antora/file-publisher": "3.1.10" + "@antora/file-publisher": "3.1.12" }, "engines": { "node": ">=16.0.0" } }, "node_modules/@antora/ui-loader": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.10.tgz", - "integrity": "sha512-H1f5wI5a5HjLuE/Wexvc8NZy8w83Bhqjka7t1DbwOOqP+LyxFGLx/QbBVKdTtgFNDHVMtNBlplQq0ixeoTSh0A==", + "version": "3.1.12", + "resolved": "https://registry.npmjs.org/@antora/ui-loader/-/ui-loader-3.1.12.tgz", + "integrity": "sha512-KSHoSPt7yOvTrqNpxFwelTa5HN10yGDmBcjmrbZ8dDM3fQyor8O3oTBEdqCYq+uGT5VdMHHbJ/IbFakKQrdnww==", "license": "MPL-2.0", "dependencies": { "@antora/expand-path-helper": "~3.0", @@ -956,6 +310,23 @@ "integrity": "sha512-trnsAYxU3xnS1gPHPyU961coFyLkh4gAD/0zQ5mymY4yOZ+CYvsPqUbOFSw0aDM4y0tV7tiFxL/1XfXPNC6IPg==", "license": "ISC" }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -991,6 +362,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -1003,11 +384,22 @@ "node": ">=6.5" } }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -1019,17 +411,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/append-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/append-buffer/-/append-buffer-1.0.2.tgz", - "integrity": "sha512-WLbYiXzD3y/ATLZFufV/rZvWdZOs+Z/+5v1rBZ463Jn398pa6kcde27cvozYnBoxXblGZTFfoPpsaEw0orU5BA==", - "dependencies": { - "buffer-equal": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1071,6 +452,21 @@ "node": ">=8.0.0" } }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -1084,9 +480,9 @@ "license": "MIT" }, "node_modules/bare-events": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz", - "integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.6.0.tgz", + "integrity": "sha512-EKZ5BTXYExaNqi3I3f9RtEsaI/xBSGjE0XZCZilPzFAV/goswFHuPd9jEZlPIZ/iNZJwDSao9qRiScySz7MbQg==", "license": "Apache-2.0", "optional": true }, @@ -1124,13 +520,12 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -1178,17 +573,6 @@ "node": "*" } }, - "node_modules/buffer-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.1.tgz", - "integrity": "sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg==", - "engines": { - "node": ">=0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/cache-directory": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/cache-directory/-/cache-directory-2.0.0.tgz", @@ -1205,6 +589,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", @@ -1279,57 +664,10 @@ "node": ">=0.8" } }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz", - "integrity": "sha512-KLLTJWrvwIP+OPfMn0x2PheDEP20RPUcGXj/ERegTgdmPEZylALQldygiqrPPu8P45uNuPs7ckmReLY6v/iA5g==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/clone-stats": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/clone-stats/-/clone-stats-1.0.0.tgz", - "integrity": "sha512-au6ydSpg6nsrigcZ4m8Bc9hxjeW+GJ8xh5G3BJCMt4WXe1H10UNaVOamqQTmrx1kjVuxAHIQSNU6hY4Nsn9/ag==", - "license": "MIT" - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", - "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/cloneable-readable/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/cloneable-readable/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -1342,7 +680,6 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, "node_modules/colorette": { @@ -1352,24 +689,14 @@ "license": "MIT" }, "node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=16" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==" - }, "node_modules/convict": { "version": "6.2.4", "resolved": "https://registry.npmjs.org/convict/-/convict-6.2.4.tgz", @@ -1383,11 +710,6 @@ "node": ">=6" } }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, "node_modules/corser": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", @@ -1410,6 +732,20 @@ "node": ">=0.8" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/dateformat": { "version": "4.6.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", @@ -1420,9 +756,9 @@ } }, "node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1456,6 +792,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -1468,22 +805,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/diff3": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", @@ -1504,21 +825,22 @@ "node": ">= 0.4" } }, - "node_modules/duplexify": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", - "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", - "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.2" - } + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" }, "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", "license": "MIT", "dependencies": { "once": "^1.4.0" @@ -1579,11 +901,6 @@ "node": ">=0.8.x" } }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, "node_modules/fast-copy": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", @@ -1636,14 +953,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -1656,41 +965,10 @@ "node": ">=8" } }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/flush-write-stream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/flush-write-stream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "dev": true, "funding": [ { @@ -1708,23 +986,36 @@ } } }, - "node_modules/fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha512-+vSd9frUnapVC2RZYfL3FCB2p3g4TBhaUmrsWlSudsGdnxIuUvBB2QM1VZeBtc49QFwrp+wQLrDs3+xxDgI5gQ==", + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", "dependencies": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" + "is-callable": "^1.2.7" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } }, "node_modules/function-bind": { "version": "1.1.2", @@ -1773,21 +1064,23 @@ } }, "node_modules/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "deprecated": "Glob versions prior to v9 are no longer supported", + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" }, - "engines": { - "node": "*" + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/glob-parent": { @@ -1802,123 +1095,16 @@ "node": ">= 6" } }, - "node_modules/glob-stream": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-7.0.0.tgz", - "integrity": "sha512-evR4kvr6s0Yo5t4CD4H171n4T8XcnPFznvsbeN8K9FPzc0Q0wYqcOWyGtck2qcvJSLXKnU6DnDyfmbDDabYvRQ==", - "dependencies": { - "extend": "^3.0.2", - "glob": "^7.2.0", - "glob-parent": "^6.0.2", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.1", - "pumpify": "^2.0.1", - "readable-stream": "^3.6.0", - "remove-trailing-separator": "^1.1.0", - "to-absolute-glob": "^2.0.2", - "unique-stream": "^2.3.1" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/glob-stream/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-stream/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" - }, - "node_modules/gulp-vinyl-zip": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.5.0.tgz", - "integrity": "sha512-KPi5/2SUmkXXDvKU4L2U1dkPOP03SbhONTOgNZlL23l9Yopt+euJ1bBXwWrSMbsyh3JLW/TYuC8CI4c4Kq4qrw==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "queue": "^4.2.1", - "through": "^2.3.8", - "through2": "^2.0.3", - "vinyl": "^2.0.2", - "vinyl-fs": "^3.0.3", - "yauzl": "^2.2.1", - "yazl": "^2.2.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/gulp-vinyl-zip/node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", - "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/handlebars": { @@ -1956,6 +1142,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", "dependencies": { "es-define-property": "^1.0.0" }, @@ -1975,6 +1162,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -2110,40 +1312,24 @@ "node": ">= 4" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, - "node_modules/is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dependencies": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2153,6 +1339,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2165,14 +1360,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-negated-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", - "integrity": "sha512-czXVVn/QEmgvej1f50BZ648vUI+em0xqMq2Sn+QncCLN4zj1UAxlT+kw/6ggQTOaZPd1HqKQGEqbpQVtJucWug==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -2182,53 +1369,32 @@ "node": ">=0.12.0" } }, - "node_modules/is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", "dependencies": { - "is-unc-path": "^1.0.0" + "which-typed-array": "^1.1.16" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dependencies": { - "unc-path-regex": "^0.1.2" + "node": ">= 0.4" }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" - }, - "node_modules/is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "engines": { - "node": ">=0.10.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" }, "node_modules/isomorphic-git": { "version": "1.25.10", @@ -2255,6 +1421,21 @@ "node": ">=12" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/joycon": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", @@ -2276,11 +1457,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2293,56 +1469,18 @@ "node": ">=6" } }, - "node_modules/lazystream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", - "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", - "dependencies": { - "readable-stream": "^2.0.5" - }, - "engines": { - "node": ">= 0.6.3" - } - }, - "node_modules/lazystream/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/lazystream/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha512-IpSVCk9AYvLHo5ctcIXxOBpMWUe+4TKN3VPWAKUbJikkmsGp0VrSM8IttVc32D6J4WUsiPE6aEFRNmIoF/gdow==", - "dependencies": { - "flush-write-stream": "^1.0.2" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "license": "MIT" }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2433,15 +1571,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "license": "ISC", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -2462,6 +1603,15 @@ "minimist": "^1.2.5" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2484,28 +1634,6 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "license": "MIT" }, - "node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/now-and-later": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", - "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", - "dependencies": { - "once": "^1.3.2" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -2519,33 +1647,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", @@ -2574,35 +1675,11 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/ordered-read-streams": { + "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha512-Z87aSjx3r5c0ZB7bcJqIgIRX5bxR7A4aSzvIbaxd0oTkWBCOoKfuGHiKj60CHVUgg1Phm5yMZzBdt8XqRs73Mw==", - "dependencies": { - "readable-stream": "^2.0.1" - } - }, - "node_modules/ordered-read-streams/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/ordered-read-streams/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" }, "node_modules/pako": { "version": "1.0.11", @@ -2610,18 +1687,29 @@ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==" - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/pend": { @@ -2631,9 +1719,9 @@ "license": "MIT" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -2747,9 +1835,9 @@ "license": "MIT" }, "node_modules/portfinder": { - "version": "1.0.35", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.35.tgz", - "integrity": "sha512-73JaFg4NwYNAufDtS5FsFu/PdM49ahJrO1i44aCRsDWju1z5wuGDaqyFUQWR6aJoK2JPDWlaYYAGFNIGTSUHSw==", + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", "dev": true, "license": "MIT", "dependencies": { @@ -2760,6 +1848,15 @@ "node": ">= 10.12" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2769,11 +1866,6 @@ "node": ">= 0.6.0" } }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, "node_modules/process-warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-3.0.0.tgz", @@ -2790,25 +1882,15 @@ } }, "node_modules/pump": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", - "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", "license": "MIT", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, - "node_modules/pumpify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz", - "integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==", - "dependencies": { - "duplexify": "^4.1.1", - "inherits": "^2.0.3", - "pump": "^3.0.0" - } - }, "node_modules/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", @@ -2825,14 +1907,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/queue": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.1.tgz", - "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", - "dependencies": { - "inherits": "~2.0.0" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -2882,31 +1956,6 @@ "node": ">= 12.13.0" } }, - "node_modules/remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dependencies": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha512-wigO8/O08XHb8YPzpDDT+QmRANfW6vLqxfaXm1YXhnFf3AkSLyjfG3GEFg4McZkmgL7KvCj5u2KczkvSP6NfHA==", - "dependencies": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -2938,17 +1987,6 @@ "dev": true, "license": "MIT" }, - "node_modules/resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha512-NYDgziiroVeDC29xq7bp/CacZERYsA9bXYd1ZmcJlF3BcrZv5pTb4NG7SjdyKDnXZ84aC4vo2u6sNKIA1LCu/A==", - "dependencies": { - "value-or-function": "^3.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -2986,6 +2024,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, "license": "MIT" }, "node_modules/safe-stable-stringify": { @@ -3021,29 +2060,78 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "license": "(MIT AND BSD-3-Clause)", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "bin": { + "sha.js": "bin.js" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sha.js/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" + "shebang-regex": "^3.0.0" }, "engines": { - "node": ">= 0.4" + "node": ">=8" } }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" } }, "node_modules/should-proxy": { @@ -3128,6 +2216,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/simple-concat": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", @@ -3200,15 +2300,10 @@ "node": ">= 10.x" } }, - "node_modules/stream-shift": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", - "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==" - }, "node_modules/streamx": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz", - "integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==", + "version": "2.22.1", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.1.tgz", + "integrity": "sha512-znKXEBxfatz2GBNK02kRnCXjV+AA4kjZIUxeWSr3UGirZMJfTE9uiwKHobnbgxWyL/JWro8tTq+vOqAK1/qbSA==", "license": "MIT", "dependencies": { "fast-fifo": "^1.3.2", @@ -3247,6 +2342,102 @@ ], "license": "MIT" }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3299,63 +2490,40 @@ "real-require": "^0.2.0" } }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/through2-filter": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", - "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", - "dependencies": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "node_modules/through2/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/through2/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha512-rtwLUQEwT8ZeKQbyFJyomBRYXyE16U5VKuy0ftxLMK/PZb2fkOsg5r9kHdauuVDbsNdIBoC/HCthpidamQFXYA==", + "node_modules/to-buffer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz", + "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==", + "license": "MIT", "dependencies": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" } }, + "node_modules/to-buffer/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3368,15 +2536,18 @@ "node": ">=8.0" } }, - "node_modules/to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha512-+QIz37Ly7acM4EMdw2PRN389OneM5+d844tirkGp4dPKzI5OE72V9OsbFp+CIYJDahZ41ZV05hNtcPAQUAm9/Q==", + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", "dependencies": { - "through2": "^2.0.3" + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" } }, "node_modules/uglify-js": { @@ -3392,14 +2563,6 @@ "node": ">=0.8.0" } }, - "node_modules/unc-path-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", - "integrity": "sha512-eXL4nmJT7oCpkZsHZUOJo8hcX3GbsiDOa0Qu9F646fi8dT3XuSVopVqAcEiVzSKKH7UoDti23wNX3qGFxcW5Qg==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", @@ -3412,15 +2575,6 @@ "node": ">= 0.8.0" } }, - "node_modules/unique-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", - "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", - "dependencies": { - "json-stable-stringify-without-jsonify": "^1.0.1", - "through2-filter": "^3.0.0" - } - }, "node_modules/unxhr": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unxhr/-/unxhr-1.0.1.tgz", @@ -3443,22 +2597,13 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/value-or-function": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-3.0.0.tgz", - "integrity": "sha512-jdBB2FrWvQC/pnPtIqcLsMaQgjhdb6B7tk1MMyTKapox+tQZbdRP4uLxu/JY0t7fbfDCUMnuelzEYv5GsxHhdg==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vinyl": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", - "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.1.tgz", + "integrity": "sha512-0QwqXteBNXgnLCdWdvPQBX6FXRHtIH3VhJPTd5Lwn28tJXc34YqSCWUmkOvtJHBmB3gGoPtrOKk3Ts8/kEZ9aA==", "license": "MIT", "dependencies": { "clone": "^2.1.2", - "clone-stats": "^1.0.0", "remove-trailing-separator": "^1.1.0", "replace-ext": "^2.0.0", "teex": "^1.0.1" @@ -3467,208 +2612,148 @@ "node": ">=10.13.0" } }, - "node_modules/vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dependencies": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-fs/node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/vinyl-fs/node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/vinyl-fs/node_modules/glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", "dependencies": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">= 0.10" + "node": ">=12" } }, - "node_modules/vinyl-fs/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", "dependencies": { - "is-extglob": "^2.1.0" + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vinyl-fs/node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/vinyl-fs/node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" + "node": ">= 8" } }, - "node_modules/vinyl-fs/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "license": "MIT", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/vinyl-fs/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, "engines": { - "node": ">= 0.10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/vinyl-fs/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dependencies": { - "safe-buffer": "~5.1.0" - } + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "license": "MIT" }, - "node_modules/vinyl-fs/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/vinyl-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha512-NiibMgt6VJGJmyw7vtzhctDcfKch4e4n9TBeoWlirb7FMg9/1Ov9k+A5ZRAtywBpRPiyECvQRQllYM8dECegVA==", + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", "dependencies": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/vinyl-sourcemap/node_modules/replace-ext": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.1.tgz", - "integrity": "sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==", + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", "engines": { - "node": ">= 0.10" + "node": ">=8" } }, - "node_modules/vinyl-sourcemap/node_modules/vinyl": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", - "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">= 0.10" + "node": ">=8" } }, - "node_modules/whatwg-encoding": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", - "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", - "dev": true, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "license": "MIT", "dependencies": { - "iconv-lite": "0.6.3" + "ansi-regex": "^5.0.1" }, "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "license": "MIT" + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, "node_modules/wrappy": { "version": "1.0.2", @@ -3685,14 +2770,6 @@ "node": ">=4" } }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - }, "node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", diff --git a/docs/package.json b/docs/package.json index a055e380..c69bf1ec 100644 --- a/docs/package.json +++ b/docs/package.json @@ -3,9 +3,9 @@ "version": "1.0.0", "description": "Documentation for Redis OM Spring", "dependencies": { - "@antora/cli": "^3.1.4", - "@antora/site-generator": "^3.1.10", - "@antora/site-generator-default": "^3.1.4" + "@antora/cli": "^3.1.12", + "@antora/site-generator": "3.1.12", + "@antora/site-generator-default": "^3.1.12" }, "scripts": { "build": "antora --stacktrace antora-playbook.yml", @@ -13,5 +13,10 @@ }, "devDependencies": { "http-server": "^14.1.1" + }, + "overrides": { + "glob": "^10.3.10", + "inflight": "npm:@isaacs/inflight@^1.0.1", + "gulp-vinyl-zip": "npm:vinyl-zip@^2.5.0" } } diff --git a/gradle.properties b/gradle.properties index b40f397f..2cfdf639 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,5 +18,5 @@ elementaryVersion = 2.0.1 gsonVersion = 2.10.1 djlStarterVersion = 0.26 djlVersion = 0.30.0 -springAiVersion = 1.0.0 +springAiVersion = 1.0.1 azureIdentityVersion = 1.15.4 diff --git a/jreleaser.yml b/jreleaser.yml index 176c6214..81729a83 100644 --- a/jreleaser.yml +++ b/jreleaser.yml @@ -23,11 +23,39 @@ release: name: redis-om-spring overwrite: true sign: true + prerelease: + enabled: auto + pattern: .*-SNAPSHOT.* + draft: false changelog: formatted: ALWAYS preset: conventional-commits + includeLabels: + - 'feat' + - 'feature' + - 'fix' + - 'docs' + - 'chore' + - 'perf' + - 'refactor' + - 'test' + - 'build' + - 'ci' contributors: - enabled: false + enabled: true + format: '- {{contributorName}} ({{contributorUsernameAsLink}})' + extraProperties: + categorizeScopes: false + replacers: + # Escape Java annotations to prevent GitHub from treating them as user mentions + - search: '\s@([A-Z][a-zA-Z]+)' + replace: ' `@$1`' + hide: + contributors: + - 'GitHub Actions' + - 'actions' + - 'github-actions' + - 'github-actions[bot]' signing: active: ALWAYS diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/RedisEnhancedKeyValueAdapter.java b/redis-om-spring/src/main/java/com/redis/om/spring/RedisEnhancedKeyValueAdapter.java index ab994342..8f78d8a5 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/RedisEnhancedKeyValueAdapter.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/RedisEnhancedKeyValueAdapter.java @@ -392,7 +392,21 @@ public long count(String keyspace) { String indexName = indexer.getIndexName(keyspace); SearchOperations search = modulesOperations.opsForSearch(indexName); var info = search.getInfo(); - return (long) info.get("num_docs"); + return extractNumDocs(info); + } + + private long extractNumDocs(Map info) { + Object numDocsValue = info.get("num_docs"); + + // Handle different return types from Redis + if (numDocsValue instanceof String) { + return Long.parseLong((String) numDocsValue); + } else if (numDocsValue instanceof Number) { + return ((Number) numDocsValue).longValue(); + } else { + // Fallback to 0 if the value is null or unexpected type + return 0L; + } } /* diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/RedisJSONKeyValueAdapter.java b/redis-om-spring/src/main/java/com/redis/om/spring/RedisJSONKeyValueAdapter.java index eaa40506..dc5197e1 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/RedisJSONKeyValueAdapter.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/RedisJSONKeyValueAdapter.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; @@ -346,7 +347,21 @@ public long count(String keyspace) { String indexName = indexer.getIndexName(keyspace); SearchOperations search = modulesOperations.opsForSearch(indexName); var info = search.getInfo(); - return (long) info.get("num_docs"); + return extractNumDocs(info); + } + + private long extractNumDocs(Map info) { + Object numDocsValue = info.get("num_docs"); + + // Handle different return types from Redis + if (numDocsValue instanceof String) { + return Long.parseLong((String) numDocsValue); + } else if (numDocsValue instanceof Number) { + return ((Number) numDocsValue).longValue(); + } else { + // Fallback to 0 if the value is null or unexpected type + return 0L; + } } /* @@ -439,9 +454,21 @@ private Optional getTTLForEntity(Object entity) { entityClassKey = entity.getClass(); } - KeyspaceConfiguration keyspaceConfig = mappingContext.getMappingConfiguration().getKeyspaceConfiguration(); - if (keyspaceConfig.hasSettingsFor(entityClassKey)) { - var settings = keyspaceConfig.getKeyspaceSettings(entityClassKey); + // Use the resolver if available for cross-class-loader compatibility + KeyspaceConfiguration.KeyspaceSettings settings = null; + if (mappingContext instanceof com.redis.om.spring.mapping.RedisEnhancedMappingContext) { + var resolver = ((com.redis.om.spring.mapping.RedisEnhancedMappingContext) mappingContext).getKeyspaceResolver(); + if (resolver.hasSettingsFor(entityClassKey)) { + settings = resolver.getKeyspaceSettings(entityClassKey); + } + } else { + KeyspaceConfiguration keyspaceConfig = mappingContext.getMappingConfiguration().getKeyspaceConfiguration(); + if (keyspaceConfig.hasSettingsFor(entityClassKey)) { + settings = keyspaceConfig.getKeyspaceSettings(entityClassKey); + } + } + + if (settings != null) { if (StringUtils.hasText(settings.getTimeToLivePropertyName())) { Method ttlGetter; try { diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java b/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java index f4391374..f866b479 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/RedisModulesConfiguration.java @@ -111,10 +111,14 @@ public RedisModulesConfiguration() { private static final Log logger = LogFactory.getLog(RedisModulesConfiguration.class); /** - * Creates the primary Redis mapping context for enhanced entity mapping. + * Creates the default Redis mapping context for enhanced entity mapping. *

* This mapping context provides metadata about Redis-mapped entities including * field information, type conversions, and persistence properties. + *

+ * Users can override this by providing their own bean named "redisEnhancedMappingContext" + * with @Primary annotation. The @ConditionalOnMissingBean ensures this default + * bean is only created if users haven't provided their own. * * @return the enhanced mapping context instance */ @@ -122,6 +126,9 @@ public RedisModulesConfiguration() { name = "redisEnhancedMappingContext" ) @Primary + @ConditionalOnMissingBean( + name = "redisEnhancedMappingContext" + ) public RedisEnhancedMappingContext redisMappingContext() { return new RedisEnhancedMappingContext(); } @@ -156,6 +163,9 @@ public GsonBuilder gsonBuilder(List customizers) { builder.addSerializationExclusionStrategy(GsonReferencesSerializationExclusionStrategy.INSTANCE); + // Register factory for handling Boolean values in Maps (must be after type adapters) + builder.registerTypeAdapterFactory(MapBooleanTypeAdapterFactory.getInstance()); + return builder; } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/RedisOMProperties.java b/redis-om-spring/src/main/java/com/redis/om/spring/RedisOMProperties.java index ae9cc2c7..ee274d87 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/RedisOMProperties.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/RedisOMProperties.java @@ -243,6 +243,13 @@ public static class Repository { */ private int deleteBatchSize = 500; + /** + * Whether to throw exceptions when saveAll operations fail. + * When false (default), failures are logged as warnings. + * When true, failures throw an exception. + */ + private boolean throwOnSaveAllFailure = false; + /** * Default constructor for Repository configuration. */ @@ -296,6 +303,25 @@ public void setDeleteBatchSize(int deleteBatchSize) { this.deleteBatchSize = deleteBatchSize; } + /** + * Checks if exceptions should be thrown when saveAll operations fail. + * + * @return {@code true} if exceptions should be thrown on failures, {@code false} otherwise + */ + public boolean isThrowOnSaveAllFailure() { + return throwOnSaveAllFailure; + } + + /** + * Sets whether exceptions should be thrown when saveAll operations fail. + * + * @param throwOnSaveAllFailure {@code true} to throw exceptions on failures, + * {@code false} to log warnings only + */ + public void setThrowOnSaveAllFailure(boolean throwOnSaveAllFailure) { + this.throwOnSaveAllFailure = throwOnSaveAllFailure; + } + /** * Configuration properties for query behavior. *

diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/indexing/LexicographicIndexer.java b/redis-om-spring/src/main/java/com/redis/om/spring/indexing/LexicographicIndexer.java index 491b42ca..a7c4175e 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/indexing/LexicographicIndexer.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/indexing/LexicographicIndexer.java @@ -23,6 +23,12 @@ public class LexicographicIndexer { private final RedisTemplate redisTemplate; private final RediSearchIndexer indexer; + /** + * Creates a new LexicographicIndexer with the specified dependencies. + * + * @param redisTemplate the Redis template for executing Redis operations + * @param indexer the RediSearch indexer for accessing field metadata + */ public LexicographicIndexer(RedisTemplate redisTemplate, RediSearchIndexer indexer) { this.redisTemplate = redisTemplate; this.indexer = indexer; diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/indexing/RediSearchIndexer.java b/redis-om-spring/src/main/java/com/redis/om/spring/indexing/RediSearchIndexer.java index ef9a1fc9..d01be037 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/indexing/RediSearchIndexer.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/indexing/RediSearchIndexer.java @@ -411,22 +411,33 @@ public boolean indexDefinitionExistsFor(Class entityClass) { * * @param entityClass the entity class to check for index existence in Redis * @return true if the search index exists in Redis, false otherwise - * @throws JedisDataException if a Redis error occurs other than "Unknown index name" + * @throws JedisDataException if a Redis error occurs other than index not found errors */ public boolean indexExistsFor(Class entityClass) { try { return getIndexInfo(entityClass) != null; } catch (JedisDataException jde) { - if (jde.getMessage().contains("Unknown index name")) { - return false; - } else { - throw jde; + String errorMessage = jde.getMessage(); + if (errorMessage != null) { + String lowerCaseMessage = errorMessage.toLowerCase(); + // Handle various error messages for missing index across different Redis versions + // - "Unknown index name" or "Unknown Index name" - Redis Stack / Redis 7.x + // - Potentially other variations in Redis 8.0+ + if (lowerCaseMessage.contains("unknown index") || lowerCaseMessage.contains("no such index") || lowerCaseMessage + .contains("index does not exist") || lowerCaseMessage.contains("not found")) { + return false; + } } + throw jde; } } Map getIndexInfo(Class entityClass) { String indexName = entityClassToIndexName.get(entityClass); + if (indexName == null) { + // No index mapping exists for this entity class + return null; + } SearchOperations opsForSearch = rmo.opsForSearch(indexName); return opsForSearch.getInfo(); } @@ -543,6 +554,59 @@ else if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fi } } // + // Map fields (only for JSON documents) + // + else if (Map.class.isAssignableFrom(fieldType) && isDocument) { + logger.info(String.format("Processing Map field: %s of type %s", field.getName(), fieldType)); + Optional> maybeValueType = getMapValueClass(field); + if (maybeValueType.isPresent()) { + Class valueType = maybeValueType.get(); + logger.info(String.format("Map field %s has value type: %s", field.getName(), valueType)); + String mapJsonPath = (prefix == null || prefix.isBlank()) ? + "$." + field.getName() + ".*" : + "$." + prefix + "." + field.getName() + ".*"; + String mapFieldAlias = field.getName() + "_values"; + + // Support all value types that we support for regular fields + if (CharSequence.class.isAssignableFrom( + valueType) || valueType == UUID.class || valueType == Ulid.class || valueType.isEnum()) { + // Index as TAG field + TagField tagField = TagField.of(FieldName.of(mapJsonPath).as(mapFieldAlias)); + if (indexed.sortable()) + tagField.sortable(); + if (indexed.indexMissing()) + tagField.indexMissing(); + if (indexed.indexEmpty()) + tagField.indexEmpty(); + if (!indexed.separator().isEmpty()) { + tagField.separator(indexed.separator().charAt(0)); + } + fields.add(SearchField.of(field, tagField)); + logger.info(String.format("Added TAG field for Map: %s as %s", field.getName(), mapFieldAlias)); + } else if (Number.class.isAssignableFrom( + valueType) || valueType == Boolean.class || valueType == LocalDateTime.class || valueType == LocalDate.class || valueType == Date.class || valueType == Instant.class || valueType == OffsetDateTime.class) { + // Index as NUMERIC field + NumericField numField = NumericField.of(FieldName.of(mapJsonPath).as(mapFieldAlias)); + if (indexed.sortable()) + numField.sortable(); + if (indexed.noindex()) + numField.noIndex(); + if (indexed.indexMissing()) + numField.indexMissing(); + // NumericField doesn't have indexEmpty() method + fields.add(SearchField.of(field, numField)); + logger.info(String.format("Added NUMERIC field for Map: %s as %s", field.getName(), mapFieldAlias)); + } else if (valueType == Point.class) { + // Index as GEO field + GeoField geoField = GeoField.of(FieldName.of(mapJsonPath).as(mapFieldAlias)); + fields.add(SearchField.of(field, geoField)); + logger.info(String.format("Added GEO field for Map: %s as %s", field.getName(), mapFieldAlias)); + } + // For complex object values, we could recursively index their fields + // but that would require more complex implementation + } + } + // // Point // else if (fieldType == Point.class) { @@ -1176,7 +1240,13 @@ private void updateTTLSettings(Class cl, String entityPrefix, boolean isDocum allClassFields.stream().filter(field -> field.isAnnotationPresent(TimeToLive.class)).findFirst().ifPresent( field -> setting.setTimeToLivePropertyName(field.getName())); - mappingContext.getMappingConfiguration().getKeyspaceConfiguration().addKeyspaceSettings(setting); + // Use the resolver if the mapping context is enhanced + if (mappingContext instanceof com.redis.om.spring.mapping.RedisEnhancedMappingContext) { + ((com.redis.om.spring.mapping.RedisEnhancedMappingContext) mappingContext).getKeyspaceResolver() + .addKeyspaceSettings(cl, setting); + } else { + mappingContext.getMappingConfiguration().getKeyspaceConfiguration().addKeyspaceSettings(setting); + } } } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolver.java b/redis-om-spring/src/main/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolver.java new file mode 100644 index 00000000..6dc3cb29 --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolver.java @@ -0,0 +1,98 @@ +package com.redis.om.spring.mapping; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; + +/** + * A resolver that handles KeyspaceSettings lookups across different class loaders. + *

+ * This class addresses the issue where spring-boot-devtools uses a different class loader + * (RestartClassLoader) which causes entity classes to not be recognized by the + * KeyspaceConfiguration when comparing Class instances directly. + *

+ * By storing and retrieving settings based on fully qualified class names instead of + * Class instances, this resolver ensures that TTL and other keyspace configurations + * work correctly regardless of the class loader being used. + * + * @since 1.0.0 + */ +public class ClassLoaderAwareKeyspaceResolver { + + private final KeyspaceConfiguration keyspaceConfiguration; + private final Map settingsByClassName = new ConcurrentHashMap<>(); + + /** + * Creates a new resolver wrapping the given KeyspaceConfiguration. + * + * @param keyspaceConfiguration the keyspace configuration to wrap + */ + public ClassLoaderAwareKeyspaceResolver(KeyspaceConfiguration keyspaceConfiguration) { + this.keyspaceConfiguration = keyspaceConfiguration; + } + + /** + * Registers keyspace settings for a class. + *

+ * This method stores settings both by the Class instance (for normal operation) + * and by the fully qualified class name (for cross-class-loader compatibility). + * + * @param entityClass the entity class + * @param settings the keyspace settings for the class + */ + public void addKeyspaceSettings(Class entityClass, KeyspaceSettings settings) { + // Store by class name for cross-class-loader lookup + settingsByClassName.put(entityClass.getName(), settings); + // Also add to the original configuration for backward compatibility + keyspaceConfiguration.addKeyspaceSettings(settings); + } + + /** + * Checks if settings exist for the given class. + *

+ * This method first checks the original KeyspaceConfiguration, then falls back + * to checking by class name if not found. This handles the case where the class + * was loaded by a different class loader. + * + * @param entityClass the entity class to check + * @return true if settings exist for the class, false otherwise + */ + public boolean hasSettingsFor(Class entityClass) { + // First try the normal lookup + if (keyspaceConfiguration.hasSettingsFor(entityClass)) { + return true; + } + // Fall back to class name lookup for cross-class-loader compatibility + return settingsByClassName.containsKey(entityClass.getName()); + } + + /** + * Gets the keyspace settings for the given class. + *

+ * This method first attempts to get settings from the original KeyspaceConfiguration, + * then falls back to retrieving by class name if not found. This handles the case + * where the class was loaded by a different class loader. + * + * @param entityClass the entity class + * @return the keyspace settings, or null if not found + */ + public KeyspaceSettings getKeyspaceSettings(Class entityClass) { + // First try the normal lookup + if (keyspaceConfiguration.hasSettingsFor(entityClass)) { + return keyspaceConfiguration.getKeyspaceSettings(entityClass); + } + // Fall back to class name lookup for cross-class-loader compatibility + return settingsByClassName.get(entityClass.getName()); + } + + /** + * Gets the underlying KeyspaceConfiguration. + * + * @return the wrapped keyspace configuration + */ + public KeyspaceConfiguration getKeyspaceConfiguration() { + return keyspaceConfiguration; + } +} \ No newline at end of file diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/mapping/RedisEnhancedMappingContext.java b/redis-om-spring/src/main/java/com/redis/om/spring/mapping/RedisEnhancedMappingContext.java index cb24d6bb..e75342e8 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/mapping/RedisEnhancedMappingContext.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/mapping/RedisEnhancedMappingContext.java @@ -38,6 +38,7 @@ public class RedisEnhancedMappingContext extends RedisMappingContext { private static final Log logger = LogFactory.getLog(RedisEnhancedMappingContext.class); private final MappingConfiguration mappingConfiguration; private final TimeToLiveAccessor timeToLiveAccessor; + private final ClassLoaderAwareKeyspaceResolver keyspaceResolver; /** * Creates a new {@code RedisEnhancedMappingContext} with the specified mapping configuration. @@ -51,6 +52,7 @@ public class RedisEnhancedMappingContext extends RedisMappingContext { public RedisEnhancedMappingContext(MappingConfiguration mappingConfiguration) { super(mappingConfiguration); this.mappingConfiguration = mappingConfiguration; + this.keyspaceResolver = new ClassLoaderAwareKeyspaceResolver(mappingConfiguration.getKeyspaceConfiguration()); this.timeToLiveAccessor = new RedisEnhancedTimeToLiveAccessor(mappingConfiguration.getKeyspaceConfiguration(), this); } @@ -71,4 +73,13 @@ public RedisEnhancedMappingContext() { protected RedisPersistentEntity createPersistentEntity(TypeInformation typeInformation) { return new RedisEnhancedPersistentEntity<>(typeInformation, getKeySpaceResolver(), timeToLiveAccessor); } + + /** + * Gets the class loader aware keyspace resolver. + * + * @return the keyspace resolver that handles cross-class-loader lookups + */ + public ClassLoaderAwareKeyspaceResolver getKeyspaceResolver() { + return keyspaceResolver; + } } \ No newline at end of file diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/MetamodelGenerator.java b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/MetamodelGenerator.java index c1fa1862..7d77e953 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/MetamodelGenerator.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/MetamodelGenerator.java @@ -441,6 +441,72 @@ else if (Set.class.isAssignableFrom(targetCls) || List.class.isAssignableFrom(ta } } // + // Map fields + // + else if (Map.class.isAssignableFrom(targetCls)) { + // For Map fields, generate both the map field and special VALUES field for queries + String mapValueTypeName = ObjectUtils.getMapValueClassName(fullTypeClassName); + + try { + // Handle inner classes by replacing dot with $ for the last component + String classNameForLoader = mapValueTypeName; + if (mapValueTypeName.contains(".") && !mapValueTypeName.startsWith("java.")) { + // Try to detect inner class pattern: package.OuterClass.InnerClass + int lastDotIndex = mapValueTypeName.lastIndexOf('.'); + if (lastDotIndex > 0) { + String beforeLastDot = mapValueTypeName.substring(0, lastDotIndex); + String afterLastDot = mapValueTypeName.substring(lastDotIndex + 1); + int secondLastDotIndex = beforeLastDot.lastIndexOf('.'); + if (secondLastDotIndex > 0) { + String potentialOuterClass = beforeLastDot.substring(secondLastDotIndex + 1); + // If the part before the last dot looks like a class name (starts with uppercase) + // then this might be an inner class + if (Character.isUpperCase(potentialOuterClass.charAt(0)) && Character.isUpperCase(afterLastDot.charAt( + 0))) { + // Convert Outer.Inner to Outer$Inner for class loader + classNameForLoader = beforeLastDot + "$" + afterLastDot; + } + } + } + } + Class valueClass = ClassUtils.forName(classNameForLoader, MetamodelGenerator.class.getClassLoader()); + + // Determine the field type based on the value type + Class valuesInterceptor = null; + if (CharSequence.class.isAssignableFrom( + valueClass) || valueClass == Boolean.class || valueClass == UUID.class || valueClass == Ulid.class || valueClass + .isEnum()) { + targetInterceptor = TextTagField.class; + valuesInterceptor = TextTagField.class; + } else if (Number.class.isAssignableFrom( + valueClass) || valueClass == LocalDateTime.class || valueClass == LocalDate.class || valueClass == Date.class || valueClass == Instant.class || valueClass == OffsetDateTime.class) { + targetInterceptor = NumericField.class; + valuesInterceptor = NumericField.class; + } else if (valueClass == Point.class) { + targetInterceptor = GeoField.class; + valuesInterceptor = GeoField.class; + } + + // Generate the special VALUES field for querying map values + if (valuesInterceptor != null) { + String mapFieldName = field.getSimpleName().toString(); + String mapValuesFieldName = mapFieldName.toUpperCase().replace("_", "") + "_VALUES"; + + // Add the VALUES field as a special metamodel field + Triple valuesField = generateMapValuesFieldMetamodel(entity, + chain, chainedFieldName, mapValuesFieldName, valuesInterceptor, mapValueTypeName); + fieldMetamodelSpec.add(valuesField); + } + + // We still want the regular Map field for direct access + // targetInterceptor remains set for the Map field itself + } catch (ClassNotFoundException cnfe) { + messager.printMessage(Diagnostic.Kind.WARNING, + "Processing class " + entityName + " could not resolve map value type " + mapValueTypeName); + targetInterceptor = null; // Don't generate field if we can't resolve the type + } + } + // // Point // else if (targetCls == Point.class) { @@ -508,6 +574,41 @@ else if (targetCls == Point.class) { return fieldMetamodelSpec; } + private Triple generateMapValuesFieldMetamodel( // + TypeName parentEntity, // + List chain, // + String chainedFieldName, // + String mapValuesFieldName, // + Class targetInterceptor, // + String valueTypeName // + ) { + Element field = chain.get(chain.size() - 1); + String fieldName = field.getSimpleName().toString(); + + // Create field spec for map values - the type parameter should be the value type, not the Map + TypeName interceptorType = ParameterizedTypeName.get(ClassName.get(targetInterceptor), parentEntity, ClassName + .bestGuess(valueTypeName)); + + FieldSpec valuesFieldSpec = FieldSpec.builder(interceptorType, mapValuesFieldName).addModifiers(Modifier.PUBLIC, + Modifier.STATIC).build(); + + // Create initialization code + // The search path matches what we index in RediSearchIndexer: $.fieldName.* + // The accessor name matches what RediSearchQuery expects: fieldName_values + String searchPath = "$." + fieldName + ".*"; + String accessorName = fieldName + "_values"; + + CodeBlock initCode = CodeBlock.builder().addStatement( + "$L = new $T<$T, $T>(new SearchFieldAccessor($S, $S, $T.class, $T.class), true)", mapValuesFieldName, + targetInterceptor, parentEntity, ClassName.bestGuess(valueTypeName), accessorName, searchPath, ClassName + .bestGuess(valueTypeName), parentEntity).build(); + + // Don't create an ObjectGraphFieldSpec for synthetic VALUES fields + // We only need the FieldSpec in the interceptors list + // Return null for the ObjectGraphFieldSpec to avoid duplication + return Tuples.of((ObjectGraphFieldSpec) null, valuesFieldSpec, initCode); + } + private Triple generateCollectionFieldMetamodel( // TypeName parentEntity, // List chain, // @@ -572,12 +673,15 @@ private void extractFieldMetamodels(TypeName entity, List interceptor List initCodeBlocks, List> fieldMetamodels) { for (Triple fieldMetamodel : fieldMetamodels) { FieldSpec fieldSpec = fieldMetamodel.getSecond(); - fields.add(fieldMetamodel.getFirst()); + // Only add to fields if not null (synthetic fields like Map VALUES don't have ObjectGraphFieldSpec) + if (fieldMetamodel.getFirst() != null) { + fields.add(fieldMetamodel.getFirst()); + } interceptors.add(fieldMetamodel.getSecond()); initCodeBlocks.add(fieldMetamodel.getThird()); // Add _SCORE field to Vector - if (fieldSpec.type.toString().startsWith(VectorField.class.getName())) { + if (fieldSpec.type.toString().startsWith(VectorField.class.getName()) && fieldMetamodel.getFirst() != null) { String fieldName = fieldMetamodel.getFirst().fieldSpec().name; Pair vectorFieldScore = generateUnboundMetamodelField(entity, "_" + fieldSpec.name + "_SCORE", "__" + fieldName + "_score", Double.class); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/SearchFieldAccessor.java b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/SearchFieldAccessor.java index 4f760adc..d83f0648 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/SearchFieldAccessor.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/SearchFieldAccessor.java @@ -34,18 +34,39 @@ public class SearchFieldAccessor { public SearchFieldAccessor(String searchAlias, String jsonPath, Field... fields) { this.searchAlias = searchAlias; this.jsonPath = jsonPath; - this.fields.addAll(Arrays.asList(fields)); - this.targetClass = this.fields.get(0).getType(); - this.declaringClass = this.fields.get(0).getDeclaringClass(); + if (fields != null) { + this.fields.addAll(Arrays.asList(fields)); + this.targetClass = this.fields.get(0).getType(); + this.declaringClass = this.fields.get(0).getDeclaringClass(); + } else { + this.targetClass = null; + this.declaringClass = null; + } + } + + /** + * Creates a new SearchFieldAccessor for synthetic fields (like Map VALUES) with explicit type information. + * + * @param searchAlias the alias used for search operations + * @param jsonPath the JSON path for accessing the field value + * @param targetClass the target class type for this accessor + * @param declaringClass the class that declares the related field + */ + public SearchFieldAccessor(String searchAlias, String jsonPath, Class targetClass, Class declaringClass) { + this.searchAlias = searchAlias; + this.jsonPath = jsonPath; + this.targetClass = targetClass; + this.declaringClass = declaringClass; } /** * Returns the primary Java reflection field associated with this accessor. + * For synthetic fields (like Map VALUES), this may return null. * - * @return the primary field + * @return the primary field, or null for synthetic fields */ public Field getField() { - return fields.get(0); + return fields.isEmpty() ? null : fields.get(0); } /** diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/NumericField.java b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/NumericField.java index 9e338548..919ebe14 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/NumericField.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/NumericField.java @@ -74,6 +74,11 @@ public NotEqualPredicate notEq(T value) { * @return a GreaterThanPredicate that matches entities where this field is greater than the specified value */ public GreaterThanPredicate gt(T value) { + // For synthetic fields (like Map VALUES), use explicit NUMERIC field type + if (searchFieldAccessor.getField() == null) { + return new GreaterThanPredicate<>(searchFieldAccessor, value, + redis.clients.jedis.search.Schema.FieldType.NUMERIC); + } return new GreaterThanPredicate<>(searchFieldAccessor, value); } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/TagField.java b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/TagField.java index 42733f24..40685dbf 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/TagField.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/metamodel/indexed/TagField.java @@ -71,6 +71,10 @@ public TagField(Class targetClass, String fieldName) { * @return an equality predicate for query building */ public EqualPredicate eq(T value) { + // For synthetic fields (like Map VALUES), use explicit TAG field type + if (searchFieldAccessor.getField() == null) { + return new EqualPredicate<>(searchFieldAccessor, value, redis.clients.jedis.search.Schema.FieldType.TAG); + } return new EqualPredicate<>(searchFieldAccessor, value); } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java index acfe736c..47ef81be 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RediSearchQuery.java @@ -124,6 +124,25 @@ private static FieldType getRedisFieldType(Class fieldType) { } } + /** + * Gets the Redis field type for Map value types. + * This differs from getRedisFieldType() for Boolean values - Map Boolean values + * are indexed as NUMERIC fields (serialized as 1/0) rather than TAG fields. + */ + private static FieldType getRedisFieldTypeForMapValue(Class fieldType) { + if (CharSequence.class.isAssignableFrom( + fieldType) || fieldType == UUID.class || fieldType == Ulid.class || fieldType.isEnum()) { + return FieldType.TAG; + } else if (Number.class.isAssignableFrom( + fieldType) || fieldType == Boolean.class || fieldType == LocalDateTime.class || fieldType == LocalDate.class || fieldType == Date.class || fieldType == Instant.class || fieldType == OffsetDateTime.class) { + return FieldType.NUMERIC; + } else if (fieldType == Point.class || "org.springframework.data.geo.Point".equals(fieldType.getName())) { + return FieldType.GEO; + } else { + return null; // Unsupported type + } + } + private final QueryMethod queryMethod; private final RedisOMProperties redisOMProperties; private final boolean hasLanguageParameter; @@ -159,6 +178,7 @@ private static FieldType getRedisFieldType(Class fieldType) { private Boolean aggregationVerbatim; private Gson gson; private boolean isNullParamQuery; + private boolean isMapContainsQuery; private Dialect dialect = Dialect.TWO; /** @@ -226,10 +246,15 @@ public RediSearchQuery(// ) Class[] params = queryMethod.getParameters().stream().map(Parameter::getType).toArray(Class[]::new); hasLanguageParameter = Arrays.stream(params).anyMatch(c -> c.isAssignableFrom(SearchLanguage.class)); isANDQuery = QueryClause.hasContainingAllClause(queryMethod.getName()); + this.isMapContainsQuery = QueryClause.hasMapContainsClause(queryMethod.getName()); - String methodName = isANDQuery ? - QueryClause.getPostProcessMethodName(queryMethod.getName()) : - queryMethod.getName(); + String methodName = queryMethod.getName(); + if (isANDQuery) { + methodName = QueryClause.getPostProcessMethodName(methodName); + } + if (this.isMapContainsQuery) { + methodName = QueryClause.processMapContainsMethodName(methodName); + } try { java.lang.reflect.Method method = repoClass.getMethod(queryMethod.getName(), params); @@ -512,6 +537,24 @@ else if (Set.class.isAssignableFrom(fieldType) || List.class.isAssignableFrom(fi } } // + // Map fields + // + else if (Map.class.isAssignableFrom(fieldType)) { + // For Map fields, queries on the field actually query the indexed values + // The indexed field has a "_values" suffix in the search index + String mapValueKey = key + "_values"; + + Optional> maybeValueType = ObjectUtils.getMapValueClass(field); + if (maybeValueType.isPresent()) { + Class valueType = maybeValueType.get(); + FieldType valueFieldType = getRedisFieldTypeForMapValue(valueType); + + if (valueFieldType != null) { + qf.add(Pair.of(mapValueKey, QueryClause.get(valueFieldType, part.getType()))); + } + } + } + // // Point // else if (redisFieldType == FieldType.GEO) { @@ -677,7 +720,12 @@ private Object executeQuery(Object[] parameters) { // what to return Object result = null; - if (queryMethod.getReturnedObjectType() == SearchResult.class) { + // Check if this is an exists query + if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() + .getReturnedType() == Boolean.class) { + // For exists queries, return true if we have any results, false otherwise + result = searchResult.getTotalResults() > 0; + } else if (queryMethod.getReturnedObjectType() == SearchResult.class) { result = searchResult; } else if (queryMethod.isPageQuery()) { List content = searchResult.getDocuments().stream().map(this::parseDocumentResult).toList(); @@ -909,10 +957,13 @@ private Object executeFtTagVals() { private String prepareQuery(final Object[] parameters, boolean excludeNullParams) { logger.debug(String.format("parameters: %s", Arrays.toString(parameters))); + logger.info(String.format("Preparing query for method: %s, isMapContainsQuery: %s", queryMethod.getName(), + isMapContainsQuery)); List params = new ArrayList<>(Arrays.asList(parameters)); StringBuilder preparedQuery = new StringBuilder(); boolean multipleOrParts = queryOrParts.size() > 1; logger.debug(String.format("queryOrParts: %s", queryOrParts.size())); + logger.info(String.format("queryOrParts details: %s", queryOrParts)); if (!queryOrParts.isEmpty()) { preparedQuery.append(queryOrParts.stream().map(qop -> { String orPart = multipleOrParts ? "(" : ""; @@ -923,6 +974,7 @@ private String prepareQuery(final Object[] parameters, boolean excludeNullParams } String fieldName = QueryUtils.escape(fieldClauses.getFirst()); QueryClause queryClause = fieldClauses.getSecond(); + logger.info(String.format("Processing field: %s with queryClause: %s", fieldName, queryClause)); int paramsCnt = queryClause.getClauseTemplate().getNumberOfArguments(); Object[] ps = params.subList(0, paramsCnt).toArray(); @@ -974,6 +1026,7 @@ private String prepareQuery(final Object[] parameters, boolean excludeNullParams preparedQuery.append("*"); } + logger.info(String.format("Final query string: %s", preparedQuery)); logger.debug(String.format("query: %s", preparedQuery)); return preparedQuery.toString(); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java index 2e2e6180..ee78c400 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/RedisEnhancedQuery.java @@ -577,7 +577,12 @@ private Object executeQuery(Object[] parameters) { // what to return Object result; - if (queryMethod.getReturnedObjectType() == SearchResult.class) { + // Check if this is an exists query + if (processor.getReturnedType().getReturnedType() == boolean.class || processor.getReturnedType() + .getReturnedType() == Boolean.class) { + // For exists queries, return true if we have any results, false otherwise + result = searchResult.getTotalResults() > 0; + } else if (queryMethod.getReturnedObjectType() == SearchResult.class) { result = searchResult; } else if (queryMethod.isPageQuery()) { List content = searchResult.getDocuments().stream().map(d -> { diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/clause/QueryClause.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/clause/QueryClause.java index 81779c2e..14d1f01f 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/clause/QueryClause.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/clause/QueryClause.java @@ -1,5 +1,7 @@ package com.redis.om.spring.repository.query.clause; +import java.time.Instant; +import java.time.OffsetDateTime; import java.util.*; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -251,6 +253,13 @@ public enum QueryClause { GEO_CONTAINING_ALL( // QueryClauseTemplate.of(FieldType.GEO, Part.Type.CONTAINING, QueryClause.FIRST_PARAM, 1) // ), + /** + * Geo field query clause for point equality checks. + * Matches geographic fields that are equal to the specified point using a very small radius. + */ + GEO_SIMPLE_PROPERTY( // + QueryClauseTemplate.of(FieldType.GEO, Part.Type.SIMPLE_PROPERTY, QueryClause.FIELD_GEO_POINT_EQUAL, 1) // + ), // TAG /** * Tag field query clause for exact value matching. @@ -369,6 +378,13 @@ public enum QueryClause { * Used to distinguish between OR logic (Containing) and AND logic (ContainingAll) operations. */ public static final Pattern CONTAINING_ALL_PATTERN = Pattern.compile("(IsContainingAll|ContainingAll|ContainsAll)"); + + /** + * Pattern for matching Map value queries in method names. + * Used to identify queries on Map field values (e.g., findByFieldMapContains). + */ + public static final Pattern MAP_CONTAINS_PATTERN = Pattern.compile("([A-Za-z]+)MapContains"); + private static final String PARAM_PREFIX = "$param_"; private static final String FIRST_PARAM = "$param_0"; private static final String FIELD_EQUAL = "@$field:$param_0"; @@ -390,6 +406,7 @@ public enum QueryClause { private static final String FIELD_NUMERIC_BEFORE = "@$field:[-inf ($param_0]"; private static final String FIELD_NUMERIC_AFTER = "@$field:[($param_0 inf]"; private static final String FIELD_GEO_NEAR = "@$field:[$param_0 $param_1 $param_2]"; + private static final String FIELD_GEO_POINT_EQUAL = "@$field:[$param_0 $param_1 .000001 ft]"; private static final String FIELD_IS_NULL = "!exists(@$field)"; private static final String FIELD_IS_NOT_NULL = "exists(@$field)"; private static final String FIELD_LEXICOGRAPHIC = "__LEXICOGRAPHIC__"; @@ -434,6 +451,20 @@ public static boolean hasContainingAllClause(String methodName) { return CONTAINING_ALL_PATTERN.matcher(methodName).find(); } + /** + * Checks if the given method name contains a Map value query pattern. + *

+ * This method searches for patterns like "MapContains" or "MapContainsGreaterThan" + * in the method name to determine if it represents a query on Map field values. + *

+ * + * @param methodName the Spring Data repository method name to check + * @return true if the method name contains a Map value query pattern, false otherwise + */ + public static boolean hasMapContainsClause(String methodName) { + return MAP_CONTAINS_PATTERN.matcher(methodName).find(); + } + /** * Post-processes a method name by replacing "containing all" patterns with their simpler equivalents. *

@@ -461,6 +492,25 @@ public static String getPostProcessMethodName(String methodName) { return methodName; } + /** + * Processes a method name to handle Map value queries by removing the MapContains pattern. + *

+ * This method transforms method names containing "MapContains" patterns into standard + * query forms that PartTree can parse. For example, "findByFieldMapContains" becomes "findByField". + * The actual field mapping to the indexed values field is handled in the query extraction logic. + *

+ * + * @param methodName the original Spring Data repository method name + * @return the processed method name with MapContains patterns removed, or the original name if no patterns are found + */ + public static String processMapContainsMethodName(String methodName) { + if (hasMapContainsClause(methodName)) { + // Replace MapContains and its variations with empty string to get standard method name + return methodName.replaceAll("MapContains(GreaterThan|LessThan|After|Before)?", "$1"); + } + return methodName; + } + /** * Returns the query clause template associated with this QueryClause. *

@@ -547,6 +597,26 @@ public String prepareQuery(String field, Object... params) { } else { if (clauseTemplate.getIndexType() == FieldType.TEXT) { prepared = prepared.replace(PARAM_PREFIX + i++, param.toString()); + } else if (clauseTemplate.getIndexType() == FieldType.NUMERIC && param instanceof Boolean) { + // Special handling for Boolean values in NUMERIC fields (Map Boolean values) + // Convert true -> "1" and false -> "0" for RediSearch NUMERIC queries + String boolValue = ((Boolean) param) ? "1" : "0"; + prepared = prepared.replace(PARAM_PREFIX + i++, boolValue); + } else if (clauseTemplate.getIndexType() == FieldType.NUMERIC && param instanceof Date) { + // Special handling for Date values in NUMERIC fields + // Convert to epoch milliseconds for RediSearch NUMERIC queries + long timestamp = ((Date) param).getTime(); + prepared = prepared.replace(PARAM_PREFIX + i++, Long.toString(timestamp)); + } else if (clauseTemplate.getIndexType() == FieldType.NUMERIC && param instanceof Instant) { + // Special handling for Instant values in NUMERIC fields + // Convert to epoch milliseconds for RediSearch NUMERIC queries + long timestamp = ((Instant) param).toEpochMilli(); + prepared = prepared.replace(PARAM_PREFIX + i++, Long.toString(timestamp)); + } else if (clauseTemplate.getIndexType() == FieldType.NUMERIC && param instanceof OffsetDateTime) { + // Special handling for OffsetDateTime values in NUMERIC fields + // Convert to epoch milliseconds for RediSearch NUMERIC queries + long timestamp = ((OffsetDateTime) param).toInstant().toEpochMilli(); + prepared = prepared.replace(PARAM_PREFIX + i++, Long.toString(timestamp)); } else if (clauseTemplate.getIndexType() == FieldType.NUMERIC && !paramClass.equalsIgnoreCase( "java.time.LocalDateTime") && !paramClass.equalsIgnoreCase("java.time.LocalDate")) { prepared = prepared.replace(PARAM_PREFIX + i++, param.toString()); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/lexicographic/LexicographicQueryExecutor.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/lexicographic/LexicographicQueryExecutor.java index 1e367f9c..547af1b1 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/lexicographic/LexicographicQueryExecutor.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/query/lexicographic/LexicographicQueryExecutor.java @@ -6,6 +6,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.Limit; import org.springframework.data.util.Pair; import org.springframework.util.ReflectionUtils; @@ -36,6 +38,13 @@ public class LexicographicQueryExecutor { private final RedisModulesOperations modulesOperations; private final RediSearchIndexer indexer; + /** + * Creates a new LexicographicQueryExecutor with the specified dependencies. + * + * @param rediSearchQuery the RediSearch query being executed + * @param modulesOperations the Redis modules operations for executing commands + * @param indexer the RediSearch indexer for accessing field metadata + */ public LexicographicQueryExecutor(RediSearchQuery rediSearchQuery, RedisModulesOperations modulesOperations, RediSearchIndexer indexer) { this.rediSearchQuery = rediSearchQuery; @@ -176,9 +185,8 @@ private Set executeRangeQuery(String sortedSetKey, QueryClause queryClau // When doing greater than, we need to exclude exact matches with the same prefix // Since our format is "value#id", we append a high character to ensure we skip all entries with this prefix String gtParam = params[0].toString() + "\uffff"; // Unicode max character - Set results = modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gt(gtParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + Set results = modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, Range.rightUnbounded( + Range.Bound.exclusive(gtParam)), Limit.unlimited()); logger.debug(String.format("ZRANGEBYLEX %s (%s +inf returned: %s", sortedSetKey, gtParam, results)); return results; @@ -187,27 +195,24 @@ private Set executeRangeQuery(String sortedSetKey, QueryClause queryClau // For less than, we need to ensure we don't include the value itself // Since format is "value#id", we need to get everything before "value#" (excluded) String ltParam = params[0].toString() + "#"; // Exclude exact matches with this prefix - return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().lt(ltParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, Range.leftUnbounded(Range.Bound + .exclusive(ltParam)), Limit.unlimited()); case TEXT_GREATER_THAN_EQUAL: case TAG_GREATER_THAN_EQUAL: // For greater than or equal, we include the value itself // Since format is "value#id", we start from exactly "value#" String gteParam = params[0].toString() + "#"; // Include exact matches with this prefix - return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gte(gteParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, Range.rightUnbounded(Range.Bound + .inclusive(gteParam)), Limit.unlimited()); case TEXT_LESS_THAN_EQUAL: case TAG_LESS_THAN_EQUAL: // For less than or equal, we include all values with this prefix // Since format is "value#id", we use high unicode char to include all IDs with this value String lteParam = params[0].toString() + "\uffff"; // Include all exact matches with this prefix - return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().lte(lteParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, Range.leftUnbounded(Range.Bound + .inclusive(lteParam)), Limit.unlimited()); case TEXT_BETWEEN: case TAG_BETWEEN: @@ -215,9 +220,8 @@ private Set executeRangeQuery(String sortedSetKey, QueryClause queryClau // Start from exactly "minValue#" (inclusive) to "maxValue\uffff" (inclusive of all with maxValue) String minParam = params[0].toString() + "#"; String maxParam = params[1].toString() + "\uffff"; - return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gte(minParam).lte(maxParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + return modulesOperations.template().opsForZSet().rangeByLex(sortedSetKey, Range.closed(minParam, maxParam), + Limit.unlimited()); default: return Collections.emptySet(); diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java index 835391c7..b0a07aca 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisDocumentRepository.java @@ -319,13 +319,21 @@ public List saveAll(Iterable entities) { // Process responses using streams to avoid iterator issues if (responses != null && !responses.isEmpty()) { + List failedIds = new ArrayList<>(); long failedCount = IntStream.range(0, Math.min(responses.size(), entityIds.size())).filter(i -> responses.get( - i) instanceof JedisDataException).peek(i -> logger.warn( - "Failed JSON.SET command for entity with id: {} Error: {}", entityIds.get(i), - ((JedisDataException) responses.get(i)).getMessage())).count(); + i) instanceof JedisDataException).peek(i -> { + failedIds.add(entityIds.get(i).toString()); + logger.warn("Failed JSON.SET command for entity with id: {} Error: {}", entityIds.get(i), + ((JedisDataException) responses.get(i)).getMessage()); + }).count(); if (failedCount > 0) { - logger.warn("Total failed JSON.SET commands: {}", failedCount); + String errorMsg = String.format("Failed to save %d entities with IDs: %s", failedCount, failedIds); + if (properties.getRepository().isThrowOnSaveAllFailure()) { + throw new RuntimeException(errorMsg); + } else { + logger.warn("Total failed JSON.SET commands: {}", failedCount); + } } } } @@ -513,9 +521,21 @@ private void processReferenceAnnotations(byte[] objectKey, Object entity, Pipeli * @return an {@link Optional} containing the TTL in seconds, or empty if no TTL is configured */ private Optional getTTLForEntity(Object entity) { - KeyspaceConfiguration keyspaceConfig = mappingContext.getMappingConfiguration().getKeyspaceConfiguration(); - if (keyspaceConfig.hasSettingsFor(entity.getClass())) { - var settings = keyspaceConfig.getKeyspaceSettings(entity.getClass()); + // Use the resolver if available for cross-class-loader compatibility + KeyspaceConfiguration.KeyspaceSettings settings = null; + if (mappingContext instanceof com.redis.om.spring.mapping.RedisEnhancedMappingContext) { + var resolver = ((com.redis.om.spring.mapping.RedisEnhancedMappingContext) mappingContext).getKeyspaceResolver(); + if (resolver.hasSettingsFor(entity.getClass())) { + settings = resolver.getKeyspaceSettings(entity.getClass()); + } + } else { + KeyspaceConfiguration keyspaceConfig = mappingContext.getMappingConfiguration().getKeyspaceConfiguration(); + if (keyspaceConfig.hasSettingsFor(entity.getClass())) { + settings = keyspaceConfig.getKeyspaceSettings(entity.getClass()); + } + } + + if (settings != null) { if (org.springframework.util.StringUtils.hasText(settings.getTimeToLivePropertyName())) { Method ttlGetter; diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java index 860c23eb..39b44704 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/repository/support/SimpleRedisEnhancedRepository.java @@ -7,8 +7,11 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.IntStream; import java.util.stream.StreamSupport; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.BeanWrapper; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -54,6 +57,7 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; +import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.jedis.search.Query; import redis.clients.jedis.search.SearchResult; import redis.clients.jedis.util.SafeEncoder; @@ -73,6 +77,8 @@ public class SimpleRedisEnhancedRepository extends SimpleKeyValueRepository implements RedisEnhancedRepository { + private static final Logger logger = LoggerFactory.getLogger(SimpleRedisEnhancedRepository.class); + /** Operations for Redis modules (Search, JSON, etc.) */ protected final RedisModulesOperations modulesOperations; /** Metadata about the entity type managed by this repository */ @@ -409,6 +415,7 @@ private String getKey(Object id) { public List saveAll(Iterable entities) { Assert.notNull(entities, "The given Iterable of entities must not be null!"); List saved = new ArrayList<>(); + List entityIds = new ArrayList<>(); embedder.processEntities(entities); @@ -426,6 +433,7 @@ public List saveAll(Iterable entities) { keyValueEntity.getPropertyAccessor(entity).setProperty(keyValueEntity.getIdProperty(), id); String idAsString = validateKeyForWriting(id, entity); + entityIds.add(idAsString); String keyspace = keyValueEntity.getKeySpace(); byte[] objectKey = createKey(keyspace, idAsString); @@ -448,7 +456,28 @@ public List saveAll(Iterable entities) { saved.add(entity); } - pipeline.sync(); + + List responses = pipeline.syncAndReturnAll(); + + // Process responses to check for errors + if (responses != null && !responses.isEmpty()) { + List failedIds = new ArrayList<>(); + long failedCount = IntStream.range(0, Math.min(responses.size(), entityIds.size())).filter(i -> responses.get( + i) instanceof JedisDataException).peek(i -> { + failedIds.add(entityIds.get(i)); + logger.warn("Failed HMSET command for entity with id: {} Error: {}", entityIds.get(i), + ((JedisDataException) responses.get(i)).getMessage()); + }).count(); + + if (failedCount > 0) { + String errorMsg = String.format("Failed to save %d entities with IDs: %s", failedCount, failedIds); + if (properties.getRepository().isThrowOnSaveAllFailure()) { + throw new RuntimeException(errorMsg); + } else { + logger.warn("Total failed HMSET commands: {}", failedCount); + } + } + } } return saved; diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java index 162a9cb1..ce17d5a2 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/RedisFluentQueryByExample.java @@ -271,7 +271,17 @@ public Page page(Pageable pageable) { count = searchResult.getTotalResults(); } else { var info = searchOps.getInfo(); - count = (long) info.get("num_docs"); + Object numDocsValue = info.get("num_docs"); + + // Handle different return types from Redis + if (numDocsValue instanceof String) { + count = Long.parseLong((String) numDocsValue); + } else if (numDocsValue instanceof Number) { + count = ((Number) numDocsValue).longValue(); + } else { + // Fallback to 0 if the value is null or unexpected type + count = 0L; + } } var pageContents = searchStream.limit(pageable.getPageSize()).skip(pageable.getOffset()).collect(Collectors diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java index c619a4a1..95f21389 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/SearchStreamImpl.java @@ -496,7 +496,17 @@ public long count() { return searchResult.getTotalResults(); } else { var info = search.getInfo(); - return (long) info.get("num_docs"); + Object numDocsValue = info.get("num_docs"); + + // Handle different return types from Redis (fixes issue #639) + if (numDocsValue instanceof String) { + return Long.parseLong((String) numDocsValue); + } else if (numDocsValue instanceof Number) { + return ((Number) numDocsValue).longValue(); + } else { + // Fallback to 0 if the value is null or unexpected type + return 0L; + } } } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/BaseAbstractPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/BaseAbstractPredicate.java index 1d746b5e..3db723a5 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/BaseAbstractPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/BaseAbstractPredicate.java @@ -62,8 +62,27 @@ protected BaseAbstractPredicate(SearchFieldAccessor field) { this.fieldType = getFieldTypeFor(field.getField()); } + /** + * Creates a new BaseAbstractPredicate for the specified field with explicit field type. + * This constructor is used for synthetic fields (like Map VALUES) where the field type + * cannot be determined from annotations. + * + * @param field the field accessor for the target field + * @param fieldType the explicit Redis field type for this predicate + */ + protected BaseAbstractPredicate(SearchFieldAccessor field, FieldType fieldType) { + this.field = field; + this.fieldType = fieldType; + } + private static FieldType getFieldTypeFor(java.lang.reflect.Field field) { FieldType result = null; + + // Handle null fields (synthetic fields like Map VALUES) + if (field == null) { + return null; // Will be determined by the field type in constructor + } + // Searchable - behaves like Text indexed if (field.isAnnotationPresent(Searchable.class)) { result = FieldType.GEO; diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/SearchFieldPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/SearchFieldPredicate.java index 7e787208..15e89ca5 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/SearchFieldPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/SearchFieldPredicate.java @@ -74,6 +74,48 @@ default Predicate and(Predicate other) { return andPredicate; } + /** + * Combines this predicate with another predicate of a potentially different field type + * using a logical OR operation. + * + *

This method allows combining predicates with different field types, which is useful + * when building complex queries across multiple fields of different types.

+ * + * @param the field type of the other predicate + * @param other the predicate to combine with this one + * @return a new OR predicate combining both predicates + */ + @SuppressWarnings( + { "unchecked", "rawtypes" } + ) + default SearchFieldPredicate orAny(SearchFieldPredicate other) { + Objects.requireNonNull(other); + OrPredicate orPredicate = new OrPredicate(this); + orPredicate.addPredicate(other); + return orPredicate; + } + + /** + * Combines this predicate with another predicate of a potentially different field type + * using a logical AND operation. + * + *

This method allows combining predicates with different field types, which is useful + * when building complex queries across multiple fields of different types.

+ * + * @param the field type of the other predicate + * @param other the predicate to combine with this one + * @return a new AND predicate combining both predicates + */ + @SuppressWarnings( + { "unchecked", "rawtypes" } + ) + default SearchFieldPredicate andAny(SearchFieldPredicate other) { + Objects.requireNonNull(other); + AndPredicate andPredicate = new AndPredicate(this); + andPredicate.addPredicate(other); + return andPredicate; + } + /** * Applies this predicate to a RediSearch query node. * This method transforms the predicate into a RediSearch query node diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicBetweenMarker.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicBetweenMarker.java index 8f3f747d..1260869c 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicBetweenMarker.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicBetweenMarker.java @@ -19,16 +19,33 @@ public class LexicographicBetweenMarker extends BaseAbstractPredicate matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gte(minParam).lte(maxParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + Set matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, Range.closed(minParam, maxParam), Limit + .unlimited()); if (matches == null || matches.isEmpty()) { // No matches, return a query that matches nothing by using an impossible ID diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanMarker.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanMarker.java index 9d374296..6a6b150c 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanMarker.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanMarker.java @@ -19,11 +19,22 @@ public class LexicographicGreaterThanMarker extends BaseAbstractPredicate< LexicographicPredicate { private final T value; + /** + * Creates a new LexicographicGreaterThanMarker for the specified field and threshold. + * + * @param field the field accessor for the target string field + * @param value the threshold value (field must be lexicographically greater than this) + */ public LexicographicGreaterThanMarker(SearchFieldAccessor field, T value) { super(field); this.value = value; } + /** + * Returns the threshold value for comparison. + * + * @return the threshold value + */ public T getValue() { return value; } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanPredicate.java index 087aa325..e52b8854 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicGreaterThanPredicate.java @@ -5,6 +5,9 @@ import java.util.Set; import java.util.stream.Collectors; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.Limit; + import com.redis.om.spring.indexing.RediSearchIndexer; import com.redis.om.spring.metamodel.SearchFieldAccessor; import com.redis.om.spring.ops.RedisModulesOperations; @@ -107,9 +110,8 @@ public Node apply(Node root) { // For greater than, we need to exclude exact matches with the same prefix // Since our format is "value#id", we append a high character to ensure we skip all entries with this prefix String gtParam = value.toString() + "\uffff"; // Unicode max character - Set matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gt(gtParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + Set matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, Range.rightUnbounded(Range.Bound + .exclusive(gtParam)), Limit.unlimited()); if (matches == null || matches.isEmpty()) { // No matches, return a query that matches nothing by using an impossible ID diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanMarker.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanMarker.java index f178ed43..a4c6daa1 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanMarker.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanMarker.java @@ -18,11 +18,22 @@ public class LexicographicLessThanMarker extends BaseAbstractPredicate implements LexicographicPredicate { private final T value; + /** + * Creates a new LexicographicLessThanMarker for the specified field and threshold. + * + * @param field the field accessor for the target string field + * @param value the threshold value (field must be lexicographically less than this) + */ public LexicographicLessThanMarker(SearchFieldAccessor field, T value) { super(field); this.value = value; } + /** + * Returns the threshold value for comparison. + * + * @return the threshold value + */ public T getValue() { return value; } diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanPredicate.java index dda751b3..fc8d231c 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/lexicographic/LexicographicLessThanPredicate.java @@ -5,6 +5,9 @@ import java.util.Set; import java.util.stream.Collectors; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.Limit; + import com.redis.om.spring.indexing.RediSearchIndexer; import com.redis.om.spring.metamodel.SearchFieldAccessor; import com.redis.om.spring.ops.RedisModulesOperations; @@ -107,9 +110,8 @@ public Node apply(Node root) { // For less than, we need to ensure we don't include the value itself // Since format is "value#id", we need to get everything before "value#" (excluded) String ltParam = value.toString() + "#"; // Exclude exact matches with this prefix - Set matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().lt(ltParam), - org.springframework.data.redis.connection.RedisZSetCommands.Limit.unlimited()); + Set matches = rmo.template().opsForZSet().rangeByLex(sortedSetKey, Range.leftUnbounded(Range.Bound + .exclusive(ltParam)), Limit.unlimited()); if (matches == null || matches.isEmpty()) { // No matches, return a query that matches nothing by using an impossible ID diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/numeric/GreaterThanPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/numeric/GreaterThanPredicate.java index 42229d4a..21a79a20 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/numeric/GreaterThanPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/numeric/GreaterThanPredicate.java @@ -12,6 +12,7 @@ import com.redis.om.spring.search.stream.predicates.BaseAbstractPredicate; import com.redis.om.spring.search.stream.predicates.jedis.JedisValues; +import redis.clients.jedis.search.Schema.FieldType; import redis.clients.jedis.search.querybuilder.Node; import redis.clients.jedis.search.querybuilder.QueryBuilders; import redis.clients.jedis.search.querybuilder.Values; @@ -59,6 +60,20 @@ public GreaterThanPredicate(SearchFieldAccessor field, T value) { this.value = value; } + /** + * Creates a new GreaterThanPredicate for the specified field and threshold with explicit field type. + * This constructor is used for synthetic fields (like Map VALUES) where the field type + * cannot be determined from annotations. + * + * @param field the field accessor for the target numeric field + * @param value the threshold value (field must be greater than this) + * @param fieldType the explicit Redis field type for this predicate + */ + public GreaterThanPredicate(SearchFieldAccessor field, T value, FieldType fieldType) { + super(field, fieldType); + this.value = value; + } + /** * Returns the threshold value for comparison. * diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/tag/EqualPredicate.java b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/tag/EqualPredicate.java index 055c1c33..6a077349 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/tag/EqualPredicate.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/search/stream/predicates/tag/EqualPredicate.java @@ -5,6 +5,7 @@ import com.redis.om.spring.metamodel.SearchFieldAccessor; import com.redis.om.spring.search.stream.predicates.BaseAbstractPredicate; +import redis.clients.jedis.search.Schema.FieldType; import redis.clients.jedis.search.querybuilder.Node; import redis.clients.jedis.search.querybuilder.QueryBuilders; import redis.clients.jedis.search.querybuilder.QueryNode; @@ -57,6 +58,20 @@ public EqualPredicate(SearchFieldAccessor field, T value) { this.value = value; } + /** + * Creates a new EqualPredicate for the specified field and value(s) with explicit field type. + * This constructor is used for synthetic fields (like Map VALUES) where the field type + * cannot be determined from annotations. + * + * @param field the field accessor for the target tag field + * @param value the value to match, or collection of values for AND matching + * @param fieldType the explicit Redis field type for this predicate + */ + public EqualPredicate(SearchFieldAccessor field, T value, FieldType fieldType) { + super(field, fieldType); + this.value = value; + } + /** * Returns the value or collection of values to match against. * diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/BooleanTypeAdapter.java b/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/BooleanTypeAdapter.java new file mode 100644 index 00000000..2d2dc447 --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/BooleanTypeAdapter.java @@ -0,0 +1,71 @@ +package com.redis.om.spring.serialization.gson; + +import java.io.IOException; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * Gson TypeAdapter for Boolean values that serializes them as 1/0 for RediSearch compatibility. + * This adapter is specifically used for Boolean values in indexed Map fields to ensure + * they are stored in a format that RediSearch TAG fields can query. + */ +public class BooleanTypeAdapter extends TypeAdapter { + + private static final BooleanTypeAdapter INSTANCE = new BooleanTypeAdapter(); + + /** + * Private constructor to enforce singleton pattern. + * Use {@link #getInstance()} to obtain the singleton instance. + */ + private BooleanTypeAdapter() { + // Private constructor for singleton + } + + /** + * Returns the singleton instance of BooleanTypeAdapter. + * This adapter serializes Boolean values as 1/0 for RediSearch compatibility. + * + * @return the singleton BooleanTypeAdapter instance + */ + public static BooleanTypeAdapter getInstance() { + return INSTANCE; + } + + @Override + public void write(JsonWriter out, Boolean value) throws IOException { + if (value == null) { + out.nullValue(); + } else { + // Write Boolean as 1 or 0 for RediSearch TAG field compatibility + out.value(value ? 1 : 0); + } + } + + @Override + public Boolean read(JsonReader in) throws IOException { + JsonToken token = in.peek(); + if (token == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (token == JsonToken.BOOLEAN) { + return in.nextBoolean(); + } else if (token == JsonToken.NUMBER) { + // Read 1 as true, 0 as false + int value = in.nextInt(); + return value != 0; + } else if (token == JsonToken.STRING) { + String value = in.nextString(); + // Handle string representations + if ("1".equals(value) || "true".equalsIgnoreCase(value)) { + return true; + } else if ("0".equals(value) || "false".equalsIgnoreCase(value)) { + return false; + } + throw new IOException("Cannot parse boolean value: " + value); + } + throw new IOException("Expected BOOLEAN, NUMBER, or STRING but was " + token); + } +} \ No newline at end of file diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/MapBooleanTypeAdapterFactory.java b/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/MapBooleanTypeAdapterFactory.java new file mode 100644 index 00000000..3c143321 --- /dev/null +++ b/redis-om-spring/src/main/java/com/redis/om/spring/serialization/gson/MapBooleanTypeAdapterFactory.java @@ -0,0 +1,149 @@ +package com.redis.om.spring.serialization.gson; + +import java.io.IOException; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.Map; + +import com.google.gson.*; +import com.google.gson.reflect.TypeToken; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonToken; +import com.google.gson.stream.JsonWriter; + +/** + * TypeAdapterFactory that handles Boolean values in Maps specially for RediSearch compatibility. + * When serializing Map values with Boolean type, Booleans are converted to 1/0 instead of true/false + * to ensure compatibility with RediSearch TAG fields. + */ +public class MapBooleanTypeAdapterFactory implements TypeAdapterFactory { + + private static final MapBooleanTypeAdapterFactory INSTANCE = new MapBooleanTypeAdapterFactory(); + + /** + * Private constructor to enforce singleton pattern. + * Use {@link #getInstance()} to obtain the singleton instance. + */ + private MapBooleanTypeAdapterFactory() { + // Private constructor for singleton + } + + /** + * Returns the singleton instance of MapBooleanTypeAdapterFactory. + * This factory creates TypeAdapters for Map types with Boolean values, + * serializing them as 1/0 for RediSearch compatibility. + * + * @return the singleton MapBooleanTypeAdapterFactory instance + */ + public static MapBooleanTypeAdapterFactory getInstance() { + return INSTANCE; + } + + @Override + @SuppressWarnings( + "unchecked" + ) + public TypeAdapter create(Gson gson, TypeToken type) { + // Only handle Map types + if (!Map.class.isAssignableFrom(type.getRawType())) { + return null; + } + + Type mapType = type.getType(); + if (!(mapType instanceof ParameterizedType)) { + return null; + } + + ParameterizedType parameterizedType = (ParameterizedType) mapType; + Type[] typeArguments = parameterizedType.getActualTypeArguments(); + + // Check if this is a Map with Boolean values + if (typeArguments.length != 2) { + return null; + } + + Type valueType = typeArguments[1]; + boolean isBooleanValue = (valueType == Boolean.class || valueType == boolean.class); + + if (!isBooleanValue) { + // Not a Boolean map, delegate to default adapter + return null; + } + + // Get the default Map adapter + TypeAdapter delegate = gson.getDelegateAdapter(this, type); + + // Return our custom adapter that wraps the delegate + return new TypeAdapter() { + @Override + public void write(JsonWriter out, T value) throws IOException { + if (value == null) { + out.nullValue(); + return; + } + + Map map = (Map) value; + out.beginObject(); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() != null) { + out.name(String.valueOf(entry.getKey())); + if (entry.getValue() == null) { + out.nullValue(); + } else { + // Write Boolean as 1 or 0 for RediSearch TAG field compatibility + out.value(entry.getValue() ? 1 : 0); + } + } + } + + out.endObject(); + } + + @Override + public T read(JsonReader in) throws IOException { + if (in.peek() == JsonToken.NULL) { + in.nextNull(); + return null; + } + + Map map = new HashMap<>(); + in.beginObject(); + + while (in.hasNext()) { + String key = in.nextName(); + Boolean value = readBooleanValue(in); + map.put(key, value); + } + + in.endObject(); + return (T) map; + } + + private Boolean readBooleanValue(JsonReader in) throws IOException { + JsonToken token = in.peek(); + if (token == JsonToken.NULL) { + in.nextNull(); + return null; + } else if (token == JsonToken.BOOLEAN) { + return in.nextBoolean(); + } else if (token == JsonToken.NUMBER) { + // Read 1 as true, 0 as false + int value = in.nextInt(); + return value != 0; + } else if (token == JsonToken.STRING) { + String value = in.nextString(); + // Handle string representations + if ("1".equals(value) || "true".equalsIgnoreCase(value)) { + return true; + } else if ("0".equals(value) || "false".equalsIgnoreCase(value)) { + return false; + } + throw new JsonParseException("Cannot parse boolean value: " + value); + } + throw new JsonParseException("Expected BOOLEAN, NUMBER, or STRING but was " + token); + } + }; + } +} \ No newline at end of file diff --git a/redis-om-spring/src/main/java/com/redis/om/spring/util/ObjectUtils.java b/redis-om-spring/src/main/java/com/redis/om/spring/util/ObjectUtils.java index 212f1c23..a8d2914b 100644 --- a/redis-om-spring/src/main/java/com/redis/om/spring/util/ObjectUtils.java +++ b/redis-om-spring/src/main/java/com/redis/om/spring/util/ObjectUtils.java @@ -188,6 +188,40 @@ public static Optional> getCollectionElementClass(Field field) { return Optional.empty(); } + /** + * Retrieves the value type from a Map field. + * + * @param field the Map field to analyze + * @return an Optional containing the value class if the field is a Map, empty otherwise + */ + public static Optional> getMapValueClass(Field field) { + if (Map.class.isAssignableFrom(field.getType())) { + ResolvableType mapType = ResolvableType.forField(field); + Class valueType = mapType.getGeneric(1).getRawClass(); + return valueType != null ? Optional.of(valueType) : Optional.empty(); + } + return Optional.empty(); + } + + /** + * Extracts the Map value class name from a Map type string. + * + * @param fullTypeClassName the full type name string like "java.util.Map<java.lang.String, java.lang.Integer>" + * @return the value type class name + */ + public static String getMapValueClassName(String fullTypeClassName) { + int openBracketPos = fullTypeClassName.indexOf('<'); + int closeBracketPos = fullTypeClassName.lastIndexOf('>'); + if (openBracketPos != -1 && closeBracketPos != -1) { + String genericTypes = fullTypeClassName.substring(openBracketPos + 1, closeBracketPos); + String[] types = genericTypes.split(","); + if (types.length == 2) { + return types[1].trim(); + } + } + return "java.lang.Object"; + } + /** * Retrieves the generic element type from a collection field. * diff --git a/tests/build.gradle b/tests/build.gradle index 9ce9712d..de9541d7 100644 --- a/tests/build.gradle +++ b/tests/build.gradle @@ -39,6 +39,7 @@ dependencies { // Important for RedisOM annotation processing! annotationProcessor project(':redis-om-spring') testAnnotationProcessor project(':redis-om-spring') + testAnnotationProcessor "com.google.auto.service:auto-service:${autoServiceVersion}" // Lombok compileOnly 'org.projectlombok:lombok' diff --git a/tests/lombok.config b/tests/lombok.config new file mode 100644 index 00000000..86b262a8 --- /dev/null +++ b/tests/lombok.config @@ -0,0 +1,10 @@ +# Lombok configuration for tests module + +# Note: @NonNull on primitive types generates warnings but is used intentionally +# to include primitive fields in @RequiredArgsConstructor. +# These warnings are expected and can be ignored as they don't affect functionality. +# The primitives are used in test fixtures where we want them included in the +# constructor for test data setup. + +lombok.nonNull.exceptionType = IllegalArgumentException +config.stopBubbling = true \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/annotations/LexicographicLessThanDebugTest.java b/tests/src/test/java/com/redis/om/spring/annotations/LexicographicLessThanDebugTest.java index e6cc9780..a9512ed3 100644 --- a/tests/src/test/java/com/redis/om/spring/annotations/LexicographicLessThanDebugTest.java +++ b/tests/src/test/java/com/redis/om/spring/annotations/LexicographicLessThanDebugTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Range; import org.springframework.data.redis.core.RedisTemplate; import java.util.Arrays; @@ -56,7 +57,7 @@ void setup() { // Test direct ZRANGEBYLEX Set directQuery = redisTemplate.opsForZSet().rangeByLex(nameLexKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().lt("Product Delta#")); + Range.leftUnbounded(Range.Bound.exclusive("Product Delta#"))); System.out.println("Direct ZRANGEBYLEX LT 'Product Delta#': " + directQuery); } diff --git a/tests/src/test/java/com/redis/om/spring/annotations/LexicographicQueryDebugTest.java b/tests/src/test/java/com/redis/om/spring/annotations/LexicographicQueryDebugTest.java index 29cc20fb..366f1712 100644 --- a/tests/src/test/java/com/redis/om/spring/annotations/LexicographicQueryDebugTest.java +++ b/tests/src/test/java/com/redis/om/spring/annotations/LexicographicQueryDebugTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Range; import org.springframework.data.redis.core.RedisTemplate; import java.util.Arrays; @@ -78,7 +79,7 @@ void setup() { // Test direct sorted set query Set directQuery = redisTemplate.opsForZSet().rangeByLex(skuLexKey, - org.springframework.data.redis.connection.RedisZSetCommands.Range.range().gt("product002")); + Range.rightUnbounded(Range.Bound.exclusive("product002"))); System.out.println("Direct ZRANGEBYLEX result: " + directQuery); } diff --git a/tests/src/test/java/com/redis/om/spring/annotations/document/BasicRedisDocumentMappingTest.java b/tests/src/test/java/com/redis/om/spring/annotations/document/BasicRedisDocumentMappingTest.java index 6a7ca887..2e8e0b41 100644 --- a/tests/src/test/java/com/redis/om/spring/annotations/document/BasicRedisDocumentMappingTest.java +++ b/tests/src/test/java/com/redis/om/spring/annotations/document/BasicRedisDocumentMappingTest.java @@ -916,4 +916,44 @@ void testEffectOfNotNullAnnotation() { assertThat(fields).hasSize(1); assertThat(fields).first().isEqualTo("001"); } + + @Test + void testIssue517_SaveAllErrorHandlingConfiguration() { + // Test for issue #517: saveAll error handling configuration + // This verifies that the configuration property is available and doesn't break normal operations + // In a real scenario with memory limits, the throwOnSaveAllFailure property would cause exceptions + + Company company1 = Company.of("TestCompany1", 2023, LocalDate.now(), + new Point(-122.066540, 37.377690), "test1@company.com"); + Company company2 = Company.of("TestCompany2", 2023, LocalDate.now(), + new Point(-122.066540, 37.377690), "test2@company.com"); + + List companies = List.of(company1, company2); + List saved = repository.saveAll(companies); + + assertThat(saved).hasSize(2); + assertThat(repository.count()).isGreaterThanOrEqualTo(2); + + // Clean up + repository.deleteAll(saved); + } + + @Test + void testIssue622_ExistsByQueryReturnsBoolean() { + // Test for issue #622: existsBy* queries should return boolean instead of throwing ClassCastException + Company redis = Company.of("RedisInc", 2011, LocalDate.of(2021, 5, 1), + new Point(-122.066540, 37.377690), "stack@redis.com"); + repository.save(redis); + + // Test exists query returns true for existing email + boolean exists = repository.existsByEmail("stack@redis.com"); + assertTrue(exists, "Should return true for existing email"); + + // Test exists query returns false for non-existing email + boolean notExists = repository.existsByEmail("nonexisting@redis.com"); + assertFalse(notExists, "Should return false for non-existing email"); + + // Clean up + repository.delete(redis); + } } diff --git a/tests/src/test/java/com/redis/om/spring/annotations/document/ComprehensiveMapFieldTest.java b/tests/src/test/java/com/redis/om/spring/annotations/document/ComprehensiveMapFieldTest.java new file mode 100644 index 00000000..07343e27 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/annotations/document/ComprehensiveMapFieldTest.java @@ -0,0 +1,543 @@ +package com.redis.om.spring.annotations.document; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.within; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import com.redis.om.spring.AbstractBaseDocumentTest; +import com.redis.om.spring.fixtures.document.model.ComprehensiveMapEntity; +import com.redis.om.spring.fixtures.document.repository.ComprehensiveMapEntityRepository; + +import java.util.UUID; +import java.time.LocalDateTime; +import java.time.LocalDate; +import java.time.Instant; +import java.time.OffsetDateTime; +import java.util.Date; +import org.springframework.data.geo.Point; +import com.github.f4b6a3.ulid.Ulid; +import java.math.BigDecimal; +import java.time.temporal.ChronoUnit; +import java.util.List; + +/** + * Comprehensive test for all Map field value types in Redis OM Spring. + * Tests indexing, querying, and retrieval for all supported Map value types. + */ +class ComprehensiveMapFieldTest extends AbstractBaseDocumentTest { + + @Autowired + ComprehensiveMapEntityRepository repository; + + @BeforeEach + void setUp() { + repository.deleteAll(); + } + + @Test + void testStringMapValues() { + // Create entity with String Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("StringMapTest"); + entity.getStringValues().put("name", "John"); + entity.getStringValues().put("city", "San Francisco"); + entity.getStringValues().put("country", "USA"); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byValue1 = repository.findByStringValuesMapContains("John"); + List byValue2 = repository.findByStringValuesMapContains("San Francisco"); + List byValue3 = repository.findByStringValuesMapContains("USA"); + List byNonExistent = repository.findByStringValuesMapContains("NonExistent"); + + assertThat(byValue1).hasSize(1).containsExactly(saved); + assertThat(byValue2).hasSize(1).containsExactly(saved); + assertThat(byValue3).hasSize(1).containsExactly(saved); + assertThat(byNonExistent).isEmpty(); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byValue1.get(0); + assertThat(retrieved.getStringValues()).containsEntry("name", "John"); + assertThat(retrieved.getStringValues()).containsEntry("city", "San Francisco"); + assertThat(retrieved.getStringValues()).containsEntry("country", "USA"); + } + + @Test + void testBooleanMapValues() { + // Create entity with Boolean Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("BooleanMapTest"); + entity.getBooleanValues().put("isActive", true); + entity.getBooleanValues().put("isVerified", false); + entity.getBooleanValues().put("hasPermission", true); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byTrue = repository.findByBooleanValuesMapContains(true); + List byFalse = repository.findByBooleanValuesMapContains(false); + + assertThat(byTrue).hasSize(1).containsExactly(saved); + assertThat(byFalse).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byTrue.get(0); + assertThat(retrieved.getBooleanValues()).containsEntry("isActive", true); + assertThat(retrieved.getBooleanValues()).containsEntry("isVerified", false); + assertThat(retrieved.getBooleanValues()).containsEntry("hasPermission", true); + } + + @Test + void testUuidMapValues() { + // Create entity with UUID Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("UuidMapTest"); + UUID uuid1 = UUID.randomUUID(); + UUID uuid2 = UUID.randomUUID(); + entity.getUuidValues().put("user", uuid1); + entity.getUuidValues().put("session", uuid2); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byUuid1 = repository.findByUuidValuesMapContains(uuid1); + List byUuid2 = repository.findByUuidValuesMapContains(uuid2); + UUID nonExistentUuid = UUID.randomUUID(); + List byNonExistent = repository.findByUuidValuesMapContains(nonExistentUuid); + + assertThat(byUuid1).hasSize(1).containsExactly(saved); + assertThat(byUuid2).hasSize(1).containsExactly(saved); + assertThat(byNonExistent).isEmpty(); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byUuid1.get(0); + assertThat(retrieved.getUuidValues()).containsEntry("user", uuid1); + assertThat(retrieved.getUuidValues()).containsEntry("session", uuid2); + } + + @Test + void testUlidMapValues() { + // Create entity with Ulid Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("UlidMapTest"); + Ulid ulid1 = Ulid.fast(); + Ulid ulid2 = Ulid.fast(); + entity.getUlidValues().put("request", ulid1); + entity.getUlidValues().put("response", ulid2); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byUlid1 = repository.findByUlidValuesMapContains(ulid1); + List byUlid2 = repository.findByUlidValuesMapContains(ulid2); + + assertThat(byUlid1).hasSize(1).containsExactly(saved); + assertThat(byUlid2).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byUlid1.get(0); + assertThat(retrieved.getUlidValues()).containsEntry("request", ulid1); + assertThat(retrieved.getUlidValues()).containsEntry("response", ulid2); + } + + @Test + void testEnumMapValues() { + // Create entity with Enum Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("EnumMapTest"); + entity.getEnumValues().put("status", ComprehensiveMapEntity.TestEnum.OPTION_A); + entity.getEnumValues().put("priority", ComprehensiveMapEntity.TestEnum.OPTION_B); + entity.getEnumValues().put("category", ComprehensiveMapEntity.TestEnum.OPTION_C); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byOptionA = repository.findByEnumValuesMapContains(ComprehensiveMapEntity.TestEnum.OPTION_A); + List byOptionB = repository.findByEnumValuesMapContains(ComprehensiveMapEntity.TestEnum.OPTION_B); + List byOptionC = repository.findByEnumValuesMapContains(ComprehensiveMapEntity.TestEnum.OPTION_C); + + assertThat(byOptionA).hasSize(1).containsExactly(saved); + assertThat(byOptionB).hasSize(1).containsExactly(saved); + assertThat(byOptionC).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byOptionA.get(0); + assertThat(retrieved.getEnumValues()).containsEntry("status", ComprehensiveMapEntity.TestEnum.OPTION_A); + assertThat(retrieved.getEnumValues()).containsEntry("priority", ComprehensiveMapEntity.TestEnum.OPTION_B); + assertThat(retrieved.getEnumValues()).containsEntry("category", ComprehensiveMapEntity.TestEnum.OPTION_C); + } + + @Test + void testIntegerMapValues() { + // Create entity with Integer Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("IntegerMapTest"); + entity.getIntegerValues().put("age", 25); + entity.getIntegerValues().put("score", 100); + entity.getIntegerValues().put("level", 5); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byAge = repository.findByIntegerValuesMapContains(25); + List byScore = repository.findByIntegerValuesMapContains(100); + List byLevel = repository.findByIntegerValuesMapContains(5); + List byNonExistent = repository.findByIntegerValuesMapContains(999); + + assertThat(byAge).hasSize(1).containsExactly(saved); + assertThat(byScore).hasSize(1).containsExactly(saved); + assertThat(byLevel).hasSize(1).containsExactly(saved); + assertThat(byNonExistent).isEmpty(); + + // Test range queries + List byGreaterThan = repository.findByIntegerValuesMapContainsGreaterThan(20); + List byLessThan = repository.findByIntegerValuesMapContainsLessThan(30); + + assertThat(byGreaterThan).hasSize(1).containsExactly(saved); + assertThat(byLessThan).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byAge.get(0); + assertThat(retrieved.getIntegerValues()).containsEntry("age", 25); + assertThat(retrieved.getIntegerValues()).containsEntry("score", 100); + assertThat(retrieved.getIntegerValues()).containsEntry("level", 5); + } + + @Test + void testLongMapValues() { + // Create entity with Long Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("LongMapTest"); + entity.getLongValues().put("timestamp", 1234567890L); + entity.getLongValues().put("fileSize", 1048576L); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byTimestamp = repository.findByLongValuesMapContains(1234567890L); + List byFileSize = repository.findByLongValuesMapContains(1048576L); + + assertThat(byTimestamp).hasSize(1).containsExactly(saved); + assertThat(byFileSize).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byTimestamp.get(0); + assertThat(retrieved.getLongValues()).containsEntry("timestamp", 1234567890L); + assertThat(retrieved.getLongValues()).containsEntry("fileSize", 1048576L); + } + + @Test + void testDoubleMapValues() { + // Create entity with Double Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("DoubleMapTest"); + entity.getDoubleValues().put("price", 99.99); + entity.getDoubleValues().put("rating", 4.5); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byPrice = repository.findByDoubleValuesMapContains(99.99); + List byRating = repository.findByDoubleValuesMapContains(4.5); + + assertThat(byPrice).hasSize(1).containsExactly(saved); + assertThat(byRating).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byPrice.get(0); + assertThat(retrieved.getDoubleValues()).containsEntry("price", 99.99); + assertThat(retrieved.getDoubleValues()).containsEntry("rating", 4.5); + } + + @Test + void testFloatMapValues() { + // Create entity with Float Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("FloatMapTest"); + entity.getFloatValues().put("temperature", 25.5f); + entity.getFloatValues().put("humidity", 60.0f); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byTemp = repository.findByFloatValuesMapContains(25.5f); + List byHumidity = repository.findByFloatValuesMapContains(60.0f); + + assertThat(byTemp).hasSize(1).containsExactly(saved); + assertThat(byHumidity).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byTemp.get(0); + assertThat(retrieved.getFloatValues()).containsEntry("temperature", 25.5f); + assertThat(retrieved.getFloatValues()).containsEntry("humidity", 60.0f); + } + + @Test + void testBigDecimalMapValues() { + // Create entity with BigDecimal Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("BigDecimalMapTest"); + BigDecimal amount = new BigDecimal("1234.56"); + BigDecimal balance = new BigDecimal("9876.54"); + entity.getBigDecimalValues().put("amount", amount); + entity.getBigDecimalValues().put("balance", balance); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byAmount = repository.findByBigDecimalValuesMapContains(amount); + List byBalance = repository.findByBigDecimalValuesMapContains(balance); + + assertThat(byAmount).hasSize(1).containsExactly(saved); + assertThat(byBalance).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byAmount.get(0); + assertThat(retrieved.getBigDecimalValues()).containsEntry("amount", amount); + assertThat(retrieved.getBigDecimalValues()).containsEntry("balance", balance); + } + + @Test + void testLocalDateTimeMapValues() { + // Create entity with LocalDateTime Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("LocalDateTimeMapTest"); + LocalDateTime created = LocalDateTime.now().minusDays(1); + LocalDateTime updated = LocalDateTime.now(); + entity.getLocalDateTimeValues().put("created", created); + entity.getLocalDateTimeValues().put("updated", updated); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byCreated = repository.findByLocalDateTimeValuesMapContains(created); + List byUpdated = repository.findByLocalDateTimeValuesMapContains(updated); + + assertThat(byCreated).hasSize(1); + assertThat(byUpdated).hasSize(1); + assertThat(byCreated.get(0).getId()).isEqualTo(saved.getId()); + assertThat(byUpdated.get(0).getId()).isEqualTo(saved.getId()); + + // Verify retrieved entity has correct values (with tolerance for precision loss) + ComprehensiveMapEntity retrieved = byCreated.get(0); + assertThat(retrieved.getLocalDateTimeValues()).containsKey("created"); + assertThat(retrieved.getLocalDateTimeValues()).containsKey("updated"); + + // Check values are within 1 second tolerance due to serialization precision loss + LocalDateTime retrievedCreated = retrieved.getLocalDateTimeValues().get("created"); + LocalDateTime retrievedUpdated = retrieved.getLocalDateTimeValues().get("updated"); + assertThat(retrievedCreated).isCloseTo(created, within(1, ChronoUnit.SECONDS)); + assertThat(retrievedUpdated).isCloseTo(updated, within(1, ChronoUnit.SECONDS)); + } + + @Test + void testLocalDateMapValues() { + // Create entity with LocalDate Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("LocalDateMapTest"); + LocalDate startDate = LocalDate.of(2023, 1, 1); + LocalDate endDate = LocalDate.of(2023, 12, 31); + entity.getLocalDateValues().put("start", startDate); + entity.getLocalDateValues().put("end", endDate); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byStart = repository.findByLocalDateValuesMapContains(startDate); + List byEnd = repository.findByLocalDateValuesMapContains(endDate); + + assertThat(byStart).hasSize(1).containsExactly(saved); + assertThat(byEnd).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = byStart.get(0); + assertThat(retrieved.getLocalDateValues()).containsEntry("start", startDate); + assertThat(retrieved.getLocalDateValues()).containsEntry("end", endDate); + } + + @Test + void testDateMapValues() { + // Create entity with Date Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("DateMapTest"); + Date date1 = new Date(System.currentTimeMillis() - 86400000); // Yesterday + Date date2 = new Date(); // Now + entity.getDateValues().put("yesterday", date1); + entity.getDateValues().put("today", date2); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byDate1 = repository.findByDateValuesMapContains(date1); + List byDate2 = repository.findByDateValuesMapContains(date2); + + assertThat(byDate1).hasSize(1); + assertThat(byDate2).hasSize(1); + assertThat(byDate1.get(0).getId()).isEqualTo(saved.getId()); + assertThat(byDate2.get(0).getId()).isEqualTo(saved.getId()); + + // Verify retrieved entity has correct values (with tolerance for precision loss) + ComprehensiveMapEntity retrieved = byDate1.get(0); + assertThat(retrieved.getDateValues()).containsKey("yesterday"); + assertThat(retrieved.getDateValues()).containsKey("today"); + + // Check values are within 1 second tolerance due to serialization precision loss + Date retrievedYesterday = retrieved.getDateValues().get("yesterday"); + Date retrievedToday = retrieved.getDateValues().get("today"); + assertThat(Math.abs(retrievedYesterday.getTime() - date1.getTime())).isLessThan(1000); + assertThat(Math.abs(retrievedToday.getTime() - date2.getTime())).isLessThan(1000); + } + + @Test + void testInstantMapValues() { + // Create entity with Instant Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("InstantMapTest"); + Instant instant1 = Instant.now().minusSeconds(3600); // 1 hour ago + Instant instant2 = Instant.now(); + entity.getInstantValues().put("past", instant1); + entity.getInstantValues().put("present", instant2); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byPast = repository.findByInstantValuesMapContains(instant1); + List byPresent = repository.findByInstantValuesMapContains(instant2); + + assertThat(byPast).hasSize(1); + assertThat(byPresent).hasSize(1); + assertThat(byPast.get(0).getId()).isEqualTo(saved.getId()); + assertThat(byPresent.get(0).getId()).isEqualTo(saved.getId()); + + // Verify retrieved entity has correct values (with tolerance for precision loss) + ComprehensiveMapEntity retrieved = byPast.get(0); + assertThat(retrieved.getInstantValues()).containsKey("past"); + assertThat(retrieved.getInstantValues()).containsKey("present"); + + // Check values are within 1 second tolerance due to serialization precision loss + Instant retrievedPast = retrieved.getInstantValues().get("past"); + Instant retrievedPresent = retrieved.getInstantValues().get("present"); + assertThat(retrievedPast).isCloseTo(instant1, within(1, ChronoUnit.SECONDS)); + assertThat(retrievedPresent).isCloseTo(instant2, within(1, ChronoUnit.SECONDS)); + } + + @Test + void testOffsetDateTimeMapValues() { + // Create entity with OffsetDateTime Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("OffsetDateTimeMapTest"); + OffsetDateTime odt1 = OffsetDateTime.now().minusDays(1); + OffsetDateTime odt2 = OffsetDateTime.now(); + entity.getOffsetDateTimeValues().put("yesterday", odt1); + entity.getOffsetDateTimeValues().put("today", odt2); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List byYesterday = repository.findByOffsetDateTimeValuesMapContains(odt1); + List byToday = repository.findByOffsetDateTimeValuesMapContains(odt2); + + assertThat(byYesterday).hasSize(1); + assertThat(byToday).hasSize(1); + assertThat(byYesterday.get(0).getId()).isEqualTo(saved.getId()); + assertThat(byToday.get(0).getId()).isEqualTo(saved.getId()); + + // Verify retrieved entity has correct values (with tolerance for precision loss) + ComprehensiveMapEntity retrieved = byYesterday.get(0); + assertThat(retrieved.getOffsetDateTimeValues()).containsKey("yesterday"); + assertThat(retrieved.getOffsetDateTimeValues()).containsKey("today"); + + // Check values are within 1 second tolerance due to serialization precision loss + OffsetDateTime retrievedYesterday = retrieved.getOffsetDateTimeValues().get("yesterday"); + OffsetDateTime retrievedToday = retrieved.getOffsetDateTimeValues().get("today"); + assertThat(retrievedYesterday).isCloseTo(odt1, within(1, ChronoUnit.SECONDS)); + assertThat(retrievedToday).isCloseTo(odt2, within(1, ChronoUnit.SECONDS)); + } + + @Test + void testPointMapValues() { + // Create entity with Point (GEO) Map values + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("PointMapTest"); + Point sanFrancisco = new Point(-122.4194, 37.7749); + Point newYork = new Point(-74.0059, 40.7128); + entity.getPointValues().put("sf", sanFrancisco); + entity.getPointValues().put("nyc", newYork); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test repository queries + List bySF = repository.findByPointValuesMapContains(sanFrancisco); + List byNYC = repository.findByPointValuesMapContains(newYork); + + assertThat(bySF).hasSize(1).containsExactly(saved); + assertThat(byNYC).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has correct values + ComprehensiveMapEntity retrieved = bySF.get(0); + assertThat(retrieved.getPointValues()).containsEntry("sf", sanFrancisco); + assertThat(retrieved.getPointValues()).containsEntry("nyc", newYork); + } + + @Test + void testMultipleMapTypesInSingleEntity() { + // Create entity with multiple Map field types + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("MultiMapTest"); + + // Add values for different Map types + entity.getStringValues().put("name", "MultiTest"); + entity.getBooleanValues().put("active", true); + entity.getIntegerValues().put("count", 42); + entity.getDoubleValues().put("rating", 4.8); + UUID uuid = UUID.randomUUID(); + entity.getUuidValues().put("id", uuid); + entity.getEnumValues().put("status", ComprehensiveMapEntity.TestEnum.OPTION_A); + + ComprehensiveMapEntity saved = repository.save(entity); + + // Test queries for each type + List byString = repository.findByStringValuesMapContains("MultiTest"); + List byBoolean = repository.findByBooleanValuesMapContains(true); + List byInteger = repository.findByIntegerValuesMapContains(42); + List byDouble = repository.findByDoubleValuesMapContains(4.8); + List byUuid = repository.findByUuidValuesMapContains(uuid); + List byEnum = repository.findByEnumValuesMapContains(ComprehensiveMapEntity.TestEnum.OPTION_A); + + // All queries should return the same entity + assertThat(byString).hasSize(1).containsExactly(saved); + assertThat(byBoolean).hasSize(1).containsExactly(saved); + assertThat(byInteger).hasSize(1).containsExactly(saved); + assertThat(byDouble).hasSize(1).containsExactly(saved); + assertThat(byUuid).hasSize(1).containsExactly(saved); + assertThat(byEnum).hasSize(1).containsExactly(saved); + + // Verify retrieved entity has all values + ComprehensiveMapEntity retrieved = byString.get(0); + assertThat(retrieved.getStringValues()).containsEntry("name", "MultiTest"); + assertThat(retrieved.getBooleanValues()).containsEntry("active", true); + assertThat(retrieved.getIntegerValues()).containsEntry("count", 42); + assertThat(retrieved.getDoubleValues()).containsEntry("rating", 4.8); + assertThat(retrieved.getUuidValues()).containsEntry("id", uuid); + assertThat(retrieved.getEnumValues()).containsEntry("status", ComprehensiveMapEntity.TestEnum.OPTION_A); + } + + @Test + void testEmptyMapValues() { + // Create entity with empty Maps + ComprehensiveMapEntity entity = ComprehensiveMapEntity.of("EmptyMapTest"); + // Don't add any Map values - should remain empty + + ComprehensiveMapEntity saved = repository.save(entity); + + // Verify entity was saved and can be retrieved by name + List byName = repository.findByName("EmptyMapTest"); + assertThat(byName).hasSize(1).containsExactly(saved); + + // Verify all Maps are empty + ComprehensiveMapEntity retrieved = byName.get(0); + assertThat(retrieved.getStringValues()).isEmpty(); + assertThat(retrieved.getBooleanValues()).isEmpty(); + assertThat(retrieved.getUuidValues()).isEmpty(); + assertThat(retrieved.getUlidValues()).isEmpty(); + assertThat(retrieved.getEnumValues()).isEmpty(); + assertThat(retrieved.getIntegerValues()).isEmpty(); + assertThat(retrieved.getLongValues()).isEmpty(); + assertThat(retrieved.getDoubleValues()).isEmpty(); + assertThat(retrieved.getFloatValues()).isEmpty(); + assertThat(retrieved.getBigDecimalValues()).isEmpty(); + assertThat(retrieved.getLocalDateTimeValues()).isEmpty(); + assertThat(retrieved.getLocalDateValues()).isEmpty(); + assertThat(retrieved.getDateValues()).isEmpty(); + assertThat(retrieved.getInstantValues()).isEmpty(); + assertThat(retrieved.getOffsetDateTimeValues()).isEmpty(); + assertThat(retrieved.getPointValues()).isEmpty(); + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/annotations/hash/BasicRedisHashMappingTest.java b/tests/src/test/java/com/redis/om/spring/annotations/hash/BasicRedisHashMappingTest.java index b9277c5f..7d0133d0 100644 --- a/tests/src/test/java/com/redis/om/spring/annotations/hash/BasicRedisHashMappingTest.java +++ b/tests/src/test/java/com/redis/om/spring/annotations/hash/BasicRedisHashMappingTest.java @@ -756,4 +756,64 @@ void testRepositoryGetKeyFor() { assertThat(redisKey).isEqualTo(getKey(Company.class.getName(), redis.getId())); assertThat(microsoftKey).isEqualTo(getKey(Company.class.getName(), microsoft.getId())); } + + @Test + void testIssue517_HashSaveAllErrorHandlingConfiguration() { + // Test for issue #517: saveAll error handling configuration for hash entities + // This verifies that syncAndReturnAll is now used and errors can be detected + + Person person1 = new Person(); + person1.setName("Test Person 1"); + person1.setEmail("test1@example.com"); + person1.setNickname("test1"); + person1.setRoles(Set.of("user")); + person1.setFavoriteFoods(Set.of("pizza")); + + Person person2 = new Person(); + person2.setName("Test Person 2"); + person2.setEmail("test2@example.com"); + person2.setNickname("test2"); + person2.setRoles(Set.of("user")); + person2.setFavoriteFoods(Set.of("burger")); + + List people = List.of(person1, person2); + List saved = personRepo.saveAll(people); + + assertThat(saved).hasSize(2); + assertThat(personRepo.count()).isGreaterThanOrEqualTo(2); + + // Clean up + personRepo.deleteAll(saved); + } + + @Test + void testIssue622_HashExistsByQueryReturnsBoolean() { + // Test for issue #622: existsBy* queries should return boolean for hash entities + Person john = new Person(); + john.setName("John Doe"); + john.setEmail("john@example.com"); + john.setNickname("johnd"); + john.setRoles(Set.of("admin")); + john.setFavoriteFoods(Set.of("pizza")); + personRepo.save(john); + + // Test exists query returns true for existing email + boolean existsByEmail = personRepo.existsByEmail("john@example.com"); + assertTrue(existsByEmail, "Should return true for existing email"); + + // Test exists query returns true for existing nickname + boolean existsByNickname = personRepo.existsByNickname("johnd"); + assertTrue(existsByNickname, "Should return true for existing nickname"); + + // Test exists query returns false for non-existing email + boolean notExistsByEmail = personRepo.existsByEmail("nonexisting@example.com"); + assertFalse(notExistsByEmail, "Should return false for non-existing email"); + + // Test exists query returns false for non-existing nickname + boolean notExistsByNickname = personRepo.existsByNickname("nonexisting"); + assertFalse(notExistsByNickname, "Should return false for non-existing nickname"); + + // Clean up + personRepo.delete(john); + } } diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/model/ComprehensiveMapEntity.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/ComprehensiveMapEntity.java new file mode 100644 index 00000000..2c48b293 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/ComprehensiveMapEntity.java @@ -0,0 +1,95 @@ +package com.redis.om.spring.fixtures.document.model; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import org.springframework.data.annotation.Id; +import org.springframework.data.geo.Point; + +import com.github.f4b6a3.ulid.Ulid; +import com.redis.om.spring.annotations.Document; +import com.redis.om.spring.annotations.Indexed; +import com.redis.om.spring.annotations.IndexingOptions; +import com.redis.om.spring.annotations.IndexCreationMode; + +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; + +@Data +@NoArgsConstructor +@RequiredArgsConstructor(staticName = "of") +@Document +@IndexingOptions(indexName = "ComprehensiveMapEntityIdx", creationMode = IndexCreationMode.DROP_AND_RECREATE) +public class ComprehensiveMapEntity { + + @Id + private String id; + + @NonNull + @Indexed + private String name; + + // TAG field types (String, Boolean, UUID, Ulid, Enum) + @Indexed + private Map stringValues = new HashMap<>(); + + @Indexed + private Map booleanValues = new HashMap<>(); + + @Indexed + private Map uuidValues = new HashMap<>(); + + @Indexed + private Map ulidValues = new HashMap<>(); + + @Indexed + private Map enumValues = new HashMap<>(); + + // NUMERIC field types (Integer, Long, Double, Float, BigDecimal, Date types) + @Indexed + private Map integerValues = new HashMap<>(); + + @Indexed + private Map longValues = new HashMap<>(); + + @Indexed + private Map doubleValues = new HashMap<>(); + + @Indexed + private Map floatValues = new HashMap<>(); + + @Indexed + private Map bigDecimalValues = new HashMap<>(); + + @Indexed + private Map localDateTimeValues = new HashMap<>(); + + @Indexed + private Map localDateValues = new HashMap<>(); + + @Indexed + private Map dateValues = new HashMap<>(); + + @Indexed + private Map instantValues = new HashMap<>(); + + @Indexed + private Map offsetDateTimeValues = new HashMap<>(); + + // GEO field types (Point) + @Indexed + private Map pointValues = new HashMap<>(); + + public enum TestEnum { + OPTION_A, OPTION_B, OPTION_C + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/model/StudentWithMap.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/StudentWithMap.java new file mode 100644 index 00000000..be674495 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/model/StudentWithMap.java @@ -0,0 +1,36 @@ +package com.redis.om.spring.fixtures.document.model; + +import com.redis.om.spring.annotations.Document; +import com.redis.om.spring.annotations.Indexed; +import lombok.*; +import org.springframework.data.annotation.Id; + +import java.util.HashMap; +import java.util.Map; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Document +public class StudentWithMap { + @Id + private String id; + + @Indexed + private String name; + + @Indexed + private Map courseGrades = new HashMap<>(); + + @Indexed + private Map courseInstructors = new HashMap<>(); + + public static StudentWithMap of(String name) { + StudentWithMap student = new StudentWithMap(); + student.setName(name); + student.setCourseGrades(new HashMap<>()); + student.setCourseInstructors(new HashMap<>()); + return student; + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/ComprehensiveMapEntityRepository.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/ComprehensiveMapEntityRepository.java new file mode 100644 index 00000000..e0926420 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/ComprehensiveMapEntityRepository.java @@ -0,0 +1,65 @@ +package com.redis.om.spring.fixtures.document.repository; + +import com.redis.om.spring.fixtures.document.model.ComprehensiveMapEntity; +import com.redis.om.spring.fixtures.document.model.ComprehensiveMapEntity.TestEnum; +import com.redis.om.spring.repository.RedisDocumentRepository; + +import java.math.BigDecimal; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.OffsetDateTime; +import java.util.Date; +import java.util.List; +import java.util.UUID; + +import com.github.f4b6a3.ulid.Ulid; +import org.springframework.data.geo.Point; + +public interface ComprehensiveMapEntityRepository extends RedisDocumentRepository { + + // Standard field queries + List findByName(String name); + + // TAG field queries (exact matching) - using MapContains suffix for Map value queries + List findByStringValuesMapContains(String value); + List findByBooleanValuesMapContains(Boolean value); + List findByUuidValuesMapContains(UUID value); + List findByUlidValuesMapContains(Ulid value); + List findByEnumValuesMapContains(TestEnum value); + + // NUMERIC field queries (comparison operations) - using MapContains suffix for Map value queries + List findByIntegerValuesMapContains(Integer value); + List findByIntegerValuesMapContainsGreaterThan(Integer value); + List findByIntegerValuesMapContainsLessThan(Integer value); + + List findByLongValuesMapContains(Long value); + List findByLongValuesMapContainsGreaterThan(Long value); + + List findByDoubleValuesMapContains(Double value); + List findByDoubleValuesMapContainsGreaterThan(Double value); + + List findByFloatValuesMapContains(Float value); + List findByFloatValuesMapContainsGreaterThan(Float value); + + List findByBigDecimalValuesMapContains(BigDecimal value); + List findByBigDecimalValuesMapContainsGreaterThan(BigDecimal value); + + List findByLocalDateTimeValuesMapContains(LocalDateTime value); + List findByLocalDateTimeValuesMapContainsAfter(LocalDateTime value); + + List findByLocalDateValuesMapContains(LocalDate value); + List findByLocalDateValuesMapContainsAfter(LocalDate value); + + List findByDateValuesMapContains(Date value); + List findByDateValuesMapContainsAfter(Date value); + + List findByInstantValuesMapContains(Instant value); + List findByInstantValuesMapContainsAfter(Instant value); + + List findByOffsetDateTimeValuesMapContains(OffsetDateTime value); + List findByOffsetDateTimeValuesMapContainsAfter(OffsetDateTime value); + + // GEO field queries would typically use spatial queries, but for basic testing we'll use equality + List findByPointValuesMapContains(Point value); +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/StudentWithMapRepository.java b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/StudentWithMapRepository.java new file mode 100644 index 00000000..9c8f42a1 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/fixtures/document/repository/StudentWithMapRepository.java @@ -0,0 +1,14 @@ +package com.redis.om.spring.fixtures.document.repository; + +import com.redis.om.spring.fixtures.document.model.StudentWithMap; +import com.redis.om.spring.repository.RedisDocumentRepository; + +import java.util.List; + +public interface StudentWithMapRepository extends RedisDocumentRepository { + // Test query methods for Map fields - queries work on the indexed map values + List findByCourseGradesGreaterThan(int grade); + List findByCourseInstructorsContaining(String instructor); + // Alternative approach - try exact matching instead of substring + List findByCourseInstructors(String instructor); +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/issues/Issue636IndexExistsTest.java b/tests/src/test/java/com/redis/om/spring/issues/Issue636IndexExistsTest.java new file mode 100644 index 00000000..76784f03 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/issues/Issue636IndexExistsTest.java @@ -0,0 +1,151 @@ +package com.redis.om.spring.issues; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.annotation.Id; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.redis.om.spring.AbstractBaseOMTest; +import com.redis.om.spring.TestConfig; +import com.redis.om.spring.annotations.Document; +import com.redis.om.spring.annotations.EnableRedisDocumentRepositories; +import com.redis.om.spring.annotations.Indexed; +import com.redis.om.spring.indexing.RediSearchIndexer; +import com.redis.om.spring.ops.RedisModulesOperations; +import com.redis.om.spring.ops.search.SearchOperations; + +import redis.clients.jedis.exceptions.JedisDataException; + +/** + * Test for issue #636: "Improve indexExistsFor method in RediSearchIndexer" + * + * This test verifies that the indexExistsFor method properly handles different + * error messages from different Redis versions when checking for non-existent indexes. + */ +@Testcontainers +@DirtiesContext +@SpringBootTest(classes = Issue636IndexExistsTest.Config.class) +class Issue636IndexExistsTest extends AbstractBaseOMTest { + + @Autowired + RediSearchIndexer indexer; + + @Autowired + RedisModulesOperations modulesOperations; + + @Test + void testIssue636_VerifyErrorMessageHandling() { + // Try to get info for a non-existent index to verify error handling + SearchOperations searchOps = modulesOperations.opsForSearch("non_existent_index"); + + JedisDataException capturedError = null; + try { + searchOps.getInfo(); + fail("Expected JedisDataException for non-existent index"); + } catch (JedisDataException e) { + capturedError = e; + } + + assertNotNull(capturedError, "Should have caught an exception"); + String errorMessage = capturedError.getMessage(); + + // The fixed implementation now handles multiple error message patterns + // to support different Redis versions (Redis Stack, Redis 7.x, Redis 8.0+) + String lowerCaseMessage = errorMessage.toLowerCase(); + boolean isRecognizedError = lowerCaseMessage.contains("unknown index") || + lowerCaseMessage.contains("no such index") || + lowerCaseMessage.contains("index does not exist") || + lowerCaseMessage.contains("not found"); + + assertTrue(isRecognizedError, + "Error message should be recognized. Actual: " + errorMessage); + } + + @Test + void testIssue636_IndexExistsForNonExistentEntity() { + // Test that indexExistsFor returns false for an entity that has no index + boolean exists = indexer.indexExistsFor(NonIndexedEntity.class); + assertFalse(exists, "Index should not exist for NonIndexedEntity"); + } + + @Test + void testIssue636_IndexExistsForIndexedEntity() { + // Create an index for TestEntity + indexer.createIndexFor(TestEntity636.class); + + // Verify it exists + boolean exists = indexer.indexExistsFor(TestEntity636.class); + assertTrue(exists, "Index should exist after creation"); + + // Drop the index + indexer.dropIndexFor(TestEntity636.class); + + // Verify it no longer exists + exists = indexer.indexExistsFor(TestEntity636.class); + assertFalse(exists, "Index should not exist after dropping"); + } + + @Test + void testIssue636_ErrorMessageCompatibility() { + // Test that both error message patterns are handled correctly + // This simulates what the fixed method should do + + String[] possibleErrorMessages = { + "ERR Unknown index name", // Redis Stack / Redis 7.x + "ERR no such index", // Potential Redis 8.0 message + "ERR index does not exist", // Alternative format + "Unknown index name 'test_idx'", // With index name included + "no such index: test_idx" // Alternative with index name + }; + + for (String errorMsg : possibleErrorMessages) { + // The fixed logic should handle all these patterns + boolean shouldReturnFalse = errorMsg.toLowerCase().contains("unknown index") || + errorMsg.toLowerCase().contains("no such index") || + errorMsg.toLowerCase().contains("index does not exist"); + + assertTrue(shouldReturnFalse, + "Error message should be recognized as index not found: " + errorMsg); + } + } + + // Test entities + @Document + static class TestEntity636 { + @Id + private String id; + + @Indexed + private String name; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getName() { return name; } + public void setName(String name) { this.name = name; } + } + + static class NonIndexedEntity { + private String id; + private String value; + + public String getId() { return id; } + public void setId(String id) { this.id = id; } + public String getValue() { return value; } + public void setValue(String value) { this.value = value; } + } + + @SpringBootApplication + @Configuration + @EnableRedisDocumentRepositories( + basePackages = "com.redis.om.spring.fixtures.document.repository" + ) + static class Config extends TestConfig { + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/issues/Issue637CustomMappingContextTest.java b/tests/src/test/java/com/redis/om/spring/issues/Issue637CustomMappingContextTest.java new file mode 100644 index 00000000..584bbc70 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/issues/Issue637CustomMappingContextTest.java @@ -0,0 +1,174 @@ +package com.redis.om.spring.issues; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.List; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.mapping.RedisMappingContext; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.junit.jupiter.Testcontainers; + +import com.google.gson.JsonObject; +import com.redis.om.spring.AbstractBaseOMTest; +import com.redis.om.spring.TestConfig; +import com.redis.om.spring.annotations.EnableRedisDocumentRepositories; +import com.redis.om.spring.fixtures.document.model.MyDoc; +import com.redis.om.spring.fixtures.document.repository.MyDocRepository; +import com.redis.om.spring.mapping.RedisEnhancedMappingContext; +import com.redis.om.spring.ops.RedisModulesOperations; +import com.redis.om.spring.ops.json.JSONOperations; +import com.redis.om.spring.ops.search.SearchOperations; + +/** + * Test for issue #637: "Not able to override keyValueMappingContext" + * + * This test verifies that users can now provide their own RedisEnhancedMappingContext + * bean with a custom keyspace resolver, without needing to set + * spring.main.allow-bean-definition-overriding=true + * + * The fix removes @Primary and adds @ConditionalOnMissingBean to allow user overrides. + */ +@Testcontainers +@DirtiesContext +@SpringBootTest(classes = Issue637CustomMappingContextTest.Config.class) +class Issue637CustomMappingContextTest extends AbstractBaseOMTest { + + private static final String TENANT_PREFIX = "tenant_prod"; + + @Autowired + MyDocRepository myDocRepository; + + @Autowired + RedisTemplate template; + + @Autowired + RedisModulesOperations modulesOperations; + + @Autowired + RedisMappingContext mappingContext; + + String myDocId; + + @BeforeEach + void loadTestData() { + Point point = new Point(-122.124500, 47.640160); + MyDoc myDoc = MyDoc.of("issue 637 test", point, point, 1); + myDoc = myDocRepository.save(myDoc); + myDocId = myDoc.getId(); + } + + @AfterEach + void cleanUp() { + myDocRepository.deleteAll(); + } + + @Test + void testIssue637_CustomMappingContextIsUsed() { + // Verify our custom mapping context is being used + assertThat(mappingContext).isInstanceOf(RedisEnhancedMappingContext.class); + + // Verify the custom keyspace resolver is applied + String keyspace = mappingContext.getKeySpaceResolver().resolveKeySpace(MyDoc.class); + assertThat(keyspace).isEqualTo(TENANT_PREFIX + ":MyDoc"); + } + + @Test + void testIssue637_DocumentsUseCustomKeyspace() { + // Verify the document was saved with custom keyspace + JSONOperations ops = modulesOperations.opsForJSON(); + + // The key should use our custom prefix + String expectedKey = TENANT_PREFIX + ":MyDoc:" + myDocId; + JsonObject rawJSON = ops.get(expectedKey, JsonObject.class); + + assertNotNull(rawJSON, "Document should exist at custom keyspace"); + assertEquals(myDocId, rawJSON.get("id").getAsString()); + } + + @Test + void testIssue637_SearchIndexUsesCustomKeyspace() { + SearchOperations searchOps = modulesOperations.opsForSearch(MyDoc.class.getName() + "Idx"); + var info = searchOps.getInfo(); + + @SuppressWarnings("unchecked") + var definition = (List) info.get("index_definition"); + assertNotNull(definition); + + int prefixesIndex = definition.indexOf("prefixes"); + assertTrue(prefixesIndex >= 0, "Index definition should contain prefixes"); + + @SuppressWarnings("unchecked") + var prefixes = (List) definition.get(prefixesIndex + 1); + assertNotNull(prefixes); + assertEquals(1, prefixes.size()); + assertEquals(TENANT_PREFIX + ":MyDoc:", prefixes.get(0), + "Index should use custom keyspace prefix"); + } + + @Test + void testIssue637_RepositoryOperationsWork() { + // Test find by ID + Optional maybeDoc = myDocRepository.findById(myDocId); + assertTrue(maybeDoc.isPresent()); + assertEquals("issue 637 test", maybeDoc.get().getTitle()); + + // Test update + MyDoc doc = maybeDoc.get(); + doc.setTitle("updated title"); + myDocRepository.save(doc); + + maybeDoc = myDocRepository.findById(myDocId); + assertTrue(maybeDoc.isPresent()); + assertEquals("updated title", maybeDoc.get().getTitle()); + + // Test delete + myDocRepository.deleteById(myDocId); + maybeDoc = myDocRepository.findById(myDocId); + assertFalse(maybeDoc.isPresent()); + } + + @SpringBootApplication + @Configuration + @EnableRedisDocumentRepositories( + basePackages = { + "com.redis.om.spring.fixtures.document.model", + "com.redis.om.spring.fixtures.document.repository" + } + ) + static class Config extends TestConfig { + + /** + * This demonstrates the fix for issue #637. + * Users can now provide their own RedisEnhancedMappingContext bean + * with a custom keyspace resolver. + * + * The @ConditionalOnMissingBean annotation on the default bean + * ensures this user-provided bean takes precedence without + * requiring spring.main.allow-bean-definition-overriding=true + */ + @Bean(name = "redisEnhancedMappingContext") + @Primary + public RedisEnhancedMappingContext customMappingContext() { + RedisEnhancedMappingContext mappingContext = new RedisEnhancedMappingContext(); + + // Custom keyspace resolver for multi-tenant support + mappingContext.setKeySpaceResolver(type -> + TENANT_PREFIX + ":" + type.getSimpleName()); + + return mappingContext; + } + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolverTest.java b/tests/src/test/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolverTest.java new file mode 100644 index 00000000..19358c72 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/mapping/ClassLoaderAwareKeyspaceResolverTest.java @@ -0,0 +1,148 @@ +package com.redis.om.spring.mapping; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration; +import org.springframework.data.redis.core.convert.KeyspaceConfiguration.KeyspaceSettings; + +import com.redis.om.spring.annotations.Document; + +class ClassLoaderAwareKeyspaceResolverTest { + + private ClassLoaderAwareKeyspaceResolver resolver; + private KeyspaceConfiguration keyspaceConfiguration; + + @BeforeEach + void setUp() { + keyspaceConfiguration = new KeyspaceConfiguration(); + resolver = new ClassLoaderAwareKeyspaceResolver(keyspaceConfiguration); + } + + @Test + void testNormalClassLoaderLookup() { + // Given + Class entityClass = TestEntity.class; + KeyspaceSettings settings = new KeyspaceSettings(entityClass, "test:"); + settings.setTimeToLive(300L); + + // When + resolver.addKeyspaceSettings(entityClass, settings); + + // Then + assertThat(resolver.hasSettingsFor(entityClass)).isTrue(); + KeyspaceSettings retrieved = resolver.getKeyspaceSettings(entityClass); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getTimeToLive()).isEqualTo(300L); + } + + @Test + void testCrossClassLoaderLookupByName() { + // Given + Class entityClass = TestEntity.class; + KeyspaceSettings settings = new KeyspaceSettings(entityClass, "test:"); + settings.setTimeToLive(600L); + + // When + resolver.addKeyspaceSettings(entityClass, settings); + + // Then - simulate a different class loader by using a class with the same name + // In reality, this would be a different Class instance with the same name + // For testing, we verify the name-based lookup works + assertThat(resolver.hasSettingsFor(TestEntity.class)).isTrue(); + KeyspaceSettings retrieved = resolver.getKeyspaceSettings(TestEntity.class); + assertThat(retrieved).isNotNull(); + assertThat(retrieved.getTimeToLive()).isEqualTo(600L); + } + + @Test + void testMultipleEntities() { + // Given + Class entity1 = TestEntity.class; + Class entity2 = AnotherTestEntity.class; + + KeyspaceSettings settings1 = new KeyspaceSettings(entity1, "test1:"); + settings1.setTimeToLive(100L); + + KeyspaceSettings settings2 = new KeyspaceSettings(entity2, "test2:"); + settings2.setTimeToLive(200L); + + // When + resolver.addKeyspaceSettings(entity1, settings1); + resolver.addKeyspaceSettings(entity2, settings2); + + // Then + assertThat(resolver.hasSettingsFor(entity1)).isTrue(); + assertThat(resolver.hasSettingsFor(entity2)).isTrue(); + + assertThat(resolver.getKeyspaceSettings(entity1).getTimeToLive()).isEqualTo(100L); + assertThat(resolver.getKeyspaceSettings(entity2).getTimeToLive()).isEqualTo(200L); + } + + @Test + void testNonExistentEntity() { + // Given + Class entityClass = NonExistentEntity.class; + + // Then + assertThat(resolver.hasSettingsFor(entityClass)).isFalse(); + assertThat(resolver.getKeyspaceSettings(entityClass)).isNull(); + } + + @Document(timeToLive = 300) + static class TestEntity { + private String id; + private String name; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Document(timeToLive = 200) + static class AnotherTestEntity { + private String id; + private String value; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + } + + static class NonExistentEntity { + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } +} \ No newline at end of file diff --git a/tests/src/test/java/com/redis/om/spring/metamodel/MetamodelGeneratorTest.java b/tests/src/test/java/com/redis/om/spring/metamodel/MetamodelGeneratorTest.java index 0af0631b..9124afea 100644 --- a/tests/src/test/java/com/redis/om/spring/metamodel/MetamodelGeneratorTest.java +++ b/tests/src/test/java/com/redis/om/spring/metamodel/MetamodelGeneratorTest.java @@ -429,7 +429,7 @@ public final class IdOnly$ { _THIS = new MetamodelField("__this", IdOnly.class, true); } } - """; + """; assertThat(fileContents).containsIgnoringWhitespaces(expected); } diff --git a/tests/src/test/java/com/redis/om/spring/search/stream/EntityStreamsIssuesTest.java b/tests/src/test/java/com/redis/om/spring/search/stream/EntityStreamsIssuesTest.java index fa19b897..b62475e9 100644 --- a/tests/src/test/java/com/redis/om/spring/search/stream/EntityStreamsIssuesTest.java +++ b/tests/src/test/java/com/redis/om/spring/search/stream/EntityStreamsIssuesTest.java @@ -609,4 +609,37 @@ void testEqAgainstContentWithForwardSlash() { doc2Repository.deleteAll(List.of(doc1, doc2)); } + @Test + void testIssue639_CountMethodHandlesStringNumDocs() { + // Issue #639: ClassCastException in count() when num_docs is returned as String + // This test ensures that count() properly handles String values from Redis FT.INFO + + // Clear existing data + docRepository.deleteAll(); + + // Create exactly 21 documents (matching the issue report) + List docs = new ArrayList<>(); + for (int i = 1; i <= 21; i++) { + Doc doc = Doc.of("first" + i, "second" + i); + doc.setId("doc" + i); + docs.add(doc); + } + docRepository.saveAll(docs); + + // Test count with no filter - this triggers the info.get("num_docs") path + long count = entityStream.of(Doc.class).count(); + assertThat(count).isEqualTo(21); + + // Test count with filter - uses query-based count + long filteredCount = entityStream.of(Doc.class) + .filter(Doc$.FIRST.startsWith("first")) + .count(); + assertThat(filteredCount).isEqualTo(21); + + // Clean up + docRepository.deleteAll(); + long finalCount = entityStream.of(Doc.class).count(); + assertThat(finalCount).isEqualTo(0); + } + } diff --git a/tests/src/test/java/com/redis/om/spring/search/stream/predicates/MixedTypePredicatesTest.java b/tests/src/test/java/com/redis/om/spring/search/stream/predicates/MixedTypePredicatesTest.java new file mode 100644 index 00000000..c74f2293 --- /dev/null +++ b/tests/src/test/java/com/redis/om/spring/search/stream/predicates/MixedTypePredicatesTest.java @@ -0,0 +1,161 @@ +package com.redis.om.spring.search.stream.predicates; + +import static org.junit.jupiter.api.Assertions.*; + +import java.lang.reflect.Field; + +import org.junit.jupiter.api.Test; + +import com.redis.om.spring.metamodel.SearchFieldAccessor; +import com.redis.om.spring.metamodel.indexed.NumericField; +import com.redis.om.spring.metamodel.indexed.TagField; + +/** + * Unit test for issue #342: Verifying that predicates with different field types + * can be combined using the new andAny() and orAny() methods. + */ +class MixedTypePredicatesTest { + + static class TestEntity { + private String nameSpace; + private Long relateId; + private String status; + + public String getNameSpace() { return nameSpace; } + public void setNameSpace(String nameSpace) { this.nameSpace = nameSpace; } + + public Long getRelateId() { return relateId; } + public void setRelateId(Long relateId) { this.relateId = relateId; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + } + + @Test + void testIssue342_CanCombineDifferentTypePredicatesWithAndAny() throws NoSuchFieldException { + // Create field accessors + Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace"); + Field relateIdField = TestEntity.class.getDeclaredField("relateId"); + + SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField); + SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField); + + // Create predicates with different types + TagField nameSpacePredicate = new TagField<>(nameSpaceAccessor, true); + NumericField relateIdPredicate = new NumericField<>(relateIdAccessor, true); + + // Test that we can combine String and Long predicates with andAny() + SearchFieldPredicate stringPred = nameSpacePredicate.eq("PERSONAL"); + SearchFieldPredicate longPred = relateIdPredicate.eq(100L); + + // This should compile without type errors + SearchFieldPredicate combined = stringPred.andAny(longPred); + assertNotNull(combined); + assertTrue(combined instanceof AndPredicate); + + // Verify the predicate was added + AndPredicate andPred = (AndPredicate) combined; + assertEquals(2, andPred.stream().count()); + } + + @Test + void testIssue342_CanCombineDifferentTypePredicatesWithOrAny() throws NoSuchFieldException { + // Create field accessors + Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace"); + Field relateIdField = TestEntity.class.getDeclaredField("relateId"); + + SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField); + SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField); + + // Create predicates with different types + TagField nameSpacePredicate = new TagField<>(nameSpaceAccessor, true); + NumericField relateIdPredicate = new NumericField<>(relateIdAccessor, true); + + // Test that we can combine String and Long predicates with orAny() + SearchFieldPredicate stringPred = nameSpacePredicate.eq("BUSINESS"); + SearchFieldPredicate longPred = relateIdPredicate.eq(200L); + + // This should compile without type errors + SearchFieldPredicate combined = stringPred.orAny(longPred); + assertNotNull(combined); + assertTrue(combined instanceof OrPredicate); + + // Verify the predicate was added + OrPredicate orPred = (OrPredicate) combined; + assertEquals(2, orPred.stream().count()); + } + + @Test + void testIssue342_ChainMultipleDifferentTypes() throws NoSuchFieldException { + // Create field accessors + Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace"); + Field relateIdField = TestEntity.class.getDeclaredField("relateId"); + Field statusField = TestEntity.class.getDeclaredField("status"); + + SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField); + SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField); + SearchFieldAccessor statusAccessor = new SearchFieldAccessor("@status", "$.status", statusField); + + // Create predicates with different types + TagField nameSpacePredicate = new TagField<>(nameSpaceAccessor, true); + NumericField relateIdPredicate = new NumericField<>(relateIdAccessor, true); + TagField statusPredicate = new TagField<>(statusAccessor, true); + + // Test chaining multiple different types + SearchFieldPredicate combined = nameSpacePredicate.eq("PERSONAL") + .andAny(relateIdPredicate.eq(100L)) + .andAny(statusPredicate.eq("ACTIVE")); + + assertNotNull(combined); + assertTrue(combined instanceof AndPredicate); + } + + @Test + void testIssue342_MixAndAnyAndOrAny() throws NoSuchFieldException { + // Create field accessors + Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace"); + Field relateIdField = TestEntity.class.getDeclaredField("relateId"); + + SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField); + SearchFieldAccessor relateIdAccessor = new SearchFieldAccessor("@relateId", "$.relateId", relateIdField); + + // Create predicates with different types + TagField nameSpacePredicate = new TagField<>(nameSpaceAccessor, true); + NumericField relateIdPredicate = new NumericField<>(relateIdAccessor, true); + + // Test mixing andAny and orAny + SearchFieldPredicate combined = nameSpacePredicate.eq("PERSONAL") + .andAny(relateIdPredicate.gt(50L)) + .orAny(nameSpacePredicate.eq("BUSINESS")); + + assertNotNull(combined); + assertTrue(combined instanceof OrPredicate); + } + + @Test + void testIssue342_SameTypeStillWorksWithRegularAnd() throws NoSuchFieldException { + // Create field accessors for same type + Field nameSpaceField = TestEntity.class.getDeclaredField("nameSpace"); + Field statusField = TestEntity.class.getDeclaredField("status"); + + SearchFieldAccessor nameSpaceAccessor = new SearchFieldAccessor("@nameSpace", "$.nameSpace", nameSpaceField); + SearchFieldAccessor statusAccessor = new SearchFieldAccessor("@status", "$.status", statusField); + + // Create predicates with same type (String) + TagField nameSpacePredicate = new TagField<>(nameSpaceAccessor, true); + TagField statusPredicate = new TagField<>(statusAccessor, true); + + // Test that same-type predicates still work with regular and() + // Note: and() returns Predicate, not SearchFieldPredicate, so we use andAny for consistency + SearchFieldPredicate combined = nameSpacePredicate.eq("PERSONAL") + .andAny(statusPredicate.eq("ACTIVE")); + + assertNotNull(combined); + assertTrue(combined instanceof AndPredicate); + + // Verify both predicates are included + @SuppressWarnings("unchecked") + AndPredicate andPred = (AndPredicate) combined; + assertEquals(2, andPred.stream().count()); + } +} \ No newline at end of file