Skip to content

Commit

Permalink
Add POST version of /search
Browse files Browse the repository at this point in the history
This allows us to pass a body with customfields to filter by
  • Loading branch information
jnatten committed Jan 23, 2024
1 parent b2a76cb commit 3b2eb98
Show file tree
Hide file tree
Showing 8 changed files with 176 additions and 30 deletions.
55 changes: 53 additions & 2 deletions src/main/java/no/ndla/taxonomy/rest/v1/Nodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import no.ndla.taxonomy.repositories.NodeConnectionRepository;
import no.ndla.taxonomy.repositories.NodeRepository;
import no.ndla.taxonomy.rest.v1.commands.NodePostPut;
import no.ndla.taxonomy.rest.v1.commands.NodeSearchBody;
import no.ndla.taxonomy.service.*;
import no.ndla.taxonomy.service.dtos.*;
import org.springframework.data.domain.PageRequest;
Expand Down Expand Up @@ -142,10 +143,60 @@ public SearchResultDTO<NodeDTO> searchNodes(
Optional<Boolean> includeContexts,
@Parameter(description = "Filter out programme contexts")
@RequestParam(value = "filterProgrammes", required = false, defaultValue = "false")
boolean filterProgrammes) {
boolean filterProgrammes
) {
return nodeService.searchByNodeType(
query,
ids,
contentUris,
language,
includeContexts,
filterProgrammes,
pageSize,
page,
nodeType,
Optional.empty());
}

@PostMapping("/search")
@Operation(summary = "Search all nodes")
@Transactional(readOnly = true)
public SearchResultDTO<NodeDTO> searchNodes(
@Parameter(description = "ISO-639-1 language code", example = "nb")
@RequestParam(value = "language", defaultValue = Constants.DefaultLanguage, required = false)
Optional<String> language,
@Parameter(description = "How many results to return per page")
@RequestParam(value = "pageSize", defaultValue = "10")
int pageSize,
@Parameter(description = "Which page to fetch") @RequestParam(value = "page", defaultValue = "1") int page,
@Parameter(description = "Query to search names") @RequestParam(value = "query", required = false)
Optional<String> query,
@Parameter(description = "Ids to fetch for query") @RequestParam(value = "ids", required = false)
Optional<List<String>> ids,
@Parameter(description = "ContentURIs to fetch for query")
@RequestParam(value = "contentUris", required = false)
Optional<List<String>> contentUris,
@Parameter(description = "Filter by nodeType") @RequestParam(value = "nodeType", required = false)
Optional<List<NodeType>> nodeType,
@Parameter(description = "Include all contexts") @RequestParam(value = "includeContexts", required = false)
Optional<Boolean> includeContexts,
@Parameter(description = "Filter out programme contexts")
@RequestParam(value = "filterProgrammes", required = false, defaultValue = "false")
boolean filterProgrammes,
@Parameter(description = "Customfields key,values to filter by (Must have one of, but not all)")
@RequestBody
Optional<NodeSearchBody> searchBodyParams) {
return nodeService.searchByNodeType(
query, ids, contentUris, language, includeContexts, filterProgrammes, pageSize, page, nodeType);
query,
ids,
contentUris,
language,
includeContexts,
filterProgrammes,
pageSize,
page,
nodeType,
searchBodyParams.flatMap(NodeSearchBody::getCustomFields));
}

@GetMapping("/page")
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/no/ndla/taxonomy/rest/v1/Resources.java
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ public SearchResultDTO<NodeDTO> searchResources(
false,
pageSize,
page,
Optional.of(List.of(NodeType.RESOURCE)));
Optional.of(List.of(NodeType.RESOURCE)),
Optional.empty());
}

@Deprecated
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/no/ndla/taxonomy/rest/v1/Subjects.java
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ public SearchResultDTO<NodeDTO> searchSubjects(
false,
pageSize,
page,
Optional.of(List.of(NodeType.SUBJECT)));
Optional.of(List.of(NodeType.SUBJECT)),
Optional.empty());
}

