diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/Nodes.java b/src/main/java/no/ndla/taxonomy/rest/v1/Nodes.java index ab31bf70..5622c8bf 100644 --- a/src/main/java/no/ndla/taxonomy/rest/v1/Nodes.java +++ b/src/main/java/no/ndla/taxonomy/rest/v1/Nodes.java @@ -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; @@ -142,10 +143,60 @@ public SearchResultDTO searchNodes( Optional 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 searchNodes( + @Parameter(description = "ISO-639-1 language code", example = "nb") + @RequestParam(value = "language", defaultValue = Constants.DefaultLanguage, required = false) + Optional 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 query, + @Parameter(description = "Ids to fetch for query") @RequestParam(value = "ids", required = false) + Optional> ids, + @Parameter(description = "ContentURIs to fetch for query") + @RequestParam(value = "contentUris", required = false) + Optional> contentUris, + @Parameter(description = "Filter by nodeType") @RequestParam(value = "nodeType", required = false) + Optional> nodeType, + @Parameter(description = "Include all contexts") @RequestParam(value = "includeContexts", required = false) + Optional 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 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") diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/Resources.java b/src/main/java/no/ndla/taxonomy/rest/v1/Resources.java index c4738c55..fe4c08a9 100644 --- a/src/main/java/no/ndla/taxonomy/rest/v1/Resources.java +++ b/src/main/java/no/ndla/taxonomy/rest/v1/Resources.java @@ -117,7 +117,8 @@ public SearchResultDTO searchResources( false, pageSize, page, - Optional.of(List.of(NodeType.RESOURCE))); + Optional.of(List.of(NodeType.RESOURCE)), + Optional.empty()); } @Deprecated diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/Subjects.java b/src/main/java/no/ndla/taxonomy/rest/v1/Subjects.java index 27168051..6cee76e6 100644 --- a/src/main/java/no/ndla/taxonomy/rest/v1/Subjects.java +++ b/src/main/java/no/ndla/taxonomy/rest/v1/Subjects.java @@ -115,7 +115,8 @@ public SearchResultDTO searchSubjects( false, pageSize, page, - Optional.of(List.of(NodeType.SUBJECT))); + Optional.of(List.of(NodeType.SUBJECT)), + Optional.empty()); } @Deprecated diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/Topics.java b/src/main/java/no/ndla/taxonomy/rest/v1/Topics.java index f0357e3e..9f9f3bd1 100644 --- a/src/main/java/no/ndla/taxonomy/rest/v1/Topics.java +++ b/src/main/java/no/ndla/taxonomy/rest/v1/Topics.java @@ -102,7 +102,8 @@ public SearchResultDTO searchTopics( false, pageSize, page, - Optional.of(List.of(NodeType.TOPIC))); + Optional.of(List.of(NodeType.TOPIC)), + Optional.empty()); } @Deprecated diff --git a/src/main/java/no/ndla/taxonomy/rest/v1/commands/NodeSearchBody.java b/src/main/java/no/ndla/taxonomy/rest/v1/commands/NodeSearchBody.java new file mode 100644 index 00000000..8590f606 --- /dev/null +++ b/src/main/java/no/ndla/taxonomy/rest/v1/commands/NodeSearchBody.java @@ -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> customFields = Optional.empty(); + + public Optional> getCustomFields() { + return customFields; + } +} diff --git a/src/main/java/no/ndla/taxonomy/service/NodeService.java b/src/main/java/no/ndla/taxonomy/service/NodeService.java index dd5b3b98..b18f6415 100644 --- a/src/main/java/no/ndla/taxonomy/service/NodeService.java +++ b/src/main/java/no/ndla/taxonomy/service/NodeService.java @@ -322,10 +322,20 @@ public SearchResultDTO searchByNodeType( boolean filterProgrammes, int pageSize, int page, - Optional> nodeType) { + Optional> nodeType, + Optional> customfieldsFilter) { Optional> 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 diff --git a/src/main/java/no/ndla/taxonomy/service/SearchService.java b/src/main/java/no/ndla/taxonomy/service/SearchService.java index 76d55d77..7268a3d0 100644 --- a/src/main/java/no/ndla/taxonomy/service/SearchService.java +++ b/src/main/java/no/ndla/taxonomy/service/SearchService.java @@ -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; @@ -46,6 +47,16 @@ private Specification withContentUriIn(List contentUris) { return (root, query, criteriaBuilder) -> root.get("contentUri").in(contentUris); } + private Specification 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 search( Optional query, Optional> ids, @@ -54,7 +65,17 @@ default SearchResultDTO search( Optional 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 search( @@ -66,7 +87,8 @@ default SearchResultDTO search( boolean filterProgrammes, int pageSize, int page, - Optional> applySpecLambda) { + Optional> applySpecLambda, + Optional> customFieldFilters) { if (page < 1) throw new IllegalArgumentException("page parameter must be bigger than 0"); var pageRequest = PageRequest.of(page - 1, pageSize); @@ -76,45 +98,77 @@ default SearchResultDTO search( spec = spec.and(withNameLike(query.get())); } - if (ids.isPresent()) { - List 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 applyContentUriFilters( + Optional> contentUris, Specification spec) { + if (contentUris.isPresent()) { + List 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.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 urisToPass = contentUris.get().stream() + private Specification applyIdFilters(Optional> ids, Specification spec) { + if (ids.isPresent()) { + List 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.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 applyCustomFieldsFilters( + Specification spec, Optional> metadataFilters) { + if (metadataFilters.isPresent()) { + Specification 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; } } diff --git a/src/test/java/no/ndla/taxonomy/service/NodeServiceTest.java b/src/test/java/no/ndla/taxonomy/service/NodeServiceTest.java index f77d0e34..d746915f 100644 --- a/src/test/java/no/ndla/taxonomy/service/NodeServiceTest.java +++ b/src/test/java/no/ndla/taxonomy/service/NodeServiceTest.java @@ -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(), @@ -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(), @@ -140,6 +142,7 @@ public void allSearch() { false, 10, 1, + Optional.empty(), Optional.empty()); assertEquals(subjects.getResults().get(0).getId(), subject.getPublicId());