Skip to content

Commit

Permalink
[Fix #2158] Adding support for querying workflow variables
Browse files Browse the repository at this point in the history
  • Loading branch information
fjtirado committed Dec 10, 2024
1 parent c049aea commit 32271a1
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 57 deletions.
4 changes: 4 additions & 0 deletions data-index/data-index-graphql/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-vertx-graphql</artifactId>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<artifactId>graphql-java-extended-scalars</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-reactive-routes</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,46 @@ public GraphQLQueryParser apply(GraphQLInputObjectType type) {
case "KogitoMetadataArgument":
parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), GraphQLQueryParserRegistry.get().getParser("KogitoMetadataArgument")));
break;
case "JSON":
parser.mapAttribute(field.getName(), mapJsonArgument(field.getName()));
break;
default:
parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), new GraphQLQueryMapper().apply((GraphQLInputObjectType) field.getType())));
if (field.getType() instanceof GraphQLInputObjectType) {
parser.mapAttribute(field.getName(), mapSubEntityArgument(field.getName(), new GraphQLQueryMapper().apply((GraphQLInputObjectType) field.getType())));
}
}
}
});

return parser;
}

private Function<Object, Stream<AttributeFilter<?>>> mapJsonArgument(String attribute) {
return argument -> ((Map<String, Object>) argument).entrySet().stream().map(e -> mapJsonArgument(attribute, e.getKey(), e.getValue()));
}

private AttributeFilter<?> mapJsonArgument(String attribute, String key, Object value) {
StringBuilder sb = new StringBuilder(attribute);
FilterCondition condition = FilterCondition.fromLabel(key);
while (condition == null && value instanceof Map) {
sb.append('.').append(key);
Map.Entry<String, Object> entry = ((Map<String, Object>) value).entrySet().iterator().next();
key = entry.getKey();
value = entry.getValue();
condition = FilterCondition.fromLabel(key);
}
if (condition != null) {
AttributeFilter<?> filter;
switch (condition) {
case EQUAL:
default:
filter = equalTo(sb.toString(), value);
}
filter.setJson(true);
return filter;
}
return null;
}

private boolean isListOfType(GraphQLInputType source, String type) {
if (isList(source)) {
return ((GraphQLNamedType) unwrapNonNull(unwrapOne(source))).getName().equals(type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ input ProcessInstanceArgument {
id: IdArgument
processId: StringArgument
processName: StringArgument
variables: JSON
parentProcessInstanceId: IdArgument
rootProcessInstanceId: IdArgument
rootProcessId: StringArgument
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public GraphQLSchema createSchema() {
typeDefinitionRegistry.merge(loadSchemaDefinitionFile("domain.schema.graphqls"));

RuntimeWiring runtimeWiring = RuntimeWiring.newRuntimeWiring()
.scalar(ExtendedScalars.Json)
.type("Query", builder -> {
builder.dataFetcher("ProcessDefinitions", this::getProcessDefinitionsValues);
builder.dataFetcher("ProcessInstances", this::getProcessInstancesValues);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@

public class JPAQuery<K, E extends AbstractEntity, T> implements Query<T> {

private PanacheRepositoryBase<E, K> repository;
protected final PanacheRepositoryBase<E, K> repository;
private Integer limit;
private Integer offset;
private List<AttributeFilter<?>> filters;
private List<AttributeSort> sortBy;
private Class<E> entityClass;
private Function<E, T> mapper;
protected final Class<E> entityClass;
protected final Function<E, T> mapper;

public JPAQuery(PanacheRepositoryBase<E, K> repository, Function<E, T> mapper, Class<E> entityClass) {
this.repository = repository;
Expand Down Expand Up @@ -113,66 +113,69 @@ protected List<Predicate> getPredicates(CriteriaBuilder builder, Root<E> root) {
return filters.stream().map(filterPredicateFunction(root, builder)).collect(toList());
}

private Function<AttributeFilter<?>, Predicate> filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
return filter -> {
switch (filter.getCondition()) {
case CONTAINS:
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[] {}));
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[] {}));
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:
return builder.equal(getAttributePath(root, filter.getAttribute()), filter.getValue());
case IS_NULL:
Path pathNull = getAttributePath(root, filter.getAttribute());
return isPluralAttribute(filter.getAttribute()) ? builder.isEmpty(pathNull) : builder.isNull(pathNull);
case NOT_NULL:
Path pathNotNull = getAttributePath(root, filter.getAttribute());
return isPluralAttribute(filter.getAttribute()) ? builder.isNotEmpty(pathNotNull) : builder.isNotNull(pathNotNull);
case BETWEEN:
List<Object> value = (List<Object>) filter.getValue();
return builder
.between(getAttributePath(root, filter.getAttribute()), (Comparable) value.get(0),
(Comparable) value.get(1));
case GT:
return builder.greaterThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case GTE:
return builder.greaterThanOrEqualTo(getAttributePath(root, filter.getAttribute()),
(Comparable) filter.getValue());
case LT:
return builder.lessThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case LTE:
return builder
.lessThanOrEqualTo(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case OR:
return builder.or(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {}));
case AND:
return builder.and(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {}));
case NOT:
return builder.not(filterPredicateFunction(root, builder).apply((AttributeFilter<?>) filter.getValue()));
default:
return null;
}
};
protected Function<AttributeFilter<?>, Predicate> filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
return filter -> buildPredicateFunction(filter, root, builder);
}

protected final Predicate buildPredicateFunction(AttributeFilter filter, Root<E> root, CriteriaBuilder builder) {
switch (filter.getCondition()) {
case CONTAINS:
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[] {}));
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[] {}));
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:
return builder.equal(getAttributePath(root, filter.getAttribute()), filter.getValue());
case IS_NULL:
Path pathNull = getAttributePath(root, filter.getAttribute());
return isPluralAttribute(filter.getAttribute()) ? builder.isEmpty(pathNull) : builder.isNull(pathNull);
case NOT_NULL:
Path pathNotNull = getAttributePath(root, filter.getAttribute());
return isPluralAttribute(filter.getAttribute()) ? builder.isNotEmpty(pathNotNull) : builder.isNotNull(pathNotNull);
case BETWEEN:
List<Object> value = (List<Object>) filter.getValue();
return builder
.between(getAttributePath(root, filter.getAttribute()), (Comparable) value.get(0),
(Comparable) value.get(1));
case GT:
return builder.greaterThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case GTE:
return builder.greaterThanOrEqualTo(getAttributePath(root, filter.getAttribute()),
(Comparable) filter.getValue());
case LT:
return builder.lessThan(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case LTE:
return builder
.lessThanOrEqualTo(getAttributePath(root, filter.getAttribute()), (Comparable) filter.getValue());
case OR:
return builder.or(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {}));
case AND:
return builder.and(getRecursivePredicate(filter, root, builder).toArray(new Predicate[] {}));
case NOT:
return builder.not(filterPredicateFunction(root, builder).apply((AttributeFilter<?>) filter.getValue()));
default:
return null;
}

}

