This is an automated email from the ASF dual-hosted git repository.

martinweiler pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-kie-kogito-apps.git


The following commit(s) were added to refs/heads/main by this push:
     new 9e1900f8d [Incubator-kie-issues#2323] Allow filtering schema on nested 
arrays (#2328)
9e1900f8d is described below

commit 9e1900f8d8e3f0960a5679ad486b2dbec7db36ff
Author: GopikaReghunath <[email protected]>
AuthorDate: Thu Jul 2 19:14:26 2026 +0530

    [Incubator-kie-issues#2323] Allow filtering schema on nested arrays (#2328)
    
    * Allow filtering schema on nested arrays
    
    * Allow filtering schema on nested arrays
    
    * Allow filtering schema on nested arrays
    
    * Allow filtering schema on nested arrays
    
    ---------
    
    Co-authored-by: Gopikar <[email protected]>
---
 .../query/GraphQLInputObjectTypeMapper.java        |  18 +
 .../index/graphql/query/GraphQLQueryMapper.java    | 194 ++++++++++-
 .../main/resources/graphql/basic.schema.graphqls   |  80 ++++-
 .../graphql/query/GraphQLQueryMapperTest.java      | 284 ++++++++++++++++
 .../java/org/kie/kogito/index/test/TestUtils.java  |  28 ++
 .../org/kie/kogito/index/jpa/storage/JPAQuery.java | 247 +++++++++++++-
 .../AbstractProcessInstanceEntityQueryIT.java      | 366 +++++++++++++++++++++
 .../persistence/api/query/FilterCondition.java     |  12 +
 8 files changed, 1203 insertions(+), 26 deletions(-)

diff --git 
a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLInputObjectTypeMapper.java
 
b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLInputObjectTypeMapper.java
index 92d08f4db..0c839452c 100644
--- 
a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLInputObjectTypeMapper.java
+++ 
b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLInputObjectTypeMapper.java
@@ -45,6 +45,7 @@ public class GraphQLInputObjectTypeMapper extends 
AbstractInputObjectTypeMapper
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(GraphQLInputObjectTypeMapper.class);
     private static final String ARGUMENT = "Argument";
+    public static final String ARRAY_ARGUMENT = "ArrayArgument";
 
     private boolean mapOperators;
 
@@ -93,6 +94,10 @@ public class GraphQLInputObjectTypeMapper extends 
AbstractInputObjectTypeMapper
 
     private GraphQLInputType getInputTypeByField(GraphQLFieldDefinition field) 
{
         String name = resolveBaseTypeName(field.getType());
+
+        // Check if this is an array field
+        boolean isArray = field.getType() instanceof GraphQLList;
+
         switch (name) {
             case "Int":
                 return getInputObjectType("NumericArgument");
@@ -108,7 +113,20 @@ public class GraphQLInputObjectTypeMapper extends 
AbstractInputObjectTypeMapper
                 return getInputObjectType("BooleanArgument");
             case "DateTime":
                 return getInputObjectType("DateArgument");
+            case "StringArray":
+                return getInputObjectType("StringArrayArgument");
             default:
+                // For array fields, try to use ArrayArgument type first
+                if (isArray) {
+                    String arrayTypeName = name + ARRAY_ARGUMENT;
+                    GraphQLType arraySchemaType = 
getExistingType(arrayTypeName);
+                    if (arraySchemaType != null) {
+                        LOGGER.debug("Using array argument type: {} for field: 
{}", arrayTypeName, field.getName());
+                        return (GraphQLInputType) arraySchemaType;
+                    }
+                }
+
+                // Fall back to regular Argument type
                 String typeName = name + ARGUMENT;
                 GraphQLType schemaType = getExistingType(typeName);
                 if (schemaType == null) {
diff --git 
a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java
 
b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java
index 1fb357ffa..5b05a8d0d 100644
--- 
a/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java
+++ 
b/data-index/data-index-graphql/src/main/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapper.java
@@ -34,12 +34,14 @@ import graphql.schema.GraphQLInputObjectType;
 import graphql.schema.GraphQLInputType;
 import graphql.schema.GraphQLList;
 import graphql.schema.GraphQLNamedType;
+import graphql.schema.GraphQLType;
 
 import static graphql.schema.GraphQLTypeUtil.isList;
 import static graphql.schema.GraphQLTypeUtil.simplePrint;
 import static graphql.schema.GraphQLTypeUtil.unwrapNonNull;
 import static graphql.schema.GraphQLTypeUtil.unwrapOne;
 import static java.util.stream.Collectors.toList;
+import static 
org.kie.kogito.index.graphql.query.GraphQLInputObjectTypeMapper.ARRAY_ARGUMENT;
 import static org.kie.kogito.index.json.JsonUtils.jsonFilter;
 import static org.kie.kogito.persistence.api.query.FilterCondition.NOT;
 import static org.kie.kogito.persistence.api.query.QueryFilterFactory.and;
@@ -82,10 +84,10 @@ public class GraphQLQueryMapper implements 
Function<GraphQLInputObjectType, Grap
                         parser.mapAttribute(field.getName(), 
mapEnumArgument(field.getName()));
                     } else if (isListOfType(field.getType(), type.getName())) {
                         parser.mapAttribute(field.getName(), 
mapRecursiveListArgument(field.getName(), parser));
-                    } else if (((GraphQLNamedType) 
field.getType()).getName().equals(type.getName())) {
+                    } else if (field.getType() instanceof GraphQLNamedType 
namedType && namedType.getName().equals(type.getName())) {
                         parser.mapAttribute(field.getName(), 
mapRecursiveArgument(field.getName(), parser));
-                    } else {
-                        String name = ((GraphQLNamedType) 
field.getType()).getName();
+                    } else if (field.getType() instanceof GraphQLNamedType 
namedType) {
+                        String name = namedType.getName();
                         switch (name) {
                             case "IdArgument":
                                 parser.mapAttribute(field.getName(), 
mapIdArgument(field.getName()));
@@ -115,8 +117,13 @@ public class GraphQLQueryMapper implements 
Function<GraphQLInputObjectType, Grap
                                 parser.mapAttribute(field.getName(), 
mapJsonArgument(field.getName()));
                                 break;
                             default:
-                                if (field.getType() instanceof 
GraphQLInputObjectType) {
-                                    parser.mapAttribute(field.getName(), 
mapSubEntityArgument(field.getName(), new 
GraphQLQueryMapper().apply((GraphQLInputObjectType) field.getType())));
+                                if (field.getType() instanceof 
GraphQLInputObjectType inputType) {
+                                    // Check if this is an array argument type 
(ends with "ArrayArgument")
+                                    if 
(inputType.getName().endsWith(ARRAY_ARGUMENT)) {
+                                        parser.mapAttribute(field.getName(), 
mapArrayArgument(field.getName(), inputType));
+                                    } else {
+                                        parser.mapAttribute(field.getName(), 
mapSubEntityArgument(field.getName(), new 
GraphQLQueryMapper().apply(inputType)));
+                                    }
                                 }
                         }
                     }
@@ -229,6 +236,183 @@ public class GraphQLQueryMapper implements 
Function<GraphQLInputObjectType, Grap
         });
     }
 
+    private Function<Object, Stream<AttributeFilter<?>>> 
mapArrayArgument(String attribute, GraphQLInputObjectType arrayArgType) {
+        return argument -> {
+            Map<String, Object> argMap = (Map<String, Object>) argument;
+
+            // Separate array operations from backward-compatible fields
+            Map<String, Object> backwardCompatFields = new 
java.util.HashMap<>();
+            List<Stream<AttributeFilter<?>>> arrayOpStreams = new 
java.util.ArrayList<>();
+
+            // Lazy initialization of element parser (only when needed)
+            GraphQLQueryParser elementParser = null;
+
+            // Helper to get element parser (lazy initialization)
+            final GraphQLQueryParser[] parserHolder = new 
GraphQLQueryParser[1];
+            java.util.function.Supplier<GraphQLQueryParser> getElementParser = 
() -> {
+                if (parserHolder[0] == null) {
+                    GraphQLInputType containsFieldType = 
arrayArgType.getField("contains") != null ? 
arrayArgType.getField("contains").getType() : null;
+                    if (containsFieldType == null) {
+                        LOGGER.warn("Array argument type {} does not have a 
'contains' field required for element operations", arrayArgType.getName());
+                        return null;
+                    }
+                    GraphQLType unwrappedType = 
unwrapNonNull(containsFieldType);
+                    if (!(unwrappedType instanceof GraphQLInputObjectType 
elementType)) {
+                        LOGGER.warn("Contains field type is not an input 
object type: {}", simplePrint(unwrappedType));
+                        return null;
+                    }
+                    parserHolder[0] = new 
GraphQLQueryMapper().apply(elementType);
+                }
+                return parserHolder[0];
+            };
+
+            for (Map.Entry<String, Object> entry : argMap.entrySet()) {
+                FilterCondition condition = 
FilterCondition.fromLabel(entry.getKey());
+                Object value = entry.getValue();
+
+                if (condition == null) {
+                    // Backward compatibility: treat as element property (like 
'contains')
+                    backwardCompatFields.put(entry.getKey(), value);
+                    continue;
+                }
+
+                switch (condition) {
+                    case CONTAINS:
+                        // Single element filter: nodes.name = 'X'
+                        if (value instanceof Map) {
+                            GraphQLQueryParser parser = getElementParser.get();
+                            if (parser != null) {
+                                
arrayOpStreams.add(parser.apply(value).stream().map(f -> {
+                                    f.setAttribute(attribute + "." + 
f.getAttribute());
+                                    return f;
+                                }));
+                            }
+                        }
+                        break;
+
+                    case CONTAINS_ALL:
+                        // All elements must match: AND(nodes.name = 'X', 
nodes.name = 'Y')
+                        if (value instanceof List) {
+                            GraphQLQueryParser parser = getElementParser.get();
+                            if (parser != null) {
+                                List<AttributeFilter<?>> allFilters = 
((List<?>) value).stream()
+                                        .flatMap(elem -> {
+                                            if (elem instanceof Map) {
+                                                return 
parser.apply(elem).stream().map(f -> {
+                                                    f.setAttribute(attribute + 
"." + f.getAttribute());
+                                                    return f;
+                                                });
+                                            }
+                                            return Stream.empty();
+                                        })
+                                        .collect(toList());
+                                if (!allFilters.isEmpty()) {
+                                    
arrayOpStreams.add(Stream.of(and(allFilters)));
+                                }
+                            }
+                        }
+                        break;
+
+                    case CONTAINS_ANY:
+                        // Any element matches: OR(nodes.name = 'X', 
nodes.name = 'Y')
+                        if (value instanceof List) {
+                            GraphQLQueryParser parser = getElementParser.get();
+                            if (parser != null) {
+                                List<AttributeFilter<?>> anyFilters = 
((List<?>) value).stream()
+                                        .flatMap(elem -> {
+                                            if (elem instanceof Map) {
+                                                return 
parser.apply(elem).stream().map(f -> {
+                                                    f.setAttribute(attribute + 
"." + f.getAttribute());
+                                                    return f;
+                                                });
+                                            }
+                                            return Stream.empty();
+                                        })
+                                        .collect(toList());
+                                if (!anyFilters.isEmpty()) {
+                                    
arrayOpStreams.add(Stream.of(or(anyFilters)));
+                                }
+                            }
+                        }
+                        break;
+
+                    case IS_NULL:
+                        // Check if array is null or empty
+                        if (value instanceof Boolean) {
+                            
arrayOpStreams.add(Stream.of(Boolean.TRUE.equals(value) ? isNull(attribute) : 
notNull(attribute)));
+                        }
+                        break;
+
+                    case AND:
+                        // Recursively process AND conditions
+                        if (value instanceof List) {
+                            List<AttributeFilter<?>> andFilters = ((List<?>) 
value).stream()
+                                    .flatMap(elem -> {
+                                        if (elem instanceof Map) {
+                                            return mapArrayArgument(attribute, 
arrayArgType).apply(elem);
+                                        }
+                                        return Stream.empty();
+                                    })
+                                    .collect(toList());
+                            if (!andFilters.isEmpty()) {
+                                arrayOpStreams.add(Stream.of(and(andFilters)));
+                            }
+                        }
+                        break;
+
+                    case OR:
+                        // Recursively process OR conditions
+                        if (value instanceof List) {
+                            List<AttributeFilter<?>> orFilters = ((List<?>) 
value).stream()
+                                    .flatMap(elem -> {
+                                        if (elem instanceof Map) {
+                                            return mapArrayArgument(attribute, 
arrayArgType).apply(elem);
+                                        }
+                                        return Stream.empty();
+                                    })
+                                    .collect(toList());
+                            if (!orFilters.isEmpty()) {
+                                arrayOpStreams.add(Stream.of(or(orFilters)));
+                            }
+                        }
+                        break;
+
+                    case NOT:
+                        // Recursively process NOT condition
+                        if (value instanceof Map) {
+                            List<AttributeFilter<?>> notFilters = 
mapArrayArgument(attribute, arrayArgType).apply(value).collect(toList());
+                            if (notFilters.size() == 1) {
+                                // Single filter: apply NOT directly
+                                
arrayOpStreams.add(Stream.of(not(notFilters.get(0))));
+                            } else if (notFilters.size() > 1) {
+                                // Multiple filters: combine with AND, then 
apply NOT
+                                
arrayOpStreams.add(Stream.of(not(and(notFilters))));
+                            }
+                        }
+                        break;
+
+                    default:
+                        // Unknown condition - ignore
+                        break;
+                }
+            }
+
+            // Process backward-compatible fields as 'contains'
+            if (!backwardCompatFields.isEmpty()) {
+                GraphQLQueryParser parser = getElementParser.get();
+                if (parser != null) {
+                    
arrayOpStreams.add(parser.apply(backwardCompatFields).stream().map(f -> {
+                        f.setAttribute(attribute + "." + f.getAttribute());
+                        return f;
+                    }));
+                }
+            }
+
+            // Combine all streams
+            return arrayOpStreams.stream().flatMap(s -> s);
+        };
+    }
+
     private Function<Object, Stream<AttributeFilter<?>>> mapIdArgument(String 
attribute) {
         return argument -> ((Map<String, Object>) 
argument).entrySet().stream().map(entry -> {
             FilterCondition condition = 
FilterCondition.fromLabel(entry.getKey());
diff --git 
a/data-index/data-index-graphql/src/main/resources/graphql/basic.schema.graphqls
 
b/data-index/data-index-graphql/src/main/resources/graphql/basic.schema.graphqls
index 32edd6e27..6d12f99e2 100644
--- 
a/data-index/data-index-graphql/src/main/resources/graphql/basic.schema.graphqls
+++ 
b/data-index/data-index-graphql/src/main/resources/graphql/basic.schema.graphqls
@@ -214,8 +214,8 @@ input ProcessInstanceArgument {
     rootProcessId: StringArgument
     state: ProcessInstanceStateArgument
     error: ProcessInstanceErrorArgument
-    nodes: NodeInstanceArgument
-    milestones: MilestoneArgument
+    nodes: NodeInstanceArrayArgument
+    milestones: MilestoneArrayArgument
     endpoint: StringArgument
     roles: StringArrayArgument
     start: DateArgument
@@ -252,6 +252,28 @@ input NodeInstanceArgument {
     slaDueDate: DateArgument
 }
 
+input NodeInstanceArrayArgument {
+    # Array-specific operations
+    contains: NodeInstanceArgument
+    containsAll: [NodeInstanceArgument!]
+    containsAny: [NodeInstanceArgument!]
+    isNull: Boolean
+    and: [NodeInstanceArrayArgument!]
+    or: [NodeInstanceArrayArgument!]
+    not: NodeInstanceArrayArgument
+    # Backward compatibility:
+    id: IdArgument
+    name: StringArgument
+    definitionId: StringArgument
+    nodeId: StringArgument
+    type: StringArgument
+    enter: DateArgument
+    exit: DateArgument
+    errorMessage: StringArgument
+    retrigger: BooleanArgument
+    slaDueDate: DateArgument
+}
+
 input MilestoneStatusArgument {
     equal: MilestoneStatus
     in: [MilestoneStatus]
@@ -263,6 +285,20 @@ input MilestoneArgument {
     status: MilestoneStatusArgument
 }
 
+input MilestoneArrayArgument {
+    contains: MilestoneArgument
+    containsAll: [MilestoneArgument!]
+    containsAny: [MilestoneArgument!]
+    isNull: Boolean
+    and: [MilestoneArrayArgument!]
+    or: [MilestoneArrayArgument!]
+    not: MilestoneArrayArgument
+    # Backward compatibility fields
+    id: IdArgument
+    name: StringArgument
+    status: MilestoneStatusArgument
+}
+
 input StringArrayArgument {
     contains: String
     containsAll: [String!]
@@ -437,8 +473,8 @@ input UserTaskInstanceArgument {
     started: DateArgument
     referenceName: StringArgument
     lastUpdate: DateArgument
-    comments: CommentArgument
-    attachments: AttachmentArgument
+    comments: CommentArrayArgument
+    attachments: AttachmentArrayArgument
     slaDueDate: DateArgument
     rootProcessInstanceId: StringArgument
     rootProcessId: StringArgument
@@ -448,7 +484,26 @@ input UserTaskInstanceArgument {
 
 input CommentArgument {
     id: IdArgument
-    name: StringArgument
+    content: StringArgument
+    updatedBy: StringArgument
+    updatedAt: DateArgument
+}
+
+input CommentArrayArgument {
+    # Array-specific operations
+    contains: CommentArgument
+    containsAll: [CommentArgument!]
+    containsAny: [CommentArgument!]
+    isNull: Boolean
+    # Logical operators
+    and: [CommentArrayArgument!]
+    or: [CommentArrayArgument!]
+    not: CommentArrayArgument
+    # Backward compatibility: direct property access (treated as 'contains')
+    id: IdArgument
+    content: StringArgument
+    updatedBy: StringArgument
+    updatedAt: DateArgument
 }
 
 input AttachmentArgument {
@@ -456,6 +511,21 @@ input AttachmentArgument {
     name: StringArgument
 }
 
+input AttachmentArrayArgument {
+    # Array-specific operations
+    contains: AttachmentArgument
+    containsAll: [AttachmentArgument!]
+    containsAny: [AttachmentArgument!]
+    isNull: Boolean
+    # Logical operators
+    and: [AttachmentArrayArgument!]
+    or: [AttachmentArrayArgument!]
+    not: AttachmentArrayArgument
+    # Backward compatibility: direct property access (treated as 'contains')
+    id: IdArgument
+    name: StringArgument
+}
+
 input UserTaskInstanceOrderBy {
     state: OrderBy
     actualOwner: OrderBy
diff --git 
a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java
 
b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java
index 27f053f9a..9febcd77f 100644
--- 
a/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java
+++ 
b/data-index/data-index-graphql/src/test/java/org/kie/kogito/index/graphql/query/GraphQLQueryMapperTest.java
@@ -24,6 +24,13 @@ import java.util.Map;
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.kie.kogito.persistence.api.query.AttributeFilter;
+import org.kie.kogito.persistence.api.query.FilterCondition;
+
+import graphql.Scalars;
+import graphql.schema.GraphQLInputObjectField;
+import graphql.schema.GraphQLInputObjectType;
+import graphql.schema.GraphQLList;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.kie.kogito.index.json.JsonUtils.jsonFilter;
@@ -115,4 +122,281 @@ public class GraphQLQueryMapperTest {
         
assertThat(mapper.mapJsonArgument("variables").apply(Map.of("workflowdata", 
Map.of("number", Map.of("isNull", false))))).containsExactly(
                 jsonFilter(notNull("variables.workflowdata.number")));
     }
+
+    // ========== Array Argument Tests ==========
+
+    @Test
+    void testArrayArgumentContains() {
+        // Create the input type and parser
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { contains: { name: { equal: "StartProcess" } } }
+        var filters = parser.apply(Map.of("nodes", Map.of("contains", 
Map.of("name", Map.of("equal", "StartProcess")))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getAttribute()).isEqualTo("nodes.name");
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.EQUAL);
+        assertThat(filter.getValue()).isEqualTo("StartProcess");
+    }
+
+    @Test
+    void testArrayArgumentContainsAll() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { containsAll: [{ name: { equal: "Start" } }, { name: 
{ equal: "End" } }] }
+        var filters = parser.apply(Map.of("nodes", Map.of("containsAll", 
List.of(
+                Map.of("name", Map.of("equal", "Start")),
+                Map.of("name", Map.of("equal", "End"))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.AND);
+
+        @SuppressWarnings("unchecked")
+        List<AttributeFilter<?>> andFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+        assertThat(andFilters).hasSize(2);
+        assertThat(andFilters.get(0).getAttribute()).isEqualTo("nodes.name");
+        assertThat(andFilters.get(0).getValue()).isEqualTo("Start");
+        assertThat(andFilters.get(1).getAttribute()).isEqualTo("nodes.name");
+        assertThat(andFilters.get(1).getValue()).isEqualTo("End");
+    }
+
+    @Test
+    void testArrayArgumentContainsAny() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { containsAny: [{ name: { equal: "Start" } }, { name: 
{ equal: "End" } }] }
+        var filters = parser.apply(Map.of("nodes", Map.of("containsAny", 
List.of(
+                Map.of("name", Map.of("equal", "Start")),
+                Map.of("name", Map.of("equal", "End"))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.OR);
+
+        @SuppressWarnings("unchecked")
+        List<AttributeFilter<?>> orFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+        assertThat(orFilters).hasSize(2);
+        assertThat(orFilters.get(0).getAttribute()).isEqualTo("nodes.name");
+        assertThat(orFilters.get(1).getAttribute()).isEqualTo("nodes.name");
+    }
+
+    @Test
+    void testArrayArgumentIsNull() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { isNull: true }
+        var filters = parser.apply(Map.of("nodes", Map.of("isNull", true)));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getAttribute()).isEqualTo("nodes");
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.IS_NULL);
+    }
+
+    @Test
+    void testArrayArgumentIsNotNull() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { isNull: false }
+        var filters = parser.apply(Map.of("nodes", Map.of("isNull", false)));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getAttribute()).isEqualTo("nodes");
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.NOT_NULL);
+    }
+
+    @Test
+    void testArrayArgumentWithNot() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { not: { contains: { name: { equal: "StartProcess" } } 
} }
+        var filters = parser.apply(Map.of("nodes", Map.of("not", 
Map.of("contains", Map.of("name", Map.of("equal", "StartProcess"))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.NOT);
+
+        AttributeFilter<?> notFilter = (AttributeFilter<?>) filter.getValue();
+        assertThat(notFilter.getAttribute()).isEqualTo("nodes.name");
+        assertThat(notFilter.getValue()).isEqualTo("StartProcess");
+    }
+
+    @Test
+    void testArrayArgumentWithNotContainsAll() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { not: { containsAll: [{ name: { equal: "Start" } }, { 
name: { equal: "End" } }] } }
+        var filters = parser.apply(Map.of("nodes", Map.of("not", 
Map.of("containsAll", List.of(
+                Map.of("name", Map.of("equal", "Start")),
+                Map.of("name", Map.of("equal", "End")))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.NOT);
+
+        AttributeFilter<?> notFilter = (AttributeFilter<?>) filter.getValue();
+        assertThat(notFilter.getCondition()).isEqualTo(FilterCondition.AND);
+    }
+
+    @Test
+    void testArrayArgumentBackwardCompatibility() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { name: { equal: "StartProcess" } }
+        // Should work like 'contains' for backward compatibility
+        var filters = parser.apply(Map.of("nodes", Map.of("name", 
Map.of("equal", "StartProcess"))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getAttribute()).isEqualTo("nodes.name");
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.EQUAL);
+        assertThat(filter.getValue()).isEqualTo("StartProcess");
+    }
+
+    @Test
+    void testArrayArgumentWithAndOperator() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { and: [{ contains: { name: { equal: "Start" } } }, { 
contains: { type: { equal: "HumanTask" } } }] }
+        var filters = parser.apply(Map.of("nodes", Map.of("and", List.of(
+                Map.of("contains", Map.of("name", Map.of("equal", "Start"))),
+                Map.of("contains", Map.of("type", Map.of("equal", 
"HumanTask")))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.AND);
+
+        @SuppressWarnings("unchecked")
+        List<AttributeFilter<?>> andFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+        assertThat(andFilters).hasSize(2);
+        assertThat(andFilters.get(0).getAttribute()).isEqualTo("nodes.name");
+        assertThat(andFilters.get(1).getAttribute()).isEqualTo("nodes.type");
+    }
+
+    @Test
+    void testArrayArgumentWithOrOperator() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { or: [{ contains: { name: { equal: "Start" } } }, { 
contains: { name: { equal: "End" } } }] }
+        var filters = parser.apply(Map.of("nodes", Map.of("or", List.of(
+                Map.of("contains", Map.of("name", Map.of("equal", "Start"))),
+                Map.of("contains", Map.of("name", Map.of("equal", "End")))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.OR);
+
+        @SuppressWarnings("unchecked")
+        List<AttributeFilter<?>> orFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+        assertThat(orFilters).hasSize(2);
+        assertThat(orFilters.get(0).getAttribute()).isEqualTo("nodes.name");
+        assertThat(orFilters.get(1).getAttribute()).isEqualTo("nodes.name");
+    }
+
+    @Test
+    void testArrayArgumentComplexQuery() {
+        GraphQLInputObjectType inputType = 
createProcessInstanceArgumentWithNodes();
+        var parser = mapper.apply(inputType);
+
+        // Test: nodes: { and: [{ contains: { name: { equal: "HumanTask" } } 
}, { not: { contains: { type: { equal: "EndEvent" } } } }] }
+        var filters = parser.apply(Map.of("nodes", Map.of("and", List.of(
+                Map.of("contains", Map.of("name", Map.of("equal", 
"HumanTask"))),
+                Map.of("not", Map.of("contains", Map.of("type", 
Map.of("equal", "EndEvent"))))))));
+
+        assertThat(filters).hasSize(1);
+        AttributeFilter<?> filter = filters.get(0);
+        assertThat(filter.getCondition()).isEqualTo(FilterCondition.AND);
+
+        @SuppressWarnings("unchecked")
+        List<AttributeFilter<?>> andFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+        assertThat(andFilters).hasSize(2);
+        assertThat(andFilters.get(0).getAttribute()).isEqualTo("nodes.name");
+        
assertThat(andFilters.get(1).getCondition()).isEqualTo(FilterCondition.NOT);
+    }
+
+    // Helper method to create ProcessInstanceArgument with 
NodeInstanceArrayArgument
+    private GraphQLInputObjectType createProcessInstanceArgumentWithNodes() {
+        // Create StringArgument type
+        GraphQLInputObjectType stringArgument = 
GraphQLInputObjectType.newInputObject()
+                .name("StringArgument")
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("equal")
+                        .type(Scalars.GraphQLString)
+                        .build())
+                .build();
+
+        // Create NodeInstanceArgument type
+        GraphQLInputObjectType nodeInstanceArgument = 
GraphQLInputObjectType.newInputObject()
+                .name("NodeInstanceArgument")
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("name")
+                        .type(stringArgument)
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("type")
+                        .type(stringArgument)
+                        .build())
+                .build();
+
+        // Create NodeInstanceArrayArgument type (note the "ArrayArgument" 
suffix)
+        GraphQLInputObjectType nodeInstanceArrayArgument = 
GraphQLInputObjectType.newInputObject()
+                .name("NodeInstanceArrayArgument")
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("contains")
+                        .type(nodeInstanceArgument)
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("containsAll")
+                        .type(new GraphQLList(nodeInstanceArgument))
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("containsAny")
+                        .type(new GraphQLList(nodeInstanceArgument))
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("isNull")
+                        .type(Scalars.GraphQLBoolean)
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("and")
+                        .type(new 
GraphQLList(GraphQLInputObjectType.newInputObject()
+                                .name("NodeInstanceArrayArgumentRecursive")
+                                .build()))
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("or")
+                        .type(new 
GraphQLList(GraphQLInputObjectType.newInputObject()
+                                .name("NodeInstanceArrayArgumentRecursive2")
+                                .build()))
+                        .build())
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("not")
+                        .type(GraphQLInputObjectType.newInputObject()
+                                .name("NodeInstanceArrayArgumentRecursive3")
+                                .build())
+                        .build())
+                .build();
+
+        // Create ProcessInstanceArgument type with nodes field
+        return GraphQLInputObjectType.newInputObject()
+                .name("ProcessInstanceArgument")
+                .field(GraphQLInputObjectField.newInputObjectField()
+                        .name("nodes")
+                        .type(nodeInstanceArrayArgument)
+                        .build())
+                .build();
+    }
 }
diff --git 
a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java
 
b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java
index ffbb87fc6..9b0cdf5ee 100644
--- 
a/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java
+++ 
b/data-index/data-index-storage/data-index-storage-api/src/test/java/org/kie/kogito/index/test/TestUtils.java
@@ -372,4 +372,32 @@ public class TestUtils {
 
         return task;
     }
+
+    public static ProcessInstanceStateDataEvent getProcessCloudEvent(String 
processId, String processInstanceId, ProcessInstanceState status, String 
rootProcessInstanceId, String rootProcessId,
+            String parentProcessInstanceId, String identity) {
+
+        int eventType = status.equals(ProcessInstanceState.COMPLETED) ? 
ProcessInstanceStateEventBody.EVENT_TYPE_ENDED : 
ProcessInstanceStateEventBody.EVENT_TYPE_STARTED;
+        ProcessInstanceStateEventBody body = 
ProcessInstanceStateEventBody.create()
+                .processInstanceId(processInstanceId)
+                .parentInstanceId(parentProcessInstanceId)
+                .rootProcessInstanceId(rootProcessInstanceId)
+                .rootProcessId(rootProcessId)
+                .processId(processId)
+                .eventDate(new Date())
+                .state(status.ordinal())
+                .businessKey(processInstanceId)
+                .roles("admin")
+                .eventUser(identity)
+                .eventType(eventType)
+                .build();
+
+        ProcessInstanceStateDataEvent event = new 
ProcessInstanceStateDataEvent();
+        event.setKogitoProcessId(processId);
+        event.setKogitoRootProcessId(rootProcessId);
+        event.setKogitoRootProcessInstanceId(rootProcessInstanceId);
+        event.setKogitoProcessInstanceId(processInstanceId);
+        event.setData(body);
+
+        return event;
+    }
 }
diff --git 
a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java
 
b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java
index 66f608ab0..29f34644f 100644
--- 
a/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java
+++ 
b/data-index/data-index-storage/data-index-storage-jpa-common/src/main/java/org/kie/kogito/index/jpa/storage/JPAQuery.java
@@ -18,6 +18,7 @@
  */
 package org.kie.kogito.index.jpa.storage;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Optional;
@@ -26,17 +27,20 @@ import java.util.function.Function;
 import org.kie.kogito.index.jpa.model.AbstractEntity;
 import org.kie.kogito.persistence.api.query.AttributeFilter;
 import org.kie.kogito.persistence.api.query.AttributeSort;
+import org.kie.kogito.persistence.api.query.FilterCondition;
 import org.kie.kogito.persistence.api.query.Query;
 import org.kie.kogito.persistence.api.query.SortDirection;
 
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.criteria.CriteriaBuilder;
 import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Expression;
 import jakarta.persistence.criteria.Join;
 import jakarta.persistence.criteria.Order;
 import jakarta.persistence.criteria.Path;
 import jakarta.persistence.criteria.Predicate;
 import jakarta.persistence.criteria.Root;
+import jakarta.persistence.criteria.Subquery;
 import jakarta.persistence.metamodel.Attribute;
 
 import static java.util.stream.Collectors.toList;
@@ -92,6 +96,8 @@ public class JPAQuery<E extends AbstractEntity, T> implements 
Query<T> {
         CriteriaBuilder builder = em.getCriteriaBuilder();
         CriteriaQuery<E> criteriaQuery = builder.createQuery(entityClass);
         Root<E> root = criteriaQuery.from(entityClass);
+
+        criteriaQuery.select(root);
         addWhere(builder, criteriaQuery, root);
         if (sortBy != null && !sortBy.isEmpty()) {
             List<Order> orderBy = sortBy.stream().map(f -> {
@@ -110,29 +116,52 @@ public class JPAQuery<E extends AbstractEntity, T> 
implements Query<T> {
         return (List<T>) 
query.getResultList().stream().map(mapper).collect(toList());
     }
 
-    protected Function<AttributeFilter<?>, Predicate> 
filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
+    /**
+     * Creates a function that converts an AttributeFilter to a JPA Predicate.
+     * 
+     * @param root the query root
+     * @param builder the criteria builder
+     * @param criteriaQuery the criteria query
+     * @return a function that converts filters to predicates
+     */
+    protected Function<AttributeFilter<?>, Predicate> 
filterPredicateFunction(Root<E> root, CriteriaBuilder builder, CriteriaQuery<?> 
criteriaQuery) {
         return filter -> jsonPredicateBuilder.filter(b -> 
filter.isJson()).map(b -> b.buildPredicate(filter, root, builder))
-                .orElseGet(() -> buildPredicateFunction(filter, root, 
builder));
+                .orElseGet(() -> buildPredicateFunction(filter, root, builder, 
criteriaQuery));
     }
 
-    protected final Predicate buildPredicateFunction(AttributeFilter filter, 
Root<E> root, CriteriaBuilder builder) {
+    protected final Predicate buildPredicateFunction(AttributeFilter filter, 
Root<E> root, CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery) {
         switch (filter.getCondition()) {
             case CONTAINS:
-                return builder.isMember(filter.getValue(), 
getAttributePath(root, filter.getAttribute()));
+                if (isCollectionAttribute(filter.getAttribute())) {
+                    return buildCollectionPredicate(filter, root, builder, 
criteriaQuery, false);
+                } else {
+                    return builder.isMember(filter.getValue(), 
getAttributePath(root, filter.getAttribute()));
+                }
             case CONTAINS_ALL:
-                List<Predicate> predicatesAll = (List<Predicate>) ((List) 
filter.getValue()).stream()
-                        .map(o -> builder.isMember(o, getAttributePath(root, 
filter.getAttribute()))).collect(toList());
-                return builder.and(predicatesAll.toArray(new Predicate[] {}));
+                if (isCollectionAttribute(filter.getAttribute())) {
+                    return buildCollectionPredicate(filter, root, builder, 
criteriaQuery, false);
+                } else {
+                    List<Predicate> predicatesAll = (List<Predicate>) ((List) 
filter.getValue()).stream()
+                            .map(o -> builder.isMember(o, 
getAttributePath(root, filter.getAttribute()))).collect(toList());
+                    return builder.and(predicatesAll.toArray(new Predicate[] 
{}));
+                }
             case CONTAINS_ANY:
-                List<Predicate> predicatesAny = (List<Predicate>) ((List) 
filter.getValue()).stream()
-                        .map(o -> builder.isMember(o, getAttributePath(root, 
filter.getAttribute()))).collect(toList());
-                return builder.or(predicatesAny.toArray(new Predicate[] {}));
+                if (isCollectionAttribute(filter.getAttribute())) {
+                    return buildCollectionPredicate(filter, root, builder, 
criteriaQuery, false);
+                } else {
+                    List<Predicate> predicatesAny = (List<Predicate>) ((List) 
filter.getValue()).stream()
+                            .map(o -> builder.isMember(o, 
getAttributePath(root, filter.getAttribute()))).collect(toList());
+                    return builder.or(predicatesAny.toArray(new Predicate[] 
{}));
+                }
             case IN:
                 return getAttributePath(root, 
filter.getAttribute()).in((Collection<?>) filter.getValue());
             case LIKE:
                 return builder.like(getAttributePath(root, 
filter.getAttribute()),
                         filter.getValue().toString().replaceAll("\\*", "%"));
             case EQUAL:
+                if (filter.getAttribute() != null && 
isCollectionAttribute(filter.getAttribute())) {
+                    return buildCollectionPredicate(filter, root, builder, 
criteriaQuery, false);
+                }
                 return builder.equal(getAttributePath(root, 
filter.getAttribute()), filter.getValue());
             case IS_NULL:
                 Path pathNull = getAttributePath(root, filter.getAttribute());
@@ -156,17 +185,135 @@ public class JPAQuery<E extends AbstractEntity, T> 
implements Query<T> {
                 return builder
                         .lessThanOrEqualTo(getAttributePath(root, 
filter.getAttribute()), (Comparable) filter.getValue());
             case OR:
-                return builder.or(getRecursivePredicate(filter, root, 
builder).toArray(new Predicate[] {}));
+                return buildGroupedPredicate(filter, root, builder, 
criteriaQuery, false, false);
             case AND:
-                return builder.and(getRecursivePredicate(filter, root, 
builder).toArray(new Predicate[] {}));
+                return buildGroupedPredicate(filter, root, builder, 
criteriaQuery, true, false);
             case NOT:
-                return builder.not(filterPredicateFunction(root, 
builder).apply((AttributeFilter<?>) filter.getValue()));
+                AttributeFilter<?> innerFilter = (AttributeFilter<?>) 
filter.getValue();
+
+                // Handle NOT with AND/OR: Apply De Morgan's Law by negating 
and flipping the operator
+                if (innerFilter.getCondition() == FilterCondition.AND || 
innerFilter.getCondition() == FilterCondition.OR) {
+                    // NOT (A AND B) = NOT A OR NOT B, NOT (A OR B) = NOT A 
AND NOT B
+                    boolean flipToAnd = innerFilter.getCondition() == 
FilterCondition.OR;
+                    return buildGroupedPredicate(innerFilter, root, builder, 
criteriaQuery, flipToAnd, true);
+                }
+
+                if (innerFilter.getAttribute() != null && 
isCollectionAttribute(innerFilter.getAttribute()) &&
+                        innerFilter.getCondition().isCollectionOperation()) {
+                    return buildCollectionPredicate(innerFilter, root, 
builder, criteriaQuery, true);
+                }
+
+                if (innerFilter.getAttribute() != null && 
isCollectionAttribute(innerFilter.getAttribute())) {
+                    return buildNegatedCollectionPredicate(innerFilter, root, 
builder, criteriaQuery);
+                }
+
+                return builder.not(filterPredicateFunction(root, builder, 
criteriaQuery).apply(innerFilter));
             default:
                 return null;
         }
 
     }
 
+    /**
+     * Builds predicates for AND/OR, grouping CONTAINS filters on same 
collection into single EXISTS.
+     * 
+     * @param isNegated if true, negates each predicate (for NOT operations 
with De Morgan's Law)
+     */
+    private Predicate buildGroupedPredicate(AttributeFilter<?> filter, Root<E> 
root,
+            CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean 
isAnd, boolean isNegated) {
+
+        List<AttributeFilter<?>> nestedFilters = (List<AttributeFilter<?>>) 
filter.getValue();
+
+        java.util.Map<String, List<AttributeFilter<?>>> groups = new 
java.util.HashMap<>();
+        List<Predicate> otherPredicates = new ArrayList<>();
+
+        for (AttributeFilter<?> f : nestedFilters) {
+            if (f.getAttribute() != null && 
isCollectionAttribute(f.getAttribute()) &&
+                    f.getCondition().isCollectionOperation()) {
+                String collection = f.getAttribute().split("\\.")[0];
+                groups.computeIfAbsent(collection, k -> new 
ArrayList<>()).add(f);
+            } else {
+                Predicate pred = filterPredicateFunction(root, builder, 
criteriaQuery).apply(f);
+                otherPredicates.add(isNegated ? builder.not(pred) : pred);
+            }
+        }
+
+        List<Predicate> allPredicates = new ArrayList<>(otherPredicates);
+        for (List<AttributeFilter<?>> groupFilters : groups.values()) {
+            
allPredicates.add(buildMultiFilterCollectionPredicate(groupFilters, root, 
builder, criteriaQuery, isAnd, isNegated));
+        }
+
+        return isAnd ? builder.and(allPredicates.toArray(new Predicate[0]))
+                : builder.or(allPredicates.toArray(new Predicate[0]));
+    }
+
+    /**
+     * Builds single EXISTS with HAVING for multiple CONTAINS filters on same 
collection.
+     * Example: [{ comments.id: "A" }, { comments.status: "ACTIVE" }]
+     * → EXISTS(... HAVING SUM(id='A')>0 AND SUM(status='ACTIVE')>0)
+     * 
+     * @param isNegated if true, uses = 0 instead of > 0 and applies De 
Morgan's Law
+     */
+    private Predicate 
buildMultiFilterCollectionPredicate(List<AttributeFilter<?>> filters, Root<E> 
root,
+            CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean 
combineWithAnd, boolean isNegated) {
+
+        String collectionName = filters.get(0).getAttribute().split("\\.")[0];
+
+        Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
+        Root<E> subRoot = subquery.from(entityClass);
+        Join<?, ?> collectionJoin = subRoot.join(collectionName);
+
+        List<Predicate> havingConditions = new ArrayList<>();
+
+        for (AttributeFilter<?> filter : filters) {
+            String property = filter.getAttribute().split("\\.")[1];
+            List<Object> values = filter.getCondition().expectsSingleValue()
+                    ? List.of(filter.getValue())
+                    : (List<Object>) filter.getValue();
+            boolean filterUseAnd = 
filter.getCondition().combineValuesWithAnd();
+
+            List<Predicate> valuePreds = new ArrayList<>();
+            for (Object value : values) {
+                Expression<Integer> caseExpr = builder.sum(
+                        builder.<Integer> selectCase()
+                                
.when(builder.equal(collectionJoin.get(property), value), 1)
+                                .otherwise(0));
+                // Apply negation: > 0 becomes = 0
+                valuePreds.add(isNegated ? builder.equal(caseExpr, 0) : 
builder.greaterThan(caseExpr, 0));
+            }
+            // Apply De Morgan's Law via XOR for value combination
+            boolean useAnd = filterUseAnd ^ isNegated;
+            havingConditions.add(useAnd ? builder.and(valuePreds.toArray(new 
Predicate[0]))
+                    : builder.or(valuePreds.toArray(new Predicate[0])));
+        }
+
+        Predicate havingPredicate = combineWithAnd
+                ? builder.and(havingConditions.toArray(new Predicate[0]))
+                : builder.or(havingConditions.toArray(new Predicate[0]));
+
+        subquery.select(builder.literal(1))
+                .where(builder.equal(subRoot.get("id"), root.get("id")))
+                .groupBy(subRoot.get("id"))
+                .having(havingPredicate);
+
+        return builder.exists(subquery);
+    }
+
+    /**
+     * Builds negated collection predicate using EXISTS with HAVING = 0.
+     * Uses EXISTS with inverted HAVING clause instead of NOT EXISTS for 
better optimization.
+     *
+     * Examples:
+     * - NOT contains: EXISTS(...HAVING SUM(CASE WHEN id='A' THEN 1 ELSE 0 
END) = 0)
+     * - NOT containsAll: EXISTS(...HAVING SUM(id='A')=0 OR SUM(id='B')=0)
+     * - NOT containsAny: EXISTS(...HAVING SUM(id='A')=0 AND SUM(id='B')=0)
+     */
+    private Predicate buildNegatedCollectionPredicate(AttributeFilter<?> 
filter, Root<E> root,
+            CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery) {
+
+        return buildCollectionPredicate(filter, root, builder, criteriaQuery, 
true);
+    }
+
     private Path getAttributePath(Root<E> root, String attribute) {
         String[] split = attribute.split("\\.");
         if (split.length == 1) {
@@ -186,10 +333,23 @@ public class JPAQuery<E extends AbstractEntity, T> 
implements Query<T> {
                 .anyMatch(pluralAttribute -> 
pluralAttribute.equals(attribute));
     }
 
-    private List<Predicate> getRecursivePredicate(AttributeFilter<?> filter, 
Root<E> root, CriteriaBuilder builder) {
+    /**
+     * Checks if the attribute is a collection attribute (e.g., "nodes.name" 
starts with "nodes").
+     * This helps detect when NOT operations need special subquery handling.
+     */
+    private boolean isCollectionAttribute(final String attribute) {
+        if (attribute == null || !attribute.contains(".")) {
+            return false;
+        }
+        // Extract the first part (e.g., "nodes" from "nodes.name")
+        String firstPart = attribute.split("\\.")[0];
+        return isPluralAttribute(firstPart);
+    }
+
+    private List<Predicate> getRecursivePredicate(AttributeFilter<?> filter, 
Root<E> root, CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery) {
         return ((List<AttributeFilter<?>>) filter.getValue())
                 .stream()
-                .map(filterPredicateFunction(root, builder))
+                .map(filterPredicateFunction(root, builder, criteriaQuery))
                 .collect(toList());
     }
 
@@ -205,7 +365,62 @@ public class JPAQuery<E extends AbstractEntity, T> 
implements Query<T> {
 
     private <V> void addWhere(CriteriaBuilder builder, CriteriaQuery<V> 
criteriaQuery, Root<E> root) {
         if (filters != null && !filters.isEmpty()) {
-            
criteriaQuery.where(filters.stream().map(filterPredicateFunction(root, 
builder)).toArray(Predicate[]::new));
+            
criteriaQuery.where(filters.stream().map(filterPredicateFunction(root, builder, 
criteriaQuery)).toArray(Predicate[]::new));
         }
     }
+
+    /**
+     * Unified method to build collection predicates using EXISTS + HAVING 
clause.
+     * Handles CONTAINS, CONTAINS_ALL, CONTAINS_ANY, and EQUAL operations.
+     */
+    private Predicate buildCollectionPredicate(AttributeFilter<?> filter, 
Root<E> root,
+            CriteriaBuilder builder, CriteriaQuery<?> criteriaQuery, boolean 
isNegated) {
+
+        // Parse attribute: "comments.id" -> collection="comments", 
property="id"
+        String[] parts = filter.getAttribute().split("\\.");
+        String collectionName = parts[0];
+        String propertyPath = parts[1];
+
+        // Get values: single value for CONTAINS/EQUAL, list for 
CONTAINS_ALL/CONTAINS_ANY
+        List<Object> values = filter.getCondition().expectsSingleValue()
+                ? List.of(filter.getValue())
+                : (List<Object>) filter.getValue();
+
+        // Determine combination logic: AND for CONTAINS/CONTAINS_ALL/EQUAL, 
OR for CONTAINS_ANY
+        boolean combineUsingAnd = filter.getCondition().combineValuesWithAnd();
+
+        // Create subquery
+        Subquery<Integer> subquery = criteriaQuery.subquery(Integer.class);
+        Root<E> subRoot = subquery.from(entityClass);
+        Join<?, ?> collectionJoin = subRoot.join(collectionName);
+
+        // Build CASE expressions: SUM(CASE WHEN c.id = 'A' THEN 1 ELSE 0 END)
+        List<Predicate> havingConditions = new ArrayList<>();
+        for (Object value : values) {
+            Expression<Integer> caseExpr = builder.sum(
+                    builder.<Integer> selectCase()
+                            
.when(builder.equal(collectionJoin.get(propertyPath), value), 1)
+                            .otherwise(0));
+
+            // Condition: > 0 for positive, = 0 for negative
+            Predicate condition = isNegated
+                    ? builder.equal(caseExpr, 0)
+                    : builder.greaterThan(caseExpr, 0);
+            havingConditions.add(condition);
+        }
+
+        // Combine with AND/OR (apply De Morgan's Law via XOR)
+        boolean useAnd = combineUsingAnd ^ isNegated;
+        Predicate havingPredicate = useAnd
+                ? builder.and(havingConditions.toArray(new Predicate[0]))
+                : builder.or(havingConditions.toArray(new Predicate[0]));
+
+        // Build complete subquery
+        subquery.select(builder.literal(1))
+                .where(builder.equal(subRoot.get("id"), root.get("id")))
+                .groupBy(subRoot.get("id"))
+                .having(havingPredicate);
+
+        return builder.exists(subquery);
+    }
 }
diff --git 
a/data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/query/AbstractProcessInstanceEntityQueryIT.java
 
b/data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/query/AbstractProcessInstanceEntityQueryIT.java
index b61d47fb5..f7aa7736a 100644
--- 
a/data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/query/AbstractProcessInstanceEntityQueryIT.java
+++ 
b/data-index/data-index-storage/data-index-storage-jpa-common/src/test/java/org/kie/kogito/index/jpa/query/AbstractProcessInstanceEntityQueryIT.java
@@ -22,12 +22,18 @@ import java.util.List;
 import java.util.UUID;
 
 import org.junit.jupiter.api.Test;
+import org.kie.kogito.event.process.ProcessInstanceNodeDataEvent;
+import org.kie.kogito.event.process.ProcessInstanceNodeEventBody;
 import org.kie.kogito.event.process.ProcessInstanceStateDataEvent;
 import org.kie.kogito.index.jpa.storage.ProcessInstanceEntityStorage;
+import org.kie.kogito.index.model.ProcessInstance;
 import org.kie.kogito.index.test.TestUtils;
 import org.kie.kogito.index.test.query.AbstractProcessInstanceQueryIT;
 
+import jakarta.transaction.Transactional;
+
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.kie.kogito.index.model.ProcessInstanceState.ACTIVE;
 import static org.kie.kogito.index.model.ProcessInstanceState.COMPLETED;
 import static org.kie.kogito.persistence.api.query.QueryFilterFactory.*;
 
@@ -51,4 +57,364 @@ public abstract class AbstractProcessInstanceEntityQueryIT 
extends AbstractProce
         assertThat(storage.query().count()).isNotZero();
         assertThat(storage.query().filter(List.of(in("state", 
List.of(34)))).count()).isZero();
     }
+
+    // ========================================
+    // NOT Operator Tests for Collection Operations
+    // ========================================
+
+    /**
+     * Test: NOT with CONTAINS on collection attribute
+     * Verifies entity-level negation using NOT EXISTS subquery
+     */
+    @Test
+    @Transactional
+    void testNotContainsOnCollectionAttribute() {
+        // Setup: Create process instances with different nodes
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+        String pi3 = UUID.randomUUID().toString();
+
+        // PI1: Has nodes ["Start", "HR Interview", "End"]
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "Start", "node1");
+        addNodeToProcessInstance(pi1, "HR Interview", "node2");
+        addNodeToProcessInstance(pi1, "End", "node3");
+
+        // PI2: Has nodes ["Start", "Technical Interview", "End"]
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Start", "node1");
+        addNodeToProcessInstance(pi2, "Technical Interview", "node4");
+        addNodeToProcessInstance(pi2, "End", "node3");
+
+        // PI3: Has nodes ["Start", "End"]
+        ProcessInstanceStateDataEvent event3 = 
TestUtils.getProcessCloudEvent(processId, pi3, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event3);
+        addNodeToProcessInstance(pi3, "Start", "node1");
+        addNodeToProcessInstance(pi3, "End", "node3");
+
+        // Test: Find processes that DON'T have "HR Interview" node
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(contains("nodes.name", "HR Interview"))))
+                .execute();
+
+        // Verify: Should return PI2 and PI3 (not PI1)
+        assertThat(result).hasSize(2);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactlyInAnyOrder(pi2,
 pi3);
+        
assertThat(result).extracting(ProcessInstance::getId).doesNotContain(pi1);
+    }
+
+    /**
+     * Test: NOT with CONTAINS_ALL on collection
+     * Verifies De Morgan's Law: NOT (A AND B) = (NOT A) OR (NOT B)
+     */
+    @Test
+    @Transactional
+    void testNotContainsAllOnCollection() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+        String pi3 = UUID.randomUUID().toString();
+
+        // PI1: Has all required nodes ["Start", "HR Interview", "End"]
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "Start", "node1");
+        addNodeToProcessInstance(pi1, "HR Interview", "node2");
+        addNodeToProcessInstance(pi1, "End", "node3");
+
+        // PI2: Missing "HR Interview" - has ["Start", "End"]
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Start", "node1");
+        addNodeToProcessInstance(pi2, "End", "node3");
+
+        // PI3: Missing "End" - has ["Start", "HR Interview"]
+        ProcessInstanceStateDataEvent event3 = 
TestUtils.getProcessCloudEvent(processId, pi3, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event3);
+        addNodeToProcessInstance(pi3, "Start", "node1");
+        addNodeToProcessInstance(pi3, "HR Interview", "node2");
+
+        // Test: Find processes that DON'T have ALL of ["HR Interview", "End"]
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(containsAll("nodes.name", List.of("HR Interview", 
"End")))))
+                .execute();
+
+        // Verify: Should return PI2 and PI3 (missing at least one node)
+        assertThat(result).hasSize(2);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactlyInAnyOrder(pi2,
 pi3);
+        
assertThat(result).extracting(ProcessInstance::getId).doesNotContain(pi1);
+    }
+
+    /**
+     * Test: NOT with CONTAINS_ANY on collection
+     * Verifies De Morgan's Law: NOT (A OR B) = (NOT A) AND (NOT B)
+     */
+    @Test
+    @Transactional
+    void testNotContainsAnyOnCollection() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+        String pi3 = UUID.randomUUID().toString();
+
+        // PI1: Has "HR Interview"
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "Start", "node1");
+        addNodeToProcessInstance(pi1, "HR Interview", "node2");
+
+        // PI2: Has "Technical Interview"
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Start", "node1");
+        addNodeToProcessInstance(pi2, "Technical Interview", "node3");
+
+        // PI3: Has neither
+        ProcessInstanceStateDataEvent event3 = 
TestUtils.getProcessCloudEvent(processId, pi3, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event3);
+        addNodeToProcessInstance(pi3, "Start", "node1");
+        addNodeToProcessInstance(pi3, "End", "node4");
+
+        // Test: Find processes that DON'T have ANY of ["HR Interview", 
"Technical Interview"]
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(containsAny("nodes.name", List.of("HR Interview", 
"Technical Interview")))))
+                .execute();
+
+        // Verify: Should return only PI3 (has neither)
+        assertThat(result).hasSize(1);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactly(pi3);
+    }
+
+    /**
+     * Test: NOT with AND containing collection operations
+     * Verifies De Morgan's Law: NOT (A AND B) = (NOT A) OR (NOT B)
+     */
+    @Test
+    @Transactional
+    void testNotWithAndContainingCollectionOperations() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+        String pi3 = UUID.randomUUID().toString();
+
+        // PI1: Has both "HR Interview" and "Technical Interview"
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "HR Interview", "node1");
+        addNodeToProcessInstance(pi1, "Technical Interview", "node2");
+
+        // PI2: Has only "HR Interview"
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "HR Interview", "node1");
+
+        // PI3: Has only "Technical Interview"
+        ProcessInstanceStateDataEvent event3 = 
TestUtils.getProcessCloudEvent(processId, pi3, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event3);
+        addNodeToProcessInstance(pi3, "Technical Interview", "node2");
+
+        // Test: Find processes that DON'T have (HR Interview AND Technical 
Interview)
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(and(List.of(
+                                contains("nodes.name", "HR Interview"),
+                                contains("nodes.name", "Technical 
Interview"))))))
+                .execute();
+
+        // Verify: Should return PI2 and PI3 (missing at least one)
+        assertThat(result).hasSize(2);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactlyInAnyOrder(pi2,
 pi3);
+    }
+
+    /**
+     * Test: NOT with OR containing collection operations
+     * Verifies De Morgan's Law: NOT (A OR B) = (NOT A) AND (NOT B)
+     */
+    @Test
+    @Transactional
+    void testNotWithOrContainingCollectionOperations() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+        String pi3 = UUID.randomUUID().toString();
+
+        // PI1: Has "HR Interview"
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "HR Interview", "node1");
+
+        // PI2: Has "Technical Interview"
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Technical Interview", "node2");
+
+        // PI3: Has neither
+        ProcessInstanceStateDataEvent event3 = 
TestUtils.getProcessCloudEvent(processId, pi3, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event3);
+        addNodeToProcessInstance(pi3, "Start", "node3");
+
+        // Test: Find processes that DON'T have (HR Interview OR Technical 
Interview)
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(or(List.of(
+                                contains("nodes.name", "HR Interview"),
+                                contains("nodes.name", "Technical 
Interview"))))))
+                .execute();
+
+        // Verify: Should return only PI3 (has neither)
+        assertThat(result).hasSize(1);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactly(pi3);
+    }
+
+    /**
+     * Test: NOT with simple field (non-collection)
+     * Verifies regular NOT is used for simple fields (no NOT EXISTS overhead)
+     */
+    @Test
+    @Transactional
+    void testNotWithSimpleField() {
+        String processId1 = "hiringProcess";
+        String processId2 = "onboardingProcess";
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+
+        // PI1: hiringProcess
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId1, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+
+        // PI2: onboardingProcess
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId2, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+
+        // Test: Find processes that are NOT "hiringProcess"
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        not(equalTo("processId", processId1))))
+                .execute();
+
+        // Verify: Should return PI2
+        assertThat(result).extracting(ProcessInstance::getId).contains(pi2);
+        
assertThat(result).extracting(ProcessInstance::getId).doesNotContain(pi1);
+    }
+
+    /**
+     * Test: NOT with mixed collection and simple field operations
+     * Verifies correct handling of mixed filter types
+     */
+    @Test
+    @Transactional
+    void testNotWithMixedCollectionAndSimpleFields() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+
+        // PI1: hiringProcess with "HR Interview" node
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "HR Interview", "node1");
+
+        // PI2: hiringProcess without "HR Interview" node
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Start", "node2");
+
+        // Test: Find processes that are NOT (processId = testProcess AND has 
HR Interview node)
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        not(and(List.of(
+                                equalTo("processId", processId),
+                                contains("nodes.name", "HR Interview"))))))
+                .execute();
+
+        // Verify: Should return PI2 and any other processes
+        assertThat(result).extracting(ProcessInstance::getId).contains(pi2);
+        
assertThat(result).extracting(ProcessInstance::getId).doesNotContain(pi1);
+    }
+
+    /**
+     * Test: Backward compatibility - existing queries still work
+     * Verifies no breaking changes to existing functionality
+     */
+    @Test
+    @Transactional
+    void testBackwardCompatibilityWithExistingQueries() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "HR Interview", "node1");
+
+        // Test: Old-style positive query still works
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        contains("nodes.name", "HR Interview")))
+                .execute();
+
+        // Verify: Should return PI1
+        assertThat(result).hasSize(1);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactly(pi1);
+    }
+
+    /**
+     * Test: Complex nested NOT operations
+     * Verifies recursive handling of deeply nested filters
+     */
+    @Test
+    @Transactional
+    void testComplexNestedNotOperations() {
+        String processId = "testProcess_" + UUID.randomUUID().toString();
+        String pi1 = UUID.randomUUID().toString();
+        String pi2 = UUID.randomUUID().toString();
+
+        // PI1: Has "HR Interview" and "Technical Interview"
+        ProcessInstanceStateDataEvent event1 = 
TestUtils.getProcessCloudEvent(processId, pi1, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event1);
+        addNodeToProcessInstance(pi1, "HR Interview", "node1");
+        addNodeToProcessInstance(pi1, "Technical Interview", "node2");
+
+        // PI2: Has only "Start"
+        ProcessInstanceStateDataEvent event2 = 
TestUtils.getProcessCloudEvent(processId, pi2, ACTIVE, null, null, null, 
"user1");
+        storage.indexState(event2);
+        addNodeToProcessInstance(pi2, "Start", "node3");
+
+        // Test: Complex nested NOT: NOT (NOT (has HR Interview) OR NOT (has 
Technical Interview))
+        // This should return processes that have BOTH interviews
+        List<ProcessInstance> result = storage.query()
+                .filter(List.of(
+                        equalTo("processId", processId),
+                        not(or(List.of(
+                                not(contains("nodes.name", "HR Interview")),
+                                not(contains("nodes.name", "Technical 
Interview")))))))
+                .execute();
+
+        // Verify: Should return only PI1 (has both)
+        assertThat(result).hasSize(1);
+        
assertThat(result).extracting(ProcessInstance::getId).containsExactly(pi1);
+    }
+
+    // Helper method to add nodes to process instances
+    private void addNodeToProcessInstance(String processInstanceId, String 
nodeName, String nodeId) {
+        ProcessInstanceNodeDataEvent nodeEvent = 
TestUtils.createProcessInstanceNodeDataEvent(
+                processInstanceId,
+                "testProcess",
+                nodeId,
+                UUID.randomUUID().toString(),
+                nodeName,
+                "HumanTaskNode",
+                ProcessInstanceNodeEventBody.EVENT_TYPE_ENTER);
+        storage.indexNode(nodeEvent);
+    }
 }
diff --git 
a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/FilterCondition.java
 
b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/FilterCondition.java
index 5cece3ed6..cbabe11a5 100644
--- 
a/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/FilterCondition.java
+++ 
b/persistence-commons/persistence-commons-api/src/main/java/org/kie/kogito/persistence/api/query/FilterCondition.java
@@ -55,4 +55,16 @@ public enum FilterCondition {
         }
         return null;
     }
+
+    public boolean isCollectionOperation() {
+        return this == CONTAINS || this == CONTAINS_ALL || this == 
CONTAINS_ANY || this == EQUAL;
+    }
+
+    public boolean expectsSingleValue() {
+        return this == CONTAINS || this == EQUAL;
+    }
+
+    public boolean combineValuesWithAnd() {
+        return this != CONTAINS_ANY;
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to