diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..c20156c6d1f9d0dee893f07ba1eabd8244f75cf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Docker Output +/mongo diff --git a/backend-services/feed-service/.g8/form/app/controllers/$model__Camel$Controller.scala b/backend-services/feed-service/.g8/form/app/controllers/$model__Camel$Controller.scala new file mode 100644 index 0000000000000000000000000000000000000000..6977727b37e444381e28f6db09bff98d066ac4a4 --- /dev/null +++ b/backend-services/feed-service/.g8/form/app/controllers/$model__Camel$Controller.scala @@ -0,0 +1,46 @@ +package controllers + +import javax.inject._ +import play.api.mvc._ + +import play.api.data._ +import play.api.data.Forms._ + +case class $model;format="Camel"$Data(name: String, age: Int) + +// NOTE: Add the following to conf/routes to enable compilation of this class: +/* +GET /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Get() +POST /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Post() +*/ + +/** + * $model;format="Camel"$ form controller for Play Scala + */ +class $model;format="Camel"$Controller @Inject()(mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) { + + val $model;format="camel"$Form = Form( + mapping( + "name" -> text, + "age" -> number + )($model;format="Camel"$Data.apply)($model;format="Camel"$Data.unapply) + ) + + def $model;format="camel"$Get() = Action { implicit request: MessagesRequest[AnyContent] => + Ok(views.html.$model;format="camel"$.form($model;format="camel"$Form)) + } + + def $model;format="camel"$Post() = Action { implicit request: MessagesRequest[AnyContent] => + $model;format="camel"$Form.bindFromRequest().fold( + formWithErrors => { + // binding failure, you retrieve the form containing errors: + BadRequest(views.html.$model;format="camel"$.form(formWithErrors)) + }, + $model;format="camel"$Data => { + /* binding success, you get the actual value. */ + /* flashing uses a short lived cookie */ + Redirect(routes.$model;format="Camel"$Controller.$model;format="camel"$Get()).flashing("success" -> ("Successful " + $model;format="camel"$Data.toString)) + } + ) + } +} diff --git a/backend-services/feed-service/.g8/form/app/views/$model__camel$/form.scala.html b/backend-services/feed-service/.g8/form/app/views/$model__camel$/form.scala.html new file mode 100644 index 0000000000000000000000000000000000000000..14674ba6e4da6a88308d6cf305f9b56e59ea0a56 --- /dev/null +++ b/backend-services/feed-service/.g8/form/app/views/$model__camel$/form.scala.html @@ -0,0 +1,12 @@ +@($model;format="camel"$Form: Form[$model;format="Camel"$Data])(implicit request: MessagesRequestHeader) + +<h1>$model;format="camel"$ form</h1> + +@request.flash.get("success").getOrElse("") + +@helper.form(action = routes.$model;format="Camel"$Controller.$model;format="camel"$Post()) { + @helper.CSRF.formField + @helper.inputText($model;format="camel"$Form("name")) + @helper.inputText($model;format="camel"$Form("age")) + <input type="submit" value="submit"/> +} diff --git a/backend-services/feed-service/.g8/form/default.properties b/backend-services/feed-service/.g8/form/default.properties new file mode 100644 index 0000000000000000000000000000000000000000..32090f30cb4cc9955f1fca778b4b41772709f90c --- /dev/null +++ b/backend-services/feed-service/.g8/form/default.properties @@ -0,0 +1,2 @@ +description = Generates a Controller with form handling +model = user diff --git a/backend-services/feed-service/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala b/backend-services/feed-service/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..d25174315b09b99cac91b38ff9c6fdf344c3c577 --- /dev/null +++ b/backend-services/feed-service/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala @@ -0,0 +1,71 @@ +package controllers + +import play.api.mvc._ +import play.api.i18n._ +import org.scalatestplus.play._ +import org.scalatestplus.play.guice.GuiceOneAppPerTest +import play.api.http.FileMimeTypes +import play.api.test._ +import play.api.test.Helpers._ +import play.api.test.CSRFTokenHelper._ + +import scala.concurrent.ExecutionContext + +/** + * $model;format="Camel"$ form controller specs + */ +class $model;format="Camel"$ControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { + + // Provide stubs for components based off Helpers.stubControllerComponents() + class StubComponents(cc:ControllerComponents = stubControllerComponents()) extends MessagesControllerComponents { + override val parsers: PlayBodyParsers = cc.parsers + override val messagesApi: MessagesApi = cc.messagesApi + override val langs: Langs = cc.langs + override val fileMimeTypes: FileMimeTypes = cc.fileMimeTypes + override val executionContext: ExecutionContext = cc.executionContext + override val actionBuilder: ActionBuilder[Request, AnyContent] = cc.actionBuilder + override val messagesActionBuilder: MessagesActionBuilder = new DefaultMessagesActionBuilderImpl(parsers.default, messagesApi)(executionContext) + } + + "$model;format="Camel"$Controller GET" should { + + "render the index page from a new instance of controller" in { + val controller = new $model;format="Camel"$Controller(new StubComponents()) + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the application" in { + val controller = inject[$model;format="Camel"$Controller] + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the router" in { + val request = CSRFTokenHelper.addCSRFToken(FakeRequest(GET, "/$model;format="camel"$")) + val home = route(app, request).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + } + + "$model;format="Camel"$Controller POST" should { + "process form" in { + val request = { + FakeRequest(POST, "/$model;format="camel"$") + .withFormUrlEncodedBody("name" -> "play", "age" -> "4") + } + val home = route(app, request).get + + status(home) mustBe SEE_OTHER + } + } + +} diff --git a/backend-services/feed-service/.gitignore b/backend-services/feed-service/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..dce73038415f7e68f232c49cefe21ad58a9056e7 --- /dev/null +++ b/backend-services/feed-service/.gitignore @@ -0,0 +1,9 @@ +logs +target +/.bsp +/.idea +/.idea_modules +/.classpath +/.project +/.settings +/RUNNING_PID diff --git a/backend-services/feed-service/Dockerfile b/backend-services/feed-service/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..34a02ae0137be7841d93128e3a8c06ecba521a96 --- /dev/null +++ b/backend-services/feed-service/Dockerfile @@ -0,0 +1,9 @@ +FROM sbtscala/scala-sbt:eclipse-temurin-focal-11.0.17_8_1.8.2_2.13.10 + +WORKDIR /feed-service/app + +ADD . . + +EXPOSE 9000 + +CMD sbt run diff --git a/backend-services/feed-service/app/controllers/HomeController.scala b/backend-services/feed-service/app/controllers/HomeController.scala new file mode 100644 index 0000000000000000000000000000000000000000..60a6e853619c672b573c0e1b974e44f16f86bf5d --- /dev/null +++ b/backend-services/feed-service/app/controllers/HomeController.scala @@ -0,0 +1,24 @@ +package controllers + +import javax.inject._ +import play.api._ +import play.api.mvc._ + +/** + * This controller creates an `Action` to handle HTTP requests to the + * application's home page. + */ +@Singleton +class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { + + /** + * Create an Action to render an HTML page. + * + * The configuration in the `routes` file means that this method + * will be called when the application receives a `GET` request with + * a path of `/`. + */ + def index() = Action { implicit request: Request[AnyContent] => + Ok("OK") + } +} diff --git a/backend-services/feed-service/app/controllers/MongoTestController.scala b/backend-services/feed-service/app/controllers/MongoTestController.scala new file mode 100644 index 0000000000000000000000000000000000000000..c086129a767005645bf5a1b040cb5375a3f0914e --- /dev/null +++ b/backend-services/feed-service/app/controllers/MongoTestController.scala @@ -0,0 +1,49 @@ +package controllers + +import javax.inject._ +import play.api._ +import play.api.mvc._ + +import models.MongoDBClient +import org.mongodb.scala.{Document} +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{Future, Await, TimeoutException} +import scala.concurrent.duration._ + +/** + * This controller creates an `Action` to handle HTTP MongoDB requests. + */ +@Singleton +class MongoTestController @Inject()(val controllerComponents: ControllerComponents) extends BaseController { + + /** + * Create an Action to test the Mongo Client. + */ + def index() = Action { implicit request: Request[AnyContent] => + println("MongoController") + + var database_name = "{DATABASE_NAME}" + var collection_name = "{COLLECTION_NAME}" + + var mongo = new MongoDBClient() + + // Sequentially waits for Future objects to complete before calling next method + val result: Future[Seq[Document]] = for { + db <- mongo.getDatabase(database_name) + collection <- mongo.getCollection(db, collection_name) + findResult <- mongo.find(collection) + } yield findResult + + + try { + // Wait for 10 seconds for the Future to complete + val resultCompleted = Await.result(result, 4.seconds) + + val jsonResult: Seq[String] = resultCompleted.map(doc => doc.toJson()) + Ok(jsonResult.toString()) + } catch { + case e: TimeoutException => + BadRequest("Request timed out") + } + } +} diff --git a/backend-services/feed-service/app/models/MongoDBClient.scala b/backend-services/feed-service/app/models/MongoDBClient.scala new file mode 100644 index 0000000000000000000000000000000000000000..96c5dd630909f964630b16097b7fb77cb7820711 --- /dev/null +++ b/backend-services/feed-service/app/models/MongoDBClient.scala @@ -0,0 +1,96 @@ +package models + +import utils.ConfigHelper + +import org.mongodb.scala.{MongoClient, MongoDatabase, MongoCollection, Document, FindObservable, Observer, Observable} +import org.mongodb.scala.model.{Filters, Projections, Sorts} +import org.bson.conversions.Bson + +import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future +import scala.util.{Success, Failure, Try} + + +/** + * A MongoDB client for connecting to and interacting with a MongoDB database. + * + * @constructor Creates a new instance of the MongoDBClient class. + */ +class MongoDBClient { + private val mongoUri = ConfigHelper.getString("mongodb.uri") + + // Connects to a MongoDB Client when class is constructed + private var client: MongoClient = this.connect() + + /** + * Connects to a MongoDB database using the default MongoDB connection string. + * + * @return A MongoClient instance. + */ + def connect(): MongoClient = { + MongoClient(mongoUri) + } + + /** + * Gets a reference to a MongoDB database. + * + * @param database The name of the database to retrieve. + * @return A Future containing a MongoDatabase instance. + */ + def getDatabase(database: String): Future[MongoDatabase] = Future { + client.getDatabase(database) + } + + /** + * Gets a reference to a MongoDB collection within a database. + * + * @param database The MongoDatabase instance containing the desired collection. + * @param collection The name of the collection to retrieve. + * @return A Future containing a MongoCollection instance. + */ + def getCollection(database: MongoDatabase, collection: String): Future[MongoCollection[Document]] = Future { + database.getCollection(collection) + } + + /** + * Finds documents in a MongoDB collection. + * + * @param collection The MongoCollection instance to search. + * @param filter A Bson filter to apply to the search. + * @param projection A Bson projection to apply to the search. + * @param sort A Bson sort to apply to the search. + * @return A Future containing a sequence of matching documents as Documents. + */ + def find( + collection: MongoCollection[Document], + filter: Bson = Filters.empty(), + projection: Bson = Projections.excludeId(), + sort: Bson = Sorts.ascending("_id") + ): Future[Seq[Document]] = { + collection.find(filter) + .projection(projection) + .sort(sort) + .toFuture() + } + + /** + * Inserts a document into a MongoDB collection. + * + * @param collection The MongoCollection instance to insert into. + * @param document The document to insert. + * @return A Future containing the ID of the inserted document. + * @throws RuntimeException if the insertion was not acknowledged by the database. + */ + def insertOne(collection: MongoCollection[Document], document: Document): Future[String] = { + val futureResult = collection.insertOne(document).toFuture() + + futureResult.map(result => { + if (result.wasAcknowledged()) { + // Grab the generated ID of the inserted document + result.getInsertedId().asObjectId.getValue().toString() + } else { + throw new RuntimeException("Insertion was not acknowledged") + } + }) + } +} diff --git a/backend-services/feed-service/app/utils/ConfigHelper.scala b/backend-services/feed-service/app/utils/ConfigHelper.scala new file mode 100644 index 0000000000000000000000000000000000000000..06b6cc3bb81a42481f354820be45367cff6b8bb6 --- /dev/null +++ b/backend-services/feed-service/app/utils/ConfigHelper.scala @@ -0,0 +1,11 @@ +package utils + +import com.typesafe.config.{Config, ConfigFactory} + +object ConfigHelper { + private val applicationConfig: Config = ConfigFactory.load("application.conf") + private val referenceConfig: Config = ConfigFactory.parseResources("reference.conf") + private val config: Config = referenceConfig.withFallback(applicationConfig).resolve() + + def getString(key: String): String = config.getString(key) +} diff --git a/backend-services/feed-service/build.sbt b/backend-services/feed-service/build.sbt new file mode 100644 index 0000000000000000000000000000000000000000..ad929924b648464b550c0b595083d5984381e964 --- /dev/null +++ b/backend-services/feed-service/build.sbt @@ -0,0 +1,18 @@ +name := """feed-service""" +organization := "com.daily" + +version := "1.0.0" + +lazy val root = (project in file(".")).enablePlugins(PlayScala) + +scalaVersion := "2.13.10" + +libraryDependencies += guice +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test + +// Adds additional packages into Twirl +//TwirlKeys.templateImports += "com.daily.controllers._" + +// Adds additional packages into conf/routes +// play.sbt.routes.RoutesKeys.routesImport += "com.daily.binders._" +libraryDependencies += "org.mongodb.scala" %% "mongo-scala-driver" % "4.3.0" diff --git a/backend-services/feed-service/build.sc b/backend-services/feed-service/build.sc new file mode 100644 index 0000000000000000000000000000000000000000..fe14b81a812a0ff0be0144f12d4901153f80af4b --- /dev/null +++ b/backend-services/feed-service/build.sc @@ -0,0 +1,11 @@ +import mill._ +import $ivy.`com.lihaoyi::mill-contrib-playlib:`, mill.playlib._ + +object feedservice extends PlayModule with SingleModule { + + def scalaVersion = "2.13.10" + def playVersion = "2.8.19" + def twirlVersion = "1.5.1" + + object test extends PlayTests +} diff --git a/backend-services/feed-service/conf/application.conf b/backend-services/feed-service/conf/application.conf new file mode 100644 index 0000000000000000000000000000000000000000..852c11d6e6d10588b23cae3f2b3e8131c4827c11 --- /dev/null +++ b/backend-services/feed-service/conf/application.conf @@ -0,0 +1,4 @@ +# Default Configuration File + +# MongoDB Connection Strings +mongodb.uri = "mongodb://localhost:27017/" diff --git a/backend-services/feed-service/conf/logback.xml b/backend-services/feed-service/conf/logback.xml new file mode 100644 index 0000000000000000000000000000000000000000..a73d2016eb86a363ba7e705f4023ee2da87672fa --- /dev/null +++ b/backend-services/feed-service/conf/logback.xml @@ -0,0 +1,40 @@ +<!-- https://www.playframework.com/documentation/latest/SettingsLogger --> +<configuration> + + <appender name="FILE" class="ch.qos.logback.core.FileAppender"> + <file>${application.home:-.}/logs/application.log</file> + <encoder> + <charset>UTF-8</charset> + <pattern> + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n + </pattern> + </encoder> + </appender> + + <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> + <withJansi>true</withJansi> + <encoder> + <charset>UTF-8</charset> + <pattern> + %d{yyyy-MM-dd HH:mm:ss} %highlight(%-5level) %cyan(%logger{36}) %magenta(%X{akkaSource}) %msg%n + </pattern> + </encoder> + </appender> + + <appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="FILE" /> + </appender> + + <appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender"> + <appender-ref ref="STDOUT" /> + </appender> + + <logger name="play" level="INFO" /> + <logger name="application" level="DEBUG" /> + + <root level="WARN"> + <appender-ref ref="ASYNCFILE" /> + <appender-ref ref="ASYNCSTDOUT" /> + </root> + +</configuration> diff --git a/backend-services/feed-service/conf/messages b/backend-services/feed-service/conf/messages new file mode 100644 index 0000000000000000000000000000000000000000..0226738a648fe29c551b43fe4bddaf8a39977e4f --- /dev/null +++ b/backend-services/feed-service/conf/messages @@ -0,0 +1 @@ +# https://www.playframework.com/documentation/latest/ScalaI18N diff --git a/backend-services/feed-service/conf/reference.conf b/backend-services/feed-service/conf/reference.conf new file mode 100644 index 0000000000000000000000000000000000000000..1eba4d767f858cae4f72900eb06b7f47bfbbf5f1 --- /dev/null +++ b/backend-services/feed-service/conf/reference.conf @@ -0,0 +1,4 @@ +# Configuration File to be replaced with environment variables, if present + +# MongoDB Connection Strings +mongodb.uri = ${?MONGO_URI} diff --git a/backend-services/feed-service/conf/routes b/backend-services/feed-service/conf/routes new file mode 100644 index 0000000000000000000000000000000000000000..c987e3139a94547944c6f45cb7398242a2986f69 --- /dev/null +++ b/backend-services/feed-service/conf/routes @@ -0,0 +1,12 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# https://www.playframework.com/documentation/latest/ScalaRouting +# ~~~~ + +# An example controller showing a sample home page +GET / controllers.HomeController.index() + +# Map static resources from the /public folder to the /assets URL path +GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset) + +GET /mongo_test controllers.MongoTestController.index() diff --git a/backend-services/feed-service/project/build.properties b/backend-services/feed-service/project/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..563a014da4aae6dcdf1d6c1b3a74c728e4bd6f5e --- /dev/null +++ b/backend-services/feed-service/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.7.2 diff --git a/backend-services/feed-service/project/plugins.sbt b/backend-services/feed-service/project/plugins.sbt new file mode 100644 index 0000000000000000000000000000000000000000..8846622eca954bfb69de96dea021ca9d3f2aa767 --- /dev/null +++ b/backend-services/feed-service/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.19") +addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.13.1") diff --git a/backend-services/feed-service/public/images/favicon.png b/backend-services/feed-service/public/images/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..c7d92d2ae47434d9a61c90bc205e099b673b9dd5 Binary files /dev/null and b/backend-services/feed-service/public/images/favicon.png differ diff --git a/backend-services/feed-service/public/javascripts/main.js b/backend-services/feed-service/public/javascripts/main.js new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend-services/feed-service/public/stylesheets/main.css b/backend-services/feed-service/public/stylesheets/main.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/backend-services/feed-service/test/controllers/HomeControllerSpec.scala b/backend-services/feed-service/test/controllers/HomeControllerSpec.scala new file mode 100644 index 0000000000000000000000000000000000000000..97947556d450c154b7a8a4843d48a522d597be9b --- /dev/null +++ b/backend-services/feed-service/test/controllers/HomeControllerSpec.scala @@ -0,0 +1,45 @@ +package controllers + +import org.scalatestplus.play._ +import org.scalatestplus.play.guice._ +import play.api.test._ +import play.api.test.Helpers._ + +/** + * Add your spec here. + * You can mock out a whole application including requests, plugins etc. + * + * For more information, see https://www.playframework.com/documentation/latest/ScalaTestingWithScalaTest + */ +class HomeControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { + + "HomeController GET" should { + + "render the index page from a new instance of controller" in { + val controller = new HomeController(stubControllerComponents()) + val home = controller.index().apply(FakeRequest(GET, "/")) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + + "render the index page from the application" in { + val controller = inject[HomeController] + val home = controller.index().apply(FakeRequest(GET, "/")) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + + "render the index page from the router" in { + val request = FakeRequest(GET, "/") + val home = route(app, request).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + contentAsString(home) must include ("Welcome to Play") + } + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000000000000000000000000000000000..d354d3cdf5120eb132636b78f2a868f2489dbba6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" +services: + feed-service: + build: + context: './backend-services/feed-service' + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "9001:9000" + environment: + - MONGO_URI=mongodb://feed-mongo:27017/ + + mongo: + image: mongo + container_name: feed-mongo + ports: + - "27017:27017" + \ No newline at end of file