Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(example): Support getAll and search queries #45

Merged
merged 13 commits into from
Jan 27, 2023
2 changes: 1 addition & 1 deletion modules/example/src/main/scala/example/Main.scala
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ object Main extends ZIOAppDefault {

val populate: RIO[SttpBackend[Task, Any] with ElasticExecutor, Unit] =
(for {
repositories <- RepoFetcher.fetchAllByOrganization("zio")
repositories <- RepoFetcher.fetchAllByOrganization(organization)
_ <- ZIO.logInfo("Adding GitHub repositories...")
_ <- RepositoriesElasticsearch.createAll(repositories)
} yield ()).provideSome(RepositoriesElasticsearch.live)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,44 @@
package example

import zio._
import zio.elasticsearch.{DeletionOutcome, DocumentId, ElasticExecutor, ElasticRequest, Routing}
import zio.elasticsearch.ElasticQuery.matchAll
import zio.elasticsearch.{
CreationOutcome,
DeletionOutcome,
DocumentId,
ElasticExecutor,
ElasticQuery,
ElasticRequest,
Routing
}
import zio.prelude.Newtype.unsafeWrap

final case class RepositoriesElasticsearch(executor: ElasticExecutor) {

def findAll(): Task[List[GitHubRepo]] =
executor.execute(ElasticRequest.search[GitHubRepo](Index, matchAll()))

def findById(organization: String, id: String): Task[Option[GitHubRepo]] =
for {
routing <- routingOf(organization)
req = ElasticRequest.getById[GitHubRepo](Index, DocumentId(id)).routing(routing)
res <- executor.execute(req)
} yield res

def create(repository: GitHubRepo): Task[DocumentId] =
def create(repository: GitHubRepo): Task[CreationOutcome] =
for {
routing <- routingOf(repository.organization)
req = ElasticRequest.create(Index, repository).routing(routing).refreshTrue
req = ElasticRequest.create(Index, DocumentId(repository.id), repository).routing(routing).refreshTrue
res <- executor.execute(req)
} yield res

def createAll(repositories: List[GitHubRepo]): Task[Unit] =
for {
reqs <- ZIO.collectAllPar {
repositories.map { repository =>
routingOf(repository.organization).map(
ElasticRequest
.create[GitHubRepo](Index, unsafeWrap(DocumentId)(repository.id), repository)
.routing(_)
)
}
}
bulkReq = ElasticRequest.bulk(reqs: _*)
routing <- routingOf(organization)
reqs = repositories.map { repository =>
ElasticRequest.create[GitHubRepo](Index, unsafeWrap(DocumentId)(repository.id), repository)
}
bulkReq = ElasticRequest.bulk(reqs: _*).routing(routing)
_ <- executor.execute(bulkReq)
} yield ()

Expand All @@ -49,17 +56,23 @@ final case class RepositoriesElasticsearch(executor: ElasticExecutor) {
res <- executor.execute(req)
} yield res

def search(query: ElasticQuery[_]): Task[List[GitHubRepo]] =
executor.execute(ElasticRequest.search[GitHubRepo](Index, query))

private def routingOf(value: String): IO[IllegalArgumentException, Routing.Type] =
Routing.make(value).toZIO.mapError(e => new IllegalArgumentException(e))

}

object RepositoriesElasticsearch {

def findAll(): RIO[RepositoriesElasticsearch, List[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.findAll())

def findById(organization: String, id: String): RIO[RepositoriesElasticsearch, Option[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.findById(organization, id))

def create(repository: GitHubRepo): RIO[RepositoriesElasticsearch, DocumentId] =
def create(repository: GitHubRepo): RIO[RepositoriesElasticsearch, CreationOutcome] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.create(repository))

def createAll(repositories: List[GitHubRepo]): RIO[RepositoriesElasticsearch, Unit] =
Expand All @@ -71,6 +84,9 @@ object RepositoriesElasticsearch {
def remove(organization: String, id: String): RIO[RepositoriesElasticsearch, DeletionOutcome] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.remove(organization, id))

def search(query: ElasticQuery[_]): RIO[RepositoriesElasticsearch, List[GitHubRepo]] =
ZIO.serviceWithZIO[RepositoriesElasticsearch](_.search(query))

lazy val live: URLayer[ElasticExecutor, RepositoriesElasticsearch] =
ZLayer.fromFunction(RepositoriesElasticsearch(_))
}
53 changes: 53 additions & 0 deletions modules/example/src/main/scala/example/api/Criteria.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package example.api

import zio.schema.{DeriveSchema, Schema}

import java.time.LocalDateTime

sealed trait Criteria

object Criteria {
implicit val schema: Schema[Criteria] = DeriveSchema.gen[Criteria]
}

final case class IntCriteria(field: IntFilter, operator: FilterOperator, value: Int) extends Criteria

final case class DateCriteria(field: DateFilter, operator: FilterOperator, value: LocalDateTime) extends Criteria

final case class CompoundCriteria(
operator: CompoundOperator,
filters: List[Criteria]
) extends Criteria

sealed trait FilterOperator

object FilterOperator {
case object GreaterThan extends FilterOperator
case object LessThan extends FilterOperator
case object EqualTo extends FilterOperator
}

sealed trait CompoundOperator

object CompoundOperator {
case object And extends CompoundOperator
case object Or extends CompoundOperator
}

sealed trait IntFilter

object IntFilter {
case object Stars extends IntFilter {
override def toString: String = "stars"
}

case object Forks extends IntFilter {
override def toString: String = "forks"
}
}

sealed trait DateFilter

case object LastCommitAt extends DateFilter {
override def toString: String = "lastCommitAt"
}
60 changes: 52 additions & 8 deletions modules/example/src/main/scala/example/api/Repositories.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,25 @@ package example.api

import example.{GitHubRepo, RepositoriesElasticsearch}
import zio.ZIO
import zio.elasticsearch.{DeletionOutcome, DocumentId}
import zio.elasticsearch.ElasticQuery.boolQuery
import zio.elasticsearch.{CreationOutcome, DeletionOutcome, ElasticQuery}
import zio.http._
import zio.http.model.Method
import zio.http.model.Status._
import zio.json.EncoderOps
import zio.schema.codec.JsonCodec

import CompoundOperator._
import FilterOperator._

object Repositories {

private final val BasePath = !! / "api" / "repositories"

final val Routes: Http[RepositoriesElasticsearch, Nothing, Request, Response] =
Http.collectZIO[Request] {
case Method.GET -> BasePath =>
ZIO.succeed(Response.text("TODO: Get a list of repositories").setStatus(NotImplemented))
RepositoriesElasticsearch.findAll().map(repositories => Response.json(repositories.toJson)).orDie

case Method.GET -> BasePath / organization / id =>
RepositoriesElasticsearch
Expand All @@ -36,12 +40,28 @@ object Repositories {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(repo) =>
RepositoriesElasticsearch.create(repo).map { id =>
Response.json(repo.copy(id = DocumentId.unwrap(id)).toJson).setStatus(Created)
RepositoriesElasticsearch.create(repo).map {
case CreationOutcome.Created =>
Response.json(repo.toJson).setStatus(Created)
case CreationOutcome.AlreadyExists =>
Response.json("A repository with a given ID already exists.").setStatus(BadRequest)
}
}
.orDie

case req @ Method.POST -> BasePath / "search" =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[Criteria](Criteria.schema, _))
.flatMap {
case Left(e) =>
ZIO.succeed(Response.json(ErrorResponse.fromReasons(e.message).toJson).setStatus(BadRequest))
case Right(queryBody) =>
RepositoriesElasticsearch
.search(createElasticQuery(queryBody))
.map(repositories => Response.json(repositories.toJson))
}
.orDie

case req @ Method.PUT -> BasePath / id =>
req.body.asString
.map(JsonCodec.JsonDecoder.decode[GitHubRepo](GitHubRepo.schema, _))
Expand All @@ -58,10 +78,7 @@ object Repositories {
)
case Right(repo) =>
(RepositoriesElasticsearch
.upsert(id, repo.copy(id = id)) *> RepositoriesElasticsearch.findById(
repo.organization,
id
)).map {
.upsert(id, repo.copy(id = id)) *> RepositoriesElasticsearch.findById(repo.organization, id)).map {
case Some(updated) => Response.json(updated.toJson)
case None => Response.json(ErrorResponse.fromReasons("Operation failed.").toJson).setStatus(BadRequest)
}
Expand All @@ -80,4 +97,31 @@ object Repositories {
.orDie
}

private def createElasticQuery(query: Criteria): ElasticQuery[_] =
query match {
case IntCriteria(field, operator, value) =>
operator match {
case GreaterThan =>
ElasticQuery.range(field.toString).gt(value)
case LessThan =>
ElasticQuery.range(field.toString).lt(value)
case EqualTo =>
ElasticQuery.matches(field.toString, value)
}
case DateCriteria(field, operator, value) =>
operator match {
case GreaterThan =>
ElasticQuery.range(field.toString).gt(value.toString)
case LessThan =>
ElasticQuery.range(field.toString).lt(value.toString)
case EqualTo =>
ElasticQuery.matches(field.toString, value.toString)
}
case CompoundCriteria(operator, filters) =>
operator match {
case And => boolQuery().must(filters.map(createElasticQuery): _*)
case Or => boolQuery().should(filters.map(createElasticQuery): _*)
}
}

}
3 changes: 2 additions & 1 deletion modules/example/src/main/scala/example/package.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import zio.elasticsearch.IndexName

package object example {
val Index: IndexName = IndexName("repositories")
final val Index: IndexName = IndexName("repositories")
final val organization: String = "zio"
}
51 changes: 48 additions & 3 deletions modules/example/zio-elasticsearch-example.postman_collection.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"scrul-detector\",\n \"url\": \"https://github.com/lambdaworks/scurl-detector\",\n \"description\": \"Scala library that detects and extracts URLs from text.\",\n \"lastCommitAt\": \"2022-12-01T14:27:11.436\",\n \"stars\": 14,\n \"forks\": 1\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"scrul-detector\",\n \"url\": \"https://github.com/lambdaworks/scurl-detector\",\n \"description\": \"Scala library that detects and extracts URLs from text.\",\n \"lastCommitAt\": \"2022-12-01T14:27:11.436\",\n \"stars\": 14,\n \"forks\": 1\n}",
"options": {
"raw": {
"language": "json"
Expand Down Expand Up @@ -142,7 +142,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"scountries\",\n \"url\": \"https://github.com/lambdaworks/scountries\",\n \"description\": \"Scala library that provides an enumeration of ISO 3166 codes for countries, along with their subdivisions.\",\n \"lastCommitAt\": \"2022-12-08T19:10:46.016\",\n \"stars\": 16,\n \"forks\": 1\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"scountries\",\n \"url\": \"https://github.com/lambdaworks/scountries\",\n \"description\": \"Scala library that provides an enumeration of ISO 3166 codes for countries, along with their subdivisions.\",\n \"lastCommitAt\": \"2022-12-08T19:10:46.016\",\n \"stars\": 16,\n \"forks\": 1\n}",
"options": {
"raw": {
"language": "json"
Expand All @@ -164,6 +164,26 @@
},
"response": []
},
{
"name": "Retrieving all repositories",
"request": {
"method": "GET",
"header": [],
"url": {
"raw": "http://localhost:{{HTTP_PORT}}/api/repositories",
"protocol": "http",
"host": [
"localhost"
],
"port": "{{HTTP_PORT}}",
"path": [
"api",
"repositories"
]
}
},
"response": []
},
{
"name": "Retrieving an existing repository",
"request": {
Expand Down Expand Up @@ -281,7 +301,7 @@
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"organization\": \"lambdaworks\",\n \"name\": \"zio-elasticsearch\",\n \"url\": \"https://github.com/lambdaworks/zio-elasticsearch\",\n \"description\": \"ZIO Elasticsearch is a type-safe, testable and streaming-friendly ZIO native Elasticsearch client.\",\n \"lastCommitAt\": \"2022-12-27T15:58:30.996\",\n \"stars\": 21,\n \"forks\": 5\n}",
"raw": "{\n \"id\": \"1234567\",\n \"organization\": \"lambdaworks\",\n \"name\": \"zio-elasticsearch\",\n \"url\": \"https://github.com/lambdaworks/zio-elasticsearch\",\n \"description\": \"ZIO Elasticsearch is a type-safe, testable and streaming-friendly ZIO native Elasticsearch client.\",\n \"lastCommitAt\": \"2022-12-27T15:58:30.996\",\n \"stars\": 21,\n \"forks\": 5\n}",
"options": {
"raw": {
"language": "json"
Expand Down Expand Up @@ -363,6 +383,31 @@
}
},
"response": []
},
{
"name": "Search for repositories with 4 forks and with latest commit after 2019-12-04",
"request": {
"method": "POST",
"header": [],
"body": {
"mode": "raw",
"raw": "{\n \"CompoundCriteria\": {\n \"operator\": {\"And\": {}},\n \"filters\": [\n {\n \"IntCriteria\": {\n \"field\": {\"Forks\": {}},\n \"operator\": {\"EqualTo\": {}},\n \"value\": 4\n }\n },\n {\n \"DateCriteria\": {\n \"field\": {\"LastCommitAt\": {}},\n \"operator\": {\"GreaterThan\": {}},\n \"value\": \"2019-12-04T18:38:04\"\n }\n }\n ]\n }\n}\n"
},
"url": {
"raw": "http://localhost:{{HTTP_PORT}}/api/repositories/search",
"protocol": "http",
"host": [
"localhost"
],
"port": "{{HTTP_PORT}}",
"path": [
"api",
"repositories",
"search"
]
}
},
"response": []
}
],
"event": [
Expand Down