Add pagination and search by name capabilities to WorkflowsResource

Closes #44164

Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen 2025-11-10 10:54:31 -03:00 committed by Pedro Igor
parent 84a679224b
commit 7acf2ceccb
4 changed files with 76 additions and 2 deletions

View File

@ -9,6 +9,7 @@ import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.workflows.WorkflowRepresentation;
@ -35,6 +36,15 @@ public interface WorkflowsResource {
@Produces(MediaType.APPLICATION_JSON)
List<WorkflowRepresentation> list();
@GET
@Produces(MediaType.APPLICATION_JSON)
List<WorkflowRepresentation> list(
@QueryParam("search") String search,
@QueryParam("exact") Boolean exact,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults
);
@Path("{id}")
WorkflowResource workflow(@PathParam("id") String id);
}

View File

@ -17,10 +17,12 @@
package org.keycloak.models.workflow;
import java.util.Comparator;
import java.util.stream.Stream;
import org.keycloak.provider.Provider;
import org.keycloak.representations.workflows.WorkflowRepresentation;
import org.keycloak.utils.StringUtil;
public interface WorkflowProvider extends Provider {
@ -40,6 +42,17 @@ public interface WorkflowProvider extends Provider {
Stream<Workflow> getWorkflows();
default Stream<Workflow> getWorkflows(String search, Boolean exact, Integer first, Integer max) {
return getWorkflows().sorted(Comparator.comparing(Workflow::getName))
.filter(workflow -> {
if (StringUtil.isBlank(search)) {
return true;
}
return Boolean.TRUE.equals(exact) ? workflow.getName().equals(search) : workflow.getName().toLowerCase().contains(search.toLowerCase());
})
.skip(first).limit(max);
}
WorkflowRepresentation toRepresentation(Workflow workflow);
void updateWorkflow(Workflow workflow, WorkflowRepresentation rep);

View File

@ -5,14 +5,17 @@ import java.util.Optional;
import com.fasterxml.jackson.jakarta.rs.yaml.YAMLMediaTypes;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.models.KeycloakSession;
@ -79,9 +82,16 @@ public class WorkflowsResource {
@GET
@Produces({MediaType.APPLICATION_JSON, YAMLMediaTypes.APPLICATION_JACKSON_YAML})
public List<WorkflowRepresentation> list() {
public List<WorkflowRepresentation> list(
@Parameter(description = "A String representing the workflow name - either partial or exact") @QueryParam("search") String search,
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact,
@Parameter(description = "The position of the first result to be processed (pagination offset)") @QueryParam("first") @DefaultValue("0") Integer firstResult,
@Parameter(description = "The maximum number of results to be returned - defaults to 10") @QueryParam("max") @DefaultValue("10") Integer maxResults
) {
auth.realm().requireManageRealm();
return provider.getWorkflows().map(provider::toRepresentation).toList();
int first = Optional.ofNullable(firstResult).orElse(0);
int max = Optional.ofNullable(maxResults).orElse(10);
return provider.getWorkflows(search, exact, first, max).map(provider::toRepresentation).toList();
}
}

View File

@ -264,6 +264,47 @@ public class WorkflowManagementTest extends AbstractWorkflowTest {
}
@Test
public void testSearch() {
// create a few workflows with different names
String[] workflowNames = {"alpha-workflow", "beta-workflow", "gamma-workflow", "delta-workflow"};
for (String name : workflowNames) {
managedRealm.admin().workflows().create(WorkflowRepresentation.withName(name)
.onEvent(ResourceOperationType.USER_ADDED.toString())
.withSteps(
WorkflowStepRepresentation.create().of(NotifyUserStepProviderFactory.ID)
.after(Duration.ofDays(5))
.build()
).build()).close();
}
// use the API to search for workflows by name, both partial and exact matches
WorkflowsResource workflows = managedRealm.admin().workflows();
List<WorkflowRepresentation> representations = workflows.list("alpha", false, null, null);
assertThat(representations, Matchers.hasSize(1));
assertThat(representations.get(0).getName(), is("alpha-workflow"));
representations = workflows.list("workflow", false, null, null);
assertThat(representations, Matchers.hasSize(4));
representations = workflows.list("beta-workflow", true, null, null);
assertThat(representations, Matchers.hasSize(1));
assertThat(representations.get(0).getName(), is("beta-workflow"));
representations = workflows.list("nonexistent", false, null, null);
assertThat(representations, Matchers.hasSize(0));
// test pagination parameters
representations = workflows.list(null, null, 1, 2);
assertThat(representations, Matchers.hasSize(2));
// returned workflows should be ordered by name
assertThat(representations.get(0).getName(), is("beta-workflow"));
assertThat(representations.get(1).getName(), is("delta-workflow"));
representations = workflows.list("gamma", false, 0, 10);
assertThat(representations, Matchers.hasSize(1));
assertThat(representations.get(0).getName(), is("gamma-workflow"));
}
@Test
public void testWorkflowDoesNotFallThroughStepsInSingleRun() {
managedRealm.admin().workflows().create(WorkflowRepresentation.withName("myworkflow")