diff --git a/build.sbt b/build.sbt index 47b9ca2114..d7b621035e 100644 --- a/build.sbt +++ b/build.sbt @@ -144,7 +144,6 @@ lazy val constants = Module.setup( network, language, mapping, - `concept-api`, testWith(scalatestsuite) ) ) diff --git a/common/src/main/scala/no/ndla/common/CirceUtil.scala b/common/src/main/scala/no/ndla/common/CirceUtil.scala index 0cae6499c7..14ed2cd7a4 100644 --- a/common/src/main/scala/no/ndla/common/CirceUtil.scala +++ b/common/src/main/scala/no/ndla/common/CirceUtil.scala @@ -7,8 +7,9 @@ package no.ndla.common -import io.circe.{Decoder, Encoder, HCursor, parser} +import enumeratum.* import io.circe.syntax.* +import io.circe.* import scala.util.{Failure, Try} @@ -38,4 +39,20 @@ object CirceUtil { cur.downField(key).as[Option[T]].map(_.getOrElse(default)) } + private val stringDecoder = implicitly[Decoder[String]] + + /** Trait that does the same as `CirceEnum`, but with slightly better error message */ + trait CirceEnumWithErrors[A <: EnumEntry] extends CirceEnum[A] { + this: Enum[A] => + override implicit val circeDecoder: Decoder[A] = (c: HCursor) => + stringDecoder(c).flatMap { s => + withNameEither(s).left.map { notFound => + val enumName = this.getClass.getSimpleName.stripSuffix("$") + val enumList = s"[${notFound.enumValues.mkString("'", "','", "'")}]" + val message = s"'${notFound.notFoundName}' is not a member of enum '$enumName'. Must be one of $enumList" + DecodingFailure(message, c.history) + } + } + } + } diff --git a/common/src/main/scala/no/ndla/common/errors/NDLAErrors.scala b/common/src/main/scala/no/ndla/common/errors/NDLAErrors.scala index c41d4dcf36..c05431cbbc 100644 --- a/common/src/main/scala/no/ndla/common/errors/NDLAErrors.scala +++ b/common/src/main/scala/no/ndla/common/errors/NDLAErrors.scala @@ -8,6 +8,7 @@ object AccessDeniedException { def forbidden: AccessDeniedException = AccessDeniedException("User is missing required permission(s) to perform this operation") } -case class NotFoundException(message: String) extends RuntimeException(message) -case class RollbackException(ex: Throwable) extends RuntimeException -case class FileTooBigException() extends RuntimeException +case class NotFoundException(message: String) extends RuntimeException(message) +case class RollbackException(ex: Throwable) extends RuntimeException +case class FileTooBigException() extends RuntimeException +case class InvalidStatusException(message: String) extends RuntimeException(message) diff --git a/common/src/main/scala/no/ndla/common/model/domain/concept/Concept.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/Concept.scala new file mode 100644 index 0000000000..8faeadba90 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/Concept.scala @@ -0,0 +1,44 @@ +/* + * Part of NDLA common + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.common.model.domain.concept + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.draft.DraftCopyright +import no.ndla.common.model.domain.{Content, Responsible, Tag, Title} +import no.ndla.language.Language.getSupportedLanguages + +case class Concept( + id: Option[Long], + revision: Option[Int], + title: Seq[Title], + content: Seq[ConceptContent], + copyright: Option[DraftCopyright], + created: NDLADate, + updated: NDLADate, + updatedBy: Seq[String], + metaImage: Seq[ConceptMetaImage], + tags: Seq[Tag], + subjectIds: Set[String], + articleIds: Seq[Long], + status: Status, + visualElement: Seq[VisualElement], + responsible: Option[Responsible], + conceptType: ConceptType, + glossData: Option[GlossData], + editorNotes: Seq[ConceptEditorNote] +) extends Content { + def supportedLanguages: Set[String] = + getSupportedLanguages(title, content, tags, visualElement, metaImage).toSet +} + +object Concept { + implicit val encoder: Encoder[Concept] = deriveEncoder + implicit val decoder: Decoder[Concept] = deriveDecoder +} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptContent.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptContent.scala similarity index 84% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptContent.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/ConceptContent.scala index ecec42c285..fa178916c3 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptContent.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptContent.scala @@ -1,14 +1,14 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2019 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept -import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField case class ConceptContent(content: String, language: String) extends LanguageField[String] { diff --git a/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptEditorNote.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptEditorNote.scala new file mode 100644 index 0000000000..c0b6edfdda --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptEditorNote.scala @@ -0,0 +1,25 @@ +/* + * Part of NDLA common. + * Copyright (C) 2024 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.domain.concept + +import no.ndla.common.model.NDLADate +import io.circe.{Encoder, Decoder} +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} + +case class ConceptEditorNote( + note: String, + user: String, + status: Status, + timestamp: NDLADate +) + +object ConceptEditorNote { + implicit val encoder: Encoder[ConceptEditorNote] = deriveEncoder + implicit val decoder: Decoder[ConceptEditorNote] = deriveDecoder +} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptMetaImage.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptMetaImage.scala similarity index 86% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptMetaImage.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/ConceptMetaImage.scala index c3e71d9f0b..772b93f49f 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptMetaImage.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptMetaImage.scala @@ -1,11 +1,11 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2019 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} diff --git a/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptStatus.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptStatus.scala new file mode 100644 index 0000000000..3ef11db72d --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptStatus.scala @@ -0,0 +1,50 @@ +/* + * Part of NDLA common + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.common.model.domain.concept + +import enumeratum.* +import no.ndla.common.errors.ValidationException + +import scala.util.{Failure, Success, Try} + +sealed trait ConceptStatus extends EnumEntry {} +object ConceptStatus extends Enum[ConceptStatus] with CirceEnum[ConceptStatus] { + case object IN_PROGRESS extends ConceptStatus + case object EXTERNAL_REVIEW extends ConceptStatus + case object INTERNAL_REVIEW extends ConceptStatus + case object QUALITY_ASSURANCE extends ConceptStatus + case object LANGUAGE extends ConceptStatus + case object FOR_APPROVAL extends ConceptStatus + case object END_CONTROL extends ConceptStatus + case object PUBLISHED extends ConceptStatus + case object UNPUBLISHED extends ConceptStatus + case object ARCHIVED extends ConceptStatus + + val values: IndexedSeq[ConceptStatus] = findValues + + def valueOfOrError(s: String): Try[ConceptStatus] = + valueOf(s) match { + case Some(st) => Success(st) + case None => + val validStatuses = values.map(_.toString).mkString(", ") + Failure( + ValidationException( + "status", + s"'$s' is not a valid concept status. Must be one of $validStatuses" + ) + ) + } + + def valueOf(s: String): Option[ConceptStatus] = values.find(_.toString == s.toUpperCase) + + val thatDoesNotRequireResponsible: Seq[ConceptStatus] = Seq(PUBLISHED, UNPUBLISHED, ARCHIVED) + val thatRequiresResponsible: Set[ConceptStatus] = this.values.filterNot(thatDoesNotRequireResponsible.contains).toSet + + implicit def ordering[A <: ConceptStatus]: Ordering[ConceptStatus] = + (x: ConceptStatus, y: ConceptStatus) => indexOf(x) - indexOf(y) +} diff --git a/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptType.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptType.scala new file mode 100644 index 0000000000..6051561fcd --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/ConceptType.scala @@ -0,0 +1,37 @@ +/* + * Part of NDLA common + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.common.model.domain.concept + +import enumeratum.* +import no.ndla.common.CirceUtil.CirceEnumWithErrors +import no.ndla.common.errors.InvalidStatusException + +import scala.util.{Failure, Success, Try} + +sealed abstract class ConceptType(override val entryName: String) extends EnumEntry { + override def toString: String = entryName +} + +object ConceptType extends Enum[ConceptType] with CirceEnumWithErrors[ConceptType] { + case object CONCEPT extends ConceptType("concept") + case object GLOSS extends ConceptType("gloss") + + def all: Seq[String] = ConceptType.values.map(_.toString) + def valueOf(s: String): Option[ConceptType] = ConceptType.values.find(_.toString == s) + def valueOf(s: Option[String]): Option[ConceptType] = s.flatMap(valueOf) + + def valueOfOrError(s: String): Try[ConceptType] = { + valueOf(s) match { + case None => + Failure(InvalidStatusException(s"'$s' is not a valid concept type. Valid options are ${all.mkString(", ")}.")) + case Some(conceptType) => Success(conceptType) + } + } + + override def values: IndexedSeq[ConceptType] = findValues +} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/EditorNote.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/EditorNote.scala similarity index 80% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/EditorNote.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/EditorNote.scala index 500d08d6d5..469de766da 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/EditorNote.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/EditorNote.scala @@ -1,11 +1,11 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2023 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} diff --git a/common/src/main/scala/no/ndla/common/model/domain/concept/Gloss.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/Gloss.scala new file mode 100644 index 0000000000..a576080d56 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/Gloss.scala @@ -0,0 +1,31 @@ +/* + * Part of NDLA common + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.common.model.domain.concept + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class GlossExample(example: String, language: String, transcriptions: Map[String, String]) + +object GlossExample { + implicit val encoder: Encoder[GlossExample] = deriveEncoder + implicit val decoder: Decoder[GlossExample] = deriveDecoder +} + +case class GlossData( + gloss: String, + wordClass: WordClass, + originalLanguage: String, + transcriptions: Map[String, String], + examples: List[List[GlossExample]] +) + +object GlossData { + implicit val encoder: Encoder[GlossData] = deriveEncoder + implicit val decoder: Decoder[GlossData] = deriveDecoder +} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Status.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/Status.scala similarity index 82% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Status.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/Status.scala index 36012b1b63..49f7fe068b 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Status.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/Status.scala @@ -1,11 +1,11 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2020 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/VisualElement.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/VisualElement.scala similarity index 84% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/VisualElement.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/VisualElement.scala index 62c6897495..b31f4b1983 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/VisualElement.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/VisualElement.scala @@ -1,11 +1,11 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2019 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Gloss.scala b/common/src/main/scala/no/ndla/common/model/domain/concept/WordClass.scala similarity index 80% rename from concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Gloss.scala rename to common/src/main/scala/no/ndla/common/model/domain/concept/WordClass.scala index 56e0b74a96..8791ba396f 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Gloss.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/concept/WordClass.scala @@ -1,18 +1,16 @@ /* - * Part of NDLA concept-api - * Copyright (C) 2023 NDLA + * Part of NDLA common + * Copyright (C) 2024 NDLA * * See LICENSE */ -package no.ndla.conceptapi.model.domain +package no.ndla.common.model.domain.concept import com.scalatsi.TypescriptType.TSEnum import com.scalatsi.{TSNamedType, TSType} -import enumeratum._ -import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import io.circe.{Decoder, Encoder} -import no.ndla.conceptapi.model.api.InvalidStatusException +import enumeratum.* +import no.ndla.common.errors.InvalidStatusException import scala.util.{Failure, Success, Try} @@ -77,23 +75,3 @@ object WordClass extends Enum[WordClass] with CirceEnum[WordClass] { TSEnum.string("WordClassEnum", tsEnumValues: _*) ) } - -case class GlossExample(example: String, language: String, transcriptions: Map[String, String]) - -object GlossExample { - implicit val encoder: Encoder[GlossExample] = deriveEncoder - implicit val decoder: Decoder[GlossExample] = deriveDecoder -} - -case class GlossData( - gloss: String, - wordClass: WordClass, - originalLanguage: String, - transcriptions: Map[String, String], - examples: List[List[GlossExample]] -) - -object GlossData { - implicit val encoder: Encoder[GlossData] = deriveEncoder - implicit val decoder: Decoder[GlossData] = deriveDecoder -} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/controller/ConceptControllerHelpers.scala b/concept-api/src/main/scala/no/ndla/conceptapi/controller/ConceptControllerHelpers.scala index 49bd1235ea..bf52765bde 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/controller/ConceptControllerHelpers.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/controller/ConceptControllerHelpers.scala @@ -8,8 +8,9 @@ package no.ndla.conceptapi.controller import no.ndla.common.model.api.CommaSeparatedList._ +import no.ndla.common.model.domain.concept.ConceptType import no.ndla.conceptapi.Props -import no.ndla.conceptapi.model.domain.{ConceptType, Sort} +import no.ndla.conceptapi.model.domain.Sort import no.ndla.language.Language import sttp.tapir._ import sttp.tapir.model.Delimited diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala b/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala index 1c4f72bdfc..a88387c7c3 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/controller/DraftConceptController.scala @@ -10,8 +10,9 @@ package no.ndla.conceptapi.controller import cats.implicits._ import no.ndla.common.implicits._ import no.ndla.common.model.api.CommaSeparatedList._ +import no.ndla.common.model.domain.concept.ConceptStatus import no.ndla.conceptapi.model.api._ -import no.ndla.conceptapi.model.domain.{ConceptStatus, Sort} +import no.ndla.conceptapi.model.domain.Sort import no.ndla.conceptapi.model.search.DraftSearchSettings import no.ndla.conceptapi.service.search.{DraftConceptSearchService, SearchConverterService} import no.ndla.conceptapi.service.{ConverterService, ReadService, WriteService} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/controller/InternController.scala b/concept-api/src/main/scala/no/ndla/conceptapi/controller/InternController.scala index cd0c7216bc..b33cc4ce17 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/controller/InternController.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/controller/InternController.scala @@ -7,10 +7,10 @@ package no.ndla.conceptapi.controller -import cats.implicits._ +import cats.implicits.* +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.Eff import no.ndla.conceptapi.model.api.{ConceptDomainDump, ConceptImportResults, ErrorHelpers, NotFoundException} -import no.ndla.conceptapi.model.domain.Concept import no.ndla.conceptapi.repository.{DraftConceptRepository, PublishedConceptRepository} import no.ndla.conceptapi.service.search.{DraftConceptIndexService, IndexService, PublishedConceptIndexService} import no.ndla.conceptapi.service.{ConverterService, ImportService, ReadService} @@ -21,12 +21,12 @@ import sttp.model.StatusCode import sttp.tapir.server.ServerEndpoint import java.util.concurrent.Executors -import scala.concurrent.duration._ +import scala.concurrent.duration.* import scala.concurrent.{Await, ExecutionContext, ExecutionContextExecutorService, Future} import scala.language.postfixOps import scala.util.{Failure, Success, Try} -import sttp.tapir._ -import sttp.tapir.generic.auto._ +import sttp.tapir.* +import sttp.tapir.generic.auto.* trait InternController { this: IndexService diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/integration/ArticleApiClient.scala b/concept-api/src/main/scala/no/ndla/conceptapi/integration/ArticleApiClient.scala index 683c9498ce..fc23b1524a 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/integration/ArticleApiClient.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/integration/ArticleApiClient.scala @@ -11,9 +11,9 @@ import com.typesafe.scalalogging.StrictLogging import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.conceptapi.Props -import no.ndla.conceptapi.model.domain import no.ndla.network.NdlaClient import io.lemonlabs.uri.typesafe.dsl.* +import no.ndla.common.model.domain.concept.Concept import no.ndla.network.tapir.auth.TokenUser import sttp.client3.quick.* @@ -23,7 +23,7 @@ import scala.util.{Failure, Success, Try} case class ConceptDomainDumpResults( totalCount: Long, - results: List[domain.Concept] + results: List[Concept] ) object ConceptDomainDumpResults { @@ -39,7 +39,7 @@ trait ArticleApiClient { val baseUrl: String = s"http://${props.ArticleApiHost}/intern" val dumpDomainPath = "dump/concepts" - def getChunks(user: TokenUser): Iterator[Try[Seq[domain.Concept]]] = { + def getChunks(user: TokenUser): Iterator[Try[Seq[Concept]]] = { getChunk(0, 0, user) match { case Success(initSearch) => val dbCount = initSearch.totalCount @@ -47,7 +47,7 @@ trait ArticleApiClient { val numPages = ceil(dbCount.toDouble / pageSize.toDouble).toInt val pages = Seq.range(1, numPages + 1) - val iterator: Iterator[Try[Seq[domain.Concept]]] = pages.iterator.map(p => { + val iterator: Iterator[Try[Seq[Concept]]] = pages.iterator.map(p => { getChunk(p, pageSize, user).map(_.results) }) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala index 02a2565388..5771eea8aa 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/ConceptDomainDump.scala @@ -9,7 +9,7 @@ package no.ndla.conceptapi.model.api import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import no.ndla.conceptapi.model.domain +import no.ndla.common.model.domain.concept.Concept as DomainConcept import sttp.tapir.Schema.annotations.description @description("Information about articles") @@ -17,7 +17,7 @@ case class ConceptDomainDump( @description("The total number of concepts in the database") totalCount: Long, @description("For which page results are shown from") page: Int, @description("The number of results per page") pageSize: Int, - @description("The search results") results: Seq[domain.Concept] + @description("The search results") results: Seq[DomainConcept] ) object ConceptDomainDump { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala index 2ebc885b0c..3de9d35572 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/api/NDLAErrors.scala @@ -78,4 +78,3 @@ case class ConceptExistsAlreadyException(message: String) extends RuntimeExcepti case class ImportException(message: String) extends RuntimeException(message) case class ElasticIndexingException(message: String) extends RuntimeException(message) case class OperationNotAllowedException(message: String) extends RuntimeException(message) -case class InvalidStatusException(message: String) extends RuntimeException(message) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Concept.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Concept.scala deleted file mode 100644 index 5b9a78ea82..0000000000 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/Concept.scala +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Part of NDLA concept-api - * Copyright (C) 2019 NDLA - * - * See LICENSE - */ - -package no.ndla.conceptapi.model.domain - -import enumeratum.* -import io.circe.{Decoder, Encoder} -import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import no.ndla.common.CirceUtil -import no.ndla.common.model.domain.{Responsible, Tag, Title} -import no.ndla.common.errors.ValidationException -import no.ndla.common.model.NDLADate -import no.ndla.common.model.domain.draft.DraftCopyright -import no.ndla.language.Language.getSupportedLanguages -import scalikejdbc.* - -import scala.util.{Failure, Success, Try} - -case class Concept( - id: Option[Long], - revision: Option[Int], - title: Seq[Title], - content: Seq[ConceptContent], - copyright: Option[DraftCopyright], - created: NDLADate, - updated: NDLADate, - updatedBy: Seq[String], - metaImage: Seq[ConceptMetaImage], - tags: Seq[Tag], - subjectIds: Set[String], - articleIds: Seq[Long], - status: Status, - visualElement: Seq[VisualElement], - responsible: Option[Responsible], - conceptType: ConceptType.Value, - glossData: Option[GlossData], - editorNotes: Seq[EditorNote] -) { - def supportedLanguages: Set[String] = - getSupportedLanguages(title, content, tags, visualElement, metaImage).toSet -} -object Concept extends SQLSyntaxSupport[Concept] { - implicit val encoder: Encoder[Concept] = deriveEncoder - implicit val decoder: Decoder[Concept] = deriveDecoder - - override val tableName = "conceptdata" - - def fromResultSet(lp: SyntaxProvider[Concept])(rs: WrappedResultSet): Concept = - fromResultSet(lp.resultName)(rs) - - def fromResultSet(lp: ResultName[Concept])(rs: WrappedResultSet): Concept = { - - val id = rs.long(lp.c("id")) - val revision = rs.int(lp.c("revision")) - val jsonStr = rs.string(lp.c("document")) - - val meta = CirceUtil.unsafeParseAs[Concept](jsonStr) - - new Concept( - id = Some(id), - revision = Some(revision), - meta.title, - meta.content, - meta.copyright, - meta.created, - meta.updated, - meta.updatedBy, - meta.metaImage, - meta.tags, - meta.subjectIds, - meta.articleIds, - meta.status, - meta.visualElement, - meta.responsible, - meta.conceptType, - meta.glossData, - meta.editorNotes - ) - } -} - -object PublishedConcept extends SQLSyntaxSupport[Concept] { - override val tableName = "publishedconceptdata" -} - -sealed trait ConceptStatus extends EnumEntry {} -object ConceptStatus extends Enum[ConceptStatus] with CirceEnum[ConceptStatus] { - case object IN_PROGRESS extends ConceptStatus - case object EXTERNAL_REVIEW extends ConceptStatus - case object INTERNAL_REVIEW extends ConceptStatus - case object QUALITY_ASSURANCE extends ConceptStatus - case object LANGUAGE extends ConceptStatus - case object FOR_APPROVAL extends ConceptStatus - case object END_CONTROL extends ConceptStatus - case object PUBLISHED extends ConceptStatus - case object UNPUBLISHED extends ConceptStatus - case object ARCHIVED extends ConceptStatus - - val values: IndexedSeq[ConceptStatus] = findValues - - def valueOfOrError(s: String): Try[ConceptStatus] = - valueOf(s) match { - case Some(st) => Success(st) - case None => - val validStatuses = values.map(_.toString).mkString(", ") - Failure( - ValidationException( - "status", - s"'$s' is not a valid concept status. Must be one of $validStatuses" - ) - ) - } - - def valueOf(s: String): Option[ConceptStatus] = values.find(_.toString == s.toUpperCase) - - val thatDoesNotRequireResponsible: Seq[ConceptStatus] = Seq(PUBLISHED, UNPUBLISHED, ARCHIVED) - val thatRequiresResponsible: Set[ConceptStatus] = this.values.filterNot(thatDoesNotRequireResponsible.contains).toSet - - implicit def ordering[A <: ConceptStatus]: Ordering[ConceptStatus] = - (x: ConceptStatus, y: ConceptStatus) => indexOf(x) - indexOf(y) -} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptType.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptType.scala deleted file mode 100644 index 26bfe1b7d7..0000000000 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/ConceptType.scala +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Part of NDLA concept-api - * Copyright (C) 2023 NDLA - * - * See LICENSE - */ - -package no.ndla.conceptapi.model.domain - -import io.circe.{Decoder, Encoder} -import no.ndla.conceptapi.model.api.InvalidStatusException - -import scala.util.{Failure, Success, Try} - -object ConceptType extends Enumeration { - val CONCEPT: ConceptType.Value = Value("concept") - val GLOSS: ConceptType.Value = Value("gloss") - - implicit val encoder: Encoder[ConceptType.Value] = Encoder.encodeEnumeration(ConceptType) - implicit val decoder: Decoder[ConceptType.Value] = Decoder.decodeEnumeration(ConceptType) - - def all: Seq[String] = ConceptType.values.map(_.toString).toSeq - def valueOf(s: String): Option[ConceptType.Value] = ConceptType.values.find(_.toString == s) - def valueOf(s: Option[String]): Option[ConceptType.Value] = s.flatMap(valueOf) - - def valueOfOrError(s: String): Try[ConceptType.Value] = { - valueOf(s) match { - case None => - Failure(InvalidStatusException(s"'$s' is not a valid concept type. Valid options are ${all.mkString(", ")}.")) - case Some(conceptType) => Success(conceptType) - } - } -} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/DBConcept.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/DBConcept.scala new file mode 100644 index 0000000000..c83ee97231 --- /dev/null +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/DBConcept.scala @@ -0,0 +1,53 @@ +/* + * Part of NDLA concept-api + * Copyright (C) 2019 NDLA + * + * See LICENSE + */ + +package no.ndla.conceptapi.model.domain + +import no.ndla.common.CirceUtil +import no.ndla.common.model.domain.concept.Concept +import scalikejdbc.* + +object DBConcept extends SQLSyntaxSupport[Concept] { + override val tableName = "conceptdata" + + def fromResultSet(lp: SyntaxProvider[Concept])(rs: WrappedResultSet): Concept = + fromResultSet(lp.resultName)(rs) + + def fromResultSet(lp: ResultName[Concept])(rs: WrappedResultSet): Concept = { + + val id = rs.long(lp.c("id")) + val revision = rs.int(lp.c("revision")) + val jsonStr = rs.string(lp.c("document")) + + val meta = CirceUtil.unsafeParseAs[Concept](jsonStr) + + new Concept( + id = Some(id), + revision = Some(revision), + meta.title, + meta.content, + meta.copyright, + meta.created, + meta.updated, + meta.updatedBy, + meta.metaImage, + meta.tags, + meta.subjectIds, + meta.articleIds, + meta.status, + meta.visualElement, + meta.responsible, + meta.conceptType, + meta.glossData, + meta.editorNotes + ) + } +} + +object PublishedConcept extends SQLSyntaxSupport[Concept] { + override val tableName = "publishedconceptdata" +} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/SideEffect.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/SideEffect.scala index d5a72457ec..f73e399d90 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/SideEffect.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/SideEffect.scala @@ -7,6 +7,7 @@ package no.ndla.conceptapi.model.domain +import no.ndla.common.model.domain.concept.Concept import no.ndla.network.tapir.auth.TokenUser import scala.util.{Success, Try} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/StateTransition.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/StateTransition.scala index 6b0a931044..01eddb2eb2 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/StateTransition.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/domain/StateTransition.scala @@ -7,6 +7,7 @@ package no.ndla.conceptapi.model.domain +import no.ndla.common.model.domain.concept.ConceptStatus import no.ndla.conceptapi.model.domain.SideEffect.SideEffect import no.ndla.network.tapir.auth.Permission import no.ndla.network.tapir.auth.Permission.CONCEPT_API_WRITE diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/model/search/SearchableConcept.scala b/concept-api/src/main/scala/no/ndla/conceptapi/model/search/SearchableConcept.scala index 2449158eff..18ebb8d328 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/model/search/SearchableConcept.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/model/search/SearchableConcept.scala @@ -10,17 +10,17 @@ package no.ndla.conceptapi.model.search import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.domain.Responsible -import no.ndla.conceptapi.model.domain import no.ndla.search.model.domain.EmbedValues import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.concept.{Concept, ConceptMetaImage} case class SearchableConcept( id: Long, conceptType: String, title: SearchableLanguageValues, content: SearchableLanguageValues, - metaImage: Seq[domain.ConceptMetaImage], + metaImage: Seq[ConceptMetaImage], defaultTitle: Option[String], tags: SearchableLanguageList, subjectIds: Seq[String], @@ -36,7 +36,7 @@ case class SearchableConcept( source: Option[String], responsible: Option[Responsible], gloss: Option[String], - domainObject: domain.Concept, + domainObject: Concept, sortableSubject: SearchableLanguageValues, sortableConceptType: SearchableLanguageValues, defaultSortableSubject: Option[String], diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/repository/DraftConceptRepository.scala b/concept-api/src/main/scala/no/ndla/conceptapi/repository/DraftConceptRepository.scala index ce075e9935..d306a97f82 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/repository/DraftConceptRepository.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/repository/DraftConceptRepository.scala @@ -10,10 +10,11 @@ package no.ndla.conceptapi.repository import com.typesafe.scalalogging.StrictLogging import no.ndla.common.CirceUtil import no.ndla.common.model.domain.Tag +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.Props import no.ndla.conceptapi.integration.DataSource import no.ndla.conceptapi.model.api.{ConceptMissingIdException, ErrorHelpers, NotFoundException} -import no.ndla.conceptapi.model.domain.Concept +import no.ndla.conceptapi.model.domain.DBConcept import org.postgresql.util.PGobject import scalikejdbc.* @@ -33,7 +34,7 @@ trait DraftConceptRepository { val conceptId: Long = sql""" - insert into ${Concept.table} (document, revision) + insert into ${DBConcept.table} (document, revision) values (${dataObject}, $newRevision) """.updateAndReturnGeneratedKey() @@ -53,7 +54,7 @@ trait DraftConceptRepository { val conceptId: Long = sql""" - insert into ${Concept.table} (listing_id, document, revision) + insert into ${DBConcept.table} (listing_id, document, revision) values ($listingId, $dataObject, $newRevision) """.updateAndReturnGeneratedKey() @@ -70,7 +71,7 @@ trait DraftConceptRepository { Try( sql""" - update ${Concept.table} + update ${DBConcept.table} set document=${dataObject} where listing_id=${listingId} """.updateAndReturnGeneratedKey() @@ -85,7 +86,7 @@ trait DraftConceptRepository { def allSubjectIds(implicit session: DBSession = ReadOnlyAutoSession): Set[String] = { sql""" select distinct jsonb_array_elements_text(document->'subjectIds') as subject_id - from ${Concept.table} + from ${DBConcept.table} where jsonb_array_length(document->'subjectIds') != 0;""" .map(rs => rs.string("subject_id")) .list() @@ -95,7 +96,7 @@ trait DraftConceptRepository { def everyTagFromEveryConcept(implicit session: DBSession = ReadOnlyAutoSession): List[List[Tag]] = { sql""" select distinct id, document#>'{tags}' as tags - from ${Concept.table} + from ${DBConcept.table} where jsonb_array_length(document#>'{tags}') > 0 order by id """ @@ -120,7 +121,7 @@ trait DraftConceptRepository { Try( sql""" - insert into ${Concept.table} (id, document, revision) + insert into ${DBConcept.table} (id, document, revision) values ($id, ${dataObject}, $newRevision) """.update() ).map(_ => { @@ -145,13 +146,13 @@ trait DraftConceptRepository { Try( sql""" - update ${Concept.table} + update ${DBConcept.table} set document=${dataObject}, revision=$newRevision where id=$conceptId and revision=$oldRevision - and revision=(select max(revision) from ${Concept.table} where id=$conceptId) + and revision=(select max(revision) from ${DBConcept.table} where id=$conceptId) """.update() ) match { case Success(updatedRows) => failIfRevisionMismatch(updatedRows, concept, newRevision) @@ -177,20 +178,20 @@ trait DraftConceptRepository { conceptWhere(sqls"co.id=${id.toInt} ORDER BY revision DESC LIMIT 1") def exists(id: Long)(implicit session: DBSession = AutoSession): Boolean = { - sql"select id from ${Concept.table} where id=${id}" + sql"select id from ${DBConcept.table} where id=${id}" .map(rs => rs.long("id")) .single() .isDefined } def getIdFromExternalId(externalId: String)(implicit session: DBSession = AutoSession): Option[Long] = { - sql"select id from ${Concept.table} where $externalId = any(external_id)" + sql"select id from ${DBConcept.table} where $externalId = any(external_id)" .map(rs => rs.long("id")) .single() } override def minMaxId(implicit session: DBSession = AutoSession): (Long, Long) = { - sql"select coalesce(MIN(id),0) as mi, coalesce(MAX(id),0) as ma from ${Concept.table}" + sql"select coalesce(MIN(id),0) as mi, coalesce(MAX(id),0) as ma from ${DBConcept.table}" .map(rs => { (rs.long("mi"), rs.long("ma")) }) @@ -206,29 +207,29 @@ trait DraftConceptRepository { private def conceptWhere( whereClause: SQLSyntax )(implicit session: DBSession = ReadOnlyAutoSession): Option[Concept] = { - val co = Concept.syntax("co") - sql"select ${co.result.*} from ${Concept.as(co)} where co.document is not NULL and $whereClause" - .map(Concept.fromResultSet(co)) + val co = DBConcept.syntax("co") + sql"select ${co.result.*} from ${DBConcept.as(co)} where co.document is not NULL and $whereClause" + .map(DBConcept.fromResultSet(co)) .single() } private def conceptsWhere( whereClause: SQLSyntax )(implicit session: DBSession = ReadOnlyAutoSession): List[Concept] = { - val co = Concept.syntax("co") - sql"select ${co.result.*} from ${Concept.as(co)} where co.document is not NULL and $whereClause" - .map(Concept.fromResultSet(co)) + val co = DBConcept.syntax("co") + sql"select ${co.result.*} from ${DBConcept.as(co)} where co.document is not NULL and $whereClause" + .map(DBConcept.fromResultSet(co)) .list() } def conceptCount(implicit session: DBSession = ReadOnlyAutoSession): Long = - sql"select count(*) from ${Concept.table}" + sql"select count(*) from ${DBConcept.table}" .map(rs => rs.long("count")) .single() .getOrElse(0) private def getHighestId(implicit session: DBSession = ReadOnlyAutoSession): Long = { - sql"select id from ${Concept.table} order by id desc limit 1" + sql"select id from ${DBConcept.table} order by id desc limit 1" .map(rs => rs.long("id")) .single() .getOrElse(0) @@ -237,7 +238,7 @@ trait DraftConceptRepository { def updateIdCounterToHighestId()(implicit session: DBSession = AutoSession): Unit = { val idToStartAt = SQLSyntax.createUnsafely((getHighestId() + 1).toString) val sequenceName = SQLSyntax.createUnsafely( - s"${Concept.schemaName.getOrElse(props.MetaSchema)}.${Concept.tableName}_id_seq" + s"${DBConcept.schemaName.getOrElse(props.MetaSchema)}.${DBConcept.tableName}_id_seq" ) sql"alter sequence $sequenceName restart with $idToStartAt;".executeUpdate(): Unit @@ -252,7 +253,7 @@ trait DraftConceptRepository { val tags = sql"""select tags from (select distinct JSONB_ARRAY_ELEMENTS_TEXT(tagObj->'tags') tags from - (select JSONB_ARRAY_ELEMENTS(document#>'{tags}') tagObj from ${Concept.table}) _ + (select JSONB_ARRAY_ELEMENTS(document#>'{tags}') tagObj from ${DBConcept.table}) _ where tagObj->>'language' like ${langOrAll} order by tags) sorted_tags where sorted_tags.tags ilike ${sanitizedInput + '%'} @@ -266,7 +267,7 @@ trait DraftConceptRepository { sql""" select count(*) from (select distinct JSONB_ARRAY_ELEMENTS_TEXT(tagObj->'tags') tags from - (select JSONB_ARRAY_ELEMENTS(document#>'{tags}') tagObj from ${Concept.table}) _ + (select JSONB_ARRAY_ELEMENTS(document#>'{tags}') tagObj from ${DBConcept.table}) _ where tagObj->>'language' like ${langOrAll}) all_tags where all_tags.tags ilike ${sanitizedInput + '%'}; """ @@ -279,16 +280,16 @@ trait DraftConceptRepository { } def getByPage(pageSize: Int, offset: Int)(implicit session: DBSession = ReadOnlyAutoSession): Seq[Concept] = { - val co = Concept.syntax("co") + val co = DBConcept.syntax("co") sql""" select ${co.result.*}, ${co.revision} as revision - from ${Concept.as(co)} + from ${DBConcept.as(co)} where document is not null order by ${co.id} offset $offset limit $pageSize """ - .map(Concept.fromResultSet(co)) + .map(DBConcept.fromResultSet(co)) .list() } } diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/repository/PublishedConceptRepository.scala b/concept-api/src/main/scala/no/ndla/conceptapi/repository/PublishedConceptRepository.scala index 345761212c..1e8ca6dcd5 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/repository/PublishedConceptRepository.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/repository/PublishedConceptRepository.scala @@ -10,9 +10,10 @@ package no.ndla.conceptapi.repository import com.typesafe.scalalogging.StrictLogging import no.ndla.common.CirceUtil import no.ndla.common.model.domain.Tag +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.integration.DataSource import no.ndla.conceptapi.model.api.NotFoundException -import no.ndla.conceptapi.model.domain.{Concept, PublishedConcept} +import no.ndla.conceptapi.model.domain.{DBConcept, PublishedConcept} import org.postgresql.util.PGobject import scalikejdbc.* @@ -95,7 +96,7 @@ trait PublishedConceptRepository { )(implicit session: DBSession = ReadOnlyAutoSession): Option[Concept] = { val co = PublishedConcept.syntax("co") sql"select ${co.result.*} from ${PublishedConcept.as(co)} where co.document is not NULL and $whereClause" - .map(Concept.fromResultSet(co)) + .map(DBConcept.fromResultSet(co)) .single() } @@ -124,7 +125,7 @@ trait PublishedConceptRepository { )(implicit session: DBSession = ReadOnlyAutoSession): List[Concept] = { val co = PublishedConcept.syntax("co") sql"select ${co.result.*} from ${PublishedConcept.as(co)} where co.document is not NULL and $whereClause" - .map(Concept.fromResultSet(co)) + .map(DBConcept.fromResultSet(co)) .list() } @@ -138,7 +139,7 @@ trait PublishedConceptRepository { offset $offset limit $pageSize """ - .map(Concept.fromResultSet(co)) + .map(DBConcept.fromResultSet(co)) .list() } } diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/repository/Repository.scala b/concept-api/src/main/scala/no/ndla/conceptapi/repository/Repository.scala index 4b66e11cc9..e32cc54cc4 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/repository/Repository.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/repository/Repository.scala @@ -7,7 +7,7 @@ package no.ndla.conceptapi.repository -import no.ndla.conceptapi.model.domain.Concept +import no.ndla.common.model.domain.concept.Concept import scalikejdbc.{AutoSession, DBSession} trait Repository[T <: Concept] { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala index 91fc84fb1e..44f25dea94 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/ConverterService.scala @@ -7,18 +7,30 @@ package no.ndla.conceptapi.service -import cats.implicits._ +import cats.implicits.* import com.typesafe.scalalogging.StrictLogging import io.lemonlabs.uri.{Path, Url} -import no.ndla.common.model.domain.{Responsible, Tag, Title} -import no.ndla.common.Clock +import no.ndla.common.model.domain.{Responsible, Tag, Title, concept} +import no.ndla.common.model.domain.concept.{ + ConceptContent, + ConceptEditorNote, + ConceptMetaImage, + ConceptStatus, + ConceptType, + GlossData, + GlossExample, + Status, + VisualElement, + WordClass, + Concept as DomainConcept +} +import no.ndla.common.{Clock, model} import no.ndla.common.configuration.Constants.EmbedTagName import no.ndla.common.model.api.{Delete, Missing, UpdateWith} -import no.ndla.common.model.{api => commonApi, domain => commonDomain} +import no.ndla.common.model.{api as commonApi, domain as commonDomain} import no.ndla.conceptapi.Props import no.ndla.conceptapi.model.api.{ConceptTags, NotFoundException} -import no.ndla.conceptapi.model.domain.{Concept, ConceptStatus, ConceptType, Status, WordClass} -import no.ndla.conceptapi.model.{api, domain} +import no.ndla.conceptapi.model.api import no.ndla.conceptapi.repository.DraftConceptRepository import no.ndla.language.Language.{AllLanguages, UnknownLanguage, findByLanguageOrBestEffort, mergeLanguageFields} import no.ndla.mapping.License.getLicense @@ -29,7 +41,7 @@ import no.ndla.validation.{EmbedTagRules, HtmlTagRules, ResourceType, TagAttribu import org.jsoup.Jsoup import org.jsoup.nodes.Element -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import scala.util.{Failure, Success, Try} trait ConverterService { @@ -40,7 +52,7 @@ trait ConverterService { import props.externalApiUrls def toApiConcept( - concept: domain.Concept, + concept: DomainConcept, language: String, fallback: Boolean, user: Option[TokenUser] @@ -85,7 +97,7 @@ trait ConverterService { status = status, visualElement = visualElement, responsible = responsible, - conceptType = concept.conceptType.toString, + conceptType = concept.conceptType.entryName, glossData = toApiGlossData(concept.glossData), editorNotes = editorNotes ) @@ -100,7 +112,7 @@ trait ConverterService { } } - def toApiGlossData(domainGlossData: Option[domain.GlossData]): Option[api.GlossData] = { + def toApiGlossData(domainGlossData: Option[GlossData]): Option[api.GlossData] = { domainGlossData.map(glossData => api.GlossData( gloss = glossData.gloss, @@ -120,13 +132,13 @@ trait ConverterService { ) } - def toApiStatus(status: domain.Status): api.Status = { + def toApiStatus(status: Status): api.Status = { api.Status( current = status.current.toString, other = status.other.map(_.toString).toSeq ) } - private def toApiEditorNote(editorNote: domain.EditorNote) = { + private def toApiEditorNote(editorNote: ConceptEditorNote) = { api.EditorNote( note = editorNote.note, updatedBy = editorNote.user, @@ -169,35 +181,35 @@ trait ConverterService { def toApiConceptTitle(title: Title): api.ConceptTitle = api.ConceptTitle(title.title, title.language) - def toApiConceptContent(content: domain.ConceptContent): api.ConceptContent = + def toApiConceptContent(content: ConceptContent): api.ConceptContent = api.ConceptContent(Jsoup.parseBodyFragment(content.content).body().text(), content.content, content.language) - def toApiMetaImage(metaImage: domain.ConceptMetaImage): api.ConceptMetaImage = + def toApiMetaImage(metaImage: ConceptMetaImage): api.ConceptMetaImage = api.ConceptMetaImage( s"${externalApiUrls("raw-image")}/${metaImage.imageId}", metaImage.altText, metaImage.language ) - def toApiVisualElement(visualElement: domain.VisualElement): api.VisualElement = + def toApiVisualElement(visualElement: VisualElement): api.VisualElement = api.VisualElement(converterService.addUrlOnElement(visualElement.visualElement), visualElement.language) private def toApiConceptResponsible(responsible: Responsible): api.ConceptResponsible = api.ConceptResponsible(responsibleId = responsible.responsibleId, lastUpdated = responsible.lastUpdated) - def toDomainGlossData(apiGlossData: Option[api.GlossData]): Try[Option[domain.GlossData]] = { + def toDomainGlossData(apiGlossData: Option[api.GlossData]): Try[Option[GlossData]] = { apiGlossData .map(glossData => WordClass.valueOfOrError(glossData.wordClass) match { case Failure(ex) => Failure(ex) case Success(wordClass) => Success( - domain.GlossData( + concept.GlossData( gloss = glossData.gloss, wordClass = wordClass, examples = glossData.examples.map(gl => gl.map(g => - domain.GlossExample(language = g.language, example = g.example, transcriptions = g.transcriptions) + GlossExample(language = g.language, example = g.example, transcriptions = g.transcriptions) ) ), originalLanguage = glossData.originalLanguage, @@ -209,10 +221,10 @@ trait ConverterService { .sequence } - def toDomainConcept(concept: api.NewConcept, userInfo: TokenUser): Try[domain.Concept] = { + def toDomainConcept(concept: api.NewConcept, userInfo: TokenUser): Try[DomainConcept] = { val conceptType = ConceptType.valueOfOrError(concept.conceptType).getOrElse(ConceptType.CONCEPT) val content = concept.content - .map(content => Seq(domain.ConceptContent(content, concept.language))) + .map(content => Seq(model.domain.concept.ConceptContent(content, concept.language))) .getOrElse(Seq.empty) val visualElement = concept.visualElement .filterNot(_.isEmpty) @@ -222,7 +234,7 @@ trait ConverterService { for { glossData <- toDomainGlossData(concept.glossData) - } yield domain.Concept( + } yield DomainConcept( id = None, revision = None, title = Seq(Title(concept.title, concept.language)), @@ -231,7 +243,8 @@ trait ConverterService { created = now, updated = now, updatedBy = Seq(userInfo.id), - metaImage = concept.metaImage.map(m => domain.ConceptMetaImage(m.id, m.alt, concept.language)).toSeq, + metaImage = + concept.metaImage.map(m => model.domain.concept.ConceptMetaImage(m.id, m.alt, concept.language)).toSeq, tags = concept.tags.map(t => toDomainTags(t, concept.language)).getOrElse(Seq.empty), subjectIds = concept.subjectIds.getOrElse(Seq.empty).toSet, articleIds = concept.articleIds.getOrElse(Seq.empty), @@ -240,7 +253,7 @@ trait ConverterService { responsible = concept.responsibleId.map(responsibleId => Responsible(responsibleId, clock.now())), conceptType = conceptType, glossData = glossData, - editorNotes = Seq(domain.EditorNote(s"Created $conceptType", userInfo.id, Status.default, now)) + editorNotes = Seq(ConceptEditorNote(s"Created $conceptType", userInfo.id, Status.default, now)) ) } @@ -259,8 +272,8 @@ trait ConverterService { HtmlTagRules.jsoupDocumentToString(document) } - private def toDomainVisualElement(visualElement: String, language: String): domain.VisualElement = { - domain.VisualElement( + private def toDomainVisualElement(visualElement: String, language: String): VisualElement = { + concept.VisualElement( visualElement = removeUnknownEmbedTagAttribute(visualElement), language = language ) @@ -270,15 +283,15 @@ trait ConverterService { if (tags.isEmpty) Seq.empty else Seq(Tag(tags, language)) def toDomainConcept( - toMergeInto: domain.Concept, + toMergeInto: DomainConcept, updateConcept: api.UpdatedConcept, userInfo: TokenUser - ): Try[domain.Concept] = { + ): Try[DomainConcept] = { val domainTitle = updateConcept.title .map(t => Title(t, updateConcept.language)) .toSeq val domainContent = updateConcept.content - .map(c => domain.ConceptContent(c, updateConcept.language)) + .map(c => concept.ConceptContent(c, updateConcept.language)) .toSeq val domainTags = updateConcept.tags.map(t => Tag(t, updateConcept.language)).toSeq @@ -289,7 +302,7 @@ trait ConverterService { val updatedMetaImage = updateConcept.metaImage match { case Delete => toMergeInto.metaImage.filterNot(_.language == updateConcept.language) case UpdateWith(m) => - val domainMetaImage = domain.ConceptMetaImage(m.id, m.alt, updateConcept.language) + val domainMetaImage = concept.ConceptMetaImage(m.id, m.alt, updateConcept.language) mergeLanguageFields(toMergeInto.metaImage, Seq(domainMetaImage)) case Missing => toMergeInto.metaImage } @@ -309,7 +322,7 @@ trait ConverterService { } toDomainGlossData(updateConcept.glossData).map(glossData => - domain.Concept( + DomainConcept( id = toMergeInto.id, revision = toMergeInto.revision, title = mergeLanguageFields(toMergeInto.title, domainTitle), @@ -332,14 +345,14 @@ trait ConverterService { ) } - def updateStatus(status: ConceptStatus, concept: domain.Concept, user: TokenUser): Try[domain.Concept] = + def updateStatus(status: ConceptStatus, concept: DomainConcept, user: TokenUser): Try[DomainConcept] = StateTransitionRules.doTransition(concept, status, user) - def toDomainConcept(id: Long, concept: api.UpdatedConcept, userInfo: TokenUser): domain.Concept = { + def toDomainConcept(id: Long, concept: api.UpdatedConcept, userInfo: TokenUser): DomainConcept = { val lang = concept.language val newMetaImage = concept.metaImage match { - case UpdateWith(m) => Seq(domain.ConceptMetaImage(m.id, m.alt, lang)) + case UpdateWith(m) => Seq(model.domain.concept.ConceptMetaImage(m.id, m.alt, lang)) case _ => Seq.empty } @@ -350,11 +363,11 @@ trait ConverterService { // format: off val glossData = concept.glossData.map(gloss => - domain.GlossData( + model.domain.concept.GlossData( gloss = gloss.gloss, wordClass = WordClass.valueOf(gloss.wordClass).getOrElse(WordClass.NOUN), // Default to NOUN, this is NullDocumentConcept case, so we have to improvise examples = gloss.examples.map(ge => - ge.map(g => domain.GlossExample(language = g.language, example = g.example, transcriptions = g.transcriptions))), + ge.map(g => model.domain.concept.GlossExample(language = g.language, example = g.example, transcriptions = g.transcriptions))), originalLanguage = gloss.originalLanguage, transcriptions = gloss.transcriptions ) @@ -363,11 +376,11 @@ trait ConverterService { val conceptType = ConceptType.valueOf(concept.conceptType).getOrElse(ConceptType.CONCEPT) - domain.Concept( + DomainConcept( id = Some(id), revision = None, title = concept.title.map(t => Title(t, lang)).toSeq, - content = concept.content.map(c => domain.ConceptContent(c, lang)).toSeq, + content = concept.content.map(c => model.domain.concept.ConceptContent(c, lang)).toSeq, copyright = concept.copyright.map(toDomainCopyright), created = clock.now(), updated = clock.now(), @@ -381,7 +394,7 @@ trait ConverterService { responsible = responsible, conceptType = conceptType, glossData = glossData, - editorNotes = Seq(domain.EditorNote(s"Created $conceptType", userInfo.id, Status.default, clock.now())) + editorNotes = Seq(ConceptEditorNote(s"Created $conceptType", userInfo.id, Status.default, clock.now())) ) } @@ -415,7 +428,7 @@ trait ConverterService { .toList } - def addUrlOnVisualElement(concept: Concept): Concept = { + def addUrlOnVisualElement(concept: DomainConcept): DomainConcept = { val visualElementWithUrls = concept.visualElement.map(visual => visual.copy(visualElement = addUrlOnElement(visual.visualElement))) concept.copy(visualElement = visualElementWithUrls) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/StateTransitionRules.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/StateTransitionRules.scala index 44688927ce..87d0a96930 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/StateTransitionRules.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/StateTransitionRules.scala @@ -7,12 +7,12 @@ package no.ndla.conceptapi.service -import no.ndla.common.model.domain.Responsible +import no.ndla.common.model.domain.concept.{ConceptEditorNote, ConceptStatus, Status, Concept as DomainConcept} +import no.ndla.common.model.domain.{Responsible, concept} import no.ndla.conceptapi.model.api.ErrorHelpers -import no.ndla.conceptapi.model.domain -import no.ndla.conceptapi.model.domain.ConceptStatus._ +import no.ndla.common.model.domain.concept.ConceptStatus.* import no.ndla.conceptapi.model.domain.SideEffect.SideEffect -import no.ndla.conceptapi.model.domain.{ConceptStatus, StateTransition} +import no.ndla.conceptapi.model.domain.StateTransition import no.ndla.conceptapi.repository.{DraftConceptRepository, PublishedConceptRepository} import no.ndla.conceptapi.service.search.DraftConceptIndexService import no.ndla.conceptapi.validation.ContentValidator @@ -38,14 +38,14 @@ trait StateTransitionRules { object StateTransitionRules { private[service] val unpublishConcept: SideEffect = - (concept: domain.Concept, _: TokenUser) => writeService.unpublishConcept(concept) + (concept: DomainConcept, _: TokenUser) => writeService.unpublishConcept(concept) private[service] val publishConcept: SideEffect = - (concept: domain.Concept, _: TokenUser) => writeService.publishConcept(concept) + (concept: DomainConcept, _: TokenUser) => writeService.publishConcept(concept) - private val resetResponsible: SideEffect = (concept: domain.Concept, _: TokenUser) => + private val resetResponsible: SideEffect = (concept: DomainConcept, _: TokenUser) => Success(concept.copy(responsible = None)) - private val addResponsible: SideEffect = (concept: domain.Concept, user: TokenUser) => { + private val addResponsible: SideEffect = (concept: DomainConcept, user: TokenUser) => { val responsible = concept.responsible.getOrElse(Responsible(user.id, clock.now())) Success(concept.copy(responsible = Some(responsible))) } @@ -110,7 +110,7 @@ trait StateTransitionRules { .find(transition => transition.from == from && transition.to == to) .filter(t => user.hasPermissions(t.requiredPermissions)) - private def validateTransition(current: domain.Concept, transition: StateTransition): Try[Unit] = { + private def validateTransition(current: DomainConcept, transition: StateTransition): Try[Unit] = { val statusRequiresResponsible = ConceptStatus.thatRequiresResponsible.contains(transition.to) val statusFromPublishedToInProgress = current.status.current == PUBLISHED && transition.to == IN_PROGRESS @@ -133,13 +133,13 @@ trait StateTransitionRules { Success(()) } private def newEditorNotesForTransition( - current: domain.Concept, + current: DomainConcept, to: ConceptStatus, - newStatus: domain.Status, + newStatus: Status, user: TokenUser ) = { if (current.status.current != to) - current.editorNotes :+ domain.EditorNote( + current.editorNotes :+ ConceptEditorNote( "Status changed", user.id, newStatus, @@ -148,10 +148,10 @@ trait StateTransitionRules { else current.editorNotes } private[service] def doTransitionWithoutSideEffect( - current: domain.Concept, + current: DomainConcept, to: ConceptStatus, user: TokenUser - ): (Try[domain.Concept], Seq[SideEffect]) = { + ): (Try[DomainConcept], Seq[SideEffect]) = { getTransition(current.status.current, to, user) match { case Some(t) => validateTransition(current, t) match { @@ -161,7 +161,7 @@ trait StateTransitionRules { if (t.addCurrentStateToOthersOnTransition) Set(current.status.current) else Set.empty val other = current.status.other.intersect(t.otherStatesToKeepOnTransition) ++ currentToOther - val newStatus = domain.Status(to, other) + val newStatus = concept.Status(to, other) val newEditorNotes = newEditorNotesForTransition(current, to, newStatus, user) val convertedArticle = current.copy(status = newStatus, editorNotes = newEditorNotes) (Success(convertedArticle), t.sideEffects) @@ -175,10 +175,10 @@ trait StateTransitionRules { } def doTransition( - current: domain.Concept, + current: DomainConcept, to: ConceptStatus, user: TokenUser - ): Try[domain.Concept] = { + ): Try[DomainConcept] = { val (convertedArticle, sideEffects) = doTransitionWithoutSideEffect(current, to, user) convertedArticle.flatMap(conceptBeforeSideEffect => { sideEffects.foldLeft(Try(conceptBeforeSideEffect))((accumulatedConcept, sideEffect) => { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/WriteService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/WriteService.scala index c296f68040..a5f67869b2 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/WriteService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/WriteService.scala @@ -9,20 +9,19 @@ package no.ndla.conceptapi.service import com.typesafe.scalalogging.StrictLogging import no.ndla.common.Clock -import no.ndla.conceptapi.repository.{DraftConceptRepository, PublishedConceptRepository} -import no.ndla.conceptapi.model.domain -import no.ndla.conceptapi.model.domain.ConceptStatus._ +import no.ndla.common.model.NDLADate +import no.ndla.common.model.api.UpdateWith +import no.ndla.common.model.domain.concept.ConceptStatus.* +import no.ndla.common.model.domain.concept.{ConceptEditorNote, ConceptStatus, Concept as DomainConcept} import no.ndla.conceptapi.model.api import no.ndla.conceptapi.model.api.{ConceptExistsAlreadyException, ConceptMissingIdException, NotFoundException} -import no.ndla.conceptapi.model.domain.{Concept, ConceptStatus} +import no.ndla.conceptapi.repository.{DraftConceptRepository, PublishedConceptRepository} import no.ndla.conceptapi.service.search.{DraftConceptIndexService, PublishedConceptIndexService} -import no.ndla.conceptapi.validation._ +import no.ndla.conceptapi.validation.* import no.ndla.language.Language import no.ndla.network.tapir.auth.TokenUser import scala.util.{Failure, Success, Try} -import no.ndla.common.model.NDLADate -import no.ndla.common.model.api.UpdateWith trait WriteService { this: DraftConceptRepository @@ -38,9 +37,9 @@ trait WriteService { class WriteService { def insertListingImportedConcepts( - conceptsWithListingId: Seq[(domain.Concept, Long)], + conceptsWithListingId: Seq[(DomainConcept, Long)], forceUpdate: Boolean - ): Seq[Try[domain.Concept]] = { + ): Seq[Try[DomainConcept]] = { conceptsWithListingId.map { case (concept, listingId) => val existing = draftConceptRepository.withListingId(listingId).nonEmpty if (existing && !forceUpdate) { @@ -56,7 +55,7 @@ trait WriteService { } } - def saveImportedConcepts(concepts: Seq[domain.Concept], forceUpdate: Boolean): Seq[Try[domain.Concept]] = { + def saveImportedConcepts(concepts: Seq[DomainConcept], forceUpdate: Boolean): Seq[Try[DomainConcept]] = { concepts.map(concept => { concept.id match { case Some(id) if draftConceptRepository.exists(id) => @@ -87,10 +86,10 @@ trait WriteService { } yield apiC } - private def shouldUpdateStatus(existing: domain.Concept, changed: domain.Concept): Boolean = { + private def shouldUpdateStatus(existing: DomainConcept, changed: DomainConcept): Boolean = { // Function that sets values we don't want to include when comparing concepts to check if we should update status val withComparableValues = - (concept: domain.Concept) => + (concept: DomainConcept) => concept.copy( revision = None, created = NDLADate.fromUnixTime(0), @@ -100,11 +99,11 @@ trait WriteService { } private def updateStatusIfNeeded( - existing: domain.Concept, - changed: domain.Concept, + existing: DomainConcept, + changed: DomainConcept, updateStatus: Option[String], user: TokenUser - ): Try[Concept] = { + ): Try[DomainConcept] = { if (!shouldUpdateStatus(existing, changed) && updateStatus.isEmpty) { Success(changed) } else { @@ -116,10 +115,10 @@ trait WriteService { } } - private def shouldUpdateNotes(existing: domain.Concept, changed: domain.Concept): Boolean = { + private def shouldUpdateNotes(existing: DomainConcept, changed: DomainConcept): Boolean = { // Function that sets values we don't want to include when comparing concepts to check if we should update notes val withComparableValues = - (concept: domain.Concept) => + (concept: DomainConcept) => concept.copy( revision = None, created = NDLADate.fromUnixTime(0), @@ -131,11 +130,11 @@ trait WriteService { } private def updateNotes( - old: domain.Concept, + old: DomainConcept, updated: api.UpdatedConcept, - changed: domain.Concept, + changed: DomainConcept, user: TokenUser - ): domain.Concept = { + ): DomainConcept = { val isNewLanguage = !old.supportedLanguages.contains(updated.language) && changed.supportedLanguages.contains(updated.language) val dataChanged = shouldUpdateNotes(old, changed); @@ -154,11 +153,11 @@ trait WriteService { val allNewNotes = newEditorNote ++ changedResponsibleNote changed.copy(editorNotes = - changed.editorNotes ++ allNewNotes.map(domain.EditorNote(_, user.id, changed.status, clock.now())) + changed.editorNotes ++ allNewNotes.map(ConceptEditorNote(_, user.id, changed.status, clock.now())) ) } - private def updateConcept(toUpdate: domain.Concept): Try[domain.Concept] = { + private def updateConcept(toUpdate: DomainConcept): Try[DomainConcept] = { for { _ <- contentValidator.validateConcept(toUpdate) domainConcept <- draftConceptRepository.update(toUpdate) @@ -222,7 +221,7 @@ trait WriteService { withStatus <- updateStatusIfNeeded(existingConcept, newConcept, None, userInfo) conceptWithUpdatedNotes = withStatus.copy(editorNotes = withStatus.editorNotes ++ Seq( - domain.EditorNote( + ConceptEditorNote( s"Deleted language '$language'.", userInfo.id, withStatus.status, @@ -244,7 +243,7 @@ trait WriteService { } - def updateConceptStatus(status: domain.ConceptStatus, id: Long, user: TokenUser): Try[api.Concept] = { + def updateConceptStatus(status: ConceptStatus, id: Long, user: TokenUser): Try[api.Concept] = { draftConceptRepository.withId(id) match { case None => Failure(NotFoundException(s"No article with id $id was found")) case Some(draft) => @@ -262,14 +261,14 @@ trait WriteService { } } - def publishConcept(concept: domain.Concept): Try[domain.Concept] = { + def publishConcept(concept: DomainConcept): Try[DomainConcept] = { for { inserted <- publishedConceptRepository.insertOrUpdate(concept) indexed <- publishedConceptIndexService.indexDocument(inserted) } yield indexed } - def unpublishConcept(concept: domain.Concept): Try[domain.Concept] = { + def unpublishConcept(concept: DomainConcept): Try[DomainConcept] = { concept.id match { case Some(id) => for { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptIndexService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptIndexService.scala index a4d4d3d523..39c571f80c 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptIndexService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptIndexService.scala @@ -8,8 +8,8 @@ package no.ndla.conceptapi.service.search import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.Props -import no.ndla.conceptapi.model.domain.Concept import no.ndla.conceptapi.repository.{DraftConceptRepository, Repository} trait DraftConceptIndexService { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptSearchService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptSearchService.scala index 7a555c6497..1273f48424 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptSearchService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/DraftConceptSearchService.scala @@ -11,10 +11,11 @@ import cats.implicits._ import com.sksamuel.elastic4s.ElasticDsl._ import com.sksamuel.elastic4s.requests.searches.queries.compound.BoolQuery import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.model.domain.concept.ConceptStatus import no.ndla.conceptapi.Props import no.ndla.conceptapi.model.api import no.ndla.conceptapi.model.api.{ErrorHelpers, OperationNotAllowedException, SubjectTags} -import no.ndla.conceptapi.model.domain.{ConceptStatus, SearchResult} +import no.ndla.conceptapi.model.domain.SearchResult import no.ndla.conceptapi.model.search.{DraftSearchSettings, DraftSearchSettingsHelper} import no.ndla.conceptapi.service.ConverterService import no.ndla.language.Language.AllLanguages diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/IndexService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/IndexService.scala index 7f90c0b70d..d92312f3a2 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/IndexService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/IndexService.scala @@ -16,11 +16,12 @@ import com.sksamuel.elastic4s.requests.mappings.MappingDefinition import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.common.CirceUtil +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.Props import no.ndla.conceptapi.integration.TaxonomyApiClient import no.ndla.conceptapi.integration.model.TaxonomyData import no.ndla.conceptapi.model.api.{ConceptMissingIdException, ElasticIndexingException} -import no.ndla.conceptapi.model.domain.{Concept, ReindexResult} +import no.ndla.conceptapi.model.domain.ReindexResult import no.ndla.conceptapi.repository.Repository import no.ndla.search.SearchLanguage.{NynorskLanguageAnalyzer, languageAnalyzers} import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/PublishedConceptIndexService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/PublishedConceptIndexService.scala index 650fef69de..d1476aca6c 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/PublishedConceptIndexService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/PublishedConceptIndexService.scala @@ -8,8 +8,8 @@ package no.ndla.conceptapi.service.search import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.model.domain.concept.Concept import no.ndla.conceptapi.Props -import no.ndla.conceptapi.model.domain.Concept import no.ndla.conceptapi.repository.{PublishedConceptRepository, Repository} trait PublishedConceptIndexService { diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala index aab1e3f3b1..4268e49fdc 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/service/search/SearchConverterService.scala @@ -11,13 +11,14 @@ import com.sksamuel.elastic4s.requests.searches.SearchHit import com.typesafe.scalalogging.StrictLogging import no.ndla.common.CirceUtil import no.ndla.common.model.domain.draft.DraftCopyright -import no.ndla.common.model.domain.{Tag, Title} +import no.ndla.common.model.domain.{Tag, Title, concept} import no.ndla.common.model.api as commonApi +import no.ndla.common.model.domain.concept.{Concept, ConceptContent, ConceptMetaImage, ConceptType, VisualElement} import no.ndla.conceptapi.integration.model.TaxonomyData import no.ndla.conceptapi.model.api.{ConceptResponsible, ConceptSearchResult, SubjectTags} -import no.ndla.conceptapi.model.domain.{Concept, ConceptType, SearchResult} +import no.ndla.conceptapi.model.domain.SearchResult import no.ndla.conceptapi.model.search.* -import no.ndla.conceptapi.model.{api, domain} +import no.ndla.conceptapi.model.api import no.ndla.conceptapi.service.ConverterService import no.ndla.language.Language.{UnknownLanguage, findByLanguageOrBestEffort, getSupportedLanguages} import no.ndla.mapping.ISO639 @@ -33,8 +34,8 @@ trait SearchConverterService { class SearchConverterService extends StrictLogging { private def getEmbedResourcesAndIdsToIndex( - visualElement: Seq[domain.VisualElement], - metaImage: Seq[domain.ConceptMetaImage] + visualElement: Seq[VisualElement], + metaImage: Seq[ConceptMetaImage] ): List[EmbedValues] = { val visualElementTuples = visualElement.flatMap(v => getEmbedValues(v.visualElement, v.language)) val metaImageTuples = @@ -95,7 +96,7 @@ trait SearchConverterService { SearchableConcept( id = c.id.get, - conceptType = c.conceptType.toString, + conceptType = c.conceptType.entryName, title = title, content = content, defaultTitle = title.defaultValue, @@ -125,10 +126,10 @@ trait SearchConverterService { def hitAsConceptSummary(hitString: String, language: String): api.ConceptSummary = { val searchableConcept = CirceUtil.unsafeParseAs[SearchableConcept](hitString) val titles = searchableConcept.title.languageValues.map(lv => Title(lv.value, lv.language)) - val contents = searchableConcept.content.languageValues.map(lv => domain.ConceptContent(lv.value, lv.language)) - val tags = searchableConcept.tags.languageValues.map(lv => Tag(lv.value, lv.language)) + val contents = searchableConcept.content.languageValues.map(lv => ConceptContent(lv.value, lv.language)) + val tags = searchableConcept.tags.languageValues.map(lv => Tag(lv.value, lv.language)) val visualElements = - searchableConcept.visualElement.languageValues.map(lv => domain.VisualElement(lv.value, lv.language)) + searchableConcept.visualElement.languageValues.map(lv => concept.VisualElement(lv.value, lv.language)) val supportedLanguages = getSupportedLanguages(titles, contents) diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/validation/ContentValidator.scala b/concept-api/src/main/scala/no/ndla/conceptapi/validation/ContentValidator.scala index aab3ee8fa5..2093a44da5 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/validation/ContentValidator.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/validation/ContentValidator.scala @@ -9,16 +9,16 @@ package no.ndla.conceptapi.validation import no.ndla.common.model.domain.{Author, Title} import no.ndla.common.errors.{ValidationException, ValidationMessage} +import no.ndla.common.model.domain.concept.{Concept, ConceptContent, ConceptMetaImage, ConceptStatus, VisualElement} import no.ndla.common.model.domain.draft.DraftCopyright import no.ndla.conceptapi.Props -import no.ndla.conceptapi.model.domain._ import no.ndla.conceptapi.repository.DraftConceptRepository import no.ndla.conceptapi.service.ConverterService import no.ndla.conceptapi.validation.GlossDataValidator.validateGlossData import no.ndla.language.model.{Iso639, WithLanguage} import no.ndla.mapping.License.getLicense import no.ndla.validation.HtmlTagRules.allLegalTags -import no.ndla.validation._ +import no.ndla.validation.* import scala.util.{Failure, Success, Try} diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/validation/GlossDataValidator.scala b/concept-api/src/main/scala/no/ndla/conceptapi/validation/GlossDataValidator.scala index 2c5658cbe4..b2585f707d 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/validation/GlossDataValidator.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/validation/GlossDataValidator.scala @@ -8,8 +8,8 @@ package no.ndla.conceptapi.validation import no.ndla.common.errors.ValidationMessage -import no.ndla.conceptapi.model.domain.ConceptType.{CONCEPT, GLOSS} -import no.ndla.conceptapi.model.domain.{ConceptType, GlossData} +import no.ndla.common.model.domain.concept.{ConceptType, GlossData} +import no.ndla.common.model.domain.concept.ConceptType.{CONCEPT, GLOSS} object GlossDataValidator { @@ -33,10 +33,10 @@ object GlossDataValidator { def validateGlossData( maybeGlossData: Option[GlossData], - conceptType: ConceptType.Value + conceptType: ConceptType ): Option[ValidationMessage] = { (maybeGlossData, conceptType) match { - case (None, GLOSS) => glossDataValidationMessage(conceptType.toString) + case (None, GLOSS) => glossDataValidationMessage(conceptType.entryName) case (Some(_), CONCEPT) => conceptTypeValidationMessage case (_, _) => None } diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/TestData.scala b/concept-api/src/test/scala/no/ndla/conceptapi/TestData.scala index 210238a7a8..115bec8353 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/TestData.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/TestData.scala @@ -8,13 +8,21 @@ package no.ndla.conceptapi import no.ndla.common.configuration.Constants.EmbedTagName -import no.ndla.common.model.{domain => common} -import no.ndla.conceptapi.model.{api, domain} -import no.ndla.conceptapi.model.domain.{ConceptContent, Status} +import no.ndla.common.model.domain as common +import no.ndla.conceptapi.model.api import no.ndla.network.tapir.auth.Permission.{CONCEPT_API_ADMIN, CONCEPT_API_WRITE} import no.ndla.network.tapir.auth.TokenUser import no.ndla.common.model.NDLADate import no.ndla.common.model.api.Missing +import no.ndla.common.model.domain.concept +import no.ndla.common.model.domain.concept.{ + Concept, + ConceptContent, + ConceptMetaImage, + ConceptType, + Status, + VisualElement +} object TestData { @@ -69,28 +77,28 @@ object TestData { editorNotes = Some(Seq.empty) ) - val sampleNbDomainConcept: domain.Concept = domain.Concept( + val sampleNbDomainConcept: Concept = Concept( id = Some(1), revision = Some(1), title = Seq(common.Title("Tittel", "nb")), - content = Seq(domain.ConceptContent("Innhold", "nb")), + content = Seq(ConceptContent("Innhold", "nb")), copyright = None, created = yesterday, updated = today, updatedBy = Seq.empty, - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb")), + metaImage = Seq(ConceptMetaImage("1", "Hei", "nb")), tags = Seq(common.Tag(Seq("stor", "kaktus"), "nb")), subjectIds = Set("urn:subject:3", "urn:subject:4"), articleIds = Seq(42), status = Status.default, - visualElement = Seq(domain.VisualElement(visualElementString, "nb")), + visualElement = Seq(VisualElement(visualElementString, "nb")), responsible = None, - conceptType = domain.ConceptType.CONCEPT, + conceptType = ConceptType.CONCEPT, glossData = None, editorNotes = Seq.empty ) - val sampleConcept: domain.Concept = domain.Concept( + val sampleConcept: Concept = Concept( id = Some(1), revision = Some(1), title = Seq(common.Title("Tittel for begrep", "nb")), @@ -101,40 +109,40 @@ object TestData { created = NDLADate.now().minusDays(4), updated = NDLADate.now().minusDays(2), updatedBy = Seq.empty, - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hei", "nb")), tags = Seq(common.Tag(Seq("liten", "fisk"), "nb")), subjectIds = Set("urn:subject:3", "urn:subject:4"), articleIds = Seq(42), status = Status.default, - visualElement = Seq(domain.VisualElement("VisualElement for begrep", "nb")), + visualElement = Seq(concept.VisualElement("VisualElement for begrep", "nb")), responsible = None, - conceptType = domain.ConceptType.CONCEPT, + conceptType = concept.ConceptType.CONCEPT, glossData = None, editorNotes = Seq.empty ) - val domainConcept: domain.Concept = domain.Concept( + val domainConcept: Concept = Concept( id = Some(1), revision = Some(1), title = Seq(common.Title("Tittel", "nb"), common.Title("Tittelur", "nn")), - content = Seq(domain.ConceptContent("Innhold", "nb"), domain.ConceptContent("Innhald", "nn")), + content = Seq(concept.ConceptContent("Innhold", "nb"), concept.ConceptContent("Innhald", "nn")), copyright = None, created = yesterday, updated = today, updatedBy = Seq(""), - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb"), domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hei", "nb"), concept.ConceptMetaImage("2", "Hej", "nn")), tags = Seq(common.Tag(Seq("stor", "kaktus"), "nb"), common.Tag(Seq("liten", "fisk"), "nn")), subjectIds = Set("urn:subject:3", "urn:subject:4"), articleIds = Seq(42), status = Status.default, - visualElement = Seq(domain.VisualElement(visualElementString, "nb")), + visualElement = Seq(concept.VisualElement(visualElementString, "nb")), responsible = None, - conceptType = domain.ConceptType.CONCEPT, + conceptType = concept.ConceptType.CONCEPT, glossData = None, editorNotes = Seq.empty ) - val domainConcept_toDomainUpdateWithId: domain.Concept = domain.Concept( + val domainConcept_toDomainUpdateWithId: Concept = Concept( id = None, revision = None, title = Seq.empty, @@ -150,7 +158,7 @@ object TestData { status = Status.default, visualElement = Seq.empty, responsible = None, - conceptType = domain.ConceptType.CONCEPT, + conceptType = concept.ConceptType.CONCEPT, glossData = None, editorNotes = Seq.empty ) diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/repository/DraftConceptRepositoryTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/repository/DraftConceptRepositoryTest.scala index b0c6170efb..a742420f85 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/repository/DraftConceptRepositoryTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/repository/DraftConceptRepositoryTest.scala @@ -8,19 +8,17 @@ package no.ndla.conceptapi.repository import com.zaxxer.hikari.HikariDataSource - -import java.net.Socket -import no.ndla.common.model.{domain => common} -import no.ndla.conceptapi.model.domain +import no.ndla.common.model.{NDLADate, domain as common} +import no.ndla.common.model.domain.concept +import no.ndla.common.model.domain.concept.ConceptContent +import no.ndla.conceptapi.TestData.* import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} -import scalikejdbc.DB -import no.ndla.conceptapi.TestData._ import no.ndla.scalatestsuite.IntegrationSuite import org.scalatest.Outcome +import scalikejdbc.* +import java.net.Socket import scala.util.{Failure, Success, Try} -import scalikejdbc._ -import no.ndla.common.model.NDLADate class DraftConceptRepositoryTest extends IntegrationSuite(EnablePostgresContainer = true) @@ -87,7 +85,7 @@ class DraftConceptRepositoryTest val id2 = repository.insert(art2).id.get val id3 = repository.insert(art3).id.get - val updatedContent = Seq(domain.ConceptContent("What u do mr", "nb")) + val updatedContent = Seq(ConceptContent("What u do mr", "nb")) repository.update(art1.copy(id = Some(id1), content = updatedContent)) repository.withId(id1).get.content should be(updatedContent) @@ -255,7 +253,7 @@ class DraftConceptRepositoryTest test("Revision mismatch fail with optimistic lock exception") { val art1 = domainConcept.copy( revision = None, - content = Seq(domain.ConceptContent("Originalpls", "nb")), + content = Seq(concept.ConceptContent("Originalpls", "nb")), created = NDLADate.fromUnixTime(0), updated = NDLADate.fromUnixTime(0) ) @@ -265,7 +263,7 @@ class DraftConceptRepositoryTest repository.withId(insertedId).get.revision should be(Some(1)) - val updatedContent = Seq(domain.ConceptContent("Updatedpls", "nb")) + val updatedContent = Seq(concept.ConceptContent("Updatedpls", "nb")) val updatedArt1 = art1.copy(revision = Some(10), id = Some(insertedId), content = updatedContent) val updateResult1 = repository.update(updatedArt1) @@ -285,17 +283,17 @@ class DraftConceptRepositoryTest test("That getByPage returns all concepts in database") { val con1 = domainConcept.copy( - content = Seq(domain.ConceptContent("Hei", "nb")), + content = Seq(concept.ConceptContent("Hei", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) val con2 = domainConcept.copy( - content = Seq(domain.ConceptContent("På", "nb")), + content = Seq(concept.ConceptContent("På", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) val con3 = domainConcept.copy( - content = Seq(domain.ConceptContent("Deg", "nb")), + content = Seq(concept.ConceptContent("Deg", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/repository/PublishedConceptRepositoryTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/repository/PublishedConceptRepositoryTest.scala index 7a4b8a8b07..3150fb15f4 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/repository/PublishedConceptRepositoryTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/repository/PublishedConceptRepositoryTest.scala @@ -8,18 +8,17 @@ package no.ndla.conceptapi.repository import com.zaxxer.hikari.HikariDataSource - -import java.net.Socket -import no.ndla.common.model.{domain => common} -import no.ndla.conceptapi._ -import no.ndla.conceptapi.model.domain +import no.ndla.common.model.{NDLADate, domain as common} +import no.ndla.common.model.domain.concept +import no.ndla.common.model.domain.concept.ConceptContent +import no.ndla.conceptapi.* +import no.ndla.conceptapi.model.domain.PublishedConcept import no.ndla.scalatestsuite.IntegrationSuite import org.scalatest.Outcome -import scalikejdbc.{DB, _} +import scalikejdbc.{DB, *} +import java.net.Socket import scala.util.{Failure, Success, Try} -import no.ndla.common.model.NDLADate -import no.ndla.conceptapi.model.domain.PublishedConcept class PublishedConceptRepositoryTest extends IntegrationSuite(EnablePostgresContainer = true) with TestEnvironment { @@ -241,20 +240,20 @@ class PublishedConceptRepositoryTest extends IntegrationSuite(EnablePostgresCont test("That getByPage returns all concepts in database") { val con1 = TestData.domainConcept.copy( id = Some(1), - content = Seq(domain.ConceptContent("Hei", "nb")), + content = Seq(ConceptContent("Hei", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) val con2 = TestData.domainConcept.copy( id = Some(2), revision = Some(100), - content = Seq(domain.ConceptContent("På", "nb")), + content = Seq(concept.ConceptContent("På", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) val con3 = TestData.domainConcept.copy( id = Some(3), - content = Seq(domain.ConceptContent("Deg", "nb")), + content = Seq(concept.ConceptContent("Deg", "nb")), updated = NDLADate.fromUnixTime(0), created = NDLADate.fromUnixTime(0) ) diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/ConverterServiceTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/ConverterServiceTest.scala index 4a456092b9..b4d58935fd 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/ConverterServiceTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/ConverterServiceTest.scala @@ -7,19 +7,18 @@ package no.ndla.conceptapi.service -import no.ndla.common.model.domain.Responsible -import no.ndla.common.model.{api as commonApi, domain as common} +import no.ndla.common.model.api.{Delete, Missing, UpdateWith} +import no.ndla.common.model.domain.concept.* +import no.ndla.common.model.domain.{Responsible, concept} +import no.ndla.common.model.{NDLADate, api as commonApi, domain as common} +import no.ndla.conceptapi.model.api import no.ndla.conceptapi.model.api.{NewConcept, NotFoundException, UpdatedConcept} -import no.ndla.conceptapi.model.domain.{ConceptType, VisualElement, WordClass} -import no.ndla.conceptapi.model.{api, domain} import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} import no.ndla.network.tapir.auth.Permission.{CONCEPT_API_ADMIN, CONCEPT_API_WRITE} import no.ndla.network.tapir.auth.TokenUser +import org.mockito.Mockito.when import scala.util.{Failure, Success} -import no.ndla.common.model.NDLADate -import no.ndla.common.model.api.{Delete, Missing, UpdateWith} -import org.mockito.Mockito.when class ConverterServiceTest extends UnitSuite with TestEnvironment { @@ -106,8 +105,8 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { converterService.toDomainConcept(TestData.domainConcept, updateWith, userInfo).get should be( TestData.domainConcept.copy( content = Seq( - domain.ConceptContent("Innhold", "nb"), - domain.ConceptContent("Nytt innhald", "nn") + ConceptContent("Innhold", "nb"), + concept.ConceptContent("Nytt innhald", "nn") ), updated = updated ) @@ -142,9 +141,9 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { common.Title("Title", "en") ), content = Seq( - domain.ConceptContent("Innhold", "nb"), - domain.ConceptContent("Innhald", "nn"), - domain.ConceptContent("My content", "en") + concept.ConceptContent("Innhold", "nb"), + concept.ConceptContent("Innhald", "nn"), + concept.ConceptContent("My content", "en") ), updated = updated ) @@ -184,8 +183,8 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { converterService.toDomainConcept(TestData.domainConcept, updateWith, userInfo).get should be( TestData.domainConcept.copy( content = Seq( - domain.ConceptContent("Innhold", "nb"), - domain.ConceptContent("Nytt innhald", "nn") + concept.ConceptContent("Innhold", "nb"), + concept.ConceptContent("Nytt innhald", "nn") ), copyright = Option( common.draft.DraftCopyright( @@ -265,10 +264,10 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + Status(ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -288,10 +287,10 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + concept.Status(concept.ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -306,11 +305,11 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { when(clock.now()).thenReturn(updated) val beforeUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb"), domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(ConceptMetaImage("1", "Hei", "nb"), concept.ConceptMetaImage("2", "Hej", "nn")), updated = updated ) val afterUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(concept.ConceptMetaImage("2", "Hej", "nn")), updated = updated ) val updateWith = TestData.emptyApiUpdatedConcept.copy(language = "nb", metaImage = Delete) @@ -323,11 +322,11 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { when(clock.now()).thenReturn(updated) val beforeUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb"), domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hei", "nb"), concept.ConceptMetaImage("2", "Hej", "nn")), updated = updated ) val afterUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("2", "Hej", "nn"), domain.ConceptMetaImage("1", "Hola", "nb")), + metaImage = Seq(concept.ConceptMetaImage("2", "Hej", "nn"), concept.ConceptMetaImage("1", "Hola", "nb")), updated = updated ) val updateWith = TestData.emptyApiUpdatedConcept.copy( @@ -343,11 +342,11 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { when(clock.now()).thenReturn(updated) val beforeUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb"), domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hei", "nb"), concept.ConceptMetaImage("2", "Hej", "nn")), updated = updated ) val afterUpdate = TestData.domainConcept.copy( - metaImage = Seq(domain.ConceptMetaImage("1", "Hei", "nb"), domain.ConceptMetaImage("2", "Hej", "nn")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hei", "nb"), concept.ConceptMetaImage("2", "Hej", "nn")), updated = updated ) val updateWith = TestData.emptyApiUpdatedConcept.copy(language = "nb", metaImage = Missing) @@ -361,14 +360,14 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val afterUpdate = TestData.domainConcept_toDomainUpdateWithId.copy( id = Some(12), - metaImage = Seq(domain.ConceptMetaImage("1", "Hola", "nb")), + metaImage = Seq(concept.ConceptMetaImage("1", "Hola", "nb")), created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + concept.Status(concept.ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -391,10 +390,10 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + concept.Status(concept.ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -450,10 +449,10 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "test", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + concept.Status(concept.ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -474,10 +473,10 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { created = today, updated = today, editorNotes = Seq( - domain.EditorNote( + ConceptEditorNote( "Created concept", "test", - domain.Status(domain.ConceptStatus.IN_PROGRESS, Set.empty), + concept.Status(concept.ConceptStatus.IN_PROGRESS, Set.empty), today ) ) @@ -536,21 +535,21 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val newConcept = TestData.emptyApiNewConcept.copy(conceptType = "gloss", glossData = Some(newGlossData)) val expectedGlossExample1 = List( - domain.GlossExample(example = "nei men saa", language = "nb", transcriptions = Map("a" -> "b")), - domain.GlossExample(example = "jog har inta", "nn", transcriptions = Map("b" -> "c")) + GlossExample(example = "nei men saa", language = "nb", transcriptions = Map("a" -> "b")), + concept.GlossExample(example = "jog har inta", "nn", transcriptions = Map("b" -> "c")) ) val expectedGlossExample2 = - List(domain.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) + List(concept.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) val expectedGlossData = Some( - domain.GlossData( + GlossData( gloss = "juan", - wordClass = domain.WordClass.NOUN, + wordClass = WordClass.NOUN, originalLanguage = "nb", examples = List(expectedGlossExample1, expectedGlossExample2), transcriptions = Map("zh" -> "a", "pinyin" -> "b") ) ) - val expectedConceptType = domain.ConceptType.GLOSS + val expectedConceptType = ConceptType.GLOSS val result = converterService.toDomainConcept(newConcept, TestData.userWithWriteAccess).get result.conceptType should be(expectedConceptType) @@ -604,26 +603,26 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { TestData.emptyApiUpdatedConcept.copy(conceptType = Some("gloss"), glossData = Some(updatedGlossData)) val expectedGlossExample1 = List( - domain.GlossExample( + concept.GlossExample( example = "nei men saa", language = "nb", transcriptions = Map("a" -> "b") ), - domain.GlossExample(example = "jog har inta", "nn", transcriptions = Map("a" -> "b")) + concept.GlossExample(example = "jog har inta", "nn", transcriptions = Map("a" -> "b")) ) val expectedGlossExample2 = - List(domain.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) + List(concept.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) val expectedGlossData = Some( - domain.GlossData( + concept.GlossData( gloss = "huehue", - wordClass = domain.WordClass.NOUN, + wordClass = concept.WordClass.NOUN, originalLanguage = "nb", examples = List(expectedGlossExample1, expectedGlossExample2), transcriptions = Map("zh" -> "a", "pinyin" -> "b") ) ) - val expectedConceptType = domain.ConceptType.GLOSS - val existingConcept = TestData.domainConcept.copy(conceptType = domain.ConceptType.CONCEPT, glossData = None) + val expectedConceptType = concept.ConceptType.GLOSS + val existingConcept = TestData.domainConcept.copy(conceptType = concept.ConceptType.CONCEPT, glossData = None) val result = converterService.toDomainConcept(existingConcept, updatedConcept, TestData.userWithWriteAccess).get result.conceptType should be(expectedConceptType) @@ -649,7 +648,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val updatedConcept = TestData.emptyApiUpdatedConcept.copy(conceptType = Some("gloss"), glossData = Some(updatedGlossData)) - val existingConcept = TestData.domainConcept.copy(conceptType = domain.ConceptType.CONCEPT, glossData = None) + val existingConcept = TestData.domainConcept.copy(conceptType = concept.ConceptType.CONCEPT, glossData = None) val Failure(result) = converterService.toDomainConcept(existingConcept, updatedConcept, TestData.userWithWriteAccess) @@ -658,15 +657,15 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { test("that toApiConcept converts gloss data correctly") { val domainGlossExample1 = List( - domain.GlossExample(example = "nei men saa", language = "nb", transcriptions = Map("a" -> "b")), - domain.GlossExample(example = "jog har inta", "nn", transcriptions = Map("b" -> "c")) + concept.GlossExample(example = "nei men saa", language = "nb", transcriptions = Map("a" -> "b")), + concept.GlossExample(example = "jog har inta", "nn", transcriptions = Map("b" -> "c")) ) val domainGlossExample2 = - List(domain.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) + List(concept.GlossExample(example = "nei men da saa", language = "nb", transcriptions = Map("a" -> "b"))) val domainGlossData = Some( - domain.GlossData( + concept.GlossData( gloss = "gestalt", - wordClass = domain.WordClass.NOUN, + wordClass = concept.WordClass.NOUN, originalLanguage = "nb", examples = List(domainGlossExample1, domainGlossExample2), transcriptions = Map("zh" -> "a", "pinyin" -> "b") @@ -674,7 +673,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { ) val existingConcept = TestData.domainConcept.copy( - conceptType = domain.ConceptType.GLOSS, + conceptType = concept.ConceptType.GLOSS, glossData = domainGlossData, title = Seq(common.Title("title", "nb")) ) @@ -712,9 +711,9 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { ) val expectedGlossExample = - domain.GlossExample(example = "some example", language = "nb", transcriptions = Map("a" -> "b")) + concept.GlossExample(example = "some example", language = "nb", transcriptions = Map("a" -> "b")) val expectedGlossData = - domain.GlossData( + concept.GlossData( gloss = "yoink", wordClass = WordClass.VERB, originalLanguage = "nb", diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/ReadServiceTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/ReadServiceTest.scala index 8dc6153f7f..ab27e4375b 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/ReadServiceTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/ReadServiceTest.scala @@ -9,7 +9,7 @@ package no.ndla.conceptapi.service import no.ndla.common.configuration.Constants.EmbedTagName import no.ndla.common.model.domain as common -import no.ndla.conceptapi.model.domain.VisualElement +import no.ndla.common.model.domain.concept.VisualElement import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} import no.ndla.network.tapir.auth.Permission.CONCEPT_API_WRITE import no.ndla.network.tapir.auth.TokenUser diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/StateTransitionRulesTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/StateTransitionRulesTest.scala index b1caf74d8e..d7eb54924f 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/StateTransitionRulesTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/StateTransitionRulesTest.scala @@ -1,10 +1,10 @@ package no.ndla.conceptapi.service +import no.ndla.common.model.domain.concept.{Concept, ConceptContent, ConceptStatus, ConceptType, Status} import no.ndla.common.model.domain.draft.DraftCopyright import no.ndla.common.model.domain.{Author, Responsible, Tag, Title} import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} -import no.ndla.conceptapi.model.domain -import no.ndla.conceptapi.model.domain.{ConceptContent, ConceptStatus, ConceptType, StateTransition, Status} +import no.ndla.conceptapi.model.domain.StateTransition import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.when import org.mockito.invocation.InvocationOnMock @@ -14,7 +14,7 @@ import scala.util.Success class StateTransitionRulesTest extends UnitSuite with TestEnvironment { test("That publishing concept results in responsibleId being reset") { val beforeResponsible = Responsible("heisann", clock.now()) - val concept = domain.Concept( + val concept = Concept( id = Some(1L), revision = Some(1), title = Seq(Title("tittel", "nb")), @@ -45,14 +45,10 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { glossData = None, editorNotes = Seq.empty ) - val status = domain.Status(ConceptStatus.IN_PROGRESS, Set.empty) + val status = Status(ConceptStatus.IN_PROGRESS, Set.empty) val transitionsToTest = StateTransitionRules.StateTransitions.filter(_.to == ConceptStatus.PUBLISHED) - when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) - when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) + when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) + when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) for (t <- transitionsToTest) { val fromDraft = concept.copy(status = status.copy(current = t.from), responsible = Some(beforeResponsible)) val result = @@ -66,7 +62,7 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { test("That archiving concept results in responsibleId being reset") { val beforeResponsible = Responsible("heisann", clock.now()) - val concept = domain.Concept( + val concept = Concept( id = Some(1L), revision = Some(1), title = Seq(Title("tittel", "nb")), @@ -97,14 +93,10 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { glossData = None, editorNotes = Seq.empty ) - val status = domain.Status(ConceptStatus.IN_PROGRESS, Set.empty) + val status = Status(ConceptStatus.IN_PROGRESS, Set.empty) val transitionsToTest = StateTransitionRules.StateTransitions.filter(_.to == ConceptStatus.ARCHIVED) - when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) - when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) + when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) + when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) for (t <- transitionsToTest) { val fromDraft = concept.copy(status = status.copy(current = t.from), responsible = Some(beforeResponsible)) val result = StateTransitionRules @@ -118,7 +110,7 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { test("That unpublishing concept results in responsibleId being reset") { val beforeResponsible = Responsible("heisann", clock.now()) - val concept = domain.Concept( + val concept = Concept( id = Some(1L), revision = Some(1), title = Seq(Title("tittel", "nb")), @@ -149,14 +141,10 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { glossData = None, editorNotes = Seq.empty ) - val status = domain.Status(ConceptStatus.IN_PROGRESS, Set.empty) + val status = Status(ConceptStatus.IN_PROGRESS, Set.empty) val transitionsToTest = StateTransitionRules.StateTransitions.filter(_.to == ConceptStatus.UNPUBLISHED) - when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) - when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) + when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) + when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) for (t <- transitionsToTest) { val fromDraft = concept.copy(status = status.copy(current = t.from), responsible = Some(beforeResponsible)) val result = StateTransitionRules @@ -169,7 +157,7 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { } test("That responsibleId is updated at status change from published to in progress") { - val concept = domain.Concept( + val concept = Concept( id = Some(1L), revision = Some(1), title = Seq(Title("tittel", "nb")), @@ -200,15 +188,11 @@ class StateTransitionRulesTest extends UnitSuite with TestEnvironment { glossData = None, editorNotes = Seq.empty ) - val status = domain.Status(ConceptStatus.PUBLISHED, Set.empty) + val status = Status(ConceptStatus.PUBLISHED, Set.empty) val transitionToTest: StateTransition = ConceptStatus.PUBLISHED -> ConceptStatus.IN_PROGRESS val expected = TestData.userWithWriteAndPublishAccess.id - when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) - when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => - Success(i.getArgument[domain.Concept](0)) - ) + when(writeService.publishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) + when(writeService.unpublishConcept(any)).thenAnswer((i: InvocationOnMock) => Success(i.getArgument[Concept](0))) val fromConcept = concept.copy(status = status.copy(current = transitionToTest.from)) val result = StateTransitionRules diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/WriteServiceTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/WriteServiceTest.scala index 88197e1e1d..02771e6c6f 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/WriteServiceTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/WriteServiceTest.scala @@ -10,8 +10,7 @@ package no.ndla.conceptapi.service import no.ndla.common.model.domain.{Responsible, Tag, Title} import no.ndla.common.model.api as commonApi import no.ndla.conceptapi.model.api.ConceptResponsible -import no.ndla.conceptapi.model.domain.* -import no.ndla.conceptapi.model.{api, domain} +import no.ndla.conceptapi.model.api import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} import no.ndla.network.tapir.auth.TokenUser import org.mockito.invocation.InvocationOnMock @@ -21,6 +20,14 @@ import scalikejdbc.DBSession import scala.util.{Failure, Success, Try} import no.ndla.common.model.NDLADate import no.ndla.common.model.api.{Missing, UpdateWith} +import no.ndla.common.model.domain.concept.{ + Concept, + ConceptContent, + ConceptMetaImage, + ConceptStatus, + Status, + VisualElement +} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.{reset, times, verify, when} import org.mockito.stubbing.OngoingStubbing @@ -42,12 +49,12 @@ class WriteServiceTest extends UnitSuite with TestEnvironment { responsible = Some(ConceptResponsible("hei", TestData.today)) ) - val domainConcept: domain.Concept = TestData.sampleNbDomainConcept.copy( + val domainConcept: Concept = TestData.sampleNbDomainConcept.copy( id = Some(conceptId), responsible = Some(Responsible("hei", TestData.today)) ) - def mockWithConcept(concept: domain.Concept): OngoingStubbing[NDLADate] = { + def mockWithConcept(concept: Concept): OngoingStubbing[NDLADate] = { when(draftConceptRepository.withId(conceptId)).thenReturn(Option(concept)) when(draftConceptRepository.update(any[Concept])(any[DBSession])) .thenAnswer((invocation: InvocationOnMock) => Try(invocation.getArgument[Concept](0))) diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/search/DraftConceptSearchServiceTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/search/DraftConceptSearchServiceTest.scala index 67490312bb..0ebf9a4e1e 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/search/DraftConceptSearchServiceTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/search/DraftConceptSearchServiceTest.scala @@ -9,7 +9,7 @@ package no.ndla.conceptapi.service.search import no.ndla.common.configuration.Constants.EmbedTagName import no.ndla.common.model.domain.draft.DraftCopyright -import no.ndla.common.model.domain.{Author, Responsible, Tag, Title} +import no.ndla.common.model.domain.{Author, Responsible, Tag, Title, concept} import no.ndla.conceptapi.* import no.ndla.conceptapi.model.api.SubjectTags import no.ndla.conceptapi.model.domain.* @@ -20,6 +20,17 @@ import org.scalatest.Outcome import scala.util.Success import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.concept.{ + Concept, + ConceptContent, + ConceptMetaImage, + ConceptStatus, + ConceptType, + GlossData, + Status, + VisualElement, + WordClass +} import no.ndla.conceptapi.integration.model.TaxonomyData import org.mockito.Mockito.when @@ -159,7 +170,7 @@ class DraftConceptSearchServiceTest extends IntegrationSuite(EnableElasticsearch content = List(ConceptContent("
Bilde av Baldurs som har mareritt.", "nb")), tags = Seq(Tag(Seq("stor", "klovn"), "nb")), subjectIds = Set("urn:subject:1", "urn:subject:100"), - status = Status(current = ConceptStatus.PUBLISHED, other = Set.empty), + status = concept.Status(current = ConceptStatus.PUBLISHED, other = Set.empty), metaImage = Seq(ConceptMetaImage("test.image", "imagealt", "nb"), ConceptMetaImage("test.url2", "imagealt", "en")), responsible = Some(Responsible("test1", today)) ) @@ -172,7 +183,7 @@ class DraftConceptSearchServiceTest extends IntegrationSuite(EnableElasticsearch tags = Seq(Tag(Seq("cageowl"), "en"), Tag(Seq("burugle"), "nb")), updated = NDLADate.now().minusDays(1), subjectIds = Set("urn:subject:2"), - status = Status(current = ConceptStatus.FOR_APPROVAL, other = Set(ConceptStatus.PUBLISHED)), + status = concept.Status(current = ConceptStatus.FOR_APPROVAL, other = Set(ConceptStatus.PUBLISHED)), updatedBy = Seq("Test1"), visualElement = List( VisualElement( @@ -194,7 +205,7 @@ class DraftConceptSearchServiceTest extends IntegrationSuite(EnableElasticsearch copyright = Some(publicDomain), title = List(Title("deleted", "en"), Title("slettet", "nb")), content = List(ConceptContent("deleted", "en"), ConceptContent("slettet", "nb")), - status = Status(current = ConceptStatus.ARCHIVED, other = Set.empty) + status = concept.Status(current = ConceptStatus.ARCHIVED, other = Set.empty) ) val concept13: Concept = TestData.sampleConcept.copy( diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/service/search/PublishedConceptSearchServiceTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/service/search/PublishedConceptSearchServiceTest.scala index f9632f513d..9ea076fcb6 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/service/search/PublishedConceptSearchServiceTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/service/search/PublishedConceptSearchServiceTest.scala @@ -21,6 +21,15 @@ import org.scalatest.Outcome import java.time.LocalDateTime import scala.util.Success import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.concept.{ + Concept, + ConceptContent, + ConceptMetaImage, + ConceptType, + GlossData, + VisualElement, + WordClass +} import no.ndla.conceptapi.integration.model.TaxonomyData import no.ndla.search.model.domain.{Bucket, TermAggregation} import org.mockito.Mockito.when diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/validation/ContentValidatorTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/validation/ContentValidatorTest.scala index c828f858f4..1d5bc9ff6d 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/validation/ContentValidatorTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/validation/ContentValidatorTest.scala @@ -7,10 +7,10 @@ package no.ndla.conceptapi.validation -import no.ndla.common.model.domain.{Author, Responsible, Title} import no.ndla.common.errors.{ValidationException, ValidationMessage} +import no.ndla.common.model.domain.concept.{Concept, ConceptContent} import no.ndla.common.model.domain.draft.DraftCopyright -import no.ndla.conceptapi.model.domain.{Concept, ConceptContent} +import no.ndla.common.model.domain.{Author, Responsible, Title} import no.ndla.conceptapi.{TestData, TestEnvironment, UnitSuite} import scala.util.{Failure, Success} diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/validation/GlossDataValidatorTest.scala b/concept-api/src/test/scala/no/ndla/conceptapi/validation/GlossDataValidatorTest.scala index dd58342378..70d8dac158 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/validation/GlossDataValidatorTest.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/validation/GlossDataValidatorTest.scala @@ -7,8 +7,8 @@ package no.ndla.conceptapi.validation +import no.ndla.common.model.domain.concept.{ConceptType, GlossData, GlossExample, WordClass} import no.ndla.conceptapi.{TestEnvironment, UnitSuite} -import no.ndla.conceptapi.model.domain.{ConceptType, GlossExample, GlossData, WordClass} class GlossDataValidatorTest extends UnitSuite with TestEnvironment { test("that GlossDataValidator fails if ConceptType is concept and glossData is defined") { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index aab64c356f..b27901309a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -95,6 +95,7 @@ object Dependencies { lazy val tapir: Seq[ModuleID] = Seq( "com.softwaremill.sttp.tapir" %% "tapir-http4s-server" % TapirV, "com.softwaremill.sttp.tapir" %% "tapir-swagger-ui-bundle" % TapirV, + "com.softwaremill.sttp.tapir" %% "tapir-enumeratum" % TapirV, "com.softwaremill.sttp.tapir" %% "tapir-json-circe" % TapirV, "com.softwaremill.sttp.tapir" %% "tapir-jdkhttp-server" % TapirV, "com.softwaremill.sttp.tapir" %% "tapir-prometheus-metrics" % TapirV, diff --git a/project/constantslib.scala b/project/constantslib.scala index 66ad172634..c068ba0c4a 100644 --- a/project/constantslib.scala +++ b/project/constantslib.scala @@ -33,7 +33,7 @@ object constantslib extends Module { "no.ndla.common.model.domain.config._", "no.ndla.network.tapir.auth._", "no.ndla.common.model.domain.draft._", - "no.ndla.conceptapi.model.domain._" + "no.ndla.common.model.domain.concept._" ), typescriptExports := Seq( "ConfigKey", diff --git a/project/searchapi.scala b/project/searchapi.scala index 244d3ed4da..090d157fb9 100644 --- a/project/searchapi.scala +++ b/project/searchapi.scala @@ -41,7 +41,6 @@ object searchapi extends Module { "ImageResults", "LearningpathResults", "SearchError", - "ValidationError", "DraftSearchParams", "SubjectAggregations", "SubjectAggsInput" diff --git a/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala b/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala index df092219af..42f689bf57 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala @@ -23,15 +23,17 @@ import no.ndla.network.tapir.{ } import no.ndla.search.{BaseIndexService, Elastic4sClient} import no.ndla.searchapi.controller.{InternController, SearchController, SwaggerDocControllerConfig} -import no.ndla.searchapi.integration._ +import no.ndla.searchapi.integration.* import no.ndla.searchapi.model.api.ErrorHelpers -import no.ndla.searchapi.service.search._ +import no.ndla.searchapi.service.search.* import no.ndla.searchapi.service.ConverterService class ComponentRegistry(properties: SearchApiProperties) extends BaseComponentRegistry[SearchApiProperties] with ArticleApiClient with ArticleIndexService + with DraftConceptApiClient + with DraftConceptIndexService with LearningPathIndexService with DraftIndexService with MultiSearchService @@ -75,6 +77,7 @@ class ComponentRegistry(properties: SearchApiProperties) lazy val grepApiClient = new GrepApiClient lazy val draftApiClient = new DraftApiClient(DraftApiUrl) + lazy val draftConceptApiClient = new DraftConceptApiClient(ConceptApiUrl) lazy val learningPathApiClient = new LearningPathApiClient(LearningpathApiUrl) lazy val articleApiClient = new ArticleApiClient(ArticleApiUrl) lazy val feideApiClient = new FeideApiClient @@ -84,6 +87,7 @@ class ComponentRegistry(properties: SearchApiProperties) lazy val searchConverterService = new SearchConverterService lazy val multiSearchService = new MultiSearchService lazy val articleIndexService = new ArticleIndexService + lazy val draftConceptIndexService = new DraftConceptIndexService lazy val learningPathIndexService = new LearningPathIndexService lazy val draftIndexService = new DraftIndexService lazy val multiDraftSearchService = new MultiDraftSearchService diff --git a/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala b/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala index d85f25f4d8..87654be3a7 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala @@ -13,7 +13,8 @@ import no.ndla.common.configuration.{BaseProps, HasBaseProps} import no.ndla.network.{AuthUser, Domains} import no.ndla.searchapi.model.search.SearchType -import scala.util.Properties._ +import scala.util.Properties.* +import scala.util.{Failure, Success, Try} trait Props extends HasBaseProps { val props: SearchApiProperties @@ -30,17 +31,25 @@ class SearchApiProperties extends BaseProps with StrictLogging { def SearchServer: String = propOrElse("SEARCH_SERVER", "http://search-search-api.ndla-local") - def SearchIndexes: Map[SearchType.Value, String] = Map( - SearchType.Articles -> propOrElse("ARTICLE_SEARCH_INDEX_NAME", "articles"), - SearchType.Drafts -> propOrElse("DRAFT_SEARCH_INDEX_NAME", "drafts"), - SearchType.LearningPaths -> propOrElse("LEARNINGPATH_SEARCH_INDEX_NAME", "learningpaths") - ) + val articleIndexName = propOrElse("ARTICLE_SEARCH_INDEX_NAME", "articles") + val draftIndexName = propOrElse("DRAFT_SEARCH_INDEX_NAME", "drafts") + val learningpathIndexName = propOrElse("LEARNINGPATH_SEARCH_INDEX_NAME", "learningpaths") + val conceptIndexName = propOrElse("DRAFT_CONCEPT_SEARCH_INDEX_NAME", "draftconcepts") - def SearchDocuments: Map[SearchType.Value, String] = Map( - SearchType.Articles -> "article", - SearchType.Drafts -> "draft", - SearchType.LearningPaths -> "learningpath" - ) + def SearchIndex(searchType: SearchType) = searchType match { + case SearchType.Articles => articleIndexName + case SearchType.Drafts => draftIndexName + case SearchType.LearningPaths => learningpathIndexName + case SearchType.Concepts => conceptIndexName + } + + def indexToSearchType(indexName: String): Try[SearchType] = indexName match { + case `articleIndexName` => Success(SearchType.Articles) + case `draftIndexName` => Success(SearchType.Drafts) + case `learningpathIndexName` => Success(SearchType.LearningPaths) + case `conceptIndexName` => Success(SearchType.Concepts) + case _ => Failure(new IllegalArgumentException(s"Unknown index name: $indexName")) + } def DefaultPageSize = 10 def MaxPageSize = 10000 @@ -54,6 +63,7 @@ class SearchApiProperties extends BaseProps with StrictLogging { def ExternalApiUrls: Map[String, String] = Map( "article-api" -> s"$Domain/article-api/v2/articles", + "concept-api" -> s"$Domain/concept-api/v1/drafts", "draft-api" -> s"$Domain/draft-api/v1/drafts", "learningpath-api" -> s"$Domain/learningpath-api/v2/learningpaths", "raw-image" -> s"$Domain/image-api/raw/id" diff --git a/search-api/src/main/scala/no/ndla/searchapi/controller/InternController.scala b/search-api/src/main/scala/no/ndla/searchapi/controller/InternController.scala index 4e41b3e3fc..c862192a8b 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/controller/InternController.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/controller/InternController.scala @@ -14,6 +14,7 @@ import no.ndla.common.CirceUtil import no.ndla.common.model.domain.article.Article import no.ndla.common.model.domain.draft.Draft import no.ndla.common.model.domain.Content +import no.ndla.common.model.domain.concept.Concept import no.ndla.network.model.RequestInfo import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody import no.ndla.network.tapir.{AllErrors, Service} @@ -23,7 +24,14 @@ import no.ndla.searchapi.integration.{GrepApiClient, MyNDLAApiClient, TaxonomyAp import no.ndla.searchapi.model.api.ErrorHelpers import no.ndla.searchapi.model.domain.{IndexingBundle, ReindexResult} import no.ndla.searchapi.model.domain.learningpath.LearningPath -import no.ndla.searchapi.service.search.{ArticleIndexService, DraftIndexService, IndexService, LearningPathIndexService} +import no.ndla.searchapi.model.search.SearchType +import no.ndla.searchapi.service.search.{ + ArticleIndexService, + DraftConceptIndexService, + DraftIndexService, + IndexService, + LearningPathIndexService +} import sttp.model.StatusCode import java.util.concurrent.{Executors, TimeUnit} @@ -40,6 +48,7 @@ trait InternController { with ArticleIndexService with LearningPathIndexService with DraftIndexService + with DraftConceptIndexService with TaxonomyApiClient with GrepApiClient with Props @@ -51,7 +60,7 @@ trait InternController { import ErrorHelpers._ implicit val ec: ExecutionContext = - ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(props.SearchIndexes.size)) + ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(SearchType.values.size)) override val prefix: EndpointInput[Unit] = "intern" override val enableSwagger = false @@ -98,7 +107,8 @@ trait InternController { reindexById, reindexArticle, reindexDraft, - reindexLearningpath + reindexLearningpath, + reindexConcept ) def deleteDocument: ServerEndpoint[Any, Eff] = endpoint.delete @@ -110,6 +120,7 @@ trait InternController { case articleIndexService.documentType => articleIndexService.deleteDocument(documentId) case draftIndexService.documentType => draftIndexService.deleteDocument(documentId) case learningPathIndexService.documentType => learningPathIndexService.deleteDocument(documentId) + case draftConceptIndexService.documentType => draftConceptIndexService.deleteDocument(documentId) case _ => Success(()) }).map(_ => ()).handleErrorsOrOk } @@ -139,7 +150,8 @@ trait InternController { oneOf[Content]( oneOfVariant(jsonBody[Article]), oneOfVariant(jsonBody[Draft]), - oneOfVariant(jsonBody[LearningPath]) + oneOfVariant(jsonBody[LearningPath]), + oneOfVariant(jsonBody[Concept]) ) ) .errorOut(errorOutputsFor(400)) @@ -148,6 +160,7 @@ trait InternController { case articleIndexService.documentType => indexRequestWithService(articleIndexService, body) case draftIndexService.documentType => indexRequestWithService(draftIndexService, body) case learningPathIndexService.documentType => indexRequestWithService(learningPathIndexService, body) + case draftConceptIndexService.documentType => indexRequestWithService(draftConceptIndexService, body) case _ => badRequest( s"Bad type passed to POST /:type/, must be one of: '${articleIndexService.documentType}', '${draftIndexService.documentType}', '${learningPathIndexService.documentType}'" @@ -162,7 +175,8 @@ trait InternController { oneOf[Content]( oneOfVariant(jsonBody[Article]), oneOfVariant(jsonBody[Draft]), - oneOfVariant(jsonBody[LearningPath]) + oneOfVariant(jsonBody[LearningPath]), + oneOfVariant(jsonBody[Concept]) ) ) .serverLogicPure { case (indexType, id) => @@ -170,6 +184,7 @@ trait InternController { case articleIndexService.documentType => articleIndexService.reindexDocument(id).handleErrorsOrOk case draftIndexService.documentType => draftIndexService.reindexDocument(id).handleErrorsOrOk case learningPathIndexService.documentType => learningPathIndexService.reindexDocument(id).handleErrorsOrOk + case draftConceptIndexService.documentType => draftConceptIndexService.reindexDocument(id).handleErrorsOrOk case _ => badRequest( s"Bad type passed to POST /:type/:id, must be one of: '${articleIndexService.documentType}', '${draftIndexService.documentType}', '${learningPathIndexService.documentType}'" @@ -192,6 +207,21 @@ trait InternController { resolveResultFutures(List(draftIndex)) } + def reindexConcept: ServerEndpoint[Any, Eff] = endpoint.post + .in("index" / "concept") + .in(query[Option[Int]]("numShards")) + .errorOut(stringInternalServerError) + .out(stringBody) + .serverLogicPure { numShards => + val requestInfo = RequestInfo.fromThreadContext() + val conceptIndex = Future { + requestInfo.setThreadContextRequestInfo() + ("concepts", draftConceptIndexService.indexDocuments(shouldUsePublishedTax = false, numShards)) + } + + resolveResultFutures(List(conceptIndex)) + } + def reindexArticle: ServerEndpoint[Any, Eff] = endpoint.post .in("index" / "article") .in(query[Option[Int]]("numShards")) @@ -232,11 +262,13 @@ trait InternController { articleIndexService.cleanupIndexes(): Unit draftIndexService.cleanupIndexes(): Unit learningPathIndexService.cleanupIndexes(): Unit + draftConceptIndexService.cleanupIndexes(): Unit val articles = articleIndexService.reindexWithShards(numShards) val drafts = draftIndexService.reindexWithShards(numShards) val learningpaths = learningPathIndexService.reindexWithShards(numShards) - List(articles, drafts, learningpaths).sequence match { + val concept = draftConceptIndexService.reindexWithShards(numShards) + List(articles, drafts, learningpaths, concept).sequence match { case Success(_) => s"Reindexing with $numShards shards completed in ${System.currentTimeMillis() - startTime}ms".asRight case Failure(ex) => @@ -254,11 +286,13 @@ trait InternController { articleIndexService.cleanupIndexes(): Unit draftIndexService.cleanupIndexes(): Unit learningPathIndexService.cleanupIndexes(): Unit + draftConceptIndexService.cleanupIndexes(): Unit val articles = articleIndexService.updateReplicaNumber(numReplicas) val drafts = draftIndexService.updateReplicaNumber(numReplicas) val learningpaths = learningPathIndexService.updateReplicaNumber(numReplicas) - List(articles, drafts, learningpaths).sequence match { + val concepts = draftConceptIndexService.updateReplicaNumber(numReplicas) + List(articles, drafts, learningpaths, concepts).sequence match { case Success(_) => s"Updated replication setting for indexes to $numReplicas replicas. Populating may take some time.".asRight case Failure(ex) => @@ -298,6 +332,7 @@ trait InternController { learningPathIndexService.cleanupIndexes(): Unit articleIndexService.cleanupIndexes(): Unit draftIndexService.cleanupIndexes(): Unit + draftConceptIndexService.cleanupIndexes(): Unit val publishedIndexingBundle = IndexingBundle( grepBundle = Some(grepBundle), @@ -324,6 +359,10 @@ trait InternController { Future { requestInfo.setThreadContextRequestInfo() ("drafts", draftIndexService.indexDocuments(numShards, draftIndexingBundle)) + }, + Future { + requestInfo.setThreadContextRequestInfo() + ("concepts", draftConceptIndexService.indexDocuments(numShards, draftIndexingBundle)) } ) if (runInBackground) { diff --git a/search-api/src/main/scala/no/ndla/searchapi/controller/SearchController.scala b/search-api/src/main/scala/no/ndla/searchapi/controller/SearchController.scala index db9d4fefb4..e0e060d383 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/controller/SearchController.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/controller/SearchController.scala @@ -25,6 +25,7 @@ import no.ndla.searchapi.{Eff, Props} import no.ndla.searchapi.integration.SearchApiClient import no.ndla.searchapi.model.api.{ErrorHelpers, GroupSearchResult, MultiSearchResult, SubjectAggregations} import no.ndla.searchapi.model.domain.{LearningResourceType, Sort} +import no.ndla.searchapi.model.search.SearchType import no.ndla.searchapi.model.search.settings.{MultiDraftSearchSettings, SearchSettings} import no.ndla.searchapi.service.search.{ MultiDraftSearchService, @@ -563,7 +564,8 @@ trait SearchController { priority = stringListParam("priority").some, topics = stringListParam("topics").some, publishedDateFrom = dateParamOrNone("published-date-from"), - publishedDateTo = dateParamOrNone("published-date-to") + publishedDateTo = dateParamOrNone("published-date-to"), + resultTypes = stringListParam("result-types").flatMap(SearchType.withNameOption).some ) ) @@ -687,7 +689,8 @@ trait SearchController { prioritized = params.prioritized, priority = params.priority.getOrElse(List.empty), publishedFilterFrom = params.publishedDateFrom, - publishedFilterTo = params.publishedDateTo + publishedFilterTo = params.publishedDateTo, + resultTypes = params.resultTypes ) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/DraftSearchParams.scala b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/DraftSearchParams.scala index 99d6bdd7bc..4393e384bd 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/DraftSearchParams.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/DraftSearchParams.scala @@ -11,6 +11,7 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate import no.ndla.network.tapir.NonEmptyString +import no.ndla.searchapi.model.search.SearchType import sttp.tapir.Schema import sttp.tapir.Schema.annotations.description @@ -125,7 +126,11 @@ import sttp.tapir.Schema.annotations.description @description("Return only results having published date before this date.") publishedDateTo: Option[NDLADate], -) + + @description("Types of hits to appear in the result") + resultTypes: Option[List[SearchType]] + ) +// format: on object DraftSearchParams { implicit val encoder: Encoder[DraftSearchParams] = deriveEncoder diff --git a/search-api/src/main/scala/no/ndla/searchapi/integration/DraftConceptApiClient.scala b/search-api/src/main/scala/no/ndla/searchapi/integration/DraftConceptApiClient.scala new file mode 100644 index 0000000000..8f9b852251 --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/integration/DraftConceptApiClient.scala @@ -0,0 +1,21 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.searchapi.integration + +import no.ndla.network.NdlaClient + +trait DraftConceptApiClient { + this: NdlaClient & SearchApiClient => + val draftConceptApiClient: DraftConceptApiClient + + class DraftConceptApiClient(val baseUrl: String) extends SearchApiClient { + override val searchPath = "concept-api/v1/drafts" + override val name = "concepts" + override val dumpDomainPath = "intern/dump/draft-concept" + } +} diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala index 50f92d0c45..01bfa31c3b 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/Error.scala @@ -8,36 +8,12 @@ package no.ndla.searchapi.model.api +import cats.implicits.catsSyntaxOptionId import no.ndla.common.Clock -import no.ndla.common.errors.AccessDeniedException -import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers} +import no.ndla.common.errors.{AccessDeniedException, ValidationException} +import no.ndla.network.tapir.{AllErrors, TapirErrorHelpers, ValidationErrorBody} import no.ndla.search.{IndexNotFoundException, NdlaSearchException} import no.ndla.searchapi.Props -import sttp.tapir.Schema.annotations.description - -import java.time.LocalDateTime - -@description("Information about an error") -case class Error( - @description("Code stating the type of error") code: String, - @description("Description of the error") description: String, - @description("An optional id referring to the cover") id: Option[Long] = None, - @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() -) - -@description("Information about validation errors") -case class ValidationError( - @description("Code stating the type of error") code: String, - @description("Description of the error") description: String, - @description("List of validation messages") messages: Seq[ValidationMessage], - @description("When the error occured") occuredAt: LocalDateTime = LocalDateTime.now() -) - -@description("A message describing a validation error on a specific field") -case class ValidationMessage( - @description("The field the error occured in") field: String, - @description("The validation message") message: String -) trait ErrorHelpers extends TapirErrorHelpers { this: Props with Clock => @@ -50,7 +26,9 @@ trait ErrorHelpers extends TapirErrorHelpers { case _: IndexNotFoundException => errorBody(INDEX_MISSING, INDEX_MISSING_DESCRIPTION, 503) case _: InvalidIndexBodyException => errorBody(INVALID_BODY, INVALID_BODY_DESCRIPTION, 400) case te: TaxonomyException => errorBody(TAXONOMY_FAILURE, te.getMessage, 500) - case ade: AccessDeniedException => forbiddenMsg(ade.getMessage) + case v: ValidationException => + ValidationErrorBody(VALIDATION, VALIDATION_DESCRIPTION, clock.now(), messages = v.errors.some, 400) + case ade: AccessDeniedException => forbiddenMsg(ade.getMessage) case NdlaSearchException(_, Some(rf), _) if rf.error.rootCause .exists(x => x.`type` == "search_context_missing_exception" || x.reason == "Cannot parse scroll id") => @@ -58,28 +36,15 @@ trait ErrorHelpers extends TapirErrorHelpers { } object SearchErrorHelpers { - val GENERIC = "GENERIC" - val INVALID_BODY = "INVALID_BODY" - val TAXONOMY_FAILURE = "TAXONOMY_FAILURE" - val INVALID_SEARCH_CONTEXT = "INVALID_SEARCH_CONTEXT" - val ACCESS_DENIED = "ACCESS DENIED" - - val GENERIC_DESCRIPTION: String = - s"Ooops. Something we didn't anticipate occured. We have logged the error, and will look into it. But feel free to contact ${props.ContactEmail} if the error persists." + val GENERIC = "GENERIC" + val INVALID_BODY = "INVALID_BODY" + val TAXONOMY_FAILURE = "TAXONOMY_FAILURE" val WINDOW_TOO_LARGE_DESCRIPTION: String = s"The result window is too large. Fetching pages above ${props.ElasticSearchIndexMaxResultWindow} results requires scrolling, see query-parameter 'search-context'." val INVALID_BODY_DESCRIPTION = "Unable to index the requested document because body was invalid." - - val INVALID_SEARCH_CONTEXT_DESCRIPTION = - "The search-context specified was not expected. Please create one by searching from page 1." - - val GenericError: Error = Error(GENERIC, GENERIC_DESCRIPTION) - val IndexMissingError: Error = Error(INDEX_MISSING, INDEX_MISSING_DESCRIPTION) - val InvalidBody: Error = Error(INVALID_BODY, INVALID_BODY_DESCRIPTION) - val InvalidSearchContext: Error = Error(INVALID_SEARCH_CONTEXT, INVALID_SEARCH_CONTEXT_DESCRIPTION) } case class ResultWindowTooLargeException(message: String = SearchErrorHelpers.WINDOW_TOO_LARGE_DESCRIPTION) extends RuntimeException(message) @@ -87,8 +52,6 @@ trait ErrorHelpers extends TapirErrorHelpers { extends RuntimeException(message) } -class ValidationException(message: String = "Validation Error", val errors: Seq[ValidationMessage]) - extends RuntimeException(message) class ApiSearchException(val apiName: String, message: String) extends RuntimeException(message) case class ElasticIndexingException(message: String) extends RuntimeException(message) case class TaxonomyException(message: String) extends RuntimeException(message) diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResult.scala index 456ba6d567..9b8d8ceece 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResult.scala @@ -12,7 +12,6 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import no.ndla.search.api.MultiSearchTermsAggregation import sttp.tapir.Schema.annotations.description -// format: off @description("Search result for group search") case class GroupSearchResult( @description("The total number of resources matching this query") totalCount: Long, @@ -24,7 +23,6 @@ case class GroupSearchResult( @description("The aggregated fields if specified in query") aggregations: Seq[MultiSearchTermsAggregation], @description("Type of resources in this object") resourceType: String ) -// format: on object GroupSearchResult { implicit val encoder: Encoder[GroupSearchResult] = deriveEncoder diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummary.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummary.scala index b72d490fdf..338ae239ae 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummary.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummary.scala @@ -11,6 +11,8 @@ import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import no.ndla.common.model.NDLADate import no.ndla.common.model.api.draft.Comment +import no.ndla.searchapi.model.domain.LearningResourceType +import no.ndla.searchapi.model.search.SearchType import sttp.tapir.Schema.annotations.description @description("Object describing matched field with matching words emphasized") @@ -34,7 +36,7 @@ case class MultiSearchSummary( @description("Url pointing to the resource") url: String, @description("Contexts of the resource") contexts: List[ApiTaxonomyContext], @description("Languages the resource exists in") supportedLanguages: Seq[String], - @description("Learning resource type, either 'standard', 'topic-article' or 'learningpath'") learningResourceType: String, + @description("Learning resource type") learningResourceType: LearningResourceType, @description("Status information of the resource") status: Option[Status], @description("Traits for the resource") traits: List[String], @description("Relevance score. The higher the score, the better the document matches your search criteria.") score: Float, @@ -51,7 +53,9 @@ case class MultiSearchSummary( @description("Name of the parent topic if exists") parentTopicName: Option[String], @description("Name of the primary context root if exists") primaryRootName: Option[String], @description("When the article was last published") published: Option[NDLADate], - @description("Number of times favorited in MyNDLA") favorited: Option[Long] + @description("Number of times favorited in MyNDLA") favorited: Option[Long], + @description("Type of the resource") resultType: SearchType, + @description("Subject ids for the resource, if a concept") conceptSubjectIds: Option[List[String]] ) // format: on diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/domain/LearningResourceType.scala b/search-api/src/main/scala/no/ndla/searchapi/model/domain/LearningResourceType.scala index ab3d0ee0ab..a27d422b2f 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/domain/LearningResourceType.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/domain/LearningResourceType.scala @@ -8,12 +8,48 @@ package no.ndla.searchapi.model.domain -object LearningResourceType extends Enumeration { - val Article: LearningResourceType.Value = Value("standard") - val TopicArticle: LearningResourceType.Value = Value("topic-article") - val LearningPath: LearningResourceType.Value = Value("learningpath") +import com.scalatsi.TypescriptType.{TSLiteralString, TSUnion} +import com.scalatsi.{TSNamedType, TSType} +import enumeratum.* +import no.ndla.common.model.domain.ArticleType +import no.ndla.common.model.domain.concept.ConceptType +import sttp.tapir.Schema +import sttp.tapir.codec.enumeratum.* - def all: List[String] = LearningResourceType.values.map(_.toString).toList +sealed abstract class LearningResourceType(override val entryName: String) extends EnumEntry { + override def toString: String = entryName +} + +object LearningResourceType extends Enum[LearningResourceType] with CirceEnum[LearningResourceType] { + case object Article extends LearningResourceType("standard") + case object TopicArticle extends LearningResourceType("topic-article") + case object FrontpageArticle extends LearningResourceType("frontpage-article") + case object LearningPath extends LearningResourceType("learningpath") + case object Concept extends LearningResourceType("concept") + case object Gloss extends LearningResourceType("gloss") + + implicit val enumTsType: TSNamedType[LearningResourceType] = + TSType.alias[LearningResourceType]("LearningResourceType", TSUnion(values.map(e => TSLiteralString(e.entryName)))) + + def all: List[String] = LearningResourceType.values.map(_.entryName).toList + def valueOf(s: String): Option[LearningResourceType] = LearningResourceType.values.find(_.entryName == s) + override def values: IndexedSeq[LearningResourceType] = findValues + + implicit def schema: Schema[LearningResourceType] = schemaForEnumEntry[LearningResourceType] + + def fromArticleType(articleType: ArticleType): LearningResourceType = { + articleType match { + case ArticleType.Standard => Article + case ArticleType.TopicArticle => TopicArticle + case ArticleType.FrontpageArticle => FrontpageArticle + } + } + + def fromConceptType(conceptType: ConceptType): LearningResourceType = { + conceptType match { + case ConceptType.CONCEPT => LearningResourceType.Concept + case ConceptType.GLOSS => LearningResourceType.Gloss + } - def valueOf(s: String): Option[LearningResourceType.Value] = LearningResourceType.values.find(_.toString == s) + } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/domain/learningpath/LearningStep.scala b/search-api/src/main/scala/no/ndla/searchapi/model/domain/learningpath/LearningStep.scala index a3654cb67c..f94313ad21 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/domain/learningpath/LearningStep.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/domain/learningpath/LearningStep.scala @@ -9,8 +9,8 @@ package no.ndla.searchapi.model.domain.learningpath import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} +import no.ndla.common.errors.{ValidationException, ValidationMessage} import no.ndla.common.model.domain.learningpath.EmbedUrl -import no.ndla.searchapi.model.api.{ValidationException, ValidationMessage} import no.ndla.common.model.domain.Title case class LearningStep( diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchType.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchType.scala index caee7621a1..dd82ea92eb 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchType.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchType.scala @@ -7,11 +7,27 @@ package no.ndla.searchapi.model.search -object SearchType extends Enumeration { - val Articles: SearchType.Value = Value("article") - val Drafts: SearchType.Value = Value("draft") - val LearningPaths: SearchType.Value = Value("learningpath") +import com.scalatsi.TypescriptType.{TSLiteralString, TSUnion} +import com.scalatsi.{TSNamedType, TSType} +import enumeratum.* +import no.ndla.common.CirceUtil.CirceEnumWithErrors +import sttp.tapir.Schema +import sttp.tapir.codec.enumeratum.* - def all: List[String] = SearchType.values.map(_.toString).toList +sealed abstract class SearchType(override val entryName: String) extends EnumEntry { + override def toString: String = entryName +} +object SearchType extends Enum[SearchType] with CirceEnumWithErrors[SearchType] { + case object Articles extends SearchType("article") + case object Drafts extends SearchType("draft") + case object LearningPaths extends SearchType("learningpath") + case object Concepts extends SearchType("concept") + + def all: List[String] = SearchType.values.map(_.toString).toList + override def values: IndexedSeq[SearchType] = findValues + + implicit def schema: Schema[SearchType] = schemaForEnumEntry[SearchType] + implicit val enumTsType: TSNamedType[SearchType] = + TSType.alias[SearchType]("SearchType", TSUnion(values.map(e => TSLiteralString(e.entryName)))) } diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableArticle.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableArticle.scala index 880b48aec9..b4042e3952 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableArticle.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableArticle.scala @@ -13,6 +13,7 @@ import no.ndla.common.model.NDLADate import no.ndla.common.model.domain.ArticleMetaImage import no.ndla.search.model.domain.EmbedValues import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} +import no.ndla.searchapi.model.domain.LearningResourceType case class SearchableArticle( id: Long, @@ -34,7 +35,8 @@ case class SearchableArticle( traits: List[String], embedAttributes: SearchableLanguageList, embedResourcesAndIds: List[EmbedValues], - availability: String + availability: String, + learningResourceType: LearningResourceType ) object SearchableArticle { diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableConcept.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableConcept.scala new file mode 100644 index 0000000000..335037f632 --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableConcept.scala @@ -0,0 +1,46 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.searchapi.model.search + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.Responsible +import no.ndla.common.model.domain.concept.{Concept, ConceptMetaImage} +import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} +import no.ndla.searchapi.model.api.Status +import no.ndla.searchapi.model.domain.LearningResourceType + +case class SearchableConcept( + id: Long, + conceptType: String, + title: SearchableLanguageValues, + content: SearchableLanguageValues, + metaImage: Seq[ConceptMetaImage], + defaultTitle: Option[String], + tags: SearchableLanguageList, + subjectIds: List[String], + lastUpdated: NDLADate, + status: Status, + updatedBy: Seq[String], + license: Option[String], + authors: List[String], + articleIds: Seq[Long], + created: NDLADate, + source: Option[String], + responsible: Option[Responsible], + gloss: Option[String], + domainObject: Concept, + favorited: Long, + learningResourceType: LearningResourceType +) + +object SearchableConcept { + implicit val encoder: Encoder[SearchableConcept] = deriveEncoder + implicit val decoder: Decoder[SearchableConcept] = deriveDecoder +} diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableDraft.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableDraft.scala index 98cdd85377..036db65a01 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableDraft.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableDraft.scala @@ -14,6 +14,7 @@ import no.ndla.common.model.domain.{Priority, Responsible} import no.ndla.common.model.domain.draft.{Draft, RevisionMeta} import no.ndla.search.model.domain.EmbedValues import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} +import no.ndla.searchapi.model.domain.LearningResourceType case class SearchableDraft( id: Long, @@ -50,7 +51,8 @@ case class SearchableDraft( resourceTypeName: SearchableLanguageValues, defaultResourceTypeName: Option[String], published: NDLADate, - favorited: Long + favorited: Long, + learningResourceType: LearningResourceType ) object SearchableDraft { diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableLearningPath.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableLearningPath.scala index 05c66348c1..e29512f209 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableLearningPath.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableLearningPath.scala @@ -12,6 +12,7 @@ import io.circe.{Decoder, Encoder} import no.ndla.common.model.NDLADate import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} import no.ndla.searchapi.model.api.learningpath.Copyright +import no.ndla.searchapi.model.domain.LearningResourceType case class SearchableLearningPath( id: Long, @@ -32,7 +33,8 @@ case class SearchableLearningPath( supportedLanguages: List[String], authors: List[String], contexts: List[SearchableTaxonomyContext], - favorited: Long + favorited: Long, + learningResourceType: LearningResourceType ) object SearchableLearningPath { diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/MultiDraftSearchSettings.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/MultiDraftSearchSettings.scala index 1c6bfd9de5..1f8f25ccf3 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/MultiDraftSearchSettings.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/MultiDraftSearchSettings.scala @@ -12,6 +12,7 @@ import no.ndla.common.model.domain.draft.DraftStatus import no.ndla.language.Language import no.ndla.network.tapir.NonEmptyString import no.ndla.searchapi.model.domain.{LearningResourceType, Sort} +import no.ndla.searchapi.model.search.SearchType case class MultiDraftSearchSettings( query: Option[NonEmptyString], @@ -26,7 +27,7 @@ case class MultiDraftSearchSettings( subjects: List[String], topics: List[String], resourceTypes: List[String], - learningResourceTypes: List[LearningResourceType.Value], + learningResourceTypes: List[LearningResourceType], supportedLanguages: List[String], relevanceIds: List[String], statusFilter: List[DraftStatus], @@ -47,7 +48,8 @@ case class MultiDraftSearchSettings( prioritized: Option[Boolean], priority: List[String], publishedFilterFrom: Option[NDLADate], - publishedFilterTo: Option[NDLADate] + publishedFilterTo: Option[NDLADate], + resultTypes: Option[List[SearchType]] ) object MultiDraftSearchSettings { @@ -85,6 +87,12 @@ object MultiDraftSearchSettings { prioritized = None, priority = List.empty, publishedFilterTo = None, - publishedFilterFrom = None + publishedFilterFrom = None, + resultTypes = Some( + List( + SearchType.Articles, + SearchType.LearningPaths + ) + ) ) } diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/SearchSettings.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/SearchSettings.scala index ba21f5ce10..f076ecb593 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/SearchSettings.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/settings/SearchSettings.scala @@ -23,7 +23,7 @@ case class SearchSettings( withIdIn: List[Long], subjects: List[String], resourceTypes: List[String], - learningResourceTypes: List[LearningResourceType.Value], + learningResourceTypes: List[LearningResourceType], supportedLanguages: List[String], relevanceIds: List[String], grepCodes: List[String], diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/StandaloneIndexing.scala b/search-api/src/main/scala/no/ndla/searchapi/service/StandaloneIndexing.scala index e5d240ba82..7023a48ce6 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/StandaloneIndexing.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/StandaloneIndexing.scala @@ -13,6 +13,7 @@ import no.ndla.common.CirceUtil import no.ndla.common.Environment.{booleanPropOrFalse, prop} import no.ndla.common.model.domain.Content import no.ndla.searchapi.model.domain.{IndexingBundle, ReindexResult} +import no.ndla.searchapi.model.search.SearchType import no.ndla.searchapi.{ComponentRegistry, SearchApiProperties} import sttp.client3.quick.* @@ -101,7 +102,7 @@ class StandaloneIndexing(props: SearchApiProperties, componentRegistry: Componen case Failure(ex) => Seq(Failure(ex)) case Success((taxonomyBundleDraft, taxonomyBundlePublished, grepBundle, myndlaBundle)) => implicit val ec: ExecutionContextExecutorService = - ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(props.SearchIndexes.size)) + ExecutionContext.fromExecutorService(Executors.newFixedThreadPool(SearchType.values.size)) def reindexWithIndexService[C <: Content]( indexService: componentRegistry.IndexService[C], diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/ArticleIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/ArticleIndexService.scala index cd0a25ab2f..4cb90efc20 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/ArticleIndexService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/ArticleIndexService.scala @@ -25,8 +25,9 @@ trait ArticleIndexService { val articleIndexService: ArticleIndexService class ArticleIndexService extends StrictLogging with IndexService[Article] { - override val documentType: String = props.SearchDocuments(SearchType.Articles) - override val searchIndex: String = props.SearchIndexes(SearchType.Articles) + import props.SearchIndex + override val documentType: String = "article" + override val searchIndex: String = SearchIndex(SearchType.Articles) override val apiClient: ArticleApiClient = articleApiClient override def createIndexRequest( @@ -53,6 +54,7 @@ trait ArticleIndexService { textField("grepContexts.title"), keywordField("traits"), keywordField("availability"), + keywordField("learningResourceType"), getTaxonomyContextMapping, nestedField("embedResourcesAndIds").fields( keywordField("resource"), diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftConceptIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftConceptIndexService.scala new file mode 100644 index 0000000000..5e35fac8c0 --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftConceptIndexService.scala @@ -0,0 +1,86 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.searchapi.service.search + +import com.sksamuel.elastic4s.ElasticDsl.* +import com.sksamuel.elastic4s.fields.ObjectField +import com.sksamuel.elastic4s.requests.indexes.IndexRequest +import com.sksamuel.elastic4s.requests.mappings.MappingDefinition +import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.CirceUtil +import no.ndla.searchapi.Props +import no.ndla.searchapi.integration.DraftConceptApiClient +import no.ndla.searchapi.model.domain.IndexingBundle +import no.ndla.searchapi.model.search.SearchType +import no.ndla.common.model.domain.concept.Concept + +import scala.util.Try + +trait DraftConceptIndexService { + this: SearchConverterService & IndexService & DraftConceptApiClient & Props => + val draftConceptIndexService: DraftConceptIndexService + + class DraftConceptIndexService extends StrictLogging with IndexService[Concept] { + import props.SearchIndex + override val documentType: String = "concept" + override val searchIndex: String = SearchIndex(SearchType.Concepts) + override val apiClient: DraftConceptApiClient = draftConceptApiClient + + override def createIndexRequest( + domainModel: Concept, + indexName: String, + indexingBundle: IndexingBundle + ): Try[IndexRequest] = { + searchConverterService.asSearchableConcept(domainModel, indexingBundle).map { searchable => + val source = CirceUtil.toJsonString(searchable) + indexInto(indexName).doc(source).id(domainModel.id.get.toString) + } + } + + def getMapping: MappingDefinition = { + val fields = List( + longField("id"), + keywordField("conceptType"), + nestedField("metaImage").fields( + keywordField("imageId"), + keywordField("altText"), + keywordField("language") + ), + keywordField("defaultTitle"), + keywordField("subjectIds"), + dateField("lastUpdated"), + keywordField("status.current"), + keywordField("status.other"), + keywordField("updatedBy"), + keywordField("license"), + keywordField("authors"), + longField("articleIds"), + dateField("created"), + keywordField("learningResourceType"), + keywordField("source"), + ObjectField( + "responsible", + properties = Seq( + keywordField("responsibleId"), + dateField("lastUpdated") + ) + ), + textField("gloss"), + longField("favorited"), + ObjectField("domainObject", enabled = Some(false)) + ) + val dynamics = + generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ + generateLanguageSupportedDynamicTemplates("content", keepRaw = true) ++ + generateLanguageSupportedDynamicTemplates("tags") + + properties(fields).dynamicTemplates(dynamics) + } + } + +} diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftIndexService.scala index a643156c63..00a87513e8 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftIndexService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/DraftIndexService.scala @@ -23,11 +23,12 @@ import scala.util.Try trait DraftIndexService { this: SearchConverterService with IndexService with DraftApiClient with Props => + import props.SearchIndex val draftIndexService: DraftIndexService class DraftIndexService extends StrictLogging with IndexService[Draft] { - override val documentType: String = props.SearchDocuments(SearchType.Drafts) - override val searchIndex: String = props.SearchIndexes(SearchType.Drafts) + override val documentType: String = "draft" + override val searchIndex: String = SearchIndex(SearchType.Drafts) override val apiClient: DraftApiClient = draftApiClient override def createIndexRequest( @@ -61,6 +62,7 @@ trait DraftIndexService { textField("grepContexts.title"), keywordField("traits"), longField("favorited"), + keywordField("learningResourceType"), ObjectField( "responsible", properties = Seq( diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/LearningPathIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/LearningPathIndexService.scala index 844a8a4ab4..8847975c1e 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/LearningPathIndexService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/LearningPathIndexService.scala @@ -23,11 +23,12 @@ import scala.util.Try trait LearningPathIndexService { this: SearchConverterService with IndexService with LearningPathApiClient with Props => + import props.SearchIndex val learningPathIndexService: LearningPathIndexService class LearningPathIndexService extends StrictLogging with IndexService[LearningPath] { - override val documentType: String = props.SearchDocuments(SearchType.LearningPaths) - override val searchIndex: String = props.SearchIndexes(SearchType.LearningPaths) + override val documentType: String = "learningpath" + override val searchIndex: String = SearchIndex(SearchType.LearningPaths) override val apiClient: LearningPathApiClient = learningPathApiClient override def createIndexRequest( @@ -46,6 +47,7 @@ trait LearningPathIndexService { intField("id"), textField("coverPhotoId"), intField("duration"), + keywordField("learningResourceType"), textField("status"), textField("verificationStatus"), dateField("lastUpdated"), diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiDraftSearchService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiDraftSearchService.scala index 85a26548ff..5a21ecb6eb 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiDraftSearchService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiDraftSearchService.scala @@ -13,6 +13,8 @@ import com.sksamuel.elastic4s.requests.searches.aggs.responses.{AggResult, AggSe import com.sksamuel.elastic4s.requests.searches.queries.compound.BoolQuery import com.sksamuel.elastic4s.requests.searches.queries.{Query, RangeQuery} import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.errors.{ValidationException, ValidationMessage} +import no.ndla.common.implicits.TryQuestionMark import no.ndla.common.model.NDLADate import no.ndla.common.model.domain.Priority import no.ndla.common.model.domain.draft.DraftStatus @@ -23,7 +25,7 @@ import no.ndla.search.AggregationBuilder.{buildTermsAggregation, getAggregations import no.ndla.search.Elastic4sClient import no.ndla.searchapi.Props import no.ndla.searchapi.model.api.{ErrorHelpers, SubjectAggregation, SubjectAggregations} -import no.ndla.searchapi.model.domain.SearchResult +import no.ndla.searchapi.model.domain.{LearningResourceType, SearchResult} import no.ndla.searchapi.model.search.SearchType import no.ndla.searchapi.model.search.settings.MultiDraftSearchSettings @@ -40,14 +42,23 @@ trait MultiDraftSearchService { with DraftIndexService with LearningPathIndexService with Props - with ErrorHelpers => + with ErrorHelpers + with DraftConceptIndexService => val multiDraftSearchService: MultiDraftSearchService class MultiDraftSearchService extends StrictLogging with SearchService with TaxonomyFiltering { - import props.{ElasticSearchIndexMaxResultWindow, ElasticSearchScrollKeepAlive, SearchIndexes} - override val searchIndex: List[String] = - List(SearchIndexes(SearchType.Drafts), SearchIndexes(SearchType.LearningPaths)) - override val indexServices: List[IndexService[_ <: Content]] = List(draftIndexService, learningPathIndexService) + import props.{ElasticSearchIndexMaxResultWindow, ElasticSearchScrollKeepAlive, SearchIndex} + override val searchIndex: List[String] = List( + SearchType.Drafts, + SearchType.LearningPaths, + SearchType.Concepts + ).map(SearchIndex) + + override val indexServices: List[IndexService[_ <: Content]] = List( + draftIndexService, + learningPathIndexService, + draftConceptIndexService + ) case class SumAggResult(value: Long) extends AggResult @@ -114,6 +125,34 @@ trait MultiDraftSearchService { .map(aggregations => SubjectAggregations(aggregations)) } + private def getSearchIndexes(settings: MultiDraftSearchSettings): Try[List[String]] = { + settings.resultTypes match { + case Some(list) if list.nonEmpty => + val idxs = list.map { st => + val index = SearchIndex(st) + val isValidIndex = searchIndex.contains(index) + + if (isValidIndex) Right(index) + else { + val validSearchTypes = searchIndex.traverse(props.indexToSearchType).getOrElse(List.empty) + val validTypesString = s"[${validSearchTypes.mkString("'", "','", "'")}]" + Left( + ValidationMessage( + "resultTypes", + s"Invalid result type for endpoint: '$st', expected one of: $validTypesString" + ) + ) + } + } + + val errors = idxs.collect { case Left(e) => e } + if (errors.nonEmpty) Failure(new ValidationException(s"Got invalid `resultTypes` for endpoint", errors)) + else Success(idxs.collect { case Right(i) => i }) + + case _ => Success(List(SearchType.Drafts, SearchType.LearningPaths).map(SearchIndex)) + } + } + def matchingQuery(settings: MultiDraftSearchSettings): Try[SearchResult] = { val contentSearch = settings.query.map(queryString => { @@ -138,7 +177,8 @@ trait MultiDraftSearchService { langQueryFunc("embedAttributes", 1), simpleStringQuery(queryString.underlying).field("authors", 1), simpleStringQuery(queryString.underlying).field("grepContexts.title", 1), - nestedQuery("contexts", boolQuery().should(termQuery("contexts.contextId", queryString.underlying))), + nestedQuery("contexts", boolQuery().should(termQuery("contexts.contextId", queryString.underlying))) + .ignoreUnmapped(true), idsQuery(queryString.underlying), nestedQuery("revisionMeta", simpleStringQuery(queryString.underlying).field("revisionMeta.note")) .ignoreUnmapped(true) @@ -195,7 +235,8 @@ trait MultiDraftSearchService { val aggregations = buildTermsAggregation(settings.aggregatePaths, indexServices.map(_.getMapping)) - val searchToExecute = search(searchIndex) + val index = getSearchIndexes(settings).? + val searchToExecute = search(index) .query(filteredSearch) .suggestions(suggestions(settings.query.underlying, searchLanguage, settings.fallback)) .trackTotalHits(true) @@ -279,9 +320,10 @@ trait MultiDraftSearchService { val articleTypeFilter = Some( boolQuery().should(settings.articleTypes.map(articleType => termQuery("articleType", articleType))) ) - val taxonomyContextFilter = contextTypeFilter(settings.learningResourceTypes) + val learningResourceType = learningResourceFilter(settings.learningResourceTypes) val taxonomyResourceTypesFilter = resourceTypeFilter(settings.resourceTypes, filterByNoResourceType = false) val taxonomySubjectFilter = subjectFilter(settings.subjects, settings.filterInactive) + val conceptSubjectFilter = subjectFilterForConcept(settings.subjects) val taxonomyTopicFilter = topicFilter(settings.topics, settings.filterInactive) val taxonomyRelevanceFilter = relevanceFilter(settings.relevanceIds, settings.subjects) val taxonomyContextActiveFilter = contextActiveFilter(settings.filterInactive) @@ -292,9 +334,9 @@ trait MultiDraftSearchService { articleTypeFilter, languageFilter, taxonomySubjectFilter, + conceptSubjectFilter, taxonomyTopicFilter, taxonomyResourceTypesFilter, - taxonomyContextFilter, taxonomyContextActiveFilter, supportedLanguageFilter, taxonomyRelevanceFilter, @@ -306,10 +348,22 @@ trait MultiDraftSearchService { publishedDateFilter, responsibleIdFilter, prioritizedFilter, - priorityFilter + priorityFilter, + learningResourceType ).flatten } + private def subjectFilterForConcept(subjectIds: List[String]): Option[Query] = { + Option.when(subjectIds.nonEmpty) { + mustBeNotConceptOr(termsQuery("subjectIds", subjectIds)) + } + } + + private def learningResourceFilter(types: List[LearningResourceType]): Option[Query] = + Option.when(types.nonEmpty)( + termsQuery("learningResourceType", types.map(_.entryName)) + ) + private def getRevisionHistoryLogQuery(queryString: String, excludeHistoryLog: Boolean): Seq[Query] = { Seq( simpleStringQuery(queryString).field("notes", 1) @@ -371,8 +425,14 @@ trait MultiDraftSearchService { learningPathIndexService.indexDocuments(shouldUsePublishedTax = true) } - handleScheduledIndexResults(SearchIndexes(SearchType.Drafts), draftFuture) - handleScheduledIndexResults(SearchIndexes(SearchType.LearningPaths), learningPathFuture) + val conceptFuture = Future { + requestInfo.setThreadContextRequestInfo() + draftConceptIndexService.indexDocuments(shouldUsePublishedTax = true) + } + + handleScheduledIndexResults(SearchIndex(SearchType.Drafts), draftFuture) + handleScheduledIndexResults(SearchIndex(SearchType.LearningPaths), learningPathFuture) + handleScheduledIndexResults(SearchIndex(SearchType.Concepts), conceptFuture) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiSearchService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiSearchService.scala index 9f3292f4c1..67d0f11bea 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiSearchService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/MultiSearchService.scala @@ -41,12 +41,10 @@ trait MultiSearchService { val multiSearchService: MultiSearchService class MultiSearchService extends StrictLogging with SearchService with TaxonomyFiltering { - import props.{ElasticSearchIndexMaxResultWindow, ElasticSearchScrollKeepAlive, SearchIndexes} + import props.{ElasticSearchIndexMaxResultWindow, ElasticSearchScrollKeepAlive, SearchIndex} - override val searchIndex: List[String] = - List(SearchIndexes(SearchType.Articles), SearchIndexes(SearchType.LearningPaths)) - override val indexServices: List[IndexService[_ <: Content]] = - List(articleIndexService, learningPathIndexService) + override val searchIndex: List[String] = List(SearchType.Articles, SearchType.LearningPaths).map(SearchIndex) + override val indexServices: List[IndexService[_ <: Content]] = List(articleIndexService, learningPathIndexService) def matchingQuery(settings: SearchSettings): Try[SearchResult] = { @@ -217,8 +215,8 @@ trait MultiSearchService { learningPathIndexService.indexDocuments(shouldUsePublishedTax = true) } - handleScheduledIndexResults(SearchIndexes(SearchType.Articles), articleFuture) - handleScheduledIndexResults(SearchIndexes(SearchType.LearningPaths), learningPathFuture) + handleScheduledIndexResults(SearchIndex(SearchType.Articles), articleFuture) + handleScheduledIndexResults(SearchIndex(SearchType.LearningPaths), learningPathFuture) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala index 0067a72251..c893dabe1d 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchConverterService.scala @@ -16,12 +16,14 @@ import no.ndla.common.implicits.* import no.ndla.common.model.api.{Author, License} import no.ndla.common.model.api.draft.Comment import no.ndla.common.model.domain.article.Article +import no.ndla.common.model.domain.concept.Concept import no.ndla.common.model.domain.draft.{Draft, RevisionStatus} import no.ndla.common.model.domain.{ ArticleContent, ArticleMetaImage, ArticleType, Priority, + Tag, VisualElement, ResourceType as MyNDLAResourceType } @@ -237,7 +239,8 @@ trait SearchConverterService { traits = traits.toList.distinct, embedAttributes = embedAttributes, embedResourcesAndIds = embedResourcesAndIds, - availability = ai.availability.toString + availability = ai.availability.toString, + learningResourceType = LearningResourceType.fromArticleType(ai.articleType) ) ) @@ -256,14 +259,7 @@ trait SearchConverterService { ) } - val favorited = (indexingBundle.myndlaBundle match { - case Some(value) => - Success(value.getFavorites(lp.id.get.toString, MyNDLAResourceType.Learningpath)) - case None => - myndlaapiClient - .getStatsFor(lp.id.get.toString, List(MyNDLAResourceType.Learningpath)) - .map(_.map(_.favourites).sum) - }).? + val favorited = getFavoritedCountFor(indexingBundle, lp.id.get.toString, List(MyNDLAResourceType.Learningpath)).? val supportedLanguages = getSupportedLanguages(lp.title, lp.description).toList val defaultTitle = lp.title.sortBy(title => ISO639.languagePriority.reverse.indexOf(title.language)).lastOption @@ -295,7 +291,63 @@ trait SearchConverterService { supportedLanguages = supportedLanguages, authors = lp.copyright.contributors.map(_.name).toList, contexts = asSearchableTaxonomyContexts(taxonomyContexts.getOrElse(List.empty)), - favorited = favorited + favorited = favorited, + learningResourceType = LearningResourceType.LearningPath + ) + ) + } + + private def getFavoritedCountFor( + indexingBundle: IndexingBundle, + id: String, + resourceTypes: List[MyNDLAResourceType] + ): Try[Long] = { + (indexingBundle.myndlaBundle match { + case Some(value) => Success(value.getFavorites(id, resourceTypes)) + case None => + myndlaapiClient + .getStatsFor(id, resourceTypes) + .map(_.map(_.favourites).sum) + }) + } + + def asSearchableConcept(c: Concept, indexingBundle: IndexingBundle): Try[SearchableConcept] = { + val title = SearchableLanguageValues.fromFields(c.title) + val content = SearchableLanguageValues.fromFieldsMap(c.content, toPlaintext) + val tags = SearchableLanguageList.fromFields(c.tags) + val favorited = getFavoritedCountFor(indexingBundle, c.id.get.toString, List(MyNDLAResourceType.Concept)).? + + val authors = ( + c.copyright.map(_.creators).toList ++ + c.copyright.map(_.processors).toList ++ + c.copyright.map(_.rightsholders).toList + ).flatten.map(_.name) + + val status = Status(c.status.current.toString, c.status.other.map(_.toString).toSeq) + + Success( + SearchableConcept( + id = c.id.get, + conceptType = c.conceptType.entryName, + title = title, + content = content, + defaultTitle = title.defaultValue, + metaImage = c.metaImage, + tags = tags, + subjectIds = c.subjectIds.toList, + lastUpdated = c.updated, + status = status, + updatedBy = c.updatedBy, + license = c.copyright.flatMap(_.license), + articleIds = c.articleIds, + created = c.created, + source = c.copyright.flatMap(_.origin), + responsible = c.responsible, + gloss = c.glossData.map(_.gloss), + domainObject = c, + authors = authors, + favorited = favorited, + learningResourceType = LearningResourceType.fromConceptType(c.conceptType) ) ) } @@ -421,7 +473,8 @@ trait SearchConverterService { resourceTypeName = sortableResourceTypeName, defaultResourceTypeName = sortableResourceTypeName.defaultValue, published = draft.published, - favorited = favorited + favorited = favorited, + learningResourceType = LearningResourceType.fromArticleType(draft.articleType) ) ) } @@ -559,7 +612,7 @@ trait SearchConverterService { url = url, contexts = contexts, supportedLanguages = supportedLanguages, - learningResourceType = searchableArticle.articleType, + learningResourceType = searchableArticle.learningResourceType, status = None, traits = searchableArticle.traits, score = hit.score, @@ -576,7 +629,9 @@ trait SearchConverterService { parentTopicName = None, primaryRootName = None, published = None, - favorited = None + favorited = None, + resultType = SearchType.Articles, + conceptSubjectIds = None ) } @@ -624,7 +679,7 @@ trait SearchConverterService { url = url, contexts = contexts, supportedLanguages = supportedLanguages, - learningResourceType = searchableDraft.articleType, + learningResourceType = searchableDraft.learningResourceType, status = Some(api.Status(searchableDraft.draftStatus.current, searchableDraft.draftStatus.other)), traits = searchableDraft.traits, score = hit.score, @@ -641,7 +696,9 @@ trait SearchConverterService { parentTopicName = parentTopicName, primaryRootName = primaryRootName, published = Some(searchableDraft.published), - favorited = Some(searchableDraft.favorited) + favorited = Some(searchableDraft.favorited), + resultType = SearchType.Drafts, + conceptSubjectIds = None ) } @@ -680,7 +737,7 @@ trait SearchConverterService { url = url, contexts = contexts, supportedLanguages = supportedLanguages, - learningResourceType = LearningResourceType.LearningPath.toString, + learningResourceType = LearningResourceType.LearningPath, status = Some(api.Status(searchableLearningPath.status, Seq.empty)), traits = List.empty, score = hit.score, @@ -697,7 +754,63 @@ trait SearchConverterService { parentTopicName = None, primaryRootName = None, published = None, - favorited = Some(searchableLearningPath.favorited) + favorited = Some(searchableLearningPath.favorited), + resultType = SearchType.LearningPaths, + conceptSubjectIds = None + ) + } + + def conceptHitAsMultiSummary(hit: SearchHit, language: String): MultiSearchSummary = { + val searchableConcept = CirceUtil.unsafeParseAs[SearchableConcept](hit.sourceAsString) + + val titles = searchableConcept.title.languageValues.map(lv => api.Title(lv.value, lv.language)) + + val content = searchableConcept.content.languageValues.map(lv => api.MetaDescription(lv.value, lv.language)) + val tags = searchableConcept.tags.languageValues.map(lv => Tag(lv.value, lv.language)) + + val supportedLanguages = getSupportedLanguages(titles, content, tags) + + val title = findByLanguageOrBestEffort(titles, language).getOrElse(api.Title("", UnknownLanguage.toString)) + val url = s"${props.ExternalApiUrls("concept-api")}/${searchableConcept.id}" + val metaImages = searchableConcept.domainObject.metaImage.map(image => { + val metaImageUrl = s"${props.ExternalApiUrls("raw-image")}/${image.imageId}" + api.MetaImage(metaImageUrl, image.altText, image.language) + }) + val metaImage = findByLanguageOrBestEffort(metaImages, language) + + val responsible = searchableConcept.responsible.map(r => api.DraftResponsible(r.responsibleId, r.lastUpdated)) + val metaDescription = findByLanguageOrBestEffort(content, language).getOrElse( + api.MetaDescription("", UnknownLanguage.toString) + ) + + MultiSearchSummary( + id = searchableConcept.id, + title = title, + metaDescription = metaDescription, + metaImage = metaImage, + url = url, + contexts = List.empty, + supportedLanguages = supportedLanguages, + learningResourceType = searchableConcept.learningResourceType, + status = Some(searchableConcept.status), + traits = List.empty, + score = hit.score, + highlights = getHighlights(hit.highlight), + paths = List.empty, + lastUpdated = searchableConcept.lastUpdated, + license = searchableConcept.license, + revisions = Seq.empty, + responsible = responsible, + comments = None, + prioritized = None, + priority = None, + resourceTypeName = None, + parentTopicName = None, + primaryRootName = None, + published = None, + favorited = Some(searchableConcept.favorited), + resultType = SearchType.Concepts, + conceptSubjectIds = Some(searchableConcept.subjectIds) ) } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchService.scala index a0740865dc..96f844f7a7 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/SearchService.scala @@ -50,17 +50,18 @@ trait SearchService { * api-model summary of hit */ private def hitToApiModel(hit: SearchHit, language: String, filterInactive: Boolean) = { - val articleType = props.SearchIndexes(SearchType.Articles) - val draftType = props.SearchIndexes(SearchType.Drafts) - val learningPathType = props.SearchIndexes(SearchType.LearningPaths) - - hit.index.split("_").headOption match { - case Some(`articleType`) => + val indexName = hit.index.split("_").headOption.traverse(x => props.indexToSearchType(x)) + indexName.flatMap { + case Some(SearchType.Articles) => Success(searchConverterService.articleHitAsMultiSummary(hit, language, filterInactive)) - case Some(`draftType`) => Success(searchConverterService.draftHitAsMultiSummary(hit, language, filterInactive)) - case Some(`learningPathType`) => + case Some(SearchType.Drafts) => + Success(searchConverterService.draftHitAsMultiSummary(hit, language, filterInactive)) + case Some(SearchType.LearningPaths) => Success(searchConverterService.learningpathHitAsMultiSummary(hit, language, filterInactive)) - case _ => Failure(NdlaSearchException("Index type was bad when determining search result type.")) + case Some(SearchType.Concepts) => + Success(searchConverterService.conceptHitAsMultiSummary(hit, language)) + case None => + Failure(NdlaSearchException("Index type was bad when determining search result type.")) } } @@ -119,7 +120,7 @@ trait SearchService { nestedQuery( "embedResourcesAndIds", boolQuery().must(buildTermQueryForEmbed("embedResourcesAndIds", resource, id, language, fallback)) - ) + ).ignoreUnmapped(true) ) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/TaxonomyFiltering.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/TaxonomyFiltering.scala index e5d4ec223f..7fb5aeb787 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/TaxonomyFiltering.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/TaxonomyFiltering.scala @@ -6,13 +6,39 @@ */ package no.ndla.searchapi.service.search -import com.sksamuel.elastic4s.ElasticDsl._ +import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.requests.searches.queries.{NestedQuery, Query} import com.sksamuel.elastic4s.requests.searches.queries.compound.BoolQuery import no.ndla.searchapi.model.domain.LearningResourceType trait TaxonomyFiltering { + private val notConceptType = LearningResourceType.values + .filter(x => x != LearningResourceType.Concept && x != LearningResourceType.Gloss) + .map(_.entryName) + + private val mustBeConceptQuery = termsQuery( + "learningResourceType", + Seq(LearningResourceType.Concept.entryName, LearningResourceType.Gloss.entryName) + ) + + private val mustNotBeConceptQuery = termsQuery("learningResourceType", notConceptType) + + def mustBeConceptOr(query: Query): Query = { + val newQuery = query match { + case nested: NestedQuery if nested.path == "contexts" => nested.ignoreUnmapped(true) + case query => query + } + boolQuery().should(newQuery, mustBeConceptQuery) + } + def mustBeNotConceptOr(query: Query): Query = { + val newQuery = query match { + case nested: NestedQuery if nested.path == "contexts" => nested.ignoreUnmapped(true) + case query => query + } + boolQuery().should(newQuery, mustNotBeConceptQuery) + } + protected def relevanceFilter(relevanceIds: List[String], subjectIds: List[String]): Option[BoolQuery] = if (relevanceIds.isEmpty) None else @@ -33,26 +59,36 @@ trait TaxonomyFiltering { private val booleanMust: (String, String) => BoolQuery = (field: String, id: String) => boolQuery().must(termQuery(field, id)) - protected def subjectFilter(subjects: List[String], filterInactive: Boolean): Option[NestedQuery] = + protected def subjectFilter(subjects: List[String], filterInactive: Boolean): Option[Query] = if (subjects.isEmpty) None else { val subjectQueries = subjects.map(subjectId => if (filterInactive) - boolQuery().must(booleanMust("contexts.rootId", subjectId), booleanMust("contexts.isActive", "true")) + boolQuery().must( + booleanMust("contexts.rootId", subjectId), + booleanMust("contexts.isActive", "true") + ) else booleanMust("contexts.rootId", subjectId) ) - Some(nestedQuery("contexts", boolQuery().should(subjectQueries))) + Some( + mustBeConceptOr( + nestedQuery("contexts", boolQuery().should(subjectQueries)).ignoreUnmapped(true) + ) + ) } - protected def topicFilter(topics: List[String], filterInactive: Boolean): Option[NestedQuery] = + protected def topicFilter(topics: List[String], filterInactive: Boolean): Option[Query] = if (topics.isEmpty) None else { val subjectQueries = topics.map(subjectId => if (filterInactive) - boolQuery().must(booleanMust("contexts.parentIds", subjectId), booleanMust("contexts.isActive", "true")) + boolQuery().must( + booleanMust("contexts.parentIds", subjectId), + booleanMust("contexts.isActive", "true") + ) else booleanMust("contexts.parentIds", subjectId) ) - Some(nestedQuery("contexts", boolQuery().should(subjectQueries))) + Some(mustBeConceptOr(nestedQuery("contexts", boolQuery().should(subjectQueries)).ignoreUnmapped(true))) } protected def resourceTypeFilter(resourceTypes: List[String], filterByNoResourceType: Boolean): Option[Query] = { @@ -76,18 +112,18 @@ trait TaxonomyFiltering { } } - protected def contextTypeFilter(contextTypes: List[LearningResourceType.Value]): Option[BoolQuery] = + protected def contextTypeFilter(contextTypes: List[LearningResourceType]): Option[BoolQuery] = if (contextTypes.isEmpty) None else { val taxonomyContextQuery = - contextTypes.map(ct => nestedQuery("contexts", termQuery("contexts.contextType", ct.toString))) + contextTypes.map(ct => nestedQuery("contexts", termQuery("contexts.contextType", ct.entryName))) Some(boolQuery().should(taxonomyContextQuery)) } protected def contextActiveFilter(filterInactive: Boolean): Option[Query] = if (filterInactive) { - val contextActiveQuery = nestedQuery("contexts", termQuery("contexts.isActive", true)) - Some(contextActiveQuery) + val contextActiveQuery = nestedQuery("contexts", termQuery("contexts.isActive", true)).ignoreUnmapped(true) + Some(mustBeConceptOr(contextActiveQuery)) } else None } diff --git a/search-api/src/test/scala/no/ndla/searchapi/TestData.scala b/search-api/src/test/scala/no/ndla/searchapi/TestData.scala index f9f5c1a12d..8e3d43fbef 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/TestData.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/TestData.scala @@ -26,6 +26,17 @@ import no.ndla.common.model.domain.{ draft } import no.ndla.common.model.domain.article.{Article, Copyright} +import no.ndla.common.model.domain.concept.{ + Concept, + ConceptContent, + ConceptEditorNote, + ConceptMetaImage, + ConceptStatus, + ConceptType, + GlossData, + GlossExample, + WordClass +} import no.ndla.common.model.domain.draft.{Draft, DraftCopyright, DraftStatus, RevisionMeta, RevisionStatus} import no.ndla.common.model.domain.learningpath.LearningpathCopyright import no.ndla.common.model.{NDLADate, domain as common} @@ -1621,7 +1632,8 @@ object TestData { prioritized = None, priority = List.empty, publishedFilterFrom = None, - publishedFilterTo = None + publishedFilterTo = None, + resultTypes = None ) val searchableResourceTypes: List[SearchableTaxonomyResourceType] = List( @@ -1752,6 +1764,60 @@ object TestData { resourceTypeName = searchableTitles, defaultResourceTypeName = searchableTitles.defaultValue, published = TestData.today, - favorited = 0 + favorited = 0, + learningResourceType = LearningResourceType.Article ) + + val sampleNbDomainConcept: Concept = Concept( + id = Some(1), + revision = Some(1), + title = Seq(common.Title("Tittel", "nb")), + content = Seq(ConceptContent("Innhold", "nb")), + copyright = None, + created = today, + updated = today, + updatedBy = Seq("noen"), + metaImage = Seq(ConceptMetaImage("1", "Hei", "nb")), + tags = Seq(common.Tag(Seq("stor", "kaktus"), "nb")), + subjectIds = Set("urn:subject:3", "urn:subject:4"), + articleIds = Seq(42), + status = no.ndla.common.model.domain.concept.Status( + ConceptStatus.LANGUAGE, + Set(ConceptStatus.PUBLISHED) + ), + visualElement = Seq( + no.ndla.common.model.domain.concept.VisualElement( + s"""<$EmbedTagName data-caption="some capt" data-align="" data-resource_id="1" data-resource="image" data-alt="some alt" data-size="full" />""", + "nb" + ) + ), + responsible = Some(Responsible("some-id", today)), + conceptType = ConceptType.CONCEPT, + glossData = Some( + GlossData( + gloss = "hei", + wordClass = WordClass.TIME_WORD, + originalLanguage = "nb", + transcriptions = Map("pling" -> "plong"), + examples = List( + List( + GlossExample( + example = "hei", + language = "nb", + transcriptions = Map("nb" -> "lai") + ) + ) + ) + ) + ), + editorNotes = Seq( + ConceptEditorNote( + note = "hei", + user = "some-id", + status = no.ndla.common.model.domain.concept.Status(ConceptStatus.LANGUAGE, Set(ConceptStatus.PUBLISHED)), + timestamp = today + ) + ) + ) + } diff --git a/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala b/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala index 2fd9e8acbf..e8cce6a2bd 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala @@ -15,9 +15,9 @@ import no.ndla.network.clients.{FeideApiClient, RedisClient} import no.ndla.network.tapir.{NdlaMiddleware, Routes, Service} import no.ndla.search.{BaseIndexService, Elastic4sClient} import no.ndla.searchapi.controller.{InternController, SearchController} -import no.ndla.searchapi.integration._ +import no.ndla.searchapi.integration.* import no.ndla.searchapi.model.api.ErrorHelpers -import no.ndla.searchapi.service.search._ +import no.ndla.searchapi.service.search.* import no.ndla.searchapi.service.ConverterService import org.scalatestplus.mockito.MockitoSugar @@ -27,6 +27,8 @@ trait TestEnvironment with ArticleIndexService with MultiSearchService with DraftIndexService + with DraftConceptApiClient + with DraftConceptIndexService with MultiDraftSearchService with ConverterService with DraftApiClient @@ -68,18 +70,22 @@ trait TestEnvironment val draftApiClient: DraftApiClient = mock[DraftApiClient] val learningPathApiClient: LearningPathApiClient = mock[LearningPathApiClient] val articleApiClient: ArticleApiClient = mock[ArticleApiClient] + val draftConceptApiClient: DraftConceptApiClient = mock[DraftConceptApiClient] val feideApiClient: FeideApiClient = mock[FeideApiClient] val redisClient: RedisClient = mock[RedisClient] val clock: SystemClock = mock[SystemClock] - val converterService: ConverterService = mock[ConverterService] - val searchConverterService: SearchConverterService = mock[SearchConverterService] - val multiSearchService: MultiSearchService = mock[MultiSearchService] + val converterService: ConverterService = mock[ConverterService] + val searchConverterService: SearchConverterService = mock[SearchConverterService] + val multiSearchService: MultiSearchService = mock[MultiSearchService] + val articleIndexService: ArticleIndexService = mock[ArticleIndexService] val learningPathIndexService: LearningPathIndexService = mock[LearningPathIndexService] val draftIndexService: DraftIndexService = mock[DraftIndexService] - val multiDraftSearchService: MultiDraftSearchService = mock[MultiDraftSearchService] + val draftConceptIndexService: DraftConceptIndexService = mock[DraftConceptIndexService] + + val multiDraftSearchService: MultiDraftSearchService = mock[MultiDraftSearchService] override def services: List[Service[Eff]] = List() } diff --git a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableArticleTest.scala b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableArticleTest.scala index 43cd981ee0..5ac2f869f3 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableArticleTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableArticleTest.scala @@ -82,7 +82,8 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { traits = List.empty, embedAttributes = embedAttrs, embedResourcesAndIds = embedResourcesAndIds, - availability = "everyone" + availability = "everyone", + learningResourceType = LearningResourceType.Article ) val json = CirceUtil.toJsonString(original) val deserialized = CirceUtil.unsafeParseAs[SearchableArticle](json) @@ -157,7 +158,8 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { traits = List.empty, embedAttributes = embedAttrs, embedResourcesAndIds = embedResourcesAndIds, - availability = "everyone" + availability = "everyone", + learningResourceType = LearningResourceType.Article ) val json = CirceUtil.toJsonString(original) diff --git a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableDraftTest.scala b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableDraftTest.scala index 55b0baf2a2..6dcc3f1bf6 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableDraftTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableDraftTest.scala @@ -130,7 +130,8 @@ class SearchableDraftTest extends UnitSuite with TestEnvironment { resourceTypeName = titles, defaultResourceTypeName = titles.defaultValue, published = TestData.today, - favorited = 0 + favorited = 0, + learningResourceType = LearningResourceType.Article ) val json = CirceUtil.toJsonString(original) diff --git a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableLearningPathTest.scala b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableLearningPathTest.scala index 2bea7c0aed..699dd1fec2 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableLearningPathTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/model/search/SearchableLearningPathTest.scala @@ -14,6 +14,7 @@ import no.ndla.searchapi.model.api.learningpath.Copyright import no.ndla.searchapi.model.domain.learningpath.{LearningPathStatus, LearningPathVerificationStatus, StepType} import no.ndla.searchapi.{TestData, TestEnvironment, UnitSuite} import no.ndla.searchapi.TestData.* +import no.ndla.searchapi.model.domain.LearningResourceType class SearchableLearningPathTest extends UnitSuite with TestEnvironment { @@ -63,7 +64,8 @@ class SearchableLearningPathTest extends UnitSuite with TestEnvironment { authors = List("Yap"), contexts = searchableTaxonomyContexts, license = "by-sa", - favorited = 0 + favorited = 0, + learningResourceType = LearningResourceType.LearningPath ) val json = CirceUtil.toJsonString(original) diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/ArticleIndexServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/ArticleIndexServiceTest.scala index 3038eeb170..94205fabe2 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/ArticleIndexServiceTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/ArticleIndexServiceTest.scala @@ -17,7 +17,7 @@ import no.ndla.search.TestUtility.{getFields, getMappingFields} import no.ndla.search.model.domain.EmbedValues import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} import no.ndla.searchapi.TestData.* -import no.ndla.searchapi.model.domain.IndexingBundle +import no.ndla.searchapi.model.domain.{IndexingBundle, LearningResourceType} import no.ndla.searchapi.model.search.{SearchableArticle, SearchableGrepContext} import no.ndla.searchapi.{TestData, TestEnvironment, UnitSuite} @@ -163,7 +163,8 @@ class ArticleIndexServiceTest SearchableGrepContext("KE12", None), SearchableGrepContext("KM123", None), SearchableGrepContext("TT2", None) - ) + ), + learningResourceType = LearningResourceType.Article ) val searchableFields = searchableToTestWith.asJson diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/DraftConceptIndexServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/DraftConceptIndexServiceTest.scala new file mode 100644 index 0000000000..97d1b8846d --- /dev/null +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/DraftConceptIndexServiceTest.scala @@ -0,0 +1,100 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2024 NDLA + * + * See LICENSE + */ + +package no.ndla.searchapi.service.search + +import io.circe.syntax.* +import no.ndla.common.model.NDLADate +import no.ndla.common.model.domain.Responsible +import no.ndla.common.model.domain.concept.ConceptMetaImage +import no.ndla.scalatestsuite.IntegrationSuite +import no.ndla.search.TestUtility.{getFields, getMappingFields} +import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} +import no.ndla.searchapi.model.api +import no.ndla.searchapi.model.domain.LearningResourceType +import no.ndla.searchapi.model.search.SearchableConcept +import no.ndla.searchapi.{TestData, TestEnvironment, UnitSuite} + +import scala.util.Failure + +class DraftConceptIndexServiceTest + extends IntegrationSuite(EnableElasticsearchContainer = true) + with UnitSuite + with TestEnvironment { + + e4sClient = Elastic4sClientFactory.getClient(elasticSearchHost.getOrElse("")) + // Skip tests if no docker environment available + override def withFixture(test: NoArgTest) = { + elasticSearchContainer match { + case Failure(ex) => + println(s"Elasticsearch container not running, cancelling '${this.getClass.getName}'") + println(s"Got exception: ${ex.getMessage}") + ex.printStackTrace() + case _ => + } + + assume(elasticSearchContainer.isSuccess) + super.withFixture(test) + } + + override val draftConceptIndexService: DraftConceptIndexService = new DraftConceptIndexService { + override val indexShards = 1 + } + + override def beforeEach(): Unit = { + super.beforeEach() + articleIndexService.deleteIndexAndAlias() + articleIndexService.createIndexWithGeneratedName + } + + override val converterService = new ConverterService + override val searchConverterService = new SearchConverterService + + test("That mapping contains every field after serialization") { + val languageValues = SearchableLanguageValues(Seq(LanguageValue("nb", "hei"), LanguageValue("en", "hå"))) + val languageList = SearchableLanguageList(Seq(LanguageValue("nb", Seq("")), LanguageValue("en", Seq("")))) + val now = NDLADate.now() + + val searchableToTestWith = SearchableConcept( + id = 1, + conceptType = "concept", + title = languageValues, + content = languageValues, + metaImage = Seq(ConceptMetaImage("1", "alt", "nb")), + defaultTitle = Some("hei"), + tags = languageList, + subjectIds = List("urn:subject:1"), + lastUpdated = now, + status = api.Status("IN_PROGRESS", Seq("PUBLISHED")), + updatedBy = Seq("noen"), + license = Some("CC-BY-SA-4.0"), + authors = List("Noen Kule"), + articleIds = Seq(1, 2, 3), + created = now, + source = Some("heidu"), + responsible = Some(Responsible("some-id", now)), + gloss = Some("hei"), + domainObject = TestData.sampleNbDomainConcept, + favorited = 0, + learningResourceType = LearningResourceType.Concept + ) + val searchableFields = searchableToTestWith.asJson + val fields = getFields(searchableFields, None, Seq("domainObject")) + val mapping = draftConceptIndexService.getMapping + + val staticMappingFields = getMappingFields(mapping.properties, None) + val dynamicMappingFields = mapping.templates.map(_.name) + for (field <- fields) { + val hasStatic = staticMappingFields.contains(field) + val hasDynamic = dynamicMappingFields.contains(field) + + if (!(hasStatic || hasDynamic)) { + fail(s"'$field' was not found in mapping, i think you would want to add it to the index mapping?") + } + } + } +} diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceAtomicTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceAtomicTest.scala index 10dde6ea86..f791cd8cb3 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceAtomicTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceAtomicTest.scala @@ -13,6 +13,7 @@ import no.ndla.common.CirceUtil import no.ndla.common.configuration.Constants.EmbedTagName import no.ndla.common.model.NDLADate import no.ndla.common.model.domain.* +import no.ndla.common.model.domain.concept.{ConceptContent, ConceptType} import no.ndla.common.model.domain.draft.{Draft, DraftStatus, RevisionMeta, RevisionStatus} import no.ndla.common.model.domain.{EditorNote, Priority, Responsible} import no.ndla.network.tapir.NonEmptyString @@ -20,7 +21,8 @@ import no.ndla.scalatestsuite.IntegrationSuite import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} import no.ndla.searchapi.TestData.* import no.ndla.searchapi.model.api.ApiTaxonomyContext -import no.ndla.searchapi.model.domain.{IndexingBundle, Sort} +import no.ndla.searchapi.model.domain.{IndexingBundle, LearningResourceType, Sort} +import no.ndla.searchapi.model.search.SearchType import no.ndla.searchapi.model.taxonomy.* import no.ndla.searchapi.{TestData, TestEnvironment} import org.scalatest.Outcome @@ -55,15 +57,19 @@ class MultiDraftSearchServiceAtomicTest override val learningPathIndexService: LearningPathIndexService = new LearningPathIndexService { override val indexShards = 1 } + override val draftConceptIndexService: DraftConceptIndexService = new DraftConceptIndexService { + override val indexShards = 1 + } override val multiDraftSearchService = new MultiDraftSearchService override val converterService = new ConverterService override val searchConverterService = new SearchConverterService override def beforeEach(): Unit = { if (elasticSearchContainer.isSuccess) { - articleIndexService.createIndexAndAlias() - draftIndexService.createIndexAndAlias() - learningPathIndexService.createIndexAndAlias() + draftConceptIndexService.createIndexAndAlias().get + articleIndexService.createIndexAndAlias().get + draftIndexService.createIndexAndAlias().get + learningPathIndexService.createIndexAndAlias().get } } @@ -72,6 +78,7 @@ class MultiDraftSearchServiceAtomicTest articleIndexService.deleteIndexAndAlias() draftIndexService.deleteIndexAndAlias() learningPathIndexService.deleteIndexAndAlias() + draftConceptIndexService.deleteIndexAndAlias() } } @@ -1062,4 +1069,298 @@ class MultiDraftSearchServiceAtomicTest .map(_.id) should be(Seq(1, 3, 2, 4)) } + test("Test that concepts appear in the search, but not by default") { + val draft1 = TestData.draft1.copy( + id = Some(1) + ) + val draft2 = TestData.draft1.copy( + id = Some(2) + ) + val draft3 = TestData.draft1.copy( + id = Some(3) + ) + val draft4 = TestData.draft1.copy( + id = Some(4) + ) + + val concept1 = TestData.sampleNbDomainConcept.copy( + id = Some(1) + ) + val concept2 = TestData.sampleNbDomainConcept.copy( + id = Some(2) + ) + val concept3 = TestData.sampleNbDomainConcept.copy( + id = Some(3) + ) + val concept4 = TestData.sampleNbDomainConcept.copy( + id = Some(4) + ) + draftIndexService.indexDocument(draft1, indexingBundle).get + draftIndexService.indexDocument(draft2, indexingBundle).get + draftIndexService.indexDocument(draft3, indexingBundle).get + draftIndexService.indexDocument(draft4, indexingBundle).get + draftConceptIndexService.indexDocument(concept1, indexingBundle).get + draftConceptIndexService.indexDocument(concept2, indexingBundle).get + draftConceptIndexService.indexDocument(concept3, indexingBundle).get + draftConceptIndexService.indexDocument(concept4, indexingBundle).get + + blockUntil(() => draftIndexService.countDocuments == 4 && draftConceptIndexService.countDocuments == 4) + + multiDraftSearchService + .matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByIdAsc)) + .get + .results + .map(r => r.id -> r.resultType) should be( + Seq( + 1 -> SearchType.Drafts, + 2 -> SearchType.Drafts, + 3 -> SearchType.Drafts, + 4 -> SearchType.Drafts + ) + ) + + multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + sort = Sort.ByIdAsc, + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts)) + ) + ) + .get + .results + .map(r => r.id -> r.resultType) should be( + Seq( + 1 -> SearchType.Concepts, + 1 -> SearchType.Drafts, + 2 -> SearchType.Concepts, + 2 -> SearchType.Drafts, + 3 -> SearchType.Concepts, + 3 -> SearchType.Drafts, + 4 -> SearchType.Concepts, + 4 -> SearchType.Drafts + ) + ) + } + test("that concepts are indexed with content and are searchable") { + val draft1 = TestData.draft1.copy( + id = Some(1) + ) + val draft2 = TestData.draft1.copy( + id = Some(2) + ) + val draft3 = TestData.draft1.copy( + id = Some(3) + ) + + val concept1 = TestData.sampleNbDomainConcept.copy( + id = Some(1), + content = Seq(ConceptContent("Liten apekatt", "nb")) + ) + val concept2 = TestData.sampleNbDomainConcept.copy( + id = Some(2), + content = Seq(ConceptContent("Stor giraff", "nb")) + ) + val concept3 = TestData.sampleNbDomainConcept.copy( + id = Some(3), + content = Seq(ConceptContent("Medium kylling", "nb")) + ) + draftIndexService.indexDocument(draft1, indexingBundle).get + draftIndexService.indexDocument(draft2, indexingBundle).get + draftIndexService.indexDocument(draft3, indexingBundle).get + draftConceptIndexService.indexDocument(concept1, indexingBundle).get + draftConceptIndexService.indexDocument(concept2, indexingBundle).get + draftConceptIndexService.indexDocument(concept3, indexingBundle).get + + blockUntil(() => draftIndexService.countDocuments == 3 && draftConceptIndexService.countDocuments == 3) + + multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + sort = Sort.ByIdAsc, + query = NonEmptyString.fromString("giraff"), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + .results + .map(r => r.id -> r.resultType) should be( + Seq(2 -> SearchType.Concepts) + ) + + multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + sort = Sort.ByIdAsc, + query = NonEmptyString.fromString("apekatt"), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + .results + .map(r => r.id -> r.resultType) should be( + Seq(1 -> SearchType.Concepts) + ) + } + + test("That filtering based on learningResourceType works for everyone") { + val draft1 = TestData.draft1.copy( + id = Some(1), + articleType = ArticleType.Standard + ) + val draft2 = TestData.draft1.copy( + id = Some(2), + articleType = ArticleType.TopicArticle + ) + val learningPath3 = TestData.learningPath1.copy( + id = Some(3) + ) + val concept4 = TestData.sampleNbDomainConcept.copy( + id = Some(4), + conceptType = ConceptType.CONCEPT + ) + val concept5 = TestData.sampleNbDomainConcept.copy( + id = Some(5), + conceptType = ConceptType.GLOSS + ) + + draftIndexService.indexDocument(draft1, indexingBundle).get + draftIndexService.indexDocument(draft2, indexingBundle).get + learningPathIndexService.indexDocument(learningPath3, indexingBundle).get + draftConceptIndexService.indexDocument(concept4, indexingBundle).get + draftConceptIndexService.indexDocument(concept5, indexingBundle).get + + blockUntil(() => + draftIndexService.countDocuments == 2 && + learningPathIndexService.countDocuments == 1 && + draftConceptIndexService.countDocuments == 2 + ) + + { + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + learningResourceTypes = List(LearningResourceType.Article), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + search.results.map(_.id) should be(Seq(1)) + } + { + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + learningResourceTypes = List(LearningResourceType.TopicArticle), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + search.results.map(_.id) should be(Seq(2)) + } + { + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + learningResourceTypes = List(LearningResourceType.LearningPath), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + search.results.map(_.id) should be(Seq(3)) + } + { + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + learningResourceTypes = List(LearningResourceType.Concept), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + search.results.map(_.id) should be(Seq(4)) + } + { + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + learningResourceTypes = List(LearningResourceType.Gloss), + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)) + ) + ) + .get + search.results.map(_.id) should be(Seq(5)) + } + } + + test("That responsible filtering works for concepts") { + val responsible = Responsible("some-user", TestData.today) + val draft1 = TestData.draft1.copy( + id = Some(1), + articleType = ArticleType.Standard, + responsible = Some(responsible) + ) + val draft2 = TestData.draft1.copy( + id = Some(2), + articleType = ArticleType.Standard, + responsible = None + ) + val concept3 = TestData.sampleNbDomainConcept.copy( + id = Some(3), + conceptType = ConceptType.CONCEPT, + responsible = Some(responsible) + ) + + draftIndexService.indexDocument(draft1, indexingBundle).get + draftIndexService.indexDocument(draft2, indexingBundle).get + draftConceptIndexService.indexDocument(concept3, indexingBundle).get + + blockUntil(() => draftIndexService.countDocuments == 2 && draftConceptIndexService.countDocuments == 1) + + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)), + responsibleIdFilter = List("some-user") + ) + ) + .get + search.results.map(_.id) should be(Seq(1, 3)) + } + + test("That subject filtering works for concepts") { + val responsible = Responsible("some-user", TestData.today) + val draft1 = TestData.draft1.copy( + id = Some(1), + articleType = ArticleType.Standard, + responsible = Some(responsible) + ) + val draft2 = TestData.draft1.copy( + id = Some(2), + articleType = ArticleType.Standard, + responsible = None + ) + val concept3 = TestData.sampleNbDomainConcept.copy( + id = Some(3), + conceptType = ConceptType.CONCEPT, + responsible = Some(responsible), + subjectIds = Set("urn:subject:1000") + ) + + draftIndexService.indexDocument(draft1, indexingBundle).get + draftIndexService.indexDocument(draft2, indexingBundle).get + draftConceptIndexService.indexDocument(concept3, indexingBundle).get + + blockUntil(() => draftIndexService.countDocuments == 2 && draftConceptIndexService.countDocuments == 1) + + val search = multiDraftSearchService + .matchingQuery( + multiDraftSearchSettings.copy( + resultTypes = Some(List(SearchType.Drafts, SearchType.Concepts, SearchType.LearningPaths)), + subjects = List("urn:subject:1000"), + filterInactive = true + ) + ) + .get + search.results.map(_.id) should be(Seq(3)) + } } diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceTest.scala index 3a1154a1c5..b034c408a4 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiDraftSearchServiceTest.scala @@ -48,6 +48,9 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo override val learningPathIndexService: LearningPathIndexService = new LearningPathIndexService { override val indexShards = 1 } + override val draftConceptIndexService: DraftConceptIndexService = new DraftConceptIndexService { + override val indexShards = 1 + } override val multiDraftSearchService = new MultiDraftSearchService override val converterService = new ConverterService override val searchConverterService = new SearchConverterService @@ -60,6 +63,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo if (elasticSearchContainer.isSuccess) { draftIndexService.createIndexAndAlias() learningPathIndexService.createIndexAndAlias() + draftConceptIndexService.createIndexAndAlias() draftsToIndex.map(draft => draftIndexService.indexDocument(draft, indexingBundle)) @@ -523,8 +527,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) ) - search.totalCount should be(12) - search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 15)) + search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15)) + search.totalCount should be(13) } test("That filtering out inactive contexts works as expected") { @@ -562,8 +566,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo search.totalCount should be(7) search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) - search2.totalCount should be(5) - search2.results.map(_.id) should be(Seq(8, 9, 10, 11, 15)) + search2.totalCount should be(6) + search2.results.map(_.id) should be(Seq(8, 9, 10, 11, 13, 15)) } test("That filtering on article-type works") { @@ -587,13 +591,13 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo search3.results.map(_.id) should be(Seq(16)) } - test("That filtering on learningpath learningresourcetype returns learningpaths in structure") { + test("That filtering on learningpath learningresourcetype returns learningpaths") { val Success(search) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = "*", learningResourceTypes = List(LearningResourceType.LearningPath)) ) - search.totalCount should be(5) - search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5)) + search.totalCount should be(6) + search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6)) search.results.map(_.url.contains("learningpath")).distinct should be(Seq(true)) } diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchServiceTest.scala index 92c2ba9170..ba520a4e2a 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchServiceTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchServiceTest.scala @@ -11,7 +11,6 @@ import no.ndla.searchapi.{TestEnvironment, UnitSuite} import no.ndla.searchapi.model.search.SearchType class SearchServiceTest extends UnitSuite with TestEnvironment { - import props.SearchIndexes override val draftIndexService: DraftIndexService = new DraftIndexService { override val indexShards = 1 @@ -21,7 +20,7 @@ class SearchServiceTest extends UnitSuite with TestEnvironment { } val service: SearchService = new SearchService { - override val searchIndex = List(SearchIndexes(SearchType.Drafts), SearchIndexes(SearchType.LearningPaths)) + override val searchIndex = List(SearchType.Drafts, SearchType.LearningPaths).map(props.SearchIndex) override val indexServices: List[IndexService[_]] = List(draftIndexService, learningPathIndexService) override protected def scheduleIndexDocuments(): Unit = {} } diff --git a/typescript/types-backend/search-api.ts b/typescript/types-backend/search-api.ts index 2dc7a46150..372d907d1c 100644 --- a/typescript/types-backend/search-api.ts +++ b/typescript/types-backend/search-api.ts @@ -101,6 +101,7 @@ export interface IDraftSearchParams { topics?: string[] publishedDateFrom?: string publishedDateTo?: string + resultTypes?: SearchType[] } export interface IGroupSearchResult { @@ -197,7 +198,7 @@ export interface IMultiSearchSummary { url: string contexts: IApiTaxonomyContext[] supportedLanguages: string[] - learningResourceType: string + learningResourceType: LearningResourceType status?: IStatus traits: string[] score: number @@ -215,6 +216,8 @@ export interface IMultiSearchSummary { primaryRootName?: string published?: string favorited?: number + resultType: SearchType + conceptSubjectIds?: string[] } export interface IMultiSearchTermsAggregation { @@ -296,3 +299,7 @@ export interface IValidationMessage { field: string message: string } + +export type LearningResourceType = ("standard" | "topic-article" | "frontpage-article" | "learningpath" | "concept" | "gloss") + +export type SearchType = ("article" | "draft" | "learningpath" | "concept")