diff --git a/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala new file mode 100644 index 0000000..079890c --- /dev/null +++ b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala @@ -0,0 +1,45 @@ +package works.iterative.mongo + +import zio.* +import zio.test.* +import zio.json.* +import Assertion.* +import java.io.File + +object MongoJsonFileRepositoryIntegrationSpec extends DefaultRunnableSpec: + case class ExampleMetadata(osobniCislo: String) + sealed trait ExampleCriteria + case class ByOsobniCislo(osobniCislo: String) extends ExampleCriteria + + given JsonCodec[ExampleMetadata] = DeriveJsonCodec.gen + + override def spec = suite("Mongo file repository integration spec")( + test("repo can put and read back file")( + for + repo <- ZIO + .service[MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]] + _ <- repo.put("test.txt", "Example content".getBytes(), ExampleMetadata("10123")) + byId <- repo.find("test.txt") + result <- repo.matching(ByOsobniCislo("10123")) + yield assertTrue(byId.isDefined, result.head == "test.txt") + ) + ).provideCustomLayer(layer.mapError(TestFailure.fail)) + + val layer = + import org.mongodb.scala.* + import org.mongodb.scala.model.Filters.* + import org.bson.json.JsonObject + import org.mongodb.scala.bson.conversions.Bson + import org.mongodb.scala.bson.Document + import org.mongodb.scala.gridfs.GridFSBucket + MongoConfig.fromEnv >>> MongoClient.layer >>> (for + client <- ZIO.service[MongoClient] + bucket <- Task.attempt( + GridFSBucket(client.getDatabase("test"), "testfiles") + ) + yield new MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]( + bucket, { + _ match + case ByOsobniCislo(osc) => equal("metadata.osobniCislo", osc) + } + )).toLayer diff --git a/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala new file mode 100644 index 0000000..079890c --- /dev/null +++ b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala @@ -0,0 +1,45 @@ +package works.iterative.mongo + +import zio.* +import zio.test.* +import zio.json.* +import Assertion.* +import java.io.File + +object MongoJsonFileRepositoryIntegrationSpec extends DefaultRunnableSpec: + case class ExampleMetadata(osobniCislo: String) + sealed trait ExampleCriteria + case class ByOsobniCislo(osobniCislo: String) extends ExampleCriteria + + given JsonCodec[ExampleMetadata] = DeriveJsonCodec.gen + + override def spec = suite("Mongo file repository integration spec")( + test("repo can put and read back file")( + for + repo <- ZIO + .service[MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]] + _ <- repo.put("test.txt", "Example content".getBytes(), ExampleMetadata("10123")) + byId <- repo.find("test.txt") + result <- repo.matching(ByOsobniCislo("10123")) + yield assertTrue(byId.isDefined, result.head == "test.txt") + ) + ).provideCustomLayer(layer.mapError(TestFailure.fail)) + + val layer = + import org.mongodb.scala.* + import org.mongodb.scala.model.Filters.* + import org.bson.json.JsonObject + import org.mongodb.scala.bson.conversions.Bson + import org.mongodb.scala.bson.Document + import org.mongodb.scala.gridfs.GridFSBucket + MongoConfig.fromEnv >>> MongoClient.layer >>> (for + client <- ZIO.service[MongoClient] + bucket <- Task.attempt( + GridFSBucket(client.getDatabase("test"), "testfiles") + ) + yield new MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]( + bucket, { + _ match + case ByOsobniCislo(osc) => equal("metadata.osobniCislo", osc) + } + )).toLayer diff --git a/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala b/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala new file mode 100644 index 0000000..90a43e3 --- /dev/null +++ b/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala @@ -0,0 +1,45 @@ +package works.iterative.mongo + +import zio.* +import zio.test.* +import zio.json.* +import Assertion.* + +object MongoJsonRepositoryIntegrationSpec extends DefaultRunnableSpec: + case class Example(id: String, value: String) + sealed trait ExampleCriteria + case object All extends ExampleCriteria + case class ById(id: String) extends ExampleCriteria + + given JsonCodec[Example] = DeriveJsonCodec.gen + + override def spec = suite("Mongo repository integration spec")( + test("repo can put and read back")( + for + repo <- ZIO + .service[MongoJsonRepository[Example, String, ExampleCriteria]] + _ <- repo.put(Example("1", "test")) + result <- repo.matching(ById("1")) + yield assertTrue(result.head.value == "test") + ) + ).provideCustomLayer(layer.mapError(TestFailure.fail)) + + val layer = + import org.mongodb.scala.* + import org.mongodb.scala.model.Filters.* + import org.bson.json.JsonObject + import org.mongodb.scala.bson.conversions.Bson + import org.mongodb.scala.bson.Document + MongoConfig.fromEnv >>> MongoClient.layer >>> (for + client <- ZIO.service[MongoClient] + coll <- Task.attempt( + client.getDatabase("test").getCollection[JsonObject]("example") + ) + yield new MongoJsonRepository[Example, String, ExampleCriteria]( + coll, { + _ match + case ById(id) => equal("id", id) + case All => Document() + }, + e => ("id", e.id) + )).toLayer diff --git a/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala new file mode 100644 index 0000000..079890c --- /dev/null +++ b/mongo/it/src/MongoJsonFileRepositoryIntegrationSpec.scala @@ -0,0 +1,45 @@ +package works.iterative.mongo + +import zio.* +import zio.test.* +import zio.json.* +import Assertion.* +import java.io.File + +object MongoJsonFileRepositoryIntegrationSpec extends DefaultRunnableSpec: + case class ExampleMetadata(osobniCislo: String) + sealed trait ExampleCriteria + case class ByOsobniCislo(osobniCislo: String) extends ExampleCriteria + + given JsonCodec[ExampleMetadata] = DeriveJsonCodec.gen + + override def spec = suite("Mongo file repository integration spec")( + test("repo can put and read back file")( + for + repo <- ZIO + .service[MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]] + _ <- repo.put("test.txt", "Example content".getBytes(), ExampleMetadata("10123")) + byId <- repo.find("test.txt") + result <- repo.matching(ByOsobniCislo("10123")) + yield assertTrue(byId.isDefined, result.head == "test.txt") + ) + ).provideCustomLayer(layer.mapError(TestFailure.fail)) + + val layer = + import org.mongodb.scala.* + import org.mongodb.scala.model.Filters.* + import org.bson.json.JsonObject + import org.mongodb.scala.bson.conversions.Bson + import org.mongodb.scala.bson.Document + import org.mongodb.scala.gridfs.GridFSBucket + MongoConfig.fromEnv >>> MongoClient.layer >>> (for + client <- ZIO.service[MongoClient] + bucket <- Task.attempt( + GridFSBucket(client.getDatabase("test"), "testfiles") + ) + yield new MongoJsonFileRepository[ExampleMetadata, ExampleCriteria]( + bucket, { + _ match + case ByOsobniCislo(osc) => equal("metadata.osobniCislo", osc) + } + )).toLayer diff --git a/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala b/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala new file mode 100644 index 0000000..90a43e3 --- /dev/null +++ b/mongo/it/src/MongoJsonRepositoryIntegrationSpec.scala @@ -0,0 +1,45 @@ +package works.iterative.mongo + +import zio.* +import zio.test.* +import zio.json.* +import Assertion.* + +object MongoJsonRepositoryIntegrationSpec extends DefaultRunnableSpec: + case class Example(id: String, value: String) + sealed trait ExampleCriteria + case object All extends ExampleCriteria + case class ById(id: String) extends ExampleCriteria + + given JsonCodec[Example] = DeriveJsonCodec.gen + + override def spec = suite("Mongo repository integration spec")( + test("repo can put and read back")( + for + repo <- ZIO + .service[MongoJsonRepository[Example, String, ExampleCriteria]] + _ <- repo.put(Example("1", "test")) + result <- repo.matching(ById("1")) + yield assertTrue(result.head.value == "test") + ) + ).provideCustomLayer(layer.mapError(TestFailure.fail)) + + val layer = + import org.mongodb.scala.* + import org.mongodb.scala.model.Filters.* + import org.bson.json.JsonObject + import org.mongodb.scala.bson.conversions.Bson + import org.mongodb.scala.bson.Document + MongoConfig.fromEnv >>> MongoClient.layer >>> (for + client <- ZIO.service[MongoClient] + coll <- Task.attempt( + client.getDatabase("test").getCollection[JsonObject]("example") + ) + yield new MongoJsonRepository[Example, String, ExampleCriteria]( + coll, { + _ match + case ById(id) => equal("id", id) + case All => Document() + }, + e => ("id", e.id) + )).toLayer diff --git a/mongo/src/MongoJsonRepository.scala b/mongo/src/MongoJsonRepository.scala index 411fba9..0db6331 100644 --- a/mongo/src/MongoJsonRepository.scala +++ b/mongo/src/MongoJsonRepository.scala @@ -8,6 +8,10 @@ import org.bson.json.JsonObject import org.mongodb.scala.model.ReplaceOptions import org.mongodb.scala.bson.conversions.Bson +import org.mongodb.scala.gridfs.GridFSBucket +import java.io.File +import java.nio.ByteBuffer +import com.mongodb.client.gridfs.model.GridFSUploadOptions case class MongoConfig(uri: String) @@ -28,21 +32,21 @@ .serviceWithZIO[MongoConfig](c => Task.attempt(MongoClient(c.uri))) .toLayer -class MongoJsonRepository[Elem, Key, Criteria]( +class MongoJsonRepository[Elem: JsonCodec, Key, Criteria]( collection: MongoCollection[JsonObject], toFilter: Criteria => Bson, idFilter: Elem => (String, Key) -)(using JsonCodec[Elem]): +): def matching(criteria: Criteria): Task[List[Elem]] = val filter = toFilter(criteria) val query = collection.find(filter) for result <- ZIO.fromFuture(_ => query.toFuture) - proof <- ZIO.collect(result)(j => + elems <- ZIO.collect(result)(j => ZIO.fromOption(j.getJson.fromJson[Elem].toOption) ) - yield proof.to(List) + yield elems.to(List) def put(elem: Elem): Task[Unit] = Task.async(cb => @@ -54,3 +58,31 @@ ) .subscribe(_ => cb(Task.unit), t => cb(Task.fail(t))) ) + +class MongoJsonFileRepository[Metadata: JsonCodec, Criteria]( + bucket: GridFSBucket, + toFilter: Criteria => Bson +): + + def put(name: String, file: Array[Byte], metadata: Metadata): Task[Unit] = + ZIO + .fromFuture(_ => + bucket + .uploadFromObservable( + name, + Observable(Seq(ByteBuffer.wrap(file))), + GridFSUploadOptions().metadata(Document(metadata.toJson)) + ) + .toFuture + ) + .unit + + def find(id: String): Task[Option[Array[Byte]]] = + ZIO + .fromFuture(_ => bucket.downloadToObservable(id).toFuture) + .map(_.headOption.map(_.array)) + + def matching(criteria: Criteria): Task[List[String]] = + ZIO + .fromFuture(_ => bucket.find(toFilter(criteria)).toFuture) + .map(_.map(_.getFilename).to(List))