diff --git a/modules/api/src/main/smithy/_global.smithy b/modules/api/src/main/smithy/_global.smithy index 53abdf71..b75ae67b 100644 --- a/modules/api/src/main/smithy/_global.smithy +++ b/modules/api/src/main/smithy/_global.smithy @@ -20,6 +20,10 @@ list Ids { member: String } +list Strings { + member: String +} + structure CountResponse { @required count: Integer diff --git a/modules/api/src/main/smithy/search.smithy b/modules/api/src/main/smithy/search.smithy index f40c28cf..c14cf9f9 100644 --- a/modules/api/src/main/smithy/search.smithy +++ b/modules/api/src/main/smithy/search.smithy @@ -5,11 +5,12 @@ namespace lila.search.spec use alloy#simpleRestJson use smithy4s.meta#adt use smithy.api#default +use smithy.api#jsonName @simpleRestJson service SearchService { version: "3.0.0" - operations: [Search, Count, DeleteById, DeleteByIds, Mapping, Refresh] + operations: [Search, Count, DeleteById, DeleteByIds, Mapping, Refresh, Store] } @readonly @@ -52,6 +53,12 @@ operation Refresh { errors: [InternalServerError] } +@http(method: "POST", uri: "/store/{id}", code: 200) +operation Store { + input: StoreInput + errors: [InternalServerError] +} + structure SearchInput { @required @@ -102,6 +109,16 @@ structure RefreshInput { index: Index } +structure StoreInput { + + @required + source: Source + + @httpLabel + @required + id: String +} + structure Forum { @required text: String @@ -180,6 +197,117 @@ union Query { team: Team } +structure ForumSource { + @required + @jsonName("bo") + body: String + @required + @jsonName("to") + topic: String + @required + @jsonName("ti") + topicId: String + @jsonName("au") + author: String + @required + @jsonName("tr") + troll: Boolean + @required + @jsonName("da") + date: Timestamp +} + +structure GameSource { + @required + @jsonName("s") + status: String + @required + @jsonName("t") + turns: Integer + @required + @jsonName("r") + rated: Boolean + @required + @jsonName("p") + perf: String + @jsonName("u") + uids: Ids + @jsonName("w") + winner: String + @jsonName("o") + loser: String + @required + @jsonName("c") + winnerColor: Integer + @required + @jsonName("a") + averageRating: Integer + @required + @jsonName("i") + hasAi: Boolean + @required + @jsonName("d") + date: Timestamp // or string? + @required + @jsonName("l") + duration: Integer + @required + @jsonName("ct") + clockInit: Integer + @jsonName("ci") + clockInc: Integer + @jsonName("n") + analysed: Boolean + @required + @jsonName("wu") + whiteUser: String + @required + @jsonName("bu") + blackUser: String + @jsonName("so") + source: Integer +} + +structure StudySource { + @required + name: String + @required + owner: String + @required + members: Ids + @required + chapterNames: String + + @required + chapterTexts: String + @default + topics: Strings + @required + likes: Integer + @required + public: Boolean +} + +structure TeamSource { + @required + @jsonName("na") + name: String + @required + @jsonName("de") + description: String + @required + @jsonName("nbm") + nbMembers: Integer +} + +@adt +union Source { + forum: ForumSource + game: GameSource + study: StudySource + team: TeamSource +} + enum Index { Forum = "forum" Game = "game" diff --git a/modules/app/src/main/scala/service.search.scala b/modules/app/src/main/scala/service.search.scala index a1c1ea1a..f8ccf65d 100644 --- a/modules/app/src/main/scala/service.search.scala +++ b/modules/app/src/main/scala/service.search.scala @@ -9,11 +9,20 @@ import forum.ForumQuery.* import io.github.arainko.ducktape.* import org.joda.time.DateTime import smithy4s.Timestamp +import com.sksamuel.elastic4s.Indexable +import smithy4s.schema.Schema class SearchServiceImpl(esClient: ESClient[IO])(using Logger[IO]) extends SearchService[IO]: import SearchServiceImpl.{ given, * } + override def store(source: Source, id: String): IO[Unit] = + esClient + .store(source.index, Id(id), source) + .handleErrorWith: e => + error"Error in store: source=$source, id=$id" *> + IO.raiseError(InternalServerError("Internal server error")) + override def refresh(index: Index): IO[Unit] = esClient .refreshIndex(index.transform) @@ -108,3 +117,26 @@ object SearchServiceImpl: case q: Query.Game => lila.search.Index("game") case q: Query.Study => lila.search.Index("study") case q: Query.Team => lila.search.Index("team") + + // given Indexable[Source] = (s: Source) => writeToString(s) + + import smithy4s.json.Json.given + import com.github.plokhotnyuk.jsoniter_scala.core._ + given Schema[Source.ForumSource] = lila.search.spec.Source.ForumSource.schema + given Schema[Source.GameSource] = lila.search.spec.Source.GameSource.schema + given Schema[Source.StudySource] = lila.search.spec.Source.StudySource.schema + given Schema[Source.TeamSource] = lila.search.spec.Source.TeamSource.schema + + given Indexable[Source] = (s: Source) => + s match + case s: Source.ForumSource => writeToString[Source.ForumSource](s) + case s: Source.GameSource => writeToString[Source.GameSource](s) + case s: Source.StudySource => writeToString[Source.StudySource](s) + case s: Source.TeamSource => writeToString[Source.TeamSource](s) + + extension (source: Source) + def index = source match + case s: Source.ForumSource => lila.search.Index("forum") + case s: Source.GameSource => lila.search.Index("game") + case s: Source.StudySource => lila.search.Index("study") + case s: Source.TeamSource => lila.search.Index("team") diff --git a/modules/client/src/main/scala/PlayClient.scala b/modules/client/src/main/scala/PlayClient.scala index f5dfc3d3..e48ad12a 100644 --- a/modules/client/src/main/scala/PlayClient.scala +++ b/modules/client/src/main/scala/PlayClient.scala @@ -18,6 +18,12 @@ class PlayClient(client: StandaloneWSClient, baseUrl: String)(using ExecutionCon import implicits.given + override def store(source: Source, id: String): Future[Unit] = + client + .url(s"$baseUrl/store/$id") + .post(source) + .map(_ => ()) + override def refresh(index: Index): Future[Unit] = client .url(s"$baseUrl/refresh/${index.name}") diff --git a/modules/e2e/src/test/scala/CompatSuite.scala b/modules/e2e/src/test/scala/CompatSuite.scala index 2958bb51..0b9b9f08 100644 --- a/modules/e2e/src/test/scala/CompatSuite.scala +++ b/modules/e2e/src/test/scala/CompatSuite.scala @@ -13,7 +13,7 @@ import com.comcast.ip4s.* import akka.actor.ActorSystem import play.api.libs.ws.* import play.api.libs.ws.ahc.* -import lila.search.spec.{ Query, Index as SpecIndex } +import lila.search.spec.{ Query, Index as SpecIndex, Source } import scala.concurrent.ExecutionContext.Implicits.* import com.sksamuel.elastic4s.Indexable @@ -50,6 +50,10 @@ object CompatSuite extends weaver.IOSuite: test("refresh endpoint"): client => IO.fromFuture(IO(client.refresh(SpecIndex.Forum))).map(expect.same(_, ())) + test("store endpoint"): client => + val source = Source.teamSource("names", "desc", 100) + IO.fromFuture(IO(client.store(source, "id"))).map(expect.same(_, ())) + def testAppConfig = AppConfig( server = HttpServerConfig(ip"0.0.0.0", port"9999", shutdownTimeout = 1), elastic = ElasticConfig("http://0.0.0.0:9200") diff --git a/play/app/src/main/scala/controllers/WebApi.scala b/play/app/src/main/scala/controllers/WebApi.scala index 9f6fae61..d5ab2572 100644 --- a/play/app/src/main/scala/controllers/WebApi.scala +++ b/play/app/src/main/scala/controllers/WebApi.scala @@ -14,11 +14,6 @@ class WebApi @Inject() (cc: ControllerComponents, client: ESClient[Future])(impl implicit val indexableJsValue: Indexable[JsValue] = (t: JsValue) => Json.stringify(t) implicit val indexableJsObject: Indexable[JsObject] = (t: JsObject) => Json.stringify(t) - def store(index: String, id: String) = - JsObjectBody { obj => - client.store(Index(index), Id(id), obj).inject(Ok(s"inserted $index/$id")) - } - def deleteById(index: String, id: String) = Action.async { client.deleteOne(Index(index), Id(id)).inject(Ok(s"deleted $index/$id")) @@ -66,6 +61,11 @@ class WebApi @Inject() (cc: ControllerComponents, client: ESClient[Future])(impl } } + def store(index: String, id: String) = + JsObjectBody { obj => + client.store(Index(index), Id(id), obj).inject(Ok(s"inserted $index/$id")) + } + def storeBulk(index: String) = JsObjectBody { objs => Chronometer(s"bulk ${objs.fields.size} $index") {