private Path getAttributePath(Root<E> root, String attribute) {
String[] split = attribute.split("\\.");
if (split.length == 1) {
return root.get(attribute);
}

Join join = root.join(split[0]);

for (int i = 1; i < split.length - 1; i++) {
join = join.join(split[i]);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
import org.kie.kogito.index.model.ProcessInstance;
import org.kie.kogito.index.storage.ProcessInstanceStorage;

import io.quarkus.arc.DefaultBean;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
Expand All @@ -57,6 +59,7 @@
import static org.kie.kogito.index.DateTimeUtils.toZonedDateTime;

@ApplicationScoped
@DefaultBean
public class ProcessInstanceEntityStorage extends AbstractJPAStorageFetcher<String, ProcessInstanceEntity, ProcessInstance> implements ProcessInstanceStorage {

protected ProcessInstanceEntityStorage() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.index.postgresql;

import java.util.function.Function;

import org.kie.kogito.index.jpa.model.AbstractEntity;
import org.kie.kogito.index.jpa.storage.JPAQuery;
import org.kie.kogito.persistence.api.query.AttributeFilter;

import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

public class PostgresqlJsonJPAQuery<K, E extends AbstractEntity, T> extends JPAQuery<K, E, T> {

public PostgresqlJsonJPAQuery(PanacheRepositoryBase<E, K> repository, Function<E, T> mapper, Class<E> entityClass) {
super(repository, mapper, entityClass);
}

protected Function<AttributeFilter<?>, Predicate> filterPredicateFunction(Root<E> root, CriteriaBuilder builder) {
return filter -> filter.isJson() ? PostgresqlJsonNavigator.buildPredicate(filter, root, builder) : buildPredicateFunction(filter, root, builder);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.index.postgresql;

import org.kie.kogito.persistence.api.query.AttributeFilter;

import jakarta.persistence.criteria.CriteriaBuilder;
import jakarta.persistence.criteria.Expression;
import jakarta.persistence.criteria.Predicate;
import jakarta.persistence.criteria.Root;

public class PostgresqlJsonNavigator {

private PostgresqlJsonNavigator() {
}

public static Predicate buildPredicate(AttributeFilter<?> filter, Root<?> root,
CriteriaBuilder builder) {
switch (filter.getCondition()) {
case EQUAL:
boolean isString = filter.getValue() instanceof String;
return builder.equal(buildPathExpression(builder, root, filter.getAttribute(), isString), buildObjectExpression(builder, filter.getValue(), isString));
}
throw new UnsupportedOperationException();
}

private static Expression<?> buildObjectExpression(CriteriaBuilder builder, Object value, boolean isString) {
return isString ? builder.literal(value) : builder.function("to_jsonb", Object.class, builder.literal(value));
}

private static Expression<?> buildPathExpression(CriteriaBuilder builder, Root<?> root, String attributeName, boolean isStr) {
String[] attributes = attributeName.split("\\.");
Expression<?>[] arguments = new Expression[attributes.length];
arguments[0] = root.get(attributes[0]);
for (int i = 1; i < attributes.length; i++) {
arguments[i] = builder.literal(attributes[i]);
}
return isStr ? builder.function("jsonb_extract_path_text", String.class, arguments) : builder.function("jsonb_extract_path", Object.class, arguments);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.kogito.index.postgresql;

import org.kie.kogito.index.jpa.mapper.ProcessInstanceEntityMapper;
import org.kie.kogito.index.jpa.model.ProcessInstanceEntityRepository;
import org.kie.kogito.index.jpa.storage.ProcessInstanceEntityStorage;
import org.kie.kogito.index.model.ProcessInstance;
import org.kie.kogito.persistence.api.query.Query;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

@ApplicationScoped
public class PostgresqlProcessInstanceEntityStorage extends ProcessInstanceEntityStorage {

@Inject
public PostgresqlProcessInstanceEntityStorage(ProcessInstanceEntityRepository repository, ProcessInstanceEntityMapper mapper) {
super(repository, mapper);
}

@Override
public Query<ProcessInstance> query() {
return new PostgresqlJsonJPAQuery<>(repository, mapToModel, entityClass);
}
}
Loading

0 comments on commit 32271a1

Please sign in to comment.