@Deprecated
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/no/ndla/taxonomy/rest/v1/Topics.java
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ public SearchResultDTO<NodeDTO> searchTopics(
false,
pageSize,
page,
Optional.of(List.of(NodeType.TOPIC)));
Optional.of(List.of(NodeType.TOPIC)),
Optional.empty());
}

@Deprecated
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Part of NDLA taxonomy-api
* Copyright (C) 2024 NDLA
*
* See LICENSE
*/

package no.ndla.taxonomy.rest.v1.commands;

import com.fasterxml.jackson.annotation.JsonProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.Map;
import java.util.Optional;

public class NodeSearchBody {
@JsonProperty
@Schema(
description =
"If specified, the search result will be filtered by whether they include the key,value combination provided. If more than one provided only one will be required (OR)")
public Optional<Map<String, String>> customFields = Optional.empty();

public Optional<Map<String, String>> getCustomFields() {
return customFields;
}
}
14 changes: 12 additions & 2 deletions src/main/java/no/ndla/taxonomy/service/NodeService.java
Original file line number Diff line number Diff line change
Expand Up @@ -322,10 +322,20 @@ public SearchResultDTO<NodeDTO> searchByNodeType(
boolean filterProgrammes,
int pageSize,
int page,
Optional<List<NodeType>> nodeType) {
Optional<List<NodeType>> nodeType,
Optional<Map<String, String>> customfieldsFilter) {
Optional<ExtraSpecification<Node>> nodeSpecLambda = nodeType.map(nt -> (s -> s.and(nodeHasOneOfNodeType(nt))));
return SearchService.super.search(
query, ids, contentUris, language, includeContexts, filterProgrammes, pageSize, page, nodeSpecLambda);
query,
ids,
contentUris,
language,
includeContexts,
filterProgrammes,
pageSize,
page,
nodeSpecLambda,
customfieldsFilter);
}

@Transactional
Expand Down
96 changes: 75 additions & 21 deletions src/main/java/no/ndla/taxonomy/service/SearchService.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import no.ndla.taxonomy.domain.DomainEntity;
Expand Down Expand Up @@ -46,6 +47,16 @@ private Specification<DOMAIN> withContentUriIn(List<URI> contentUris) {
return (root, query, criteriaBuilder) -> root.get("contentUri").in(contentUris);
}

private Specification<DOMAIN> withKeyAndValue(String key, String value) {
return (root, query, criteriaBuilder) -> criteriaBuilder.equal(
criteriaBuilder.function(
"jsonb_extract_path_text",
String.class,
root.get("customfields"),
criteriaBuilder.literal(key)),
value);
}

