diff --git a/article-api/src/main/scala/no/ndla/articleapi/ArticleApiProperties.scala b/article-api/src/main/scala/no/ndla/articleapi/ArticleApiProperties.scala index 2c25984a68..121058e4ab 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/ArticleApiProperties.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/ArticleApiProperties.scala @@ -107,12 +107,6 @@ class ArticleApiProperties extends BaseProps with DatabaseProps { ).getOrElse(Environment, "https://h5p.ndla.no") ) - def ndlaFrontendUrl: String = Environment match { - case "local" => "http://localhost:30017" - case "prod" => "https://ndla.no" - case _ => s"https://$Environment.ndla.no" - } - private def BrightcoveAccountId: String = prop("NDLA_BRIGHTCOVE_ACCOUNT_ID") private def BrightcovePlayerId: String = prop("NDLA_BRIGHTCOVE_PLAYER_ID") diff --git a/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala b/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala index 2e5c6f0c8c..6db25c57c9 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/ComponentRegistry.scala @@ -35,7 +35,7 @@ import no.ndla.database.{DBMigrator, DBUtility, DataSource} import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication import no.ndla.network.clients.{FeideApiClient, RedisClient} -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} class ComponentRegistry(properties: ArticleApiProperties) extends BaseComponentRegistry[ArticleApiProperties] @@ -52,6 +52,7 @@ class ComponentRegistry(properties: ArticleApiProperties) with ArticleSearchService with IndexService with BaseIndexService + with SearchLanguage with ArticleIndexService with SearchService with ConverterService diff --git a/article-api/src/main/scala/no/ndla/articleapi/service/search/ArticleIndexService.scala b/article-api/src/main/scala/no/ndla/articleapi/service/search/ArticleIndexService.scala index 066f7995c8..8c8615039d 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/service/search/ArticleIndexService.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/service/search/ArticleIndexService.scala @@ -47,14 +47,14 @@ trait ArticleIndexService { ), keywordField("grepCodes") ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("visualElement") ++ - generateLanguageSupportedDynamicTemplates("introduction") ++ - generateLanguageSupportedDynamicTemplates("metaDescription") ++ - generateLanguageSupportedDynamicTemplates("tags") - - properties(fields).dynamicTemplates(dynamics) + val dynamics = generateLanguageSupportedFieldList("title", keepRaw = true) ++ + generateLanguageSupportedFieldList("content") ++ + generateLanguageSupportedFieldList("visualElement") ++ + generateLanguageSupportedFieldList("introduction") ++ + generateLanguageSupportedFieldList("metaDescription") ++ + generateLanguageSupportedFieldList("tags") + + properties(fields ++ dynamics) } } diff --git a/article-api/src/main/scala/no/ndla/articleapi/service/search/IndexService.scala b/article-api/src/main/scala/no/ndla/articleapi/service/search/IndexService.scala index 2e44143dc8..45363dfde0 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/service/search/IndexService.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/service/search/IndexService.scala @@ -12,20 +12,17 @@ import cats.implicits.* import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.requests.indexes.IndexRequest -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.articleapi.Props import no.ndla.articleapi.repository.ArticleRepository import no.ndla.common.model.domain.article.Article -import no.ndla.search.SearchLanguage.languageAnalyzers import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} -import scala.collection.mutable.ListBuffer import scala.util.{Failure, Success, Try} trait IndexService { - this: Elastic4sClient & BaseIndexService & Props & ArticleRepository => + this: Elastic4sClient & BaseIndexService & Props & ArticleRepository & SearchLanguage => trait IndexService extends BaseIndexService with StrictLogging { override val MaxResultWindowOption: Int = props.ElasticSearchIndexMaxResultWindow @@ -99,55 +96,19 @@ trait IndexService { */ protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { if (keepRaw) { - languageAnalyzers.map(langAnalyzer => + SearchLanguage.languageAnalyzers.map(langAnalyzer => textField(s"$fieldName.${langAnalyzer.languageTag.toString}") .fielddata(false) .analyzer(langAnalyzer.analyzer) .fields(keywordField("raw")) ) } else { - languageAnalyzers.map(langAnalyzer => + SearchLanguage.languageAnalyzers.map(langAnalyzer => textField(s"$fieldName.${langAnalyzer.languageTag.toString}") .fielddata(false) .analyzer(langAnalyzer.analyzer) ) } } - - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val fields = new ListBuffer[ElasticField]() - if (keepRaw) { - fields += keywordField("raw") - } - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(languageAnalyzer.analyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(name) - ) - }) - val catchAllTemplate = DynamicTemplateRequest( - name = fieldName, - mapping = textField(fieldName).analyzer(SearchLanguage.standardAnalyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(s"$fieldName.*") - ) - languageTemplates ++ Seq(catchAllTemplate) - } } - } diff --git a/article-api/src/main/scala/no/ndla/articleapi/service/search/SearchConverterService.scala b/article-api/src/main/scala/no/ndla/articleapi/service/search/SearchConverterService.scala index 5967331fdd..8450cb776c 100644 --- a/article-api/src/main/scala/no/ndla/articleapi/service/search/SearchConverterService.scala +++ b/article-api/src/main/scala/no/ndla/articleapi/service/search/SearchConverterService.scala @@ -13,12 +13,12 @@ import no.ndla.articleapi.model.api.{ArticleSummaryV2DTO, SearchResultV2DTO} import no.ndla.articleapi.model.search.* import no.ndla.articleapi.service.ConverterService import no.ndla.common.model.domain.article.Article -import no.ndla.search.SearchLanguage.languageAnalyzers +import no.ndla.search.SearchLanguage import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} import org.jsoup.Jsoup trait SearchConverterService { - this: ConverterService => + this: ConverterService & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService extends StrictLogging { @@ -26,7 +26,7 @@ trait SearchConverterService { def asSearchableArticle(ai: Article): SearchableArticle = { val defaultTitle = ai.title .sortBy(title => { - val languagePriority = languageAnalyzers.map(la => la.languageTag.toString).reverse + val languagePriority = SearchLanguage.languageAnalyzers.map(la => la.languageTag.toString).reverse languagePriority.indexOf(title.language) }) .lastOption diff --git a/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala b/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala index 7e93d03ad9..01b5bd901a 100644 --- a/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala +++ b/article-api/src/test/scala/no/ndla/articleapi/TestEnvironment.scala @@ -24,7 +24,7 @@ import no.ndla.database.{DBMigrator, DBUtility, DataSource} import no.ndla.network.NdlaClient import no.ndla.network.clients.{FeideApiClient, RedisClient} import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.scalatestplus.mockito.MockitoSugar trait TestEnvironment @@ -57,6 +57,7 @@ trait TestEnvironment with Props with TestData with DBMigrator + with SearchLanguage with FrontpageApiClient with ImageApiClient { val props: ArticleApiProperties = new ArticleApiProperties { diff --git a/audio-api/src/main/scala/no/ndla/audioapi/ComponentRegistry.scala b/audio-api/src/main/scala/no/ndla/audioapi/ComponentRegistry.scala index 4d055dd79e..a70a231895 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/ComponentRegistry.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/ComponentRegistry.scala @@ -22,7 +22,7 @@ import no.ndla.common.configuration.BaseComponentRegistry import no.ndla.database.{DBMigrator, DataSource} import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} class ComponentRegistry(properties: AudioApiProperties) extends BaseComponentRegistry[AudioApiProperties] @@ -47,6 +47,7 @@ class ComponentRegistry(properties: AudioApiProperties) with Elastic4sClient with IndexService with BaseIndexService + with SearchLanguage with AudioIndexService with SeriesIndexService with TagIndexService diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/search/AudioIndexService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/search/AudioIndexService.scala index c27b6175e2..f341505d47 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/search/AudioIndexService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/search/AudioIndexService.scala @@ -12,7 +12,6 @@ import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.fields.{ElasticField, ObjectField} import com.sksamuel.elastic4s.requests.indexes.IndexRequest import com.sksamuel.elastic4s.requests.mappings.MappingDefinition -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.audioapi.Props import no.ndla.audioapi.model.domain.AudioMetaInformation @@ -74,14 +73,14 @@ trait AudioIndexService { ) ) - val dynamics: Seq[DynamicTemplateRequest] = - generateLanguageSupportedDynamicTemplates("titles", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("tags") ++ - generateLanguageSupportedDynamicTemplates("manuscript") ++ - generateLanguageSupportedDynamicTemplates("filePaths") ++ - generateLanguageSupportedDynamicTemplates("podcastMetaIntroduction") + val dynamics = + generateLanguageSupportedFieldList("titles", keepRaw = true) ++ + generateLanguageSupportedFieldList("tags") ++ + generateLanguageSupportedFieldList("manuscript") ++ + generateLanguageSupportedFieldList("filePaths") ++ + generateLanguageSupportedFieldList("podcastMetaIntroduction") - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/search/IndexService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/search/IndexService.scala index d058506c7a..804b387232 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/search/IndexService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/search/IndexService.scala @@ -13,19 +13,16 @@ import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.requests.indexes.IndexRequest import com.sksamuel.elastic4s.requests.mappings.MappingDefinition -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.audioapi.Props import no.ndla.audioapi.repository.{AudioRepository, Repository} -import no.ndla.search.SearchLanguage.languageAnalyzers import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} -import scala.collection.mutable.ListBuffer import scala.util.{Failure, Success, Try} trait IndexService { - this: Elastic4sClient & BaseIndexService & SearchConverterService & AudioRepository & Props => + this: Elastic4sClient & BaseIndexService & SearchConverterService & AudioRepository & Props & SearchLanguage => trait IndexService[D, T] extends BaseIndexService with StrictLogging { override val MaxResultWindowOption: Int = props.ElasticSearchIndexMaxResultWindow @@ -107,53 +104,17 @@ trait IndexService { */ protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { if (keepRaw) { - languageAnalyzers.map(langAnalyzer => + SearchLanguage.languageAnalyzers.map(langAnalyzer => textField(s"$fieldName.${langAnalyzer.languageTag.toString()}") .analyzer(langAnalyzer.analyzer) .fields(keywordField("raw")) ) } else { - languageAnalyzers.map(langAnalyzer => + SearchLanguage.languageAnalyzers.map(langAnalyzer => textField(s"$fieldName.${langAnalyzer.languageTag.toString()}").analyzer(langAnalyzer.analyzer) ) } } - - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val fields = new ListBuffer[ElasticField]() - if (keepRaw) { - fields += keywordField("raw") - } - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(languageAnalyzer.analyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(name) - ) - }) - val catchAlltemplate = DynamicTemplateRequest( - name = fieldName, - mapping = textField(fieldName).analyzer(SearchLanguage.standardAnalyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(s"$fieldName.*") - ) - languageTemplates ++ Seq(catchAlltemplate) - } - } } diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchConverterService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchConverterService.scala index 2b42d5f968..134e6cbe4e 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchConverterService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchConverterService.scala @@ -25,7 +25,7 @@ import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLa import scala.util.Try trait SearchConverterService { - this: ConverterService with Props => + this: ConverterService & Props & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService extends StrictLogging { diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchService.scala index f1a6f8efbd..65707dc156 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SearchService.scala @@ -26,7 +26,7 @@ import no.ndla.search.{Elastic4sClient, IndexNotFoundException, NdlaSearchExcept import scala.util.{Failure, Success, Try} trait SearchService { - this: Elastic4sClient with SearchConverterService with Props => + this: Elastic4sClient & SearchConverterService & Props & SearchLanguage => trait SearchService[T] extends StrictLogging { import props._ diff --git a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SeriesIndexService.scala b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SeriesIndexService.scala index 2cac98d30f..4a359c3466 100644 --- a/audio-api/src/main/scala/no/ndla/audioapi/service/search/SeriesIndexService.scala +++ b/audio-api/src/main/scala/no/ndla/audioapi/service/search/SeriesIndexService.scala @@ -12,7 +12,6 @@ import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.requests.indexes.IndexRequest import com.sksamuel.elastic4s.requests.mappings.MappingDefinition -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.audioapi.Props import no.ndla.audioapi.model.domain.Series @@ -24,7 +23,7 @@ import no.ndla.search.Elastic4sClient import scala.util.{Failure, Success, Try} trait SeriesIndexService { - this: Elastic4sClient with SearchConverterService with IndexService with SeriesRepository with Props => + this: Elastic4sClient & SearchConverterService & IndexService & SeriesRepository & Props => val seriesIndexService: SeriesIndexService @@ -50,11 +49,11 @@ trait SeriesIndexService { dateField("lastUpdated") ) - val seriesDynamics: Seq[DynamicTemplateRequest] = - generateLanguageSupportedDynamicTemplates("titles", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("descriptions", keepRaw = true) + val seriesDynamics = + generateLanguageSupportedFieldList("titles", keepRaw = true) ++ + generateLanguageSupportedFieldList("descriptions", keepRaw = true) - def getMapping: MappingDefinition = properties(seriesIndexFields).dynamicTemplates(seriesDynamics) + def getMapping: MappingDefinition = properties(seriesIndexFields ++ seriesDynamics) } } diff --git a/audio-api/src/test/scala/no/ndla/audioapi/TestEnvironment.scala b/audio-api/src/test/scala/no/ndla/audioapi/TestEnvironment.scala index a20e8992a0..49f3985a38 100644 --- a/audio-api/src/test/scala/no/ndla/audioapi/TestEnvironment.scala +++ b/audio-api/src/test/scala/no/ndla/audioapi/TestEnvironment.scala @@ -20,7 +20,7 @@ import no.ndla.common.brightcove.NdlaBrightcoveClient import no.ndla.database.DataSource import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.scalatestplus.mockito.MockitoSugar trait TestEnvironment @@ -41,6 +41,7 @@ trait TestEnvironment with Elastic4sClient with IndexService with BaseIndexService + with SearchLanguage with AudioIndexService with SeriesIndexService with SearchConverterService diff --git a/audio-api/src/test/scala/no/ndla/audioapi/service/search/AudioSearchServiceTest.scala b/audio-api/src/test/scala/no/ndla/audioapi/service/search/AudioSearchServiceTest.scala index 691d0bc6a9..5312585f83 100644 --- a/audio-api/src/test/scala/no/ndla/audioapi/service/search/AudioSearchServiceTest.scala +++ b/audio-api/src/test/scala/no/ndla/audioapi/service/search/AudioSearchServiceTest.scala @@ -210,23 +210,23 @@ class AudioSearchServiceTest val audio7: domain.AudioMetaInformation = domain.AudioMetaInformation( Some(7), Some(1), - List(Title("Não relacionado", "pt-br"), Title("Dogosé", "dos")), - List(Audio("pt-br.mp3", "audio/mpeg", 1024, "pt-br"), Audio("pt-br.mp3", "audio/mpeg", 1024, "dos")), + List(Title("Não relacionado", "es"), Title("ukranian", "ukr")), + List(Audio("pt-br.mp3", "audio/mpeg", 1024, "es"), Audio("pt-br.mp3", "audio/mpeg", 1024, "ukr")), byNcSa, - List(Tag(List("wubbi"), "pt-br"), Tag(List("asdf"), "dos")), + List(Tag(List("wubbi"), "es"), Tag(List("asdf"), "ukr")), "ndla123", updated7, created, Seq( domain.PodcastMeta( - introduction = "portugeseintro", + introduction = "spanishintro", coverPhoto = domain.CoverPhoto("2", "meta"), - language = "pt-br" + language = "es" ), domain.PodcastMeta( - introduction = "dogose intro", + introduction = "ukranian intro", coverPhoto = domain.CoverPhoto("1", "alt "), - language = "dos" + language = "ukr" ) ), AudioType.Podcast, @@ -412,12 +412,12 @@ class AudioSearchServiceTest } test("That searching for language not in predefined list should work") { - val Success(result) = audioSearchService.matchingQuery(searchSettings.copy(language = Some("dos"))) + val Success(result) = audioSearchService.matchingQuery(searchSettings.copy(language = Some("ukr"))) result.totalCount should be(1) - result.language should be("dos") + result.language should be("ukr") - result.results.head.title.title should be("Dogosé") - result.results.head.title.language should be("dos") + result.results.head.title.title should be("ukranian") + result.results.head.title.language should be("ukr") } test("That searching for language not in indexed data should not fail") { @@ -590,7 +590,7 @@ class AudioSearchServiceTest result1.results(2).title.language should be("en") result1.results(3).title.language should be("nb") result1.results(4).title.language should be("en") - result1.results(5).title.language should be("pt-br") + result1.results(5).title.language should be("es") val Success(result2) = audioSearchService.matchingQuery( searchSettings.copy( @@ -606,7 +606,7 @@ class AudioSearchServiceTest result2.results(2).title.language should be("nb") result2.results(3).title.language should be("nb") result2.results(4).title.language should be("nb") - result2.results(5).title.language should be("pt-br") + result2.results(5).title.language should be("es") } test("That fallback searching includes audios with languages outside the search with query") { diff --git a/common/src/main/scala/no/ndla/common/CirceUtil.scala b/common/src/main/scala/no/ndla/common/CirceUtil.scala index 0851e530ab..5b8de2b29f 100644 --- a/common/src/main/scala/no/ndla/common/CirceUtil.scala +++ b/common/src/main/scala/no/ndla/common/CirceUtil.scala @@ -9,8 +9,12 @@ package no.ndla.common import enumeratum.* -import io.circe.syntax.* import io.circe.* +import io.circe.generic.semiauto.deriveEncoder +import io.circe.syntax.* +import io.circe.generic.encoding.DerivedAsObjectEncoder +import io.circe.{Decoder, Encoder} +import shapeless.Lazy import scala.util.{Failure, Try} @@ -25,24 +29,38 @@ object CirceUtil { } } - def tryParseAs[T](str: String)(implicit d: Decoder[T]): Try[T] = { + def tryParse(str: String): Try[Json] = { parser .parse(str) .toTry - .flatMap(_.as[T].toTry) .recoverWith { ex => Failure(CirceFailure(str, ex)) } } + def tryParseAs[T](str: String)(implicit d: Decoder[T]): Try[T] = tryParse(str).flatMap(_.as[T].toTry) + /** This might throw an exception! Use with care, probably only use this in tests */ def unsafeParseAs[T: Decoder](str: String): T = tryParseAs(str).get def toJsonString[T: Encoder](obj: T): String = obj.asJson.noSpaces /** Helper to simplify making decoders with default values */ - def getOrDefault[T: Decoder](cur: HCursor, key: String, default: T) = { + def getOrDefault[T: Decoder](cur: HCursor, key: String, default: T): Either[DecodingFailure, T] = { cur.downField(key).as[Option[T]].map(_.getOrElse(default)) } + def addTypenameDiscriminator(json: Json, clazz: Class[?]): Json = { + json.mapObject(_.add("typename", Json.fromString(clazz.getSimpleName))) + } + + def deriveEncoderWithTypename[T](implicit encode: Lazy[DerivedAsObjectEncoder[T]]): Encoder[T] = { + val encoder = deriveEncoder[T] + Encoder.instance[T] { value => + val json = encoder(value) + + addTypenameDiscriminator(json, value.getClass) + } + } + private val stringDecoder = implicitly[Decoder[String]] /** Trait that does the same as `CirceEnum`, but with slightly better error message */ diff --git a/common/src/main/scala/no/ndla/common/ContentURIUtil.scala b/common/src/main/scala/no/ndla/common/ContentURIUtil.scala index 97d3704a05..9fb6a8f039 100644 --- a/common/src/main/scala/no/ndla/common/ContentURIUtil.scala +++ b/common/src/main/scala/no/ndla/common/ContentURIUtil.scala @@ -13,18 +13,34 @@ import scala.util.{Failure, Try} object ContentURIUtil { case class NotUrnPatternException(message: String) extends RuntimeException(message) - private val Pattern = """(urn:)?(article:)?(\d*)#?(\d*)""".r + private val ArticlePattern = """(urn:)?(article:)?(\d*)#?(\d*)""".r + private val FrontpagePattern = """urn:frontpage:(\d*)""".r + + def parseFrontpageId(idString: String): Try[Long] = { + idString match { + case FrontpagePattern(id) => + Try(id.toLong) + case _ => + Failure( + NotUrnPatternException(s"Pattern \"$idString\" passed to `parseFrontpageId` did not match urn pattern.") + ) + } + + } + type Result = (Try[Long], Option[Int]) def parseArticleIdAndRevision(idString: String): Result = { idString match { - case Pattern(_, _, id, rev) => + case ArticlePattern(_, _, id, rev) => ( Try(id.toLong), Try(rev.toInt).toOption ) case _ => Failure( - NotUrnPatternException("Pattern passed to `parseArticleIdAndRevision` did not match urn pattern.") + NotUrnPatternException( + s"Pattern \"$idString\" passed to `parseArticleIdAndRevision` did not match urn pattern." + ) ) -> None } } diff --git a/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala b/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala index 890e0df6f0..601ceb8e3e 100644 --- a/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala +++ b/common/src/main/scala/no/ndla/common/configuration/BaseProps.scala @@ -58,6 +58,15 @@ trait BaseProps { def TaxonomyUrl: String = s"http://$TaxonomyApiHost" def disableWarmup: Boolean = booleanPropOrElse("DISABLE_WARMUP", default = false) + def SupportedLanguages: List[String] = + propOrElse("SUPPORTED_LANGUAGES", "nb,nn,en,sma,se,de,es,zh,ukr").split(",").toList + + def ndlaFrontendUrl: String = Environment match { + case "local" => "http://localhost:30017" + case "prod" => "https://ndla.no" + case _ => s"https://$Environment.ndla.no" + } + def MAX_SEARCH_THREADS: Int = intPropOrDefault("MAX_SEARCH_THREADS", 100) def SEARCH_INDEX_SHARDS: Int = intPropOrDefault("SEARCH_INDEX_SHARDS", 1) def SEARCH_INDEX_REPLICAS: Int = intPropOrDefault("SEARCH_INDEX_REPLICAS", 1) diff --git a/common/src/main/scala/no/ndla/common/model/api/frontpage/AboutSubjectDTO.scala b/common/src/main/scala/no/ndla/common/model/api/frontpage/AboutSubjectDTO.scala new file mode 100644 index 0000000000..1b26880549 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/api/frontpage/AboutSubjectDTO.scala @@ -0,0 +1,23 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.api.frontpage + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class AboutSubjectDTO( + title: String, + description: String, + visualElement: VisualElementDTO +) + +object AboutSubjectDTO { + implicit val encoder: Encoder[AboutSubjectDTO] = deriveEncoder + implicit val decoder: Decoder[AboutSubjectDTO] = deriveDecoder +} diff --git a/common/src/main/scala/no/ndla/common/model/api/frontpage/BannerImageDTO.scala b/common/src/main/scala/no/ndla/common/model/api/frontpage/BannerImageDTO.scala new file mode 100644 index 0000000000..2f6cd288f3 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/api/frontpage/BannerImageDTO.scala @@ -0,0 +1,24 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.api.frontpage + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class BannerImageDTO( + mobileUrl: Option[String], + mobileId: Option[Long], + desktopUrl: String, + desktopId: Long +) + +object BannerImageDTO { + implicit val encoder: Encoder[BannerImageDTO] = deriveEncoder + implicit val decoder: Decoder[BannerImageDTO] = deriveDecoder +} diff --git a/common/src/main/scala/no/ndla/common/model/api/frontpage/SubjectPageDTO.scala b/common/src/main/scala/no/ndla/common/model/api/frontpage/SubjectPageDTO.scala new file mode 100644 index 0000000000..9e4720293e --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/api/frontpage/SubjectPageDTO.scala @@ -0,0 +1,30 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.api.frontpage + +import io.circe.* +import io.circe.generic.semiauto.* + +case class SubjectPageDTO( + id: Long, + name: String, + banner: BannerImageDTO, + about: Option[AboutSubjectDTO], + metaDescription: Option[String], + editorsChoices: List[String], + supportedLanguages: Seq[String], + connectedTo: List[String], + buildsOn: List[String], + leadsTo: List[String] +) + +object SubjectPageDTO { + implicit def encoder: Encoder[SubjectPageDTO] = deriveEncoder[SubjectPageDTO] + implicit def decoder: Decoder[SubjectPageDTO] = deriveDecoder[SubjectPageDTO] +} diff --git a/common/src/main/scala/no/ndla/common/model/api/frontpage/VisualElementDTO.scala b/common/src/main/scala/no/ndla/common/model/api/frontpage/VisualElementDTO.scala new file mode 100644 index 0000000000..db2aa2e7fc --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/api/frontpage/VisualElementDTO.scala @@ -0,0 +1,19 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.api.frontpage + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class VisualElementDTO(`type`: String, url: String, alt: Option[String]) + +object VisualElementDTO { + implicit val encoder: Encoder[VisualElementDTO] = deriveEncoder + implicit val decoder: Decoder[VisualElementDTO] = deriveDecoder +} diff --git a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchResultDTO.scala b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchResultDTO.scala index 6ed2769749..0612ddebcc 100644 --- a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchResultDTO.scala +++ b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchResultDTO.scala @@ -18,7 +18,7 @@ case class MultiSearchResultDTO( @description("For which page results are shown from") page: Option[Int], @description("The number of results per page") pageSize: Int, @description("The chosen search language") language: String, - @description("The search results") results: Seq[MultiSearchSummaryDTO], + @description("The search results") results: Seq[MultiSummaryBaseDTO], @description("The suggestions for other searches") suggestions: Seq[MultiSearchSuggestionDTO], @description("The aggregated fields if specified in query") aggregations: Seq[MultiSearchTermsAggregationDTO] ) diff --git a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala index 1b8dea1843..92b246ad70 100644 --- a/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala +++ b/common/src/main/scala/no/ndla/common/model/api/search/MultiSearchSummaryDTO.scala @@ -8,8 +8,11 @@ package no.ndla.common.model.api.search +import cats.implicits.toFunctorOps import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.syntax.EncoderOps import io.circe.{Decoder, Encoder} +import no.ndla.common.CirceUtil.deriveEncoderWithTypename import no.ndla.common.model.NDLADate import no.ndla.common.model.api.draft.CommentDTO import sttp.tapir.Schema.annotations.description @@ -25,6 +28,36 @@ object HighlightedFieldDTO { implicit val decoder: Decoder[HighlightedFieldDTO] = deriveDecoder } +sealed trait MultiSummaryBaseDTO + +object MultiSummaryBaseDTO { + implicit val encoder: Encoder[MultiSummaryBaseDTO] = Encoder.instance { + case x: MultiSearchSummaryDTO => x.asJson + case x: NodeHitDTO => x.asJson + } + + implicit val decoder: Decoder[MultiSummaryBaseDTO] = List[Decoder[MultiSummaryBaseDTO]]( + Decoder[MultiSearchSummaryDTO].widen, + Decoder[NodeHitDTO].widen + ).reduceLeft(_ or _) +} + +case class NodeHitDTO( + @description("The unique id of the taxonomy node") + id: String, + @description("The title of the taxonomy node") + title: String, + @description("The url to the frontend page of the taxonomy node") + url: Option[String], + @description("Subject page summary if the node is connected to a subject page") + subjectPage: Option[SubjectPageSummaryDTO] +) extends MultiSummaryBaseDTO + +object NodeHitDTO { + implicit val encoder: Encoder[NodeHitDTO] = deriveEncoderWithTypename[NodeHitDTO] + implicit val decoder: Decoder[NodeHitDTO] = deriveDecoder +} + @description("Short summary of information about the resource") case class MultiSearchSummaryDTO( @description("The unique id of the resource") @@ -81,9 +114,9 @@ case class MultiSearchSummaryDTO( favorited: Option[Long], @description("Type of the resource") resultType: SearchType -) +) extends MultiSummaryBaseDTO object MultiSearchSummaryDTO { - implicit val encoder: Encoder[MultiSearchSummaryDTO] = deriveEncoder + implicit val encoder: Encoder[MultiSearchSummaryDTO] = deriveEncoderWithTypename[MultiSearchSummaryDTO] implicit val decoder: Decoder[MultiSearchSummaryDTO] = deriveDecoder } diff --git a/common/src/main/scala/no/ndla/common/model/api/search/SearchType.scala b/common/src/main/scala/no/ndla/common/model/api/search/SearchType.scala index 5d84d3ed78..380c03a019 100644 --- a/common/src/main/scala/no/ndla/common/model/api/search/SearchType.scala +++ b/common/src/main/scala/no/ndla/common/model/api/search/SearchType.scala @@ -24,6 +24,7 @@ object SearchType extends Enum[SearchType] with CirceEnumWithErrors[SearchType] case object LearningPaths extends SearchType("learningpath") case object Concepts extends SearchType("concept") case object Grep extends SearchType("grep") + case object Nodes extends SearchType("node") def all: List[String] = SearchType.values.map(_.toString).toList override def values: IndexedSeq[SearchType] = findValues diff --git a/common/src/main/scala/no/ndla/common/model/api/search/SubjectPageSummaryDTO.scala b/common/src/main/scala/no/ndla/common/model/api/search/SubjectPageSummaryDTO.scala new file mode 100644 index 0000000000..73c578fd7c --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/api/search/SubjectPageSummaryDTO.scala @@ -0,0 +1,24 @@ +/* + * Part of NDLA common + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.common.model.api.search + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import sttp.tapir.Schema.annotations.description + +@description("DTO for subject page summary in search results") +case class SubjectPageSummaryDTO( + id: Long, + name: String, + metaDescription: MetaDescriptionDTO +) + +object SubjectPageSummaryDTO { + implicit val encoder: Encoder[SubjectPageSummaryDTO] = deriveEncoder + implicit val decoder: Decoder[SubjectPageSummaryDTO] = deriveDecoder +} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/AboutSubject.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/AboutSubject.scala similarity index 61% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/AboutSubject.scala rename to common/src/main/scala/no/ndla/common/model/domain/frontpage/AboutSubject.scala index df645d93b0..84047a0efe 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/AboutSubject.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/AboutSubject.scala @@ -1,13 +1,15 @@ /* - * Part of NDLA frontpage-api + * Part of NDLA common * Copyright (C) 2018 NDLA * * See LICENSE * */ -package no.ndla.frontpageapi.model.domain +package no.ndla.common.model.domain.frontpage +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} import no.ndla.language.model.LanguageField case class AboutSubject(title: String, description: String, language: String, visualElement: VisualElement) @@ -16,3 +18,8 @@ case class AboutSubject(title: String, description: String, language: String, vi override def isEmpty: Boolean = title.isEmpty && description.isEmpty && visualElement.id.isEmpty && visualElement.alt.isEmpty } + +object AboutSubject { + implicit val encoder: Encoder[AboutSubject] = deriveEncoder + implicit val decoder: Decoder[AboutSubject] = deriveDecoder +} diff --git a/common/src/main/scala/no/ndla/common/model/domain/frontpage/BannerImage.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/BannerImage.scala new file mode 100644 index 0000000000..54fe45ee88 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/BannerImage.scala @@ -0,0 +1,19 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.domain.frontpage + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} + +case class BannerImage(mobileImageId: Option[Long], desktopImageId: Long) + +object BannerImage { + implicit val encoder: Encoder[BannerImage] = deriveEncoder + implicit val decoder: Decoder[BannerImage] = deriveDecoder +} diff --git a/common/src/main/scala/no/ndla/common/model/domain/frontpage/MetaDescription.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MetaDescription.scala new file mode 100644 index 0000000000..c8e01f48fc --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MetaDescription.scala @@ -0,0 +1,23 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.domain.frontpage + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.language.model.LanguageField + +case class MetaDescription(metaDescription: String, language: String) extends LanguageField[String] { + override def isEmpty: Boolean = metaDescription.isEmpty + override def value: String = metaDescription +} + +object MetaDescription { + implicit val encoder: Encoder[MetaDescription] = deriveEncoder + implicit val decoder: Decoder[MetaDescription] = deriveDecoder +} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MovieTheme.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala similarity index 71% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MovieTheme.scala rename to common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala index ec81b4706a..a4927e4851 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MovieTheme.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/MovieTheme.scala @@ -1,13 +1,12 @@ /* - * Part of NDLA frontpage-api + * Part of NDLA common * Copyright (C) 2021 NDLA * * See LICENSE * */ -package no.ndla.frontpageapi.model.domain +package no.ndla.common.model.domain.frontpage case class MovieTheme(name: Seq[MovieThemeName], movies: Seq[String]) - case class MovieThemeName(name: String, language: String) diff --git a/common/src/main/scala/no/ndla/common/model/domain/frontpage/SubjectPage.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/SubjectPage.scala new file mode 100644 index 0000000000..84435ba483 --- /dev/null +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/SubjectPage.scala @@ -0,0 +1,41 @@ +/* + * Part of NDLA common + * Copyright (C) 2018 NDLA + * + * See LICENSE + * + */ + +package no.ndla.common.model.domain.frontpage + +import cats.implicits.* +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.parser.* +import io.circe.{Decoder, Encoder} +import no.ndla.language.Language.getSupportedLanguages + +import scala.util.Try + +case class SubjectPage( + id: Option[Long], + name: String, + bannerImage: BannerImage, + about: Seq[AboutSubject], + metaDescription: Seq[MetaDescription], + editorsChoices: List[String], + connectedTo: List[String], + buildsOn: List[String], + leadsTo: List[String] +) { + + def supportedLanguages: Seq[String] = getSupportedLanguages(about, metaDescription) + +} + +object SubjectPage { + implicit val decoder: Decoder[SubjectPage] = deriveDecoder + implicit val encoder: Encoder[SubjectPage] = deriveEncoder + def decodeJson(json: String, id: Long): Try[SubjectPage] = { + parse(json).flatMap(_.as[SubjectPage]).map(_.copy(id = id.some)).toTry + } +} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/VisualElement.scala b/common/src/main/scala/no/ndla/common/model/domain/frontpage/VisualElement.scala similarity index 62% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/VisualElement.scala rename to common/src/main/scala/no/ndla/common/model/domain/frontpage/VisualElement.scala index 8e5c814fb1..2a9fce1306 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/VisualElement.scala +++ b/common/src/main/scala/no/ndla/common/model/domain/frontpage/VisualElement.scala @@ -1,20 +1,27 @@ /* - * Part of NDLA frontpage-api + * Part of NDLA common * Copyright (C) 2018 NDLA * * See LICENSE * */ -package no.ndla.frontpageapi.model.domain +package no.ndla.common.model.domain.frontpage -import no.ndla.frontpageapi.model.domain.Errors.ValidationException +import enumeratum.* +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.common.errors.ValidationException import scala.util.{Failure, Success, Try} -import enumeratum._ case class VisualElement(`type`: VisualElementType, id: String, alt: Option[String]) +object VisualElement { + implicit val encoder: Encoder[VisualElement] = deriveEncoder + implicit val decoder: Decoder[VisualElement] = deriveDecoder +} + sealed abstract class VisualElementType(override val entryName: String) extends EnumEntry object VisualElementType extends Enum[VisualElementType] with CirceEnum[VisualElementType] { @@ -29,7 +36,7 @@ object VisualElementType extends Enum[VisualElementType] with CirceEnum[VisualEl visualElement.`type` match { case Image => visualElement.id.toLongOption match { - case None => Failure(ValidationException("Image of visual element should be numeric")) + case None => Failure(ValidationException("visualElement.id", "Image of visual element should be numeric")) case _ => Success(visualElement) } case Brightcove => Success(visualElement) @@ -38,7 +45,7 @@ object VisualElementType extends Enum[VisualElementType] with CirceEnum[VisualEl def fromString(str: String): Try[VisualElementType] = VisualElementType.values.find(_.entryName == str) match { case Some(v) => Success(v) - case None => Failure(ValidationException(s"'$str' is an invalid visual element type")) + case None => Failure(ValidationException("visualElement.type", s"'$str' is an invalid visual element type")) } } diff --git a/common/src/test/scala/no/ndla/common/ContentURIUtilTest.scala b/common/src/test/scala/no/ndla/common/ContentURIUtilTest.scala index f5f6a2f688..b6a0968119 100644 --- a/common/src/test/scala/no/ndla/common/ContentURIUtilTest.scala +++ b/common/src/test/scala/no/ndla/common/ContentURIUtilTest.scala @@ -30,10 +30,21 @@ class ContentURIUtilTest extends UnitTestSuite { val result = ContentURIUtil.parseArticleIdAndRevision("one") result should be( ( - Failure(NotUrnPatternException("Pattern passed to `parseArticleIdAndRevision` did not match urn pattern.")), + Failure( + NotUrnPatternException("Pattern \"one\" passed to `parseArticleIdAndRevision` did not match urn pattern.") + ), None ) ) } + test("That frontpages are matched, but does not match article :^)") { + ContentURIUtil.parseFrontpageId("urn:frontpage:15") should be(Success(15)) + ContentURIUtil.parseFrontpageId("urn:article:15") should be( + Failure( + NotUrnPatternException("Pattern \"urn:article:15\" passed to `parseFrontpageId` did not match urn pattern.") + ) + ) + } + } diff --git a/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala b/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala index cfd7440767..868aea3234 100644 --- a/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala +++ b/concept-api/src/main/scala/no/ndla/conceptapi/ComponentRegistry.scala @@ -19,7 +19,7 @@ import no.ndla.conceptapi.service.search.* import no.ndla.conceptapi.service.* import no.ndla.conceptapi.validation.ContentValidator import no.ndla.network.NdlaClient -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import no.ndla.common.Clock import no.ndla.common.configuration.BaseComponentRegistry import no.ndla.conceptapi.db.migrationwithdependencies.{V23__SubjectNameAsTags, V25__SubjectNameAsTagsPublished} @@ -44,6 +44,7 @@ class ComponentRegistry(properties: ConceptApiProperties) with DraftConceptSearchService with PublishedConceptSearchService with SearchService + with SearchLanguage with SearchConverterService with Elastic4sClient with DraftConceptIndexService 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 fff1fad7cd..67625ee5e2 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 @@ -13,21 +13,19 @@ import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.analysis.{Analysis, CustomNormalizer} import com.sksamuel.elastic4s.fields.{ElasticField, ObjectField} 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.model.api.ConceptMissingIdException import no.ndla.conceptapi.repository.Repository -import no.ndla.search.SearchLanguage.{NynorskLanguageAnalyzer, languageAnalyzers} import no.ndla.search.model.domain.{BulkIndexResult, ElasticIndexingException, ReindexResult} import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import scala.util.{Failure, Success, Try} trait IndexService { - this: Elastic4sClient & BaseIndexService & Props & SearchConverterService => + this: Elastic4sClient & BaseIndexService & Props & SearchConverterService & SearchLanguage => trait IndexService extends BaseIndexService with StrictLogging { val repository: Repository[Concept] override val MaxResultWindowOption: Int = props.ElasticSearchIndexMaxResultWindow @@ -37,7 +35,7 @@ trait IndexService { override val analysis: Analysis = Analysis( - analyzers = List(NynorskLanguageAnalyzer), + analyzers = List(SearchLanguage.NynorskLanguageAnalyzer), tokenFilters = SearchLanguage.NynorskTokenFilters, normalizers = List(lowerNormalizer) ) @@ -115,47 +113,26 @@ trait IndexService { } } - def findAllIndexes: Try[Seq[String]] = findAllIndexes(this.searchIndex) - - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - private def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val fields = - if (keepRaw) - List( - keywordField("raw"), - keywordField("lower").normalizer("lower") - ) - else List.empty - - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(languageAnalyzer.analyzer).fields(fields), - matchMappingType = Some("string"), - pathMatch = Some(name) + protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { + if (keepRaw) { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .analyzer(langAnalyzer.analyzer) + .fields( + keywordField("raw"), + keywordField("lower").normalizer("lower") + ) ) - }) - val catchAlltemplate = DynamicTemplateRequest( - name = fieldName, - mapping = textField(fieldName).analyzer("standard").fields(fields), - matchMappingType = Some("string"), - pathMatch = Some(s"$fieldName.*") - ) - languageTemplates ++ Seq(catchAlltemplate) + } else { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .analyzer(langAnalyzer.analyzer) + ) + } } + def findAllIndexes: Try[Seq[String]] = findAllIndexes(this.searchIndex) + def getMapping: MappingDefinition = { val fields: Seq[ElasticField] = List( intField("id"), @@ -205,12 +182,12 @@ trait IndexService { textField("gloss"), ObjectField("domainObject", enabled = Some(false)) ) - val dynamics: Seq[DynamicTemplateRequest] = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("tags", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("sortableConceptType", keepRaw = true) + val dynamics = generateLanguageSupportedFieldList("title", keepRaw = true) ++ + generateLanguageSupportedFieldList("content") ++ + generateLanguageSupportedFieldList("tags", keepRaw = true) ++ + generateLanguageSupportedFieldList("sortableConceptType", keepRaw = true) - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } diff --git a/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala b/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala index 30c00a257a..4acbd81a0d 100644 --- a/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala +++ b/concept-api/src/test/scala/no/ndla/conceptapi/TestEnvironment.scala @@ -27,7 +27,7 @@ import no.ndla.conceptapi.validation.ContentValidator import no.ndla.database.{DBMigrator, DataSource} import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.scalatestplus.mockito.MockitoSugar trait TestEnvironment @@ -61,6 +61,7 @@ trait TestEnvironment with Props with ErrorHandling with SearchSettingsHelper + with SearchLanguage with DraftSearchSettingsHelper with DBMigrator with InternController { diff --git a/draft-api/src/main/scala/no/ndla/draftapi/ComponentRegistry.scala b/draft-api/src/main/scala/no/ndla/draftapi/ComponentRegistry.scala index 455336ff95..a4920af26a 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/ComponentRegistry.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/ComponentRegistry.scala @@ -36,7 +36,7 @@ import no.ndla.draftapi.validation.ContentValidator import no.ndla.network.NdlaClient import no.ndla.network.clients.SearchApiClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} class ComponentRegistry(properties: DraftApiProperties) extends BaseComponentRegistry[DraftApiProperties] @@ -83,6 +83,7 @@ class ComponentRegistry(properties: DraftApiProperties) with DBMigrator with ErrorHandling with SwaggerDocControllerConfig + with SearchLanguage with V57__MigrateSavedSearch with V66__SetHideBylineForImagesNotCopyrighted { override val props: DraftApiProperties = properties diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/search/ArticleIndexService.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/search/ArticleIndexService.scala index 91d7f0f4a7..377baa4293 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/search/ArticleIndexService.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/search/ArticleIndexService.scala @@ -48,13 +48,13 @@ trait ArticleIndexService { keywordField("grepCodes"), ObjectField("status", properties = Seq(keywordField("current"), keywordField("other"))) ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("visualElement") ++ - generateLanguageSupportedDynamicTemplates("introduction") ++ - generateLanguageSupportedDynamicTemplates("tags") + val dynamics = generateLanguageSupportedFieldList("title", keepRaw = true) ++ + generateLanguageSupportedFieldList("content") ++ + generateLanguageSupportedFieldList("visualElement") ++ + generateLanguageSupportedFieldList("introduction") ++ + generateLanguageSupportedFieldList("tags") - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/search/IndexService.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/search/IndexService.scala index bfeae6afcd..5283ab4c5f 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/search/IndexService.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/search/IndexService.scala @@ -11,14 +11,11 @@ package no.ndla.draftapi.service.search import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.requests.indexes.IndexRequest -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.draftapi.Props import no.ndla.draftapi.repository.Repository -import no.ndla.search.SearchLanguage.languageAnalyzers import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} -import scala.collection.mutable.ListBuffer import scala.util.{Failure, Success, Try} import cats.implicits.* import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} @@ -26,7 +23,7 @@ import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} import scala.concurrent.{ExecutionContext, Future} trait IndexService { - this: Elastic4sClient with BaseIndexService with Props => + this: Elastic4sClient & BaseIndexService & Props & SearchLanguage => trait IndexService[D, T <: AnyRef] extends BaseIndexService with StrictLogging { override val MaxResultWindowOption: Int = props.ElasticSearchIndexMaxResultWindow @@ -108,57 +105,20 @@ trait IndexService { * Sequence of FieldDefinitions for a field. */ protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { - keepRaw match { - case true => - languageAnalyzers.map(langAnalyzer => - textField(s"$fieldName.${langAnalyzer.languageTag.toString}") - .fielddata(false) - .analyzer(langAnalyzer.analyzer) - .fields(keywordField("raw")) - ) - case false => - languageAnalyzers.map(langAnalyzer => - textField(s"$fieldName.${langAnalyzer.languageTag.toString}") - .fielddata(false) - .analyzer(langAnalyzer.analyzer) - ) - } - } - - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val fields = new ListBuffer[ElasticField]() if (keepRaw) { - fields += keywordField("raw") - } - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(languageAnalyzer.analyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(name) + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .fielddata(false) + .analyzer(langAnalyzer.analyzer) + .fields(keywordField("raw")) ) - }) - val catchAlltemplate = DynamicTemplateRequest( - name = fieldName, - mapping = textField(fieldName).analyzer(SearchLanguage.standardAnalyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(s"$fieldName.*") - ) - languageTemplates ++ Seq(catchAlltemplate) + } else { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .fielddata(false) + .analyzer(langAnalyzer.analyzer) + ) + } } - } } diff --git a/draft-api/src/main/scala/no/ndla/draftapi/service/search/SearchConverterService.scala b/draft-api/src/main/scala/no/ndla/draftapi/service/search/SearchConverterService.scala index 60c6bdd03e..b4d6bef98e 100644 --- a/draft-api/src/main/scala/no/ndla/draftapi/service/search/SearchConverterService.scala +++ b/draft-api/src/main/scala/no/ndla/draftapi/service/search/SearchConverterService.scala @@ -26,7 +26,7 @@ import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLa import org.jsoup.Jsoup trait SearchConverterService { - this: ConverterService => + this: ConverterService & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService extends StrictLogging { diff --git a/draft-api/src/test/scala/no/ndla/draftapi/TestEnvironment.scala b/draft-api/src/test/scala/no/ndla/draftapi/TestEnvironment.scala index 8049d353ad..4508670474 100644 --- a/draft-api/src/test/scala/no/ndla/draftapi/TestEnvironment.scala +++ b/draft-api/src/test/scala/no/ndla/draftapi/TestEnvironment.scala @@ -25,7 +25,7 @@ import no.ndla.draftapi.validation.ContentValidator import no.ndla.network.NdlaClient import no.ndla.network.clients.SearchApiClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.scalatestplus.mockito.MockitoSugar trait TestEnvironment @@ -38,6 +38,7 @@ trait TestEnvironment with GrepCodesSearchService with GrepCodesIndexService with IndexService + with SearchLanguage with BaseIndexService with SearchService with StrictLogging diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/ComponentRegistry.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/ComponentRegistry.scala index 36f84de044..9db2c31ad8 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/ComponentRegistry.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/ComponentRegistry.scala @@ -13,7 +13,7 @@ import no.ndla.common.Clock import no.ndla.database.{DBMigrator, DataSource} import no.ndla.frontpageapi.controller.* import no.ndla.frontpageapi.model.api.ErrorHandling -import no.ndla.frontpageapi.model.domain.{DBFilmFrontPageData, DBFrontPageData, DBSubjectFrontPageData} +import no.ndla.frontpageapi.model.domain.{DBFilmFrontPage, DBFrontPage, DBSubjectPage} import no.ndla.frontpageapi.repository.{FilmFrontPageRepository, FrontPageRepository, SubjectPageRepository} import no.ndla.frontpageapi.service.{ConverterService, ReadService, WriteService} import no.ndla.network.tapir.TapirApplication @@ -30,9 +30,9 @@ class ComponentRegistry(properties: FrontpageApiProperties) with SubjectPageController with FrontPageController with FilmPageController - with DBFilmFrontPageData - with DBSubjectFrontPageData - with DBFrontPageData + with DBFilmFrontPage + with DBSubjectPage + with DBFrontPage with ErrorHandling with Clock with Props diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala index b935825f77..1fe3b6df19 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FilmPageController.scala @@ -9,8 +9,8 @@ package no.ndla.frontpageapi.controller import cats.implicits.* import io.circe.generic.auto.* +import no.ndla.common.errors.ValidationException import no.ndla.frontpageapi.model.api.* -import no.ndla.frontpageapi.model.domain.Errors.ValidationException import no.ndla.frontpageapi.service.{ReadService, WriteService} import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody import no.ndla.network.tapir.TapirController @@ -21,7 +21,7 @@ import sttp.tapir.generic.auto.* import sttp.tapir.server.ServerEndpoint trait FilmPageController { - this: ReadService with WriteService with ErrorHandling with TapirController => + this: ReadService & WriteService & ErrorHandling & TapirController => val filmPageController: FilmPageController class FilmPageController extends TapirController { @@ -31,7 +31,7 @@ trait FilmPageController { endpoint.get .summary("Get data to display on the film front page") .in(query[Option[String]]("language")) - .out(jsonBody[FilmFrontPageDataDTO]) + .out(jsonBody[FilmFrontPageDTO]) .errorOut(errorOutputsFor(404)) .serverLogicPure { language => readService.filmFrontPage(language) match { @@ -42,8 +42,8 @@ trait FilmPageController { endpoint.post .summary("Update film front page") .errorOut(errorOutputsFor(400, 401, 403, 404, 422)) - .in(jsonBody[NewOrUpdatedFilmFrontPageDataDTO]) - .out(jsonBody[FilmFrontPageDataDTO]) + .in(jsonBody[NewOrUpdatedFilmFrontPageDTO]) + .out(jsonBody[FilmFrontPageDTO]) .requirePermission(FRONTPAGE_API_WRITE) .serverLogicPure { _ => filmFrontPage => writeService.updateFilmFrontPage(filmFrontPage).partialOverride { case ex: ValidationException => diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FrontPageController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FrontPageController.scala index ce74a0729e..70baa53daa 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FrontPageController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/FrontPageController.scala @@ -21,7 +21,7 @@ import sttp.tapir.generic.auto.* import sttp.tapir.server.ServerEndpoint trait FrontPageController { - this: ReadService with WriteService with ErrorHandling with TapirController => + this: ReadService & WriteService & ErrorHandling & TapirController => val frontPageController: FrontPageController class FrontPageController() extends TapirController { diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/InternController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/InternController.scala index afefec1a6c..b66a684513 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/InternController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/InternController.scala @@ -10,6 +10,7 @@ package no.ndla.frontpageapi.controller import cats.implicits.* import io.circe.generic.auto.* +import no.ndla.common.model.domain.frontpage.SubjectPage import no.ndla.frontpageapi.Props import no.ndla.frontpageapi.model.api.* import no.ndla.frontpageapi.service.{ReadService, WriteService} @@ -43,27 +44,20 @@ trait InternController { case Failure(ex) => returnLeftError(ex) } }, - endpoint.post - .summary("Create new subject page") - .in("subjectpage") - .in(jsonBody[NewSubjectFrontPageDataDTO]) - .errorOut(errorOutputsFor()) - .out(jsonBody[SubjectPageDataDTO]) - .serverLogicPure { subjectPage => - writeService - .newSubjectPage(subjectPage) - + endpoint.get + .in("dump" / "subjectpage") + .in(query[Int]("page").default(1)) + .in(query[Int]("page-size").default(100)) + .out(jsonBody[SubjectPageDomainDumpDTO]) + .serverLogicPure { case (pageNo, pageSize) => + readService.getSubjectPageDomainDump(pageNo, pageSize).asRight }, - endpoint.put - .in("subjectpage" / path[Long]("subject-id").description("The subject id")) - .in(jsonBody[NewSubjectFrontPageDataDTO]) + endpoint.get + .in("dump" / "subjectpage" / path[Long]("subjectId")) + .out(jsonBody[SubjectPage]) .errorOut(errorOutputsFor(400, 404)) - .summary("Update subject page") - .out(jsonBody[SubjectPageDataDTO]) - .serverLogicPure { case (id, subjectPage) => - writeService - .updateSubjectPage(id, subjectPage, props.DefaultLanguage) - + .serverLogicPure { subjectId => + readService.domainSubjectPage(subjectId) } ) } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala index efef450ef4..3476691925 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/controller/SubjectPageController.scala @@ -9,15 +9,11 @@ package no.ndla.frontpageapi.controller import io.circe.generic.auto.* +import no.ndla.common.errors.ValidationException import no.ndla.common.model.api.CommaSeparatedList.* +import no.ndla.common.model.api.frontpage.SubjectPageDTO import no.ndla.frontpageapi.Props -import no.ndla.frontpageapi.model.api.{ - ErrorHandling, - NewSubjectFrontPageDataDTO, - SubjectPageDataDTO, - UpdatedSubjectFrontPageDataDTO -} -import no.ndla.frontpageapi.model.domain.Errors.ValidationException +import no.ndla.frontpageapi.model.api.{ErrorHandling, NewSubjectPageDTO, UpdatedSubjectPageDTO} import no.ndla.frontpageapi.service.{ReadService, WriteService} import no.ndla.network.tapir.NoNullJsonPrinter.jsonBody import no.ndla.network.tapir.TapirController @@ -42,7 +38,7 @@ trait SubjectPageController { .in(query[String]("language").default(props.DefaultLanguage)) .in(query[Boolean]("fallback").default(false)) .errorOut(errorOutputsFor(400, 404)) - .out(jsonBody[List[SubjectPageDataDTO]]) + .out(jsonBody[List[SubjectPageDTO]]) .serverLogicPure { case (page, pageSize, language, fallback) => readService.subjectPages(page, pageSize, language, fallback) } @@ -52,7 +48,7 @@ trait SubjectPageController { .in(path[Long]("subjectpage-id").description("The subjectpage id")) .in(query[String]("language").default(props.DefaultLanguage)) .in(query[Boolean]("fallback").default(false)) - .out(jsonBody[SubjectPageDataDTO]) + .out(jsonBody[SubjectPageDTO]) .errorOut(errorOutputsFor(400, 404)) .serverLogicPure { case (id, language, fallback) => readService.subjectPage(id, language, fallback) @@ -66,7 +62,7 @@ trait SubjectPageController { .in(query[Boolean]("fallback").default(false)) .in(query[Int]("page-size").default(props.DefaultPageSize)) .in(query[Int]("page").default(1)) - .out(jsonBody[List[SubjectPageDataDTO]]) + .out(jsonBody[List[SubjectPageDTO]]) .errorOut(errorOutputsFor(400, 404)) .serverLogicPure { case (ids, language, fallback, pageSize, page) => val parsedPageSize = if (pageSize < 1) props.DefaultPageSize else pageSize @@ -77,8 +73,8 @@ trait SubjectPageController { def createNewSubjectPage: ServerEndpoint[Any, Eff] = endpoint.post .summary("Create new subject page") - .in(jsonBody[NewSubjectFrontPageDataDTO]) - .out(jsonBody[SubjectPageDataDTO]) + .in(jsonBody[NewSubjectPageDTO]) + .out(jsonBody[SubjectPageDTO]) .errorOut(errorOutputsFor(400, 404)) .requirePermission(FRONTPAGE_API_WRITE) .serverLogicPure { _ => newSubjectFrontPageData => @@ -92,11 +88,11 @@ trait SubjectPageController { } def updateSubjectPage: ServerEndpoint[Any, Eff] = endpoint.patch .summary("Update subject page") - .in(jsonBody[UpdatedSubjectFrontPageDataDTO]) + .in(jsonBody[UpdatedSubjectPageDTO]) .in(path[Long]("subjectpage-id").description("The subjectpage id")) .in(query[String]("language").default(props.DefaultLanguage)) .in(query[Boolean]("fallback").default(false)) - .out(jsonBody[SubjectPageDataDTO]) + .out(jsonBody[SubjectPageDTO]) .errorOut(errorOutputsFor(400, 404)) .requirePermission(FRONTPAGE_API_WRITE) .serverLogicPure { _ => diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V10__subjectpage_visual_element_ids.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V10__subjectpage_visual_element_ids.scala index de8994c945..513002591b 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V10__subjectpage_visual_element_ids.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V10__subjectpage_visual_element_ids.scala @@ -10,10 +10,10 @@ package no.ndla.frontpageapi.db.migration import io.circe.parser.parse import io.circe.{Json, JsonObject} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -76,7 +76,7 @@ class V10__subjectpage_visual_element_ids extends BaseJavaMigration { } } - private def update(subjectPageData: V10__DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V10__DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V11__frontpage_hide_level_flag.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V11__frontpage_hide_level_flag.scala index d108acbaf3..a525f37e45 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V11__frontpage_hide_level_flag.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V11__frontpage_hide_level_flag.scala @@ -12,7 +12,7 @@ import io.circe.parser.parse import io.circe.{Json, JsonObject} import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -65,7 +65,7 @@ class V11__frontpage_hide_level_flag extends BaseJavaMigration { addHideLevelFlag(json).mapObject(_.remove("hideLevel")) } - private def update(frontPageData: V11__DBFrontPage)(implicit session: DBSession) = { + private def update(frontPageData: V11__DBFrontPage)(implicit session: DBSession): Int = { val pgObject = new PGobject() pgObject.setType("jsonb") pgObject.setValue(frontPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V2__convert_subjects_to_object.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V2__convert_subjects_to_object.scala index 8e137740b3..1f70f0c173 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V2__convert_subjects_to_object.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V2__convert_subjects_to_object.scala @@ -8,15 +8,15 @@ package no.ndla.frontpageapi.db.migration -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ -import io.circe.parser._ -import io.circe.syntax._ +import io.circe.generic.auto.* +import io.circe.generic.semiauto.* +import io.circe.parser.* +import io.circe.syntax.* import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -31,13 +31,13 @@ class V2__convert_subjects_to_object extends BaseJavaMigration { frontPageData.flatMap(convertSubjects).foreach(update) } - def frontPageData(implicit session: DBSession): Option[DBFrontPage] = { + def frontPageData(implicit session: DBSession): Option[V1_DBFrontPage] = { sql"select id, document from mainfrontpage" - .map(rs => DBFrontPage(rs.long("id"), rs.string("document"))) + .map(rs => V1_DBFrontPage(rs.long("id"), rs.string("document"))) .single() } - def convertSubjects(frontPage: DBFrontPage): Option[V2_FrontPageData] = { + def convertSubjects(frontPage: V1_DBFrontPage): Option[V2_FrontPageData] = { parse(frontPage.document).flatMap(_.as[V1_DBFrontPageData]).toTry match { case Success(value) => Some(V2_FrontPageData(value.topical, toDomainCategories(value.categories))) @@ -49,7 +49,7 @@ class V2__convert_subjects_to_object extends BaseJavaMigration { dbCategories.map(sc => V2_SubjectCollection(sc.name, sc.subjects.map(s => V2_SubjectFilters(s, List())))) } - private def update(frontPageData: V2_FrontPageData)(implicit session: DBSession) = { + private def update(frontPageData: V2_FrontPageData)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(frontPageData.asJson.noSpacesDropNull) @@ -63,6 +63,6 @@ case class V2_FrontPageData(topical: List[String], categories: List[V2_SubjectCo case class V2_SubjectCollection(name: String, subjects: List[V2_SubjectFilters]) case class V2_SubjectFilters(id: String, filters: List[String]) -case class DBFrontPage(id: Long, document: String) +case class V1_DBFrontPage(id: Long, document: String) case class V1_DBFrontPageData(topical: List[String], categories: List[V1_DBSubjectCollection]) case class V1_DBSubjectCollection(name: String, subjects: List[String]) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V3__introduce_layout.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V3__introduce_layout.scala index 6c2d817b14..3fcceb6298 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V3__introduce_layout.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V3__introduce_layout.scala @@ -8,15 +8,15 @@ package no.ndla.frontpageapi.db.migration -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ -import io.circe.parser._ -import io.circe.syntax._ +import io.circe.generic.auto.* +import io.circe.generic.semiauto.* +import io.circe.parser.* +import io.circe.syntax.* import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -31,13 +31,13 @@ class V3__introduce_layout extends BaseJavaMigration { subjectPageData.flatMap(convertSubjectpage).foreach(update) } - private def subjectPageData(implicit session: DBSession): List[DBSubjectPage] = { + private def subjectPageData(implicit session: DBSession): List[V2_DBSubjectPage] = { sql"select id, document from subjectpage" - .map(rs => DBSubjectPage(rs.long("id"), rs.string("document"))) + .map(rs => V2_DBSubjectPage(rs.long("id"), rs.string("document"))) .list() } - private def convertSubjectpage(subjectPageData: DBSubjectPage): Option[DBSubjectPage] = { + private def convertSubjectpage(subjectPageData: V2_DBSubjectPage): Option[V2_DBSubjectPage] = { parse(subjectPageData.document).flatMap(_.as[V2_SubjectFrontPageData]).toTry match { case Success(value) => val newSubjectPage = V3_SubjectFrontPageData( @@ -55,12 +55,12 @@ class V3__introduce_layout extends BaseJavaMigration { latestContent = value.latestContent, goTo = value.goTo ) - Some(DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) + Some(V2_DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) case Failure(_) => None } } - private def update(subjectPageData: DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V2_DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) @@ -70,7 +70,7 @@ class V3__introduce_layout extends BaseJavaMigration { } } -case class DBSubjectPage(id: Long, document: String) +case class V2_DBSubjectPage(id: Long, document: String) case class V2_SubjectFrontPageData( id: Option[Long], name: String, diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V4__add_language_to_about.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V4__add_language_to_about.scala index e981031451..703d106298 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V4__add_language_to_about.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V4__add_language_to_about.scala @@ -8,16 +8,16 @@ package no.ndla.frontpageapi.db.migration -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ +import io.circe.generic.auto.* +import io.circe.generic.semiauto.* import io.circe.parser.parse -import io.circe.syntax._ +import io.circe.syntax.* import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject import scalikejdbc.{DB, DBSession} -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -32,13 +32,13 @@ class V4__add_language_to_about extends BaseJavaMigration { subjectPageData.flatMap(convertSubjectpage).foreach(update) } - private def subjectPageData(implicit session: DBSession): List[DBSubjectPage] = { + private def subjectPageData(implicit session: DBSession): List[V2_DBSubjectPage] = { sql"select id, document from subjectpage" - .map(rs => DBSubjectPage(rs.long("id"), rs.string("document"))) + .map(rs => V2_DBSubjectPage(rs.long("id"), rs.string("document"))) .list() } - def convertSubjectpage(subjectPageData: DBSubjectPage): Option[DBSubjectPage] = { + def convertSubjectpage(subjectPageData: V2_DBSubjectPage): Option[V2_DBSubjectPage] = { parse(subjectPageData.document).flatMap(_.as[V3_SubjectFrontPageData]).toTry match { case Success(value) => val newSubjectPage = V4_SubjectFrontPageData( @@ -56,7 +56,7 @@ class V4__add_language_to_about extends BaseJavaMigration { latestContent = value.latestContent, goTo = value.goTo ) - Some(DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) + Some(V2_DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) case Failure(_) => None } } @@ -65,7 +65,7 @@ class V4__add_language_to_about extends BaseJavaMigration { Seq(V4_AboutSubject(aboutSubject.title, aboutSubject.description, "nb", aboutSubject.visualElement)) } - private def update(subjectPageData: DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V2_DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V5__add_meta_description.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V5__add_meta_description.scala index 67eb60cc6e..71c84918f4 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V5__add_meta_description.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V5__add_meta_description.scala @@ -8,16 +8,16 @@ package no.ndla.frontpageapi.db.migration -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ +import io.circe.generic.auto.* +import io.circe.generic.semiauto.* import io.circe.parser.parse -import io.circe.syntax._ +import io.circe.syntax.* import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject import scalikejdbc.{DB, DBSession} -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -32,13 +32,13 @@ class V5__add_meta_description extends BaseJavaMigration { subjectPageData.flatMap(convertSubjectpage).foreach(update) } - private def subjectPageData(implicit session: DBSession): List[DBSubjectPage] = { + private def subjectPageData(implicit session: DBSession): List[V2_DBSubjectPage] = { sql"select id, document from subjectpage" - .map(rs => DBSubjectPage(rs.long("id"), rs.string("document"))) + .map(rs => V2_DBSubjectPage(rs.long("id"), rs.string("document"))) .list() } - def convertSubjectpage(subjectPageData: DBSubjectPage): Option[DBSubjectPage] = { + def convertSubjectpage(subjectPageData: V2_DBSubjectPage): Option[V2_DBSubjectPage] = { parse(subjectPageData.document).flatMap(_.as[V4_SubjectFrontPageData]).toTry match { case Success(value) => val newSubjectPage = V5_SubjectFrontPageData( @@ -57,12 +57,12 @@ class V5__add_meta_description extends BaseJavaMigration { latestContent = value.latestContent, goTo = value.goTo ) - Some(DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) + Some(V2_DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) case Failure(_) => None } } - private def update(subjectPageData: DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V2_DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V8__add_subject_links.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V8__add_subject_links.scala index ee019fb059..054545d1c0 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V8__add_subject_links.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V8__add_subject_links.scala @@ -8,16 +8,16 @@ package no.ndla.frontpageapi.db.migration -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ +import io.circe.generic.auto.* +import io.circe.generic.semiauto.* import io.circe.parser.parse -import io.circe.syntax._ +import io.circe.syntax.* import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject import scalikejdbc.{DB, DBSession} -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -32,13 +32,13 @@ class V8__add_subject_links extends BaseJavaMigration { subjectPageData.flatMap(convertSubjectpage).foreach(update) } - private def subjectPageData(implicit session: DBSession): List[DBSubjectPage] = { + private def subjectPageData(implicit session: DBSession): List[V2_DBSubjectPage] = { sql"select id, document from subjectpage" - .map(rs => DBSubjectPage(rs.long("id"), rs.string("document"))) + .map(rs => V2_DBSubjectPage(rs.long("id"), rs.string("document"))) .list() } - def convertSubjectpage(subjectPageData: DBSubjectPage): Option[DBSubjectPage] = { + def convertSubjectpage(subjectPageData: V2_DBSubjectPage): Option[V2_DBSubjectPage] = { parse(subjectPageData.document).flatMap(_.as[V5_SubjectFrontPageData]).toTry match { case Success(value) => val newSubjectPage = V8_SubjectFrontPageData( @@ -52,12 +52,12 @@ class V8__add_subject_links extends BaseJavaMigration { buildsOn = List(), leadsTo = List() ) - Some(DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) + Some(V2_DBSubjectPage(subjectPageData.id, newSubjectPage.asJson.noSpacesDropNull)) case Failure(_) => None } } - private def update(subjectPageData: DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V2_DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields.scala index 1c360d3e8c..c40792584b 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields.scala @@ -10,10 +10,10 @@ package no.ndla.frontpageapi.db.migration import io.circe.parser.parse import io.circe.{Json, JsonObject} -import no.ndla.frontpageapi.repository._ +import no.ndla.frontpageapi.repository.* import org.flywaydb.core.api.migration.{BaseJavaMigration, Context} import org.postgresql.util.PGobject -import scalikejdbc._ +import scalikejdbc.* import scala.util.{Failure, Success} @@ -26,9 +26,9 @@ class V9__add_missing_fields extends BaseJavaMigration { .foreach(update) } - private def subjectPageData(implicit session: DBSession): List[DBSubjectPage] = { + private def subjectPageData(implicit session: DBSession): List[V2_DBSubjectPage] = { sql"select id, document from subjectpage" - .map(rs => DBSubjectPage(rs.long("id"), rs.string("document"))) + .map(rs => V2_DBSubjectPage(rs.long("id"), rs.string("document"))) .list() } @@ -39,7 +39,7 @@ class V9__add_missing_fields extends BaseJavaMigration { } } - def convertSubjectpage(subjectPageData: DBSubjectPage): DBSubjectPage = + def convertSubjectpage(subjectPageData: V2_DBSubjectPage): V2_DBSubjectPage = parse(subjectPageData.document).toTry match { case Success(value) => val newSubjectPage = value.mapObject(obj => { @@ -56,13 +56,13 @@ class V9__add_missing_fields extends BaseJavaMigration { .remove("facebook") .remove("goTo") }) - DBSubjectPage(subjectPageData.id, newSubjectPage.noSpacesDropNull) + V2_DBSubjectPage(subjectPageData.id, newSubjectPage.noSpacesDropNull) case Failure(ex) => println(s"Failed to parse subject page data for id '${subjectPageData.id}': ${ex.getMessage}") throw ex } - private def update(subjectPageData: DBSubjectPage)(implicit session: DBSession) = { + private def update(subjectPageData: V2_DBSubjectPage)(implicit session: DBSession): Int = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subjectPageData.document) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutFilmSubjectDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutFilmSubjectDTO.scala index 3be2444814..3d6aac440b 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutFilmSubjectDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutFilmSubjectDTO.scala @@ -8,6 +8,8 @@ package no.ndla.frontpageapi.model.api +import no.ndla.common.model.api.frontpage.VisualElementDTO + case class AboutFilmSubjectDTO( title: String, description: String, diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutSubjectDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutSubjectDTO.scala deleted file mode 100644 index a15ff50e5c..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/AboutSubjectDTO.scala +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.api - -case class AboutSubjectDTO( - title: String, - description: String, - visualElement: VisualElementDTO -) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/BannerImageDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/BannerImageDTO.scala deleted file mode 100644 index 2b5d007016..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/BannerImageDTO.scala +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.api - -case class BannerImageDTO( - mobileUrl: Option[String], - mobileId: Option[Long], - desktopUrl: String, - desktopId: Long -) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/ErrorHandling.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/ErrorHandling.scala index e6b74e81e6..c8176914da 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/ErrorHandling.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/ErrorHandling.scala @@ -9,19 +9,15 @@ package no.ndla.frontpageapi.model.api import no.ndla.common.Clock -import no.ndla.common.errors.NotFoundException +import no.ndla.common.errors.{NotFoundException, ValidationException} import no.ndla.frontpageapi.Props -import no.ndla.frontpageapi.model.domain.Errors.{ - LanguageNotFoundException, - SubjectPageNotFoundException, - ValidationException -} +import no.ndla.frontpageapi.model.domain.Errors.{LanguageNotFoundException, SubjectPageNotFoundException} import no.ndla.network.tapir.{ErrorBody, TapirErrorHandling} trait ErrorHandling extends TapirErrorHandling { - this: Props with Clock => + this: Props & Clock => - import ErrorHelpers._ + import ErrorHelpers.* override def handleErrors: PartialFunction[Throwable, ErrorBody] = { case ex: ValidationException => badRequest(ex.getMessage) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDataDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala similarity index 89% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDataDTO.scala rename to frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala index 4bdf0a4c22..9e90226fec 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDataDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/FilmFrontPageDTO.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.model.api -case class FilmFrontPageDataDTO( +case class FilmFrontPageDTO( name: String, about: Seq[AboutFilmSubjectDTO], movieThemes: Seq[MovieThemeDTO], diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/MovieThemeDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/MovieThemeDTO.scala index 3e7dd89771..21e35fa49e 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/MovieThemeDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/MovieThemeDTO.scala @@ -9,5 +9,4 @@ package no.ndla.frontpageapi.model.api case class MovieThemeDTO(name: Seq[MovieThemeNameDTO], movies: Seq[String]) - case class MovieThemeNameDTO(name: String, language: String) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDataDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDTO.scala similarity index 86% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDataDTO.scala rename to frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDTO.scala index e778ba1b76..e7044851ba 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDataDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedFilmFrontPageDTO.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.model.api -case class NewOrUpdatedFilmFrontPageDataDTO( +case class NewOrUpdatedFilmFrontPageDTO( name: String, about: Seq[NewOrUpdatedAboutSubjectDTO], movieThemes: Seq[NewOrUpdatedMovieThemeDTO], diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedMovieThemeDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedMovieThemeDTO.scala index b481731b8a..bdd649bed3 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedMovieThemeDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewOrUpdatedMovieThemeDTO.scala @@ -9,5 +9,4 @@ package no.ndla.frontpageapi.model.api case class NewOrUpdatedMovieThemeDTO(name: Seq[NewOrUpdatedMovieNameDTO], movies: Seq[String]) - case class NewOrUpdatedMovieNameDTO(name: String, language: String) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectFrontPageDataDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectPageDTO.scala similarity index 92% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectFrontPageDataDTO.scala rename to frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectPageDTO.scala index 18842f9f20..f72f7f7746 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectFrontPageDataDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/NewSubjectPageDTO.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.model.api -case class NewSubjectFrontPageDataDTO( +case class NewSubjectPageDTO( name: String, externalId: Option[String], banner: NewOrUpdateBannerImageDTO, diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDataDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDataDTO.scala deleted file mode 100644 index 479ca1419c..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDataDTO.scala +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.api - -import io.circe.generic.semiauto._ -import io.circe.generic.auto._ -import io.circe._ - -case class SubjectPageDataDTO( - id: Long, - name: String, - banner: BannerImageDTO, - about: Option[AboutSubjectDTO], - metaDescription: Option[String], - editorsChoices: List[String], - supportedLanguages: Seq[String], - connectedTo: List[String], - buildsOn: List[String], - leadsTo: List[String] -) - -object SubjectPageDataDTO { - implicit def encoder: Encoder[SubjectPageDataDTO] = deriveEncoder[SubjectPageDataDTO] - - implicit def decoder: Decoder[SubjectPageDataDTO] = deriveDecoder[SubjectPageDataDTO] -} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDomainDumpDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDomainDumpDTO.scala new file mode 100644 index 0000000000..b3777695a9 --- /dev/null +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageDomainDumpDTO.scala @@ -0,0 +1,19 @@ +/* + * Part of NDLA frontpage-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.frontpageapi.model.api + +import no.ndla.common.model.domain.frontpage.SubjectPage +import sttp.tapir.Schema.annotations.description + +@description("All the subjectpages") +case class SubjectPageDomainDumpDTO( + @description("The total number of articles 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[SubjectPage] +) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageIdDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageIdDTO.scala index cd4207dc4f..7651982a4e 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageIdDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/SubjectPageIdDTO.scala @@ -8,13 +8,12 @@ package no.ndla.frontpageapi.model.api -import io.circe.generic.semiauto._ +import io.circe.generic.semiauto.* import io.circe.{Decoder, Encoder} case class SubjectPageIdDTO(id: Long) object SubjectPageIdDTO { implicit def encoder: Encoder.AsObject[SubjectPageIdDTO] = deriveEncoder[SubjectPageIdDTO] - - implicit def decoder: Decoder[SubjectPageIdDTO] = deriveDecoder[SubjectPageIdDTO] + implicit def decoder: Decoder[SubjectPageIdDTO] = deriveDecoder[SubjectPageIdDTO] } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectFrontPageDataDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectPageDTO.scala similarity index 92% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectFrontPageDataDTO.scala rename to frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectPageDTO.scala index e0ab3040ed..3131f9b083 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectFrontPageDataDTO.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/UpdatedSubjectPageDTO.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.model.api -case class UpdatedSubjectFrontPageDataDTO( +case class UpdatedSubjectPageDTO( name: Option[String], externalId: Option[String], banner: Option[NewOrUpdateBannerImageDTO], diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/VisualElementDTO.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/VisualElementDTO.scala deleted file mode 100644 index 31d036330a..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/api/VisualElementDTO.scala +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.api - -case class VisualElementDTO(`type`: String, url: String, alt: Option[String]) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/BannerImage.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/BannerImage.scala deleted file mode 100644 index 465e6b64f4..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/BannerImage.scala +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain - -case class BannerImage(mobileImageId: Option[Long], desktopImageId: Long) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/DBSubjectPage.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/DBSubjectPage.scala new file mode 100644 index 0000000000..563c456e12 --- /dev/null +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/DBSubjectPage.scala @@ -0,0 +1,35 @@ +/* + * Part of NDLA frontpage-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.frontpageapi.model.domain + +import cats.implicits.catsSyntaxOptionId +import no.ndla.common.model.domain.frontpage.SubjectPage +import no.ndla.frontpageapi.Props +import scalikejdbc.* + +import scala.util.Try + +trait DBSubjectPage { + this: Props => + + object DBSubjectPage extends SQLSyntaxSupport[SubjectPage] { + override val tableName = "subjectpage" + override val schemaName: Option[String] = props.MetaSchema.some + + def fromDb(lp: SyntaxProvider[SubjectPage])(rs: WrappedResultSet): Try[SubjectPage] = + fromDb(lp.resultName)(rs) + + private def fromDb(lp: ResultName[SubjectPage])(rs: WrappedResultSet): Try[SubjectPage] = { + val id = rs.long(lp.c("id")) + val document = rs.string(lp.c("document")) + + SubjectPage.decodeJson(document, id) + } + + } +} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Errors.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Errors.scala index e7714dec48..19895ddf24 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Errors.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Errors.scala @@ -12,6 +12,5 @@ object Errors { case class SubjectPageNotFoundException(id: Long) extends RuntimeException(s"The page with id $id was not found") case class LanguageNotFoundException(message: String, supportedLanguages: Seq[String] = Seq.empty) extends RuntimeException(message) - case class ValidationException(message: String) extends RuntimeException(message) case class OperationNotAllowedException(message: String) extends RuntimeException(message) } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPageData.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala similarity index 50% rename from frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPageData.scala rename to frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala index 169634fbc7..6692bd5db4 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPageData.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FilmFrontPage.scala @@ -8,17 +8,18 @@ package no.ndla.frontpageapi.model.domain -import cats.implicits._ -import io.circe.generic.auto._ -import io.circe.generic.semiauto._ -import io.circe.parser._ +import cats.implicits.* +import io.circe.generic.semiauto.* +import io.circe.generic.auto.* +import io.circe.parser.* import io.circe.{Decoder, Encoder} +import no.ndla.common.model.domain.frontpage.{AboutSubject, MovieTheme} import no.ndla.frontpageapi.Props -import scalikejdbc.{WrappedResultSet, _} +import scalikejdbc.{WrappedResultSet, *} import scala.util.Try -case class FilmFrontPageData( +case class FilmFrontPage( name: String, about: Seq[AboutSubject], movieThemes: Seq[MovieTheme], @@ -26,28 +27,28 @@ case class FilmFrontPageData( article: Option[String] ) -object FilmFrontPageData { - implicit val decoder: Decoder[FilmFrontPageData] = deriveDecoder - implicit val encoder: Encoder[FilmFrontPageData] = deriveEncoder +object FilmFrontPage { + implicit val decoder: Decoder[FilmFrontPage] = deriveDecoder + implicit val encoder: Encoder[FilmFrontPage] = deriveEncoder - private[domain] def decodeJson(json: String): Try[FilmFrontPageData] = { - parse(json).flatMap(_.as[FilmFrontPageData]).toTry + private[domain] def decodeJson(json: String): Try[FilmFrontPage] = { + parse(json).flatMap(_.as[FilmFrontPage]).toTry } } -trait DBFilmFrontPageData { +trait DBFilmFrontPage { this: Props => - object DBFilmFrontPageData extends SQLSyntaxSupport[FilmFrontPageData] { + object DBFilmFrontPageData extends SQLSyntaxSupport[FilmFrontPage] { override val tableName = "filmfrontpage" override val schemaName: Option[String] = props.MetaSchema.some - def fromDb(lp: SyntaxProvider[FilmFrontPageData])(rs: WrappedResultSet): Try[FilmFrontPageData] = + def fromDb(lp: SyntaxProvider[FilmFrontPage])(rs: WrappedResultSet): Try[FilmFrontPage] = fromDb(lp.resultName)(rs) - private def fromDb(lp: ResultName[FilmFrontPageData])(rs: WrappedResultSet): Try[FilmFrontPageData] = { + private def fromDb(lp: ResultName[FilmFrontPage])(rs: WrappedResultSet): Try[FilmFrontPage] = { val document = rs.string(lp.c("document")) - FilmFrontPageData.decodeJson(document) + FilmFrontPage.decodeJson(document) } } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FrontPage.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FrontPage.scala index 1dc45c614e..51c46f4f1f 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FrontPage.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/FrontPage.scala @@ -9,13 +9,13 @@ package no.ndla.frontpageapi.model.domain import io.circe.Encoder -import io.circe.generic.semiauto._ -import io.circe.generic.auto._ -import io.circe.parser._ +import io.circe.generic.semiauto.* +import io.circe.generic.auto.* +import io.circe.parser.* import no.ndla.frontpageapi.Props import scalikejdbc.WrappedResultSet -import scalikejdbc._ -import cats.implicits._ +import scalikejdbc.* +import cats.implicits.* import scala.util.Try @@ -34,7 +34,7 @@ object FrontPage { } } -trait DBFrontPageData { +trait DBFrontPage { this: Props => object DBFrontPageData extends SQLSyntaxSupport[FrontPage] { diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Layout.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Layout.scala deleted file mode 100644 index a5f141c559..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/Layout.scala +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain -import no.ndla.frontpageapi.model.domain.Errors.ValidationException - -import scala.util.{Failure, Success, Try} - -case class Layout(`type`: LayoutType.Value) - -object LayoutType extends Enumeration { - val Single: LayoutType.Value = Value("single") - val Double: LayoutType.Value = Value("double") - val Stacked: LayoutType.Value = Value("stacked") - - def fromString(string: String): Try[LayoutType.Value] = - LayoutType.values.find(_.toString == string) match { - case Some(v) => Success(v) - case None => Failure(ValidationException(s"'$string' is an invalid layout")) - } -} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MetaDescription.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MetaDescription.scala deleted file mode 100644 index 0a54d63508..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/MetaDescription.scala +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain - -import no.ndla.language.model.LanguageField - -case class MetaDescription(metaDescription: String, language: String) extends LanguageField[String] { - override def isEmpty: Boolean = metaDescription.isEmpty - override def value: String = metaDescription -} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectCollection.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectCollection.scala deleted file mode 100644 index 701ff007c5..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectCollection.scala +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain - -case class SubjectCollection(name: String, subjects: List[SubjectFilters]) -case class SubjectFilters(id: String, filters: List[String]) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageData.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageData.scala deleted file mode 100644 index 3c0fc73e2f..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageData.scala +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain - -import cats.implicits._ -import io.circe.generic.auto._ -import io.circe.parser._ -import io.circe.{Decoder, Encoder} -import no.ndla.frontpageapi.Props -import no.ndla.language.Language.getSupportedLanguages -import scalikejdbc.{WrappedResultSet, _} - -import scala.util.Try - -case class SubjectFrontPageData( - id: Option[Long], - name: String, - bannerImage: BannerImage, - about: Seq[AboutSubject], - metaDescription: Seq[MetaDescription], - editorsChoices: List[String], - connectedTo: List[String], - buildsOn: List[String], - leadsTo: List[String] -) { - - def supportedLanguages: Seq[String] = getSupportedLanguages(about, metaDescription) -} - -object SubjectFrontPageData { - implicit val layoutDecoder: Decoder[LayoutType.Value] = Decoder.decodeEnumeration(LayoutType) - implicit val layoutEncoder: Encoder[LayoutType.Value] = Encoder.encodeEnumeration(LayoutType) - - private[domain] def decodeJson(json: String, id: Long): Try[SubjectFrontPageData] = { - parse(json).flatMap(_.as[SubjectFrontPageData]).map(_.copy(id = id.some)).toTry - } -} - -trait DBSubjectFrontPageData { - this: Props => - - object DBSubjectFrontPageData extends SQLSyntaxSupport[SubjectFrontPageData] { - override val tableName = "subjectpage" - override val schemaName: Option[String] = props.MetaSchema.some - - def fromDb(lp: SyntaxProvider[SubjectFrontPageData])(rs: WrappedResultSet): Try[SubjectFrontPageData] = - fromDb(lp.resultName)(rs) - - private def fromDb(lp: ResultName[SubjectFrontPageData])(rs: WrappedResultSet): Try[SubjectFrontPageData] = { - val id = rs.long(lp.c("id")) - val document = rs.string(lp.c("document")) - - SubjectFrontPageData.decodeJson(document, id) - } - - } -} diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectTopical.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectTopical.scala deleted file mode 100644 index 8f3ccc6fa3..0000000000 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/model/domain/SubjectTopical.scala +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Part of NDLA frontpage-api - * Copyright (C) 2018 NDLA - * - * See LICENSE - * - */ - -package no.ndla.frontpageapi.model.domain - -case class SubjectTopical(location: Int, id: String) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala index 9ef7869f0e..9a7dab7052 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FilmFrontPageRepository.scala @@ -10,7 +10,7 @@ package no.ndla.frontpageapi.repository import io.circe.syntax.* import no.ndla.database.DataSource -import no.ndla.frontpageapi.model.domain.{DBFilmFrontPageData, FilmFrontPageData} +import no.ndla.frontpageapi.model.domain.{DBFilmFrontPage, FilmFrontPage} import org.log4s.Logger import org.postgresql.util.PGobject import scalikejdbc.* @@ -18,14 +18,14 @@ import scalikejdbc.* import scala.util.{Failure, Success, Try} trait FilmFrontPageRepository { - this: DataSource with DBFilmFrontPageData => + this: DataSource & DBFilmFrontPage => val filmFrontPageRepository: FilmFrontPageRepository class FilmFrontPageRepository { val logger: Logger = org.log4s.getLogger - import FilmFrontPageData._ + import FilmFrontPage._ - def newFilmFrontPage(page: FilmFrontPageData)(implicit session: DBSession = AutoSession): Try[FilmFrontPageData] = { + def newFilmFrontPage(page: FilmFrontPage)(implicit session: DBSession = AutoSession): Try[FilmFrontPage] = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(page.asJson.noSpacesDropNull) @@ -43,7 +43,7 @@ trait FilmFrontPageRepository { ).map(_ => id) } - def get(implicit session: DBSession = ReadOnlyAutoSession): Option[FilmFrontPageData] = { + def get(implicit session: DBSession = ReadOnlyAutoSession): Option[FilmFrontPage] = { val fr = DBFilmFrontPageData.syntax("fr") Try( diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FrontPageRepository.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FrontPageRepository.scala index 58abd3541f..5a59863413 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FrontPageRepository.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/FrontPageRepository.scala @@ -10,7 +10,7 @@ package no.ndla.frontpageapi.repository import com.typesafe.scalalogging.StrictLogging import io.circe.syntax.* -import no.ndla.frontpageapi.model.domain.{DBFrontPageData, FrontPage} +import no.ndla.frontpageapi.model.domain.{DBFrontPage, FrontPage} import org.postgresql.util.PGobject import scalikejdbc.* import cats.implicits.* @@ -19,7 +19,7 @@ import no.ndla.database.DataSource import scala.util.Try trait FrontPageRepository { - this: DataSource with DBFrontPageData => + this: DataSource & DBFrontPage => val frontPageRepository: FrontPageRepository class FrontPageRepository extends StrictLogging { diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/SubjectPageRepository.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/SubjectPageRepository.scala index 52f800236c..0974791b42 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/SubjectPageRepository.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/repository/SubjectPageRepository.scala @@ -9,32 +9,33 @@ package no.ndla.frontpageapi.repository import cats.implicits.* -import no.ndla.frontpageapi.model.domain.{DBSubjectFrontPageData, SubjectFrontPageData} -import org.log4s.getLogger +import org.log4s.{Logger, getLogger} import org.postgresql.util.PGobject import scalikejdbc.* import io.circe.syntax.* import io.circe.generic.auto.* +import no.ndla.common.model.domain.frontpage.SubjectPage import no.ndla.database.DataSource +import no.ndla.frontpageapi.model.domain.DBSubjectPage import scala.util.{Failure, Success, Try} trait SubjectPageRepository { - this: DataSource with DBSubjectFrontPageData => + this: DataSource & DBSubjectPage => val subjectPageRepository: SubjectPageRepository class SubjectPageRepository { - val logger = getLogger + val logger: Logger = getLogger - def newSubjectPage(subj: SubjectFrontPageData, externalId: String)(implicit + def newSubjectPage(subj: SubjectPage, externalId: String)(implicit session: DBSession = AutoSession - ): Try[SubjectFrontPageData] = { + ): Try[SubjectPage] = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subj.copy(id = None).asJson.noSpacesDropNull) Try( - sql"insert into ${DBSubjectFrontPageData.table} (document, external_id) values (${dataObject}, ${externalId})" + sql"insert into ${DBSubjectPage.table} (document, external_id) values (${dataObject}, ${externalId})" .updateAndReturnGeneratedKey() ).map(id => { logger.info(s"Inserted new subject page: $id") @@ -43,58 +44,58 @@ trait SubjectPageRepository { } def updateSubjectPage( - subj: SubjectFrontPageData - )(implicit session: DBSession = AutoSession): Try[SubjectFrontPageData] = { + subj: SubjectPage + )(implicit session: DBSession = AutoSession): Try[SubjectPage] = { val dataObject = new PGobject() dataObject.setType("jsonb") dataObject.setValue(subj.copy(id = None).asJson.noSpacesDropNull) - Try(sql"update ${DBSubjectFrontPageData.table} set document=${dataObject} where id=${subj.id}".update()) + Try(sql"update ${DBSubjectPage.table} set document=${dataObject} where id=${subj.id}".update()) .map(_ => subj) } def all(offset: Int, limit: Int)(implicit session: DBSession = ReadOnlyAutoSession - ): Try[List[SubjectFrontPageData]] = { - val su = DBSubjectFrontPageData.syntax("su") + ): Try[List[SubjectPage]] = { + val su = DBSubjectPage.syntax("su") Try { sql""" select ${su.result.*} - from ${DBSubjectFrontPageData.as(su)} + from ${DBSubjectPage.as(su)} where su.document is not null order by su.id offset $offset limit $limit """ - .map(DBSubjectFrontPageData.fromDb(su)) + .map(DBSubjectPage.fromDb(su)) .list() .sequence }.flatten } - def withId(subjectId: Long): Try[Option[SubjectFrontPageData]] = + def withId(subjectId: Long): Try[Option[SubjectPage]] = subjectPageWhere(sqls"su.id=${subjectId.toInt}") def withIds(subjectIds: List[Long], offset: Int, pageSize: Int)(implicit session: DBSession = AutoSession - ): Try[List[SubjectFrontPageData]] = Try { - val su = DBSubjectFrontPageData.syntax("su") + ): Try[List[SubjectPage]] = Try { + val su = DBSubjectPage.syntax("su") sql""" select ${su.result.*} - from ${DBSubjectFrontPageData.as(su)} + from ${DBSubjectPage.as(su)} where su.document is not NULL and su.id in ($subjectIds) offset $offset limit $pageSize """ - .map(DBSubjectFrontPageData.fromDb(su)) + .map(DBSubjectPage.fromDb(su)) .list() .sequence }.flatten def getIdFromExternalId(externalId: String)(implicit sesstion: DBSession = AutoSession): Try[Option[Long]] = { Try( - sql"select id from ${DBSubjectFrontPageData.table} where external_id=${externalId}" + sql"select id from ${DBSubjectPage.table} where external_id=${externalId}" .map(rs => rs.long("id")) .single() ) @@ -102,20 +103,27 @@ trait SubjectPageRepository { def exists(subjectId: Long)(implicit sesstion: DBSession = AutoSession): Try[Boolean] = { Try( - sql"select id from ${DBSubjectFrontPageData.table} where id=${subjectId}" + sql"select id from ${DBSubjectPage.table} where id=${subjectId}" .map(rs => rs.long("id")) .single() ).map(_.isDefined) } + def totalCount(implicit session: DBSession = ReadOnlyAutoSession): Long = { + sql"select count(*) from ${DBSubjectPage.table} where document is not NULL" + .map(rs => rs.long("count")) + .single() + .getOrElse(0) + } + private def subjectPageWhere( whereClause: SQLSyntax - )(implicit session: DBSession = ReadOnlyAutoSession): Try[Option[SubjectFrontPageData]] = { - val su = DBSubjectFrontPageData.syntax("su") + )(implicit session: DBSession = ReadOnlyAutoSession): Try[Option[SubjectPage]] = { + val su = DBSubjectPage.syntax("su") Try( - sql"select ${su.result.*} from ${DBSubjectFrontPageData.as(su)} where su.document is not NULL and $whereClause" - .map(DBSubjectFrontPageData.fromDb(su)) + sql"select ${su.result.*} from ${DBSubjectPage.as(su)} where su.document is not NULL and $whereClause" + .map(DBSubjectPage.fromDb(su)) .single() ) match { case Success(Some(Success(s))) => Success(Some(s)) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala index 1d9a9c25a3..a0a1256faf 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ConverterService.scala @@ -9,13 +9,24 @@ package no.ndla.frontpageapi.service import no.ndla.frontpageapi.model.domain.Errors.LanguageNotFoundException -import no.ndla.frontpageapi.model.domain.* import no.ndla.frontpageapi.model.{api, domain} import scala.util.{Failure, Success, Try} import cats.implicits.* import no.ndla.common.errors.MissingIdException import no.ndla.common.model +import no.ndla.common.model.api.frontpage.{AboutSubjectDTO, BannerImageDTO, SubjectPageDTO, VisualElementDTO} +import no.ndla.common.model.domain.frontpage +import no.ndla.common.model.domain.frontpage.{ + AboutSubject, + BannerImage, + MetaDescription, + MovieTheme, + MovieThemeName, + SubjectPage, + VisualElement, + VisualElementType +} import no.ndla.frontpageapi.Props import no.ndla.language.Language.{findByLanguageOrBestEffort, mergeLanguageFields} @@ -31,16 +42,50 @@ trait ConverterService { def toApiFrontPage(frontPage: domain.FrontPage): model.api.FrontPageDTO = model.api.FrontPageDTO(articleId = frontPage.articleId, menu = frontPage.menu.map(toApiMenu)) - private def toApiBannerImage(banner: domain.BannerImage): api.BannerImageDTO = - api.BannerImageDTO( + private def toApiBannerImage(banner: BannerImage): BannerImageDTO = + model.api.frontpage.BannerImageDTO( banner.mobileImageId.map(createImageUrl), banner.mobileImageId, createImageUrl(banner.desktopImageId), banner.desktopImageId ) - def toApiFilmFrontPage(page: domain.FilmFrontPageData, language: Option[String]): api.FilmFrontPageDataDTO = { - api.FilmFrontPageDataDTO( + def toApiSubjectPage( + sub: SubjectPage, + language: String, + fallback: Boolean = false + ): Try[SubjectPageDTO] = { + if (sub.supportedLanguages.contains(language) || fallback) { + sub.id match { + case None => Failure(MissingIdException("No id found for subjectpage while converting, this is a bug.")) + case Some(subjectPageId) => + Success( + SubjectPageDTO( + subjectPageId, + sub.name, + toApiBannerImage(sub.bannerImage), + toApiAboutSubject(findByLanguageOrBestEffort(sub.about, language)), + toApiMetaDescription(findByLanguageOrBestEffort(sub.metaDescription, language)), + sub.editorsChoices, + sub.supportedLanguages, + sub.connectedTo, + sub.buildsOn, + sub.leadsTo + ) + ) + } + } else { + Failure( + LanguageNotFoundException( + s"The subjectpage with id ${sub.id.get} and language $language was not found", + sub.supportedLanguages + ) + ) + } + } + + def toApiFilmFrontPage(page: domain.FilmFrontPage, language: Option[String]): api.FilmFrontPageDTO = { + api.FilmFrontPageDTO( page.name, toApiAboutFilmSubject(page.about, language), toApiMovieThemes(page.movieThemes, language), @@ -50,7 +95,7 @@ trait ConverterService { } private def toApiAboutFilmSubject( - aboutSeq: Seq[domain.AboutSubject], + aboutSeq: Seq[AboutSubject], language: Option[String] ): Seq[api.AboutFilmSubjectDTO] = { val filteredAboutSeq = language match { @@ -62,12 +107,12 @@ trait ConverterService { ) } - private def toApiMovieThemes(themes: Seq[domain.MovieTheme], language: Option[String]): Seq[api.MovieThemeDTO] = { + private def toApiMovieThemes(themes: Seq[MovieTheme], language: Option[String]): Seq[api.MovieThemeDTO] = { themes.map(theme => api.MovieThemeDTO(toApiMovieName(theme.name, language), theme.movies)) } private def toApiMovieName( - names: Seq[domain.MovieThemeName], + names: Seq[MovieThemeName], language: Option[String] ): Seq[api.MovieThemeNameDTO] = { val filteredNames = language match { @@ -78,72 +123,35 @@ trait ConverterService { filteredNames.map(name => api.MovieThemeNameDTO(name.name, name.language)) } - def toApiSubjectPage( - sub: domain.SubjectFrontPageData, - language: String, - fallback: Boolean = false - ): Try[api.SubjectPageDataDTO] = { - if (sub.supportedLanguages.contains(language) || fallback) { - sub.id match { - case None => - Failure( - MissingIdException(s"Could not convert to api.SubjectPageData since domain object did not have an id") - ) - case Some(subjectPageId) => - Success( - api.SubjectPageDataDTO( - subjectPageId, - sub.name, - toApiBannerImage(sub.bannerImage), - toApiAboutSubject(findByLanguageOrBestEffort(sub.about, language)), - toApiMetaDescription(findByLanguageOrBestEffort(sub.metaDescription, language)), - sub.editorsChoices, - sub.supportedLanguages, - sub.connectedTo, - sub.buildsOn, - sub.leadsTo - ) - ) - } - } else { - Failure( - LanguageNotFoundException( - s"The subjectpage with id ${sub.id.get} and language $language was not found", - sub.supportedLanguages - ) - ) - } - } - - private def toApiAboutSubject(about: Option[domain.AboutSubject]): Option[api.AboutSubjectDTO] = { + private def toApiAboutSubject(about: Option[AboutSubject]): Option[AboutSubjectDTO] = { about - .map(about => api.AboutSubjectDTO(about.title, about.description, toApiVisualElement(about.visualElement))) + .map(about => AboutSubjectDTO(about.title, about.description, toApiVisualElement(about.visualElement))) } - private def toApiMetaDescription(meta: Option[domain.MetaDescription]): Option[String] = { + private def toApiMetaDescription(meta: Option[MetaDescription]): Option[String] = { meta .map(_.metaDescription) } - private def toApiVisualElement(visual: domain.VisualElement): api.VisualElementDTO = { + private def toApiVisualElement(visual: VisualElement): VisualElementDTO = { val url = visual.`type` match { case VisualElementType.Image => createImageUrl(visual.id.toLong) case VisualElementType.Brightcove => s"https://players.brightcove.net/$BrightcoveAccountId/${BrightcovePlayer}_default/index.html?videoId=${visual.id}" } - api.VisualElementDTO(visual.`type`.entryName, url, visual.alt) + VisualElementDTO(visual.`type`.entryName, url, visual.alt) } - def toDomainSubjectPage(id: Long, subject: api.NewSubjectFrontPageDataDTO): Try[domain.SubjectFrontPageData] = + def toDomainSubjectPage(id: Long, subject: api.NewSubjectPageDTO): Try[SubjectPage] = toDomainSubjectPage(subject).map(_.copy(id = Some(id))) - private def toDomainBannerImage(banner: api.NewOrUpdateBannerImageDTO): domain.BannerImage = - domain.BannerImage(banner.mobileImageId, banner.desktopImageId) + private def toDomainBannerImage(banner: api.NewOrUpdateBannerImageDTO): BannerImage = + frontpage.BannerImage(banner.mobileImageId, banner.desktopImageId) - def toDomainSubjectPage(subject: api.NewSubjectFrontPageDataDTO): Try[domain.SubjectFrontPageData] = { + def toDomainSubjectPage(subject: api.NewSubjectPageDTO): Try[SubjectPage] = { for { about <- toDomainAboutSubject(subject.about) - newSubject = domain.SubjectFrontPageData( + newSubject = frontpage.SubjectPage( id = None, name = subject.name, bannerImage = toDomainBannerImage(subject.banner), @@ -159,9 +167,9 @@ trait ConverterService { } def toDomainSubjectPage( - toMergeInto: domain.SubjectFrontPageData, - subject: api.UpdatedSubjectFrontPageDataDTO - ): Try[domain.SubjectFrontPageData] = { + toMergeInto: SubjectPage, + subject: api.UpdatedSubjectPageDTO + ): Try[SubjectPage] = { for { aboutSubject <- subject.about.traverse(toDomainAboutSubject) metaDescription = subject.metaDescription.map(toDomainMetaDescription) @@ -179,23 +187,23 @@ trait ConverterService { } yield merged } - private def toDomainAboutSubject(aboutSeq: Seq[api.NewOrUpdatedAboutSubjectDTO]): Try[Seq[domain.AboutSubject]] = { + private def toDomainAboutSubject(aboutSeq: Seq[api.NewOrUpdatedAboutSubjectDTO]): Try[Seq[AboutSubject]] = { aboutSeq.traverse(about => toDomainVisualElement(about.visualElement) - .map(domain.AboutSubject(about.title, about.description, about.language, _)) + .map(frontpage.AboutSubject(about.title, about.description, about.language, _)) ) } private def toDomainMetaDescription( metaSeq: Seq[api.NewOrUpdatedMetaDescriptionDTO] - ): Seq[domain.MetaDescription] = { - metaSeq.map(meta => domain.MetaDescription(meta.metaDescription, meta.language)) + ): Seq[MetaDescription] = { + metaSeq.map(meta => frontpage.MetaDescription(meta.metaDescription, meta.language)) } - private def toDomainVisualElement(visual: api.NewOrUpdatedVisualElementDTO): Try[domain.VisualElement] = + private def toDomainVisualElement(visual: api.NewOrUpdatedVisualElementDTO): Try[VisualElement] = for { t <- VisualElementType.fromString(visual.`type`) - ve = domain.VisualElement(t, visual.id, visual.alt) + ve = frontpage.VisualElement(t, visual.id, visual.alt) validated <- VisualElementType.validateVisualElement(ve) } yield validated @@ -208,9 +216,9 @@ trait ConverterService { domain.FrontPage(articleId = page.articleId, menu = page.menu.map(toDomainMenu)) } - def toDomainFilmFrontPage(page: api.NewOrUpdatedFilmFrontPageDataDTO): Try[domain.FilmFrontPageData] = { + def toDomainFilmFrontPage(page: api.NewOrUpdatedFilmFrontPageDTO): Try[domain.FilmFrontPage] = { val withoutAboutSubject = - domain.FilmFrontPageData(page.name, Seq(), toDomainMovieThemes(page.movieThemes), page.slideShow, page.article) + domain.FilmFrontPage(page.name, Seq(), toDomainMovieThemes(page.movieThemes), page.slideShow, page.article) toDomainAboutSubject(page.about) match { case Failure(ex) => Failure(ex) @@ -218,12 +226,12 @@ trait ConverterService { } } - private def toDomainMovieThemes(themes: Seq[api.NewOrUpdatedMovieThemeDTO]): Seq[domain.MovieTheme] = { - themes.map(theme => domain.MovieTheme(toDomainMovieNames(theme.name), theme.movies)) + private def toDomainMovieThemes(themes: Seq[api.NewOrUpdatedMovieThemeDTO]): Seq[MovieTheme] = { + themes.map(theme => frontpage.MovieTheme(toDomainMovieNames(theme.name), theme.movies)) } - private def toDomainMovieNames(names: Seq[api.NewOrUpdatedMovieNameDTO]): Seq[domain.MovieThemeName] = { - names.map(name => domain.MovieThemeName(name.name, name.language)) + private def toDomainMovieNames(names: Seq[api.NewOrUpdatedMovieNameDTO]): Seq[MovieThemeName] = { + names.map(name => frontpage.MovieThemeName(name.name, name.language)) } private def createImageUrl(id: Long): String = createImageUrl(id.toString) diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ReadService.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ReadService.scala index 5703173134..8cfd9446a2 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ReadService.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/ReadService.scala @@ -12,15 +12,17 @@ import cats.implicits.* import no.ndla.common.errors as common import no.ndla.common.implicits.* import no.ndla.common.model.api.FrontPageDTO +import no.ndla.common.model.api.frontpage.SubjectPageDTO +import no.ndla.common.model.domain.frontpage.SubjectPage import no.ndla.frontpageapi.model.api -import no.ndla.frontpageapi.model.api.SubjectPageIdDTO +import no.ndla.frontpageapi.model.api.{SubjectPageDomainDumpDTO, SubjectPageIdDTO} import no.ndla.frontpageapi.model.domain.Errors.{LanguageNotFoundException, SubjectPageNotFoundException} import no.ndla.frontpageapi.repository.{FilmFrontPageRepository, FrontPageRepository, SubjectPageRepository} import scala.util.{Failure, Success, Try} trait ReadService { - this: SubjectPageRepository with FrontPageRepository with FilmFrontPageRepository with ConverterService => + this: SubjectPageRepository & FrontPageRepository & FilmFrontPageRepository & ConverterService => val readService: ReadService class ReadService { @@ -37,7 +39,15 @@ trait ReadService { case Failure(ex) => Failure(ex) } - def subjectPage(id: Long, language: String, fallback: Boolean): Try[api.SubjectPageDataDTO] = { + def domainSubjectPage(id: Long): Try[SubjectPage] = { + subjectPageRepository.withId(id) match { + case Failure(ex) => Failure(ex) + case Success(Some(subject)) => Success(subject) + case Success(None) => Failure(SubjectPageNotFoundException(id)) + } + } + + def subjectPage(id: Long, language: String, fallback: Boolean): Try[SubjectPageDTO] = { val maybeSubject = subjectPageRepository.withId(id).? val converted = maybeSubject.traverse(ConverterService.toApiSubjectPage(_, language, fallback)).? converted.toTry(SubjectPageNotFoundException(id)) @@ -48,7 +58,7 @@ trait ReadService { pageSize: Int, language: String, fallback: Boolean - ): Try[List[api.SubjectPageDataDTO]] = { + ): Try[List[SubjectPageDTO]] = { val offset = pageSize * (page - 1) val data = subjectPageRepository.all(offset, pageSize).? val converted = data.map(ConverterService.toApiSubjectPage(_, language, fallback)) @@ -70,7 +80,7 @@ trait ReadService { fallback: Boolean, pageSize: Int, page: Int - ): Try[List[api.SubjectPageDataDTO]] = { + ): Try[List[SubjectPageDTO]] = { val offset = (page - 1) * pageSize for { ids <- validateSubjectPageIdsOrError(subjectIds) @@ -79,6 +89,11 @@ trait ReadService { } yield api } + def getSubjectPageDomainDump(pageNo: Int, pageSize: Int): SubjectPageDomainDumpDTO = { + val results = subjectPageRepository.all(pageNo, pageSize).get + SubjectPageDomainDumpDTO(subjectPageRepository.totalCount, pageNo, pageSize, results) + } + def getFrontPage: Try[FrontPageDTO] = { frontPageRepository.getFrontPage.flatMap { case None => Failure(common.NotFoundException("Front page was not found")) @@ -86,7 +101,7 @@ trait ReadService { } } - def filmFrontPage(language: Option[String]): Option[api.FilmFrontPageDataDTO] = { + def filmFrontPage(language: Option[String]): Option[api.FilmFrontPageDTO] = { filmFrontPageRepository.get.map(page => ConverterService.toApiFilmFrontPage(page, language)) } } diff --git a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala index 5c1318580d..592e4d1cc2 100644 --- a/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala +++ b/frontpage-api/src/main/scala/no/ndla/frontpageapi/service/WriteService.scala @@ -8,21 +8,23 @@ package no.ndla.frontpageapi.service +import no.ndla.common.errors.ValidationException import no.ndla.common.model.api.FrontPageDTO +import no.ndla.common.model.api.frontpage.SubjectPageDTO import no.ndla.frontpageapi.Props import no.ndla.frontpageapi.model.api -import no.ndla.frontpageapi.model.domain.Errors.{SubjectPageNotFoundException, ValidationException} +import no.ndla.frontpageapi.model.domain.Errors.SubjectPageNotFoundException import no.ndla.frontpageapi.repository.{FilmFrontPageRepository, FrontPageRepository, SubjectPageRepository} import scala.util.{Failure, Success, Try} trait WriteService { - this: SubjectPageRepository with FrontPageRepository with FilmFrontPageRepository with Props with ConverterService => + this: SubjectPageRepository & FrontPageRepository & FilmFrontPageRepository & Props & ConverterService => val writeService: WriteService class WriteService { - def newSubjectPage(subject: api.NewSubjectFrontPageDataDTO): Try[api.SubjectPageDataDTO] = { + def newSubjectPage(subject: api.NewSubjectPageDTO): Try[SubjectPageDTO] = { for { convertedSubject <- ConverterService.toDomainSubjectPage(subject) subjectPage <- subjectPageRepository.newSubjectPage(convertedSubject, subject.externalId.getOrElse("")) @@ -32,9 +34,9 @@ trait WriteService { def updateSubjectPage( id: Long, - subject: api.NewSubjectFrontPageDataDTO, + subject: api.NewSubjectPageDTO, language: String - ): Try[api.SubjectPageDataDTO] = { + ): Try[SubjectPageDTO] = { subjectPageRepository.exists(id) match { case Success(exists) if exists => for { @@ -50,10 +52,10 @@ trait WriteService { def updateSubjectPage( id: Long, - subject: api.UpdatedSubjectFrontPageDataDTO, + subject: api.UpdatedSubjectPageDTO, language: String, fallback: Boolean - ): Try[api.SubjectPageDataDTO] = { + ): Try[SubjectPageDTO] = { subjectPageRepository.withId(id) match { case Failure(ex) => Failure(ex) case Success(Some(existingSubject)) => @@ -64,21 +66,22 @@ trait WriteService { } yield converted case Success(None) => newFromUpdatedSubjectPage(subject) match { - case None => Failure(ValidationException(s"Subjectpage can't be converted to NewSubjectFrontPageData")) + case None => + Failure(ValidationException("subjectpage", s"Subjectpage can't be converted to NewSubjectFrontPageData")) case Some(newSubjectPage) => updateSubjectPage(id, newSubjectPage, language) } } } private def newFromUpdatedSubjectPage( - updatedSubjectPage: api.UpdatedSubjectFrontPageDataDTO - ): Option[api.NewSubjectFrontPageDataDTO] = { + updatedSubjectPage: api.UpdatedSubjectPageDTO + ): Option[api.NewSubjectPageDTO] = { for { name <- updatedSubjectPage.name banner <- updatedSubjectPage.banner about <- updatedSubjectPage.about metaDescription <- updatedSubjectPage.metaDescription - } yield api.NewSubjectFrontPageDataDTO( + } yield api.NewSubjectPageDTO( name = name, externalId = updatedSubjectPage.externalId, banner = banner, @@ -99,7 +102,7 @@ trait WriteService { } yield api } - def updateFilmFrontPage(page: api.NewOrUpdatedFilmFrontPageDataDTO): Try[api.FilmFrontPageDataDTO] = { + def updateFilmFrontPage(page: api.NewOrUpdatedFilmFrontPageDTO): Try[api.FilmFrontPageDTO] = { val domainFilmFrontPageT = ConverterService.toDomainFilmFrontPage(page) for { domainFilmFrontPage <- domainFilmFrontPageT diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala index 288bc0f706..84201014d1 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestData.scala @@ -8,27 +8,40 @@ package no.ndla.frontpageapi -import io.circe.generic.auto._ -import io.circe.syntax._ -import no.ndla.frontpageapi.model.api.{NewSubjectFrontPageDataDTO, SubjectPageDataDTO, UpdatedSubjectFrontPageDataDTO} -import no.ndla.frontpageapi.model.domain.{FilmFrontPageData, SubjectFrontPageData, VisualElementType} +import io.circe.generic.auto.* +import io.circe.syntax.* +import no.ndla.common.model +import no.ndla.common.model.api.frontpage.{AboutSubjectDTO, BannerImageDTO, SubjectPageDTO, VisualElementDTO} +import no.ndla.common.model.domain.frontpage +import no.ndla.common.model.domain.frontpage.{ + AboutSubject, + BannerImage, + MetaDescription, + MovieTheme, + MovieThemeName, + SubjectPage, + VisualElement, + VisualElementType +} +import no.ndla.frontpageapi.model.api.{NewSubjectPageDTO, UpdatedSubjectPageDTO} +import no.ndla.frontpageapi.model.domain.FilmFrontPage import no.ndla.frontpageapi.model.{api, domain} object TestData { - val domainSubjectPage: SubjectFrontPageData = domain.SubjectFrontPageData( + val domainSubjectPage: SubjectPage = frontpage.SubjectPage( Some(1), "Samfunnsfag", - domain.BannerImage(Some(29668), 29668), + BannerImage(Some(29668), 29668), Seq( - domain.AboutSubject( + AboutSubject( "Om Samfunnsfag", "Dette er samfunnsfag", "nb", - domain.VisualElement(VisualElementType.Image, "123", Some("alt text")) + VisualElement(VisualElementType.Image, "123", Some("alt text")) ) ), - Seq(domain.MetaDescription("meta", "nb")), + Seq(MetaDescription("meta", "nb")), List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204"), List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204"), List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204"), @@ -36,20 +49,20 @@ object TestData { ) val domainSubjectJson: String = domainSubjectPage.asJson.noSpaces - val domainUpdatedSubjectPage: SubjectFrontPageData = domain.SubjectFrontPageData( + val domainUpdatedSubjectPage: SubjectPage = frontpage.SubjectPage( Some(1), "Samfunnsfag", - domain.BannerImage(Some(29668), 29668), + frontpage.BannerImage(Some(29668), 29668), Seq( - domain.AboutSubject( + frontpage.AboutSubject( "Om Samfunnsfag", "Dette er oppdatert om samfunnsfag", "nb", - domain.VisualElement(VisualElementType.Image, "123", Some("alt text")) + frontpage.VisualElement(VisualElementType.Image, "123", Some("alt text")) ) ), Seq( - domain.MetaDescription("meta", "nb") + frontpage.MetaDescription("meta", "nb") ), List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204"), List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204"), @@ -57,20 +70,20 @@ object TestData { List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204") ) - val apiSubjectPage: SubjectPageDataDTO = api.SubjectPageDataDTO( + val apiSubjectPage: SubjectPageDTO = model.api.frontpage.SubjectPageDTO( 1, "Samfunnsfag", - api.BannerImageDTO( + BannerImageDTO( Some("http://api-gateway.ndla-local/image-api/raw/id/29668"), Some(29668), "http://api-gateway.ndla-local/image-api/raw/id/29668", 29668 ), Some( - api.AboutSubjectDTO( + AboutSubjectDTO( "Om Samfunnsfag", "Dette er samfunnsfag", - api.VisualElementDTO("image", "http://api-gateway.ndla-local/image-api/raw/id/123", Some("alt text")) + VisualElementDTO("image", "http://api-gateway.ndla-local/image-api/raw/id/123", Some("alt text")) ) ), Some("meta"), @@ -81,7 +94,7 @@ object TestData { List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204") ) - val apiNewSubjectPage: NewSubjectFrontPageDataDTO = api.NewSubjectFrontPageDataDTO( + val apiNewSubjectPage: NewSubjectPageDTO = api.NewSubjectPageDTO( "Samfunnsfag", None, api.NewOrUpdateBannerImageDTO(Some(29668), 29668), @@ -100,7 +113,7 @@ object TestData { Some(List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204")) ) - val apiUpdatedSubjectPage: UpdatedSubjectFrontPageDataDTO = api.UpdatedSubjectFrontPageDataDTO( + val apiUpdatedSubjectPage: UpdatedSubjectPageDTO = api.UpdatedSubjectPageDTO( Some("Samfunnsfag"), None, Some(api.NewOrUpdateBannerImageDTO(Some(29668), 29668)), @@ -121,27 +134,27 @@ object TestData { Some(List("urn:resource:1:161411", "urn:resource:1:182176", "urn:resource:1:183636", "urn:resource:1:170204")) ) - val domainFilmFrontPage: FilmFrontPageData = domain.FilmFrontPageData( + val domainFilmFrontPage: FilmFrontPage = domain.FilmFrontPage( "Film", Seq( - domain.AboutSubject( + frontpage.AboutSubject( "Film", "Film faget", "nb", - domain.VisualElement(VisualElementType.Image, "123", Some("alt text")) + frontpage.VisualElement(VisualElementType.Image, "123", Some("alt text")) ), - domain.AboutSubject( + frontpage.AboutSubject( "Film", "Subject film", "en", - domain.VisualElement(VisualElementType.Image, "123", Some("alt text")) + frontpage.VisualElement(VisualElementType.Image, "123", Some("alt text")) ) ), Seq( - domain.MovieTheme( + MovieTheme( Seq( - domain.MovieThemeName("Første filmtema", "nb"), - domain.MovieThemeName("First movie theme", "en") + MovieThemeName("Første filmtema", "nb"), + frontpage.MovieThemeName("First movie theme", "en") ), Seq("movieref1", "movieref2") ) @@ -150,5 +163,5 @@ object TestData { None ) - val apiFilmFrontPage: api.FilmFrontPageDataDTO = api.FilmFrontPageDataDTO("", Seq(), Seq(), Seq(), None) + val apiFilmFrontPage: api.FilmFrontPageDTO = api.FilmFrontPageDTO("", Seq(), Seq(), Seq(), None) } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestEnvironment.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestEnvironment.scala index b98901581b..9d850d7771 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestEnvironment.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/TestEnvironment.scala @@ -13,7 +13,7 @@ import no.ndla.common.Clock import no.ndla.database.{DBMigrator, DataSource} import no.ndla.frontpageapi.controller.{FilmPageController, FrontPageController, SubjectPageController} import no.ndla.frontpageapi.model.api.ErrorHandling -import no.ndla.frontpageapi.model.domain.{DBFilmFrontPageData, DBFrontPageData, DBSubjectFrontPageData} +import no.ndla.frontpageapi.model.domain.{DBFilmFrontPage, DBFrontPage, DBSubjectPage} import no.ndla.frontpageapi.repository.{FilmFrontPageRepository, FrontPageRepository, SubjectPageRepository} import no.ndla.frontpageapi.service.{ConverterService, ReadService, WriteService} import no.ndla.network.tapir.TapirApplication @@ -33,9 +33,9 @@ trait TestEnvironment with WriteService with ConverterService with Props - with DBFilmFrontPageData - with DBSubjectFrontPageData - with DBFrontPageData + with DBFilmFrontPage + with DBSubjectPage + with DBFrontPage with ErrorHandling with Clock with DBMigrator { diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/FrontPageControllerTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/PageControllerTest.scala similarity index 98% rename from frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/FrontPageControllerTest.scala rename to frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/PageControllerTest.scala index 9e230f65d0..04a29ec0d8 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/FrontPageControllerTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/controller/PageControllerTest.scala @@ -19,7 +19,7 @@ import sttp.client3.quick.* import scala.concurrent.duration.Duration import scala.util.{Failure, Success} -class FrontPageControllerTest extends UnitSuite with TestEnvironment with TapirControllerTest { +class PageControllerTest extends UnitSuite with TestEnvironment with TapirControllerTest { val controller: FrontPageController = new FrontPageController when(clock.now()).thenCallRealMethod() diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V4__AddLanguageToAboutTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V4__AddLanguageToAboutTest.scala index 1e1ac80c4b..c0448564ec 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V4__AddLanguageToAboutTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V4__AddLanguageToAboutTest.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.db.migration -import no.ndla.frontpageapi.db.migration.{DBSubjectPage, V4__add_language_to_about} +import no.ndla.frontpageapi.db.migration.{V2_DBSubjectPage, V4__add_language_to_about} import no.ndla.frontpageapi.{TestEnvironment, UnitSuite} class V4__AddLanguageToAboutTest extends UnitSuite with TestEnvironment { @@ -21,7 +21,7 @@ class V4__AddLanguageToAboutTest extends UnitSuite with TestEnvironment { val after = """{"name":"Kinesisk","layout":"double","bannerImage":{"mobileImageId":66,"desktopImageId":65},"about":[{"title":"Om kinesisk","description":"Kinesiskfaget gir en grunnleggende innsikt i levemåter og tankesett i Kina.","language":"nb","visualElement":{"type":"brightcove","id":"182071"}}],"mostRead":["urn:resource:1:148063"],"editorsChoices":["urn:resource:1:163488"],"goTo":[]}""" - migration.convertSubjectpage(DBSubjectPage(1, before)).get.document should equal(after) + migration.convertSubjectpage(V2_DBSubjectPage(1, before)).get.document should equal(after) } } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V5__AddMetaDescriptionTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V5__AddMetaDescriptionTest.scala index cb5161b643..14fc4f5977 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V5__AddMetaDescriptionTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V5__AddMetaDescriptionTest.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.db.migration -import no.ndla.frontpageapi.db.migration.{DBSubjectPage, V5__add_meta_description} +import no.ndla.frontpageapi.db.migration.{V2_DBSubjectPage, V5__add_meta_description} import no.ndla.frontpageapi.{TestEnvironment, UnitSuite} class V5__AddMetaDescriptionTest extends UnitSuite with TestEnvironment { @@ -21,7 +21,7 @@ class V5__AddMetaDescriptionTest extends UnitSuite with TestEnvironment { val after = """{"name":"Kinesisk","layout":"double","bannerImage":{"mobileImageId":66,"desktopImageId":65},"about":[{"title":"Om kinesisk","description":"Kinesiskfaget gir en grunnleggende innsikt i levemåter og tankesett i Kina.","language":"nb","visualElement":{"type":"brightcove","id":"182071"}}],"metaDescription":[],"mostRead":["urn:resource:1:148063"],"editorsChoices":["urn:resource:1:163488"],"goTo":[]}""" - migration.convertSubjectpage(DBSubjectPage(1, before)).get.document should equal(after) + migration.convertSubjectpage(V2_DBSubjectPage(1, before)).get.document should equal(after) } } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V8__AddSubjectLinksTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V8__AddSubjectLinksTest.scala index 6fd43261a3..0fe52608cf 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V8__AddSubjectLinksTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V8__AddSubjectLinksTest.scala @@ -8,7 +8,7 @@ package no.ndla.frontpageapi.db.migration -import no.ndla.frontpageapi.db.migration.{DBSubjectPage, V8__add_subject_links} +import no.ndla.frontpageapi.db.migration.{V2_DBSubjectPage, V8__add_subject_links} import no.ndla.frontpageapi.{TestEnvironment, UnitSuite} class V8__AddSubjectLinksTest extends UnitSuite with TestEnvironment { @@ -21,6 +21,6 @@ class V8__AddSubjectLinksTest extends UnitSuite with TestEnvironment { val after = """{"name":"Kinesisk","bannerImage":{"mobileImageId":66,"desktopImageId":65},"about":[{"title":"Om kinesisk","description":"Kinesiskfaget gir en grunnleggende innsikt i levemåter og tankesett i Kina.","language":"nb","visualElement":{"type":"brightcove","id":"182071"}}],"metaDescription":[],"editorsChoices":["urn:resource:1:163488"],"connectedTo":[],"buildsOn":[],"leadsTo":[]}""" - migration.convertSubjectpage(DBSubjectPage(1, before)).get.document should equal(after) + migration.convertSubjectpage(V2_DBSubjectPage(1, before)).get.document should equal(after) } } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields_Test.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields_Test.scala index f88b59e494..3b2b42c6fe 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields_Test.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/db/migration/V9__add_missing_fields_Test.scala @@ -20,6 +20,6 @@ class V9__add_missing_fields_Test extends UnitSuite with TestEnvironment { val after = """{"name":"Kinesisk","bannerImage":{"desktopImageId":65},"about":[{"title":"Om kinesisk","description":"Kinesiskfaget gir en grunnleggende innsikt i levemåter og tankesett i Kina.","language":"nb","visualElement":{"type":"brightcove","id":"182071"}}],"metaDescription":[],"editorsChoices":["urn:resource:1:163488"],"connectedTo":[],"buildsOn":[],"leadsTo":[]}""" - migration.convertSubjectpage(DBSubjectPage(1, before)).document should be(after) + migration.convertSubjectpage(V2_DBSubjectPage(1, before)).document should be(after) } } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/api/FrontPageTest/FrontPageTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/api/FrontPageTest/FrontPageTest.scala index fc3c96bb86..b09a7c5871 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/api/FrontPageTest/FrontPageTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/api/FrontPageTest/FrontPageTest.scala @@ -9,9 +9,9 @@ package no.ndla.frontpageapi.model.api.FrontPageTest import no.ndla.frontpageapi.{TestEnvironment, UnitSuite} -import io.circe.generic.auto._ -import io.circe.syntax._ -import io.circe.parser._ +import io.circe.generic.auto.* +import io.circe.syntax.* +import io.circe.parser.* import no.ndla.common.model.api.{FrontPageDTO, MenuDTO} class FrontPageTest extends UnitSuite with TestEnvironment { diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageDataTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectPageTest.scala similarity index 60% rename from frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageDataTest.scala rename to frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectPageTest.scala index 6daa24d483..e103935b32 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectFrontPageDataTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/model/domain/SubjectPageTest.scala @@ -8,13 +8,14 @@ package no.ndla.frontpageapi.model.domain +import no.ndla.common.model.domain.frontpage.SubjectPage import no.ndla.frontpageapi.{TestData, TestEnvironment, UnitSuite} import scala.util.Success -class SubjectFrontPageDataTest extends UnitSuite with TestEnvironment { +class SubjectPageTest extends UnitSuite with TestEnvironment { test("decodeJson should use correct id") { - val Success(subject) = SubjectFrontPageData.decodeJson(TestData.domainSubjectJson, 10) + val Success(subject) = SubjectPage.decodeJson(TestData.domainSubjectJson, 10) subject.id should be(Some(10)) } diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ConverterServiceTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ConverterServiceTest.scala index f412bf522a..bbe4fa2f36 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ConverterServiceTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ConverterServiceTest.scala @@ -8,10 +8,11 @@ package no.ndla.frontpageapi.service -import no.ndla.frontpageapi.model.api._ -import no.ndla.frontpageapi.model.domain +import no.ndla.common.errors.ValidationException +import no.ndla.common.model.domain.frontpage +import no.ndla.common.model.domain.frontpage.{AboutSubject, MetaDescription, VisualElement, VisualElementType} +import no.ndla.frontpageapi.model.api.* import no.ndla.frontpageapi.model.domain.Errors.LanguageNotFoundException -import no.ndla.frontpageapi.model.domain.{AboutSubject, Errors, MetaDescription, VisualElement, VisualElementType} import no.ndla.frontpageapi.{TestData, TestEnvironment, UnitSuite} import scala.util.{Failure, Success} @@ -41,8 +42,12 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { val about = TestData.apiNewSubjectPage.about.map(_.copy(visualElement = visualElement)) val page = TestData.apiNewSubjectPage.copy(about = about) - val Failure(res: Errors.ValidationException) = ConverterService.toDomainSubjectPage(page) - res.message should equal("'not an image' is an invalid visual element type") + val Failure(res: ValidationException) = ConverterService.toDomainSubjectPage(page) + val expectedError = ValidationException( + "visualElement.type", + "'not an image' is an invalid visual element type" + ) + res should be(expectedError) } test("toDomainSubjectPage should return a success if visual element type is valid") { @@ -63,7 +68,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { } test("toDomainSubjectPage updates subject links correctly") { - val updateWith = UpdatedSubjectFrontPageDataDTO( + val updateWith = UpdatedSubjectPageDTO( None, None, None, @@ -87,7 +92,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { } test("toDomainSubjectPage updates meta description correctly") { - val updateWith = UpdatedSubjectFrontPageDataDTO( + val updateWith = UpdatedSubjectPageDTO( None, None, None, @@ -105,7 +110,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { } test("toDomainSubjectPage updates aboutSubject correctly") { - val updateWith = UpdatedSubjectFrontPageDataDTO( + val updateWith = UpdatedSubjectPageDTO( None, None, None, @@ -139,7 +144,7 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { } test("toDomainSubjectPage adds new language correctly") { - val updateWith = UpdatedSubjectFrontPageDataDTO( + val updateWith = UpdatedSubjectPageDTO( None, None, None, @@ -168,27 +173,28 @@ class ConverterServiceTest extends UnitSuite with TestEnvironment { Success( TestData.domainSubjectPage.copy( about = Seq( - domain.AboutSubject( + frontpage.AboutSubject( "Om Samfunnsfag", "Dette er samfunnsfag", "nb", - domain.VisualElement(VisualElementType.Image, "123", Some("alt text")) + frontpage.VisualElement(VisualElementType.Image, "123", Some("alt text")) ), - domain.AboutSubject( + frontpage.AboutSubject( "About Social studies", "This is social studies", "en", - domain.VisualElement(VisualElementType.Image, "123", None) + frontpage.VisualElement(VisualElementType.Image, "123", None) ) ), - metaDescription = Seq(domain.MetaDescription("meta", "nb"), domain.MetaDescription("meta description", "en")) + metaDescription = + Seq(frontpage.MetaDescription("meta", "nb"), frontpage.MetaDescription("meta description", "en")) ) ) ) } test("toApiSubjectPage failure if subject not found in specified language without fallback") { - ConverterService.toApiSubjectPage(TestData.domainSubjectPage, "hei", fallback = false) should be( + ConverterService.toApiSubjectPage(TestData.domainSubjectPage, "hei") should be( Failure( LanguageNotFoundException( s"The subjectpage with id ${TestData.domainSubjectPage.id.get} and language hei was not found", diff --git a/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ReadServiceTest.scala b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ReadServiceTest.scala index 21708cd50a..712bd5fc34 100644 --- a/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ReadServiceTest.scala +++ b/frontpage-api/src/test/scala/no/ndla/frontpageapi/service/ReadServiceTest.scala @@ -8,8 +8,8 @@ package no.ndla.frontpageapi.service -import no.ndla.frontpageapi.model.domain -import no.ndla.frontpageapi.model.domain.VisualElementType +import no.ndla.common.model.domain.frontpage +import no.ndla.common.model.domain.frontpage.{AboutSubject, MetaDescription, VisualElement, VisualElementType} import no.ndla.frontpageapi.{TestData, TestEnvironment, UnitSuite} import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.when @@ -23,19 +23,19 @@ class ReadServiceTest extends UnitSuite with TestEnvironment { val norwegianSubjectPage = TestData.domainSubjectPage.copy( id = Some(2), metaDescription = Seq( - domain.MetaDescription("hei", "nb") + MetaDescription("hei", "nb") ), about = Seq( - domain.AboutSubject("tittel", "besk", "nb", domain.VisualElement(VisualElementType.Image, "", None)) + AboutSubject("tittel", "besk", "nb", VisualElement(VisualElementType.Image, "", None)) ) ) val englishSubjectPage = TestData.domainSubjectPage.copy( id = Some(2), metaDescription = Seq( - domain.MetaDescription("hello", "en") + frontpage.MetaDescription("hello", "en") ), about = Seq( - domain.AboutSubject("title", "desc", "en", domain.VisualElement(VisualElementType.Image, "1", None)) + frontpage.AboutSubject("title", "desc", "en", frontpage.VisualElement(VisualElementType.Image, "1", None)) ) ) diff --git a/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala b/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala index b0ae9f412b..9dc9b0a2df 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/ComponentRegistry.scala @@ -29,7 +29,7 @@ import no.ndla.imageapi.service.search.{ } import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} class ComponentRegistry(properties: ImageApiProperties) extends BaseComponentRegistry[ImageApiProperties] @@ -41,6 +41,7 @@ class ComponentRegistry(properties: ImageApiProperties) with ImageIndexService with SearchService with ImageSearchService + with SearchLanguage with TagSearchService with SearchConverterService with DataSource diff --git a/image-api/src/main/scala/no/ndla/imageapi/service/search/ImageIndexService.scala b/image-api/src/main/scala/no/ndla/imageapi/service/search/ImageIndexService.scala index 31219ac225..8a620665d1 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/service/search/ImageIndexService.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/service/search/ImageIndexService.scala @@ -12,16 +12,16 @@ import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.fields.{ElasticField, ObjectField} import com.sksamuel.elastic4s.requests.indexes.IndexRequest 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.imageapi.Props import no.ndla.imageapi.model.domain.ImageMetaInformation import no.ndla.imageapi.model.search.SearchableImage import no.ndla.imageapi.repository.{ImageRepository, Repository} +import no.ndla.search.SearchLanguage trait ImageIndexService { - this: SearchConverterService with IndexService with ImageRepository with Props => + this: SearchConverterService & IndexService & ImageRepository & Props & SearchLanguage => val imageIndexService: ImageIndexService class ImageIndexService extends StrictLogging with IndexService[ImageMetaInformation, SearchableImage] { @@ -36,6 +36,23 @@ trait ImageIndexService { Seq(indexInto(indexName).doc(source).id(domainModel.id.get.toString)) } + protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { + if (keepRaw) { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .fielddata(false) + .analyzer(langAnalyzer.analyzer) + .fields(keywordField("raw")) + ) + } else { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .fielddata(false) + .analyzer(langAnalyzer.analyzer) + ) + } + } + def getMapping: MappingDefinition = { val fields: Seq[ElasticField] = List( ObjectField("domainObject", enabled = Some(false)), @@ -60,12 +77,12 @@ trait ImageIndexService { ) ) - val dynamics: Seq[DynamicTemplateRequest] = generateLanguageSupportedDynamicTemplates("titles", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("alttexts", keepRaw = false) ++ - generateLanguageSupportedDynamicTemplates("captions", keepRaw = false) ++ - generateLanguageSupportedDynamicTemplates("tags", keepRaw = false) + val dynamics = generateLanguageSupportedFieldList("titles", keepRaw = true) ++ + generateLanguageSupportedFieldList("alttexts") ++ + generateLanguageSupportedFieldList("captions") ++ + generateLanguageSupportedFieldList("tags") - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } diff --git a/image-api/src/main/scala/no/ndla/imageapi/service/search/IndexService.scala b/image-api/src/main/scala/no/ndla/imageapi/service/search/IndexService.scala index ab4fbfa848..35e621cb72 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/service/search/IndexService.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/service/search/IndexService.scala @@ -9,18 +9,13 @@ package no.ndla.imageapi.service.search import cats.implicits.* -import com.sksamuel.elastic4s.ElasticDsl.* -import com.sksamuel.elastic4s.fields.ElasticField import com.sksamuel.elastic4s.requests.indexes.IndexRequest -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.imageapi.Props import no.ndla.imageapi.repository.{ImageRepository, Repository} -import no.ndla.search.SearchLanguage.languageAnalyzers import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} -import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} +import no.ndla.search.{BaseIndexService, Elastic4sClient} -import scala.collection.mutable.ListBuffer import scala.util.{Failure, Success, Try} trait IndexService { @@ -82,67 +77,5 @@ trait IndexService { } } } - - /** Returns Sequence of FieldDefinitions for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of FieldDefinitions for a field. - */ - protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { - if (keepRaw) { - languageAnalyzers.map(langAnalyzer => - textField(s"$fieldName.${langAnalyzer.languageTag.toString()}") - .fielddata(false) - .analyzer(langAnalyzer.analyzer) - .fields(keywordField("raw")) - ) - } else { - languageAnalyzers.map(langAnalyzer => - textField(s"$fieldName.${langAnalyzer.languageTag.toString()}") - .fielddata(false) - .analyzer(langAnalyzer.analyzer) - ) - } - } - - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val fields = new ListBuffer[ElasticField]() - if (keepRaw) { - fields += keywordField("raw") - } - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(languageAnalyzer.analyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(name) - ) - }) - val catchAlltemplate = DynamicTemplateRequest( - name = fieldName, - mapping = textField(fieldName).analyzer(SearchLanguage.standardAnalyzer).fields(fields.toList), - matchMappingType = Some("string"), - pathMatch = Some(s"$fieldName.*") - ) - languageTemplates ++ Seq(catchAlltemplate) - } - } } diff --git a/image-api/src/main/scala/no/ndla/imageapi/service/search/SearchConverterService.scala b/image-api/src/main/scala/no/ndla/imageapi/service/search/SearchConverterService.scala index 732cb3b150..93a0967490 100644 --- a/image-api/src/main/scala/no/ndla/imageapi/service/search/SearchConverterService.scala +++ b/image-api/src/main/scala/no/ndla/imageapi/service/search/SearchConverterService.scala @@ -30,7 +30,7 @@ import no.ndla.network.tapir.auth.TokenUser import scala.util.{Failure, Success, Try} trait SearchConverterService { - this: ConverterService with Props => + this: ConverterService & Props & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService extends StrictLogging { diff --git a/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala b/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala index 72c8182819..e93618a2a2 100644 --- a/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala +++ b/image-api/src/test/scala/no/ndla/imageapi/TestEnvironment.scala @@ -34,7 +34,7 @@ import no.ndla.imageapi.service.search.{ } import no.ndla.network.NdlaClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.scalatestplus.mockito.MockitoSugar trait TestEnvironment @@ -47,6 +47,7 @@ trait TestEnvironment with ImageSearchService with TagSearchService with SearchConverterService + with SearchLanguage with DataSource with ConverterService with ValidationService diff --git a/language/src/main/scala/no/ndla/language/Language.scala b/language/src/main/scala/no/ndla/language/Language.scala index 7e1d6b4eca..2fd7cfe0a3 100644 --- a/language/src/main/scala/no/ndla/language/Language.scala +++ b/language/src/main/scala/no/ndla/language/Language.scala @@ -55,6 +55,7 @@ object Language { "sv", "th", "tr", + "ukr", "und" ) diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala index f2324e24e3..4a09b5ce38 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/ComponentRegistry.scala @@ -42,7 +42,7 @@ import no.ndla.learningpathapi.validation.{ import no.ndla.network.NdlaClient import no.ndla.network.clients.MyNDLAApiClient import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} class ComponentRegistry(properties: LearningpathApiProperties) extends BaseComponentRegistry[LearningpathApiProperties] @@ -75,7 +75,8 @@ class ComponentRegistry(properties: LearningpathApiProperties) with TextValidator with UrlValidator with ErrorHandling - with SwaggerDocControllerConfig { + with SwaggerDocControllerConfig + with SearchLanguage { override val props: LearningpathApiProperties = properties override val migrator: DBMigrator = DBMigrator( new V11__CreatedByNdlaStatusForOwnersWithRoles, diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchConverterServiceComponent.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchConverterServiceComponent.scala index 509574001a..054c4ff6a5 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchConverterServiceComponent.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchConverterServiceComponent.scala @@ -23,7 +23,7 @@ import no.ndla.search.SearchLanguage import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} trait SearchConverterServiceComponent { - this: ConverterService & Props => + this: ConverterService & Props & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService { diff --git a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchIndexService.scala b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchIndexService.scala index 44b9b59bdd..c697bf7826 100644 --- a/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchIndexService.scala +++ b/learningpath-api/src/main/scala/no/ndla/learningpathapi/service/search/SearchIndexService.scala @@ -12,15 +12,12 @@ import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.RequestSuccess import com.sksamuel.elastic4s.fields.{ElasticField, ObjectField} import com.sksamuel.elastic4s.requests.mappings.MappingDefinition -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import no.ndla.learningpathapi.Props import no.ndla.learningpathapi.integration.SearchApiClient import no.ndla.learningpathapi.repository.LearningPathRepositoryComponent -import no.ndla.search.SearchLanguage.languageAnalyzers import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} -import scala.collection.mutable.ListBuffer import scala.util.{Failure, Success, Try} import cats.implicits.* import no.ndla.common.CirceUtil @@ -30,7 +27,7 @@ import no.ndla.search.model.domain.{BulkIndexResult, ElasticIndexingException, R trait SearchIndexService { this: Elastic4sClient & SearchConverterServiceComponent & LearningPathRepositoryComponent & SearchApiClient & - BaseIndexService & Props => + BaseIndexService & Props & SearchLanguage => val searchIndexService: SearchIndexService class SearchIndexService extends BaseIndexService with StrictLogging { @@ -161,51 +158,29 @@ trait SearchIndexService { ), intField("isBasedOn") ) - val dynamics = generateLanguageSupportedDynamicTemplates("titles", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("descriptions") ++ - generateLanguageSupportedDynamicTemplates("tags", keepRaw = true) + val dynamics = generateLanguageSupportedFieldList("titles", keepRaw = true) ++ + generateLanguageSupportedFieldList("descriptions") ++ + generateLanguageSupportedFieldList("tags", keepRaw = true) - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - - val dynamicFunc = (name: String, analyzer: String, subFields: List[ElasticField]) => { - DynamicTemplateRequest( - name = name, - mapping = textField(name).analyzer(analyzer).fields(subFields), - matchMappingType = Some("string"), - pathMatch = Some(name) - ) - } - val fields = new ListBuffer[ElasticField]() + protected def generateLanguageSupportedFieldList(fieldName: String, keepRaw: Boolean = false): Seq[ElasticField] = { if (keepRaw) { - fields += keywordField("raw") + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .analyzer(langAnalyzer.analyzer) + .fields( + keywordField("raw") + ) + ) + } else { + SearchLanguage.languageAnalyzers.map(langAnalyzer => + textField(s"$fieldName.${langAnalyzer.languageTag.toString}") + .analyzer(langAnalyzer.analyzer) + ) } - val languageTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - dynamicFunc(name, languageAnalyzer.analyzer, fields.toList) - }) - val languageSubTemplates = languageAnalyzers.map(languageAnalyzer => { - val name = s"*.$fieldName.${languageAnalyzer.languageTag.toString()}" - dynamicFunc(name, languageAnalyzer.analyzer, fields.toList) - }) - val catchAllTemplate = dynamicFunc(s"$fieldName.*", SearchLanguage.standardAnalyzer, fields.toList) - languageTemplates ++ languageSubTemplates ++ Seq(catchAllTemplate) } - } } diff --git a/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala b/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala index 4163b16af6..faaede8a05 100644 --- a/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala +++ b/learningpath-api/src/test/scala/no/ndla/learningpathapi/TestEnvironment.scala @@ -21,7 +21,7 @@ import no.ndla.learningpathapi.validation.* import no.ndla.network.NdlaClient import no.ndla.network.clients.{FeideApiClient, MyNDLAApiClient, RedisClient} import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import org.mockito.Mockito.reset import org.scalatestplus.mockito.MockitoSugar @@ -35,6 +35,7 @@ trait TestEnvironment with UpdateService with SearchConverterServiceComponent with SearchService + with SearchLanguage with SearchIndexService with BaseIndexService with SearchApiClient diff --git a/network/src/main/scala/no/ndla/network/clients/FrontpageApiClient.scala b/network/src/main/scala/no/ndla/network/clients/FrontpageApiClient.scala new file mode 100644 index 0000000000..b9937e81e9 --- /dev/null +++ b/network/src/main/scala/no/ndla/network/clients/FrontpageApiClient.scala @@ -0,0 +1,37 @@ +/* + * Part of NDLA network + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.network.clients + +import io.circe.Decoder +import no.ndla.common.configuration.HasBaseProps +import no.ndla.common.model.domain.frontpage.SubjectPage +import no.ndla.network.NdlaClient +import sttp.client3.quick.* +import scala.concurrent.duration.* +import scala.util.Try + +trait FrontpageApiClient { + this: HasBaseProps & NdlaClient => + val frontpageApiClient: FrontpageApiClient + + class FrontpageApiClient { + val timeout: FiniteDuration = 15.seconds + + def getSubjectPage(id: Long): Try[SubjectPage] = { + get[SubjectPage](s"${props.FrontpageApiUrl}/intern/dump/subjectpage/$id", Map.empty, Seq.empty) + } + + private def get[A: Decoder](url: String, headers: Map[String, String], params: Seq[(String, String)]): Try[A] = { + ndlaClient.fetchWithForwardedAuth[A]( + quickRequest.get(uri"$url?$params").headers(headers).readTimeout(timeout), + None + ) + } + + } +} diff --git a/network/src/main/scala/no/ndla/network/clients/SearchApiClient.scala b/network/src/main/scala/no/ndla/network/clients/SearchApiClient.scala index 5ec42a230a..d7daf89986 100644 --- a/network/src/main/scala/no/ndla/network/clients/SearchApiClient.scala +++ b/network/src/main/scala/no/ndla/network/clients/SearchApiClient.scala @@ -87,7 +87,7 @@ trait SearchApiClient { "embed-resource" -> "content-link,related-content", "embed-id" -> s"$articleId" ) match { - case Success(value) => value.results + case Success(value) => value.results.collect { case x: MultiSearchSummaryDTO => x } case Failure(_) => Seq.empty } } diff --git a/project/frontpageapi.scala b/project/frontpageapi.scala index fc2be0c673..de6231f50f 100644 --- a/project/frontpageapi.scala +++ b/project/frontpageapi.scala @@ -21,16 +21,20 @@ object frontpageapi extends Module { ) lazy val tsSettings: Seq[Def.Setting[?]] = typescriptSettings( - imports = Seq("no.ndla.frontpageapi.model.api._", "no.ndla.network.tapir._"), + imports = Seq( + "no.ndla.frontpageapi.model.api._", + "no.ndla.network.tapir._", + "no.ndla.common.model.api.frontpage._" + ), exports = Seq( "no.ndla.common.model.api.FrontPageDTO", "no.ndla.common.model.api.MenuDataDTO", "no.ndla.common.model.api.MenuDTO", - "FilmFrontPageDataDTO", - "NewOrUpdatedFilmFrontPageDataDTO", - "SubjectPageDataDTO", - "NewSubjectFrontPageDataDTO", - "UpdatedSubjectFrontPageDataDTO", + "FilmFrontPageDTO", + "NewOrUpdatedFilmFrontPageDTO", + "SubjectPageDTO", + "NewSubjectPageDTO", + "UpdatedSubjectPageDTO", "ErrorBody" ) ) 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 bfb88692e8..04f742e463 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/ComponentRegistry.scala @@ -12,9 +12,9 @@ import com.typesafe.scalalogging.StrictLogging import no.ndla.common.Clock import no.ndla.common.configuration.BaseComponentRegistry import no.ndla.network.NdlaClient -import no.ndla.network.clients.{FeideApiClient, MyNDLAApiClient, RedisClient} +import no.ndla.network.clients.{FeideApiClient, FrontpageApiClient, MyNDLAApiClient, RedisClient} import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import no.ndla.searchapi.controller.parameters.GetSearchQueryParams import no.ndla.searchapi.controller.{InternController, SearchController, SwaggerDocControllerConfig} import no.ndla.searchapi.integration.* @@ -31,6 +31,8 @@ class ComponentRegistry(properties: SearchApiProperties) with DraftConceptIndexService with LearningPathIndexService with DraftIndexService + with NodeIndexService + with FrontpageApiClient with MultiSearchService with ErrorHandling with Clock @@ -41,6 +43,7 @@ class ComponentRegistry(properties: SearchApiProperties) with TaxonomyApiClient with IndexService with BaseIndexService + with SearchLanguage with StrictLogging with LearningPathApiClient with NdlaClient @@ -75,6 +78,7 @@ class ComponentRegistry(properties: SearchApiProperties) lazy val articleApiClient = new ArticleApiClient(ArticleApiUrl) lazy val feideApiClient = new FeideApiClient lazy val redisClient = new RedisClient(props.RedisHost, props.RedisPort) + lazy val frontpageApiClient = new FrontpageApiClient lazy val converterService = new ConverterService lazy val searchConverterService = new SearchConverterService @@ -86,6 +90,7 @@ class ComponentRegistry(properties: SearchApiProperties) lazy val multiDraftSearchService = new MultiDraftSearchService lazy val grepIndexService = new GrepIndexService lazy val grepSearchService = new GrepSearchService + lazy val nodeIndexService = new NodeIndexService lazy val searchController = new SearchController lazy val healthController: TapirHealthController = new TapirHealthController 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 a5fefad108..1f187bf33b 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/SearchApiProperties.scala @@ -36,6 +36,7 @@ class SearchApiProperties extends BaseProps with StrictLogging { val learningpathIndexName = propOrElse("LEARNINGPATH_SEARCH_INDEX_NAME", "learningpaths") val conceptIndexName = propOrElse("DRAFT_CONCEPT_SEARCH_INDEX_NAME", "draftconcepts") val grepIndexName = propOrElse("GREP_SEARCH_INDEX_NAME", "greps") + val nodeIndexName = propOrElse("NODE_SEARCH_INDEX_NAME", "nodes") def SearchIndex(searchType: SearchType) = searchType match { case SearchType.Articles => articleIndexName @@ -43,6 +44,7 @@ class SearchApiProperties extends BaseProps with StrictLogging { case SearchType.LearningPaths => learningpathIndexName case SearchType.Concepts => conceptIndexName case SearchType.Grep => grepIndexName + case SearchType.Nodes => nodeIndexName } def indexToSearchType(indexName: String): Try[SearchType] = indexName match { @@ -51,6 +53,7 @@ class SearchApiProperties extends BaseProps with StrictLogging { case `learningpathIndexName` => Success(SearchType.LearningPaths) case `conceptIndexName` => Success(SearchType.Concepts) case `grepIndexName` => Success(SearchType.Grep) + case `nodeIndexName` => Success(SearchType.Nodes) case _ => Failure(new IllegalArgumentException(s"Unknown index name: $indexName")) } 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 b54af7aab6..c8dd68c1c4 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 @@ -34,7 +34,8 @@ import no.ndla.searchapi.service.search.{ DraftIndexService, GrepIndexService, IndexService, - LearningPathIndexService + LearningPathIndexService, + NodeIndexService } import sttp.model.StatusCode @@ -49,7 +50,8 @@ import sttp.tapir.server.ServerEndpoint trait InternController { this: IndexService & ArticleIndexService & LearningPathIndexService & DraftIndexService & DraftConceptIndexService & - TaxonomyApiClient & GrepApiClient & GrepIndexService & Props & ErrorHandling & MyNDLAApiClient & TapirController => + NodeIndexService & TaxonomyApiClient & GrepApiClient & GrepIndexService & Props & ErrorHandling & MyNDLAApiClient & + TapirController => val internController: InternController class InternController extends TapirController with StrictLogging { @@ -105,6 +107,7 @@ trait InternController { reindexDraft, reindexGrep, reindexLearningpath, + reindexNode, reindexConcept ) @@ -249,6 +252,21 @@ trait InternController { resolveResultFutures(List(grepIndex)) } + def reindexNode: ServerEndpoint[Any, Eff] = endpoint.post + .in("index" / "node") + .in(query[Option[Int]]("numShards")) + .errorOut(stringInternalServerError) + .out(stringBody) + .serverLogicPure { numShards => + val requestInfo = RequestInfo.fromThreadContext() + val grepIndex = Future { + requestInfo.setThreadContextRequestInfo() + ("nodes", nodeIndexService.indexDocuments(numShards)) + } + + resolveResultFutures(List(grepIndex)) + } + def reindexLearningpath: ServerEndpoint[Any, Eff] = endpoint.post .in("index" / "learningpath") .in(query[Option[Int]]("numShards")) @@ -384,6 +402,10 @@ trait InternController { Future { requestInfo.setThreadContextRequestInfo() ("greps", grepIndexService.indexDocuments(numShards, Some(grepBundle))) + }, + Future { + requestInfo.setThreadContextRequestInfo() + ("nodes", nodeIndexService.indexDocuments(numShards, publishedIndexingBundle)) } ) 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 ba109b9f7c..07c177297a 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 @@ -35,6 +35,7 @@ import no.ndla.searchapi.model.api.grep.GrepSearchResultsDTO import no.ndla.searchapi.model.api.{ErrorHandling, GroupSearchResultDTO, SubjectAggregationsDTO} import no.ndla.searchapi.model.domain.Sort import no.ndla.searchapi.model.search.settings.{MultiDraftSearchSettings, SearchSettings} +import no.ndla.searchapi.model.taxonomy.NodeType import no.ndla.searchapi.service.search.{ GrepSearchService, MultiDraftSearchService, @@ -239,7 +240,9 @@ trait SearchController { embedId = embedId, availability = availability, articleTypes = List.empty, - filterInactive = filterInactive + filterInactive = filterInactive, + resultTypes = None, + nodeTypeFilter = List.empty ) groupSearch(settings, includeMissingResourceTypeGroup) @@ -346,7 +349,9 @@ trait SearchController { .out(EndpointOutput.derived[DynamicHeaders]) .in(GetSearchQueryParams.input) .in(feideHeader) - .serverLogicPure { case (q, feideToken) => + .serverLogicPure { case (queryWrapper, feideToken) => + val pagination = queryWrapper.pagination + val q = queryWrapper.searchParams scrollWithOr(q.scrollId, q.language, multiSearchService) { val sort = q.sort.flatMap(Sort.valueOf) val shouldScroll = q.scrollId.exists(InitialScrollContextKeywords.contains) @@ -356,8 +361,8 @@ trait SearchController { fallback = q.fallback, language = q.language, license = q.license, - page = q.page, - pageSize = q.pageSize, + page = pagination.page, + pageSize = pagination.pageSize, sort = sort.getOrElse(Sort.ByRelevanceDesc), withIdIn = q.learningResourceIds.values, subjects = q.subjects.values, @@ -374,7 +379,9 @@ trait SearchController { availability = availability, articleTypes = q.articleTypes.values, filterInactive = q.filterInactive, - traits = q.traits.values.flatMap(SearchTrait.valueOf) + traits = q.traits.values.flatMap(SearchTrait.valueOf), + resultTypes = q.resultTypes.values.flatMap(SearchType.withNameOption).some, + nodeTypeFilter = q.nodeTypeFilter.values.flatMap(NodeType.withNameOption) ) multiSearchService.matchingQuery(settings) match { case Success(searchResult) => @@ -610,7 +617,9 @@ trait SearchController { embedId = params.embedId, availability = availability, articleTypes = params.articleTypes.getOrElse(List.empty), - filterInactive = params.filterInactive.getOrElse(false) + filterInactive = params.filterInactive.getOrElse(false), + resultTypes = params.resultTypes, + nodeTypeFilter = params.nodeTypeFilter.getOrElse(List.empty) ) } diff --git a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/GetSearchQueryParams.scala b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/GetSearchQueryParams.scala index fd951e3c67..5e1bdcd2ca 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/GetSearchQueryParams.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/GetSearchQueryParams.scala @@ -20,7 +20,7 @@ import sttp.tapir.ValidationResult.{Invalid, Valid} trait GetSearchQueryParams { this: Props => - case class GetSearchQueryParams( + case class PaginationParams( @query("page") @description("The page number of the search hits to display.") @default(1) @@ -28,7 +28,15 @@ trait GetSearchQueryParams { @query("page-size") @description("The number of search hits to display for each page.") @default(props.DefaultPageSize) - pageSize: Int, + pageSize: Int + ) + + case class GetParamsWrapper( + pagination: PaginationParams, + searchParams: GetSearchQueryParams + ) + + case class GetSearchQueryParams( @query("article-types") @description("A comma separated list of article-types the search should be filtered by.") articleTypes: CommaSeparatedList[String], @@ -97,14 +105,22 @@ trait GetSearchQueryParams { filterInactive: Boolean, @query("traits") @description("A comma separated list of traits the resources should be filtered by.") - traits: CommaSeparatedList[String] + traits: CommaSeparatedList[String], + @query("result-types") + @description("A comma separated list of indexes that should be searched.") + resultTypes: CommaSeparatedList[String], + @query("node-types") + @description("A comma separated list of node types the nodes should be filtered by.") + nodeTypeFilter: CommaSeparatedList[String] ) object GetSearchQueryParams { implicit val schema: Schema[GetSearchQueryParams] = Schema.derived[GetSearchQueryParams] implicit val schemaOpt: Schema[Option[GetSearchQueryParams]] = schema.asOption - def input = EndpointInput - .derived[GetSearchQueryParams] + + def input2 = EndpointInput.derived[GetSearchQueryParams] + def input1 = EndpointInput + .derived[PaginationParams] .validate { Validator.custom { case q if q.page < 1 => Invalid("page must be greater than 0") @@ -113,5 +129,7 @@ trait GetSearchQueryParams { case _ => Valid } } + def input = input1.and(input2).mapTo[GetParamsWrapper] + } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/SearchParamsDTO.scala b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/SearchParamsDTO.scala index bb43650e58..421dac8139 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/SearchParamsDTO.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/controller/parameters/SearchParamsDTO.scala @@ -12,9 +12,10 @@ import com.scalatsi.TypescriptType.{TSString, TSUndefined} import com.scalatsi.{TSIType, TSType} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import no.ndla.common.model.api.search.SearchTrait +import no.ndla.common.model.api.search.{SearchTrait, SearchType} import no.ndla.network.tapir.NonEmptyString import no.ndla.searchapi.model.domain.Sort +import no.ndla.searchapi.model.taxonomy.NodeType import sttp.tapir.Schema import sttp.tapir.Schema.annotations.description @@ -71,7 +72,11 @@ case class SearchParamsDTO( @description("Return only results with embed data-resource_id, data-videoid or data-url with the specified id.") embedId: Option[String], @description("Filter out inactive taxonomy contexts.") - filterInactive: Option[Boolean] + filterInactive: Option[Boolean], + @description("Which types the search request should return") + resultTypes: Option[List[SearchType]], + @description("Which node types the search request should return") + nodeTypeFilter: Option[List[NodeType]] ) object SearchParamsDTO { diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResultDTO.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResultDTO.scala index f4243fe1b6..ce5397a4ff 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResultDTO.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/GroupSearchResultDTO.scala @@ -10,7 +10,7 @@ package no.ndla.searchapi.model.api import io.circe.{Decoder, Encoder} import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import no.ndla.common.model.api.search.{MultiSearchSuggestionDTO, MultiSearchSummaryDTO, MultiSearchTermsAggregationDTO} +import no.ndla.common.model.api.search.{MultiSearchSuggestionDTO, MultiSearchTermsAggregationDTO, MultiSummaryBaseDTO} import sttp.tapir.Schema.annotations.description @description("Search result for group search") @@ -19,7 +19,7 @@ case class GroupSearchResultDTO( @description("For which page results are shown from") page: Option[Int], @description("The number of results per page") pageSize: Int, @description("The chosen search language") language: String, - @description("The search results") results: Seq[MultiSearchSummaryDTO], + @description("The search results") results: Seq[MultiSummaryBaseDTO], @description("The suggestions for other searches") suggestions: Seq[MultiSearchSuggestionDTO], @description("The aggregated fields if specified in query") aggregations: Seq[MultiSearchTermsAggregationDTO], @description("Type of resources in this object") resourceType: String diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummaryDTO.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummaryDTO.scala deleted file mode 100644 index 3ace623c50..0000000000 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/MultiSearchSummaryDTO.scala +++ /dev/null @@ -1,7 +0,0 @@ -/* - * Part of NDLA search-api - * Copyright (C) 2025 NDLA - * - * See LICENSE - * - */ diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/api/grep/GrepResultDTO.scala b/search-api/src/main/scala/no/ndla/searchapi/model/api/grep/GrepResultDTO.scala index 6ad6d59834..7ff17ac5b1 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/api/grep/GrepResultDTO.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/api/grep/GrepResultDTO.scala @@ -14,7 +14,8 @@ import com.scalatsi.{TSNamedType, TSType, TypescriptType} import io.circe.generic.auto.* import sttp.tapir.generic.auto.* import io.circe.syntax.* -import io.circe.{Decoder, Encoder, Json} +import io.circe.{Decoder, Encoder} +import no.ndla.common.CirceUtil import no.ndla.common.model.api.search.TitleDTO import no.ndla.language.Language import no.ndla.language.Language.findByLanguageOrBestEffort @@ -51,7 +52,7 @@ object GrepResultDTO { } // NOTE: Adding the discriminator field that scala-tsi generates in the typescript type. // Useful for guarding the type of the object in the frontend. - json.mapObject(_.add("typename", Json.fromString(result.getClass.getSimpleName))) + CirceUtil.addTypenameDiscriminator(json, result.getClass) } val typescriptUnionTypes: Seq[TypescriptType.TSInterface] = Seq( diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/domain/SearchResult.scala b/search-api/src/main/scala/no/ndla/searchapi/model/domain/SearchResult.scala index b290b713d3..ad1d80f132 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/domain/SearchResult.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/domain/SearchResult.scala @@ -8,7 +8,7 @@ package no.ndla.searchapi.model.domain -import no.ndla.common.model.api.search.{MultiSearchSuggestionDTO, MultiSearchSummaryDTO} +import no.ndla.common.model.api.search.{MultiSearchSuggestionDTO, MultiSummaryBaseDTO} import no.ndla.search.model.domain.TermAggregation case class SearchResult( @@ -16,7 +16,7 @@ case class SearchResult( page: Option[Int], pageSize: Int, language: String, - results: Seq[MultiSearchSummaryDTO], + results: Seq[MultiSummaryBaseDTO], suggestions: Seq[MultiSearchSuggestionDTO], aggregations: Seq[TermAggregation], scrollId: Option[String] = None 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 1b8edea2ed..ae002f253c 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 @@ -21,7 +21,6 @@ case class SearchableArticle( id: Long, title: SearchableLanguageValues, content: SearchableLanguageValues, - visualElement: SearchableLanguageValues, introduction: SearchableLanguageValues, metaDescription: SearchableLanguageValues, tags: SearchableLanguageList, 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 fe5adb8f77..3c3acb4fc7 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 @@ -21,7 +21,6 @@ case class SearchableDraft( id: Long, title: SearchableLanguageValues, content: SearchableLanguageValues, - visualElement: SearchableLanguageValues, introduction: SearchableLanguageValues, metaDescription: SearchableLanguageValues, tags: SearchableLanguageList, diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableNode.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableNode.scala new file mode 100644 index 0000000000..44ed842b0a --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableNode.scala @@ -0,0 +1,27 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.searchapi.model.search + +import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} +import io.circe.{Decoder, Encoder} +import no.ndla.search.model.SearchableLanguageValues +import no.ndla.searchapi.model.taxonomy.NodeType + +case class SearchableNode( + nodeId: String, + title: SearchableLanguageValues, + contentUri: Option[String], + url: Option[String], + nodeType: NodeType, + subjectPage: Option[SearchableSubjectPage] +) + +object SearchableNode { + implicit val encoder: Encoder[SearchableNode] = deriveEncoder + implicit val decoder: Decoder[SearchableNode] = deriveDecoder +} diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableSubjectPage.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableSubjectPage.scala new file mode 100644 index 0000000000..4e6472770c --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableSubjectPage.scala @@ -0,0 +1,23 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2025 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.domain.frontpage.SubjectPage + +case class SearchableSubjectPage( + id: Long, + name: String, + domainObject: SubjectPage +) + +object SearchableSubjectPage { + implicit val encoder: Encoder[SearchableSubjectPage] = deriveEncoder + implicit val decoder: Decoder[SearchableSubjectPage] = deriveDecoder +} diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableTaxonomyContext.scala b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableTaxonomyContext.scala index f9a97ba0ba..e8fe97c103 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableTaxonomyContext.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/search/SearchableTaxonomyContext.scala @@ -10,20 +10,19 @@ package no.ndla.searchapi.model.search import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} -import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} +import no.ndla.search.model.SearchableLanguageList +import no.ndla.searchapi.model.taxonomy.TaxonomyContext -// NOTE: This will need to match `TaxonomyContextDTO` in `taxonomy-api` case class SearchableTaxonomyContext( + domainObject: TaxonomyContext, publicId: String, contextId: String, rootId: String, - root: SearchableLanguageValues, path: String, breadcrumbs: SearchableLanguageList, contextType: String, relevanceId: String, - relevance: SearchableLanguageValues, - resourceTypes: List[SearchableTaxonomyResourceType], + resourceTypeIds: List[String], parentIds: List[String], isPrimary: Boolean, isActive: Boolean, 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 b39249749a..a653bf0954 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 @@ -8,11 +8,12 @@ package no.ndla.searchapi.model.search.settings -import no.ndla.common.model.api.search.{LearningResourceType, SearchTrait} +import no.ndla.common.model.api.search.{LearningResourceType, SearchTrait, SearchType} import no.ndla.common.model.domain.Availability import no.ndla.language.Language import no.ndla.network.tapir.NonEmptyString import no.ndla.searchapi.model.domain.Sort +import no.ndla.searchapi.model.taxonomy.NodeType case class SearchSettings( query: Option[NonEmptyString], @@ -37,7 +38,9 @@ case class SearchSettings( embedId: Option[String], availability: List[Availability], articleTypes: List[String], - filterInactive: Boolean + filterInactive: Boolean, + resultTypes: Option[List[SearchType]], + nodeTypeFilter: List[NodeType] ) object SearchSettings { @@ -64,6 +67,13 @@ object SearchSettings { embedId = None, availability = List.empty, articleTypes = List.empty, - filterInactive = false + filterInactive = false, + nodeTypeFilter = List(NodeType.SUBJECT), + resultTypes = Some( + List( + SearchType.Articles, + SearchType.LearningPaths + ) + ) ) } diff --git a/search-api/src/main/scala/no/ndla/searchapi/model/taxonomy/Node.scala b/search-api/src/main/scala/no/ndla/searchapi/model/taxonomy/Node.scala index e999ee75dd..b705933422 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/model/taxonomy/Node.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/model/taxonomy/Node.scala @@ -13,6 +13,8 @@ import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} import io.circe.{Decoder, Encoder} import no.ndla.search.model.{SearchableLanguageList, SearchableLanguageValues} import no.ndla.searchapi.model.search.SearchableTaxonomyResourceType +import sttp.tapir.Schema +import sttp.tapir.codec.enumeratum.* sealed trait NodeType extends EnumEntry {} object NodeType extends Enum[NodeType] with CirceEnum[NodeType] { @@ -23,6 +25,8 @@ object NodeType extends Enum[NodeType] with CirceEnum[NodeType] { case object PROGRAMME extends NodeType val values: IndexedSeq[NodeType] = findValues + + implicit def schema: Schema[NodeType] = schemaForEnumEntry[NodeType] } case class Node( @@ -57,6 +61,7 @@ object Node { }) } +// NOTE: This will need to match `TaxonomyContextDTO` in `taxonomy-api` case class TaxonomyContext( publicId: String, rootId: String, 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 6126103485..77867a602d 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 @@ -73,25 +73,18 @@ trait ArticleIndexService { ), dateField("nextRevision.revisionDate") // This is needed for sorting, even if it is never used for articles ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("metaDescription") ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("visualElement") ++ - generateLanguageSupportedDynamicTemplates("introduction") ++ - generateLanguageSupportedDynamicTemplates("metaDescription") ++ - generateLanguageSupportedDynamicTemplates("tags") ++ - generateLanguageSupportedDynamicTemplates("embedAttributes") ++ - generateLanguageSupportedDynamicTemplates("relevance") ++ - generateLanguageSupportedDynamicTemplates("breadcrumbs") ++ - generateLanguageSupportedDynamicTemplates("name", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("context.root") ++ - generateLanguageSupportedDynamicTemplates("context.relevance") ++ - generateLanguageSupportedDynamicTemplates("context.resourceTypes.name") ++ - generateLanguageSupportedDynamicTemplates("contexts.root") ++ - generateLanguageSupportedDynamicTemplates("contexts.relevance") ++ - generateLanguageSupportedDynamicTemplates("contexts.resourceTypes.name") + val dynamics = + languageValuesMapping("title", keepRaw = true) ++ + languageValuesMapping("metaDescription") ++ + languageValuesMapping("content") ++ + languageValuesMapping("introduction") ++ + languageValuesMapping("tags") ++ + languageValuesMapping("embedAttributes") ++ + languageValuesMapping("relevance") ++ + languageValuesMapping("breadcrumbs") ++ + languageValuesMapping("name", keepRaw = true) - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } 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 index 259a0df924..221f374d9f 100644 --- 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 @@ -75,11 +75,12 @@ trait DraftConceptIndexService { ObjectField("domainObject", enabled = Some(false)) ) val dynamics = - generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("content", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("tags") + languageValuesMapping("title", keepRaw = true) ++ + languageValuesMapping("content", keepRaw = true) ++ + languageValuesMapping("tags") + + properties(fields ++ dynamics) - 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 ab97f57302..e33349c75f 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 @@ -99,29 +99,21 @@ trait DraftIndexService { keywordField("defaultRoot"), keywordField("defaultResourceTypeName") ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("metaDescription") ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("visualElement") ++ - generateLanguageSupportedDynamicTemplates("introduction") ++ - generateLanguageSupportedDynamicTemplates("tags") ++ - generateLanguageSupportedDynamicTemplates("embedAttributes") ++ - generateLanguageSupportedDynamicTemplates("relevance") ++ - generateLanguageSupportedDynamicTemplates("breadcrumbs") ++ - generateLanguageSupportedDynamicTemplates("name", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("contexts.root", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("parentTopicName", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("resourceTypeName", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("primaryRoot", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("context.root") ++ - generateLanguageSupportedDynamicTemplates("context.relevance") ++ - generateLanguageSupportedDynamicTemplates("context.resourceTypes.name") ++ - generateLanguageSupportedDynamicTemplates("contexts.root") ++ - generateLanguageSupportedDynamicTemplates("contexts.relevance") ++ - generateLanguageSupportedDynamicTemplates("contexts.resourceTypes.name") - - properties(fields).dynamicTemplates(dynamics) + val dynamics = + languageValuesMapping("title", keepRaw = true) ++ + languageValuesMapping("metaDescription") ++ + languageValuesMapping("content") ++ + languageValuesMapping("introduction") ++ + languageValuesMapping("tags") ++ + languageValuesMapping("embedAttributes") ++ + languageValuesMapping("relevance") ++ + languageValuesMapping("breadcrumbs") ++ + languageValuesMapping("name", keepRaw = true) ++ + languageValuesMapping("parentTopicName", keepRaw = true) ++ + languageValuesMapping("resourceTypeName", keepRaw = true) ++ + languageValuesMapping("primaryRoot", keepRaw = true) + properties(fields ++ dynamics) } } diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/GrepIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/GrepIndexService.scala index 16547ec129..9ec3542ae0 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/GrepIndexService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/GrepIndexService.scala @@ -44,8 +44,8 @@ trait GrepIndexService { ObjectField("domainObject", enabled = Some(false)) ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) - properties(fields).dynamicTemplates(dynamics) + val dynamics = languageValuesMapping("title", keepRaw = true) + properties(fields ++ dynamics) } def indexDocuments(numShards: Option[Int], grepBundle: Option[GrepBundle]): Try[ReindexResult] = { diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/IndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/IndexService.scala index 202c8eccd9..e0cd9ac8b9 100644 --- a/search-api/src/main/scala/no/ndla/searchapi/service/search/IndexService.scala +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/IndexService.scala @@ -10,14 +10,12 @@ package no.ndla.searchapi.service.search import com.sksamuel.elastic4s.ElasticDsl.* import com.sksamuel.elastic4s.analysis.* -import com.sksamuel.elastic4s.fields.{ElasticField, NestedField} +import com.sksamuel.elastic4s.fields.{ElasticField, NestedField, ObjectField} import com.sksamuel.elastic4s.requests.indexes.IndexRequest -import com.sksamuel.elastic4s.requests.mappings.dynamictemplate.DynamicTemplateRequest import com.typesafe.scalalogging.StrictLogging import io.circe.Decoder import no.ndla.common.model.domain.Content import no.ndla.network.clients.MyNDLAApiClient -import no.ndla.search.SearchLanguage.NynorskLanguageAnalyzer import no.ndla.search.model.domain.{BulkIndexResult, ElasticIndexingException, ReindexResult} import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import no.ndla.searchapi.Props @@ -28,51 +26,25 @@ import scala.util.{Failure, Success, Try} trait IndexService { this: Elastic4sClient & SearchApiClient & BaseIndexService & TaxonomyApiClient & GrepApiClient & Props & - MyNDLAApiClient => + MyNDLAApiClient & SearchLanguage => trait BulkIndexingService extends BaseIndexService { - /** Returns Sequence of DynamicTemplateRequest for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of DynamicTemplateRequest for a field. - */ - protected def generateLanguageSupportedDynamicTemplates( - fieldName: String, - keepRaw: Boolean = false - ): Seq[DynamicTemplateRequest] = { - val dynamicFunc = (name: String, analyzer: String, subFields: List[ElasticField]) => { - val field = textField(name).analyzer(analyzer).fields(subFields) - DynamicTemplateRequest( - name = name, - mapping = field, - matchMappingType = Some("string"), - pathMatch = Some(name) - ) - } - - val sf = List( + protected def languageValuesMapping(name: String, keepRaw: Boolean = false): Seq[ElasticField] = { + val subfields = List( textField("trigram").analyzer("trigram"), textField("decompounded").searchAnalyzer("standard").analyzer("compound_analyzer"), textField("exact").analyzer("exact") - ) - val subFields = if (keepRaw) sf :+ keywordField("raw") else sf + ) ++ + Option.when(keepRaw)(keywordField("raw")).toList - val languageTemplates = SearchLanguage.languageAnalyzers.map(languageAnalyzer => { - val name = s"$fieldName.${languageAnalyzer.languageTag.toString()}" - dynamicFunc(name, languageAnalyzer.analyzer, subFields) - }) - val languageSubTemplates = SearchLanguage.languageAnalyzers.map(languageAnalyzer => { - val name = s"*.$fieldName.${languageAnalyzer.languageTag.toString()}" - dynamicFunc(name, languageAnalyzer.analyzer, subFields) + val analyzedFields = SearchLanguage.languageAnalyzers.map(langAnalyzer => { + textField(s"$name.${langAnalyzer.languageTag.toString}") + .analyzer(langAnalyzer.analyzer) + .fields(subfields) }) - val catchAllTemplate = dynamicFunc(s"$fieldName.*", "standard", subFields) - val catchAllSubTemplate = dynamicFunc(s"*.$fieldName.*", "standard", subFields) - languageTemplates ++ languageSubTemplates ++ Seq(catchAllTemplate, catchAllSubTemplate) + + analyzedFields } private val hyphDecompounderTokenFilter: CompoundWordTokenFilter = CompoundWordTokenFilter( @@ -104,7 +76,7 @@ trait IndexService { override val analysis: Analysis = Analysis( - analyzers = List(trigram, customExactAnalyzer, customCompoundAnalyzer, NynorskLanguageAnalyzer), + analyzers = List(trigram, customExactAnalyzer, customCompoundAnalyzer, SearchLanguage.NynorskLanguageAnalyzer), tokenFilters = List(hyphDecompounderTokenFilter) ++ SearchLanguage.NynorskTokenFilters, normalizers = List(lowerNormalizer) ) @@ -194,10 +166,11 @@ trait IndexService { val chunks = apiClient.getChunks[D] val results = chunks .map({ - case Failure(ex) => Failure(ex) + case Failure(ex) => + logger.error(s"Failed to fetch chunk from with api client '${apiClient.name}'", ex) + Failure(ex) case Success(c) => - indexDocuments(c, indexName, indexingBundle) - .map(numIndexed => (numIndexed, c.size)) + indexDocuments(c, indexName, indexingBundle).map(numIndexed => (numIndexed, c.size)) }) .toList @@ -260,51 +233,21 @@ trait IndexService { } } - /** Returns Sequence of FieldDefinitions for a given field. - * - * @param fieldName - * Name of field in mapping. - * @param keepRaw - * Whether to add a keywordField named raw. Usually used for sorting, aggregations or scripts. - * @return - * Sequence of FieldDefinitions for a field. - */ - protected def generateLanguageSupportedFieldList( - fieldName: String, - keepRaw: Boolean = false - ): Seq[ElasticField] = { - SearchLanguage.languageAnalyzers.map(langAnalyzer => { - val sf = List( - textField("trigram").analyzer("trigram"), - textField("decompounded") - .searchAnalyzer("standard") - .analyzer("compound_analyzer"), - textField("exact") - .analyzer("exact") - ) - - val subFields = if (keepRaw) sf :+ keywordField("raw") else sf - - textField(s"$fieldName.${langAnalyzer.languageTag.toString}") - .analyzer(langAnalyzer.analyzer) - .fields(subFields) - }) - } - protected def getTaxonomyContextMapping(fieldName: String): NestedField = { nestedField(fieldName).fields( - keywordField("publicId"), - keywordField("contextId"), - keywordField("path"), - keywordField("contextType"), - keywordField("rootId"), - keywordField("parentIds"), - keywordField("relevanceId"), - booleanField("isActive"), - booleanField("isPrimary"), - keywordField("url"), - nestedField("resourceTypes").fields( - keywordField("id") + List( + ObjectField("domainObject", enabled = Some(false)), + keywordField("publicId"), + keywordField("contextId"), + keywordField("path"), + keywordField("contextType"), + keywordField("rootId"), + keywordField("parentIds"), + keywordField("relevanceId"), + booleanField("isActive"), + booleanField("isPrimary"), + keywordField("url"), + keywordField("resourceTypeIds") ) ) } 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 8cb398868a..c40c993958 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 @@ -88,21 +88,16 @@ trait LearningPathIndexService { ), dateField("nextRevision.revisionDate") // This is needed for sorting, even if it is never used for learningpaths ) - val dynamics = generateLanguageSupportedDynamicTemplates("title", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("content") ++ - generateLanguageSupportedDynamicTemplates("description") ++ - generateLanguageSupportedDynamicTemplates("tags", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("relevance") ++ - generateLanguageSupportedDynamicTemplates("breadcrumbs") ++ - generateLanguageSupportedDynamicTemplates("name", keepRaw = true) ++ - generateLanguageSupportedDynamicTemplates("context.root") ++ - generateLanguageSupportedDynamicTemplates("context.relevance") ++ - generateLanguageSupportedDynamicTemplates("context.resourceTypes.name") ++ - generateLanguageSupportedDynamicTemplates("contexts.root") ++ - generateLanguageSupportedDynamicTemplates("contexts.relevance") ++ - generateLanguageSupportedDynamicTemplates("contexts.resourceTypes.name") + val dynamics = + languageValuesMapping("title", keepRaw = true) ++ + languageValuesMapping("content") ++ + languageValuesMapping("description") ++ + languageValuesMapping("tags", keepRaw = true) ++ + languageValuesMapping("relevance") ++ + languageValuesMapping("breadcrumbs") ++ + languageValuesMapping("name", keepRaw = true) - properties(fields).dynamicTemplates(dynamics) + properties(fields ++ dynamics) } } 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 2d96dfac0b..ccb890db30 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 @@ -8,12 +8,18 @@ package no.ndla.searchapi.service.search +import cats.implicits.{catsSyntaxOptionId, toTraverseOps} import com.sksamuel.elastic4s.ElasticDsl.* +import com.sksamuel.elastic4s.RequestSuccess +import com.sksamuel.elastic4s.requests.searches.SearchResponse import com.sksamuel.elastic4s.requests.searches.queries.Query import com.sksamuel.elastic4s.requests.searches.queries.compound.BoolQuery import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.CirceUtil +import no.ndla.common.errors.{ValidationException, ValidationMessage} +import no.ndla.common.implicits.TryQuestionMark import no.ndla.common.model.api.search.SearchType -import no.ndla.common.model.domain.{Availability, Content} +import no.ndla.common.model.domain.Availability import no.ndla.language.Language.AllLanguages import no.ndla.language.model.Iso639 import no.ndla.mapping.License @@ -23,20 +29,31 @@ import no.ndla.searchapi.Props import no.ndla.searchapi.model.api.ErrorHandling import no.ndla.searchapi.model.domain.SearchResult import no.ndla.searchapi.model.search.settings.SearchSettings +import no.ndla.searchapi.model.taxonomy.NodeType import scala.util.{Failure, Success, Try} trait MultiSearchService { this: Elastic4sClient & SearchConverterService & SearchService & IndexService & ArticleIndexService & - LearningPathIndexService & Props & ErrorHandling => + LearningPathIndexService & Props & ErrorHandling & NodeIndexService => val multiSearchService: MultiSearchService class MultiSearchService extends StrictLogging with SearchService with TaxonomyFiltering { import props.{ElasticSearchScrollKeepAlive, SearchIndex} - override val searchIndex: List[String] = List(SearchType.Articles, SearchType.LearningPaths).map(SearchIndex) - override val indexServices: List[IndexService[? <: Content]] = List(articleIndexService, learningPathIndexService) + override val searchIndex: List[String] = + List(SearchType.Articles, SearchType.LearningPaths, SearchType.Nodes).map(SearchIndex) + override val indexServices: List[BulkIndexingService] = List( + articleIndexService, + learningPathIndexService, + nodeIndexService + ) + + def getIndexFilter(indexes: List[SearchType]): Query = { + val indexNames = indexes.map(SearchIndex) + termsQuery("_index", indexNames) + } def matchingQuery(settings: SearchSettings): Try[SearchResult] = { @@ -71,24 +88,101 @@ trait MultiSearchService { ) }) - val boolQueries: List[BoolQuery] = List(contentSearch).flatten - val fullQuery = boolQuery().must(boolQueries) + val nodeSearch = settings.query.map { q => + val langQueryFunc = (fieldName: String, boost: Double) => + buildSimpleStringQueryForField( + q, + fieldName, + boost, + settings.language, + settings.fallback, + searchDecompounded = true + ) + boolQuery().must( + boolQuery().should( + langQueryFunc("title", 6) + ) + ) + } + val indexFilterNode = getIndexFilter(List(SearchType.Nodes)) + val indexFilterContent = getIndexFilter(List(SearchType.Articles, SearchType.LearningPaths)) + + val boolQueries: List[BoolQuery] = List( + contentSearch.map(_.filter(indexFilterContent)), + nodeSearch.map(_.filter(indexFilterNode)) + ).flatten + + val contentFilter = boolQuery().must(getSearchFilters(settings)).filter(indexFilterContent) + val ntf = getNodeTypeFilter(settings.nodeTypeFilter) + val nodeFilter = boolQuery().must(ntf).filter(indexFilterNode) - executeSearch(settings, fullQuery) + val fullQuery = boolQuery() + .should(boolQueries) + .minimumShouldMatch(Math.min(boolQueries.size, 1)) + .filter(boolQuery().should(contentFilter, nodeFilter).minimumShouldMatch(1)) + + val filteredSearch = fullQuery + + executeSearch(settings, filteredSearch) + } + + private def getSearchIndexes(settings: SearchSettings): 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.Articles, SearchType.LearningPaths).map(SearchIndex)) + } } - def executeSearch(settings: SearchSettings, baseQuery: BoolQuery): Try[SearchResult] = { + private def logShardErrors(response: RequestSuccess[SearchResponse]) = { + if (response.result.shards.failed > 0) { + response.body.map { body => + CirceUtil.tryParse(body) match { + case Failure(ex) => + logger.error(s"Got error parsing search response: $body", ex) + case Success(jsonBody) => + val failures = jsonBody.hcursor.downField("_shards").downField("failures").focus.map(_.spaces2) + failures match { + case Some(shardFailure) => + logger.error(s"${response.result.shards.failed} failed shards in search response: \n$shardFailure") + case None => + logger.error(s"${response.result.shards.failed} failed shards in search response") + } + } + } + } + } + + def executeSearch(settings: SearchSettings, filteredSearch: BoolQuery): Try[SearchResult] = { val searchLanguage = settings.language match { case lang if Iso639.get(lang).isSuccess && !settings.fallback => lang case _ => AllLanguages } - val filteredSearch = baseQuery.filter(getSearchFilters(settings)) - getStartAtAndNumResults(settings.page, settings.pageSize).flatMap { pagination => 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)) .from(pagination.startAt) @@ -106,6 +200,7 @@ trait MultiSearchService { e4sClient.execute(searchWithScroll) match { case Success(response) => + logShardErrors(response) getHits(response.result, settings.language, settings.filterInactive).map(hits => { SearchResult( totalCount = response.result.totalHits, @@ -123,6 +218,19 @@ trait MultiSearchService { } } + def getNodeTypeFilter(maybeTypes: List[NodeType]): Option[Query] = { + maybeTypes match { + case types if types.nonEmpty => + boolQuery() + .should( + boolQuery().not(existsQuery("nodeType")), + termsQuery("nodeType", types.map(_.entryName)) + ) + .some + case _ => None + } + } + /** Returns a list of QueryDefinitions of different search filters depending on settings. * * @param settings diff --git a/search-api/src/main/scala/no/ndla/searchapi/service/search/NodeIndexService.scala b/search-api/src/main/scala/no/ndla/searchapi/service/search/NodeIndexService.scala new file mode 100644 index 0000000000..ab185d0162 --- /dev/null +++ b/search-api/src/main/scala/no/ndla/searchapi/service/search/NodeIndexService.scala @@ -0,0 +1,118 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ + +package no.ndla.searchapi.service.search + +import com.sksamuel.elastic4s.ElasticApi.* +import com.sksamuel.elastic4s.fields.ObjectField +import com.sksamuel.elastic4s.requests.mappings.MappingDefinition +import com.typesafe.scalalogging.StrictLogging +import no.ndla.common.implicits.* +import no.ndla.search.model.domain.{BulkIndexResult, ReindexResult} +import no.ndla.searchapi.Props +import no.ndla.searchapi.integration.{ArticleApiClient, GrepApiClient, TaxonomyApiClient} +import no.ndla.searchapi.model.domain.IndexingBundle +import no.ndla.searchapi.model.taxonomy.Node +import cats.implicits.* +import com.sksamuel.elastic4s.requests.indexes.IndexRequest +import no.ndla.common.model.api.search.SearchType +import no.ndla.common.model.domain.frontpage.SubjectPage +import no.ndla.common.{CirceUtil, ContentURIUtil} +import no.ndla.network.clients.FrontpageApiClient +import no.ndla.network.model.HttpRequestException + +import scala.util.{Failure, Success, Try} + +trait NodeIndexService { + this: SearchConverterService & IndexService & Props & TaxonomyApiClient & ArticleApiClient & FrontpageApiClient & + GrepApiClient => + val nodeIndexService: NodeIndexService + class NodeIndexService extends StrictLogging with BulkIndexingService { + import props.SearchIndex + override val documentType: String = "nodes" + override val searchIndex: String = SearchIndex(SearchType.Nodes) + override val MaxResultWindowOption: Int = props.ElasticSearchIndexMaxResultWindow + + override def getMapping: MappingDefinition = { + val fields = List( + keywordField("nodeId"), + keywordField("contentUri"), + keywordField("nodeType"), + keywordField("url"), + nestedField("subjectPage").fields( + keywordField("id"), + keywordField("name"), + ObjectField("domainObject", enabled = Some(false)) + ) + ) + + val dynamics = + languageValuesMapping("title") ++ + languageValuesMapping("content") + + properties(fields ++ dynamics) + } + + def indexDocuments(numShards: Option[Int], indexingBundle: IndexingBundle): Try[ReindexResult] = { + indexDocumentsInBulk(numShards) { indexName => + sendToElastic(indexingBundle, indexName) + } + } + + def indexDocuments(numShards: Option[Int]): Try[ReindexResult] = for { + grepBundle <- grepApiClient.getGrepBundle() + taxonomyBundle <- taxonomyApiClient.getTaxonomyBundle(true) + indexingBundle = IndexingBundle(grepBundle.some, taxonomyBundle.some, None) + result <- indexDocuments(numShards, indexingBundle) + } yield result + + private def getFrontPage(contentUri: Option[String]): Try[Option[SubjectPage]] = { + contentUri.map(ContentURIUtil.parseFrontpageId) match { + case Some(Success(frontpageId)) => + val subjectPage = frontpageApiClient.getSubjectPage(frontpageId) + subjectPage match { + case Failure(exception: HttpRequestException) if exception.is404 => + Success(None) + case Failure(ex) => + Failure(ex) + case Success(value) => + Success(Some(value)) + } + case _ => Success(None) + } + } + + def createIndexRequest(node: Node, indexName: String): Try[IndexRequest] = { + for { + frontpage <- getFrontPage(node.contentUri) + searchable <- searchConverterService.asSearchableNode(node, frontpage) + source = CirceUtil.toJsonString(searchable) + } yield indexInto(indexName).doc(source).id(node.id) + } + + def sendChunkToElastic(chunk: List[Node], indexName: String): Try[BulkIndexResult] = { + chunk + .traverse(node => createIndexRequest(node, indexName)) + .map(executeRequests) + .flatten + } + + def sendToElastic(indexingBundle: IndexingBundle, indexName: String): Try[BulkIndexResult] = { + val taxBundle = indexingBundle.taxonomyBundle match { + case None => taxonomyApiClient.getTaxonomyBundle(true).? + case Some(value) => value + } + + taxBundle.nodes + .grouped(props.IndexBulkSize) + .toList + .traverse(group => sendChunkToElastic(group, indexName)) + .map(countBulkIndexed) + } + } +} 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 c2fe0cbeaf..a52ff21b3c 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 @@ -14,6 +14,7 @@ import com.typesafe.scalalogging.StrictLogging import no.ndla.common import no.ndla.common.CirceUtil import no.ndla.common.configuration.Constants.EmbedTagName +import no.ndla.common.errors.MissingIdException import no.ndla.common.implicits.* import no.ndla.common.model.api.draft.CommentDTO import no.ndla.common.model.api.search.{ @@ -25,10 +26,12 @@ import no.ndla.common.model.api.search.{ MetaImageDTO, MultiSearchResultDTO, MultiSearchSummaryDTO, + NodeHitDTO, RevisionMetaDTO, SearchTrait, SearchType, StatusDTO, + SubjectPageSummaryDTO, TaxonomyResourceTypeDTO, TitleWithHtmlDTO } @@ -36,6 +39,7 @@ import no.ndla.common.model.api.{AuthorDTO, LicenseDTO} 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.frontpage.SubjectPage import no.ndla.common.model.domain.learningpath.{LearningPath, LearningStep} import no.ndla.common.model.domain.{ ArticleContent, @@ -71,10 +75,10 @@ import org.jsoup.nodes.Entities.EscapeMode import scala.collection.mutable.ListBuffer import scala.jdk.CollectionConverters.* -import scala.util.{Success, Try} +import scala.util.{Failure, Success, Try} trait SearchConverterService { - this: DraftApiClient & TaxonomyApiClient & ConverterService & Props & MyNDLAApiClient => + this: DraftApiClient & TaxonomyApiClient & ConverterService & Props & MyNDLAApiClient & SearchLanguage => val searchConverterService: SearchConverterService class SearchConverterService extends StrictLogging { @@ -117,6 +121,34 @@ trait SearchConverterService { }) } + def nodeHitAsMultiSummary(hit: SearchHit, language: String): Try[NodeHitDTO] = { + val searchableNode = CirceUtil.tryParseAs[SearchableNode](hit.sourceAsString).? + val title = searchableNode.title.getLanguageOrDefault(language).getOrElse("") + val url = searchableNode.url.map(urlPath => s"${props.ndlaFrontendUrl}$urlPath") + + Success( + NodeHitDTO( + id = searchableNode.nodeId, + title = title, + url = url, + subjectPage = searchableNode.subjectPage.map(subjectPageToSummary(_, language)) + ) + ) + } + + def subjectPageToSummary(subjectPage: SearchableSubjectPage, language: String): SubjectPageSummaryDTO = { + val metaDescription = + findByLanguageOrBestEffort(subjectPage.domainObject.metaDescription, language) + .map(meta => MetaDescriptionDTO(meta.metaDescription, meta.language)) + .getOrElse(MetaDescriptionDTO("", UnknownLanguage.toString)) + + SubjectPageSummaryDTO( + id = subjectPage.id, + name = subjectPage.name, + metaDescription = metaDescription + ) + } + private[service] def getAttributes(html: String): List[String] = { parseHtml(html) .select(EmbedTagName) @@ -177,16 +209,15 @@ trait SearchConverterService { ): List[SearchableTaxonomyContext] = { taxonomyContexts.map(context => SearchableTaxonomyContext( + domainObject = context, publicId = context.publicId, contextId = context.contextId, rootId = context.rootId, - root = context.root, path = context.path, breadcrumbs = context.breadcrumbs, contextType = context.contextType.getOrElse(""), relevanceId = context.relevanceId, - relevance = context.relevance, - resourceTypes = context.resourceTypes, + resourceTypeIds = context.resourceTypes.map(_.id), parentIds = context.parentIds, isPrimary = context.isPrimary, isActive = context.isActive, @@ -242,9 +273,6 @@ trait SearchConverterService { content = model.SearchableLanguageValues( ai.content.map(article => LanguageValue(article.language, toPlaintext(article.content))) ), - visualElement = model.SearchableLanguageValues( - ai.visualElement.map(visual => LanguageValue(visual.language, visual.resource)) - ), introduction = model.SearchableLanguageValues( ai.introduction.map(intro => LanguageValue(intro.language, toPlaintext(intro.introduction))) ), @@ -503,7 +531,6 @@ trait SearchConverterService { val title = model.SearchableLanguageValues.fromFieldsMap(draft.title, toPlaintext) val content = model.SearchableLanguageValues.fromFieldsMap(draft.content, toPlaintext) - val visualElement = model.SearchableLanguageValues.fromFields(draft.visualElement) val introduction = model.SearchableLanguageValues.fromFieldsMap(draft.introduction, toPlaintext) val metaDescription = model.SearchableLanguageValues.fromFields(draft.metaDescription) val contexts = asSearchableTaxonomyContexts(taxonomyContexts) @@ -513,7 +540,6 @@ trait SearchConverterService { id = draft.id.get, title = title, content = content, - visualElement = visualElement, introduction = introduction, metaDescription = metaDescription, tags = SearchableLanguageList(draft.tags.map(tag => LanguageValue(tag.language, tag.tags))), @@ -651,8 +677,12 @@ trait SearchConverterService { filtered.sortBy(!_.isPrimary).map(c => searchableContextToApiContext(c, language)) } - def articleHitAsMultiSummary(hit: SearchHit, language: String, filterInactive: Boolean): MultiSearchSummaryDTO = { - val searchableArticle = CirceUtil.unsafeParseAs[SearchableArticle](hit.sourceAsString) + def articleHitAsMultiSummary( + hit: SearchHit, + language: String, + filterInactive: Boolean + ): Try[MultiSearchSummaryDTO] = { + val searchableArticle = CirceUtil.tryParseAs[SearchableArticle](hit.sourceAsString).? val context = searchableArticle.context.map(c => searchableContextToApiContext(c, language)) val contexts = filterContexts(searchableArticle.contexts, language, filterInactive) @@ -670,7 +700,7 @@ trait SearchConverterService { val metaDescriptions = searchableArticle.metaDescription.languageValues.map(lv => MetaDescriptionDTO(lv.value, lv.language)) val visualElements = - searchableArticle.visualElement.languageValues.map(lv => api.article.VisualElementDTO(lv.value, lv.language)) + searchableArticle.domainObject.visualElement.map(lv => api.article.VisualElementDTO(lv.value, lv.language)) val metaImages = searchableArticle.metaImage.map(image => { val metaImageUrl = s"${props.ExternalApiUrls("raw-image")}/${image.imageId}" MetaImageDTO(metaImageUrl, image.altText, image.language) @@ -689,39 +719,45 @@ trait SearchConverterService { val url = s"${props.ExternalApiUrls("article-api")}/${searchableArticle.id}" - MultiSearchSummaryDTO( - id = searchableArticle.id, - title = title, - metaDescription = metaDescription, - metaImage = metaImage, - url = url, - context = context, - contexts = contexts, - supportedLanguages = supportedLanguages, - learningResourceType = searchableArticle.learningResourceType, - status = None, - traits = searchableArticle.traits, - score = hit.score, - highlights = getHighlights(hit.highlight), - paths = getPathsFromContext(searchableArticle.contexts), - lastUpdated = searchableArticle.lastUpdated, - license = Some(searchableArticle.license), - revisions = Seq.empty, - responsible = None, - comments = None, - prioritized = None, - priority = None, - resourceTypeName = None, - parentTopicName = None, - primaryRootName = None, - published = None, - favorited = None, - resultType = SearchType.Articles + Success( + MultiSearchSummaryDTO( + id = searchableArticle.id, + title = title, + metaDescription = metaDescription, + metaImage = metaImage, + url = url, + context = context, + contexts = contexts, + supportedLanguages = supportedLanguages, + learningResourceType = searchableArticle.learningResourceType, + status = None, + traits = searchableArticle.traits, + score = hit.score, + highlights = getHighlights(hit.highlight), + paths = getPathsFromContext(searchableArticle.contexts), + lastUpdated = searchableArticle.lastUpdated, + license = Some(searchableArticle.license), + revisions = Seq.empty, + responsible = None, + comments = None, + prioritized = None, + priority = None, + resourceTypeName = None, + parentTopicName = None, + primaryRootName = None, + published = None, + favorited = None, + resultType = SearchType.Articles + ) ) } - def draftHitAsMultiSummary(hit: SearchHit, language: String, filterInactive: Boolean): MultiSearchSummaryDTO = { - val searchableDraft = CirceUtil.unsafeParseAs[SearchableDraft](hit.sourceAsString) + def draftHitAsMultiSummary( + hit: SearchHit, + language: String, + filterInactive: Boolean + ): Try[MultiSearchSummaryDTO] = { + val searchableDraft = CirceUtil.tryParseAs[SearchableDraft](hit.sourceAsString).? val context = searchableDraft.context.map(c => searchableContextToApiContext(c, language)) val contexts = filterContexts(searchableDraft.contexts, language, filterInactive) @@ -742,7 +778,7 @@ trait SearchConverterService { common.model.api.search.MetaDescriptionDTO(lv.value, lv.language) ) val visualElements = - searchableDraft.visualElement.languageValues.map(lv => api.article.VisualElementDTO(lv.value, lv.language)) + searchableDraft.domainObject.visualElement.map(lv => api.article.VisualElementDTO(lv.value, lv.language)) val metaImages = searchableDraft.domainObject.metaImage.map(image => { val metaImageUrl = s"${props.ExternalApiUrls("raw-image")}/${image.imageId}" common.model.api.search.MetaImageDTO(metaImageUrl, image.altText, image.language) @@ -770,36 +806,38 @@ trait SearchConverterService { val parentTopicName = searchableDraft.parentTopicName.getLanguageOrDefault(language) val primaryRootName = searchableDraft.primaryRoot.getLanguageOrDefault(language) - MultiSearchSummaryDTO( - id = searchableDraft.id, - title = title, - metaDescription = metaDescription, - metaImage = metaImage, - url = url, - context = context, - contexts = contexts, - supportedLanguages = supportedLanguages, - learningResourceType = searchableDraft.learningResourceType, - status = Some( - common.model.api.search.StatusDTO(searchableDraft.draftStatus.current, searchableDraft.draftStatus.other) - ), - traits = searchableDraft.traits, - score = hit.score, - highlights = getHighlights(hit.highlight), - paths = getPathsFromContext(searchableDraft.contexts), - lastUpdated = searchableDraft.lastUpdated, - license = searchableDraft.license, - revisions = revisions, - responsible = responsible, - comments = Some(comments), - prioritized = Some(searchableDraft.priority == Priority.Prioritized), - priority = Some(searchableDraft.priority.entryName), - resourceTypeName = resourceTypeName, - parentTopicName = parentTopicName, - primaryRootName = primaryRootName, - published = Some(searchableDraft.published), - favorited = Some(searchableDraft.favorited), - resultType = SearchType.Drafts + Success( + MultiSearchSummaryDTO( + id = searchableDraft.id, + title = title, + metaDescription = metaDescription, + metaImage = metaImage, + url = url, + context = context, + contexts = contexts, + supportedLanguages = supportedLanguages, + learningResourceType = searchableDraft.learningResourceType, + status = Some( + common.model.api.search.StatusDTO(searchableDraft.draftStatus.current, searchableDraft.draftStatus.other) + ), + traits = searchableDraft.traits, + score = hit.score, + highlights = getHighlights(hit.highlight), + paths = getPathsFromContext(searchableDraft.contexts), + lastUpdated = searchableDraft.lastUpdated, + license = searchableDraft.license, + revisions = revisions, + responsible = responsible, + comments = Some(comments), + prioritized = Some(searchableDraft.priority == Priority.Prioritized), + priority = Some(searchableDraft.priority.entryName), + resourceTypeName = resourceTypeName, + parentTopicName = parentTopicName, + primaryRootName = primaryRootName, + published = Some(searchableDraft.published), + favorited = Some(searchableDraft.favorited), + resultType = SearchType.Drafts + ) ) } @@ -807,8 +845,8 @@ trait SearchConverterService { hit: SearchHit, language: String, filterInactive: Boolean - ): MultiSearchSummaryDTO = { - val searchableLearningPath = CirceUtil.unsafeParseAs[SearchableLearningPath](hit.sourceAsString) + ): Try[MultiSearchSummaryDTO] = { + val searchableLearningPath = CirceUtil.tryParseAs[SearchableLearningPath](hit.sourceAsString).? val context = searchableLearningPath.context.map(c => searchableContextToApiContext(c, language)) val contexts = filterContexts(searchableLearningPath.contexts, language, filterInactive) @@ -844,39 +882,41 @@ trait SearchConverterService { ) ) - MultiSearchSummaryDTO( - id = searchableLearningPath.id, - title = title, - metaDescription = metaDescription, - metaImage = metaImage, - url = url, - context = context, - contexts = contexts, - supportedLanguages = supportedLanguages, - learningResourceType = LearningResourceType.LearningPath, - status = Some(common.model.api.search.StatusDTO(searchableLearningPath.status, Seq.empty)), - traits = List.empty, - score = hit.score, - highlights = getHighlights(hit.highlight), - paths = getPathsFromContext(searchableLearningPath.contexts), - lastUpdated = searchableLearningPath.lastUpdated, - license = Some(searchableLearningPath.license), - revisions = Seq.empty, - responsible = None, - comments = None, - prioritized = None, - priority = None, - resourceTypeName = None, - parentTopicName = None, - primaryRootName = None, - published = None, - favorited = Some(searchableLearningPath.favorited), - resultType = SearchType.LearningPaths + Success( + MultiSearchSummaryDTO( + id = searchableLearningPath.id, + title = title, + metaDescription = metaDescription, + metaImage = metaImage, + url = url, + context = context, + contexts = contexts, + supportedLanguages = supportedLanguages, + learningResourceType = LearningResourceType.LearningPath, + status = Some(common.model.api.search.StatusDTO(searchableLearningPath.status, Seq.empty)), + traits = List.empty, + score = hit.score, + highlights = getHighlights(hit.highlight), + paths = getPathsFromContext(searchableLearningPath.contexts), + lastUpdated = searchableLearningPath.lastUpdated, + license = Some(searchableLearningPath.license), + revisions = Seq.empty, + responsible = None, + comments = None, + prioritized = None, + priority = None, + resourceTypeName = None, + parentTopicName = None, + primaryRootName = None, + published = None, + favorited = Some(searchableLearningPath.favorited), + resultType = SearchType.LearningPaths + ) ) } - def conceptHitAsMultiSummary(hit: SearchHit, language: String): MultiSearchSummaryDTO = { - val searchableConcept = CirceUtil.unsafeParseAs[SearchableConcept](hit.sourceAsString) + def conceptHitAsMultiSummary(hit: SearchHit, language: String): Try[MultiSearchSummaryDTO] = { + val searchableConcept = CirceUtil.tryParseAs[SearchableConcept](hit.sourceAsString).? val titles = searchableConcept.title.languageValues.map(lv => @@ -908,34 +948,36 @@ trait SearchConverterService { common.model.api.search.MetaDescriptionDTO("", UnknownLanguage.toString) ) - MultiSearchSummaryDTO( - id = searchableConcept.id, - title = title, - metaDescription = metaDescription, - metaImage = metaImage, - url = url, - context = None, - contexts = List.empty, - supportedLanguages = supportedLanguages, - learningResourceType = searchableConcept.learningResourceType, - status = Some(searchableConcept.draftStatus), - 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 + Success( + MultiSearchSummaryDTO( + id = searchableConcept.id, + title = title, + metaDescription = metaDescription, + metaImage = metaImage, + url = url, + context = None, + contexts = List.empty, + supportedLanguages = supportedLanguages, + learningResourceType = searchableConcept.learningResourceType, + status = Some(searchableConcept.draftStatus), + 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 + ) ) } @@ -943,19 +985,21 @@ trait SearchConverterService { context: SearchableTaxonomyContext, language: String ): ApiTaxonomyContextDTO = { - val subjectName = findByLanguageOrBestEffort(context.root.languageValues, language).map(_.value).getOrElse("") + val subjectName = + findByLanguageOrBestEffort(context.domainObject.root.languageValues, language).map(_.value).getOrElse("") val breadcrumbs = findByLanguageOrBestEffort(context.breadcrumbs.languageValues, language) .map(_.value) .getOrElse(Seq.empty) .toList - val resourceTypes = context.resourceTypes.map(rt => { + val resourceTypes = context.domainObject.resourceTypes.map(rt => { val name = findByLanguageOrBestEffort(rt.name.languageValues, language) .getOrElse(LanguageValue(UnknownLanguage.toString, "")) TaxonomyResourceTypeDTO(id = rt.id, name = name.value, language = name.language) }) - val relevance = findByLanguageOrBestEffort(context.relevance.languageValues, language).map(_.value).getOrElse("") + val relevance = + findByLanguageOrBestEffort(context.domainObject.relevance.languageValues, language).map(_.value).getOrElse("") ApiTaxonomyContextDTO( publicId = context.publicId, @@ -1074,6 +1118,30 @@ trait SearchConverterService { group ) - } + def asFrontPage(frontpage: Option[SubjectPage]): Try[Option[SearchableSubjectPage]] = { + frontpage match { + case None => Success(None) + case Some(fp) => + fp.id match { + case None => + Failure(MissingIdException("Missing id for fetched frontpage. This is weird and probably a bug.")) + case Some(id) => + Success(Some(SearchableSubjectPage(id = id, name = fp.name, domainObject = fp))) + } + } + } + def asSearchableNode(node: Node, frontpage: Option[SubjectPage]): Try[SearchableNode] = { + asFrontPage(frontpage).map { frontpage => + SearchableNode( + nodeId = node.id, + title = getSearchableLanguageValues(node.name, node.translations), + url = node.url, + contentUri = node.contentUri, + nodeType = node.nodeType, + subjectPage = frontpage + ) + } + } + } } 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 d6f8abfc9e..82ec11e93e 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 @@ -20,7 +20,7 @@ import SortOrder.{Asc, Desc} import com.typesafe.scalalogging.StrictLogging import no.ndla.common.model.api.search.{ MultiSearchSuggestionDTO, - MultiSearchSummaryDTO, + MultiSummaryBaseDTO, SearchSuggestionDTO, SearchType, SuggestOptionDTO @@ -41,7 +41,7 @@ import scala.util.{Failure, Success, Try} trait SearchService { this: Elastic4sClient & IndexService & SearchConverterService & StrictLogging & Props & BaseIndexService & - ErrorHandling => + ErrorHandling & SearchLanguage => trait SearchService { import props.{DefaultLanguage, ElasticSearchScrollKeepAlive, MaxPageSize} @@ -57,17 +57,19 @@ trait SearchService { * @return * api-model summary of hit */ - private def hitToApiModel(hit: SearchHit, language: String, filterInactive: Boolean): Try[MultiSearchSummaryDTO] = { + private def hitToApiModel(hit: SearchHit, language: String, filterInactive: Boolean): Try[MultiSummaryBaseDTO] = { val indexName = hit.index.split("_").headOption.traverse(x => props.indexToSearchType(x)) indexName.flatMap { case Some(SearchType.Articles) => - Success(searchConverterService.articleHitAsMultiSummary(hit, language, filterInactive)) + searchConverterService.articleHitAsMultiSummary(hit, language, filterInactive) case Some(SearchType.Drafts) => - Success(searchConverterService.draftHitAsMultiSummary(hit, language, filterInactive)) + searchConverterService.draftHitAsMultiSummary(hit, language, filterInactive) case Some(SearchType.LearningPaths) => - Success(searchConverterService.learningpathHitAsMultiSummary(hit, language, filterInactive)) + searchConverterService.learningpathHitAsMultiSummary(hit, language, filterInactive) case Some(SearchType.Concepts) => - Success(searchConverterService.conceptHitAsMultiSummary(hit, language)) + searchConverterService.conceptHitAsMultiSummary(hit, language) + case Some(SearchType.Nodes) => + searchConverterService.nodeHitAsMultiSummary(hit, language) case Some(SearchType.Grep) => Failure(NdlaSearchException("Got hit from grep index (SearchType.Grep) in `hitToApiModel`. This is a bug.")) case None => @@ -148,7 +150,7 @@ trait SearchService { response: SearchResponse, language: String, filterInactive: Boolean - ): Try[Seq[MultiSearchSummaryDTO]] = { + ): Try[Seq[MultiSummaryBaseDTO]] = { response.totalHits match { case count if count > 0 => val resultArray = response.hits.hits.toList 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 e6535ae29a..1aa1779dac 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 @@ -97,16 +97,16 @@ trait TaxonomyFiltering { if (filterByNoResourceType) { Some( boolQuery().not( - nestedQuery("contexts.resourceTypes", existsQuery("contexts.resourceTypes")) + nestedQuery("contexts", existsQuery("contexts.resourceTypeIds")) ) ) } else { None } } else { Some( nestedQuery( - "contexts.resourceTypes", + "contexts", boolQuery().should( - resourceTypes.map(resourceTypeId => termQuery("contexts.resourceTypes.id", resourceTypeId)) + resourceTypes.map(resourceTypeId => termQuery("contexts.resourceTypeIds", resourceTypeId)) ) ) ) diff --git a/search-api/src/test/scala/no/ndla/searchapi/SearchTestUtility.scala b/search-api/src/test/scala/no/ndla/searchapi/SearchTestUtility.scala new file mode 100644 index 0000000000..a8e3b3a596 --- /dev/null +++ b/search-api/src/test/scala/no/ndla/searchapi/SearchTestUtility.scala @@ -0,0 +1,27 @@ +/* + * Part of NDLA search-api + * Copyright (C) 2025 NDLA + * + * See LICENSE + * + */ +package no.ndla.searchapi + +import no.ndla.common.model.api.search.MultiSearchSummaryDTO +import no.ndla.searchapi.model.domain.SearchResult +import org.scalatest.Assertions + +object SearchTestUtility extends Assertions { + implicit class searchResultHelper(result: SearchResult) { + + /** Helper to convert search results to only `MultiSearchSummaryDTO` If the result contains other types the test + * fails + */ + def summaryResults: Seq[MultiSearchSummaryDTO] = { + result.results.map { + case x: MultiSearchSummaryDTO => x + case x => fail(s"Did not expect type of '${x.getClass.getSimpleName}'") + } + } + } +} 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 c8674c6b54..41baec7575 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/TestData.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/TestData.scala @@ -1713,7 +1713,9 @@ object TestData { embedId = None, availability = List.empty, articleTypes = List.empty, - filterInactive = false + filterInactive = false, + resultTypes = None, + nodeTypeFilter = List.empty ) val multiDraftSearchSettings: MultiDraftSearchSettings = MultiDraftSearchSettings( @@ -1771,7 +1773,6 @@ object TestData { publicId = "urn:resource:101", contextId = "contextId", rootId = "urn:subject:1", - root = SearchableLanguageValues(Seq(LanguageValue("nb", "Matte"))), path = "/subject:3/topic:1/topic:151/resource:101", breadcrumbs = SearchableLanguageList( Seq( @@ -1780,12 +1781,34 @@ object TestData { ), contextType = LearningResourceType.Article.toString, relevanceId = "urn:relevance:core", - relevance = SearchableLanguageValues(Seq(LanguageValue("nb", "Kjernestoff"))), - resourceTypes = searchableResourceTypes, + resourceTypeIds = searchableResourceTypes.map(_.id), parentIds = List("urn:topic:1"), isPrimary = true, isActive = true, - url = "/subject:3/topic:1/topic:151/resource:101" + url = "/subject:3/topic:1/topic:151/resource:101", + domainObject = TaxonomyContext( + publicId = "urn:resource:101", + rootId = "urn:subject:1", + root = SearchableLanguageValues(Seq(LanguageValue("nb", "Matte"))), + path = "/subject:3/topic:1/topic:151/resource:101", + breadcrumbs = SearchableLanguageList( + Seq( + LanguageValue("nb", Seq("Matte", "Østen for solen", "Vesten for månen")) + ) + ), + contextType = Some(LearningResourceType.Article.toString), + relevanceId = "urn:relevance:core", + relevance = SearchableLanguageValues(Seq(LanguageValue("nb", "Kjernestoff"))), + resourceTypes = resourceTypes.map(rt => + SearchableTaxonomyResourceType(rt.id, SearchableLanguageValues(Seq(LanguageValue("nb", rt.name)))) + ), + parentIds = List("urn:topic:1"), + isPrimary = true, + contextId = Random.alphanumeric.take(12).mkString, + isVisible = true, + isActive = true, + url = "/subject:3/topic:1/topic:151/resource:101" + ) ) val searchableTaxonomyContexts: List[SearchableTaxonomyContext] = List( @@ -1840,7 +1863,6 @@ object TestData { id = 100, title = searchableTitles, content = searchableContents, - visualElement = searchableVisualElements, introduction = searchableIntroductions, metaDescription = searchableMetaDescriptions, tags = searchableTags, 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 c1af234283..f339034405 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/TestEnvironment.scala @@ -12,9 +12,9 @@ import com.typesafe.scalalogging.StrictLogging import no.ndla.common.Clock import no.ndla.database.DBUtility import no.ndla.network.NdlaClient -import no.ndla.network.clients.{FeideApiClient, MyNDLAApiClient, RedisClient} +import no.ndla.network.clients.{FeideApiClient, FrontpageApiClient, MyNDLAApiClient, RedisClient} import no.ndla.network.tapir.TapirApplication -import no.ndla.search.{BaseIndexService, Elastic4sClient} +import no.ndla.search.{BaseIndexService, Elastic4sClient, SearchLanguage} import no.ndla.searchapi.controller.parameters.GetSearchQueryParams import no.ndla.searchapi.controller.{InternController, SearchController} import no.ndla.searchapi.integration.* @@ -30,6 +30,8 @@ trait TestEnvironment with ArticleIndexService with MultiSearchService with DraftIndexService + with NodeIndexService + with FrontpageApiClient with DraftConceptApiClient with DraftConceptIndexService with MultiDraftSearchService @@ -41,6 +43,7 @@ trait TestEnvironment with TaxonomyApiClient with DBUtility with IndexService + with SearchLanguage with BaseIndexService with StrictLogging with LearningPathApiClient @@ -78,7 +81,8 @@ trait TestEnvironment val draftConceptApiClient: DraftConceptApiClient = mock[DraftConceptApiClient] val feideApiClient: FeideApiClient = mock[FeideApiClient] val redisClient: RedisClient = mock[RedisClient] - val DBUtil = mock[DBUtility] + val frontpageApiClient: FrontpageApiClient = mock[FrontpageApiClient] + val DBUtil: DBUtility = mock[DBUtility] val clock: SystemClock = mock[SystemClock] @@ -92,6 +96,7 @@ trait TestEnvironment val draftIndexService: DraftIndexService = mock[DraftIndexService] val draftConceptIndexService: DraftConceptIndexService = mock[DraftConceptIndexService] val grepIndexService: GrepIndexService = mock[GrepIndexService] + val nodeIndexService: NodeIndexService = mock[NodeIndexService] val multiDraftSearchService: MultiDraftSearchService = mock[MultiDraftSearchService] diff --git a/search-api/src/test/scala/no/ndla/searchapi/controller/SearchControllerTest.scala b/search-api/src/test/scala/no/ndla/searchapi/controller/SearchControllerTest.scala index 5e4f37a4e3..fce284e19e 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/controller/SearchControllerTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/controller/SearchControllerTest.scala @@ -215,7 +215,8 @@ class SearchControllerTest extends UnitSuite with TestEnvironment with TapirCont language = "nn", pageSize = 10, shouldScroll = true, - sort = Sort.ByRelevanceDesc + sort = Sort.ByRelevanceDesc, + resultTypes = Some(List.empty) ) verify(multiDraftSearchService, times(0)).scroll(any[String], any[String]) @@ -230,7 +231,12 @@ class SearchControllerTest extends UnitSuite with TestEnvironment with TapirCont val multiResult = domain.SearchResult(0, None, 10, "nn", Seq.empty, Seq.empty, Seq.empty, None) when(multiSearchService.matchingQuery(any)).thenReturn(Success(multiResult)) - val baseSettings = TestData.searchSettings.copy(language = "*", pageSize = 10, sort = Sort.ByRelevanceDesc) + val baseSettings = TestData.searchSettings.copy( + language = "*", + pageSize = 10, + sort = Sort.ByRelevanceDesc, + resultTypes = Some(List.empty) + ) val response = simpleHttpClient.send( quickRequest.get(uri"http://localhost:$serverPort/search-api/v1/search/") @@ -255,7 +261,12 @@ class SearchControllerTest extends UnitSuite with TestEnvironment with TapirCont when(feideApiClient.getFeideExtendedUser(any)).thenReturn(Success(teacheruser)) when(multiSearchService.matchingQuery(any)).thenReturn(Success(multiResult)) - val baseSettings = TestData.searchSettings.copy(language = "*", pageSize = 10, sort = Sort.ByRelevanceDesc) + val baseSettings = TestData.searchSettings.copy( + language = "*", + pageSize = 10, + sort = Sort.ByRelevanceDesc, + resultTypes = Some(List.empty) + ) val teacherToken = "abcd" val response = simpleHttpClient.send( 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 590b1d5a80..87989a484f 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 @@ -31,8 +31,6 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { ) ) - val visualElements = SearchableLanguageValues(Seq(LanguageValue("nn", "image"), LanguageValue("nb", "image"))) - val introductions = SearchableLanguageValues( Seq( LanguageValue("en", "Wroom wroom") @@ -67,7 +65,6 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { id = 100, title = titles, content = contents, - visualElement = visualElements, introduction = introductions, metaDescription = metaDescriptions, tags = tags, @@ -110,8 +107,6 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { ) ) - val visualElements = SearchableLanguageValues(Seq(LanguageValue("nn", "image"), LanguageValue("nb", "image"))) - val introductions = SearchableLanguageValues( Seq( LanguageValue("en", "Wroom wroom") @@ -146,7 +141,6 @@ class SearchableArticleTest extends UnitSuite with TestEnvironment { id = 100, title = titles, content = contents, - visualElement = visualElements, introduction = introductions, metaDescription = metaDescriptions, tags = tags, 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 81f00012cb..6853b9c3d0 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 @@ -35,8 +35,6 @@ class SearchableDraftTest extends UnitSuite with TestEnvironment { ) ) - val visualElements = SearchableLanguageValues(Seq(LanguageValue("nn", "image"), LanguageValue("nb", "image"))) - val introductions = SearchableLanguageValues( Seq( LanguageValue("en", "Wroom wroom") @@ -87,7 +85,6 @@ class SearchableDraftTest extends UnitSuite with TestEnvironment { id = 100, title = titles, content = contents, - visualElement = visualElements, introduction = introductions, metaDescription = metaDescriptions, tags = tags, 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 71d391a032..720587ceaf 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 @@ -25,6 +25,7 @@ import no.ndla.searchapi.TestData.* import no.ndla.searchapi.model.domain.{IndexingBundle, Sort} import no.ndla.searchapi.model.taxonomy.* import no.ndla.searchapi.{TestData, TestEnvironment} +import no.ndla.searchapi.SearchTestUtility.* import java.util.UUID import scala.util.{Success, Try} @@ -106,7 +107,7 @@ class MultiDraftSearchServiceAtomicTest ) search1.totalCount should be(1) - search1.results.map(_.id) should be(List(2)) + search1.summaryResults.map(_.id) should be(List(2)) val Success(search2) = multiDraftSearchService.matchingQuery( @@ -114,7 +115,7 @@ class MultiDraftSearchServiceAtomicTest ) search2.totalCount should be(2) - search2.results.map(_.id) should be(List(1, 2)) + search2.summaryResults.map(_.id) should be(List(1, 2)) } test("That sorting by revision date sorts by the earliest 'needs-revision'") { @@ -184,7 +185,7 @@ class MultiDraftSearchServiceAtomicTest ) search1.totalCount should be(4) - search1.results.map(_.id) should be(List(3, 1, 2, 4)) + search1.summaryResults.map(_.id) should be(List(3, 1, 2, 4)) val Success(search2) = multiDraftSearchService.matchingQuery( @@ -192,7 +193,7 @@ class MultiDraftSearchServiceAtomicTest ) search2.totalCount should be(4) - search2.results.map(_.id) should be(List(1, 3, 2, 4)) + search2.summaryResults.map(_.id) should be(List(1, 3, 2, 4)) } test("Test that searching for note in revision meta works as expected") { @@ -262,7 +263,7 @@ class MultiDraftSearchServiceAtomicTest ) search1.totalCount should be(1) - search1.results.map(_.id) should be(List(3)) + search1.summaryResults.map(_.id) should be(List(3)) } test("Test that filtering revision dates works as expected") { @@ -326,7 +327,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1)) multiDraftSearchService @@ -337,7 +338,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(3)) multiDraftSearchService @@ -348,7 +349,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 3)) } @@ -401,7 +402,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2, 3)) multiDraftSearchService @@ -412,7 +413,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 2, 3)) } @@ -442,7 +443,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 2, 3)) multiDraftSearchService @@ -452,7 +453,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 3)) multiDraftSearchService @@ -462,7 +463,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2)) multiDraftSearchService @@ -472,7 +473,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 2, 3)) } @@ -508,7 +509,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 3, 2, 4)) multiDraftSearchService @@ -519,7 +520,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2, 3, 1, 4)) } @@ -735,7 +736,7 @@ class MultiDraftSearchServiceAtomicTest val result = multiDraftSearchService.matchingQuery(multiDraftSearchSettings).get - def ctxsFor(id: Long): List[ApiTaxonomyContextDTO] = result.results.find(_.id == id).get.contexts + def ctxsFor(id: Long): List[ApiTaxonomyContextDTO] = result.summaryResults.find(_.id == id).get.contexts def ctxFor(id: Long): ApiTaxonomyContextDTO = { val ctxs = ctxsFor(id) ctxs.length should be(1) @@ -783,7 +784,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(4, 3, 1, 2)) multiDraftSearchService @@ -794,7 +795,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2, 1, 3, 4)) } @@ -834,7 +835,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2, 3)) multiDraftSearchService @@ -844,7 +845,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 4)) multiDraftSearchService .matchingQuery( @@ -853,7 +854,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(5)) multiDraftSearchService @@ -863,7 +864,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 2, 3, 4, 5)) } @@ -888,7 +889,7 @@ class MultiDraftSearchServiceAtomicTest ) search1.totalCount should be(1) - search1.results.map(_.id) should be(List(1)) + search1.summaryResults.map(_.id) should be(List(1)) } test("That sorting on resource types works as expected") { @@ -949,17 +950,17 @@ class MultiDraftSearchServiceAtomicTest val Success(search1) = searchService.matchingQuery( multiDraftSearchSettings.copy(sort = Sort.ByParentTopicNameAsc) ) - search1.results.map(_.id) should be(List(1, 2, 3)) + search1.summaryResults.map(_.id) should be(List(1, 2, 3)) val Success(search2) = searchService.matchingQuery( multiDraftSearchSettings.copy(sort = Sort.ByPrimaryRootAsc) ) - search2.results.map(_.id) should be(List(2, 3, 1)) + search2.summaryResults.map(_.id) should be(List(2, 3, 1)) val Success(search3) = searchService.matchingQuery( multiDraftSearchSettings.copy(sort = Sort.ByResourceTypeAsc) ) - search3.results.map(_.id) should be(List(3, 1, 2)) + search3.summaryResults.map(_.id) should be(List(3, 1, 2)) } @@ -997,7 +998,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1)) multiDraftSearchService @@ -1008,7 +1009,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(2, 3, 4)) multiDraftSearchService @@ -1019,7 +1020,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 2, 3)) } @@ -1056,7 +1057,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(4, 2, 3, 1)) multiDraftSearchService @@ -1066,7 +1067,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(_.id) should be(Seq(1, 3, 2, 4)) } @@ -1102,7 +1103,7 @@ class MultiDraftSearchServiceAtomicTest multiDraftSearchService .matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByIdAsc)) .get - .results + .summaryResults .map(r => r.id -> r.resultType) should be( Seq( 1 -> SearchType.Drafts, @@ -1120,7 +1121,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(r => r.id -> r.resultType) should be( Seq( 1 -> SearchType.Concepts, @@ -1168,7 +1169,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(r => r.id -> r.resultType) should be( Seq(2 -> SearchType.Concepts) ) @@ -1182,7 +1183,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - .results + .summaryResults .map(r => r.id -> r.resultType) should be( Seq(1 -> SearchType.Concepts) ) @@ -1224,7 +1225,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(1)) + search.summaryResults.map(_.id) should be(Seq(1)) } { val search = multiDraftSearchService @@ -1235,7 +1236,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(2)) + search.summaryResults.map(_.id) should be(Seq(2)) } { val search = multiDraftSearchService @@ -1246,7 +1247,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(3)) + search.summaryResults.map(_.id) should be(Seq(3)) } { val search = multiDraftSearchService @@ -1257,7 +1258,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(4)) + search.summaryResults.map(_.id) should be(Seq(4)) } { val search = multiDraftSearchService @@ -1268,7 +1269,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(5)) + search.summaryResults.map(_.id) should be(Seq(5)) } } @@ -1304,7 +1305,7 @@ class MultiDraftSearchServiceAtomicTest ) ) .get - search.results.map(_.id) should be(Seq(1, 3)) + search.summaryResults.map(_.id) should be(Seq(1, 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 8f2d509d93..ba0ced700c 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 @@ -20,6 +20,7 @@ import no.ndla.searchapi.TestData.* import no.ndla.searchapi.model.domain.{IndexingBundle, Sort} import no.ndla.searchapi.model.search.SearchPagination import no.ndla.searchapi.{TestData, TestEnvironment} +import no.ndla.searchapi.SearchTestUtility.* import scala.util.Success @@ -107,28 +108,28 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByIdAsc)) val expected = idsForLang("nb").sorted results.totalCount should be(expected.size) - results.results.map(_.id) should be(expected) + results.summaryResults.map(_.id) should be(expected) } test("That all returns all documents ordered by id descending") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByIdDesc)) val expected = idsForLang("nb").sorted.reverse results.totalCount should be(expected.size) - results.results.map(_.id) should be(expected) + results.summaryResults.map(_.id) should be(expected) } test("That all returns all documents ordered by title ascending") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByTitleAsc)) val expected = titlesForLang("nb").sorted results.totalCount should be(expected.size) - results.results.map(_.title.title) should be(expected) + results.summaryResults.map(_.title.title) should be(expected) } test("That all returns all documents ordered by title descending") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByTitleDesc)) val expected = titlesForLang("nb").sorted.reverse results.totalCount should be(expected.size) - results.results.map(_.title.title) should be(expected) + results.summaryResults.map(_.title.title) should be(expected) } test("That all returns all documents ordered by lastUpdated descending") { @@ -136,8 +137,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByLastUpdatedDesc)) val expected = idsForLang("nb") results.totalCount should be(expected.size) - results.results.head.id should be(4) - results.results.last.id should be(5) + results.summaryResults.head.id should be(4) + results.summaryResults.last.id should be(5) } test("That all returns all documents ordered by lastUpdated ascending") { @@ -145,9 +146,9 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(sort = Sort.ByLastUpdatedAsc)) val expected = idsForLang("nb") results.totalCount should be(expected.size) - results.results.head.id should be(5) - results.results(1).id should be(1) - results.results.last.id should be(4) + results.summaryResults.head.id should be(5) + results.summaryResults(1).id should be(1) + results.summaryResults.last.id should be(4) } test("That paging returns only hits on current page and not more than page-size") { @@ -156,8 +157,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(page2) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(page = 2, pageSize = 2, sort = Sort.ByIdAsc)) val expected = idsForLang("nb") - val hits1 = page1.results - val hits2 = page2.results + val hits1 = page1.summaryResults + val hits2 = page2.summaryResults page1.totalCount should be(expected.size) page1.page.get should be(1) hits1.size should be(2) @@ -176,7 +177,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("bil").get), sort = Sort.ByRelevanceDesc) ) results.totalCount should be(3) - results.results.map(_.id) should be(Seq(1, 5, 3)) + results.summaryResults.map(_.id) should be(Seq(1, 5, 3)) } test("That search combined with filter by id only returns documents matching the query with one of the given ids") { @@ -188,7 +189,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo withIdIn = List(3) ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(3) hits.last.id should be(3) @@ -200,8 +201,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("Pingvinen").get), sort = Sort.ByTitleAsc) ) - results.results.map(_.contexts.head.contextType) should be(Seq("learningpath", "standard")) - results.results.map(_.id) should be(Seq(1, 2)) + results.summaryResults.map(_.contexts.head.contextType) should be(Seq("learningpath", "standard")) + results.summaryResults.map(_.id) should be(Seq(1, 2)) results.totalCount should be(2) } @@ -210,7 +211,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(sort = Sort.ByIdAsc, userFilter = List("ndalId54321")) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(13) hits.head.id should be(1) hits.head.contexts.head.contextType should be("standard") @@ -222,7 +223,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(sort = Sort.ByIdAsc, userFilter = List("ndalId12345")) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(5) hits.head.contexts.head.contextType should be("standard") @@ -233,7 +234,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("and").get), sort = Sort.ByTitleAsc) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(2) hits.head.id should be(3) hits(1).id should be(3) @@ -249,7 +250,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(4) } @@ -262,7 +263,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - val hits1 = search1.results + val hits1 = search1.summaryResults hits1.map(_.id) should equal(Seq(1, 3, 5)) val Success(search2) = @@ -272,7 +273,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - val hits2 = search2.results + val hits2 = search2.summaryResults hits2.map(_.id) should equal(Seq(1)) } @@ -283,7 +284,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - search1.results.map(_.id) should equal(Seq(3, 5)) + search1.summaryResults.map(_.id) should equal(Seq(3, 5)) val Success(search2) = multiDraftSearchService.matchingQuery( @@ -292,7 +293,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - search2.results.map(_.id) should equal(Seq(1, 3)) + search2.summaryResults.map(_.id) should equal(Seq(1, 3)) } test("search in content should be ranked lower than introduction and title") { @@ -303,7 +304,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByRelevanceDesc ) ) - val hits = search.results + val hits = search.summaryResults hits.map(_.id) should equal(Seq(9, 8)) } @@ -318,7 +319,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("Search for all languages should return all articles in correct language") { val Success(search) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(language = AllLanguages, pageSize = 100)) - val hits = search.results + val hits = search.summaryResults search.totalCount should equal(idsForLang("*").size) hits.head.id should be(1) @@ -353,7 +354,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo sort = Sort.ByTitleAsc ) ) - val hits = search.results + val hits = search.summaryResults search.totalCount should equal(1) hits.head.id should equal(4) @@ -374,14 +375,14 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) searchEn.totalCount should equal(1) - searchEn.results.head.id should equal(11) - searchEn.results.head.title.title should equal("Cats") - searchEn.results.head.title.language should equal("en") + searchEn.summaryResults.head.id should equal(11) + searchEn.summaryResults.head.title.title should equal("Cats") + searchEn.summaryResults.head.title.language should equal("en") searchNb.totalCount should equal(1) - searchNb.results.head.id should equal(11) - searchNb.results.head.title.title should equal("Katter") - searchNb.results.head.title.language should equal("nb") + searchNb.summaryResults.head.id should equal(11) + searchNb.summaryResults.head.title.title should equal("Katter") + searchNb.summaryResults.head.title.language should equal("nb") } test("Searching with query for unknown language should return nothing") { @@ -392,21 +393,6 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo search.totalCount should equal(0) } - test("Searching with query for language not in analyzers should work as expected") { - val Success(search) = multiDraftSearchService.matchingQuery( - multiDraftSearchSettings.copy( - query = Some(NonEmptyString.fromString("Chhattisgarhi").get), - language = "hne", - sort = Sort.ByRelevanceDesc - ) - ) - - search.totalCount should equal(1) - search.results.head.id should equal(13) - search.results.head.title.title should equal("Chhattisgarhi title") - search.results.head.title.language should equal("hne") - } - test("metadescription is searchable") { val Success(search) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings @@ -418,9 +404,9 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should equal(1) - search.results.head.id should equal(11) - search.results.head.title.title should equal("Cats") - search.results.head.title.language should equal("en") + search.summaryResults.head.id should equal(11) + search.summaryResults.head.title.title should equal("Cats") + search.summaryResults.head.title.language should equal("en") } test("That searching with fallback parameter returns article in language priority even if doesnt match on language") { @@ -430,12 +416,12 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should equal(3) - search.results.head.id should equal(9) - search.results.head.title.language should equal("nb") - search.results(1).id should equal(10) - search.results(1).title.language should equal("en") - search.results(2).id should equal(11) - search.results(2).title.language should equal("en") + search.summaryResults.head.id should equal(9) + search.summaryResults.head.title.language should equal("nb") + search.summaryResults(1).id should equal(10) + search.summaryResults(1).title.language should equal("en") + search.summaryResults(2).id should equal(11) + search.summaryResults(2).title.language should equal("en") } test("That filtering for subjects works as expected") { @@ -444,7 +430,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(language = "*", subjects = List("urn:subject:2")) ) search.totalCount should be(7) - search.results.map(_.id) should be(Seq(1, 5, 5, 6, 7, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 5, 5, 6, 7, 11, 12)) } test("That filtering for subjects with inactive contexts works as expected") { @@ -453,8 +439,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(language = "*", subjects = List("urn:subject:2"), filterInactive = true) ) search.totalCount should be(4) - search.results.flatMap(_.contexts).toList.length should be(5) - search.results.map(_.id) should be(Seq(5, 6, 11, 12)) + search.summaryResults.flatMap(_.contexts).toList.length should be(5) + search.summaryResults.map(_.id) should be(Seq(5, 6, 11, 12)) } test("That filtering for subjects returns all drafts with any of listed subjects") { @@ -463,14 +449,14 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(subjects = List("urn:subject:2", "urn:subject:1")) ) search.totalCount should be(15) - search.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 11, 12)) } test("That filtering for invisible subjects returns all drafts with any of listed subjects") { val Success(search) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(subjects = List("urn:subject:3"))) search.totalCount should be(2) - search.results.map(_.id) should be(Seq(1, 15)) + search.summaryResults.map(_.id) should be(Seq(1, 15)) } test("That filtering for resource-types works as expected") { @@ -479,21 +465,21 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(resourceTypes = List("urn:resourcetype:academicArticle")) ) search.totalCount should be(2) - search.results.map(_.id) should be(Seq(2, 5)) + search.summaryResults.map(_.id) should be(Seq(2, 5)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(resourceTypes = List("urn:resourcetype:subjectMaterial")) ) search2.totalCount should be(8) - search2.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) + search2.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) val Success(search3) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(resourceTypes = List("urn:resourcetype:learningpath")) ) search3.totalCount should be(4) - search3.results.map(_.id) should be(Seq(1, 2, 3, 4)) + search3.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4)) } test("That filtering on multiple context-types returns every selected type") { @@ -504,7 +490,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) ) - search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15)) search.totalCount should be(14) } @@ -517,8 +503,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) val totalCount = search.totalCount - val ids = search.results.map(_.id).length - val contextCount = search.results.flatMap(_.contexts).toList.length + val ids = search.summaryResults.map(_.id).length + val contextCount = search.summaryResults.flatMap(_.contexts).toList.length val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -528,8 +514,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) totalCount should be > search2.totalCount - ids should be > search2.results.map(_.id).length - contextCount should be > search2.results.flatMap(_.contexts).toList.length + ids should be > search2.summaryResults.map(_.id).length + contextCount should be > search2.summaryResults.flatMap(_.contexts).toList.length } test("That filtering on learning-resource-type works") { @@ -541,10 +527,10 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should be(8) - search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) search2.totalCount should be(6) - search2.results.map(_.id) should be(Seq(8, 9, 10, 11, 13, 15)) + search2.summaryResults.map(_.id) should be(Seq(8, 9, 10, 11, 13, 15)) } test("That filtering on article-type works") { @@ -559,13 +545,13 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should be(8) - search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6, 7, 12)) search2.totalCount should be(6) - search2.results.map(_.id) should be(Seq(8, 9, 10, 11, 13, 15)) + search2.summaryResults.map(_.id) should be(Seq(8, 9, 10, 11, 13, 15)) search3.totalCount should be(1) - search3.results.map(_.id) should be(Seq(16)) + search3.summaryResults.map(_.id) should be(Seq(16)) } test("That filtering on learningpath learningresourcetype returns learningpaths") { @@ -574,8 +560,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) 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)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6)) + search.summaryResults.map(_.url.contains("learningpath")).distinct should be(Seq(true)) } test("That filtering on supportedLanguages works") { @@ -584,21 +570,21 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings.copy(language = "*", supportedLanguages = List("en")) ) search.totalCount should be(9) - search.results.map(_.id) should be(Seq(2, 3, 4, 5, 6, 10, 11, 13, 15)) + search.summaryResults.map(_.id) should be(Seq(2, 3, 4, 5, 6, 10, 11, 13, 15)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = "*", supportedLanguages = List("en", "nb"), pageSize = 100) ) search2.totalCount should be(21) - search2.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16)) + search2.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16)) val Success(search3) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = "*", supportedLanguages = List("nb")) ) search3.totalCount should be(18) - search3.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16)) + search3.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 6, 7, 8, 9, 11, 12, 13, 15, 16)) } test("That filtering on supportedLanguages should still prioritize the selected language") { @@ -608,8 +594,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should be(6) - search.results.map(_.id) should be(Seq(2, 3, 4, 11, 13, 15)) - search.results.map(_.title.language) should be(Seq("nb", "nb", "nb", "nb", "nb", "nb")) + search.summaryResults.map(_.id) should be(Seq(2, 3, 4, 11, 13, 15)) + search.summaryResults.map(_.title.language) should be(Seq("nb", "nb", "nb", "nb", "nb", "nb")) } test("That meta image are returned when searching") { @@ -617,8 +603,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(language = "en", withIdIn = List(10))) search.totalCount should be(1) - search.results.head.id should be(10) - search.results.head.metaImage should be( + search.summaryResults.head.id should be(10) + search.summaryResults.head.metaImage should be( Some(MetaImageDTO("http://api-gateway.ndla-local/image-api/raw/id/123", "alt", "en")) ) } @@ -630,7 +616,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should be(1) - search.results.head.id should be(5) + search.summaryResults.head.id should be(5) val Success(search2) = multiDraftSearchService.matchingQuery( @@ -647,7 +633,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search.totalCount should be(1) - search.results.head.id should be(5) + search.summaryResults.head.id should be(5) } test("That filtering for topics returns every child learningResource") { @@ -656,7 +642,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo search.totalCount should be(8) - search.results.map(_.id) should be(Seq(1, 1, 2, 2, 4, 4, 9, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 4, 4, 9, 12)) } test("That searching for authors works as expected") { @@ -667,25 +653,25 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) ) search1.totalCount should be(1) - search1.results.map(_.id) should be(Seq(1)) + search1.summaryResults.map(_.id) should be(Seq(1)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("Svims").get), language = AllLanguages) ) search2.totalCount should be(2) - search2.results.map(_.id) should be(Seq(2, 5)) + search2.summaryResults.map(_.id) should be(Seq(2, 5)) } test("That filtering by relevance id works when no subject is specified") { val Success(search1) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = AllLanguages, relevanceIds = List("urn:relevance:core")) ) - search1.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) + search1.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = AllLanguages, relevanceIds = List("urn:relevance:supplementary")) ) - search2.results.map(_.id) should be(Seq(1, 2, 3, 4, 4, 5, 12, 15)) + search2.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 4, 5, 12, 15)) val Success(search3) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -693,7 +679,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo relevanceIds = List("urn:relevance:supplementary", "urn:relevance:core") ) ) - search3.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12, 15)) + search3.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12, 15)) } test("That filtering by relevance and subject only returns for relevances in filtered subjects") { @@ -702,7 +688,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo .copy(language = AllLanguages, subjects = List("urn:subject:2"), relevanceIds = List("urn:relevance:core")) ) - search1.results.map(_.id) should be(Seq(1, 5, 6, 7, 11)) + search1.summaryResults.map(_.id) should be(Seq(1, 5, 6, 7, 11)) } test("That scrolling works as expected") { @@ -726,19 +712,19 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(scroll11) = multiDraftSearchService.scroll(scroll10.scrollId.get, "*") val Success(scroll12) = multiDraftSearchService.scroll(scroll11.scrollId.get, "*") - initialSearch.results.map(_.id) should be(ids.head) - scroll1.results.map(_.id) should be(ids(1)) - scroll2.results.map(_.id) should be(ids(2)) - scroll3.results.map(_.id) should be(ids(3)) - scroll4.results.map(_.id) should be(ids(4)) - scroll5.results.map(_.id) should be(ids(5)) - scroll6.results.map(_.id) should be(ids(6)) - scroll7.results.map(_.id) should be(ids(7)) - scroll8.results.map(_.id) should be(ids(8)) - scroll9.results.map(_.id) should be(ids(9)) - scroll10.results.map(_.id) should be(ids(10)) - scroll11.results.map(_.id) should be(List.empty) - scroll12.results.map(_.id) should be(List.empty) + initialSearch.summaryResults.map(_.id) should be(ids.head) + scroll1.summaryResults.map(_.id) should be(ids(1)) + scroll2.summaryResults.map(_.id) should be(ids(2)) + scroll3.summaryResults.map(_.id) should be(ids(3)) + scroll4.summaryResults.map(_.id) should be(ids(4)) + scroll5.summaryResults.map(_.id) should be(ids(5)) + scroll6.summaryResults.map(_.id) should be(ids(6)) + scroll7.summaryResults.map(_.id) should be(ids(7)) + scroll8.summaryResults.map(_.id) should be(ids(8)) + scroll9.summaryResults.map(_.id) should be(ids(9)) + scroll10.summaryResults.map(_.id) should be(ids(10)) + scroll11.summaryResults.map(_.id) should be(List.empty) + scroll12.summaryResults.map(_.id) should be(List.empty) } test("Filtering for statuses should only return drafts with the specified statuses") { @@ -749,7 +735,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo statusFilter = List(DraftStatus.IN_PROGRESS) ) ) - search1.results.map(_.id) should be(Seq(10, 11)) + search1.summaryResults.map(_.id) should be(Seq(10, 11)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -758,7 +744,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo statusFilter = List(DraftStatus.IMPORTED) ) ) - search2.results.map(_.id) should be(Seq()) + search2.summaryResults.map(_.id) should be(Seq()) val Success(search3) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -768,7 +754,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo includeOtherStatuses = true ) ) - search3.results.map(_.id) should be(Seq(12)) + search3.summaryResults.map(_.id) should be(Seq(12)) } test("Filtering for statuses should also filter learningPaths") { @@ -778,7 +764,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(search1) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(language = AllLanguages, statusFilter = List(DraftStatus.IN_PROGRESS)) ) - search1.results.map(_.id) should be(expectedIds) + search1.summaryResults.map(_.id) should be(expectedIds) } @@ -793,10 +779,10 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search1.totalCount should be(1) - search1.results.head.id should be(5) + search1.summaryResults.head.id should be(5) search2.totalCount should be(1) - search2.results.head.id should be(5) + search2.summaryResults.head.id should be(5) } test("That filtering on grepCodes returns articles which has grepCodes") { @@ -807,9 +793,9 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(search3) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(grepCodes = List("K123", "K456"))) - search1.results.map(_.id) should be(Seq(1, 2, 3)) - search2.results.map(_.id) should be(Seq(1, 2, 5)) - search3.results.map(_.id) should be(Seq(1, 2, 3, 5)) + search1.summaryResults.map(_.id) should be(Seq(1, 2, 3)) + search2.summaryResults.map(_.id) should be(Seq(1, 2, 5)) + search3.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5)) } test("ARCHIVED drafts should only be returned if filtered by ARCHIVED") { @@ -827,8 +813,8 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) ) - search1.results.map(_.id) should be(Seq(14)) - search2.results.map(_.id) should be(Seq.empty) + search1.summaryResults.map(_.id) should be(Seq(14)) + search2.summaryResults.map(_.id) should be(Seq.empty) } test("that search with query returns suggestion for query") { @@ -856,7 +842,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search1.totalCount should be(1) - search1.results.map(_.id) should be(Seq(13)) + search1.summaryResults.map(_.id) should be(Seq(13)) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -867,7 +853,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search2.totalCount should be(1) - search2.results.map(_.id) should be(Seq(13)) + search2.summaryResults.map(_.id) should be(Seq(13)) } test("That compound words are matched when searched wrongly if disabled") { @@ -881,7 +867,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search1.totalCount should be(0) - search1.results.map(_.id) should be(Seq.empty) + search1.summaryResults.map(_.id) should be(Seq.empty) val Success(search2) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy( @@ -892,7 +878,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search2.totalCount should be(0) - search2.results.map(_.id) should be(Seq.empty) + search2.summaryResults.map(_.id) should be(Seq.empty) } test("Search query should not be decompounded (only indexed documents)") { @@ -907,7 +893,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo val Success(search) = multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("Flubber").get), language = AllLanguages) ) - search.results.map(_.id) should be(Seq(12)) + search.summaryResults.map(_.id) should be(Seq(12)) } test("That searches for embedResource does not partial match") { @@ -921,7 +907,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That searches for data-resource_id matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(embedId = Some("222"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -935,7 +921,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo embedId = Some("55") ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -956,7 +942,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on embed data-resource matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(embedResource = List("video"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -964,7 +950,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on embed data-url matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(embedId = Some("http://test.test"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -974,7 +960,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("77").get)) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -984,7 +970,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("video").get)) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -994,7 +980,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("11").get)) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(11) } @@ -1002,7 +988,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on embed data-content-id matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(embedId = Some("111"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -1010,7 +996,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on embed id with language filter does only return correct language") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(language = "en", embedId = Some("222"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(13) } @@ -1018,7 +1004,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on embed id with language filter=all matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(language = "*", embedId = Some("222"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(2) hits.map(_.id) should be(Seq(12, 13)) } @@ -1026,7 +1012,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on visual element id matches") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(embedId = Some("333"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(12)) } @@ -1034,7 +1020,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo test("That search on meta image url matches ") { val Success(results) = multiDraftSearchService.matchingQuery(multiDraftSearchSettings.copy(language = "*", embedId = Some("123"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(10)) } @@ -1044,7 +1030,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("\"delt-streng\"").get), language = "*") ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(15)) } @@ -1054,7 +1040,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchService.matchingQuery( multiDraftSearchSettings.copy(query = Some(NonEmptyString.fromString("\"delt\\-streng\"").get), language = "*") ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(15)) } @@ -1067,7 +1053,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(13)) } @@ -1091,7 +1077,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(13)) } @@ -1115,7 +1101,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(13)) } @@ -1128,7 +1114,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(12)) } @@ -1139,7 +1125,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo multiDraftSearchSettings .copy(language = AllLanguages, embedResource = List("concept"), embedId = Some("222")) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -1155,9 +1141,9 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo ) search1.totalCount should be(1) - search1.results.head.id should be(12) + search1.summaryResults.head.id should be(12) search2.totalCount should be(1) - search2.results.head.id should be(12) + search2.summaryResults.head.id should be(12) } @@ -1170,7 +1156,7 @@ class MultiDraftSearchServiceTest extends IntegrationSuite(EnableElasticsearchCo withIdIn = List(3) ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.lastUpdated should be(a[NDLADate]) hits.head.license should be(Some(License.PublicDomain.toString)) diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceAtomicTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceAtomicTest.scala index 48a69ce796..8d2309d6ae 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceAtomicTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceAtomicTest.scala @@ -9,14 +9,20 @@ package no.ndla.searchapi.service.search import no.ndla.common.configuration.Constants.EmbedTagName -import no.ndla.common.model.domain.ArticleContent +import no.ndla.common.model.api.search.{MultiSearchSummaryDTO, NodeHitDTO, SearchType} +import no.ndla.common.model.domain.frontpage.{BannerImage, MetaDescription, SubjectPage} +import no.ndla.common.model.domain.{ArticleContent, Title} +import no.ndla.network.tapir.NonEmptyString import no.ndla.scalatestsuite.IntegrationSuite import no.ndla.search.model.domain.{Bucket, TermAggregation} import no.ndla.search.model.{LanguageValue, SearchableLanguageList, SearchableLanguageValues} import no.ndla.searchapi.TestData.{core, generateContexts, subjectMaterial} -import no.ndla.searchapi.model.domain.IndexingBundle +import no.ndla.searchapi.model.domain.{IndexingBundle, Sort} import no.ndla.searchapi.model.taxonomy.* import no.ndla.searchapi.{TestData, TestEnvironment} +import no.ndla.searchapi.SearchTestUtility.* +import org.mockito.Mockito.* +import org.mockito.ArgumentMatchers.eq as eqTo import scala.util.Success @@ -32,6 +38,9 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC override val learningPathIndexService: LearningPathIndexService = new LearningPathIndexService { override val indexShards = 1 } + override val nodeIndexService: NodeIndexService = new NodeIndexService { + override val indexShards = 1 + } override val multiSearchService = new MultiSearchService override val converterService = new ConverterService override val searchConverterService = new SearchConverterService @@ -89,7 +98,7 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC ) search1.totalCount should be(1) - search1.results.map(_.id) should be(List(2)) + search1.summaryResults.map(_.id) should be(List(2)) val Success(search2) = multiSearchService.matchingQuery( @@ -97,7 +106,7 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC ) search2.totalCount should be(2) - search2.results.map(_.id) should be(List(1, 2)) + search2.summaryResults.map(_.id) should be(List(1, 2)) } @@ -294,7 +303,7 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC ) .get - result.results.head.contexts.map(_.publicId) should be(Seq("urn:resource:2")) + result.summaryResults.head.contexts.map(_.publicId) should be(Seq("urn:resource:2")) } test("That topic taxonomy contexts with hidden elements are ignored") { @@ -440,7 +449,7 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC ) .get - result.results.head.contexts.map(_.publicId) should be(Seq("urn:topic:3")) + result.summaryResults.head.contexts.map(_.publicId) should be(Seq("urn:topic:3")) } test("That aggregating rootId works as expected") { val article1 = TestData.article1.copy(id = Some(1)) @@ -694,4 +703,120 @@ class MultiSearchServiceAtomicTest extends IntegrationSuite(EnableElasticsearchC ) result.aggregations should be(Seq(expectedAggs)) } + + test("That nodes and articles are searchable in the same searchresult") { + val taxonomyBundle = TaxonomyBundle( + List( + Node( + id = "urn:subject:19284", + name = "Apekatt fag", + contentUri = Some("urn:frontpage:1"), + path = Some("/subject:19284"), + url = Some("/f/sub1/asdf2362"), + metadata = Some(Metadata(List.empty, visible = true, Map.empty)), + translations = List.empty, + nodeType = NodeType.SUBJECT, + contextids = List(), + contexts = List() + ), + Node( + id = "urn:subject:19285", + name = "Snabel fag", + contentUri = Some("urn:frontpage:2"), + path = Some("/subject:19285"), + url = Some("/f/sub1/asdf2362"), + metadata = Some(Metadata(List.empty, visible = true, Map.empty)), + translations = List.empty, + nodeType = NodeType.SUBJECT, + contextids = List(), + contexts = List() + ) + ) ++ + indexingBundle.taxonomyBundle.get.nodes + ) + + doReturn( + Success( + SubjectPage( + id = Some(1), + name = "Apekatt fag", + bannerImage = BannerImage(None, 5), + about = Seq(), + metaDescription = Seq(MetaDescription("Apekatt fag beskrivelse", "nb")), + editorsChoices = List(), + connectedTo = List(), + buildsOn = List(), + leadsTo = List() + ) + ) + ).when(frontpageApiClient).getSubjectPage(eqTo(1L)) + + doReturn( + Success( + SubjectPage( + id = Some(2), + name = "Snabel fag", + bannerImage = BannerImage(None, 5), + about = Seq(), + metaDescription = Seq(MetaDescription("Snabel fag beskrivelse", "nb")), + editorsChoices = List(), + connectedTo = List(), + buildsOn = List(), + leadsTo = List() + ) + ) + ).when(frontpageApiClient).getSubjectPage(eqTo(2L)) + + val article1 = TestData.article1.copy( + id = Some(1), + title = Seq(Title("Apekatt en", "nb")) + ) + val article2 = TestData.article1.copy( + id = Some(2), + title = Seq(Title("Apekatt to", "nb")) + ) + val article3 = TestData.article1.copy( + id = Some(3), + title = Seq(Title("Noe helt annet", "nb")) + ) + val bundle = indexingBundle.copy(taxonomyBundle = Some(taxonomyBundle)) + + nodeIndexService.indexDocuments(None, bundle).get + articleIndexService.indexDocument(article1, bundle).get + articleIndexService.indexDocument(article2, bundle).get + articleIndexService.indexDocument(article3, bundle).get + + blockUntil(() => { + val indexedNodes = nodeIndexService.countDocuments + val indexedArticles = articleIndexService.countDocuments + indexedNodes == 23 && indexedArticles == 3 + }) + + val search1 = + multiSearchService.matchingQuery( + TestData.searchSettings.copy( + sort = Sort.ByRelevanceDesc, + query = NonEmptyString.fromString("Apekatt"), + nodeTypeFilter = List(NodeType.SUBJECT), + resultTypes = Some( + List( + SearchType.Nodes, + SearchType.Articles + ) + ) + ) + ) + + search1.get.totalCount should be(3) + search1.get.results.map { + case x: MultiSearchSummaryDTO => s"Multi:${x.id}" + case x: NodeHitDTO => s"Node:${x.id}" + } should be( + List( + "Node:urn:subject:19284", + "Multi:1", + "Multi:2" + ) + ) + } } diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceTest.scala index 3864860af9..028c8b7cc3 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/MultiSearchServiceTest.scala @@ -21,6 +21,7 @@ import no.ndla.searchapi.TestData.* import no.ndla.searchapi.model.domain.{IndexingBundle, Sort} import no.ndla.searchapi.model.search.SearchPagination import no.ndla.searchapi.{TestData, TestEnvironment, UnitSuite} +import no.ndla.searchapi.SearchTestUtility.* import scala.util.Success @@ -41,6 +42,10 @@ class MultiSearchServiceTest override val learningPathIndexService: LearningPathIndexService = new LearningPathIndexService { override val indexShards = 1 } + override val nodeIndexService: NodeIndexService = new NodeIndexService { + override val indexShards = 1 + } + override val multiSearchService = new MultiSearchService override val converterService = new ConverterService override val searchConverterService = new SearchConverterService @@ -131,7 +136,7 @@ class MultiSearchServiceTest test("That all returns all documents ordered by id ascending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByIdAsc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(idsForLang("nb").size) hits.map(_.id) should be(idsForLang("nb").sorted) } @@ -139,28 +144,28 @@ class MultiSearchServiceTest test("That all returns all documents ordered by id descending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByIdDesc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(idsForLang("nb").size) hits.map(_.id) should be(idsForLang("nb").sorted.reverse) } test("That all returns all documents ordered by title ascending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByTitleAsc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(titlesForLang("nb").size) hits.map(_.title.title) should be(titlesForLang("nb").sorted) } test("That all returns all documents ordered by title descending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByTitleDesc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(titlesForLang("nb").size) hits.map(_.title.title) should be(titlesForLang("nb").sorted.reverse) } test("That all returns all documents ordered by lastUpdated descending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByLastUpdatedDesc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(idsForLang("nb").size) hits.head.id should be(3) hits.last.id should be(5) @@ -168,7 +173,7 @@ class MultiSearchServiceTest test("That all returns all documents ordered by lastUpdated ascending") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(sort = Sort.ByLastUpdatedAsc)) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(idsForLang("nb").size) hits.head.id should be(5) hits(1).id should be(1) @@ -180,8 +185,8 @@ class MultiSearchServiceTest multiSearchService.matchingQuery(searchSettings.copy(page = 1, pageSize = 2, sort = Sort.ByTitleAsc)) val Success(page2) = multiSearchService.matchingQuery(searchSettings.copy(page = 2, pageSize = 2, sort = Sort.ByTitleAsc)) - val hits1 = page1.results - val hits2 = page2.results + val hits1 = page1.summaryResults + val hits2 = page2.summaryResults page1.totalCount should be(idsForLang("nb").size) page1.page.get should be(1) hits1.size should be(2) @@ -199,7 +204,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("bil").get), sort = Sort.ByRelevanceDesc) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(3) hits.map(_.id) should be(Seq(1, 5, 3)) } @@ -209,7 +214,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("bil").get), sort = Sort.ByRelevanceDesc, withIdIn = List(3)) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(3) hits.last.id should be(3) @@ -220,7 +225,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("Pingvinen").get), sort = Sort.ByTitleAsc) ) - val hits = results.results + val hits = results.summaryResults hits.map(_.contexts.head.contextType) should be(Seq("learningpath", "standard")) hits.map(_.id) should be(Seq(1, 2)) } @@ -229,7 +234,7 @@ class MultiSearchServiceTest val Success(results) = multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("and").get), sort = Sort.ByTitleAsc) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(2) hits.head.id should be(3) hits(1).id should be(3) @@ -253,7 +258,7 @@ class MultiSearchServiceTest sort = Sort.ByTitleAsc ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(4) } @@ -263,14 +268,14 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("bilde + bil").get), sort = Sort.ByTitleAsc) ) - val hits1 = search1.results + val hits1 = search1.summaryResults hits1.map(_.id) should equal(Seq(1, 3, 5)) val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("batmen + bil").get), sort = Sort.ByTitleAsc) ) - val hits2 = search2.results + val hits2 = search2.summaryResults hits2.map(_.id) should equal(Seq(1)) } @@ -279,13 +284,13 @@ class MultiSearchServiceTest val Success(search1) = multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("bil + bilde + -flaggermusmann").get), sort = Sort.ByTitleAsc) ) - search1.results.map(_.id) should equal(Seq(3, 5)) + search1.summaryResults.map(_.id) should equal(Seq(3, 5)) val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("bil + -hulken").get), sort = Sort.ByTitleAsc) ) - search2.results.map(_.id) should equal(Seq(1, 3)) + search2.summaryResults.map(_.id) should equal(Seq(1, 3)) } test("search in content should be ranked lower than introduction and title") { @@ -293,7 +298,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("mareritt+ragnarok").get), sort = Sort.ByRelevanceDesc) ) - val hits = search.results + val hits = search.summaryResults hits.map(_.id) should equal(Seq(9, 8)) } @@ -308,7 +313,7 @@ class MultiSearchServiceTest test("Search for all languages should return all articles in correct language") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(language = AllLanguages, pageSize = 100)) - val hits = search.results + val hits = search.summaryResults val exp = titlesForLang("*") @@ -345,7 +350,7 @@ class MultiSearchServiceTest sort = Sort.ByTitleAsc ) ) - val hits = search.results + val hits = search.summaryResults search.totalCount should equal(1) hits.head.id should equal(4) @@ -368,14 +373,14 @@ class MultiSearchServiceTest ) searchEn.totalCount should equal(1) - searchEn.results.head.id should equal(11) - searchEn.results.head.title.title should equal("Cats") - searchEn.results.head.title.language should equal("en") + searchEn.summaryResults.head.id should equal(11) + searchEn.summaryResults.head.title.title should equal("Cats") + searchEn.summaryResults.head.title.language should equal("en") searchNb.totalCount should equal(1) - searchNb.results.head.id should equal(11) - searchNb.results.head.title.title should equal("Katter") - searchNb.results.head.title.language should equal("nb") + searchNb.summaryResults.head.id should equal(11) + searchNb.summaryResults.head.title.title should equal("Katter") + searchNb.summaryResults.head.title.language should equal("nb") } test("Searching for unknown language should return nothing") { @@ -385,21 +390,6 @@ class MultiSearchServiceTest searchEn.totalCount should equal(0) } - test("Searching with query for language not in analyzer should return something") { - val Success(searchEn) = multiSearchService.matchingQuery( - searchSettings.copy( - query = Some(NonEmptyString.fromString("Chhattisgarhi").get), - language = "hne", - sort = Sort.ByRelevanceDesc - ) - ) - - searchEn.totalCount should equal(1) - searchEn.results.head.id should equal(11) - searchEn.results.head.title.title should equal("Chhattisgarhi") - searchEn.results.head.title.language should equal("hne") - } - test("metadescription is searchable") { val Success(search) = multiSearchService.matchingQuery( searchSettings.copy( @@ -410,9 +400,9 @@ class MultiSearchServiceTest ) search.totalCount should equal(1) - search.results.head.id should equal(11) - search.results.head.title.title should equal("Cats") - search.results.head.title.language should equal("en") + search.summaryResults.head.id should equal(11) + search.summaryResults.head.title.title should equal("Cats") + search.summaryResults.head.title.language should equal("en") } test("That searching with fallback parameter returns article in language priority even if doesnt match on language") { @@ -422,29 +412,29 @@ class MultiSearchServiceTest ) search.totalCount should equal(3) - search.results.head.id should equal(9) - search.results.head.title.language should equal("nb") - search.results(1).id should equal(10) - search.results(1).title.language should equal("en") - search.results(2).id should equal(11) - search.results(2).title.language should equal("en") + search.summaryResults.head.id should equal(9) + search.summaryResults.head.title.language should equal("nb") + search.summaryResults(1).id should equal(10) + search.summaryResults(1).title.language should equal("en") + search.summaryResults(2).id should equal(11) + search.summaryResults(2).title.language should equal("en") } test("That filtering for subjects works as expected") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(language = "*", subjects = List("urn:subject:2"))) search.totalCount should be(7) - search.results.head.contexts.length should be(2) - search.results.head.contexts + search.summaryResults.head.contexts.length should be(2) + search.summaryResults.head.contexts .map(_.rootId) should be(List("urn:subject:1", "urn:subject:2")) // urn:subject:3 is not visible - search.results.map(_.id) should be(Seq(1, 5, 5, 6, 7, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 5, 5, 6, 7, 11, 12)) } test("That filtering for subjects returns all resources with any of listed subjects") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(subjects = List("urn:subject:2", "urn:subject:1"))) search.totalCount should be(14) - search.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 11, 12)) } test("That filtering for invisible subjects returns nothing") { @@ -457,17 +447,17 @@ class MultiSearchServiceTest val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(resourceTypes = List("urn:resourcetype:academicArticle"))) search.totalCount should be(2) - search.results.map(_.id) should be(Seq(2, 5)) + search.summaryResults.map(_.id) should be(Seq(2, 5)) val Success(search2) = multiSearchService.matchingQuery(searchSettings.copy(resourceTypes = List("urn:resourcetype:subjectMaterial"))) search2.totalCount should be(7) - search2.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) + search2.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) val Success(search3) = multiSearchService.matchingQuery(searchSettings.copy(resourceTypes = List("urn:resourcetype:learningpath"))) search3.totalCount should be(4) - search3.results.map(_.id) should be(Seq(1, 2, 3, 4)) + search3.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4)) } test("That filtering for multiple resource-types returns resources from both") { @@ -475,7 +465,7 @@ class MultiSearchServiceTest searchSettings.copy(resourceTypes = List("urn:resourcetype:subjectMaterial", "urn:resourcetype:reviewResource")) ) search.totalCount should be(7) - search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) } test("That filtering on learning-resource-type works") { @@ -487,10 +477,10 @@ class MultiSearchServiceTest ) search.totalCount should be(7) - search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) search2.totalCount should be(4) - search2.results.map(_.id) should be(Seq(8, 9, 10, 11)) + search2.summaryResults.map(_.id) should be(Seq(8, 9, 10, 11)) } test("That filtering on article-type works") { @@ -505,13 +495,13 @@ class MultiSearchServiceTest ) search.totalCount should be(7) - search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 12)) search2.totalCount should be(4) - search2.results.map(_.id) should be(Seq(8, 9, 10, 11)) + search2.summaryResults.map(_.id) should be(Seq(8, 9, 10, 11)) search3.totalCount should be(1) - search3.results.map(_.id) should be(Seq(14)) + search3.summaryResults.map(_.id) should be(Seq(14)) } test("That filtering on multiple context-types returns every type") { @@ -524,7 +514,7 @@ class MultiSearchServiceTest ) search.totalCount should be(11) - search.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) } test("That filtering on learningpath learningresourcetype returns learningpaths") { @@ -533,8 +523,8 @@ class MultiSearchServiceTest ) search.totalCount should be(6) - search.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6)) - search.results.filter(_.contexts.nonEmpty).map(_.contexts.head.contextType) should be( + search.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 6)) + search.summaryResults.filter(_.contexts.nonEmpty).map(_.contexts.head.contextType) should be( // only 5 learningpaths with contexts Seq.fill(5) { LearningResourceType.LearningPath.toString } ) @@ -549,8 +539,8 @@ class MultiSearchServiceTest ) val totalCount = search.totalCount - val ids = search.results.map(_.id).length - val contextCount = search.results.flatMap(_.contexts).toList.length + val ids = search.summaryResults.map(_.id).length + val contextCount = search.summaryResults.flatMap(_.contexts).toList.length val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy( @@ -560,25 +550,25 @@ class MultiSearchServiceTest ) totalCount should be > search2.totalCount - ids should be > search2.results.map(_.id).length - contextCount should be > search2.results.flatMap(_.contexts).toList.length + ids should be > search2.summaryResults.map(_.id).length + contextCount should be > search2.summaryResults.flatMap(_.contexts).toList.length } test("That filtering on supportedLanguages works") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(language = "*", supportedLanguages = List("en"))) search.totalCount should be(8) - search.results.map(_.id) should be(Seq(2, 3, 4, 5, 6, 10, 11, 12)) + search.summaryResults.map(_.id) should be(Seq(2, 3, 4, 5, 6, 10, 11, 12)) val Success(search2) = multiSearchService.matchingQuery(searchSettings.copy(language = "*", supportedLanguages = List("en", "nb"))) search2.totalCount should be(18) - search2.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12, 14)) + search2.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 6, 7, 8, 9, 10, 11, 12, 14)) val Success(search3) = multiSearchService.matchingQuery(searchSettings.copy(language = "*", supportedLanguages = List("nb"))) search3.totalCount should be(15) - search3.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14)) + search3.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14)) } test("That filtering on supportedLanguages should still prioritize the selected language") { @@ -586,16 +576,16 @@ class MultiSearchServiceTest multiSearchService.matchingQuery(searchSettings.copy(language = "nb", supportedLanguages = List("en"))) search.totalCount should be(5) - search.results.map(_.id) should be(Seq(2, 3, 4, 11, 12)) - search.results.map(_.title.language) should be(Seq("nb", "nb", "nb", "nb", "nb")) + search.summaryResults.map(_.id) should be(Seq(2, 3, 4, 11, 12)) + search.summaryResults.map(_.title.language) should be(Seq("nb", "nb", "nb", "nb", "nb")) } test("That meta image are returned when searching") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(language = "en", withIdIn = List(10))) search.totalCount should be(1) - search.results.head.id should be(10) - search.results.head.metaImage should be( + search.summaryResults.head.id should be(10) + search.summaryResults.head.metaImage should be( Some(MetaImageDTO("http://api-gateway.ndla-local/image-api/raw/id/442", "alt", "en")) ) } @@ -606,26 +596,26 @@ class MultiSearchServiceTest searchSettings.copy(Some(NonEmptyString.fromString("Kjekspolitiet").get), language = AllLanguages) ) search1.totalCount should be(1) - search1.results.map(_.id) should be(Seq(1)) + search1.summaryResults.map(_.id) should be(Seq(1)) val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy(Some(NonEmptyString.fromString("Svims").get), language = AllLanguages) ) search2.totalCount should be(2) - search2.results.map(_.id) should be(Seq(2, 5)) + search2.summaryResults.map(_.id) should be(Seq(2, 5)) } test("That filtering by relevance id makes sense (with and without subject/filter)") { val Success(search1) = multiSearchService.matchingQuery( searchSettings.copy(language = AllLanguages, relevanceIds = List("urn:relevance:core")) ) - search1.results.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) + search1.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12)) val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy(language = AllLanguages, relevanceIds = List("urn:relevance:supplementary")) ) - search2.results.map(_.id) should be(Seq(1, 2, 3, 4, 5, 12)) + search2.summaryResults.map(_.id) should be(Seq(1, 2, 3, 4, 5, 12)) val Success(search3) = multiSearchService.matchingQuery( searchSettings.copy( @@ -633,7 +623,7 @@ class MultiSearchServiceTest relevanceIds = List("urn:relevance:supplementary", "urn:relevance:core") ) ) - search3.results.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12)) + search3.summaryResults.map(_.id) should be(Seq(1, 1, 2, 2, 3, 3, 4, 5, 5, 6, 7, 8, 9, 10, 11, 12)) } test("That filtering by relevance and subject only returns for relevances in filtered subjects") { @@ -642,7 +632,7 @@ class MultiSearchServiceTest .copy(language = AllLanguages, subjects = List("urn:subject:2"), relevanceIds = List("urn:relevance:core")) ) - search1.results.map(_.id) should be(Seq(1, 5, 6, 7, 11)) + search1.summaryResults.map(_.id) should be(Seq(1, 5, 6, 7, 11)) } test("That scrolling works as expected") { @@ -664,16 +654,16 @@ class MultiSearchServiceTest val Success(scroll8) = multiSearchService.scroll(scroll7.scrollId.get, "*") val Success(scroll9) = multiSearchService.scroll(scroll8.scrollId.get, "*") - initialSearch.results.map(_.id) should be(ids.head) - scroll1.results.map(_.id) should be(ids(1)) - scroll2.results.map(_.id) should be(ids(2)) - scroll3.results.map(_.id) should be(ids(3)) - scroll4.results.map(_.id) should be(ids(4)) - scroll5.results.map(_.id) should be(ids(5)) - scroll6.results.map(_.id) should be(ids(6)) - scroll7.results.map(_.id) should be(ids(7)) - scroll8.results.map(_.id) should be(ids(8)) - scroll9.results.map(_.id) should be(List.empty) + initialSearch.summaryResults.map(_.id) should be(ids.head) + scroll1.summaryResults.map(_.id) should be(ids(1)) + scroll2.summaryResults.map(_.id) should be(ids(2)) + scroll3.summaryResults.map(_.id) should be(ids(3)) + scroll4.summaryResults.map(_.id) should be(ids(4)) + scroll5.summaryResults.map(_.id) should be(ids(5)) + scroll6.summaryResults.map(_.id) should be(ids(6)) + scroll7.summaryResults.map(_.id) should be(ids(7)) + scroll8.summaryResults.map(_.id) should be(ids(8)) + scroll9.summaryResults.map(_.id) should be(List.empty) } test("That filtering on context-types works") { @@ -683,7 +673,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery(searchSettings.copy(resourceTypes = List("urn:resourcetype:movieAndClip"))) search.totalCount should be(2) - search.results.map(_.id) should be(Seq(2, 5)) + search.summaryResults.map(_.id) should be(Seq(2, 5)) search2.totalCount should be(0) } @@ -694,9 +684,9 @@ class MultiSearchServiceTest val Success(search3) = multiSearchService.matchingQuery(searchSettings.copy(grepCodes = List("KM123", "KE34", "TT2"))) - search1.results.map(_.id) should be(Seq(1, 2, 3)) - search2.results.map(_.id) should be(Seq(1, 5)) - search3.results.map(_.id) should be(Seq(1, 2, 3, 5)) + search1.summaryResults.map(_.id) should be(Seq(1, 2, 3)) + search2.summaryResults.map(_.id) should be(Seq(1, 5)) + search3.summaryResults.map(_.id) should be(Seq(1, 2, 3, 5)) } test("That search for grep text returns articles which has grep texts fetched from grepCodes") { @@ -704,31 +694,31 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("\"utforsking og problemløysing\"").get)) ) - search1.results.map(_.id) should be(Seq(1, 5)) + search1.summaryResults.map(_.id) should be(Seq(1, 5)) } test("That search result has traits if content has embeds") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(query = Some(NonEmptyString.fromString("Ekstrastoff").get))) search.totalCount should be(1) - search.results.head.id should be(12) - search.results.head.traits should be(List(SearchTrait.H5p)) + search.summaryResults.head.id should be(12) + search.summaryResults.head.traits should be(List(SearchTrait.H5p)) } test("That search can be filtered by traits") { val Success(search) = multiSearchService.matchingQuery(searchSettings.copy(traits = List(SearchTrait.H5p))) search.totalCount should be(1) - search.results.head.id should be(12) - search.results.head.traits should be(List(SearchTrait.H5p)) + search.summaryResults.head.id should be(12) + search.summaryResults.head.traits should be(List(SearchTrait.H5p)) } test("That searches for embed attributes matches") { - val Success(search) = + val search = multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("Flubber").get), language = "nb") ) - search.results.map(_.id) should be(Seq(12)) + search.get.summaryResults.map(_.id) should be(Seq(12)) } test("That compound words are matched when searched wrongly") { @@ -738,7 +728,7 @@ class MultiSearchServiceTest ) search1.totalCount should be(1) - search1.results.map(_.id) should be(Seq(12)) + search1.summaryResults.map(_.id) should be(Seq(12)) val Success(search2) = multiSearchService.matchingQuery( @@ -746,14 +736,14 @@ class MultiSearchServiceTest ) search2.totalCount should be(1) - search2.results.map(_.id) should be(Seq(12)) + search2.summaryResults.map(_.id) should be(Seq(12)) } test("That filterByNoResourceType works by filtering out every document that does not have resourceTypes") { val Success(search1) = multiSearchService.matchingQuery( searchSettings.copy(language = AllLanguages, sort = Sort.ByIdAsc, filterByNoResourceType = true) ) - search1.results.map(_.id).sorted should be(Seq(6, 8, 9, 10, 11, 14)) + search1.summaryResults.map(_.id).sorted should be(Seq(6, 8, 9, 10, 11, 14)) } test("Search query should not be decompounded (only indexed documents)") { @@ -774,7 +764,7 @@ class MultiSearchServiceTest test("That searches for data-resource_id matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(embedId = Some("66"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -788,7 +778,7 @@ class MultiSearchServiceTest embedId = Some("77") ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -809,7 +799,7 @@ class MultiSearchServiceTest test("That search on embed data-resource matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(embedResource = List("video"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -817,7 +807,7 @@ class MultiSearchServiceTest test("That search on embed data-content-id matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(embedId = Some("111"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -825,7 +815,7 @@ class MultiSearchServiceTest test("That search on embed data-url matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(embedId = Some("http://test"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -833,7 +823,7 @@ class MultiSearchServiceTest test("That search on query as embed data-resource_id matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(query = Some(NonEmptyString.fromString("77").get))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -841,7 +831,7 @@ class MultiSearchServiceTest test("That search on query as embed data-resouce matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(query = Some(NonEmptyString.fromString("video").get))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -849,7 +839,7 @@ class MultiSearchServiceTest test("That search on query as article id matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(query = Some(NonEmptyString.fromString("11").get))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(11) } @@ -857,7 +847,7 @@ class MultiSearchServiceTest test("That search on query as deleted context id matches") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(query = Some(NonEmptyString.fromString("asdf1255").get))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -865,7 +855,7 @@ class MultiSearchServiceTest test("That search on embed id with language filter does only return correct language") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(language = "en", embedId = Some("222"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(12) } @@ -873,7 +863,7 @@ class MultiSearchServiceTest test("That search on embed id with language filter=all matches ") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(language = AllLanguages, embedId = Some("222"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(2) hits.map(_.id) should be(Seq(11, 12)) } @@ -881,7 +871,7 @@ class MultiSearchServiceTest test("That search on visual element id matches ") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(embedId = Some("333"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(12)) } @@ -889,7 +879,7 @@ class MultiSearchServiceTest test("That search on meta image url matches ") { val Success(results) = multiSearchService.matchingQuery(searchSettings.copy(language = "*", embedId = Some("442"))) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(10)) } @@ -899,7 +889,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("\"delt-streng\"").get), language = "*") ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(12)) } @@ -909,7 +899,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("\"delt\\-streng\"").get), language = "*") ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(12)) } @@ -922,7 +912,7 @@ class MultiSearchServiceTest language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(11)) } @@ -946,7 +936,7 @@ class MultiSearchServiceTest language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(11)) } @@ -964,7 +954,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(query = Some(NonEmptyString.fromString("\"delt!streng\" + katt").get), language = "*") ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(11)) } @@ -977,7 +967,7 @@ class MultiSearchServiceTest language = "*" ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.map(_.id) should be(Seq(10)) } @@ -987,7 +977,7 @@ class MultiSearchServiceTest multiSearchService.matchingQuery( searchSettings.copy(language = AllLanguages, embedResource = List("concept"), embedId = Some("222")) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.id should be(11) } @@ -1003,9 +993,9 @@ class MultiSearchServiceTest ) search1.totalCount should be(1) - search1.results.head.id should be(12) + search1.summaryResults.head.id should be(12) search2.totalCount should be(1) - search2.results.head.id should be(12) + search2.summaryResults.head.id should be(12) } @@ -1015,7 +1005,7 @@ class MultiSearchServiceTest searchSettings.copy(query = Some(NonEmptyString.fromString("utilgjengelig").get), availability = List.empty) ) search1.totalCount should be(0) - search1.results.map(_.id) should be(Seq.empty) + search1.summaryResults.map(_.id) should be(Seq.empty) val Success(search2) = multiSearchService.matchingQuery( searchSettings.copy( @@ -1024,7 +1014,7 @@ class MultiSearchServiceTest ) ) search2.totalCount should be(0) - search2.results.map(_.id) should be(Seq.empty) + search2.summaryResults.map(_.id) should be(Seq.empty) val Success(search3) = multiSearchService.matchingQuery( searchSettings.copy( @@ -1033,7 +1023,7 @@ class MultiSearchServiceTest ) ) search3.totalCount should be(1) - search3.results.map(_.id) should be(Seq(13)) + search3.summaryResults.map(_.id) should be(Seq(13)) } test("That search result has license and lastUpdated data") { @@ -1045,7 +1035,7 @@ class MultiSearchServiceTest withIdIn = List(3) ) ) - val hits = results.results + val hits = results.summaryResults results.totalCount should be(1) hits.head.lastUpdated should be(a[NDLADate]) hits.head.license should be(Some(License.PublicDomain.toString)) diff --git a/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchConverterServiceTest.scala b/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchConverterServiceTest.scala index 1f26b23e1e..d45c73186b 100644 --- a/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchConverterServiceTest.scala +++ b/search-api/src/test/scala/no/ndla/searchapi/service/search/SearchConverterServiceTest.scala @@ -189,11 +189,11 @@ class SearchConverterServiceTest extends UnitSuite with TestEnvironment { ) ) - searchable2.contexts.head.resourceTypes.map(_.id).sorted should be( + searchable2.contexts.head.resourceTypeIds.sorted should be( Seq("urn:resourcetype:subjectMaterial", "urn:resourcetype:academicArticle").sorted ) - searchable4.contexts.head.resourceTypes.map(_.id).sorted should be(Seq("urn:resourcetype:subjectMaterial").sorted) - searchable7.contexts.head.resourceTypes.map(_.id).sorted should be( + searchable4.contexts.head.resourceTypeIds.sorted should be(Seq("urn:resourcetype:subjectMaterial").sorted) + searchable7.contexts.head.resourceTypeIds.sorted should be( Seq( "urn:resourcetype:nested", "urn:resourcetype:peerEvaluation", @@ -304,13 +304,17 @@ class SearchConverterServiceTest extends UnitSuite with TestEnvironment { ) searchable1.contexts.size should be(2) - searchable1.contexts.map(_.root.languageValues.map(_.value)) should be(Seq(Seq("Matte"), Seq("Historie"))) + searchable1.contexts.map(_.domainObject.root.languageValues.map(_.value)) should be( + Seq(Seq("Matte"), Seq("Historie")) + ) searchable4.contexts.size should be(1) - searchable4.contexts.head.root.languageValues.map(_.value) should be(Seq("Matte")) + searchable4.contexts.head.domainObject.root.languageValues.map(_.value) should be(Seq("Matte")) searchable5.contexts.size should be(2) - searchable5.contexts.map(_.root.languageValues.map(_.value)) should be(Seq(Seq("Historie"), Seq("Matte"))) + searchable5.contexts.map(_.domainObject.root.languageValues.map(_.value)) should be( + Seq(Seq("Historie"), Seq("Matte")) + ) } test("That invisible contexts are not indexed") { diff --git a/search/src/main/scala/no/ndla/search/BaseIndexService.scala b/search/src/main/scala/no/ndla/search/BaseIndexService.scala index ededeb98c7..8c21117a4e 100644 --- a/search/src/main/scala/no/ndla/search/BaseIndexService.scala +++ b/search/src/main/scala/no/ndla/search/BaseIndexService.scala @@ -18,7 +18,6 @@ import com.sksamuel.elastic4s.requests.mappings.MappingDefinition import com.typesafe.scalalogging.StrictLogging import no.ndla.common.configuration.HasBaseProps import no.ndla.common.implicits.TryQuestionMark -import no.ndla.search.SearchLanguage.NynorskLanguageAnalyzer import no.ndla.search.model.domain.{BulkIndexResult, ElasticIndexingException, ReindexResult} import java.text.SimpleDateFormat @@ -26,9 +25,10 @@ import java.util.Calendar import scala.util.{Failure, Success, Try} trait BaseIndexService { - this: Elastic4sClient with HasBaseProps => + this: Elastic4sClient & HasBaseProps & SearchLanguage => trait BaseIndexService extends StrictLogging { + import SearchLanguage.NynorskLanguageAnalyzer val documentType: String val searchIndex: String val MaxResultWindowOption: Int diff --git a/search/src/main/scala/no/ndla/search/NdlaSearchException.scala b/search/src/main/scala/no/ndla/search/NdlaSearchException.scala index 819e633d3a..d6db6218e9 100644 --- a/search/src/main/scala/no/ndla/search/NdlaSearchException.scala +++ b/search/src/main/scala/no/ndla/search/NdlaSearchException.scala @@ -18,31 +18,14 @@ case class NdlaSearchException[T]( ) extends RuntimeException(message) object NdlaSearchException { - private def message( - errorType: String, - reason: String, - index: Option[String], - shard: Option[String], - requestString: String - ): String = { - val indexError = index.map(idx => s"\nindex: $idx") - val shardError = shard.map(s => s"\nshard: $s") - s"""SearchError with following content occurred: - |Error type: $errorType - |reason: $reason - |$indexError$shardError - |Caused by request: $requestString - |""".stripMargin - } - def apply[T](request: T, rf: RequestFailure): NdlaSearchException[T] = { - val msg = message( - rf.error.`type`, - rf.error.reason, - rf.error.index, - rf.error.shard, - request.toString - ) + val msg = + s"""Got error from elasticsearch: + | Status: ${rf.status} + | Error: ${rf.error} + | Caused by request: $request + |""".stripMargin + new NdlaSearchException(msg, Some(rf)) } diff --git a/search/src/main/scala/no/ndla/search/SearchLanguage.scala b/search/src/main/scala/no/ndla/search/SearchLanguage.scala index 349dee7ef8..fb0e481d25 100644 --- a/search/src/main/scala/no/ndla/search/SearchLanguage.scala +++ b/search/src/main/scala/no/ndla/search/SearchLanguage.scala @@ -9,72 +9,81 @@ package no.ndla.search import com.sksamuel.elastic4s.analysis.{CustomAnalyzer, LanguageAnalyzers, StemmerTokenFilter, StopTokenFilter} -import no.ndla.language.Language._ +import no.ndla.language.Language.* import no.ndla.language.model.LanguageTag import com.sksamuel.elastic4s.analysis.TokenFilter +import no.ndla.common.configuration.HasBaseProps -object SearchLanguage { +trait SearchLanguage { + this: HasBaseProps => - val NynorskTokenFilters: List[TokenFilter] = List( - StopTokenFilter("norwegian_stop", language = Some("norwegian")), - StemmerTokenFilter("nynorsk_stemmer", lang = "light_nynorsk") - ) + object SearchLanguage { - // Must be included in search index settings - val NynorskLanguageAnalyzer: CustomAnalyzer = CustomAnalyzer( - name = Nynorsk, - tokenizer = "standard", - tokenFilters = List( - "lowercase", - "norwegian_stop", - "nynorsk_stemmer" + val NynorskTokenFilters: List[TokenFilter] = List( + StopTokenFilter("norwegian_stop", language = Some("norwegian")), + StemmerTokenFilter("nynorsk_stemmer", lang = "light_nynorsk") ) - ) - val standardAnalyzer = "standard" + // Must be included in search index settings + val NynorskLanguageAnalyzer: CustomAnalyzer = CustomAnalyzer( + name = Nynorsk, + tokenizer = "standard", + tokenFilters = List( + "lowercase", + "norwegian_stop", + "nynorsk_stemmer" + ) + ) - val languageAnalyzers: Seq[LanguageAnalyzer] = Seq( - LanguageAnalyzer(LanguageTag("nb"), LanguageAnalyzers.norwegian), - LanguageAnalyzer(LanguageTag("nn"), NynorskLanguageAnalyzer.name), - LanguageAnalyzer(LanguageTag("sma"), standardAnalyzer), // Southern sami - LanguageAnalyzer(LanguageTag("se"), standardAnalyzer), // Northern Sami - LanguageAnalyzer(LanguageTag("en"), LanguageAnalyzers.english), - LanguageAnalyzer(LanguageTag("ar"), LanguageAnalyzers.arabic), - LanguageAnalyzer(LanguageTag("hy"), LanguageAnalyzers.armenian), - LanguageAnalyzer(LanguageTag("eu"), LanguageAnalyzers.basque), - LanguageAnalyzer(LanguageTag("pt-br"), LanguageAnalyzers.brazilian), - LanguageAnalyzer(LanguageTag("bg"), LanguageAnalyzers.bulgarian), - LanguageAnalyzer(LanguageTag("ca"), LanguageAnalyzers.catalan), - LanguageAnalyzer(LanguageTag("ja"), LanguageAnalyzers.cjk), - LanguageAnalyzer(LanguageTag("ko"), LanguageAnalyzers.cjk), - LanguageAnalyzer(LanguageTag("zh"), LanguageAnalyzers.cjk), - LanguageAnalyzer(LanguageTag("cs"), LanguageAnalyzers.czech), - LanguageAnalyzer(LanguageTag("da"), LanguageAnalyzers.danish), - LanguageAnalyzer(LanguageTag("nl"), LanguageAnalyzers.dutch), - LanguageAnalyzer(LanguageTag("fi"), LanguageAnalyzers.finnish), - LanguageAnalyzer(LanguageTag("fr"), LanguageAnalyzers.french), - LanguageAnalyzer(LanguageTag("gl"), LanguageAnalyzers.galician), - LanguageAnalyzer(LanguageTag("de"), LanguageAnalyzers.german), - LanguageAnalyzer(LanguageTag("el"), LanguageAnalyzers.greek), - LanguageAnalyzer(LanguageTag("hi"), LanguageAnalyzers.hindi), - LanguageAnalyzer(LanguageTag("hu"), LanguageAnalyzers.hungarian), - LanguageAnalyzer(LanguageTag("id"), LanguageAnalyzers.indonesian), - LanguageAnalyzer(LanguageTag("ga"), LanguageAnalyzers.irish), - LanguageAnalyzer(LanguageTag("it"), LanguageAnalyzers.italian), - LanguageAnalyzer(LanguageTag("lt"), LanguageAnalyzers.lithuanian), - LanguageAnalyzer(LanguageTag("lv"), LanguageAnalyzers.latvian), - LanguageAnalyzer(LanguageTag("fa"), LanguageAnalyzers.persian), - LanguageAnalyzer(LanguageTag("pt"), LanguageAnalyzers.portuguese), - LanguageAnalyzer(LanguageTag("ro"), LanguageAnalyzers.romanian), - LanguageAnalyzer(LanguageTag("ru"), LanguageAnalyzers.russian), - LanguageAnalyzer(LanguageTag("srb"), LanguageAnalyzers.sorani), - LanguageAnalyzer(LanguageTag("es"), LanguageAnalyzers.spanish), - LanguageAnalyzer(LanguageTag("sv"), LanguageAnalyzers.swedish), - LanguageAnalyzer(LanguageTag("th"), LanguageAnalyzers.thai), - LanguageAnalyzer(LanguageTag("tr"), LanguageAnalyzers.turkish), - LanguageAnalyzer(UnknownLanguage, standardAnalyzer) - ) + val standardAnalyzer = "standard" + val languageAnalyzers: Seq[LanguageAnalyzer] = Seq( + LanguageAnalyzer(LanguageTag("nb"), LanguageAnalyzers.norwegian), + LanguageAnalyzer(LanguageTag("nn"), NynorskLanguageAnalyzer.name), + LanguageAnalyzer(LanguageTag("sma"), standardAnalyzer), // Southern sami + LanguageAnalyzer(LanguageTag("se"), standardAnalyzer), // Northern Sami + LanguageAnalyzer(LanguageTag("en"), LanguageAnalyzers.english), + LanguageAnalyzer(LanguageTag("ar"), LanguageAnalyzers.arabic), + LanguageAnalyzer(LanguageTag("hy"), LanguageAnalyzers.armenian), + LanguageAnalyzer(LanguageTag("eu"), LanguageAnalyzers.basque), + LanguageAnalyzer(LanguageTag("pt-br"), LanguageAnalyzers.brazilian), + LanguageAnalyzer(LanguageTag("bg"), LanguageAnalyzers.bulgarian), + LanguageAnalyzer(LanguageTag("ca"), LanguageAnalyzers.catalan), + LanguageAnalyzer(LanguageTag("ja"), LanguageAnalyzers.cjk), + LanguageAnalyzer(LanguageTag("ko"), LanguageAnalyzers.cjk), + LanguageAnalyzer(LanguageTag("zh"), LanguageAnalyzers.cjk), + LanguageAnalyzer(LanguageTag("cs"), LanguageAnalyzers.czech), + LanguageAnalyzer(LanguageTag("da"), LanguageAnalyzers.danish), + LanguageAnalyzer(LanguageTag("nl"), LanguageAnalyzers.dutch), + LanguageAnalyzer(LanguageTag("fi"), LanguageAnalyzers.finnish), + LanguageAnalyzer(LanguageTag("fr"), LanguageAnalyzers.french), + LanguageAnalyzer(LanguageTag("gl"), LanguageAnalyzers.galician), + LanguageAnalyzer(LanguageTag("de"), LanguageAnalyzers.german), + LanguageAnalyzer(LanguageTag("el"), LanguageAnalyzers.greek), + LanguageAnalyzer(LanguageTag("hi"), LanguageAnalyzers.hindi), + LanguageAnalyzer(LanguageTag("hu"), LanguageAnalyzers.hungarian), + LanguageAnalyzer(LanguageTag("id"), LanguageAnalyzers.indonesian), + LanguageAnalyzer(LanguageTag("ga"), LanguageAnalyzers.irish), + LanguageAnalyzer(LanguageTag("it"), LanguageAnalyzers.italian), + LanguageAnalyzer(LanguageTag("lt"), LanguageAnalyzers.lithuanian), + LanguageAnalyzer(LanguageTag("lv"), LanguageAnalyzers.latvian), + LanguageAnalyzer(LanguageTag("fa"), LanguageAnalyzers.persian), + LanguageAnalyzer(LanguageTag("pt"), LanguageAnalyzers.portuguese), + LanguageAnalyzer(LanguageTag("ro"), LanguageAnalyzers.romanian), + LanguageAnalyzer(LanguageTag("ru"), LanguageAnalyzers.russian), + LanguageAnalyzer(LanguageTag("srb"), LanguageAnalyzers.sorani), + LanguageAnalyzer(LanguageTag("es"), LanguageAnalyzers.spanish), + LanguageAnalyzer(LanguageTag("sv"), LanguageAnalyzers.swedish), + LanguageAnalyzer(LanguageTag("th"), LanguageAnalyzers.thai), + LanguageAnalyzer(LanguageTag("tr"), LanguageAnalyzers.turkish), + LanguageAnalyzer(LanguageTag("ukr"), standardAnalyzer), + LanguageAnalyzer(UnknownLanguage, standardAnalyzer) + ).filter { analyzer => + // NOTE: Since we use the indexers to determine which fields should be mapped + // we need to limit them to not exceed the elasticsearch limit of fields in a index. + props.SupportedLanguages.contains(analyzer.languageTag.toString) + } + } } case class LanguageAnalyzer(languageTag: LanguageTag, analyzer: String) diff --git a/search/src/main/scala/no/ndla/search/TestUtility.scala b/search/src/main/scala/no/ndla/search/TestUtility.scala index aecb7d47e2..033bf0e00f 100644 --- a/search/src/main/scala/no/ndla/search/TestUtility.scala +++ b/search/src/main/scala/no/ndla/search/TestUtility.scala @@ -12,15 +12,15 @@ import com.sksamuel.elastic4s.fields.{ElasticField, NestedField, ObjectField} import io.circe.Json object TestUtility { - private def getArrayFields(json: Vector[Json], prefix: String): Seq[String] = { + private def getArrayFields(json: Vector[Json], prefix: String, skipFields: Seq[String]): Seq[String] = { val firstElement = json.headOption.getOrElse( throw new RuntimeException(s"Array '$prefix' seems to be empty, this makes checking subfields hard") ) firstElement.arrayOrObject( or = { Seq.empty }, - jsonObject = { obj => getFields(obj.toJson, Some(prefix)) }, - jsonArray = { arr => getArrayFields(arr, s"$prefix[0]") } + jsonObject = { obj => getFields(obj.toJson, Some(prefix), skipFields) }, + jsonArray = { arr => getArrayFields(arr, s"$prefix[0]", skipFields) } ) } @@ -28,31 +28,31 @@ object TestUtility { val pre = prefix.map(x => s"$x.").getOrElse("") json.asArray match { - case Some(value) => return getArrayFields(value, s"$pre") + case Some(value) => return getArrayFields(value, s"$pre", skipFields) case _ => } json.arrayOrObject( or = { List.empty }, - jsonArray = { arr => getArrayFields(arr, s"$pre") }, + jsonArray = { arr => getArrayFields(arr, s"$pre", skipFields) }, jsonObject = { obj => obj.toMap.foldLeft(List.empty[String]) { case (acc, (name, value)) if value.isObject => - if (skipFields.contains(name)) acc + if (skipFields.contains(name) || skipFields.contains(s"$pre$name")) acc else { val fix = s"$pre$name" val subfields = getFields(value, Some(fix), skipFields) acc ++ subfields } case (acc, (name, value)) if value.isArray => - if (skipFields.contains(name)) acc + if (skipFields.contains(name) || skipFields.contains(s"$pre$name")) acc else { val fix = s"$pre$name" - val subfields = getArrayFields(value.asArray.getOrElse(Vector.empty), fix) + val subfields = getArrayFields(value.asArray.getOrElse(Vector.empty), fix, skipFields) acc ++ subfields } case (acc, (name, _)) => - if (skipFields.contains(name)) acc + if (skipFields.contains(name) || skipFields.contains(s"$pre$name")) acc else { val fix = s"$pre$name" acc :+ fix diff --git a/typescript/types-backend/search-api.ts b/typescript/types-backend/search-api.ts index 7deee87f02..2531638d0c 100644 --- a/typescript/types-backend/search-api.ts +++ b/typescript/types-backend/search-api.ts @@ -199,7 +199,7 @@ export interface IGroupSearchResultDTO { page?: number pageSize: number language: string - results: IMultiSearchSummaryDTO[] + results: MultiSummaryBaseDTO[] suggestions: IMultiSearchSuggestionDTO[] aggregations: IMultiSearchTermsAggregationDTO[] resourceType: string @@ -270,7 +270,7 @@ export interface IMultiSearchResultDTO { page?: number pageSize: number language: string - results: IMultiSearchSummaryDTO[] + results: MultiSummaryBaseDTO[] suggestions: IMultiSearchSuggestionDTO[] aggregations: IMultiSearchTermsAggregationDTO[] } @@ -308,6 +308,7 @@ export interface IMultiSearchSummaryDTO { published?: string favorited?: number resultType: SearchType + typename: "MultiSearchSummaryDTO" } export interface IMultiSearchTermsAggregationDTO { @@ -317,6 +318,12 @@ export interface IMultiSearchTermsAggregationDTO { values: ITermValueDTO[] } +export interface INodeHitDTO { + id: string + subjectPage?: ISubjectPageSummaryDTO + typename: "NodeHitDTO" +} + export interface IRevisionMetaDTO { revisionDate: string note: string @@ -344,6 +351,7 @@ export interface ISearchParamsDTO { embedResource?: string[] embedId?: string filterInactive?: boolean + resultTypes?: SearchType[] sort?: string } @@ -376,6 +384,12 @@ export interface ISubjectAggsInputDTO { subjects?: string[] } +export interface ISubjectPageSummaryDTO { + id: number + name: string + metaDescription: IMetaDescriptionDTO +} + export interface ISuggestOptionDTO { text: string score: number @@ -405,9 +419,11 @@ export interface ITitleWithHtmlDTO { export type LearningResourceType = ("standard" | "topic-article" | "frontpage-article" | "learningpath" | "concept" | "gloss") +export type MultiSummaryBaseDTO = (IMultiSearchSummaryDTO | INodeHitDTO) + export type SearchTrait = ("VIDEO" | "H5P" | "AUDIO" | "PODCAST") -export type SearchType = ("article" | "draft" | "learningpath" | "concept" | "grep") +export type SearchType = ("article" | "draft" | "learningpath" | "concept" | "grep" | "node") export type Sort = SortEnum