diff --git a/.github/scripts/post-elastic-test-queries.sh b/.github/scripts/post-elastic-test-queries.sh index d37702f5..46658c03 100755 --- a/.github/scripts/post-elastic-test-queries.sh +++ b/.github/scripts/post-elastic-test-queries.sh @@ -47,7 +47,7 @@ access_token="$(curl -s --request POST \ --data password=testpassword \ --data scope=openid | jq '.access_token' | tr -d '"')" -response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/terminology/entry/search?searchterm=Blutdruck") +response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/terminology/entry/search?searchterm=Blutdruck") http_code="${response: -3}" json_body=$(cat response_body) @@ -63,7 +63,7 @@ else exit 1 fi -response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/terminology/entry/$onto_example_id") +response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/terminology/entry/$onto_example_id") http_code="${response: -3}" json_body=$(cat response_body) @@ -79,7 +79,7 @@ else exit 1 fi -response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/codeable-concept/entry/search?searchterm=Vectorcardiogram") +response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/codeable-concept/entry/search?searchterm=Vectorcardiogram") http_code="${response: -3}" json_body=$(cat response_body) @@ -95,7 +95,7 @@ else exit 1 fi -response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v4/codeable-concept/entry?ids=$cc_example_id") +response=$(curl -s -w "%{http_code}" --header "Authorization: Bearer $access_token" -o response_body "http://localhost:8091/api/v5/codeable-concept/entry?ids=$cc_example_id") http_code="${response: -3}" json_body=$(cat response_body) diff --git a/.github/scripts/post-test-query.sh b/.github/scripts/post-test-query.sh index d82fde5f..c0d309b8 100755 --- a/.github/scripts/post-test-query.sh +++ b/.github/scripts/post-test-query.sh @@ -10,7 +10,7 @@ access_token="$(curl -s --request POST \ --data scope=openid | jq '.access_token' | tr -d '"')" response=$(curl -s -i \ - --url http://localhost:8091/api/v4/query \ + --url http://localhost:8091/api/v5/query/feasibility \ --header "Authorization: Bearer $access_token" \ --header 'Content-Type: application/json' \ --data '{ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0b375f99..10e6c06e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,7 +148,7 @@ jobs: needs: tests runs-on: ubuntu-latest env: - ONTOLOGY_GIT_TAG: v3.0.2-alpha.5 + ONTOLOGY_GIT_TAG: v3.1.0 ELASTIC_HOST: http://localhost:9200 ELASTIC_FILEPATH: https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/download/TAGPLACEHOLDER/ ELASTIC_FILENAME: elastic.zip diff --git a/CHANGELOG.md b/CHANGELOG.md index 3737bb72..c9090479 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [UNRELEASED] - 2025-mm-dd + +### Changed +- REST API changed to v5 +### Removed +- Query Templates have been removed + ## [6.1.0] - 2025-02-14 - Based on ontology **[v3.1.0](https://github.com/medizininformatik-initiative/fhir-ontology-generator/releases/tag/v3.1.0)** diff --git a/pom.xml b/pom.xml index 6f8b9a38..b9533a56 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ de.medizininformatik-initiative DataportalBackend - 6.2.0-SNAPSHOT + 7.0.0-SNAPSHOT Dataportal Backend Backend of the Dataportal diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java index b15628e8..c74e3a7b 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/config/RateLimitingConfig.java @@ -1,11 +1,5 @@ package de.numcodex.feasibility_gui_backend.config; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_API; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_QUERY; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_ID_MATCHER; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_SUMMARY_RESULT; - import de.numcodex.feasibility_gui_backend.query.ratelimiting.RateLimitingInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; @@ -13,6 +7,8 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; + @Component public class RateLimitingConfig implements WebMvcConfigurer { @@ -23,7 +19,7 @@ public class RateLimitingConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(interceptor) - .addPathPatterns(PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_SUMMARY_RESULT) - .addPathPatterns(PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_DETAILED_OBFUSCATED_RESULT); + .addPathPatterns(PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_SUMMARY_RESULT) + .addPathPatterns(PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_DETAILED_OBFUSCATED_RESULT); } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java index 1f6982de..76a6afd3 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/config/WebSecurityConfig.java @@ -37,17 +37,17 @@ public class WebSecurityConfig { public static final String KEY_SPRING_ADDONS_CONFIDENTIAL = "spring-addons-confidential"; public static final String KEY_SPRING_ADDONS_PUBLIC = "spring-addons-public"; public static final String PATH_ACTUATOR_HEALTH = "/actuator/health"; - public static final String PATH_API = "/api/v4"; + public static final String PATH_API = "/api/v5"; public static final String PATH_QUERY = "/query"; + public static final String PATH_DATA = "/data"; + public static final String PATH_FEASIBILITY = "/feasibility"; public static final String PATH_ID_MATCHER = "/{id:\\d+}"; public static final String PATH_USER_ID_MATCHER = "/by-user/{id:[\\w-]+}"; - public static final String PATH_SAVED = "/saved"; public static final String PATH_CONTENT = "/content"; public static final String PATH_SUMMARY_RESULT = "/summary-result"; public static final String PATH_DETAILED_OBFUSCATED_RESULT = "/detailed-obfuscated-result"; public static final String PATH_DETAILED_RESULT = "/detailed-result"; public static final String PATH_TERMINOLOGY = "/terminology"; - public static final String PATH_TEMPLATE = "/template"; public static final String PATH_DSE = "/dse"; public static final String PATH_CODEABLE_CONCEPT = "/codeable-concept"; public static final String PATH_SWAGGER_UI = "/swagger-ui/**"; @@ -102,12 +102,11 @@ public SecurityFilterChain apiFilterChain( http.authorizeHttpRequests(authorize -> authorize .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_TERMINOLOGY + "/**")).hasAuthority(keycloakAllowedRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY)).hasAuthority(keycloakAllowedRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_USER_ID_MATCHER)).hasAuthority(keycloakAdminRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_SAVED)).hasAuthority(keycloakAllowedRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_ID_MATCHER + PATH_DETAILED_RESULT)).hasAuthority(keycloakAdminRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_TEMPLATE)).hasAuthority(keycloakAllowedRole) - .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_TEMPLATE + "/*")).hasAuthority(keycloakAllowedRole) + .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA)).hasAuthority(keycloakAllowedRole) + .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA + PATH_USER_ID_MATCHER)).hasAuthority(keycloakAdminRole) + .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_DATA + "/*")).hasAuthority(keycloakAllowedRole) + .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_FEASIBILITY)).hasAuthority(keycloakAllowedRole) + .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_QUERY + PATH_FEASIBILITY + PATH_ID_MATCHER + PATH_DETAILED_RESULT)).hasAuthority(keycloakAdminRole) .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole) .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_DSE + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole) .requestMatchers(new MvcRequestMatcher(introspector, PATH_API + PATH_CODEABLE_CONCEPT + "/**")).hasAnyAuthority(keycloakAdminRole, keycloakAllowedRole) diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java similarity index 96% rename from src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java rename to src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java index c8da0388..a5b12921 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestController.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.dse.v4; +package de.numcodex.feasibility_gui_backend.dse.v5; import de.numcodex.feasibility_gui_backend.dse.DseService; import de.numcodex.feasibility_gui_backend.dse.api.DseProfile; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java index 2bfccdec..63ad40df 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerService.java @@ -3,8 +3,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.query.api.Query; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.SavedQuery; import de.numcodex.feasibility_gui_backend.query.api.*; import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchException; import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatcher; @@ -12,21 +10,15 @@ import de.numcodex.feasibility_gui_backend.query.result.RandomSiteNameGenerator; import de.numcodex.feasibility_gui_backend.query.result.ResultLine; import de.numcodex.feasibility_gui_backend.query.result.ResultService; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateHandler; import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; import lombok.NonNull; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Mono; -import java.sql.Timestamp; -import java.time.Instant; import java.util.ArrayList; import java.util.List; -import java.util.Optional; @Service @RequiredArgsConstructor @@ -41,9 +33,6 @@ public enum ResultDetail { @NonNull private final QueryDispatcher queryDispatcher; - @NonNull - private final QueryTemplateHandler queryTemplateHandler; - @NonNull private final QueryRepository queryRepository; @@ -53,12 +42,6 @@ public enum ResultDetail { @NonNull private final ResultService resultService; - @NonNull - private final QueryTemplateRepository queryTemplateRepository; - - @NonNull - private final SavedQueryRepository savedQueryRepository; - @NonNull private final StructuredQueryValidation structuredQueryValidation; @@ -101,9 +84,8 @@ public QueryResult getQueryResult(Long queryId, ResultDetail resultDetail) { public Query getQuery(Long queryId) throws JsonProcessingException { var query = queryRepository.findById(queryId); - var savedQuery = savedQueryRepository.findByQueryId(queryId); if (query.isPresent()) { - return convertQueryToApi(query.get(), savedQuery); + return convertQueryToApi(query.get()); } else { return null; } @@ -118,186 +100,20 @@ public StructuredQuery getQueryContent(Long queryId) throws JsonProcessingExcept } } - public Long storeQueryTemplate(QueryTemplate queryTemplate, String userId) - throws QueryTemplateException { - return queryTemplateHandler.storeTemplate(queryTemplate, userId); - } - - public Long saveQuery(Long queryId, String userId, SavedQuery savedQueryApi) { - if (savedQueryRepository.existsSavedQueryByLabelAndUserId(savedQueryApi.label(), userId)) { - throw new DataIntegrityViolationException(String.format("User %s already has a saved query named %s", userId, savedQueryApi.label())); - } - de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery savedQuery = convertSavedQueryApiToPersistence(savedQueryApi, queryId); - return savedQueryRepository.save(savedQuery).getId(); - } - - public void updateSavedQuery(Long queryId, SavedQuery savedQuery) throws QueryNotFoundException { - Optional savedQueryOptional = savedQueryRepository.findByQueryId(queryId); - if (savedQueryOptional.isEmpty()) { - throw new QueryNotFoundException(); - } - var oldSavedQuery = savedQueryOptional.get(); - oldSavedQuery.setLabel(savedQuery.label()); - oldSavedQuery.setComment(savedQuery.comment()); - savedQueryRepository.save(oldSavedQuery); - } - - public de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate getQueryTemplate( - Long queryId, String authorId) throws QueryTemplateException { - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate queryTemplate = queryTemplateRepository.findById( - queryId).orElseThrow(QueryTemplateException::new); - if (!queryTemplate.getQuery().getCreatedBy().equalsIgnoreCase(authorId)) { - throw new QueryTemplateException(); - } - return queryTemplate; - } - - public List getQueryTemplatesForAuthor( - String authorId) { - return queryTemplateRepository.findByAuthor(authorId); - } - - public void updateQueryTemplate(Long queryTemplateId, QueryTemplate queryTemplate, String authorId) throws QueryTemplateException { - var templates = getQueryTemplatesForAuthor(authorId); - Optional templateToUpdate = templates.stream(). - filter(t -> t.getId().equals(queryTemplateId)). - findFirst(); - - if (templateToUpdate.isPresent()) { - var template = templateToUpdate.get(); - template.setLabel(queryTemplate.label()); - template.setComment(queryTemplate.comment()); - template.setLastModified(Timestamp.from(Instant.now())); - queryTemplateRepository.save(template); - } else { - throw new QueryTemplateException("not found"); - } - } - - public void deleteQueryTemplate(Long queryTemplateId, String authorId) throws QueryTemplateException { - var templates = getQueryTemplatesForAuthor(authorId); - Optional templateToDelete = templates.stream(). - filter(t -> t.getId().equals(queryTemplateId)). - findFirst(); - - if (templateToDelete.isPresent()) { - queryTemplateRepository.delete(templateToDelete.get()); - } else { - throw new QueryTemplateException("not found"); - } - } - - public QueryTemplate convertTemplatePersistenceToApi( - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate in) - throws JsonProcessingException { - return queryTemplateHandler.convertPersistenceToApi(in); - } - - private Query convertQueryToApi(de.numcodex.feasibility_gui_backend.query.persistence.Query in, - Optional savedQuery) - throws JsonProcessingException { - - if (savedQuery.isPresent()) { - return Query.builder() - .id(in.getId()) - .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class)) - .label(savedQuery.get().getLabel()) - .comment(savedQuery.get().getComment()) - .totalNumberOfPatients(savedQuery.get().getResultSize()) - .build(); - } else { - return Query.builder() - .id(in.getId()) - .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class)) - .build(); - } - } - - private de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery convertSavedQueryApiToPersistence( - SavedQuery in, Long queryId) { - var out = new de.numcodex.feasibility_gui_backend.query.persistence.SavedQuery(); - out.setQuery(queryRepository.getReferenceById(queryId)); - out.setComment(in.comment()); - out.setLabel(in.label()); - out.setResultSize(in.totalNumberOfPatients()); - return out; - } - public List getQueryListForAuthor( - String userId, boolean savedOnly) { - Optional> queries; + private Query convertQueryToApi(de.numcodex.feasibility_gui_backend.query.persistence.Query in) + throws JsonProcessingException { - if (savedOnly) { - queries = queryRepository.findSavedQueriesByAuthor(userId); - } else { - queries = queryRepository.findByAuthor(userId); - } - - return queries.orElseGet(ArrayList::new); + return Query.builder() + .id(in.getId()) + .content(jsonUtil.readValue(in.getQueryContent().getQueryContent(), StructuredQuery.class)) + .build(); } public String getAuthorId(Long queryId) throws QueryNotFoundException { return queryRepository.getAuthor(queryId).orElseThrow(QueryNotFoundException::new); } - public QueryListEntry convertQueryToQueryListEntry(de.numcodex.feasibility_gui_backend.query.persistence.Query query, - boolean skipValidation) { - boolean isValid = true; - if (!skipValidation) { - try { - var sq = jsonUtil.readValue(query.getQueryContent().getQueryContent(), StructuredQuery.class); - isValid = structuredQueryValidation.isValid(sq); - } catch (JsonProcessingException e) { - isValid = false; - } - } - - if (query.getSavedQuery() != null) { - if (skipValidation) { - return - QueryListEntry.builder() - .id(query.getId()) - .label(query.getSavedQuery().getLabel()) - .comment(query.getSavedQuery().getComment()) - .totalNumberOfPatients(query.getSavedQuery().getResultSize()) - .createdAt(query.getCreatedAt()) - .build(); - } else { - return - QueryListEntry.builder() - .id(query.getId()) - .label(query.getSavedQuery().getLabel()) - .comment(query.getSavedQuery().getComment()) - .totalNumberOfPatients(query.getSavedQuery().getResultSize()) - .createdAt(query.getCreatedAt()) - .isValid(isValid) - .build(); - } - } else { - if (skipValidation) { - return - QueryListEntry.builder() - .id(query.getId()) - .createdAt(query.getCreatedAt()) - .build(); - } else { - return - QueryListEntry.builder() - .id(query.getId()) - .createdAt(query.getCreatedAt()) - .isValid(isValid) - .build(); - } - } - } - - public List convertQueriesToQueryListEntries(List queryList, - boolean skipValidation) { - var ret = new ArrayList(); - queryList.forEach(q -> ret.add(convertQueryToQueryListEntry(q, skipValidation))); - return ret; - } - public Long getAmountOfQueriesByUserAndInterval(String userId, int minutes) { return queryRepository.countQueriesByAuthorInTheLastNMinutes(userId, minutes); } @@ -309,18 +125,4 @@ public Long getRetryAfterTime(String userId, int offset, long interval) { return 0L; } } - - public Long getAmountOfSavedQueriesByUser(String userId) { - var queries = queryRepository.findSavedQueriesByAuthor(userId); - return queries.map(queryList -> (long) queryList.size()).orElse(0L); - } - - public void deleteSavedQuery(Long queryId) throws QueryNotFoundException { - var savedQueryOptional = savedQueryRepository.findByQueryId(queryId); - if (savedQueryOptional.isPresent()) { - savedQueryRepository.deleteById(savedQueryOptional.get().getId()); - } else { - throw new QueryNotFoundException(); - } - } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java new file mode 100644 index 00000000..a0cb7324 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Crtdl.java @@ -0,0 +1,13 @@ +package de.numcodex.feasibility_gui_backend.query.api; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record Crtdl( + @JsonProperty String version, + @JsonProperty String display, + @JsonProperty StructuredQuery cohortDefinition, + @JsonProperty DataExtraction dataExtraction +) { +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java similarity index 79% rename from src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java rename to src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java index d3877d7d..6017d03c 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/QueryTemplate.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/DataExtraction.java @@ -3,13 +3,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; -import de.numcodex.feasibility_gui_backend.query.api.validation.QueryTemplateValidation; import lombok.Builder; @JsonInclude(Include.NON_NULL) -@QueryTemplateValidation @Builder -public record QueryTemplate( +public record DataExtraction( @JsonProperty long id, @JsonProperty StructuredQuery content, @JsonProperty String label, diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java similarity index 61% rename from src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java rename to src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java index 24114474..cd5b6aea 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/SavedQuery.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/Dataquery.java @@ -7,10 +7,15 @@ @JsonInclude(Include.NON_NULL) @Builder -public record SavedQuery( +public record Dataquery( + @JsonProperty long id, + @JsonProperty Crtdl content, @JsonProperty String label, @JsonProperty String comment, - @JsonProperty Long totalNumberOfPatients + @JsonProperty String lastModified, + @JsonProperty String createdBy, + @JsonProperty Boolean isValid, + @JsonProperty Long resultSize ) { } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java deleted file mode 100644 index fa13235a..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidator.java +++ /dev/null @@ -1,15 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; - -/** - * Validator for {@link QueryTemplate} that always passes no matter what instance gets checked. - */ -public class QueryTemplatePassValidator implements ConstraintValidator { - @Override - public boolean isValid(QueryTemplate queryTemplate, ConstraintValidatorContext constraintValidatorContext) { - return true; - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java deleted file mode 100644 index 383d6726..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidation.java +++ /dev/null @@ -1,19 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; -import jakarta.validation.Constraint; -import jakarta.validation.Payload; - -@Target({ElementType.TYPE}) -@Retention(RetentionPolicy.RUNTIME) -@Constraint(validatedBy = QueryTemplateValidator.class) -public @interface QueryTemplateValidation { - String message() default "Query template is invalid"; - - Class[] groups() default {}; - - Class[] payload() default {}; -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java deleted file mode 100644 index 362621ac..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidator.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import jakarta.validation.ConstraintValidator; -import jakarta.validation.ConstraintValidatorContext; -import lombok.NonNull; -import lombok.extern.slf4j.Slf4j; -import org.everit.json.schema.Schema; -import org.everit.json.schema.ValidationException; -import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Qualifier; - -/** - * Validator for {@link QueryTemplate} that does an actual check based on a JSON schema. - */ -@Slf4j -public class QueryTemplateValidator implements ConstraintValidator { - - @NonNull - private Schema jsonSchema; - - @NonNull - private ObjectMapper jsonUtil; - - /** - * Required args constructor. - * - * Lombok annotation had to be removed since it could not take the necessary Schema Qualifier - */ - public QueryTemplateValidator(@Qualifier(value = "validation-template") Schema jsonSchema, ObjectMapper jsonUtil) { - this.jsonSchema = jsonSchema; - this.jsonUtil = jsonUtil; - } - - /** - * Validate the submitted {@link QueryTemplate} against the json query schema. - * - * @param queryTemplate the {@link QueryTemplate} to validate - */ - @Override - public boolean isValid(QueryTemplate queryTemplate, - ConstraintValidatorContext constraintValidatorContext) { - try { - var jsonSubject = new JSONObject(jsonUtil.writeValueAsString(queryTemplate)); - jsonSchema.validate(jsonSubject); - return true; - } catch (ValidationException | JsonProcessingException e) { - log.debug("Stored query is invalid", e); - return false; - } - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java deleted file mode 100644 index 9c63057c..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorSpringConfig.java +++ /dev/null @@ -1,48 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import java.io.InputStream; -import jakarta.validation.ConstraintValidator; -import lombok.extern.slf4j.Slf4j; -import org.everit.json.schema.Schema; -import org.everit.json.schema.loader.SchemaClient; -import org.everit.json.schema.loader.SchemaLoader; -import org.json.JSONObject; -import org.json.JSONTokener; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -@Slf4j -public class QueryTemplateValidatorSpringConfig { - - private static final String JSON_SCHEMA = "query-template-schema.json"; - - @Value("${app.enableQueryValidation}") - private boolean enabled; - - @Bean - public ConstraintValidator createStoredQueryValidator( - @Qualifier("validation-template") Schema schema) { - return enabled - ? new QueryTemplateValidator(schema, new ObjectMapper()) - : new QueryTemplatePassValidator(); - } - - @Qualifier("validation-template") - @Bean - public Schema createStoredQueryValidatorJsonSchema() { - InputStream inputStream = QueryTemplateValidator.class.getResourceAsStream(JSON_SCHEMA); - var jsonSchema = new JSONObject(new JSONTokener(inputStream)); - SchemaLoader loader = SchemaLoader.builder() - .schemaClient(SchemaClient.classPathAwareClient()) - .schemaJson(jsonSchema) - .resolutionScope("classpath://query/") - .draftV7Support() - .build(); - return loader.load().build(); - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java new file mode 100644 index 00000000..44cb1a72 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryException.java @@ -0,0 +1,12 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +public class DataqueryException extends Exception { + + public DataqueryException() { + super(); + } + + public DataqueryException(String message) { + super(message); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java new file mode 100644 index 00000000..2e5ed5a3 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandler.java @@ -0,0 +1,146 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.feasibility_gui_backend.query.api.Crtdl; +import de.numcodex.feasibility_gui_backend.query.api.Dataquery; +import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots; +import de.numcodex.feasibility_gui_backend.query.persistence.DataqueryRepository; +import jakarta.transaction.Transactional; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Transactional +@RequiredArgsConstructor +public class DataqueryHandler { + + @NonNull + private ObjectMapper jsonUtil; + + @NonNull + private DataqueryRepository dataqueryRepository; + + @NonNull + private Integer maxDataqueriesPerUser; + + public Long storeDataquery(@NonNull Dataquery dataquery, @NonNull String userId) throws DataqueryException, DataqueryStorageFullException { + + // By definition, a user can save an unlimited amount of queries without result + if (dataquery.resultSize() != null && dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId) >= maxDataqueriesPerUser) { + throw new DataqueryStorageFullException(); + } + + var tmp = Dataquery.builder() + .resultSize(dataquery.resultSize()) + .content(dataquery.content()) + .label(dataquery.label()) + .comment(dataquery.comment()) + .createdBy(userId) + .build(); + + try { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataqueryEntity = convertApiToPersistence(tmp); + dataqueryEntity = dataqueryRepository.save(dataqueryEntity); + return dataqueryEntity.getId(); + } catch (JsonProcessingException e) { + throw new DataqueryException(e.getMessage()); + } + } + + public Dataquery getDataqueryById(Long dataqueryId, String userId) throws DataqueryException, JsonProcessingException { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery = dataqueryRepository.findById(dataqueryId).orElseThrow(DataqueryException::new); + if (dataquery.getCreatedBy() == null || !dataquery.getCreatedBy().equals(userId)) { + throw new DataqueryException(); + } + return convertPersistenceToApi(dataquery); + } + + public void updateDataquery(Long queryId, Dataquery dataquery, String userId) throws DataqueryException, DataqueryStorageFullException, JsonProcessingException { + var usedSlots = dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId); + var existingDataquery = dataqueryRepository.findById(queryId).orElseThrow(DataqueryException::new); + + if (usedSlots >= maxDataqueriesPerUser) { + // Only throw an exception when the updated query contains a result and the original didn't + if (dataquery.resultSize() != null && existingDataquery.getResultSize() == null) { + throw new DataqueryStorageFullException(); + } + } + + if (existingDataquery.getCreatedBy().equals(userId)) { + var dataqueryToUpdate = convertApiToPersistence(dataquery); + dataqueryToUpdate.setId(existingDataquery.getId()); + dataqueryToUpdate.setCreatedBy(userId); + dataqueryToUpdate.setLastModified(Timestamp.valueOf(LocalDateTime.now())); + dataqueryRepository.save(dataqueryToUpdate); + } else { + throw new DataqueryException(); + } + } + + public List getDataqueriesByAuthor(String userId) throws DataqueryException { + List dataqueries = dataqueryRepository.findAllByCreatedBy(userId); + List ret = new ArrayList<>(); + + for (de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery : dataqueries) { + try { + ret.add(convertPersistenceToApi(dataquery)); + } catch (JsonProcessingException e) { + throw new DataqueryException(); + } + } + + return ret; + } + + public void deleteDataquery(Long dataqueryId, String userId) throws DataqueryException { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery dataquery = dataqueryRepository.findById(dataqueryId).orElseThrow(DataqueryException::new); + if (!dataquery.getCreatedBy().equals(userId)) { + throw new DataqueryException(); + } else { + dataqueryRepository.delete(dataquery); + } + } + + public SavedQuerySlots getDataquerySlotsJson(String userId) { + var queryAmount = dataqueryRepository.countByCreatedByWhereResultIsNotNull(userId); + + return SavedQuerySlots.builder() + .used(queryAmount) + .total(maxDataqueriesPerUser) + .build(); + } + + public de.numcodex.feasibility_gui_backend.query.persistence.Dataquery convertApiToPersistence(de.numcodex.feasibility_gui_backend.query.api.Dataquery in) throws JsonProcessingException { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery out = new de.numcodex.feasibility_gui_backend.query.persistence.Dataquery(); + out.setId(in.id() > 0 ? in.id() : null); + out.setLabel(in.label()); + out.setComment(in.comment()); + if (in.lastModified() != null) { + out.setLastModified(Timestamp.valueOf(in.lastModified())); + } + out.setCreatedBy(in.createdBy()); + out.setResultSize(in.resultSize()); + out.setCrtdl(jsonUtil.writeValueAsString(in.content())); + return out; + } + + public de.numcodex.feasibility_gui_backend.query.api.Dataquery convertPersistenceToApi(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery in) throws JsonProcessingException { + return de.numcodex.feasibility_gui_backend.query.api.Dataquery.builder() + .id(in.getId()) + .label(in.getLabel()) + .comment(in.getComment()) + .createdBy(in.getCreatedBy()) + .resultSize(in.getResultSize()) + .lastModified(in.getLastModified() == null ? null : in.getLastModified().toString()) + .content(jsonUtil.readValue(in.getCrtdl(), Crtdl.class)) + .build(); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java new file mode 100644 index 00000000..a3834f72 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataquerySpringConfig.java @@ -0,0 +1,21 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.feasibility_gui_backend.query.persistence.DataqueryRepository; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class DataquerySpringConfig { + + @Bean + public DataqueryHandler createDataqueryHandler( + @Qualifier("translation") ObjectMapper jsonUtil, + DataqueryRepository dataqueryRepository, + @Value("${app.maxSavedQueriesPerUser}") Integer maxSavedQueriesPerUser + ) { + return new DataqueryHandler(jsonUtil, dataqueryRepository, maxSavedQueriesPerUser); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java new file mode 100644 index 00000000..4ff437de --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryStorageFullException.java @@ -0,0 +1,8 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +public class DataqueryStorageFullException extends Exception { + + public DataqueryStorageFullException() { + super(); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java new file mode 100644 index 00000000..484e8888 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Dataquery.java @@ -0,0 +1,52 @@ +package de.numcodex.feasibility_gui_backend.query.persistence; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +import java.sql.Timestamp; +import java.util.Objects; + +@Getter +@Setter +@ToString +@RequiredArgsConstructor +@Entity +public class Dataquery { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "created_by", updatable = false) + private String createdBy; + + @Column(name = "label", nullable = false) + private String label; + + @Column(name = "comment") + private String comment; + + @Column(name = "crtdl") + private String crtdl; + + @Column(name = "last_modified", insertable = false) + private Timestamp lastModified; + + @Column(name = "result_size") + private Long resultSize; + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Dataquery dataquery = (Dataquery) o; + return Objects.equals(id, dataquery.id) && Objects.equals(createdBy, dataquery.createdBy) && Objects.equals(label, dataquery.label) && Objects.equals(comment, dataquery.comment) && Objects.equals(crtdl, dataquery.crtdl) && Objects.equals(lastModified, dataquery.lastModified) && Objects.equals(resultSize, dataquery.resultSize); + } + + @Override + public int hashCode() { + return Objects.hash(id, createdBy, label, comment, crtdl, lastModified, resultSize); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java new file mode 100644 index 00000000..c5d8fec1 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/DataqueryRepository.java @@ -0,0 +1,13 @@ +package de.numcodex.feasibility_gui_backend.query.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface DataqueryRepository extends JpaRepository { + List findAllByCreatedBy(String userId); + + @Query(value = "SELECT COUNT(*) FROM Dataquery WHERE createdBy = ?1 AND resultSize IS NOT NULL") + Long countByCreatedByWhereResultIsNotNull(String userId); +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java index bf615eb1..0176c670 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/Query.java @@ -29,9 +29,6 @@ public class Query { @ToString.Exclude private QueryContent queryContent; - @OneToOne(mappedBy = "query", cascade = CascadeType.ALL) - private SavedQuery savedQuery; - @Override public final boolean equals(Object o) { if (this == o) return true; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java index 4e5e4db6..58138250 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryRepository.java @@ -9,9 +9,6 @@ public interface QueryRepository extends JpaRepository { @org.springframework.data.jpa.repository.Query("SELECT t FROM Query t WHERE t.createdBy = ?1") Optional> findByAuthor(String authorId); - @org.springframework.data.jpa.repository.Query("SELECT t FROM Query t left join SavedQuery s on t.id = s.query.id WHERE t.createdBy = ?1 AND s.id IS NOT NULL") - Optional> findSavedQueriesByAuthor(String authorId); - @org.springframework.data.jpa.repository.Query("SELECT t.createdBy FROM Query t WHERE t.id = ?1") Optional getAuthor(Long queryId); diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java deleted file mode 100644 index e2f51c50..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplate.java +++ /dev/null @@ -1,50 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.persistence; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.proxy.HibernateProxy; - -import java.sql.Timestamp; -import java.util.Objects; - -@Getter -@Setter -@ToString -@RequiredArgsConstructor -@Entity -public class QueryTemplate { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(referencedColumnName = "id", name = "query_id") - @ToString.Exclude - private Query query; - - @Column(name = "label", nullable = false) - private String label; - - @Column(name = "comment") - private String comment; - - @Column(name = "last_modified", insertable = false) - private Timestamp lastModified; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - QueryTemplate that = (QueryTemplate) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java deleted file mode 100644 index 3de68ba7..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/QueryTemplateRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.persistence; - -import java.util.List; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface QueryTemplateRepository extends JpaRepository { - - @Query("SELECT qt FROM QueryTemplate qt left join Query q ON qt.query.id = q.id WHERE q.createdBy = ?1") - List findByAuthor(String authorId); - - @Query("select case when count(qt) > 0 then true else false end from QueryTemplate qt left join Query q on qt.query.id = q.id where qt.label =?1 and q.createdBy = ?2") - boolean existsQueryTemplateByLabelAndUserId(String label, String authorId); -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java deleted file mode 100644 index abd9b682..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQuery.java +++ /dev/null @@ -1,49 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.persistence; - -import jakarta.persistence.*; -import lombok.*; -import org.hibernate.proxy.HibernateProxy; - -import java.util.Objects; - -@Getter -@Setter -@ToString -@RequiredArgsConstructor -@Entity -public class SavedQuery { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @JoinColumn(referencedColumnName = "id", name = "query_id", nullable = false) - @OneToOne(fetch = FetchType.LAZY) - @ToString.Exclude - private Query query; - - @Column(name = "label", nullable = false) - private String label; - - @Column(name = "comment") - private String comment; - - @Column(name = "result_size") - private Long resultSize; - - @Override - public final boolean equals(Object o) { - if (this == o) return true; - if (o == null) return false; - Class oEffectiveClass = o instanceof HibernateProxy ? ((HibernateProxy) o).getHibernateLazyInitializer().getPersistentClass() : o.getClass(); - Class thisEffectiveClass = this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass() : this.getClass(); - if (thisEffectiveClass != oEffectiveClass) return false; - SavedQuery that = (SavedQuery) o; - return getId() != null && Objects.equals(getId(), that.getId()); - } - - @Override - public final int hashCode() { - return this instanceof HibernateProxy ? ((HibernateProxy) this).getHibernateLazyInitializer().getPersistentClass().hashCode() : getClass().hashCode(); - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java deleted file mode 100644 index 3a51334e..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/persistence/SavedQueryRepository.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.persistence; - -import java.util.Optional; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; - -public interface SavedQueryRepository extends JpaRepository { - - @Query("SELECT sq FROM SavedQuery sq left join Query q ON sq.query.id = q.id WHERE q.id = ?1") - Optional findByQueryId(Long queryId); - - @Query("select case when count(sq) > 0 then true else false end from SavedQuery sq left join Query q on sq.query.id = q.id where sq.label =?1 and q.createdBy = ?2") - boolean existsSavedQueryByLabelAndUserId(String label, String authorId); -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java index 39da211f..3a4f8673 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptor.java @@ -2,7 +2,7 @@ import de.numcodex.feasibility_gui_backend.config.WebSecurityConfig; import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssue; -import de.numcodex.feasibility_gui_backend.query.v4.QueryHandlerRestController; +import de.numcodex.feasibility_gui_backend.query.v5.FeasibilityQueryHandlerRestController; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; @@ -136,14 +136,14 @@ public void afterCompletion(HttpServletRequest request, if (request.getRequestURI() .endsWith(WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT) && response.containsHeader( - QueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY)) { + FeasibilityQueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY)) { var authentication = SecurityContextHolder.getContext() .getAuthentication(); var detailedObfuscatedResultTokenBucket = rateLimitingService.resolveViewDetailedObfuscatedBucket( authentication.getName()); detailedObfuscatedResultTokenBucket.addTokens(1); response.setHeader( - QueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY, + FeasibilityQueryHandlerRestController.HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY, null); } } diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java index 215c2804..452d4a38 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingService.java @@ -1,9 +1,6 @@ package de.numcodex.feasibility_gui_backend.query.ratelimiting; -import io.github.bucket4j.Bandwidth; -import io.github.bucket4j.BandwidthBuilder; import io.github.bucket4j.Bucket; -import io.github.bucket4j.Refill; import lombok.Getter; import java.time.Duration; diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java deleted file mode 100644 index 2a527a57..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateException.java +++ /dev/null @@ -1,31 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.templates; - -public class QueryTemplateException extends Exception { - - /** - * Constructs a new {@link QueryTemplateException} without further details. - */ - public QueryTemplateException() { - super(); - } - - /** - * Constructs a new {@link QueryTemplateException} with the specified detail message. - * - * @param message The detail message. - */ - public QueryTemplateException(String message) { - super(message); - } - - /** - * Constructs a new {@link QueryTemplateException} with the specified detail message and cause. - * - * @param message The detail message. - * @param cause The cause. - */ - public QueryTemplateException(String message, Throwable cause) { - super(message, cause); - } - -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java deleted file mode 100644 index 6a0ce08b..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandler.java +++ /dev/null @@ -1,117 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.templates; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; -import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator; -import de.numcodex.feasibility_gui_backend.query.persistence.Query; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryContent; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryContentRepository; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryRepository; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplateRepository; -import java.sql.Timestamp; -import java.time.Instant; -import jakarta.transaction.Transactional; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.dao.DataIntegrityViolationException; - -@Slf4j -@Transactional -@RequiredArgsConstructor -public class QueryTemplateHandler { - - @NonNull - private QueryHashCalculator queryHashCalculator; - - @NonNull - private ObjectMapper jsonUtil; - - @NonNull - private QueryRepository queryRepository; - - @NonNull - private QueryContentRepository queryContentRepository; - - @NonNull - private QueryTemplateRepository queryTemplateRepository; - - public Long storeTemplate(QueryTemplate queryTemplateApi, String userId) - throws QueryTemplateException { - - if (queryTemplateRepository.existsQueryTemplateByLabelAndUserId(queryTemplateApi.label(), userId)) { - throw new DataIntegrityViolationException(String.format("User %s already has a query template named %s", userId, queryTemplateApi.label())); - } - - Long queryId = storeNewQuery(queryTemplateApi.content(), userId); - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate queryTemplate - = convertApiToPersistence(queryTemplateApi, queryId); - queryTemplate = queryTemplateRepository.save(queryTemplate); - return queryTemplate.getId(); - } - - public Long storeNewQuery(StructuredQuery query, String userId) throws QueryTemplateException { - var querySerialized = serializedStructuredQuery(query); - - var queryHash = queryHashCalculator.calculateSerializedQueryBodyHash(querySerialized); - var queryBody = queryContentRepository.findByHash(queryHash) - .orElseGet(() -> { - var freshQueryBody = new QueryContent(querySerialized); - freshQueryBody.setHash(queryHash); - return queryContentRepository.save(freshQueryBody); - }); - - var queryId = persistQuery(queryBody, userId); - log.info("enqueued query '{}'", queryId); - return queryId; - } - - private Long persistQuery(QueryContent queryBody, String userId) { - var feasibilityQuery = new Query(); - feasibilityQuery.setCreatedAt(Timestamp.from(Instant.now())); - feasibilityQuery.setCreatedBy(userId); - feasibilityQuery.setQueryContent(queryBody); - return queryRepository.save(feasibilityQuery).getId(); - } - - private String serializedStructuredQuery(StructuredQuery query) throws QueryTemplateException { - if (query == null) { - throw new QueryTemplateException("Submitted query is null"); - } - try { - return jsonUtil.writeValueAsString(query); - } catch (JsonProcessingException e) { - throw new QueryTemplateException("could not serialize query in order to save it for a template", e); - } - } - - public de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate convertApiToPersistence( - QueryTemplate in, Long queryId) { - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate out = new de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate(); - - out.setQuery(queryRepository.getReferenceById(queryId)); - out.setComment(in.comment()); - out.setLabel(in.label()); - if (in.lastModified() != null) { - out.setLastModified(Timestamp.valueOf(in.lastModified())); - } - return out; - } - - public QueryTemplate convertPersistenceToApi( - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate in) - throws JsonProcessingException { - - ObjectMapper jsonUtil = new ObjectMapper(); - return QueryTemplate.builder() - .id(in.getId()) - .content(jsonUtil.readValue(in.getQuery().getQueryContent().getQueryContent(), StructuredQuery.class)) - .label(in.getLabel()) - .comment(in.getComment()) - .lastModified(in.getLastModified().toString()) - .createdBy(in.getQuery().getCreatedBy()) - .build(); - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java deleted file mode 100644 index 2068c702..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateSpringConfig.java +++ /dev/null @@ -1,25 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.templates; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryContentRepository; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryRepository; -import de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplateRepository; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - -@Configuration -public class QueryTemplateSpringConfig { - - @Bean - public QueryTemplateHandler createQueryTemplateHandler( - QueryHashCalculator queryHashCalculator, - @Qualifier("translation") ObjectMapper jsonUtil, - QueryRepository queryRepository, - QueryContentRepository queryContentRepository, - QueryTemplateRepository queryTemplateRepository) { - return new QueryTemplateHandler(queryHashCalculator, jsonUtil, - queryRepository, queryContentRepository, queryTemplateRepository); - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java deleted file mode 100644 index b95f925b..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestController.java +++ /dev/null @@ -1,160 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.v4; - -import com.fasterxml.jackson.core.JsonProcessingException; -import de.numcodex.feasibility_gui_backend.query.QueryHandlerService; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException; -import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; - -import java.security.Principal; -import java.util.ArrayList; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.validation.Valid; -import jakarta.ws.rs.core.Context; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; -import org.springframework.web.servlet.support.ServletUriComponentsBuilder; -import org.springframework.web.util.UriComponentsBuilder; - -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; - -/* -Rest Interface for the UI to send and receive query templates from the backend. -*/ -@RequestMapping(PATH_API + PATH_QUERY + PATH_TEMPLATE) -@RestController("QueryTemplateHandlerRestController-v4") -@Slf4j -@CrossOrigin(origins = "${cors.allowedOrigins}", exposedHeaders = "Location") -public class QueryTemplateHandlerRestController { - - private final QueryHandlerService queryHandlerService; - private final StructuredQueryValidation structuredQueryValidation; - private final String apiBaseUrl; - - public QueryTemplateHandlerRestController(QueryHandlerService queryHandlerService, - StructuredQueryValidation structuredQueryValidation, @Value("${app.apiBaseUrl}") String apiBaseUrl) { - this.queryHandlerService = queryHandlerService; - this.structuredQueryValidation = structuredQueryValidation; - this.apiBaseUrl = apiBaseUrl; - } - - @PostMapping(path = "") - public ResponseEntity storeQueryTemplate(@Valid @RequestBody QueryTemplate queryTemplate, - @Context HttpServletRequest httpServletRequest, Principal principal) { - - Long queryId; - try { - queryId = queryHandlerService.storeQueryTemplate(queryTemplate, principal.getName()); - } catch (QueryTemplateException e) { - log.error("Error while storing queryTemplate", e); - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } catch (DataIntegrityViolationException e) { - return new ResponseEntity<>(HttpStatus.CONFLICT); - } - - UriComponentsBuilder uriBuilder = (apiBaseUrl != null && !apiBaseUrl.isEmpty()) - ? ServletUriComponentsBuilder.fromUriString(apiBaseUrl) - : ServletUriComponentsBuilder.fromRequestUri(httpServletRequest); - - var uriString = uriBuilder.replacePath("") - .pathSegment("api", "v4", "query", "template", String.valueOf(queryId)) - .build() - .toUriString(); - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add(HttpHeaders.LOCATION, uriString); - return new ResponseEntity<>(httpHeaders, HttpStatus.CREATED); - } - - @GetMapping(path = "/{queryId}") - public ResponseEntity getQueryTemplate(@PathVariable(value = "queryId") Long queryId, - @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, - Principal principal) { - - try { - var query = queryHandlerService.getQueryTemplate(queryId, principal.getName()); - var queryTemplate = queryHandlerService.convertTemplatePersistenceToApi(query); - var queryTemplateWithInvalidCritiera = QueryTemplate.builder() - .id(queryTemplate.id()) - .content(structuredQueryValidation.annotateStructuredQuery(queryTemplate.content(), skipValidation)) - .label(queryTemplate.label()) - .comment(queryTemplate.comment()) - .lastModified(queryTemplate.lastModified()) - .createdBy(queryTemplate.createdBy()) - .isValid(queryTemplate.isValid()) - .build(); - return new ResponseEntity<>(queryTemplateWithInvalidCritiera, HttpStatus.OK); - } catch (JsonProcessingException e) { - return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); - } catch (QueryTemplateException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - } - - @GetMapping(path = "") - public ResponseEntity getQueryTemplates( - @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, - Principal principal) { - - var queries = queryHandlerService.getQueryTemplatesForAuthor(principal.getName()); - var ret = new ArrayList(); - queries.forEach(q -> { - try { - QueryTemplate convertedQuery = queryHandlerService.convertTemplatePersistenceToApi(q); - if (skipValidation) { - ret.add( - QueryTemplate.builder() - .id(convertedQuery.id()) - .label(convertedQuery.label()) - .comment(convertedQuery.comment()) - .lastModified(convertedQuery.lastModified()) - .createdBy(convertedQuery.createdBy()) - .build() - ); - } else { - ret.add( - QueryTemplate.builder() - .id(convertedQuery.id()) - .label(convertedQuery.label()) - .comment(convertedQuery.comment()) - .lastModified(convertedQuery.lastModified()) - .createdBy(convertedQuery.createdBy()) - .isValid(structuredQueryValidation.isValid(convertedQuery.content())) - .build() - ); - } - } catch (JsonProcessingException e) { - log.error("Error converting query"); - } - }); - return new ResponseEntity<>(ret, HttpStatus.OK); - } - - @PutMapping(path = "/{queryTemplateId}") - public ResponseEntity updateQueryTemplate(@PathVariable(value = "queryTemplateId") Long queryTemplateId, - @Valid @RequestBody QueryTemplate queryTemplate, - Principal principal) { - try { - queryHandlerService.updateQueryTemplate(queryTemplateId, queryTemplate, principal.getName()); - return new ResponseEntity<>(HttpStatus.OK); - } catch (QueryTemplateException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - } - - @DeleteMapping(path = "/{queryTemplateId}") - public ResponseEntity deleteQueryTemplate(@PathVariable(value = "queryTemplateId") Long queryTemplateId, - Principal principal) { - try { - queryHandlerService.deleteQueryTemplate(queryTemplateId, principal.getName()); - return new ResponseEntity<>(HttpStatus.OK); - } catch (QueryTemplateException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestController.java new file mode 100644 index 00000000..66550509 --- /dev/null +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestController.java @@ -0,0 +1,237 @@ +package de.numcodex.feasibility_gui_backend.query.v5; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.numcodex.feasibility_gui_backend.query.api.Crtdl; +import de.numcodex.feasibility_gui_backend.query.api.Dataquery; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryException; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryHandler; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryStorageFullException; +import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.Context; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponentsBuilder; + +import java.security.Principal; +import java.util.ArrayList; + +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; + +/* +Rest Interface for the UI to send and receive dataqueries from the backend. +*/ +@RequestMapping(PATH_API + PATH_QUERY + PATH_DATA) +@RestController("DataqueryHandlerRestController-v5") +@Slf4j +@CrossOrigin(origins = "${cors.allowedOrigins}", exposedHeaders = "Location") +public class DataqueryHandlerRestController { + + private final DataqueryHandler dataqueryHandler; + private final StructuredQueryValidation structuredQueryValidation; + private final String apiBaseUrl; + + public DataqueryHandlerRestController(DataqueryHandler dataqueryHandler, + StructuredQueryValidation structuredQueryValidation, @Value("${app.apiBaseUrl}") String apiBaseUrl) { + this.dataqueryHandler = dataqueryHandler; + this.structuredQueryValidation = structuredQueryValidation; + this.apiBaseUrl = apiBaseUrl; + } + + @PostMapping(path = "") + public ResponseEntity storeDataquery(@RequestBody Dataquery dataquery, + @Context HttpServletRequest httpServletRequest, Principal principal) { + + Long dataqueryId; + try { + dataqueryId = dataqueryHandler.storeDataquery(dataquery, principal.getName()); + } catch (DataqueryException e) { + log.error("Error while storing dataquery", e); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } catch (DataqueryStorageFullException e) { + return new ResponseEntity<>("storage exceeded", HttpStatus.FORBIDDEN); + } + + var dataquerySlots = dataqueryHandler.getDataquerySlotsJson(principal.getName()); + + UriComponentsBuilder uriBuilder = (apiBaseUrl != null && !apiBaseUrl.isEmpty()) + ? ServletUriComponentsBuilder.fromUriString(apiBaseUrl) + : ServletUriComponentsBuilder.fromRequestUri(httpServletRequest); + + var uriString = uriBuilder.replacePath("") + .pathSegment("api", "v5", "query", "data", String.valueOf(dataqueryId)) + .build() + .toUriString(); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.LOCATION, uriString); + return new ResponseEntity<>(dataquerySlots, httpHeaders, HttpStatus.CREATED); + } + + @GetMapping(path = "/{dataqueryId}") + public ResponseEntity getDataquery(@PathVariable(value = "dataqueryId") Long dataqueryId, + @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, + Principal principal) { + + try { + var dataquery = dataqueryHandler.getDataqueryById(dataqueryId, principal.getName()); + var dataqueryWithInvalidCriteria = Dataquery.builder() + .id(dataquery.id()) + .content( + Crtdl.builder() + .display(dataquery.content().display()) + .version(dataquery.content().version()) + .dataExtraction(dataquery.content().dataExtraction()) + .cohortDefinition(structuredQueryValidation.annotateStructuredQuery(dataquery.content().cohortDefinition(), skipValidation)) + .build() + ) + .label(dataquery.label()) + .comment(dataquery.comment()) + .lastModified(dataquery.lastModified()) + .createdBy(dataquery.createdBy()) + .isValid(dataquery.isValid()) + .resultSize(dataquery.resultSize()) + .build(); + return new ResponseEntity<>(dataqueryWithInvalidCriteria, HttpStatus.OK); + } catch (JsonProcessingException e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping(path = "/{dataqueryId}/crtdl") + public ResponseEntity getDataqueryCrtdl(@PathVariable(value = "dataqueryId") Long dataqueryId, + @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, + Principal principal) { + + try { + var dataquery = dataqueryHandler.getDataqueryById(dataqueryId, principal.getName()); + var crtdlWithInvalidCritiera = Crtdl.builder() + .display(dataquery.content().display()) + .version(dataquery.content().version()) + .dataExtraction(dataquery.content().dataExtraction()) + .cohortDefinition(structuredQueryValidation.annotateStructuredQuery(dataquery.content().cohortDefinition(), skipValidation)) + .build(); + return new ResponseEntity<>(crtdlWithInvalidCritiera, HttpStatus.OK); + } catch (JsonProcessingException e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping(path = "") + public ResponseEntity getDataqueries( + @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, + Principal principal) { + + try { + var dataqueries = dataqueryHandler.getDataqueriesByAuthor(principal.getName()); + var ret = new ArrayList(); + dataqueries.forEach(dq -> { + if (skipValidation) { + ret.add( + Dataquery.builder() + .id(dq.id()) + .label(dq.label()) + .comment(dq.comment()) + .lastModified(dq.lastModified()) + .createdBy(dq.createdBy()) + .resultSize(dq.resultSize()) + .build() + ); + } else { + ret.add( + Dataquery.builder() + .id(dq.id()) + .label(dq.label()) + .comment(dq.comment()) + .lastModified(dq.lastModified()) + .createdBy(dq.createdBy()) + .resultSize(dq.resultSize()) + .isValid(structuredQueryValidation.isValid(dq.content().cohortDefinition())) + .build() + ); + } + }); + return new ResponseEntity<>(ret, HttpStatus.OK); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @GetMapping(path = "/by-user/{userId}") + public ResponseEntity getDataqueriesByUserId(@PathVariable(value = "userId") String userId, + @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation) { + + try { + var dataqueries = dataqueryHandler.getDataqueriesByAuthor(userId); + var ret = new ArrayList(); + dataqueries.forEach(dq -> { + if (skipValidation) { + ret.add( + Dataquery.builder() + .id(dq.id()) + .label(dq.label()) + .comment(dq.comment()) + .lastModified(dq.lastModified()) + .createdBy(dq.createdBy()) + .build() + ); + } else { + ret.add( + Dataquery.builder() + .id(dq.id()) + .label(dq.label()) + .comment(dq.comment()) + .lastModified(dq.lastModified()) + .createdBy(dq.createdBy()) + .isValid(structuredQueryValidation.isValid(dq.content().cohortDefinition())) + .build() + ); + } + }); + return new ResponseEntity<>(ret, HttpStatus.OK); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @PutMapping(path = "/{dataqueryId}") + public ResponseEntity updateDataquery(@PathVariable(value = "dataqueryId") Long dataqueryId, + @RequestBody Dataquery dataquery, + Principal principal) { + try { + dataqueryHandler.updateDataquery(dataqueryId, dataquery, principal.getName()); + var dataquerySlots = dataqueryHandler.getDataquerySlotsJson(principal.getName()); + return new ResponseEntity<>(dataquerySlots, HttpStatus.OK); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } catch (JsonProcessingException e) { + return new ResponseEntity<>(HttpStatus.UNPROCESSABLE_ENTITY); + } catch (DataqueryStorageFullException e) { + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + } + + @DeleteMapping(path = "/{dataqueryId}") + public ResponseEntity deleteDataquery(@PathVariable(value = "dataqueryId") Long dataqueryId, + Principal principal) { + try { + dataqueryHandler.deleteDataquery(dataqueryId, principal.getName()); + return new ResponseEntity<>(HttpStatus.OK); + } catch (DataqueryException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping("/query-slots") + public ResponseEntity getDataquerySlots(Principal principal) { + return new ResponseEntity<>(dataqueryHandler.getDataquerySlotsJson(principal.getName()), HttpStatus.OK); + } +} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestController.java similarity index 61% rename from src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java rename to src/main/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestController.java index 055d9bbc..1d3e4f27 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestController.java @@ -1,6 +1,5 @@ -package de.numcodex.feasibility_gui_backend.query.v4; +package de.numcodex.feasibility_gui_backend.query.v5; -import com.fasterxml.jackson.core.JsonProcessingException; import de.numcodex.feasibility_gui_backend.config.WebSecurityConfig; import de.numcodex.feasibility_gui_backend.query.QueryHandlerService; import de.numcodex.feasibility_gui_backend.query.QueryHandlerService.ResultDetail; @@ -8,7 +7,6 @@ import de.numcodex.feasibility_gui_backend.query.api.*; import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssue; import de.numcodex.feasibility_gui_backend.query.api.status.FeasibilityIssues; -import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots; import de.numcodex.feasibility_gui_backend.query.persistence.UserBlacklist; import de.numcodex.feasibility_gui_backend.query.persistence.UserBlacklistRepository; import de.numcodex.feasibility_gui_backend.query.ratelimiting.AuthenticationHelper; @@ -18,15 +16,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.validation.Valid; import jakarta.ws.rs.core.Context; -import java.net.URI; -import java.security.Principal; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -37,17 +28,23 @@ import org.springframework.web.util.UriComponentsBuilder; import reactor.core.publisher.Mono; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_API; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_QUERY; +import java.net.URI; +import java.security.Principal; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; /* Rest Interface for the UI to send queries from the ui to the ui backend. */ -@RequestMapping(PATH_API + PATH_QUERY) -@RestController("QueryHandlerRestController-v4") +@RequestMapping(PATH_API + PATH_QUERY + PATH_FEASIBILITY) +@RestController("FeasibilityQueryHandlerRestController-v5") @Slf4j @CrossOrigin(origins = "${cors.allowedOrigins}", exposedHeaders = {HttpHeaders.LOCATION, HttpHeaders.RETRY_AFTER}) -public class QueryHandlerRestController { +public class FeasibilityQueryHandlerRestController { public static final String HEADER_X_DETAILED_OBFUSCATED_RESULT_WAS_EMPTY = "X-Detailed-Obfuscated-Result-Was-Empty"; private final QueryHandlerService queryHandlerService; @@ -83,15 +80,12 @@ public class QueryHandlerRestController { @Value("${app.privacy.threshold.sitesResult}") private int privacyThresholdSitesResult; - @Value("${app.maxSavedQueriesPerUser}") - private int maxSavedQueriesPerUser; - - public QueryHandlerRestController(QueryHandlerService queryHandlerService, - RateLimitingService rateLimitingService, - StructuredQueryValidation structuredQueryValidation, - UserBlacklistRepository userBlacklistRepository, - AuthenticationHelper authenticationHelper, - @Value("${app.apiBaseUrl}") String apiBaseUrl) { + public FeasibilityQueryHandlerRestController(QueryHandlerService queryHandlerService, + RateLimitingService rateLimitingService, + StructuredQueryValidation structuredQueryValidation, + UserBlacklistRepository userBlacklistRepository, + AuthenticationHelper authenticationHelper, + @Value("${app.apiBaseUrl}") String apiBaseUrl) { this.queryHandlerService = queryHandlerService; this.rateLimitingService = rateLimitingService; this.structuredQueryValidation = structuredQueryValidation; @@ -174,146 +168,11 @@ private URI buildResultLocationUri(HttpServletRequest httpServletRequest, : ServletUriComponentsBuilder.fromRequestUri(httpServletRequest); return uriBuilder.replacePath("") - .pathSegment("api", "v4", "query", String.valueOf(queryId)) + .pathSegment("api", "v5", "query", "feasibility", String.valueOf(queryId)) .build() .toUri(); } - @GetMapping("") - public List getQueryList( - @RequestParam(name = "filter", required = false) String filter, - @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, - Principal principal) { - var userId = principal.getName(); - var savedOnly = (filter != null && filter.equalsIgnoreCase("saved")); - var queryList = queryHandlerService.getQueryListForAuthor(userId, savedOnly); - return queryHandlerService.convertQueriesToQueryListEntries(queryList, skipValidation); - } - - @PostMapping("/{id}/saved") - public ResponseEntity saveQuery(@PathVariable("id") Long queryId, - @RequestBody SavedQuery savedQuery, Principal principal) { - - String authorId; - try { - authorId = queryHandlerService.getAuthorId(queryId); - } catch (QueryNotFoundException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - if (!authorId.equalsIgnoreCase(principal.getName())) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } - - Long amountOfSavedQueriesByUser = queryHandlerService.getAmountOfSavedQueriesByUser(authorId); - if (amountOfSavedQueriesByUser >= maxSavedQueriesPerUser) { - var issues = FeasibilityIssues.builder() - .issues(List.of(FeasibilityIssue.SAVED_QUERY_STORAGE_FULL)) - .build(); - return new ResponseEntity<>(issues, HttpStatus.FORBIDDEN); - } - - try { - queryHandlerService.saveQuery(queryId, authorId, savedQuery); - amountOfSavedQueriesByUser++; - var savedQuerySlots = SavedQuerySlots.builder() - .used(amountOfSavedQueriesByUser) - .total(maxSavedQueriesPerUser) - .build(); - return new ResponseEntity<>(savedQuerySlots, HttpStatus.OK); - } catch (DataIntegrityViolationException e) { - return new ResponseEntity<>(HttpStatus.CONFLICT); - } - } - - @GetMapping("/saved-query-slots") - public ResponseEntity getSavedQuerySlots(Principal principal) { - return new ResponseEntity<>(getSavedQuerySlotsJson(principal), HttpStatus.OK); - } - - @PutMapping("/{id}/saved") - public ResponseEntity updateSavedQuery(@PathVariable("id") Long queryId, - @RequestBody SavedQuery savedQuery, - Principal principal) { - - String authorId; - try { - authorId = queryHandlerService.getAuthorId(queryId); - } catch (QueryNotFoundException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - if (!authorId.equalsIgnoreCase(principal.getName())) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } - - try { - queryHandlerService.updateSavedQuery(queryId, savedQuery); - return new ResponseEntity<>(getSavedQuerySlotsJson(principal), HttpStatus.OK); - } catch (QueryNotFoundException e) { - return new ResponseEntity<>(getSavedQuerySlotsJson(principal), HttpStatus.NOT_FOUND); - } - - } - - @DeleteMapping("/{id}/saved") - public ResponseEntity deleteSavedQuery(@PathVariable("id") Long queryId, Principal principal) { - - String authorId; - try { - authorId = queryHandlerService.getAuthorId(queryId); - } catch (QueryNotFoundException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } - - if (!authorId.equalsIgnoreCase(principal.getName())) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } - - try { - queryHandlerService.deleteSavedQuery(queryId); - return new ResponseEntity<>(getSavedQuerySlotsJson(principal), HttpStatus.OK); - } catch (QueryNotFoundException e) { - return new ResponseEntity<>(getSavedQuerySlotsJson(principal), HttpStatus.NOT_FOUND); - } - } - - private SavedQuerySlots getSavedQuerySlotsJson(Principal principal) { - Long amountOfSavedQueriesByUser = queryHandlerService.getAmountOfSavedQueriesByUser(principal.getName()); - - return SavedQuerySlots.builder() - .used(amountOfSavedQueriesByUser) - .total(maxSavedQueriesPerUser) - .build(); - } - - @GetMapping("/by-user/{id}") - public List getQueryListForUser( - @PathVariable("id") String userId, - @RequestParam(name = "filter", required = false) String filter) { - var savedOnly = (filter != null && filter.equalsIgnoreCase("saved")); - var queryList = queryHandlerService.getQueryListForAuthor(userId, savedOnly); - return queryHandlerService.convertQueriesToQueryListEntries(queryList, true); - } - - @GetMapping("/{id}") - public ResponseEntity getQuery(@PathVariable("id") Long queryId, - @RequestParam(value = "skip-validation", required = false, defaultValue = "false") boolean skipValidation, - Authentication authentication) throws JsonProcessingException { - if (!hasAccess(queryId, authentication)) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } - var query = queryHandlerService.getQuery(queryId); - var annotatedQuery = Query.builder() - .id(query.id()) - .content(structuredQueryValidation.annotateStructuredQuery(query.content(), skipValidation)) - .label(query.label()) - .comment(query.comment()) - .totalNumberOfPatients(query.totalNumberOfPatients()) - .build(); - return new ResponseEntity<>(annotatedQuery, HttpStatus.OK); - } - @GetMapping("/{id}" + WebSecurityConfig.PATH_DETAILED_RESULT) public QueryResult getDetailedQueryResult(@PathVariable("id") Long queryId) { return queryHandlerService.getQueryResult(queryId, ResultDetail.DETAILED); @@ -386,19 +245,6 @@ public ResponseEntity getSummaryQueryResult( return new ResponseEntity<>(queryResult, HttpStatus.OK); } - @GetMapping("/{id}" + WebSecurityConfig.PATH_CONTENT) - public ResponseEntity getQueryContent( - @PathVariable("id") Long queryId, - Authentication authentication) - throws JsonProcessingException { - - if (!hasAccess(queryId, authentication)) { - return new ResponseEntity<>(HttpStatus.FORBIDDEN); - } - var queryContent = queryHandlerService.getQueryContent(queryId); - return new ResponseEntity<>(queryContent, HttpStatus.OK); - } - @PostMapping("/validate") public ResponseEntity validateStructuredQuery( @Valid @RequestBody StructuredQuery query) { diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/MappingNotFoundException.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/MappingNotFoundException.java deleted file mode 100644 index e73a89f5..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/MappingNotFoundException.java +++ /dev/null @@ -1,12 +0,0 @@ -package de.numcodex.feasibility_gui_backend.terminology; - - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.NOT_FOUND) -public class MappingNotFoundException extends RuntimeException { - - public MappingNotFoundException() { - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/NodeNotFoundException.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/NodeNotFoundException.java deleted file mode 100644 index 5960ce6b..00000000 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/NodeNotFoundException.java +++ /dev/null @@ -1,11 +0,0 @@ -package de.numcodex.feasibility_gui_backend.terminology; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(value = HttpStatus.NOT_FOUND) -public class NodeNotFoundException extends RuntimeException { - - public NodeNotFoundException() { - } -} diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestController.java similarity index 94% rename from src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestController.java rename to src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestController.java index 122db8ed..8c092b69 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestController.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.terminology.v4; +package de.numcodex.feasibility_gui_backend.terminology.v5; import de.numcodex.feasibility_gui_backend.terminology.api.CcSearchResult; import de.numcodex.feasibility_gui_backend.terminology.api.CodeableConceptEntry; @@ -11,7 +11,7 @@ import java.util.List; @RestController -@RequestMapping("api/v4/codeable-concept") +@RequestMapping("api/v5/codeable-concept") @ConditionalOnExpression("${app.elastic.enabled}") @CrossOrigin public class CodeableConceptRestController { diff --git a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestController.java b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestController.java similarity index 97% rename from src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestController.java rename to src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestController.java index 546b20e7..9db17dfa 100644 --- a/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestController.java +++ b/src/main/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestController.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.terminology.v4; +package de.numcodex.feasibility_gui_backend.terminology.v5; import de.numcodex.feasibility_gui_backend.terminology.TerminologyService; @@ -18,7 +18,7 @@ */ -@RequestMapping("api/v4/terminology") +@RequestMapping("api/v5/terminology") @RestController @CrossOrigin @ConditionalOnExpression("${app.elastic.enabled}") diff --git a/src/main/resources/db/migration/V10__replace_templates_with_dataqueries.sql b/src/main/resources/db/migration/V10__replace_templates_with_dataqueries.sql new file mode 100644 index 00000000..7b52a340 --- /dev/null +++ b/src/main/resources/db/migration/V10__replace_templates_with_dataqueries.sql @@ -0,0 +1,35 @@ +CREATE TABLE dataquery +( + id SERIAL PRIMARY KEY, + created_by TEXT NOT NULL, + label TEXT NOT NULL, + comment TEXT, + crtdl TEXT, + last_modified TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + result_size INTEGER +); + +insert into dataquery (created_by, label, comment, crtdl, last_modified, result_size) +select q.created_by, + sq.label, + sq.comment, + CONCAT('{"version":null,"display":"","cohortDefinition":', qc.query_content, '}'), + q.created_at, + sq.result_size +from saved_query sq + left join query q on sq.query_id = q.id + left join query_content qc on q.query_content_id = qc.id ; + +insert into dataquery (created_by, label, comment, crtdl, last_modified) +select q.created_by, + qt.label, + qt.comment, + CONCAT('{"version":null,"display":"","cohortDefinition":', qc.query_content, '}'), + q.created_at +from query_template qt + left join query q on qt.query_id = q.id + left join query_content qc on q.query_content_id = qc.id ; + +-- Not sure if we should already delete those tables or wait for the next release to check if everything was converted as expected +-- drop table query_template; +-- drop table saved_query; \ No newline at end of file diff --git a/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/ccdl-schema.json b/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/ccdl-schema.json new file mode 100644 index 00000000..1d5526fd --- /dev/null +++ b/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/ccdl-schema.json @@ -0,0 +1,449 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://medizininformatik-initiative.de/fdpg/ClinicalCohortDefinitionLanguage/v1/schema", + "$defs": { + "termCode": { + "type": "object", + "description": "The termCode defines a concept based on a coding system (i.e. LOINC). The triplet of code, system and version identify the concept.", + "properties": { + "code": { + "type": "string" + }, + "system": { + "type": "string" + }, + "version": { + "type": "string" + }, + "display": { + "type": "string" + } + }, + "required": [ + "code", + "system", + "display" + ], + "additionalProperties": false + }, + "criterion": { + "type": "object", + "properties": { + "context": { + "$ref": "#/$defs/termCode" + }, + "termCodes": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/termCode" + } + }, + "valueFilter": { + "$ref": "#/$defs/valueFilter" + }, + "attributeFilters": { + "$ref": "#/$defs/attributeFilters" + }, + "timeRestriction": { + "$ref": "#/$defs/timeRestriction" + } + }, + "required": [ + "termCodes", + "context" + ], + "additionalProperties": false + }, + "timeRestriction": { + "type": "object", + "description": "TimeRestirction specify the interval within the critiera has to be fullfilled. An intersection of the criterias interval with the interval defined in this timeRestriction is sufficient", + "properties": { + "afterDate": { + "type": "string", + "format": "date", + "description": "afterDate is the start of the date interval that further limits the resources." + }, + "beforeDate": { + "type": "string", + "format": "date", + "description": "beforeDate is the end of the date interval that further limits the resources." + } + }, + "anyOf": [ + { + "required": [ + "afterDate" + ] + }, + { + "required": [ + "beforeDate" + ] + } + ], + "additionalProperties": false + }, + "unit": { + "type": "object", + "title": "UCUM Unit", + "description": "The unit is a ucum unit (https://ucum.org/trac)", + "properties": { + "code": { + "type": "string" + }, + "display": { + "type": "string" + } + }, + "required": [ + "code", + "display" + ], + "additionalProperties": false + }, + "attributeFilters": { + "type": "array", + "items": { + "$ref": "#/$defs/attributeFilter" + } + }, + "valueFilter": { + "type": "object", + "properties": { + "type": { + "enum": [ + "concept", + "quantity-comparator", + "quantity-range" + ] + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "concept" + } + } + }, + "then": { + "properties": { + "type": { + "const": "concept" + }, + "selectedConcepts": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/termCode" + } + } + }, + "required": [ + "type", + "selectedConcepts" + ], + "additionalProperties": false + } + }, + { + "if": { + "properties": { + "type": { + "const": "quantity-comparator" + } + } + }, + "then": { + "properties": { + "type": { + "const": "quantity-comparator" + }, + "comparator": { + "enum": [ + "gt", + "ge", + "lt", + "le", + "eq", + "ne" + ] + }, + "value": { + "type": "number" + }, + "unit": { + "$ref": "#/$defs/unit" + } + }, + "required": [ + "type", + "comparator", + "value" + ], + "additionalProperties": false + } + }, + { + "if": { + "properties": { + "type": { + "const": "quantity-range" + } + } + }, + "then": { + "properties": { + "type": { + "const": "quantity-range" + }, + "minValue": { + "type": "number" + }, + "maxValue": { + "type": "number" + }, + "unit": { + "$ref": "#/$defs/unit" + } + }, + "required": [ + "type", + "minValue", + "maxValue" + ], + "additionalProperties": false + } + } + ] + }, + "attributeFilter": { + "type": "object", + "properties": { + "type": { + "enum": [ + "concept", + "quantity-comparator", + "quantity-range", + "reference" + ] + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "if": { + "properties": { + "type": { + "const": "concept" + } + } + }, + "then": { + "properties": { + "attributeCode": { + "$ref": "#/$defs/termCode" + }, + "type": { + "const": "concept" + }, + "selectedConcepts": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/termCode" + } + } + }, + "required": [ + "type", + "selectedConcepts", + "attributeCode" + ], + "additionalProperties": false + } + }, + { + "if": { + "properties": { + "type": { + "const": "quantity-comparator" + } + } + }, + "then": { + "properties": { + "attributeCode": { + "$ref": "#/$defs/termCode" + }, + "type": { + "const": "quantity-comparator" + }, + "comparator": { + "enum": [ + "gt", + "ge", + "lt", + "le", + "eq", + "ne" + ] + }, + "value": { + "type": "number" + }, + "unit": { + "$ref": "#/$defs/unit" + } + }, + "required": [ + "type", + "comparator", + "value", + "attributeCode" + ], + "additionalProperties": false + } + }, + { + "if": { + "properties": { + "type": { + "const": "quantity-range" + } + } + }, + "then": { + "properties": { + "attributeCode": { + "$ref": "#/$defs/termCode" + }, + "type": { + "const": "quantity-range" + }, + "minValue": { + "type": "number" + }, + "maxValue": { + "type": "number" + }, + "unit": { + "$ref": "#/$defs/unit" + } + }, + "required": [ + "type", + "minValue", + "maxValue", + "attributeCode" + ], + "additionalProperties": false + } + }, + { + "if": { + "properties": { + "type": { + "const": "reference" + } + } + }, + "then": { + "properties": { + "attributeCode": { + "$ref": "#/$defs/termCode" + }, + "type": { + "const": "reference" + }, + "criteria": { + "type": "array", + "minItems": 1, + "items": { + "allOf": [ + { + "properties": { + "attributeFilters": { + "type": "array", + "items": { + "$ref": "#/$defs/attributeFilter", + "not": { + "properties": { + "type": { + "const": "reference" + } + } + } + } + } + } + }, + { + "$ref": "#/$defs/criterion" + } + ] + } + } + }, + "required": [ + "type", + "criteria", + "attributeCode" + ], + "additionalProperties": false + } + } + ] + } + }, + "title": "cohortDefinition", + "description": "Within a CCDL the inclusion and exclusion criteria are conjuncted with AND NOT", + "type": "object", + "properties": { + "version": { + "type": "string", + "format": "uri" + }, + "inclusionCriteria": { + "type": "array", + "minItems": 1, + "description": "All elements within the array are conjuncted with an AND operator", + "items": { + "type": "array", + "minItems": 1, + "description": "All elements within the array are conjuncted with an OR operator", + "items": { + "$ref": "#/$defs/criterion" + } + } + }, + "exclusionCriteria": { + "type": "array", + "minItems": 1, + "description": "All elements within the array are conjuncted with an OR operator", + "items": { + "type": "array", + "minItems": 1, + "description": "All elements within the array are conjuncted with an AND operator", + "items": { + "$ref": "#/$defs/criterion" + } + } + }, + "display": { + "type": "string" + } + }, + "required": [ + "version", + "inclusionCriteria" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/crtdl-schema.json b/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/crtdl-schema.json new file mode 100644 index 00000000..7949ae90 --- /dev/null +++ b/src/main/resources/de/numcodex/feasibility_gui_backend/query/api/validation/crtdl-schema.json @@ -0,0 +1,140 @@ +{ + "$schema": "http://json-schema.org/to-be-done/schema#", + "$id": "http://example.com/schema/data-extraction-schema.json", + "type": "object", + "properties": { + "version": { + "type": "string", + "format": "uri", + "description": "Version identifier with a reference to a schema definition." + }, + "display": { + "type": "string", + "description": "A display string." + }, + "cohortDefinition": { + "$ref": "ccdl-schema.json#/definitions/cohortDefinition", + "description": "Reference to a cohort definition in CCDL schemas." + }, + "dataExtraction": { + "type": "object", + "properties": { + "attributeGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "groupReference": { + "type": "string", + "format": "uri" + }, + "includeReferenceOnly": { + "type": "boolean" + }, + "attributes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attributeRef": { + "type": "string" + }, + "mustHave": { + "type": "boolean" + }, + "linkedGroups": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "attributeRef", + "mustHave" + ] + } + }, + "filter": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "codes": { + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "system": { + "type": "string", + "format": "uri" + }, + "display": { + "type": "string" + }, + "version": { + "type": "string" + } + }, + "required": [ + "code", + "system", + "display" + ], + "additionalProperties": false + } + }, + "start": { + "type": "string", + "format": "date" + }, + "end": { + "type": "string", + "format": "date" + } + }, + "required": [ + "type", + "name" + ], + "additionalProperties": false + } + } + }, + "required": [ + "groupReference", + "attributes", + "id" + ], + "additionalProperties": false + } + } + }, + "required": [ + "attributeGroups" + ], + "additionalProperties": false + } + }, + "required": [ + "version", + "cohortDefinition", + "dataExtraction" + ], + "additionalProperties": false +} diff --git a/src/main/resources/static/v3/api-docs/swagger.yaml b/src/main/resources/static/v3/api-docs/swagger.yaml index f4aabe82..66812aa4 100644 --- a/src/main/resources/static/v3/api-docs/swagger.yaml +++ b/src/main/resources/static/v3/api-docs/swagger.yaml @@ -1,10 +1,10 @@ -openapi: 3.1.0 +openapi: 3.1.1 info: title: Dataportal Backend REST API license: name: Apache 2.0 url: http://www.apache.org/licenses/LICENSE-2.0.html - version: 6.0.0 + version: 7.0.0 externalDocs: description: Check out the github repository url: https://github.com/medizininformatik-initiative/feasibility-backend @@ -12,12 +12,12 @@ servers: - url: https://to.be.defined variables: basePath: - default: /api/v4 + default: /api/v5 tags: - - name: query - description: operations for queries - - name: templates - description: operations to work with query templates + - name: dataquery + description: operations for dataqueries + - name: feasibility + description: operations for feasibility queries - name: terminology description: operations to work with the ontology - name: codeable concepts @@ -27,63 +27,54 @@ tags: - name: intrinsics description: Offers intrinsic information about this application. paths: - /query: + /query/data: post: tags: - - query - summary: Create a query in the broker - description: The query will be spawned in the broker and directly be dispatched - operationId: runQuery + - dataquery + summary: Store a dataquery in the backend. + description: The query will only be stored, nothing will be dispatched. Does not have to pass any validation and may be incomplete. May or may not contain result. + operationId: storeDataQuery requestBody: - description: Structured query to create and dispatch + description: Dataquery content: application/json: schema: - $ref: "#/components/schemas/StructuredQuery" + $ref: "#/components/schemas/Dataquery" required: true responses: 201: - description: Query successfully dispatched + description: Dataquery successfully stored headers: Location: - description: Path to the result of your newly created query + description: Path to the result of your newly created dataquery schema: type: string - examples: - - https://to.be.defined/api/v4/query/42 - content: {} + format: url 401: description: Unauthorized - please login first - 403: - description: Forbidden - insufficient access rights 422: description: Invalid input - 429: - description: Too many requests in a given amount of time (configurable) - 500: - description: Dispatch error security: - dataportal_auth: - user - x-codegen-request-body-name: body get: tags: - - query - summary: Get the list of the calling users queries - description: This returns a list with basic information about the queries. Id, label (if present) and creation date. - operationId: getQueryList + - dataquery + summary: Get the list of the calling users dataqueries + description: This returns a list with basic information about the queries. Id, label (if present) and creation date. Basically for all parameters I'm not sure if we need them (at least right away) + operationId: getDataQueryList parameters: - name: filter in: query - description: filters query + description: filters query...either for those with linked feasibility and or results? Or by name? required: false schema: type: string enum: - - saved + - feasibility - name: skip-validation in: query - description: If true, do not validate the query and do not include a list of invalid terms + description: If true, do not validate the query and do not include a list of invalid terms. TODO - maybe invert this? required: false schema: type: boolean @@ -96,45 +87,18 @@ paths: schema: type: array items: - $ref: "#/components/schemas/QueryListEntry" + $ref: "#/components/schemas/DataQueryListEntry" 401: description: Unauthorized - please login first security: - dataportal_auth: - user - /query/validate: - post: - tags: - - query - - validation - summary: Validates a submitted (structured) query to check for schema violations or invalid termCodes - operationId: validateQuery - requestBody: - description: Structured query to validate - content: - application/json: - schema: - $ref: "#/components/schemas/StructuredQuery" - required: true - responses: - 200: - description: Query adheres to json schema. If invalid termCodes are present, they will be in the response. - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/StructuredQuery" - 400: - description: Query does not adhere to json schema - 401: - description: Unauthorized - please login first - /query/by-user/{userId}: + /query/data/by-user/{userId}: get: tags: - - query + - dataquery summary: Finds query summary (id, label, lastModified) of all queries of one user - operationId: findQueriesByUser + operationId: findDataQueriesByUser parameters: - name: userId in: path @@ -142,14 +106,13 @@ paths: required: true schema: type: string - - name: filter + - name: skip-validation in: query - description: filters query + description: If true, do not validate the query and do not include a list of invalid terms. TODO - maybe invert this? required: false schema: - type: string - enum: - - saved + type: boolean + default: false responses: 200: description: successful operation @@ -158,7 +121,7 @@ paths: schema: type: array items: - $ref: "#/components/schemas/QueryListEntry" + $ref: "#/components/schemas/DataQueryListEntry" 401: description: Unauthorized - please login first 403: @@ -168,13 +131,13 @@ paths: security: - dataportal_auth: - admin - /query/{queryId}: + /query/data/{queryId}: get: tags: - - query - summary: Read query by ID - description: Returns a single query. Contains everything known about the query, including results and structured query - operationId: getQueryById + - dataquery + summary: Read dataquery by ID + description: Returns a single dataquery. + operationId: getDataQueryById parameters: - name: queryId in: path @@ -196,7 +159,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/Query" + $ref: "#/components/schemas/Dataquery" 401: description: Unauthorized - please login first 403: @@ -206,95 +169,87 @@ paths: security: - dataportal_auth: - user - - admin - /query/{queryId}/content: - get: + put: tags: - - query - summary: Read the content (=structured query) of a query by the query id - description: Returns the structured query of a single query. - operationId: getQueryContentByQueryId - parameters: - - name: queryId - in: path - description: ID of query for which the contents shall be returned - required: true - schema: - type: integer - format: int64 + - dataquery + summary: Update a dataquery in the backend. + description: The query will be updated. It must be provided completely, there are no partial updates. + operationId: updateDataQuery + requestBody: + description: CRTDL + content: + application/json: + schema: + $ref: "#/components/schemas/Dataquery" + required: true responses: - 200: - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/StructuredQuery" + 204: + description: Dataquery successfully updated + content: {} 401: description: Unauthorized - please login first 403: description: Forbidden - insufficient access rights 404: - description: Query not found + description: The dataquery could not be found security: - dataportal_auth: - - admin - user - /query/{queryId}/summary-result: - get: + delete: tags: - - query - summary: Read query result summary by query ID - description: Returns the aggregated results to a query. There is no breakdown by site. So, the resultLines parameter of the response is de facto an array of QueryResultLines, but it will always be empty in this case. - operationId: getQueryResultSummary + - dataquery + summary: Delete a dataquery + operationId: deleteDataQuery parameters: - name: queryId in: path - description: ID of query for which the results are requested required: true schema: type: integer format: int64 responses: - 200: - description: OK + 204: + description: No content content: - application/json: - schema: - $ref: "#/components/schemas/QueryResultSummary" + {} 401: description: Unauthorized - please login first 403: description: Forbidden - insufficient access rights 404: - description: Query not found - 429: - description: Too many requests + description: The dataquery could not be found security: - dataportal_auth: - - admin - user - /query/{queryId}/detailed-result: + /query/data/{queryId}/crtdl: get: tags: - - query - summary: Read query result by ID - description: Returns results to query with the real site names - admin rights required - operationId: getQueryResultDetailed + - dataquery + summary: Read dataquery CRTDL by ID + description: Returns only the CRTDL part of a dataquery + operationId: getDataQueryCRTDLById parameters: - name: queryId in: path - description: ID of query for which the results are requested + description: ID of query to return required: true schema: type: integer format: int64 + - name: skip-validation + in: query + description: If true, do not validate the query and do not include a list of invalid terms + required: false + schema: + type: boolean + default: false responses: 200: description: OK content: application/json: schema: - $ref: "#/components/schemas/QueryResult" + $ref: "#/components/schemas/CRTDL" 401: description: Unauthorized - please login first 403: @@ -304,149 +259,119 @@ paths: security: - dataportal_auth: - admin - /query/{queryId}/detailed-obfuscated-result: - get: + - user + /query/feasibility: + post: tags: - - query - summary: Read obfuscated query result by ID - description: Returns all results to query with the site names obfuscated. - operationId: getQueryResultDetailedObfuscated + - feasibility + summary: Create a feasibility query in the broker + description: The query will be spawned in the broker and directly be dispatched. + operationId: runQuery parameters: - - name: queryId - in: path - description: ID of query for which the results are requested - required: true - schema: - type: integer - format: int64 + requestBody: + description: Structured query to create and dispatch + content: + application/json: + schema: + $ref: "#/components/schemas/StructuredQuery" + required: true responses: - 200: - description: OK - content: - application/json: + 201: + description: Query successfully dispatched + headers: + Location: + description: Path to the result of your newly created query schema: - $ref: "#/components/schemas/QueryResultObfuscated" + type: string 401: description: Unauthorized - please login first 403: description: Forbidden - insufficient access rights - 404: - description: Query not found + 422: + description: Invalid input 429: - description: Too many requests + description: Too many requests in a given amount of time (configurable) + 500: + description: Dispatch error security: - dataportal_auth: - - admin - user - /query/detailed-obfuscated-result-rate-limit: + /query/feasibility/{queryId}/summary-result: get: - summary: get the rate limit for detailed obfuscated results - operationId: getDetailedObfuscatedResultRateLimit - responses: - 200: - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/QueryResultRateLimit" - /query/{queryId}/saved: - post: tags: - - query - summary: Store additional information to an executed query - operationId: saveQuery + - feasibility + summary: Read query result summary by query ID + description: Returns the aggregated results to a query. There is no breakdown by site. So, the resultLines parameter of the response is de facto an array of QueryResultLines, but it will always be empty in this case. + operationId: getQueryResultSummary parameters: - name: queryId in: path - description: ID of query to which the additional information shall be saved + description: ID of query for which the results are requested required: true schema: type: integer format: int64 - requestBody: - description: The additionally needed information to save a query - content: - application/json: - schema: - $ref: "#/components/schemas/SavedQuery" - required: true responses: 200: - description: Saved Query successfully stored + description: OK content: application/json: schema: - $ref: "#/components/schemas/SavedQuerySlots" + $ref: "#/components/schemas/QueryResultSummary" 401: description: Unauthorized - please login first 403: - description: Forbidden - insufficient access rights, or no free slots left - content: - application/json: - schema: - type: object - properties: - issues: - type: array - items: - type: object - properties: - message: - type: string - type: - type: string - code: - type: string - severity: - type: string + description: Forbidden - insufficient access rights 404: - description: The query for which the additional information should be stored could not be found - 409: - description: Query has already been saved + description: Query not found + 429: + description: Too many requests security: - dataportal_auth: + - admin - user - put: + /query/feasibility/{queryId}/detailed-result: + get: tags: - - query - summary: Update a saved query. Only label and comment can be changed. - operationId: updateSavedQuery + - feasibility + summary: Read query result by ID + description: Returns results to query with the real site names - admin rights required + operationId: getQueryResultDetailed parameters: - name: queryId in: path - description: ID of the saved query to update + description: ID of query for which the results are requested required: true schema: type: integer format: int64 - requestBody: - description: The additionally needed information to update a query - content: - application/json: - schema: - $ref: "#/components/schemas/SavedQuery" - required: true responses: 200: - description: Saved Query successfully updated + description: OK content: application/json: schema: - $ref: "#/components/schemas/SavedQuerySlots" + $ref: "#/components/schemas/QueryResult" 401: description: Unauthorized - please login first 403: description: Forbidden - insufficient access rights 404: - description: The query to be updated could not be found - delete: + description: Query not found + security: + - dataportal_auth: + - admin + /query/feasibility/{queryId}/detailed-obfuscated-result: + get: tags: - - query - summary: Remove a saved query from a given query - operationId: deleteSavedQuery + - feasibility + summary: Read obfuscated query result by ID + description: Returns all results to query with the site names obfuscated. + operationId: getQueryResultDetailedObfuscated parameters: - name: queryId in: path + description: ID of query for which the results are requested required: true schema: type: integer @@ -457,181 +382,75 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/SavedQuerySlots" + $ref: "#/components/schemas/QueryResultObfuscated" 401: description: Unauthorized - please login first 403: description: Forbidden - insufficient access rights 404: - description: The query for which the additional information should be stored could not be found - /query/saved-query-slots: - get: - tags: - - query - summary: Show how many saved query slots a user already used and how many he has left. - operationId: getSavedQuerySlots - responses: - 200: - description: OK - content: - application/json: - schema: - $ref: "#/components/schemas/SavedQuerySlots" - security: - - dataportal_auth: - - user - /query/template: - post: - tags: - - templates - summary: Store a structured query with additional label and comment - operationId: storeTemplate - requestBody: - description: Query template to persist - content: - application/json: - schema: - $ref: "#/components/schemas/QueryTemplate" - required: true - responses: - 201: - description: Query template successfully stored - headers: - Location: - description: Path to the newly stored query template - schema: - type: string - format: uri - examples: - - https://to.be.defined/api/v4/query/template/42 - 401: - description: Unauthorized - please login first - 403: - description: Forbidden - insufficient access rights - 409: - description: Query with the same label exists for this user + description: Query not found + 429: + description: Too many requests security: - dataportal_auth: + - admin - user + /query/feasibility/detailed-obfuscated-result-rate-limit: get: tags: - - templates - summary: Read list of query templates - description: Returns the list of all query templates of the current user - operationId: getQueryTemplateList - parameters: - - name: skip-validation - in: query - description: If true, do not validate the query and do not include a list of invalid terms - required: false - schema: - type: boolean - default: false + - feasibility + summary: get the rate limit for detailed obfuscated results + operationId: getDetailedObfuscatedResultRateLimit responses: 200: description: OK content: application/json: schema: - items: - $ref: "#/components/schemas/QueryTemplateListItem" - 401: - description: Unauthorized - please login first - security: - - dataportal_auth: - - user - /query/template/{queryTemplateId}: + $ref: "#/components/schemas/QueryResultRateLimit" + /query/data/query-slots: get: tags: - - templates - summary: Read a query template - description: Returns the query template with the given id - operationId: getQueryTemplate - parameters: - - name: queryTemplateId - in: path - description: ID of the requested query template - required: true - schema: - type: integer - format: int64 - - name: skip-validation - in: query - description: If true, do not validate the query and do not include a list of invalid terms - required: false - schema: - type: boolean - default: false + - dataquery + summary: Show how many data query slots a user already used and how many he has left. + operationId: getDataQuerySlots responses: 200: description: OK content: application/json: schema: - items: - $ref: "#/components/schemas/QueryTemplate" - 401: - description: Unauthorized - please login first - 404: - description: Query not found (or user has no access) + $ref: "#/components/schemas/DataQuerySlots" security: - dataportal_auth: - user - put: + /query/validate: + post: tags: - - templates - summary: Update a query template - description: Update the label and comment of a query template - operationId: updateQueryTemplate - parameters: - - name: queryTemplateId - in: path - description: ID of the query template to update - required: true - schema: - type: integer - format: int64 + - dataquery + summary: Validates a submitted CRTDL or just CCDL? to check for schema violations or invalid termCodes + description: It could be useful to split this in ccdl and dataquery or at least offer a parameter to determine which should be checked. + operationId: validateQuery requestBody: - description: Query template to persist + description: Query to validate content: application/json: schema: - $ref: "#/components/schemas/QueryTemplate" + $ref: "#/components/schemas/CRTDL" required: true responses: 200: - description: OK - 401: - description: Unauthorized - please login first - 404: - description: Query not found (or user has no access) - security: - - dataportal_auth: - - user - delete: - tags: - - templates - summary: Delete a query template - description: Deletes the query template with the given id - operationId: deleteQueryTemplate - parameters: - - name: queryTemplateId - in: path - description: ID of the requested query template - required: true - schema: - type: integer - format: int64 - responses: - 200: - description: OK + description: Query adheres to json schema. If invalid termCodes are present, they will be in the response. + content: + application/json: + schema: + type: array + items: + $ref: "#/components/schemas/CRTDL" + 400: + description: Query does not adhere to json schema 401: description: Unauthorized - please login first - 404: - description: Query not found (or user has no access) - security: - - dataportal_auth: - - user /terminology/search/filter: get: tags: @@ -672,7 +491,7 @@ paths: description: The term to search for. In case of an empty searchterm, return the first page of the whole index, sorted alphabetically schema: type: string - example: Diabetes Mellitus + examples: Diabetes Mellitus - name: contexts in: query description: Limit the search to any of the given contexts @@ -956,7 +775,52 @@ paths: - intrinsics components: schemas: - QueryListEntry: + Dataquery: + type: object + required: + - info + - crtdl + properties: + label: + type: string + examples: + - my query + comment: + type: string + examples: + - this can be a longer text explaining what this query is for + totalNumberOfResults: + type: integer + crtdl: + type: object + $ref: "#/components/schemas/CRTDL" + CRTDL: + type: object + properties: + display: + type: string + examples: + - Example CRTDL + cohortDefinition: + type: object + $ref: "#/components/schemas/StructuredQuery" + dataExtraction: + type: object + $ref: "#/components/schemas/DataExtractionQuery" + DataExtractionQuery: + type: object + properties: + id: + type: string + examples: + - 123 + attributeGroups: + type: array + items: + type: string + examples: + - to be done correctly...for now just a placeholder because it doesn't matter for this conversation + DataQueryListEntry: type: object required: - id @@ -972,10 +836,22 @@ components: createdAt: type: string format: date-time - totalNumberOfPatients: + totalNumberOfResults: type: integer - isValid: - type: boolean + ccdl: + type: object + properties: + exists: + type: boolean + isValid: + type: boolean + dataSelection: + type: object + properties: + exists: + type: boolean + isValid: + type: boolean Query: type: object required: @@ -1059,59 +935,6 @@ components: remaining: type: number format: int64 - QueryTemplateListItem: - type: object - required: - - label - properties: - id: - type: integer - format: int64 - label: - type: string - description: The 'name' of the query. Is assigned by the user via GUI. - examples: - - my-query-1 - comment: - type: string - description: A more detailed information about the query. Is also assigned by the user via GUI. - examples: - - I wanted to see how many patients I could find for my study XYZ - lastModified: - type: string - format: date-time - createdBy: - type: string - description: Keycloak id of the user who created the query - isValid: - type: boolean - description: is the query valid? - QueryTemplate: - type: object - required: - - label - properties: - id: - type: integer - format: int64 - label: - type: string - description: The 'name' of the query. Is assigned by the user via GUI. - examples: - - my-query-1 - comment: - type: string - description: A more detailed information about the query. Is also assigned by the user via GUI. - examples: - - I wanted to see how many patients I could find for my study XYZ - content: - $ref: "#/components/schemas/StructuredQuery" - lastModified: - type: string - format: date-time - createdBy: - type: string - description: Keycloak id of the user who created the query StructuredQuery: type: object required: @@ -1136,28 +959,7 @@ components: type: array items: $ref: "#/components/schemas/CriterionList" - SavedQuery: - type: object - required: - - label - properties: - label: - type: string - description: The 'name' of the query. Is assigned by the user via GUI. - examples: - - my-query-1 - comment: - type: string - description: A more detailed information about the query. Is also assigned by the user via GUI. - examples: - - I wanted to see how many patients I could find for my study XYZ - totalNumberOfPatients: - type: integer - format: int64 - description: The number of results that were found for this query. - examples: - - 12345 - SavedQuerySlots: + DataQuerySlots: type: object required: - used @@ -1165,12 +967,12 @@ components: properties: used: type: integer - description: The amount of used saved query slots for a user. + description: The amount of used dataquery slots for a user. examples: - 2 total: type: integer - description: The total amount of saved query slots per user. + description: The total amount of dataquery slots per user. examples: - 10 TermCode: diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestControllerIT.java similarity index 98% rename from src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java rename to src/test/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestControllerIT.java index e0664699..312bc87d 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/dse/v4/DseRestControllerIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/dse/v5/DseRestControllerIT.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.dse.v4; +package de.numcodex.feasibility_gui_backend.dse.v5; import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.dse.DseService; diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceIT.java index a639d647..4e08333c 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceIT.java @@ -6,19 +6,16 @@ import de.numcodex.feasibility_gui_backend.common.api.TermCode; import de.numcodex.feasibility_gui_backend.query.QueryHandlerService.ResultDetail; import de.numcodex.feasibility_gui_backend.query.api.QueryResultLine; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.SavedQuery; import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; import de.numcodex.feasibility_gui_backend.query.broker.BrokerSpringConfig; import de.numcodex.feasibility_gui_backend.query.collect.QueryCollectSpringConfig; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataquerySpringConfig; import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchSpringConfig; import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator; import de.numcodex.feasibility_gui_backend.query.persistence.*; import de.numcodex.feasibility_gui_backend.query.result.ResultLine; import de.numcodex.feasibility_gui_backend.query.result.ResultService; import de.numcodex.feasibility_gui_backend.query.result.ResultServiceSpringConfig; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateHandler; import de.numcodex.feasibility_gui_backend.query.translation.QueryTranslatorSpringConfig; import java.net.URI; @@ -35,7 +32,6 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.context.annotation.Import; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.testcontainers.junit.jupiter.Testcontainers; @@ -55,8 +51,8 @@ QueryDispatchSpringConfig.class, QueryCollectSpringConfig.class, QueryHandlerService.class, - QueryTemplateHandler.class, - ResultServiceSpringConfig.class + ResultServiceSpringConfig.class, + DataquerySpringConfig.class }) @DataJpaTest( properties = { @@ -76,9 +72,6 @@ public class QueryHandlerServiceIT { public static final String SITE_NAME_2 = "site-name-114610"; public static final String CREATOR = "creator-114634"; public static final long UNKNOWN_QUERY_ID = 9999999L; - public static final String LABEL = "some-label"; - public static final String COMMENT = "some-comment"; - public static final String TIME_STRING = "1969-07-20 20:17:40.0"; @Autowired private QueryHandlerService queryHandlerService; @@ -86,9 +79,6 @@ public class QueryHandlerServiceIT { @Autowired private QueryRepository queryRepository; - @Autowired - private SavedQueryRepository savedQueryRepository; - @Autowired private QueryContentRepository queryContentRepository; @@ -290,7 +280,7 @@ public void testGetQuery_nullOnNotFound() throws JsonProcessingException { } @Test - public void testGetQuery_succeedsWithNoSavedQuery() throws JsonProcessingException { + public void testGetQuery_succeess() throws JsonProcessingException { var queryContentString = jsonUtil.writeValueAsString(createValidStructuredQuery("foo")); var queryContentHash = queryHashCalculator.calculateSerializedQueryBodyHash(queryContentString); var queryContent = new QueryContent(queryContentString); @@ -307,26 +297,6 @@ public void testGetQuery_succeedsWithNoSavedQuery() throws JsonProcessingExcepti assertThat(queryFromDb.content().inclusionCriteria()).isEqualTo(createValidStructuredQuery("foo").inclusionCriteria()); } - @Test - public void testGetQuery_succeedsWithSavedQuery() throws JsonProcessingException { - var queryContentString = jsonUtil.writeValueAsString(createValidStructuredQuery("foo")); - var queryContentHash = queryHashCalculator.calculateSerializedQueryBodyHash(queryContentString); - var queryContent = new QueryContent(queryContentString); - queryContent.setHash(queryContentHash); - var query = new Query(); - query.setCreatedBy(CREATOR); - query.setQueryContent(queryContent); - var queryId = queryRepository.save(query).getId(); - var savedQuery = new SavedQuery(LABEL, COMMENT, 150L); - queryHandlerService.saveQuery(queryId, CREATOR, savedQuery); - - var queryFromDb = queryHandlerService.getQuery(queryId); - - assertThat(queryFromDb.label()).isEqualTo(LABEL); - assertThat(queryFromDb.comment()).isEqualTo(COMMENT); - assertThat(queryFromDb.content().inclusionCriteria()).isEqualTo(createValidStructuredQuery("foo").inclusionCriteria()); - } - @Test public void testGetQueryContent_nullIfNotFound() throws JsonProcessingException { var queryContentString = jsonUtil.writeValueAsString(createValidStructuredQuery("foo")); @@ -349,102 +319,6 @@ public void testGetAuthorId_UnknownQueryIdThrows() { () -> queryHandlerService.getAuthorId(UNKNOWN_QUERY_ID)); } - @Test - @DisplayName("saveQuery() -> duplicate labels for the same user id fails") - public void saveQuery_DuplicateSavedQueryLabelsFails() throws Exception { - var query1 = new Query(); - query1.setCreatedBy(CREATOR); - var query2 = new Query(); - query2.setCreatedBy(CREATOR); - var queryId1 = queryRepository.save(query1).getId(); - var queryId2 = queryRepository.save(query2).getId(); - var label = "label-152431"; - - var savedQuery1 = new SavedQuery(label, "comment-152508", 100L); - var savedQuery2 = new SavedQuery(label, "comment-152546", 200L); - - assertThat(queryHandlerService.saveQuery(queryId1, CREATOR, savedQuery1)).isNotNull(); - assertThrows(DataIntegrityViolationException.class, - () -> queryHandlerService.saveQuery(queryId2, CREATOR, savedQuery2)); - } - - @Test - @DisplayName("saveQuery() -> different labels for the same user id succeeds") - public void saveQuery_DifferentSavedQueryLabelsSucceeds() throws Exception { - var query1 = new Query(); - query1.setCreatedBy(CREATOR); - var query2 = new Query(); - query2.setCreatedBy(CREATOR); - var queryId1 = queryRepository.save(query1).getId(); - var queryId2 = queryRepository.save(query2).getId(); - var label1 = "label-152431"; - var label2 = "label-160123"; - - var savedQuery1 = new SavedQuery(label1, "comment-152508", 100L); - var savedQuery2 = new SavedQuery(label2, "comment-152546", 200L); - - assertThat(queryHandlerService.saveQuery(queryId1, CREATOR, savedQuery1)).isNotNull(); - assertDoesNotThrow(() -> queryHandlerService.saveQuery(queryId2, CREATOR, savedQuery2)); - } - - @Test - @DisplayName("saveQuery() -> same labels for different user id succeeds") - public void saveQuery_SameSavedQueryLabelsForDifferentUsersSucceeds() throws Exception { - var otherCreator = "some-other-creator"; - var query1 = new Query(); - query1.setCreatedBy(CREATOR); - var query2 = new Query(); - query2.setCreatedBy(otherCreator); - var queryId1 = queryRepository.save(query1).getId(); - var queryId2 = queryRepository.save(query2).getId(); - var label = "label-152431"; - - var savedQuery1 = new SavedQuery(label, "comment-152508", 100L); - var savedQuery2 = new SavedQuery(label, "comment-152546", 200L); - - assertThat(queryHandlerService.saveQuery(queryId1, CREATOR, savedQuery1)).isNotNull(); - assertDoesNotThrow(() -> queryHandlerService.saveQuery(queryId2, otherCreator, savedQuery2)); - } - - @Test - public void testDeleteSavedQuery_succeeds() throws QueryNotFoundException { - var query1 = new Query(); - query1.setCreatedBy(CREATOR); - var query2 = new Query(); - query2.setCreatedBy(CREATOR); - var queryId1 = queryRepository.save(query1).getId(); - var queryId2 = queryRepository.save(query2).getId(); - var label1 = "label-152431"; - var label2 = "label-152432"; - - var savedQuery1 = new SavedQuery(label1, "comment-152508", 100L); - var savedQuery2 = new SavedQuery(label2, "comment-152546", 200L); - - queryHandlerService.saveQuery(queryId1, CREATOR, savedQuery1); - queryHandlerService.saveQuery(queryId2, CREATOR, savedQuery2); - - assertThat(queryHandlerService.getAmountOfSavedQueriesByUser(CREATOR)).isEqualTo(2); - - queryHandlerService.deleteSavedQuery(queryId1); - assertThat(queryHandlerService.getAmountOfSavedQueriesByUser(CREATOR)).isEqualTo(1); - queryHandlerService.deleteSavedQuery(queryId2); - assertThat(queryHandlerService.getAmountOfSavedQueriesByUser(CREATOR)).isEqualTo(0); - } - - @Test - public void testDeleteSavedQuery_failsOnUnknownQueryId() throws QueryNotFoundException { - var query = new Query(); - query.setCreatedBy(CREATOR); - var queryId = queryRepository.save(query).getId(); - var label = "label-152431"; - - var savedQuery = new SavedQuery(label, "comment-152508", 100L); - - queryHandlerService.saveQuery(queryId, CREATOR, savedQuery); - - assertThrows(QueryNotFoundException.class, () -> queryHandlerService.deleteSavedQuery(queryId + 1)); - } - @Test public void testGetAmountOfQueriesByUserAndInterval() throws JsonProcessingException { var query = new Query(); @@ -459,194 +333,6 @@ public void testGetAmountOfQueriesByUserAndInterval() throws JsonProcessingExcep } @Test - @DisplayName("storeQueryTemplate() -> duplicate labels for the same user id fails") - public void storeQueryTemplate_DuplicateSavedQueryLabelsFails() throws Exception { - var queryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR)); - assertThrows(DataIntegrityViolationException.class, - () -> queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR)); - } - - @Test - @DisplayName("storeQueryTemplate() -> different labels for the same user id succeeds") - public void storeQueryTemplate_DifferentSavedQueryLabelsSucceeds() throws Exception { - var queryTemplate1 = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - var queryTemplate2 = QueryTemplate.builder() - .label(LABEL + "modified") - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate1, CREATOR)); - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate2, CREATOR)); - } - - @Test - @DisplayName("storeQueryTemplate() -> same labels for different user id succeeds") - public void storeQueryTemplate_SameSavedQueryLabelsForDifferentUsersSucceeds() { - var queryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR)); - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate, "some-other-creator")); - } - - @Test - public void testGetQueryTemplate_succeeds() throws QueryTemplateException { - var queryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - var queryTemplateId = queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR); - - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate loadedQueryTemplate - = assertDoesNotThrow(() -> queryHandlerService.getQueryTemplate(queryTemplateId, CREATOR)); - assertThat(loadedQueryTemplate.getLabel()).isEqualTo(LABEL); - assertThat(loadedQueryTemplate.getComment()).isEqualTo(COMMENT); - assertThat(loadedQueryTemplate.getLastModified().toString()).isEqualTo(TIME_STRING); - } - - @Test - public void testGetQueryTemplate_UnknownQueryIdThrows() { - assertThrows(QueryTemplateException.class, () -> queryHandlerService.getQueryTemplate(0L, CREATOR)); - } - - @Test - public void testGetQueryTemplate_WrongAuthorThrows() throws QueryTemplateException { - var queryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - var queryTemplateId = queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR); - - assertThrows(QueryTemplateException.class, () -> queryHandlerService.getQueryTemplate(queryTemplateId, "unknown-creator")); - } - - @Test - public void testGetQueryTemplatesForAuthor_succeeds() { - var queryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - assertDoesNotThrow(() -> queryHandlerService.storeQueryTemplate(queryTemplate, CREATOR)); - - assertThat(queryHandlerService.getQueryTemplatesForAuthor(CREATOR).size()).isEqualTo(1); - assertThat(queryHandlerService.getQueryTemplatesForAuthor("unknown-creator").size()).isEqualTo(0); - } - - @Test - public void testUpdateQueryTemplate_succeeds() throws QueryTemplateException, JsonProcessingException { - var originalQueryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - var updatedQueryTemplate = QueryTemplate.builder() - .label(LABEL + "-modified") - .comment(COMMENT + "-modified") - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("bar")) - .build(); - - var queryTemplateId = queryHandlerService.storeQueryTemplate(originalQueryTemplate, CREATOR); - - assertDoesNotThrow(() -> queryHandlerService.updateQueryTemplate(queryTemplateId, updatedQueryTemplate, CREATOR)); - de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate loadedQueryTemplate - = assertDoesNotThrow(() -> queryHandlerService.getQueryTemplate(queryTemplateId, CREATOR)); - - var loadedQueryContent = jsonUtil.readValue(loadedQueryTemplate.getQuery().getQueryContent().getQueryContent(), StructuredQuery.class); - - assertThat(loadedQueryTemplate.getLabel()).isEqualTo(updatedQueryTemplate.label()); - assertThat(loadedQueryTemplate.getComment()).isEqualTo(updatedQueryTemplate.comment()); - // Query Content shall remain untouched - so check against the original instead of the updated content - assertThat(loadedQueryContent.display()).isEqualTo(originalQueryTemplate.content().display()); - } - - @Test - public void testUpdateQueryTemplate_throwsOnUnknownId() throws QueryTemplateException, JsonProcessingException { - var updatedQueryTemplate = QueryTemplate.builder() - .label(LABEL + "-modified") - .comment(COMMENT + "-modified") - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("bar")) - .build(); - - assertThrows(QueryTemplateException.class, () -> queryHandlerService.updateQueryTemplate(0L, updatedQueryTemplate, CREATOR)); - } - - @Test - public void testDeleteQueryTemplate_succeeds() throws QueryTemplateException, JsonProcessingException { - var originalQueryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - var queryTemplateId = queryHandlerService.storeQueryTemplate(originalQueryTemplate, CREATOR); - assertThat(queryHandlerService.getQueryTemplatesForAuthor(CREATOR).size()).isEqualTo(1); - assertDoesNotThrow(() -> queryHandlerService.deleteQueryTemplate(queryTemplateId, CREATOR)); - assertThat(queryHandlerService.getQueryTemplatesForAuthor(CREATOR).size()).isEqualTo(0); - } - - @Test - public void testDeleteQueryTemplate_throwsOnUnknownId() throws QueryTemplateException, JsonProcessingException { - assertThrows(QueryTemplateException.class, () -> queryHandlerService.deleteQueryTemplate(0L, CREATOR)); - } - - @Test - public void testDeleteQueryTemplate_throwsOnWrongAuthor() throws QueryTemplateException, JsonProcessingException { - var originalQueryTemplate = QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery("foo")) - .build(); - - queryHandlerService.storeQueryTemplate(originalQueryTemplate, CREATOR); - assertThat(queryHandlerService.getQueryTemplatesForAuthor(CREATOR).size()).isEqualTo(1); - assertThrows(QueryTemplateException.class, () -> queryHandlerService.deleteQueryTemplate(0L, "unknown-creator")); - assertThat(queryHandlerService.getQueryTemplatesForAuthor(CREATOR).size()).isEqualTo(1); - } - @DisplayName("getRetryAfterTime() -> return 0 on empty") public void getRetryAfterTime_zeroOnEmpty() { Long retryAfterTime = queryHandlerService.getRetryAfterTime(CREATOR, 0, 1000000L); @@ -663,29 +349,6 @@ public void getRetryAfterTime_nonZeroOnNotEmpty() { assertThat(retryAfterTime).isGreaterThan(0L); } - @Test - @DisplayName("getAmountOfSavedQueriesByUser() -> return list size") - public void getAmountOfSavedQueriesByUser_listSizeWhenNotEmpty() { - var query = new Query(); - query.setCreatedBy(CREATOR); - var queryId = queryRepository.save(query).getId(); - var label = "label-152431"; - var savedQuery = new SavedQuery(label, "comment-152508", 100L); - queryHandlerService.saveQuery(queryId, CREATOR, savedQuery); - - var queryAmount = queryHandlerService.getAmountOfSavedQueriesByUser(CREATOR); - - assertEquals(queryAmount, 1); - } - - @Test - @DisplayName("getAmountOfSavedQueriesByUser() -> return 0 on empty") - public void getAmountOfSavedQueriesByUser_zeroOnEmpty() { - var queryAmount = queryHandlerService.getAmountOfSavedQueriesByUser(CREATOR); - - assertEquals(queryAmount, 0L); - } - private StructuredQuery createValidStructuredQuery(String display) { var termCode = TermCode.builder() .code("LL2191-6") diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceTest.java index 6352f7ba..2007606e 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceTest.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/QueryHandlerServiceTest.java @@ -1,200 +1,74 @@ package de.numcodex.feasibility_gui_backend.query; -import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchException; -import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.common.api.Criterion; -import de.numcodex.feasibility_gui_backend.common.api.TermCode; -import de.numcodex.feasibility_gui_backend.query.api.QueryListEntry; import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; +import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchException; import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatcher; -import de.numcodex.feasibility_gui_backend.query.persistence.*; +import de.numcodex.feasibility_gui_backend.query.persistence.QueryContentRepository; +import de.numcodex.feasibility_gui_backend.query.persistence.QueryRepository; import de.numcodex.feasibility_gui_backend.query.result.ResultService; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateHandler; import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import reactor.test.StepVerifier; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; - -import java.net.URI; -import java.sql.Timestamp; import java.util.List; -import java.util.Random; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; @ExtendWith(MockitoExtension.class) class QueryHandlerServiceTest { - private static final long QUERY_ID = 1L; - private static final long RESULT_SIZE = 150L; - private static final String CREATOR = "author-123"; - private static final String QUERY_CONTENT_HASH = "c85f4c5c8e22275b6efcf41f4f3b6d4b"; - private static final String LABEL = "label"; - private static final String COMMENT = "comment"; - public static final String LAST_MODIFIED_STRING = "1969-07-20 20:17:40.0"; - private static final Timestamp LAST_MODIFIED = Timestamp.valueOf(LAST_MODIFIED_STRING); - - @Spy - private ObjectMapper jsonUtil = new ObjectMapper(); - - @Mock - private QueryDispatcher queryDispatcher; - - @Mock - private QueryTemplateHandler queryTemplateHandler; - - @Mock - private QueryRepository queryRepository; - - @Mock - private QueryContentRepository queryContentRepository; - - @Mock - private ResultService resultService; - - @Mock - private QueryTemplateRepository queryTemplateRepository; - - @Mock - private SavedQueryRepository savedQueryRepository; - - @Mock - private StructuredQueryValidation structuredQueryValidation; - - private QueryHandlerService queryHandlerService; - - private QueryHandlerService createQueryHandlerService() { - return new QueryHandlerService(queryDispatcher, queryTemplateHandler, queryRepository, queryContentRepository, - resultService, queryTemplateRepository, savedQueryRepository, structuredQueryValidation, jsonUtil); - } - - @BeforeEach - void setUp() { - Mockito.reset(queryDispatcher, queryTemplateHandler, queryRepository, queryContentRepository, - resultService, queryTemplateRepository, savedQueryRepository, jsonUtil); - queryHandlerService = createQueryHandlerService(); - } - - @Test - public void testRunQuery_failsWithMonoErrorOnQueryDispatchException() throws QueryDispatchException { - var testStructuredQuery = StructuredQuery.builder() - .inclusionCriteria(List.of(List.of())) - .exclusionCriteria(List.of(List.of())) - .build(); - var queryHandlerService = createQueryHandlerService(); - doThrow(QueryDispatchException.class).when(queryDispatcher).enqueueNewQuery(any(StructuredQuery.class), any(String.class)); - - StepVerifier.create(queryHandlerService.runQuery(testStructuredQuery, "uerid")) - .expectError(QueryDispatchException.class) - .verify(); - } - - @ParameterizedTest - @CsvSource({"true,true", "true,false", "false,true", "false,false"}) - void convertQueriesToQueryListEntries(String withSavedQuery, String skipValidation) throws JsonProcessingException { - var queryList = List.of(createQuery(Boolean.parseBoolean(withSavedQuery))); - if (!Boolean.parseBoolean(skipValidation)) { - doReturn( - new Random().nextBoolean() - ).when(structuredQueryValidation).isValid(any(StructuredQuery.class)); - } - - List queryListEntries = queryHandlerService.convertQueriesToQueryListEntries(queryList, Boolean.parseBoolean(skipValidation)); - - assertThat(queryListEntries.size()).isEqualTo(1); - assertThat(queryListEntries.get(0).id()).isEqualTo(QUERY_ID); - assertThat(queryListEntries.get(0).createdAt()).isEqualTo(LAST_MODIFIED); - if (Boolean.parseBoolean(withSavedQuery)) { - assertThat(queryListEntries.get(0).label()).isEqualTo(LABEL); - assertThat(queryListEntries.get(0).totalNumberOfPatients()).isEqualTo(RESULT_SIZE); - } - if (Boolean.parseBoolean(skipValidation)) { - assertThat(queryListEntries.get(0).isValid()).isNull(); - } else { - assertThat(queryListEntries.get(0).isValid()).isNotNull(); - } - } - - @Test - void convertQueriesToQueryListEntries_JsonProcessingExceptionCausesInvalidQuery() throws JsonProcessingException { - var queryList = List.of(createQuery(false)); - doThrow(JsonProcessingException.class).when(jsonUtil).readValue(any(String.class), any(Class.class)); - - List queryListEntries = queryHandlerService.convertQueriesToQueryListEntries(queryList, false); - - assertThat(queryListEntries.size()).isEqualTo(1); - assertThat(queryListEntries.get(0).id()).isEqualTo(QUERY_ID); - assertThat(queryListEntries.get(0).createdAt()).isEqualTo(LAST_MODIFIED); - assertThat(queryListEntries.get(0).isValid()).isFalse(); - } - - private Query createQuery(boolean withSavedQuery) throws JsonProcessingException { - var query = new Query(); - query.setId(QUERY_ID); - query.setCreatedAt(LAST_MODIFIED); - query.setCreatedBy(CREATOR); - query.setQueryContent(createQueryContent()); - if (withSavedQuery) { - query.setSavedQuery(createSavedQuery()); - } - return query; - } - - private SavedQuery createSavedQuery() { - var savedQuery = new SavedQuery(); - savedQuery.setId(0L); - savedQuery.setLabel(LABEL); - savedQuery.setComment(COMMENT); - savedQuery.setResultSize(RESULT_SIZE); - return savedQuery; - } - - private QueryContent createQueryContent() throws JsonProcessingException { - var queryContentString = jsonUtil.writeValueAsString(createValidStructuredQuery()); - var queryContent = new QueryContent(queryContentString); - queryContent.setHash(QUERY_CONTENT_HASH); - return queryContent; - } - - private StructuredQuery createValidStructuredQuery() { - var termCode = TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - var inclusionCriterion = Criterion.builder() - .termCodes(List.of(termCode)) - .attributeFilters(List.of()) - .build(); - return StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(inclusionCriterion))) - .exclusionCriteria(List.of()) - .display("foo") - .build(); - } - - private Criterion createInvalidCriterion() { - var termCode = TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - return Criterion.builder() - .context(null) - .termCodes(List.of(termCode)) - .build(); - } + + @Spy + private ObjectMapper jsonUtil = new ObjectMapper(); + + @Mock + private QueryDispatcher queryDispatcher; + + @Mock + private QueryRepository queryRepository; + + @Mock + private QueryContentRepository queryContentRepository; + + @Mock + private ResultService resultService; + + @Mock + private StructuredQueryValidation structuredQueryValidation; + + private QueryHandlerService queryHandlerService; + + private QueryHandlerService createQueryHandlerService() { + return new QueryHandlerService(queryDispatcher, queryRepository, queryContentRepository, + resultService, structuredQueryValidation, jsonUtil); + } + + @BeforeEach + void setUp() { + Mockito.reset(queryDispatcher, queryRepository, queryContentRepository, + resultService, jsonUtil); + queryHandlerService = createQueryHandlerService(); + } + + @Test + public void testRunQuery_failsWithMonoErrorOnQueryDispatchException() throws QueryDispatchException { + var testStructuredQuery = StructuredQuery.builder() + .inclusionCriteria(List.of(List.of())) + .exclusionCriteria(List.of(List.of())) + .build(); + var queryHandlerService = createQueryHandlerService(); + doThrow(QueryDispatchException.class).when(queryDispatcher).enqueueNewQuery(any(StructuredQuery.class), any(String.class)); + + StepVerifier.create(queryHandlerService.runQuery(testStructuredQuery, "uerid")) + .expectError(QueryDispatchException.class) + .verify(); + } } diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidatorTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidatorTest.java deleted file mode 100644 index 75cc215a..00000000 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplatePassValidatorTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import de.numcodex.feasibility_gui_backend.common.api.Criterion; -import de.numcodex.feasibility_gui_backend.common.api.TermCode; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; -import java.net.URI; -import java.util.List; -import jakarta.validation.ConstraintValidatorContext; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -@Tag("query") -@Tag("api") -@Tag("validation-template") -@ExtendWith(MockitoExtension.class) -public class QueryTemplatePassValidatorTest { - - @Spy - private QueryTemplatePassValidator validator; - - @Mock - private ConstraintValidatorContext ctx; - - @Test - public void testIsValid_validQueryPasses() { - var termCode = TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - - var inclusionCriterion = Criterion.builder() - .termCodes(List.of(termCode)) - .build(); - - var testQuery = StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(inclusionCriterion))) - .exclusionCriteria(List.of()) - .display("foo") - .build(); - var testStoredQuery = QueryTemplate.builder() - .id(0) - .content(testQuery) - .label("test") - .build(); - - var validationResult = assertDoesNotThrow(() -> validator.isValid(testStoredQuery, ctx)); - assertTrue(validationResult); - } - - @Test - public void testIsValid_invalidQueryPasses() { - var testStoredQuery = QueryTemplate.builder() - .id(0) - .build(); - - var validationResult = assertDoesNotThrow(() -> validator.isValid(testStoredQuery, ctx)); - assertTrue(validationResult); - } -} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorTest.java deleted file mode 100644 index 5ec9e1b3..00000000 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/api/validation/QueryTemplateValidatorTest.java +++ /dev/null @@ -1,160 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.api.validation; - -import static de.numcodex.feasibility_gui_backend.common.api.Comparator.GREATER_EQUAL; -import static de.numcodex.feasibility_gui_backend.query.api.ValueFilterType.QUANTITY_COMPARATOR; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.common.api.Criterion; -import de.numcodex.feasibility_gui_backend.common.api.TermCode; -import de.numcodex.feasibility_gui_backend.common.api.Unit; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; -import de.numcodex.feasibility_gui_backend.query.api.TimeRestriction; -import de.numcodex.feasibility_gui_backend.query.api.ValueFilter; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.util.List; -import jakarta.validation.ConstraintValidatorContext; -import org.everit.json.schema.loader.SchemaClient; -import org.everit.json.schema.loader.SchemaLoader; -import org.json.JSONObject; -import org.json.JSONTokener; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@Tag("query") -@Tag("api") -@Tag("validation-template") -@ExtendWith(MockitoExtension.class) -public class QueryTemplateValidatorTest { - public static QueryTemplateValidator validator; - - @Mock - private ConstraintValidatorContext constraintValidatorContext; - - @BeforeAll - public static void setUp() throws IOException { - var jsonUtil = new ObjectMapper(); - InputStream inputStream = QueryTemplateValidator.class.getResourceAsStream( - "/de/numcodex/feasibility_gui_backend/query/api/validation/query-template-schema.json"); - var jsonSchema = new JSONObject(new JSONTokener(inputStream)); - SchemaLoader loader = SchemaLoader.builder() - .schemaClient(SchemaClient.classPathAwareClient()) - .schemaJson(jsonSchema) - .resolutionScope("classpath://de/numcodex/feasibility_gui_backend/query/api/validation/") - .draftV7Support() - .build(); - var schema = loader.load().build(); - validator = new QueryTemplateValidator(schema, jsonUtil); - } - - @Test - public void testValidate_validQueryOk() { - var queryTemplate = buildValidQuery(); - assertTrue(validator.isValid(queryTemplate, constraintValidatorContext)); - } - - @Test - public void testValidate_invalidQueriesFail() { - var queryWithoutLabel = buildInvalidValidQueryWithoutLabel(); - assertFalse(validator.isValid(queryWithoutLabel, constraintValidatorContext)); - - var queryWithoutStructuredQuery = buildInvalidValidQueryWithoutContent(); - assertTrue(validator.isValid(queryWithoutStructuredQuery, constraintValidatorContext)); - - var queryWithMalformedStructuredQuery = buildInvalidQueryWithMalformedStructuredQuery(); - assertFalse(validator.isValid(queryWithMalformedStructuredQuery, constraintValidatorContext)); - } - - private QueryTemplate buildValidQuery() { - var bodyWeightTermCode = TermCode.builder() - .code("27113001") - .system("http://snomed.info/sct") - .version("v1") - .display("Body weight (observable entity)") - .build(); - var kgUnit = Unit.builder() - .code("kg") - .display("kilogram") - .build(); - var bodyWeightValueFilter = ValueFilter.builder() - .type(QUANTITY_COMPARATOR) - .comparator(GREATER_EQUAL) - .quantityUnit(kgUnit) - .value(50.0) - .build(); - var timeRestriction = TimeRestriction.builder() - .afterDate("2021-01-01") - .beforeDate("2021-12-31") - .build(); - - var hasBmiGreaterThanFifty = Criterion.builder() - .termCodes(List.of(bodyWeightTermCode)) - .valueFilter(bodyWeightValueFilter) - .timeRestriction(timeRestriction) - .build(); - var testQuery = StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(hasBmiGreaterThanFifty))) - .exclusionCriteria(List.of(List.of(hasBmiGreaterThanFifty))) - .display(null) - .build(); - return QueryTemplate.builder() - .id(0) - .content(testQuery) - .label("testquery") - .comment("this is just a test query") - .createdBy("foo-bar-1234") - .build(); - } - - private QueryTemplate buildInvalidValidQueryWithoutLabel() { - var validQuery = buildValidQuery(); - return QueryTemplate.builder() - .id(validQuery.id()) - .content(validQuery.content()) - .comment(validQuery.comment()) - .lastModified(validQuery.lastModified()) - .createdBy(validQuery.createdBy()) - .isValid(validQuery.isValid()) - .build(); - } - - private QueryTemplate buildInvalidValidQueryWithoutContent() { - var validQuery = buildValidQuery(); - return QueryTemplate.builder() - .id(validQuery.id()) - .label(validQuery.label()) - .comment(validQuery.comment()) - .lastModified(validQuery.lastModified()) - .createdBy(validQuery.createdBy()) - .isValid(validQuery.isValid()) - .build(); - } - - private QueryTemplate buildInvalidQueryWithMalformedStructuredQuery() { - var validQuery = buildValidQuery(); - var invalidTestQuery = StructuredQuery.builder() - .version(validQuery.content().version()) - .exclusionCriteria(validQuery.content().exclusionCriteria()) - .display(validQuery.content().display()) - .build(); - return QueryTemplate.builder() - .id(validQuery.id()) - .content(invalidTestQuery) - .label(validQuery.label()) - .comment(validQuery.comment()) - .lastModified(validQuery.lastModified()) - .createdBy(validQuery.createdBy()) - .isValid(validQuery.isValid()) - .build(); - } - -} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerIT.java new file mode 100644 index 00000000..2c435ba6 --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerIT.java @@ -0,0 +1,215 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.feasibility_gui_backend.common.api.Criterion; +import de.numcodex.feasibility_gui_backend.common.api.TermCode; +import de.numcodex.feasibility_gui_backend.query.QueryHandlerService; +import de.numcodex.feasibility_gui_backend.query.api.Crtdl; +import de.numcodex.feasibility_gui_backend.query.api.Dataquery; +import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; +import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots; +import de.numcodex.feasibility_gui_backend.query.broker.BrokerSpringConfig; +import de.numcodex.feasibility_gui_backend.query.collect.QueryCollectSpringConfig; +import de.numcodex.feasibility_gui_backend.query.dispatch.QueryDispatchSpringConfig; +import de.numcodex.feasibility_gui_backend.query.persistence.DataqueryRepository; +import de.numcodex.feasibility_gui_backend.query.result.ResultServiceSpringConfig; +import de.numcodex.feasibility_gui_backend.query.translation.QueryTranslatorSpringConfig; +import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.net.URI; +import java.sql.Timestamp; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +@Tag("query") +@Tag("handler") +@Import({ + BrokerSpringConfig.class, + QueryTranslatorSpringConfig.class, + QueryDispatchSpringConfig.class, + QueryCollectSpringConfig.class, + QueryHandlerService.class, + ResultServiceSpringConfig.class, + DataquerySpringConfig.class +}) +@DataJpaTest( + properties = { + "app.cqlTranslationEnabled=false", + "app.fhirTranslationEnabled=false", + "app.broker.mock.enabled=true", + "app.broker.direct.enabled=false", + "app.broker.aktin.enabled=false", + "app.broker.dsf.enabled=false" + } +) +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +@Testcontainers +public class DataqueryHandlerIT { + public static final String SITE_NAME_1 = "site-name-114606"; + public static final String SITE_NAME_2 = "site-name-114610"; + public static final String CREATOR = "creator-114634"; + public static final long UNKNOWN_QUERY_ID = 9999999L; + public static final String LABEL = "some-label"; + public static final String COMMENT = "some-comment"; + public static final String TIME_STRING = "1969-07-20 20:17:40.0"; + + @Autowired + private DataqueryHandler dataqueryHandler; + + @Autowired + private DataqueryRepository dataqueryRepository; + + @MockitoBean + private StructuredQueryValidation structuredQueryValidation; + + @Autowired + @Qualifier("translation") + private ObjectMapper jsonUtil; + + @Test + public void testStoreDataquery() throws DataqueryStorageFullException, DataqueryException { + var testDataquery = createDataquery(false); + + dataqueryHandler.storeDataquery(testDataquery, "test"); + + assertThat(dataqueryRepository.count()).isOne(); + } + + @Test + public void testGetDataquery() throws JsonProcessingException { + var dataqueryEntity = createDataqueryEntity(false); + var dataqueryId = dataqueryRepository.save(dataqueryEntity).getId(); + + var loadedDataquery = assertDoesNotThrow(() -> dataqueryHandler.getDataqueryById(dataqueryId, dataqueryEntity.getCreatedBy())); + + assertThat(loadedDataquery).isNotNull(); + assertThat(jsonUtil.writeValueAsString(loadedDataquery.content())).isEqualTo(dataqueryEntity.getCrtdl()); + } + + @Test + public void testUpdateDataquery() throws JsonProcessingException, DataqueryStorageFullException, DataqueryException { + var dataquery = createDataquery(false); + var dataqueryWithResult = createDataquery(true); + + var dataqueryId = dataqueryHandler.storeDataquery(dataquery, CREATOR); + var loadedDataqueryOld = assertDoesNotThrow(() -> dataqueryHandler.getDataqueryById(dataqueryId, CREATOR)); + dataqueryHandler.updateDataquery(dataqueryId, dataqueryWithResult, CREATOR); + var loadedDataqueryUpdated = assertDoesNotThrow(() -> dataqueryHandler.getDataqueryById(dataqueryId, CREATOR)); + + assertThat(loadedDataqueryUpdated).isNotNull(); + assertThat(loadedDataqueryUpdated.resultSize()).isEqualTo(dataqueryWithResult.resultSize()); + assertThat(loadedDataqueryUpdated.resultSize()).isNotEqualTo(loadedDataqueryOld.resultSize()); + } + + @Test + public void testGetDataqueriesByAuthor() throws JsonProcessingException { + var listSize = 10; + + for (int i = 0; i < listSize; ++i) { + dataqueryRepository.save(createDataqueryEntity(false)); + } + + var dataqueryList = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + var dataqueryListOtherAuthor = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor("other-" + CREATOR)); + + assertThat(dataqueryList.size()).isEqualTo(listSize); + assertThat(dataqueryListOtherAuthor).isEmpty(); + } + + @Test + public void testDeleteDataquery() throws JsonProcessingException { + var listSize = 10; + + for (int i = 0; i < listSize; ++i) { + dataqueryRepository.save(createDataqueryEntity(false)); + } + + var dataqueryListBeforeDelete = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + + assertDoesNotThrow(() -> dataqueryHandler.deleteDataquery(dataqueryListBeforeDelete.get(0).id(), CREATOR)); + + var dataqueryListAfterDelete = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + + assertThat(dataqueryListBeforeDelete.size()).isEqualTo(listSize); + assertThat(dataqueryListAfterDelete.size()).isEqualTo(listSize - 1); + } + + @Test + public void testGetDataquerySlotsJson() throws JsonProcessingException, DataqueryException { + var dataqueriesWithResult = 3; + var dataqueriesWithoutResult = 5; + + for (int i = 0; i < dataqueriesWithResult; ++i) { + dataqueryRepository.save(createDataqueryEntity(true)); + } + + for (int i = 0; i < dataqueriesWithoutResult; ++i) { + dataqueryRepository.save(createDataqueryEntity(false)); + } + + var dataqueryList = dataqueryHandler.getDataqueriesByAuthor(CREATOR); + var dataquerySlots = assertDoesNotThrow(() -> dataqueryHandler.getDataquerySlotsJson(CREATOR)); + + assertThat(dataqueryList.size()).isEqualTo(dataqueriesWithoutResult + dataqueriesWithResult); + assertThat(dataquerySlots).isInstanceOf(SavedQuerySlots.class); + assertThat(dataquerySlots.used()).isEqualTo(dataqueriesWithResult); + } + + private Dataquery createDataquery(boolean withResult) { + var testDataquery = Dataquery.builder() + .label("test") + .comment("test") + .content(createCrtdl()) + .resultSize(withResult ? 123L : null) + .build(); + return testDataquery; + } + + private Crtdl createCrtdl() { + return Crtdl.builder() + .cohortDefinition(createValidStructuredQuery()) + .display("foo") + .build(); + } + + private StructuredQuery createValidStructuredQuery() { + var termCode = TermCode.builder() + .code("LL2191-6") + .system("http://loinc.org") + .display("Geschlecht") + .build(); + var inclusionCriterion = Criterion.builder() + .termCodes(List.of(termCode)) + .attributeFilters(List.of()) + .build(); + return StructuredQuery.builder() + .version(URI.create("http://to_be_decided.com/draft-2/schema#")) + .inclusionCriteria(List.of(List.of(inclusionCriterion))) + .exclusionCriteria(null) + .display("foo") + .build(); + } + + private de.numcodex.feasibility_gui_backend.query.persistence.Dataquery createDataqueryEntity(boolean withResult) throws JsonProcessingException { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery out = new de.numcodex.feasibility_gui_backend.query.persistence.Dataquery(); + out.setLabel(LABEL); + out.setComment(COMMENT); + out.setLastModified(Timestamp.valueOf(TIME_STRING)); + out.setCreatedBy(CREATOR); + out.setResultSize(withResult ? 123L : null); + out.setCrtdl(jsonUtil.writeValueAsString(createCrtdl())); + return out; + } +} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerTest.java new file mode 100644 index 00000000..236ab93d --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/dataquery/DataqueryHandlerTest.java @@ -0,0 +1,415 @@ +package de.numcodex.feasibility_gui_backend.query.dataquery; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.feasibility_gui_backend.common.api.Criterion; +import de.numcodex.feasibility_gui_backend.common.api.TermCode; +import de.numcodex.feasibility_gui_backend.query.api.Crtdl; +import de.numcodex.feasibility_gui_backend.query.api.Dataquery; +import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; +import de.numcodex.feasibility_gui_backend.query.persistence.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.net.URI; +import java.sql.Timestamp; +import java.util.List; +import java.util.Optional; +import java.util.Random; + +import static org.junit.Assert.assertThrows; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@Tag("query") +@Tag("peristence") +@ExtendWith(MockitoExtension.class) +class DataqueryHandlerTest { + + public static final String CREATOR = "creator-557261"; + public static final String LABEL = "some-label"; + public static final String COMMENT = "some-comment"; + public static final String TIME_STRING = "1969-07-20 20:17:40.0"; + private static final int MAX_QUERIES_PER_USER = 5; + + @Spy + private ObjectMapper jsonUtil = new ObjectMapper(); + + @Mock + private DataqueryRepository dataqueryRepository; + + private DataqueryHandler createDataqueryHandler() { + return new DataqueryHandler(jsonUtil, dataqueryRepository, MAX_QUERIES_PER_USER); + } + + @Test + @DisplayName("storeDataquery() -> trying to store a valid object with a user does not throw") + void storeDataquery_succeeds() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + doReturn(createDataqueryEntity()).when(dataqueryRepository).save(any()); + + assertDoesNotThrow(() -> dataqueryHandler.storeDataquery(createDataquery(), CREATOR)); + } + + @Test + @DisplayName("storeDataquery() -> trying to store a null object with a null user throws an exception") + void storeDataquery_throwsOnEmptyQueryAndEmptyUser() { + var dataqueryHandler = createDataqueryHandler(); + assertThrows(NullPointerException.class, () -> dataqueryHandler.storeDataquery(null, null)); + } + + @Test + @DisplayName("storeDataquery() -> trying to store a null object with a user throws an exception") + void storeDataquery_throwsOnEmptyQueryAndNonEmptyUser() { + var dataqueryHandler = createDataqueryHandler(); + assertThrows(NullPointerException.class, () -> dataqueryHandler.storeDataquery(null, CREATOR)); + } + + @Test + @DisplayName("storeDataquery() -> trying to store an object with a null user throws an exception") + void storeDataquery_throwsOnNonEmptyQueryAndEmptyUser() { + var dataqueryHandler = createDataqueryHandler(); + assertThrows(NullPointerException.class, () -> dataqueryHandler.storeDataquery(createDataquery(), null)); + } + + @ParameterizedTest + @CsvSource({"true", "false"}) + @DisplayName("storeDataquery() -> trying to store a dataquery when no slots are free throws") + void storeDataquery_throwsOnNoFreeSlots(boolean withResult) throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + lenient().doReturn(MAX_QUERIES_PER_USER + 1L).when(dataqueryRepository).countByCreatedByWhereResultIsNotNull(any(String.class)); + lenient().doReturn(createDataqueryEntity()).when(dataqueryRepository).save(any(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery.class)); + + if (withResult) { + assertThrows(DataqueryStorageFullException.class, () -> dataqueryHandler.storeDataquery(createDataquery(withResult), CREATOR)); + } else { + assertDoesNotThrow(() -> dataqueryHandler.storeDataquery(createDataquery(withResult), CREATOR)); + } + } + + @ParameterizedTest + @CsvSource({"true,-1", "true,0", "true,1", "false,-1", "false,0", "false,1"}) + @DisplayName("storeDataquery() -> checking around the query limit") + void storeDataquery_testFreeSlotOnEdgeCases(boolean withResult, long offset) throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + lenient().doReturn(MAX_QUERIES_PER_USER + offset).when(dataqueryRepository).countByCreatedByWhereResultIsNotNull(any(String.class)); + lenient().doReturn(createDataqueryEntity()).when(dataqueryRepository).save(any(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery.class)); + + if (withResult) { + if (offset < 0) { + assertDoesNotThrow(() -> dataqueryHandler.storeDataquery(createDataquery(withResult), CREATOR)); + } else { + assertThrows(DataqueryStorageFullException.class, () -> dataqueryHandler.storeDataquery(createDataquery(withResult), CREATOR)); + } + } else { + assertDoesNotThrow(() -> dataqueryHandler.storeDataquery(createDataquery(withResult), CREATOR)); + } + } + + @Test + @DisplayName("storeDataquery() -> error in json serialization throws an exception") + void storeDataquery_throwsOnJsonSerializationError() throws JsonProcessingException { + var dataquery = createDataquery(); + doThrow(JsonProcessingException.class).when(jsonUtil).writeValueAsString(any(Crtdl.class)); + + var dataqueryHandler = createDataqueryHandler(); + assertThrows(DataqueryException.class, () -> dataqueryHandler.storeDataquery(dataquery, CREATOR)); + } + + @Test + @DisplayName("getDataqueryById() -> retrieving a single dataquery by its id succeeds") + void getDataqueryById_succeeds() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + Dataquery dataquery = assertDoesNotThrow(() -> dataqueryHandler.getDataqueryById(1L, CREATOR)); + assertNotNull(dataquery); + assertInstanceOf(Dataquery.class, dataquery); + } + + @Test + @DisplayName("getDataqueryById() -> Throw exception if the user is not the author") + void getDataqueryById_throwsOnWrongUser() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.getDataqueryById(1L, "NOT THE " + CREATOR)); + } + + @Test + @DisplayName("getDataqueryById() -> Throw exception if the query does not exist") + void getDataqueryById_throwsOnNotFound() { + var dataqueryHandler = createDataqueryHandler(); + doReturn(Optional.empty()).when(dataqueryRepository).findById(any(Long.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.getDataqueryById(1L, CREATOR)); + } + + @Test + @DisplayName("updateDataquery() -> trying to update a dataquery succeeds") + void updateDataquery_succeeds() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataquery = createDataquery(); + var dataqueryEntity = createDataqueryEntity(); + + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + assertDoesNotThrow(() -> dataqueryHandler.updateDataquery(1L, dataquery, CREATOR)); + } + + @Test + @DisplayName("updateDataquery() -> throws exception when actor is not the original creator") + void updateDataquery_throwsOnWrongUser() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataquery = createDataquery(); + var dataqueryEntity = createDataqueryEntity(); + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.updateDataquery(1L, dataquery, "NOT THE " + CREATOR)); + } + + @Test + @DisplayName("updateDataquery() -> throws exception when query is not found") + void updateDataquery_throwsOnNotFound() { + var dataqueryHandler = createDataqueryHandler(); + doReturn(Optional.empty()).when(dataqueryRepository).findById(any(Long.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.updateDataquery(1L, createDataquery(), CREATOR)); + } + + @ParameterizedTest + @CsvSource({"true,true", "true,false", "false,true", "false,false"}) + @DisplayName("updateDataquery() -> check if storage full exceptions are thrown correctly") + void updateDataquery_throwsOnNoFreeSlots(boolean withResultNew, boolean withResultOld) throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataquery = createDataquery(withResultNew); + var dataqueryEntity = createDataqueryEntity(withResultOld); + + lenient().doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + lenient().doReturn(Long.valueOf(MAX_QUERIES_PER_USER)).when(dataqueryRepository).countByCreatedByWhereResultIsNotNull(any(String.class)); + lenient().doReturn(createDataqueryEntity()).when(dataqueryRepository).save(any(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery.class)); + + // When the new dataquery has no result, this should never throw. It should only throw if the old had no result and the new has a result + if (withResultNew && !withResultOld) { + assertThrows(DataqueryStorageFullException.class, () -> dataqueryHandler.updateDataquery(1L, dataquery, CREATOR)); + } else { + assertDoesNotThrow(() -> dataqueryHandler.updateDataquery(1L, dataquery, CREATOR)); + } + } + + @ParameterizedTest + @CsvSource({ + "-1,true,true", "-1,true,false", "-1,false,true", "-1,false,false", + "0,true,true", "0,true,false", "0,false,true", "0,false,false", + "1,true,true", "1,true,false", "1,false,true", "1,false,false" + }) + @DisplayName("updateDataquery() -> checking around the query limit") + void updateDataquery_testFreeSlotOnEdgeCases(long offset, boolean withResultNew, boolean withResultOld) throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataquery = createDataquery(withResultNew); + var dataqueryEntity = createDataqueryEntity(withResultOld); + + lenient().doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + lenient().doReturn(MAX_QUERIES_PER_USER + offset).when(dataqueryRepository).countByCreatedByWhereResultIsNotNull(any(String.class)); + lenient().doReturn(createDataqueryEntity()).when(dataqueryRepository).save(any(de.numcodex.feasibility_gui_backend.query.persistence.Dataquery.class)); + + // It should only throw when the new query has a result, the old didn't have one and the storage is full + // If the storage is full but the old query already had a result, it should not fail. + if (withResultNew && !withResultOld && offset >= 0) { + assertThrows(DataqueryStorageFullException.class, () -> dataqueryHandler.updateDataquery(1L, dataquery, CREATOR)); + } else { + assertDoesNotThrow(() -> dataqueryHandler.updateDataquery(1L, dataquery, CREATOR)); + } + } + + @Test + @DisplayName("getDataqueriesByAuthor() -> succeeds") + void getDataqueriesByAuthor_succeedsWithEntry() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + + doReturn(List.of(dataqueryEntity)).when(dataqueryRepository).findAllByCreatedBy(any(String.class)); + + List dataqueries = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + + assertNotNull(dataqueries); + assertEquals(1, dataqueries.size()); + assertEquals(dataqueryEntity.getLabel(), dataqueries.get(0).label()); + assertEquals(dataqueryEntity.getComment(), dataqueries.get(0).comment()); + } + + @Test + @DisplayName("getDataqueriesByAuthor() -> succeeds with empty list") + void getDataqueriesByAuthor_succeedsWithEmptyList() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + + doReturn(List.of()).when(dataqueryRepository).findAllByCreatedBy(any(String.class)); + + List dataqueries = assertDoesNotThrow(() -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + + assertNotNull(dataqueries); + assertEquals(0, dataqueries.size()); + } + + @Test + @DisplayName("getDataqueriesByAuthor() -> throws on json error") + void getDataqueriesByAuthor_throwsOnJsonException() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + + doReturn(List.of(dataqueryEntity)).when(dataqueryRepository).findAllByCreatedBy(any(String.class)); + doThrow(JsonProcessingException.class).when(jsonUtil).readValue(any(String.class), any(Class.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.getDataqueriesByAuthor(CREATOR)); + } + + @Test + @DisplayName("deleteDataquery() -> succeeds") + void deleteDataquery_succeeds() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + assertDoesNotThrow(() -> dataqueryHandler.deleteDataquery(1L, CREATOR)); + } + + @Test + @DisplayName("deleteDataquery() -> throws on wrong user") + void deleteDataquery_throwsOnWrongUser() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var dataqueryEntity = createDataqueryEntity(); + + doReturn(Optional.of(dataqueryEntity)).when(dataqueryRepository).findById(any(Long.class)); + + assertThrows(DataqueryException.class, () -> dataqueryHandler.deleteDataquery(1L, "NOT THE " +CREATOR)); + } + + @Test + @DisplayName("getDataquerySlotsJson() -> succeeds") + void getDataquerySlotsJson_succeeds() throws JsonProcessingException { + var dataqueryHandler = createDataqueryHandler(); + var usedSlots = new Random().nextLong(); + + doReturn(usedSlots).when(dataqueryRepository).countByCreatedByWhereResultIsNotNull(any(String.class)); + + var savedQuerySlots = assertDoesNotThrow(() -> dataqueryHandler.getDataquerySlotsJson(CREATOR)); + + assertEquals(usedSlots, savedQuerySlots.used()); + } + + @Test + @DisplayName("convertApiToPersistence() -> converting a dataquery from the rest api to the format that will be stored in the database succeeds") + void convertApiToPersistence() throws JsonProcessingException { + var dataQuery = createDataquery(); + var dataqueryHandler = createDataqueryHandler(); + + var convertedDataquery = dataqueryHandler.convertApiToPersistence(dataQuery); + var convertedCrtdl = jsonUtil.readValue(convertedDataquery.getCrtdl(), Crtdl.class); + + assertEquals(convertedDataquery.getId(), dataQuery.id()); + assertEquals(convertedDataquery.getLabel(), dataQuery.label()); + assertEquals(convertedDataquery.getComment(), dataQuery.comment()); + assertEquals(convertedDataquery.getLastModified().toString(), dataQuery.lastModified()); + assertEquals(convertedDataquery.getResultSize(), dataQuery.resultSize()); + assertEquals(convertedCrtdl.display(), dataQuery.content().display()); + assertEquals(convertedCrtdl.version(), dataQuery.content().version()); + assertEquals(convertedCrtdl.cohortDefinition().display(), dataQuery.content().cohortDefinition().display()); + assertEquals(convertedCrtdl.cohortDefinition().version(), dataQuery.content().cohortDefinition().version()); + assertEquals(convertedCrtdl.cohortDefinition().inclusionCriteria(), dataQuery.content().cohortDefinition().inclusionCriteria()); + assertEquals(convertedCrtdl.cohortDefinition().exclusionCriteria(), dataQuery.content().cohortDefinition().exclusionCriteria()); + } + + @Test + @DisplayName("convertPersistenceToApi() -> converting a dataquery from the database to the format that will be sent out via api succeeds") + void convertPersistenceToApi() throws JsonProcessingException { + var dataqueryEntity = createDataqueryEntity(); + var dataqueryHandler = createDataqueryHandler(); + var convertedDataquery = dataqueryHandler.convertPersistenceToApi(dataqueryEntity); + var originalCrtdl = jsonUtil.readValue(dataqueryEntity.getCrtdl(), Crtdl.class); + + assertEquals(convertedDataquery.id(), dataqueryEntity.getId()); + assertEquals(convertedDataquery.label(), dataqueryEntity.getLabel()); + assertEquals(convertedDataquery.comment(), dataqueryEntity.getComment()); + assertEquals(convertedDataquery.createdBy(), dataqueryEntity.getCreatedBy()); + assertEquals(convertedDataquery.lastModified(), dataqueryEntity.getLastModified().toString()); + assertEquals(convertedDataquery.content().display(), originalCrtdl.display()); + assertEquals(convertedDataquery.content().version(), originalCrtdl.version()); + assertEquals(convertedDataquery.content().cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).code(), + originalCrtdl.cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).code()); + assertEquals(convertedDataquery.content().cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).display(), + originalCrtdl.cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).display()); + assertEquals(convertedDataquery.content().cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).version(), + originalCrtdl.cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).version()); + assertEquals(convertedDataquery.content().cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).system(), + originalCrtdl.cohortDefinition().inclusionCriteria().get(0).get(0).termCodes().get(0).system()); + assertEquals(convertedDataquery.content().cohortDefinition().exclusionCriteria(), originalCrtdl.cohortDefinition().exclusionCriteria()); + } + + private Dataquery createDataquery(boolean withResult) { + return Dataquery.builder() + .id(1L) + .label(LABEL) + .comment(COMMENT) + .content(createCrtdl()) + .resultSize(withResult ? 123L : null) + .createdBy(CREATOR) + .lastModified(TIME_STRING) + .build(); + } + + private Dataquery createDataquery() { + return createDataquery(false); + } + + private Crtdl createCrtdl() { + return Crtdl.builder() + .cohortDefinition(createValidStructuredQuery()) + .display("foo") + .build(); + } + + private StructuredQuery createValidStructuredQuery() { + var termCode = TermCode.builder() + .code("LL2191-6") + .system("http://loinc.org") + .display("Geschlecht") + .build(); + var inclusionCriterion = Criterion.builder() + .termCodes(List.of(termCode)) + .attributeFilters(List.of()) + .build(); + return StructuredQuery.builder() + .version(URI.create("http://to_be_decided.com/draft-2/schema#")) + .inclusionCriteria(List.of(List.of(inclusionCriterion))) + .exclusionCriteria(null) + .display("foo") + .build(); + } + + private de.numcodex.feasibility_gui_backend.query.persistence.Dataquery createDataqueryEntity(boolean withResult) throws JsonProcessingException { + de.numcodex.feasibility_gui_backend.query.persistence.Dataquery out = new de.numcodex.feasibility_gui_backend.query.persistence.Dataquery(); + out.setId(1L); + out.setLabel(LABEL); + out.setComment(COMMENT); + out.setLastModified(Timestamp.valueOf(TIME_STRING)); + out.setCreatedBy(CREATOR); + out.setResultSize(withResult ? 123L : null); + out.setCrtdl(jsonUtil.writeValueAsString(createCrtdl())); + return out; + } + + private de.numcodex.feasibility_gui_backend.query.persistence.Dataquery createDataqueryEntity() throws JsonProcessingException { + return createDataqueryEntity(false); + } +} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptorIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptorIT.java index 73263244..578cd841 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptorIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/ratelimiting/RateLimitingInterceptorIT.java @@ -1,6 +1,6 @@ package de.numcodex.feasibility_gui_backend.query.ratelimiting; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_API; +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; import static de.numcodex.feasibility_gui_backend.query.persistence.ResultType.SUCCESS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -18,7 +18,7 @@ import de.numcodex.feasibility_gui_backend.query.api.validation.StructuredQueryValidatorSpringConfig; import de.numcodex.feasibility_gui_backend.query.persistence.UserBlacklistRepository; import de.numcodex.feasibility_gui_backend.query.result.ResultLine; -import de.numcodex.feasibility_gui_backend.query.v4.QueryHandlerRestController; +import de.numcodex.feasibility_gui_backend.query.v5.FeasibilityQueryHandlerRestController; import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; import java.net.URI; @@ -46,7 +46,7 @@ RateLimitingServiceSpringConfig.class }) @WebMvcTest( - controllers = QueryHandlerRestController.class, + controllers = FeasibilityQueryHandlerRestController.class, properties = { "app.enableQueryValidation=true", "app.privacy.quota.read.resultSummary.pollingIntervalSeconds=1", @@ -89,7 +89,7 @@ void setupMockBehaviour() throws InvalidAuthenticationException { @EnumSource public void testGetResult_SucceedsOnFirstCall(ResultDetail resultDetail) throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1"; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1"; boolean isAdmin = false; switch (resultDetail) { @@ -118,7 +118,7 @@ public void testGetResult_SucceedsOnFirstCall(ResultDetail resultDetail) throws @EnumSource public void testGetResult_FailsOnImmediateSecondCall(ResultDetail resultDetail) throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1"; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1"; switch (resultDetail) { case SUMMARY -> requestUri = requestUri + WebSecurityConfig.PATH_SUMMARY_RESULT; @@ -152,7 +152,7 @@ public void testGetResult_FailsOnImmediateSecondCall(ResultDetail resultDetail) @EnumSource public void testGetResult_SucceedsOnDelayedSecondCall(ResultDetail resultDetail) throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1"; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1"; switch (resultDetail) { case SUMMARY -> requestUri = requestUri + WebSecurityConfig.PATH_SUMMARY_RESULT; @@ -178,7 +178,7 @@ public void testGetResult_SucceedsOnDelayedSecondCall(ResultDetail resultDetail) mockMvc .perform( - get(URI.create(PATH_API + "/query/1" + WebSecurityConfig.PATH_SUMMARY_RESULT)).with(csrf()) + get(URI.create(PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1" + WebSecurityConfig.PATH_SUMMARY_RESULT)).with(csrf()) .with(user(authorName).password("pass").roles("DATAPORTAL_TEST_USER")) ) .andExpect(status().isOk()); @@ -190,7 +190,7 @@ public void testGetResult_SucceedsOnImmediateMultipleCallsAsAdmin(ResultDetail r throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1"; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1"; switch (resultDetail) { case SUMMARY -> requestUri = requestUri + WebSecurityConfig.PATH_SUMMARY_RESULT; @@ -217,7 +217,7 @@ public void testGetResult_SucceedsOnImmediateMultipleCallsAsAdmin(ResultDetail r public void testGetResult_SucceedsOnImmediateSecondCallAsOtherUser(ResultDetail resultDetail) throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1"; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1"; switch (resultDetail) { case SUMMARY -> requestUri = requestUri + WebSecurityConfig.PATH_SUMMARY_RESULT; @@ -244,7 +244,7 @@ public void testGetResult_SucceedsOnImmediateSecondCallAsOtherUser(ResultDetail mockMvc .perform( - get(URI.create(PATH_API + "/query/1" + WebSecurityConfig.PATH_SUMMARY_RESULT)).with(csrf()) + get(URI.create(PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1" + WebSecurityConfig.PATH_SUMMARY_RESULT)).with(csrf()) .with(user(authorName).password("pass").roles("DATAPORTAL_TEST_USER")) ) .andExpect(status().isOk()); @@ -253,7 +253,7 @@ public void testGetResult_SucceedsOnImmediateSecondCallAsOtherUser(ResultDetail @Test public void testGetDetailedObfuscatedResult_FailsOnLimitExceedingCall() throws Exception { var authorName = UUID.randomUUID().toString(); - var requestUri = PATH_API + "/query/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT; + var requestUri = PATH_API + PATH_QUERY + PATH_FEASIBILITY + "/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT; doReturn(false).when(authenticationHelper) .hasAuthority(any(Authentication.class), eq("DATAPORTAL_TEST_ADMIN")); diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandlerTest.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandlerTest.java deleted file mode 100644 index f8a5ea48..00000000 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/templates/QueryTemplateHandlerTest.java +++ /dev/null @@ -1,174 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.templates; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.common.api.Criterion; -import de.numcodex.feasibility_gui_backend.common.api.TermCode; -import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; -import de.numcodex.feasibility_gui_backend.query.dispatch.QueryHashCalculator; -import de.numcodex.feasibility_gui_backend.query.persistence.*; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; - -import java.net.URI; -import java.sql.Timestamp; -import java.util.List; - -import static org.junit.Assert.assertThrows; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.doThrow; - -@Tag("query") -@Tag("template") -@Tag("peristence") -@ExtendWith(MockitoExtension.class) -class QueryTemplateHandlerTest { - - public static final String CREATOR = "creator-557261"; - public static final String LABEL = "some-label"; - public static final String COMMENT = "some-comment"; - public static final String TIME_STRING = "1969-07-20 20:17:40.0"; - public static final String QUERY_HASH = "fb4eb37b9125f9356d536d2d90d020ea"; - - @Mock - private QueryHashCalculator queryHashCalculator; - - @Spy - private ObjectMapper jsonUtil = new ObjectMapper(); - - @Mock - private QueryRepository queryRepository; - - @Mock - private QueryContentRepository queryContentRepository; - - @Mock - private QueryTemplateRepository queryTemplateRepository; - - private QueryTemplateHandler createQueryTemplateHandler() { - return new QueryTemplateHandler(queryHashCalculator, jsonUtil, queryRepository, queryContentRepository, queryTemplateRepository); - } - - @Test - @DisplayName("storeNewQuery() -> trying to store a null object throws an exception") - void storeNewQuery_throwsOnEmptyQuery() { - var queryTemplateHandler = createQueryTemplateHandler(); - assertThrows(QueryTemplateException.class, () -> queryTemplateHandler.storeNewQuery(null, null)); - } - - @Test - @DisplayName("storeNewQuery() -> error in json serialization throws an exception") - void storeNewQuery_throwsOnJsonSerializationError() throws JsonProcessingException { - var structuredQuery = createValidStructuredQuery(); - doThrow(JsonProcessingException.class).when(jsonUtil).writeValueAsString(any(StructuredQuery.class)); - - var queryTemplateHandler = createQueryTemplateHandler(); - assertThrows(QueryTemplateException.class, () -> queryTemplateHandler.storeNewQuery(structuredQuery, CREATOR)); - } - - @Test - @DisplayName("convertApiToPersistence() -> converting a query from the rest api to the format that will be stored in the database succeeds") - void convertApiToPersistence() { - var apiQueryTemplate = createApiQueryTemplate(); - var persistenceQueryTemplate = createPersistenceQueryTemplate(); - doReturn(createQuery()).when(queryRepository).getReferenceById(any(Long.class)); - - var queryTemplateHandler = createQueryTemplateHandler(); - var convertedQueryTemplate = queryTemplateHandler.convertApiToPersistence(apiQueryTemplate, 1L); - - assertNull(convertedQueryTemplate.getId()); - assertEquals(convertedQueryTemplate.getLabel(), persistenceQueryTemplate.getLabel()); - assertEquals(convertedQueryTemplate.getComment(), persistenceQueryTemplate.getComment()); - assertEquals(convertedQueryTemplate.getLastModified(), persistenceQueryTemplate.getLastModified()); - assertEquals(convertedQueryTemplate.getQuery().getCreatedAt(), persistenceQueryTemplate.getQuery().getCreatedAt()); - assertEquals(convertedQueryTemplate.getQuery().getQueryContent().getQueryContent(), persistenceQueryTemplate.getQuery().getQueryContent().getQueryContent()); - } - - @Test - @DisplayName("convertPersistenceToApi() -> converting a query from the database to the format that will be sent out via api succeeds") - void convertPersistenceToApi() throws JsonProcessingException { - var persistenceQueryTemplate = createPersistenceQueryTemplate(); - var apiQueryTemplate = createApiQueryTemplate(); - - var queryTemplateHandler = createQueryTemplateHandler(); - var convertedQueryTemplate = queryTemplateHandler.convertPersistenceToApi(persistenceQueryTemplate); - - assertEquals(convertedQueryTemplate.id(), apiQueryTemplate.id()); - assertEquals(convertedQueryTemplate.label(), apiQueryTemplate.label()); - assertEquals(convertedQueryTemplate.comment(), apiQueryTemplate.comment()); - assertEquals(convertedQueryTemplate.createdBy(), apiQueryTemplate.createdBy()); - assertEquals(convertedQueryTemplate.lastModified(), apiQueryTemplate.lastModified()); - assertEquals(convertedQueryTemplate.content().display(), apiQueryTemplate.content().display()); - assertEquals(convertedQueryTemplate.content().version(), apiQueryTemplate.content().version()); - assertEquals(convertedQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).code(), - apiQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).code()); - assertEquals(convertedQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).display(), - apiQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).display()); - assertEquals(convertedQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).version(), - apiQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).version()); - assertEquals(convertedQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).system(), - apiQueryTemplate.content().inclusionCriteria().get(0).get(0).termCodes().get(0).system()); - assertEquals(convertedQueryTemplate.content().exclusionCriteria(), apiQueryTemplate.content().exclusionCriteria()); - } - - private de.numcodex.feasibility_gui_backend.query.api.QueryTemplate createApiQueryTemplate() { - return de.numcodex.feasibility_gui_backend.query.api.QueryTemplate.builder() - .label(LABEL) - .comment(COMMENT) - .createdBy(CREATOR) - .lastModified(TIME_STRING) - .content(createValidStructuredQuery()) - .id(1L) - .build(); - } - - private de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate createPersistenceQueryTemplate() { - var queryTemplate = new de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate(); - queryTemplate.setLabel(LABEL); - queryTemplate.setComment(COMMENT); - queryTemplate.setLastModified(Timestamp.valueOf(TIME_STRING)); - queryTemplate.setId(1L); - queryTemplate.setQuery(createQuery()); - return queryTemplate; - } - - private Query createQuery() { - var queryContent = new QueryContent(createValidQueryContentString()); - queryContent.setHash(QUERY_HASH); - var query = new Query(); - query.setQueryContent(queryContent); - query.setCreatedBy(CREATOR); - query.setCreatedAt(Timestamp.valueOf(TIME_STRING)); - return query; - } - - private StructuredQuery createValidStructuredQuery() { - var termCode = TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - var inclusionCriterion = Criterion.builder() - .termCodes(List.of(termCode)) - .attributeFilters(List.of()) - .build(); - return StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(inclusionCriterion))) - .exclusionCriteria(List.of()) - .display("foo") - .build(); - } - - private String createValidQueryContentString() { - return "{ \"version\": \"http://to_be_decided.com/draft-2/schema#\", \"display\": \"foo\", \"inclusionCriteria\": [ [ { \"termCodes\": [ { \"code\": \"LL2191-6\", \"display\": \"Geschlecht\", \"system\": \"http://loinc.org\" } ] } ] ], \"exclusionCriteria\": [ ]}"; - } -} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestControllerIT.java deleted file mode 100644 index 38211d64..00000000 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryTemplateHandlerRestControllerIT.java +++ /dev/null @@ -1,362 +0,0 @@ -package de.numcodex.feasibility_gui_backend.query.v4; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import de.numcodex.feasibility_gui_backend.common.api.Criterion; -import de.numcodex.feasibility_gui_backend.common.api.TermCode; -import de.numcodex.feasibility_gui_backend.query.QueryHandlerService; -import de.numcodex.feasibility_gui_backend.query.api.QueryTemplate; -import de.numcodex.feasibility_gui_backend.query.api.StructuredQuery; -import de.numcodex.feasibility_gui_backend.query.api.status.ValidationIssue; -import de.numcodex.feasibility_gui_backend.query.api.validation.QueryTemplateValidatorSpringConfig; -import de.numcodex.feasibility_gui_backend.query.persistence.Query; -import de.numcodex.feasibility_gui_backend.query.ratelimiting.AuthenticationHelper; -import de.numcodex.feasibility_gui_backend.query.ratelimiting.RateLimitingServiceSpringConfig; -import de.numcodex.feasibility_gui_backend.query.templates.QueryTemplateException; -import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.context.annotation.Import; -import org.springframework.dao.DataIntegrityViolationException; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.test.web.servlet.MockMvc; - -import java.net.URI; -import java.sql.Timestamp; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.ThreadLocalRandom; - -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.everyItem; -import static org.mockito.Mockito.*; -import static org.springframework.http.MediaType.APPLICATION_JSON; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; - -@Tag("query") -@ExtendWith(SpringExtension.class) -@Import({QueryTemplateValidatorSpringConfig.class, - RateLimitingServiceSpringConfig.class -}) -@WebMvcTest( - controllers = QueryTemplateHandlerRestController.class, - properties = { - "app.enableQueryValidation=true" - } -) -public class QueryTemplateHandlerRestControllerIT { - - @Autowired - private MockMvc mockMvc; - - @Autowired - private ObjectMapper jsonUtil; - - @MockitoBean - private QueryHandlerService queryHandlerService; - - @MockitoBean - private StructuredQueryValidation structuredQueryValidation; - - @MockitoBean - private AuthenticationHelper authenticationHelper; - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testStoreQueryTemplate_succeedsWith201() throws Exception { - long queryId = 1; - doReturn(queryId).when(queryHandlerService).storeQueryTemplate(any(QueryTemplate.class), any(String.class)); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(createValidQueryTemplateToStore(queryId)))) - .andExpect(status().isCreated()) - .andExpect(header().exists("location")) - .andExpect(header().string("location", PATH_API + PATH_QUERY + PATH_TEMPLATE + "/" + queryId)); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testStoreQueryTemplate_failsOnInvalidQueryTemplate() throws Exception { - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf()) - .contentType(APPLICATION_JSON) - .content("{}")) - .andExpect(status().isBadRequest()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testStoreQueryTemplate_failsOnTemplateExceptionWith500() throws Exception { - doThrow(QueryTemplateException.class).when(queryHandlerService).storeQueryTemplate(any(QueryTemplate.class), any(String.class)); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(createValidQueryTemplateToStore(1L)))) - .andExpect(status().isInternalServerError()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testStoreQueryTemplate_failsOnDuplicateWith409() throws Exception { - doThrow(DataIntegrityViolationException.class).when(queryHandlerService).storeQueryTemplate(any(QueryTemplate.class), any(String.class)); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(createValidQueryTemplateToStore(1L)))) - .andExpect(status().isConflict()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplate_succeeds() throws Exception { - long queryTemplateId = 1; - var annotatedQuery = createValidAnnotatedStructuredQuery(false); - - doReturn(createValidPersistenceQueryTemplateToGet(queryTemplateId)).when(queryHandlerService).getQueryTemplate(any(Long.class), any(String.class)); - doReturn(createValidApiQueryTemplateToGet(queryTemplateId)).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/" + queryTemplateId)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(queryTemplateId)); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplate_failsOnNotFound() throws Exception { - long queryTemplateId = 1; - var annotatedQuery = createValidAnnotatedStructuredQuery(false); - - doThrow(QueryTemplateException.class).when(queryHandlerService).getQueryTemplate(any(Long.class), any(String.class)); - doReturn(createValidApiQueryTemplateToGet(queryTemplateId)).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/" + queryTemplateId)).with(csrf())) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplate_failsOnJsonError() throws Exception { - long queryTemplateId = 1; - var annotatedQuery = createValidAnnotatedStructuredQuery(false); - - doReturn(createValidPersistenceQueryTemplateToGet(queryTemplateId)).when(queryHandlerService).getQueryTemplate(any(Long.class), any(String.class)); - doThrow(JsonProcessingException.class).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/" + queryTemplateId)).with(csrf())) - .andExpect(status().isInternalServerError()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplateList_succeeds() throws Exception { - int listSize = 5; - doReturn(createValidPersistenceQueryTemplateListToGet(listSize)).when(queryHandlerService).getQueryTemplatesForAuthor(any(String.class)); - doReturn(createValidApiQueryTemplateToGet(ThreadLocalRandom.current().nextInt())).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE+ "?skip-validation=true")).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(listSize)) - .andExpect(jsonPath("$.[0].id").exists()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplateList_emptyListOnJsonErrors() throws Exception { - doReturn(createValidPersistenceQueryTemplateListToGet(5)).when(queryHandlerService).getQueryTemplatesForAuthor(any(String.class)); - doThrow(JsonProcessingException.class).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(0)); - } - - @ParameterizedTest - @ValueSource(booleans = {true, false}) - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplateListWithValidation_succeeds(boolean isValid) throws Exception { - int listSize = 5; - doReturn(createValidPersistenceQueryTemplateListToGet(listSize)).when(queryHandlerService).getQueryTemplatesForAuthor(any(String.class)); - doReturn(createValidApiQueryTemplateToGet(ThreadLocalRandom.current().nextInt())).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - doReturn(isValid).when(structuredQueryValidation).isValid(any(StructuredQuery.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(listSize)) - .andExpect(jsonPath("$.[*].id").exists()) - .andExpect(jsonPath("$.[*].isValid").exists()) - .andExpect(jsonPath("$.[*].isValid", everyItem(equalTo(isValid)))); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testGetQueryTemplateListWithValidation_emptyListOnJsonErrors() throws Exception { - int listSize = 5; - doReturn(createValidPersistenceQueryTemplateListToGet(listSize)).when(queryHandlerService).getQueryTemplatesForAuthor(any(String.class)); - doThrow(JsonProcessingException.class).when(queryHandlerService).convertTemplatePersistenceToApi(any(de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate.class)); - doReturn(false).when(structuredQueryValidation).isValid(any(StructuredQuery.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.length()").value(0)); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testUpdateQueryTemplate_succeeds() throws Exception { - doNothing().when(queryHandlerService).updateQueryTemplate(any(Long.class), any(QueryTemplate.class), any(String.class)); - - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/1")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(createValidQueryTemplateToStore(1L)))) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testUpdateQueryTemplate_failsOnInvalidQueryTemplate() throws Exception { - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/1")).with(csrf()) - .contentType(APPLICATION_JSON) - .content("{}")) - .andExpect(status().isBadRequest()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testUpdateQueryTemplate_failsOnTemplateExceptionWith404() throws Exception { - doThrow(QueryTemplateException.class).when(queryHandlerService).updateQueryTemplate(any(Long.class), any(QueryTemplate.class), any(String.class)); - - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE+ "/1")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(createValidQueryTemplateToStore(1L)))) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testDeleteQueryTemplate_succeeds() throws Exception { - doNothing().when(queryHandlerService).deleteQueryTemplate(any(Long.class), any(String.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/1")).with(csrf())) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(roles = "DATAPORTAL_TEST_USER") - public void testDeleteQueryTemplate_failsWith404OnNotFound() throws Exception { - doThrow(QueryTemplateException.class).when(queryHandlerService).deleteQueryTemplate(any(Long.class), any(String.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + PATH_TEMPLATE + "/1")).with(csrf())) - .andExpect(status().isNotFound()); - } - - @NotNull - private static QueryTemplate createValidQueryTemplateToStore(long id) { - return QueryTemplate.builder() - .id(id) - .content(createValidStructuredQuery()) - .label("TestLabel") - .comment("TestComment") - .isValid(true) - .build(); - } - - @NotNull - private static de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate createValidPersistenceQueryTemplateToGet(long id) { - var queryTemplate = new de.numcodex.feasibility_gui_backend.query.persistence.QueryTemplate(); - queryTemplate.setId(id); - queryTemplate.setQuery(new Query()); - queryTemplate.setLabel("TestLabel"); - queryTemplate.setComment("TestComment"); - queryTemplate.setLastModified(new Timestamp(new java.util.Date().getTime())); - return queryTemplate; - } - - @NotNull - private static List createValidPersistenceQueryTemplateListToGet(int entries) { - var queryTemplateList = new ArrayList(); - for (int i = 0; i < entries; ++i) { - queryTemplateList.add(createValidPersistenceQueryTemplateToGet(i)); - } - return queryTemplateList; - } - - @NotNull - private static QueryTemplate createValidApiQueryTemplateToGet(long id) { - return QueryTemplate.builder() - .id(id) - .content(createValidStructuredQuery()) - .label("TestLabel") - .comment("TestComment") - .lastModified(new Timestamp(new java.util.Date().getTime()).toString()) - .createdBy("someone") - .isValid(true) - .build(); - } - - @NotNull - private static StructuredQuery createValidStructuredQuery() { - var inclusionCriterion = Criterion.builder() - .termCodes(List.of(createTermCode())) - .attributeFilters(List.of()) - .context(createTermCode()) - .build(); - return StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(inclusionCriterion))) - .exclusionCriteria(List.of()) - .display("foo") - .build(); - } - - @NotNull - private static StructuredQuery createValidAnnotatedStructuredQuery(boolean withIssues) { - var termCode = TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - var inclusionCriterion = Criterion.builder() - .termCodes(List.of(termCode)) - .attributeFilters(List.of()) - .context(termCode) - .validationIssues(withIssues ? List.of(ValidationIssue.TERMCODE_CONTEXT_COMBINATION_INVALID) : List.of()) - .build(); - return StructuredQuery.builder() - .version(URI.create("http://to_be_decided.com/draft-2/schema#")) - .inclusionCriteria(List.of(List.of(inclusionCriterion))) - .exclusionCriteria(List.of()) - .display("foo") - .build(); - } - - @NotNull - private static TermCode createTermCode() { - return TermCode.builder() - .code("LL2191-6") - .system("http://loinc.org") - .display("Geschlecht") - .build(); - } - - @NotNull - private static Criterion createInvalidCriterion() { - return Criterion.builder() - .context(null) - .termCodes(List.of(createTermCode())) - .build(); - } -} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestControllerIT.java new file mode 100644 index 00000000..46c5909a --- /dev/null +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/v5/DataqueryHandlerRestControllerIT.java @@ -0,0 +1,417 @@ +package de.numcodex.feasibility_gui_backend.query.v5; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.numcodex.feasibility_gui_backend.query.api.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.numcodex.feasibility_gui_backend.common.api.Criterion; +import de.numcodex.feasibility_gui_backend.common.api.TermCode; +import de.numcodex.feasibility_gui_backend.query.api.status.SavedQuerySlots; +import de.numcodex.feasibility_gui_backend.query.api.status.ValidationIssue; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryException; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryHandler; +import de.numcodex.feasibility_gui_backend.query.dataquery.DataqueryStorageFullException; +import de.numcodex.feasibility_gui_backend.query.ratelimiting.AuthenticationHelper; +import de.numcodex.feasibility_gui_backend.query.ratelimiting.RateLimitingServiceSpringConfig; +import de.numcodex.feasibility_gui_backend.terminology.validation.StructuredQueryValidation; + +import java.sql.Timestamp; +import java.util.ArrayList; + +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.test.web.servlet.MockMvc; + +import java.net.URI; +import java.util.List; + +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; +import static org.mockito.Mockito.*; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@Tag("query") +@ExtendWith(SpringExtension.class) +@Import(RateLimitingServiceSpringConfig.class) +@WebMvcTest( + controllers = DataqueryHandlerRestController.class +) +public class DataqueryHandlerRestControllerIT { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper jsonUtil; + + @MockitoBean + private DataqueryHandler dataqueryHandler; + + @MockitoBean + private StructuredQueryValidation structuredQueryValidation; + + @MockitoBean + private AuthenticationHelper authenticationHelper; + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testStoreDataquery_succeedsWith201() throws Exception { + long queryId = 1L; + doReturn(queryId).when(dataqueryHandler).storeDataquery(any(Dataquery.class), any(String.class)); + doReturn(createSavedQuerySlots()).when(dataqueryHandler).getDataquerySlotsJson(any(String.class)); + + mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_DATA)).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(queryId)))) + .andExpect(status().isCreated()) + .andExpect(header().exists("location")) + .andExpect(header().string("location", PATH_API + PATH_QUERY + PATH_DATA + "/" + queryId)) + .andExpect(jsonPath("$.used").exists()) + .andExpect(jsonPath("$.total").exists()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testStoreDataqueryExceptionWith500() throws Exception { + doThrow(DataqueryException.class).when(dataqueryHandler).storeDataquery(any(Dataquery.class), any(String.class)); + + mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_DATA)).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isInternalServerError()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testStoreDataqueryExceptionWith403() throws Exception { + doThrow(DataqueryStorageFullException.class).when(dataqueryHandler).storeDataquery(any(Dataquery.class), any(String.class)); + + mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + PATH_DATA)).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isForbidden()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataquery_succeeds() throws Exception { + long dataqueryId = 1L; + var annotatedQuery = createValidAnnotatedStructuredQuery(false); + + doReturn(createValidApiDataqueryToGet(dataqueryId)).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId)).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(dataqueryId)); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataquery_failsOnNotFound() throws Exception { + long dataqueryId = 1; + + doThrow(DataqueryException.class).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId)).with(csrf())) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataquery_failsOnJsonError() throws Exception { + long dataqueryId = 1; + + doThrow(JsonProcessingException.class).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId)).with(csrf())) + .andExpect(status().isInternalServerError()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataqueryCrtdl_succeeds() throws Exception { + long dataqueryId = 1L; + var annotatedQuery = createValidAnnotatedStructuredQuery(false); + + doReturn(createValidApiDataqueryToGet(dataqueryId)).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId + "/crtdl")).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.cohortDefinition.display").value(annotatedQuery.display())); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataqueryCrtdl_failsOnNotFound() throws Exception { + long dataqueryId = 1; + + doThrow(DataqueryException.class).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId + "/crtdl")).with(csrf())) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataqueryCrtdl_failsOnJsonError() throws Exception { + long dataqueryId = 1; + + doThrow(JsonProcessingException.class).when(dataqueryHandler).getDataqueryById(any(Long.class), any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/" + dataqueryId + "/crtdl")).with(csrf())) + .andExpect(status().isInternalServerError()); + } + + @ParameterizedTest + @CsvSource({"true","false"}) + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataqueryList_succeeds(String skipValidation) throws Exception { + int listSize = 5; + doReturn(createValidApiDataqueryListToGet(listSize)).when(dataqueryHandler).getDataqueriesByAuthor(any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "?skip-validation=" + skipValidation)).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(listSize)) + .andExpect(jsonPath("$.[0].id").exists()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataqueryList_500onDataqueryException() throws Exception { + doThrow(DataqueryException.class).when(dataqueryHandler).getDataqueriesByAuthor(any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "?skip-validation=true")).with(csrf())) + .andExpect(status().isInternalServerError()); + } + + @ParameterizedTest + @CsvSource({"true","false"}) + @WithMockUser(roles = "DATAPORTAL_TEST_ADMIN") + public void testGetDataqueryListByUser_succeeds(String skipValidation) throws Exception { + int listSize = 5; + doReturn(createValidApiDataqueryListToGet(listSize)).when(dataqueryHandler).getDataqueriesByAuthor(any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/by-user/123" + "?skip-validation=" + skipValidation)).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.length()").value(listSize)) + .andExpect(jsonPath("$.[0].id").exists()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_ADMIN") + public void testGetDataqueryListByUser_500onDataqueryException() throws Exception { + doThrow(DataqueryException.class).when(dataqueryHandler).getDataqueriesByAuthor(any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/by-user/123" + "?skip-validation=true")).with(csrf())) + .andExpect(status().isInternalServerError()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testUpdateDataquery_succeeds() throws Exception { + doNothing().when(dataqueryHandler).updateDataquery(any(Long.class), any(Dataquery.class), any(String.class)); + doReturn(createSavedQuerySlots()).when(dataqueryHandler).getDataquerySlotsJson(any(String.class)); + + mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.used").exists()) + .andExpect(jsonPath("$.total").exists()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testUpdateDataquery_failsOnNotFound() throws Exception { + doThrow(DataqueryException.class).when(dataqueryHandler).updateDataquery(any(Long.class), any(Dataquery.class), any(String.class)); + mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testUpdateDataquery_failsOnJsonProcessingException() throws Exception { + doThrow(JsonProcessingException.class).when(dataqueryHandler).updateDataquery(any(Long.class), any(Dataquery.class), any(String.class)); + mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isUnprocessableEntity()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testUpdateDataquery_failsOnStorageFull() throws Exception { + doThrow(DataqueryStorageFullException.class).when(dataqueryHandler).updateDataquery(any(Long.class), any(Dataquery.class), any(String.class)); + mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf()) + .contentType(APPLICATION_JSON) + .content(jsonUtil.writeValueAsString(createValidDataqueryToStore(1L)))) + .andExpect(status().isForbidden()); + } + + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testDeleteDataquery_succeeds() throws Exception { + doNothing().when(dataqueryHandler).deleteDataquery(any(Long.class), any(String.class)); + + mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf())) + .andExpect(status().isOk()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testDeleteDataquery_failsWith404OnNotFound() throws Exception { + doThrow(DataqueryException.class).when(dataqueryHandler).deleteDataquery(any(Long.class), any(String.class)); + + mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/1")).with(csrf())) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "DATAPORTAL_TEST_USER") + public void testGetDataquerySlots_succeeds() throws Exception { + doReturn(createSavedQuerySlots()).when(dataqueryHandler).getDataquerySlotsJson(any(String.class)); + + mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + PATH_DATA + "/query-slots")).with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.used").exists()) + .andExpect(jsonPath("$.total").exists()); + } + + @NotNull + private Dataquery createValidDataqueryToStore(long id) { + return Dataquery.builder() + .id(id) + .content(createCrtdl()) + .label("TestLabel") + .comment("TestComment") + .isValid(true) + .build(); + } + + @NotNull + private de.numcodex.feasibility_gui_backend.query.persistence.Dataquery createValidPersistenceDataqueryToGet(long id) throws JsonProcessingException { + var dataquery = new de.numcodex.feasibility_gui_backend.query.persistence.Dataquery(); + dataquery.setId(id); + dataquery.setCrtdl(jsonUtil.writeValueAsString(createCrtdl())); + dataquery.setLabel("TestLabel"); + dataquery.setComment("TestComment"); + dataquery.setLastModified(new Timestamp(new java.util.Date().getTime())); + return dataquery; + } + + @NotNull + private List createValidPersistenceDataqueryListToGet(int entries) throws JsonProcessingException { + var dataqueryList = new ArrayList(); + for (int i = 0; i < entries; ++i) { + dataqueryList.add(createValidPersistenceDataqueryToGet(i)); + } + return dataqueryList; + } + + @NotNull + private Dataquery createValidApiDataqueryToGet(long id) { + return Dataquery.builder() + .id(id) + .content(createCrtdl()) + .label("TestLabel") + .comment("TestComment") + .lastModified(new Timestamp(new java.util.Date().getTime()).toString()) + .createdBy("someone") + .isValid(true) + .build(); + } + + @NotNull + private List createValidApiDataqueryListToGet(int entries) throws JsonProcessingException { + var dataqueryList = new ArrayList(); + for (int i = 0; i < entries; ++i) { + dataqueryList.add(createValidApiDataqueryToGet(i)); + } + return dataqueryList; + } + + @NotNull + private Crtdl createCrtdl() { + return Crtdl.builder() + .cohortDefinition(createValidStructuredQuery()) + .display("foo") + .build(); + } + + @NotNull + private StructuredQuery createValidStructuredQuery() { + var inclusionCriterion = Criterion.builder() + .termCodes(List.of(createTermCode())) + .attributeFilters(List.of()) + .context(createTermCode()) + .build(); + return StructuredQuery.builder() + .version(URI.create("http://to_be_decided.com/draft-2/schema#")) + .inclusionCriteria(List.of(List.of(inclusionCriterion))) + .exclusionCriteria(List.of()) + .display("foo") + .build(); + } + + @NotNull + private StructuredQuery createValidAnnotatedStructuredQuery(boolean withIssues) { + var termCode = TermCode.builder() + .code("LL2191-6") + .system("http://loinc.org") + .display("Geschlecht") + .build(); + var inclusionCriterion = Criterion.builder() + .termCodes(List.of(termCode)) + .attributeFilters(List.of()) + .context(termCode) + .validationIssues(withIssues ? List.of(ValidationIssue.TERMCODE_CONTEXT_COMBINATION_INVALID) : List.of()) + .build(); + return StructuredQuery.builder() + .version(URI.create("http://to_be_decided.com/draft-2/schema#")) + .inclusionCriteria(List.of(List.of(inclusionCriterion))) + .exclusionCriteria(List.of()) + .display("foo") + .build(); + } + + @NotNull + private TermCode createTermCode() { + return TermCode.builder() + .code("LL2191-6") + .system("http://loinc.org") + .display("Geschlecht") + .build(); + } + + @NotNull + private Criterion createInvalidCriterion() { + return Criterion.builder() + .context(null) + .termCodes(List.of(createTermCode())) + .build(); + } + + private SavedQuerySlots createSavedQuerySlots() { + return SavedQuerySlots.builder() + .used(5) + .total(10) + .build(); + } +} diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestControllerIT.java similarity index 60% rename from src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestControllerIT.java rename to src/test/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestControllerIT.java index b5e10405..b7935277 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/query/v4/QueryHandlerRestControllerIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/query/v5/FeasibilityQueryHandlerRestControllerIT.java @@ -1,6 +1,5 @@ -package de.numcodex.feasibility_gui_backend.query.v4; +package de.numcodex.feasibility_gui_backend.query.v5; import de.numcodex.feasibility_gui_backend.config.WebSecurityConfig; -import de.numcodex.feasibility_gui_backend.query.QueryNotFoundException; import de.numcodex.feasibility_gui_backend.query.api.*; import com.fasterxml.jackson.databind.ObjectMapper; @@ -33,7 +32,6 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.context.annotation.Import; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.http.HttpStatus; import org.springframework.security.core.Authentication; import org.springframework.security.test.context.support.WithMockUser; @@ -45,8 +43,7 @@ import java.net.URI; import java.util.List; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_API; -import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.PATH_QUERY; +import static de.numcodex.feasibility_gui_backend.config.WebSecurityConfig.*; import static de.numcodex.feasibility_gui_backend.query.persistence.ResultType.SUCCESS; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.not; @@ -63,13 +60,15 @@ RateLimitingServiceSpringConfig.class }) @WebMvcTest( - controllers = QueryHandlerRestController.class, + controllers = FeasibilityQueryHandlerRestController.class, properties = { "app.enableQueryValidation=true" } ) @SuppressWarnings("NewClassNamingConvention") -public class QueryHandlerRestControllerIT { +public class FeasibilityQueryHandlerRestControllerIT { + + private static final String PATH = PATH_API + PATH_QUERY + PATH_FEASIBILITY; @Autowired private MockMvc mockMvc; @@ -120,7 +119,7 @@ void initTest() throws Exception { public void testRunQueryEndpoint_FailsOnInvalidStructuredQueryWith400() throws Exception { var testQuery = StructuredQuery.builder().build(); - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(status().isBadRequest()); @@ -135,7 +134,7 @@ public void testRunQueryEndpoint_SucceedsOnValidStructuredQueryWith201() throws doReturn(Mono.just(1L)).when(queryHandlerService).runQuery(any(StructuredQuery.class), eq("test")); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -144,7 +143,7 @@ public void testRunQueryEndpoint_SucceedsOnValidStructuredQueryWith201() throws mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isCreated()) .andExpect(header().exists("location")) - .andExpect(header().string("location", PATH_API + PATH_QUERY + "/1")); + .andExpect(header().string("location", PATH + "/1")); } @Test @@ -158,7 +157,7 @@ public void testRunQueryEndpoint_FailsOnDownstreamServiceError() throws Exceptio doReturn(Mono.error(dispatchError)).when(queryHandlerService).runQuery(any(StructuredQuery.class), eq("test")); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -177,7 +176,7 @@ public void testRunQueryEndpoint_FailsOnSoftQuotaExceeded() throws Exception { doReturn((long)quotaSoftCreateAmount + 1).when(queryHandlerService).getAmountOfQueriesByUserAndInterval(any(String.class), any(Integer.class)); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -195,7 +194,7 @@ public void testValidateQueryEndpoint_SucceedsOnValidQuery() throws Exception { doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/validate")).with(csrf()) + mockMvc.perform(post(URI.create(PATH + "/validate")).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(status().isOk()) @@ -211,7 +210,7 @@ public void testValidateQueryEndpoint_SucceedsDespiteInvalidCriteriaWith200() th doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/validate")).with(csrf()) + mockMvc.perform(post(URI.create(PATH + "/validate")).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(status().isOk()) @@ -233,7 +232,7 @@ public void testRunQueryEndpoint_FailsOnBeingBlacklistedWith403() throws Excepti doReturn(Mono.just(1L)).when(queryHandlerService).runQuery(any(StructuredQuery.class), eq("test")); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -253,7 +252,7 @@ public void testRunQueryEndpoint_FailsOnExceedingHardLimitWith403() throws Excep doReturn(Mono.just(1L)).when(queryHandlerService).runQuery(any(StructuredQuery.class), eq("test")); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -275,7 +274,7 @@ public void testRunQueryEndpoint_SucceedsOnExceedingHardlimitAsPowerUserWith201( doReturn(Mono.just(1L)).when(queryHandlerService).runQuery(any(StructuredQuery.class), eq("test")); doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - var mvcResult = mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY)).with(csrf()) + var mvcResult = mockMvc.perform(post(URI.create(PATH)).with(csrf()) .contentType(APPLICATION_JSON) .content(jsonUtil.writeValueAsString(testQuery))) .andExpect(request().asyncStarted()) @@ -284,280 +283,14 @@ public void testRunQueryEndpoint_SucceedsOnExceedingHardlimitAsPowerUserWith201( mockMvc.perform(asyncDispatch(mvcResult)) .andExpect(status().isCreated()) .andExpect(header().exists("location")) - .andExpect(header().string("location", PATH_API + PATH_QUERY + "/1")); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQueryList_SucceedsWithValidation() throws Exception { - long queryId = 1; - doReturn(List.of(createValidQuery(queryId))).when(queryHandlerService).getQueryListForAuthor(any(String.class), any(Boolean.class)); - doReturn(List.of(createValidQueryListEntry(queryId, false))).when(queryHandlerService).convertQueriesToQueryListEntries(anyList(), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY)).with(csrf()).param("skip-validation", "false")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(queryId)) - .andExpect(jsonPath("$[0].isValid").exists()) - .andExpect(jsonPath("$[0].isValid").value(true)); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQueryList_SucceedsWithoutValidation() throws Exception { - long queryId = 1; - doReturn(List.of(createValidQuery(queryId))).when(queryHandlerService).getQueryListForAuthor(any(String.class), any(Boolean.class)); - doReturn(List.of(createValidQueryListEntry(queryId, true))).when(queryHandlerService).convertQueriesToQueryListEntries(anyList(), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY)).with(csrf()).param("skip-validation", "true")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(queryId)) - .andExpect(jsonPath("$[0].isValid").doesNotExist()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQueryList_SucceedsWithoutDefiningSkipValidation() throws Exception { - long queryId = 1; - doReturn(List.of(createValidQuery(queryId))).when(queryHandlerService).getQueryListForAuthor(any(String.class), any(Boolean.class)); - doReturn(List.of(createValidQueryListEntry(queryId, false))).when(queryHandlerService).convertQueriesToQueryListEntries(anyList(), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(queryId)) - .andExpect(jsonPath("$[0].isValid").exists()) - .andExpect(jsonPath("$[0].isValid").value(true)); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_ADMIN"}, username = "test") - public void testGetQueryListForUser_SucceedsOnValidUser() throws Exception { - long queryId = 1; - String userId = "user1"; - doReturn(List.of(createValidQuery(queryId))).when(queryHandlerService).getQueryListForAuthor(any(String.class), any(Boolean.class)); - doReturn(List.of(createValidQueryListEntry(queryId, false))).when(queryHandlerService).convertQueriesToQueryListEntries(anyList(), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/by-user/" + userId)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0].id").value(queryId)); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_ADMIN"}, username = "test") - public void testGetQueryListForUser_ReturnsEmptyOnUnknownUser() throws Exception { - String userId = "user1"; - doReturn(List.of()).when(queryHandlerService).getQueryListForAuthor(any(String.class), any(Boolean.class)); - doReturn(List.of()).when(queryHandlerService).convertQueriesToQueryListEntries(anyList(), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/by-user/" + userId)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$[0]").doesNotExist()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQuery_succeeds() throws Exception { - long queryId = 1; - var annotatedQuery = createValidAnnotatedStructuredQuery(false); - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - doReturn(createValidApiQuery(queryId)).when(queryHandlerService).getQuery(any(Long.class)); - doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/" + queryId)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.id").value(queryId)); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQuery_failsOnWrongAuthorWith403() throws Exception { - long queryId = 1; - var annotatedQuery = createValidAnnotatedStructuredQuery(false); - doReturn("some-other-user").when(queryHandlerService).getAuthorId(any(Long.class)); - doReturn(createValidApiQuery(queryId)).when(queryHandlerService).getQuery(any(Long.class)); - doReturn(annotatedQuery).when(structuredQueryValidation).annotateStructuredQuery(any(StructuredQuery.class), any(Boolean.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/" + queryId)).with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testSaveQuery_Succeeds() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testSaveQuery_failsWith404OnAuthorForQueryNotFound() throws Exception { - doThrow(QueryNotFoundException.class).when(queryHandlerService).getAuthorId(any(Long.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testSaveQuery_failsWith403OnAuthorMismatch() throws Exception { - doReturn("SomeOtherUser").when(queryHandlerService).getAuthorId(any(Long.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testSaveQuery_failsWith409OnExistingSavedQuery() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - doThrow(DataIntegrityViolationException.class).when(queryHandlerService).saveQuery(any(Long.class), any(String.class), any(SavedQuery.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isConflict()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testSaveQuery_failsWith403OnNoFreeSlots() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - doReturn(maxSavedQueriesPerUser).when(queryHandlerService).getAmountOfSavedQueriesByUser(any(String.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(post(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testUpdateSavedQuery_Succeeds() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testUpdateSavedQuery_failsWith403OnAuthorMismatch() throws Exception { - doReturn("SomeOtherUser").when(queryHandlerService).getAuthorId(any(Long.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testUpdateSavedQuery_failsWith404OnAuthorForQueryNotFound() throws Exception { - doThrow(QueryNotFoundException.class).when(queryHandlerService).getAuthorId(any(Long.class)); - - var savedQuery = SavedQuery.builder() - .label("foo") - .comment("bar") - .totalNumberOfPatients(100L) - .build(); - - mockMvc.perform(put(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf()) - .contentType(APPLICATION_JSON) - .content(jsonUtil.writeValueAsString(savedQuery))) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testDeleteSavedQuery_Succeeds() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf())) - .andExpect(status().isOk()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testDeleteSavedQuery_FailsWith403OnWrongAuthor() throws Exception { - doReturn("some-other-user").when(queryHandlerService).getAuthorId(any(Long.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf())) - .andExpect(status().isForbidden()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testDeleteSavedQuery_FailsWith404IfQueryNotFound() throws Exception { - doThrow(QueryNotFoundException.class).when(queryHandlerService).getAuthorId(any(Long.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf())) - .andExpect(status().isNotFound()); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - void testDeleteSavedQuery_FailsWith404IfSavedQueryNotFound() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - doThrow(QueryNotFoundException.class).when(queryHandlerService).deleteSavedQuery(any(Long.class)); - - mockMvc.perform(delete(URI.create(PATH_API + PATH_QUERY + "/1/saved")).with(csrf())) - .andExpect(status().isNotFound()); + .andExpect(header().string("location", PATH + "/1")); } @ParameterizedTest @EnumSource @WithMockUser(roles = {"DATAPORTAL_TEST_ADMIN"}, username = "test") public void testGetQueryResult_succeeds(QueryHandlerService.ResultDetail resultDetail) throws Exception { - var requestUri = PATH_API + PATH_QUERY + "/1"; + var requestUri = PATH + "/1"; doReturn(true).when(authenticationHelper).hasAuthority(any(Authentication.class), eq("DATAPORTAL_TEST_ADMIN")); doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); doReturn(createTestQueryResult(resultDetail)).when(queryHandlerService).getQueryResult(any(Long.class), any(QueryHandlerService.ResultDetail.class)); @@ -589,7 +322,7 @@ public void testGetQueryResult_succeeds(QueryHandlerService.ResultDetail resultD @Test @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") public void testGetDetailedObfuscatedQueryResult_returnsIssueWhenBelowThreshold() throws Exception { - var requestUri = PATH_API + PATH_QUERY + "/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT; + var requestUri = PATH + "/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT; doReturn(true).when(authenticationHelper).hasAuthority(any(Authentication.class), eq("DATAPORTAL_TEST_USER")); doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); doReturn(createTestDetailedObfuscatedQueryResultWithTooFewResults(thresholdSitesResult)) @@ -609,37 +342,15 @@ public void testGetDetailedObfuscatedQueryResult_returnsIssueWhenBelowThreshold( public void testGetDetailedObfuscatedResult_failsOnWrongAuthorWith403() throws Exception { doReturn("some-other-user").when(queryHandlerService).getAuthorId(any(Long.class)); - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT)) + mockMvc.perform(get(URI.create(PATH + "/1" + WebSecurityConfig.PATH_DETAILED_OBFUSCATED_RESULT)) .with(csrf())) .andExpect(status().isForbidden()); } - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQueryContent_succeeds() throws Exception { - doReturn("test").when(queryHandlerService).getAuthorId(any(Long.class)); - doReturn(createValidStructuredQuery()).when(queryHandlerService).getQueryContent(any(Long.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/1" + WebSecurityConfig.PATH_CONTENT)).with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.inclusionCriteria").exists()) - .andExpect(jsonPath("$.inclusionCriteria", not(empty()))); - } - - @Test - @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") - public void testGetQueryContent_failsOnWrongAuthorWith403() throws Exception { - doReturn("not-test").when(queryHandlerService).getAuthorId(any(Long.class)); - doReturn(createValidStructuredQuery()).when(queryHandlerService).getQueryContent(any(Long.class)); - - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/1" + WebSecurityConfig.PATH_CONTENT)).with(csrf())) - .andExpect(status().isForbidden()); - } - @Test @WithMockUser(roles = {"DATAPORTAL_TEST_USER"}, username = "test") public void testGetDetailedObfuscatedResultRateLimit_succeeds() throws Exception { - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/detailed-obfuscated-result-rate-limit")).with(csrf())) + mockMvc.perform(get(URI.create(PATH + "/detailed-obfuscated-result-rate-limit")).with(csrf())) .andExpect(status().isOk()) .andExpect(jsonPath("$.limit").exists()) .andExpect(jsonPath("$.remaining").exists()); @@ -647,7 +358,7 @@ public void testGetDetailedObfuscatedResultRateLimit_succeeds() throws Exception @Test public void testGetDetailedObfuscatedResultRateLimit_failsOnNotLoggedIn() throws Exception { - mockMvc.perform(get(URI.create(PATH_API + PATH_QUERY + "/detailed-obfuscated-result-rate-limit")).with(csrf())) + mockMvc.perform(get(URI.create(PATH + "/detailed-obfuscated-result-rate-limit")).with(csrf())) .andExpect(status().isUnauthorized()); } diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestControllerIT.java similarity index 99% rename from src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestControllerIT.java rename to src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestControllerIT.java index 4a72dc89..ea42abbc 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/CodeableConceptRestControllerIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/CodeableConceptRestControllerIT.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.terminology.v4; +package de.numcodex.feasibility_gui_backend.terminology.v5; import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.common.api.DisplayEntry; diff --git a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestControllerIT.java b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestControllerIT.java similarity index 99% rename from src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestControllerIT.java rename to src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestControllerIT.java index baf1839c..35133b9b 100644 --- a/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v4/TerminologyRestControllerIT.java +++ b/src/test/java/de/numcodex/feasibility_gui_backend/terminology/v5/TerminologyRestControllerIT.java @@ -1,4 +1,4 @@ -package de.numcodex.feasibility_gui_backend.terminology.v4; +package de.numcodex.feasibility_gui_backend.terminology.v5; import com.fasterxml.jackson.databind.ObjectMapper; import de.numcodex.feasibility_gui_backend.common.api.Comparator; diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index a2fd418f..096a8da6 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -14,7 +14,7 @@ app: keycloakAllowedRole: "DATAPORTAL_TEST_USER" keycloakPowerRole: "DATAPORTAL_TEST_POWER" keycloakAdminRole: "DATAPORTAL_TEST_ADMIN" - maxSavedQueriesPerUser: 2 + maxSavedQueriesPerUser: 10 broker: aktin: enabled: false