From 08b18bec8aec38986e1cd5f0a3b44c07d8f03840 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Fri, 17 Oct 2025 23:58:28 -0300 Subject: [PATCH 01/30] feat(delete-question): add DeleteQuestion service and presenter interface --- .../delete/DeleteQuestionPresenter.kt | 5 +++++ .../question/delete/DeleteQuestionService.kt | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt create mode 100644 review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt new file mode 100644 index 00000000..c6389872 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt @@ -0,0 +1,5 @@ +package br.all.application.question.delete + +import br.all.domain.shared.presenter.GenericPresenter + +interface DeleteQuestionPresenter : GenericPresenter \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt new file mode 100644 index 00000000..8bcd64af --- /dev/null +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt @@ -0,0 +1,22 @@ +package br.all.application.question.delete + +import io.swagger.v3.oas.annotations.media.Schema +import java.util.UUID + +interface DeleteQuestionService { + fun delete(presenter: DeleteQuestionPresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val questionId: UUID, + val questionContext: String, + ) + + @Schema(name = "DeleteQuestionResponseModel") + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val questionId: UUID, + ) +} \ No newline at end of file From 8665aaad11c956e17f68417d07cffd6990261d0b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:20:36 -0300 Subject: [PATCH 02/30] refactor(delete-question): remove unused parameter from request model --- .../br/all/application/question/delete/DeleteQuestionService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt index 8bcd64af..75e07ac4 100644 --- a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt @@ -10,7 +10,6 @@ interface DeleteQuestionService { val userId: UUID, val systematicStudyId: UUID, val questionId: UUID, - val questionContext: String, ) @Schema(name = "DeleteQuestionResponseModel") From 8ed260912f9633fc91234909f8a9875d5e274444 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:23:13 -0300 Subject: [PATCH 03/30] fix(delete-question): wrong generic presenter model --- .../all/application/question/delete/DeleteQuestionPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt index c6389872..2e5fbf96 100644 --- a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionPresenter.kt @@ -2,4 +2,4 @@ package br.all.application.question.delete import br.all.domain.shared.presenter.GenericPresenter -interface DeleteQuestionPresenter : GenericPresenter \ No newline at end of file +interface DeleteQuestionPresenter : GenericPresenter \ No newline at end of file From f9ce03d91d6af2c223990131dfa4265b6d2e51d6 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:24:14 -0300 Subject: [PATCH 04/30] feat(question-repository): add deleteById method --- .../br/all/application/question/repository/QuestionRepository.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/review/src/main/kotlin/br/all/application/question/repository/QuestionRepository.kt b/review/src/main/kotlin/br/all/application/question/repository/QuestionRepository.kt index 457af8af..d3e7568e 100644 --- a/review/src/main/kotlin/br/all/application/question/repository/QuestionRepository.kt +++ b/review/src/main/kotlin/br/all/application/question/repository/QuestionRepository.kt @@ -7,4 +7,5 @@ interface QuestionRepository { fun createOrUpdate(dto: QuestionDto) fun findById(systematicStudyId: UUID, id: UUID): QuestionDto? fun findAllBySystematicStudyId(systematicStudyId: UUID, context: QuestionContextEnum? = null): List + fun deleteById(systematicStudyId: UUID, id: UUID) } \ No newline at end of file From e3ab25a001971ef17f18fde01f017f1bc9087098 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:24:42 -0300 Subject: [PATCH 05/30] feat(delete-question): implement DeleteQuestion service logic --- .../delete/DeleteQuestionServiceImpl.kt | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt new file mode 100644 index 00000000..17d9f776 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt @@ -0,0 +1,45 @@ +package br.all.application.question.delete + +import br.all.application.question.repository.QuestionRepository +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.user.CredentialsService +import br.all.domain.model.question.QuestionId +import br.all.domain.model.review.SystematicStudy +import org.springframework.stereotype.Service + +@Service +class DeleteQuestionServiceImpl( + private val systematicStudyRepository: SystematicStudyRepository, + private val questionRepository: QuestionRepository, + private val credentialsService: CredentialsService, +) : DeleteQuestionService { + override fun delete(presenter: DeleteQuestionPresenter, request: DeleteQuestionService.RequestModel) { + val systematicStudyId = request.systematicStudyId + val userId = request.userId + + val user = credentialsService.loadCredentials(userId)?.toUser() + val systematicStudyDto = systematicStudyRepository.findById(systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + if (presenter.isDone()) return + + val questionId = QuestionId(request.questionId) + val question = questionRepository.findById(systematicStudyId, questionId.value) + if (question == null) { + presenter.prepareFailView(NoSuchElementException("Question with id ${questionId.value} not found for study $systematicStudyId.")) + return + } + + try { + questionRepository.deleteById(systematicStudyId, questionId.value) + } catch (e: Exception) { + presenter.prepareFailView(e) + return + } + + presenter.prepareSuccessView(DeleteQuestionService.ResponseModel(userId, systematicStudyId, question.questionId)) + } +} \ No newline at end of file From d56bbbd212217bacea79eef7d2df1ff5e5842684 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:34:00 -0300 Subject: [PATCH 06/30] feat(delete-question): add restful presenters for both rob and extraction --- ...estfulDeleteExtractionQuestionPresenter.kt | 34 +++++++++++++++++++ .../RestfulDeleteRoBQuestionPresenter.kt | 34 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt create mode 100644 web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt diff --git a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt new file mode 100644 index 00000000..49b9b49b --- /dev/null +++ b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt @@ -0,0 +1,34 @@ +package br.all.question.presenter.extraction + +import br.all.application.question.delete.DeleteQuestionPresenter +import br.all.application.question.delete.DeleteQuestionService +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.UUID + +class RestfulDeleteExtractionQuestionPresenter( + private val linksFactory: LinksFactory, +) : DeleteQuestionPresenter { + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: DeleteQuestionService.ResponseModel) { + val restfulResponse = ViewModel( + response.userId, response.systematicStudyId, response.questionId, + ) + + responseEntity = ResponseEntity.status(HttpStatus.NO_CONTENT).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val userId: UUID, + val systematicStudyId: UUID, + val questionId: UUID + ): RepresentationModel() +} \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt new file mode 100644 index 00000000..93aeae26 --- /dev/null +++ b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt @@ -0,0 +1,34 @@ +package br.all.question.presenter.riskOfBias + +import br.all.application.question.delete.DeleteQuestionPresenter +import br.all.application.question.delete.DeleteQuestionService +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.UUID + +class RestfulDeleteRoBQuestionPresenter( + private val linksFactory: LinksFactory, +) : DeleteQuestionPresenter { + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: DeleteQuestionService.ResponseModel) { + val restfulResponse = ViewModel( + response.userId, response.systematicStudyId, response.questionId, + ) + + responseEntity = ResponseEntity.status(HttpStatus.NO_CONTENT).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val userId: UUID, + val systematicStudyId: UUID, + val questionId: UUID + ): RepresentationModel() +} \ No newline at end of file From 4f4122c9a7521a59b52f994a12d291fccf874b3e Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:44:32 -0300 Subject: [PATCH 07/30] feat(delete-question): add endpoint to delete extraction question by ID --- .../ExtractionQuestionController.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt index 69ee16bb..a3848e68 100644 --- a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt @@ -3,12 +3,14 @@ package br.all.question.controller import br.all.application.question.create.CreateQuestionService import br.all.application.question.create.CreateQuestionService.QuestionType.* import br.all.application.question.create.CreateQuestionService.RequestModel +import br.all.application.question.delete.DeleteQuestionService import br.all.application.question.find.FindQuestionService import br.all.application.question.findAll.FindAllBySystematicStudyIdService import br.all.application.question.update.services.UpdateQuestionService import br.all.application.question.findAll.FindAllBySystematicStudyIdService.RequestModel as FindAllRequest import br.all.question.presenter.extraction.RestfulFindExtractionQuestionPresenter import br.all.question.presenter.extraction.RestfulCreateExtractionQuestionPresenter +import br.all.question.presenter.extraction.RestfulDeleteExtractionQuestionPresenter import br.all.question.presenter.extraction.RestfulFindAllExtractionQuestionPresenter import br.all.question.presenter.extraction.RestfulUpdateExtractionQuestionPresenter import br.all.question.requests.PutRequest @@ -33,6 +35,7 @@ class ExtractionQuestionController( val findOneService: FindQuestionService, val findAllService: FindAllBySystematicStudyIdService, val updateQuestionService: UpdateQuestionService, + val deleteQuestionService: DeleteQuestionService, val linksFactory: LinksFactory ) { data class TextualRequest(val code: String, val description: String) @@ -364,4 +367,47 @@ class ExtractionQuestionController( updateQuestionService.update(presenter, requestModel) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @DeleteMapping("/{questionId}") + @Operation(summary = "Delete a extraction question by id") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "204", + description = "Success deleting an extraction question by id", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = DeleteQuestionService.ResponseModel::class) + )] + ), + ApiResponse( + responseCode = "404", + description = "Fail deleting an extraction question by id - not found", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail deleting an extraction question by id - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail deleting an extraction question by id - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun deleteQuestion( + @PathVariable systematicStudyId: UUID, + @PathVariable questionId: UUID + ): ResponseEntity<*> { + val presenter = RestfulDeleteExtractionQuestionPresenter(linksFactory) + val request = DeleteQuestionService.RequestModel( + authenticationInfoService.getAuthenticatedUserId(), + systematicStudyId, + questionId + ) + deleteQuestionService.delete(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } \ No newline at end of file From 3fe270b6f4c613f545977613f2c7425b954a738c Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:47:13 -0300 Subject: [PATCH 08/30] feat(delete-question): add endpoint to delete risk of bias question by ID --- .../RiskOfBiasQuestionController.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt index 41499029..58ab2f85 100644 --- a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt @@ -3,10 +3,12 @@ package br.all.question.controller import br.all.application.question.create.CreateQuestionService import br.all.application.question.create.CreateQuestionService.QuestionType.* import br.all.application.question.create.CreateQuestionService.RequestModel +import br.all.application.question.delete.DeleteQuestionService import br.all.application.question.find.FindQuestionService import br.all.application.question.findAll.FindAllBySystematicStudyIdService import br.all.application.question.update.services.UpdateQuestionService import br.all.question.presenter.riskOfBias.RestfulCreateRoBQuestionPresenter +import br.all.question.presenter.riskOfBias.RestfulDeleteRoBQuestionPresenter import br.all.question.presenter.riskOfBias.RestfulFindAllRoBQuestionPresenter import br.all.question.presenter.riskOfBias.RestfulFindRoBQuestionPresenter import br.all.question.presenter.riskOfBias.RestfulUpdateRoBQuestionPresenter @@ -32,6 +34,7 @@ class RiskOfBiasQuestionController( val findOneService: FindQuestionService, val findAllService: FindAllBySystematicStudyIdService, val updateQuestionService: UpdateQuestionService, + val deleteQuestionService: DeleteQuestionService, val linksFactory: LinksFactory ) { data class TextualRequest(val code: String, val description: String) @@ -360,4 +363,47 @@ class RiskOfBiasQuestionController( updateQuestionService.update(presenter, requestModel) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @DeleteMapping("/{questionId}") + @Operation(summary = "Delete a extraction question by id") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "204", + description = "Success deleting an risk of bias question by id", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = DeleteQuestionService.ResponseModel::class) + )] + ), + ApiResponse( + responseCode = "404", + description = "Fail deleting an risk of bias question by id - not found", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "401", + description = "Fail deleting an risk of bias question by id - unauthenticated user", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Fail deleting an risk of bias question by id - unauthorized user", + content = [Content(schema = Schema(hidden = true))] + ), + ] + ) + fun deleteQuestion( + @PathVariable systematicStudyId: UUID, + @PathVariable questionId: UUID + ): ResponseEntity<*> { + val presenter = RestfulDeleteRoBQuestionPresenter(linksFactory) + val request = DeleteQuestionService.RequestModel( + authenticationInfoService.getAuthenticatedUserId(), + systematicStudyId, + questionId + ) + deleteQuestionService.delete(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } From b8e2ebf4fd6fbcaab20c38aa3a528c5f97ea0e81 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 00:53:20 -0300 Subject: [PATCH 09/30] feat(question-repository): implement deleteById method --- .../br/all/infrastructure/question/QuestionRepositoryImpl.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/review/src/main/kotlin/br/all/infrastructure/question/QuestionRepositoryImpl.kt b/review/src/main/kotlin/br/all/infrastructure/question/QuestionRepositoryImpl.kt index 751edb8f..849ac1dd 100644 --- a/review/src/main/kotlin/br/all/infrastructure/question/QuestionRepositoryImpl.kt +++ b/review/src/main/kotlin/br/all/infrastructure/question/QuestionRepositoryImpl.kt @@ -29,4 +29,8 @@ class QuestionRepositoryImpl(private val repository: MongoQuestionRepository) : return filteredQuestions.map { it.toDto() } } + + override fun deleteById(systematicStudyId: UUID, id: UUID) { + repository.deleteById(id) + } } \ No newline at end of file From 5fdaddcff15f40c0c45abb7bb0f6c3ea079a4c4a Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 13:19:51 -0300 Subject: [PATCH 10/30] docs(delete-question): update documentation to reflect removal of answers when deleting extraction questions --- .../controller/ExtractionQuestionController.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt index a3848e68..2bea6db6 100644 --- a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt @@ -369,12 +369,12 @@ class ExtractionQuestionController( } @DeleteMapping("/{questionId}") - @Operation(summary = "Delete a extraction question by id") + @Operation(summary = "Delete a extraction question by id and remove all its answers from Study Reviews") @ApiResponses( value = [ ApiResponse( responseCode = "204", - description = "Success deleting an extraction question by id", + description = "Success deleting an extraction question by id and removing all its answers from Study Reviews", content = [Content( mediaType = "application/json", schema = Schema(implementation = DeleteQuestionService.ResponseModel::class) @@ -382,17 +382,17 @@ class ExtractionQuestionController( ), ApiResponse( responseCode = "404", - description = "Fail deleting an extraction question by id - not found", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - not found", content = [Content(schema = Schema(hidden = true))] ), ApiResponse( responseCode = "401", - description = "Fail deleting an extraction question by id - unauthenticated user", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - unauthenticated user", content = [Content(schema = Schema(hidden = true))] ), ApiResponse( responseCode = "403", - description = "Fail deleting an extraction question by id - unauthorized user", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - unauthorized user", content = [Content(schema = Schema(hidden = true))] ), ] From dba5aa18a8ca33f124613343c03fb03b9265d905 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 13:40:28 -0300 Subject: [PATCH 11/30] feat(delete-question): remove associated answers from study reviews on question deletion --- .../delete/DeleteQuestionServiceImpl.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt index 17d9f776..892d349c 100644 --- a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt @@ -4,6 +4,7 @@ import br.all.application.question.repository.QuestionRepository import br.all.application.review.repository.SystematicStudyRepository import br.all.application.review.repository.fromDto import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.repository.StudyReviewRepository import br.all.application.user.CredentialsService import br.all.domain.model.question.QuestionId import br.all.domain.model.review.SystematicStudy @@ -14,6 +15,7 @@ class DeleteQuestionServiceImpl( private val systematicStudyRepository: SystematicStudyRepository, private val questionRepository: QuestionRepository, private val credentialsService: CredentialsService, + private val studyReviewRepository: StudyReviewRepository, ) : DeleteQuestionService { override fun delete(presenter: DeleteQuestionPresenter, request: DeleteQuestionService.RequestModel) { val systematicStudyId = request.systematicStudyId @@ -35,6 +37,22 @@ class DeleteQuestionServiceImpl( try { questionRepository.deleteById(systematicStudyId, questionId.value) + + val studyReviews = studyReviewRepository.findAllFromReview(systematicStudyId) + studyReviews.forEach { review -> + val updatedFormAnswers = review.formAnswers.filterNot { it.key == questionId.value } + val updatedRobAnswers = review.robAnswers.filterNot { it.key == questionId.value } + + if (updatedFormAnswers != review.formAnswers || updatedRobAnswers != review.robAnswers) { + val updatedReview = review.copy( + formAnswers = updatedFormAnswers, + robAnswers = updatedRobAnswers, + ) + + studyReviewRepository.saveOrUpdate(updatedReview) + } + } + } catch (e: Exception) { presenter.prepareFailView(e) return From cfd8f5b630e702ac2e2a925781e80ea8b9b916ce Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 13:44:47 -0300 Subject: [PATCH 12/30] feat(delete-question): track affected study review IDs during question deletion --- .../all/application/question/delete/DeleteQuestionService.kt | 1 + .../application/question/delete/DeleteQuestionServiceImpl.kt | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt index 75e07ac4..1f8fff68 100644 --- a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionService.kt @@ -17,5 +17,6 @@ interface DeleteQuestionService { val userId: UUID, val systematicStudyId: UUID, val questionId: UUID, + val affectedStudyReviewIds: List ) } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt index 892d349c..e31fc281 100644 --- a/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/question/delete/DeleteQuestionServiceImpl.kt @@ -35,6 +35,8 @@ class DeleteQuestionServiceImpl( return } + val affectedStudyReviewIds = mutableListOf() + try { questionRepository.deleteById(systematicStudyId, questionId.value) @@ -50,6 +52,7 @@ class DeleteQuestionServiceImpl( ) studyReviewRepository.saveOrUpdate(updatedReview) + affectedStudyReviewIds.add(review.studyReviewId) } } @@ -58,6 +61,6 @@ class DeleteQuestionServiceImpl( return } - presenter.prepareSuccessView(DeleteQuestionService.ResponseModel(userId, systematicStudyId, question.questionId)) + presenter.prepareSuccessView(DeleteQuestionService.ResponseModel(userId, systematicStudyId, question.questionId, affectedStudyReviewIds)) } } \ No newline at end of file From 39276c5d9631759eea11e369fd69f36501dd28d9 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 13:45:48 -0300 Subject: [PATCH 13/30] feat(delete-question): include affected study review IDs in delete response and update status code to OK --- .../extraction/RestfulDeleteExtractionQuestionPresenter.kt | 7 ++++--- .../riskOfBias/RestfulDeleteRoBQuestionPresenter.kt | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt index 49b9b49b..2c41edeb 100644 --- a/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/extraction/RestfulDeleteExtractionQuestionPresenter.kt @@ -16,10 +16,10 @@ class RestfulDeleteExtractionQuestionPresenter( override fun prepareSuccessView(response: DeleteQuestionService.ResponseModel) { val restfulResponse = ViewModel( - response.userId, response.systematicStudyId, response.questionId, + response.userId, response.systematicStudyId, response.questionId, response.affectedStudyReviewIds ) - responseEntity = ResponseEntity.status(HttpStatus.NO_CONTENT).body(restfulResponse) + responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) } override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } @@ -29,6 +29,7 @@ class RestfulDeleteExtractionQuestionPresenter( private data class ViewModel( val userId: UUID, val systematicStudyId: UUID, - val questionId: UUID + val questionId: UUID, + val affectedStudyReviewIds: List ): RepresentationModel() } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt index 93aeae26..54ac165d 100644 --- a/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt +++ b/web/src/main/kotlin/br/all/question/presenter/riskOfBias/RestfulDeleteRoBQuestionPresenter.kt @@ -16,10 +16,10 @@ class RestfulDeleteRoBQuestionPresenter( override fun prepareSuccessView(response: DeleteQuestionService.ResponseModel) { val restfulResponse = ViewModel( - response.userId, response.systematicStudyId, response.questionId, + response.userId, response.systematicStudyId, response.questionId, response.affectedStudyReviewIds ) - responseEntity = ResponseEntity.status(HttpStatus.NO_CONTENT).body(restfulResponse) + responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) } override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } @@ -29,6 +29,7 @@ class RestfulDeleteRoBQuestionPresenter( private data class ViewModel( val userId: UUID, val systematicStudyId: UUID, - val questionId: UUID + val questionId: UUID, + val affectedStudyReviewIds: List ): RepresentationModel() } \ No newline at end of file From c8c51f62fa6322e3b7426785ca74e43035f99145 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 13:48:23 -0300 Subject: [PATCH 14/30] docs(delete-question): update descriptions and status codes for question deletion endpoints --- .../controller/ExtractionQuestionController.kt | 2 +- .../controller/RiskOfBiasQuestionController.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt index 2bea6db6..72eb9490 100644 --- a/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/ExtractionQuestionController.kt @@ -373,7 +373,7 @@ class ExtractionQuestionController( @ApiResponses( value = [ ApiResponse( - responseCode = "204", + responseCode = "200", description = "Success deleting an extraction question by id and removing all its answers from Study Reviews", content = [Content( mediaType = "application/json", diff --git a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt index 58ab2f85..da07f7ce 100644 --- a/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt +++ b/web/src/main/kotlin/br/all/question/controller/RiskOfBiasQuestionController.kt @@ -365,12 +365,12 @@ class RiskOfBiasQuestionController( } @DeleteMapping("/{questionId}") - @Operation(summary = "Delete a extraction question by id") + @Operation(summary = "Delete a extraction question by id and remove all its answers from Study Reviews") @ApiResponses( value = [ ApiResponse( - responseCode = "204", - description = "Success deleting an risk of bias question by id", + responseCode = "200", + description = "Success deleting an extraction question by id and removing all its answers from Study Reviews", content = [Content( mediaType = "application/json", schema = Schema(implementation = DeleteQuestionService.ResponseModel::class) @@ -378,17 +378,17 @@ class RiskOfBiasQuestionController( ), ApiResponse( responseCode = "404", - description = "Fail deleting an risk of bias question by id - not found", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - not found", content = [Content(schema = Schema(hidden = true))] ), ApiResponse( responseCode = "401", - description = "Fail deleting an risk of bias question by id - unauthenticated user", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - unauthenticated user", content = [Content(schema = Schema(hidden = true))] ), ApiResponse( responseCode = "403", - description = "Fail deleting an risk of bias question by id - unauthorized user", + description = "Fail deleting an extraction question by id and removing all its answers from Study Reviews - unauthorized user", content = [Content(schema = Schema(hidden = true))] ), ] From 15afcb82d8b50ed584bcf2b31fd1384580b899e1 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 15:06:50 -0300 Subject: [PATCH 15/30] feat(study-review): add paginated retrieval for reviews by session ID --- .../application/study/repository/StudyReviewRepository.kt | 1 + .../infrastructure/study/MongoStudyReviewRepository.kt | 2 ++ .../all/infrastructure/study/StudyReviewRepositoryImpl.kt | 8 ++++++++ 3 files changed, 11 insertions(+) diff --git a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt index 6978afce..5275bff1 100644 --- a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt @@ -11,6 +11,7 @@ interface StudyReviewRepository { fun findAllFromReviewPaged(reviewId: UUID, pageable: Pageable = Pageable.unpaged()): Page fun findAllBySource(reviewId: UUID, source: String): List fun findAllBySession(reviewId: UUID, searchSessionId: UUID): List + fun findAllBySessionPaged(reviewId: UUID, searchSessionId: UUID, pageable: Pageable = Pageable.unpaged()): Page fun findById(reviewId: UUID, studyId: Long) : StudyReviewDto? fun updateSelectionStatus(reviewId: UUID, studyId: Long, attributeName: String, newStatus: Any) fun saveOrUpdateBatch(dtos: List) diff --git a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt index c6d8893b..47441b43 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt @@ -21,6 +21,8 @@ interface MongoStudyReviewRepository : MongoRepository + fun findAllById_SystematicStudyIdAndSearchSessionId(reviewID: UUID, searchSessionId: UUID, pageable: Pageable): Page + @Update("{ '\$set' : { ?1 : ?2 } }") fun findAndUpdateAttributeById(id: StudyReviewId, attributeName:String, newStatus: Any) diff --git a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt index 838f133c..1312f51c 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt @@ -32,6 +32,14 @@ open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRe override fun findAllBySession(reviewId: UUID, searchSessionId: UUID): List = repository.findAllById_SystematicStudyIdAndSearchSessionId(reviewId, searchSessionId).map { it.toDto() } + override fun findAllBySessionPaged( + reviewId: UUID, + searchSessionId: UUID, + pageable: Pageable + ): Page { + val documentsPage = repository.findAllById_SystematicStudyIdAndSearchSessionId(reviewId, searchSessionId, pageable) + return documentsPage.map { it.toDto() } + } override fun updateSelectionStatus(reviewId: UUID, studyId: Long, attributeName: String, newStatus: Any) { repository.findAndUpdateAttributeById(StudyReviewId(reviewId, studyId), attributeName, newStatus) From 6f2f7eb373ca9c779331c7f12cf5a4dc3ae4b443 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Sat, 18 Oct 2025 15:06:59 -0300 Subject: [PATCH 16/30] feat(study-review): add pagination support and sorting for reviews by session ID --- .../FindAllStudyReviewsBySessionService.kt | 10 ++- ...FindAllStudyReviewsBySessionServiceImpl.kt | 37 ++++++++++- .../study/controller/StudyReviewController.kt | 12 +++- ...ulFindAllStudyReviewsBySessionPresenter.kt | 66 ++++++++++++++++++- .../main/kotlin/br/all/utils/LinksFactory.kt | 62 ++++++++++++++--- 5 files changed, 170 insertions(+), 17 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionService.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionService.kt index 200aeece..156c46cc 100644 --- a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionService.kt +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionService.kt @@ -13,12 +13,20 @@ interface FindAllStudyReviewsBySessionService { val userId: UUID, val systematicStudyId: UUID, val searchSessionId: UUID, + val page: Int = 0, + val pageSize: Int = 20, + val sort: String = "id,asc" ) + @Schema(name = "FindAllStudyReviewsBySessionResponseModel") data class ResponseModel( val userId: UUID, val systematicStudyId: UUID, val searchSessionId: UUID, - val studyReviews: List + val studyReviews: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int ) } \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionServiceImpl.kt index afcc5867..f85c142a 100644 --- a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsBySessionServiceImpl.kt @@ -7,6 +7,8 @@ import br.all.application.study.find.presenter.FindAllStudyReviewsBySessionPrese import br.all.application.study.repository.StudyReviewRepository import br.all.application.user.CredentialsService import br.all.domain.model.review.SystematicStudy +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort class FindAllStudyReviewsBySessionServiceImpl ( private val systematicStudyRepository: SystematicStudyRepository, @@ -14,6 +16,17 @@ class FindAllStudyReviewsBySessionServiceImpl ( private val credentialsService: CredentialsService, ) : FindAllStudyReviewsBySessionService { + private fun parseSortParameter(sortParam: String): Sort { + val parts = sortParam.split(",") + val property = parts[0] + val direction = if (parts.size > 1 && parts[1].equals("desc", ignoreCase = true)) { + Sort.Direction.DESC + } else { + Sort.Direction.ASC + } + return Sort.by(direction, property) + } + override fun findAllBySearchSession( presenter: FindAllStudyReviewsBySessionPresenter, request: FindAllStudyReviewsBySessionService.RequestModel @@ -27,11 +40,29 @@ class FindAllStudyReviewsBySessionServiceImpl ( if (presenter.isDone()) return - val studyReviews = studyReviewRepository.findAllBySession(request.systematicStudyId, request.searchSessionId) + val sort = parseSortParameter(request.sort) + val pageable = PageRequest.of( + request.page, + request.pageSize, + sort + ) + + val studyReviewsPage = studyReviewRepository.findAllBySessionPaged( + request.systematicStudyId, + request.searchSessionId, + pageable + ) + presenter.prepareSuccessView( FindAllStudyReviewsBySessionService.ResponseModel( - request.userId, request.systematicStudyId, - request.searchSessionId, studyReviews + userId = request.userId, + systematicStudyId = request.systematicStudyId, + searchSessionId = request.searchSessionId, + studyReviews = studyReviewsPage.content, + page = pageable.pageNumber, + size = pageable.pageSize, + totalElements = studyReviewsPage.totalElements, + totalPages = studyReviewsPage.totalPages ) ) } diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt index 3ccce027..92dc32e6 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt @@ -156,10 +156,20 @@ class StudyReviewController( fun findAllStudyReviewsBySession( @PathVariable systematicStudy: UUID, @PathVariable searchSessionId: UUID, + @RequestParam("page", required = false, defaultValue = "0") page: Int, + @RequestParam("size", required = false, defaultValue = "20") size: Int, + @RequestParam("sort", required = false, defaultValue = "id,asc") sort: String ): ResponseEntity<*> { val presenter = RestfulFindAllStudyReviewsBySessionPresenter(linksFactory) val userId = authenticationInfoService.getAuthenticatedUserId() - val request = FindAllStudyReviewsBySessionService.RequestModel(userId, systematicStudy, searchSessionId) + val request = FindAllStudyReviewsBySessionService.RequestModel( + userId = userId, + systematicStudyId = systematicStudy, + searchSessionId = searchSessionId, + page = page, + pageSize = size, + sort = sort + ) findAllBySessionService.findAllBySearchSession(presenter, request) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsBySessionPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsBySessionPresenter.kt index f02fbf82..7c49a580 100644 --- a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsBySessionPresenter.kt +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsBySessionPresenter.kt @@ -18,8 +18,67 @@ class RestfulFindAllStudyReviewsBySessionPresenter ( var responseEntity: ResponseEntity<*>? = null override fun prepareSuccessView(response: FindAllStudyReviewsBySessionService.ResponseModel) { - val (_, systematicStudyId, searchSessionId, studyReviews) = response - val restfulResponse = ViewModel(systematicStudyId, searchSessionId, studyReviews.size, studyReviews) + val ( + _, + systematicStudyId, + searchSessionId, + studyReviews, + page, + size, + totalElements, + totalPages + ) = response + + val restfulResponse = ViewModel( + systematicStudyId = systematicStudyId, + searchSessionId = searchSessionId, + size = studyReviews.size, + studyReviews = studyReviews, + page = page, + totalElements = totalElements, + totalPages = totalPages + ) + + val selfRef = linksFactory.findAllStudiesBySession( + systematicStudyId, + searchSessionId, + page, + size + ) + + if (totalPages > 0) { + restfulResponse.add(linksFactory.findAllStudiesBySessionFirstPage( + systematicStudyId, + searchSessionId, + size + )) + restfulResponse.add(linksFactory.findAllStudiesBySessionLastPage( + systematicStudyId, + searchSessionId, + totalPages, + size + )) + + if (page < totalPages - 1) { + restfulResponse.add(linksFactory.findAllStudiesBySessionNextPage( + systematicStudyId, + searchSessionId, + page, + size + )) + } + + if (page > 0) { + restfulResponse.add(linksFactory.findAllStudiesBySessionPrevPage( + systematicStudyId, + searchSessionId, + page, + size + )) + } + } + + restfulResponse.add(selfRef) responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) } @@ -33,5 +92,8 @@ class RestfulFindAllStudyReviewsBySessionPresenter ( val searchSessionId: UUID, val size: Int, val studyReviews: List, + val page: Int, + val totalElements: Long, + val totalPages: Int ) : RepresentationModel() } \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt index ea7d1b7f..be2975d8 100644 --- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt +++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt @@ -208,27 +208,69 @@ class LinksFactory { fun findAllStudies(systematicStudyId: UUID, page: Int = 0, size: Int = 20, sort: String = "id,asc"): Link = linkTo { findAllStudyReviews(systematicStudyId, page, size, sort) }.withRel("find-all-studies").withType("GET") - - fun findAllStudiesFirstPage(systematicStudyId: UUID, size: Int = 20, sort: String = "id,asc"): Link = + + fun findAllStudiesFirstPage(systematicStudyId: UUID, size: Int = 20, sort: String = "id,asc"): Link = findAllStudies(systematicStudyId, 0, size, sort).withRel("first").withType("GET") - - fun findAllStudiesLastPage(systematicStudyId: UUID, totalPages: Int, size: Int = 20, sort: String = "id,asc"): Link = + + fun findAllStudiesLastPage(systematicStudyId: UUID, totalPages: Int, size: Int = 20, sort: String = "id,asc"): Link = findAllStudies(systematicStudyId, totalPages - 1, size, sort).withRel("last").withType("GET") - - fun findAllStudiesNextPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = + + fun findAllStudiesNextPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = findAllStudies(systematicStudyId, currentPage + 1, size, sort).withRel("next").withType("GET") - - fun findAllStudiesPrevPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = + + fun findAllStudiesPrevPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = findAllStudies(systematicStudyId, currentPage - 1, size, sort).withRel("prev").withType("GET") fun findAllStudiesBySource(systematicStudyId: UUID, source: String): Link = linkTo { findAllStudyReviewsBySource(systematicStudyId, source) }.withRel("find-all-studies-by-source").withType("GET") - fun findAllStudiesBySession(systematicStudyId: UUID, searchSessionId: UUID): Link = linkTo { - findAllStudyReviewsBySession(systematicStudyId, searchSessionId) + fun findAllStudiesBySession( + systematicStudyId: UUID, + searchSessionId: UUID, + page: Int = 0, + size: Int = 20, + sort: String = "id,asc" + ): Link = linkTo { + findAllStudyReviewsBySession(systematicStudyId, searchSessionId, page, size, sort) }.withRel("find-all-studies-by-session").withType("GET") + fun findAllStudiesBySessionFirstPage( + systematicStudyId: UUID, + searchSessionId: UUID, + size: Int = 20, + sort: String = "id,asc" + ): Link = + findAllStudiesBySession(systematicStudyId, searchSessionId, 0, size, sort).withRel("first").withType("GET") + + fun findAllStudiesBySessionLastPage( + systematicStudyId: UUID, + searchSessionId: UUID, + totalPages: Int, + size: Int = 20, + sort: String = "id,asc" + ): Link = + findAllStudiesBySession(systematicStudyId, searchSessionId, totalPages - 1, size, sort).withRel("last").withType("GET") + + fun findAllStudiesBySessionNextPage( + systematicStudyId: UUID, + searchSessionId: UUID, + currentPage: Int, + size: Int = 20, + sort: String = "id,asc" + ): Link = + findAllStudiesBySession(systematicStudyId, searchSessionId, currentPage + 1, size, sort).withRel("next").withType("GET") + + fun findAllStudiesBySessionPrevPage( + systematicStudyId: UUID, + searchSessionId: UUID, + currentPage: Int, + size: Int = 20, + sort: String = "id,asc" + ): Link = + findAllStudiesBySession(systematicStudyId, searchSessionId, currentPage - 1, size, sort).withRel("prev").withType("GET") + + fun updateStudySelectionStatus(systematicStudyId: UUID): Link = linkTo { updateStudyReviewSelectionStatus( systematicStudyId, From c50185a8fc71e3746b278e74361e70c4e6d88e8f Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:05:46 -0300 Subject: [PATCH 17/30] feat(study-review): add service and presenter for retrieving included study reviews with pagination and sorting --- .../FindAllIncludedStudyReviewsPresenter.kt | 6 ++++ .../FindAllIncludedStudyReviewsService.kt | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/find/presenter/FindAllIncludedStudyReviewsPresenter.kt create mode 100644 review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt diff --git a/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllIncludedStudyReviewsPresenter.kt b/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllIncludedStudyReviewsPresenter.kt new file mode 100644 index 00000000..e94dffd8 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllIncludedStudyReviewsPresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.study.find.presenter + +import br.all.application.study.find.service.FindAllIncludedStudyReviewsService.ResponseModel +import br.all.domain.shared.presenter.GenericPresenter + +interface FindAllIncludedStudyReviewsPresenter : GenericPresenter \ No newline at end of file diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt new file mode 100644 index 00000000..77199154 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt @@ -0,0 +1,32 @@ +package br.all.application.study.find.service + +import br.all.application.study.find.presenter.FindAllIncludedStudyReviewsPresenter +import br.all.application.study.repository.StudyReviewDto +import io.swagger.v3.oas.annotations.media.Schema +import java.util.UUID + +interface FindAllIncludedStudyReviewsService { + + fun findAllIncluded(presenter: FindAllIncludedStudyReviewsPresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val searchSessionId: UUID, + val page: Int = 0, + val pageSize: Int = 20, + val sort: String = "id,asc" + ) + + @Schema(name = "FindAllIncludedStudyReviewsResponseModel") + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val searchSessionId: UUID, + val studyReviews: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int + ) +} \ No newline at end of file From 55c20c1ac729c0633721ceae98eb5cbc75c92329 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:19:57 -0300 Subject: [PATCH 18/30] feat(study-review): add support for filtering reviews by selection status with pagination --- .../study/repository/StudyReviewRepository.kt | 17 +++++++++++++++-- .../study/MongoStudyReviewRepository.kt | 8 ++++++++ .../study/StudyReviewRepositoryImpl.kt | 16 ++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt index 5275bff1..bae4af8f 100644 --- a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt @@ -1,5 +1,6 @@ package br.all.application.study.repository +import br.all.domain.model.study.SelectionStatus import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import java.util.UUID @@ -11,8 +12,20 @@ interface StudyReviewRepository { fun findAllFromReviewPaged(reviewId: UUID, pageable: Pageable = Pageable.unpaged()): Page fun findAllBySource(reviewId: UUID, source: String): List fun findAllBySession(reviewId: UUID, searchSessionId: UUID): List - fun findAllBySessionPaged(reviewId: UUID, searchSessionId: UUID, pageable: Pageable = Pageable.unpaged()): Page - fun findById(reviewId: UUID, studyId: Long) : StudyReviewDto? + fun findAllBySessionPaged( + reviewId: UUID, + searchSessionId: UUID, + pageable: Pageable = Pageable.unpaged() + ): Page + + fun findAllBySessionPagedAndSelectionStatus( + reviewId: UUID, + searchSessionId: UUID, + status: SelectionStatus, + pageable: Pageable = Pageable.unpaged() + ): Page + + fun findById(reviewId: UUID, studyId: Long): StudyReviewDto? fun updateSelectionStatus(reviewId: UUID, studyId: Long, attributeName: String, newStatus: Any) fun saveOrUpdateBatch(dtos: List) fun findAllQuestionAnswers(reviewId: UUID, questionId: UUID): List diff --git a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt index 47441b43..5810b0d4 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt @@ -1,6 +1,7 @@ package br.all.infrastructure.study import br.all.application.study.repository.AnswerDto +import br.all.domain.model.study.SelectionStatus import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.mongodb.repository.Aggregation @@ -23,6 +24,13 @@ interface MongoStudyReviewRepository : MongoRepository + fun findAllById_SystematicStudyIdAndSearchSessionIdAndSelectionStatus( + reviewID: UUID, + searchSessionId: UUID, + selectionStatus: SelectionStatus, + pageable: Pageable + ): Page + @Update("{ '\$set' : { ?1 : ?2 } }") fun findAndUpdateAttributeById(id: StudyReviewId, attributeName:String, newStatus: Any) diff --git a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt index 1312f51c..cbd3678b 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt @@ -3,6 +3,7 @@ package br.all.infrastructure.study import br.all.application.study.repository.AnswerDto import br.all.application.study.repository.StudyReviewDto import br.all.application.study.repository.StudyReviewRepository +import br.all.domain.model.study.SelectionStatus import br.all.infrastructure.shared.toNullable import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl @@ -41,6 +42,21 @@ open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRe return documentsPage.map { it.toDto() } } + override fun findAllBySessionPagedAndSelectionStatus( + reviewId: UUID, + searchSessionId: UUID, + status: SelectionStatus, + pageable: Pageable + ): Page { + val documentsPage = repository.findAllById_SystematicStudyIdAndSearchSessionIdAndSelectionStatus( + reviewId, + searchSessionId, + status, // Pass the enum directly. Spring Data will handle it. + pageable + ) + return documentsPage.map { it.toDto() } + } + override fun updateSelectionStatus(reviewId: UUID, studyId: Long, attributeName: String, newStatus: Any) { repository.findAndUpdateAttributeById(StudyReviewId(reviewId, studyId), attributeName, newStatus) } From 86d2443a15e69adcfe36662f1da4af59caa43c2b Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:20:20 -0300 Subject: [PATCH 19/30] feat(study-review): implement service for retrieving included study reviews with sorting and pagination --- .../FindAllIncludedStudyReviewsServiceImpl.kt | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt new file mode 100644 index 00000000..718f19d1 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt @@ -0,0 +1,72 @@ +package br.all.application.study.find.service + +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.find.presenter.FindAllIncludedStudyReviewsPresenter +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.user.CredentialsService +import br.all.domain.model.review.SystematicStudy +import br.all.domain.model.study.SelectionStatus +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort + +class FindAllIncludedStudyReviewsServiceImpl( + private val systematicStudyRepository: SystematicStudyRepository, + private val studyReviewRepository: StudyReviewRepository, + private val credentialsService: CredentialsService, +) : FindAllIncludedStudyReviewsService { + + private fun parseSortParameter(sortParam: String): Sort { + val parts = sortParam.split(",") + val property = parts[0] + val direction = if (parts.size > 1 && parts[1].equals("desc", ignoreCase = true)) { + Sort.Direction.DESC + } else { + Sort.Direction.ASC + } + return Sort.by(direction, property) + } + + override fun findAllIncluded( + presenter: FindAllIncludedStudyReviewsPresenter, + request: FindAllIncludedStudyReviewsService.RequestModel + ) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + + if (presenter.isDone()) return + + val sort = parseSortParameter(request.sort) + val pageable = PageRequest.of( + request.page, + request.pageSize, + sort + ) + + val studyReviewsPage = studyReviewRepository.findAllBySessionPagedAndSelectionStatus( + request.systematicStudyId, + request.searchSessionId, + SelectionStatus.INCLUDED, + pageable + ) + + presenter.prepareSuccessView( + FindAllIncludedStudyReviewsService.ResponseModel( + userId = request.userId, + systematicStudyId = request.systematicStudyId, + searchSessionId = request.searchSessionId, + studyReviews = studyReviewsPage.content, + page = pageable.pageNumber, + size = pageable.pageSize, + totalElements = studyReviewsPage.totalElements, + totalPages = studyReviewsPage.totalPages + ) + ) + } + +} \ No newline at end of file From b721e71b09484b0d728d6544c1a81452571698aa Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:22:33 -0300 Subject: [PATCH 20/30] feat(study-review): add bean configuration for included study reviews retrieval service --- .../controller/StudyReviewServicesConfiguration.kt | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt index 54d9e3f2..251f13aa 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt @@ -66,6 +66,16 @@ class StudyReviewServicesConfiguration { ) = FindAllStudyReviewsBySessionServiceImpl( systematicStudyRepository, studyReviewRepository, credentialsService ) + + @Bean + fun findAllIncludedStudyReviewsService( + systematicStudyRepository: SystematicStudyRepository, + studyReviewRepository: StudyReviewRepository, + credentialsService: CredentialsService, + ) = FindAllIncludedStudyReviewsServiceImpl( + systematicStudyRepository, studyReviewRepository, credentialsService + ) + @Bean fun findReviewService( systematicStudyRepository: SystematicStudyRepository, From 06929760993a1cd580c63084d99cb8a14e91018a Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:28:45 -0300 Subject: [PATCH 21/30] refactor(study-review): simplify repository methods by removing `searchSessionId` parameter and renaming functions --- .../service/FindAllIncludedStudyReviewsServiceImpl.kt | 3 +-- .../application/study/repository/StudyReviewRepository.kt | 3 +-- .../infrastructure/study/MongoStudyReviewRepository.kt | 3 +-- .../all/infrastructure/study/StudyReviewRepositoryImpl.kt | 8 +++----- 4 files changed, 6 insertions(+), 11 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt index 718f19d1..81b7f924 100644 --- a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt @@ -48,9 +48,8 @@ class FindAllIncludedStudyReviewsServiceImpl( sort ) - val studyReviewsPage = studyReviewRepository.findAllBySessionPagedAndSelectionStatus( + val studyReviewsPage = studyReviewRepository.findAllBySystematicStudyIdAndSelectionStatusPaged( request.systematicStudyId, - request.searchSessionId, SelectionStatus.INCLUDED, pageable ) diff --git a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt index bae4af8f..819f83b5 100644 --- a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt @@ -18,9 +18,8 @@ interface StudyReviewRepository { pageable: Pageable = Pageable.unpaged() ): Page - fun findAllBySessionPagedAndSelectionStatus( + fun findAllBySystematicStudyIdAndSelectionStatusPaged( reviewId: UUID, - searchSessionId: UUID, status: SelectionStatus, pageable: Pageable = Pageable.unpaged() ): Page diff --git a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt index 5810b0d4..66c23cd6 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/MongoStudyReviewRepository.kt @@ -24,9 +24,8 @@ interface MongoStudyReviewRepository : MongoRepository - fun findAllById_SystematicStudyIdAndSearchSessionIdAndSelectionStatus( + fun findAllById_SystematicStudyIdAndSelectionStatus( reviewID: UUID, - searchSessionId: UUID, selectionStatus: SelectionStatus, pageable: Pageable ): Page diff --git a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt index cbd3678b..59a90590 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt @@ -42,16 +42,14 @@ open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRe return documentsPage.map { it.toDto() } } - override fun findAllBySessionPagedAndSelectionStatus( + override fun findAllBySystematicStudyIdAndSelectionStatusPaged( reviewId: UUID, - searchSessionId: UUID, status: SelectionStatus, pageable: Pageable ): Page { - val documentsPage = repository.findAllById_SystematicStudyIdAndSearchSessionIdAndSelectionStatus( + val documentsPage = repository.findAllById_SystematicStudyIdAndSelectionStatus( reviewId, - searchSessionId, - status, // Pass the enum directly. Spring Data will handle it. + status, pageable ) return documentsPage.map { it.toDto() } From 596fdb014f5399b7afabebc656289f48d30080fc Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:29:46 -0300 Subject: [PATCH 22/30] refactor(study-review): remove `searchSessionId` from request and response models for included study reviews --- .../study/find/service/FindAllIncludedStudyReviewsService.kt | 2 -- .../find/service/FindAllIncludedStudyReviewsServiceImpl.kt | 1 - 2 files changed, 3 deletions(-) diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt index 77199154..8a5044d2 100644 --- a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsService.kt @@ -12,7 +12,6 @@ interface FindAllIncludedStudyReviewsService { data class RequestModel( val userId: UUID, val systematicStudyId: UUID, - val searchSessionId: UUID, val page: Int = 0, val pageSize: Int = 20, val sort: String = "id,asc" @@ -22,7 +21,6 @@ interface FindAllIncludedStudyReviewsService { data class ResponseModel( val userId: UUID, val systematicStudyId: UUID, - val searchSessionId: UUID, val studyReviews: List, val page: Int, val size: Int, diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt index 81b7f924..38642610 100644 --- a/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllIncludedStudyReviewsServiceImpl.kt @@ -58,7 +58,6 @@ class FindAllIncludedStudyReviewsServiceImpl( FindAllIncludedStudyReviewsService.ResponseModel( userId = request.userId, systematicStudyId = request.systematicStudyId, - searchSessionId = request.searchSessionId, studyReviews = studyReviewsPage.content, page = pageable.pageNumber, size = pageable.pageSize, From 6bb74930268c65e8cba5eb6dc0c43f4aca6426c6 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:37:14 -0300 Subject: [PATCH 23/30] feat(study-review): add links for paginated navigation of included study reviews and implement presenter for response handling --- ...fulFindAllIncludedStudyReviewsPresenter.kt | 90 +++++++++++++++++++ .../main/kotlin/br/all/utils/LinksFactory.kt | 20 +++++ 2 files changed, 110 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/presenter/RestfulFindAllIncludedStudyReviewsPresenter.kt diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllIncludedStudyReviewsPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllIncludedStudyReviewsPresenter.kt new file mode 100644 index 00000000..e17fdba7 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllIncludedStudyReviewsPresenter.kt @@ -0,0 +1,90 @@ +package br.all.study.presenter + +import br.all.application.study.find.presenter.FindAllIncludedStudyReviewsPresenter +import br.all.application.study.find.service.FindAllIncludedStudyReviewsService +import br.all.application.study.repository.StudyReviewDto +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.* + +class RestfulFindAllIncludedStudyReviewsPresenter ( + private val linksFactory: LinksFactory +): FindAllIncludedStudyReviewsPresenter { + + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: FindAllIncludedStudyReviewsService.ResponseModel) { + val ( + _, + systematicStudyId, + studyReviews, + page, + size, + totalElements, + totalPages + ) = response + + val restfulResponse = ViewModel( + systematicStudyId = systematicStudyId, + size = studyReviews.size, + studyReviews = studyReviews, + page = page, + totalElements = totalElements, + totalPages = totalPages + ) + + val selfRef = linksFactory.findAllIncludedStudies( + systematicStudyId, + page, + size + ) + + if (totalPages > 0) { + restfulResponse.add(linksFactory.findAllIncludedStudiesFirstPage( + systematicStudyId, + size + )) + restfulResponse.add(linksFactory.findAllIncludedStudiesLastPage( + systematicStudyId, + totalPages, + size + )) + + if (page < totalPages - 1) { + restfulResponse.add(linksFactory.findAllIncludedStudiesNextPage( + systematicStudyId, + page, + size + )) + } + + if (page > 0) { + restfulResponse.add(linksFactory.findAllIncludedStudiesPrevPage( + systematicStudyId, + page, + size + )) + } + } + + restfulResponse.add(selfRef) + + responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) = run { responseEntity = createErrorResponseFrom(throwable) } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val systematicStudyId: UUID, + val size: Int, + val studyReviews: List, + val page: Int, + val totalElements: Long, + val totalPages: Int + ) : RepresentationModel() +} \ No newline at end of file diff --git a/web/src/main/kotlin/br/all/utils/LinksFactory.kt b/web/src/main/kotlin/br/all/utils/LinksFactory.kt index be2975d8..996b486a 100644 --- a/web/src/main/kotlin/br/all/utils/LinksFactory.kt +++ b/web/src/main/kotlin/br/all/utils/LinksFactory.kt @@ -225,6 +225,26 @@ class LinksFactory { findAllStudyReviewsBySource(systematicStudyId, source) }.withRel("find-all-studies-by-source").withType("GET") + fun findAllIncludedStudies(systematicStudyId: UUID, page: Int = 0, size: Int = 20, sort: String = "id,asc"): Link = linkTo { + findAllStudyReviews(systematicStudyId, page, size, sort) + }.withRel("find-all-studies").withType("GET") + + fun findAllIncludedStudiesFirstPage(systematicStudyId: UUID, size: Int = 20, sort: String = "id,asc"): Link = + findAllStudies(systematicStudyId, 0, size, sort).withRel("first").withType("GET") + + fun findAllIncludedStudiesLastPage(systematicStudyId: UUID, totalPages: Int, size: Int = 20, sort: String = "id,asc"): Link = + findAllStudies(systematicStudyId, totalPages - 1, size, sort).withRel("last").withType("GET") + + fun findAllIncludedStudiesNextPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = + findAllStudies(systematicStudyId, currentPage + 1, size, sort).withRel("next").withType("GET") + + fun findAllIncludedStudiesPrevPage(systematicStudyId: UUID, currentPage: Int, size: Int = 20, sort: String = "id,asc"): Link = + findAllStudies(systematicStudyId, currentPage - 1, size, sort).withRel("prev").withType("GET") + + fun findAllIncludedStudiesBySource(systematicStudyId: UUID, source: String): Link = linkTo { + findAllStudyReviewsBySource(systematicStudyId, source) + }.withRel("find-all-studies-by-source").withType("GET") + fun findAllStudiesBySession( systematicStudyId: UUID, searchSessionId: UUID, From 98aa3c8ebbde8284d68a890ce1927aced6f60825 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Tue, 21 Oct 2025 16:39:53 -0300 Subject: [PATCH 24/30] feat(study-review): add endpoint for retrieving included study reviews with pagination and sorting --- .../study/controller/StudyReviewController.kt | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt index 92dc32e6..aabe0a5f 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt @@ -34,6 +34,7 @@ class StudyReviewController( private val findAllBySourceService: FindAllStudyReviewsBySourceService, private val findAllBySessionService: FindAllStudyReviewsBySessionService, private val findAllByAuthorService: FindAllStudyReviewsByAuthorService, + private val findAllIncludedStudyReviewsService: FindAllIncludedStudyReviewsService, private val findOneService: FindStudyReviewService, private val updateSelectionService: UpdateStudyReviewSelectionService, private val updateExtractionService: UpdateStudyReviewExtractionService, @@ -43,7 +44,6 @@ class StudyReviewController( private val batchAnswerQuestionService: BatchAnswerQuestionService, private val authenticationInfoService: AuthenticationInfoService, private val linksFactory: LinksFactory - ) { @PostMapping("/study-review") @@ -105,6 +105,37 @@ class StudyReviewController( return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + @GetMapping("/study-review/selection-included") + @Operation(summary = "Get all existing studies of a systematic review with included selection status") + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Success getting studies of a systematic review with included selection status, either found all studies or found none", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = FindAllStudyReviewsService.ResponseModel::class) + )] + ), + ApiResponse(responseCode = "401", description = "Fail getting all study reviews with included selection status - unauthenticated user", + content = [Content(schema = Schema(hidden = true))]), + ApiResponse(responseCode = "403", description = "Fail getting all study reviews with included selection status - unauthorized user", + content = [Content(schema = Schema(hidden = true))]), + ] + ) + fun findAllIncludedStudyReviews( + @PathVariable systematicStudy: UUID, + @RequestParam("page", required = false, defaultValue = "0") page: Int, + @RequestParam("size", required = false, defaultValue = "20") size: Int, + @RequestParam("sort", required = false, defaultValue = "id,asc") sort: String, + ): ResponseEntity<*> { + val presenter = RestfulFindAllIncludedStudyReviewsPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + val request = FindAllIncludedStudyReviewsService.RequestModel(userId, systematicStudy, page, size, sort) + findAllIncludedStudyReviewsService.findAllIncluded(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } + @GetMapping("/search-source/{searchSource}") @Operation(summary = "Get all existing studies of a systematic review search source") @ApiResponses( From 14eed6355e7dc16ce45a268865a60378672946f1 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 10:55:22 -0300 Subject: [PATCH 25/30] feat: add service and presenter for advanced search of study reviews --- ...llStudyReviewsByAdvancedSearchPresenter.kt | 6 +++ ...dAllStudyReviewsByAdvancedSearchService.kt | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/find/presenter/FindAllStudyReviewsByAdvancedSearchPresenter.kt create mode 100644 review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchService.kt diff --git a/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllStudyReviewsByAdvancedSearchPresenter.kt b/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllStudyReviewsByAdvancedSearchPresenter.kt new file mode 100644 index 00000000..4cbaa361 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/presenter/FindAllStudyReviewsByAdvancedSearchPresenter.kt @@ -0,0 +1,6 @@ +package br.all.application.study.find.presenter + +import br.all.application.study.find.service.FindAllStudyReviewsByAdvancedSearchService.ResponseModel +import br.all.domain.shared.presenter.GenericPresenter + +interface FindAllStudyReviewsByAdvancedSearchPresenter : GenericPresenter diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchService.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchService.kt new file mode 100644 index 00000000..16406898 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchService.kt @@ -0,0 +1,40 @@ +package br.all.application.study.find.service + +import br.all.application.study.find.presenter.FindAllStudyReviewsByAdvancedSearchPresenter +import br.all.application.study.repository.StudyReviewDto +import io.swagger.v3.oas.annotations.media.Schema +import java.util.* + +interface FindAllStudyReviewsByAdvancedSearchService { + + fun findAllByAdvancedSearch(presenter: FindAllStudyReviewsByAdvancedSearchPresenter, request: RequestModel) + + data class RequestModel( + val userId: UUID, + val systematicStudyId: UUID, + val id: Long? = null, + val studyReviewId: String? = null, + val title: String? = null, + val authors: String? = null, + val venue: String? = null, + val year: Int? = null, + val selectionStatus: String? = null, + val extractionStatus: String? = null, + val score: Double? = null, + val readingPriority: Int? = null, + val page: Int = 0, + val pageSize: Int = 20, + val sort: String = "id,asc" + ) + + @Schema(name = "FindAllStudyReviewsByAdvancedSearchResponseModel") + data class ResponseModel( + val userId: UUID, + val systematicStudyId: UUID, + val studyReviews: List, + val page: Int, + val size: Int, + val totalElements: Long, + val totalPages: Int + ) +} From bbc1f27b9eee270197369ae9598791294f1f47e5 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 10:59:00 -0300 Subject: [PATCH 26/30] feat: add mongodb query for advanced study review search --- .../study/repository/StudyReviewRepository.kt | 6 ++ .../study/StudyReviewRepositoryImpl.kt | 55 ++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt index 819f83b5..f0f15de2 100644 --- a/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt +++ b/review/src/main/kotlin/br/all/application/study/repository/StudyReviewRepository.kt @@ -28,6 +28,12 @@ interface StudyReviewRepository { fun updateSelectionStatus(reviewId: UUID, studyId: Long, attributeName: String, newStatus: Any) fun saveOrUpdateBatch(dtos: List) fun findAllQuestionAnswers(reviewId: UUID, questionId: UUID): List + + fun findAllByAdvancedSearch( + reviewId: UUID, + filters: Map, + pageable: Pageable + ): Page } data class AnswerDto( diff --git a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt index 59a90590..5eda830d 100644 --- a/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt +++ b/review/src/main/kotlin/br/all/infrastructure/study/StudyReviewRepositoryImpl.kt @@ -8,11 +8,16 @@ import br.all.infrastructure.shared.toNullable import org.springframework.data.domain.Page import org.springframework.data.domain.PageImpl import org.springframework.data.domain.Pageable +import org.springframework.data.mongodb.core.MongoTemplate +import org.springframework.data.mongodb.core.query.Criteria +import org.springframework.data.mongodb.core.query.Query import org.springframework.stereotype.Repository import java.util.* @Repository -open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRepository) : StudyReviewRepository { +open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRepository, + private val mongoTemplate: MongoTemplate +) : StudyReviewRepository { override fun saveOrUpdate(dto: StudyReviewDto): StudyReviewDocument = repository.save(dto.toDocument()) override fun findById(reviewId: UUID, studyId: Long) = @@ -72,5 +77,53 @@ open class StudyReviewRepositoryImpl(private val repository: MongoStudyReviewRe answer = infraDto.answer ) } + + override fun findAllByAdvancedSearch( + reviewId: UUID, + filters: Map, + pageable: Pageable + ): Page { + val criteria = mutableListOf() + criteria += Criteria.where("_id.systematicStudyId").`is`(reviewId) + + filters["id"]?.let { criteria += Criteria.where("_id.studyReviewId").`is`(it) } + filters["studyReviewId"]?.let { + criteria += Criteria.where("studyReviewId").regex(".*${Regex.escape(it as String)}.*", "i") + } + filters["title"]?.let { + criteria += Criteria.where("title").regex(".*${Regex.escape(it as String)}.*", "i") + } + filters["authors"]?.let { + criteria += Criteria.where("authors").regex(".*${Regex.escape(it as String)}.*", "i") + } + filters["venue"]?.let { + criteria += Criteria.where("venue").regex(".*${Regex.escape(it as String)}.*", "i") + } + filters["year"]?.let { + criteria += Criteria.where("year").`is`(it) + } + filters["selectionStatus"]?.let { + criteria += Criteria.where("selectionStatus").`is`(it) + } + filters["extractionStatus"]?.let { + criteria += Criteria.where("extractionStatus").`is`(it) + } + filters["score"]?.let { + criteria += Criteria.where("score").`is`(it) + } + filters["readingPriority"]?.let { + criteria += Criteria.where("readingPriority").`is`(it) + } + + val query = Query().addCriteria(Criteria().andOperator(*criteria.toTypedArray())) + query.with(pageable) + + val total = mongoTemplate.count(query, StudyReviewDocument::class.java) + val documents = mongoTemplate.find(query, StudyReviewDocument::class.java) + + val content = documents.map { it.toDto() } + + return PageImpl(content, pageable, total) + } } From d79d262e62131770e58ec1d7c730b651865fb055 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 10:59:38 -0300 Subject: [PATCH 27/30] feat: implement study review advanced search service --- ...StudyReviewsByAdvancedSearchServiceImpl.kt | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchServiceImpl.kt diff --git a/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchServiceImpl.kt b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchServiceImpl.kt new file mode 100644 index 00000000..9bbfd490 --- /dev/null +++ b/review/src/main/kotlin/br/all/application/study/find/service/FindAllStudyReviewsByAdvancedSearchServiceImpl.kt @@ -0,0 +1,73 @@ +package br.all.application.study.find.service + +import br.all.application.review.repository.SystematicStudyRepository +import br.all.application.review.repository.fromDto +import br.all.application.shared.presenter.prepareIfFailsPreconditions +import br.all.application.study.find.presenter.FindAllStudyReviewsByAdvancedSearchPresenter +import br.all.application.study.repository.StudyReviewRepository +import br.all.application.user.CredentialsService +import br.all.domain.model.review.SystematicStudy +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort + +class FindAllStudyReviewsByAdvancedSearchServiceImpl( + private val systematicStudyRepository: SystematicStudyRepository, + private val studyReviewRepository: StudyReviewRepository, + private val credentialsService: CredentialsService +) : FindAllStudyReviewsByAdvancedSearchService { + + private fun parseSortParameter(sortParam: String): Sort { + val parts = sortParam.split(",") + val property = parts[0] + val direction = if (parts.size > 1 && parts[1].equals("desc", ignoreCase = true)) { + Sort.Direction.DESC + } else Sort.Direction.ASC + return Sort.by(direction, property) + } + + override fun findAllByAdvancedSearch( + presenter: FindAllStudyReviewsByAdvancedSearchPresenter, + request: FindAllStudyReviewsByAdvancedSearchService.RequestModel + ) { + val user = credentialsService.loadCredentials(request.userId)?.toUser() + val systematicStudyDto = systematicStudyRepository.findById(request.systematicStudyId) + val systematicStudy = systematicStudyDto?.let { SystematicStudy.fromDto(it) } + + presenter.prepareIfFailsPreconditions(user, systematicStudy) + if (presenter.isDone()) return + + val sort = parseSortParameter(request.sort) + val pageable = PageRequest.of(request.page, request.pageSize, sort) + + val filters = mapOf( + "id" to request.id, + "studyReviewId" to request.studyReviewId, + "title" to request.title, + "authors" to request.authors, + "venue" to request.venue, + "year" to request.year, + "selectionStatus" to request.selectionStatus, + "extractionStatus" to request.extractionStatus, + "score" to request.score, + "readingPriority" to request.readingPriority + ) + + val pageResult = studyReviewRepository.findAllByAdvancedSearch( + reviewId = request.systematicStudyId, + filters = filters, + pageable = pageable + ) + + presenter.prepareSuccessView( + FindAllStudyReviewsByAdvancedSearchService.ResponseModel( + userId = request.userId, + systematicStudyId = request.systematicStudyId, + studyReviews = pageResult.content, + page = pageable.pageNumber, + size = pageable.pageSize, + totalElements = pageResult.totalElements, + totalPages = pageResult.totalPages + ) + ) + } +} From a1c3aa2a5f068a6e06ccb7f2e258f15f50dfe3d5 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 11:01:42 -0300 Subject: [PATCH 28/30] feat: implement restful presenter for advanced search of study reviews --- ...llStudyReviewsByAdvancedSearchPresenter.kt | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsByAdvancedSearchPresenter.kt diff --git a/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsByAdvancedSearchPresenter.kt b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsByAdvancedSearchPresenter.kt new file mode 100644 index 00000000..540a4706 --- /dev/null +++ b/web/src/main/kotlin/br/all/study/presenter/RestfulFindAllStudyReviewsByAdvancedSearchPresenter.kt @@ -0,0 +1,50 @@ +package br.all.study.presenter + +import br.all.application.study.find.presenter.FindAllStudyReviewsByAdvancedSearchPresenter +import br.all.application.study.find.service.FindAllStudyReviewsByAdvancedSearchService +import br.all.application.study.repository.StudyReviewDto +import br.all.shared.error.createErrorResponseFrom +import br.all.utils.LinksFactory +import org.springframework.hateoas.RepresentationModel +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import java.util.* + +class RestfulFindAllStudyReviewsByAdvancedSearchPresenter( + private val linksFactory: LinksFactory +) : FindAllStudyReviewsByAdvancedSearchPresenter { + + var responseEntity: ResponseEntity<*>? = null + + override fun prepareSuccessView(response: FindAllStudyReviewsByAdvancedSearchService.ResponseModel) { + val restfulResponse = ViewModel( + systematicStudyId = response.systematicStudyId, + size = response.studyReviews.size, + studyReviews = response.studyReviews, + page = response.page, + totalElements = response.totalElements, + totalPages = response.totalPages + ) + + restfulResponse.add( + linksFactory.findAllIncludedStudies(response.systematicStudyId, response.page, response.size) + ) + + responseEntity = ResponseEntity.status(HttpStatus.OK).body(restfulResponse) + } + + override fun prepareFailView(throwable: Throwable) { + responseEntity = createErrorResponseFrom(throwable) + } + + override fun isDone() = responseEntity != null + + private data class ViewModel( + val systematicStudyId: UUID, + val size: Int, + val studyReviews: List, + val page: Int, + val totalElements: Long, + val totalPages: Int + ) : RepresentationModel() +} From b8b3c3863eca5076a47c1f1d49841521a600daa8 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 11:02:53 -0300 Subject: [PATCH 29/30] feat: add bean configuration for advanced search service --- .../controller/StudyReviewServicesConfiguration.kt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt index 251f13aa..e5f9eb20 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewServicesConfiguration.kt @@ -159,4 +159,15 @@ class StudyReviewServicesConfiguration { systematicStudyRepository, credentialsService ) + + @Bean + fun findAllStudyReviewsByAdvancedSearchService( + systematicStudyRepository: SystematicStudyRepository, + studyReviewRepository: StudyReviewRepository, + credentialsService: CredentialsService + ) = FindAllStudyReviewsByAdvancedSearchServiceImpl( + systematicStudyRepository, + studyReviewRepository, + credentialsService + ) } \ No newline at end of file From 7ada943fb602b9a13924abc6b79ad3e9d8733121 Mon Sep 17 00:00:00 2001 From: matheusspacifico Date: Mon, 27 Oct 2025 11:23:17 -0300 Subject: [PATCH 30/30] feat: add endpoint for advanced search of study reviews with multi-criteria filtering, pagination, and sorting --- .../study/controller/StudyReviewController.kt | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt index aabe0a5f..a582ab0e 100644 --- a/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt +++ b/web/src/main/kotlin/br/all/study/controller/StudyReviewController.kt @@ -24,6 +24,9 @@ import br.all.application.study.find.service.FindStudyReviewService.RequestModel import br.all.application.study.update.interfaces.BatchAnswerQuestionService import br.all.study.presenter.RestfulBatchAnswerQuestionPresenter import br.all.study.requests.PatchBatchAnswerQuestionStudyReviewRequest +import io.swagger.v3.oas.annotations.Parameter +import io.swagger.v3.oas.annotations.Parameters +import io.swagger.v3.oas.annotations.media.ExampleObject @RestController @RequestMapping("/api/v1/systematic-study/{systematicStudy}") @@ -35,6 +38,7 @@ class StudyReviewController( private val findAllBySessionService: FindAllStudyReviewsBySessionService, private val findAllByAuthorService: FindAllStudyReviewsByAuthorService, private val findAllIncludedStudyReviewsService: FindAllIncludedStudyReviewsService, + private val findAllStudyReviewsByAdvancedSearchService: FindAllStudyReviewsByAdvancedSearchService, private val findOneService: FindStudyReviewService, private val updateSelectionService: UpdateStudyReviewSelectionService, private val updateExtractionService: UpdateStudyReviewExtractionService, @@ -526,4 +530,105 @@ class StudyReviewController( removeCriteriaService.removeCriteria(presenter, request) return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) } + + @GetMapping("/study-review/search") + @Operation( + summary = "Advanced search for study reviews within a systematic study", + description = """ + Performs a flexible multi-criteria search across study reviews belonging to a given systematic study. + You can filter by fields such as id, studyReviewId, title, authors, venue, year, selectionStatus, + extractionStatus, score, and readingPriority. Supports pagination, sorting, and partial (case-insensitive) + text matches for textual fields. + """ + ) + @ApiResponses( + value = [ + ApiResponse( + responseCode = "200", + description = "Successfully executed the advanced search for study reviews. The response may include zero or more results.", + content = [Content( + mediaType = "application/json", + schema = Schema(implementation = FindAllStudyReviewsByAdvancedSearchService.ResponseModel::class), + examples = [ + ExampleObject( + name = "ComplexSearchExample", + summary = "Example of a multi-criteria advanced search", + description = "This example demonstrates how to combine multiple filters and sorting options.", + value = "GET /api/v1/systematic-study/7b8e22cc-4e09-4d4f-86bb-0a1f908d2f64/study-review/search?title=deep%20learning&authors=Goodfellow&venue=IEEE&year=2021&selectionStatus=INCLUDED&extractionStatus=PENDING&readingPriority=2&page=0&size=10&sort=year,desc" + ) + ] + )] + ), + ApiResponse( + responseCode = "401", + description = "Unauthorized – the user must be authenticated to perform this search.", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "403", + description = "Forbidden – the authenticated user does not have permission to access this systematic study.", + content = [Content(schema = Schema(hidden = true))] + ), + ApiResponse( + responseCode = "500", + description = "Internal server error – unexpected failure during search execution.", + content = [Content(schema = Schema(hidden = true))] + ) + ] + ) + @Parameters( + Parameter(name = "id", description = "Internal numeric ID of the study review", example = "15"), + Parameter(name = "studyReviewId", description = "External study review identifier (partial match allowed)", example = "S123"), + Parameter(name = "title", description = "Partial title match (case-insensitive)", example = "deep learning"), + Parameter(name = "authors", description = "Partial author name match (case-insensitive)", example = "Goodfellow"), + Parameter(name = "venue", description = "Conference or journal name match (case-insensitive)", example = "IEEE"), + Parameter(name = "year", description = "Exact publication year", example = "2021"), + Parameter(name = "selectionStatus", description = "Exact selection status filter", example = "INCLUDED"), + Parameter(name = "extractionStatus", description = "Exact extraction status filter", example = "PENDING"), + Parameter(name = "score", description = "Exact numeric score filter", example = "0.85"), + Parameter(name = "readingPriority", description = "Exact reading priority filter", example = "2"), + Parameter(name = "page", description = "Page index (0-based, default = 0)", example = "0"), + Parameter(name = "size", description = "Number of elements per page (default = 20)", example = "20"), + Parameter(name = "sort", description = "Sorting criteria in the format `,` (default = id,asc)", example = "year,desc") + ) + fun searchStudyReviews( + @PathVariable systematicStudy: UUID, + @RequestParam(required = false) id: Long?, + @RequestParam(required = false) studyReviewId: String?, + @RequestParam(required = false) title: String?, + @RequestParam(required = false) authors: String?, + @RequestParam(required = false) venue: String?, + @RequestParam(required = false) year: Int?, + @RequestParam(required = false) selectionStatus: String?, + @RequestParam(required = false) extractionStatus: String?, + @RequestParam(required = false) score: Double?, + @RequestParam(required = false) readingPriority: Int?, + @RequestParam(required = false, defaultValue = "0") page: Int, + @RequestParam(required = false, defaultValue = "20") size: Int, + @RequestParam(required = false, defaultValue = "id,asc") sort: String, + ): ResponseEntity<*> { + val presenter = RestfulFindAllStudyReviewsByAdvancedSearchPresenter(linksFactory) + val userId = authenticationInfoService.getAuthenticatedUserId() + + val request = FindAllStudyReviewsByAdvancedSearchService.RequestModel( + userId = userId, + systematicStudyId = systematicStudy, + id = id, + studyReviewId = studyReviewId, + title = title, + authors = authors, + venue = venue, + year = year, + selectionStatus = selectionStatus, + extractionStatus = extractionStatus, + score = score, + readingPriority = readingPriority, + page = page, + pageSize = size, + sort = sort + ) + + findAllStudyReviewsByAdvancedSearchService.findAllByAdvancedSearch(presenter, request) + return presenter.responseEntity ?: ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR) + } } \ No newline at end of file