default SearchResultDTO<DTO> search(
Optional<String> query,
Optional<List<String>> ids,
Expand All @@ -54,7 +65,17 @@ default SearchResultDTO<DTO> search(
Optional<Boolean> includeContext,
int pageSize,
int page) {
return search(query, ids, contentUris, language, includeContext, false, pageSize, page, Optional.empty());
return search(
query,
ids,
contentUris,
language,
includeContext,
false,
pageSize,
page,
Optional.empty(),
Optional.empty());
}

default SearchResultDTO<DTO> search(
Expand All @@ -66,7 +87,8 @@ default SearchResultDTO<DTO> search(
boolean filterProgrammes,
int pageSize,
int page,
Optional<ExtraSpecification<DOMAIN>> applySpecLambda) {
Optional<ExtraSpecification<DOMAIN>> applySpecLambda,
Optional<Map<String, String>> customFieldFilters) {
if (page < 1) throw new IllegalArgumentException("page parameter must be bigger than 0");

var pageRequest = PageRequest.of(page - 1, pageSize);
Expand All @@ -76,45 +98,77 @@ default SearchResultDTO<DTO> search(
spec = spec.and(withNameLike(query.get()));
}

if (ids.isPresent()) {
List<URI> urisToPass = ids.get().stream()
spec = applyIdFilters(ids, spec);
spec = applyContentUriFilters(contentUris, spec);

if (applySpecLambda.isPresent()) {
spec = applySpecLambda.get().applySpecification(spec);
}

spec = applyCustomFieldsFilters(spec, customFieldFilters);

var fetched = getRepository().findAll(spec, pageRequest);

var languageCode = language.orElse("");
var dtos = fetched.stream()
.map(r -> createDTO(r, languageCode, includeContexts, filterProgrammes))
.collect(Collectors.toList());

return new SearchResultDTO<>(fetched.getTotalElements(), page, pageSize, dtos);
}

private Specification<DOMAIN> applyContentUriFilters(
Optional<List<String>> contentUris, Specification<DOMAIN> spec) {
if (contentUris.isPresent()) {
List<URI> urisToPass = contentUris.get().stream()
.flatMap(id -> {
try {
return Optional.of(new URI(id)).stream();
} catch (URISyntaxException ignored) {
/* ignore invalid urls sent by user */ }
/* ignore invalid urls sent by user */
}
return Optional.<URI>empty().stream();
})
.collect(Collectors.toList());

if (!urisToPass.isEmpty()) spec = spec.and(withPublicIdsIn(urisToPass));
if (!urisToPass.isEmpty()) spec = spec.and(withContentUriIn(urisToPass));
}
return spec;
}

if (contentUris.isPresent()) {
List<URI> urisToPass = contentUris.get().stream()
private Specification<DOMAIN> applyIdFilters(Optional<List<String>> ids, Specification<DOMAIN> spec) {
if (ids.isPresent()) {
List<URI> urisToPass = ids.get().stream()
.flatMap(id -> {
try {
return Optional.of(new URI(id)).stream();
} catch (URISyntaxException ignored) {
/* ignore invalid urls sent by user */ }
/* ignore invalid urls sent by user */
}
return Optional.<URI>empty().stream();
})
.collect(Collectors.toList());

if (!urisToPass.isEmpty()) spec = spec.and(withContentUriIn(urisToPass));
if (!urisToPass.isEmpty()) spec = spec.and(withPublicIdsIn(urisToPass));
}
return spec;
}

if (applySpecLambda.isPresent()) {
spec = applySpecLambda.get().applySpecification(spec);
private Specification<DOMAIN> applyCustomFieldsFilters(
Specification<DOMAIN> spec, Optional<Map<String, String>> metadataFilters) {
if (metadataFilters.isPresent()) {
Specification<DOMAIN> filterSpec = null;
for (var entry : metadataFilters.get().entrySet()) {
if (filterSpec != null) {
filterSpec = filterSpec.or(withKeyAndValue(entry.getKey(), entry.getValue()));
} else {
filterSpec = withKeyAndValue(entry.getKey(), entry.getValue());
}
}
if (filterSpec != null) {
return spec.and(filterSpec);
}
}

var fetched = getRepository().findAll(spec, pageRequest);

var languageCode = language.orElse("");
var dtos = fetched.stream()
.map(r -> createDTO(r, languageCode, includeContexts, filterProgrammes))
.collect(Collectors.toList());

return new SearchResultDTO<>(fetched.getTotalElements(), page, pageSize, dtos);
return spec;
}
}
7 changes: 5 additions & 2 deletions src/test/java/no/ndla/taxonomy/service/NodeServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ public void allSearch() {
false,
10,
1,
Optional.of(List.of(NodeType.SUBJECT)));
Optional.of(List.of(NodeType.SUBJECT)),
Optional.empty());

var topics = nodeService.searchByNodeType(
Optional.empty(),
Expand All @@ -130,7 +131,8 @@ public void allSearch() {
false,
10,
1,
Optional.of(List.of(NodeType.TOPIC)));
Optional.of(List.of(NodeType.TOPIC)),
Optional.empty());
var all = nodeService.searchByNodeType(
Optional.empty(),
Optional.empty(),
Expand All @@ -140,6 +142,7 @@ public void allSearch() {
false,
10,
1,
Optional.empty(),
Optional.empty());

assertEquals(subjects.getResults().get(0).getId(), subject.getPublicId());
Expand Down

0 comments on commit 3b2eb98

Please sign in to comment.